diff --git a/.circleci/config.yml b/.circleci/config.yml
index a7dfda5324fd1..13fa2674d6003 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -306,6 +306,13 @@ jobs:
test_path: integration-tests/head-function-export
test_command: yarn test
+ integration_tests_esm_in_gatsby_files:
+ executor: node
+ steps:
+ - e2e-test:
+ test_path: integration-tests/esm-in-gatsby-files
+ test_command: yarn test
+
e2e_tests_path-prefix:
<<: *e2e-executor
environment:
@@ -592,6 +599,8 @@ workflows:
<<: *e2e-test-workflow
- integration_tests_head_function_export:
<<: *e2e-test-workflow
+ - integration_tests_esm_in_gatsby_files:
+ <<: *e2e-test-workflow
- integration_tests_gatsby_cli:
requires:
- bootstrap
diff --git a/e2e-tests/mdx/cypress/integration/using-esm-only-remark-rehype-plugins.js b/e2e-tests/mdx/cypress/integration/using-esm-only-remark-rehype-plugins.js
new file mode 100644
index 0000000000000..708b3ea538f20
--- /dev/null
+++ b/e2e-tests/mdx/cypress/integration/using-esm-only-remark-rehype-plugins.js
@@ -0,0 +1,15 @@
+describe(`ESM only Rehype & Remark plugins`, () => {
+ describe("Remark Plugin", () => {
+ it(`transforms to github-like checkbox list`, () => {
+ cy.visit(`/using-esm-only-rehype-remark-plugins/`).waitForRouteChange()
+ cy.get(`.task-list-item`).should("have.length", 2)
+ })
+ })
+
+ describe("Rehype Plugin", () => {
+ it(`use heading text as id `, () => {
+ cy.visit(`/using-esm-only-rehype-remark-plugins/`).waitForRouteChange()
+ cy.get(`#heading-two`).invoke(`text`).should(`eq`, `Heading two`)
+ })
+ })
+})
diff --git a/e2e-tests/mdx/gatsby-config.js b/e2e-tests/mdx/gatsby-config.mjs
similarity index 66%
rename from e2e-tests/mdx/gatsby-config.js
rename to e2e-tests/mdx/gatsby-config.mjs
index 20690d9594df7..a51e422cabfd1 100644
--- a/e2e-tests/mdx/gatsby-config.js
+++ b/e2e-tests/mdx/gatsby-config.mjs
@@ -1,4 +1,11 @@
-module.exports = {
+import remarkGfm from "remark-gfm"
+import rehypeSlug from "rehype-slug"
+import { dirname } from "path"
+import { fileURLToPath } from "url"
+
+const __dirname = dirname(fileURLToPath(import.meta.url))
+
+const config = {
siteMetadata: {
title: `Gatsby MDX e2e`,
},
@@ -28,7 +35,16 @@ module.exports = {
`gatsby-remark-autolink-headers`,
],
mdxOptions: {
- remarkPlugins: [remarkRequireFilePathPlugin],
+ remarkPlugins: [
+ remarkRequireFilePathPlugin,
+ // This is an esm only packages, It should work out of the box
+ remarkGfm,
+ ],
+
+ rehypePlugins: [
+ // This is an esm only packages, It should work out of the box
+ rehypeSlug,
+ ],
},
},
},
@@ -47,3 +63,5 @@ function remarkRequireFilePathPlugin() {
}
}
}
+
+export default config
diff --git a/e2e-tests/mdx/package.json b/e2e-tests/mdx/package.json
index 8ac2e303ab078..5ce04b582b85b 100644
--- a/e2e-tests/mdx/package.json
+++ b/e2e-tests/mdx/package.json
@@ -16,6 +16,8 @@
"gatsby-source-filesystem": "next",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "rehype-slug": "^5.1.0",
+ "remark-gfm": "^3.0.1",
"theme-ui": "^0.3.1"
},
"keywords": [
diff --git a/e2e-tests/mdx/src/pages/using-esm-only-rehype-remark-plugins.mdx b/e2e-tests/mdx/src/pages/using-esm-only-rehype-remark-plugins.mdx
new file mode 100644
index 0000000000000..607c0285b54fb
--- /dev/null
+++ b/e2e-tests/mdx/src/pages/using-esm-only-rehype-remark-plugins.mdx
@@ -0,0 +1,8 @@
+---
+title: Using esm only rehype & rehype plugins
+---
+
+## Heading two
+
+- [ ] A
+- [x] B
diff --git a/integration-tests/esm-in-gatsby-files/.gitignore b/integration-tests/esm-in-gatsby-files/.gitignore
new file mode 100644
index 0000000000000..61c008bc45207
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/.gitignore
@@ -0,0 +1,8 @@
+__tests__/__debug__
+node_modules
+yarn.lock
+
+# These are removed and created during the test lifecycle
+./gatsby-config.js
+./gatsby-config.mjs
+./gatsby-config.ts
\ No newline at end of file
diff --git a/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-config.js b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-config.js
new file mode 100644
index 0000000000000..6a02dcb45acc1
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-config.js
@@ -0,0 +1,13 @@
+// This fixture is moved during the test lifecycle
+
+const helloDefaultCJS = require(`./cjs-default.js`)
+const { helloNamedCJS } = require(`./cjs-named.js`)
+
+helloDefaultCJS()
+helloNamedCJS()
+
+const config = {
+ plugins: [],
+}
+
+module.exports = config
\ No newline at end of file
diff --git a/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-config.mjs b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-config.mjs
new file mode 100644
index 0000000000000..1fefdb3b4552f
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-config.mjs
@@ -0,0 +1,21 @@
+// This fixture is moved during the test lifecycle
+
+import slugify from "@sindresorhus/slugify";
+import helloDefaultESM from "./esm-default.mjs"
+import { helloNamedESM } from "./esm-named.mjs"
+
+helloDefaultESM()
+helloNamedESM()
+
+const config = {
+ plugins: [
+ {
+ resolve: `a-local-plugin`,
+ options: {
+ slugify,
+ },
+ },
+ ],
+}
+
+export default config
\ No newline at end of file
diff --git a/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-node-engine-bundled.mjs b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-node-engine-bundled.mjs
new file mode 100644
index 0000000000000..97a673e52030a
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-node-engine-bundled.mjs
@@ -0,0 +1,12 @@
+const createResolvers = ({ createResolvers }) => {
+ createResolvers({
+ Query: {
+ fieldAddedByESMPlugin: {
+ type: `String`,
+ resolve: () => `gatsby-node-engine-bundled-mjs`
+ }
+ }
+ })
+}
+
+export { createResolvers }
\ No newline at end of file
diff --git a/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-node.js b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-node.js
new file mode 100644
index 0000000000000..61fe76542d8eb
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-node.js
@@ -0,0 +1,11 @@
+// This fixture is moved during the test lifecycle
+
+const helloDefaultCJS = require(`./cjs-default.js`)
+const { helloNamedCJS } = require(`./cjs-named.js`)
+
+helloDefaultCJS()
+helloNamedCJS()
+
+exports.onPreBuild = () => {
+ console.info(`gatsby-node-cjs-on-pre-build`);
+};
diff --git a/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-node.mjs b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-node.mjs
new file mode 100644
index 0000000000000..24a62a96a7c09
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/gatsby-node.mjs
@@ -0,0 +1,11 @@
+// This fixture is moved during the test lifecycle
+
+import helloDefaultESM from "./esm-default.mjs"
+import { helloNamedESM } from "./esm-named.mjs"
+
+helloDefaultESM()
+helloNamedESM()
+
+export const onPreBuild = () => {
+ console.info(`gatsby-node-esm-on-pre-build`);
+};
diff --git a/integration-tests/esm-in-gatsby-files/__tests__/fixtures/pages/ssr.js b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/pages/ssr.js
new file mode 100644
index 0000000000000..65a8c7bb24f10
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/pages/ssr.js
@@ -0,0 +1,22 @@
+import React from "react"
+import { graphql } from "gatsby"
+
+function SSRPage({ data, serverData }) {
+ return
{JSON.stringify({ data, serverData }, null, 2)}
+}
+
+export const query = graphql`
+ {
+ fieldAddedByESMPlugin
+ }
+`
+
+export const getServerData = () => {
+ return {
+ props: {
+ ssr: true,
+ },
+ }
+}
+
+export default SSRPage
diff --git a/integration-tests/esm-in-gatsby-files/__tests__/fixtures/plugins/a-local-plugin/gatsby-config.mjs b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/plugins/a-local-plugin/gatsby-config.mjs
new file mode 100644
index 0000000000000..8a55b80d425c1
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/plugins/a-local-plugin/gatsby-config.mjs
@@ -0,0 +1,9 @@
+// This fixture is moved during the test lifecycle
+
+console.info(`a-local-plugin-gatsby-config-mjs`)
+
+const config = {
+ plugins: [],
+}
+
+export default config
\ No newline at end of file
diff --git a/integration-tests/esm-in-gatsby-files/__tests__/fixtures/plugins/a-local-plugin/gatsby-node.mjs b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/plugins/a-local-plugin/gatsby-node.mjs
new file mode 100644
index 0000000000000..5872e2c0501df
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/plugins/a-local-plugin/gatsby-node.mjs
@@ -0,0 +1,3 @@
+export const onPreBuild = (_, { slugify }) => {
+ console.info(slugify(`a local plugin using passed esm module`));
+};
diff --git a/integration-tests/esm-in-gatsby-files/__tests__/fixtures/plugins/a-local-plugin/index.js b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/plugins/a-local-plugin/index.js
new file mode 100644
index 0000000000000..625c0891b2c30
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/plugins/a-local-plugin/index.js
@@ -0,0 +1 @@
+// noop
\ No newline at end of file
diff --git a/integration-tests/esm-in-gatsby-files/__tests__/fixtures/plugins/a-local-plugin/package.json b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/plugins/a-local-plugin/package.json
new file mode 100644
index 0000000000000..2f211daf3667e
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/__tests__/fixtures/plugins/a-local-plugin/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "a-local-plugin",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC"
+}
diff --git a/integration-tests/esm-in-gatsby-files/__tests__/gatsby-config.test.js b/integration-tests/esm-in-gatsby-files/__tests__/gatsby-config.test.js
new file mode 100644
index 0000000000000..c835de09a8d09
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/__tests__/gatsby-config.test.js
@@ -0,0 +1,84 @@
+import path from "path"
+import fs from "fs-extra"
+import execa from "execa"
+
+jest.setTimeout(100000)
+
+const fixtureRoot = path.resolve(__dirname, `fixtures`)
+const siteRoot = path.resolve(__dirname, `..`)
+
+const fixturePath = {
+ cjs: path.join(fixtureRoot, `gatsby-config.js`),
+ esm: path.join(fixtureRoot, `gatsby-config.mjs`),
+}
+
+const configPath = {
+ cjs: path.join(siteRoot, `gatsby-config.js`),
+ esm: path.join(siteRoot, `gatsby-config.mjs`),
+}
+
+const localPluginFixtureDir = path.join(fixtureRoot, `plugins`)
+const localPluginTargetDir = path.join(siteRoot, `plugins`)
+
+const gatsbyBin = path.join(`node_modules`, `gatsby`, `cli.js`)
+
+async function build() {
+ const { stdout } = await execa(process.execPath, [gatsbyBin, `build`], {
+ env: {
+ ...process.env,
+ NODE_ENV: `production`,
+ },
+ })
+
+ return stdout
+}
+
+// Tests include multiple assertions since running multiple builds is time consuming
+
+describe(`gatsby-config.js`, () => {
+ afterEach(() => {
+ fs.rmSync(configPath.cjs)
+ })
+
+ it(`works with required CJS modules`, async () => {
+ await fs.copyFile(fixturePath.cjs, configPath.cjs)
+
+ const stdout = await build()
+
+ // Build succeeded
+ expect(stdout).toContain(`Done building`)
+
+ // Requires work
+ expect(stdout).toContain(`hello-default-cjs`)
+ expect(stdout).toContain(`hello-named-cjs`)
+ })
+})
+
+describe(`gatsby-config.mjs`, () => {
+ afterEach(async () => {
+ await fs.rm(configPath.esm)
+ await fs.rm(localPluginTargetDir, { recursive: true })
+ })
+
+ it(`works with imported ESM modules`, async () => {
+ await fs.copyFile(fixturePath.esm, configPath.esm)
+
+ await fs.ensureDir(localPluginTargetDir)
+ await fs.copy(localPluginFixtureDir, localPluginTargetDir)
+
+ const stdout = await build()
+
+ // Build succeeded
+ expect(stdout).toContain(`Done building`)
+
+ // Imports work
+ expect(stdout).toContain(`hello-default-esm`)
+ expect(stdout).toContain(`hello-named-esm`)
+
+ // Local plugin gatsby-config.mjs works
+ expect(stdout).toContain(`a-local-plugin-gatsby-config-mjs`)
+
+ // Local plugin with an esm module passed via options works, this implicitly tests gatsby-node.mjs too
+ expect(stdout).toContain(`a-local-plugin-using-passed-esm-module`)
+ })
+})
diff --git a/integration-tests/esm-in-gatsby-files/__tests__/gatsby-node.test.js b/integration-tests/esm-in-gatsby-files/__tests__/gatsby-node.test.js
new file mode 100644
index 0000000000000..0c2aba7ac1d24
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/__tests__/gatsby-node.test.js
@@ -0,0 +1,151 @@
+const path = require(`path`)
+const fs = require(`fs-extra`)
+const execa = require(`execa`)
+const { spawn } = require(`child_process`)
+const fetch = require(`node-fetch`)
+
+jest.setTimeout(100000)
+
+const fixtureRoot = path.resolve(__dirname, `fixtures`)
+const siteRoot = path.resolve(__dirname, `..`)
+
+const fixturePath = {
+ gatsbyNodeCjs: path.join(fixtureRoot, `gatsby-node.js`),
+ gatsbyNodeEsm: path.join(fixtureRoot, `gatsby-node.mjs`),
+ gatsbyNodeEsmBundled: path.join(
+ fixtureRoot,
+ `gatsby-node-engine-bundled.mjs`
+ ),
+ ssrPage: path.join(fixtureRoot, `pages`, `ssr.js`),
+}
+
+const targetPath = {
+ gatsbyNodeCjs: path.join(siteRoot, `gatsby-node.js`),
+ gatsbyNodeEsm: path.join(siteRoot, `gatsby-node.mjs`),
+ ssrPage: path.join(siteRoot, `src`, `pages`, `ssr.js`),
+}
+
+const gatsbyBin = path.join(`node_modules`, `gatsby`, `cli.js`)
+
+async function build() {
+ const { stdout } = await execa(process.execPath, [gatsbyBin, `build`], {
+ env: {
+ ...process.env,
+ NODE_ENV: `production`,
+ },
+ })
+
+ return stdout
+}
+
+async function serve() {
+ const serveProcess = spawn(process.execPath, [gatsbyBin, `serve`], {
+ env: {
+ ...process.env,
+ NODE_ENV: `production`,
+ },
+ })
+
+ await waitForServeReady(serveProcess)
+
+ return serveProcess
+}
+
+function wait(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms))
+}
+
+async function waitForServeReady(serveProcess, retries = 0) {
+ if (retries > 10) {
+ console.error(
+ `Server for esm-in-gatsby-files > gatsby-node.test.js failed to get ready after 10 tries`
+ )
+ serveProcess.kill()
+ }
+
+ await wait(500)
+
+ let ready = false
+
+ try {
+ const { ok } = await fetch(`http://localhost:9000/`)
+ ready = ok
+ } catch (_) {
+ // Do nothing
+ }
+
+ if (!ready) {
+ retries++
+ return waitForServeReady(serveProcess, retries)
+ }
+}
+
+// Tests include multiple assertions since running multiple builds is time consuming
+
+describe(`gatsby-node.js`, () => {
+ afterEach(() => {
+ fs.rmSync(targetPath.gatsbyNodeCjs)
+ })
+
+ it(`works with required CJS modules`, async () => {
+ await fs.copyFile(fixturePath.gatsbyNodeCjs, targetPath.gatsbyNodeCjs)
+
+ const stdout = await build()
+
+ // Build succeeded
+ expect(stdout).toContain(`Done building`)
+
+ // Requires work
+ expect(stdout).toContain(`hello-default-cjs`)
+ expect(stdout).toContain(`hello-named-cjs`)
+
+ // Node API works
+ expect(stdout).toContain(`gatsby-node-cjs-on-pre-build`)
+ })
+})
+
+describe(`gatsby-node.mjs`, () => {
+ afterEach(async () => {
+ await fs.rm(targetPath.gatsbyNodeEsm)
+ if (fs.existsSync(targetPath.ssrPage)) {
+ await fs.rm(targetPath.ssrPage)
+ }
+ })
+
+ it(`works with imported ESM modules`, async () => {
+ await fs.copyFile(fixturePath.gatsbyNodeEsm, targetPath.gatsbyNodeEsm)
+
+ const stdout = await build()
+
+ // Build succeeded
+ expect(stdout).toContain(`Done building`)
+
+ // Imports work
+ expect(stdout).toContain(`hello-default-esm`)
+ expect(stdout).toContain(`hello-named-esm`)
+
+ // Node API works
+ expect(stdout).toContain(`gatsby-node-esm-on-pre-build`)
+ })
+
+ it(`works when bundled in engines`, async () => {
+ await fs.copyFile(
+ fixturePath.gatsbyNodeEsmBundled,
+ targetPath.gatsbyNodeEsm
+ )
+ // Need to copy this because other runs fail on unfound query node type if the page is left in src
+ await fs.copyFile(fixturePath.ssrPage, targetPath.ssrPage)
+
+ const buildStdout = await build()
+ const serveProcess = await serve()
+ const response = await fetch(`http://localhost:9000/ssr/`)
+ const html = await response.text()
+ serveProcess.kill()
+
+ // Build succeeded
+ expect(buildStdout).toContain(`Done building`)
+
+ // Engine bundling works
+ expect(html).toContain(`gatsby-node-engine-bundled-mjs`)
+ })
+})
diff --git a/integration-tests/esm-in-gatsby-files/cjs-default.js b/integration-tests/esm-in-gatsby-files/cjs-default.js
new file mode 100644
index 0000000000000..d164627b7ca9a
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/cjs-default.js
@@ -0,0 +1,5 @@
+function helloDefaultCJS() {
+ console.info(`hello-default-cjs`)
+}
+
+module.exports = helloDefaultCJS
diff --git a/integration-tests/esm-in-gatsby-files/cjs-named.js b/integration-tests/esm-in-gatsby-files/cjs-named.js
new file mode 100644
index 0000000000000..49a68202a716b
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/cjs-named.js
@@ -0,0 +1,7 @@
+function helloNamedCJS() {
+ console.info(`hello-named-cjs`)
+}
+
+module.exports = {
+ helloNamedCJS,
+}
diff --git a/integration-tests/esm-in-gatsby-files/esm-default.mjs b/integration-tests/esm-in-gatsby-files/esm-default.mjs
new file mode 100644
index 0000000000000..fdf53bdd32539
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/esm-default.mjs
@@ -0,0 +1,5 @@
+function helloDefaultESM() {
+ console.info(`hello-default-esm`)
+}
+
+export default helloDefaultESM
diff --git a/integration-tests/esm-in-gatsby-files/esm-named.mjs b/integration-tests/esm-in-gatsby-files/esm-named.mjs
new file mode 100644
index 0000000000000..f3d30a9a52174
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/esm-named.mjs
@@ -0,0 +1,5 @@
+function helloNamedESM() {
+ console.info(`hello-named-esm`)
+}
+
+export { helloNamedESM }
diff --git a/integration-tests/esm-in-gatsby-files/jest-transformer.js b/integration-tests/esm-in-gatsby-files/jest-transformer.js
new file mode 100644
index 0000000000000..02167a152534c
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/jest-transformer.js
@@ -0,0 +1,5 @@
+const babelJest = require(`babel-jest`)
+
+module.exports = babelJest.default.createTransformer({
+ presets: [`babel-preset-gatsby-package`],
+})
diff --git a/integration-tests/esm-in-gatsby-files/jest.config.js b/integration-tests/esm-in-gatsby-files/jest.config.js
new file mode 100644
index 0000000000000..613531f9107df
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/jest.config.js
@@ -0,0 +1,14 @@
+module.exports = {
+ testPathIgnorePatterns: [
+ `/node_modules/`,
+ `.cache`,
+ `public`,
+ `/__tests__/fixtures/`,
+ `gatsby-config.js`,
+ `gatsby-config.mjs`,
+ `gatsby-config.ts`,
+ ],
+ transform: {
+ "^.+\\.[jt]sx?$": `./jest-transformer.js`,
+ },
+}
diff --git a/integration-tests/esm-in-gatsby-files/package.json b/integration-tests/esm-in-gatsby-files/package.json
new file mode 100644
index 0000000000000..3bb2320f48f58
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/package.json
@@ -0,0 +1,27 @@
+{
+ "private": true,
+ "name": "esm-in-gatsby-files-integration-test",
+ "version": "1.0.0",
+ "scripts": {
+ "clean": "gatsby clean",
+ "build": "gatsby build",
+ "serve": "gatsby serve",
+ "test": "jest --runInBand"
+ },
+ "dependencies": {
+ "gatsby": "next",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "devDependencies": {
+ "@types/node": "^18.11.9",
+ "@types/react": "^18.0.25",
+ "@types/react-dom": "^18.0.8",
+ "babel-preset-gatsby-package": "^2.4.0",
+ "execa": "^4.0.1",
+ "fs-extra": "^10.1.0",
+ "jest": "^27.2.1",
+ "node-fetch": "^2.6.0",
+ "typescript": "^4.8.4"
+ }
+}
diff --git a/integration-tests/esm-in-gatsby-files/src/pages/index.js b/integration-tests/esm-in-gatsby-files/src/pages/index.js
new file mode 100644
index 0000000000000..625d90f6a4511
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/src/pages/index.js
@@ -0,0 +1,7 @@
+import * as React from "react"
+
+function IndexPage() {
+ return Index page
+}
+
+export default IndexPage
diff --git a/integration-tests/esm-in-gatsby-files/tsconfig.json b/integration-tests/esm-in-gatsby-files/tsconfig.json
new file mode 100644
index 0000000000000..15128c8efd05b
--- /dev/null
+++ b/integration-tests/esm-in-gatsby-files/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "lib": ["dom", "esnext"],
+ "jsx": "react",
+ "module": "esnext",
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "skipLibCheck": true
+ },
+ "include": [
+ "./src/**/*",
+ "./gatsby-node.ts",
+ "./gatsby-config.ts",
+ "./plugins/**/*"
+ ]
+}
diff --git a/jest.config.js b/jest.config.js
index 24daa567e0961..5ca27e4a58357 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -124,7 +124,7 @@ module.exports = {
],
transformIgnorePatterns: [`/node_modules/(?!${esModules})`],
transform: {
- "^.+\\.[jt]sx?$": `/jest-transformer.js`,
+ "^.+\\.(jsx|js|mjs|ts|tsx)$": `/jest-transformer.js`,
},
moduleNameMapper: {
"^highlight.js$": `/node_modules/highlight.js/lib/index.js`,
diff --git a/packages/gatsby/babel.config.js b/packages/gatsby/babel.config.js
index 563246ebc554e..3d14ea9341573 100644
--- a/packages/gatsby/babel.config.js
+++ b/packages/gatsby/babel.config.js
@@ -4,6 +4,13 @@
module.exports = {
sourceMaps: true,
presets: [["babel-preset-gatsby-package", {
- keepDynamicImports: [`./src/utils/feedback.ts`]
+ keepDynamicImports: [
+ `./src/utils/feedback.ts`,
+
+ // These files use dynamic imports to load gatsby-config and gatsby-node so esm works
+ `./src/bootstrap/get-config-file.ts`,
+ `./src/bootstrap/resolve-module-exports.ts`,
+ `./src/utils/import-gatsby-plugin.ts`
+ ]
}]],
}
diff --git a/packages/gatsby/cache-dir/develop-static-entry.js b/packages/gatsby/cache-dir/develop-static-entry.js
index 78c04d8e11b4d..77a1cd9357a1f 100644
--- a/packages/gatsby/cache-dir/develop-static-entry.js
+++ b/packages/gatsby/cache-dir/develop-static-entry.js
@@ -5,10 +5,6 @@ import { merge } from "lodash"
import { apiRunner } from "./api-runner-ssr"
import asyncRequires from "$virtual/async-requires"
-// import testRequireError from "./test-require-error"
-// For some extremely mysterious reason, webpack adds the above module *after*
-// this module so that when this code runs, testRequireError is undefined.
-// So in the meantime, we'll just inline it.
const testRequireError = (moduleName, err) => {
const regex = new RegExp(`Error: Cannot find module\\s.${moduleName}`)
const firstLine = err.toString().split(`\n`)[0]
diff --git a/packages/gatsby/cache-dir/ssr-develop-static-entry.js b/packages/gatsby/cache-dir/ssr-develop-static-entry.js
index fc3988fde5bee..3b472acd3c700 100644
--- a/packages/gatsby/cache-dir/ssr-develop-static-entry.js
+++ b/packages/gatsby/cache-dir/ssr-develop-static-entry.js
@@ -16,10 +16,6 @@ import { getStaticQueryResults } from "./loader"
// prefer default export if available
const preferDefault = m => (m && m.default) || m
-// import testRequireError from "./test-require-error"
-// For some extremely mysterious reason, webpack adds the above module *after*
-// this module so that when this code runs, testRequireError is undefined.
-// So in the meantime, we'll just inline it.
const testRequireError = (moduleName, err) => {
const regex = new RegExp(`Error: Cannot find module\\s.${moduleName}`)
const firstLine = err.toString().split(`\n`)[0]
diff --git a/packages/gatsby/cache-dir/static-entry.js b/packages/gatsby/cache-dir/static-entry.js
index 475170c202652..f6674f978bdcb 100644
--- a/packages/gatsby/cache-dir/static-entry.js
+++ b/packages/gatsby/cache-dir/static-entry.js
@@ -28,10 +28,6 @@ const { ServerSliceRenderer } = require(`./slice/server-slice-renderer`)
// we want to force posix-style joins, so Windows doesn't produce backslashes for urls
const { join } = path.posix
-// const testRequireError = require("./test-require-error")
-// For some extremely mysterious reason, webpack adds the above module *after*
-// this module so that when this code runs, testRequireError is undefined.
-// So in the meantime, we'll just inline it.
const testRequireError = (moduleName, err) => {
const regex = new RegExp(`Error: Cannot find module\\s.${moduleName}`)
const firstLine = err.toString().split(`\n`)[0]
diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/compiled-dir/compiled/gatsby-config.js b/packages/gatsby/src/bootstrap/__mocks__/get-config/cjs/compiled/compiled/gatsby-config.js
similarity index 100%
rename from packages/gatsby/src/bootstrap/__mocks__/get-config/compiled-dir/compiled/gatsby-config.js
rename to packages/gatsby/src/bootstrap/__mocks__/get-config/cjs/compiled/compiled/gatsby-config.js
diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/gatsby-config.js b/packages/gatsby/src/bootstrap/__mocks__/get-config/cjs/gatsby-config.js
similarity index 100%
rename from packages/gatsby/src/bootstrap/__mocks__/get-config/gatsby-config.js
rename to packages/gatsby/src/bootstrap/__mocks__/get-config/cjs/gatsby-config.js
diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/near-match-dir/gatsby-confi.js b/packages/gatsby/src/bootstrap/__mocks__/get-config/cjs/near-match/gatsby-confi.js
similarity index 100%
rename from packages/gatsby/src/bootstrap/__mocks__/get-config/near-match-dir/gatsby-confi.js
rename to packages/gatsby/src/bootstrap/__mocks__/get-config/cjs/near-match/gatsby-confi.js
diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/src-dir/src/gatsby-config.js b/packages/gatsby/src/bootstrap/__mocks__/get-config/cjs/src/src/gatsby-config.js
similarity index 100%
rename from packages/gatsby/src/bootstrap/__mocks__/get-config/src-dir/src/gatsby-config.js
rename to packages/gatsby/src/bootstrap/__mocks__/get-config/cjs/src/src/gatsby-config.js
diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/user-require-dir/compiled/gatsby-config.js b/packages/gatsby/src/bootstrap/__mocks__/get-config/cjs/user-require-compiled/compiled/gatsby-config.js
similarity index 100%
rename from packages/gatsby/src/bootstrap/__mocks__/get-config/user-require-dir/compiled/gatsby-config.js
rename to packages/gatsby/src/bootstrap/__mocks__/get-config/cjs/user-require-compiled/compiled/gatsby-config.js
diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/cjs/user-require/gatsby-config.js b/packages/gatsby/src/bootstrap/__mocks__/get-config/cjs/user-require/gatsby-config.js
new file mode 100644
index 0000000000000..ea1e68c533fb5
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/__mocks__/get-config/cjs/user-require/gatsby-config.js
@@ -0,0 +1,9 @@
+const something = require(`some-place-that-does-not-exist`)
+
+module.exports = {
+ siteMetadata: {
+ title: `user-require-error`,
+ siteUrl: `https://www.yourdomain.tld`,
+ },
+ plugins: [],
+}
diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/esm/gatsby-config.mjs b/packages/gatsby/src/bootstrap/__mocks__/get-config/esm/gatsby-config.mjs
new file mode 100644
index 0000000000000..0465574572a2c
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/__mocks__/get-config/esm/gatsby-config.mjs
@@ -0,0 +1,9 @@
+const config = {
+ siteMetadata: {
+ title: `uncompiled`,
+ siteUrl: `https://www.yourdomain.tld`,
+ },
+ plugins: [],
+}
+
+export default config
diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/esm/near-match/gatsby-confi.mjs b/packages/gatsby/src/bootstrap/__mocks__/get-config/esm/near-match/gatsby-confi.mjs
new file mode 100644
index 0000000000000..52998f353f01f
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/__mocks__/get-config/esm/near-match/gatsby-confi.mjs
@@ -0,0 +1,9 @@
+const config = {
+ siteMetadata: {
+ title: `near-match`,
+ siteUrl: `https://www.yourdomain.tld`,
+ },
+ plugins: [],
+}
+
+export default config
diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/esm/src/src/gatsby-config.mjs b/packages/gatsby/src/bootstrap/__mocks__/get-config/esm/src/src/gatsby-config.mjs
new file mode 100644
index 0000000000000..47b20dd9dfc28
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/__mocks__/get-config/esm/src/src/gatsby-config.mjs
@@ -0,0 +1,9 @@
+const config = {
+ siteMetadata: {
+ title: `in-src`,
+ siteUrl: `https://www.yourdomain.tld`,
+ },
+ plugins: [],
+}
+
+export default config
diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/esm/user-import/gatsby-config.mjs b/packages/gatsby/src/bootstrap/__mocks__/get-config/esm/user-import/gatsby-config.mjs
new file mode 100644
index 0000000000000..aa5a484f9418c
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/__mocks__/get-config/esm/user-import/gatsby-config.mjs
@@ -0,0 +1,11 @@
+import something from "some-place-that-does-not-exist"
+
+const config = {
+ siteMetadata: {
+ title: `user-import-error`,
+ siteUrl: `https://www.yourdomain.tld`,
+ },
+ plugins: [],
+}
+
+export default config
diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/ts-dir/gatsby-config.ts b/packages/gatsby/src/bootstrap/__mocks__/get-config/ts/gatsby-config.ts
similarity index 100%
rename from packages/gatsby/src/bootstrap/__mocks__/get-config/ts-dir/gatsby-config.ts
rename to packages/gatsby/src/bootstrap/__mocks__/get-config/ts/gatsby-config.ts
diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/tsx-dir/gatsby-confi.tsx b/packages/gatsby/src/bootstrap/__mocks__/get-config/tsx/gatsby-confi.tsx
similarity index 100%
rename from packages/gatsby/src/bootstrap/__mocks__/get-config/tsx-dir/gatsby-confi.tsx
rename to packages/gatsby/src/bootstrap/__mocks__/get-config/tsx/gatsby-confi.tsx
diff --git a/packages/gatsby/src/bootstrap/__mocks__/require/exports.js b/packages/gatsby/src/bootstrap/__mocks__/import/exports.js
similarity index 100%
rename from packages/gatsby/src/bootstrap/__mocks__/require/exports.js
rename to packages/gatsby/src/bootstrap/__mocks__/import/exports.js
diff --git a/packages/gatsby/src/bootstrap/__mocks__/require/module-error.js b/packages/gatsby/src/bootstrap/__mocks__/import/module-error.js
similarity index 100%
rename from packages/gatsby/src/bootstrap/__mocks__/require/module-error.js
rename to packages/gatsby/src/bootstrap/__mocks__/import/module-error.js
diff --git a/packages/gatsby/src/bootstrap/__mocks__/require/unusual-exports.js b/packages/gatsby/src/bootstrap/__mocks__/import/unusual-exports.js
similarity index 100%
rename from packages/gatsby/src/bootstrap/__mocks__/require/unusual-exports.js
rename to packages/gatsby/src/bootstrap/__mocks__/import/unusual-exports.js
diff --git a/packages/gatsby/src/bootstrap/__mocks__/resolve-js-file-path/both/gatsby-config.js b/packages/gatsby/src/bootstrap/__mocks__/resolve-js-file-path/both/gatsby-config.js
new file mode 100644
index 0000000000000..5ae66ab282a51
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/__mocks__/resolve-js-file-path/both/gatsby-config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ plugins: [],
+}
diff --git a/packages/gatsby/src/bootstrap/__mocks__/resolve-js-file-path/both/gatsby-config.mjs b/packages/gatsby/src/bootstrap/__mocks__/resolve-js-file-path/both/gatsby-config.mjs
new file mode 100644
index 0000000000000..7963522537abd
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/__mocks__/resolve-js-file-path/both/gatsby-config.mjs
@@ -0,0 +1,5 @@
+const config = {
+ plugins: [],
+}
+
+export default config
diff --git a/packages/gatsby/src/bootstrap/__mocks__/resolve-js-file-path/cjs/gatsby-config.js b/packages/gatsby/src/bootstrap/__mocks__/resolve-js-file-path/cjs/gatsby-config.js
new file mode 100644
index 0000000000000..5ae66ab282a51
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/__mocks__/resolve-js-file-path/cjs/gatsby-config.js
@@ -0,0 +1,3 @@
+module.exports = {
+ plugins: [],
+}
diff --git a/packages/gatsby/src/bootstrap/__mocks__/resolve-js-file-path/esm/gatsby-config.mjs b/packages/gatsby/src/bootstrap/__mocks__/resolve-js-file-path/esm/gatsby-config.mjs
new file mode 100644
index 0000000000000..7963522537abd
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/__mocks__/resolve-js-file-path/esm/gatsby-config.mjs
@@ -0,0 +1,5 @@
+const config = {
+ plugins: [],
+}
+
+export default config
diff --git a/packages/gatsby/src/bootstrap/__tests__/get-config-file.ts b/packages/gatsby/src/bootstrap/__tests__/get-config-file.ts
index 59d9b1e6d3ac1..6783e847e84d1 100644
--- a/packages/gatsby/src/bootstrap/__tests__/get-config-file.ts
+++ b/packages/gatsby/src/bootstrap/__tests__/get-config-file.ts
@@ -1,6 +1,5 @@
import path from "path"
import { getConfigFile } from "../get-config-file"
-import { testRequireError } from "../../utils/test-require-error"
import reporter from "gatsby-cli/lib/reporter"
jest.mock(`path`, () => {
@@ -11,9 +10,9 @@ jest.mock(`path`, () => {
}
})
-jest.mock(`../../utils/test-require-error`, () => {
+jest.mock(`../../utils/test-import-error`, () => {
return {
- testRequireError: jest.fn(),
+ testImportError: jest.fn(),
}
})
@@ -33,50 +32,59 @@ jest.mock(`gatsby-cli/lib/reporter`, () => {
const pathJoinMock = path.join as jest.MockedFunction
-const testRequireErrorMock = testRequireError as jest.MockedFunction<
- typeof testRequireError
->
-
const reporterPanicMock = reporter.panic as jest.MockedFunction<
typeof reporter.panic
>
// Separate config directories so cases can be tested separately
-const dir = path.resolve(__dirname, `../__mocks__/get-config`)
-const compiledDir = `${dir}/compiled-dir`
-const userRequireDir = `${dir}/user-require-dir`
-const tsDir = `${dir}/ts-dir`
-const tsxDir = `${dir}/tsx-dir`
-const nearMatchDir = `${dir}/near-match-dir`
-const srcDir = `${dir}/src-dir`
-
-describe(`getConfigFile`, () => {
+const baseDir = path.resolve(__dirname, `..`, `__mocks__`, `get-config`)
+const cjsDir = path.join(baseDir, `cjs`)
+const esmDir = path.join(baseDir, `esm`)
+
+const configDir = {
+ cjs: {
+ compiled: path.join(cjsDir, `compiled`),
+ userRequireCompiled: path.join(cjsDir, `user-require-compiled`),
+ userRequire: path.join(cjsDir, `user-require`),
+ nearMatch: path.join(cjsDir, `near-match`),
+ src: path.join(cjsDir, `src`),
+ },
+ esm: {
+ userImport: path.join(esmDir, `user-import`),
+ nearMatch: path.join(esmDir, `near-match`),
+ src: path.join(esmDir, `src`),
+ },
+ ts: path.join(baseDir, `ts`),
+ tsx: path.join(baseDir, `tsx`),
+}
+
+describe(`getConfigFile with cjs files`, () => {
beforeEach(() => {
reporterPanicMock.mockClear()
})
it(`should get an uncompiled gatsby-config.js`, async () => {
const { configModule, configFilePath } = await getConfigFile(
- dir,
+ cjsDir,
`gatsby-config`
)
- expect(configFilePath).toBe(path.join(dir, `gatsby-config.js`))
+ expect(configFilePath).toBe(path.join(cjsDir, `gatsby-config.js`))
expect(configModule.siteMetadata.title).toBe(`uncompiled`)
})
it(`should get a compiled gatsby-config.js`, async () => {
const { configModule, configFilePath } = await getConfigFile(
- compiledDir,
+ configDir.cjs.compiled,
`gatsby-config`
)
expect(configFilePath).toBe(
- path.join(compiledDir, `compiled`, `gatsby-config.js`)
+ path.join(configDir.cjs.compiled, `compiled`, `gatsby-config.js`)
)
expect(configModule.siteMetadata.title).toBe(`compiled`)
})
it(`should handle user require errors found in compiled gatsby-config.js`, async () => {
- await getConfigFile(userRequireDir, `gatsby-config`)
+ await getConfigFile(configDir.cjs.userRequireCompiled, `gatsby-config`)
expect(reporterPanicMock).toBeCalledWith({
id: `11902`,
@@ -88,10 +96,8 @@ describe(`getConfigFile`, () => {
})
})
- it(`should handle non-require errors`, async () => {
- testRequireErrorMock.mockImplementationOnce(() => false)
-
- await getConfigFile(nearMatchDir, `gatsby-config`)
+ it(`should handle user require errors found in uncompiled gatsby-config.js`, async () => {
+ await getConfigFile(configDir.cjs.userRequire, `gatsby-config`)
expect(reporterPanicMock).toBeCalledWith({
id: `10123`,
@@ -103,28 +109,61 @@ describe(`getConfigFile`, () => {
})
})
- it(`should handle case where gatsby-config.ts exists but no compiled gatsby-config.js exists`, async () => {
- // Force outer and inner errors so we can hit the code path that checks if gatsby-config.ts exists
- pathJoinMock
- .mockImplementationOnce(() => `force-outer-error`)
- .mockImplementationOnce(() => `force-inner-error`)
- testRequireErrorMock.mockImplementationOnce(() => true)
+ it(`should handle near matches`, async () => {
+ await getConfigFile(configDir.cjs.nearMatch, `gatsby-config`)
+
+ expect(reporterPanicMock).toBeCalledWith({
+ id: `10124`,
+ error: expect.toBeObject(),
+ context: {
+ configName: `gatsby-config`,
+ isTSX: false,
+ nearMatch: `gatsby-confi.js`,
+ },
+ })
+ })
- await getConfigFile(tsDir, `gatsby-config`)
+ it(`should handle gatsby config incorrectly located in src dir`, async () => {
+ await getConfigFile(configDir.cjs.src, `gatsby-config`)
expect(reporterPanicMock).toBeCalledWith({
- id: `10127`,
+ id: `10125`,
+ context: {
+ configName: `gatsby-config`,
+ },
+ })
+ })
+})
+
+describe(`getConfigFile with esm files`, () => {
+ beforeEach(() => {
+ reporterPanicMock.mockClear()
+ })
+
+ it(`should get an uncompiled gatsby-config.mjs`, async () => {
+ const { configModule, configFilePath } = await getConfigFile(
+ esmDir,
+ `gatsby-config`
+ )
+ expect(configFilePath).toBe(path.join(esmDir, `gatsby-config.mjs`))
+ expect(configModule.siteMetadata.title).toBe(`uncompiled`)
+ })
+
+ it(`should handle user require errors found in uncompiled gatsby-config.mjs`, async () => {
+ await getConfigFile(configDir.esm.userImport, `gatsby-config`)
+
+ expect(reporterPanicMock).toBeCalledWith({
+ id: `10123`,
error: expect.toBeObject(),
context: {
configName: `gatsby-config`,
+ message: expect.toBeString(),
},
})
})
it(`should handle near matches`, async () => {
- testRequireErrorMock.mockImplementationOnce(() => true)
-
- await getConfigFile(nearMatchDir, `gatsby-config`)
+ await getConfigFile(configDir.esm.nearMatch, `gatsby-config`)
expect(reporterPanicMock).toBeCalledWith({
id: `10124`,
@@ -132,36 +171,51 @@ describe(`getConfigFile`, () => {
context: {
configName: `gatsby-config`,
isTSX: false,
- nearMatch: `gatsby-confi.js`,
+ nearMatch: `gatsby-confi.mjs`,
},
})
})
- it(`should handle .tsx extension`, async () => {
- testRequireErrorMock.mockImplementationOnce(() => true)
+ it(`should handle gatsby config incorrectly located in src dir`, async () => {
+ await getConfigFile(configDir.esm.src, `gatsby-config`)
- await getConfigFile(tsxDir, `gatsby-config`)
+ expect(reporterPanicMock).toBeCalledWith({
+ id: `10125`,
+ context: {
+ configName: `gatsby-config`,
+ },
+ })
+ })
+})
+
+describe(`getConfigFile with ts/tsx files`, () => {
+ it(`should handle case where gatsby-config.ts exists but no compiled gatsby-config.js exists`, async () => {
+ // Force outer and inner errors so we can hit the code path that checks if gatsby-config.ts exists
+ pathJoinMock
+ .mockImplementationOnce(() => `force-outer-error`)
+ .mockImplementationOnce(() => `force-inner-error`)
+
+ await getConfigFile(configDir.ts, `gatsby-config`)
expect(reporterPanicMock).toBeCalledWith({
- id: `10124`,
+ id: `10127`,
error: expect.toBeObject(),
context: {
configName: `gatsby-config`,
- isTSX: true,
- nearMatch: `gatsby-confi.tsx`,
},
})
})
- it(`should handle gatsby config incorrectly located in src dir`, async () => {
- testRequireErrorMock.mockImplementationOnce(() => true)
-
- await getConfigFile(srcDir, `gatsby-config`)
+ it(`should handle .tsx extension`, async () => {
+ await getConfigFile(configDir.tsx, `gatsby-config`)
expect(reporterPanicMock).toBeCalledWith({
- id: `10125`,
+ id: `10124`,
+ error: expect.toBeObject(),
context: {
configName: `gatsby-config`,
+ isTSX: true,
+ nearMatch: `gatsby-confi.tsx`,
},
})
})
diff --git a/packages/gatsby/src/bootstrap/__tests__/resolve-js-file-path.ts b/packages/gatsby/src/bootstrap/__tests__/resolve-js-file-path.ts
new file mode 100644
index 0000000000000..d494a7347279e
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/__tests__/resolve-js-file-path.ts
@@ -0,0 +1,93 @@
+import path from "path"
+import reporter from "gatsby-cli/lib/reporter"
+import { resolveJSFilepath } from "../resolve-js-file-path"
+
+const mockDir = path.resolve(
+ __dirname,
+ `..`,
+ `__mocks__`,
+ `resolve-js-file-path`
+)
+
+jest.mock(`gatsby-cli/lib/reporter`, () => {
+ return {
+ warn: jest.fn(),
+ }
+})
+
+const reporterWarnMock = reporter.warn as jest.MockedFunction<
+ typeof reporter.warn
+>
+
+beforeEach(() => {
+ reporterWarnMock.mockClear()
+})
+
+it(`resolves gatsby-config.js if it exists`, async () => {
+ const configFilePath = path.join(mockDir, `cjs`, `gatsby-config`)
+ const resolvedConfigFilePath = await resolveJSFilepath({
+ rootDir: mockDir,
+ filePath: configFilePath,
+ })
+ expect(resolvedConfigFilePath).toBe(`${configFilePath}.js`)
+})
+
+it(`resolves gatsby-config.js the same way if a file path with extension is provided`, async () => {
+ const configFilePath = path.join(mockDir, `cjs`, `gatsby-config.js`)
+ const resolvedConfigFilePath = await resolveJSFilepath({
+ rootDir: mockDir,
+ filePath: configFilePath,
+ })
+ expect(resolvedConfigFilePath).toBe(configFilePath)
+})
+
+it(`resolves gatsby-config.mjs if it exists`, async () => {
+ const configFilePath = path.join(mockDir, `esm`, `gatsby-config`)
+ const resolvedConfigFilePath = await resolveJSFilepath({
+ rootDir: mockDir,
+ filePath: configFilePath,
+ })
+ expect(resolvedConfigFilePath).toBe(`${configFilePath}.mjs`)
+})
+
+it(`resolves gatsby-config.mjs the same way if a file path with extension is provided`, async () => {
+ const configFilePath = path.join(mockDir, `esm`, `gatsby-config.mjs`)
+ const resolvedConfigFilePath = await resolveJSFilepath({
+ rootDir: mockDir,
+ filePath: configFilePath,
+ })
+ expect(resolvedConfigFilePath).toBe(configFilePath)
+})
+
+it(`warns by default if both variants exist and defaults to the gatsby-config.js variant`, async () => {
+ const configFilePath = path.join(mockDir, `both`, `gatsby-config`)
+ const relativeFilePath = path.relative(mockDir, configFilePath)
+ const resolvedConfigFilePath = await resolveJSFilepath({
+ rootDir: mockDir,
+ filePath: configFilePath,
+ })
+ expect(reporterWarnMock).toBeCalledWith(
+ `The file '${relativeFilePath}' has both .js and .mjs variants, please use one or the other. Using .js by default.`
+ )
+ expect(resolvedConfigFilePath).toBe(`${configFilePath}.js`)
+})
+
+it(`does NOT warn if both variants exist and warnings are disabled`, async () => {
+ const configFilePath = path.join(mockDir, `both`, `gatsby-config`)
+ const resolvedConfigFilePath = await resolveJSFilepath({
+ rootDir: mockDir,
+ filePath: configFilePath,
+ warn: false,
+ })
+ expect(reporterWarnMock).not.toBeCalled()
+ expect(resolvedConfigFilePath).toBe(`${configFilePath}.js`)
+})
+
+it(`returns an empty string if no file exists`, async () => {
+ const configFilePath = path.join(mockDir)
+ const resolvedConfigFilePath = await resolveJSFilepath({
+ rootDir: mockDir,
+ filePath: configFilePath,
+ })
+ expect(resolvedConfigFilePath).toBe(``)
+})
diff --git a/packages/gatsby/src/bootstrap/__tests__/resolve-module-exports.ts b/packages/gatsby/src/bootstrap/__tests__/resolve-module-exports.ts
index 9a18d031a4dfb..ba91e4dcbb819 100644
--- a/packages/gatsby/src/bootstrap/__tests__/resolve-module-exports.ts
+++ b/packages/gatsby/src/bootstrap/__tests__/resolve-module-exports.ts
@@ -12,8 +12,10 @@ jest.mock(`gatsby-cli/lib/reporter`, () => {
})
import * as fs from "fs-extra"
+import path from "path"
import reporter from "gatsby-cli/lib/reporter"
import { resolveModuleExports } from "../resolve-module-exports"
+
let resolver
describe(`Resolve module exports`, () => {
@@ -127,6 +129,8 @@ describe(`Resolve module exports`, () => {
"/export/function": `export function foo() {}`,
}
+ const mockDir = path.resolve(__dirname, `..`, `__mocks__`)
+
beforeEach(() => {
resolver = jest.fn(arg => arg)
// @ts-ignore
@@ -138,18 +142,18 @@ describe(`Resolve module exports`, () => {
reporter.panic.mockClear()
})
- it(`Returns empty array for file paths that don't exist`, () => {
- const result = resolveModuleExports(`/file/path/does/not/exist`)
+ it(`Returns empty array for file paths that don't exist`, async () => {
+ const result = await resolveModuleExports(`/file/path/does/not/exist`)
expect(result).toEqual([])
})
- it(`Returns empty array for directory paths that don't exist`, () => {
- const result = resolveModuleExports(`/directory/path/does/not/exist/`)
+ it(`Returns empty array for directory paths that don't exist`, async () => {
+ const result = await resolveModuleExports(`/directory/path/does/not/exist/`)
expect(result).toEqual([])
})
- it(`Show meaningful error message for invalid JavaScript`, () => {
- resolveModuleExports(`/bad/file`, { resolver })
+ it(`Show meaningful error message for invalid JavaScript`, async () => {
+ await resolveModuleExports(`/bad/file`, { resolver })
expect(
// @ts-ignore
reporter.panic.mock.calls.map(c =>
@@ -160,120 +164,137 @@ describe(`Resolve module exports`, () => {
).toMatchSnapshot()
})
- it(`Resolves an export`, () => {
- const result = resolveModuleExports(`/simple/export`, { resolver })
+ it(`Resolves an export`, async () => {
+ const result = await resolveModuleExports(`/simple/export`, { resolver })
expect(result).toEqual([`foo`])
})
- it(`Resolves multiple exports`, () => {
- const result = resolveModuleExports(`/multiple/export`, { resolver })
+ it(`Resolves multiple exports`, async () => {
+ const result = await resolveModuleExports(`/multiple/export`, { resolver })
expect(result).toEqual([`bar`, `baz`, `foo`])
})
- it(`Resolves an export from an ES6 file`, () => {
- const result = resolveModuleExports(`/import/with/export`, { resolver })
+ it(`Resolves an export from an ES6 file`, async () => {
+ const result = await resolveModuleExports(`/import/with/export`, {
+ resolver,
+ })
expect(result).toEqual([`baz`])
})
- it(`Resolves an exported const`, () => {
- const result = resolveModuleExports(`/export/const`, { resolver })
+ it(`Resolves an exported const`, async () => {
+ const result = await resolveModuleExports(`/export/const`, { resolver })
expect(result).toEqual([`fooConst`])
})
- it(`Resolves module.exports`, () => {
- const result = resolveModuleExports(`/module/exports`, { resolver })
+ it(`Resolves module.exports`, async () => {
+ const result = await resolveModuleExports(`/module/exports`, { resolver })
expect(result).toEqual([`barExports`])
})
- it(`Resolves exports from a larger file`, () => {
- const result = resolveModuleExports(`/realistic/export`, { resolver })
+ it(`Resolves exports from a larger file`, async () => {
+ const result = await resolveModuleExports(`/realistic/export`, { resolver })
expect(result).toEqual([`replaceHistory`, `replaceComponentRenderer`])
})
- it(`Ignores exports.__esModule`, () => {
- const result = resolveModuleExports(`/esmodule/export`, { resolver })
+ it(`Ignores exports.__esModule`, async () => {
+ const result = await resolveModuleExports(`/esmodule/export`, { resolver })
expect(result).toEqual([`foo`])
})
- it(`Resolves a named export`, () => {
- const result = resolveModuleExports(`/export/named`, { resolver })
+ it(`Resolves a named export`, async () => {
+ const result = await resolveModuleExports(`/export/named`, { resolver })
expect(result).toEqual([`foo`])
})
- it(`Resolves a named export from`, () => {
- const result = resolveModuleExports(`/export/named/from`, { resolver })
+ it(`Resolves a named export from`, async () => {
+ const result = await resolveModuleExports(`/export/named/from`, {
+ resolver,
+ })
expect(result).toEqual([`Component`])
})
- it(`Resolves a named export as`, () => {
- const result = resolveModuleExports(`/export/named/as`, { resolver })
+ it(`Resolves a named export as`, async () => {
+ const result = await resolveModuleExports(`/export/named/as`, { resolver })
expect(result).toEqual([`bar`])
})
- it(`Resolves multiple named exports`, () => {
- const result = resolveModuleExports(`/export/named/multiple`, { resolver })
+ it(`Resolves multiple named exports`, async () => {
+ const result = await resolveModuleExports(`/export/named/multiple`, {
+ resolver,
+ })
expect(result).toEqual([`foo`, `bar`, `baz`])
})
- it(`Resolves default export`, () => {
- const result = resolveModuleExports(`/export/default`, { resolver })
+ it(`Resolves default export`, async () => {
+ const result = await resolveModuleExports(`/export/default`, { resolver })
expect(result).toEqual([`export default`])
})
- it(`Resolves default export with name`, () => {
- const result = resolveModuleExports(`/export/default/name`, { resolver })
+ it(`Resolves default export with name`, async () => {
+ const result = await resolveModuleExports(`/export/default/name`, {
+ resolver,
+ })
expect(result).toEqual([`export default foo`])
})
- it(`Resolves default function`, () => {
- const result = resolveModuleExports(`/export/default/function`, {
+ it(`Resolves default function`, async () => {
+ const result = await resolveModuleExports(`/export/default/function`, {
resolver,
})
expect(result).toEqual([`export default`])
})
- it(`Resolves default function with name`, () => {
- const result = resolveModuleExports(`/export/default/function/name`, {
+ it(`Resolves default function with name`, async () => {
+ const result = await resolveModuleExports(`/export/default/function/name`, {
resolver,
})
expect(result).toEqual([`export default foo`])
})
- it(`Resolves function declaration`, () => {
- const result = resolveModuleExports(`/export/function`, { resolver })
+ it(`Resolves function declaration`, async () => {
+ const result = await resolveModuleExports(`/export/function`, { resolver })
expect(result).toEqual([`foo`])
})
- it(`Resolves exports when using require mode - simple case`, () => {
- jest.mock(`require/exports`)
+ it(`Resolves exports when using import mode - simple case`, async () => {
+ jest.mock(`import/exports`)
- const result = resolveModuleExports(`require/exports`, {
- mode: `require`,
- })
+ const result = await resolveModuleExports(
+ path.join(mockDir, `import`, `exports`),
+ {
+ mode: `import`,
+ }
+ )
expect(result).toEqual([`foo`, `bar`])
})
- it(`Resolves exports when using require mode - unusual case`, () => {
- jest.mock(`require/unusual-exports`)
+ it(`Resolves exports when using import mode - unusual case`, async () => {
+ jest.mock(`import/unusual-exports`)
- const result = resolveModuleExports(`require/unusual-exports`, {
- mode: `require`,
- })
+ const result = await resolveModuleExports(
+ path.join(mockDir, `import`, `unusual-exports`),
+ {
+ mode: `import`,
+ }
+ )
expect(result).toEqual([`foo`])
})
- it(`Resolves exports when using require mode - returns empty array when module doesn't exist`, () => {
- const result = resolveModuleExports(`require/not-existing-module`, {
- mode: `require`,
- })
+ it(`Resolves exports when using import mode - returns empty array when module doesn't exist`, async () => {
+ const result = await resolveModuleExports(
+ path.join(mockDir, `import`, `not-existing-module`),
+ {
+ mode: `import`,
+ }
+ )
expect(result).toEqual([])
})
- it(`Resolves exports when using require mode - panic on errors`, () => {
- jest.mock(`require/module-error`)
+ it(`Resolves exports when using import mode - panic on errors`, async () => {
+ jest.mock(`import/module-error`)
- resolveModuleExports(`require/module-error`, {
- mode: `require`,
+ await resolveModuleExports(path.join(mockDir, `import`, `module-error`), {
+ mode: `import`,
})
expect(reporter.panic).toBeCalled()
diff --git a/packages/gatsby/src/bootstrap/get-config-file.ts b/packages/gatsby/src/bootstrap/get-config-file.ts
index f89c59a3738aa..c2dd3651608e4 100644
--- a/packages/gatsby/src/bootstrap/get-config-file.ts
+++ b/packages/gatsby/src/bootstrap/get-config-file.ts
@@ -1,10 +1,11 @@
import fs from "fs-extra"
-import { testRequireError } from "../utils/test-require-error"
+import { testImportError } from "../utils/test-import-error"
import report from "gatsby-cli/lib/reporter"
import path from "path"
-import { sync as existsSync } from "fs-exists-cached"
import { COMPILED_CACHE_DIR } from "../utils/parcel/compile-gatsby-files"
import { isNearMatch } from "../utils/is-near-match"
+import { resolveJSFilepath } from "./resolve-js-file-path"
+import { preferDefault } from "./prefer-default"
export async function getConfigFile(
siteDirectory: string,
@@ -14,114 +15,188 @@ export async function getConfigFile(
configModule: any
configFilePath: string
}> {
- let configPath = ``
- let configFilePath = ``
- let configModule: any
+ const compiledResult = await attemptImportCompiled(siteDirectory, configName)
+
+ if (compiledResult?.configModule && compiledResult?.configFilePath) {
+ return compiledResult
+ }
+
+ const uncompiledResult = await attemptImportUncompiled(
+ siteDirectory,
+ configName,
+ distance
+ )
+
+ return uncompiledResult || {}
+}
+
+async function attemptImport(
+ siteDirectory: string,
+ configPath: string
+): Promise<{
+ configModule: unknown
+ configFilePath: string
+}> {
+ const configFilePath = await resolveJSFilepath({
+ rootDir: siteDirectory,
+ filePath: configPath,
+ })
+
+ // The file does not exist, no sense trying to import it
+ if (!configFilePath) {
+ return { configFilePath: ``, configModule: undefined }
+ }
+
+ const importedModule = await import(configFilePath)
+ const configModule = preferDefault(importedModule)
+
+ return { configFilePath, configModule }
+}
+
+async function attemptImportCompiled(
+ siteDirectory: string,
+ configName: string
+): Promise<{
+ configModule: unknown
+ configFilePath: string
+}> {
+ let compiledResult
+
+ try {
+ const compiledConfigPath = path.join(
+ `${siteDirectory}/${COMPILED_CACHE_DIR}`,
+ configName
+ )
+ compiledResult = await attemptImport(siteDirectory, compiledConfigPath)
+ } catch (error) {
+ report.panic({
+ id: `11902`,
+ error: error,
+ context: {
+ configName,
+ message: error.message,
+ },
+ })
+ }
+
+ return compiledResult
+}
+
+async function attemptImportUncompiled(
+ siteDirectory: string,
+ configName: string,
+ distance: number
+): Promise<{
+ configModule: unknown
+ configFilePath: string
+}> {
+ let uncompiledResult
+
+ const uncompiledConfigPath = path.join(siteDirectory, configName)
- // Attempt to find compiled gatsby-config.js in .cache/compiled/gatsby-config.js
try {
- configPath = path.join(`${siteDirectory}/${COMPILED_CACHE_DIR}`, configName)
- configFilePath = require.resolve(configPath)
- configModule = require(configFilePath)
- } catch (outerError) {
- // Not all plugins will have a compiled file, so the err.message can look like this:
- // "Cannot find module '/node_modules/gatsby-source-filesystem/.cache/compiled/gatsby-config'"
- // But the compiled file can also have an error like this:
- // "Cannot find module 'foobar'"
- // So this is trying to differentiate between an error we're fine ignoring and an error that we should throw
- const isModuleNotFoundError = outerError.code === `MODULE_NOT_FOUND`
- const isThisFileRequireError =
- outerError?.requireStack?.[0]?.includes(`get-config-file`) ?? true
-
- // User's module require error inside gatsby-config.js
- if (!(isModuleNotFoundError && isThisFileRequireError)) {
+ uncompiledResult = await attemptImport(siteDirectory, uncompiledConfigPath)
+ } catch (error) {
+ if (!testImportError(uncompiledConfigPath, error)) {
report.panic({
- id: `11902`,
- error: outerError,
+ id: `10123`,
+ error,
context: {
configName,
- message: outerError.message,
+ message: error.message,
},
})
}
+ }
+
+ if (uncompiledResult?.configFilePath) {
+ return uncompiledResult
+ }
+
+ const error = new Error(`Cannot find package '${uncompiledConfigPath}'`)
+
+ const { tsConfig, nearMatch } = await checkTsAndNearMatch(
+ siteDirectory,
+ configName,
+ distance
+ )
+
+ // gatsby-config.ts exists but compiled gatsby-config.js does not
+ if (tsConfig) {
+ report.panic({
+ id: `10127`,
+ error,
+ context: {
+ configName,
+ },
+ })
+ }
+
+ // gatsby-config is misnamed
+ if (nearMatch) {
+ const isTSX = nearMatch.endsWith(`.tsx`)
+ report.panic({
+ id: `10124`,
+ error,
+ context: {
+ configName,
+ nearMatch,
+ isTSX,
+ },
+ })
+ }
+
+ // gatsby-config is incorrectly located in src directory
+ const isInSrcDir = await resolveJSFilepath({
+ rootDir: siteDirectory,
+ filePath: path.join(siteDirectory, `src`, configName),
+ warn: false,
+ })
+
+ if (isInSrcDir) {
+ report.panic({
+ id: `10125`,
+ context: {
+ configName,
+ },
+ })
+ }
- // Attempt to find uncompiled gatsby-config.js in root dir
- configPath = path.join(siteDirectory, configName)
-
- try {
- configFilePath = require.resolve(configPath)
- configModule = require(configFilePath)
- } catch (innerError) {
- // Some other error that is not a require error
- if (!testRequireError(configPath, innerError)) {
- report.panic({
- id: `10123`,
- error: innerError,
- context: {
- configName,
- message: innerError.message,
- },
- })
- }
-
- const files = await fs.readdir(siteDirectory)
-
- let tsConfig = false
- let nearMatch = ``
-
- for (const file of files) {
- if (tsConfig || nearMatch) {
- break
- }
-
- const { name, ext } = path.parse(file)
-
- if (name === configName && ext === `.ts`) {
- tsConfig = true
- break
- }
-
- if (isNearMatch(name, configName, distance)) {
- nearMatch = file
- }
- }
-
- // gatsby-config.ts exists but compiled gatsby-config.js does not
- if (tsConfig) {
- report.panic({
- id: `10127`,
- error: innerError,
- context: {
- configName,
- },
- })
- }
-
- // gatsby-config is misnamed
- if (nearMatch) {
- const isTSX = nearMatch.endsWith(`.tsx`)
- report.panic({
- id: `10124`,
- error: innerError,
- context: {
- configName,
- nearMatch,
- isTSX,
- },
- })
- }
-
- // gatsby-config.js is incorrectly located in src/gatsby-config.js
- if (existsSync(path.join(siteDirectory, `src`, configName + `.js`))) {
- report.panic({
- id: `10125`,
- context: {
- configName,
- },
- })
- }
+ return uncompiledResult
+}
+
+async function checkTsAndNearMatch(
+ siteDirectory: string,
+ configName: string,
+ distance: number
+): Promise<{
+ tsConfig: boolean
+ nearMatch: string
+}> {
+ const files = await fs.readdir(siteDirectory)
+
+ let tsConfig = false
+ let nearMatch = ``
+
+ for (const file of files) {
+ if (tsConfig || nearMatch) {
+ break
+ }
+
+ const { name, ext } = path.parse(file)
+
+ if (name === configName && ext === `.ts`) {
+ tsConfig = true
+ break
+ }
+
+ if (isNearMatch(name, configName, distance)) {
+ nearMatch = file
}
}
- return { configModule, configFilePath }
+ return {
+ tsConfig,
+ nearMatch,
+ }
}
diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.ts b/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.ts
index 70ab856efcaaf..817fbc2c262e7 100644
--- a/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.ts
+++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/load-plugins.ts
@@ -25,6 +25,17 @@ jest.mock(`gatsby-cli/lib/reporter`, () => {
}
})
+// Previously babel transpiled src ts plugin files (e.g. gatsby-node files) on the fly,
+// making them require-able/test-able without running compileGatsbyFiles prior (as would happen in a real scenario).
+// After switching to import to support esm, point file path resolution to the real compiled JS files in dist instead.
+jest.mock(`../../resolve-js-file-path`, () => {
+ return {
+ resolveJSFilepath: jest.fn(
+ ({ filePath }) => `${filePath.replace(`src`, `dist`)}.js`
+ ),
+ }
+})
+
jest.mock(`resolve-from`)
const mockProcessExit = jest.spyOn(process, `exit`).mockImplementation(() => {})
diff --git a/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.ts b/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.ts
index 2fd2bed7826b8..d27e9601bcf02 100644
--- a/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.ts
+++ b/packages/gatsby/src/bootstrap/load-plugins/__tests__/validate.ts
@@ -79,7 +79,10 @@ describe(`collatePluginAPIs`, () => {
},
]
- const result = collatePluginAPIs({ currentAPIs: apis, flattenedPlugins })
+ const result = await collatePluginAPIs({
+ currentAPIs: apis,
+ flattenedPlugins,
+ })
expect(result).toMatchSnapshot()
})
@@ -106,7 +109,10 @@ describe(`collatePluginAPIs`, () => {
},
]
- const result = collatePluginAPIs({ currentAPIs: apis, flattenedPlugins })
+ const result = await collatePluginAPIs({
+ currentAPIs: apis,
+ flattenedPlugins,
+ })
expect(result).toMatchSnapshot()
})
})
diff --git a/packages/gatsby/src/bootstrap/load-plugins/index.ts b/packages/gatsby/src/bootstrap/load-plugins/index.ts
index 7d743760e5cc4..36696adf4f747 100644
--- a/packages/gatsby/src/bootstrap/load-plugins/index.ts
+++ b/packages/gatsby/src/bootstrap/load-plugins/index.ts
@@ -40,11 +40,11 @@ export async function loadPlugins(
// Work out which plugins use which APIs, including those which are not
// valid Gatsby APIs, aka 'badExports'
- const x = collatePluginAPIs({ currentAPIs, flattenedPlugins: pluginArray })
-
- // From this point on, these are fully-resolved plugins.
- let flattenedPlugins = x.flattenedPlugins
- const badExports = x.badExports
+ let { flattenedPlugins, badExports } = await collatePluginAPIs({
+ currentAPIs,
+ flattenedPlugins: pluginArray,
+ rootDir,
+ })
// Show errors for any non-Gatsby APIs exported from plugins
await handleBadExports({ currentAPIs, badExports })
diff --git a/packages/gatsby/src/bootstrap/load-plugins/validate.ts b/packages/gatsby/src/bootstrap/load-plugins/validate.ts
index 7dffe0706b5cf..c884102c5dafb 100644
--- a/packages/gatsby/src/bootstrap/load-plugins/validate.ts
+++ b/packages/gatsby/src/bootstrap/load-plugins/validate.ts
@@ -403,13 +403,18 @@ export async function validateConfigPluginsOptions(
/**
* Identify which APIs each plugin exports
*/
-export function collatePluginAPIs({
+export async function collatePluginAPIs({
currentAPIs,
flattenedPlugins,
+ rootDir,
}: {
currentAPIs: ICurrentAPIs
flattenedPlugins: Array>
-}): { flattenedPlugins: Array; badExports: IEntryMap } {
+ rootDir: string
+}): Promise<{
+ flattenedPlugins: Array
+ badExports: IEntryMap
+}> {
// Get a list of bad exports
const badExports: IEntryMap = {
node: [],
@@ -417,7 +422,7 @@ export function collatePluginAPIs({
ssr: [],
}
- flattenedPlugins.forEach(plugin => {
+ for (const plugin of flattenedPlugins) {
plugin.nodeAPIs = []
plugin.browserAPIs = []
plugin.ssrAPIs = []
@@ -425,17 +430,22 @@ export function collatePluginAPIs({
// Discover which APIs this plugin implements and store an array against
// the plugin node itself *and* in an API to plugins map for faster lookups
// later.
- const pluginNodeExports = resolveModuleExports(
+ const pluginNodeExports = await resolveModuleExports(
plugin.resolvedCompiledGatsbyNode ?? `${plugin.resolve}/gatsby-node`,
{
- mode: `require`,
+ mode: `import`,
+ rootDir,
}
)
- const pluginBrowserExports = resolveModuleExports(
- `${plugin.resolve}/gatsby-browser`
+ const pluginBrowserExports = await resolveModuleExports(
+ `${plugin.resolve}/gatsby-browser`,
+ {
+ rootDir,
+ }
)
- const pluginSSRExports = resolveModuleExports(
- `${plugin.resolve}/gatsby-ssr`
+ const pluginSSRExports = await resolveModuleExports(
+ `${plugin.resolve}/gatsby-ssr`,
+ { rootDir }
)
if (pluginNodeExports.length > 0) {
@@ -461,7 +471,7 @@ export function collatePluginAPIs({
getBadExports(plugin, pluginSSRExports, currentAPIs.ssr)
) // Collate any bad exports
}
- })
+ }
return {
flattenedPlugins: flattenedPlugins as Array,
diff --git a/packages/gatsby/src/bootstrap/resolve-js-file-path.ts b/packages/gatsby/src/bootstrap/resolve-js-file-path.ts
new file mode 100644
index 0000000000000..b4ad51bb88b40
--- /dev/null
+++ b/packages/gatsby/src/bootstrap/resolve-js-file-path.ts
@@ -0,0 +1,61 @@
+import path from "path"
+import report from "gatsby-cli/lib/reporter"
+
+/**
+ * Figure out if the file path is .js or .mjs without relying on the fs module, and return the file path if it exists.
+ */
+export async function resolveJSFilepath({
+ rootDir,
+ filePath,
+ warn = true,
+}: {
+ rootDir: string
+ filePath: string
+ warn?: boolean
+}): Promise {
+ const filePathWithJSExtension = filePath.endsWith(`.js`)
+ ? filePath
+ : `${filePath}.js`
+ const filePathWithMJSExtension = filePath.endsWith(`.mjs`)
+ ? filePath
+ : `${filePath}.mjs`
+
+ // Check if both variants exist
+ try {
+ if (
+ require.resolve(filePathWithJSExtension) &&
+ require.resolve(filePathWithMJSExtension)
+ ) {
+ if (warn) {
+ report.warn(
+ `The file '${path.relative(
+ rootDir,
+ filePath
+ )}' has both .js and .mjs variants, please use one or the other. Using .js by default.`
+ )
+ }
+ return filePathWithJSExtension
+ }
+ } catch (_) {
+ // Do nothing
+ }
+
+ // Check if .js variant exists
+ try {
+ if (require.resolve(filePathWithJSExtension)) {
+ return filePathWithJSExtension
+ }
+ } catch (_) {
+ // Do nothing
+ }
+
+ try {
+ if (require.resolve(filePathWithMJSExtension)) {
+ return filePathWithMJSExtension
+ }
+ } catch (_) {
+ // Do nothing
+ }
+
+ return ``
+}
diff --git a/packages/gatsby/src/bootstrap/resolve-module-exports.ts b/packages/gatsby/src/bootstrap/resolve-module-exports.ts
index c81d0763a5b1f..d530a096abfe0 100644
--- a/packages/gatsby/src/bootstrap/resolve-module-exports.ts
+++ b/packages/gatsby/src/bootstrap/resolve-module-exports.ts
@@ -4,8 +4,10 @@ import traverse from "@babel/traverse"
import { codeFrameColumns, SourceLocation } from "@babel/code-frame"
import report from "gatsby-cli/lib/reporter"
import { babelParseToAst } from "../utils/babel-parse-to-ast"
-import { testRequireError } from "../utils/test-require-error"
-import { resolveModule } from "../utils/module-resolver"
+import { testImportError } from "../utils/test-import-error"
+import { resolveModule, ModuleResolver } from "../utils/module-resolver"
+import { resolveJSFilepath } from "./resolve-js-file-path"
+import { preferDefault } from "./prefer-default"
const staticallyAnalyzeExports = (
modulePath: string,
@@ -176,32 +178,56 @@ https://gatsby.dev/no-mixed-modules
return exportNames
}
+interface IResolveModuleExportsOptions {
+ mode?: `analysis` | `import`
+ resolver?: ModuleResolver
+ rootDir?: string
+}
+
/**
- * Given a `require.resolve()` compatible path pointing to a JS module,
- * return an array listing the names of the module's exports.
+ * Given a path to a module, return an array of the module's exports.
*
- * Returns [] for invalid paths and modules without exports.
+ * It can run in two modes:
+ * 1. `analysis` mode gets exports via static analysis by traversing the file's AST with babel
+ * 2. `import` mode gets exports by directly importing the module and accessing its properties
*
- * @param modulePath
- * @param mode
- * @param resolver
+ * At the time of writing, analysis mode is used for files that can be jsx (e.g. gatsby-browser, gatsby-ssr)
+ * and import mode is used for files that can be js or mjs.
+ *
+ * Returns [] for invalid paths and modules without exports.
*/
-export const resolveModuleExports = (
+export async function resolveModuleExports(
modulePath: string,
- { mode = `analysis`, resolver = resolveModule } = {}
-): Array => {
- if (mode === `require`) {
- let absPath: string | undefined
+ {
+ mode = `analysis`,
+ resolver = resolveModule,
+ rootDir = process.cwd(),
+ }: IResolveModuleExportsOptions = {}
+): Promise> {
+ if (mode === `import`) {
try {
- absPath = require.resolve(modulePath)
- return Object.keys(require(modulePath)).filter(
+ const moduleFilePath = await resolveJSFilepath({
+ rootDir,
+ filePath: modulePath,
+ })
+
+ if (!moduleFilePath) {
+ return []
+ }
+
+ const rawImportedModule = await import(moduleFilePath)
+
+ // If the module is cjs, the properties we care about are nested under a top-level `default` property
+ const importedModule = preferDefault(rawImportedModule)
+
+ return Object.keys(importedModule).filter(
exportName => exportName !== `__esModule`
)
- } catch (e) {
- if (!testRequireError(modulePath, e)) {
+ } catch (error) {
+ if (!testImportError(modulePath, error)) {
// if module exists, but requiring it cause errors,
// show the error to the user and terminate build
- report.panic(`Error in "${absPath}":`, e)
+ report.panic(`Error in "${modulePath}":`, error)
}
}
} else {
diff --git a/packages/gatsby/src/schema/graphql-engine/entry.ts b/packages/gatsby/src/schema/graphql-engine/entry.ts
index e9a13f9b63a22..c7383fbf0d0b6 100644
--- a/packages/gatsby/src/schema/graphql-engine/entry.ts
+++ b/packages/gatsby/src/schema/graphql-engine/entry.ts
@@ -11,7 +11,7 @@ import { actions } from "../../redux/actions"
import reporter from "gatsby-cli/lib/reporter"
import { GraphQLRunner, IQueryOptions } from "../../query/graphql-runner"
import { waitJobsByRequest } from "../../utils/wait-until-jobs-complete"
-import { setGatsbyPluginCache } from "../../utils/require-gatsby-plugin"
+import { setGatsbyPluginCache } from "../../utils/import-gatsby-plugin"
import apiRunnerNode from "../../utils/api-runner-node"
import type { IGatsbyPage, IGatsbyState } from "../../redux/types"
import { findPageByPath } from "../../utils/find-page-by-path"
diff --git a/packages/gatsby/src/schema/graphql-engine/print-plugins.ts b/packages/gatsby/src/schema/graphql-engine/print-plugins.ts
index b35dd623f1931..2019e93184f6f 100644
--- a/packages/gatsby/src/schema/graphql-engine/print-plugins.ts
+++ b/packages/gatsby/src/schema/graphql-engine/print-plugins.ts
@@ -5,7 +5,7 @@ import * as _ from "lodash"
import { slash } from "gatsby-core-utils"
import { store } from "../../redux"
import { IGatsbyState } from "../../redux/types"
-import { requireGatsbyPlugin } from "../../utils/require-gatsby-plugin"
+import { importGatsbyPlugin } from "../../utils/import-gatsby-plugin"
export const schemaCustomizationAPIs = new Set([
`setFieldsOnGraphQLNodeType`,
@@ -26,13 +26,11 @@ export async function printQueryEnginePlugins(): Promise {
} catch (e) {
// no-op
}
- return await fs.writeFile(
- schemaCustomizationPluginsPath,
- renderQueryEnginePlugins()
- )
+ const queryEnginePlugins = await renderQueryEnginePlugins()
+ return await fs.writeFile(schemaCustomizationPluginsPath, queryEnginePlugins)
}
-function renderQueryEnginePlugins(): string {
+async function renderQueryEnginePlugins(): Promise {
const { flattenedPlugins } = store.getState()
const usedPlugins = flattenedPlugins.filter(
p =>
@@ -41,7 +39,8 @@ function renderQueryEnginePlugins(): string {
p.nodeAPIs.some(api => schemaCustomizationAPIs.has(api)))
)
const usedSubPlugins = findSubPlugins(usedPlugins, flattenedPlugins)
- return render(usedPlugins, usedSubPlugins)
+ const result = await render(usedPlugins, usedSubPlugins)
+ return result
}
function relativePluginPath(resolve: string): string {
@@ -50,10 +49,10 @@ function relativePluginPath(resolve: string): string {
)
}
-function render(
+async function render(
usedPlugins: IGatsbyState["flattenedPlugins"],
usedSubPlugins: IGatsbyState["flattenedPlugins"]
-): string {
+): Promise {
const uniqGatsbyNode = uniq(usedPlugins)
const uniqSubPlugins = uniq(usedSubPlugins)
@@ -67,7 +66,7 @@ function render(
}
})
- const pluginsWithWorkers = filterPluginsWithWorkers(uniqGatsbyNode)
+ const pluginsWithWorkers = await filterPluginsWithWorkers(uniqGatsbyNode)
const subPluginModuleToImportNameMapping = new Map()
const imports: Array = [
@@ -144,16 +143,23 @@ export const flattenedPlugins =
return output
}
-function filterPluginsWithWorkers(
+async function filterPluginsWithWorkers(
plugins: IGatsbyState["flattenedPlugins"]
-): IGatsbyState["flattenedPlugins"] {
- return plugins.filter(plugin => {
+): Promise {
+ const filteredPlugins: Array = []
+
+ for (const plugin of plugins) {
try {
- return Boolean(requireGatsbyPlugin(plugin, `gatsby-worker`))
- } catch (err) {
- return false
+ const pluginWithWorker = await importGatsbyPlugin(plugin, `gatsby-worker`)
+ if (pluginWithWorker) {
+ filteredPlugins.push(plugin)
+ }
+ } catch (_) {
+ // Do nothing
}
- })
+ }
+
+ return filteredPlugins
}
type ArrayElement> = ArrayType extends Array<
diff --git a/packages/gatsby/src/services/initialize.ts b/packages/gatsby/src/services/initialize.ts
index ae1af55b5b21c..2667dbad71cce 100644
--- a/packages/gatsby/src/services/initialize.ts
+++ b/packages/gatsby/src/services/initialize.ts
@@ -493,15 +493,16 @@ export async function initialize({
activity = reporter.activityTimer(`copy gatsby files`, {
parentSpan,
})
+
activity.start()
+
const srcDir = `${__dirname}/../../cache-dir`
const siteDir = cacheDirectory
- const tryRequire = `${__dirname}/../utils/test-require-error.js`
+
try {
await fs.copy(srcDir, siteDir, {
overwrite: true,
})
- await fs.copy(tryRequire, `${siteDir}/test-require-error.js`)
await fs.ensureDir(`${cacheDirectory}/${lmdbCacheDirectoryName}`)
// Ensure .cache/fragments exists and is empty. We want fragments to be
@@ -636,6 +637,16 @@ export async function initialize({
const workerPool = WorkerPool.create()
+ const siteDirectoryFiles = await fs.readdir(siteDirectory)
+
+ const gatsbyFilesIsInESM = siteDirectoryFiles.some(file =>
+ file.match(/gatsby-(node|config)\.mjs/)
+ )
+
+ if (gatsbyFilesIsInESM) {
+ telemetry.trackFeatureIsUsed(`ESMInGatsbyFiles`)
+ }
+
if (state.config.graphqlTypegen) {
telemetry.trackFeatureIsUsed(`GraphQLTypegen`)
// This is only run during `gatsby develop`
diff --git a/packages/gatsby/src/utils/__tests__/api-runner-node.js b/packages/gatsby/src/utils/__tests__/api-runner-node.js
index 7a2d4644f8345..69d3e505c4482 100644
--- a/packages/gatsby/src/utils/__tests__/api-runner-node.js
+++ b/packages/gatsby/src/utils/__tests__/api-runner-node.js
@@ -1,4 +1,5 @@
const apiRunnerNode = require(`../api-runner-node`)
+const path = require(`path`)
jest.mock(`../../redux`, () => {
return {
@@ -40,6 +41,8 @@ const { store, emitter } = require(`../../redux`)
const reporter = require(`gatsby-cli/lib/reporter`)
const { getCache } = require(`../get-cache`)
+const fixtureDir = path.resolve(__dirname, `fixtures`, `api-runner-node`)
+
beforeEach(() => {
store.getState.mockClear()
emitter.on.mockClear()
@@ -54,93 +57,6 @@ beforeEach(() => {
describe(`api-runner-node`, () => {
it(`Ends activities if plugin didn't end them`, async () => {
- jest.doMock(
- `test-plugin-correct/gatsby-node`,
- () => {
- return {
- testAPIHook: ({ reporter }) => {
- const spinnerActivity = reporter.activityTimer(
- `control spinner activity`
- )
- spinnerActivity.start()
- // calling activity.end() to make sure api runner doesn't call it more than needed
- spinnerActivity.end()
-
- const progressActivity = reporter.createProgress(
- `control progress activity`
- )
- progressActivity.start()
- // calling activity.done() to make sure api runner doesn't call it more than needed
- progressActivity.done()
- },
- }
- },
- { virtual: true }
- )
- jest.doMock(
- `test-plugin-spinner/gatsby-node`,
- () => {
- return {
- testAPIHook: ({ reporter }) => {
- const activity = reporter.activityTimer(`spinner activity`)
- activity.start()
- // not calling activity.end() - api runner should do end it
- },
- }
- },
- { virtual: true }
- )
- jest.doMock(
- `test-plugin-progress/gatsby-node`,
- () => {
- return {
- testAPIHook: ({ reporter }) => {
- const activity = reporter.createProgress(
- `progress activity`,
- 100,
- 0
- )
- activity.start()
- // not calling activity.end() or done() - api runner should do end it
- },
- }
- },
- { virtual: true }
- )
- jest.doMock(
- `test-plugin-spinner-throw/gatsby-node`,
- () => {
- return {
- testAPIHook: ({ reporter }) => {
- const activity = reporter.activityTimer(
- `spinner activity with throwing`
- )
- activity.start()
- throw new Error(`error`)
- // not calling activity.end() - api runner should do end it
- },
- }
- },
- { virtual: true }
- )
- jest.doMock(
- `test-plugin-progress-throw/gatsby-node`,
- () => {
- return {
- testAPIHook: ({ reporter }) => {
- const activity = reporter.createProgress(
- `progress activity with throwing`,
- 100,
- 0
- )
- activity.start()
- throw new Error(`error`)
- // not calling activity.end() or done() - api runner should do end it
- },
- }
- },
- { virtual: true }
- )
store.getState.mockImplementation(() => {
return {
program: {},
@@ -148,27 +64,27 @@ describe(`api-runner-node`, () => {
flattenedPlugins: [
{
name: `test-plugin-correct`,
- resolve: `test-plugin-correct`,
+ resolve: path.join(fixtureDir, `test-plugin-correct`),
nodeAPIs: [`testAPIHook`],
},
{
name: `test-plugin-spinner`,
- resolve: `test-plugin-spinner`,
+ resolve: path.join(fixtureDir, `test-plugin-spinner`),
nodeAPIs: [`testAPIHook`],
},
{
name: `test-plugin-progress`,
- resolve: `test-plugin-progress`,
+ resolve: path.join(fixtureDir, `test-plugin-progress`),
nodeAPIs: [`testAPIHook`],
},
{
name: `test-plugin-spinner-throw`,
- resolve: `test-plugin-spinner-throw`,
+ resolve: path.join(fixtureDir, `test-plugin-spinner-throw`),
nodeAPIs: [`testAPIHook`],
},
{
name: `test-plugin-progress-throw`,
- resolve: `test-plugin-progress-throw`,
+ resolve: path.join(fixtureDir, `test-plugin-progress-throw`),
nodeAPIs: [`testAPIHook`],
},
],
@@ -183,27 +99,6 @@ describe(`api-runner-node`, () => {
})
it(`Doesn't initialize cache in onPreInit API`, async () => {
- jest.doMock(
- `test-plugin-on-preinit-works/gatsby-node`,
- () => {
- return {
- onPreInit: () => {},
- otherTestApi: () => {},
- }
- },
- { virtual: true }
- )
- jest.doMock(
- `test-plugin-on-preinit-fails/gatsby-node`,
- () => {
- return {
- onPreInit: async ({ cache }) => {
- await cache.get(`foo`)
- },
- }
- },
- { virtual: true }
- )
store.getState.mockImplementation(() => {
return {
program: {},
@@ -211,12 +106,12 @@ describe(`api-runner-node`, () => {
flattenedPlugins: [
{
name: `test-plugin-on-preinit-works`,
- resolve: `test-plugin-on-preinit-works`,
+ resolve: path.join(fixtureDir, `test-plugin-on-preinit-works`),
nodeAPIs: [`onPreInit`, `otherTestApi`],
},
{
name: `test-plugin-on-preinit-fails`,
- resolve: `test-plugin-on-preinit-fails`,
+ resolve: path.join(fixtureDir, `test-plugin-on-preinit-fails`),
nodeAPIs: [`onPreInit`],
},
],
@@ -239,19 +134,6 @@ describe(`api-runner-node`, () => {
})
it(`Correctly handle error args`, async () => {
- jest.doMock(
- `test-plugin-error-args/gatsby-node`,
- () => {
- return {
- onPreInit: ({ reporter }) => {
- reporter.panicOnBuild(`Konohagakure`)
- reporter.panicOnBuild(new Error(`Rasengan`))
- reporter.panicOnBuild(`Jiraiya`, new Error(`Tsunade`))
- },
- }
- },
- { virtual: true }
- )
store.getState.mockImplementation(() => {
return {
program: {},
@@ -259,7 +141,7 @@ describe(`api-runner-node`, () => {
flattenedPlugins: [
{
name: `test-plugin-error-args`,
- resolve: `test-plugin-error-args`,
+ resolve: path.join(fixtureDir, `test-plugin-error-args`),
nodeAPIs: [`onPreInit`],
},
],
@@ -289,28 +171,6 @@ describe(`api-runner-node`, () => {
})
it(`Correctly uses setErrorMap with pluginName prefixes`, async () => {
- jest.doMock(
- `test-plugin-plugin-prefixes/gatsby-node`,
- () => {
- return {
- onPreInit: ({ reporter }) => {
- reporter.setErrorMap({
- 1337: {
- text: context => `Error text is ${context.someProp}`,
- level: `ERROR`,
- docsUrl: `https://www.gatsbyjs.com/docs/gatsby-cli/#new`,
- },
- })
-
- reporter.panicOnBuild({
- id: `1337`,
- context: { someProp: `Naruto` },
- })
- },
- }
- },
- { virtual: true }
- )
store.getState.mockImplementation(() => {
return {
program: {},
@@ -318,7 +178,7 @@ describe(`api-runner-node`, () => {
flattenedPlugins: [
{
name: `test-plugin-plugin-prefixes`,
- resolve: `test-plugin-plugin-prefixes`,
+ resolve: path.join(fixtureDir, `test-plugin-plugin-prefixes`),
nodeAPIs: [`onPreInit`],
},
],
diff --git a/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-correct/gatsby-node.js b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-correct/gatsby-node.js
new file mode 100644
index 0000000000000..e3f482f7b892b
--- /dev/null
+++ b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-correct/gatsby-node.js
@@ -0,0 +1,15 @@
+exports.testAPIHook = ({ reporter }) => {
+ const spinnerActivity = reporter.activityTimer(
+ `control spinner activity`
+ )
+ spinnerActivity.start()
+ // calling activity.end() to make sure api runner doesn't call it more than needed
+ spinnerActivity.end()
+
+ const progressActivity = reporter.createProgress(
+ `control progress activity`
+ )
+ progressActivity.start()
+ // calling activity.done() to make sure api runner doesn't call it more than needed
+ progressActivity.done()
+}
\ No newline at end of file
diff --git a/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-error-args/gatsby-node.js b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-error-args/gatsby-node.js
new file mode 100644
index 0000000000000..d88555bcff28a
--- /dev/null
+++ b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-error-args/gatsby-node.js
@@ -0,0 +1,5 @@
+exports.onPreInit = ({ reporter }) => {
+ reporter.panicOnBuild(`Konohagakure`)
+ reporter.panicOnBuild(new Error(`Rasengan`))
+ reporter.panicOnBuild(`Jiraiya`, new Error(`Tsunade`))
+}
\ No newline at end of file
diff --git a/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-on-preinit-fails/gatsby-node.js b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-on-preinit-fails/gatsby-node.js
new file mode 100644
index 0000000000000..fb355bf1625e5
--- /dev/null
+++ b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-on-preinit-fails/gatsby-node.js
@@ -0,0 +1,3 @@
+exports.onPreInit = async ({ cache }) => {
+ await cache.get(`foo`)
+}
\ No newline at end of file
diff --git a/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-on-preinit-works/gatsby-node.js b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-on-preinit-works/gatsby-node.js
new file mode 100644
index 0000000000000..a142133faa81e
--- /dev/null
+++ b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-on-preinit-works/gatsby-node.js
@@ -0,0 +1,2 @@
+exports.onPreInit = () => {}
+exports.otherTestApi = () => {}
\ No newline at end of file
diff --git a/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-plugin-prefixes/gatsby-node.js b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-plugin-prefixes/gatsby-node.js
new file mode 100644
index 0000000000000..48f6bb0163aca
--- /dev/null
+++ b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-plugin-prefixes/gatsby-node.js
@@ -0,0 +1,14 @@
+exports.onPreInit = ({ reporter }) => {
+ reporter.setErrorMap({
+ 1337: {
+ text: context => `Error text is ${context.someProp}`,
+ level: `ERROR`,
+ docsUrl: `https://www.gatsbyjs.com/docs/gatsby-cli/#new`,
+ },
+ })
+
+ reporter.panicOnBuild({
+ id: `1337`,
+ context: { someProp: `Naruto` },
+ })
+}
\ No newline at end of file
diff --git a/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-progress-throw/gatsby-node.js b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-progress-throw/gatsby-node.js
new file mode 100644
index 0000000000000..76ec49e8519a5
--- /dev/null
+++ b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-progress-throw/gatsby-node.js
@@ -0,0 +1,10 @@
+exports.testAPIHook = ({ reporter }) => {
+ const activity = reporter.createProgress(
+ `progress activity with throwing`,
+ 100,
+ 0
+ )
+ activity.start()
+ throw new Error(`error`)
+ // not calling activity.end() or done() - api runner should do end it
+}
\ No newline at end of file
diff --git a/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-progress/gatsby-node.js b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-progress/gatsby-node.js
new file mode 100644
index 0000000000000..5b41db466a434
--- /dev/null
+++ b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-progress/gatsby-node.js
@@ -0,0 +1,9 @@
+exports.testAPIHook = ({ reporter }) => {
+ const activity = reporter.createProgress(
+ `progress activity`,
+ 100,
+ 0
+ )
+ activity.start()
+ // not calling activity.end() or done() - api runner should do end it
+}
diff --git a/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-spinner-throw/gatsby-node.js b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-spinner-throw/gatsby-node.js
new file mode 100644
index 0000000000000..50839f361ba68
--- /dev/null
+++ b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-spinner-throw/gatsby-node.js
@@ -0,0 +1,8 @@
+exports.testAPIHook = ({ reporter }) => {
+ const activity = reporter.activityTimer(
+ `spinner activity with throwing`
+ )
+ activity.start()
+ throw new Error(`error`)
+ // not calling activity.end() - api runner should do end it
+}
\ No newline at end of file
diff --git a/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-spinner/gatsby-node.js b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-spinner/gatsby-node.js
new file mode 100644
index 0000000000000..1206b97a5bede
--- /dev/null
+++ b/packages/gatsby/src/utils/__tests__/fixtures/api-runner-node/test-plugin-spinner/gatsby-node.js
@@ -0,0 +1,5 @@
+exports.testAPIHook = ({ reporter }) => {
+ const activity = reporter.activityTimer(`spinner activity`)
+ activity.start()
+ // not calling activity.end() - api runner should do end it
+}
\ No newline at end of file
diff --git a/packages/gatsby/src/utils/__tests__/fixtures/bad-module-import.js b/packages/gatsby/src/utils/__tests__/fixtures/bad-module-import.js
new file mode 100644
index 0000000000000..06c1f288f970c
--- /dev/null
+++ b/packages/gatsby/src/utils/__tests__/fixtures/bad-module-import.js
@@ -0,0 +1 @@
+await import(`cheese`)
diff --git a/packages/gatsby/src/utils/__tests__/fixtures/bad-module-require.js b/packages/gatsby/src/utils/__tests__/fixtures/bad-module-require.js
deleted file mode 100644
index 42322ab5537fc..0000000000000
--- a/packages/gatsby/src/utils/__tests__/fixtures/bad-module-require.js
+++ /dev/null
@@ -1 +0,0 @@
-require(`cheese`)
diff --git a/packages/gatsby/src/utils/__tests__/test-require-error.ts b/packages/gatsby/src/utils/__tests__/test-import-error.ts
similarity index 55%
rename from packages/gatsby/src/utils/__tests__/test-require-error.ts
rename to packages/gatsby/src/utils/__tests__/test-import-error.ts
index 5414588916c73..77d47bd0d9a0d 100644
--- a/packages/gatsby/src/utils/__tests__/test-require-error.ts
+++ b/packages/gatsby/src/utils/__tests__/test-import-error.ts
@@ -1,79 +1,86 @@
-import { testRequireError } from "../test-require-error"
+import path from "path"
+import { testImportError } from "../test-import-error"
-describe(`test-require-error`, () => {
- it(`detects require errors`, () => {
+describe(`test-import-error`, () => {
+ it(`detects import errors`, async () => {
try {
- require(`./fixtures/module-does-not-exist`)
+ // @ts-expect-error Module doesn't really exist
+ await import(`./fixtures/module-does-not-exist.js`)
} catch (err) {
- expect(testRequireError(`./fixtures/module-does-not-exist`, err)).toEqual(
- true
- )
+ expect(
+ testImportError(`./fixtures/module-does-not-exist.js`, err)
+ ).toEqual(true)
}
})
- it(`detects require errors when using windows path`, () => {
+
+ it(`detects import errors when using windows path`, async () => {
try {
- require(`.\\fixtures\\module-does-not-exist`)
+ // @ts-expect-error Module doesn't really exist
+ await import(`.\\fixtures\\module-does-not-exist.js`)
} catch (err) {
expect(
- testRequireError(`.\\fixtures\\module-does-not-exist`, err)
+ testImportError(`.\\fixtures\\module-does-not-exist.js`, err)
).toEqual(true)
}
})
- it(`handles windows paths with double slashes`, () => {
+
+ it(`handles windows paths with double slashes`, async () => {
expect(
- testRequireError(
- `C:\\fixtures\\nothing`,
- `Error: Cannot find module 'C:\\\\fixtures\\\\nothing'`
+ testImportError(
+ `C:\\fixtures\\nothing.js`,
+ `Error: Cannot find module 'C:\\\\fixtures\\\\nothing.js'`
)
).toEqual(true)
})
- it(`Only returns true on not found errors for actual module not "not found" errors of requires inside the module`, () => {
+
+ it(`Only returns true on not found errors for actual module not "not found" errors of imports inside the module`, async () => {
try {
- require(`./fixtures/bad-module-require`)
+ await import(path.resolve(`./fixtures/bad-module-import.js`))
} catch (err) {
- expect(testRequireError(`./fixtures/bad-module-require`, err)).toEqual(
+ expect(testImportError(`./fixtures/bad-module-import.js`, err)).toEqual(
false
)
}
})
- it(`ignores other errors`, () => {
+
+ it(`ignores other errors`, async () => {
try {
- require(`./fixtures/bad-module-syntax`)
+ await import(path.resolve(`./fixtures/bad-module-syntax.js`))
} catch (err) {
- expect(testRequireError(`./fixtures/bad-module-syntax`, err)).toEqual(
+ expect(testImportError(`./fixtures/bad-module-syntax.js`, err)).toEqual(
false
)
}
})
describe(`handles error message thrown by Bazel`, () => {
- it(`detects require errors`, () => {
+ it(`detects import errors`, () => {
const bazelModuleNotFoundError =
- new Error(`Error: //:build_bin cannot find module './fixtures/module-does-not-exist' required by '/private/var/tmp/_bazel_misiek/eba1803983a26276494495d851e478a5/execroot/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/bazel-out/darwin-fastbuild/bin/build.runfiles/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/node_modules/gatsby/dist/bootstrap/get-config-file.js'
+ new Error(`Error: //:build_bin cannot find module './fixtures/module-does-not-exist.js' required by '/private/var/tmp/_bazel_misiek/eba1803983a26276494495d851e478a5/execroot/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/bazel-out/darwin-fastbuild/bin/build.runfiles/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/node_modules/gatsby/dist/bootstrap/get-config-file.js'
looked in:
- built-in, relative, absolute, nested node_modules - Error: Cannot find module './fixtures/module-does-not-exist'
- runfiles - Error: Cannot find module '/private/var/tmp/_bazel_misiek/eba1803983a26276494495d851e478a5/execroot/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/bazel-out/darwin-fastbuild/bin/build.runfiles/fixtures/module-does-not-exist'
- node_modules attribute (com_github_bweston92_debug_gatsby_bazel_rules_nodejs/node_modules) - Error: Cannot find module '/private/var/tmp/_bazel_misiek/eba1803983a26276494495d851e478a5/execroot/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/bazel-out/darwin-fastbuild/bin/build.runfiles/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/node_modules/fixtures/module-does-not-exist'`)
+ built-in, relative, absolute, nested node_modules - Error: Cannot find module './fixtures/module-does-not-exist.js'
+ runfiles - Error: Cannot find module '/private/var/tmp/_bazel_misiek/eba1803983a26276494495d851e478a5/execroot/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/bazel-out/darwin-fastbuild/bin/build.runfiles/fixtures/module-does-not-exist.js'
+ node_modules attribute (com_github_bweston92_debug_gatsby_bazel_rules_nodejs/node_modules) - Error: Cannot find module '/private/var/tmp/_bazel_misiek/eba1803983a26276494495d851e478a5/execroot/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/bazel-out/darwin-fastbuild/bin/build.runfiles/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/node_modules/fixtures/module-does-not-exist.js'`)
expect(
- testRequireError(
- `./fixtures/module-does-not-exist`,
+ testImportError(
+ `./fixtures/module-does-not-exist.js`,
bazelModuleNotFoundError
)
).toEqual(true)
})
- it(`detects require errors`, () => {
+ it(`detects import errors`, () => {
const bazelModuleNotFoundError =
- new Error(`Error: //:build_bin cannot find module 'cheese' required by '/private/var/tmp/_bazel_misiek/eba1803983a26276494495d851e478a5/execroot/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/bazel-out/darwin-fastbuild/bin/build.runfiles/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/node_modules/gatsby/dist/bootstrap/fixtures/bad-module-require.js'
+ new Error(`Error: //:build_bin cannot find module 'cheese' required by '/private/var/tmp/_bazel_misiek/eba1803983a26276494495d851e478a5/execroot/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/bazel-out/darwin-fastbuild/bin/build.runfiles/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/node_modules/gatsby/dist/bootstrap/fixtures/bad-module-import.js'
looked in:
built-in, relative, absolute, nested node_modules - Error: Cannot find module 'cheese'
runfiles - Error: Cannot find module '/private/var/tmp/_bazel_misiek/eba1803983a26276494495d851e478a5/execroot/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/bazel-out/darwin-fastbuild/bin/build.runfiles/cheese'
node_modules attribute (com_github_bweston92_debug_gatsby_bazel_rules_nodejs/node_modules) - Error: Cannot find module '/private/var/tmp/_bazel_misiek/eba1803983a26276494495d851e478a5/execroot/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/bazel-out/darwin-fastbuild/bin/build.runfiles/com_github_bweston92_debug_gatsby_bazel_rules_nodejs/node_modules/cheese'`)
expect(
- testRequireError(
- `./fixtures/bad-module-require`,
+ testImportError(
+ `./fixtures/bad-module-import.js`,
bazelModuleNotFoundError
)
).toEqual(false)
diff --git a/packages/gatsby/src/utils/api-runner-node.js b/packages/gatsby/src/utils/api-runner-node.js
index f78512253e541..b55713dfa5f90 100644
--- a/packages/gatsby/src/utils/api-runner-node.js
+++ b/packages/gatsby/src/utils/api-runner-node.js
@@ -28,7 +28,7 @@ const { emitter, store } = require(`../redux`)
const { getNodes, getNode, getNodesByType } = require(`../datastore`)
const { getNodeAndSavePathDependency, loadNodeContent } = require(`./nodes`)
const { getPublicPath } = require(`./get-public-path`)
-const { requireGatsbyPlugin } = require(`./require-gatsby-plugin`)
+const { importGatsbyPlugin } = require(`./import-gatsby-plugin`)
const { getNonGatsbyCodeFrameFormatted } = require(`./stack-trace-utils`)
const { trackBuildError, decorateEvent } = require(`gatsby-telemetry`)
import errorParser from "./api-runner-error-parser"
@@ -293,7 +293,7 @@ const getUninitializedCache = plugin => {
const availableActionsCache = new Map()
let publicPath
const runAPI = async (plugin, api, args, activity) => {
- const gatsbyNode = requireGatsbyPlugin(plugin, `gatsby-node`)
+ const gatsbyNode = await importGatsbyPlugin(plugin, `gatsby-node`)
if (gatsbyNode[api]) {
const parentSpan = args && args.parentSpan
@@ -616,83 +616,86 @@ function apiRunnerNode(api, args = {}, { pluginSource, activity } = {}) {
return null
}
- const gatsbyNode = requireGatsbyPlugin(plugin, `gatsby-node`)
- const pluginName =
- plugin.name === `default-site-plugin` ? `gatsby-node.js` : plugin.name
-
- // TODO: rethink createNode API to handle this better
- if (
- api === `onCreateNode` &&
- gatsbyNode?.shouldOnCreateNode && // Don't bail if this api is not exported
- !gatsbyNode.shouldOnCreateNode(
- { node: args.node },
- plugin.pluginOptions
- )
- ) {
- // Do not try to schedule an async event for this node for this plugin
- return null
- }
-
- return new Promise(resolve => {
- resolve(
- runAPI(plugin, api, { ...args, parentSpan: apiSpan }, activity)
- )
- }).catch(err => {
- decorateEvent(`BUILD_PANIC`, {
- pluginName: `${plugin.name}@${plugin.version}`,
- })
+ return importGatsbyPlugin(plugin, `gatsby-node`).then(gatsbyNode => {
+ const pluginName =
+ plugin.name === `default-site-plugin`
+ ? `gatsby-node.js`
+ : plugin.name
+
+ // TODO: rethink createNode API to handle this better
+ if (
+ api === `onCreateNode` &&
+ gatsbyNode?.shouldOnCreateNode && // Don't bail if this api is not exported
+ !gatsbyNode.shouldOnCreateNode(
+ { node: args.node },
+ plugin.pluginOptions
+ )
+ ) {
+ // Do not try to schedule an async event for this node for this plugin
+ return null
+ }
- const localReporter = getLocalReporter({ activity, reporter })
-
- const file = stackTrace
- .parse(err)
- .find(file => /gatsby-node/.test(file.fileName))
-
- let codeFrame = ``
- const structuredError = errorParser({ err })
-
- if (file) {
- const { fileName, lineNumber: line, columnNumber: column } = file
- const trimmedFileName = fileName.match(/^(async )?(.*)/)[2]
-
- try {
- const code = fs.readFileSync(trimmedFileName, {
- encoding: `utf-8`,
- })
- codeFrame = codeFrameColumns(
- code,
- {
- start: {
- line,
- column,
+ return new Promise(resolve => {
+ resolve(
+ runAPI(plugin, api, { ...args, parentSpan: apiSpan }, activity)
+ )
+ }).catch(err => {
+ decorateEvent(`BUILD_PANIC`, {
+ pluginName: `${plugin.name}@${plugin.version}`,
+ })
+
+ const localReporter = getLocalReporter({ activity, reporter })
+
+ const file = stackTrace
+ .parse(err)
+ .find(file => /gatsby-node/.test(file.fileName))
+
+ let codeFrame = ``
+ const structuredError = errorParser({ err })
+
+ if (file) {
+ const { fileName, lineNumber: line, columnNumber: column } = file
+ const trimmedFileName = fileName.match(/^(async )?(.*)/)[2]
+
+ try {
+ const code = fs.readFileSync(trimmedFileName, {
+ encoding: `utf-8`,
+ })
+ codeFrame = codeFrameColumns(
+ code,
+ {
+ start: {
+ line,
+ column,
+ },
},
- },
- {
- highlightCode: true,
- }
- )
- } catch (_e) {
- // sometimes stack trace point to not existing file
- // particularly when file is transpiled and path actually changes
- // (like pointing to not existing `src` dir or original typescript file)
+ {
+ highlightCode: true,
+ }
+ )
+ } catch (_e) {
+ // sometimes stack trace point to not existing file
+ // particularly when file is transpiled and path actually changes
+ // (like pointing to not existing `src` dir or original typescript file)
+ }
+
+ structuredError.location = {
+ start: { line: line, column: column },
+ }
+ structuredError.filePath = fileName
}
- structuredError.location = {
- start: { line: line, column: column },
+ structuredError.context = {
+ ...structuredError.context,
+ pluginName,
+ api,
+ codeFrame,
}
- structuredError.filePath = fileName
- }
-
- structuredError.context = {
- ...structuredError.context,
- pluginName,
- api,
- codeFrame,
- }
- localReporter.panicOnBuild(structuredError)
+ localReporter.panicOnBuild(structuredError)
- return null
+ return null
+ })
})
},
apiRunPromiseOptions
diff --git a/packages/gatsby/src/utils/import-gatsby-plugin.ts b/packages/gatsby/src/utils/import-gatsby-plugin.ts
new file mode 100644
index 0000000000000..f902c262dfa97
--- /dev/null
+++ b/packages/gatsby/src/utils/import-gatsby-plugin.ts
@@ -0,0 +1,50 @@
+import { resolveJSFilepath } from "../bootstrap/resolve-js-file-path"
+import { preferDefault } from "../bootstrap/prefer-default"
+
+const pluginModuleCache = new Map()
+
+export function setGatsbyPluginCache(
+ plugin: { name: string; resolve: string },
+ module: string,
+ moduleObject: any
+): void {
+ const key = `${plugin.name}/${module}`
+ pluginModuleCache.set(key, moduleObject)
+}
+
+export async function importGatsbyPlugin(
+ plugin: {
+ name: string
+ resolve: string
+ resolvedCompiledGatsbyNode?: string
+ },
+ module: string
+): Promise {
+ const key = `${plugin.name}/${module}`
+
+ let pluginModule = pluginModuleCache.get(key)
+
+ if (!pluginModule) {
+ let importPluginModulePath: string
+
+ if (module === `gatsby-node` && plugin.resolvedCompiledGatsbyNode) {
+ importPluginModulePath = plugin.resolvedCompiledGatsbyNode
+ } else {
+ importPluginModulePath = `${plugin.resolve}/${module}`
+ }
+
+ const pluginFilePath = await resolveJSFilepath({
+ rootDir: process.cwd(),
+ filePath: importPluginModulePath,
+ })
+
+ const rawPluginModule = await import(pluginFilePath)
+
+ // If the module is cjs, the properties we care about are nested under a top-level `default` property
+ pluginModule = preferDefault(rawPluginModule)
+
+ pluginModuleCache.set(key, pluginModule)
+ }
+
+ return pluginModule
+}
diff --git a/packages/gatsby/src/utils/jobs/__tests__/fixtures/.gitignore b/packages/gatsby/src/utils/jobs/__tests__/fixtures/.gitignore
new file mode 100644
index 0000000000000..736e8ae58ad87
--- /dev/null
+++ b/packages/gatsby/src/utils/jobs/__tests__/fixtures/.gitignore
@@ -0,0 +1 @@
+!node_modules
\ No newline at end of file
diff --git a/packages/gatsby/src/utils/jobs/__tests__/fixtures/gatsby-plugin-local/gatsby-worker.js b/packages/gatsby/src/utils/jobs/__tests__/fixtures/gatsby-plugin-local/gatsby-worker.js
new file mode 100644
index 0000000000000..1869565e10eca
--- /dev/null
+++ b/packages/gatsby/src/utils/jobs/__tests__/fixtures/gatsby-plugin-local/gatsby-worker.js
@@ -0,0 +1,4 @@
+module.exports = {
+ TEST_JOB: jest.fn(),
+ NEXT_JOB: jest.fn()
+}
\ No newline at end of file
diff --git a/packages/gatsby/src/utils/jobs/__tests__/fixtures/node_modules/gatsby-plugin-test/gatsby-worker.js b/packages/gatsby/src/utils/jobs/__tests__/fixtures/node_modules/gatsby-plugin-test/gatsby-worker.js
new file mode 100644
index 0000000000000..1869565e10eca
--- /dev/null
+++ b/packages/gatsby/src/utils/jobs/__tests__/fixtures/node_modules/gatsby-plugin-test/gatsby-worker.js
@@ -0,0 +1,4 @@
+module.exports = {
+ TEST_JOB: jest.fn(),
+ NEXT_JOB: jest.fn()
+}
\ No newline at end of file
diff --git a/packages/gatsby/src/utils/jobs/__tests__/manager.js b/packages/gatsby/src/utils/jobs/__tests__/manager.js
index 31670a2eab328..b4d9ce34e7537 100644
--- a/packages/gatsby/src/utils/jobs/__tests__/manager.js
+++ b/packages/gatsby/src/utils/jobs/__tests__/manager.js
@@ -1,13 +1,14 @@
const path = require(`path`)
const _ = require(`lodash`)
const { slash } = require(`gatsby-core-utils`)
-const worker = require(`/node_modules/gatsby-plugin-test/gatsby-worker`)
+const worker = require(`./fixtures/node_modules/gatsby-plugin-test/gatsby-worker`)
const reporter = require(`gatsby-cli/lib/reporter`)
const hasha = require(`hasha`)
const fs = require(`fs-extra`)
const pDefer = require(`p-defer`)
const { uuid } = require(`gatsby-core-utils`)
const timers = require(`timers`)
+const { MESSAGE_TYPES } = require(`../types`)
let WorkerError
let jobManager = null
@@ -25,26 +26,6 @@ jest.mock(`gatsby-cli/lib/reporter`, () => {
}
})
-jest.mock(
- `/node_modules/gatsby-plugin-test/gatsby-worker`,
- () => {
- return {
- TEST_JOB: jest.fn(),
- }
- },
- { virtual: true }
-)
-
-jest.mock(
- `/gatsby-plugin-local/gatsby-worker`,
- () => {
- return {
- TEST_JOB: jest.fn(),
- }
- },
- { virtual: true }
-)
-
jest.mock(`gatsby-core-utils`, () => {
const realCoreUtils = jest.requireActual(`gatsby-core-utils`)
@@ -61,10 +42,14 @@ jest.mock(`hasha`, () => jest.requireActual(`hasha`))
fs.ensureDir = jest.fn().mockResolvedValue(true)
+const nodeModulesPluginPath = slash(
+ path.resolve(__dirname, `fixtures`, `node_modules`, `gatsby-plugin-test`)
+)
+
const plugin = {
name: `gatsby-plugin-test`,
version: `1.0.0`,
- resolve: `/node_modules/gatsby-plugin-test`,
+ resolve: nodeModulesPluginPath,
}
const createMockJob = (overrides = {}) => {
@@ -94,6 +79,7 @@ describe(`Jobs manager`, () => {
beforeEach(() => {
worker.TEST_JOB.mockReset()
+ worker.NEXT_JOB.mockReset()
endActivity.mockClear()
pDefer.mockClear()
uuid.v4.mockClear()
@@ -143,7 +129,7 @@ describe(`Jobs manager`, () => {
plugin: {
name: `gatsby-plugin-test`,
version: `1.0.0`,
- resolve: `/node_modules/gatsby-plugin-test`,
+ resolve: nodeModulesPluginPath,
isLocal: false,
},
})
@@ -180,7 +166,7 @@ describe(`Jobs manager`, () => {
it(`should schedule a job`, async () => {
const { enqueueJob } = jobManager
worker.TEST_JOB.mockReturnValue({ output: `myresult` })
- worker.NEXT_JOB = jest.fn().mockReturnValue({ output: `another result` })
+ worker.NEXT_JOB.mockReturnValue({ output: `another result` })
const mockedJob = createInternalMockJob()
const job1 = enqueueJob(mockedJob)
const job2 = enqueueJob(
@@ -395,6 +381,11 @@ describe(`Jobs manager`, () => {
let originalProcessOn
let originalSend
+ /**
+ * enqueueJob will run some async code before it sends IPC JOB_CREATED message
+ * This promise allow to await until that moment, to make assertions or execute more code
+ */
+ let waitForJobCreatedIPCSend
beforeEach(() => {
process.env.ENABLE_GATSBY_EXTERNAL_JOBS = `true`
listeners = []
@@ -404,7 +395,14 @@ describe(`Jobs manager`, () => {
listeners.push(cb)
}
- process.send = jest.fn()
+ waitForJobCreatedIPCSend = new Promise(resolve => {
+ process.send = jest.fn(msg => {
+ if (msg?.type === MESSAGE_TYPES.JOB_CREATED) {
+ resolve(msg.payload)
+ }
+ })
+ })
+
jest.useFakeTimers()
})
@@ -425,6 +423,8 @@ describe(`Jobs manager`, () => {
enqueueJob(jobArgs)
+ await waitForJobCreatedIPCSend
+
jest.runAllTimers()
expect(process.send).toHaveBeenCalled()
@@ -442,6 +442,9 @@ describe(`Jobs manager`, () => {
const jobArgs = createInternalMockJob()
const promise = enqueueJob(jobArgs)
+
+ await waitForJobCreatedIPCSend
+
jest.runAllTimers()
listeners[0]({
@@ -468,6 +471,8 @@ describe(`Jobs manager`, () => {
const promise = enqueueJob(jobArgs)
+ await waitForJobCreatedIPCSend
+
jest.runAllTimers()
listeners[0]({
@@ -492,6 +497,8 @@ describe(`Jobs manager`, () => {
const jobArgs = createInternalMockJob()
const promise = enqueueJob(jobArgs)
+ await waitForJobCreatedIPCSend
+
listeners[0]({
type: `JOB_NOT_WHITELISTED`,
payload: {
@@ -512,12 +519,16 @@ describe(`Jobs manager`, () => {
it(`should run the worker locally when it's a local plugin`, async () => {
jest.useRealTimers()
- const worker = require(`/gatsby-plugin-local/gatsby-worker`)
+ const localPluginPath = slash(
+ path.resolve(__dirname, `fixtures`, `gatsby-plugin-local`)
+ )
+ const localPluginWorkerPath = path.join(localPluginPath, `gatsby-worker`)
+ const worker = require(localPluginWorkerPath)
const { enqueueJob, createInternalJob } = jobManager
const jobArgs = createInternalJob(createMockJob(), {
name: `gatsby-plugin-local`,
version: `1.0.0`,
- resolve: `/gatsby-plugin-local`,
+ resolve: localPluginPath,
})
await expect(enqueueJob(jobArgs)).resolves.toBeUndefined()
diff --git a/packages/gatsby/src/utils/jobs/manager.ts b/packages/gatsby/src/utils/jobs/manager.ts
index b820cd4d1d79c..20ecb52773d89 100644
--- a/packages/gatsby/src/utils/jobs/manager.ts
+++ b/packages/gatsby/src/utils/jobs/manager.ts
@@ -16,7 +16,7 @@ import {
IJobNotWhitelisted,
WorkerError,
} from "./types"
-import { requireGatsbyPlugin } from "../require-gatsby-plugin"
+import { importGatsbyPlugin } from "../import-gatsby-plugin"
type IncomingMessages = IJobCompletedMessage | IJobFailed | IJobNotWhitelisted
@@ -157,30 +157,31 @@ function runJob(
): Promise> {
const { plugin } = job
try {
- const worker = requireGatsbyPlugin(plugin, `gatsby-worker`)
- if (!worker[job.name]) {
- throw new Error(`No worker function found for ${job.name}`)
- }
-
- if (!forceLocal && !job.plugin.isLocal && hasExternalJobsEnabled()) {
- if (process.send) {
- if (!isListeningForMessages) {
- isListeningForMessages = true
- listenForJobMessages()
- }
+ return importGatsbyPlugin(plugin, `gatsby-worker`).then(worker => {
+ if (!worker[job.name]) {
+ throw new Error(`No worker function found for ${job.name}`)
+ }
- return runExternalWorker(job)
- } else {
- // only show the offloading warning once
- if (!hasShownIPCDisabledWarning) {
- hasShownIPCDisabledWarning = true
- reporter.warn(
- `Offloading of a job failed as IPC could not be detected. Running job locally.`
- )
+ if (!forceLocal && !job.plugin.isLocal && hasExternalJobsEnabled()) {
+ if (process.send) {
+ if (!isListeningForMessages) {
+ isListeningForMessages = true
+ listenForJobMessages()
+ }
+
+ return runExternalWorker(job)
+ } else {
+ // only show the offloading warning once
+ if (!hasShownIPCDisabledWarning) {
+ hasShownIPCDisabledWarning = true
+ reporter.warn(
+ `Offloading of a job failed as IPC could not be detected. Running job locally.`
+ )
+ }
}
}
- }
- return runLocalWorker(worker[job.name], job)
+ return runLocalWorker(worker[job.name], job)
+ })
} catch (err) {
throw new Error(
`We couldn't find a gatsby-worker.js(${plugin.resolve}/gatsby-worker.js) file for ${plugin.name}@${plugin.version}`
diff --git a/packages/gatsby/src/utils/module-resolver.ts b/packages/gatsby/src/utils/module-resolver.ts
index 84aae171261c8..1736f2ae5df4d 100644
--- a/packages/gatsby/src/utils/module-resolver.ts
+++ b/packages/gatsby/src/utils/module-resolver.ts
@@ -1,7 +1,7 @@
import * as fs from "fs"
import enhancedResolve, { CachedInputFileSystem } from "enhanced-resolve"
-type ModuleResolver = (modulePath: string) => string | false
+export type ModuleResolver = (modulePath: string) => string | false
type ResolveType = (context?: any, path?: any, request?: any) => string | false
export const resolveModule: ModuleResolver = modulePath => {
diff --git a/packages/gatsby/src/utils/parcel/compile-gatsby-files.ts b/packages/gatsby/src/utils/parcel/compile-gatsby-files.ts
index 613b27a445065..a2ad269033d59 100644
--- a/packages/gatsby/src/utils/parcel/compile-gatsby-files.ts
+++ b/packages/gatsby/src/utils/parcel/compile-gatsby-files.ts
@@ -74,7 +74,11 @@ export async function compileGatsbyFiles(
const { name } = path.parse(file)
// Of course, allow valid gatsby-node files
- if (file === `gatsby-node.js` || file === `gatsby-node.ts`) {
+ if (
+ file === `gatsby-node.js` ||
+ file === `gatsby-node.mjs` ||
+ file === `gatsby-node.ts`
+ ) {
break
}
diff --git a/packages/gatsby/src/utils/require-gatsby-plugin.ts b/packages/gatsby/src/utils/require-gatsby-plugin.ts
deleted file mode 100644
index 8167bf22cae64..0000000000000
--- a/packages/gatsby/src/utils/require-gatsby-plugin.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-const pluginModuleCache = new Map()
-
-export function setGatsbyPluginCache(
- plugin: { name: string; resolve: string },
- module: string,
- moduleObject: any
-): void {
- const key = `${plugin.name}/${module}`
- pluginModuleCache.set(key, moduleObject)
-}
-
-export function requireGatsbyPlugin(
- plugin: {
- name: string
- resolve: string
- resolvedCompiledGatsbyNode?: string
- },
- module: string
-): any {
- const key = `${plugin.name}/${module}`
-
- let pluginModule = pluginModuleCache.get(key)
- if (!pluginModule) {
- pluginModule = require(module === `gatsby-node` &&
- plugin.resolvedCompiledGatsbyNode
- ? plugin.resolvedCompiledGatsbyNode
- : `${plugin.resolve}/${module}`)
- pluginModuleCache.set(key, pluginModule)
- }
- return pluginModule
-}
diff --git a/packages/gatsby/src/utils/test-require-error.ts b/packages/gatsby/src/utils/test-import-error.ts
similarity index 64%
rename from packages/gatsby/src/utils/test-require-error.ts
rename to packages/gatsby/src/utils/test-import-error.ts
index a5bc52b3d214d..f1ef512d6d98f 100644
--- a/packages/gatsby/src/utils/test-require-error.ts
+++ b/packages/gatsby/src/utils/test-import-error.ts
@@ -1,7 +1,5 @@
-// This module is also copied into the .cache directory some modules copied there
-// from cache-dir can also use this module.
-export const testRequireError = (moduleName: string, err: any): boolean => {
- // PnP will return the following code when a require is allowed per the
+export const testImportError = (moduleName: string, err: any): boolean => {
+ // PnP will return the following code when an import is allowed per the
// dependency tree rules but the requested file doesn't exist
if (
err.code === `QUALIFIED_PATH_RESOLUTION_FAILED` ||
@@ -10,6 +8,7 @@ export const testRequireError = (moduleName: string, err: any): boolean => {
return true
}
const regex = new RegExp(
+ // stderr will show ModuleNotFoundError, but Error is correct since we toString below
`Error:\\s(\\S+\\s)?[Cc]annot find module\\s.${moduleName.replace(
/[-/\\^$*+?.()|[\]{}]/g,
`\\$&`