From 0081f77e86b71be5e2bc13ed378d68d675628842 Mon Sep 17 00:00:00 2001 From: Joakim Carlstein Date: Thu, 9 Nov 2023 22:22:43 +0100 Subject: [PATCH] feat(emigrate): add some rough support for generating new migration files And add some CLI args parsing and usage messages for upcoming commands as well --- package.json | 5 +- packages/emigrate/package.json | 18 +-- packages/emigrate/src/cli.ts | 248 ++++++++++++++++++++++++++++++++- pnpm-lock.yaml | 40 +++++- 4 files changed, 291 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 9244f2d..6e0ec30 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,10 @@ }, "xo": { "space": true, - "prettier": true + "prettier": true, + "rules": { + "complexity": 0 + } }, "dependencies": { "@changesets/cli": "2.26.2", diff --git a/packages/emigrate/package.json b/packages/emigrate/package.json index d94264b..bb09aff 100644 --- a/packages/emigrate/package.json +++ b/packages/emigrate/package.json @@ -25,16 +25,10 @@ "devDependencies": { "@emigrate/tsconfig": "workspace:*" }, - "author": { - "name": "Aboviq AB", - "email": "dev@aboviq.com", - "url": "https://www.aboviq.com" - }, - "contributors": [ - { - "name": "Joakim Carlstein", - "email": "joakim@aboviq.com" - } - ], - "license": "MIT" + "author": "Aboviq AB (https://www.aboviq.com)", + "license": "MIT", + "dependencies": { + "@emigrate/plugin-tools": "workspace:*", + "import-from-esm": "1.1.3" + } } diff --git a/packages/emigrate/src/cli.ts b/packages/emigrate/src/cli.ts index 4daebaf..6e01aa6 100644 --- a/packages/emigrate/src/cli.ts +++ b/packages/emigrate/src/cli.ts @@ -1,4 +1,248 @@ #!/usr/bin/env node -import { emigrate } from '.'; +import process from 'node:process'; +import { parseArgs } from 'node:util'; +import { isGeneratorPlugin } from '@emigrate/plugin-tools'; +import { type GeneratorPlugin } from '@emigrate/plugin-tools/types'; -emigrate(); +type Action = (args: string[]) => Promise; + +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 { values, positionals } = parseArgs({ + args, + options: { + help: { + type: 'boolean', + short: 'h', + }, + dir: { + type: 'string', + short: 'd', + }, + plugin: { + type: 'string', + short: 'p', + multiple: true, + default: [], + }, + }, + allowPositionals: true, + }); + + const hasPositionals = positionals.join('').trim() !== ''; + const showHelp = !values.dir || !hasPositionals || values.help; + + if (!values.dir) { + console.error('Missing required option: --dir\n'); + } + + if (!hasPositionals) { + console.error('Missing required migration name: \n'); + } + + if (showHelp) { + console.log(`Usage: emigrate new [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 new --dir src/migrations create users table + emigrate new --dir ./migrations --plugin @emigrate/plugin-generate-sql create_users_table +`); + process.exitCode = 1; + return; + } + + const { plugin: plugins = [] } = values; + + if (plugins.length > 0) { + let generatorPlugin: GeneratorPlugin | undefined; + + const path = await import('node:path'); + + 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) { + 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; + return; + } + + const fs = await import('node:fs/promises'); + + 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; + } + } +}; + +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 = { + 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 + +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) { + console.error(error instanceof Error ? error.message : error); + process.exit(1); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 678898b..95cc2f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + importers: .: @@ -45,9 +49,32 @@ importers: version: 0.56.0(webpack@5.89.0) packages/emigrate: + dependencies: + '@emigrate/plugin-tools': + specifier: workspace:* + version: link:../plugin-tools + import-from-esm: + specifier: 1.1.3 + version: 1.1.3 devDependencies: '@emigrate/tsconfig': - specifier: 0.0.0 + specifier: workspace:* + version: link:../tsconfig + + packages/plugin-generate-js: + dependencies: + '@emigrate/plugin-tools': + specifier: workspace:* + version: link:../plugin-tools + devDependencies: + '@emigrate/tsconfig': + specifier: workspace:* + version: link:../tsconfig + + packages/plugin-tools: + devDependencies: + '@emigrate/tsconfig': + specifier: workspace:* version: link:../tsconfig packages/tsconfig: {} @@ -3107,6 +3134,13 @@ packages: resolve-from: 4.0.0 dev: false + /import-from-esm@1.1.3: + resolution: {integrity: sha512-1BxFAthpQf5qabfPBaFBRAGIh8TVt6WB4ujqedfoF4oVjwyl6S/dZv26gL5kgPhbO1XBqu4hcELUlV/+IPsC3A==} + engines: {node: '>=16.20'} + dependencies: + import-meta-resolve: 4.0.0 + dev: false + /import-meta-resolve@4.0.0: resolution: {integrity: sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==} dev: false @@ -5849,7 +5883,3 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: false - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false