feat(plugins): automatically prefix plugins when loading if necessary

I.e. when specifying only "--plugin generate-js" Emigrate will load the @emigrate/plugin-generate-js plugin.
This commit is contained in:
Joakim Carlstein 2023-11-10 10:45:15 +01:00
parent ca3ab9ec62
commit 9c239e0ae5
8 changed files with 83 additions and 29 deletions

View file

@ -28,7 +28,6 @@
"author": "Aboviq AB <dev@aboviq.com> (https://www.aboviq.com)",
"license": "MIT",
"dependencies": {
"@emigrate/plugin-tools": "workspace:*",
"import-from-esm": "1.1.3"
"@emigrate/plugin-tools": "workspace:*"
}
}

View file

@ -1,7 +1,7 @@
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 { getTimestampPrefix, sanitizeMigrationName, loadPlugin } from '@emigrate/plugin-tools';
import { type GeneratorPlugin } from '@emigrate/plugin-tools/types';
import { ShowUsageError } from './show-usage-error.js';
@ -45,27 +45,10 @@ export default async function newCommand({ directory, template, plugins, name }:
let generatorPlugin: GeneratorPlugin | undefined;
for await (const plugin of plugins) {
const pluginPath = plugin.startsWith('.') ? path.resolve(process.cwd(), plugin) : plugin;
generatorPlugin = await loadPlugin('generator', 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) {
break;
}
}

View file

@ -32,5 +32,8 @@
"license": "MIT",
"devDependencies": {
"@emigrate/tsconfig": "workspace:*"
},
"dependencies": {
"import-from-esm": "1.2.1"
}
}

View file

@ -1,4 +1,5 @@
import { type GeneratorPlugin, type StoragePlugin } from './types.js';
import process from 'node:process';
import { type PluginFromType, type PluginType, type GeneratorPlugin, type StoragePlugin } from './types.js';
export const createStoragePlugin = (initialize: StoragePlugin['initialize']): StoragePlugin => {
return {
@ -38,6 +39,59 @@ export const isStoragePlugin = (plugin: any): plugin is StoragePlugin => {
return false;
};
export const isPluginOfType = <T extends PluginType>(type: T, plugin: any): plugin is PluginFromType<T> => {
if (type === 'generator') {
return isGeneratorPlugin(plugin);
}
if (type === 'storage') {
return isStoragePlugin(plugin);
}
throw new Error(`Unknown plugin type: ${type}`);
};
export const loadPlugin = async <T extends PluginType>(
type: T,
plugin: string,
): Promise<PluginFromType<T> | undefined> => {
let importFromEsm = await import('import-from-esm');
// Because of "allowSyntheticDefaultImports" we need to do this ugly hack
if ((importFromEsm as any).default) {
importFromEsm = (importFromEsm as any).default as unknown as typeof importFromEsm;
}
const importsToTry = plugin.startsWith('.')
? [plugin]
: [plugin, `@emigrate/plugin-${plugin}`, `emigrate-plugin-${plugin}`];
for await (const importPath of importsToTry) {
try {
const pluginModule: unknown = await importFromEsm(process.cwd(), importPath);
// Support module.exports = ...
if (isPluginOfType(type, pluginModule)) {
return pluginModule;
}
// Support export default ...
if (
pluginModule &&
typeof pluginModule === 'object' &&
'default' in pluginModule &&
isPluginOfType(type, pluginModule.default)
) {
return pluginModule.default;
}
} catch {
// Ignore errors
}
}
return undefined;
};
/**
* Get a timestamp string in the format YYYYMMDDHHmmssmmm based on the current time (UTC)
*

View file

@ -86,3 +86,7 @@ export type GeneratorPlugin = {
};
export type Plugin = StoragePlugin | GeneratorPlugin;
export type PluginType = Plugin['type'];
export type PluginFromType<T extends PluginType> = Extract<Plugin, { type: T }>;