diff --git a/docs/blog/2018-10-16-why-mobile-performance-is-crucial/index.md b/docs/blog/2018-10-16-why-mobile-performance-is-crucial/index.md index e7b93d00281b2..6251889f86897 100644 --- a/docs/blog/2018-10-16-why-mobile-performance-is-crucial/index.md +++ b/docs/blog/2018-10-16-why-mobile-performance-is-crucial/index.md @@ -15,7 +15,7 @@ Unfortunately, in practice, great performance is surprisingly hard to achieve -- Why is that? Increased site complexity often distributes bottlenecks across multiple code points and teams of stakeholders. While performance checklists exist, they’ve ballooned to 40+ items -- making them costly and time-consuming for teams to implement. -As Gatsby's co-founder Kyle Mathews likes to say (paraphrasing Tolstoy): +As Gatsby's co-founder Kyle Mathews likes to say (paraphrasing Tolstoy): > "All fast websites are alike, but all slow websites are slow in different ways." diff --git a/docs/docs/add-seo-component.md b/docs/docs/add-seo-component.md index e99781b023fdc..bde86e99d676b 100644 --- a/docs/docs/add-seo-component.md +++ b/docs/docs/add-seo-component.md @@ -211,7 +211,7 @@ const query = graphql` } } } -`; +` ``` ## Examples diff --git a/docs/docs/visual-testing-with-storybook.md b/docs/docs/visual-testing-with-storybook.md index d76b8b86be4b3..a9e9b31b0503a 100644 --- a/docs/docs/visual-testing-with-storybook.md +++ b/docs/docs/visual-testing-with-storybook.md @@ -4,8 +4,8 @@ title: Visual Testing with Storybook Knowing your components look as intended in every permutation is not only a great way to test them visually, but also provides "living documentation" for them. This makes it easier for teams to: -1) know what components are available to them in a given project and -2) what props those components accept and what all of the states of that component are. +1. know what components are available to them in a given project and +2. what props those components accept and what all of the states of that component are. As your project grows over time having this information available will be invaluable. This is the function of the Storybook library. Storybook is a UI development environment for your UI components. With it, you can visualize different states of your UI components and develop them interactively. diff --git a/packages/gatsby-plugin-sharp/src/__tests__/index.js b/packages/gatsby-plugin-sharp/src/__tests__/index.js index 0be8444a8bc29..a5bf03e437d12 100644 --- a/packages/gatsby-plugin-sharp/src/__tests__/index.js +++ b/packages/gatsby-plugin-sharp/src/__tests__/index.js @@ -27,7 +27,7 @@ describe(`gatsby-plugin-sharp`, () => { const file = getFileObject(absolutePath) // used to find all breakpoints in a srcSet string - const findAllBreakpoints = (srcSet) => { + const findAllBreakpoints = srcSet => { // RegEx to find all occurrences of 'Xw', where 'X' can be any int const regEx = /[0-9]+w/g return srcSet.match(regEx) @@ -129,12 +129,7 @@ describe(`gatsby-plugin-sharp`, () => { }) it(`accepts srcSet breakpoints`, async () => { - const srcSetBreakpoints = [ - 50, - 70, - 150, - 250, - ] + const srcSetBreakpoints = [50, 70, 150, 250] const args = { srcSetBreakpoints } const result = await fluid({ file: getFileObject(path.join(__dirname, `images/144-density.png`)), @@ -143,8 +138,7 @@ describe(`gatsby-plugin-sharp`, () => { // width of the image tested const originalWidth = 281 - const expected = srcSetBreakpoints - .map((size) => `${size}w`) + const expected = srcSetBreakpoints.map(size => `${size}w`) // add the original size of `144-density.png` expected.push(`${originalWidth}w`) @@ -154,10 +148,7 @@ describe(`gatsby-plugin-sharp`, () => { }) it(`should throw on srcSet breakpoints less than 1`, async () => { - const srcSetBreakpoints = [ - 50, - 0, - ] + const srcSetBreakpoints = [50, 0] const args = { srcSetBreakpoints } const result = fluid({ file: getFileObject(path.join(__dirname, `images/144-density.png`)), @@ -168,11 +159,7 @@ describe(`gatsby-plugin-sharp`, () => { }) it(`ensure maxWidth is in srcSet breakpoints`, async () => { - const srcSetBreakpoints = [ - 50, - 70, - 150, - ] + const srcSetBreakpoints = [50, 70, 150] const maxWidth = 200 const args = { maxWidth, @@ -208,8 +195,8 @@ describe(`gatsby-plugin-sharp`, () => { const originalWidth = 281 const expected = srcSetBreakpoints // filter out the widths that are larger than the source image width - .filter((size) => size < originalWidth) - .map((size) => `${size}w`) + .filter(size => size < originalWidth) + .map(size => `${size}w`) // add the original size of `144-density.png` expected.push(`${originalWidth}w`) @@ -221,15 +208,7 @@ describe(`gatsby-plugin-sharp`, () => { }) it(`prevents duplicate breakpoints`, async () => { - const srcSetBreakpoints = [ - 50, - 50, - 100, - 100, - 100, - 250, - 250, - ] + const srcSetBreakpoints = [50, 50, 100, 100, 100, 250, 250] const maxWidth = 100 const args = { maxWidth, @@ -241,12 +220,7 @@ describe(`gatsby-plugin-sharp`, () => { }) const originalWidth = 281 - const expected = [ - `50w`, - `100w`, - `250w`, - `${originalWidth}w`, - ] + const expected = [`50w`, `100w`, `250w`, `${originalWidth}w`] const actual = findAllBreakpoints(result.srcSet) expect(actual).toEqual(expect.arrayContaining(expected)) diff --git a/packages/gatsby/src/redux/index.js b/packages/gatsby/src/redux/index.js index 14a6f3a425aa8..4cf0bd23d3cbb 100644 --- a/packages/gatsby/src/redux/index.js +++ b/packages/gatsby/src/redux/index.js @@ -79,7 +79,9 @@ const saveState = state => { pickedState.components = mapToObject(pickedState.components) pickedState.nodes = mapToObject(pickedState.nodes) - const writeStream = fs.createWriteStream(`${process.cwd()}/.cache/redux-state.json`) + const writeStream = fs.createWriteStream( + `${process.cwd()}/.cache/redux-state.json` + ) new stringify(pickedState, null, 2, true) .pipe(writeStream) diff --git a/packages/gatsby/src/schema/__tests__/build-node-connections-test.js b/packages/gatsby/src/schema/__tests__/build-node-connections-test.js new file mode 100644 index 0000000000000..8661baab85004 --- /dev/null +++ b/packages/gatsby/src/schema/__tests__/build-node-connections-test.js @@ -0,0 +1,197 @@ +const { graphql, GraphQLObjectType, GraphQLSchema } = require(`graphql`) +const _ = require(`lodash`) +const buildNodeTypes = require(`../build-node-types`) +const buildNodeConnections = require(`../build-node-connections`) + +jest.mock(`../../redux/actions/add-page-dependency`, () => { + return { + createPageDependency: jest.fn(), + } +}) + +const { + createPageDependency, +} = require(`../../redux/actions/add-page-dependency`) + +describe(`build-node-connections`, () => { + let schema, store, types, connections + + async function runQuery(query) { + let context = { path: `foo` } + let { data, errors } = await graphql(schema, query, context, context) + expect(errors).not.toBeDefined() + return data + } + + beforeEach(async () => { + ;({ store } = require(`../../redux`)) + store.dispatch({ type: `DELETE_CACHE` }) + ;[ + { + id: `p1`, + internal: { type: `Parent` }, + hair: `red`, + children: [`c1`, `c2`, `r1`], + }, + { + id: `r1`, + internal: { type: `Relative` }, + hair: `black`, + children: [], + parent: `p1`, + }, + { + id: `c1`, + internal: { type: `Child` }, + hair: `brown`, + children: [], + parent: `p1`, + }, + { + id: `c2`, + internal: { type: `Child` }, + hair: `blonde`, + children: [], + parent: `p1`, + }, + ].forEach(n => store.dispatch({ type: `CREATE_NODE`, payload: n })) + + types = await buildNodeTypes({}) + connections = await buildNodeConnections(_.values(types)) + + schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: `RootQueryType`, + fields: { ...connections, ..._.mapValues(types, `node`) }, + }), + }) + }) + + it(`should build connections`, () => { + expect(Object.keys(connections)).toHaveLength(3) + }) + + it(`should result in a valid queryable schema`, async () => { + let { allParent, allChild, allRelative } = await runQuery( + ` + { + allParent(filter: { id: { eq: "p1" } }) { + edges { + node { + hair + } + } + } + allChild(filter: { id: { eq: "c1" } }) { + edges { + node { + hair + } + } + } + allRelative(filter: { id: { eq: "r1" } }) { + edges { + node { + hair + } + } + } + } + ` + ) + expect(allParent.edges[0].node.hair).toEqual(`red`) + expect(allChild.edges[0].node.hair).toEqual(`brown`) + expect(allRelative.edges[0].node.hair).toEqual(`black`) + }) + + it(`should link children automatically`, async () => { + let { allParent } = await runQuery( + ` + { + allParent(filter: { id: { eq: "p1" } }) { + edges { + node { + children { + id + } + } + } + } + } + ` + ) + expect(allParent.edges[0].node.children).toBeDefined() + expect(allParent.edges[0].node.children.map(c => c.id)).toEqual([ + `c1`, + `c2`, + `r1`, + ]) + }) + + it(`should create typed children fields`, async () => { + let { allParent } = await runQuery( + ` + { + allParent(filter: { id: { eq: "p1" } }) { + edges { + node { + childrenChild { # lol + id + } + } + } + } + } + ` + ) + expect(allParent.edges[0].node.childrenChild).toBeDefined() + expect(allParent.edges[0].node.childrenChild.map(c => c.id)).toEqual([ + `c1`, + `c2`, + ]) + }) + + it(`should create typed child field for singular children`, async () => { + let { allParent } = await runQuery( + ` + { + allParent(filter: { id: { eq: "p1" } }) { + edges { + node { + childRelative { # lol + id + } + } + } + } + } + ` + ) + + expect(allParent.edges[0].node.childRelative).toBeDefined() + expect(allParent.edges[0].node.childRelative.id).toEqual(`r1`) + }) + + it(`should create page dependency`, async () => { + await runQuery( + ` + { + allParent(filter: { id: { eq: "p1" } }) { + edges { + node { + childRelative { # lol + id + } + } + } + } + } + ` + ) + + expect(createPageDependency).toHaveBeenCalledWith({ + path: `foo`, + connection: `Parent`, + }) + }) +}) diff --git a/packages/gatsby/src/schema/__tests__/build-node-types-test.js b/packages/gatsby/src/schema/__tests__/build-node-types-test.js index 8b3044533d30d..597ca19f86908 100644 --- a/packages/gatsby/src/schema/__tests__/build-node-types-test.js +++ b/packages/gatsby/src/schema/__tests__/build-node-types-test.js @@ -2,6 +2,16 @@ const { graphql, GraphQLObjectType, GraphQLSchema } = require(`graphql`) const _ = require(`lodash`) const buildNodeTypes = require(`../build-node-types`) +jest.mock(`../../redux/actions/add-page-dependency`, () => { + return { + createPageDependency: jest.fn(), + } +}) + +const { + createPageDependency, +} = require(`../../redux/actions/add-page-dependency`) + describe(`build-node-types`, () => { let schema, store, types @@ -127,4 +137,23 @@ describe(`build-node-types`, () => { expect(parent.childRelative).toBeDefined() expect(parent.childRelative.id).toEqual(`r1`) }) + + it(`should create page dependency`, async () => { + await runQuery( + ` + { + parent(id: { eq: "p1" }) { + childRelative { # lol + id + } + } + } + ` + ) + + expect(createPageDependency).toHaveBeenCalledWith({ + path: `foo`, + nodeId: `p1`, + }) + }) }) diff --git a/packages/gatsby/src/schema/__tests__/infer-graphql-input-type-test.js b/packages/gatsby/src/schema/__tests__/infer-graphql-input-type-test.js index a76ffe8b7324b..6232c679a18f2 100644 --- a/packages/gatsby/src/schema/__tests__/infer-graphql-input-type-test.js +++ b/packages/gatsby/src/schema/__tests__/infer-graphql-input-type-test.js @@ -19,6 +19,7 @@ const { getExampleValues, clearTypeExampleValues, } = require(`../data-tree-utils`) +const { connectionFromArray } = require(`graphql-skip-limit`) function queryResult(nodes, query, { types = [] } = {}) { const nodeType = new GraphQLObjectType({ @@ -62,13 +63,20 @@ function queryResult(nodes, query, { types = [] } = {}) { }), }, }, - resolve(nvi, args) { - return runSift({ + async resolve(nvi, args) { + const results = await runSift({ args, nodes, - connection: true, + firstOnly: false, type: nodeType, }) + if (results.length > 0) { + const connection = connectionFromArray(results, args) + connection.totalCount = results.length + return connection + } else { + return null + } }, }, } diff --git a/packages/gatsby/src/schema/__tests__/node-tracking-test.js b/packages/gatsby/src/schema/__tests__/node-tracking-test.js index 480b7d622e9be..e3b743bee4ffd 100644 --- a/packages/gatsby/src/schema/__tests__/node-tracking-test.js +++ b/packages/gatsby/src/schema/__tests__/node-tracking-test.js @@ -116,16 +116,12 @@ describe(`Track root nodes`, () => { args: {}, nodes, type, - connection: true, + firstOnly: false, }) - expect(result.edges.length).toEqual(2) - expect(findRootNodeAncestor(result.edges[0].node.inlineObject)).toEqual( - result.edges[0].node - ) - expect(findRootNodeAncestor(result.edges[1].node.inlineObject)).toEqual( - result.edges[1].node - ) + expect(result.length).toEqual(2) + expect(findRootNodeAncestor(result[0].inlineObject)).toEqual(result[0]) + expect(findRootNodeAncestor(result[1].inlineObject)).toEqual(result[1]) }) it(`Tracks objects when running query with filter`, async () => { @@ -141,13 +137,11 @@ describe(`Track root nodes`, () => { }, nodes, type, - connection: true, + firstOnly: false, }) - expect(result.edges.length).toEqual(1) - expect(findRootNodeAncestor(result.edges[0].node.inlineObject)).toEqual( - result.edges[0].node - ) + expect(result.length).toEqual(1) + expect(findRootNodeAncestor(result[0].inlineObject)).toEqual(result[0]) }) }) }) diff --git a/packages/gatsby/src/schema/__tests__/run-sift.js b/packages/gatsby/src/schema/__tests__/run-sift.js index a253412718bb8..3307504d698d9 100644 --- a/packages/gatsby/src/schema/__tests__/run-sift.js +++ b/packages/gatsby/src/schema/__tests__/run-sift.js @@ -5,7 +5,6 @@ const { GraphQLID, GraphQLString, } = require(`graphql`) -const { connectionFromArray } = require(`graphql-skip-limit`) jest.mock(`../node-tracking`, () => { return { @@ -13,12 +12,6 @@ jest.mock(`../node-tracking`, () => { } }) -jest.mock(`../../redux/actions/add-page-dependency`, () => { - return { - createPageDependency: jest.fn(), - } -}) - const mockNodes = [ { id: `id_1`, @@ -66,20 +59,19 @@ describe(`run-sift`, () => { nodes, typeName, args, - connection: false, + firstOnly: true, }) - const resultConnection = await runSift({ + const resultMany = await runSift({ type, nodes, typeName, args, - connection: true, + firstOnly: false, }) - delete resultConnection.totalCount - expect(resultSingular).toEqual(nodes[1]) - expect(resultConnection).toEqual(connectionFromArray([nodes[1]], args)) + expect(resultSingular).toEqual([nodes[1]]) + expect(resultMany).toEqual([nodes[1]]) }) it(`non-eq operator`, async () => { @@ -94,22 +86,19 @@ describe(`run-sift`, () => { nodes, typeName, args, - connection: false, + firstOnly: true, }) - const resultConnection = await runSift({ + const resultMany = await runSift({ type, nodes, typeName, args, - connection: true, + firstOnly: false, }) - delete resultConnection.totalCount - expect(resultSingular).toEqual(nodes[0]) - expect(resultConnection).toEqual( - connectionFromArray([nodes[0], nodes[2]], args) - ) + expect(resultSingular).toEqual([nodes[0]]) + expect(resultMany).toEqual([nodes[0], nodes[2]]) }) }) }) diff --git a/packages/gatsby/src/schema/build-node-connections.js b/packages/gatsby/src/schema/build-node-connections.js index 8b90fa3d59bd8..1b7edd82e5260 100644 --- a/packages/gatsby/src/schema/build-node-connections.js +++ b/packages/gatsby/src/schema/build-node-connections.js @@ -11,6 +11,26 @@ const { const createSortField = require(`./create-sort-field`) const buildConnectionFields = require(`./build-connection-fields`) const { getNodesByType } = require(`../db/nodes`) +const { createPageDependency } = require(`../redux/actions/add-page-dependency`) +const { connectionFromArray } = require(`graphql-skip-limit`) + +function handleQueryResult({ results, resolveArgs: queryArgs, path }) { + if (results && results.length) { + const connection = connectionFromArray(results, queryArgs) + connection.totalCount = results.length + + if (results[0].internal) { + const connectionType = connection.edges[0].node.internal.type + createPageDependency({ + path, + connection: connectionType, + }) + } + return connection + } else { + return null + } +} module.exports = (types: any) => { const connections = {} @@ -62,21 +82,21 @@ module.exports = (types: any) => { }), }, }, - resolve(object, resolveArgs, b, { rootValue }) { + async resolve(object, resolveArgs, b, { rootValue }) { let path if (typeof rootValue !== `undefined`) { path = rootValue.path } const runSift = require(`./run-sift`) const latestNodes = getNodesByType(type.name) - return runSift({ + const results = await runSift({ args: resolveArgs, nodes: latestNodes, - connection: true, - path, + firstOnly: false, typeName: typeName, type: type.node.type, }) + return handleQueryResult({ results, resolveArgs, path }) }, } }) diff --git a/packages/gatsby/src/schema/build-node-types.js b/packages/gatsby/src/schema/build-node-types.js index eb92a6915cd67..c5e9b65efec41 100644 --- a/packages/gatsby/src/schema/build-node-types.js +++ b/packages/gatsby/src/schema/build-node-types.js @@ -192,7 +192,8 @@ module.exports = async ({ parentSpan }) => { name: typeName, type: gqlType, args: filterFields, - resolve(a, args, context) { + async resolve(a, args, context) { + const path = context.path ? context.path : `` const runSift = require(`./run-sift`) let latestNodes if ( @@ -207,17 +208,27 @@ module.exports = async ({ parentSpan }) => { if (!_.isObject(args)) { args = {} } - return runSift({ + + const results = await runSift({ args: { filter: { ...args, }, }, nodes: latestNodes, - path: context.path ? context.path : ``, + firstOnly: true, typeName: typeName, type: gqlType, }) + + if (results.length > 0) { + const result = results[0] + const nodeId = result.id + createPageDependency({ path, nodeId }) + return result + } else { + return null + } }, }, } diff --git a/packages/gatsby/src/schema/run-sift.js b/packages/gatsby/src/schema/run-sift.js index 2206751b34041..035b77aac509f 100644 --- a/packages/gatsby/src/schema/run-sift.js +++ b/packages/gatsby/src/schema/run-sift.js @@ -1,8 +1,6 @@ // @flow const sift = require(`sift`) const _ = require(`lodash`) -const { connectionFromArray } = require(`graphql-skip-limit`) -const { createPageDependency } = require(`../redux/actions/add-page-dependency`) const prepareRegex = require(`./prepare-regex`) const Promise = require(`bluebird`) const { trackInlineObjectsInRootNode } = require(`./node-tracking`) @@ -38,18 +36,25 @@ function awaitSiftField(fields, node, k) { return undefined } -/* +/** * Filters a list of nodes using mongodb-like syntax. - * Returns a single unwrapped element if connection = false. * + * @param args raw graphql query filter as an object + * @param nodes The nodes array to run sift over + * @param type gqlType + * @param typeName + * @param firstOnly true if you want to return only the first result + * found. This will return a collection of size 1. Not a single + * element + * @returns Collection of results. Collection will be limited to size + * if `firstOnly` is true */ module.exports = ({ args, nodes, type, typeName, - connection = false, - path = ``, + firstOnly = false, }: Object) => { // Clone args as for some reason graphql-js removes the constructor // from nested objects which breaks a check in sift.js. @@ -169,28 +174,17 @@ module.exports = ({ // If the the query for single node only has a filter for an "id" // using "eq" operator, then we'll just grab that ID and return it. if ( - !connection && + firstOnly && Object.keys(fieldsToSift).length === 1 && Object.keys(fieldsToSift)[0] === `id` && Object.keys(siftArgs[0].id).length === 1 && Object.keys(siftArgs[0].id)[0] === `$eq` ) { - const nodePromise = resolveRecursive( + return resolveRecursive( getNode(siftArgs[0].id[`$eq`]), fieldsToSift, type.getFields() - ) - - nodePromise.then(node => { - if (node) { - createPageDependency({ - path, - nodeId: node.id, - }) - } - }) - - return nodePromise + ).then(node => (node ? [node] : [])) } const nodesPromise = () => { @@ -240,7 +234,7 @@ module.exports = ({ } } const tempPromise = nodesPromise().then(myNodes => { - if (!connection) { + if (firstOnly) { const index = _.isEmpty(siftArgs) ? 0 : sift.indexOf( @@ -250,51 +244,35 @@ module.exports = ({ myNodes ) - // If a node is found, create a dependency between the resulting node and - // the path. if (index !== -1) { - createPageDependency({ - path, - nodeId: myNodes[index].id, - }) - - return myNodes[index] + return [myNodes[index]] } else { - return null + return [] } - } - - let result = _.isEmpty(siftArgs) - ? myNodes - : sift( - { - $and: siftArgs, - }, - myNodes - ) - - if (!result || !result.length) return null + } else { + let result = _.isEmpty(siftArgs) + ? myNodes + : sift( + { + $and: siftArgs, + }, + myNodes + ) - // Sort results. - if (clonedArgs.sort) { - // create functions that return the item to compare on - // uses _.get so nested fields can be retrieved - const convertedFields = clonedArgs.sort.fields - .map(field => field.replace(/___/g, `.`)) - .map(field => v => _.get(v, field)) + if (!result || !result.length) return null - result = _.orderBy(result, convertedFields, clonedArgs.sort.order) - } + // Sort results. + if (clonedArgs.sort) { + // create functions that return the item to compare on + // uses _.get so nested fields can be retrieved + const convertedFields = clonedArgs.sort.fields + .map(field => field.replace(/___/g, `.`)) + .map(field => v => _.get(v, field)) - const connectionArray = connectionFromArray(result, args) - connectionArray.totalCount = result.length - if (result.length > 0 && result[0].internal) { - createPageDependency({ - path, - connection: result[0].internal.type, - }) + result = _.orderBy(result, convertedFields, clonedArgs.sort.order) + } + return result } - return connectionArray }) return tempPromise diff --git a/yarn.lock b/yarn.lock index 1febe6cb2d1b7..75c9464ba602d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2928,6 +2928,13 @@ babel-plugin-lodash@^3.2.11: lodash "^4.17.10" require-package-name "^2.0.1" +babel-plugin-macros@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.4.0.tgz#6c5f9836e1f6c0a9743b3bab4af29f73e437e544" + integrity sha512-flIBfrqAdHWn+4l2cS/4jZEyl+m5EaBHVzTb0aOF+eu/zR7E41/MoCFHPhDNL8Wzq1nyelnXeT+vcL2byFLSZw== + dependencies: + cosmiconfig "^5.0.5" + babel-plugin-macros@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.4.2.tgz#21b1a2e82e2130403c5ff785cba6548e9b644b28" @@ -11323,11 +11330,6 @@ json-stable-stringify@^1.0.0: dependencies: jsonify "~0.0.0" -json-stream-stringify@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/json-stream-stringify/-/json-stream-stringify-2.0.1.tgz#8bc0e65ff94567d9010e14c27c043a951cb14939" - integrity sha512-5XymtJXPmzRWZ1UdLQQQXbjHV/E7NAanSClikEqORbkZKOYLSYLNHqRuooyju9W90kJUzknFhX2xvWn4cHluHQ== - json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"