Skip to content

Commit 2c0586d

Browse files
authored
use prettier format for query editor since we already use prettier for jsonc editors (#4049)
* use `allowTrailingComma` option in jsonc parser to make `tryParseJsonObject` sync parse introspection headers with jsonc parser * use `allowTrailingComma` option in jsonc parser to make `tryParseJsonObject` sync parse introspection headers with jsonc parser * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * upd * rm
1 parent b481a06 commit 2c0586d

File tree

8 files changed

+109
-137
lines changed

8 files changed

+109
-137
lines changed

.changeset/proud-bottles-punch.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@graphiql/react': patch
3+
'graphiql': patch
4+
---
5+
6+
- use `allowTrailingComma` option in jsonc parser to make `tryParseJsonObject` sync
7+
- parse introspection headers with jsonc parser
8+
- use prettier format for query editor since we already use prettier for jsonc editors

packages/graphiql-react/src/constants.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/* eslint-disable no-bitwise */
22
import { initializeMode } from 'monaco-graphql/esm/lite.js';
3-
import { parse, print } from 'graphql';
3+
// @ts-expect-error -- wrong types
4+
import { printers } from 'prettier/plugins/graphql'; // eslint-disable-line import-x/no-duplicates
5+
import { parsers } from 'prettier/parser-graphql'; // eslint-disable-line import-x/no-duplicates
6+
import prettier from 'prettier/standalone';
47
import { KeyCode, KeyMod, Uri, languages } from './monaco-editor';
58
import type { EditorSlice } from './stores';
69

@@ -143,4 +146,12 @@ export const MONACO_GRAPHQL_API = initializeMode({
143146
});
144147

145148
export const DEFAULT_PRETTIFY_QUERY: EditorSlice['onPrettifyQuery'] = query =>
146-
print(parse(query));
149+
prettier.format(query, {
150+
parser: 'graphql',
151+
plugins: [
152+
// Fix: Couldn't find plugin for AST format "graphql"
153+
{ printers },
154+
// @ts-expect-error -- Fix: Couldn't resolve parser "graphql"
155+
{ parsers },
156+
],
157+
});

packages/graphiql-react/src/stores/editor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,9 @@ export interface EditorSlice extends TabsState {
117117
* Invoked when the prettify callback is invoked.
118118
* @param query - The current value of the query editor.
119119
* @default
120-
* import { parse, print } from 'graphql'
120+
* import prettier from 'prettier/standalone'
121121
*
122-
* (query) => print(parse(query))
122+
* prettier.format(query, { parser: 'graphql' })
123123
* @returns The formatted query.
124124
*/
125125
onPrettifyQuery: (query: string) => MaybePromise<string>;

packages/graphiql-react/src/stores/execution.ts

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import setValue from 'set-value';
1414
import getValue from 'get-value';
1515

1616
import type { StateCreator } from 'zustand';
17-
import { formatJSONC, parseJSONC } from '../utility';
17+
import { tryParseJSONC } from '../utility';
1818
import type { SlicesWithActions, MonacoEditor } from '../types';
1919
import { Range } from '../monaco-editor';
2020

@@ -214,19 +214,14 @@ export const createExecutionSlice: CreateExecutionSlice =
214214
actions.updateActiveTabValues({ response: value });
215215
}
216216

217-
function setError(error: unknown, editor?: MonacoEditor): void {
217+
function setError(error: Error, editor?: MonacoEditor): void {
218218
if (!editor) {
219219
return;
220220
}
221-
let message;
222-
const name = editor === variableEditor ? 'Variables' : 'Headers';
223-
if (error instanceof TypeError) {
224-
message = `${name} are not a JSON object.`;
225-
} else {
226-
message = `${name} are invalid JSON: ${error instanceof Error ? error.message : error}.`;
227-
}
228-
// Need to stringify since the response editor uses `json` language
229-
setResponse(formatError({ message }));
221+
const name =
222+
editor === variableEditor ? 'Variables' : 'Request headers';
223+
// Need to format since the response editor uses `json` language
224+
setResponse(formatError({ message: `${name} ${error.message}` }));
230225
}
231226

232227
const newQueryId = queryId + 1;
@@ -239,16 +234,16 @@ export const createExecutionSlice: CreateExecutionSlice =
239234

240235
let variables: Record<string, unknown> | undefined;
241236
try {
242-
variables = await tryParseJsonObject(variableEditor?.getValue());
237+
variables = tryParseJSONC(variableEditor?.getValue());
243238
} catch (error) {
244-
setError(error, variableEditor);
239+
setError(error as Error, variableEditor);
245240
return;
246241
}
247242
let headers: Record<string, unknown> | undefined;
248243
try {
249-
headers = await tryParseJsonObject(headerEditor?.getValue());
244+
headers = tryParseJSONC(headerEditor?.getValue());
250245
} catch (error) {
251-
setError(error, headerEditor);
246+
setError(error as Error, headerEditor);
252247
return;
253248
}
254249
const fragmentDependencies = documentAST
@@ -338,23 +333,6 @@ export const createExecutionSlice: CreateExecutionSlice =
338333
};
339334
};
340335

341-
async function tryParseJsonObject(
342-
json = '',
343-
): Promise<Record<string, unknown> | undefined> {
344-
// `jsonc-parser` doesn't support trailing commas,
345-
// so we need first to format with prettier, which will remove them
346-
const formatted = await formatJSONC(json);
347-
const parsed = parseJSONC(formatted);
348-
if (!parsed) {
349-
return;
350-
}
351-
const isObject = typeof parsed === 'object' && !Array.isArray(parsed);
352-
if (!isObject) {
353-
throw new TypeError();
354-
}
355-
return parsed;
356-
}
357-
358336
interface IncrementalResult {
359337
data?: Record<string, unknown> | null;
360338
errors?: ReadonlyArray<GraphQLError>;

packages/graphiql-react/src/stores/schema.ts

Lines changed: 44 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import type { Dispatch } from 'react';
1616
import type { StateCreator } from 'zustand';
1717
import type { SlicesWithActions, SchemaReference } from '../types';
18+
import { tryParseJSONC } from '../utility';
1819

1920
type MaybeGraphQLSchema = GraphQLSchema | null | undefined;
2021

@@ -59,7 +60,9 @@ export const createSchemaSlice: CreateSchemaSlice = initial => (set, get) => ({
5960
onSchemaChange,
6061
headerEditor,
6162
fetcher,
62-
...rest
63+
inputValueDeprecation,
64+
introspectionQueryName,
65+
schemaDescription,
6366
} = get();
6467

6568
/**
@@ -71,67 +74,57 @@ export const createSchemaSlice: CreateSchemaSlice = initial => (set, get) => ({
7174
return;
7275
}
7376
const counter = requestCounter + 1;
74-
set({ requestCounter: counter });
75-
77+
set({ requestCounter: counter, isIntrospecting: true, fetchError: null });
7678
try {
77-
const currentHeaders = headerEditor?.getValue();
78-
const parsedHeaders = parseHeaderString(currentHeaders);
79-
if (!parsedHeaders.isValidJSON) {
80-
set({ fetchError: 'Introspection failed as headers are invalid.' });
81-
return;
79+
let headers: Record<string, unknown> | undefined;
80+
try {
81+
headers = tryParseJSONC(headerEditor?.getValue());
82+
} catch (error) {
83+
throw new Error(
84+
`Introspection failed. Request headers ${error instanceof Error ? error.message : error}`,
85+
);
8286
}
8387

84-
const fetcherOpts: FetcherOpts = parsedHeaders.headers
85-
? { headers: parsedHeaders.headers }
86-
: {};
87-
88+
const fetcherOpts: FetcherOpts = headers ? { headers } : {};
8889
/**
8990
* Get an introspection query for settings given via props
9091
*/
91-
const {
92-
introspectionQuery,
93-
introspectionQueryName,
94-
introspectionQuerySansSubscriptions,
95-
} = generateIntrospectionQuery(rest);
96-
const fetch = fetcherReturnToPromise(
97-
fetcher(
98-
{
99-
query: introspectionQuery,
100-
operationName: introspectionQueryName,
101-
},
102-
fetcherOpts,
103-
),
104-
);
105-
106-
if (!isPromise(fetch)) {
107-
set({
108-
fetchError: 'Fetcher did not return a Promise for introspection.',
109-
});
110-
return;
111-
}
112-
set({ isIntrospecting: true, fetchError: null });
113-
let result = await fetch;
92+
const introspectionQuery = getIntrospectionQuery({
93+
inputValueDeprecation,
94+
schemaDescription,
95+
});
11496

115-
if (typeof result !== 'object' || !('data' in result)) {
116-
// Try the stock introspection query first, falling back on the
117-
// sans-subscriptions query for services which do not yet support it.
118-
const fetch2 = fetcherReturnToPromise(
97+
function doIntrospection(query: string) {
98+
const fetch = fetcherReturnToPromise(
11999
fetcher(
120-
{
121-
query: introspectionQuerySansSubscriptions,
122-
operationName: introspectionQueryName,
123-
},
100+
{ query, operationName: introspectionQueryName },
124101
fetcherOpts,
125102
),
126103
);
127-
if (!isPromise(fetch2)) {
128-
throw new Error(
104+
if (!isPromise(fetch)) {
105+
throw new TypeError(
129106
'Fetcher did not return a Promise for introspection.',
130107
);
131108
}
132-
result = await fetch2;
109+
return fetch;
133110
}
134111

112+
const normalizedQuery =
113+
introspectionQueryName === 'IntrospectionQuery'
114+
? introspectionQuery
115+
: introspectionQuery.replace(
116+
'query IntrospectionQuery',
117+
`query ${introspectionQueryName}`,
118+
);
119+
let result = await doIntrospection(normalizedQuery);
120+
121+
if (typeof result !== 'object' || !('data' in result)) {
122+
// Try the stock introspection query first, falling back on the
123+
// sans-subscriptions query for services which do not yet support it.
124+
result = await doIntrospection(
125+
introspectionQuery.replace('subscriptionType { name }', ''),
126+
);
127+
}
135128
set({ isIntrospecting: false });
136129
let introspectionData: IntrospectionQuery | undefined;
137130
if (result.data && '__schema' in result.data) {
@@ -160,9 +153,12 @@ export const createSchemaSlice: CreateSchemaSlice = initial => (set, get) => ({
160153
if (counter !== get().requestCounter) {
161154
return;
162155
}
156+
if (error instanceof Error) {
157+
delete error.stack;
158+
}
163159
set({
164-
fetchError: formatError(error),
165160
isIntrospecting: false,
161+
fetchError: formatError(error),
166162
});
167163
}
168164
},
@@ -233,7 +229,7 @@ export interface SchemaActions {
233229
setSchemaReference: Dispatch<SchemaReference>;
234230
}
235231

236-
export interface SchemaProps extends IntrospectionArgs {
232+
export interface SchemaProps {
237233
/**
238234
* This prop can be used to skip validating the GraphQL schema. This applies
239235
* to both schemas fetched via introspection and schemas explicitly passed
@@ -272,9 +268,7 @@ export interface SchemaProps extends IntrospectionArgs {
272268
* run without a schema.
273269
*/
274270
schema?: GraphQLSchema | IntrospectionQuery | null;
275-
}
276271

277-
interface IntrospectionArgs {
278272
/**
279273
* Can be used to set the equally named option for introspecting a GraphQL
280274
* server.
@@ -297,45 +291,3 @@ interface IntrospectionArgs {
297291
*/
298292
schemaDescription?: boolean;
299293
}
300-
301-
function generateIntrospectionQuery({
302-
inputValueDeprecation,
303-
introspectionQueryName,
304-
schemaDescription,
305-
}: IntrospectionArgs) {
306-
const query = getIntrospectionQuery({
307-
inputValueDeprecation,
308-
schemaDescription,
309-
});
310-
const introspectionQuery =
311-
introspectionQueryName === 'IntrospectionQuery'
312-
? query
313-
: query.replace(
314-
'query IntrospectionQuery',
315-
`query ${introspectionQueryName}`,
316-
);
317-
const introspectionQuerySansSubscriptions = query.replace(
318-
'subscriptionType { name }',
319-
'',
320-
);
321-
322-
return {
323-
introspectionQueryName,
324-
introspectionQuery,
325-
introspectionQuerySansSubscriptions,
326-
};
327-
}
328-
329-
function parseHeaderString(headersString?: string) {
330-
let headers: Record<string, unknown> | null = null;
331-
let isValidJSON = true;
332-
333-
try {
334-
if (headersString) {
335-
headers = JSON.parse(headersString);
336-
}
337-
} catch {
338-
isValidJSON = false;
339-
}
340-
return { headers, isValidJSON };
341-
}

packages/graphiql-react/src/utility/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export {
66
onEditorContainerKeyDown,
77
} from './create-editor';
88
export { debounce } from './debounce';
9-
export { formatJSONC, parseJSONC } from './jsonc';
9+
export { formatJSONC, parseJSONC, tryParseJSONC } from './jsonc';
1010
export { markdown } from './markdown';
1111
export { pick } from './pick';
1212
export { useDragResize } from './resize';

packages/graphiql-react/src/utility/jsonc.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import prettier from 'prettier/standalone';
22
// @ts-expect-error -- wrong types
33
import { printers } from 'prettier/plugins/estree';
4-
import { parsers as parsersBabel } from 'prettier/parser-babel';
4+
import { parsers } from 'prettier/parser-babel';
55
import {
66
parse as jsoncParse,
77
ParseError,
@@ -12,10 +12,10 @@ export function formatJSONC(content: string) {
1212
return prettier.format(content, {
1313
parser: 'jsonc',
1414
plugins: [
15-
// Fixes ConfigError: Couldn't find plugin for AST format "estree"
15+
// Fix: Couldn't find plugin for AST format "estree"
1616
{ printers },
17-
// @ts-expect-error -- Fixes ConfigError: Couldn't resolve parser "jsonc"
18-
{ parsers: parsersBabel },
17+
// @ts-expect-error -- Fix Couldn't resolve parser "jsonc"
18+
{ parsers },
1919
],
2020
// always split into new lines, e.g. {"foo":true} => {\n "foo": true\n}
2121
printWidth: 0,
@@ -30,8 +30,14 @@ const formatter = new Intl.ListFormat('en', {
3030
export function parseJSONC(content: string) {
3131
const errors: ParseError[] = [];
3232

33-
const parsed = content.trim() && jsoncParse(content, errors);
34-
33+
const parsed: undefined | Record<string, unknown> = jsoncParse(
34+
content,
35+
errors,
36+
{
37+
allowTrailingComma: true,
38+
allowEmptyContent: true,
39+
},
40+
);
3541
if (errors.length) {
3642
const output = formatter.format(
3743
errors.map(({ error }) => printParseErrorCode(error)),
@@ -40,3 +46,22 @@ export function parseJSONC(content: string) {
4046
}
4147
return parsed;
4248
}
49+
50+
export function tryParseJSONC(json = '') {
51+
let parsed: Record<string, unknown> | undefined;
52+
try {
53+
parsed = parseJSONC(json);
54+
} catch (error) {
55+
throw new Error(
56+
`are invalid JSON: ${error instanceof Error ? error.message : error}.`,
57+
);
58+
}
59+
if (!parsed) {
60+
return;
61+
}
62+
const isObject = typeof parsed === 'object' && !Array.isArray(parsed);
63+
if (!isObject) {
64+
throw new TypeError('are not a JSON object.');
65+
}
66+
return parsed;
67+
}

0 commit comments

Comments
 (0)