diff --git a/package.json b/package.json index 0ca2126f2fcd7..fd9fb2d822933 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@types/fs-extra": "^8.0.1", "@types/got": "^9.6.9", "@types/jest": "^24.0.23", + "@types/joi": "^14.3.4", "@types/lodash": "^4.14.149", "@types/node": "^12.12.11", "@types/webpack": "^4.41.0", diff --git a/packages/gatsby-cli/src/reporter/index.js b/packages/gatsby-cli/src/reporter/index.js index 8449388a7031e..f9b9903fa9434 100644 --- a/packages/gatsby-cli/src/reporter/index.js +++ b/packages/gatsby-cli/src/reporter/index.js @@ -48,7 +48,7 @@ const tracer = require(`opentracing`).globalTracer() const { getErrorFormatter } = require(`./errors`) const { getStore } = require(`./redux`) -const constructError = require(`../structured-errors/construct-error`) +import constructError from "../structured-errors/construct-error" const errorFormatter = getErrorFormatter() diff --git a/packages/gatsby-cli/src/structured-errors/__tests__/construct-error.js b/packages/gatsby-cli/src/structured-errors/__tests__/construct-error.ts similarity index 72% rename from packages/gatsby-cli/src/structured-errors/__tests__/construct-error.js rename to packages/gatsby-cli/src/structured-errors/__tests__/construct-error.ts index dbf89cbae5050..a5318b691e1f2 100644 --- a/packages/gatsby-cli/src/structured-errors/__tests__/construct-error.js +++ b/packages/gatsby-cli/src/structured-errors/__tests__/construct-error.ts @@ -1,18 +1,21 @@ -const constructError = require(`../construct-error`) +import constructError from "../construct-error" let log let processExit beforeEach(() => { log = jest.spyOn(console, `log`).mockImplementation(() => {}) - processExit = jest.spyOn(process, `exit`).mockImplementation(() => {}) + processExit = ((jest.spyOn( + process, + `exit` + ) as unknown) as jest.Mock).mockImplementation(() => {}) log.mockReset() processExit.mockReset() }) afterAll(() => { - console.log.mockClear() - process.exit.mockClear() + ;(console.log as jest.Mock).mockClear() + ;((process.exit as unknown) as jest.Mock).mockClear() }) test(`it exits on invalid error schema`, () => { diff --git a/packages/gatsby-cli/src/structured-errors/__tests__/error-map.js b/packages/gatsby-cli/src/structured-errors/__tests__/error-map.ts similarity index 88% rename from packages/gatsby-cli/src/structured-errors/__tests__/error-map.js rename to packages/gatsby-cli/src/structured-errors/__tests__/error-map.ts index acb6ffe24fbef..ce7fc70870b57 100644 --- a/packages/gatsby-cli/src/structured-errors/__tests__/error-map.js +++ b/packages/gatsby-cli/src/structured-errors/__tests__/error-map.ts @@ -1,4 +1,4 @@ -const { errorMap, defaultError } = require(`../error-map`) +import { errorMap, defaultError } from "../error-map" test(`it defaults to generic error`, () => { expect(defaultError).toEqual( diff --git a/packages/gatsby-cli/src/structured-errors/__tests__/error-schema.js b/packages/gatsby-cli/src/structured-errors/__tests__/error-schema.ts similarity index 86% rename from packages/gatsby-cli/src/structured-errors/__tests__/error-schema.js rename to packages/gatsby-cli/src/structured-errors/__tests__/error-schema.ts index cc594a66acd31..d49787a1dc45a 100644 --- a/packages/gatsby-cli/src/structured-errors/__tests__/error-schema.js +++ b/packages/gatsby-cli/src/structured-errors/__tests__/error-schema.ts @@ -1,4 +1,4 @@ -const schema = require(`../error-schema`) +import schema from "../error-schema" test(`throws invalid on an invalid error`, () => { expect(schema.validate({ lol: `true` })).rejects.toBeDefined() diff --git a/packages/gatsby-cli/src/structured-errors/construct-error.js b/packages/gatsby-cli/src/structured-errors/construct-error.js deleted file mode 100644 index ea6decf5be686..0000000000000 --- a/packages/gatsby-cli/src/structured-errors/construct-error.js +++ /dev/null @@ -1,39 +0,0 @@ -const Joi = require(`@hapi/joi`) -const stackTrace = require(`stack-trace`) -const errorSchema = require(`./error-schema`) -const { errorMap, defaultError } = require(`./error-map`) -const { sanitizeStructuredStackTrace } = require(`../reporter/errors`) - -// Merge partial error details with information from the errorMap -// Validate the constructed object against an error schema -// TODO: 'details' is not a descriptive name -const constructError = ({ details: { id, ...otherDetails } }) => { - const result = (id && errorMap[id]) || defaultError - - // merge - const structuredError = { - context: {}, - ...otherDetails, - ...result, - text: result.text(otherDetails.context), - stack: otherDetails.error - ? sanitizeStructuredStackTrace(stackTrace.parse(otherDetails.error)) - : null, - docsUrl: result.docsUrl || `https://gatsby.dev/issue-how-to`, - } - - if (id) { - structuredError.code = id - } - - // validate - const { error } = Joi.validate(structuredError, errorSchema) - if (error !== null) { - console.log(`Failed to validate error`, error) - process.exit(1) - } - - return structuredError -} - -module.exports = constructError diff --git a/packages/gatsby-cli/src/structured-errors/construct-error.ts b/packages/gatsby-cli/src/structured-errors/construct-error.ts new file mode 100644 index 0000000000000..7269f38cd4581 --- /dev/null +++ b/packages/gatsby-cli/src/structured-errors/construct-error.ts @@ -0,0 +1,75 @@ +import Joi from "@hapi/joi" +import stackTrace from "stack-trace" +import errorSchema from "./error-schema" +import { errorMap, defaultError, IErrorMapEntry, ErrorId } from "./error-map" +import { sanitizeStructuredStackTrace } from "../reporter/errors" + +interface IConstructError { + details: { + id?: ErrorId + context?: Record + error?: string + [key: string]: unknown + } +} + +interface ILocationPosition { + line: number + column: number +} + +interface IStructuredError { + code?: string + text: string + stack: { + fileName: string + functionName?: string + lineNumber?: number + columnNumber?: number + }[] + filePath?: string + location?: { + start: ILocationPosition + end?: ILocationPosition + } + error?: unknown + group?: string + level: IErrorMapEntry["level"] + type?: IErrorMapEntry["type"] + docsUrl?: string +} + +// Merge partial error details with information from the errorMap +// Validate the constructed object against an error schema +const constructError = ({ + details: { id, ...otherDetails }, +}: IConstructError): IStructuredError => { + const result: IErrorMapEntry = (id && errorMap[id]) || defaultError + + // merge + const structuredError: IStructuredError = { + context: {}, + ...otherDetails, + ...result, + text: result.text(otherDetails.context), + stack: otherDetails.error + ? sanitizeStructuredStackTrace(stackTrace.parse(otherDetails.error)) + : null, + docsUrl: result.docsUrl || `https://gatsby.dev/issue-how-to`, + } + + if (id) { + structuredError.code = id + } + + // validate + const { error } = Joi.validate(structuredError, errorSchema) + if (error !== null) { + console.log(`Failed to validate error`, error) + process.exit(1) + } + + return structuredError +} + +export default constructError diff --git a/packages/gatsby-cli/src/structured-errors/error-map.js b/packages/gatsby-cli/src/structured-errors/error-map.ts similarity index 72% rename from packages/gatsby-cli/src/structured-errors/error-map.js rename to packages/gatsby-cli/src/structured-errors/error-map.ts index 5272fb2289c9c..a8e9c78fdd465 100644 --- a/packages/gatsby-cli/src/structured-errors/error-map.js +++ b/packages/gatsby-cli/src/structured-errors/error-map.ts @@ -1,76 +1,98 @@ -const { stripIndent, stripIndents } = require(`common-tags`) +import { stripIndent, stripIndents } from "common-tags" -const optionalGraphQLInfo = context => +interface IOptionalGraphQLInfoContext { + codeFrame?: string + filePath?: string + urlPath?: string + plugin?: string +} + +enum Level { + ERROR = `ERROR`, + WARNING = `WARNING`, + INFO = `INFO`, + DEBUG = `DEBUG`, +} + +enum Type { + GRAPHQL = `GRAPHQL`, + CONFIG = `CONFIG`, + WEBPACK = `WEBPACK`, + PLUGIN = `PLUGIN`, +} + +const optionalGraphQLInfo = (context: IOptionalGraphQLInfoContext): string => `${context.codeFrame ? `\n\n${context.codeFrame}` : ``}${ context.filePath ? `\n\nFile path: ${context.filePath}` : `` }${context.urlPath ? `\nUrl path: ${context.urlPath}` : ``}${ context.plugin ? `\nPlugin: ${context.plugin}` : `` }` -const errorMap = { +const errors = { "": { - text: context => { + text: (context): string => { const sourceMessage = context.sourceMessage ? context.sourceMessage : `There was an error` return sourceMessage }, - level: `ERROR`, + level: Level.ERROR, }, "95312": { - text: context => + text: (context): string => `"${context.ref}" is not available during server side rendering.`, - level: `ERROR`, + level: Level.ERROR, docsUrl: `https://gatsby.dev/debug-html`, }, "95313": { - text: context => + text: (context): string => `Building static HTML failed${ context.errorPath ? ` for path "${context.errorPath}"` : `` }`, - level: `ERROR`, + level: Level.ERROR, docsUrl: `https://gatsby.dev/debug-html`, }, "98123": { - text: context => `${context.stageLabel} failed\n\n${context.message}`, - type: `WEBPACK`, - level: `ERROR`, + text: (context): string => + `${context.stageLabel} failed\n\n${context.message}`, + type: Type.WEBPACK, + level: Level.ERROR, }, "85901": { - text: context => + text: (context): string => stripIndent(` There was an error in your GraphQL query:\n\n${ context.sourceMessage }${optionalGraphQLInfo(context)}`), - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, // Deprecated "85907": { - text: context => + text: (context): string => `There was an error in your GraphQL query:\n\n${context.message}`, - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, "85908": { - text: context => { + text: (context): string => { const closestFragment = context.closestFragment ? `\n\nDid you mean to use ` + `"${context.closestFragment}"?` : `` return `There was an error in your GraphQL query:\n\nThe fragment "${context.fragmentName}" does not exist.\n\n${context.codeFrame}${closestFragment}` }, - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, // Deprecated "85909": { - text: context => context.sourceMessage, - type: `GRAPHQL`, - level: `ERROR`, + text: (context): string => context.sourceMessage, + type: Type.GRAPHQL, + level: Level.ERROR, }, "85910": { - text: context => + text: (context): string => stripIndents(` Multiple "root" queries found: "${context.name}" and "${context.otherName}". Only the first ("${context.otherName}") will be registered. @@ -86,12 +108,12 @@ const errorMap = { This can happen when you use two page/static queries in one file. Please combine those into one query. If you're defining multiple components (each with a static query) in one file, you'll need to move each component to its own file. `), - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, docsUrl: `https://www.gatsbyjs.org/docs/graphql/`, }, "85911": { - text: context => + text: (context): string => stripIndent(` There was a problem parsing "${context.filePath}"; any GraphQL fragments or queries in this file were not processed. @@ -99,59 +121,60 @@ const errorMap = { This may indicate a syntax error in the code, or it may be a file type that Gatsby does not know how to parse. `), - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, "85912": { - text: context => `Failed to parse preprocessed file ${context.filePath}`, - type: `GRAPHQL`, - level: `ERROR`, + text: (context): string => + `Failed to parse preprocessed file ${context.filePath}`, + type: Type.GRAPHQL, + level: Level.ERROR, }, "85913": { - text: context => + text: (context): string => `There was a problem reading the file: ${context.filePath}`, - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, "85914": { - text: context => + text: (context): string => `There was a problem reading the file: ${context.filePath}`, - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, // default parsing error "85915": { - text: context => + text: (context): string => `There was a problem parsing the GraphQL query in file: ${context.filePath}`, - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, "85916": { - text: context => + text: (context): string => `String interpolation is not allowed in graphql tag:\n\n${context.codeFrame}`, - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, "85917": { - text: context => + text: (context): string => `Unexpected empty graphql tag${ context.codeFrame ? `\n\n${context.codeFrame}` : `` }`, - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, "85918": { - text: context => + text: (context): string => stripIndent(` GraphQL syntax error in query:\n\n${context.sourceMessage}${ context.codeFrame ? `\n\n${context.codeFrame}` : `` }`), - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, // Duplicate fragment "85919": { - text: context => + text: (context): string => stripIndent(` Found two different GraphQL fragments with identical name "${context.fragmentName}". Fragment names must be unique @@ -161,12 +184,12 @@ const errorMap = { File: ${context.rightFragment.filePath} ${context.rightFragment.codeFrame} `), - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, // Undefined variables in Queries "85920": { - text: context => { + text: (context): string => { const generalMessage = stripIndents(`You might have a typo in the variable name "${context.variableName}" or you didn't provide the variable via context to this page query. Have a look at the docs to learn how to add data to context: https://www.gatsbyjs.org/docs/page-query/#how-to-add-query-variables-to-a-page-query`) @@ -183,29 +206,29 @@ const errorMap = { context )}\n\n${generalMessage}\n\n${staticQueryMessage}`) }, - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, "85921": { - text: context => + text: (context): string => `There was an error in your GraphQL query:\n\n${context.sourceMessage}\n\nIf you're e.g. filtering for specific nodes make sure that you choose the correct field (that has the same type "${context.inputType}") or adjust the context variable to the type "${context.expectedType}".`, - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, "85922": { - text: context => + text: (context): string => `There was an error in your GraphQL query:\n\n${context.sourceMessage}\n\nThis can happen if you e.g. accidentally added { } to the field "${context.fieldName}". If you didn't expect "${context.fieldName}" to be of type "${context.fieldType}" make sure that your input source and/or plugin is correct.`, - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, "85923": { - text: context => + text: (context): string => `There was an error in your GraphQL query:\n\n${context.sourceMessage}\n\nIf you don't expect "${context.field}" to exist on the type "${context.type}" it is most likely a typo.\nHowever, if you expect "${context.field}" to exist there are a couple of solutions to common problems:\n\n- If you added a new data source and/or changed something inside gatsby-node.js/gatsby-config.js, please try a restart of your development server\n- The field might be accessible in another subfield, please try your query in GraphiQL and use the GraphiQL explorer to see which fields you can query and what shape they have\n- You want to optionally use your field "${context.field}" and right now it is not used anywhere. Therefore Gatsby can't infer the type and add it to the GraphQL schema. A quick fix is to add a least one entry with that field ("dummy content")\n\nIt is recommended to explicitly type your GraphQL schema if you want to use optional fields. This way you don't have to add the mentioned "dummy content". Visit our docs to learn how you can define the schema for "${context.type}":\nhttps://www.gatsbyjs.org/docs/schema-customization/#creating-type-definitions`, - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, "85924": { - text: context => + text: (context): string => `There was an error in your GraphQL query:\n\n${ context.sourceMessage }\n\nThis can happen when you or a plugin/theme explicitly defined the GraphQL schema for this GraphQL object type via the schema customization API and "${ @@ -213,11 +236,11 @@ const errorMap = { }" doesn't match the (scalar) type of "${ context.type }".${optionalGraphQLInfo(context)}`, - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, }, "85925": { - text: context => + text: (context): string => `There was an error in your GraphQL query:\n\n${ context.sourceMessage }\n\nThe field "${ @@ -225,40 +248,52 @@ const errorMap = { }" was explicitly defined as non-nullable via the schema customization API (by yourself or a plugin/theme). This means that this field is not optional and you have to define a value. If this is not your desired behavior and you defined the schema yourself, go to "createTypes" in gatsby-node.js. If you're using a plugin/theme, you can learn more here on how to fix field types:\nhttps://www.gatsbyjs.org/docs/schema-customization/#fixing-field-types${optionalGraphQLInfo( context )}`, - type: `GRAPHQL`, - level: `ERROR`, + type: Type.GRAPHQL, + level: Level.ERROR, + }, + "85926": { + text: (context): string => + `There was an error in your GraphQL query:\n\n${context.sourceMessage}\n\nThis can happen when you used graphql\`{ ...yourQuery }\` instead of graphql(\`{ ...yourQuery }\`) inside gatsby-node.js\n\nYou can't use the template literal function you're used to (from page queries) and rather have to call graphql() as a normal function.`, + type: Type.GRAPHQL, + level: Level.ERROR, + }, + "85927": { + text: (context): string => + `There was an error in your GraphQL query:\n\n${context.sourceMessage}\n\nSee if ${context.variable} has a typo or ${context.operation} doesn't actually require this variable.`, + type: Type.GRAPHQL, + level: Level.ERROR, }, // Config errors "10123": { - text: context => + text: (context): string => `We encountered an error while trying to load your site's ${context.configName}. Please fix the error and try again.`, - type: `CONFIG`, - level: `ERROR`, + type: Type.CONFIG, + level: Level.ERROR, }, "10124": { - text: context => + text: (context): string => `It looks like you were trying to add the config file? Please rename "${context.nearMatch}" to "${context.configName}.js"`, - type: `CONFIG`, - level: `ERROR`, + type: Type.CONFIG, + level: Level.ERROR, }, "10125": { - text: context => + text: (context): string => `Your ${context.configName} file is in the wrong place. You've placed it in the src/ directory. It must instead be at the root of your site next to your package.json file.`, - type: `CONFIG`, - level: `ERROR`, + type: Type.CONFIG, + level: Level.ERROR, }, "10126": { - text: context => + text: (context): string => `${context.path}/${context.configName} cannot export a function.` + `\n\nA ${context.configName} exported as a Function can only be used as a theme and not run directly.` + `\nIf you are trying to run a theme directly, use the theme in an example site or starter instead and run that site to test.` + `\nIf you are in the root gatsby-config.js for your site, change the export to be an object and not a function as functions` + `\nare not supported in the root gatsby-config.`, - type: `CONFIG`, - level: `ERROR`, + type: Type.CONFIG, + level: Level.ERROR, }, "10226": { - text: context => + text: (context): string => [ `Couldn't find the "${context.themeName}" plugin declared in "${context.configFilePath}".`, context.pathToLocalTheme && @@ -269,20 +304,20 @@ const errorMap = { ] .filter(Boolean) .join(`\n\n`), - type: `CONFIG`, - level: `ERROR`, + type: Type.CONFIG, + level: Level.ERROR, }, // Plugin errors "11321": { - text: context => + text: (context): string => `"${context.pluginName}" threw an error while running the ${ context.api - } lifecycle:\n\n${context.message}${optionalGraphQLInfo(context)}`, - type: `PLUGIN`, - level: `ERROR`, + } lifecycle:\n\n${context.sourceMessage}${optionalGraphQLInfo(context)}`, + type: Type.PLUGIN, + level: Level.ERROR, }, "11322": { - text: context => + text: (context): string => `${ context.pluginName } created a page and didn't pass the path to the component.\n\nThe page object passed to createPage:\n${JSON.stringify( @@ -290,10 +325,10 @@ const errorMap = { null, 4 )}\n\nSee the documentation for the "createPage" action — https://www.gatsbyjs.org/docs/actions/#createPage`, - level: `ERROR`, + level: Level.ERROR, }, "11323": { - text: context => + text: (context): string => `${ context.pluginName } must set the page path when creating a page.\n\nThe page object passed to createPage:\n${JSON.stringify( @@ -301,15 +336,15 @@ const errorMap = { null, 4 )}\n\nSee the documentation for the "createPage" action — https://www.gatsbyjs.org/docs/actions/#createPage`, - level: `ERROR`, + level: Level.ERROR, }, "11324": { - text: context => + text: (context): string => `${context.message}\n\nSee the documentation for the "createPage" action — https://www.gatsbyjs.org/docs/actions/#createPage`, - level: `ERROR`, + level: Level.ERROR, }, "11325": { - text: context => + text: (context): string => `${ context.pluginName } created a page with a component that doesn't exist.\n\nThe path to the missing component is "${ @@ -319,10 +354,10 @@ const errorMap = { null, 4 )}\n\nSee the documentation for the "createPage" action — https://www.gatsbyjs.org/docs/actions/#createPage`, - level: `ERROR`, + level: Level.ERROR, }, "11326": { - text: context => + text: (context): string => `${ context.pluginName } must set the absolute path to the page component when create creating a page.\n\nThe (relative) path you used for the component is "${ @@ -334,21 +369,21 @@ const errorMap = { null, 4 )}\n\nSee the documentation for the "createPage" action — https://www.gatsbyjs.org/docs/actions/#createPage`, - level: `ERROR`, + level: Level.ERROR, }, "11327": { - text: context => + text: (context): string => `You have an empty file in the "src/pages" directory at "${context.relativePath}". Please remove it or make it a valid component`, - level: `ERROR`, + level: Level.ERROR, }, "11328": { - text: context => + text: (context): string => `A page component must export a React component for it to be valid. Please make sure this file exports a React component:\n\n${context.fileName}`, - level: `ERROR`, + level: Level.ERROR, }, // invalid or deprecated APIs "11329": { - text: context => + text: (context): string => [ stripIndent(` Your plugins must export known APIs from their gatsby-${context.exportType}.js. @@ -368,18 +403,18 @@ const errorMap = { : [] ) .join(`\n`), - level: `ERROR`, + level: Level.ERROR, }, // "X" is not defined in Gatsby's node APIs "11330": { - text: context => - `"${context.pluginName}" threw an error while running the ${context.api} lifecycle:\n\n${context.message}\n\n${context.codeFrame}\n\nMake sure that you don't have a typo somewhere and use valid arguments in ${context.api} lifecycle.\nLearn more about ${context.api} here: https://www.gatsbyjs.org/docs/node-apis/#${context.api}`, - type: `PLUGIN`, - level: `ERROR`, + text: (context): string => + `"${context.pluginName}" threw an error while running the ${context.api} lifecycle:\n\n${context.sourceMessage}\n\n${context.codeFrame}\n\nMake sure that you don't have a typo somewhere and use valid arguments in ${context.api} lifecycle.\nLearn more about ${context.api} here: https://www.gatsbyjs.org/docs/node-apis/#${context.api}`, + type: Type.PLUGIN, + level: Level.ERROR, }, // Directory/file name exceeds OS character limit "11331": { - text: context => + text: (context): string => [ `One or more path segments are too long - they exceed OS filename length limit.\n`, `Page path: "${context.path}"`, @@ -395,11 +430,11 @@ const errorMap = { ] .filter(Boolean) .join(`\n`), - level: `ERROR`, + level: Level.ERROR, }, // node object didn't pass validation "11467": { - text: context => + text: (context): string => [ `The new node didn't pass validation: ${context.validationErrorMessage}`, `Failing node:`, @@ -409,45 +444,57 @@ const errorMap = { ] .filter(Boolean) .join(`\n\n`), - level: `ERROR`, + level: Level.ERROR, docsUrl: `https://www.gatsbyjs.org/docs/actions/#createNode`, }, // local SSL certificate errors "11521": { - text: () => + text: (): string => `for custom ssl --https, --cert-file, and --key-file must be used together`, - level: `ERROR`, + level: Level.ERROR, docsUrl: `https://www.gatsbyjs.org/docs/local-https/#custom-key-and-certificate-files`, }, "11522": { - text: () => `Failed to generate dev SSL certificate`, - level: `ERROR`, + text: (): string => `Failed to generate dev SSL certificate`, + level: Level.ERROR, docsUrl: `https://www.gatsbyjs.org/docs/local-https/#setup`, }, // cli new command errors "11610": { - text: context => + text: (context): string => `It looks like you gave wrong argument orders . Try running instead "gatsby new ${context.starter} ${context.rootPath}"`, - level: `ERROR`, + level: Level.ERROR, docsUrl: `https://www.gatsbyjs.org/docs/gatsby-cli/#new`, }, "11611": { - text: context => + text: (context): string => `It looks like you passed a URL to your project name. Try running instead "gatsby new new-gatsby-project ${context.rootPath}"`, - level: `ERROR`, + level: Level.ERROR, docsUrl: `https://www.gatsbyjs.org/docs/gatsby-cli/#new`, }, "11612": { - text: context => + text: (context): string => `Could not create a project in "${context.path}" because it's not a valid path`, - level: `ERROR`, + level: Level.ERROR, docsUrl: `https://www.gatsbyjs.org/docs/gatsby-cli/#new`, }, "11613": { - text: context => `Directory ${context.rootPath} is already an npm project`, - level: `ERROR`, + text: (context): string => + `Directory ${context.rootPath} is already an npm project`, + level: Level.ERROR, docsUrl: `https://www.gatsbyjs.org/docs/gatsby-cli/#new`, }, } -module.exports = { errorMap, defaultError: errorMap[``] } +export type ErrorId = keyof typeof errors + +export const errorMap: Record = errors + +export const defaultError = errorMap[``] + +export interface IErrorMapEntry { + text: (context) => string + level: Level + type?: Type + docsUrl?: string +} diff --git a/packages/gatsby-cli/src/structured-errors/error-schema.js b/packages/gatsby-cli/src/structured-errors/error-schema.ts similarity index 93% rename from packages/gatsby-cli/src/structured-errors/error-schema.js rename to packages/gatsby-cli/src/structured-errors/error-schema.ts index 0102eb2172dbc..a5de534971e91 100644 --- a/packages/gatsby-cli/src/structured-errors/error-schema.js +++ b/packages/gatsby-cli/src/structured-errors/error-schema.ts @@ -1,4 +1,4 @@ -const Joi = require(`@hapi/joi`) +import Joi from "@hapi/joi" const Position = Joi.object().keys({ line: Joi.number(), @@ -35,4 +35,4 @@ const errorSchema = Joi.object().keys({ panicOnBuild: Joi.boolean(), }) -module.exports = errorSchema +export default errorSchema diff --git a/packages/gatsby/src/query/__tests__/__snapshots__/error-parser.js.snap b/packages/gatsby/src/query/__tests__/__snapshots__/error-parser.ts.snap similarity index 84% rename from packages/gatsby/src/query/__tests__/__snapshots__/error-parser.js.snap rename to packages/gatsby/src/query/__tests__/__snapshots__/error-parser.ts.snap index ab8286622d102..eb9657fe4c435 100644 --- a/packages/gatsby/src/query/__tests__/__snapshots__/error-parser.js.snap +++ b/packages/gatsby/src/query/__tests__/__snapshots__/error-parser.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`specific one 1`] = ` +exports[`query-error-parser specific one 1`] = ` Object { "context": Object { "sourceMessage": "Variable \\"Foo\\" of required type \\"Bar\\" was not provided.", @@ -18,7 +18,7 @@ Object { } `; -exports[`totally vague one 1`] = ` +exports[`query-error-parser totally vague one 1`] = ` Object { "context": Object { "sourceMessage": "foo bar", diff --git a/packages/gatsby/src/query/__tests__/error-parser.js b/packages/gatsby/src/query/__tests__/error-parser.js deleted file mode 100644 index f4d39b46e78aa..0000000000000 --- a/packages/gatsby/src/query/__tests__/error-parser.js +++ /dev/null @@ -1,18 +0,0 @@ -import errorParser from "../error-parser" - -it.each([ - [ - `specific one`, - `Variable "Foo" of required type "Bar" was not provided.`, - `85920`, - ], - [`totally vague one`, `foo bar`, `85901`], -])(`%s`, (_name, message, expectedId) => { - const structured = errorParser({ - message, - filePath: `test.js`, - location: { start: { line: 5, column: 10 } }, - }) - expect(structured).toMatchSnapshot() - expect(structured.id).toEqual(expectedId) -}) diff --git a/packages/gatsby/src/query/__tests__/error-parser.ts b/packages/gatsby/src/query/__tests__/error-parser.ts new file mode 100644 index 0000000000000..90edf1f2831cf --- /dev/null +++ b/packages/gatsby/src/query/__tests__/error-parser.ts @@ -0,0 +1,20 @@ +import errorParser from "../error-parser" + +describe(`query-error-parser`, () => { + it.each([ + [ + `specific one`, + `Variable "Foo" of required type "Bar" was not provided.`, + `85920`, + ], + [`totally vague one`, `foo bar`, `85901`], + ])(`%s`, (_name, message, expectedId) => { + const structured = errorParser({ + message, + filePath: `test.js`, + location: { start: { line: 5, column: 10 } }, + }) + expect(structured).toMatchSnapshot() + expect(structured.id).toEqual(expectedId) + }) +}) diff --git a/packages/gatsby/src/query/error-parser.js b/packages/gatsby/src/query/error-parser.ts similarity index 65% rename from packages/gatsby/src/query/error-parser.js rename to packages/gatsby/src/query/error-parser.ts index 2d903d0eb225e..f8df42dd0e8c2 100644 --- a/packages/gatsby/src/query/error-parser.js +++ b/packages/gatsby/src/query/error-parser.ts @@ -1,14 +1,28 @@ +import { IMatch } from "../types" +import { SourceLocation } from "graphql" + +interface IErrorParser { + message: string + filePath: string | undefined + location: + | { + start: SourceLocation + end?: SourceLocation + } + | undefined +} + const errorParser = ({ message, filePath = undefined, location = undefined, -}) => { +}: IErrorParser): IMatch => { // Handle GraphQL errors. A list of regexes to match certain // errors to specific callbacks const handlers = [ { regex: /Variable "(.+)" of required type "(.+)" was not provided\./m, - cb: match => { + cb: (match): IMatch => { return { id: `85920`, context: { @@ -21,7 +35,7 @@ const errorParser = ({ }, { regex: /Variable "(.+)" of type "(.+)" used in position expecting type "(.+)"\./m, - cb: match => { + cb: (match): IMatch => { return { id: `85921`, context: { @@ -35,7 +49,7 @@ const errorParser = ({ }, { regex: /Field "(.+)" must not have a selection since type "(.+)" has no subfields\./m, - cb: match => { + cb: (match): IMatch => { return { id: `85922`, context: { @@ -48,7 +62,7 @@ const errorParser = ({ }, { regex: /Cannot query field "(.+)" on type "(.+)"\./m, - cb: match => { + cb: (match): IMatch => { return { id: `85923`, context: { @@ -61,7 +75,7 @@ const errorParser = ({ }, { regex: /(.+) cannot represent (.+) value: "(.+)"/m, - cb: match => { + cb: (match): IMatch => { return { id: `85924`, context: { @@ -75,7 +89,7 @@ const errorParser = ({ }, { regex: /Cannot return null for non-nullable field (.+)/m, - cb: match => { + cb: (match): IMatch => { return { id: `85925`, context: { @@ -85,10 +99,35 @@ const errorParser = ({ } }, }, + { + regex: /Must provide Source\. Received: (.+)/m, + cb: (match): IMatch => { + return { + id: `85926`, + context: { + sourceMessage: match[0], + received: match[1], + }, + } + }, + }, + { + regex: /Variable "(.+)" is never used in operation "(.+)".*/ms, + cb: (match): IMatch => { + return { + id: `85927`, + context: { + sourceMessage: match[0], + variable: match[1], + operation: match[2], + }, + } + }, + }, // Match anything with a generic catch-all error handler { regex: /[\s\S]*/gm, - cb: match => { + cb: (match): IMatch => { return { id: `85901`, context: { sourceMessage: match[0] }, @@ -100,12 +139,12 @@ const errorParser = ({ let structured for (const { regex, cb } of handlers) { - const matched = message.match(regex) + const matched = message?.match(regex) if (matched) { structured = { - ...(filePath && { filePath }), - ...(location && { location }), ...cb(matched), + ...{ location }, + ...{ filePath }, } break } @@ -116,10 +155,16 @@ const errorParser = ({ export default errorParser +interface ILocOfGraphQLDocInSrcFile { + start: SourceLocation + end: SourceLocation + fileName: boolean +} + export const locInGraphQlToLocInFile = ( - locationOfGraphQLDocInSourceFile, - graphqlLocation -) => { + locationOfGraphQLDocInSourceFile: ILocOfGraphQLDocInSrcFile, + graphqlLocation: SourceLocation +): SourceLocation => { return { line: graphqlLocation.line + locationOfGraphQLDocInSourceFile.start.line - 1, diff --git a/packages/gatsby/src/types.ts b/packages/gatsby/src/types.ts new file mode 100644 index 0000000000000..1835e473fc1dc --- /dev/null +++ b/packages/gatsby/src/types.ts @@ -0,0 +1,9 @@ +export interface IMatch { + id: string + context: { + sourceMessage: string + [key: string]: string + } + error?: Error | undefined + [key: string]: unknown +} diff --git a/packages/gatsby/src/utils/api-runner-error-parser.js b/packages/gatsby/src/utils/api-runner-error-parser.ts similarity index 71% rename from packages/gatsby/src/utils/api-runner-error-parser.js rename to packages/gatsby/src/utils/api-runner-error-parser.ts index bafa443113036..3e3a08b0714de 100644 --- a/packages/gatsby/src/utils/api-runner-error-parser.js +++ b/packages/gatsby/src/utils/api-runner-error-parser.ts @@ -1,21 +1,23 @@ -const errorParser = ({ err }) => { +import { IMatch } from "../types" + +const errorParser = ({ err }): IMatch => { const handlers = [ { regex: /(.+) is not defined/m, - cb: match => { + cb: (match): IMatch => { return { id: `11330`, - context: { message: match[0], arg: match[1] }, + context: { sourceMessage: match[0], arg: match[1] }, } }, }, // Match anything with a generic catch-all error handler { regex: /[\s\S]*/gm, - cb: match => { + cb: (match): IMatch => { return { id: `11321`, - context: { message: err instanceof Error ? match[0] : err }, + context: { sourceMessage: err instanceof Error ? match[0] : err }, error: err instanceof Error ? err : undefined, } }, diff --git a/packages/gatsby/src/utils/api-runner-node.js b/packages/gatsby/src/utils/api-runner-node.js index e9c9b5e770f45..e3eb2eaecedc0 100644 --- a/packages/gatsby/src/utils/api-runner-node.js +++ b/packages/gatsby/src/utils/api-runner-node.js @@ -23,7 +23,7 @@ const { emitter, store } = require(`../redux`) const { getPublicPath } = require(`./get-public-path`) const { getNonGatsbyCodeFrameFormatted } = require(`./stack-trace-utils`) const { trackBuildError, decorateEvent } = require(`gatsby-telemetry`) -const { default: errorParser } = require(`./api-runner-error-parser`) +import errorParser from "./api-runner-error-parser" // Bind action creators per plugin so we can auto-add // metadata to actions they create. diff --git a/yarn.lock b/yarn.lock index ad01d8a2ce62e..c2d2fc512679d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3952,6 +3952,11 @@ dependencies: jest-diff "^24.3.0" +"@types/joi@^14.3.4": + version "14.3.4" + resolved "https://registry.yarnpkg.com/@types/joi/-/joi-14.3.4.tgz#eed1e14cbb07716079c814138831a520a725a1e0" + integrity sha512-1TQNDJvIKlgYXGNIABfgFp9y0FziDpuGrd799Q5RcnsDu+krD+eeW/0Fs5PHARvWWFelOhIG2OPCo6KbadBM4A== + "@types/json-schema@^7.0.3": version "7.0.3" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636"