Browse Source

feat(cli): Allow chaining features onto a newly-created plugin

Michael Bromley 1 year ago
parent
commit
5b32c59313

+ 16 - 9
packages/cli/src/commands/add/entity/add-entity.ts

@@ -1,6 +1,7 @@
 import { outro, spinner, text } from '@clack/prompts';
 import { paramCase } from 'change-case';
 import path from 'path';
+import { ClassDeclaration } from 'ts-morph';
 
 import { getCustomEntityName, selectPluginClass } from '../../../shared/shared-prompts';
 import { createFile, getTsMorphProject } from '../../../utilities/ast-utils';
@@ -16,14 +17,17 @@ export interface AddEntityTemplateContext {
     };
 }
 
-export async function addEntity() {
-    const projectSpinner = spinner();
-    projectSpinner.start('Analyzing project...');
-    await new Promise(resolve => setTimeout(resolve, 100));
-    const project = getTsMorphProject();
-    projectSpinner.stop('Project analyzed');
-
-    const pluginClass = await selectPluginClass(project, cancelledMessage);
+export async function addEntity(providedPluginClass?: ClassDeclaration) {
+    let pluginClass = providedPluginClass;
+    let project = pluginClass?.getProject();
+    if (!pluginClass || !project) {
+        const projectSpinner = spinner();
+        projectSpinner.start('Analyzing project...');
+        await new Promise(resolve => setTimeout(resolve, 100));
+        project = getTsMorphProject();
+        projectSpinner.stop('Project analyzed');
+        pluginClass = await selectPluginClass(project, cancelledMessage);
+    }
     const customEntityName = await getCustomEntityName(cancelledMessage);
     const context: AddEntityTemplateContext = {
         entity: {
@@ -41,5 +45,8 @@ export async function addEntity() {
     addEntityToPlugin(pluginClass, entityFile);
 
     project.saveSync();
-    outro('✅  Done!');
+
+    if (!providedPluginClass) {
+        outro('✅  Done!');
+    }
 }

+ 0 - 1
packages/cli/src/commands/add/entity/codemods/add-entity-to-plugin/add-entity-to-plugin.ts

@@ -1,7 +1,6 @@
 import { ClassDeclaration, Node, SourceFile, SyntaxKind } from 'ts-morph';
 
 import { addImportsToFile } from '../../../../../utilities/ast-utils';
-import { AddEntityTemplateContext } from '../../add-entity';
 
 export function addEntityToPlugin(pluginClass: ClassDeclaration, entitySourceFile: SourceFile) {
     const pluginDecorator = pluginClass.getDecorator('VendurePlugin');

+ 44 - 9
packages/cli/src/commands/add/plugin/create-new-plugin.ts

@@ -1,9 +1,12 @@
-import { cancel, intro, isCancel, outro, spinner, text } from '@clack/prompts';
+import { cancel, intro, isCancel, outro, select, spinner, text } from '@clack/prompts';
 import { constantCase, paramCase, pascalCase } from 'change-case';
 import * as fs from 'fs-extra';
 import path from 'path';
+import { ClassDeclaration } from 'ts-morph';
 
 import { createFile, getTsMorphProject } from '../../../utilities/ast-utils';
+import { addEntity } from '../entity/add-entity';
+import { addUiExtensions } from '../ui-extensions/add-ui-extensions';
 
 import { GeneratePluginOptions, NewPluginTemplateContext } from './types';
 
@@ -11,7 +14,7 @@ const cancelledMessage = 'Plugin setup cancelled.';
 
 export async function createNewPlugin() {
     const options: GeneratePluginOptions = { name: '', customEntityName: '', pluginDir: '' } as any;
-    intro('Scaffolding a new Vendure plugin!');
+    intro('Adding a new Vendure plugin!');
     if (!options.name) {
         const name = await text({
             message: 'What is the name of the plugin?',
@@ -45,13 +48,39 @@ export async function createNewPlugin() {
     if (isCancel(confirmation)) {
         cancel(cancelledMessage);
         process.exit(0);
-    } else {
-        options.pluginDir = confirmation;
-        await generatePlugin(options);
     }
+
+    options.pluginDir = confirmation;
+    const generatedResult = await generatePlugin(options);
+
+    let done = false;
+    while (!done) {
+        const featureType = await select({
+            message: `Add features to ${options.name}?`,
+            options: [
+                { value: 'no', label: "[Finish] No, I'm done!" },
+                { value: 'entity', label: '[Plugin: Entity] Add a new entity to the plugin' },
+                { value: 'uiExtensions', label: '[Plugin: UI] Set up Admin UI extensions' },
+            ],
+        });
+        if (isCancel(featureType)) {
+            done = true;
+        }
+        if (featureType === 'no') {
+            done = true;
+        } else if (featureType === 'entity') {
+            await addEntity(generatedResult.pluginClass);
+        } else if (featureType === 'uiExtensions') {
+            await addUiExtensions(generatedResult.pluginClass);
+        }
+    }
+
+    outro('✅ Plugin setup complete!');
 }
 
-export async function generatePlugin(options: GeneratePluginOptions) {
+export async function generatePlugin(
+    options: GeneratePluginOptions,
+): Promise<{ pluginClass: ClassDeclaration }> {
     const nameWithoutPlugin = options.name.replace(/-?plugin$/i, '');
     const normalizedName = nameWithoutPlugin + '-plugin';
     const templateContext: NewPluginTemplateContext = {
@@ -66,7 +95,11 @@ export async function generatePlugin(options: GeneratePluginOptions) {
     const project = getTsMorphProject({ skipAddingFilesFromTsConfig: true });
 
     const pluginFile = createFile(project, path.join(__dirname, 'templates/plugin.template.ts'));
-    pluginFile.getClass('TemplatePlugin')?.rename(templateContext.pluginName);
+    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'));
 
@@ -83,9 +116,11 @@ export async function generatePlugin(options: GeneratePluginOptions) {
     pluginFile.move(path.join(options.pluginDir, paramCase(nameWithoutPlugin) + '.plugin.ts'));
     constantsFile.move(path.join(options.pluginDir, 'constants.ts'));
 
-    projectSpinner.stop('Done');
+    projectSpinner.stop('Generated plugin scaffold');
     project.saveSync();
-    outro('✅ Plugin scaffolding complete!');
+    return {
+        pluginClass,
+    };
 }
 
 function getPluginDirName(name: string) {

+ 15 - 10
packages/cli/src/commands/add/ui-extensions/add-ui-extensions.ts

@@ -14,17 +14,20 @@ import { determineVendureVersion, installRequiredPackages } from '../../../utili
 import { addUiExtensionStaticProp } from './codemods/add-ui-extension-static-prop/add-ui-extension-static-prop';
 import { updateAdminUiPluginInit } from './codemods/update-admin-ui-plugin-init/update-admin-ui-plugin-init';
 
-export async function addUiExtensions() {
-    const projectSpinner = spinner();
-    projectSpinner.start('Analyzing project...');
-
-    await new Promise(resolve => setTimeout(resolve, 100));
-    const project = getTsMorphProject();
-    projectSpinner.stop('Project analyzed');
+export async function addUiExtensions(providedPluginClass?: ClassDeclaration) {
+    let pluginClass = providedPluginClass;
+    let project = pluginClass?.getProject();
+    if (!pluginClass || !project) {
+        const projectSpinner = spinner();
+        projectSpinner.start('Analyzing project...');
+        await new Promise(resolve => setTimeout(resolve, 100));
+        project = getTsMorphProject();
+        projectSpinner.stop('Project analyzed');
+        pluginClass = await selectPluginClass(project, 'Add UI extensions cancelled');
+    }
 
-    const pluginClass = await selectPluginClass(project, 'Add UI extensions cancelled');
     if (pluginAlreadyHasUiExtensionProp(pluginClass)) {
-        outro('This plugin already has a UI extension configured');
+        outro('This plugin already has UI extensions configured');
         return;
     }
     addUiExtensionStaticProp(pluginClass);
@@ -79,7 +82,9 @@ export async function addUiExtensions() {
     }
 
     project.saveSync();
-    outro('✅  Done!');
+    if (!providedPluginClass) {
+        outro('✅  Done!');
+    }
 }
 
 function pluginAlreadyHasUiExtensionProp(pluginClass: ClassDeclaration) {

+ 31 - 7
packages/cli/src/utilities/ast-utils.ts

@@ -44,17 +44,41 @@ export function getPluginClasses(project: Project) {
 }
 
 export function getVendureConfig(project: Project, options: { checkFileName?: boolean } = {}) {
-    const sourceFiles = project.getSourceFiles();
     const checkFileName = options.checkFileName ?? true;
+
     function isVendureConfigVariableDeclaration(v: VariableDeclaration) {
         return v.getType().getText(v) === 'VendureConfig';
     }
-    const vendureConfigFile = sourceFiles.find(sf => {
-        return (
-            (checkFileName ? sf.getFilePath().endsWith('vendure-config.ts') : true) &&
-            sf.getVariableDeclarations().find(isVendureConfigVariableDeclaration)
-        );
-    });
+
+    function getVendureConfigSourceFile(sourceFiles: SourceFile[]) {
+        return sourceFiles.find(sf => {
+            return (
+                (checkFileName ? sf.getFilePath().endsWith('vendure-config.ts') : true) &&
+                sf.getVariableDeclarations().find(isVendureConfigVariableDeclaration)
+            );
+        });
+    }
+
+    function findAndAddVendureConfigToProject() {
+        // If the project does not contain a vendure-config.ts file, we'll look for a vendure-config.ts file
+        // in the src directory.
+        const srcDir = project.getDirectory('src');
+        if (srcDir) {
+            const srcDirPath = srcDir.getPath();
+            const srcFiles = fs.readdirSync(srcDirPath);
+
+            const filePath = srcFiles.find(file => file.includes('vendure-config.ts'));
+            if (filePath) {
+                project.addSourceFileAtPath(path.join(srcDirPath, filePath));
+            }
+        }
+    }
+
+    let vendureConfigFile = getVendureConfigSourceFile(project.getSourceFiles());
+    if (!vendureConfigFile) {
+        findAndAddVendureConfigToProject();
+        vendureConfigFile = getVendureConfigSourceFile(project.getSourceFiles());
+    }
     return vendureConfigFile
         ?.getVariableDeclarations()
         .find(isVendureConfigVariableDeclaration)