Skip to content

Commit

Permalink
refactor(cli): Use common CliCommand class
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed Apr 3, 2024
1 parent e29accc commit 455698f
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 112 deletions.
52 changes: 25 additions & 27 deletions packages/cli/src/commands/add/add.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { cancel, isCancel, log, select } from '@clack/prompts';
import { cancel, isCancel, log, outro, select } from '@clack/prompts';
import { Command } from 'commander';

import { addCodegen } from './codegen/add-codegen';
import { addEntity } from './entity/add-entity';
import { createNewPlugin } from './plugin/create-new-plugin';
import { addService } from './service/add-service';
import { addUiExtensions } from './ui-extensions/add-ui-extensions';
import { CliCommand } from '../../shared/cli-command';

import { addCodegenCommand } from './codegen/add-codegen';
import { addEntityCommand } from './entity/add-entity';
import { createNewPluginCommand } from './plugin/create-new-plugin';
import { addServiceCommand } from './service/add-service';
import { addUiExtensionsCommand } from './ui-extensions/add-ui-extensions';

const cancelledMessage = 'Add feature cancelled.';

Expand All @@ -14,36 +16,32 @@ export function registerAddCommand(program: Command) {
.command('add')
.description('Add a feature to your Vendure project')
.action(async () => {
const addCommands: Array<CliCommand<any, any>> = [
createNewPluginCommand,
addEntityCommand,
addServiceCommand,
addUiExtensionsCommand,
addCodegenCommand,
];
const featureType = await select({
message: 'Which feature would you like to add?',
options: [
{ value: 'plugin', label: '[Plugin] Add a new plugin' },
{ value: 'entity', label: '[Plugin: Entity] Add a new entity to a plugin' },
{ value: 'service', label: '[Plugin: Service] Add a new service to a plugin' },
{ value: 'uiExtensions', label: '[Plugin: UI] Set up Admin UI extensions' },
{ value: 'codegen', label: '[Project: Codegen] Set up GraphQL code generation' },
],
options: addCommands.map(c => ({
value: c.id,
label: `[${c.category}] ${c.description}`,
})),
});
if (isCancel(featureType)) {
cancel(cancelledMessage);
process.exit(0);
}
try {
if (featureType === 'plugin') {
await createNewPlugin();
}
if (featureType === 'uiExtensions') {
await addUiExtensions();
}
if (featureType === 'entity') {
await addEntity();
}
if (featureType === 'codegen') {
await addCodegen();
}
if (featureType === 'service') {
await addService();
const command = addCommands.find(c => c.id === featureType);
if (!command) {
throw new Error(`Could not find command with id "${featureType as string}"`);
}
await command.run();

outro('✅ Done!');
} catch (e: any) {
log.error(e.message as string);
if (e.stack) {
Expand Down
15 changes: 14 additions & 1 deletion packages/cli/src/commands/add/codegen/add-codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@ import { log, note, outro, spinner } from '@clack/prompts';
import path from 'path';
import { StructureKind } from 'ts-morph';

import { CliCommand } from '../../../shared/cli-command';
import { PackageJson } from '../../../shared/package-json-ref';
import { analyzeProject, selectMultiplePluginClasses } from '../../../shared/shared-prompts';
import { VendurePluginRef } from '../../../shared/vendure-plugin-ref';
import { getRelativeImportPath } from '../../../utilities/ast-utils';

import { CodegenConfigRef } from './codegen-config-ref';

export async function addCodegen(providedVendurePlugin?: VendurePluginRef) {
export interface AddCodegenOptions {
plugin?: VendurePluginRef;
}

export const addCodegenCommand = new CliCommand({
id: 'add-codegen',
category: 'Project: Codegen',
description: 'Set up GraphQL code generation',
run: addCodegen,
});

async function addCodegen(options?: AddCodegenOptions) {
const providedVendurePlugin = options?.plugin;
const project = await analyzeProject({
providedVendurePlugin,
cancelledMessage: 'Add codegen cancelled',
Expand Down
88 changes: 53 additions & 35 deletions packages/cli/src/commands/add/entity/add-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import path from 'path';
import { ClassDeclaration } from 'ts-morph';

import { pascalCaseRegex } from '../../../constants';
import { CliCommand } from '../../../shared/cli-command';
import { EntityRef } from '../../../shared/entity-ref';
import { analyzeProject, selectPlugin } from '../../../shared/shared-prompts';
import { VendurePluginRef } from '../../../shared/vendure-plugin-ref';
import { createFile } from '../../../utilities/ast-utils';
Expand All @@ -12,7 +14,8 @@ import { addEntityToPlugin } from './codemods/add-entity-to-plugin/add-entity-to

const cancelledMessage = 'Add entity cancelled';

export interface AddEntityTemplateContext {
export interface AddEntityOptions {
plugin?: VendurePluginRef;
className: string;
fileName: string;
translationFileName: string;
Expand All @@ -22,12 +25,47 @@ export interface AddEntityTemplateContext {
};
}

export async function addEntity(providedVendurePlugin?: VendurePluginRef) {
export const addEntityCommand = new CliCommand({
id: 'add-entity',
category: 'Plugin: Entity',
description: 'Add a new entity to a plugin',
run: options => addEntity(options),
});

async function addEntity(options?: Partial<AddEntityOptions>) {
const providedVendurePlugin = options?.plugin;
const project = await analyzeProject({ providedVendurePlugin, cancelledMessage });
const vendurePlugin = providedVendurePlugin ?? (await selectPlugin(project, cancelledMessage));

const customEntityName = await getCustomEntityName(cancelledMessage);
const customEntityName = options?.className ?? (await getCustomEntityName(cancelledMessage));

const context: AddEntityOptions = {
className: customEntityName,
fileName: paramCase(customEntityName) + '.entity',
translationFileName: paramCase(customEntityName) + '-translation.entity',
features: await getFeatures(options),
};

const { entityClass, translationClass } = createEntity(vendurePlugin, context);
addEntityToPlugin(vendurePlugin, entityClass);
entityClass.getSourceFile().organizeImports();
if (context.features.translatable) {
addEntityToPlugin(vendurePlugin, translationClass);
translationClass.getSourceFile().organizeImports();
}

await project.save();

if (!providedVendurePlugin) {
outro('✅ Done!');
}
return new EntityRef(entityClass);
}

async function getFeatures(options?: Partial<AddEntityOptions>): Promise<AddEntityOptions['features']> {
if (options?.features) {
return options?.features;
}
const features = await multiselect({
message: 'Entity features (use ↑, ↓, space to select)',
required: false,
Expand All @@ -49,33 +87,13 @@ export async function addEntity(providedVendurePlugin?: VendurePluginRef) {
cancel(cancelledMessage);
process.exit(0);
}

const context: AddEntityTemplateContext = {
className: customEntityName,
fileName: paramCase(customEntityName) + '.entity',
translationFileName: paramCase(customEntityName) + '-translation.entity',
features: {
customFields: features.includes('customFields'),
translatable: features.includes('translatable'),
},
return {
customFields: features.includes('customFields'),
translatable: features.includes('translatable'),
};

const { entityClass, translationClass } = createEntity(vendurePlugin, context);
addEntityToPlugin(vendurePlugin, entityClass);
entityClass.getSourceFile().organizeImports();
if (context.features.translatable) {
addEntityToPlugin(vendurePlugin, translationClass);
translationClass.getSourceFile().organizeImports();
}

await project.save();

if (!providedVendurePlugin) {
outro('✅ Done!');
}
}

function createEntity(plugin: VendurePluginRef, context: AddEntityTemplateContext) {
function createEntity(plugin: VendurePluginRef, options: AddEntityOptions) {
const entitiesDir = path.join(plugin.getPluginDir().getPath(), 'entities');
const entityFile = createFile(
plugin.getSourceFile().getProject(),
Expand All @@ -85,28 +103,28 @@ function createEntity(plugin: VendurePluginRef, context: AddEntityTemplateContex
plugin.getSourceFile().getProject(),
path.join(__dirname, 'templates/entity-translation.template.ts'),
);
entityFile.move(path.join(entitiesDir, `${context.fileName}.ts`));
translationFile.move(path.join(entitiesDir, `${context.translationFileName}.ts`));
entityFile.move(path.join(entitiesDir, `${options.fileName}.ts`));
translationFile.move(path.join(entitiesDir, `${options.translationFileName}.ts`));

const entityClass = entityFile.getClass('ScaffoldEntity')?.rename(context.className);
const entityClass = entityFile.getClass('ScaffoldEntity')?.rename(options.className);
const customFieldsClass = entityFile
.getClass('ScaffoldEntityCustomFields')
?.rename(`${context.className}CustomFields`);
?.rename(`${options.className}CustomFields`);
const translationClass = translationFile
.getClass('ScaffoldTranslation')
?.rename(`${context.className}Translation`);
?.rename(`${options.className}Translation`);
const translationCustomFieldsClass = translationFile
.getClass('ScaffoldEntityCustomFieldsTranslation')
?.rename(`${context.className}CustomFieldsTranslation`);
?.rename(`${options.className}CustomFieldsTranslation`);

if (!context.features.customFields) {
if (!options.features.customFields) {
// Remove custom fields from entity
customFieldsClass?.remove();
translationCustomFieldsClass?.remove();
removeCustomFieldsFromClass(entityClass);
removeCustomFieldsFromClass(translationClass);
}
if (!context.features.translatable) {
if (!options.features.translatable) {
// Remove translatable fields from entity
translationClass?.remove();
entityClass?.getProperty('localizedName')?.remove();
Expand Down
41 changes: 20 additions & 21 deletions packages/cli/src/commands/add/plugin/create-new-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@ import { constantCase, paramCase, pascalCase } from 'change-case';
import * as fs from 'fs-extra';
import path from 'path';

import { CliCommand } from '../../../shared/cli-command';
import { VendureConfigRef } from '../../../shared/vendure-config-ref';
import { VendurePluginRef } from '../../../shared/vendure-plugin-ref';
import { addImportsToFile, createFile, getTsMorphProject } from '../../../utilities/ast-utils';
import { addCodegen } from '../codegen/add-codegen';
import { addEntity } from '../entity/add-entity';
import { addService } from '../service/add-service';
import { addUiExtensions } from '../ui-extensions/add-ui-extensions';
import { addCodegenCommand } from '../codegen/add-codegen';
import { addEntityCommand } from '../entity/add-entity';
import { addServiceCommand } from '../service/add-service';
import { addUiExtensionsCommand } from '../ui-extensions/add-ui-extensions';

import { GeneratePluginOptions, NewPluginTemplateContext } from './types';

export const createNewPluginCommand = new CliCommand({
id: 'create-new-plugin',
category: 'Plugin',
description: 'Create a new Vendure plugin',
run: createNewPlugin,
});

const cancelledMessage = 'Plugin setup cancelled.';

export async function createNewPlugin() {
Expand Down Expand Up @@ -69,37 +77,28 @@ export async function createNewPlugin() {
configSpinner.stop('Updated VendureConfig');

let done = false;
const followUpCommands = [addEntityCommand, addServiceCommand, addUiExtensionsCommand, addCodegenCommand];
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: 'service', label: '[Plugin: Service] Add a new service to the plugin' },
{ value: 'uiExtensions', label: '[Plugin: UI] Set up Admin UI extensions' },
{
value: 'codegen',
label: '[Plugin: Codegen] Set up GraphQL code generation for this plugin',
},
...followUpCommands.map(c => ({
value: c.id,
label: `[${c.category}] ${c.description}`,
})),
],
});
if (isCancel(featureType)) {
done = true;
}
if (featureType === 'no') {
done = true;
} else if (featureType === 'entity') {
await addEntity(plugin);
} else if (featureType === 'uiExtensions') {
await addUiExtensions(plugin);
} else if (featureType === 'codegen') {
await addCodegen(plugin);
} else if (featureType === 'service') {
await addService(plugin);
} else {
const command = followUpCommands.find(c => c.id === featureType);
await command?.run({ plugin });
}
}

outro('✅ Plugin setup complete!');
}

export async function generatePlugin(options: GeneratePluginOptions): Promise<VendurePluginRef> {
Expand Down
Loading

0 comments on commit 455698f

Please sign in to comment.