From 760845a2c27294ec80ac5dc636df74ca7c8f514f Mon Sep 17 00:00:00 2001 From: Peter van der Zee <209817+pvdz@users.noreply.github.com> Date: Wed, 8 Jul 2020 09:24:16 +0200 Subject: [PATCH] perf(gatsby-plugin-mdx): prevent babel parse step at sourcing time (#25437) * perf(gatsby-plugin-mdx): prevent babel parse step at sourcing time The mdx plugin was doing its default parsing step for every time it got called. At sourcing time it's only called to retrieve the import bindings and for this we can be much faster by manually processing the import statements. So that's what this does. A very simple flat no-image mdx benchmark cuts down sourcing time in half for this change. * Support multiple import statements together * Drop comments --- .../gatsby/on-create-node.js | 31 +- .../__snapshots__/import-parser.js.snap | 934 ++++++++++++++++++ .../utils/__tests__/import-parser.js | 279 ++++++ packages/gatsby-plugin-mdx/utils/gen-mdx.js | 83 +- .../gatsby-plugin-mdx/utils/import-parser.js | 49 + 5 files changed, 1358 insertions(+), 18 deletions(-) create mode 100644 packages/gatsby-plugin-mdx/utils/__tests__/__snapshots__/import-parser.js.snap create mode 100644 packages/gatsby-plugin-mdx/utils/__tests__/import-parser.js create mode 100644 packages/gatsby-plugin-mdx/utils/import-parser.js diff --git a/packages/gatsby-plugin-mdx/gatsby/on-create-node.js b/packages/gatsby-plugin-mdx/gatsby/on-create-node.js index beaf50f2496bc..b33d246c6f6d9 100644 --- a/packages/gatsby-plugin-mdx/gatsby/on-create-node.js +++ b/packages/gatsby-plugin-mdx/gatsby/on-create-node.js @@ -6,7 +6,7 @@ const { createContentDigest } = require(`gatsby-core-utils`) const defaultOptions = require(`../utils/default-options`) const createMDXNode = require(`../utils/create-mdx-node`) const { MDX_SCOPES_LOCATION } = require(`../constants`) -const genMDX = require(`../utils/gen-mdx`) +const { findImports } = require(`../utils/gen-mdx`) const contentDigest = val => createContentDigest(val) @@ -56,22 +56,19 @@ module.exports = async ( createParentChildLink({ parent: node, child: mdxNode }) // write scope files into .cache for later consumption - const { scopeImports, scopeIdentifiers } = await genMDX( - { - node: mdxNode, - getNode, - getNodes, - reporter, - cache, - pathPrefix, - options, - loadNodeContent, - actions, - createNodeId, - ...helpers, - }, - { forceDisableCache: true } - ) + const { scopeImports, scopeIdentifiers } = await findImports({ + node: mdxNode, + getNode, + getNodes, + reporter, + cache, + pathPrefix, + options, + loadNodeContent, + actions, + createNodeId, + ...helpers, + }) await cacheScope({ cache, scopeIdentifiers, diff --git a/packages/gatsby-plugin-mdx/utils/__tests__/__snapshots__/import-parser.js.snap b/packages/gatsby-plugin-mdx/utils/__tests__/__snapshots__/import-parser.js.snap new file mode 100644 index 0000000000000..95c6d73dfb034 --- /dev/null +++ b/packages/gatsby-plugin-mdx/utils/__tests__/__snapshots__/import-parser.js.snap @@ -0,0 +1,934 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`regex import scanner syntactic coverage should parse brute force regular case 0 1`] = ` +Object { + "input": "import foo from 'bar'", + "result": Object { + "bindings": Array [ + "foo", + ], + "segments": Array [ + "foo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 1 1`] = ` +Object { + "input": "import foo as bar from 'bar'", + "result": Object { + "bindings": Array [ + "bar", + ], + "segments": Array [ + "foo as bar", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 2 1`] = ` +Object { + "input": "import * as foo from 'bar'", + "result": Object { + "bindings": Array [ + "foo", + ], + "segments": Array [ + "* as foo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 3 1`] = ` +Object { + "input": "import {foo} from 'bar'", + "result": Object { + "bindings": Array [ + "foo", + ], + "segments": Array [ + "foo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 4 1`] = ` +Object { + "input": "import {foo as bar} from 'bar'", + "result": Object { + "bindings": Array [ + "bar", + ], + "segments": Array [ + "foo as bar", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 5 1`] = ` +Object { + "input": "import {foo, bar} from 'bar'", + "result": Object { + "bindings": Array [ + "foo", + "bar", + ], + "segments": Array [ + "foo", + "bar", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 6 1`] = ` +Object { + "input": "import {foo, bar as boo} from 'bar'", + "result": Object { + "bindings": Array [ + "foo", + "boo", + ], + "segments": Array [ + "foo", + "bar as boo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 7 1`] = ` +Object { + "input": "import {foo as bar} from 'bar'", + "result": Object { + "bindings": Array [ + "bar", + ], + "segments": Array [ + "foo as bar", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 8 1`] = ` +Object { + "input": "import {foo as bar, baz} from 'bar'", + "result": Object { + "bindings": Array [ + "bar", + "baz", + ], + "segments": Array [ + "foo as bar", + "baz", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 9 1`] = ` +Object { + "input": "import {foo as bar, baz as boo} from 'bar'", + "result": Object { + "bindings": Array [ + "bar", + "boo", + ], + "segments": Array [ + "foo as bar", + "baz as boo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 10 1`] = ` +Object { + "input": "import ding, {foo} from 'bar'", + "result": Object { + "bindings": Array [ + "ding", + "foo", + ], + "segments": Array [ + "ding", + "foo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 11 1`] = ` +Object { + "input": "import ding, {foo as bar} from 'bar'", + "result": Object { + "bindings": Array [ + "ding", + "bar", + ], + "segments": Array [ + "ding", + "foo as bar", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 12 1`] = ` +Object { + "input": "import ding, {foo, bar} from 'bar'", + "result": Object { + "bindings": Array [ + "ding", + "foo", + "bar", + ], + "segments": Array [ + "ding", + "foo", + "bar", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 13 1`] = ` +Object { + "input": "import ding, {foo, bar as boo} from 'bar'", + "result": Object { + "bindings": Array [ + "ding", + "foo", + "boo", + ], + "segments": Array [ + "ding", + "foo", + "bar as boo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 14 1`] = ` +Object { + "input": "import ding, {foo as bar} from 'bar'", + "result": Object { + "bindings": Array [ + "ding", + "bar", + ], + "segments": Array [ + "ding", + "foo as bar", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 15 1`] = ` +Object { + "input": "import ding, {foo as bar, baz} from 'bar'", + "result": Object { + "bindings": Array [ + "ding", + "bar", + "baz", + ], + "segments": Array [ + "ding", + "foo as bar", + "baz", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 16 1`] = ` +Object { + "input": "import ding, {foo as bar, baz as boo} from 'bar'", + "result": Object { + "bindings": Array [ + "ding", + "bar", + "boo", + ], + "segments": Array [ + "ding", + "foo as bar", + "baz as boo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 17 1`] = ` +Object { + "input": "import ding as dong, {foo} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "foo", + ], + "segments": Array [ + "ding as dong", + "foo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 18 1`] = ` +Object { + "input": "import ding as dong, {foo as bar} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "bar", + ], + "segments": Array [ + "ding as dong", + "foo as bar", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 19 1`] = ` +Object { + "input": "import ding as dong, {foo, bar} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "foo", + "bar", + ], + "segments": Array [ + "ding as dong", + "foo", + "bar", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 20 1`] = ` +Object { + "input": "import ding as dong, {foo, bar as boo} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "foo", + "boo", + ], + "segments": Array [ + "ding as dong", + "foo", + "bar as boo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 21 1`] = ` +Object { + "input": "import ding as dong, {foo as bar} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "bar", + ], + "segments": Array [ + "ding as dong", + "foo as bar", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 22 1`] = ` +Object { + "input": "import ding as dong, {foo as bar, baz} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "bar", + "baz", + ], + "segments": Array [ + "ding as dong", + "foo as bar", + "baz", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 23 1`] = ` +Object { + "input": "import ding as dong, {foo as bar, baz as boo} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "bar", + "boo", + ], + "segments": Array [ + "ding as dong", + "foo as bar", + "baz as boo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 24 1`] = ` +Object { + "input": "import * as dong, {foo} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "foo", + ], + "segments": Array [ + "* as dong", + "foo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 25 1`] = ` +Object { + "input": "import * as dong, {foo as bar} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "bar", + ], + "segments": Array [ + "* as dong", + "foo as bar", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 26 1`] = ` +Object { + "input": "import * as dong, {foo, bar} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "foo", + "bar", + ], + "segments": Array [ + "* as dong", + "foo", + "bar", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 27 1`] = ` +Object { + "input": "import * as dong, {foo, bar as boo} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "foo", + "boo", + ], + "segments": Array [ + "* as dong", + "foo", + "bar as boo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 28 1`] = ` +Object { + "input": "import * as dong, {foo as bar} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "bar", + ], + "segments": Array [ + "* as dong", + "foo as bar", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 29 1`] = ` +Object { + "input": "import * as dong, {foo as bar, baz} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "bar", + "baz", + ], + "segments": Array [ + "* as dong", + "foo as bar", + "baz", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 30 1`] = ` +Object { + "input": "import * as dong, {foo as bar, baz as boo} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "bar", + "boo", + ], + "segments": Array [ + "* as dong", + "foo as bar", + "baz as boo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 31 1`] = ` +Object { + "input": "import * as $, {_ as bar, baz as boo} from 'bar'", + "result": Object { + "bindings": Array [ + "$", + "bar", + "boo", + ], + "segments": Array [ + "* as $", + "_ as bar", + "baz as boo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 32 1`] = ` +Object { + "input": "import _, {foo as $} from 'bar'", + "result": Object { + "bindings": Array [ + "_", + "$", + ], + "segments": Array [ + "_", + "foo as $", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 33 1`] = ` +Object { + "input": "import _ as $ from 'bar'", + "result": Object { + "bindings": Array [ + "$", + ], + "segments": Array [ + "_ as $", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 34 1`] = ` +Object { + "input": "import {_, $ as boo} from 'bar'", + "result": Object { + "bindings": Array [ + "_", + "boo", + ], + "segments": Array [ + "_", + "$ as boo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 35 1`] = ` +Object { + "input": "import {_ as $, baz as boo} from 'bar'", + "result": Object { + "bindings": Array [ + "$", + "boo", + ], + "segments": Array [ + "_ as $", + "baz as boo", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 36 1`] = ` +Object { + "input": "import {_, $} from 'bar'", + "result": Object { + "bindings": Array [ + "_", + "$", + ], + "segments": Array [ + "_", + "$", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 37 1`] = ` +Object { + "input": "import as from 'bar'", + "result": Object { + "bindings": Array [ + "as", + ], + "segments": Array [ + "as", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 38 1`] = ` +Object { + "input": "import * as as from 'bar'", + "result": Object { + "bindings": Array [ + "as", + ], + "segments": Array [ + "* as as", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 39 1`] = ` +Object { + "input": "import from from 'bar'", + "result": Object { + "bindings": Array [ + "from", + ], + "segments": Array [ + "from", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 40 1`] = ` +Object { + "input": "import * as from from 'bar'", + "result": Object { + "bindings": Array [ + "from", + ], + "segments": Array [ + "* as from", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 41 1`] = ` +Object { + "input": "import as, {from} from 'bar'", + "result": Object { + "bindings": Array [ + "as", + "from", + ], + "segments": Array [ + "as", + "from", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 42 1`] = ` +Object { + "input": "import as as x, {from as y} from 'bar'", + "result": Object { + "bindings": Array [ + "x", + "y", + ], + "segments": Array [ + "as as x", + "from as y", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 43 1`] = ` +Object { + "input": "import x as as, {x as from} from 'bar'", + "result": Object { + "bindings": Array [ + "as", + "from", + ], + "segments": Array [ + "x as as", + "x as from", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 44 1`] = ` +Object { + "input": "import from, {as} from 'bar'", + "result": Object { + "bindings": Array [ + "from", + "as", + ], + "segments": Array [ + "from", + "as", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 45 1`] = ` +Object { + "input": "import from as x, {as as y} from 'bar'", + "result": Object { + "bindings": Array [ + "x", + "y", + ], + "segments": Array [ + "from as x", + "as as y", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 46 1`] = ` +Object { + "input": "import x as from, {x as as} from 'bar'", + "result": Object { + "bindings": Array [ + "from", + "as", + ], + "segments": Array [ + "x as from", + "x as as", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 47 1`] = ` +Object { + "input": "import {as, from} from 'bar'", + "result": Object { + "bindings": Array [ + "as", + "from", + ], + "segments": Array [ + "as", + "from", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 48 1`] = ` +Object { + "input": "import {from, as} from 'bar'", + "result": Object { + "bindings": Array [ + "from", + "as", + ], + "segments": Array [ + "from", + "as", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 49 1`] = ` +Object { + "input": "import {as as x, from as y} from 'bar'", + "result": Object { + "bindings": Array [ + "x", + "y", + ], + "segments": Array [ + "as as x", + "from as y", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 50 1`] = ` +Object { + "input": "import {from as x, as as y} from 'bar'", + "result": Object { + "bindings": Array [ + "x", + "y", + ], + "segments": Array [ + "from as x", + "as as y", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 51 1`] = ` +Object { + "input": "import {x as as, y as from} from 'bar'", + "result": Object { + "bindings": Array [ + "as", + "from", + ], + "segments": Array [ + "x as as", + "y as from", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 52 1`] = ` +Object { + "input": "import {x as from, y as as} from 'bar'", + "result": Object { + "bindings": Array [ + "from", + "as", + ], + "segments": Array [ + "x as from", + "y as as", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 53 1`] = ` +Object { + "input": "import {import as x} from 'bar'", + "result": Object { + "bindings": Array [ + "x", + ], + "segments": Array [ + "import as x", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 54 1`] = ` +Object { + "input": "import {import as x, y} from 'bar'", + "result": Object { + "bindings": Array [ + "x", + "y", + ], + "segments": Array [ + "import as x", + "y", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 55 1`] = ` +Object { + "input": "import {x, import as y} from 'bar'", + "result": Object { + "bindings": Array [ + "x", + "y", + ], + "segments": Array [ + "x", + "import as y", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 56 1`] = ` +Object { + "input": "import Events from \\"@components/events/events\\"", + "result": Object { + "bindings": Array [ + "Events", + ], + "segments": Array [ + "Events", + ], + }, +} +`; + +exports[`regex import scanner syntactic coverage should parse brute force regular case 57 1`] = ` +Object { + "input": "import multi as dong, {foo} from 'bar' +import as as x, {from as y} from 'bar'", + "result": Object { + "bindings": Array [ + "dong", + "foo", + "x", + "y", + ], + "segments": Array [ + "multi as dong", + "foo", + "as as x", + "from as y", + ], + }, +} +`; diff --git a/packages/gatsby-plugin-mdx/utils/__tests__/import-parser.js b/packages/gatsby-plugin-mdx/utils/__tests__/import-parser.js new file mode 100644 index 0000000000000..5c1861a64f0d9 --- /dev/null +++ b/packages/gatsby-plugin-mdx/utils/__tests__/import-parser.js @@ -0,0 +1,279 @@ +const { parseImportBindings } = require(`../import-parser`) +const grayMatter = require(`gray-matter`) +const mdx = require(`@mdx-js/mdx`) + +function getBruteForceCases() { + // These cases will be individually tested in four different ways; + // - as is + // - replace all spaces by newlines + // - minified (drop all spaces that are not mandatory) + // - replace all spaces by three spaces + + const bruteForceCases = ` + import foo from 'bar' + import foo as bar from 'bar' + import * as foo from 'bar' + import {foo} from 'bar' + import {foo as bar} from 'bar' + import {foo, bar} from 'bar' + import {foo, bar as boo} from 'bar' + import {foo as bar} from 'bar' + import {foo as bar, baz} from 'bar' + import {foo as bar, baz as boo} from 'bar' + import ding, {foo} from 'bar' + import ding, {foo as bar} from 'bar' + import ding, {foo, bar} from 'bar' + import ding, {foo, bar as boo} from 'bar' + import ding, {foo as bar} from 'bar' + import ding, {foo as bar, baz} from 'bar' + import ding, {foo as bar, baz as boo} from 'bar' + import ding as dong, {foo} from 'bar' + import ding as dong, {foo as bar} from 'bar' + import ding as dong, {foo, bar} from 'bar' + import ding as dong, {foo, bar as boo} from 'bar' + import ding as dong, {foo as bar} from 'bar' + import ding as dong, {foo as bar, baz} from 'bar' + import ding as dong, {foo as bar, baz as boo} from 'bar' + import * as dong, {foo} from 'bar' + import * as dong, {foo as bar} from 'bar' + import * as dong, {foo, bar} from 'bar' + import * as dong, {foo, bar as boo} from 'bar' + import * as dong, {foo as bar} from 'bar' + import * as dong, {foo as bar, baz} from 'bar' + import * as dong, {foo as bar, baz as boo} from 'bar' + import * as $, {_ as bar, baz as boo} from 'bar' + import _, {foo as $} from 'bar' + import _ as $ from 'bar' + import {_, $ as boo} from 'bar' + import {_ as $, baz as boo} from 'bar' + import {_, $} from 'bar' + import as from 'bar' + import * as as from 'bar' + import from from 'bar' + import * as from from 'bar' + import as, {from} from 'bar' + import as as x, {from as y} from 'bar' + import x as as, {x as from} from 'bar' + import from, {as} from 'bar' + import from as x, {as as y} from 'bar' + import x as from, {x as as} from 'bar' + import {as, from} from 'bar' + import {from, as} from 'bar' + import {as as x, from as y} from 'bar' + import {from as x, as as y} from 'bar' + import {x as as, y as from} from 'bar' + import {x as from, y as as} from 'bar' + import {import as x} from 'bar' + import {import as x, y} from 'bar' + import {x, import as y} from 'bar' + import Events from "@components/events/events" + ` + .trim() + .split(/\n/g) + .map(s => s.trim()) + + // Add double cases + bruteForceCases.push( + `import multi as dong, {foo} from 'bar'\nimport as as x, {from as y} from 'bar'` + ) + + return bruteForceCases +} + +describe(`regex import scanner`, () => { + describe(`syntactic coverage`, () => { + const cases = getBruteForceCases() + + cases.forEach((input, i) => { + it(`should parse brute force regular case ${i}`, () => { + const output = parseImportBindings(input, true) + const bindings = output.bindings + + expect(output.bindings.length).not.toBe(0) + // Note: putting everything in the snapshot makes reviews easier + expect({ input, result: output }).toMatchSnapshot() + expect( + // All bindings should be non-empty and trimmed + output.bindings.every( + binding => binding !== `` && binding === binding.trim() + ) + ).toBe(true) + + // Confirm that the parser works when all spaces become newlines + const newlined = input.replace(/ /g, `\n`) + expect(parseImportBindings(newlined)).toEqual(bindings) + + // Confirm that the parser works with a minimal amount of spacing + const minified = input.replace( + /(?<=[_\w$]) (?![_\w$])|(? { + it(`double import ends up as one pseudo-node in md parser`, async () => { + // Note: the point of this test is to have two back2back imports clustered + // as one pseudo-node in the ast. + + const { content } = grayMatter(` +--- +title: double test +--- + +import Events from "@components/events/events" +import EmailCaptureForm from "@components/email-capture-form" + + + `) + + const compiler = mdx.createCompiler() + const fileOpts = { contents: content } + const mdast = await compiler.parse(fileOpts) + + const imports = mdast.children.filter(obj => obj.type === `import`) + + // Assert the md parser outputs same mdast (update test if this changes) + expect( + imports.map(({ type, value }) => { + return { type, value } + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "type": "import", + "value": "import Events from \\"@components/events/events\\" + import EmailCaptureForm from \\"@components/email-capture-form\\"", + }, + ] + `) + + // Take the imports being parsed and feed them to the import parser + expect(parseImportBindings(imports[0].value, true)) + .toMatchInlineSnapshot(` + Object { + "bindings": Array [ + "Events", + "EmailCaptureForm", + ], + "segments": Array [ + "Events", + "EmailCaptureForm", + ], + } + `) + }) + + it(`triple imports without newlines`, async () => { + // Note: the point of this test is to have multiple back2back imports + // clustered as one pseudo-node in the ast. + + const { content } = grayMatter(` +--- +title: double test +--- + +import x, {frp, doo as dag} from "@components/events/events" +import * as EmailCaptureForm from "@components/email-capture-form" +import {A} from "@your/name" + + + `) + + const compiler = mdx.createCompiler() + const fileOpts = { contents: content } + const mdast = await compiler.parse(fileOpts) + + const imports = mdast.children.filter(obj => obj.type === `import`) + + // Assert the md parser outputs same mdast (update test if this changes) + expect( + imports.map(({ type, value }) => { + return { type, value } + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "type": "import", + "value": "import x, {frp, doo as dag} from \\"@components/events/events\\" + import * as EmailCaptureForm from \\"@components/email-capture-form\\" + import {A} from \\"@your/name\\"", + }, + ] + `) + + // Take the imports being parsed and feed them to the import parser + expect(parseImportBindings(imports[0].value, true)) + .toMatchInlineSnapshot(` + Object { + "bindings": Array [ + "x", + "frp", + "dag", + "EmailCaptureForm", + "A", + ], + "segments": Array [ + "x", + "frp", + "doo as dag", + "* as EmailCaptureForm", + "A", + ], + } + `) + }) + + it(`triple imports with newlines`, async () => { + // Note: the point of this test is to show that imports won't get + // clustered by the parser if there are empty lines between them + + const { content } = grayMatter(` +--- +title: double test +--- + +import Events from "@components/events/events" + +import EmailCaptureForm from "@components/email-capture-form" + +import {A} from "@your/name" + + + `) + + const compiler = mdx.createCompiler() + const fileOpts = { contents: content } + const mdast = await compiler.parse(fileOpts) + + const imports = mdast.children.filter(obj => obj.type === `import`) + + expect( + imports.map(({ type, value }) => { + return { type, value } + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "type": "import", + "value": "import Events from \\"@components/events/events\\"", + }, + Object { + "type": "import", + "value": "import EmailCaptureForm from \\"@components/email-capture-form\\"", + }, + Object { + "type": "import", + "value": "import {A} from \\"@your/name\\"", + }, + ] + `) + }) + }) +}) diff --git a/packages/gatsby-plugin-mdx/utils/gen-mdx.js b/packages/gatsby-plugin-mdx/utils/gen-mdx.js index 15acbc6911bc3..d35bd60dc02b4 100644 --- a/packages/gatsby-plugin-mdx/utils/gen-mdx.js +++ b/packages/gatsby-plugin-mdx/utils/gen-mdx.js @@ -9,6 +9,7 @@ const getSourcePluginsAsRemarkPlugins = require(`./get-source-plugins-as-remark- const htmlAttrToJSXAttr = require(`./babel-plugin-html-attr-to-jsx-attr`) const removeExportKeywords = require(`./babel-plugin-remove-export-keywords`) const BabelPluginPluckImports = require(`./babel-plugin-pluck-imports`) +const { parseImportBindings } = require(`./import-parser`) /* * function mutateNode({ @@ -39,7 +40,7 @@ const BabelPluginPluckImports = require(`./babel-plugin-pluck-imports`) * } * */ -module.exports = async function genMDX( +async function genMDX( { isLoader, node, @@ -189,3 +190,83 @@ ${code}` } return results } + +module.exports = genMDX // Legacy API, drop in v3 in favor of named export +module.exports.genMDX = genMDX + +async function findImports({ + node, + options, + getNode, + getNodes, + getNodesByType, + reporter, + cache, + pathPrefix, + ...helpers +}) { + const { content } = grayMatter(node.rawBody) + + const gatsbyRemarkPluginsAsremarkPlugins = await getSourcePluginsAsRemarkPlugins( + { + gatsbyRemarkPlugins: options.gatsbyRemarkPlugins, + mdxNode: node, + getNode, + getNodes, + getNodesByType, + reporter, + cache, + pathPrefix, + compiler: { + parseString: () => compiler.parse.bind(compiler), + generateHTML: ast => mdx(ast, options), + }, + ...helpers, + } + ) + + const compilerOptions = { + filepath: node.fileAbsolutePath, + ...options, + remarkPlugins: [ + ...options.remarkPlugins, + ...gatsbyRemarkPluginsAsremarkPlugins, + ], + } + const compiler = mdx.createCompiler(compilerOptions) + + const fileOpts = { contents: content } + if (node.fileAbsolutePath) { + fileOpts.path = node.fileAbsolutePath + } + + const mdast = await compiler.parse(fileOpts) + + // Assuming valid code, identifiers must be unique (they are consts) so + // we don't need to dedupe the symbols here. + const identifiers = [] + const imports = [] + + mdast.children.forEach(node => { + if (node.type !== `import`) return + + const importCode = node.value + + imports.push(importCode) + + const bindings = parseImportBindings(importCode) + identifiers.push(...bindings) + }) + + if (!identifiers.includes(`React`)) { + identifiers.push(`React`) + imports.push(`import * as React from 'react'`) + } + + return { + scopeImports: imports, + scopeIdentifiers: identifiers, + } +} + +module.exports.findImports = findImports diff --git a/packages/gatsby-plugin-mdx/utils/import-parser.js b/packages/gatsby-plugin-mdx/utils/import-parser.js new file mode 100644 index 0000000000000..10702efb09d68 --- /dev/null +++ b/packages/gatsby-plugin-mdx/utils/import-parser.js @@ -0,0 +1,49 @@ +/** + * Parse source code containing (just) ES6 import declarations and return the + * names of all bindings created by such a declaration. + * The function will assume strict ES6 import code. + * The input may contain multiple import statements, each starting on a new line + * First it strips the irrelevant bits (like `import`, curly brackets, and + * `from` tail. What's left ought to be a string in the form of + * `id[ as id] [, id[ as id]]` + * (where the brackets represent optional repeating parts). The left-most id + * might also contain star (namespaced import). + * The second part will trim and split the string on comma, then each segment + * is split on `as` (in a proper way) and the right-most identifier is returned. + * + * For testing purposes you can also request the segments, after split on comma. + * + * @param {string} importCode + * @param {boolean=false} returnSegments + * @returns {Array | {bindings: Array, segments: Array}} + */ +function parseImportBindings(importCode, returnSegments = false) { + const str = importCode.replace( + /^\s*import|[{},]|\s*from\s*['"][^'"]*?['"]\s*$/gm, + ` , ` + ) + const segments = str + .trim() + .split(/\s*,\s*/g) + .filter(s => s !== ``) + const bindings = segments.map( + segment => + // `s` is either an ident (the binding), or `a as b` where `b` is the + // binding. We split on `as` (taking spacing edge cases into account) + // and return the right-most ident that is left, trimmed. + // If `as` is used, it must be followed by some kind of spacing. + // Notable edge case: `*as as` is legit, namespace importing to var `as` + segment.split(/as\s+(?=[\w\d$_]+$)/).pop() + // Note: since `s` was trimmed, and the split consumed any spacing after + // the `as`, the result ident must be trimmed. + ) + if (returnSegments) { + // For snapshot testing + return { bindings, segments } + } + return bindings +} + +module.exports = { + parseImportBindings, +}