1 | "use strict";
|
2 |
|
3 | var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
|
4 |
|
5 | exports.__esModule = true;
|
6 | exports.default = compile;
|
7 | exports.processQueries = exports.parseQueries = exports.resolveThemes = void 0;
|
8 |
|
9 | var actions = _interopRequireWildcard(require("../redux/actions/internal"));
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | const _ = require(`lodash`);
|
16 |
|
17 | const path = require(`path`);
|
18 |
|
19 | const normalize = require(`normalize-path`);
|
20 |
|
21 | const glob = require(`glob`);
|
22 |
|
23 | const {
|
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 |
|
43 | const getGatsbyDependents = require(`../utils/gatsby-dependents`);
|
44 |
|
45 | const {
|
46 | store
|
47 | } = require(`../redux`);
|
48 |
|
49 | const {
|
50 | default: FileParser
|
51 | } = require(`./file-parser`);
|
52 |
|
53 | const {
|
54 | graphqlError,
|
55 | multipleRootQueriesError,
|
56 | duplicateFragmentError,
|
57 | unknownFragmentError
|
58 | } = require(`./graphql-errors`);
|
59 |
|
60 | const report = require(`gatsby-cli/lib/reporter`);
|
61 |
|
62 | const {
|
63 | default: errorParser,
|
64 | locInGraphQlToLocInFile
|
65 | } = require(`./error-parser`);
|
66 |
|
67 | const websocketManager = require(`../utils/websocket-manager`);
|
68 |
|
69 | const overlayErrorID = `graphql-compiler`;
|
70 |
|
71 | async function compile({
|
72 | parentSpan
|
73 | } = {}) {
|
74 |
|
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 |
|
114 | websocketManager.emitError(overlayErrorID, null);
|
115 | }
|
116 | }
|
117 |
|
118 | activity.end();
|
119 | return queries;
|
120 | }
|
121 |
|
122 | const resolveThemes = (themes = []) => themes.reduce((merged, theme) => {
|
123 | merged.push(theme.themeDir);
|
124 | return merged;
|
125 | }, []);
|
126 |
|
127 | exports.resolveThemes = resolveThemes;
|
128 |
|
129 | const parseQueries = async ({
|
130 | base,
|
131 | additional,
|
132 | addError,
|
133 | parentSpan
|
134 | }) => {
|
135 | const filesRegex = `*.+(t|j)s?(x)`;
|
136 |
|
137 |
|
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);
|
149 |
|
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
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 |
|
167 | exports.parseQueries = parseQueries;
|
168 |
|
169 | const 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 |
|
188 | exports.processQueries = processQueries;
|
189 | const preValidationRules = [LoneAnonymousOperationRule, KnownTypeNamesRule, FragmentsOnCompositeTypesRule, VariablesAreInputTypesRule, ScalarLeafsRule, PossibleFragmentSpreadsRule, ValuesOfCorrectTypeRule, VariablesInAllowedPositionRule];
|
190 |
|
191 | const 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 | }));
|
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 |
|
232 | printedAst = print(def);
|
233 |
|
234 | if (definitionsByName.has(name)) {
|
235 | const otherDef = definitionsByName.get(name);
|
236 |
|
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 | }));
|
249 |
|
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 |
|
279 | const 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 |
|
385 | const 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 |
|
443 |
|
444 |
|
445 |
|
446 |
|
447 |
|
448 |
|
449 | const 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 |
|
456 |
|
457 | contextStack.push({
|
458 | hasTypename: false,
|
459 | hasId: false
|
460 | });
|
461 | },
|
462 | [Kind.FIELD]: node => {
|
463 |
|
464 |
|
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 |
|
479 | const context = contextStack.pop();
|
480 | const parentType = typeInfo.getParentType();
|
481 | const extraFields = [];
|
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 |
|
512 | const hasIdField = type => {
|
513 | const idField = type.getFields()[`id`];
|
514 | const fieldType = idField ? String(idField.type) : ``;
|
515 | return fieldType === `ID` || fieldType === `ID!`;
|
516 | };
|
517 |
|
\ | No newline at end of file |