UNPKG

13.3 kBJavaScriptView Raw
1"use strict";
2
3const Promise = require(`bluebird`);
4
5const _ = require(`lodash`);
6
7const chalk = require(`chalk`);
8
9const {
10 bindActionCreators
11} = require(`redux`);
12
13const tracer = require(`opentracing`).globalTracer();
14
15const reporter = require(`gatsby-cli/lib/reporter`);
16
17const stackTrace = require(`stack-trace`);
18
19const {
20 codeFrameColumns
21} = require(`@babel/code-frame`);
22
23const fs = require(`fs-extra`);
24
25const getCache = require(`./get-cache`);
26
27const createNodeId = require(`./create-node-id`);
28
29const {
30 createContentDigest
31} = require(`gatsby-core-utils`);
32
33const {
34 buildObjectType,
35 buildUnionType,
36 buildInterfaceType,
37 buildInputObjectType,
38 buildEnumType,
39 buildScalarType
40} = require(`../schema/types/type-builders`);
41
42const {
43 emitter,
44 store
45} = require(`../redux`);
46
47const getPublicPath = require(`./get-public-path`);
48
49const {
50 getNonGatsbyCodeFrameFormatted
51} = require(`./stack-trace-utils`);
52
53const {
54 trackBuildError,
55 decorateEvent
56} = require(`gatsby-telemetry`);
57
58const {
59 default: errorParser
60} = require(`./api-runner-error-parser`); // Bind action creators per plugin so we can auto-add
61// metadata to actions they create.
62
63
64const boundPluginActionCreators = {};
65
66const doubleBind = (boundActionCreators, api, plugin, actionOptions) => {
67 const {
68 traceId
69 } = actionOptions;
70
71 if (boundPluginActionCreators[plugin.name + api + traceId]) {
72 return boundPluginActionCreators[plugin.name + api + traceId];
73 } else {
74 const keys = Object.keys(boundActionCreators);
75 const doubleBoundActionCreators = {};
76
77 for (let i = 0; i < keys.length; i++) {
78 const key = keys[i];
79 const boundActionCreator = boundActionCreators[key];
80
81 if (typeof boundActionCreator === `function`) {
82 doubleBoundActionCreators[key] = (...args) => {
83 // Let action callers override who the plugin is. Shouldn't be
84 // used that often.
85 if (args.length === 1) {
86 return boundActionCreator(args[0], plugin, actionOptions);
87 } else if (args.length === 2) {
88 return boundActionCreator(args[0], args[1], actionOptions);
89 }
90
91 return undefined;
92 };
93 }
94 }
95
96 boundPluginActionCreators[plugin.name + api + traceId] = doubleBoundActionCreators;
97 return doubleBoundActionCreators;
98 }
99};
100
101const initAPICallTracing = parentSpan => {
102 const startSpan = (spanName, spanArgs = {}) => {
103 const defaultSpanArgs = {
104 childOf: parentSpan
105 };
106 return tracer.startSpan(spanName, _.merge(defaultSpanArgs, spanArgs));
107 };
108
109 return {
110 tracer,
111 parentSpan,
112 startSpan
113 };
114};
115
116const getLocalReporter = (activity, reporter) => activity ? Object.assign({}, reporter, {
117 panicOnBuild: activity.panicOnBuild.bind(activity)
118}) : reporter;
119
120const runAPI = (plugin, api, args, activity) => {
121 const gatsbyNode = require(`${plugin.resolve}/gatsby-node`);
122
123 if (gatsbyNode[api]) {
124 const parentSpan = args && args.parentSpan;
125 const spanOptions = parentSpan ? {
126 childOf: parentSpan
127 } : {};
128 const pluginSpan = tracer.startSpan(`run-plugin`, spanOptions);
129 pluginSpan.setTag(`api`, api);
130 pluginSpan.setTag(`plugin`, plugin.name);
131
132 const {
133 loadNodeContent,
134 getNodes,
135 getNode,
136 getNodesByType,
137 hasNodeChanged,
138 getNodeAndSavePathDependency
139 } = require(`../db/nodes`);
140
141 const {
142 publicActions,
143 restrictedActionsAvailableInAPI
144 } = require(`../redux/actions`);
145
146 const availableActions = Object.assign({}, publicActions, {}, restrictedActionsAvailableInAPI[api] || {});
147 const boundActionCreators = bindActionCreators(availableActions, store.dispatch);
148 const doubleBoundActionCreators = doubleBind(boundActionCreators, api, plugin, Object.assign({}, args, {
149 parentSpan: pluginSpan,
150 activity
151 }));
152 const {
153 config,
154 program
155 } = store.getState();
156 const pathPrefix = program.prefixPaths && config.pathPrefix || ``;
157 const publicPath = getPublicPath(Object.assign({}, config, {}, program), ``);
158
159 const namespacedCreateNodeId = id => createNodeId(id, plugin.name);
160
161 const tracing = initAPICallTracing(pluginSpan);
162 const cache = getCache(plugin.name); // Ideally this would be more abstracted and applied to more situations, but right now
163 // this can be potentially breaking so targeting `createPages` API and `createPage` action
164
165 let actions = doubleBoundActionCreators;
166 let apiFinished = false;
167
168 if (api === `createPages`) {
169 let alreadyDisplayed = false;
170 const createPageAction = actions.createPage; // create new actions object with wrapped createPage action
171 // doubleBoundActionCreators is memoized, so we can't just
172 // reassign createPage field as this would cause this extra logic
173 // to be used in subsequent APIs and we only want to target this `createPages` call.
174
175 actions = Object.assign({}, actions, {
176 createPage: (...args) => {
177 createPageAction(...args);
178
179 if (apiFinished && !alreadyDisplayed) {
180 const warning = [reporter.stripIndent(`
181 Action ${chalk.bold(`createPage`)} was called outside of its expected asynchronous lifecycle ${chalk.bold(`createPages`)} in ${chalk.bold(plugin.name)}.
182 Ensure that you return a Promise from ${chalk.bold(`createPages`)} and are awaiting any asynchronous method invocations (like ${chalk.bold(`graphql`)} or http requests).
183 For more info and debugging tips: see ${chalk.bold(`https://gatsby.dev/sync-actions`)}
184 `)];
185 const possiblyCodeFrame = getNonGatsbyCodeFrameFormatted();
186
187 if (possiblyCodeFrame) {
188 warning.push(possiblyCodeFrame);
189 }
190
191 reporter.warn(warning.join(`\n\n`));
192 alreadyDisplayed = true;
193 }
194 }
195 });
196 }
197
198 let localReporter = getLocalReporter(activity, reporter);
199 const apiCallArgs = [Object.assign({}, args, {
200 basePath: pathPrefix,
201 pathPrefix: publicPath,
202 boundActionCreators: actions,
203 actions,
204 loadNodeContent,
205 store,
206 emitter,
207 getCache,
208 getNodes,
209 getNode,
210 getNodesByType,
211 hasNodeChanged,
212 reporter: localReporter,
213 getNodeAndSavePathDependency,
214 cache,
215 createNodeId: namespacedCreateNodeId,
216 createContentDigest,
217 tracing,
218 schema: {
219 buildObjectType,
220 buildUnionType,
221 buildInterfaceType,
222 buildInputObjectType,
223 buildEnumType,
224 buildScalarType
225 }
226 }), plugin.pluginOptions]; // If the plugin is using a callback use that otherwise
227 // expect a Promise to be returned.
228
229 if (gatsbyNode[api].length === 3) {
230 return Promise.fromCallback(callback => {
231 const cb = (err, val) => {
232 pluginSpan.finish();
233 callback(err, val);
234 apiFinished = true;
235 };
236
237 try {
238 gatsbyNode[api](...apiCallArgs, cb);
239 } catch (e) {
240 trackBuildError(api, {
241 error: e,
242 pluginName: `${plugin.name}@${plugin.version}`
243 });
244 throw e;
245 }
246 });
247 } else {
248 const result = gatsbyNode[api](...apiCallArgs);
249 pluginSpan.finish();
250 return Promise.resolve(result).then(res => {
251 apiFinished = true;
252 return res;
253 });
254 }
255 }
256
257 return null;
258};
259
260let apisRunningById = new Map();
261let apisRunningByTraceId = new Map();
262let waitingForCasacadeToFinish = [];
263
264module.exports = async (api, args = {}, {
265 pluginSource,
266 activity
267} = {}) => new Promise(resolve => {
268 const {
269 parentSpan,
270 traceId,
271 traceTags,
272 waitForCascadingActions
273 } = args;
274 const apiSpanArgs = parentSpan ? {
275 childOf: parentSpan
276 } : {};
277 const apiSpan = tracer.startSpan(`run-api`, apiSpanArgs);
278 apiSpan.setTag(`api`, api);
279
280 _.forEach(traceTags, (value, key) => {
281 apiSpan.setTag(key, value);
282 });
283
284 const plugins = store.getState().flattenedPlugins; // Get the list of plugins that implement this API.
285 // Also: Break infinite loops. Sometimes a plugin will implement an API and
286 // call an action which will trigger the same API being called.
287 // `onCreatePage` is the only example right now. In these cases, we should
288 // avoid calling the originating plugin again.
289
290 const implementingPlugins = plugins.filter(plugin => plugin.nodeAPIs.includes(api) && plugin.name !== pluginSource);
291 const apiRunInstance = {
292 api,
293 args,
294 pluginSource,
295 resolve,
296 span: apiSpan,
297 startTime: new Date().toJSON(),
298 traceId
299 }; // Generate IDs for api runs. Most IDs we generate from the args
300 // but some API calls can have very large argument objects so we
301 // have special ways of generating IDs for those to avoid stringifying
302 // large objects.
303
304 let id;
305
306 if (api === `setFieldsOnGraphQLNodeType`) {
307 id = `${api}${apiRunInstance.startTime}${args.type.name}${traceId}`;
308 } else if (api === `onCreateNode`) {
309 id = `${api}${apiRunInstance.startTime}${args.node.internal.contentDigest}${traceId}`;
310 } else if (api === `preprocessSource`) {
311 id = `${api}${apiRunInstance.startTime}${args.filename}${traceId}`;
312 } else if (api === `onCreatePage`) {
313 id = `${api}${apiRunInstance.startTime}${args.page.path}${traceId}`;
314 } else {
315 // When tracing is turned on, the `args` object will have a
316 // `parentSpan` field that can be quite large. So we omit it
317 // before calling stringify
318 const argsJson = JSON.stringify(_.omit(args, `parentSpan`));
319 id = `${api}|${apiRunInstance.startTime}|${apiRunInstance.traceId}|${argsJson}`;
320 }
321
322 apiRunInstance.id = id;
323
324 if (waitForCascadingActions) {
325 waitingForCasacadeToFinish.push(apiRunInstance);
326 }
327
328 if (apisRunningById.size === 0) {
329 emitter.emit(`API_RUNNING_START`);
330 }
331
332 apisRunningById.set(apiRunInstance.id, apiRunInstance);
333
334 if (apisRunningByTraceId.has(apiRunInstance.traceId)) {
335 const currentCount = apisRunningByTraceId.get(apiRunInstance.traceId);
336 apisRunningByTraceId.set(apiRunInstance.traceId, currentCount + 1);
337 } else {
338 apisRunningByTraceId.set(apiRunInstance.traceId, 1);
339 }
340
341 let stopQueuedApiRuns = false;
342 let onAPIRunComplete = null;
343
344 if (api === `onCreatePage`) {
345 const path = args.page.path;
346
347 const actionHandler = action => {
348 if (action.payload.path === path) {
349 stopQueuedApiRuns = true;
350 }
351 };
352
353 emitter.on(`DELETE_PAGE`, actionHandler);
354
355 onAPIRunComplete = () => {
356 emitter.off(`DELETE_PAGE`, actionHandler);
357 };
358 }
359
360 Promise.mapSeries(implementingPlugins, plugin => {
361 if (stopQueuedApiRuns) {
362 return null;
363 }
364
365 let pluginName = plugin.name === `default-site-plugin` ? `gatsby-node.js` : plugin.name;
366 return new Promise(resolve => {
367 resolve(runAPI(plugin, api, Object.assign({}, args, {
368 parentSpan: apiSpan
369 }), activity));
370 }).catch(err => {
371 decorateEvent(`BUILD_PANIC`, {
372 pluginName: `${plugin.name}@${plugin.version}`
373 });
374 let localReporter = getLocalReporter(activity, reporter);
375 const file = stackTrace.parse(err).find(file => /gatsby-node/.test(file.fileName));
376 let codeFrame = ``;
377 const structuredError = errorParser({
378 err
379 });
380
381 if (file) {
382 const {
383 fileName,
384 lineNumber: line,
385 columnNumber: column
386 } = file;
387 const code = fs.readFileSync(fileName, {
388 encoding: `utf-8`
389 });
390 codeFrame = codeFrameColumns(code, {
391 start: {
392 line,
393 column
394 }
395 }, {
396 highlightCode: true
397 });
398 structuredError.location = {
399 start: {
400 line: line,
401 column: column
402 }
403 };
404 structuredError.filePath = fileName;
405 }
406
407 structuredError.context = Object.assign({}, structuredError.context, {
408 pluginName,
409 api,
410 codeFrame
411 });
412 localReporter.panicOnBuild(structuredError);
413 return null;
414 });
415 }).then(results => {
416 if (onAPIRunComplete) {
417 onAPIRunComplete();
418 } // Remove runner instance
419
420
421 apisRunningById.delete(apiRunInstance.id);
422 const currentCount = apisRunningByTraceId.get(apiRunInstance.traceId);
423 apisRunningByTraceId.set(apiRunInstance.traceId, currentCount - 1);
424
425 if (apisRunningById.size === 0) {
426 emitter.emit(`API_RUNNING_QUEUE_EMPTY`);
427 } // Filter empty results
428
429
430 apiRunInstance.results = results.filter(result => !_.isEmpty(result)); // Filter out empty responses and return if the
431 // api caller isn't waiting for cascading actions to finish.
432
433 if (!waitForCascadingActions) {
434 apiSpan.finish();
435 resolve(apiRunInstance.results);
436 } // Check if any of our waiters are done.
437
438
439 waitingForCasacadeToFinish = waitingForCasacadeToFinish.filter(instance => {
440 // If none of its trace IDs are running, it's done.
441 const apisByTraceIdCount = apisRunningByTraceId.get(instance.traceId);
442
443 if (apisByTraceIdCount === 0) {
444 instance.span.finish();
445 instance.resolve(instance.results);
446 return false;
447 } else {
448 return true;
449 }
450 });
451 return;
452 });
453});
454//# sourceMappingURL=api-runner-node.js.map
\No newline at end of file