diff --git a/packages/cli/src/commands/add/entity/add-entity.ts b/packages/cli/src/commands/add/entity/add-entity.ts index e3b0beedfc..a862afdd36 100644 --- a/packages/cli/src/commands/add/entity/add-entity.ts +++ b/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!'); + } } diff --git a/packages/cli/src/commands/add/entity/codemods/add-entity-to-plugin/add-entity-to-plugin.ts b/packages/cli/src/commands/add/entity/codemods/add-entity-to-plugin/add-entity-to-plugin.ts index 423d597a9b..17c25d60fb 100644 --- a/packages/cli/src/commands/add/entity/codemods/add-entity-to-plugin/add-entity-to-plugin.ts +++ b/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'); 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 711cb132e4..26657acb20 100644 --- a/packages/cli/src/commands/add/plugin/create-new-plugin.ts +++ b/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) { diff --git a/packages/cli/src/commands/add/ui-extensions/add-ui-extensions.ts b/packages/cli/src/commands/add/ui-extensions/add-ui-extensions.ts index 568bff4ee6..1ca5ba09ce 100644 --- a/packages/cli/src/commands/add/ui-extensions/add-ui-extensions.ts +++ b/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) { diff --git a/packages/cli/src/utilities/ast-utils.ts b/packages/cli/src/utilities/ast-utils.ts index 0d98f37dd4..1cf7b6817d 100644 --- a/packages/cli/src/utilities/ast-utils.ts +++ b/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)