diff --git a/packages/gatsby/src/internal-plugins/internal-data-bridge/gatsby-node.js b/packages/gatsby/src/internal-plugins/internal-data-bridge/gatsby-node.js index 73b0ed335adba..608d8baf2b78c 100644 --- a/packages/gatsby/src/internal-plugins/internal-data-bridge/gatsby-node.js +++ b/packages/gatsby/src/internal-plugins/internal-data-bridge/gatsby-node.js @@ -209,5 +209,20 @@ exports.createResolvers = ({ createResolvers }) => { emitter.on(`DELETE_PAGE`, action => { const nodeId = createPageId(action.payload.path) const node = getNode(nodeId) - store.dispatch(actions.deleteNode(node)) + let deleteNodeActions = actions.deleteNode(node) + if (action.transactionId) { + function swapToStagedDelete(action) { + return { + ...action, + type: `DELETE_NODE_STAGING`, + transactionId: action.transactionId, + } + } + + deleteNodeActions = Array.isArray(deleteNodeActions) + ? deleteNodeActions.map(swapToStagedDelete) + : swapToStagedDelete(deleteNodeActions) + } + + store.dispatch(deleteNodeActions) }) diff --git a/packages/gatsby/src/redux/actions/commit-staging-nodes.ts b/packages/gatsby/src/redux/actions/commit-staging-nodes.ts new file mode 100644 index 0000000000000..81ff8b3a77244 --- /dev/null +++ b/packages/gatsby/src/redux/actions/commit-staging-nodes.ts @@ -0,0 +1,51 @@ +import { ActionsUnion } from "../types" +import { internalCreateNodeWithoutValidation } from "./internal" +import { actions as publicActions } from "./public" +import { getNode } from "../../datastore" + +import { store } from "../index" + +export const commitStagingNodes = ( + transactionId: string +): Array => { + const transaction = store + .getState() + .nodesStaging.transactions.get(transactionId) + if (!transaction) { + return [] + } + + const actions: Array = [ + { + type: `COMMIT_STAGING_NODES`, + payload: { + transactionId, + }, + }, + ] + + const nodesState = new Map() + for (const action of transaction) { + if (action.type === `CREATE_NODE_STAGING`) { + nodesState.set(action.payload.id, action) + } else if (action.type === `DELETE_NODE_STAGING` && action.payload?.id) { + nodesState.set(action.payload.id, undefined) + } + } + for (const [id, actionOrDelete] of nodesState.entries()) { + if (actionOrDelete) { + actions.push( + ...internalCreateNodeWithoutValidation( + actionOrDelete.payload, + actionOrDelete.plugin, + actionOrDelete + ) + ) + } else { + // delete case + actions.push(publicActions.deleteNode(getNode(id))) + } + } + + return actions +} diff --git a/packages/gatsby/src/redux/actions/internal.ts b/packages/gatsby/src/redux/actions/internal.ts index d8ee3b3b2451c..79683f5f66a2b 100644 --- a/packages/gatsby/src/redux/actions/internal.ts +++ b/packages/gatsby/src/redux/actions/internal.ts @@ -1,4 +1,5 @@ import reporter from "gatsby-cli/lib/reporter" +import _ from "lodash" import { IGatsbyConfig, @@ -27,8 +28,12 @@ import { ICreatePageDependencyActionPayloadType, IDeleteNodeManifests, IClearGatsbyImageSourceUrlAction, + ActionsUnion, + IGatsbyNode, + IDeleteNodeAction, } from "../types" +import { store } from "../index" import { gatsbyConfigSchema } from "../../joi-schemas/joi" import { didYouMean } from "../../utils/did-you-mean" import { @@ -38,6 +43,8 @@ import { getInProcessJobPromise, } from "../../utils/jobs/manager" import { getEngineContext } from "../../utils/engine-context" +import { getNode } from "../../datastore" +import { hasNodeChanged } from "../../utils/nodes" /** * Create a dependency between a page and data. Probably for @@ -445,3 +452,99 @@ export const clearGatsbyImageSourceUrls = type: `CLEAR_GATSBY_IMAGE_SOURCE_URL`, } } + +// We add a counter to node.internal for fast comparisons/intersections +// of various node slices. The counter must increase even across builds. +export function getNextNodeCounter(): number { + const lastNodeCounter = 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 +} + +export const findChildren = (initialChildren: Array): Array => { + const children = [...initialChildren] + const queue = [...initialChildren] + const traversedNodes = new Set() + + while (queue.length > 0) { + const currentChild = getNode(queue.pop()!) + if (!currentChild || traversedNodes.has(currentChild.id)) { + continue + } + traversedNodes.add(currentChild.id) + const newChildren = currentChild.children + if (_.isArray(newChildren) && newChildren.length > 0) { + children.push(...newChildren) + queue.push(...newChildren) + } + } + return children +} + +function isNode(node: any): node is IGatsbyNode { + return Boolean(node) +} + +export function internalCreateNodeWithoutValidation( + node: IGatsbyNode, + plugin?: IGatsbyPlugin, + actionOptions?: any +): Array { + let deleteActions: Array | undefined + let updateNodeAction + + const oldNode = getNode(node.id) + + // marking internal-data-bridge as owner of SitePage instead of plugin that calls createPage + if (oldNode && !hasNodeChanged(node.id, node.internal.contentDigest)) { + updateNodeAction = { + ...actionOptions, + plugin, + type: `TOUCH_NODE`, + typeName: node.internal.type, + payload: node.id, + } + } else { + // Remove any previously created descendant nodes as they're all due + // to be recreated. + if (oldNode) { + const createDeleteAction = (node: IGatsbyNode): IDeleteNodeAction => { + return { + ...actionOptions, + type: `DELETE_NODE`, + plugin, + payload: node, + isRecursiveChildrenDelete: true, + } + } + deleteActions = findChildren(oldNode.children) + .map(getNode) + .filter(isNode) + .map(createDeleteAction) + } + + node.internal.counter = getNextNodeCounter() + + updateNodeAction = { + ...actionOptions, + type: `CREATE_NODE`, + plugin, + oldNode, + payload: node, + } + } + + const actions: Array = [] + + if (deleteActions && deleteActions.length) { + actions.push(...deleteActions) + } + + actions.push(updateNodeAction) + + return actions +} diff --git a/packages/gatsby/src/redux/actions/public.js b/packages/gatsby/src/redux/actions/public.js index da30fddc21b56..e83191a3a8859 100644 --- a/packages/gatsby/src/redux/actions/public.js +++ b/packages/gatsby/src/redux/actions/public.js @@ -30,10 +30,16 @@ const apiRunnerNode = require(`../../utils/api-runner-node`) const { getNonGatsbyCodeFrame } = require(`../../utils/stack-trace-utils`) const { getPageMode } = require(`../../utils/page-mode`) const normalizePath = require(`../../utils/normalize-path`).default -import { createJobV2FromInternalJob } from "./internal" +import { + createJobV2FromInternalJob, + internalCreateNodeWithoutValidation, + getNextNodeCounter, + findChildren, +} from "./internal" import { maybeSendJobToMainProcess } from "../../utils/jobs/worker-messaging" import { reportOnce } from "../../utils/report-once" import { wrapNode } from "../../utils/detect-node-mutations" +import { shouldRunOnCreatePage, shouldRunOnCreateNode } from "../plugin-runner" const isNotTestEnv = process.env.NODE_ENV !== `test` const isTestEnv = process.env.NODE_ENV === `test` @@ -57,28 +63,6 @@ const ensureWindowsDriveIsUppercase = filePath => { : filePath } -const findChildren = initialChildren => { - const children = [...initialChildren] - const queue = [...initialChildren] - const traversedNodes = new Set() - - while (queue.length > 0) { - const currentChild = getNode(queue.pop()) - if (!currentChild || traversedNodes.has(currentChild.id)) { - continue - } - traversedNodes.add(currentChild.id) - const newChildren = currentChild.children - if (_.isArray(newChildren) && newChildren.length > 0) { - children.push(...newChildren) - queue.push(...newChildren) - } - } - return children -} - -import type { Plugin } from "./types" - type Job = { id: string, } @@ -137,10 +121,15 @@ type PageDataRemove = { * @example * deletePage(page) */ -actions.deletePage = (page: IPageInput) => { +actions.deletePage = ( + page: IPageInput, + plugin?: Plugin, + actionOptions?: ActionOptions +) => { return { type: `DELETE_PAGE`, payload: page, + transactionId: actionOptions?.transactionId, } } @@ -475,70 +464,44 @@ ${reservedFields.map(f => ` * "${f}"`).join(`\n`)} contentDigest: createContentDigest(node), } node.id = `SitePage ${internalPage.path}` - const oldNode = getNode(node.id) - - let deleteActions - let updateNodeAction - // marking internal-data-bridge as owner of SitePage instead of plugin that calls createPage - if (oldNode && !hasNodeChanged(node.id, node.internal.contentDigest)) { - updateNodeAction = { - ...actionOptions, - plugin: { name: `internal-data-bridge` }, - type: `TOUCH_NODE`, - typeName: node.internal.type, - payload: node.id, - } - } 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: { name: `internal-data-bridge` }, - payload: node, - isRecursiveChildrenDelete: true, - } - } - deleteActions = findChildren(oldNode.children) - .map(getNode) - .map(createDeleteAction) - } - node.internal.counter = getNextNodeCounter() - - updateNodeAction = { - ...actionOptions, - type: `CREATE_NODE`, - plugin: { name: `internal-data-bridge` }, - oldNode, - payload: node, - } - } + const transactionId = + actionOptions?.transactionId ?? + (shouldRunOnCreatePage() ? node.internal.contentDigest : undefined) // Sanitize page object so we don't attempt to serialize user-provided objects that are not serializable later const sanitizedPayload = sanitizeNode(internalPage) - const actions = [ - { - ...actionOptions, - type: `CREATE_PAGE`, - contextModified, - componentModified, - slicesModified, - plugin, - payload: sanitizedPayload, - }, - ] + const createPageAction = { + ...actionOptions, + type: `CREATE_PAGE`, + contextModified, + componentModified, + slicesModified, + plugin, + payload: sanitizedPayload, + } + const actions = [createPageAction] - if (deleteActions && deleteActions.length) { - actions.push(...deleteActions) + if (transactionId) { + createPageAction.transactionId = transactionId + actions.push({ + ...actionOptions, + type: `CREATE_NODE_STAGING`, + plugin: { name: `internal-data-bridge` }, + payload: node, + transactionId, + }) + return actions } - actions.push(updateNodeAction) + const upsertNodeActions = internalCreateNodeWithoutValidation( + node, + { name: `internal-data-bridge` }, + actionOptions + ) - return actions + return [...actions, ...upsertNodeActions] } /** @@ -580,18 +543,6 @@ actions.deleteNode = (node: any, plugin?: Plugin) => { } } -// 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 = 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. @@ -836,7 +787,7 @@ actions.createNode = Array.isArray(actions) ? actions : [actions] ).find(action => action.type === `CREATE_NODE`) - if (!createNodeAction) { + if (!createNodeAction || !shouldRunOnCreateNode()) { return Promise.resolve(undefined) } diff --git a/packages/gatsby/src/redux/plugin-runner.ts b/packages/gatsby/src/redux/plugin-runner.ts index 8c5b32fa14a92..3f6bb380d15ec 100644 --- a/packages/gatsby/src/redux/plugin-runner.ts +++ b/packages/gatsby/src/redux/plugin-runner.ts @@ -2,7 +2,7 @@ import { Span } from "opentracing" import { emitter, store } from "./index" import apiRunnerNode from "../utils/api-runner-node" import { ActivityTracker } from "../../" -import { ICreateNodeAction } from "./types" +import { ICreateNodeAction, ICreateNodeStagingAction } from "./types" type Plugin = any // TODO @@ -39,8 +39,12 @@ interface ICreatePageAction { pluginCreatorId: string componentPath: string } + transactionId?: string } +let hasOnCreatePage = false +let hasOnCreateNode = false + export const startPluginRunner = (): void => { const plugins = store.getState().flattenedPlugins const pluginsImplementingOnCreatePage = plugins.filter(plugin => @@ -49,12 +53,21 @@ export const startPluginRunner = (): void => { const pluginsImplementingOnCreateNode = plugins.filter(plugin => plugin.nodeAPIs.includes(`onCreateNode`) ) - if (pluginsImplementingOnCreatePage.length > 0) { + + hasOnCreatePage = pluginsImplementingOnCreatePage.length > 0 + hasOnCreateNode = pluginsImplementingOnCreateNode.length > 0 + + if (hasOnCreatePage) { emitter.on(`CREATE_PAGE`, (action: ICreatePageAction) => { const page = action.payload apiRunnerNode( `onCreatePage`, - { page, traceId: action.traceId, parentSpan: action.parentSpan }, + { + page, + traceId: action.traceId, + parentSpan: action.parentSpan, + transactionId: action.transactionId, + }, { pluginSource: action.plugin.name, activity: action.activity } ) }) @@ -62,16 +75,29 @@ export const startPluginRunner = (): void => { // We make page nodes outside of the normal action for speed so we manually // call onCreateNode here for SitePage nodes. - if (pluginsImplementingOnCreateNode.length > 0) { - emitter.on(`CREATE_NODE`, (action: ICreateNodeAction) => { + if (hasOnCreateNode) { + const createNodeMiddleware = ( + action: ICreateNodeStagingAction | ICreateNodeAction + ): void => { const node = action.payload if (node.internal.type === `SitePage`) { + const transactionId = + action.type === `CREATE_NODE` ? undefined : action.transactionId apiRunnerNode(`onCreateNode`, { node, parentSpan: action.parentSpan, traceTags: { nodeId: node.id, nodeType: node.internal.type }, + traceId: transactionId, + transactionId, + waitForCascadingActions: true, }) } - }) + } + emitter.on(`CREATE_NODE`, createNodeMiddleware) + emitter.on(`CREATE_NODE_STAGING`, createNodeMiddleware) } } + +export const shouldRunOnCreateNode = (): boolean => hasOnCreateNode + +export const shouldRunOnCreatePage = (): boolean => hasOnCreatePage diff --git a/packages/gatsby/src/redux/reducers/index.ts b/packages/gatsby/src/redux/reducers/index.ts index e2b394a5de986..a64c9ffcccba5 100644 --- a/packages/gatsby/src/redux/reducers/index.ts +++ b/packages/gatsby/src/redux/reducers/index.ts @@ -39,6 +39,7 @@ import { componentsUsingSlicesReducer } from "./components-using-slices" import { slicesByTemplateReducer } from "./slices-by-template" import { adapterReducer } from "./adapter" import { remoteFileAllowedUrlsReducer } from "./remote-file-allowed-urls" +import { nodesStagingReducer } from "./nodes-staging" /** * @property exports.nodesTouched Set @@ -85,4 +86,5 @@ export { telemetryReducer as telemetry, adapterReducer as adapter, remoteFileAllowedUrlsReducer as remoteFileAllowedUrls, + nodesStagingReducer as nodesStaging, } diff --git a/packages/gatsby/src/redux/reducers/nodes-staging.ts b/packages/gatsby/src/redux/reducers/nodes-staging.ts new file mode 100644 index 0000000000000..b5a818878ac48 --- /dev/null +++ b/packages/gatsby/src/redux/reducers/nodes-staging.ts @@ -0,0 +1,53 @@ +import { ActionsUnion, IGatsbyState, TransactionActionsUnion } from "../types" + +function getInitialState(): IGatsbyState["nodesStaging"] { + return { transactions: new Map() } +} + +function addActionToTransaction( + state: IGatsbyState["nodesStaging"], + action: TransactionActionsUnion +): void { + if (!action.transactionId) { + return + } + + const transaction = state.transactions.get(action.transactionId) + if (!transaction) { + state.transactions.set(action.transactionId, [action]) + } else { + transaction.push(action) + } +} + +export const nodesStagingReducer = ( + state: IGatsbyState["nodesStaging"] = getInitialState(), + action: ActionsUnion +): IGatsbyState["nodesStaging"] => { + switch (action.type) { + case `DELETE_CACHE`: + return getInitialState() + + case `CREATE_NODE_STAGING`: { + if (action.transactionId) { + addActionToTransaction(state, action) + } + + return state + } + + case `DELETE_NODE_STAGING`: { + if (action.payload && action.transactionId) { + addActionToTransaction(state, action) + } + return state + } + case `COMMIT_STAGING_NODES`: { + state.transactions.delete(action.payload.transactionId) + return state + } + + default: + return state + } +} diff --git a/packages/gatsby/src/redux/types.ts b/packages/gatsby/src/redux/types.ts index 44e8abaafcfa2..3cb32ce8a4864 100644 --- a/packages/gatsby/src/redux/types.ts +++ b/packages/gatsby/src/redux/types.ts @@ -426,6 +426,9 @@ export interface IGatsbyState { config: IAdapterFinalConfig } remoteFileAllowedUrls: Set + nodesStaging: { + transactions: Map> + } } export type GatsbyStateKeys = keyof IGatsbyState @@ -457,11 +460,13 @@ export type ActionsUnion = | IApiFinishedAction | ICreateFieldExtension | ICreateNodeAction + | ICreateNodeStagingAction | ICreatePageAction | ICreatePageDependencyAction | ICreateTypes | IDeleteCacheAction | IDeleteNodeAction + | IDeleteNodeStagingAction | IDeletePageAction | IPageQueryRunAction | IPrintTypeDefinitions @@ -541,7 +546,11 @@ export type ActionsUnion = | ISetAdapterAction | IDisablePluginsByNameAction | IAddImageCdnAllowedUrl + | ICommitStagingNodes +export type TransactionActionsUnion = + | ICreateNodeStagingAction + | IDeleteNodeStagingAction export interface IInitAction { type: `INIT` } @@ -823,6 +832,7 @@ export interface ICreatePageAction { contextModified?: boolean componentModified?: boolean slicesModified?: boolean + transactionId?: string } export interface ICreateSliceAction { @@ -1040,6 +1050,11 @@ export interface ICreateNodeAction { plugin: IGatsbyPlugin } +export type ICreateNodeStagingAction = Omit & { + type: `CREATE_NODE_STAGING` + transactionId: string +} + export interface IAddFieldToNodeAction { type: `ADD_FIELD_TO_NODE` payload: IGatsbyNode @@ -1059,6 +1074,11 @@ export interface IDeleteNodeAction { isRecursiveChildrenDelete?: boolean } +export type IDeleteNodeStagingAction = Omit & { + type: `DELETE_NODE_STAGING` + transactionId: string +} + export interface ISetSiteFlattenedPluginsAction { type: `SET_SITE_FLATTENED_PLUGINS` payload: IGatsbyState["flattenedPlugins"] @@ -1265,6 +1285,13 @@ export interface IClearJobV2Context { } } +export interface ICommitStagingNodes { + type: `COMMIT_STAGING_NODES` + payload: { + transactionId: string + } +} + export const HTTP_STATUS_CODE = { /** * The server has received the request headers and the client should proceed to send the request body diff --git a/packages/gatsby/src/state-machines/__tests__/develop.ts b/packages/gatsby/src/state-machines/__tests__/develop.ts index bbe141b648e7a..0cdcaf3ca95b8 100644 --- a/packages/gatsby/src/state-machines/__tests__/develop.ts +++ b/packages/gatsby/src/state-machines/__tests__/develop.ts @@ -56,6 +56,10 @@ describe(`the top-level develop state machine`, () => { resetAllMocks() }) + afterEach(async () => { + await tick() + }) + it(`initialises`, async () => { const service = interpret(machine) service.start() diff --git a/packages/gatsby/src/utils/api-runner-node.js b/packages/gatsby/src/utils/api-runner-node.js index 74c3b6fd699c6..554176ff01b04 100644 --- a/packages/gatsby/src/utils/api-runner-node.js +++ b/packages/gatsby/src/utils/api-runner-node.js @@ -34,6 +34,10 @@ import errorParser from "./api-runner-error-parser" import { wrapNode, wrapNodes } from "./detect-node-mutations" import { reportOnce } from "./report-once" +/** + * @type {import('../redux/actions/commit-staging-nodes').commitStagingNodes | undefined} + */ +let commitStagingNodes // Override createContentDigest to remove autogenerated data from nodes to // ensure consistent digests. function createContentDigest(node) { @@ -81,13 +85,25 @@ const nodeMutationsWrappers = { }, } +const transactionToDoubleBindCacheKey = new Map() // 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 { traceId, deferNodeMutation, transactionId } = actionOptions const defer = deferNodeMutation ? `defer-node-mutation` : `` - const actionKey = plugin.name + api + traceId + defer + const actionKey = + plugin.name + api + traceId + defer + (transactionId ?? `no-transaction`) + + if (transactionId) { + let actionKeys = transactionToDoubleBindCacheKey.get(transactionId) + if (!actionKeys) { + actionKeys = new Set() + transactionToDoubleBindCacheKey.set(transactionId, actionKeys) + } + actionKeys.add(actionKey) + } + if (boundPluginActionCreators[actionKey]) { return boundPluginActionCreators[actionKey] } else { @@ -311,6 +327,33 @@ const getUninitializedCache = plugin => { } } +function maybeCommitTransaction(transactionId) { + if (transactionId) { + setImmediate(() => { + const count = (ongoingTransactions.get(transactionId) ?? 0) - 1 + if (count <= 0) { + ongoingTransactions.delete(transactionId) + if (!commitStagingNodes) { + commitStagingNodes = + require(`../redux/actions/commit-staging-nodes`).commitStagingNodes + } + store.dispatch(commitStagingNodes(transactionId)) + + // cleanup double bound action creators for this transaction + const actionKeys = transactionToDoubleBindCacheKey.get(transactionId) + if (actionKeys) { + actionKeys.forEach(actionKey => { + delete boundPluginActionCreators[actionKey] + }) + transactionToDoubleBindCacheKey.delete(transactionId) + } + } else { + ongoingTransactions.set(transactionId, count) + } + }) + } +} + const availableActionsCache = new Map() let publicPath const runAPI = async (plugin, api, args, activity) => { @@ -512,6 +555,7 @@ const runAPI = async (plugin, api, args, activity) => { const apisRunningById = new Map() const apisRunningByTraceId = new Map() let waitingForCasacadeToFinish = [] +const ongoingTransactions = new Map() function apiRunnerNode(api, args = {}, { pluginSource, activity } = {}) { const plugins = store.getState().flattenedPlugins @@ -532,12 +576,20 @@ function apiRunnerNode(api, args = {}, { pluginSource, activity } = {}) { } // If there's no implementing plugins, return early. - if (implementingPlugins.length === 0) { + if (implementingPlugins.length === 0 && args.transactionId) { + maybeCommitTransaction(args.transactionId) + return null } return new Promise(resolve => { - const { parentSpan, traceId, traceTags, waitForCascadingActions } = args + const { + parentSpan, + traceId, + traceTags, + waitForCascadingActions, + transactionId, + } = args const apiSpanArgs = parentSpan ? { childOf: parentSpan } : {} const apiSpan = tracer.startSpan(`run-api`, apiSpanArgs) @@ -578,6 +630,11 @@ function apiRunnerNode(api, args = {}, { pluginSource, activity } = {}) { } apiRunInstance.id = id + if (transactionId) { + const count = (ongoingTransactions.get(transactionId) ?? 0) + 1 + ongoingTransactions.set(transactionId, count) + } + if (waitForCascadingActions) { waitingForCasacadeToFinish.push(apiRunInstance) } @@ -721,6 +778,10 @@ function apiRunnerNode(api, args = {}, { pluginSource, activity } = {}) { emitter.emit(`API_RUNNING_QUEUE_EMPTY`) } + if (transactionId) { + maybeCommitTransaction(args.transactionId) + } + // Filter empty results apiRunInstance.results = results.filter(result => !_.isEmpty(result)) diff --git a/packages/gatsby/src/utils/source-nodes.ts b/packages/gatsby/src/utils/source-nodes.ts index d3b4ec0f81608..0584f6f6053ab 100644 --- a/packages/gatsby/src/utils/source-nodes.ts +++ b/packages/gatsby/src/utils/source-nodes.ts @@ -81,6 +81,14 @@ function getStaleNodes( }) } +const GatsbyManagedStatefulNodeTypes = new Set([ + // Gatsby will create and delete SitePage nodes as pages are created and deleted. + // Additionally this node type is not even created at the time we delete stale nodes + // so cleanup would happen too early resulting in deleting each nodes from previous run + // and potentially recreating all of them on `createPages` that happens later. + `SitePage`, +]) + /** * Find all stale nodes and delete them unless the node type has been opted out of stale node garbage collection. */ @@ -99,6 +107,11 @@ async function deleteStaleNodes( const { typeOwners, statefulSourcePlugins } = state for (const typeName of previouslyExistingNodeTypeNames) { + if (GatsbyManagedStatefulNodeTypes.has(typeName)) { + // skip any stateful node types that are managed by Gatsby + continue + } + const pluginName = typeOwners.typesToPlugins.get(typeName) // no need to check this type if its owner has declared its a stateful source plugin