Skip to content

Commit

Permalink
fix(gatsby): Improve get-config-file error handling (#35776)
Browse files Browse the repository at this point in the history
  • Loading branch information
tyhopp committed May 31, 2022
1 parent c3a3f68 commit 160bbf0
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 14 deletions.
7 changes: 7 additions & 0 deletions packages/gatsby-cli/src/structured-errors/error-map.ts
Expand Up @@ -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 =>
[
Expand Down
@@ -0,0 +1,7 @@
module.exports = {
siteMetadata: {
title: `compiled`,
siteUrl: `https://www.yourdomain.tld`,
},
plugins: [],
}
@@ -0,0 +1,7 @@
module.exports = {
siteMetadata: {
title: `uncompiled`,
siteUrl: `https://www.yourdomain.tld`,
},
plugins: [],
}
@@ -0,0 +1,7 @@
module.exports = {
siteMetadata: {
title: `near-match`,
siteUrl: `https://www.yourdomain.tld`,
},
plugins: [],
}
@@ -0,0 +1,7 @@
module.exports = {
siteMetadata: {
title: `in-src`,
siteUrl: `https://www.yourdomain.tld`,
},
plugins: [],
}
@@ -0,0 +1,11 @@
import type { GatsbyConfig } from "gatsby"

const config: GatsbyConfig = {
siteMetadata: {
title: `ts`,
siteUrl: `https://www.yourdomain.tld`,
},
plugins: [],
}

export default config
@@ -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: [],
}
164 changes: 164 additions & 0 deletions 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<typeof path.join>

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`,
},
})
})
})
61 changes: 47 additions & 14 deletions packages/gatsby/src/bootstrap/get-config-file.ts
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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`,
Expand All @@ -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`,
Expand All @@ -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,
Expand All @@ -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: {
Expand Down

0 comments on commit 160bbf0

Please sign in to comment.