feat(emigrate): add support for reading config from emigrate.config.js (and others)

Also add a new "extension" option for generating empty migration files with the right file extension.
This commit is contained in:
Joakim Carlstein 2023-11-14 16:19:36 +01:00
parent ff822a156d
commit aa878003b9
10 changed files with 94 additions and 14 deletions

View file

@ -8,6 +8,12 @@
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"bin": {
"emigrate": "dist/cli.js"
},
@ -28,7 +34,8 @@
"author": "Aboviq AB <dev@aboviq.com> (https://www.aboviq.com)",
"license": "MIT",
"dependencies": {
"@emigrate/plugin-tools": "workspace:*"
"@emigrate/plugin-tools": "workspace:*",
"cosmiconfig": "8.3.6"
},
"volta": {
"extends": "../../package.json"

View file

@ -2,6 +2,7 @@
import process from 'node:process';
import { parseArgs } from 'node:util';
import { ShowUsageError } from './show-usage-error.js';
import { getConfig } from './get-config.js';
type Action = (args: string[]) => Promise<void>;
@ -57,6 +58,7 @@ Examples:
};
const newMigration: Action = async (args) => {
const config = await getConfig('new');
const { values, positionals } = parseArgs({
args,
options: {
@ -72,6 +74,10 @@ const newMigration: Action = async (args) => {
type: 'string',
short: 't',
},
extension: {
type: 'string',
short: 'e',
},
plugin: {
type: 'string',
short: 'p',
@ -92,13 +98,18 @@ Options:
-d, --directory The directory where the migration files are located (required)
-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)
-e, --extension The extension to use for the new migration file
(if no template or plugin is provided an empty migration file will be created with the given extension)
Either the --template or the --plugin option is required must be specified
One of the --template, --extension or the --plugin options must be specified
Examples:
emigrate new -d src/migrations -t migration-template.js create users table
emigrate new --directory ./migrations --plugin @emigrate/plugin-generate-sql create_users_table
emigrate new -d ./migrations -e .sql create_users_table
emigrate new -d ./migrations -t .migration-template -e .sql "drop some table"
`;
if (values.help) {
@ -107,12 +118,13 @@ Examples:
return;
}
const { plugin: plugins = [], directory, template } = values;
const { directory = config.directory, template = config.template, extension = config.extension } = 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, name });
await newCommand({ directory, template, plugins, name, extension });
} catch (error) {
if (error instanceof ShowUsageError) {
console.error(error.message, '\n');

View file

@ -0,0 +1,20 @@
import { cosmiconfig } from 'cosmiconfig';
import { type Config, type EmigrateConfig } from './types.js';
export const getConfig = async (command: 'up' | 'list' | 'new'): Promise<Config> => {
const explorer = cosmiconfig('emigrate');
const result = await explorer.search();
if (!result?.config) {
return {};
}
const { plugins, directory, template, ...commandsConfig } = result.config as EmigrateConfig;
if (commandsConfig[command]) {
return { plugins, directory, template, ...commandsConfig[command] };
}
return { plugins, directory, template };
};

View file

@ -1,3 +1,5 @@
export * from './types.js';
export const emigrate = () => {
console.log('Done!');
};

View file

@ -1,18 +1,19 @@
import process from 'node:process';
import fs from 'node:fs/promises';
import path from 'node:path';
import { getTimestampPrefix, sanitizeMigrationName, loadPlugin } from '@emigrate/plugin-tools';
import { type GeneratorPlugin } from '@emigrate/plugin-tools/types';
import { getTimestampPrefix, sanitizeMigrationName, loadPlugin, isGeneratorPlugin } from '@emigrate/plugin-tools';
import { type Plugin, type GeneratorPlugin } from '@emigrate/plugin-tools/types';
import { ShowUsageError } from './show-usage-error.js';
type NewCommandOptions = {
directory?: string;
template?: string;
plugins: string[];
extension?: string;
plugins: Array<string | Plugin>;
name?: string;
};
export default async function newCommand({ directory, template, plugins, name }: NewCommandOptions) {
export default async function newCommand({ directory, template, plugins, name, extension }: NewCommandOptions) {
if (!directory) {
throw new ShowUsageError('Missing required option: directory');
}
@ -21,8 +22,8 @@ export default async function newCommand({ directory, template, plugins, name }:
throw new ShowUsageError('Missing required migration name');
}
if (!template && plugins.length === 0) {
throw new ShowUsageError('Missing required option: template or plugin');
if (!extension && !template && plugins.length === 0) {
throw new ShowUsageError('Missing required option: extension, template or plugin');
}
let filename: string | undefined;
@ -31,7 +32,7 @@ export default async function newCommand({ directory, template, plugins, name }:
if (template) {
const fs = await import('node:fs/promises');
const templatePath = path.resolve(process.cwd(), template);
const extension = path.extname(templatePath);
const fileExtension = path.extname(templatePath);
try {
content = await fs.readFile(templatePath, 'utf8');
@ -40,12 +41,17 @@ export default async function newCommand({ directory, template, plugins, name }:
throw new Error(`Failed to read template file: ${templatePath}`, { cause: error });
}
filename = `${getTimestampPrefix()}_${sanitizeMigrationName(name)}${extension}`;
filename = `${getTimestampPrefix()}_${sanitizeMigrationName(name)}${extension ?? fileExtension}`;
} else if (plugins.length > 0) {
let generatorPlugin: GeneratorPlugin | undefined;
for await (const plugin of plugins) {
generatorPlugin = await loadPlugin('generator', plugin);
if (isGeneratorPlugin(plugin)) {
generatorPlugin = plugin;
break;
}
generatorPlugin = typeof plugin === 'string' ? await loadPlugin('generator', plugin) : undefined;
if (generatorPlugin) {
break;
@ -60,9 +66,12 @@ export default async function newCommand({ directory, template, plugins, name }:
filename = generated.filename;
content = generated.content;
} else if (extension) {
content = '';
filename = `${getTimestampPrefix()}_${sanitizeMigrationName(name)}${extension}`;
}
if (!filename || !content) {
if (!filename || content === undefined) {
throw new Error('Unexpected error, missing filename or content for migration file');
}

View file

@ -0,0 +1,16 @@
import { type Plugin } from '@emigrate/plugin-tools/types';
export type EmigratePlugin = Plugin;
export type Config = {
plugins?: Array<string | EmigratePlugin>;
directory?: string;
template?: string;
extension?: string;
};
export type EmigrateConfig = Config & {
up?: Config;
new?: Config;
list?: Config;
};

View file

@ -24,6 +24,7 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveWatchOutput": true,
"preserveSymlinks": true,
"resolveJsonModule": false,
"skipLibCheck": true,
"sourceMap": true,