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.
### `-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>`
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.
### `-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>`
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.
### `-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>`
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",
"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",

View file

@ -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);

View file

@ -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) {

View file

@ -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 = {

View file

@ -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) {

View file

@ -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<number> {
if (!directory) {

View file

@ -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));

3
pnpm-lock.yaml generated
View file

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