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, `\\$&`