From cc394b519f9d02a302067636516e4a3bf7ee070a Mon Sep 17 00:00:00 2001 From: Richard Haddad Date: Tue, 13 Sep 2022 00:39:26 +0200 Subject: [PATCH] Improve errors diagnostics, narrowing to related words only (#39) * improve errors diagnostics, narrowing to related words only * improve error diagnostics related to schemas * upgrade to 1.4.2 --- package.json | 2 +- src/cached/cached-literal-parser.ts | 4 +- src/create-error-catcher.ts | 49 +++++++++++++++----- src/generators/generate-type-from-literal.ts | 22 ++++++++- src/utils/get-current-word.ts | 11 +++++ 5 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 src/utils/get-current-word.ts diff --git a/package.json b/package.json index a751b8e..1b3e8e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-gql-plugin", - "version": "1.4.1", + "version": "1.4.2", "packageManager": "yarn@3.2.1", "license": "MIT", "main": "./dist/index.js", diff --git a/src/cached/cached-literal-parser.ts b/src/cached/cached-literal-parser.ts index d33437e..bd143d5 100644 --- a/src/cached/cached-literal-parser.ts +++ b/src/cached/cached-literal-parser.ts @@ -45,7 +45,9 @@ export const createCachedLiteralParser = ({ projectName, }); if (!project) { - throw new Error(`Project not defined for name "${projectName}"`); + throw new Error( + `Project not defined for name "${projectName}", or there is an issue on schema file` + ); } return { ...project, projectName }; diff --git a/src/create-error-catcher.ts b/src/create-error-catcher.ts index fffe6d3..8605890 100644 --- a/src/create-error-catcher.ts +++ b/src/create-error-catcher.ts @@ -1,6 +1,8 @@ +import { GraphQLError } from 'graphql'; import ts from 'typescript'; -import { Logger } from './utils/logger'; +import { getCurrentWord } from './utils/get-current-word'; import { isVSCodeEnv } from './utils/is-vscode-env'; +import { Logger } from './utils/logger'; export type ErrorCatcher = ( err: unknown, @@ -9,6 +11,8 @@ export type ErrorCatcher = ( length?: number ) => null; +const unknownFileName = ''; + export const createErrorCatcher = ( pluginsDiagnostics: Map, logger: Logger @@ -25,12 +29,6 @@ export const createErrorCatcher = ( return null; } - if (!sourceFile && !vsCodeEnv) { - logger.error(err); - - throw new Error('Internal error - check previous logs.'); - } - if (err instanceof AggregateError) { err.errors.forEach((gqlErr) => errorCatcher(gqlErr, sourceFile, start, length) @@ -42,20 +40,49 @@ export const createErrorCatcher = ( logger.error(err); } - if (sourceFile) { - const gqlDiagnostics = pluginsDiagnostics.get(sourceFile.fileName) ?? []; + const addNewDiagnosticError = (file: ts.SourceFile | undefined) => { + const fileName = file?.fileName ?? unknownFileName; + const gqlDiagnostics = pluginsDiagnostics.get(fileName) ?? []; - pluginsDiagnostics.set(sourceFile.fileName, [ + pluginsDiagnostics.set(fileName, [ ...gqlDiagnostics, { category: ts.DiagnosticCategory.Error, code: 0, - file: sourceFile, + file, start, length, messageText: err.message, }, ]); + }; + + if (err instanceof GraphQLError) { + const errorLocation = err.nodes?.[0]?.loc; + const position = err.positions?.[0]; + + const text = sourceFile?.text ?? err.source?.body; + + if (errorLocation) { + start += errorLocation.start; + length = errorLocation.end - errorLocation.start; + } else if (position && text) { + start += position; + length = getCurrentWord(text, start).length; + } + } + + if (sourceFile) { + addNewDiagnosticError(sourceFile); + } else if (err instanceof GraphQLError) { + const source = err.source; + + const fileName = source?.name ?? unknownFileName; + const file = source && ts.createSourceFile(fileName, source.body, 99); + + addNewDiagnosticError(file); + } else { + addNewDiagnosticError(undefined); } return null; diff --git a/src/generators/generate-type-from-literal.ts b/src/generators/generate-type-from-literal.ts index 4537a65..76c652d 100644 --- a/src/generators/generate-type-from-literal.ts +++ b/src/generators/generate-type-from-literal.ts @@ -1,6 +1,6 @@ import { codegen } from '@graphql-codegen/core'; import * as typescriptOperationsPlugin from '@graphql-codegen/typescript-operations'; -import { DocumentNode, parse } from 'graphql'; +import { DocumentNode, GraphQLError, parse } from 'graphql'; import { DocumentInfos } from './generate-bottom-content'; type CodegenPlugin = typeof plugins[number]; @@ -27,6 +27,8 @@ export const generateTypeFromLiteral = async ( ...codegenConfig, }; + const codegenErrors: GraphQLError[] = []; + const staticTypes = await codegen({ schema, documents: [ @@ -41,6 +43,24 @@ export const generateTypeFromLiteral = async ( [i + 1]: {}, })), pluginMap, + profiler: { + collect: () => [], + run: async (fn) => { + const value = await fn(); + + if (Array.isArray(value)) { + codegenErrors.push(...value.flatMap((val) => val.errors ?? [])); + } + + return value; + }, + }, + }).catch(async (error) => { + if (codegenErrors.length === 0) { + throw error; + } + + throw new AggregateError(codegenErrors, 'Codegen errors'); }); const typeRegex = /\s=\s(.*?);\n$/gms; diff --git a/src/utils/get-current-word.ts b/src/utils/get-current-word.ts new file mode 100644 index 0000000..3d5fa99 --- /dev/null +++ b/src/utils/get-current-word.ts @@ -0,0 +1,11 @@ +const spaceRelatedChars = new Set([' ', '\n', '\t']); + +export const getCurrentWord = (text: string, start: number): string => { + const currentChar = text[start]; + + if (spaceRelatedChars.has(currentChar)) { + return ''; + } + + return currentChar + getCurrentWord(text, start + 1); +};