Просмотр исходного кода

fix(cli): Improve plugin generation in monorepos

Michael Bromley 1 год назад
Родитель
Сommit
40000a4fe8
1 измененных файлов с 70 добавлено и 30 удалено
  1. 70 30
      packages/cli/src/commands/add/plugin/create-new-plugin.ts

+ 70 - 30
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 { constantCase, paramCase, pascalCase } from 'change-case';
 import * as fs from 'fs-extra';
 import * as fs from 'fs-extra';
 import path from 'path';
 import path from 'path';
+import { Project, SourceFile } from 'ts-morph';
 
 
 import { CliCommand, CliCommandReturnVal } from '../../../shared/cli-command';
 import { CliCommand, CliCommandReturnVal } from '../../../shared/cli-command';
+import { analyzeProject } from '../../../shared/shared-prompts';
 import { VendureConfigRef } from '../../../shared/vendure-config-ref';
 import { VendureConfigRef } from '../../../shared/vendure-config-ref';
 import { VendurePluginRef } from '../../../shared/vendure-plugin-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 { pauseForPromptDisplay } from '../../../utilities/utils';
 import { addApiExtensionCommand } from '../api-extension/add-api-extension';
 import { addApiExtensionCommand } from '../api-extension/add-api-extension';
 import { addCodegenCommand } from '../codegen/add-codegen';
 import { addCodegenCommand } from '../codegen/add-codegen';
@@ -29,6 +31,7 @@ const cancelledMessage = 'Plugin setup cancelled.';
 export async function createNewPlugin(): Promise<CliCommandReturnVal> {
 export async function createNewPlugin(): Promise<CliCommandReturnVal> {
     const options: GeneratePluginOptions = { name: '', customEntityName: '', pluginDir: '' } as any;
     const options: GeneratePluginOptions = { name: '', customEntityName: '', pluginDir: '' } as any;
     intro('Adding a new Vendure plugin!');
     intro('Adding a new Vendure plugin!');
+    const { project } = await analyzeProject({ cancelledMessage });
     if (!options.name) {
     if (!options.name) {
         const name = await text({
         const name = await text({
             message: 'What is the name of the plugin?',
             message: 'What is the name of the plugin?',
@@ -47,7 +50,8 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
             options.name = name;
             options.name = name;
         }
         }
     }
     }
-    const pluginDir = getPluginDirName(options.name);
+    const existingPluginDir = findExistingPluginsDir(project);
+    const pluginDir = getPluginDirName(options.name, existingPluginDir);
     const confirmation = await text({
     const confirmation = await text({
         message: 'Plugin location',
         message: 'Plugin location',
         initialValue: pluginDir,
         initialValue: pluginDir,
@@ -65,7 +69,7 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
     }
     }
 
 
     options.pluginDir = confirmation;
     options.pluginDir = confirmation;
-    const { plugin, project, modifiedSourceFiles } = await generatePlugin(options);
+    const { plugin, modifiedSourceFiles } = await generatePlugin(project, options);
 
 
     const configSpinner = spinner();
     const configSpinner = spinner();
     configSpinner.start('Updating VendureConfig...');
     configSpinner.start('Updating VendureConfig...');
@@ -89,9 +93,6 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
         addCodegenCommand,
         addCodegenCommand,
     ];
     ];
     let allModifiedSourceFiles = [...modifiedSourceFiles];
     let allModifiedSourceFiles = [...modifiedSourceFiles];
-    const pluginClassName = plugin.name;
-    let workingPlugin = plugin;
-    let workingProject = project;
     while (!done) {
     while (!done) {
         const featureType = await select({
         const featureType = await select({
             message: `Add features to ${options.name}?`,
             message: `Add features to ${options.name}?`,
@@ -109,20 +110,11 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
         if (featureType === 'no') {
         if (featureType === 'no') {
             done = true;
             done = true;
         } else {
         } 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
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
             const command = followUpCommands.find(c => c.id === featureType)!;
             const command = followUpCommands.find(c => c.id === featureType)!;
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
             // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
             try {
             try {
-                const result = await command.run({ plugin: new VendurePluginRef(newPlugin) });
+                const result = await command.run({ plugin });
                 allModifiedSourceFiles = result.modifiedSourceFiles;
                 allModifiedSourceFiles = result.modifiedSourceFiles;
                 // We format all modified source files and re-load the
                 // We format all modified source files and re-load the
                 // project to avoid issues with the project state
                 // project to avoid issues with the project state
@@ -133,8 +125,6 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
                 log.error(`Error adding feature "${command.id}"`);
                 log.error(`Error adding feature "${command.id}"`);
                 log.error(e.stack);
                 log.error(e.stack);
             }
             }
-
-            await workingProject.save();
         }
         }
     }
     }
 
 
@@ -145,8 +135,9 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
 }
 }
 
 
 export async function generatePlugin(
 export async function generatePlugin(
+    project: Project,
     options: GeneratePluginOptions,
     options: GeneratePluginOptions,
-): Promise<CliCommandReturnVal<{ plugin: VendurePluginRef }>> {
+): Promise<{ plugin: VendurePluginRef; modifiedSourceFiles: SourceFile[] }> {
     const nameWithoutPlugin = options.name.replace(/-?plugin$/i, '');
     const nameWithoutPlugin = options.name.replace(/-?plugin$/i, '');
     const normalizedName = nameWithoutPlugin + '-plugin';
     const normalizedName = nameWithoutPlugin + '-plugin';
     const templateContext: NewPluginTemplateContext = {
     const templateContext: NewPluginTemplateContext = {
@@ -158,7 +149,6 @@ export async function generatePlugin(
     const projectSpinner = spinner();
     const projectSpinner = spinner();
     projectSpinner.start('Generating plugin scaffold...');
     projectSpinner.start('Generating plugin scaffold...');
     await pauseForPromptDisplay();
     await pauseForPromptDisplay();
-    const { project } = await getTsMorphProject({ skipAddingFilesFromTsConfig: false });
 
 
     const pluginFile = createFile(
     const pluginFile = createFile(
         project,
         project,
@@ -169,6 +159,8 @@ export async function generatePlugin(
     if (!pluginClass) {
     if (!pluginClass) {
         throw new Error('Could not find the plugin class in the generated file');
         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);
     pluginClass.rename(templateContext.pluginName);
 
 
     const typesFile = createFile(
     const typesFile = createFile(
@@ -193,24 +185,72 @@ export async function generatePlugin(
     projectSpinner.stop('Generated plugin scaffold');
     projectSpinner.stop('Generated plugin scaffold');
     await project.save();
     await project.save();
     return {
     return {
-        project,
         modifiedSourceFiles: [pluginFile, typesFile, constantsFile],
         modifiedSourceFiles: [pluginFile, typesFile, constantsFile],
         plugin: new VendurePluginRef(pluginClass),
         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 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, '');
     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('/');
 }
 }