From da1eee3c75996cdf0a81d0d2dd0495ccb0270e67 Mon Sep 17 00:00:00 2001 From: Joakim Carlstein Date: Wed, 22 Nov 2023 13:21:45 +0100 Subject: [PATCH] feat(cli): add "reporter" option for the "new" command for improved logging --- .changeset/gorgeous-falcons-train.md | 5 + packages/cli/src/cli.ts | 14 ++- packages/cli/src/new-command.ts | 57 +++++++++-- packages/cli/src/plugin-reporter-default.ts | 101 ++++++++++++-------- packages/cli/src/up-command.ts | 9 +- 5 files changed, 132 insertions(+), 54 deletions(-) create mode 100644 .changeset/gorgeous-falcons-train.md diff --git a/.changeset/gorgeous-falcons-train.md b/.changeset/gorgeous-falcons-train.md new file mode 100644 index 0000000..90e44bd --- /dev/null +++ b/.changeset/gorgeous-falcons-train.md @@ -0,0 +1,5 @@ +--- +'@emigrate/cli': minor +--- + +Add "reporter" option for the "new" command and use it for improved logging diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index c7a25bc..6aa2ca8 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -97,6 +97,10 @@ const newMigration: Action = async (args) => { type: 'string', short: 'd', }, + reporter: { + type: 'string', + short: 'r', + }, template: { type: 'string', short: 't', @@ -123,6 +127,7 @@ Options: -h, --help Show this help message and exit -d, --directory The directory where the migration files are located (required) + -r, --reporter The reporter to use for reporting the migration file creation progress -p, --plugin The plugin(s) to use (can be specified multiple times) -t, --template A template file to use as contents for the new migration file (if the extension option is not provided the template file's extension will be used) @@ -145,13 +150,18 @@ Examples: return; } - const { directory = config.directory, template = config.template, extension = config.extension } = values; + const { + directory = config.directory, + template = config.template, + extension = config.extension, + reporter = config.reporter, + } = values; const plugins = [...(config.plugins ?? []), ...(values.plugin ?? [])]; const name = positionals.join(' ').trim(); try { const { default: newCommand } = await import('./new-command.js'); - await newCommand({ directory, template, plugins, extension }, name); + await newCommand({ directory, template, plugins, extension, reporter }, name); } catch (error) { if (error instanceof ShowUsageError) { console.error(error.message, '\n'); diff --git a/packages/cli/src/new-command.ts b/packages/cli/src/new-command.ts index cd6c055..2b188b0 100644 --- a/packages/cli/src/new-command.ts +++ b/packages/cli/src/new-command.ts @@ -1,12 +1,18 @@ import process from 'node:process'; import fs from 'node:fs/promises'; import path from 'node:path'; -import { getTimestampPrefix, sanitizeMigrationName, getOrLoadPlugin } from '@emigrate/plugin-tools'; +import { getTimestampPrefix, sanitizeMigrationName, getOrLoadPlugin, getOrLoadReporter } from '@emigrate/plugin-tools'; +import { type MigrationMetadata } from '@emigrate/plugin-tools/types'; import { BadOptionError, MissingArgumentsError, MissingOptionError, UnexpectedError } from './errors.js'; import { type Config } from './types.js'; import { withLeadingPeriod } from './with-leading-period.js'; -export default async function newCommand({ directory, template, plugins = [], extension }: Config, name: string) { +const lazyDefaultReporter = async () => import('./plugin-reporter-default.js'); + +export default async function newCommand( + { directory, template, reporter: reporterConfig, plugins = [], extension }: Config, + name: string, +) { if (!directory) { throw new MissingOptionError('directory'); } @@ -19,6 +25,19 @@ export default async function newCommand({ directory, template, plugins = [], ex throw new MissingOptionError(['extension', 'template', 'plugin']); } + const cwd = process.cwd(); + + const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]); + + if (!reporter) { + throw new BadOptionError( + 'reporter', + 'No reporter found, please specify an existing reporter using the reporter option', + ); + } + + await reporter.onInit?.({ command: 'new', cwd, dry: false, directory }); + let filename: string | undefined; let content: string | undefined; @@ -31,7 +50,11 @@ export default async function newCommand({ directory, template, plugins = [], ex content = await fs.readFile(templatePath, 'utf8'); content = content.replaceAll('{{name}}', name); } catch (error) { - throw new UnexpectedError(`Failed to read template file: ${templatePath}`, { cause: error }); + await reporter.onFinished?.( + [], + new UnexpectedError(`Failed to read template file: ${templatePath}`, { cause: error }), + ); + return; } filename = `${getTimestampPrefix()}_${sanitizeMigrationName(name)}${withLeadingPeriod(extension ?? fileExtension)}`; @@ -67,8 +90,30 @@ export default async function newCommand({ directory, template, plugins = [], ex const directoryPath = path.resolve(process.cwd(), directory); const filePath = path.resolve(directoryPath, filename); - await createDirectory(directoryPath); - await saveFile(filePath, content); + const migration: MigrationMetadata = { + name: filename, + filePath, + relativeFilePath: path.relative(cwd, filePath), + extension: withLeadingPeriod(path.extname(filename)), + directory, + cwd, + }; + + await reporter.onNewMigration?.(migration, content); + + let saveError: Error | undefined; + + try { + await createDirectory(directoryPath); + await saveFile(filePath, content); + } catch (error) { + saveError = error instanceof Error ? error : new Error(String(error)); + } + + await reporter.onFinished?.( + [{ ...migration, status: saveError ? 'failed' : 'done', error: saveError, duration: 0 }], + saveError, + ); } async function createDirectory(directoryPath: string) { @@ -82,8 +127,6 @@ async function createDirectory(directoryPath: string) { async function saveFile(filePath: string, content: string) { try { await fs.writeFile(filePath, content); - - console.log(`Created migration file: ${path.relative(process.cwd(), filePath)}`); } catch (error) { throw new UnexpectedError(`Failed to write migration file: ${filePath}`, { cause: error }); } diff --git a/packages/cli/src/plugin-reporter-default.ts b/packages/cli/src/plugin-reporter-default.ts index 49d0a6d..ccb9881 100644 --- a/packages/cli/src/plugin-reporter-default.ts +++ b/packages/cli/src/plugin-reporter-default.ts @@ -1,5 +1,5 @@ import path from 'node:path'; -import ansis from 'ansis'; +import { black, blueBright, bold, cyan, dim, gray, green, red, redBright, yellow } from 'ansis'; import logUpdate from 'log-update'; import elegantSpinner from 'elegant-spinner'; import figures from 'figures'; @@ -9,6 +9,8 @@ import { type MigrationMetadata, type MigrationMetadataFinished, type EmigrateReporter, + type ReporterInitParameters, + type Awaitable, } from '@emigrate/plugin-tools/types'; type Status = ReturnType; @@ -19,12 +21,12 @@ const spinner = interactive ? elegantSpinner() : () => figures.pointerSmall; const formatDuration = (duration: number): string => { const pretty = prettyMs(duration); - return ansis.yellow(pretty.replaceAll(/([^\s\d]+)/g, ansis.dim('$1'))); + return yellow(pretty.replaceAll(/([^\s\d]+)/g, dim('$1'))); }; -const getTitle = ({ directory, dry, cwd }: { directory: string; dry: boolean; cwd: string }) => { - return `${ansis.bgBlueBright(ansis.black(' Emigrate '))} ${ansis.gray(cwd + path.sep)}${directory}${ - dry ? ansis.yellow(' (dry run)') : '' +const getTitle = ({ command, directory, dry, cwd }: ReporterInitParameters) => { + return `${black.bgBlueBright` Emigrate `} ${blueBright.bold(command)} ${gray(cwd + path.sep)}${directory}${ + dry ? yellow` (dry run)` : '' }`; }; @@ -42,23 +44,23 @@ const getMigrationStatus = ( const getIcon = (status: Status) => { switch (status) { case 'running': { - return ansis.cyan(spinner()); + return cyan(spinner()); } case 'pending': { - return ansis.gray(figures.pointerSmall); + return gray(figures.pointerSmall); } case 'done': { - return ansis.green(figures.tick); + return green(figures.tick); } case 'failed': { - return ansis.red(figures.cross); + return red(figures.cross); } case 'skipped': { - return ansis.yellow(figures.circle); + return yellow(figures.circle); } default: { @@ -70,11 +72,11 @@ const getIcon = (status: Status) => { const getName = (name: string, status?: Status) => { switch (status) { case 'failed': { - return ansis.red(name); + return red(name); } case 'skipped': { - return ansis.yellow(name); + return yellow(name); } default: { @@ -91,12 +93,12 @@ const getMigrationText = ( const status = getMigrationStatus(migration, activeMigration); const parts = [' ', getIcon(status)]; - parts.push(`${getName(nameWithoutExtension, status)}${ansis.dim(migration.extension)}`); + parts.push(`${getName(nameWithoutExtension, status)}${dim(migration.extension)}`); if ('status' in migration) { - parts.push(ansis.gray(`(${migration.status})`)); + parts.push(gray(`(${migration.status})`)); } else if (migration.name === activeMigration?.name) { - parts.push(ansis.gray('(running)')); + parts.push(gray`(running)`); } if ('duration' in migration && migration.duration) { @@ -123,17 +125,20 @@ const getError = (error?: Error, indent = ' ') => { errorTitle = error.message; } - const parts = [`${indent}${ansis.bold.red(errorTitle)}`, ...stack.map((line) => `${indent}${ansis.dim(line)}`)]; + const parts = [`${indent}${bold.red(errorTitle)}`, ...stack.map((line) => `${indent}${dim(line)}`)]; if (error.cause instanceof Error) { const nextIndent = `${indent} `; - parts.push(`\n${nextIndent}${ansis.bold('Original error cause:')}\n`, getError(error.cause, nextIndent)); + parts.push(`\n${nextIndent}${bold('Original error cause:')}\n`, getError(error.cause, nextIndent)); } return parts.join('\n'); }; -const getSummary = (migrations: Array = []) => { +const getSummary = ( + command: ReporterInitParameters['command'], + migrations: Array = [], +) => { const total = migrations.length; let done = 0; let failed = 0; @@ -163,19 +168,21 @@ const getSummary = (migrations: Array { @@ -188,18 +195,16 @@ const getHeaderMessage = (migrations?: MigrationMetadata[], lockedMigrations?: M } if (migrations.length === lockedMigrations.length) { - return ` ${ansis.bold(migrations.length.toString())} ${ansis.dim('pending migrations to run')}`; + return ` ${bold(migrations.length.toString())} ${dim('pending migrations to run')}`; } if (lockedMigrations.length === 0) { - return ` ${ansis.bold(`0 of ${migrations.length}`)} ${ansis.dim('pending migrations to run')} ${ansis.redBright( - '(all locked)', - )}`; + return ` ${bold(`0 of ${migrations.length}`)} ${dim('pending migrations to run')} ${redBright('(all locked)')}`; } - return ` ${ansis.bold(`${lockedMigrations.length} of ${migrations.length}`)} ${ansis.dim( - 'pending migrations to run', - )} ${ansis.yellow(`(${migrations.length - lockedMigrations.length} locked)`)}`; + return ` ${bold(`${lockedMigrations.length} of ${migrations.length}`)} ${dim('pending migrations to run')} ${yellow( + `(${migrations.length - lockedMigrations.length} locked)`, + )}`; }; class DefaultFancyReporter implements Required { @@ -207,15 +212,11 @@ class DefaultFancyReporter implements Required { #lockedMigrations: MigrationMetadata[] | undefined; #activeMigration: MigrationMetadata | undefined; #error: Error | undefined; - #directory!: string; - #cwd!: string; - #dry!: boolean; + #parameters!: ReporterInitParameters; #interval: NodeJS.Timeout | undefined; - onInit(parameters: { directory: string; cwd: string; dry: boolean }): void | PromiseLike { - this.#directory = parameters.directory; - this.#dry = parameters.dry; - this.#cwd = parameters.cwd; + onInit(parameters: ReporterInitParameters): void | PromiseLike { + this.#parameters = parameters; this.#start(); } @@ -228,6 +229,10 @@ class DefaultFancyReporter implements Required { this.#lockedMigrations = migrations; } + onNewMigration(migration: MigrationMetadata, _content: string): Awaitable { + this.#migrations = [migration]; + } + onMigrationStart(migration: MigrationMetadata): void | PromiseLike { this.#activeMigration = migration; } @@ -244,7 +249,13 @@ class DefaultFancyReporter implements Required { this.#finishMigration(migration); } - onFinished(_migrations: MigrationMetadataFinished[], error?: Error | undefined): void | PromiseLike { + onFinished(migrations: MigrationMetadataFinished[], error?: Error | undefined): void | PromiseLike { + if (this.#parameters.command === 'new') { + for (const migration of migrations) { + this.#finishMigration(migration); + } + } + this.#error = error; this.#activeMigration = undefined; this.#stop(); @@ -264,10 +275,10 @@ class DefaultFancyReporter implements Required { #render(): void { const parts = [ - getTitle({ directory: this.#directory, dry: this.#dry, cwd: this.#cwd }), + getTitle(this.#parameters), getHeaderMessage(this.#migrations, this.#lockedMigrations), this.#migrations?.map((migration) => getMigrationText(migration, this.#activeMigration)).join('\n') ?? '', - getSummary(this.#migrations), + getSummary(this.#parameters.command, this.#migrations), getError(this.#error), ]; logUpdate('\n' + parts.filter(Boolean).join('\n\n') + '\n'); @@ -293,8 +304,10 @@ class DefaultFancyReporter implements Required { class DefaultReporter implements Required { #migrations?: MigrationMetadata[]; #lockedMigrations?: MigrationMetadata[]; + #parameters!: ReporterInitParameters; - onInit(parameters: { directory: string; cwd: string; dry: boolean }): void | PromiseLike { + onInit(parameters: ReporterInitParameters): void | PromiseLike { + this.#parameters = parameters; console.log(''); console.log(getTitle(parameters)); console.log(''); @@ -311,6 +324,10 @@ class DefaultReporter implements Required { console.log(''); } + onNewMigration(migration: MigrationMetadata, _content: string): Awaitable { + console.log(getMigrationText(migration)); + } + onMigrationStart(migration: MigrationMetadata): void | PromiseLike { console.log(getMigrationText(migration, migration)); } @@ -329,7 +346,7 @@ class DefaultReporter implements Required { onFinished(migrations: MigrationMetadataFinished[], error?: Error | undefined): void | PromiseLike { console.log(''); - console.log(getSummary(migrations)); + console.log(getSummary(this.#parameters.command, migrations)); console.log(''); if (error) { diff --git a/packages/cli/src/up-command.ts b/packages/cli/src/up-command.ts index ae711e4..9babea4 100644 --- a/packages/cli/src/up-command.ts +++ b/packages/cli/src/up-command.ts @@ -48,13 +48,16 @@ export default async function upCommand({ } const storage = await storagePlugin.initializeStorage(); - const reporter = await getOrLoadReporter([lazyDefaultReporter, reporterConfig]); + const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]); if (!reporter) { - throw new BadOptionError('reporter', 'No reporter found, please specify a reporter using the reporter option'); + throw new BadOptionError( + 'reporter', + 'No reporter found, please specify an existing reporter using the reporter option', + ); } - await reporter.onInit?.({ cwd, dry, directory }); + await reporter.onInit?.({ command: 'up', cwd, dry, directory }); const path = await import('node:path'); const fs = await import('node:fs/promises');