"use strict"; const Promise = require(`bluebird`); const _ = require(`lodash`); const chalk = require(`chalk`); const { bindActionCreators } = require(`redux`); const tracer = require(`opentracing`).globalTracer(); const reporter = require(`gatsby-cli/lib/reporter`); const getCache = require(`./get-cache`); const createNodeId = require(`./create-node-id`); const { createContentDigest } = require(`gatsby-core-utils`); const { buildObjectType, buildUnionType, buildInterfaceType, buildInputObjectType, buildEnumType, buildScalarType } = require(`../schema/types/type-builders`); const { emitter } = require(`../redux`); const getPublicPath = require(`./get-public-path`); const { getNonGatsbyCodeFrame } = require(`./stack-trace-utils`); const { trackBuildError, decorateEvent } = require(`gatsby-telemetry`); // Bind action creators per plugin so we can auto-add // metadata to actions they create. const boundPluginActionCreators = {}; const doubleBind = (boundActionCreators, api, plugin, actionOptions) => { const { traceId } = actionOptions; if (boundPluginActionCreators[plugin.name + api + traceId]) { return boundPluginActionCreators[plugin.name + api + traceId]; } else { const keys = Object.keys(boundActionCreators); const doubleBoundActionCreators = {}; for (let i = 0; i < keys.length; i++) { const key = keys[i]; const boundActionCreator = boundActionCreators[key]; if (typeof boundActionCreator === `function`) { doubleBoundActionCreators[key] = (...args) => { // Let action callers override who the plugin is. Shouldn't be // used that often. if (args.length === 1) { return boundActionCreator(args[0], plugin, actionOptions); } else if (args.length === 2) { return boundActionCreator(args[0], args[1], actionOptions); } return undefined; }; } } boundPluginActionCreators[plugin.name + api + traceId] = doubleBoundActionCreators; return doubleBoundActionCreators; } }; const initAPICallTracing = parentSpan => { const startSpan = (spanName, spanArgs = {}) => { const defaultSpanArgs = { childOf: parentSpan }; return tracer.startSpan(spanName, _.merge(defaultSpanArgs, spanArgs)); }; return { tracer, parentSpan, startSpan }; }; const runAPI = (plugin, api, args) => { const gatsbyNode = require(`${plugin.resolve}/gatsby-node`); if (gatsbyNode[api]) { const parentSpan = args && args.parentSpan; const spanOptions = parentSpan ? { childOf: parentSpan } : {}; const pluginSpan = tracer.startSpan(`run-plugin`, spanOptions); pluginSpan.setTag(`api`, api); pluginSpan.setTag(`plugin`, plugin.name); const { store, emitter } = require(`../redux`); const { loadNodeContent, getNodes, getNode, getNodesByType, hasNodeChanged, getNodeAndSavePathDependency } = require(`../db/nodes`); const { publicActions, restrictedActionsAvailableInAPI } = require(`../redux/actions`); const availableActions = Object.assign({}, publicActions, restrictedActionsAvailableInAPI[api] || {}); const boundActionCreators = bindActionCreators(availableActions, store.dispatch); const doubleBoundActionCreators = doubleBind(boundActionCreators, api, plugin, Object.assign({}, args, { parentSpan: pluginSpan })); const { config, program } = store.getState(); const pathPrefix = program.prefixPaths && config.pathPrefix || ``; const publicPath = getPublicPath(Object.assign({}, config, program), ``); const namespacedCreateNodeId = id => createNodeId(id, plugin.name); const tracing = initAPICallTracing(pluginSpan); const cache = getCache(plugin.name); // Ideally this would be more abstracted and applied to more situations, but right now // this can be potentially breaking so targeting `createPages` API and `createPage` action let actions = doubleBoundActionCreators; let apiFinished = false; if (api === `createPages`) { let alreadyDisplayed = false; const createPageAction = actions.createPage; // create new actions object with wrapped createPage action // doubleBoundActionCreators is memoized, so we can't just // reassign createPage field as this would cause this extra logic // to be used in subsequent APIs and we only want to target this `createPages` call. actions = Object.assign({}, actions, { createPage: (...args) => { createPageAction(...args); if (apiFinished && !alreadyDisplayed) { const warning = [reporter.stripIndent(` Action ${chalk.bold(`createPage`)} was called outside of its expected asynchronous lifecycle ${chalk.bold(`createPages`)} in ${chalk.bold(plugin.name)}. Ensure that you return a Promise from ${chalk.bold(`createPages`)} and are awaiting any asynchronous method invocations (like ${chalk.bold(`graphql`)} or http requests). For more info and debugging tips: see ${chalk.bold(`https://gatsby.dev/sync-actions`)} `)]; const possiblyCodeFrame = getNonGatsbyCodeFrame(); if (possiblyCodeFrame) { warning.push(possiblyCodeFrame); } reporter.warn(warning.join(`\n\n`)); alreadyDisplayed = true; } } }); } const apiCallArgs = [Object.assign({}, args, { basePath: pathPrefix, pathPrefix: publicPath, boundActionCreators: actions, actions, loadNodeContent, store, emitter, getCache, getNodes, getNode, getNodesByType, hasNodeChanged, reporter, getNodeAndSavePathDependency, cache, createNodeId: namespacedCreateNodeId, createContentDigest, tracing, schema: { buildObjectType, buildUnionType, buildInterfaceType, buildInputObjectType, buildEnumType, buildScalarType } }), plugin.pluginOptions]; // If the plugin is using a callback use that otherwise // expect a Promise to be returned. if (gatsbyNode[api].length === 3) { return Promise.fromCallback(callback => { const cb = (err, val) => { pluginSpan.finish(); callback(err, val); apiFinished = true; }; try { gatsbyNode[api](...apiCallArgs, cb); } catch (e) { trackBuildError(api, { error: e, pluginName: `${plugin.name}@${plugin.version}` }); throw e; } }); } else { const result = gatsbyNode[api](...apiCallArgs); pluginSpan.finish(); return Promise.resolve(result).then(res => { apiFinished = true; return res; }); } } return null; }; let apisRunningById = new Map(); let apisRunningByTraceId = new Map(); let waitingForCasacadeToFinish = []; module.exports = async (api, args = {}, pluginSource) => new Promise(resolve => { const { parentSpan } = args; const apiSpanArgs = parentSpan ? { childOf: parentSpan } : {}; const apiSpan = tracer.startSpan(`run-api`, apiSpanArgs); apiSpan.setTag(`api`, api); _.forEach(args.traceTags, (value, key) => { apiSpan.setTag(key, value); }); const { store } = require(`../redux`); const plugins = store.getState().flattenedPlugins; // Get the list of plugins that implement this API. // Also: Break infinite loops. Sometimes a plugin will implement an API and // call an action which will trigger the same API being called. // `onCreatePage` is the only example right now. In these cases, we should // avoid calling the originating plugin again. const implementingPlugins = plugins.filter(plugin => plugin.nodeAPIs.includes(api) && plugin.name !== pluginSource); const apiRunInstance = { api, args, pluginSource, resolve, span: apiSpan, startTime: new Date().toJSON(), traceId: args.traceId // Generate IDs for api runs. Most IDs we generate from the args // but some API calls can have very large argument objects so we // have special ways of generating IDs for those to avoid stringifying // large objects. }; let id; if (api === `setFieldsOnGraphQLNodeType`) { id = `${api}${apiRunInstance.startTime}${args.type.name}${args.traceId}`; } else if (api === `onCreateNode`) { id = `${api}${apiRunInstance.startTime}${args.node.internal.contentDigest}${args.traceId}`; } else if (api === `preprocessSource`) { id = `${api}${apiRunInstance.startTime}${args.filename}${args.traceId}`; } else if (api === `onCreatePage`) { id = `${api}${apiRunInstance.startTime}${args.page.path}${args.traceId}`; } else { // When tracing is turned on, the `args` object will have a // `parentSpan` field that can be quite large. So we omit it // before calling stringify const argsJson = JSON.stringify(_.omit(args, `parentSpan`)); id = `${api}|${apiRunInstance.startTime}|${apiRunInstance.traceId}|${argsJson}`; } apiRunInstance.id = id; if (args.waitForCascadingActions) { waitingForCasacadeToFinish.push(apiRunInstance); } apisRunningById.set(apiRunInstance.id, apiRunInstance); if (apisRunningByTraceId.has(apiRunInstance.traceId)) { const currentCount = apisRunningByTraceId.get(apiRunInstance.traceId); apisRunningByTraceId.set(apiRunInstance.traceId, currentCount + 1); } else { apisRunningByTraceId.set(apiRunInstance.traceId, 1); } let stopQueuedApiRuns = false; let onAPIRunComplete = null; if (api === `onCreatePage`) { const path = args.page.path; const actionHandler = action => { if (action.payload.path === path) { stopQueuedApiRuns = true; } }; emitter.on(`DELETE_PAGE`, actionHandler); onAPIRunComplete = () => { emitter.off(`DELETE_PAGE`, actionHandler); }; } Promise.mapSeries(implementingPlugins, plugin => { if (stopQueuedApiRuns) { return null; } let pluginName = plugin.name === `default-site-plugin` ? `gatsby-node.js` : plugin.name; return new Promise(resolve => { resolve(runAPI(plugin, api, Object.assign({}, args, { parentSpan: apiSpan }))); }).catch(err => { decorateEvent(`BUILD_PANIC`, { pluginName: `${plugin.name}@${plugin.version}` }); reporter.panicOnBuild({ id: `11321`, context: { pluginName, api, message: err instanceof Error ? err.message : err }, error: err instanceof Error ? err : undefined }); return null; }); }).then(results => { if (onAPIRunComplete) { onAPIRunComplete(); } // Remove runner instance apisRunningById.delete(apiRunInstance.id); const currentCount = apisRunningByTraceId.get(apiRunInstance.traceId); apisRunningByTraceId.set(apiRunInstance.traceId, currentCount - 1); if (apisRunningById.size === 0) { const { emitter } = require(`../redux`); emitter.emit(`API_RUNNING_QUEUE_EMPTY`); } // Filter empty results apiRunInstance.results = results.filter(result => !_.isEmpty(result)); // Filter out empty responses and return if the // api caller isn't waiting for cascading actions to finish. if (!args.waitForCascadingActions) { apiSpan.finish(); resolve(apiRunInstance.results); } // Check if any of our waiters are done. waitingForCasacadeToFinish = waitingForCasacadeToFinish.filter(instance => { // If none of its trace IDs are running, it's done. const apisByTraceIdCount = apisRunningByTraceId.get(instance.traceId); if (apisByTraceIdCount === 0) { instance.span.finish(); instance.resolve(instance.results); return false; } else { return true; } }); return; }); }); //# sourceMappingURL=api-runner-node.js.map