Browse Source

refactor(cli): Create VendurePluginDeclaration wrapper class

Michael Bromley 1 year ago
parent
commit
d7cef7ccec

+ 1 - 5
packages/cli/src/cli.ts

@@ -3,18 +3,14 @@
 import { Command } from 'commander';
 
 import { registerAddCommand } from './commands/add/add';
-import { registerNewCommand } from './commands/new/new';
 
 const program = new Command();
 
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 const version = require('../package.json').version;
 
-program
-    .version(version)
-    .description('The Vendure CLI');
+program.version(version).description('The Vendure CLI');
 
-registerNewCommand(program);
 registerAddCommand(program);
 
 void program.parseAsync(process.argv);

+ 13 - 15
packages/cli/src/commands/add/codegen/add-codegen.ts

@@ -2,21 +2,19 @@ import { log, note, outro, spinner } from '@clack/prompts';
 import path from 'path';
 import { ClassDeclaration, StructureKind, SyntaxKind } from 'ts-morph';
 
-import { selectMultiplePluginClasses } from '../../../shared/shared-prompts';
+import { analyzeProject, selectMultiplePluginClasses } from '../../../shared/shared-prompts';
 import { createFile, getRelativeImportPath, getTsMorphProject } from '../../../utilities/ast-utils';
 import { PackageJson } from '../../../utilities/package-utils';
+import { VendurePluginDeclaration } from '../../../utilities/vendure-plugin-declaration';
 
-export async function addCodegen(providedPluginClass?: ClassDeclaration) {
-    let pluginClasses = providedPluginClass ? [providedPluginClass] : [];
-    let project = providedPluginClass?.getProject();
-    if (!pluginClasses.length || !project) {
-        const projectSpinner = spinner();
-        projectSpinner.start('Analyzing project...');
-        await new Promise(resolve => setTimeout(resolve, 100));
-        project = getTsMorphProject();
-        projectSpinner.stop('Project analyzed');
-        pluginClasses = await selectMultiplePluginClasses(project, 'Add codegen cancelled');
-    }
+export async function addCodegen(providedVendurePlugin?: VendurePluginDeclaration) {
+    const project = await analyzeProject({
+        providedVendurePlugin,
+        cancelledMessage: 'Add codegen cancelled',
+    });
+    const plugins = providedVendurePlugin
+        ? [providedVendurePlugin]
+        : await selectMultiplePluginClasses(project, 'Add codegen cancelled');
 
     const packageJson = new PackageJson(project);
     const installSpinner = spinner();
@@ -59,9 +57,9 @@ export async function addCodegen(providedPluginClass?: ClassDeclaration) {
     if (!rootDir) {
         throw new Error('Could not find the root directory of the project');
     }
-    for (const pluginClass of pluginClasses) {
+    for (const plugin of plugins) {
         const relativePluginPath = getRelativeImportPath({
-            from: pluginClass.getSourceFile(),
+            from: plugin.classDeclaration.getSourceFile(),
             to: rootDir,
         });
         const generatedTypesPath = `${path.dirname(relativePluginPath)}/gql/generated.ts`;
@@ -88,7 +86,7 @@ export async function addCodegen(providedPluginClass?: ClassDeclaration) {
     ];
     note(nextSteps.join('\n'));
 
-    if (!providedPluginClass) {
+    if (!providedVendurePlugin) {
         outro('✅ Codegen setup complete!');
     }
 }

+ 11 - 18
packages/cli/src/commands/add/entity/add-entity.ts

@@ -1,10 +1,10 @@
-import { outro, spinner, text } from '@clack/prompts';
+import { outro } 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';
+import { analyzeProject, getCustomEntityName, selectPlugin } from '../../../shared/shared-prompts';
+import { createFile } from '../../../utilities/ast-utils';
+import { VendurePluginDeclaration } from '../../../utilities/vendure-plugin-declaration';
 
 import { addEntityToPlugin } from './codemods/add-entity-to-plugin/add-entity-to-plugin';
 
@@ -17,17 +17,10 @@ export interface AddEntityTemplateContext {
     };
 }
 
-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);
-    }
+export async function addEntity(providedVendurePlugin?: VendurePluginDeclaration) {
+    const project = await analyzeProject({ providedVendurePlugin, cancelledMessage });
+    const vendurePlugin = providedVendurePlugin ?? (await selectPlugin(project, cancelledMessage));
+
     const customEntityName = await getCustomEntityName(cancelledMessage);
     const context: AddEntityTemplateContext = {
         entity: {
@@ -36,17 +29,17 @@ export async function addEntity(providedPluginClass?: ClassDeclaration) {
         },
     };
 
-    const entitiesDir = path.join(pluginClass.getSourceFile().getDirectory().getPath(), 'entities');
+    const entitiesDir = path.join(vendurePlugin.getPluginDir().getPath(), 'entities');
     const entityFile = createFile(project, path.join(__dirname, 'templates/entity.template.ts'));
     entityFile.move(path.join(entitiesDir, `${context.entity.fileName}.ts`));
     entityFile.getClasses()[0].rename(`${context.entity.className}CustomFields`);
     entityFile.getClasses()[1].rename(context.entity.className);
 
-    addEntityToPlugin(pluginClass, entityFile);
+    addEntityToPlugin(vendurePlugin.classDeclaration, entityFile);
 
     project.saveSync();
 
-    if (!providedPluginClass) {
+    if (!providedVendurePlugin) {
         outro('✅  Done!');
     }
 }

+ 6 - 10
packages/cli/src/commands/add/plugin/create-new-plugin.ts

@@ -2,9 +2,9 @@ import { cancel, intro, isCancel, outro, select, spinner, text } from '@clack/pr
 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 { VendurePluginDeclaration } from '../../../utilities/vendure-plugin-declaration';
 import { addEntity } from '../entity/add-entity';
 import { addUiExtensions } from '../ui-extensions/add-ui-extensions';
 
@@ -51,7 +51,7 @@ export async function createNewPlugin() {
     }
 
     options.pluginDir = confirmation;
-    const generatedResult = await generatePlugin(options);
+    const plugin = await generatePlugin(options);
 
     let done = false;
     while (!done) {
@@ -69,18 +69,16 @@ export async function createNewPlugin() {
         if (featureType === 'no') {
             done = true;
         } else if (featureType === 'entity') {
-            await addEntity(generatedResult.pluginClass);
+            await addEntity(plugin);
         } else if (featureType === 'uiExtensions') {
-            await addUiExtensions(generatedResult.pluginClass);
+            await addUiExtensions(plugin);
         }
     }
 
     outro('✅ Plugin setup complete!');
 }
 
-export async function generatePlugin(
-    options: GeneratePluginOptions,
-): Promise<{ pluginClass: ClassDeclaration }> {
+export async function generatePlugin(options: GeneratePluginOptions): Promise<VendurePluginDeclaration> {
     const nameWithoutPlugin = options.name.replace(/-?plugin$/i, '');
     const normalizedName = nameWithoutPlugin + '-plugin';
     const templateContext: NewPluginTemplateContext = {
@@ -118,9 +116,7 @@ export async function generatePlugin(
 
     projectSpinner.stop('Generated plugin scaffold');
     project.saveSync();
-    return {
-        pluginClass,
-    };
+    return new VendurePluginDeclaration(pluginClass);
 }
 
 function getPluginDirName(name: string) {

+ 12 - 28
packages/cli/src/commands/add/ui-extensions/add-ui-extensions.ts

@@ -2,7 +2,7 @@ import { log, note, outro, spinner } from '@clack/prompts';
 import path from 'path';
 import { ClassDeclaration } from 'ts-morph';
 
-import { selectPluginClass } from '../../../shared/shared-prompts';
+import { analyzeProject, selectPlugin } from '../../../shared/shared-prompts';
 import {
     createFile,
     getRelativeImportPath,
@@ -10,28 +10,22 @@ import {
     getVendureConfig,
 } from '../../../utilities/ast-utils';
 import { PackageJson } from '../../../utilities/package-utils';
+import { VendurePluginDeclaration } from '../../../utilities/vendure-plugin-declaration';
 
 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(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');
-    }
+export async function addUiExtensions(providedVendurePlugin?: VendurePluginDeclaration) {
+    const project = await analyzeProject({ providedVendurePlugin });
+    const vendurePlugin =
+        providedVendurePlugin ?? (await selectPlugin(project, 'Add UI extensions cancelled'));
     const packageJson = new PackageJson(project);
 
-    if (pluginAlreadyHasUiExtensionProp(pluginClass)) {
+    if (vendurePlugin.hasUiExtensions()) {
         outro('This plugin already has UI extensions configured');
         return;
     }
-    addUiExtensionStaticProp(pluginClass);
+    addUiExtensionStaticProp(vendurePlugin);
 
     log.success('Updated the plugin class');
     const installSpinner = spinner();
@@ -50,7 +44,7 @@ export async function addUiExtensions(providedPluginClass?: ClassDeclaration) {
     }
     installSpinner.stop('Dependencies installed');
 
-    const pluginDir = pluginClass.getSourceFile().getDirectory().getPath();
+    const pluginDir = vendurePlugin.getPluginDir().getPath();
     const providersFile = createFile(project, path.join(__dirname, 'templates/providers.template.ts'));
     providersFile.move(path.join(pluginDir, 'ui', 'providers.ts'));
     const routesFile = createFile(project, path.join(__dirname, 'templates/routes.template.ts'));
@@ -64,10 +58,10 @@ export async function addUiExtensions(providedPluginClass?: ClassDeclaration) {
             `Could not find the VendureConfig declaration in your project. You will need to manually set up the compileUiExtensions function.`,
         );
     } else {
-        const pluginClassName = pluginClass.getName() as string;
+        const pluginClassName = vendurePlugin.name;
         const pluginPath = getRelativeImportPath({
             to: vendureConfig.getSourceFile(),
-            from: pluginClass.getSourceFile(),
+            from: vendurePlugin.classDeclaration.getSourceFile(),
         });
         const updated = updateAdminUiPluginInit(vendureConfig, { pluginClassName, pluginPath });
         if (updated) {
@@ -83,17 +77,7 @@ export async function addUiExtensions(providedPluginClass?: ClassDeclaration) {
     }
 
     project.saveSync();
-    if (!providedPluginClass) {
+    if (!providedVendurePlugin) {
         outro('✅  Done!');
     }
 }
-
-function pluginAlreadyHasUiExtensionProp(pluginClass: ClassDeclaration) {
-    const uiProperty = pluginClass.getProperty('ui');
-    if (!uiProperty) {
-        return false;
-    }
-    if (uiProperty.isStatic()) {
-        return true;
-    }
-}

+ 5 - 2
packages/cli/src/commands/add/ui-extensions/codemods/add-ui-extension-static-prop/add-ui-extension-static-prop.ts

@@ -1,13 +1,16 @@
 import { ClassDeclaration } from 'ts-morph';
 
+import { AdminUiExtensionTypeName } from '../../../../../constants';
 import { addImportsToFile, kebabize } from '../../../../../utilities/ast-utils';
+import { VendurePluginDeclaration } from '../../../../../utilities/vendure-plugin-declaration';
 
 /**
  * @description
  * Adds the static `ui` property to the plugin class, and adds the required imports.
  */
-export function addUiExtensionStaticProp(pluginClass: ClassDeclaration) {
-    const adminUiExtensionType = 'AdminUiExtension';
+export function addUiExtensionStaticProp(plugin: VendurePluginDeclaration) {
+    const pluginClass = plugin.classDeclaration;
+    const adminUiExtensionType = AdminUiExtensionTypeName;
     const extensionId = kebabize(pluginClass.getName() as string).replace(/-plugin$/, '');
     pluginClass
         .addProperty({

+ 2 - 0
packages/cli/src/constants.ts

@@ -4,3 +4,5 @@ export const defaultManipulationSettings: Partial<ManipulationSettings> = {
     quoteKind: QuoteKind.Single,
     useTrailingCommas: true,
 };
+
+export const AdminUiExtensionTypeName = 'AdminUiExtension';

+ 30 - 7
packages/cli/src/shared/shared-prompts.ts

@@ -1,8 +1,9 @@
-import { cancel, isCancel, multiselect, select, text } from '@clack/prompts';
+import { cancel, isCancel, multiselect, select, spinner, text } from '@clack/prompts';
 import { pascalCase } from 'change-case';
 import { ClassDeclaration, Project } from 'ts-morph';
 
-import { getPluginClasses } from '../utilities/ast-utils';
+import { getPluginClasses, getTsMorphProject } from '../utilities/ast-utils';
+import { VendurePluginDeclaration } from '../utilities/vendure-plugin-declaration';
 
 export async function getCustomEntityName(cancelledMessage: string) {
     const entityName = await text({
@@ -25,7 +26,26 @@ export async function getCustomEntityName(cancelledMessage: string) {
     return pascalCase(entityName);
 }
 
-export async function selectPluginClass(project: Project, cancelledMessage: string) {
+export async function analyzeProject(options: {
+    providedVendurePlugin?: VendurePluginDeclaration;
+    cancelledMessage?: string;
+}) {
+    const providedVendurePlugin = options.providedVendurePlugin;
+    let project = providedVendurePlugin?.classDeclaration.getProject();
+    if (!providedVendurePlugin) {
+        const projectSpinner = spinner();
+        projectSpinner.start('Analyzing project...');
+        await new Promise(resolve => setTimeout(resolve, 100));
+        project = getTsMorphProject();
+        projectSpinner.stop('Project analyzed');
+    }
+    return project as Project;
+}
+
+export async function selectPlugin(
+    project: Project,
+    cancelledMessage: string,
+): Promise<VendurePluginDeclaration> {
     const pluginClasses = getPluginClasses(project);
     const targetPlugin = await select({
         message: 'To which plugin would you like to add the feature?',
@@ -39,10 +59,13 @@ export async function selectPluginClass(project: Project, cancelledMessage: stri
         cancel(cancelledMessage);
         process.exit(0);
     }
-    return targetPlugin as ClassDeclaration;
+    return new VendurePluginDeclaration(targetPlugin as ClassDeclaration);
 }
 
-export async function selectMultiplePluginClasses(project: Project, cancelledMessage: string) {
+export async function selectMultiplePluginClasses(
+    project: Project,
+    cancelledMessage: string,
+): Promise<VendurePluginDeclaration[]> {
     const pluginClasses = getPluginClasses(project);
     const selectAll = await select({
         message: 'To which plugin would you like to add the feature?',
@@ -62,7 +85,7 @@ export async function selectMultiplePluginClasses(project: Project, cancelledMes
         process.exit(0);
     }
     if (selectAll === 'all') {
-        return pluginClasses;
+        return pluginClasses.map(pc => new VendurePluginDeclaration(pc));
     }
     const targetPlugins = await multiselect({
         message: 'Select one or more plugins (use ↑, ↓, space to select)',
@@ -75,5 +98,5 @@ export async function selectMultiplePluginClasses(project: Project, cancelledMes
         cancel(cancelledMessage);
         process.exit(0);
     }
-    return targetPlugins as ClassDeclaration[];
+    return (targetPlugins as ClassDeclaration[]).map(pc => new VendurePluginDeclaration(pc));
 }

+ 21 - 0
packages/cli/src/utilities/vendure-plugin-declaration.ts

@@ -0,0 +1,21 @@
+import { ClassDeclaration } from 'ts-morph';
+
+import { AdminUiExtensionTypeName } from '../constants';
+
+export class VendurePluginDeclaration {
+    constructor(public classDeclaration: ClassDeclaration) {}
+
+    get name(): string {
+        return this.classDeclaration.getName() as string;
+    }
+
+    getPluginDir() {
+        return this.classDeclaration.getSourceFile().getDirectory();
+    }
+
+    hasUiExtensions(): boolean {
+        return !!this.classDeclaration
+            .getStaticProperties()
+            .find(prop => prop.getType().getText() === AdminUiExtensionTypeName);
+    }
+}