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:
parent
ff822a156d
commit
aa878003b9
10 changed files with 94 additions and 14 deletions
5
.changeset/little-moons-rush.md
Normal file
5
.changeset/little-moons-rush.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'emigrate': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add the "extension" option for the "new" command to be able to generate empty migration files without any plugin and template and still get the right file extension. It can also be used together with the "template" option to override the template file's file extension when saving the new migration file.
|
||||||
5
.changeset/shaggy-doors-trade.md
Normal file
5
.changeset/shaggy-doors-trade.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'emigrate': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Support reading config from for instance emigrate.config.js
|
||||||
|
|
@ -8,6 +8,12 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"emigrate": "dist/cli.js"
|
"emigrate": "dist/cli.js"
|
||||||
},
|
},
|
||||||
|
|
@ -28,7 +34,8 @@
|
||||||
"author": "Aboviq AB <dev@aboviq.com> (https://www.aboviq.com)",
|
"author": "Aboviq AB <dev@aboviq.com> (https://www.aboviq.com)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emigrate/plugin-tools": "workspace:*"
|
"@emigrate/plugin-tools": "workspace:*",
|
||||||
|
"cosmiconfig": "8.3.6"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"extends": "../../package.json"
|
"extends": "../../package.json"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { parseArgs } from 'node:util';
|
import { parseArgs } from 'node:util';
|
||||||
import { ShowUsageError } from './show-usage-error.js';
|
import { ShowUsageError } from './show-usage-error.js';
|
||||||
|
import { getConfig } from './get-config.js';
|
||||||
|
|
||||||
type Action = (args: string[]) => Promise<void>;
|
type Action = (args: string[]) => Promise<void>;
|
||||||
|
|
||||||
|
|
@ -57,6 +58,7 @@ Examples:
|
||||||
};
|
};
|
||||||
|
|
||||||
const newMigration: Action = async (args) => {
|
const newMigration: Action = async (args) => {
|
||||||
|
const config = await getConfig('new');
|
||||||
const { values, positionals } = parseArgs({
|
const { values, positionals } = parseArgs({
|
||||||
args,
|
args,
|
||||||
options: {
|
options: {
|
||||||
|
|
@ -72,6 +74,10 @@ const newMigration: Action = async (args) => {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
short: 't',
|
short: 't',
|
||||||
},
|
},
|
||||||
|
extension: {
|
||||||
|
type: 'string',
|
||||||
|
short: 'e',
|
||||||
|
},
|
||||||
plugin: {
|
plugin: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
short: 'p',
|
short: 'p',
|
||||||
|
|
@ -92,13 +98,18 @@ Options:
|
||||||
-d, --directory The directory where the migration files are located (required)
|
-d, --directory The directory where the migration files are located (required)
|
||||||
-p, --plugin The plugin(s) to use (can be specified multiple times)
|
-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
|
-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:
|
Examples:
|
||||||
|
|
||||||
emigrate new -d src/migrations -t migration-template.js create users table
|
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 --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) {
|
if (values.help) {
|
||||||
|
|
@ -107,12 +118,13 @@ Examples:
|
||||||
return;
|
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();
|
const name = positionals.join(' ').trim();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { default: newCommand } = await import('./new-command.js');
|
const { default: newCommand } = await import('./new-command.js');
|
||||||
await newCommand({ directory, template, plugins, name });
|
await newCommand({ directory, template, plugins, name, extension });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ShowUsageError) {
|
if (error instanceof ShowUsageError) {
|
||||||
console.error(error.message, '\n');
|
console.error(error.message, '\n');
|
||||||
|
|
|
||||||
20
packages/emigrate/src/get-config.ts
Normal file
20
packages/emigrate/src/get-config.ts
Normal 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 };
|
||||||
|
};
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
export * from './types.js';
|
||||||
|
|
||||||
export const emigrate = () => {
|
export const emigrate = () => {
|
||||||
console.log('Done!');
|
console.log('Done!');
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import process from 'node:process';
|
import process 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, loadPlugin } from '@emigrate/plugin-tools';
|
import { getTimestampPrefix, sanitizeMigrationName, loadPlugin, isGeneratorPlugin } from '@emigrate/plugin-tools';
|
||||||
import { type GeneratorPlugin } from '@emigrate/plugin-tools/types';
|
import { type Plugin, type GeneratorPlugin } from '@emigrate/plugin-tools/types';
|
||||||
import { ShowUsageError } from './show-usage-error.js';
|
import { ShowUsageError } from './show-usage-error.js';
|
||||||
|
|
||||||
type NewCommandOptions = {
|
type NewCommandOptions = {
|
||||||
directory?: string;
|
directory?: string;
|
||||||
template?: string;
|
template?: string;
|
||||||
plugins: string[];
|
extension?: string;
|
||||||
|
plugins: Array<string | Plugin>;
|
||||||
name?: string;
|
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) {
|
if (!directory) {
|
||||||
throw new ShowUsageError('Missing required option: 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');
|
throw new ShowUsageError('Missing required migration name');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!template && plugins.length === 0) {
|
if (!extension && !template && plugins.length === 0) {
|
||||||
throw new ShowUsageError('Missing required option: template or plugin');
|
throw new ShowUsageError('Missing required option: extension, template or plugin');
|
||||||
}
|
}
|
||||||
|
|
||||||
let filename: string | undefined;
|
let filename: string | undefined;
|
||||||
|
|
@ -31,7 +32,7 @@ export default async function newCommand({ directory, template, plugins, name }:
|
||||||
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(process.cwd(), template);
|
||||||
const extension = path.extname(templatePath);
|
const fileExtension = path.extname(templatePath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
content = await fs.readFile(templatePath, 'utf8');
|
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 });
|
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) {
|
} else if (plugins.length > 0) {
|
||||||
let generatorPlugin: GeneratorPlugin | undefined;
|
let generatorPlugin: GeneratorPlugin | undefined;
|
||||||
|
|
||||||
for await (const plugin of plugins) {
|
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) {
|
if (generatorPlugin) {
|
||||||
break;
|
break;
|
||||||
|
|
@ -60,9 +66,12 @@ export default async function newCommand({ directory, template, plugins, name }:
|
||||||
|
|
||||||
filename = generated.filename;
|
filename = generated.filename;
|
||||||
content = generated.content;
|
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');
|
throw new Error('Unexpected error, missing filename or content for migration file');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
16
packages/emigrate/src/types.ts
Normal file
16
packages/emigrate/src/types.ts
Normal 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;
|
||||||
|
};
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"preserveWatchOutput": true,
|
"preserveWatchOutput": true,
|
||||||
|
"preserveSymlinks": true,
|
||||||
"resolveJsonModule": false,
|
"resolveJsonModule": false,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|
|
||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
|
@ -59,6 +59,9 @@ importers:
|
||||||
'@emigrate/plugin-tools':
|
'@emigrate/plugin-tools':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../plugin-tools
|
version: link:../plugin-tools
|
||||||
|
cosmiconfig:
|
||||||
|
specifier: 8.3.6
|
||||||
|
version: 8.3.6(typescript@5.2.2)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@emigrate/tsconfig':
|
'@emigrate/tsconfig':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue