UNPKG

@netlify/content-engine

Version:
626 lines 26.7 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const lodash_omit_1 = __importDefault(require("lodash.omit")); const lodash_isempty_1 = __importDefault(require("lodash.isempty")); const lodash_merge_1 = __importDefault(require("lodash.merge")); const chalk_1 = __importDefault(require("chalk")); const redux_1 = require("redux"); const memoizee_1 = __importDefault(require("memoizee")); const bindActionCreators = (0, memoizee_1.default)(redux_1.bindActionCreators); const tracer = require(`opentracing`).globalTracer(); const reporter_1 = __importDefault(require("../reporter")); const stack_trace_1 = __importDefault(require("stack-trace")); const code_frame_1 = require("@babel/code-frame"); const fs_extra_1 = __importDefault(require("fs-extra")); const get_cache_1 = require("./get-cache"); const create_node_id_1 = require("./create-node-id"); const core_utils_1 = require("../core-utils"); const type_builders_1 = require("../schema/types/type-builders"); const redux_2 = require("../redux"); const datastore_1 = require("../datastore"); const nodes_1 = require("./nodes"); const import_gatsby_plugin_1 = require("./import-gatsby-plugin"); const stack_trace_utils_1 = require("./stack-trace-utils"); // import { trackBuildError, decorateEvent } from "gatsby-telemetry" const api_runner_error_parser_1 = __importDefault(require("./api-runner-error-parser")); const detect_node_mutations_1 = require("./detect-node-mutations"); const report_once_1 = require("./report-once"); // Override createContentDigest to remove autogenerated data from nodes to // ensure consistent digests. function createContentDigest(node) { if (!node?.internal?.type) { // this doesn't look like a node, so let's just pass it as-is return (0, core_utils_1.createContentDigest)(node); } return (0, core_utils_1.createContentDigest)({ ...node, internal: { ...node.internal, // Remove auto-generated fields that'd prevent // creating a consistent contentDigest. contentDigest: undefined, owner: undefined, fieldOwners: undefined, ignoreType: undefined, counter: undefined, }, fields: undefined, }); } const nodeMutationsWrappers = { getNode(id) { return (0, detect_node_mutations_1.wrapNode)((0, datastore_1.getNode)(id)); }, getNodes() { return (0, detect_node_mutations_1.wrapNodes)((0, datastore_1.getNodes)()); }, getNodesByType(type) { return (0, detect_node_mutations_1.wrapNodes)((0, datastore_1.getNodesByType)(type)); }, getNodeAndSavePathDependency(id) { return (0, detect_node_mutations_1.wrapNode)((0, nodes_1.getNodeAndSavePathDependency)(id)); }, }; // 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, deferNodeMutation } = actionOptions; const defer = deferNodeMutation ? `defer-node-mutation` : ``; const actionKey = plugin.name + api + traceId + defer; if (boundPluginActionCreators[actionKey]) { return boundPluginActionCreators[actionKey]; } 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) => { if (args.length === 0) { return boundActionCreator(plugin, actionOptions); } // Let action callers override who the plugin is. Shouldn't be // used that often. else if (args.length === 1) { return boundActionCreator(args[0], plugin, actionOptions); } else if (args.length === 2) { return boundActionCreator(args[0], args[1], actionOptions); } (0, report_once_1.reportOnce)(`Unhandled redux action: ${key}, in plugin: ${plugin.name}`); return undefined; }; } } boundPluginActionCreators[actionKey] = doubleBoundActionCreators; return doubleBoundActionCreators; } }; const initAPICallTracing = (parentSpan) => { const startSpan = (spanName, spanArgs = {}) => { const defaultSpanArgs = { childOf: parentSpan }; return tracer.startSpan(spanName, (0, lodash_merge_1.default)(defaultSpanArgs, spanArgs)); }; return { tracer, parentSpan, startSpan, }; }; const deferredAction = (type) => (...args) => { // Regular createNode returns a Promise, but when deferred we need // to wrap it in another which we resolve when it's actually called if (type === `createNode`) { return new Promise((resolve) => { redux_2.emitter.emit(`ENQUEUE_NODE_MUTATION`, { type, payload: args, resolve, }); }); } return redux_2.emitter.emit(`ENQUEUE_NODE_MUTATION`, { type, payload: args, }); }; const NODE_MUTATION_ACTIONS = [ `createNode`, `deleteNode`, `touchNode`, `createParentChildLink`, `createNodeField`, ]; const deferActions = (actions) => { const deferred = { ...actions }; NODE_MUTATION_ACTIONS.forEach((action) => { deferred[action] = deferredAction(action); }); return deferred; }; /** * Create a local reporter * Used to override reporter methods with activity methods */ function getLocalReporter({ activity, reporter }) { // If we have an activity, bind panicOnBuild to the activities method to // join them if (activity) { return { ...reporter, panicOnBuild: activity.panicOnBuild.bind(activity) }; } return reporter; } function getErrorMapWithPluginName(pluginName, errorMap) { const entries = Object.entries(errorMap); return entries.reduce((memo, [key, val]) => { memo[`${pluginName}_${key}`] = val; return memo; }, {}); } function extendLocalReporterToCatchPluginErrors({ reporter, pluginName, runningActivities, }) { let setErrorMap; let error = reporter.error; let panic = reporter.panic; let panicOnBuild = reporter.panicOnBuild; if (pluginName && reporter?.setErrorMap) { setErrorMap = (errorMap) => reporter.setErrorMap(getErrorMapWithPluginName(pluginName, errorMap)); error = (errorMeta, error) => { reporter.error(errorMeta, error, pluginName); }; panic = (errorMeta, error) => { reporter.panic(errorMeta, error, pluginName); }; panicOnBuild = (errorMeta, error) => { reporter.panicOnBuild(errorMeta, error, pluginName); }; } return { ...reporter, setErrorMap, error, panic, panicOnBuild, // If you change arguments here, update reporter.ts as well activityTimer: (text, activityArgs = {}) => { let args = [text, activityArgs]; if (pluginName && setErrorMap) { args = [...args, pluginName]; } // eslint-disable-next-line prefer-spread const activity = reporter.activityTimer.apply(reporter, args); const originalStart = activity.start; const originalEnd = activity.end; activity.start = () => { originalStart.apply(activity); runningActivities.add(activity); }; activity.end = () => { originalEnd.apply(activity); runningActivities.delete(activity); }; return activity; }, // If you change arguments here, update reporter.ts as well createProgress: (text, total = 0, start = 0, activityArgs = {}) => { let args = [text, total, start, activityArgs]; if (pluginName && setErrorMap) { args = [...args, pluginName]; } // eslint-disable-next-line prefer-spread const activity = reporter.createProgress.apply(reporter, args); const originalStart = activity.start; const originalEnd = activity.end; const originalDone = activity.done; activity.start = () => { originalStart.apply(activity); runningActivities.add(activity); }; activity.end = () => { originalEnd.apply(activity); runningActivities.delete(activity); }; activity.done = () => { originalDone.apply(activity); runningActivities.delete(activity); }; return activity; }, }; } const getUninitializedCache = (plugin) => { const message = `Usage of "cache" instance in "onPreInit" API is not supported as ` + `this API runs before cache initialization` + (plugin && plugin !== `default-site-plugin` ? ` (called in ${plugin})` : ``); return { // GatsbyCache async get() { throw new Error(message); }, async set() { throw new Error(message); }, async del() { throw new Error(message); }, }; }; const availableActionsCache = new Map(); const runAPI = async (plugin, api, args, activity) => { const gatsbyNode = await (0, import_gatsby_plugin_1.importGatsbyPlugin)(plugin, `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 { publicActions, restrictedActionsAvailableInAPI } = require(`../redux/actions`); let availableActions; if (availableActionsCache.has(api)) { availableActions = availableActionsCache.get(api); } else { availableActions = { ...publicActions, ...(restrictedActionsAvailableInAPI[api] || {}), }; availableActionsCache.set(api, availableActions); } let boundActionCreators = bindActionCreators(availableActions, redux_2.store.dispatch); if (args.deferNodeMutation) { boundActionCreators = deferActions(boundActionCreators); } const doubleBoundActionCreators = doubleBind(boundActionCreators, api, plugin, { ...args, parentSpan: pluginSpan, activity }); const namespacedCreateNodeId = (id) => (0, create_node_id_1.createNodeId)(id, plugin.name); const tracing = initAPICallTracing(pluginSpan); // See https://github.com/gatsbyjs/gatsby/issues/11369 const cache = api === `onPreInit` ? getUninitializedCache(plugin.name) : (0, get_cache_1.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 = { ...actions, createPage: (...args) => { createPageAction(...args); if (apiFinished && !alreadyDisplayed) { const warning = [ reporter_1.default.stripIndent(` Action ${chalk_1.default.bold(`createPage`)} was called outside of its expected asynchronous lifecycle ${chalk_1.default.bold(`createPages`)} in ${chalk_1.default.bold(plugin.name)}. Ensure that you return a Promise from ${chalk_1.default.bold(`createPages`)} and are awaiting any asynchronous method invocations (like ${chalk_1.default.bold(`graphql`)} or http requests). For more info and debugging tips: see ${chalk_1.default.bold(`https://gatsby.dev/sync-actions`)} `), ]; const possiblyCodeFrame = (0, stack_trace_utils_1.getNonGatsbyCodeFrameFormatted)(); if (possiblyCodeFrame) { warning.push(possiblyCodeFrame); } reporter_1.default.warn(warning.join(`\n\n`)); alreadyDisplayed = true; } }, }; } const localReporter = getLocalReporter({ activity, reporter: reporter_1.default }); const runningActivities = new Set(); const extendedLocalReporter = extendLocalReporterToCatchPluginErrors({ reporter: localReporter, pluginName: plugin.name, runningActivities, }); const endInProgressActivitiesCreatedByThisRun = () => { // @ts-ignore runningActivities.forEach((activity) => activity.end()); }; const shouldDetectNodeMutations = [ `sourceNodes`, `onCreateNode`, `createResolvers`, `createSchemaCustomization`, `setFieldsOnGraphQLNodeType`, ].includes(api); const apiCallArgs = [ { ...args, parentSpan: pluginSpan, actions, loadNodeContent: nodes_1.loadNodeContent, store: redux_2.store, emitter: redux_2.emitter, getCache: get_cache_1.getCache, getNodes: shouldDetectNodeMutations ? nodeMutationsWrappers.getNodes : datastore_1.getNodes, getNode: shouldDetectNodeMutations ? nodeMutationsWrappers.getNode : datastore_1.getNode, getNodesByType: shouldDetectNodeMutations ? nodeMutationsWrappers.getNodesByType : datastore_1.getNodesByType, reporter: extendedLocalReporter, getNodeAndSavePathDependency: shouldDetectNodeMutations ? nodeMutationsWrappers.getNodeAndSavePathDependency : nodes_1.getNodeAndSavePathDependency, cache, createNodeId: namespacedCreateNodeId, createContentDigest, tracing, schema: { buildObjectType: type_builders_1.buildObjectType, buildUnionType: type_builders_1.buildUnionType, buildInterfaceType: type_builders_1.buildInterfaceType, buildInputObjectType: type_builders_1.buildInputObjectType, buildEnumType: type_builders_1.buildEnumType, buildScalarType: type_builders_1.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 new Promise((resolve, reject) => { const cb = (err, val) => { pluginSpan.finish(); apiFinished = true; endInProgressActivitiesCreatedByThisRun(); if (err) { reject(err); } else { resolve(val); } }; try { gatsbyNode[api](...apiCallArgs, cb); } catch (e) { // trackBuildError(api, { // error: e, // pluginName: `${plugin.name}@${plugin.version}`, // }) reject(e); // Properly reject the promise if an exception occurs } }); } else { try { return await gatsbyNode[api](...apiCallArgs); } finally { pluginSpan.finish(); apiFinished = true; endInProgressActivitiesCreatedByThisRun(); } } } return null; }; const apisRunningById = new Map(); const apisRunningByTraceId = new Map(); let waitingForCasacadeToFinish = []; // @ts-ignore function apiRunnerNode(api, args = {}, { pluginSource, activity } = {}) { const plugins = redux_2.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. let implementingPlugins = plugins.filter((plugin) => plugin.nodeAPIs.includes(api) && plugin.name !== pluginSource); // @ts-ignore if (api === `sourceNodes` && args.pluginName) { implementingPlugins = implementingPlugins.filter( // @ts-ignore (plugin) => plugin.name === args.pluginName); } // If there's no implementing plugins, return early. if (implementingPlugins.length === 0) { return null; } return new Promise((resolve, rejectApiRunnerNode) => { // @ts-ignore const { parentSpan, traceId, traceTags, waitForCascadingActions } = args; const apiSpanArgs = parentSpan ? { childOf: parentSpan } : {}; const apiSpan = tracer.startSpan(`run-api`, apiSpanArgs); apiSpan.setTag(`api`, api); traceTags?.forEach?.((value, key) => { apiSpan.setTag(key, value); }); const apiRunInstance = { api, args, pluginSource, resolve, span: apiSpan, startTime: new Date().toJSON(), 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`) { // @ts-ignore id = `${api}${apiRunInstance.startTime}${args.type.name}${traceId}`; } else if (api === `onCreateNode`) { // @ts-ignore id = `${api}${apiRunInstance.startTime}${args.node.internal.contentDigest}${traceId}`; } else if (api === `preprocessSource`) { // @ts-ignore id = `${api}${apiRunInstance.startTime}${args.filename}${traceId}`; } else if (api === `onCreatePage`) { // @ts-ignore id = `${api}${apiRunInstance.startTime}${args.page.path}${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((0, lodash_omit_1.default)(args, `parentSpan`)); id = `${api}|${apiRunInstance.startTime}|${apiRunInstance.traceId}|${argsJson}`; } // @ts-ignore apiRunInstance.id = id; if (waitForCascadingActions) { // @ts-ignore waitingForCasacadeToFinish.push(apiRunInstance); } if (apisRunningById.size === 0) { redux_2.emitter.emit(`API_RUNNING_START`); } // @ts-ignore 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); } function cleanup({ apiRunInstance, results, waitForCascadingActions, apiSpan, }) { // Remove runner instance // @ts-ignore apisRunningById.delete(apiRunInstance.id); const currentCount = apisRunningByTraceId.get(apiRunInstance.traceId); apisRunningByTraceId.set(apiRunInstance.traceId, currentCount - 1); if (apisRunningById.size === 0) { redux_2.emitter.emit(`API_RUNNING_QUEUE_EMPTY`); } // Filter empty results // @ts-ignore apiRunInstance.results = results?.filter((result) => !(0, lodash_isempty_1.default)(result)) || []; // Filter out empty responses and return if the // api caller isn't waiting for cascading actions to finish. if (!waitForCascadingActions) { apiSpan.finish(); // @ts-ignore 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. // @ts-ignore const apisByTraceIdCount = apisRunningByTraceId.get(instance.traceId); if (apisByTraceIdCount === 0) { // @ts-ignore instance.span.finish(); // @ts-ignore instance.resolve(instance.results); return false; } else { return true; } }); } const runImportedPlugin = (plugin, gatsbyNode) => { const pluginName = plugin.name === `default-site-plugin` ? `gatsby-node.js` : plugin.name; // TODO: rethink createNode API to handle this better if (api === `onCreateNode` && gatsbyNode?.shouldOnCreateNode && // Don't bail if this api is not exported !gatsbyNode.shouldOnCreateNode( // @ts-ignore { node: args.node }, plugin.pluginOptions)) { // Do not try to schedule an async event for this node for this plugin return null; } return runAPI(plugin, api, { ...args, parentSpan: apiSpan }, activity).catch((err) => { // decorateEvent(`BUILD_PANIC`, { // pluginName: `${plugin.name}@${plugin.version}`, // }) const localReporter = getLocalReporter({ activity, reporter: reporter_1.default }); const file = stack_trace_1.default .parse(err) // @ts-ignore .find((file) => /gatsby-node/.test(file.fileName)); let codeFrame = ``; const structuredError = (0, api_runner_error_parser_1.default)({ err }); if (file) { // @ts-ignore const { fileName, lineNumber: line, columnNumber: column } = file; const trimmedFileName = fileName.match(/^(async )?(.*)/)[2]; try { const code = fs_extra_1.default.readFileSync(trimmedFileName, { encoding: `utf-8`, }); codeFrame = (0, code_frame_1.codeFrameColumns)(code, { start: { line, column, }, }, { highlightCode: true, }); } catch (_e) { // sometimes stack trace point to not existing file // particularly when file is transpiled and path actually changes // (like pointing to not existing `src` dir or original typescript file) } structuredError.location = { start: { line: line, column: column }, }; structuredError.filePath = fileName; } structuredError.context = { ...structuredError.context, pluginName, api, codeFrame, }; localReporter.panicOnBuild(structuredError); rejectApiRunnerNode(err); throw err; }); }; // iife because I'm removing bluebird and this is an easy Promise.mapSeries (async () => { try { const results = []; for (const plugin of implementingPlugins) { const result = await (0, import_gatsby_plugin_1.importGatsbyPlugin)(plugin, `gatsby-node`).then((gatsbyNode) => runImportedPlugin(plugin, gatsbyNode)); results.push(result); } cleanup({ apiRunInstance, results, waitForCascadingActions, apiSpan, }); } catch (e) { cleanup({ apiRunInstance, results: [], waitForCascadingActions, apiSpan, }); rejectApiRunnerNode(e); } })(); }); } exports.default = apiRunnerNode; //# sourceMappingURL=api-runner-node.js.map