feat(cli): add template support for the "new" command
This commit is contained in:
parent
16340940b7
commit
ca3ab9ec62
4 changed files with 154 additions and 98 deletions
5
.changeset/gold-squids-drive.md
Normal file
5
.changeset/gold-squids-drive.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'emigrate': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add template support for the "new" migration command
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { parseArgs } from 'node:util';
|
import { parseArgs } from 'node:util';
|
||||||
import { isGeneratorPlugin } from '@emigrate/plugin-tools';
|
import { ShowUsageError } from './show-usage-error.js';
|
||||||
import { type GeneratorPlugin } from '@emigrate/plugin-tools/types';
|
|
||||||
|
|
||||||
type Action = (args: string[]) => Promise<void>;
|
type Action = (args: string[]) => Promise<void>;
|
||||||
|
|
||||||
|
|
@ -65,10 +64,14 @@ const newMigration: Action = async (args) => {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
short: 'h',
|
short: 'h',
|
||||||
},
|
},
|
||||||
dir: {
|
directory: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
short: 'd',
|
short: 'd',
|
||||||
},
|
},
|
||||||
|
template: {
|
||||||
|
type: 'string',
|
||||||
|
short: 't',
|
||||||
|
},
|
||||||
plugin: {
|
plugin: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
short: 'p',
|
short: 'p',
|
||||||
|
|
@ -79,116 +82,46 @@ const newMigration: Action = async (args) => {
|
||||||
allowPositionals: true,
|
allowPositionals: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasPositionals = positionals.join('').trim() !== '';
|
const usage = `Usage: emigrate new [options] <name>
|
||||||
const showHelp = !values.dir || !hasPositionals || values.help;
|
|
||||||
|
|
||||||
if (!values.dir) {
|
Create a new migration file with the given name in the specified directory
|
||||||
console.error('Missing required option: --dir\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasPositionals) {
|
|
||||||
console.error('Missing required migration name: <name>\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showHelp) {
|
|
||||||
console.log(`Usage: emigrate new [options] <name>
|
|
||||||
|
|
||||||
Run all pending migrations
|
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-h, --help Show this help message and exit
|
-h, --help Show this help message and exit
|
||||||
-d, --dir 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
|
||||||
|
|
||||||
|
Either the --template or the --plugin option is required must be specified
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
emigrate new --dir src/migrations create users table
|
emigrate new -d src/migrations -t migration-template.js create users table
|
||||||
emigrate new --dir ./migrations --plugin @emigrate/plugin-generate-sql create_users_table
|
emigrate new --directory ./migrations --plugin @emigrate/plugin-generate-sql create_users_table
|
||||||
`);
|
`;
|
||||||
|
|
||||||
|
if (values.help) {
|
||||||
|
console.log(usage);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { plugin: plugins = [] } = values;
|
const { plugin: plugins = [], directory, template } = values;
|
||||||
|
const name = positionals.join(' ').trim();
|
||||||
|
|
||||||
if (plugins.length > 0) {
|
try {
|
||||||
let generatorPlugin: GeneratorPlugin | undefined;
|
const { default: newCommand } = await import('./new-command.js');
|
||||||
|
await newCommand({ directory, template, plugins, name });
|
||||||
const path = await import('node:path');
|
} catch (error) {
|
||||||
|
if (error instanceof ShowUsageError) {
|
||||||
for await (const plugin of plugins) {
|
console.error(error.message, '\n');
|
||||||
const pluginPath = plugin.startsWith('.') ? path.resolve(process.cwd(), plugin) : plugin;
|
console.log(usage);
|
||||||
|
|
||||||
try {
|
|
||||||
const pluginModule: unknown = await import(pluginPath);
|
|
||||||
|
|
||||||
if (isGeneratorPlugin(pluginModule)) {
|
|
||||||
generatorPlugin = pluginModule;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
pluginModule &&
|
|
||||||
typeof pluginModule === 'object' &&
|
|
||||||
'default' in pluginModule &&
|
|
||||||
isGeneratorPlugin(pluginModule.default)
|
|
||||||
) {
|
|
||||||
generatorPlugin = pluginModule.default;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to load plugin: ${plugin}`);
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exitCode = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!generatorPlugin) {
|
|
||||||
console.error('No generator plugin found, please specify a generator plugin using the --plugin option\n');
|
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fs = await import('node:fs/promises');
|
throw error;
|
||||||
|
|
||||||
const { filename, content } = await generatorPlugin.generate(positionals.join(' '));
|
|
||||||
|
|
||||||
const directory = path.resolve(process.cwd(), values.dir!);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.mkdir(directory, { recursive: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to create migration directory: ${directory}`);
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exitCode = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = path.resolve(directory, filename);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.writeFile(file, content);
|
|
||||||
|
|
||||||
console.log(`Created migration file: ${path.relative(process.cwd(), file)}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to write migration file: ${file}`);
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.exitCode = 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -243,6 +176,14 @@ Commands:
|
||||||
try {
|
try {
|
||||||
await action(process.argv.slice(3));
|
await action(process.argv.slice(3));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error instanceof Error ? error.message : error);
|
if (error instanceof Error) {
|
||||||
|
console.error(error.message);
|
||||||
|
if (error.cause instanceof Error) {
|
||||||
|
console.error(error.cause.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
109
packages/emigrate/src/new-command.ts
Normal file
109
packages/emigrate/src/new-command.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import process from 'node:process';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { getTimestampPrefix, sanitizeMigrationName, isGeneratorPlugin } from '@emigrate/plugin-tools';
|
||||||
|
import { type GeneratorPlugin } from '@emigrate/plugin-tools/types';
|
||||||
|
import { ShowUsageError } from './show-usage-error.js';
|
||||||
|
|
||||||
|
type NewCommandOptions = {
|
||||||
|
directory?: string;
|
||||||
|
template?: string;
|
||||||
|
plugins: string[];
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function newCommand({ directory, template, plugins, name }: NewCommandOptions) {
|
||||||
|
if (!directory) {
|
||||||
|
throw new ShowUsageError('Missing required option: directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
throw new ShowUsageError('Missing required migration name');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!template && plugins.length === 0) {
|
||||||
|
throw new ShowUsageError('Missing required option: template or plugin');
|
||||||
|
}
|
||||||
|
|
||||||
|
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 extension = path.extname(templatePath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
content = await fs.readFile(templatePath, 'utf8');
|
||||||
|
content = content.replaceAll('{{name}}', name);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to read template file: ${templatePath}`, { cause: error });
|
||||||
|
}
|
||||||
|
|
||||||
|
filename = `${getTimestampPrefix()}_${sanitizeMigrationName(name)}${extension}`;
|
||||||
|
} else if (plugins.length > 0) {
|
||||||
|
let generatorPlugin: GeneratorPlugin | undefined;
|
||||||
|
|
||||||
|
for await (const plugin of plugins) {
|
||||||
|
const pluginPath = plugin.startsWith('.') ? path.resolve(process.cwd(), plugin) : plugin;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pluginModule: unknown = await import(pluginPath);
|
||||||
|
|
||||||
|
if (isGeneratorPlugin(pluginModule)) {
|
||||||
|
generatorPlugin = pluginModule;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
pluginModule &&
|
||||||
|
typeof pluginModule === 'object' &&
|
||||||
|
'default' in pluginModule &&
|
||||||
|
isGeneratorPlugin(pluginModule.default)
|
||||||
|
) {
|
||||||
|
generatorPlugin = pluginModule.default;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to load plugin: ${plugin}`, { cause: error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!generatorPlugin) {
|
||||||
|
throw new Error('No generator plugin found, please specify a generator plugin using the plugin option');
|
||||||
|
}
|
||||||
|
|
||||||
|
const generated = await generatorPlugin.generate(name);
|
||||||
|
|
||||||
|
filename = generated.filename;
|
||||||
|
content = generated.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filename || !content) {
|
||||||
|
throw new Error('Unexpected error, missing filename or content for migration file');
|
||||||
|
}
|
||||||
|
|
||||||
|
const directoryPath = path.resolve(process.cwd(), directory);
|
||||||
|
const filePath = path.resolve(directoryPath, filename);
|
||||||
|
|
||||||
|
await createDirectory(directoryPath);
|
||||||
|
await saveFile(filePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDirectory(directoryPath: string) {
|
||||||
|
try {
|
||||||
|
await fs.mkdir(directoryPath, { recursive: true });
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to create migration directory: ${directoryPath}`, { cause: error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveFile(filePath: string, content: string) {
|
||||||
|
try {
|
||||||
|
await fs.writeFile(filePath, content);
|
||||||
|
|
||||||
|
console.log(`Created migration file: ${path.relative(process.cwd(), filePath)}`);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to write migration file: ${filePath}`, { cause: error });
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/emigrate/src/show-usage-error.ts
Normal file
1
packages/emigrate/src/show-usage-error.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export class ShowUsageError extends Error {}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue