diff --git a/packages/gatsby-cli/src/structured-errors/error-map.ts b/packages/gatsby-cli/src/structured-errors/error-map.ts index e78eaf62ac7d5..787bee8595e7a 100644 --- a/packages/gatsby-cli/src/structured-errors/error-map.ts +++ b/packages/gatsby-cli/src/structured-errors/error-map.ts @@ -370,6 +370,13 @@ const errors = { level: Level.ERROR, category: ErrorCategory.USER, }, + "10127": { + text: (context): string => + `Your "${context.configName}.ts" file failed to compile to "${context.configName}.js. Please run "gatsby clean" and try again.\n\nIf the issue persists, please open an issue with a reproduction at https://github.com/gatsbyjs/gatsby/issues/new for more help."`, + type: Type.CONFIG, + level: Level.ERROR, + category: ErrorCategory.USER, + }, "10226": { text: (context): string => [ diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/compiled-dir/compiled/gatsby-config.js b/packages/gatsby/src/bootstrap/__mocks__/get-config/compiled-dir/compiled/gatsby-config.js new file mode 100644 index 0000000000000..902eb91623cdd --- /dev/null +++ b/packages/gatsby/src/bootstrap/__mocks__/get-config/compiled-dir/compiled/gatsby-config.js @@ -0,0 +1,7 @@ +module.exports = { + siteMetadata: { + title: `compiled`, + siteUrl: `https://www.yourdomain.tld`, + }, + plugins: [], +} diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/gatsby-config.js b/packages/gatsby/src/bootstrap/__mocks__/get-config/gatsby-config.js new file mode 100644 index 0000000000000..d2d39c7540947 --- /dev/null +++ b/packages/gatsby/src/bootstrap/__mocks__/get-config/gatsby-config.js @@ -0,0 +1,7 @@ +module.exports = { + siteMetadata: { + title: `uncompiled`, + siteUrl: `https://www.yourdomain.tld`, + }, + plugins: [], +} diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/near-match-dir/gatsby-confi.js b/packages/gatsby/src/bootstrap/__mocks__/get-config/near-match-dir/gatsby-confi.js new file mode 100644 index 0000000000000..1afabc2b4caf8 --- /dev/null +++ b/packages/gatsby/src/bootstrap/__mocks__/get-config/near-match-dir/gatsby-confi.js @@ -0,0 +1,7 @@ +module.exports = { + siteMetadata: { + title: `near-match`, + siteUrl: `https://www.yourdomain.tld`, + }, + plugins: [], +} diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/src-dir/src/gatsby-config.js b/packages/gatsby/src/bootstrap/__mocks__/get-config/src-dir/src/gatsby-config.js new file mode 100644 index 0000000000000..1b131c74660d2 --- /dev/null +++ b/packages/gatsby/src/bootstrap/__mocks__/get-config/src-dir/src/gatsby-config.js @@ -0,0 +1,7 @@ +module.exports = { + siteMetadata: { + title: `in-src`, + siteUrl: `https://www.yourdomain.tld`, + }, + plugins: [], +} diff --git a/packages/gatsby/src/bootstrap/__mocks__/get-config/ts-dir/gatsby-config.ts b/packages/gatsby/src/bootstrap/__mocks__/get-config/ts-dir/gatsby-config.ts new file mode 100644 index 0000000000000..88954db73357d --- /dev/null +++ b/packages/gatsby/src/bootstrap/__mocks__/get-config/ts-dir/gatsby-config.ts @@ -0,0 +1,11 @@ +import type { GatsbyConfig } from "gatsby" + +const config: GatsbyConfig = { + siteMetadata: { + title: `ts`, + siteUrl: `https://www.yourdomain.tld`, + }, + plugins: [], +} + +export default config 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/user-require-dir/compiled/gatsby-config.js new file mode 100644 index 0000000000000..ea1e68c533fb5 --- /dev/null +++ b/packages/gatsby/src/bootstrap/__mocks__/get-config/user-require-dir/compiled/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/__tests__/get-config-file.ts b/packages/gatsby/src/bootstrap/__tests__/get-config-file.ts new file mode 100644 index 0000000000000..8ab13985538d0 --- /dev/null +++ b/packages/gatsby/src/bootstrap/__tests__/get-config-file.ts @@ -0,0 +1,164 @@ +import path from "path" +import { isNearMatch, getConfigFile } from "../get-config-file" +import { testRequireError } from "../../utils/test-require-error" +import reporter from "gatsby-cli/lib/reporter" + +jest.mock(`path`, () => { + const actual = jest.requireActual(`path`) + return { + ...actual, + join: jest.fn((...arg) => actual.join(...arg)), + } +}) + +jest.mock(`../../utils/test-require-error`, () => { + return { + testRequireError: jest.fn(), + } +}) + +jest.mock(`../../utils/parcel/compile-gatsby-files`, () => { + const actual = jest.requireActual(`../../utils/parcel/compile-gatsby-files`) + return { + ...actual, + COMPILED_CACHE_DIR: `compiled`, // .cache is git ignored + } +}) + +jest.mock(`gatsby-cli/lib/reporter`, () => { + return { + panic: jest.fn(), + } +}) + +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 +> + +describe(`isNearMatch`, () => { + it(`should NOT find a near match if file name is undefined`, () => { + const nearMatchA = isNearMatch(undefined, `gatsby-config`, 1) + expect(nearMatchA).toBeFalse() + }) + + it(`should calculate near matches based on distance`, () => { + const nearMatchA = isNearMatch(`gatsby-config`, `gatsby-conf`, 2) + const nearMatchB = isNearMatch(`gatsby-config`, `gatsby-configur`, 2) + expect(nearMatchA).toBeTrue() + expect(nearMatchB).toBeTrue() + }) +}) + +// 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 nearMatchDir = `${dir}/near-match-dir` +const srcDir = `${dir}/src-dir` + +describe(`getConfigFile`, () => { + beforeEach(() => { + reporterPanicMock.mockClear() + }) + + it(`should get an uncompiled gatsby-config.js`, async () => { + const { configModule, configFilePath } = await getConfigFile( + dir, + `gatsby-config` + ) + expect(configFilePath).toBe(path.join(dir, `gatsby-config.js`)) + expect(configModule.siteMetadata.title).toBe(`uncompiled`) + }) + + it(`should get a compiled gatsby-config.js`, async () => { + const { configModule, configFilePath } = await getConfigFile( + compiledDir, + `gatsby-config` + ) + expect(configFilePath).toBe( + path.join(compiledDir, `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`) + + expect(reporterPanicMock).toBeCalledWith({ + id: `11902`, + error: expect.toBeObject(), + context: { + configName: `gatsby-config`, + message: expect.toBeString(), + }, + }) + }) + + it(`should handle non-require errors`, async () => { + testRequireErrorMock.mockImplementationOnce(() => false) + + await getConfigFile(nearMatchDir, `gatsby-config`) + + expect(reporterPanicMock).toBeCalledWith({ + id: `10123`, + error: expect.toBeObject(), + context: { + configName: `gatsby-config`, + message: expect.toBeString(), + }, + }) + }) + + 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) + + await getConfigFile(tsDir, `gatsby-config`) + + expect(reporterPanicMock).toBeCalledWith({ + id: `10127`, + error: expect.toBeObject(), + context: { + configName: `gatsby-config`, + }, + }) + }) + + it(`should handle near matches`, async () => { + testRequireErrorMock.mockImplementationOnce(() => true) + + await getConfigFile(nearMatchDir, `gatsby-config`) + + expect(reporterPanicMock).toBeCalledWith({ + id: `10124`, + error: expect.toBeObject(), + context: { + configName: `gatsby-config`, + nearMatch: `gatsby-confi.js`, + }, + }) + }) + + it(`should handle gatsby config incorrectly located in src dir`, async () => { + testRequireErrorMock.mockImplementationOnce(() => true) + + await getConfigFile(srcDir, `gatsby-config`) + + expect(reporterPanicMock).toBeCalledWith({ + id: `10125`, + context: { + configName: `gatsby-config`, + }, + }) + }) +}) diff --git a/packages/gatsby/src/bootstrap/get-config-file.ts b/packages/gatsby/src/bootstrap/get-config-file.ts index dcca8f43cf2dd..a04a10f3232f8 100644 --- a/packages/gatsby/src/bootstrap/get-config-file.ts +++ b/packages/gatsby/src/bootstrap/get-config-file.ts @@ -6,7 +6,7 @@ import path from "path" import { sync as existsSync } from "fs-exists-cached" import { COMPILED_CACHE_DIR } from "../utils/parcel/compile-gatsby-files" -function isNearMatch( +export function isNearMatch( fileName: string | undefined, configName: string, distance: number @@ -27,8 +27,8 @@ export async function getConfigFile( let configFilePath = `` let configModule: any + // Attempt to find compiled gatsby-config.js in .cache/compiled/gatsby-config.js try { - // Try .cache/compiled/gatsby-config first configPath = path.join(`${siteDirectory}/${COMPILED_CACHE_DIR}`, configName) configFilePath = require.resolve(configPath) configModule = require(configFilePath) @@ -42,6 +42,7 @@ export async function getConfigFile( const isThisFileRequireError = outerError?.requireStack?.[0]?.includes(`get-config-file`) ?? true + // User's module require error inside gatsby-config.js if (!(isModuleNotFoundError && isThisFileRequireError)) { report.panic({ id: `11902`, @@ -53,19 +54,14 @@ export async function getConfigFile( }) } - // Fallback to regular rootDir gatsby-config + // 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) { - // Only then hard fail - const nearMatch = await fs.readdir(siteDirectory).then(files => - files.find(file => { - const fileName = file.split(siteDirectory).pop() - return isNearMatch(fileName, configName, distance) - }) - ) + // Some other error that is not a require error if (!testRequireError(configPath, innerError)) { report.panic({ id: `10123`, @@ -75,7 +71,43 @@ export async function getConfigFile( message: innerError.message, }, }) - } else if (nearMatch) { + } + + 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) { report.panic({ id: `10124`, error: innerError, @@ -84,9 +116,10 @@ export async function getConfigFile( nearMatch, }, }) - } else if ( - existsSync(path.join(siteDirectory, `src`, configName + `.js`)) - ) { + } + + // gatsby-config.js is incorrectly located in src/gatsby-config.js + if (existsSync(path.join(siteDirectory, `src`, configName + `.js`))) { report.panic({ id: `10125`, context: {