diff --git a/packages/gatsby/src/redux/actions/public.js b/packages/gatsby/src/redux/actions/public.js index 1c0d8e644655d..6e7f715b65246 100644 --- a/packages/gatsby/src/redux/actions/public.js +++ b/packages/gatsby/src/redux/actions/public.js @@ -15,7 +15,7 @@ const { const { splitComponentPath } = require(`gatsby-core-utils/parse-component-path`) const { hasNodeChanged } = require(`../../utils/nodes`) const { getNode, getDataStore } = require(`../../datastore`) -import sanitizeNode from "../../utils/sanitize-node" +import { sanitizeNode } from "../../utils/sanitize-node" const { store } = require(`../index`) const { validateComponent } = require(`../../utils/validate-component`) import { nodeSchema } from "../../joi-schemas/joi" diff --git a/packages/gatsby/src/utils/__tests__/sanitize-node.ts b/packages/gatsby/src/utils/__tests__/sanitize-node.ts index 429bb825d0d0a..1e91e45912fcd 100644 --- a/packages/gatsby/src/utils/__tests__/sanitize-node.ts +++ b/packages/gatsby/src/utils/__tests__/sanitize-node.ts @@ -1,4 +1,4 @@ -import sanitizeNode from "../sanitize-node" +import { sanitizeNode } from "../sanitize-node" describe(`node sanitization`, () => { let testNode @@ -90,4 +90,24 @@ describe(`node sanitization`, () => { // should be same instance expect(result).toBe(testNodeWithoutUnserializableData) }) + + it(`keeps length field but not OOM`, () => { + const testNodeWithLength = { + id: `id2`, + parent: ``, + children: [], + length: 81185414, + foo: `bar`, + internal: { + type: `Test`, + contentDigest: `digest1`, + owner: `test`, + counter: 0, + }, + fields: [], + } + const result = sanitizeNode(testNodeWithLength) + // @ts-ignore - Just for tests + expect(result.length).toBeDefined() + }) }) diff --git a/packages/gatsby/src/utils/sanitize-node.ts b/packages/gatsby/src/utils/sanitize-node.ts index d86601a6a08fe..a92504a4cb0f4 100644 --- a/packages/gatsby/src/utils/sanitize-node.ts +++ b/packages/gatsby/src/utils/sanitize-node.ts @@ -3,15 +3,15 @@ import _ from "lodash" import type { IGatsbyNode } from "../redux/types" import type { GatsbyIterable } from "../datastore/common/iterable" -type data = IGatsbyNode | GatsbyIterable +type Data = IGatsbyNode | GatsbyIterable + +type OmitUndefined = (data: Data) => Partial /** * @param {Object|Array} data * @returns {Object|Array} data without undefined values */ -type omitUndefined = (data: data) => Partial - -const omitUndefined: omitUndefined = data => { +const omitUndefined: OmitUndefined = data => { const isPlainObject = _.isPlainObject(data) if (isPlainObject) { return _.pickBy(data, p => p !== undefined) @@ -20,12 +20,12 @@ const omitUndefined: omitUndefined = data => { return (data as GatsbyIterable).filter(p => p !== undefined) } +type isTypeSupported = (data: Data) => boolean + /** * @param {*} data - * @return {boolean} + * @return {boolean} Boolean if type is supported */ -type isTypeSupported = (data: data) => boolean - const isTypeSupported: isTypeSupported = data => { if (data === null) { return true @@ -41,42 +41,67 @@ const isTypeSupported: isTypeSupported = data => { return isSupported } +type sanitizeNode = ( + data: Data, + isNode?: boolean, + path?: Set +) => Data | undefined + /** * Make data serializable * @param {(Object|Array)} data to sanitize * @param {boolean} isNode = true * @param {Set} path = new Set */ - -type sanitizeNode = ( - data: data, - isNode?: boolean, - path?: Set -) => data | undefined - -const sanitizeNode: sanitizeNode = (data, isNode = true, path = new Set()) => { +export const sanitizeNode: sanitizeNode = ( + data, + isNode = true, + path = new Set() +) => { const isPlainObject = _.isPlainObject(data) + const isArray = _.isArray(data) - if (isPlainObject || _.isArray(data)) { + if (isPlainObject || isArray) { if (path.has(data)) return data path.add(data) - const returnData = isPlainObject ? {} : [] + const returnData = isPlainObject + ? ({} as IGatsbyNode) + : ([] as Array) let anyFieldChanged = false - _.each(data, (o, key) => { + + // _.each is a "Collection" method and thus objects with "length" property are iterated as arrays + const hasLengthProperty = isPlainObject + ? Object.prototype.hasOwnProperty.call(data, `length`) + : false + let lengthProperty + if (hasLengthProperty) { + lengthProperty = (data as IGatsbyNode).length + delete (data as IGatsbyNode).length + } + + _.each(data, (value, key) => { if (isNode && key === `internal`) { - returnData[key] = o + returnData[key] = value return } - returnData[key] = sanitizeNode(o as data, false, path) + returnData[key] = sanitizeNode(value as Data, false, path) - if (returnData[key] !== o) { + if (returnData[key] !== value) { anyFieldChanged = true } }) + if (hasLengthProperty) { + ;(data as IGatsbyNode).length = lengthProperty + returnData.length = sanitizeNode(lengthProperty as Data, false, path) + if (returnData.length !== lengthProperty) { + anyFieldChanged = true + } + } + if (anyFieldChanged) { - data = omitUndefined(returnData as data) as data + data = omitUndefined(returnData as Data) as Data } // arrays and plain objects are supported - no need to to sanitize @@ -89,5 +114,3 @@ const sanitizeNode: sanitizeNode = (data, isNode = true, path = new Set()) => { return data } } - -export default sanitizeNode