| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- import { cancel, intro, isCancel, log, select, spinner, text } from '@clack/prompts';
- import { constantCase, paramCase, pascalCase } from 'change-case';
- import * as fs from 'fs-extra';
- import path from 'path';
- import { CliCommand, CliCommandReturnVal } from '../../../shared/cli-command';
- import { VendureConfigRef } from '../../../shared/vendure-config-ref';
- import { VendurePluginRef } from '../../../shared/vendure-plugin-ref';
- import { addImportsToFile, createFile, getTsMorphProject } from '../../../utilities/ast-utils';
- import { pauseForPromptDisplay } from '../../../utilities/utils';
- import { addApiExtensionCommand } from '../api-extension/add-api-extension';
- import { addCodegenCommand } from '../codegen/add-codegen';
- import { addEntityCommand } from '../entity/add-entity';
- import { addJobQueueCommand } from '../job-queue/add-job-queue';
- import { addServiceCommand } from '../service/add-service';
- import { addUiExtensionsCommand } from '../ui-extensions/add-ui-extensions';
- import { GeneratePluginOptions, NewPluginTemplateContext } from './types';
- export const createNewPluginCommand = new CliCommand({
- id: 'create-new-plugin',
- category: 'Plugin',
- description: 'Create a new Vendure plugin',
- run: createNewPlugin,
- });
- const cancelledMessage = 'Plugin setup cancelled.';
- export async function createNewPlugin(): Promise<CliCommandReturnVal> {
- const options: GeneratePluginOptions = { name: '', customEntityName: '', pluginDir: '' } as any;
- intro('Adding a new Vendure plugin!');
- if (!options.name) {
- const name = await text({
- message: 'What is the name of the plugin?',
- initialValue: 'my-new-feature',
- validate: input => {
- if (!/^[a-z][a-z-0-9]+$/.test(input)) {
- return 'The plugin name must be lowercase and contain only letters, numbers and dashes';
- }
- },
- });
- if (isCancel(name)) {
- cancel(cancelledMessage);
- process.exit(0);
- } else {
- options.name = name;
- }
- }
- const pluginDir = getPluginDirName(options.name);
- const confirmation = await text({
- message: 'Plugin location',
- initialValue: pluginDir,
- placeholder: '',
- validate: input => {
- if (fs.existsSync(input)) {
- return `A directory named "${input}" already exists. Please specify a different directory.`;
- }
- },
- });
- if (isCancel(confirmation)) {
- cancel(cancelledMessage);
- process.exit(0);
- }
- options.pluginDir = confirmation;
- const { plugin, project, modifiedSourceFiles } = await generatePlugin(options);
- const configSpinner = spinner();
- configSpinner.start('Updating VendureConfig...');
- await pauseForPromptDisplay();
- const vendureConfig = new VendureConfigRef(project);
- vendureConfig.addToPluginsArray(`${plugin.name}.init({})`);
- addImportsToFile(vendureConfig.sourceFile, {
- moduleSpecifier: plugin.getSourceFile(),
- namedImports: [plugin.name],
- });
- await vendureConfig.sourceFile.getProject().save();
- configSpinner.stop('Updated VendureConfig');
- let done = false;
- const followUpCommands = [
- addEntityCommand,
- addServiceCommand,
- addApiExtensionCommand,
- addJobQueueCommand,
- addUiExtensionsCommand,
- addCodegenCommand,
- ];
- let allModifiedSourceFiles = [...modifiedSourceFiles];
- const pluginClassName = plugin.name;
- let workingPlugin = plugin;
- let workingProject = project;
- while (!done) {
- const featureType = await select({
- message: `Add features to ${options.name}?`,
- options: [
- { value: 'no', label: "[Finish] No, I'm done!" },
- ...followUpCommands.map(c => ({
- value: c.id,
- label: `[${c.category}] ${c.description}`,
- })),
- ],
- });
- if (isCancel(featureType)) {
- done = true;
- }
- if (featureType === 'no') {
- done = true;
- } else {
- const { project: newProject } = await getTsMorphProject();
- workingProject = newProject;
- const newPlugin = newProject
- .getSourceFile(workingPlugin.getSourceFile().getFilePath())
- ?.getClass(pluginClassName);
- if (!newPlugin) {
- throw new Error(`Could not find class "${pluginClassName}" in the new project`);
- }
- workingPlugin = new VendurePluginRef(newPlugin);
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const command = followUpCommands.find(c => c.id === featureType)!;
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- try {
- const result = await command.run({ plugin: new VendurePluginRef(newPlugin) });
- allModifiedSourceFiles = result.modifiedSourceFiles;
- // We format all modified source files and re-load the
- // project to avoid issues with the project state
- for (const sourceFile of allModifiedSourceFiles) {
- sourceFile.organizeImports();
- }
- } catch (e: any) {
- log.error(`Error adding feature "${command.id}"`);
- log.error(e.stack);
- }
- await workingProject.save();
- }
- }
- return {
- project,
- modifiedSourceFiles: [],
- };
- }
- export async function generatePlugin(
- options: GeneratePluginOptions,
- ): Promise<CliCommandReturnVal<{ plugin: VendurePluginRef }>> {
- const nameWithoutPlugin = options.name.replace(/-?plugin$/i, '');
- const normalizedName = nameWithoutPlugin + '-plugin';
- const templateContext: NewPluginTemplateContext = {
- ...options,
- pluginName: pascalCase(normalizedName),
- pluginInitOptionsName: constantCase(normalizedName) + '_OPTIONS',
- };
- const projectSpinner = spinner();
- projectSpinner.start('Generating plugin scaffold...');
- await pauseForPromptDisplay();
- const { project } = await getTsMorphProject({ skipAddingFilesFromTsConfig: false });
- const pluginFile = createFile(
- project,
- path.join(__dirname, 'templates/plugin.template.ts'),
- path.join(options.pluginDir, paramCase(nameWithoutPlugin) + '.plugin.ts'),
- );
- const pluginClass = pluginFile.getClass('TemplatePlugin');
- if (!pluginClass) {
- throw new Error('Could not find the plugin class in the generated file');
- }
- pluginClass.rename(templateContext.pluginName);
- const typesFile = createFile(
- project,
- path.join(__dirname, 'templates/types.template.ts'),
- path.join(options.pluginDir, 'types.ts'),
- );
- const constantsFile = createFile(
- project,
- path.join(__dirname, 'templates/constants.template.ts'),
- path.join(options.pluginDir, 'constants.ts'),
- );
- constantsFile
- .getVariableDeclaration('TEMPLATE_PLUGIN_OPTIONS')
- ?.rename(templateContext.pluginInitOptionsName)
- .set({ initializer: `Symbol('${templateContext.pluginInitOptionsName}')` });
- constantsFile
- .getVariableDeclaration('loggerCtx')
- ?.set({ initializer: `'${templateContext.pluginName}'` });
- projectSpinner.stop('Generated plugin scaffold');
- await project.save();
- return {
- project,
- modifiedSourceFiles: [pluginFile, typesFile, constantsFile],
- plugin: new VendurePluginRef(pluginClass),
- };
- }
- function getPluginDirName(name: string) {
- const cwd = process.cwd();
- const pathParts = cwd.split(path.sep);
- const currentlyInPluginsDir = pathParts[pathParts.length - 1] === 'plugins';
- const currentlyInRootDir = fs.pathExistsSync(path.join(cwd, 'package.json'));
- const nameWithoutPlugin = name.replace(/-?plugin$/i, '');
- if (currentlyInPluginsDir) {
- return path.join(cwd, paramCase(nameWithoutPlugin));
- }
- if (currentlyInRootDir) {
- return path.join(cwd, 'src', 'plugins', paramCase(nameWithoutPlugin));
- }
- return path.join(cwd, paramCase(nameWithoutPlugin));
- }
|