diff --git a/.changeset/curvy-days-teach.md b/.changeset/curvy-days-teach.md new file mode 100644 index 0000000..cea91df --- /dev/null +++ b/.changeset/curvy-days-teach.md @@ -0,0 +1,5 @@ +--- +'@emigrate/cli': minor +--- + +Add support for the `--import` option to import modules/packages before any command is run. This can for instance be used to load environment variables using the [dotenv](https://github.com/motdotla/dotenv) package with `--import dotenv/config`. diff --git a/docs/src/content/docs/commands/list.mdx b/docs/src/content/docs/commands/list.mdx index 6120302..8e1fb92 100644 --- a/docs/src/content/docs/commands/list.mdx +++ b/docs/src/content/docs/commands/list.mdx @@ -62,6 +62,12 @@ Show command help and exit The directory where the migration files are located. The given path should be absolute or relative to the current working directory. +### `-i`, `--import ` + +A module to import before listing the migrations. This option can be specified multiple times. + +Can for instance be used to load environment variables using [dotenv](https://github.com/motdotla/dotenv) with `--import dotenv/config`. + ### `-s`, `--storage ` The storage plugin to use, which is responsible for where to store the migration history. diff --git a/docs/src/content/docs/commands/remove.mdx b/docs/src/content/docs/commands/remove.mdx index af17a52..421b7de 100644 --- a/docs/src/content/docs/commands/remove.mdx +++ b/docs/src/content/docs/commands/remove.mdx @@ -69,6 +69,12 @@ The directory where the migration files are located. The given path should be ab Force removal of the migration history entry even if the migration file does not exist or it's in a non-failed state. +### `-i`, `--import ` + +A module to import before remove the migration. This option can be specified multiple times. + +Can for instance be used to load environment variables using [dotenv](https://github.com/motdotla/dotenv) with `--import dotenv/config`. + ### `-s`, `--storage ` The storage plugin to use, which is responsible for where to store the migration history. diff --git a/docs/src/content/docs/commands/up.mdx b/docs/src/content/docs/commands/up.mdx index d431807..64db94c 100644 --- a/docs/src/content/docs/commands/up.mdx +++ b/docs/src/content/docs/commands/up.mdx @@ -68,6 +68,12 @@ List the pending migrations that would be run without actually running them The directory where the migration files are located. The given path should be absolute or relative to the current working directory. +### `-i`, `--import ` + +A module to import before running the migrations. This option can be specified multiple times. + +Can for instance be used to load environment variables using [dotenv](https://github.com/motdotla/dotenv) with `--import dotenv/config`. + ### `-s`, `--storage ` The storage plugin to use, which is responsible for where to store the migration history. diff --git a/packages/cli/package.json b/packages/cli/package.json index e2ead0c..3a0e7e4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,6 +49,7 @@ "cosmiconfig": "8.3.6", "elegant-spinner": "3.0.0", "figures": "6.0.1", + "import-from-esm": "1.3.3", "is-interactive": "2.0.0", "log-update": "6.0.0", "pretty-ms": "8.0.0", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index ca31f55..ca1b0d7 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node --enable-source-maps import process from 'node:process'; import { parseArgs } from 'node:util'; +import importFromEsm from 'import-from-esm'; import { ShowUsageError } from './errors.js'; import { getConfig } from './get-config.js'; @@ -14,6 +15,12 @@ const useColors = (values: { color?: boolean; 'no-color'?: boolean }) => { return values.color; }; +const importAll = async (cwd: string, modules: string[]) => { + for await (const module of modules) { + await importFromEsm(cwd, module); + } +}; + const up: Action = async (args) => { const config = await getConfig('up'); const { values } = parseArgs({ @@ -27,6 +34,12 @@ const up: Action = async (args) => { type: 'string', short: 'd', }, + import: { + type: 'string', + short: 'i', + multiple: true, + default: [], + }, reporter: { type: 'string', short: 'r', @@ -60,20 +73,23 @@ Run all pending migrations Options: - -h, --help Show this help message and exit - -d, --directory The directory where the migration files are located (required) - -s, --storage The storage to use for where to store the migration history (required) - -p, --plugin The plugin(s) to use (can be specified multiple times) - -r, --reporter The reporter to use for reporting the migration progress - --dry List the pending migrations that would be run without actually running them - --color Force color output (this option is passed to the reporter) - --no-color Disable color output (this option is passed to the reporter) + -h, --help Show this help message and exit + -d, --directory The directory where the migration files are located (required) + -i, --import Additional modules/packages to import before running the migrations (can be specified multiple times) + For example if you want to use Dotenv to load environment variables or when using TypeScript + -s, --storage The storage to use for where to store the migration history (required) + -p, --plugin The plugin(s) to use (can be specified multiple times) + -r, --reporter The reporter to use for reporting the migration progress + --dry List the pending migrations that would be run without actually running them + --color Force color output (this option is passed to the reporter) + --no-color Disable color output (this option is passed to the reporter) Examples: emigrate up --directory src/migrations -s fs emigrate up -d ./migrations --storage @emigrate/mysql emigrate up -d src/migrations -s postgres -r json --dry + emigrate up -d ./migrations -s mysql --import dotenv/config `; if (values.help) { @@ -82,12 +98,21 @@ Examples: return; } - const { directory = config.directory, storage = config.storage, reporter = config.reporter, dry } = values; + const cwd = process.cwd(); + const { + directory = config.directory, + storage = config.storage, + reporter = config.reporter, + dry, + import: imports = [], + } = values; const plugins = [...(config.plugins ?? []), ...(values.plugin ?? [])]; + await importAll(cwd, imports); + try { const { default: upCommand } = await import('./commands/up.js'); - process.exitCode = await upCommand({ storage, reporter, directory, plugins, dry, color: useColors(values) }); + process.exitCode = await upCommand({ storage, reporter, directory, plugins, cwd, dry, color: useColors(values) }); } catch (error) { if (error instanceof ShowUsageError) { console.error(error.message, '\n'); @@ -178,6 +203,7 @@ Examples: return; } + const cwd = process.cwd(); const { directory = config.directory, template = config.template, @@ -189,7 +215,7 @@ Examples: try { const { default: newCommand } = await import('./commands/new.js'); - await newCommand({ directory, template, plugins, extension, reporter, color: useColors(values) }, name); + await newCommand({ directory, template, plugins, extension, reporter, cwd, color: useColors(values) }, name); } catch (error) { if (error instanceof ShowUsageError) { console.error(error.message, '\n'); @@ -215,6 +241,12 @@ const list: Action = async (args) => { type: 'string', short: 'd', }, + import: { + type: 'string', + short: 'i', + multiple: true, + default: [], + }, reporter: { type: 'string', short: 'r', @@ -239,10 +271,12 @@ List all migrations and their status. This command does not run any migrations. 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 migrations - -s, --storage The storage to use to get the migration history (required) + -h, --help Show this help message and exit + -d, --directory The directory where the migration files are located (required) + -i, --import Additional modules/packages to import before listing the migrations (can be specified multiple times) + For example if you want to use Dotenv to load environment variables + -r, --reporter The reporter to use for reporting the migrations + -s, --storage The storage to use to get the migration history (required) --color Force color output (this option is passed to the reporter) --no-color Disable color output (this option is passed to the reporter) @@ -258,11 +292,19 @@ Examples: return; } - const { directory = config.directory, storage = config.storage, reporter = config.reporter } = values; + const cwd = process.cwd(); + const { + directory = config.directory, + storage = config.storage, + reporter = config.reporter, + import: imports = [], + } = values; + + await importAll(cwd, imports); try { const { default: listCommand } = await import('./commands/list.js'); - process.exitCode = await listCommand({ directory, storage, reporter, color: useColors(values) }); + process.exitCode = await listCommand({ directory, storage, reporter, cwd, color: useColors(values) }); } catch (error) { if (error instanceof ShowUsageError) { console.error(error.message, '\n'); @@ -288,6 +330,12 @@ const remove: Action = async (args) => { type: 'string', short: 'd', }, + import: { + type: 'string', + short: 'i', + multiple: true, + default: [], + }, force: { type: 'boolean', short: 'f', @@ -323,6 +371,8 @@ Options: -h, --help Show this help message and exit -d, --directory The directory where the migration files are located (required) + -i, --import Additional modules/packages to import before removing the migration (can be specified multiple times) + For example if you want to use Dotenv to load environment variables -r, --reporter The reporter to use for reporting the removal process -s, --storage The storage to use to get the migration history (required) -f, --force Force removal of the migration history entry even if the migration file does not exist @@ -334,6 +384,7 @@ Examples: emigrate remove -d migrations -s fs 20231122120529381_some_migration_file.js emigrate remove --directory ./migrations --storage postgres 20231122120529381_some_migration_file.sql + emigrate remove -i dotenv/config -d ./migrations -s postgres 20231122120529381_some_migration_file.sql `; if (values.help) { @@ -342,12 +393,21 @@ Examples: return; } - const { directory = config.directory, storage = config.storage, reporter = config.reporter, force } = values; + const cwd = process.cwd(); + const { + directory = config.directory, + storage = config.storage, + reporter = config.reporter, + force, + import: imports = [], + } = values; + + await importAll(cwd, imports); try { const { default: removeCommand } = await import('./commands/remove.js'); process.exitCode = await removeCommand( - { directory, storage, reporter, force, color: useColors(values) }, + { directory, storage, reporter, force, cwd, color: useColors(values) }, positionals[0] ?? '', ); } catch (error) { @@ -428,9 +488,9 @@ try { await main(process.argv.slice(2)); } catch (error) { if (error instanceof Error) { - console.error(error.message); + console.error(error); if (error.cause instanceof Error) { - console.error(error.cause.stack); + console.error(error.cause); } } else { console.error(error); diff --git a/packages/cli/src/commands/list.ts b/packages/cli/src/commands/list.ts index 726351f..add45d0 100644 --- a/packages/cli/src/commands/list.ts +++ b/packages/cli/src/commands/list.ts @@ -1,4 +1,3 @@ -import process from 'node:process'; import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools'; import { BadOptionError, MissingOptionError, StorageInitError, toError } from '../errors.js'; import { type Config } from '../types.js'; @@ -10,17 +9,21 @@ import { version } from '../get-package-info.js'; const lazyDefaultReporter = async () => import('../reporters/default.js'); +type ExtraFlags = { + cwd: string; +}; + export default async function listCommand({ directory, reporter: reporterConfig, storage: storageConfig, color, -}: Config) { + cwd, +}: Config & ExtraFlags) { if (!directory) { throw MissingOptionError.fromOption('directory'); } - const cwd = process.cwd(); const storagePlugin = await getOrLoadStorage([storageConfig]); if (!storagePlugin) { diff --git a/packages/cli/src/commands/new.ts b/packages/cli/src/commands/new.ts index bd4f682..81351b8 100644 --- a/packages/cli/src/commands/new.ts +++ b/packages/cli/src/commands/new.ts @@ -1,4 +1,4 @@ -import process from 'node:process'; +import { hrtime } from 'node:process'; import fs from 'node:fs/promises'; import path from 'node:path'; import { getTimestampPrefix, sanitizeMigrationName, getOrLoadPlugin, getOrLoadReporter } from '@emigrate/plugin-tools'; @@ -18,8 +18,12 @@ import { getDuration } from '../get-duration.js'; const lazyDefaultReporter = async () => import('../reporters/default.js'); +type ExtraFlags = { + cwd: string; +}; + export default async function newCommand( - { directory, template, reporter: reporterConfig, plugins = [], extension, color }: Config, + { directory, template, reporter: reporterConfig, plugins = [], cwd, extension, color }: Config & ExtraFlags, name: string, ) { if (!directory) { @@ -34,8 +38,6 @@ export default async function newCommand( throw MissingOptionError.fromOption(['extension', 'template', 'plugin']); } - const cwd = process.cwd(); - const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]); if (!reporter) { @@ -47,14 +49,14 @@ export default async function newCommand( await reporter.onInit?.({ command: 'new', version, cwd, dry: false, directory, color }); - const start = process.hrtime(); + const start = hrtime(); let filename: string | undefined; let content: string | undefined; if (template) { const fs = await import('node:fs/promises'); - const templatePath = path.resolve(process.cwd(), template); + const templatePath = path.resolve(cwd, template); const fileExtension = path.extname(templatePath); try { @@ -98,7 +100,7 @@ export default async function newCommand( ); } - const directoryPath = path.resolve(process.cwd(), directory); + const directoryPath = path.resolve(cwd, directory); const filePath = path.resolve(directoryPath, filename); const migration: MigrationMetadata = { diff --git a/packages/cli/src/commands/remove.ts b/packages/cli/src/commands/remove.ts index b74ecaf..cd2a42d 100644 --- a/packages/cli/src/commands/remove.ts +++ b/packages/cli/src/commands/remove.ts @@ -16,13 +16,14 @@ import { exec } from '../exec.js'; import { version } from '../get-package-info.js'; type ExtraFlags = { + cwd: string; force?: boolean; }; const lazyDefaultReporter = async () => import('../reporters/default.js'); export default async function removeCommand( - { directory, reporter: reporterConfig, storage: storageConfig, color, force = false }: Config & ExtraFlags, + { directory, reporter: reporterConfig, storage: storageConfig, color, cwd, force = false }: Config & ExtraFlags, name: string, ) { if (!directory) { @@ -33,7 +34,6 @@ export default async function removeCommand( throw MissingArgumentsError.fromArgument('name'); } - const cwd = process.cwd(); const storagePlugin = await getOrLoadStorage([storageConfig]); if (!storagePlugin) { @@ -49,6 +49,8 @@ export default async function removeCommand( ); } + await reporter.onInit?.({ command: 'remove', version, cwd, dry: false, directory, color }); + const [storage, storageError] = await exec(async () => storagePlugin.initializeStorage()); if (storageError) { @@ -57,8 +59,6 @@ export default async function removeCommand( return 1; } - await reporter.onInit?.({ command: 'remove', version, cwd, dry: false, directory, color }); - const [migrationFile, fileError] = await exec(async () => getMigration(cwd, directory, name, !force)); if (fileError) { diff --git a/packages/cli/src/commands/up.ts b/packages/cli/src/commands/up.ts index 2423286..c260c46 100644 --- a/packages/cli/src/commands/up.ts +++ b/packages/cli/src/commands/up.ts @@ -1,4 +1,3 @@ -import process from 'node:process'; import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools'; import { isFinishedMigration, type LoaderPlugin } from '@emigrate/types'; import { BadOptionError, MigrationLoadError, MissingOptionError, StorageInitError, toError } from '../errors.js'; @@ -13,7 +12,7 @@ import { arrayFromAsync } from '../array-from-async.js'; import { version } from '../get-package-info.js'; type ExtraFlags = { - cwd?: string; + cwd: string; dry?: boolean; getMigrations?: GetMigrationsFunction; }; @@ -28,7 +27,7 @@ export default async function upCommand({ color, dry = false, plugins = [], - cwd = process.cwd(), + cwd, getMigrations, }: Config & ExtraFlags): Promise { if (!directory) { diff --git a/packages/cli/src/migration-runner.ts b/packages/cli/src/migration-runner.ts index a8914e2..8cdf88c 100644 --- a/packages/cli/src/migration-runner.ts +++ b/packages/cli/src/migration-runner.ts @@ -1,4 +1,4 @@ -import process from 'node:process'; +import { hrtime } from 'node:process'; import { isFinishedMigration, isFailedMigration, @@ -123,7 +123,7 @@ export const migrationRunner = async ({ await reporter.onMigrationStart?.(migration); - const start = process.hrtime(); + const start = hrtime(); const [, migrationError] = await exec(async () => execute(migration)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08e785c..daf748b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: figures: specifier: 6.0.1 version: 6.0.1 + import-from-esm: + specifier: 1.3.3 + version: 1.3.3 is-interactive: specifier: 2.0.0 version: 2.0.0