From ebc3f829cb4c30d371a9f87d30a3daae714294c5 Mon Sep 17 00:00:00 2001 From: ByungJoon Lee Date: Thu, 3 Oct 2024 00:35:38 +0900 Subject: [PATCH] feat(cli): improve error message when encountering unsupported syntax - Add NotFoundExportedKind error class - Add link for GitHub issue creation in CLI error message --- src/cli/commands/buildCommand.ts | 8 ++--- src/cli/commands/initCommand.ts | 16 ++++----- src/cli/commands/removeCommand.ts | 14 +++----- src/cli/ux/Reasoner.ts | 17 +++++++++- src/compilers/getExportedKind.ts | 12 ++++++- src/errors/NotFoundExportedKind.ts | 51 ++++++++++++++++++++++++++++ src/modules/commands/bundling.ts | 46 +++++++++++++++++++++++--- src/modules/commands/creating.ts | 53 ++++++++++++++++++++++++------ 8 files changed, 175 insertions(+), 42 deletions(-) create mode 100644 src/errors/NotFoundExportedKind.ts diff --git a/src/cli/commands/buildCommand.ts b/src/cli/commands/buildCommand.ts index c2b7b7c..fc49607 100644 --- a/src/cli/commands/buildCommand.ts +++ b/src/cli/commands/buildCommand.ts @@ -7,18 +7,14 @@ import { building } from '#/modules/commands/building'; import consola from 'consola'; import type yargs from 'yargs'; -async function buildCommandCode(argv: yargs.ArgumentsCamelCase) { - const options = await createBuildOptions(argv); - await building(options); -} - export async function buildCommand(argv: yargs.ArgumentsCamelCase) { ProgressBar.it.enable = true; Spinner.it.enable = true; Reasoner.it.enable = true; try { - await buildCommandCode(argv); + const options = await createBuildOptions(argv); + await building(options); } catch (err) { consola.error(err); } finally { diff --git a/src/cli/commands/initCommand.ts b/src/cli/commands/initCommand.ts index e5f51c3..61d8329 100644 --- a/src/cli/commands/initCommand.ts +++ b/src/cli/commands/initCommand.ts @@ -8,22 +8,18 @@ import { initializing } from '#/modules/commands/initializing'; import consola from 'consola'; import type yargs from 'yargs'; -async function initCommandCode(argv: yargs.ArgumentsCamelCase) { - const option: TCommandInitOptions = { - $kind: CE_CTIX_COMMAND.INIT_COMMAND, - forceYes: argv.forceYes, - }; - - await initializing(option); -} - export async function initCommand(argv: yargs.ArgumentsCamelCase) { ProgressBar.it.enable = true; Spinner.it.enable = true; Reasoner.it.enable = true; try { - await initCommandCode(argv); + const option: TCommandInitOptions = { + $kind: CE_CTIX_COMMAND.INIT_COMMAND, + forceYes: argv.forceYes, + }; + + await initializing(option); } catch (err) { consola.error(err); } finally { diff --git a/src/cli/commands/removeCommand.ts b/src/cli/commands/removeCommand.ts index 730d40a..6ec48cc 100644 --- a/src/cli/commands/removeCommand.ts +++ b/src/cli/commands/removeCommand.ts @@ -9,15 +9,6 @@ import { removing } from '#/modules/commands/removing'; import consola from 'consola'; import type yargs from 'yargs'; -async function removeCommandCode( - argv: yargs.ArgumentsCamelCase, -) { - const options = await createBuildOptions(argv); - const removeOptions = createRemoveOptions(argv); - - await removing({ ...options, ...removeOptions }); -} - export async function removeCommand( argv: yargs.ArgumentsCamelCase, ) { @@ -26,7 +17,10 @@ export async function removeCommand( Reasoner.it.enable = true; try { - await removeCommandCode(argv); + const options = await createBuildOptions(argv); + const removeOptions = createRemoveOptions(argv); + + await removing({ ...options, ...removeOptions }); } catch (err) { consola.error(err); } finally { diff --git a/src/cli/ux/Reasoner.ts b/src/cli/ux/Reasoner.ts index 80aaaf2..378f4f8 100644 --- a/src/cli/ux/Reasoner.ts +++ b/src/cli/ux/Reasoner.ts @@ -54,6 +54,10 @@ export class Reasoner { } } + get logger() { + return this.#logger; + } + get enable() { return this.#enable; } @@ -88,7 +92,7 @@ export class Reasoner { } else { messageBlock.push( ` ${chevronRight} ${chalk.gray( - `${filePath}:${reason.lineAndCharacter.line}:${reason.lineAndCharacter.character}`, + `${filePath}:${chalk.yellowBright(reason.lineAndCharacter.line)}:${chalk.yellowBright(reason.lineAndCharacter.character)}`, )}`, ); } @@ -120,6 +124,17 @@ export class Reasoner { this.#logger(warns.join('')); this.#streamFunc(errors.join('')); } + + displayNewIssueMessage() { + const messageIndent = ' > '; + this.#logger( + chalk.green( + `${messageIndent}Please submit a new GitHub issue with a reproducible repository to improve ctix!`, + ), + ); + this.#logger(chalk.green(`${messageIndent}https://github.com/imjuni/ctix/issues/new`)); + this.#logger('\n'); + } } Reasoner.bootstrap(); diff --git a/src/compilers/getExportedKind.ts b/src/compilers/getExportedKind.ts index 718a5e4..4ba2d95 100644 --- a/src/compilers/getExportedKind.ts +++ b/src/compilers/getExportedKind.ts @@ -1,4 +1,5 @@ import { getFunctionName } from '#/compilers/getFunctionName'; +import { NotFoundExportedKind } from '#/errors/NotFoundExportedKind'; import * as tsm from 'ts-morph'; import { match } from 'ts-pattern'; @@ -207,7 +208,16 @@ export function getExportedKind(node: tsm.ExportedDeclarations): { * ``` */ .otherwise(() => { - throw new Error(`Cannot support type: (${node.getKind()}) ${node.getText()}`); + const sourceFile = node.getSourceFile(); + const filePath = sourceFile.getFilePath(); + const pos = sourceFile.getLineAndColumnAtPos(node.getStart(false)); + + throw new NotFoundExportedKind( + pos, + filePath, + node, + `Cannot support type: (${node.getKind()}) ${node.getText()}`, + ); }) ); } diff --git a/src/errors/NotFoundExportedKind.ts b/src/errors/NotFoundExportedKind.ts new file mode 100644 index 0000000..6ae4c84 --- /dev/null +++ b/src/errors/NotFoundExportedKind.ts @@ -0,0 +1,51 @@ +import type { IReason } from '#/compilers/interfaces/IReason'; +import type * as tsm from 'ts-morph'; + +export class NotFoundExportedKind extends Error { + #pos: { line: number; column: number }; + + #filePath: string; + + #node: tsm.Node; + + get pos() { + return this.#pos; + } + + get filePath() { + return this.#filePath; + } + + get node() { + return this.#node; + } + + constructor( + pos: NotFoundExportedKind['pos'], + filePath: string, + node: tsm.Node, + message?: string, + ) { + super(message); + + this.#pos = pos; + this.#node = node; + this.#filePath = filePath; + } + + get reason(): IReason { + const message = + `Cannot support export statement: (${this.#node.getKind()}) ${this.#node.getText()}`.trim(); + + return { + type: 'error', + lineAndCharacter: { + line: this.#pos.line, + character: this.#pos.column, + }, + nodes: this.#node == null ? undefined : [this.#node], + filePath: this.#filePath, + message, + }; + } +} diff --git a/src/modules/commands/bundling.ts b/src/modules/commands/bundling.ts index 8665e6c..8fae567 100644 --- a/src/modules/commands/bundling.ts +++ b/src/modules/commands/bundling.ts @@ -12,6 +12,7 @@ import { isDeclarationFile } from '#/compilers/isDeclarationFile'; import { getExtendOptions } from '#/configs/getExtendOptions'; import type { TBundleOptions } from '#/configs/interfaces/TBundleOptions'; import type { TCommandBuildOptions } from '#/configs/interfaces/TCommandBuildOptions'; +import { NotFoundExportedKind } from '#/errors/NotFoundExportedKind'; import { ProjectContainer } from '#/modules/file/ProjectContainer'; import { checkOutputFile } from '#/modules/file/checkOutputFile'; import { getTsExcludeFiles } from '#/modules/file/getTsExcludeFiles'; @@ -31,6 +32,8 @@ import { getRenderData } from '#/templates/modules/getRenderData'; import { getSelectStyle } from '#/templates/modules/getSelectStyle'; import chalk from 'chalk'; import dayjs from 'dayjs'; +import { isError } from 'my-easy-fp'; +import { fail, pass } from 'my-only-either'; import type * as tsm from 'ts-morph'; export async function bundling(buildOptions: TCommandBuildOptions, bundleOption: TBundleOptions) { @@ -115,16 +118,26 @@ export async function bundling(buildOptions: TCommandBuildOptions, bundleOption: ProgressBar.it.head = ' file '; ProgressBar.it.start(filenames.length, 0); - const statements = ( + const statementEithers = ( await Promise.all( filenames .map((filename) => project.getSourceFile(filename)) .filter((sourceFile): sourceFile is tsm.SourceFile => sourceFile != null) .map(async (sourceFile) => { - const exportStatement = getExportStatement(sourceFile, bundleOption, extendOptions); ProgressBar.it.increment(); - return exportStatement; + try { + const exportStatement = await getExportStatement( + sourceFile, + bundleOption, + extendOptions, + ); + + return pass(exportStatement); + } catch (caught) { + const err = isError(caught, new Error('unknown error raised')); + return fail(err); + } }), ) ).flat(); @@ -134,8 +147,33 @@ export async function bundling(buildOptions: TCommandBuildOptions, bundleOption: const statementMap = new Map(); const statementTable = new StatementTable(); + const failStatements = statementEithers + .filter((statementEither) => statementEither.type === 'fail') + .map((statementEither) => statementEither.fail) + .filter((err) => err != null); + + const statements = statementEithers + .filter((statementEither) => statementEither.type === 'pass') + .map((statementEither) => statementEither.pass) + .filter((statement) => statement != null) + .flat(); + statementTable.inserts(statements); + if (failStatements.length > 0) { + Spinner.it.fail("ctix 'bundle' mode incomplete ..."); + Spinner.it.stop(); + + const reasons = failStatements + .filter((err) => err instanceof NotFoundExportedKind) + .map((err) => err.reason); + + Reasoner.it.start(reasons); + Reasoner.it.displayNewIssueMessage(); + + return; + } + Spinner.it.start(`build ${`"${chalk.green(bundleOption.exportFilename)}"`} file start`); statements @@ -169,7 +207,7 @@ export async function bundling(buildOptions: TCommandBuildOptions, bundleOption: Spinner.it.stop(); ProgressBar.it.head = ' export '; - ProgressBar.it.start(statements.length, 0); + ProgressBar.it.start(statementEithers.length, 0); const datas = Array.from(statementMap.entries()) .map(([filePath, exportStatements]) => { diff --git a/src/modules/commands/creating.ts b/src/modules/commands/creating.ts index a14caf3..45f5399 100644 --- a/src/modules/commands/creating.ts +++ b/src/modules/commands/creating.ts @@ -14,6 +14,7 @@ import { CE_GENERATION_STYLE } from '#/configs/const-enum/CE_GENERATION_STYLE'; import { getExtendOptions } from '#/configs/getExtendOptions'; import type { TCommandBuildOptions } from '#/configs/interfaces/TCommandBuildOptions'; import type { TCreateOptions } from '#/configs/interfaces/TCreateOptions'; +import { NotFoundExportedKind } from '#/errors/NotFoundExportedKind'; import { getAllParentDir } from '#/modules//path/getAllParentDir'; import { ProjectContainer } from '#/modules/file/ProjectContainer'; import { checkOutputFile } from '#/modules/file/checkOutputFile'; @@ -40,7 +41,9 @@ import { getAutoRenderCase } from '#/templates/modules/getAutoRenderCase'; import { getRenderData } from '#/templates/modules/getRenderData'; import chalk from 'chalk'; import dayjs from 'dayjs'; +import { isError } from 'my-easy-fp'; import { getDirnameSync } from 'my-node-fp'; +import { fail, pass } from 'my-only-either'; import type * as tsm from 'ts-morph'; export async function creating(_buildOptions: TCommandBuildOptions, createOption: TCreateOptions) { @@ -108,17 +111,22 @@ export async function creating(_buildOptions: TCommandBuildOptions, createOption ProgressBar.it.head = ' file '; ProgressBar.it.start(filenames.length, 0); - const statements = ( - await Promise.all( - filenames - .map((filename) => project.getSourceFile(filename)) - .filter((sourceFile): sourceFile is tsm.SourceFile => sourceFile != null) - .map(async (sourceFile) => { + const statementEithers = await Promise.all( + filenames + .map((filename) => project.getSourceFile(filename)) + .filter((sourceFile): sourceFile is tsm.SourceFile => sourceFile != null) + .map(async (sourceFile) => { + ProgressBar.it.increment(); + + try { const statement = await getExportStatement(sourceFile, createOption, extendOptions); - return statement; - }), - ) - ).flat(); + return pass(statement); + } catch (caught) { + const err = isError(caught, new Error('unknown error raised')); + return fail(err); + } + }), + ); ProgressBar.it.stop(); @@ -126,8 +134,33 @@ export async function creating(_buildOptions: TCommandBuildOptions, createOption const dirPathMap = new Map(); const statementTable = new StatementTable(); + const failStatements = statementEithers + .filter((statementEither) => statementEither.type === 'fail') + .map((statementEither) => statementEither.fail) + .filter((err) => err != null); + + const statements = statementEithers + .filter((statementEither) => statementEither.type === 'pass') + .map((statementEither) => statementEither.pass) + .filter((statement) => statement != null) + .flat(); + statementTable.inserts(statements); + if (failStatements.length > 0) { + Spinner.it.fail("ctix 'create' mode incomplete ..."); + Spinner.it.stop(); + + const reasons = failStatements + .filter((err) => err instanceof NotFoundExportedKind) + .map((err) => err.reason); + + Reasoner.it.start(reasons); + Reasoner.it.displayNewIssueMessage(); + + return; + } + Spinner.it.start(`build ${`"${chalk.green(createOption.exportFilename)}"`} file start`); statements