From c8db78fad43294413b950c49205adcf3dbb6dd1e Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Tue, 11 May 2021 17:41:22 +0200 Subject: [PATCH] fix(gatsby-plugin-mdx): enable hmr when importing mdx (#31288) * save MDXContent to different file * tmp: skip mdx loader unit tests * test(e2e-mdx): upgrade cypress, setup running dev * test(e2e-mdx): add hmr test case * only apply hmr workaround to develop stage * don't save mdx component to fs, use webpack tricks with query params * wait for hmr in mdx/develop * drop passthrough fs location * revert unneeded change * more reverts * revert devtool debugging change * adjust unit tests * add more e2e test - editing prop in markdown, editing component imported by mdx --- e2e-tests/mdx/cypress-dev.json | 6 + e2e-tests/mdx/cypress.json | 5 +- e2e-tests/mdx/cypress/integration/hmr.js | 71 ++ e2e-tests/mdx/package.json | 21 +- e2e-tests/mdx/scripts/history.js | 25 + e2e-tests/mdx/scripts/reset.js | 21 + e2e-tests/mdx/scripts/update.js | 103 +++ .../mdx/src/components/hmr-component-edit.js | 7 + e2e-tests/mdx/src/components/hmr-prop-edit.js | 7 + e2e-tests/mdx/src/pages/hmr.mdx | 8 + .../gatsby/create-webpack-config.js | 1 + .../__snapshots__/mdx-loader.test.js.snap | 722 ++++++++++++++++-- .../gatsby-plugin-mdx/loaders/mdx-loader.js | 23 + .../loaders/mdx-loader.test.js | 115 ++- packages/gatsby-plugin-mdx/utils/gen-mdx.js | 8 +- packages/gatsby/src/utils/webpack.config.js | 41 +- 16 files changed, 1057 insertions(+), 127 deletions(-) create mode 100644 e2e-tests/mdx/cypress-dev.json create mode 100644 e2e-tests/mdx/cypress/integration/hmr.js create mode 100644 e2e-tests/mdx/scripts/history.js create mode 100644 e2e-tests/mdx/scripts/reset.js create mode 100644 e2e-tests/mdx/scripts/update.js create mode 100644 e2e-tests/mdx/src/components/hmr-component-edit.js create mode 100644 e2e-tests/mdx/src/components/hmr-prop-edit.js create mode 100644 e2e-tests/mdx/src/pages/hmr.mdx diff --git a/e2e-tests/mdx/cypress-dev.json b/e2e-tests/mdx/cypress-dev.json new file mode 100644 index 0000000000000..89024688f2bea --- /dev/null +++ b/e2e-tests/mdx/cypress-dev.json @@ -0,0 +1,6 @@ +{ + "baseUrl": "http://localhost:8000", + "env": { + "GATSBY_COMMAND": "develop" + } +} diff --git a/e2e-tests/mdx/cypress.json b/e2e-tests/mdx/cypress.json index db2ba6ad5f8d9..42fda02830191 100644 --- a/e2e-tests/mdx/cypress.json +++ b/e2e-tests/mdx/cypress.json @@ -1,3 +1,6 @@ { - "baseUrl": "http://localhost:9000" + "baseUrl": "http://localhost:9000", + "env": { + "GATSBY_COMMAND": "build" + } } diff --git a/e2e-tests/mdx/cypress/integration/hmr.js b/e2e-tests/mdx/cypress/integration/hmr.js new file mode 100644 index 0000000000000..353184fc9ce44 --- /dev/null +++ b/e2e-tests/mdx/cypress/integration/hmr.js @@ -0,0 +1,71 @@ +if (Cypress.env("GATSBY_COMMAND") === `develop`) { + before(() => { + cy.exec(`npm run reset`) + }) + + after(() => { + cy.exec(`npm run reset`) + }) + + it(`Can hot-reload markdown content`, () => { + cy.visit(`/hmr`, { + onBeforeLoad: win => { + cy.spy(win.console, "log").as(`hmrConsoleLog`) + }, + }).waitForRouteChange() + cy.get(`h2`).invoke(`text`).should(`eq`, `Lorem`) + + cy.exec( + `npm run update -- --file src/pages/hmr.mdx --exact --replacements "Lorem:Ipsum"` + ) + + cy.get(`@hmrConsoleLog`).should(`be.calledWithMatch`, `App is up to date`) + cy.wait(1000) + + cy.get(`h2`).invoke(`text`).should(`eq`, `Ipsum`) + }) + + it(`Can hot-reload react content (i.e. change prop in mdx content)`, () => { + cy.visit(`/hmr`, { + onBeforeLoad: win => { + cy.spy(win.console, "log").as(`hmrConsoleLog`) + }, + }).waitForRouteChange() + cy.get(`[data-testid="test-prop-edit"]`) + .invoke(`text`) + .should(`eq`, `prop-before`) + + cy.exec( + `npm run update -- --file src/pages/hmr.mdx --exact --replacements "prop-before:prop-after"` + ) + + cy.get(`@hmrConsoleLog`).should(`be.calledWithMatch`, `App is up to date`) + cy.wait(1000) + + cy.get(`[data-testid="test-prop-edit"]`) + .invoke(`text`) + .should(`eq`, `prop-after`) + }) + + it(`Can hot-reload imported js components`, () => { + cy.visit(`/hmr`, { + onBeforeLoad: win => { + cy.spy(win.console, "log").as(`hmrConsoleLog`) + }, + }).waitForRouteChange() + cy.get(`[data-testid="test-imported-edit"]`) + .invoke(`text`) + .should(`eq`, `component-before`) + + cy.exec( + `npm run update -- --file src/components/hmr-component-edit.js --exact --replacements "component-before:component-after"` + ) + + cy.get(`@hmrConsoleLog`).should(`be.calledWithMatch`, `App is up to date`) + cy.wait(1000) + + cy.get(`[data-testid="test-imported-edit"]`) + .invoke(`text`) + .should(`eq`, `component-after`) + }) +} diff --git a/e2e-tests/mdx/package.json b/e2e-tests/mdx/package.json index fde0c84ad8827..5e103c197b723 100644 --- a/e2e-tests/mdx/package.json +++ b/e2e-tests/mdx/package.json @@ -5,7 +5,7 @@ "dependencies": { "@mdx-js/mdx": "^1.6.6", "@mdx-js/react": "^1.6.6", - "cypress": "^3.1.0", + "cypress": "^7.2.0", "fs-extra": "^8.1.0", "gatsby": "^3.0.0", "gatsby-plugin-mdx": "^2.0.0", @@ -19,14 +19,21 @@ ], "license": "MIT", "scripts": { - "build": "gatsby build", - "develop": "gatsby develop", + "build": "cross-env CYPRESS_SUPPORT=y gatsby build", + "develop": "cross-env CYPRESS_SUPPORT=y gatsby develop", "format": "prettier --write '**/*.js'", - "test": "cross-env CYPRESS_SUPPORT=y npm run build && npm run start-server-and-test", - "start-server-and-test": "start-server-and-test serve http://localhost:9000 cy:run", + "test:build": "cross-env CYPRESS_SUPPORT=y npm run build && npm run start-server-and-test:build", + "test:develop": "npm run start-server-and-test:develop || (npm run reset && exit 1)", + "test": "npm run test:build && npm run test:develop", + "start-server-and-test:develop": "start-server-and-test develop http://localhost:8000 cy:run:develop", + "start-server-and-test:build": "start-server-and-test serve http://localhost:9000 cy:run:build", "serve": "gatsby serve", - "cy:open": "cypress open", - "cy:run": "node ../../scripts/cypress-run-with-conditional-record-flag.js --browser chrome" + "cy:open:develop": "cypress open --config-file cypress-dev.json", + "cy:open:build": "cypress open", + "cy:run:build": "node ../../scripts/cypress-run-with-conditional-record-flag.js --browser chrome --group production", + "cy:run:develop": "node ../../scripts/cypress-run-with-conditional-record-flag.js --browser chrome --config-file cypress-dev.json --group development", + "reset": "node scripts/reset.js", + "update": "node scripts/update.js" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/e2e-tests/mdx/scripts/history.js b/e2e-tests/mdx/scripts/history.js new file mode 100644 index 0000000000000..2b6996deffe35 --- /dev/null +++ b/e2e-tests/mdx/scripts/history.js @@ -0,0 +1,25 @@ +const fs = require(`fs-extra`) + +const HISTORY_FILE = `__history__.json` + +exports.__HISTORY_FILE__ = HISTORY_FILE + +exports.getHistory = async (file = HISTORY_FILE) => { + try { + const contents = await fs + .readFile(file, `utf8`) + .then(contents => JSON.parse(contents)) + + return new Map(contents) + } catch (e) { + return new Map() + } +} + +exports.writeHistory = async (contents, file = HISTORY_FILE) => { + try { + await fs.writeFile(file, JSON.stringify([...contents]), `utf8`) + } catch (e) { + console.error(e) + } +} diff --git a/e2e-tests/mdx/scripts/reset.js b/e2e-tests/mdx/scripts/reset.js new file mode 100644 index 0000000000000..9aabb5ed7bb82 --- /dev/null +++ b/e2e-tests/mdx/scripts/reset.js @@ -0,0 +1,21 @@ +const fs = require(`fs-extra`) +const path = require(`path`) + +const { __HISTORY_FILE__, getHistory } = require(`./history`) + +async function reset() { + const history = await getHistory() + + await Promise.all( + Array.from(history).map(([filePath, value]) => { + if (typeof value === `string`) { + return fs.writeFile(path.resolve(filePath), value, `utf8`) + } + return fs.remove(path.resolve(filePath)) + }) + ) + + await fs.remove(__HISTORY_FILE__) +} + +reset() diff --git a/e2e-tests/mdx/scripts/update.js b/e2e-tests/mdx/scripts/update.js new file mode 100644 index 0000000000000..094fc0c111ee0 --- /dev/null +++ b/e2e-tests/mdx/scripts/update.js @@ -0,0 +1,103 @@ +const fs = require(`fs-extra`) +const path = require(`path`) +const yargs = require(`yargs`) + +const { getHistory, writeHistory } = require(`./history`) + +const args = yargs + .option(`file`, { + demand: true, + type: `string`, + }) + .option(`replacements`, { + default: [], + type: `array`, + }) + .option(`exact`, { + default: false, + type: `boolean`, + }) + .option(`delete`, { + default: false, + type: `boolean`, + }) + .option(`fileContent`, { + default: JSON.stringify( + ` + import * as React from 'react'; + + import Layout from '../components/layout'; + + export default function SomeComponent() { + return ( + +

Hello %REPLACEMENT%

+
+ ) + } + ` + ).trim(), + type: `string`, + }) + .option(`fileSource`, { + type: `string`, + }) + .option(`restore`, { + default: false, + type: `boolean`, + }).argv + +async function update() { + const history = await getHistory() + + const { file: fileArg, replacements, restore } = args + const filePath = path.resolve(fileArg) + if (restore) { + const original = history.get(filePath) + if (original) { + await fs.writeFile(filePath, original, `utf-8`) + } else if (original === false) { + await fs.remove(filePath) + } else { + console.log(`Didn't make changes to "${fileArg}". Nothing to restore.`) + } + history.delete(filePath) + return + } + let exists = true + if (!fs.existsSync(filePath)) { + exists = false + let fileContent + if (args.fileSource) { + fileContent = await fs.readFile(args.fileSource, `utf8`) + } else if (args.fileContent) { + fileContent = JSON.parse(args.fileContent).replace(/\+n/g, `\n`) + } + await fs.writeFile(filePath, fileContent, `utf8`) + } + const file = await fs.readFile(filePath, `utf8`) + + if (!history.has(filePath)) { + history.set(filePath, exists ? file : false) + } + + if (args.delete) { + if (exists) { + await fs.remove(filePath) + } + } else { + const contents = replacements.reduce((replaced, pair) => { + const [key, value] = pair.split(`:`) + return replaced.replace( + args.exact ? key : new RegExp(`%${key}%`, `g`), + value + ) + }, file) + + await fs.writeFile(filePath, contents, `utf8`) + } + + await writeHistory(history) +} + +update() diff --git a/e2e-tests/mdx/src/components/hmr-component-edit.js b/e2e-tests/mdx/src/components/hmr-component-edit.js new file mode 100644 index 0000000000000..c7a70d646a637 --- /dev/null +++ b/e2e-tests/mdx/src/components/hmr-component-edit.js @@ -0,0 +1,7 @@ +import React from "react" + +const HMRImportEditComponent = () => ( +
component-before
+) + +export default HMRImportEditComponent diff --git a/e2e-tests/mdx/src/components/hmr-prop-edit.js b/e2e-tests/mdx/src/components/hmr-prop-edit.js new file mode 100644 index 0000000000000..b67dd3de30a35 --- /dev/null +++ b/e2e-tests/mdx/src/components/hmr-prop-edit.js @@ -0,0 +1,7 @@ +import React from "react" + +const HMRPropEditComponent = ({ test }) => ( +
{test}
+) + +export default HMRPropEditComponent diff --git a/e2e-tests/mdx/src/pages/hmr.mdx b/e2e-tests/mdx/src/pages/hmr.mdx new file mode 100644 index 0000000000000..294f97e970378 --- /dev/null +++ b/e2e-tests/mdx/src/pages/hmr.mdx @@ -0,0 +1,8 @@ +import HMRImportEditComponent from "../components/hmr-component-edit" +import HMRPropEditComponent from "../components/hmr-prop-edit" + +## Lorem + + + + diff --git a/packages/gatsby-plugin-mdx/gatsby/create-webpack-config.js b/packages/gatsby-plugin-mdx/gatsby/create-webpack-config.js index fa441f8d38168..e08f154c58e70 100644 --- a/packages/gatsby-plugin-mdx/gatsby/create-webpack-config.js +++ b/packages/gatsby-plugin-mdx/gatsby/create-webpack-config.js @@ -76,6 +76,7 @@ module.exports = ( options: { cache: cache, actions: actions, + isolateMDXComponent: stage === `develop`, ...other, pluginOptions: options, }, diff --git a/packages/gatsby-plugin-mdx/loaders/__snapshots__/mdx-loader.test.js.snap b/packages/gatsby-plugin-mdx/loaders/__snapshots__/mdx-loader.test.js.snap index 772d412259508..a3640f04edc8d 100644 --- a/packages/gatsby-plugin-mdx/loaders/__snapshots__/mdx-loader.test.js.snap +++ b/packages/gatsby-plugin-mdx/loaders/__snapshots__/mdx-loader.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`mdx-loader snapshot [lessBabel=true] with body 1`] = ` +exports[`mdx-loader snapshot with body 1`] = ` import * as React from "react"; /* @jsx mdx */ import { mdx } from "@mdx-js/react"; @@ -29,7 +29,7 @@ MDXContent.isMDXComponent = true; `; -exports[`mdx-loader snapshot [lessBabel=true] with frontmatter 1`] = ` +exports[`mdx-loader snapshot with frontmatter 1`] = ` import * as React from "react"; /* @jsx mdx */ import { mdx } from "@mdx-js/react"; @@ -62,7 +62,77 @@ MDXContent.isMDXComponent = true; `; -exports[`mdx-loader snapshot [lessBabel=true] with frontmatter-layout 1`] = ` +exports[`mdx-loader snapshot with frontmatter-isDevelopStage 1`] = ` +import MDXContent from "/frontmatter-isDevelopStage?type=component"; +export default MDXContent; +export * from "/frontmatter-isDevelopStage?type=component"; + +export const _frontmatter = { one: "two", three: 4, array: [1, 2, 3] }; + +`; + +exports[`mdx-loader snapshot with frontmatter-isDevelopStage 2`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ + +const layoutProps = {}; +const MDXLayout = "wrapper"; +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with frontmatter-isDevelopStage-lessBabel 1`] = ` +import MDXContent from "/frontmatter-isDevelopStage-lessBabel?type=component"; +export default MDXContent; +export * from "/frontmatter-isDevelopStage-lessBabel?type=component"; + +export const _frontmatter = { one: "two", three: 4, array: [1, 2, 3] }; + +`; + +exports[`mdx-loader snapshot with frontmatter-isDevelopStage-lessBabel 2`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ + +const layoutProps = {}; +const MDXLayout = "wrapper"; +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with frontmatter-layout 1`] = ` import * as React from "react"; /* @jsx mdx */ import { mdx } from "@mdx-js/react"; @@ -97,22 +167,92 @@ MDXContent.isMDXComponent = true; `; -exports[`mdx-loader snapshot [lessBabel=true] with frontmatter-layout-namedExports 1`] = ` +exports[`mdx-loader snapshot with frontmatter-layout-isDevelopStage 1`] = ` +import MDXContent from "/frontmatter-layout-isDevelopStage?type=component"; +export default MDXContent; +export * from "/frontmatter-layout-isDevelopStage?type=component"; + +export const _frontmatter = { one: "two", three: 4, array: [1, 2, 3] }; + +`; + +exports[`mdx-loader snapshot with frontmatter-layout-isDevelopStage 2`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ + +const layoutProps = {}; + +const MDXLayout = ({ children, ...props }) =>
{children}
; + +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with frontmatter-layout-isDevelopStage-lessBabel 1`] = ` +import MDXContent from "/frontmatter-layout-isDevelopStage-lessBabel?type=component"; +export default MDXContent; +export * from "/frontmatter-layout-isDevelopStage-lessBabel?type=component"; + +export const _frontmatter = { one: "two", three: 4, array: [1, 2, 3] }; + +`; + +exports[`mdx-loader snapshot with frontmatter-layout-isDevelopStage-lessBabel 2`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ + +const layoutProps = {}; + +const MDXLayout = ({ children, ...props }) =>
{children}
; + +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with frontmatter-layout-lessBabel 1`] = ` import * as React from "react"; /* @jsx mdx */ import { mdx } from "@mdx-js/react"; /* @jsx mdx */ -export const meta = { - author: "chris", -}; export const _frontmatter = { one: "two", three: 4, array: [1, 2, 3], }; const layoutProps = { - meta, _frontmatter, }; @@ -136,7 +276,7 @@ MDXContent.isMDXComponent = true; `; -exports[`mdx-loader snapshot [lessBabel=true] with frontmatter-namedExports 1`] = ` +exports[`mdx-loader snapshot with frontmatter-layout-namedExports 1`] = ` import * as React from "react"; /* @jsx mdx */ import { mdx } from "@mdx-js/react"; @@ -154,7 +294,9 @@ const layoutProps = { meta, _frontmatter, }; -const MDXLayout = "wrapper"; + +const MDXLayout = ({ children, ...props }) =>
{children}
; + export default function MDXContent({ components, ...props }) { return (
{children}
; @@ -204,7 +357,16 @@ MDXContent.isMDXComponent = true; `; -exports[`mdx-loader snapshot [lessBabel=true] with layout-namedExports 1`] = ` +exports[`mdx-loader snapshot with frontmatter-layout-namedExports-isDevelopStage-lessBabel 1`] = ` +import MDXContent from "/frontmatter-layout-namedExports-isDevelopStage-lessBabel?type=component"; +export default MDXContent; +export * from "/frontmatter-layout-namedExports-isDevelopStage-lessBabel?type=component"; + +export const _frontmatter = { one: "two", three: 4, array: [1, 2, 3] }; + +`; + +exports[`mdx-loader snapshot with frontmatter-layout-namedExports-isDevelopStage-lessBabel 2`] = ` import * as React from "react"; /* @jsx mdx */ import { mdx } from "@mdx-js/react"; @@ -213,10 +375,8 @@ import { mdx } from "@mdx-js/react"; export const meta = { author: "chris", }; -export const _frontmatter = {}; const layoutProps = { meta, - _frontmatter, }; const MDXLayout = ({ children, ...props }) =>
{children}
; @@ -239,7 +399,7 @@ MDXContent.isMDXComponent = true; `; -exports[`mdx-loader snapshot [lessBabel=true] with namedExports 1`] = ` +exports[`mdx-loader snapshot with frontmatter-layout-namedExports-lessBabel 1`] = ` import * as React from "react"; /* @jsx mdx */ import { mdx } from "@mdx-js/react"; @@ -248,12 +408,18 @@ import { mdx } from "@mdx-js/react"; export const meta = { author: "chris", }; -export const _frontmatter = {}; +export const _frontmatter = { + one: "two", + three: 4, + array: [1, 2, 3], +}; const layoutProps = { meta, _frontmatter, }; -const MDXLayout = "wrapper"; + +const MDXLayout = ({ children, ...props }) =>
{children}
; + export default function MDXContent({ components, ...props }) { return (
{children}
; - +const MDXLayout = "wrapper"; export default function MDXContent({ components, ...props }) { return (
{children}
; - +const MDXLayout = "wrapper"; export default function MDXContent({ components, ...props }) { return ( +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with isDevelopStage-lessBabel 1`] = ` +import MDXContent from "/isDevelopStage-lessBabel?type=component"; +export default MDXContent; +export * from "/isDevelopStage-lessBabel?type=component"; + export const _frontmatter = {}; -const layoutProps = { - _frontmatter, -}; -const MDXLayout = ({ children, ...props }) =>
{children}
; +`; + +exports[`mdx-loader snapshot with isDevelopStage-lessBabel 2`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ +const layoutProps = {}; +const MDXLayout = "wrapper"; export default function MDXContent({ components, ...props }) { return (
{children}
; + +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with layout-isDevelopStage-lessBabel 1`] = ` +import MDXContent from "/layout-isDevelopStage-lessBabel?type=component"; +export default MDXContent; +export * from "/layout-isDevelopStage-lessBabel?type=component"; + +export const _frontmatter = {}; + +`; + +exports[`mdx-loader snapshot with layout-isDevelopStage-lessBabel 2`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ + +const layoutProps = {}; + +const MDXLayout = ({ children, ...props }) =>
{children}
; + +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with layout-lessBabel 1`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ + +export const _frontmatter = {}; +const layoutProps = { + _frontmatter, +}; + +const MDXLayout = ({ children, ...props }) =>
{children}
; + +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with layout-namedExports 1`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ + +export const meta = { + author: "chris", +}; +export const _frontmatter = {}; +const layoutProps = { + meta, + _frontmatter, +}; + +const MDXLayout = ({ children, ...props }) =>
{children}
; + +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with layout-namedExports-isDevelopStage 1`] = ` +import MDXContent from "/layout-namedExports-isDevelopStage?type=component"; +export default MDXContent; +export * from "/layout-namedExports-isDevelopStage?type=component"; + +export const _frontmatter = {}; + +`; + +exports[`mdx-loader snapshot with layout-namedExports-isDevelopStage 2`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ + +export const meta = { + author: "chris", +}; +const layoutProps = { + meta, +}; + +const MDXLayout = ({ children, ...props }) =>
{children}
; + +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with layout-namedExports-isDevelopStage-lessBabel 1`] = ` +import MDXContent from "/layout-namedExports-isDevelopStage-lessBabel?type=component"; +export default MDXContent; +export * from "/layout-namedExports-isDevelopStage-lessBabel?type=component"; + +export const _frontmatter = {}; + +`; + +exports[`mdx-loader snapshot with layout-namedExports-isDevelopStage-lessBabel 2`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ + +export const meta = { + author: "chris", +}; +const layoutProps = { + meta, +}; + +const MDXLayout = ({ children, ...props }) =>
{children}
; + +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with layout-namedExports-lessBabel 1`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ + +export const meta = { + author: "chris", +}; +export const _frontmatter = {}; +const layoutProps = { + meta, + _frontmatter, +}; + +const MDXLayout = ({ children, ...props }) =>
{children}
; + +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with lessBabel 1`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ + +export const _frontmatter = {}; +const layoutProps = { + _frontmatter, +}; +const MDXLayout = "wrapper"; +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with namedExports 1`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ + +export const meta = { + author: "chris", +}; +export const _frontmatter = {}; +const layoutProps = { + meta, + _frontmatter, +}; +const MDXLayout = "wrapper"; +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with namedExports-isDevelopStage 1`] = ` +import MDXContent from "/namedExports-isDevelopStage?type=component"; +export default MDXContent; +export * from "/namedExports-isDevelopStage?type=component"; + +export const _frontmatter = {}; + +`; + +exports[`mdx-loader snapshot with namedExports-isDevelopStage 2`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ + +export const meta = { + author: "chris", +}; +const layoutProps = { + meta, +}; +const MDXLayout = "wrapper"; +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with namedExports-isDevelopStage-lessBabel 1`] = ` +import MDXContent from "/namedExports-isDevelopStage-lessBabel?type=component"; +export default MDXContent; +export * from "/namedExports-isDevelopStage-lessBabel?type=component"; + +export const _frontmatter = {}; + +`; + +exports[`mdx-loader snapshot with namedExports-isDevelopStage-lessBabel 2`] = ` +import * as React from "react"; +/* @jsx mdx */ +import { mdx } from "@mdx-js/react"; +/* @jsx mdx */ + +export const meta = { + author: "chris", +}; +const layoutProps = { + meta, +}; +const MDXLayout = "wrapper"; +export default function MDXContent({ components, ...props }) { + return ( + +

{\`Some title\`}

+

{\`a bit of a paragraph\`}

+

{\`some content\`}

+
+ ); +} +MDXContent.isMDXComponent = true; + +`; + +exports[`mdx-loader snapshot with namedExports-lessBabel 1`] = ` import * as React from "react"; /* @jsx mdx */ import { mdx } from "@mdx-js/react"; diff --git a/packages/gatsby-plugin-mdx/loaders/mdx-loader.js b/packages/gatsby-plugin-mdx/loaders/mdx-loader.js index 9d9b9fb85b201..fdd574914b074 100644 --- a/packages/gatsby-plugin-mdx/loaders/mdx-loader.js +++ b/packages/gatsby-plugin-mdx/loaders/mdx-loader.js @@ -1,6 +1,7 @@ const _ = require(`lodash`) const { getOptions } = require(`loader-utils`) const grayMatter = require(`gray-matter`) +const path = require(`path`) const unified = require(`unified`) const babel = require(`@babel/core`) const { createRequireFromPath, slash } = require(`gatsby-core-utils`) @@ -95,7 +96,9 @@ const hasDefaultExport = (str, options) => { module.exports = async function mdxLoader(content) { const callback = this.async() + const { + isolateMDXComponent, getNode: rawGetNode, getNodes, getNodesByType, @@ -106,6 +109,25 @@ module.exports = async function mdxLoader(content) { ...helpers } = getOptions(this) + const resourceQuery = this.resourceQuery || `` + if (isolateMDXComponent && !resourceQuery.includes(`type=component`)) { + const { data } = grayMatter(content) + + const requestPath = `/${path.relative( + this.rootContext, + this.resourcePath + )}?type=component` + + return callback( + null, + `import MDXContent from "${requestPath}"; +export default MDXContent; +export * from "${requestPath}" + +export const _frontmatter = ${JSON.stringify(data)};` + ) + } + const options = withDefaultOptions(pluginOptions) let fileNode = getNodes().find( @@ -214,6 +236,7 @@ ${contentWithoutFrontmatter}` reporter, cache, pathPrefix, + isolateMDXComponent, }) try { diff --git a/packages/gatsby-plugin-mdx/loaders/mdx-loader.test.js b/packages/gatsby-plugin-mdx/loaders/mdx-loader.test.js index 9fcfda0f782d3..f6f72ee98ec48 100644 --- a/packages/gatsby-plugin-mdx/loaders/mdx-loader.test.js +++ b/packages/gatsby-plugin-mdx/loaders/mdx-loader.test.js @@ -34,16 +34,18 @@ some content`, input.namedExports ? code.namedExports : ``, code.body, ].join(`\n\n`), + isDevelopStage: input.isDevelopStage, + lessBabel: input.lessBabel, } } // generate a table of all possible combinations of genMDXfile input -const fixtures = new BaseN([true, false], 3) +const fixtures = new BaseN([true, false], 5) .toArray() - .map(([frontmatter, layout, namedExports]) => - genMDXFile({ frontmatter, layout, namedExports }) + .map(([frontmatter, layout, namedExports, isDevelopStage, lessBabel]) => + genMDXFile({ frontmatter, layout, namedExports, isDevelopStage, lessBabel }) ) - .map(({ name, content }) => [ + .map(({ name, content, isDevelopStage, lessBabel }) => [ name, { internal: { type: `File` }, @@ -51,6 +53,8 @@ const fixtures = new BaseN([true, false], 3) absolutePath: `/fake/${name}`, }, content, + isDevelopStage, + lessBabel, ]) describe(`mdx-loader`, () => { @@ -64,71 +68,56 @@ describe(`mdx-loader`, () => { }) test.each(fixtures)( `snapshot with %s`, - async (filename, fakeGatsbyNode, content) => { - const loader = mdxLoader.bind({ - async() { - return (err, result) => { - expect(err).toBeNull() - expect(result).toMatchSnapshot() - } - }, - query: { - getNodes(_type) { - return fixtures.map(([, node]) => node) - }, - getNodesByType(_type) { - return fixtures.map(([, node]) => node) - }, - pluginOptions: { - lessBabel: false, // default + async (filename, fakeGatsbyNode, content, isDevelopStage, lessBabel) => { + let err + let result + + const createLoader = (opts = {}) => + mdxLoader.bind({ + async() { + return (_err, _result) => { + err = _err + result = _result + } }, - cache: { - get() { - return false + query: { + getNodes(_type) { + return fixtures.map(([, node]) => node) }, - set() { - return + getNodesByType(_type) { + return fixtures.map(([, node]) => node) }, - }, - }, - resourcePath: fakeGatsbyNode.absolutePath, - }) - await loader(content) - } - ) - - test.each(fixtures)( - `snapshot [lessBabel=true] with %s`, - async (filename, fakeGatsbyNode, content) => { - const loader = mdxLoader.bind({ - async() { - return (err, result) => { - expect(err).toBeNull() - expect(result).toMatchSnapshot() - } - }, - query: { - getNodes(_type) { - return fixtures.map(([, node]) => node) - }, - getNodesByType(_type) { - return fixtures.map(([, node]) => node) - }, - pluginOptions: { - lessBabel: true, - }, - cache: { - get() { - return false + pluginOptions: { + lessBabel, }, - set() { - return + cache: { + get() { + return false + }, + set() { + return + }, }, + isolateMDXComponent: isDevelopStage, }, - }, - resourcePath: fakeGatsbyNode.absolutePath, - }) - await loader(content) + resourcePath: fakeGatsbyNode.absolutePath, + resourceQuery: fakeGatsbyNode.absolutePath, + rootContext: `/fake/`, + ...opts, + }) + + await createLoader()(content) + expect(err).toBeNull() + expect(result).toMatchSnapshot() + err = result = undefined + + if (isDevelopStage) { + await createLoader({ + resourceQuery: `${fakeGatsbyNode.absolutePath}?type=component`, + })(content) + expect(err).toBeNull() + expect(result).toMatchSnapshot() + } } ) }) diff --git a/packages/gatsby-plugin-mdx/utils/gen-mdx.js b/packages/gatsby-plugin-mdx/utils/gen-mdx.js index a991745f032a1..38ed9459eadc0 100644 --- a/packages/gatsby-plugin-mdx/utils/gen-mdx.js +++ b/packages/gatsby-plugin-mdx/utils/gen-mdx.js @@ -51,13 +51,14 @@ async function genMDX( reporter, cache, pathPrefix, + isolateMDXComponent, ...helpers }, { forceDisableCache = false } = {} ) { const pathPrefixCacheStr = pathPrefix || `` const payloadCacheKey = node => - `gatsby-plugin-mdx-entire-payload-${node.internal.contentDigest}-${pathPrefixCacheStr}` + `gatsby-plugin-mdx-entire-payload-${node.internal.contentDigest}-${pathPrefixCacheStr}-${isolateMDXComponent}` if (!forceDisableCache) { const cachedPayload = await cache.get(payloadCacheKey(node)) @@ -89,7 +90,10 @@ async function genMDX( // pull classic style frontmatter off the raw MDX body debug(`processing classic frontmatter`) const { data, content: frontMatterCodeResult } = grayMatter(node.rawBody) - const content = `${frontMatterCodeResult} + + const content = isolateMDXComponent + ? frontMatterCodeResult + : `${frontMatterCodeResult} export const _frontmatter = ${JSON.stringify(data)}` diff --git a/packages/gatsby/src/utils/webpack.config.js b/packages/gatsby/src/utils/webpack.config.js index 538442bd46b0d..034c2aeea5bd3 100644 --- a/packages/gatsby/src/utils/webpack.config.js +++ b/packages/gatsby/src/utils/webpack.config.js @@ -38,6 +38,7 @@ module.exports = async ( port, { parentSpan } = {} ) => { + let fastRefreshPlugin const modulesThatUseGatsby = await getGatsbyDependents() const directoryPath = withBasePath(directory) @@ -219,7 +220,7 @@ module.exports = async ( case `develop`: { configPlugins = configPlugins .concat([ - plugins.fastRefresh({ modulesThatUseGatsby }), + (fastRefreshPlugin = plugins.fastRefresh({ modulesThatUseGatsby })), new ForceCssHMRForEdgeCases(), plugins.hotModuleReplacement(), plugins.noEmitOnErrors(), @@ -810,5 +811,43 @@ module.exports = async ( parentSpan, }) + if (fastRefreshPlugin) { + // Fast refresh plugin has `include` option that determines + // wether HMR code gets injected. We need to make sure all custom loaders + // (like .ts or .mdx) that use our babel-loader will be taken into account + // when deciding which modules get fast-refresh HMR addition. + const fastRefreshIncludes = [] + const babelLoaderLoc = require.resolve(`./babel-loader`) + for (const rule of getConfig().module.rules) { + if (!rule.use) { + continue + } + + const hasBabelLoader = (Array.isArray(rule.use) + ? rule.use + : [rule.use] + ).some(loaderConfig => loaderConfig.loader === babelLoaderLoc) + + if (hasBabelLoader) { + fastRefreshIncludes.push(rule.test) + } + } + + // start with default include of fast refresh plugin + const includeRegex = /\.([jt]sx?|flow)$/i + includeRegex.test = modulePath => { + // drop query param from request (i.e. ?type=component for mdx-loader) + // so loader rule test work well + const queryParamStartIndex = modulePath.indexOf(`?`) + if (queryParamStartIndex !== -1) { + modulePath = modulePath.substr(0, queryParamStartIndex) + } + + return fastRefreshIncludes.some(re => re.test(modulePath)) + } + + fastRefreshPlugin.options.include = includeRegex + } + return getConfig() }