UNPKG

15 kBJavaScriptView Raw
1"use strict";
2
3var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
4
5exports.__esModule = true;
6exports.default = compile;
7exports.processQueries = exports.parseQueries = exports.resolveThemes = void 0;
8
9var actions = _interopRequireWildcard(require("../redux/actions/internal"));
10
11/** Query compiler extracts queries and fragments from all files, validates them
12 * and then collocates them with fragments they require. This way fragments
13 * have global scope and can be used in any other query or fragment.
14 */
15const _ = require(`lodash`);
16
17const path = require(`path`);
18
19const normalize = require(`normalize-path`);
20
21const glob = require(`glob`);
22
23const {
24 validate,
25 print,
26 visit,
27 visitWithTypeInfo,
28 TypeInfo,
29 isAbstractType,
30 isObjectType,
31 isInterfaceType,
32 Kind,
33 FragmentsOnCompositeTypesRule,
34 KnownTypeNamesRule,
35 LoneAnonymousOperationRule,
36 PossibleFragmentSpreadsRule,
37 ScalarLeafsRule,
38 ValuesOfCorrectTypeRule,
39 VariablesAreInputTypesRule,
40 VariablesInAllowedPositionRule
41} = require(`graphql`);
42
43const getGatsbyDependents = require(`../utils/gatsby-dependents`);
44
45const {
46 store
47} = require(`../redux`);
48
49const {
50 default: FileParser
51} = require(`./file-parser`);
52
53const {
54 graphqlError,
55 multipleRootQueriesError,
56 duplicateFragmentError,
57 unknownFragmentError
58} = require(`./graphql-errors`);
59
60const report = require(`gatsby-cli/lib/reporter`);
61
62const {
63 default: errorParser,
64 locInGraphQlToLocInFile
65} = require(`./error-parser`);
66
67const websocketManager = require(`../utils/websocket-manager`);
68
69const overlayErrorID = `graphql-compiler`;
70
71async function compile({
72 parentSpan
73} = {}) {
74 // TODO: swap plugins to themes
75 const {
76 program,
77 schema,
78 themes,
79 flattenedPlugins
80 } = store.getState();
81 const activity = report.activityTimer(`extract queries from components`, {
82 parentSpan,
83 id: `query-extraction`
84 });
85 activity.start();
86 const errors = [];
87 const addError = errors.push.bind(errors);
88 const parsedQueries = await parseQueries({
89 base: program.directory,
90 additional: resolveThemes(themes.themes ? themes.themes : flattenedPlugins.map(plugin => {
91 return {
92 themeDir: plugin.pluginFilepath
93 };
94 })),
95 addError,
96 parentSpan
97 });
98 const queries = processQueries({
99 schema,
100 parsedQueries,
101 addError,
102 parentSpan
103 });
104
105 if (errors.length !== 0) {
106 const structuredErrors = activity.panicOnBuild(errors);
107
108 if (process.env.gatsby_executing_command === `develop`) {
109 websocketManager.emitError(overlayErrorID, structuredErrors);
110 }
111 } else {
112 if (process.env.gatsby_executing_command === `develop`) {
113 // emitError with `null` as 2nd param to clear browser error overlay
114 websocketManager.emitError(overlayErrorID, null);
115 }
116 }
117
118 activity.end();
119 return queries;
120}
121
122const resolveThemes = (themes = []) => themes.reduce((merged, theme) => {
123 merged.push(theme.themeDir);
124 return merged;
125}, []);
126
127exports.resolveThemes = resolveThemes;
128
129const parseQueries = async ({
130 base,
131 additional,
132 addError,
133 parentSpan
134}) => {
135 const filesRegex = `*.+(t|j)s?(x)`; // Pattern that will be appended to searched directories.
136 // It will match any .js, .jsx, .ts, and .tsx files, that are not
137 // inside <searched_directory>/node_modules.
138
139 const pathRegex = `/{${filesRegex},!(node_modules)/**/${filesRegex}}`;
140 const modulesThatUseGatsby = await getGatsbyDependents();
141 let files = [path.join(base, `src`), path.join(base, `.cache`, `fragments`), ...additional.map(additional => path.join(additional, `src`)), ...modulesThatUseGatsby.map(module => module.path)].reduce((merged, folderPath) => {
142 merged.push(...glob.sync(path.join(folderPath, pathRegex), {
143 nodir: true
144 }));
145 return merged;
146 }, []);
147 files = files.filter(d => !d.match(/\.d\.ts$/));
148 files = files.map(normalize); // We should be able to remove the following and preliminary tests do suggest
149 // that they aren't needed anymore since we transpile node_modules now
150 // However, there could be some cases (where a page is outside of src for example)
151 // that warrant keeping this and removing later once we have more confidence (and tests)
152 // Ensure all page components added as they're not necessarily in the
153 // pages directory e.g. a plugin could add a page component. Plugins
154 // *should* copy their components (if they add a query) to .cache so that
155 // our babel plugin to remove the query on building is active.
156 // Otherwise the component will throw an error in the browser of
157 // "graphql is not defined".
158
159 files = files.concat(Array.from(store.getState().components.keys(), c => normalize(c)));
160 files = _.uniq(files);
161 const parser = new FileParser({
162 parentSpan: parentSpan
163 });
164 return await parser.parseFiles(files, addError);
165};
166
167exports.parseQueries = parseQueries;
168
169const processQueries = ({
170 schema,
171 parsedQueries,
172 addError,
173 parentSpan
174}) => {
175 const {
176 definitionsByName,
177 operations
178 } = extractOperations(schema, parsedQueries, addError, parentSpan);
179 return processDefinitions({
180 schema,
181 operations,
182 definitionsByName,
183 addError,
184 parentSpan
185 });
186};
187
188exports.processQueries = processQueries;
189const preValidationRules = [LoneAnonymousOperationRule, KnownTypeNamesRule, FragmentsOnCompositeTypesRule, VariablesAreInputTypesRule, ScalarLeafsRule, PossibleFragmentSpreadsRule, ValuesOfCorrectTypeRule, VariablesInAllowedPositionRule];
190
191const extractOperations = (schema, parsedQueries, addError, parentSpan) => {
192 const definitionsByName = new Map();
193 const operations = [];
194
195 for (const {
196 filePath,
197 text,
198 templateLoc,
199 hash,
200 doc,
201 isHook,
202 isStaticQuery
203 } of parsedQueries) {
204 const errors = validate(schema, doc, preValidationRules);
205
206 if (errors && errors.length) {
207 addError(...errors.map(error => {
208 const location = {
209 start: locInGraphQlToLocInFile(templateLoc, error.locations[0])
210 };
211 return errorParser({
212 message: error.message,
213 filePath,
214 location
215 });
216 }));
217 store.dispatch(actions.queryExtractionGraphQLError({
218 componentPath: filePath
219 })); // Something is super wrong with this document, so we report it and skip
220
221 continue;
222 }
223
224 doc.definitions.forEach(def => {
225 const name = def.name.value;
226 let printedAst = null;
227
228 if (def.kind === Kind.OPERATION_DEFINITION) {
229 operations.push(def);
230 } else if (def.kind === Kind.FRAGMENT_DEFINITION) {
231 // Check if we already registered a fragment with this name
232 printedAst = print(def);
233
234 if (definitionsByName.has(name)) {
235 const otherDef = definitionsByName.get(name); // If it's not an accidental duplicate fragment, but is a different
236 // one - we report an error
237
238 if (printedAst !== otherDef.printedAst) {
239 addError(duplicateFragmentError({
240 name,
241 leftDefinition: {
242 def,
243 filePath,
244 text,
245 templateLoc
246 },
247 rightDefinition: otherDef
248 })); // We won't know which one to use, so it's better to fail both of
249 // them.
250
251 definitionsByName.delete(name);
252 }
253
254 return;
255 }
256 }
257
258 definitionsByName.set(name, {
259 name,
260 def,
261 filePath,
262 text: text,
263 templateLoc,
264 printedAst,
265 isHook,
266 isStaticQuery,
267 isFragment: def.kind === Kind.FRAGMENT_DEFINITION,
268 hash: hash
269 });
270 });
271 }
272
273 return {
274 definitionsByName,
275 operations
276 };
277};
278
279const processDefinitions = ({
280 schema,
281 operations,
282 definitionsByName,
283 addError,
284 parentSpan
285}) => {
286 const processedQueries = new Map();
287 const fragmentsUsedByFragment = new Map();
288 const fragmentNames = Array.from(definitionsByName.entries()).filter(([_, def]) => def.isFragment).map(([name, _]) => name);
289
290 for (const operation of operations) {
291 const name = operation.name.value;
292 const originalDefinition = definitionsByName.get(name);
293 const filePath = definitionsByName.get(name).filePath;
294
295 if (processedQueries.has(filePath)) {
296 const otherQuery = processedQueries.get(filePath);
297 addError(multipleRootQueriesError(filePath, originalDefinition.def, otherQuery && definitionsByName.get(otherQuery.name).def));
298 store.dispatch(actions.queryExtractionGraphQLError({
299 componentPath: filePath
300 }));
301 continue;
302 }
303
304 const {
305 usedFragments,
306 missingFragments
307 } = determineUsedFragmentsForDefinition(originalDefinition, definitionsByName, fragmentsUsedByFragment);
308
309 if (missingFragments.length > 0) {
310 for (const {
311 filePath,
312 definition,
313 node
314 } of missingFragments) {
315 store.dispatch(actions.queryExtractionGraphQLError({
316 componentPath: filePath
317 }));
318 addError(unknownFragmentError({
319 fragmentNames,
320 filePath,
321 definition,
322 node
323 }));
324 }
325
326 continue;
327 }
328
329 let document = {
330 kind: Kind.DOCUMENT,
331 definitions: Array.from(usedFragments.values()).map(name => definitionsByName.get(name).def).concat([operation])
332 };
333 const errors = validate(schema, document);
334
335 if (errors && errors.length) {
336 for (const error of errors) {
337 const {
338 formattedMessage,
339 message
340 } = graphqlError(definitionsByName, error);
341 const filePath = originalDefinition.filePath;
342 store.dispatch(actions.queryExtractionGraphQLError({
343 componentPath: filePath,
344 error: formattedMessage
345 }));
346 const location = locInGraphQlToLocInFile(originalDefinition.templateLoc, error.locations[0]);
347 addError(errorParser({
348 location: {
349 start: location,
350 end: location
351 },
352 message,
353 filePath
354 }));
355 }
356
357 continue;
358 }
359
360 document = addExtraFields(document, schema);
361 const query = {
362 name,
363 text: print(document),
364 originalText: originalDefinition.text,
365 path: filePath,
366 isHook: originalDefinition.isHook,
367 isStaticQuery: originalDefinition.isStaticQuery,
368 hash: originalDefinition.hash
369 };
370
371 if (query.isStaticQuery) {
372 query.id = `sq--` + _.kebabCase(`${path.relative(store.getState().program.directory, filePath)}`);
373 }
374
375 if (query.isHook && process.env.NODE_ENV === `production` && typeof require(`react`).useContext !== `function`) {
376 report.panicOnBuild(`You're likely using a version of React that doesn't support Hooks\n` + `Please update React and ReactDOM to 16.8.0 or later to use the useStaticQuery hook.`);
377 }
378
379 processedQueries.set(filePath, query);
380 }
381
382 return processedQueries;
383};
384
385const determineUsedFragmentsForDefinition = (definition, definitionsByName, fragmentsUsedByFragment, visitedFragmentDefinitions = new Set()) => {
386 const {
387 def,
388 name,
389 isFragment,
390 filePath
391 } = definition;
392 const cachedUsedFragments = fragmentsUsedByFragment.get(name);
393
394 if (cachedUsedFragments) {
395 return {
396 usedFragments: cachedUsedFragments,
397 missingFragments: []
398 };
399 } else {
400 const usedFragments = new Set();
401 const missingFragments = [];
402 visit(def, {
403 [Kind.FRAGMENT_SPREAD]: node => {
404 const name = node.name.value;
405 const fragmentDefinition = definitionsByName.get(name);
406
407 if (visitedFragmentDefinitions.has(fragmentDefinition)) {
408 return;
409 }
410
411 visitedFragmentDefinitions.add(fragmentDefinition);
412
413 if (fragmentDefinition) {
414 usedFragments.add(name);
415 const {
416 usedFragments: usedFragmentsForFragment,
417 missingFragments: missingFragmentsForFragment
418 } = determineUsedFragmentsForDefinition(fragmentDefinition, definitionsByName, fragmentsUsedByFragment, visitedFragmentDefinitions);
419 usedFragmentsForFragment.forEach(fragmentName => usedFragments.add(fragmentName));
420 missingFragments.push(...missingFragmentsForFragment);
421 } else {
422 missingFragments.push({
423 filePath,
424 definition,
425 node
426 });
427 }
428 }
429 });
430
431 if (isFragment) {
432 fragmentsUsedByFragment.set(name, usedFragments);
433 }
434
435 return {
436 usedFragments,
437 missingFragments
438 };
439 }
440};
441/**
442 * Automatically add:
443 * `__typename` field to abstract types (unions, interfaces)
444 * `id` field to all object/interface types having an id
445 * TODO: Remove this in v3.0 as it is a legacy from Relay compiler
446 */
447
448
449const addExtraFields = (document, schema) => {
450 const typeInfo = new TypeInfo(schema);
451 const contextStack = [];
452 const transformer = visitWithTypeInfo(typeInfo, {
453 enter: {
454 [Kind.SELECTION_SET]: node => {
455 // Entering selection set:
456 // selection sets can be nested, so keeping their metadata stacked
457 contextStack.push({
458 hasTypename: false,
459 hasId: false
460 });
461 },
462 [Kind.FIELD]: node => {
463 // Entering a field of the current selection-set:
464 // mark which fields already exist in this selection set to avoid duplicates
465 const context = contextStack[contextStack.length - 1];
466
467 if (node.name.value === `__typename`) {
468 context.hasTypename = true;
469 }
470
471 if (node.name.value === `id`) {
472 context.hasId = true;
473 }
474 }
475 },
476 leave: {
477 [Kind.SELECTION_SET]: node => {
478 // Modify the selection-set AST on leave (add extra fields unless they already exist)
479 const context = contextStack.pop();
480 const parentType = typeInfo.getParentType();
481 const extraFields = []; // Adding __typename to unions and interfaces (if required)
482
483 if (!context.hasTypename && isAbstractType(parentType)) {
484 extraFields.push({
485 kind: Kind.FIELD,
486 name: {
487 kind: Kind.NAME,
488 value: `__typename`
489 }
490 });
491 }
492
493 if (!context.hasId && (isObjectType(parentType) || isInterfaceType(parentType)) && hasIdField(parentType)) {
494 extraFields.push({
495 kind: Kind.FIELD,
496 name: {
497 kind: Kind.NAME,
498 value: `id`
499 }
500 });
501 }
502
503 return extraFields.length > 0 ? Object.assign({}, node, {
504 selections: [...extraFields, ...node.selections]
505 }) : undefined;
506 }
507 }
508 });
509 return visit(document, transformer);
510};
511
512const hasIdField = type => {
513 const idField = type.getFields()[`id`];
514 const fieldType = idField ? String(idField.type) : ``;
515 return fieldType === `ID` || fieldType === `ID!`;
516};
517//# sourceMappingURL=query-compiler.js.map
\No newline at end of file