emigrate/packages/cli/src/commands/new.ts
Joakim Carlstein cae6d11d53
feat(types): move Emigrate types to separate package and improve types (#41)
* feat(types): move Emigrate types to separate package

Also refactor the types to use discriminating unions for easier error handling and such.
Errors passed to storage plugins should now be serialized and storage plugins are expected to return already serialized errors on failed history entries.

* fix(mysql): handle the new type changes

* fix(storage-fs): handle the new type changes

* feat(cli): better error handling and types

Adapt to the new types from the @emigrate/types package, like discriminating union types and serializing and deserializing errors
2023-12-15 13:03:35 +01:00

156 lines
4.8 KiB
TypeScript

import process from 'node:process';
import fs from 'node:fs/promises';
import path from 'node:path';
import { getTimestampPrefix, sanitizeMigrationName, getOrLoadPlugin, getOrLoadReporter } from '@emigrate/plugin-tools';
import { type MigrationMetadataFinished, type MigrationMetadata, isFailedMigration } from '@emigrate/types';
import {
BadOptionError,
EmigrateError,
MissingArgumentsError,
MissingOptionError,
UnexpectedError,
toError,
} from '../errors.js';
import { type Config } from '../types.js';
import { withLeadingPeriod } from '../with-leading-period.js';
import { version } from '../get-package-info.js';
import { getDuration } from '../get-duration.js';
const lazyDefaultReporter = async () => import('../reporters/default.js');
export default async function newCommand(
{ directory, template, reporter: reporterConfig, plugins = [], extension }: Config,
name: string,
) {
if (!directory) {
throw MissingOptionError.fromOption('directory');
}
if (!name) {
throw MissingArgumentsError.fromArgument('name');
}
if (!extension && !template && plugins.length === 0) {
throw MissingOptionError.fromOption(['extension', 'template', 'plugin']);
}
const cwd = process.cwd();
const reporter = await getOrLoadReporter([reporterConfig ?? lazyDefaultReporter]);
if (!reporter) {
throw BadOptionError.fromOption(
'reporter',
'No reporter found, please specify an existing reporter using the reporter option',
);
}
await reporter.onInit?.({ command: 'new', version, cwd, dry: false, directory });
const start = process.hrtime();
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) {
await reporter.onFinished?.(
[],
new UnexpectedError(`Failed to read template file: ${templatePath}`, { cause: error }),
);
return;
}
filename = `${getTimestampPrefix()}_${sanitizeMigrationName(name)}${withLeadingPeriod(extension ?? fileExtension)}`;
}
let hasGeneratedFile = Boolean(filename && content !== undefined);
if (plugins.length > 0 && !hasGeneratedFile) {
const generatorPlugin = await getOrLoadPlugin('generator', plugins);
if (generatorPlugin) {
const generated = await generatorPlugin.generateMigration(name);
filename = generated.filename;
content = generated.content;
}
}
hasGeneratedFile = Boolean(filename && content !== undefined);
if (extension && !hasGeneratedFile) {
content = '';
filename = `${getTimestampPrefix()}_${sanitizeMigrationName(name)}${withLeadingPeriod(extension)}`;
}
if (!filename || content === undefined) {
throw BadOptionError.fromOption(
'plugin',
'No generator plugin found, please specify a generator plugin using the plugin option',
);
}
const directoryPath = path.resolve(process.cwd(), directory);
const filePath = path.resolve(directoryPath, filename);
const migration: MigrationMetadata = {
name: filename,
filePath,
relativeFilePath: path.relative(cwd, filePath),
extension: withLeadingPeriod(path.extname(filename)),
directory,
cwd,
};
await reporter.onNewMigration?.(migration, content);
const finishedMigrations: MigrationMetadataFinished[] = [];
try {
await createDirectory(directoryPath);
await saveFile(filePath, content);
const duration = getDuration(start);
finishedMigrations.push({ ...migration, status: 'done', duration });
} catch (error) {
const duration = getDuration(start);
const errorInstance = toError(error);
finishedMigrations.push({ ...migration, status: 'failed', duration, error: errorInstance });
}
// eslint-disable-next-line unicorn/no-array-callback-reference
const firstFailed = finishedMigrations.find(isFailedMigration);
const firstError =
firstFailed?.error instanceof EmigrateError
? firstFailed.error
: firstFailed
? new UnexpectedError(`Failed to create migration file: ${firstFailed.relativeFilePath}`, {
cause: firstFailed?.error,
})
: undefined;
await reporter.onFinished?.(finishedMigrations, firstError);
}
async function createDirectory(directoryPath: string) {
try {
await fs.mkdir(directoryPath, { recursive: true });
} catch (error) {
throw new UnexpectedError(`Failed to create migration directory: ${directoryPath}`, { cause: error });
}
}
async function saveFile(filePath: string, content: string) {
try {
await fs.writeFile(filePath, content);
} catch (error) {
throw new UnexpectedError(`Failed to write migration file: ${filePath}`, { cause: error });
}
}