UNPKG

@netlify/content-engine

Version:
743 lines (737 loc) 27.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.actions = void 0; const reporter_1 = __importDefault(require("../../reporter")); const chalk_1 = __importDefault(require("chalk")); const is_object_1 = require("../../core-utils/is-object"); const common_tags_1 = require("common-tags"); const url_1 = __importDefault(require("url")); const nodes_1 = require("../../utils/nodes"); const datastore_1 = require("../../datastore"); const sanitize_node_1 = require("../../utils/sanitize-node"); const index_1 = require("../index"); // import { nodeSchema } from "../../joi-schemas/joi" const api_runner_node_1 = __importDefault(require("../../utils/api-runner-node")); // import { trackCli } from "gatsby-telemetry" // const { getNonGatsbyCodeFrame } = require(`../../utils/stack-trace-utils`) const internal_1 = require("./internal"); const report_once_1 = require("../../utils/report-once"); const detect_node_mutations_1 = require("../../utils/detect-node-mutations"); const isNotTestEnv = process.env.NODE_ENV !== `test`; const manager_1 = require("../../utils/jobs/manager"); const actions = {}; exports.actions = actions; const findChildren = (initialChildren) => { const children = [...initialChildren]; const queue = [...initialChildren]; const traversedNodes = new Set(); while (queue.length > 0) { const currentChild = (0, datastore_1.getNode)(queue.pop()); if (!currentChild || traversedNodes.has(currentChild.id)) { continue; } traversedNodes.add(currentChild.id); const newChildren = currentChild.children; if (Array.isArray(newChildren) && newChildren.length > 0) { children.push(...newChildren); queue.push(...newChildren); } } return children; }; /** * Delete a node * @param {object} node A node object. See the "createNode" action for more information about the node object details. * @example * deleteNode(node) */ // @ts-ignore actions.deleteNode = (node, plugin) => { const id = node && node.id; // Always get node from the store, as the node we get as an arg // might already have been deleted. const internalNode = (0, datastore_1.getNode)(id); const createDeleteAction = (node) => { return { type: `DELETE_NODE`, plugin, payload: node, // main node need to be owned by plugin that calls deleteNode // child nodes should skip ownership check isRecursiveChildrenDelete: node !== internalNode, }; }; const deleteAction = createDeleteAction(internalNode); // It's possible the file node was never created as sometimes tools will // write and then immediately delete temporary files to the file system. const deleteDescendantsActions = internalNode && findChildren(internalNode.children).map(datastore_1.getNode).map(createDeleteAction); if (deleteDescendantsActions && deleteDescendantsActions.length) { return [...deleteDescendantsActions, deleteAction]; } else { return deleteAction; } }; // We add a counter to node.internal for fast comparisons/intersections // of various node slices. The counter must increase even across builds. function getNextNodeCounter() { const lastNodeCounter = index_1.store.getState().status.LAST_NODE_COUNTER ?? 0; if (lastNodeCounter >= Number.MAX_SAFE_INTEGER) { throw new Error(`Could not create more nodes. Maximum node count is reached: ${lastNodeCounter}`); } return lastNodeCounter + 1; } // memberof notation is added so this code can be referenced instead of the wrapper. /** * Create a new node. * @memberof actions * @param {Object} node a node object * @param {string} node.id The node's ID. Must be globally unique. * @param {string} node.parent The ID of the parent's node. If the node is * derived from another node, set that node as the parent. Otherwise it can * just be `null`. * @param {Array} node.children An array of children node IDs. If you're * creating the children nodes while creating the parent node, add the * children node IDs here directly. If you're adding a child node to a * parent node created by a plugin, you can't mutate this value directly * to add your node id, instead use the action creator `createParentChildLink`. * @param {Object} node.internal node fields that aren't generally * interesting to consumers of node data but are very useful for plugin writers * and Gatsby core. Only fields described below are allowed in `internal` object. * Using any type of custom fields will result in validation errors. * @param {string} node.internal.mediaType An optional field to indicate to * transformer plugins that your node has raw content they can transform. * Use either an official media type (we use mime-db as our source * (https://www.npmjs.com/package/mime-db) or a made-up one if your data * doesn't fit in any existing bucket. Transformer plugins use node media types * for deciding if they should transform a node into a new one. E.g. * markdown transformers look for media types of * `text/markdown`. * @param {string} node.internal.type An arbitrary globally unique type * chosen by the plugin creating the node. Should be descriptive of the * node as the type is used in forming GraphQL types so users will query * for nodes based on the type chosen here. Nodes of a given type can * only be created by one plugin. * @param {string} node.internal.content An optional field. This is rarely * used. It is used when a source plugin sources data it doesn't know how * to transform e.g. a markdown string pulled from an API. The source plugin * can defer the transformation to a specialized transformer plugin like * gatsby-transformer-remark. This `content` field holds the raw content * (so for the markdown case, the markdown string). * * Data that's already structured should be added to the top-level of the node * object and _not_ added here. You should not `JSON.stringify` your node's * data here. * * If the content is very large and can be lazy-loaded, e.g. a file on disk, * you can define a `loadNodeContent` function for this node and the node * content will be lazy loaded when it's needed. * @param {string} node.internal.contentDigest the digest for the content * of this node. Helps Gatsby avoid doing extra work on data that hasn't * changed. * @param {string} node.internal.description An optional field. Human * readable description of what this node represent / its source. It will * be displayed when type conflicts are found, making it easier to find * and correct type conflicts. * @returns {Promise} The returned Promise resolves when all cascading * `onCreateNode` API calls triggered by `createNode` have finished. * @example * createNode({ * // Data for the node. * field1: `a string`, * field2: 10, * field3: true, * ...arbitraryOtherData, * * // Required fields. * id: `a-node-id`, * parent: `the-id-of-the-parent-node`, // or null if it's a source node without a parent * children: [], * internal: { * type: `CoolServiceMarkdownField`, * contentDigest: crypto * .createHash(`md5`) * .update(JSON.stringify(fieldData)) * .digest(`hex`), * mediaType: `text/markdown`, // optional * content: JSON.stringify(fieldData), // optional * description: `Cool Service: "Title of entry"`, // optional * } * }) */ const createNode = (node, // @ts-ignore plugin, // @ts-ignore actionOptions = {}) => { if (!(0, is_object_1.isObject)(node)) { return console.log(chalk_1.default.bold.red(`The node passed to the "createNode" action creator must be an object`)); } // Ensure the new node has an internals object. // @ts-ignore if (!node.internal) { // @ts-ignore node.internal = {}; } // Ensure the new node has a children array. // @ts-ignore if (!node.array && !Array.isArray(node.children)) { // @ts-ignore node.children = []; } // Ensure the new node has a parent field // @ts-ignore if (!node.parent) { // @ts-ignore node.parent = null; } // Tell user not to set the owner name themself. // @ts-ignore if (node.internal.owner) { reporter_1.default.error(JSON.stringify(node, null, 4)); reporter_1.default.panic(chalk_1.default.bold.red(`The node internal.owner field is set automatically by Gatsby and not by plugins`)); } const trackParams = {}; // Add the plugin name to the internal object. if (plugin) { // @ts-ignore node.internal.owner = plugin.name; // @ts-ignore trackParams[`pluginName`] = `${plugin.name}@${plugin.version}`; } // trackCli(`CREATE_NODE`, trackParams, { debounce: true }) // const result = nodeSchema.validate(node) // if (result.error) { // if (!hasErroredBecauseOfNodeValidation.has(result.error.message)) { // const errorObj = { // id: `11467`, // context: { // validationErrorMessage: result.error.message, // node, // }, // } // const possiblyCodeFrame = getNonGatsbyCodeFrame() // if (possiblyCodeFrame) { // errorObj.context.codeFrame = possiblyCodeFrame.codeFrame // errorObj.filePath = possiblyCodeFrame.fileName // errorObj.location = { // start: { // line: possiblyCodeFrame.line, // column: possiblyCodeFrame.column, // }, // } // } // reporter.error(errorObj) // hasErroredBecauseOfNodeValidation.add(result.error.message) // } // return { type: `VALIDATION_ERROR`, error: true } // } // Ensure node isn't directly setting fields. // @ts-ignore if (node.fields) { throw new Error((0, common_tags_1.stripIndent) ` Plugins creating nodes can not set data on the reserved field "fields" as this is reserved for plugins which wish to extend your nodes. If your plugin didn't add "fields" you're probably seeing this error because you're reusing an old node object. Node: ${JSON.stringify(node, null, 4)} Plugin that created the node: ${JSON.stringify(plugin, null, 4)} `); } // @ts-ignore node = (0, sanitize_node_1.sanitizeNode)(node); const oldNode = (0, datastore_1.getNode)(node.id); if (actionOptions.parentSpan) { // @ts-ignore actionOptions.parentSpan.setTag(`nodeId`, node.id); // @ts-ignore actionOptions.parentSpan.setTag(`nodeType`, node.id); } let deleteActions; let updateNodeAction; // Check if the node has already been processed. if (oldNode && !(0, nodes_1.hasNodeChanged)(node.id, node.internal.contentDigest)) { updateNodeAction = { ...actionOptions, plugin, type: `TOUCH_NODE`, payload: node.id, typeName: node.internal.type, }; } else { // Remove any previously created descendant nodes as they're all due // to be recreated. if (oldNode) { const createDeleteAction = (node) => { return { ...actionOptions, type: `DELETE_NODE`, plugin, payload: node, isRecursiveChildrenDelete: true, }; }; deleteActions = findChildren(oldNode.children) .map(datastore_1.getNode) .map(createDeleteAction); } node.internal.counter = getNextNodeCounter(); updateNodeAction = { ...actionOptions, type: `CREATE_NODE`, plugin, oldNode, payload: node, }; } if (deleteActions && deleteActions.length) { return [...deleteActions, updateNodeAction]; } else { return updateNodeAction; } }; // @ts-ignore actions.createNode = (...args) => (dispatch) => { // @ts-ignore const actions = createNode(...args); dispatch(actions); const createNodeAction = (Array.isArray(actions) ? actions : [actions]).find((action) => action.type === `CREATE_NODE`); if (!createNodeAction) { return Promise.resolve(undefined); } const { payload: node, traceId, parentSpan } = createNodeAction; const maybePromise = (0, api_runner_node_1.default)(`onCreateNode`, { node: (0, detect_node_mutations_1.wrapNode)(node), traceId, parentSpan, traceTags: { nodeId: node.id, nodeType: node.internal.type }, }); if (maybePromise?.then) { return maybePromise.then((res) => (0, datastore_1.getDataStore)() .ready() .then(() => res)); } else { return (0, datastore_1.getDataStore)() .ready() .then(() => maybePromise); } }; /** * "Touch" a node. Tells Gatsby a node still exists and shouldn't * be garbage collected. Primarily useful for source plugins fetching * nodes from a remote system that can return only nodes that have * updated. The source plugin then touches all the nodes that haven't * updated but still exist so Gatsby knows to keep them. * @param {Object} node A node object. See the "createNode" action for more information about the node object details. * @example * touchNode(node) */ // @ts-ignore actions.touchNode = (node, plugin) => { const nodeId = node?.id; if (!nodeId) { // if we don't have a node id, we don't want to dispatch this action return []; } return { type: `TOUCH_NODE`, plugin, payload: nodeId, typeName: node.internal.type, }; }; /** * Extend another node. The new node field is placed under the `fields` * key on the extended node object. * * Once a plugin has claimed a field name the field name can't be used by * other plugins. Also since nodes are immutable, you can't mutate the node * directly. So to extend another node, use this. * @param {Object} $0 * @param {Object} $0.node the target node object * @param {string} $0.name the name for the field * @param {any} $0.value the value for the field * @example * createNodeField({ * node, * name: `happiness`, * value: `is sweet graphql queries` * }) * * // The field value is now accessible at node.fields.happiness */ // @ts-ignore actions.createNodeField = ({ node, name, value }, plugin, actionOptions) => { // Ensure required fields are set. // @ts-ignore if (!node.internal.fieldOwners) { // @ts-ignore node.internal.fieldOwners = {}; } // @ts-ignore if (!node.fields) { // @ts-ignore node.fields = {}; } // Normalized name of the field that will be used in schema const schemaFieldName = name?.includes(`___NODE`) ? // @ts-ignore name.split(`___`)[0] : name; // Check that this field isn't owned by another plugin. // @ts-ignore const fieldOwner = node.internal.fieldOwners[schemaFieldName]; if (fieldOwner && fieldOwner !== plugin.name) { throw new Error((0, common_tags_1.stripIndent) ` A plugin tried to update a node field that it doesn't own: Node id: ${ // @ts-ignore node.id} Plugin: ${plugin.name} name: ${name} value: ${value} `); } // Update node // @ts-ignore node.fields[name] = value; // @ts-ignore node.internal.fieldOwners[schemaFieldName] = plugin.name; // @ts-ignore node = (0, sanitize_node_1.sanitizeNode)(node); return { ...actionOptions, type: `ADD_FIELD_TO_NODE`, plugin, payload: node, addedField: name, }; }; /** * Creates a link between a parent and child node. This is used when you * transform content from a node creating a new child node. You need to add * this new child node to the `children` array of the parent but since you * don't have direct access to the immutable parent node, use this action * instead. * @param {Object} $0 * @param {Object} $0.parent the parent node object * @param {Object} $0.child the child node object * @example * createParentChildLink({ parent: parentNode, child: childNode }) */ // @ts-ignore actions.createParentChildLink = ({ parent, child }, plugin) => { if (!parent.children.includes(child.id)) { parent.children.push(child.id); } return { type: `ADD_CHILD_NODE_TO_PARENT_NODE`, plugin, payload: parent, }; }; /** * Set top-level Babel options. Plugins and presets will be ignored. Use * setBabelPlugin and setBabelPreset for this. * @param {Object} config An options object in the shape of a normal babelrc JavaScript object * @example * setBabelOptions({ * options: { * sourceMaps: `inline`, * } * }) */ // @ts-ignore actions.setBabelOptions = (options, plugin) => { // Validate let name = `The plugin "${ // @ts-ignore plugin.name}"`; if (plugin?.name === `default-site-plugin`) { name = `Your site's "gatsby-node.js"`; } if (!(0, is_object_1.isObject)(options)) { console.log(`${name} must pass an object to "setBabelOptions"`); console.log(JSON.stringify(options, null, 4)); if (isNotTestEnv) { process.exit(1); } } // @ts-ignore if (!(0, is_object_1.isObject)(options.options)) { console.log(`${name} must pass options to "setBabelOptions"`); console.log(JSON.stringify(options, null, 4)); if (isNotTestEnv) { process.exit(1); } } return { type: `SET_BABEL_OPTIONS`, plugin, payload: options, }; }; /** * Add new plugins or merge options into existing Babel plugins. * @param {Object} config A config object describing the Babel plugin to be added. * @param {string} config.name The name of the Babel plugin * @param {Object} config.options Options to pass to the Babel plugin. * @example * setBabelPlugin({ * name: `@emotion/babel-plugin`, * options: { * sourceMap: true, * }, * }) */ // @ts-ignore actions.setBabelPlugin = (config, plugin) => { // Validate let name = `The plugin "${plugin?.name || `unknown`}"`; if (plugin?.name === `default-site-plugin`) { name = `Your site's "gatsby-node.js"`; } // @ts-ignore if (!config.name) { console.log(`${name} must set the name of the Babel plugin`); console.log(JSON.stringify(config, null, 4)); if (isNotTestEnv) { process.exit(1); } } // @ts-ignore if (!config.options) { // @ts-ignore config.options = {}; } return { type: `SET_BABEL_PLUGIN`, plugin, payload: config, }; }; /** * DEPRECATED. Use createJobV2 instead. * * Create a "job". This is a long-running process that is generally * started as a side-effect to a GraphQL query. * [`gatsby-plugin-sharp`](/plugins/gatsby-plugin-sharp/) uses this for * example. * * Gatsby doesn't finish its process until all jobs are ended. * @param {Object} job A job object with at least an id set * @param {id} job.id The id of the job * @deprecated Use "createJobV2" instead * @example * createJob({ id: `write file id: 123`, fileName: `something.jpeg` }) */ // @ts-ignore actions.createJob = (job, plugin = null) => { let msg = `Action "createJob" is deprecated. Please use "createJobV2" instead`; if (plugin?.name) { msg = msg + ` (called by ${plugin.name})`; } (0, report_once_1.reportOnce)(msg); return { type: `CREATE_JOB`, plugin, payload: job, }; }; /** * Create a "job". This is a long-running process that is generally * started as a side-effect to a GraphQL query. * [`gatsby-plugin-sharp`](/plugins/gatsby-plugin-sharp/) uses this for * example. * * Gatsby doesn't finish its process until all jobs are ended. * @param {Object} job A job object with name, inputPaths, outputDir and args * @param {string} job.name The name of the job you want to execute * @param {string[]} job.inputPaths The inputPaths that are needed to run * @param {string} job.outputDir The directory where all files are being saved to * @param {Object} job.args The arguments the job needs to execute * @returns {Promise<object>} Promise to see if the job is done executing * @example * createJobV2({ name: `IMAGE_PROCESSING`, inputPaths: [`something.jpeg`], outputDir: `public/static`, args: { width: 100, height: 100 } }) */ // @ts-ignore actions.createJobV2 = (job, plugin) => (dispatch, getState) => { // @ts-ignore const internalJob = (0, manager_1.createInternalJob)(job, plugin); return (0, internal_1.createJobV2FromInternalJob)(internalJob)(dispatch, getState); }; // @ts-ignore actions.addGatsbyImageSourceUrl = (sourceUrl) => { return { type: `PROCESS_GATSBY_IMAGE_SOURCE_URL`, payload: { sourceUrl }, }; }; /** * DEPRECATED. Use createJobV2 instead. * * Set (update) a "job". Sometimes on really long running jobs you want * to update the job as it continues. * * @param {Object} job A job object with at least an id set * @param {id} job.id The id of the job * @deprecated Use "createJobV2" instead * @example * setJob({ id: `write file id: 123`, progress: 50 }) */ // @ts-ignore actions.setJob = (job, plugin = null) => { let msg = `Action "setJob" is deprecated. Please use "createJobV2" instead`; if (plugin?.name) { msg = msg + ` (called by ${plugin.name})`; } (0, report_once_1.reportOnce)(msg); return { type: `SET_JOB`, plugin, payload: job, }; }; /** * DEPRECATED. Use createJobV2 instead. * * End a "job". * * Gatsby doesn't finish its process until all jobs are ended. * @param {Object} job A job object with at least an id set * @param {id} job.id The id of the job * @deprecated Use "createJobV2" instead * @example * endJob({ id: `write file id: 123` }) */ // @ts-ignore actions.endJob = (job, plugin = null) => { let msg = `Action "endJob" is deprecated. Please use "createJobV2" instead`; if (plugin?.name) { msg = msg + ` (called by ${plugin.name})`; } (0, report_once_1.reportOnce)(msg); return { type: `END_JOB`, plugin, payload: job, }; }; /** * Set plugin status. A plugin can use this to save status keys e.g. the last * it fetched something. These values are persisted between runs of Gatsby. * * @param {Object} status An object with arbitrary values set * @example * setPluginStatus({ lastFetched: Date.now() }) */ // @ts-ignore actions.setPluginStatus = (status, plugin) => { return { type: `SET_PLUGIN_STATUS`, plugin, payload: status, }; }; /** * Creates an individual node manifest. * This is used to tie the unique revision state within a data source at the current point in time to a page generated from the provided node when it's node manifest is processed. * * @param {Object} manifest Manifest data * @param {string} manifest.manifestId An id which ties the unique revision state of this manifest to the unique revision state of a data source. * @param {Object} manifest.node The Gatsby node to tie the manifestId to. See the "createNode" action for more information about the node object details. * @param {string} manifest.updatedAtUTC (optional) The time in which the node was last updated. If this parameter is not included, a manifest is created for every node that gets called. By default, node manifests are created for content updated in the last 30 days. To change this, set a `NODE_MANIFEST_MAX_DAYS_OLD` environment variable. * @example * unstable_createNodeManifest({ * manifestId: `post-id-1--updated-53154315`, * updatedAtUTC: `2021-07-08T21:52:28.791+01:00`, * node: { * id: `post-id-1` * }, * }) */ // @ts-ignore actions.unstable_createNodeManifest = ({ manifestId, node, updatedAtUTC }, plugin) => { return { type: `CREATE_NODE_MANIFEST`, payload: { manifestId, node, pluginName: plugin.name, updatedAtUTC, }, }; }; /** * Marks a source plugin as "stateful" which disables automatically deleting untouched nodes. Stateful source plugins manage deleting their own nodes without stale node checks in Gatsby. * Enabling this is a major performance improvement for source plugins that manage their own node deletion. It also lowers the total memory required by a source plugin. * When using this action, check if it's supported first with `hasFeature('stateful-source-nodes')`, `hasFeature` is exported from `@netlify/content-engine/dist/plugin-utils`. * * @example * import { hasFeature } from "@netlify/content-engine/dist/plugin-utils" * * exports.sourceNodes = ({ actions }) => { * if (hasFeature(`stateful-source-nodes`)) { * actions.enableStatefulSourceNodes() * } else { * // fallback to old behavior where all nodes are iterated on and touchNode is called. * } * } * * @param {void} $0 */ // @ts-ignore actions.enableStatefulSourceNodes = (plugin) => { return { type: `ENABLE_STATEFUL_SOURCE_PLUGIN`, plugin, }; }; /** * Stores request headers for a given domain to be later used when making requests for Image CDN (and potentially other features). * * @param {Object} $0 * @param {string} $0.domain The domain to store the headers for. * @param {Object} $0.headers The headers to store. */ // @ts-ignore actions.setRequestHeaders = ({ domain, headers }, plugin) => { const headersIsObject = typeof headers === `object` && headers !== null && !Array.isArray(headers); const noHeaders = !headersIsObject; const noDomain = typeof domain !== `string`; if (noHeaders) { reporter_1.default.warn(`Plugin ${plugin.name} called actions.setRequestHeaders with a headers property that isn't an object.`); } if (noDomain) { reporter_1.default.warn(`Plugin ${plugin.name} called actions.setRequestHeaders with a domain property that isn't a string.`); } if (noDomain || noHeaders) { reporter_1.default.panic(`Plugin ${plugin.name} attempted to set request headers with invalid arguments. See above warnings for more info.`); return null; } const baseDomain = url_1.default.parse(domain)?.hostname; if (baseDomain) { return { type: `SET_REQUEST_HEADERS`, payload: { domain: baseDomain, headers, }, }; } else { reporter_1.default.panic(`Plugin ${plugin.name} attempted to set request headers for a domain that is not a valid URL. (${domain})`); return null; } }; //# sourceMappingURL=public.js.map