@netlify/content-engine
Version:
626 lines • 26.7 kB
JavaScript
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
;