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()
}