Skip to content

fix(deps): update dependency @graphiql/plugin-explorer to v5 #4033

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/@graphql-yoga_graphiql-4033-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@graphql-yoga/graphiql": patch
---
dependencies updates:
- Updated dependency [`@graphiql/plugin-explorer@5.1.1` ↗︎](https://www.npmjs.com/package/@graphiql/plugin-explorer/v/5.1.1) (from `^3.0.0`, in `dependencies`)
- Updated dependency [`@graphql-tools/url-loader@8.0.33` ↗︎](https://www.npmjs.com/package/@graphql-tools/url-loader/v/8.0.33) (from `^8.0.15`, in `dependencies`)
- Updated dependency [`graphiql@5.2.0` ↗︎](https://www.npmjs.com/package/graphiql/v/5.2.0) (from `3.1.1`, in `dependencies`)
- Removed dependency [`use-url-search-params@2.5.1` ↗︎](https://www.npmjs.com/package/use-url-search-params/v/2.5.1) (from `dependencies`)
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('nextjs 13 App Router', () => {
});

expect(response.ok).toBe(true);
expect(await response.text()).toContain('<title>Yoga GraphiQL</title>');
expect(await response.text()).toContain('Yoga GraphiQL');
});

it('should run basic query', async () => {
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@
"pnpm": {
"patchedDependencies": {
"@changesets/assemble-release-plan@5.2.3": "patches/@changesets__assemble-release-plan@5.2.3.patch",
"@graphiql/react@0.20.4": "patches/@graphiql__react@0.20.4.patch",
"jest-leak-detector": "patches/jest-leak-detector.patch"
},
"overrides": {
Expand Down
9 changes: 4 additions & 5 deletions packages/graphiql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,15 @@
"start": "vite"
},
"dependencies": {
"@graphiql/plugin-explorer": "^3.0.0",
"@graphiql/plugin-explorer": "5.1.1",
"@graphiql/toolkit": "0.11.3",
"@graphql-tools/url-loader": "^8.0.15",
"graphiql": "3.1.1",
"@graphql-tools/url-loader": "8.0.33",
"graphiql": "5.2.0",
"graphql": "16.11.0",
"json-bigint-patch": "0.0.8",
"react": "19.1.0",
"react-dom": "19.1.0",
"tslib": "2.8.1",
"use-url-search-params": "2.5.1"
"tslib": "2.8.1"
},
"devDependencies": {
"@types/react": "19.1.8",
Expand Down
140 changes: 69 additions & 71 deletions packages/graphiql/src/YogaGraphiQL.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import 'graphiql/style.css';
import '@graphiql/plugin-explorer/style.css';
import { GraphiQL, GraphiQLProps } from 'graphiql';
import { DocumentNode, Kind, parse } from 'graphql';
Copy link
Preview

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Kind import appears to be unused in this file. Consider removing it to keep imports clean.

Copilot uses AI. Check for mistakes.

import { explorerPlugin } from '@graphiql/plugin-explorer';
import '@graphiql/plugin-explorer/dist/style.css';
import { GraphiQL, GraphiQLInterface, GraphiQLProps, GraphiQLProvider } from 'graphiql';
import { Fetcher, FetcherOpts, FetcherParams } from '@graphiql/toolkit';
import { LoadFromUrlOptions, SubscriptionProtocol, UrlLoader } from '@graphql-tools/url-loader';
import 'graphiql/graphiql.css';
import { DocumentNode, Kind, parse } from 'graphql';
import 'json-bigint-patch';
import React, { useMemo, useState } from 'react';
import { useUrlSearchParams } from 'use-url-search-params';
import React, { useMemo } from 'react';
import { YogaLogo } from './YogaLogo';
import './styles.css';

Expand Down Expand Up @@ -43,6 +42,19 @@ export type YogaGraphiQLProps = Partial<GraphiQLProps> &
* Extra headers you always want to pass with users' headers input
*/
additionalHeaders?: LoadFromUrlOptions['headers'];

/**
* @deprecated Use `initialQuery` instead.
*/
query?: GraphiQLProps['initialQuery'];
/**
* @deprecated Use `initialHeaders` instead.
*/
headers?: GraphiQLProps['initialHeaders'];
/**
* @deprecated Use `initialVariables` instead.
*/
variables?: GraphiQLProps['initialVariables'];
};

export function YogaGraphiQL(props: YogaGraphiQLProps): React.ReactElement {
Expand Down Expand Up @@ -81,10 +93,6 @@ export function YogaGraphiQL(props: YogaGraphiQLProps): React.ReactElement {

const endpoint = new URL(props.endpoint ?? location.pathname, location.href).toString();

const type = {
query: String,
};

const urlLoader = useMemo(() => new UrlLoader(), []);

const fetcher = useMemo(() => {
Expand Down Expand Up @@ -126,81 +134,71 @@ export function YogaGraphiQL(props: YogaGraphiQLProps): React.ReactElement {
};
}, [urlLoader, endpoint, props.fetcher]) as Fetcher;

const [params, setParams] = useUrlSearchParams(
{
query: props.defaultQuery || initialQuery,
},
type,
false,
);

const [query, setQuery] = useState(params['query']?.toString());
const explorer = explorerPlugin({
showAttribution: true,
});

if (props.query && !props.onEditQuery) {
// eslint-disable-next-line no-console
console.warn(
'If you provide `query` prop, you should also provide `onEditQuery` prop to handle query changes.',
);
}
const currentUrl = new URL(location.href);
const initialQueryFromUrl = currentUrl.searchParams.get('query') || props.query || initialQuery;

const {
query: deprecatedInitialQuery = initialQueryFromUrl,
headers: deprecatedInitialHeaders,
variables: deprecatedInitialVariables,
...otherProps
} = props;

return (
<div className="graphiql-container">
<GraphiQLProvider
<GraphiQL
// default values that can be override by props
shouldPersistHeaders
plugins={[explorer]}
schemaDescription={true}
inputValueDeprecation={true}
query={query}
{...props}
isHeadersEditorEnabled
defaultEditorToolsVisibility
initialQuery={deprecatedInitialQuery}
defaultHeaders={deprecatedInitialHeaders}
Copy link
Preview

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prop name defaultHeaders should be initialHeaders to match the GraphiQL v5 API. Based on the deprecation warnings in the type definitions, this appears to be using an incorrect prop name.

Suggested change
defaultHeaders={deprecatedInitialHeaders}
initialHeaders={deprecatedInitialHeaders}

Copilot uses AI. Check for mistakes.

initialVariables={deprecatedInitialVariables}
onEditQuery={(query, ast) => {
currentUrl.searchParams.set('query', query);
history.replaceState({}, '', currentUrl);
props.onEditQuery?.(query, ast);
}}
{...otherProps}
fetcher={fetcher}
>
<GraphiQLInterface
isHeadersEditorEnabled
defaultEditorToolsVisibility
{...props}
onEditQuery={(query, ast) => {
setParams({
query,
});
setQuery(query);
props.onEditQuery?.(query, ast);
}}
>
<GraphiQL.Logo>
<div style={{ display: 'flex', alignItems: 'center' }}>
{typeof props?.logo === 'string' ? (
// if the logo is a string, then it's coming when rendering graphiql as a static page (see render-graphiql)
<div
style={{ width: 40, display: 'flex' }}
dangerouslySetInnerHTML={{ __html: props.logo }}
/>
) : (
// otherwise, it's used inside react and we can render it as a component
<div style={{ width: 40, display: 'flex' }}>{props?.logo || <YogaLogo />}</div>
)}
{typeof props?.title === 'string' ? (
// if the title is a string, then it's coming when rendering graphiql as a static page (see render-graphiql)
<span dangerouslySetInnerHTML={{ __html: props.title }} />
) : (
// otherwise, it's used inside react and we can render it as a component
<span>
{props?.title || (
<>
Yoga Graph
<em>i</em>
QL
</>
)}
</span>
)}
</div>
</GraphiQL.Logo>
</GraphiQLInterface>
</GraphiQLProvider>
<GraphiQL.Logo>
<div style={{ display: 'flex', alignItems: 'center' }}>
{typeof props?.logo === 'string' ? (
// if the logo is a string, then it's coming when rendering graphiql as a static page (see render-graphiql)
<div
style={{ width: 40, display: 'flex' }}
dangerouslySetInnerHTML={{ __html: props.logo }}
/>
) : (
// otherwise, it's used inside react and we can render it as a component
<div style={{ width: 40, display: 'flex' }}>{props?.logo || <YogaLogo />}</div>
)}
{typeof props?.title === 'string' ? (
// if the title is a string, then it's coming when rendering graphiql as a static page (see render-graphiql)
<span dangerouslySetInnerHTML={{ __html: props.title }} />
) : (
// otherwise, it's used inside react and we can render it as a component
<span>
{props?.title || (
<>
Yoga Graph
<em>i</em>
QL
</>
)}
</span>
)}
</div>
</GraphiQL.Logo>
</GraphiQL>
</div>
);
}
85 changes: 53 additions & 32 deletions packages/graphql-yoga/__integration-tests__/browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ import { setTimeout as setTimeout$ } from 'node:timers/promises';
// eslint-disable-next-line
import { Browser, chromium, ElementHandle, Page } from 'playwright';
import { fakePromise } from '@whatwg-node/server';
import { CORSOptions, createSchema, createYoga, GraphiQLOptions, Repeater } from '../src/index.js';
import {
ansiCodes,
CORSOptions,
createSchema,
createYoga,
GraphiQLOptions,
Repeater,
} from '../src/index.js';

let resolveOnReturn: VoidFunction;
const timeoutsSignal = new AbortController();
Expand Down Expand Up @@ -251,12 +258,36 @@ describe('browser', () => {
let port: number;
const server = createServer(yogaApp);

function wrapColor(msg: string, color?: string) {
if (color != null && color in ansiCodes) {
return `${ansiCodes[color as keyof typeof ansiCodes]}${msg}${ansiCodes.reset}`;
}
return msg;
}

beforeAll(async () => {
await new Promise<void>(resolve => server.listen(0, resolve));
port = (server.address() as AddressInfo).port;
browser = await chromium.launch({
headless: process.env['PLAYWRIGHT_HEADLESS'] !== 'false',
args: ['--incognito', '--no-sandbox', '--disable-setuid-sandbox'],
// eslint-disable-next-line unicorn/no-negated-condition, no-extra-boolean-cast
logger: !!process.env['DEBUG']
? {
isEnabled(_name: string) {
return true;
},
log(name, severity, message, args, hints) {
if (severity === 'error') {
// eslint-disable-next-line no-console
console.error(wrapColor(`[${name}] ${message}`, hints.color), ...args);
} else {
// eslint-disable-next-line no-console
console.log(wrapColor(`[${name}] ${message}`), ...args);
}
},
}
: undefined,
});
});
beforeEach(async () => {
Expand All @@ -281,38 +312,41 @@ describe('browser', () => {
});

const typeOperationText = async (text: string) => {
await page.type('.graphiql-query-editor .CodeMirror textarea', text, { delay: 300 });
await page.type('[data-uri*="operation"] textarea', text, { delay: 300 });
// TODO: figure out how we can avoid this wait
// it is very likely that there is a delay from textarea -> react state update
await setTimeout$(300);
};

const typeVariablesText = async (text: string) => {
await page.type('[aria-label="Variables"] .CodeMirror textarea', text, { delay: 100 });
await page.type('[data-uri*="variables"] textarea', text, { delay: 100 });
// TODO: figure out how we can avoid this wait
// it is very likely that there is a delay from textarea -> react state update
await setTimeout$(100);
};

const waitForResult = async (): Promise<object> => {
await page.waitForSelector('.graphiql-response .CodeMirror-code');
await page.waitForSelector('[data-uri*="response"] textarea');
await page.waitForFunction(
() =>
!!window.document.querySelector('.graphiql-response .CodeMirror-code')?.textContent?.trim(),
// @ts-expect-error - value is not null
() => !!window.document.querySelector('[data-uri*="response"] textarea')?.value?.trim(),
);
const resultContents = await page.evaluate(() => {
return window.document
.querySelector('.graphiql-response .CodeMirror-code')
?.textContent?.trim()
.replaceAll('\u00A0', ' ');
return (
window.document
.querySelector('[data-uri*="response"] textarea')
// @ts-expect-error - value is not null
?.value?.trim()
Comment on lines +331 to +339
Copy link
Preview

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Instead of using @ts-expect-error, consider using optional chaining or proper type assertions. The comment suggests confidence that value is not null, so a non-null assertion (!) would be more appropriate.

Copilot uses AI. Check for mistakes.

.replaceAll('\u00A0', ' ')
);
});

return JSON.parse(resultContents!);
};

const showGraphiQLSidebar = async () => {
// Click to show sidebar
await page.click('.graphiql-sidebar [aria-label="Show Documentation Explorer"]');
await page.click('[aria-label="Show Documentation Explorer"]');
};

const getElementText = async (element: ElementHandle<Element>) =>
Expand Down Expand Up @@ -472,8 +506,9 @@ describe('browser', () => {

await page.waitForFunction(() => {
const value = window.document
.querySelector('.graphiql-response .CodeMirror-code')
?.textContent?.trim()
.querySelector('[data-uri*="response"] textarea')
// @ts-expect-error - value is not null
?.value?.trim()
.replaceAll('\u00A0', ' ');

return value?.includes('2');
Expand Down Expand Up @@ -540,27 +575,14 @@ describe('browser', () => {

it('should include default header', async () => {
await page.goto(customGraphQLEndpoint);

await page.evaluate(() => {
const tabs = Array.from(
document.querySelectorAll('.graphiql-editor-tools button'),
) as HTMLButtonElement[];
tabs.find(tab => tab.textContent === 'Headers')!.click();
});

const headerContentEl$ = page.waitForSelector(
'section.graphiql-editor-tool .graphiql-editor:not(.hidden) pre.CodeMirror-line',
);

await expect(headerContentEl$).resolves.not.toBeNull();

await expect(
headerContentEl$.then(headerContentEl => getElementText(headerContentEl!)),
).resolves.toBe(defaultHeader);
await page.click('[data-name="headers"]');
await page.click('[data-uri*="headers"]');
const headerValue = await page.inputValue('[data-uri*="headers"] textarea');
expect(headerValue).toBe(defaultHeader);
});

it('supports input value deprecations', async () => {
await page.goto(`http://localhost:${port}${endpoint}`);
await page.goto(customGraphQLEndpoint);
await page.click('.graphiql-un-styled[data-index="0"]');
await page.click('a.graphiql-doc-explorer-type-name');
await page.getByText('Show Deprecated Fields').click();
Expand Down Expand Up @@ -764,7 +786,6 @@ describe('browser', () => {
endpoint: anotherEndpoint,
};
await page.goto(`http://localhost:${port}${endpoint}`);
await page.waitForSelector('.graphiql-query-editor .CodeMirror textarea');
await typeOperationText('{ greetings }');
await page.click(playButtonSelector);
await expect(waitForResult()).resolves.toEqual({
Expand Down
2 changes: 1 addition & 1 deletion packages/logger/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */

const ansiCodes = {
export const ansiCodes = {
red: '\x1b[31m',
yellow: '\x1b[33m',
magenta: '\x1b[35m',
Expand Down
Loading
Loading