refactor(cli): rename the emigrate package to @emigrate/cli to be more in line with other tools
This commit is contained in:
parent
9f5abf727d
commit
0b78d5cf32
16 changed files with 34 additions and 11 deletions
30
packages/cli/CHANGELOG.md
Normal file
30
packages/cli/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# emigrate
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 50fce0a: Add some simple README's for each package and add homepage, repository and bugs URLs to each package.json file
|
||||
- Updated dependencies [50fce0a]
|
||||
- @emigrate/plugin-tools@0.1.1
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- aa87800: 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.
|
||||
- aa87800: Support reading config from for instance emigrate.config.js
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ca3ab9e: Add template support for the "new" migration command
|
||||
- 9c239e0: Automatically prefix plugin names when loading them if necessary. I.e. when specifying only "--plugin generate-js" Emigrate will load the @emigrate/plugin-generate-js plugin. It has a priority order that is: 1. the provided plugin name as is, 2. the name prefixed with "@emigrate/plugin-", 3. the name prefixed with "emigrate-plugin-"
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [cdafd05]
|
||||
- Updated dependencies [9c239e0]
|
||||
- Updated dependencies [1634094]
|
||||
- @emigrate/plugin-tools@0.1.0
|
||||
21
packages/cli/README.md
Normal file
21
packages/cli/README.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# @emigrate/cli
|
||||
|
||||
Emigrate is a tool for managing database migrations. It is designed to be simple yet support advanced setups, modular and extensible.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the Emigrate CLI in your project:
|
||||
|
||||
```bash
|
||||
npm install --save-dev @emigrate/cli
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Create a new migration:
|
||||
|
||||
```bash
|
||||
emigrate new -d migrations -e .js create some fancy table
|
||||
```
|
||||
|
||||
Will create a new empty JavaScript migration file with the name "YYYYMMDDHHmmssuuu_create_some_fancy_table.js" in the `migrations` directory.
|
||||
49
packages/cli/package.json
Normal file
49
packages/cli/package.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "@emigrate/cli",
|
||||
"version": "0.2.1",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"description": "",
|
||||
"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"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc --pretty",
|
||||
"build:watch": "tsc --pretty --watch"
|
||||
},
|
||||
"keywords": [
|
||||
"migrate",
|
||||
"migrations",
|
||||
"database",
|
||||
"emigrate",
|
||||
"immigration"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@emigrate/tsconfig": "workspace:*"
|
||||
},
|
||||
"author": "Aboviq AB <dev@aboviq.com> (https://www.aboviq.com)",
|
||||
"homepage": "https://github.com/aboviq/emigrate/tree/main/packages/cli#readme",
|
||||
"repository": "https://github.com/aboviq/emigrate/tree/main/packages/cli",
|
||||
"bugs": "https://github.com/aboviq/emigrate/issues",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emigrate/plugin-tools": "workspace:*",
|
||||
"cosmiconfig": "8.3.6"
|
||||
},
|
||||
"volta": {
|
||||
"extends": "../../package.json"
|
||||
}
|
||||
}
|
||||
201
packages/cli/src/cli.ts
Normal file
201
packages/cli/src/cli.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
#!/usr/bin/env node
|
||||
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>;
|
||||
|
||||
const up: Action = async (args) => {
|
||||
const { values } = parseArgs({
|
||||
args,
|
||||
options: {
|
||||
help: {
|
||||
type: 'boolean',
|
||||
short: 'h',
|
||||
},
|
||||
dir: {
|
||||
type: 'string',
|
||||
short: 'd',
|
||||
},
|
||||
plugin: {
|
||||
type: 'string',
|
||||
short: 'p',
|
||||
multiple: true,
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
allowPositionals: false,
|
||||
});
|
||||
|
||||
const showHelp = !values.dir || values.help;
|
||||
|
||||
if (!values.dir) {
|
||||
console.error('Missing required option: --dir\n');
|
||||
}
|
||||
|
||||
if (showHelp) {
|
||||
console.log(`Usage: emigrate up [options]
|
||||
|
||||
Run all pending migrations
|
||||
|
||||
Options:
|
||||
|
||||
-h, --help Show this help message and exit
|
||||
-d, --dir The directory where the migration files are located (required)
|
||||
-p, --plugin The plugin(s) to use (can be specified multiple times)
|
||||
|
||||
Examples:
|
||||
|
||||
emigrate up --dir src/migrations
|
||||
emigrate up --dir ./migrations --plugin @emigrate/plugin-storage-mysql
|
||||
`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(values);
|
||||
};
|
||||
|
||||
const newMigration: Action = async (args) => {
|
||||
const config = await getConfig('new');
|
||||
const { values, positionals } = parseArgs({
|
||||
args,
|
||||
options: {
|
||||
help: {
|
||||
type: 'boolean',
|
||||
short: 'h',
|
||||
},
|
||||
directory: {
|
||||
type: 'string',
|
||||
short: 'd',
|
||||
},
|
||||
template: {
|
||||
type: 'string',
|
||||
short: 't',
|
||||
},
|
||||
extension: {
|
||||
type: 'string',
|
||||
short: 'e',
|
||||
},
|
||||
plugin: {
|
||||
type: 'string',
|
||||
short: 'p',
|
||||
multiple: true,
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
const usage = `Usage: emigrate new [options] <name>
|
||||
|
||||
Create a new migration file with the given name in the specified directory
|
||||
|
||||
Options:
|
||||
|
||||
-h, --help Show this help message and exit
|
||||
-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)
|
||||
|
||||
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) {
|
||||
console.log(usage);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
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, extension });
|
||||
} catch (error) {
|
||||
if (error instanceof ShowUsageError) {
|
||||
console.error(error.message, '\n');
|
||||
console.log(usage);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const list: Action = async (args) => {
|
||||
const { values } = parseArgs({
|
||||
args,
|
||||
options: {
|
||||
help: {
|
||||
type: 'boolean',
|
||||
short: 'h',
|
||||
},
|
||||
plugin: {
|
||||
type: 'string',
|
||||
short: 'p',
|
||||
multiple: true,
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
allowPositionals: false,
|
||||
});
|
||||
|
||||
console.log(values);
|
||||
};
|
||||
|
||||
const commands: Record<string, Action> = {
|
||||
up,
|
||||
list,
|
||||
new: newMigration,
|
||||
};
|
||||
|
||||
const command = process.argv[2];
|
||||
const action = command ? commands[command] : undefined;
|
||||
|
||||
if (!action) {
|
||||
if (command) {
|
||||
console.error(`Unknown command: ${command}\n`);
|
||||
} else {
|
||||
console.error('No command specified\n');
|
||||
}
|
||||
|
||||
console.log(`Usage: emigrate <command>
|
||||
|
||||
Commands:
|
||||
|
||||
up Run all pending migrations
|
||||
new Create a new migration file
|
||||
list List all migrations
|
||||
`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
await action(process.argv.slice(3));
|
||||
} catch (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);
|
||||
}
|
||||
20
packages/cli/src/get-config.ts
Normal file
20
packages/cli/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 };
|
||||
};
|
||||
5
packages/cli/src/index.ts
Normal file
5
packages/cli/src/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export * from './types.js';
|
||||
|
||||
export const emigrate = () => {
|
||||
console.log('Done!');
|
||||
};
|
||||
101
packages/cli/src/new-command.ts
Normal file
101
packages/cli/src/new-command.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import process from 'node:process';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
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;
|
||||
extension?: string;
|
||||
plugins: Array<string | Plugin>;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export default async function newCommand({ directory, template, plugins, name, extension }: NewCommandOptions) {
|
||||
if (!directory) {
|
||||
throw new ShowUsageError('Missing required option: directory');
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
throw new ShowUsageError('Missing required migration name');
|
||||
}
|
||||
|
||||
if (!extension && !template && plugins.length === 0) {
|
||||
throw new ShowUsageError('Missing required option: extension, 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 fileExtension = 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 ?? fileExtension}`;
|
||||
} else if (plugins.length > 0) {
|
||||
let generatorPlugin: GeneratorPlugin | undefined;
|
||||
|
||||
for await (const plugin of plugins) {
|
||||
if (isGeneratorPlugin(plugin)) {
|
||||
generatorPlugin = plugin;
|
||||
break;
|
||||
}
|
||||
|
||||
generatorPlugin = typeof plugin === 'string' ? await loadPlugin('generator', plugin) : undefined;
|
||||
|
||||
if (generatorPlugin) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!generatorPlugin) {
|
||||
throw new Error('No generator plugin found, please specify a generator plugin using the plugin option');
|
||||
}
|
||||
|
||||
const generated = await generatorPlugin.generateMigration(name);
|
||||
|
||||
filename = generated.filename;
|
||||
content = generated.content;
|
||||
} else if (extension) {
|
||||
content = '';
|
||||
filename = `${getTimestampPrefix()}_${sanitizeMigrationName(name)}${extension}`;
|
||||
}
|
||||
|
||||
if (!filename || content === undefined) {
|
||||
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/cli/src/show-usage-error.ts
Normal file
1
packages/cli/src/show-usage-error.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export class ShowUsageError extends Error {}
|
||||
16
packages/cli/src/types.ts
Normal file
16
packages/cli/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;
|
||||
};
|
||||
8
packages/cli/tsconfig.json
Normal file
8
packages/cli/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "@emigrate/tsconfig/build.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue