feat(cli): add the --import option for importing modules/packages before commands are run

Can for instance be used to load environment variables using Dotenv
This commit is contained in:
Joakim Carlstein 2023-12-20 11:01:01 +01:00 committed by Joakim Carlstein
parent e6e4433018
commit 9f91bdcfa0
12 changed files with 131 additions and 40 deletions

View file

@ -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`.

View file

@ -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. The directory where the migration files are located. The given path should be absolute or relative to the current working directory.
### `-i`, `--import <module>`
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 <name>` ### `-s`, `--storage <name>`
The <Link href="/plugins/storage/">storage plugin</Link> to use, which is responsible for where to store the migration history. The <Link href="/plugins/storage/">storage plugin</Link> to use, which is responsible for where to store the migration history.

View file

@ -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. 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 <module>`
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 <name>` ### `-s`, `--storage <name>`
The <Link href="/plugins/storage/">storage plugin</Link> to use, which is responsible for where to store the migration history. The <Link href="/plugins/storage/">storage plugin</Link> to use, which is responsible for where to store the migration history.

View file

@ -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. The directory where the migration files are located. The given path should be absolute or relative to the current working directory.
### `-i`, `--import <module>`
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 <name>` ### `-s`, `--storage <name>`
The <Link href="/plugins/storage/">storage plugin</Link> to use, which is responsible for where to store the migration history. The <Link href="/plugins/storage/">storage plugin</Link> to use, which is responsible for where to store the migration history.

View file

@ -49,6 +49,7 @@
"cosmiconfig": "8.3.6", "cosmiconfig": "8.3.6",
"elegant-spinner": "3.0.0", "elegant-spinner": "3.0.0",
"figures": "6.0.1", "figures": "6.0.1",
"import-from-esm": "1.3.3",
"is-interactive": "2.0.0", "is-interactive": "2.0.0",
"log-update": "6.0.0", "log-update": "6.0.0",
"pretty-ms": "8.0.0", "pretty-ms": "8.0.0",

View file

@ -1,6 +1,7 @@
#!/usr/bin/env node --enable-source-maps #!/usr/bin/env node --enable-source-maps
import process from 'node:process'; import process from 'node:process';
import { parseArgs } from 'node:util'; import { parseArgs } from 'node:util';
import importFromEsm from 'import-from-esm';
import { ShowUsageError } from './errors.js'; import { ShowUsageError } from './errors.js';
import { getConfig } from './get-config.js'; import { getConfig } from './get-config.js';
@ -14,6 +15,12 @@ const useColors = (values: { color?: boolean; 'no-color'?: boolean }) => {
return values.color; 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 up: Action = async (args) => {
const config = await getConfig('up'); const config = await getConfig('up');
const { values } = parseArgs({ const { values } = parseArgs({
@ -27,6 +34,12 @@ const up: Action = async (args) => {
type: 'string', type: 'string',
short: 'd', short: 'd',
}, },
import: {
type: 'string',
short: 'i',
multiple: true,
default: [],
},
reporter: { reporter: {
type: 'string', type: 'string',
short: 'r', short: 'r',
@ -60,20 +73,23 @@ Run all pending migrations
Options: Options:
-h, --help Show this help message and exit -h, --help Show this help message and exit
-d, --directory The directory where the migration files are located (required) -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) -i, --import Additional modules/packages to import before running the migrations (can be specified multiple times)
-p, --plugin The plugin(s) to use (can be specified multiple times) For example if you want to use Dotenv to load environment variables or when using TypeScript
-r, --reporter The reporter to use for reporting the migration progress -s, --storage The storage to use for where to store the migration history (required)
--dry List the pending migrations that would be run without actually running them -p, --plugin The plugin(s) to use (can be specified multiple times)
--color Force color output (this option is passed to the reporter) -r, --reporter The reporter to use for reporting the migration progress
--no-color Disable color output (this option is passed to the reporter) --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: Examples:
emigrate up --directory src/migrations -s fs emigrate up --directory src/migrations -s fs
emigrate up -d ./migrations --storage @emigrate/mysql emigrate up -d ./migrations --storage @emigrate/mysql
emigrate up -d src/migrations -s postgres -r json --dry emigrate up -d src/migrations -s postgres -r json --dry
emigrate up -d ./migrations -s mysql --import dotenv/config
`; `;
if (values.help) { if (values.help) {
@ -82,12 +98,21 @@ Examples:
return; 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 ?? [])]; const plugins = [...(config.plugins ?? []), ...(values.plugin ?? [])];
await importAll(cwd, imports);
try { try {
const { default: upCommand } = await import('./commands/up.js'); 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) { } catch (error) {
if (error instanceof ShowUsageError) { if (error instanceof ShowUsageError) {
console.error(error.message, '\n'); console.error(error.message, '\n');
@ -178,6 +203,7 @@ Examples:
return; return;
} }
const cwd = process.cwd();
const { const {
directory = config.directory, directory = config.directory,
template = config.template, template = config.template,
@ -189,7 +215,7 @@ Examples:
try { try {
const { default: newCommand } = await import('./commands/new.js'); 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) { } catch (error) {
if (error instanceof ShowUsageError) { if (error instanceof ShowUsageError) {
console.error(error.message, '\n'); console.error(error.message, '\n');
@ -215,6 +241,12 @@ const list: Action = async (args) => {
type: 'string', type: 'string',
short: 'd', short: 'd',
}, },
import: {
type: 'string',
short: 'i',
multiple: true,
default: [],
},
reporter: { reporter: {
type: 'string', type: 'string',
short: 'r', short: 'r',
@ -239,10 +271,12 @@ List all migrations and their status. This command does not run any migrations.
Options: Options:
-h, --help Show this help message and exit -h, --help Show this help message and exit
-d, --directory The directory where the migration files are located (required) -d, --directory The directory where the migration files are located (required)
-r, --reporter The reporter to use for reporting the migrations -i, --import Additional modules/packages to import before listing the migrations (can be specified multiple times)
-s, --storage The storage to use to get the migration history (required) 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) --color Force color output (this option is passed to the reporter)
--no-color Disable 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; 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 { try {
const { default: listCommand } = await import('./commands/list.js'); 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) { } catch (error) {
if (error instanceof ShowUsageError) { if (error instanceof ShowUsageError) {
console.error(error.message, '\n'); console.error(error.message, '\n');
@ -288,6 +330,12 @@ const remove: Action = async (args) => {
type: 'string', type: 'string',
short: 'd', short: 'd',
}, },
import: {
type: 'string',
short: 'i',
multiple: true,
default: [],
},
force: { force: {
type: 'boolean', type: 'boolean',
short: 'f', short: 'f',
@ -323,6 +371,8 @@ Options:
-h, --help Show this help message and exit -h, --help Show this help message and exit
-d, --directory The directory where the migration files are located (required) -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 -r, --reporter The reporter to use for reporting the removal process
-s, --storage The storage to use to get the migration history (required) -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 -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 -d migrations -s fs 20231122120529381_some_migration_file.js
emigrate remove --directory ./migrations --storage postgres 20231122120529381_some_migration_file.sql 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) { if (values.help) {
@ -342,12 +393,21 @@ Examples:
return; 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 { try {
const { default: removeCommand } = await import('./commands/remove.js'); const { default: removeCommand } = await import('./commands/remove.js');
process.exitCode = await removeCommand( process.exitCode = await removeCommand(
{ directory, storage, reporter, force, color: useColors(values) }, { directory, storage, reporter, force, cwd, color: useColors(values) },
positionals[0] ?? '', positionals[0] ?? '',
); );
} catch (error) { } catch (error) {
@ -428,9 +488,9 @@ try {
await main(process.argv.slice(2)); await main(process.argv.slice(2));
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
console.error(error.message); console.error(error);
if (error.cause instanceof Error) { if (error.cause instanceof Error) {
console.error(error.cause.stack); console.error(error.cause);
} }
} else { } else {
console.error(error); console.error(error);

View file

@ -1,4 +1,3 @@
import process from 'node:process';
import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools'; import { getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
import { BadOptionError, MissingOptionError, StorageInitError, toError } from '../errors.js'; import { BadOptionError, MissingOptionError, StorageInitError, toError } from '../errors.js';
import { type Config } from '../types.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'); const lazyDefaultReporter = async () => import('../reporters/default.js');
type ExtraFlags = {
cwd: string;
};
export default async function listCommand({ export default async function listCommand({
directory, directory,
reporter: reporterConfig, reporter: reporterConfig,
storage: storageConfig, storage: storageConfig,
color, color,
}: Config) { cwd,
}: Config & ExtraFlags) {
if (!directory) { if (!directory) {
throw MissingOptionError.fromOption('directory'); throw MissingOptionError.fromOption('directory');
} }
const cwd = process.cwd();
const storagePlugin = await getOrLoadStorage([storageConfig]); const storagePlugin = await getOrLoadStorage([storageConfig]);
if (!storagePlugin) { if (!storagePlugin) {

View file

@ -1,4 +1,4 @@
import process from 'node:process'; import { hrtime } from 'node:process';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import { getTimestampPrefix, sanitizeMigrationName, getOrLoadPlugin, getOrLoadReporter } from '@emigrate/plugin-tools'; 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'); const lazyDefaultReporter = async () => import('../reporters/default.js');
type ExtraFlags = {
cwd: string;
};
export default async function newCommand( 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, name: string,
) { ) {
if (!directory) { if (!directory) {
@ -34,8 +38,6 @@ export default async function newCommand(
throw MissingOptionError.fromOption(['extension', 'template', 'plugin']); throw MissingOptionError.fromOption(['extension', 'template', 'plugin']);
} }
const cwd = process.cwd();
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]); const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
if (!reporter) { if (!reporter) {
@ -47,14 +49,14 @@ export default async function newCommand(
await reporter.onInit?.({ command: 'new', version, cwd, dry: false, directory, color }); await reporter.onInit?.({ command: 'new', version, cwd, dry: false, directory, color });
const start = process.hrtime(); const start = hrtime();
let filename: string | undefined; let filename: string | undefined;
let content: string | undefined; let content: string | undefined;
if (template) { if (template) {
const fs = await import('node:fs/promises'); 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); const fileExtension = path.extname(templatePath);
try { 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 filePath = path.resolve(directoryPath, filename);
const migration: MigrationMetadata = { const migration: MigrationMetadata = {

View file

@ -16,13 +16,14 @@ import { exec } from '../exec.js';
import { version } from '../get-package-info.js'; import { version } from '../get-package-info.js';
type ExtraFlags = { type ExtraFlags = {
cwd: string;
force?: boolean; force?: boolean;
}; };
const lazyDefaultReporter = async () => import('../reporters/default.js'); const lazyDefaultReporter = async () => import('../reporters/default.js');
export default async function removeCommand( 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, name: string,
) { ) {
if (!directory) { if (!directory) {
@ -33,7 +34,6 @@ export default async function removeCommand(
throw MissingArgumentsError.fromArgument('name'); throw MissingArgumentsError.fromArgument('name');
} }
const cwd = process.cwd();
const storagePlugin = await getOrLoadStorage([storageConfig]); const storagePlugin = await getOrLoadStorage([storageConfig]);
if (!storagePlugin) { 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()); const [storage, storageError] = await exec(async () => storagePlugin.initializeStorage());
if (storageError) { if (storageError) {
@ -57,8 +59,6 @@ export default async function removeCommand(
return 1; return 1;
} }
await reporter.onInit?.({ command: 'remove', version, cwd, dry: false, directory, color });
const [migrationFile, fileError] = await exec(async () => getMigration(cwd, directory, name, !force)); const [migrationFile, fileError] = await exec(async () => getMigration(cwd, directory, name, !force));
if (fileError) { if (fileError) {

View file

@ -1,4 +1,3 @@
import process from 'node:process';
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools'; import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
import { isFinishedMigration, type LoaderPlugin } from '@emigrate/types'; import { isFinishedMigration, type LoaderPlugin } from '@emigrate/types';
import { BadOptionError, MigrationLoadError, MissingOptionError, StorageInitError, toError } from '../errors.js'; 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'; import { version } from '../get-package-info.js';
type ExtraFlags = { type ExtraFlags = {
cwd?: string; cwd: string;
dry?: boolean; dry?: boolean;
getMigrations?: GetMigrationsFunction; getMigrations?: GetMigrationsFunction;
}; };
@ -28,7 +27,7 @@ export default async function upCommand({
color, color,
dry = false, dry = false,
plugins = [], plugins = [],
cwd = process.cwd(), cwd,
getMigrations, getMigrations,
}: Config & ExtraFlags): Promise<number> { }: Config & ExtraFlags): Promise<number> {
if (!directory) { if (!directory) {

View file

@ -1,4 +1,4 @@
import process from 'node:process'; import { hrtime } from 'node:process';
import { import {
isFinishedMigration, isFinishedMigration,
isFailedMigration, isFailedMigration,
@ -123,7 +123,7 @@ export const migrationRunner = async ({
await reporter.onMigrationStart?.(migration); await reporter.onMigrationStart?.(migration);
const start = process.hrtime(); const start = hrtime();
const [, migrationError] = await exec(async () => execute(migration)); const [, migrationError] = await exec(async () => execute(migration));

3
pnpm-lock.yaml generated
View file

@ -89,6 +89,9 @@ importers:
figures: figures:
specifier: 6.0.1 specifier: 6.0.1
version: 6.0.1 version: 6.0.1
import-from-esm:
specifier: 1.3.3
version: 1.3.3
is-interactive: is-interactive:
specifier: 2.0.0 specifier: 2.0.0
version: 2.0.0 version: 2.0.0