From 40000a4fe87f0e6be8b0c2ca5988e36247230f2d Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 2 May 2024 09:50:52 +0200 Subject: [PATCH] fix(cli): Improve plugin generation in monorepos --- .../commands/add/plugin/create-new-plugin.ts | 100 ++++++++++++------ 1 file changed, 70 insertions(+), 30 deletions(-) diff --git a/packages/cli/src/commands/add/plugin/create-new-plugin.ts b/packages/cli/src/commands/add/plugin/create-new-plugin.ts index 3fa07e79be..fb54a36100 100644 --- a/packages/cli/src/commands/add/plugin/create-new-plugin.ts +++ b/packages/cli/src/commands/add/plugin/create-new-plugin.ts @@ -2,11 +2,13 @@ import { cancel, intro, isCancel, log, select, spinner, text } from '@clack/prom import { constantCase, paramCase, pascalCase } from 'change-case'; import * as fs from 'fs-extra'; import path from 'path'; +import { Project, SourceFile } from 'ts-morph'; import { CliCommand, CliCommandReturnVal } from '../../../shared/cli-command'; +import { analyzeProject } from '../../../shared/shared-prompts'; import { VendureConfigRef } from '../../../shared/vendure-config-ref'; import { VendurePluginRef } from '../../../shared/vendure-plugin-ref'; -import { addImportsToFile, createFile, getTsMorphProject } from '../../../utilities/ast-utils'; +import { addImportsToFile, createFile, getPluginClasses } from '../../../utilities/ast-utils'; import { pauseForPromptDisplay } from '../../../utilities/utils'; import { addApiExtensionCommand } from '../api-extension/add-api-extension'; import { addCodegenCommand } from '../codegen/add-codegen'; @@ -29,6 +31,7 @@ const cancelledMessage = 'Plugin setup cancelled.'; export async function createNewPlugin(): Promise { const options: GeneratePluginOptions = { name: '', customEntityName: '', pluginDir: '' } as any; intro('Adding a new Vendure plugin!'); + const { project } = await analyzeProject({ cancelledMessage }); if (!options.name) { const name = await text({ message: 'What is the name of the plugin?', @@ -47,7 +50,8 @@ export async function createNewPlugin(): Promise { options.name = name; } } - const pluginDir = getPluginDirName(options.name); + const existingPluginDir = findExistingPluginsDir(project); + const pluginDir = getPluginDirName(options.name, existingPluginDir); const confirmation = await text({ message: 'Plugin location', initialValue: pluginDir, @@ -65,7 +69,7 @@ export async function createNewPlugin(): Promise { } options.pluginDir = confirmation; - const { plugin, project, modifiedSourceFiles } = await generatePlugin(options); + const { plugin, modifiedSourceFiles } = await generatePlugin(project, options); const configSpinner = spinner(); configSpinner.start('Updating VendureConfig...'); @@ -89,9 +93,6 @@ export async function createNewPlugin(): Promise { 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}?`, @@ -109,20 +110,11 @@ export async function createNewPlugin(): Promise { 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) }); + const result = await command.run({ plugin }); allModifiedSourceFiles = result.modifiedSourceFiles; // We format all modified source files and re-load the // project to avoid issues with the project state @@ -133,8 +125,6 @@ export async function createNewPlugin(): Promise { log.error(`Error adding feature "${command.id}"`); log.error(e.stack); } - - await workingProject.save(); } } @@ -145,8 +135,9 @@ export async function createNewPlugin(): Promise { } export async function generatePlugin( + project: Project, options: GeneratePluginOptions, -): Promise> { +): Promise<{ plugin: VendurePluginRef; modifiedSourceFiles: SourceFile[] }> { const nameWithoutPlugin = options.name.replace(/-?plugin$/i, ''); const normalizedName = nameWithoutPlugin + '-plugin'; const templateContext: NewPluginTemplateContext = { @@ -158,7 +149,6 @@ export async function generatePlugin( const projectSpinner = spinner(); projectSpinner.start('Generating plugin scaffold...'); await pauseForPromptDisplay(); - const { project } = await getTsMorphProject({ skipAddingFilesFromTsConfig: false }); const pluginFile = createFile( project, @@ -169,6 +159,8 @@ export async function generatePlugin( if (!pluginClass) { throw new Error('Could not find the plugin class in the generated file'); } + pluginFile.getImportDeclaration('./constants.template')?.setModuleSpecifier('./constants'); + pluginFile.getImportDeclaration('./types.template')?.setModuleSpecifier('./types'); pluginClass.rename(templateContext.pluginName); const typesFile = createFile( @@ -193,24 +185,72 @@ export async function generatePlugin( projectSpinner.stop('Generated plugin scaffold'); await project.save(); return { - project, modifiedSourceFiles: [pluginFile, typesFile, constantsFile], plugin: new VendurePluginRef(pluginClass), }; } -function getPluginDirName(name: string) { +function findExistingPluginsDir(project: Project): { prefix: string; suffix: string } | undefined { + const pluginClasses = getPluginClasses(project); + if (pluginClasses.length === 0) { + return; + } + const pluginDirs = pluginClasses.map(c => { + return c.getSourceFile().getDirectoryPath(); + }); + const prefix = findCommonPath(pluginDirs); + const suffixStartIndex = prefix.length; + const rest = pluginDirs[0].substring(suffixStartIndex).replace(/^\//, '').split('/'); + const suffix = rest.length > 1 ? rest.slice(1).join('/') : ''; + return { prefix, suffix }; +} + +function getPluginDirName( + name: string, + existingPluginDirPattern: { prefix: string; suffix: string } | undefined, +) { 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 (existingPluginDirPattern) { + return path.join( + existingPluginDirPattern.prefix, + paramCase(nameWithoutPlugin), + existingPluginDirPattern.suffix, + ); + } else { + return path.join(cwd, 'src', 'plugins', paramCase(nameWithoutPlugin)); + } +} - if (currentlyInPluginsDir) { - return path.join(cwd, paramCase(nameWithoutPlugin)); +function findCommonPath(paths: string[]): string { + if (paths.length === 0) { + return ''; // If no paths provided, return empty string } - if (currentlyInRootDir) { - return path.join(cwd, 'src', 'plugins', paramCase(nameWithoutPlugin)); + + // Split each path into segments + const pathSegmentsList = paths.map(p => p.split('/')); + + // Find the minimum length of path segments (to avoid out of bounds) + const minLength = Math.min(...pathSegmentsList.map(segments => segments.length)); + + // Initialize the common path + const commonPath: string[] = []; + + // Loop through each segment index up to the minimum length + for (let i = 0; i < minLength; i++) { + // Get the segment at the current index from the first path + const currentSegment = pathSegmentsList[0][i]; + // Check if this segment is common across all paths + const isCommon = pathSegmentsList.every(segments => segments[i] === currentSegment); + if (isCommon) { + // If it's common, add this segment to the common path + commonPath.push(currentSegment); + } else { + // If it's not common, break out of the loop + break; + } } - return path.join(cwd, paramCase(nameWithoutPlugin)); + + // Join the common path segments back into a string + return commonPath.join('/'); }