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, +}