Skip to content

Commit

Permalink
refactor(cli): Simplify new plugin command
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed Apr 3, 2024
1 parent e543e5e commit 6ba362c
Show file tree
Hide file tree
Showing 15 changed files with 193 additions and 44 deletions.
9 changes: 7 additions & 2 deletions packages/cli/src/commands/add/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { cancel, isCancel, log, select } from '@clack/prompts';
import { Command } from 'commander';

import { addEntity } from './entity/add-entity';
import { createNewPlugin } from './plugin/create-new-plugin';
import { addUiExtensions } from './ui-extensions/add-ui-extensions';

const cancelledMessage = 'Add feature cancelled.';
Expand All @@ -14,15 +15,19 @@ export function registerAddCommand(program: Command) {
const featureType = await select({
message: 'Which feature would you like to add?',
options: [
{ value: 'uiExtensions', label: 'Set up Admin UI extensions' },
{ value: 'entity', label: 'Add a new entity to a plugin' },
{ value: 'plugin', label: '[Plugin] Add a new plugin' },
{ value: 'entity', label: '[Plugin: Entity] Add a new entity to a plugin' },
{ value: 'uiExtensions', label: '[Plugin: UI] Set up Admin UI extensions' },
],
});
if (isCancel(featureType)) {
cancel(cancelledMessage);
process.exit(0);
}
try {
if (featureType === 'plugin') {
await createNewPlugin();
}
if (featureType === 'uiExtensions') {
await addUiExtensions();
}
Expand Down
9 changes: 3 additions & 6 deletions packages/cli/src/commands/add/entity/add-entity.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { outro, spinner } from '@clack/prompts';
import { outro, spinner, text } from '@clack/prompts';
import { paramCase } from 'change-case';
import path from 'path';

import { getCustomEntityName, selectPluginClass } from '../../../shared/shared-prompts';
import { renderEntity } from '../../../shared/shared-scaffold/entity';
import { createSourceFileFromTemplate, getTsMorphProject } from '../../../utilities/ast-utils';
import { Scaffolder } from '../../../utilities/scaffolder';
import { createFile, getTsMorphProject } from '../../../utilities/ast-utils';

import { addEntityToPlugin } from './codemods/add-entity-to-plugin/add-entity-to-plugin';

Expand Down Expand Up @@ -35,8 +33,7 @@ export async function addEntity() {
};

const entitiesDir = path.join(pluginClass.getSourceFile().getDirectory().getPath(), 'entities');
const entityTemplatePath = path.join(__dirname, 'templates/entity.template.ts');
const entityFile = createSourceFileFromTemplate(project, entityTemplatePath);
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Project } from 'ts-morph';
import { describe, expect, it } from 'vitest';

import { defaultManipulationSettings } from '../../../../../constants';
import { createSourceFileFromTemplate, getPluginClasses } from '../../../../../utilities/ast-utils';
import { createFile, getPluginClasses } from '../../../../../utilities/ast-utils';
import { expectSourceFileContentToMatch } from '../../../../../utilities/testing-utils';

import { addEntityToPlugin } from './add-entity-to-plugin';
Expand All @@ -17,7 +17,7 @@ describe('addEntityToPlugin', () => {
const pluginClasses = getPluginClasses(project);
expect(pluginClasses.length).toBe(1);
const entityTemplatePath = path.join(__dirname, '../../templates/entity.template.ts');
const entityFile = createSourceFileFromTemplate(project, entityTemplatePath);
const entityFile = createFile(project, entityTemplatePath);
entityFile.move(path.join(__dirname, 'fixtures', 'entity.ts'));
addEntityToPlugin(pluginClasses[0], entityFile);

Expand Down
105 changes: 105 additions & 0 deletions packages/cli/src/commands/add/plugin/create-new-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { cancel, intro, isCancel, outro, spinner, text } from '@clack/prompts';
import { constantCase, paramCase, pascalCase } from 'change-case';
import * as fs from 'fs-extra';
import path from 'path';

import { createFile, getTsMorphProject } from '../../../utilities/ast-utils';

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

const cancelledMessage = 'Plugin setup cancelled.';

export async function createNewPlugin() {
const options: GeneratePluginOptions = { name: '', customEntityName: '', pluginDir: '' } as any;
intro('Scaffolding a new Vendure plugin!');
if (!options.name) {
const name = await text({
message: 'What is the name of the plugin?',
initialValue: '',
validate: input => {
if (!/^[a-z][a-z-0-9]+$/.test(input)) {
return 'The plugin name must be lowercase and contain only letters, numbers and dashes';
}
},
});

if (isCancel(name)) {
cancel(cancelledMessage);
process.exit(0);
} else {
options.name = name;
}
}
const pluginDir = getPluginDirName(options.name);
const confirmation = await text({
message: 'Plugin location',
initialValue: pluginDir,
placeholder: '',
validate: input => {
if (fs.existsSync(input)) {
return `A directory named "${input}" already exists. Please specify a different directory.`;
}
},
});

if (isCancel(confirmation)) {
cancel(cancelledMessage);
process.exit(0);
} else {
options.pluginDir = confirmation;
await generatePlugin(options);
}
}

export async function generatePlugin(options: GeneratePluginOptions) {
const nameWithoutPlugin = options.name.replace(/-?plugin$/i, '');
const normalizedName = nameWithoutPlugin + '-plugin';
const templateContext: NewPluginTemplateContext = {
...options,
pluginName: pascalCase(normalizedName),
pluginInitOptionsName: constantCase(normalizedName) + '_OPTIONS',
};

const projectSpinner = spinner();
projectSpinner.start('Generating plugin scaffold...');
await new Promise(resolve => setTimeout(resolve, 100));
const project = getTsMorphProject({ skipAddingFilesFromTsConfig: true });

const pluginFile = createFile(project, path.join(__dirname, 'templates/plugin.template.ts'));
pluginFile.getClass('TemplatePlugin')?.rename(templateContext.pluginName);

const typesFile = createFile(project, path.join(__dirname, 'templates/types.template.ts'));

const constantsFile = createFile(project, path.join(__dirname, 'templates/constants.template.ts'));
constantsFile
.getVariableDeclaration('TEMPLATE_PLUGIN_OPTIONS')
?.rename(templateContext.pluginInitOptionsName)
.set({ initializer: `Symbol('${templateContext.pluginInitOptionsName}')` });
constantsFile
.getVariableDeclaration('loggerCtx')
?.set({ initializer: `'${templateContext.pluginName}'` });

typesFile.move(path.join(options.pluginDir, 'types.ts'));
pluginFile.move(path.join(options.pluginDir, paramCase(nameWithoutPlugin) + '.plugin.ts'));
constantsFile.move(path.join(options.pluginDir, 'constants.ts'));

projectSpinner.stop('Done');
project.saveSync();
outro('✅ Plugin scaffolding complete!');
}

function getPluginDirName(name: string) {
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, '');

if (currentlyInPluginsDir) {
return path.join(cwd, paramCase(nameWithoutPlugin));
}
if (currentlyInRootDir) {
return path.join(cwd, 'src', 'plugins', paramCase(nameWithoutPlugin));
}
return path.join(cwd, paramCase(nameWithoutPlugin));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const TEMPLATE_PLUGIN_OPTIONS = Symbol('TEMPLATE_PLUGIN_OPTIONS');
export const loggerCtx = 'TemplatePlugin';
18 changes: 18 additions & 0 deletions packages/cli/src/commands/add/plugin/templates/plugin.template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { PluginCommonModule, Type, VendurePlugin } from '@vendure/core';

import { TEMPLATE_PLUGIN_OPTIONS } from './constants.template';
import { PluginInitOptions } from './types.template';

@VendurePlugin({
imports: [PluginCommonModule],
providers: [{ provide: TEMPLATE_PLUGIN_OPTIONS, useFactory: () => TemplatePlugin.options }],
compatibility: '^2.0.0',
})
export class TemplatePlugin {
static options: PluginInitOptions;

static init(options: PluginInitOptions): Type<TemplatePlugin> {
this.options = options;
return TemplatePlugin;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @description
* The plugin can be configured using the following options:
*/
export interface PluginInitOptions {
exampleOption: string;
}
9 changes: 9 additions & 0 deletions packages/cli/src/commands/add/plugin/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface GeneratePluginOptions {
name: string;
pluginDir: string;
}

export type NewPluginTemplateContext = GeneratePluginOptions & {
pluginName: string;
pluginInitOptionsName: string;
};
26 changes: 13 additions & 13 deletions packages/cli/src/commands/add/ui-extensions/add-ui-extensions.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { note, outro, spinner, log } from '@clack/prompts';
import { log, note, outro, spinner } from '@clack/prompts';
import path from 'path';
import { ClassDeclaration } from 'ts-morph';

import { selectPluginClass } from '../../../shared/shared-prompts';
import { getRelativeImportPath, getTsMorphProject, getVendureConfig } from '../../../utilities/ast-utils';
import {
createFile,
getRelativeImportPath,
getTsMorphProject,
getVendureConfig,
} from '../../../utilities/ast-utils';
import { determineVendureVersion, installRequiredPackages } from '../../../utilities/package-utils';
import { Scaffolder } from '../../../utilities/scaffolder';

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';
import { renderProviders } from './scaffold/providers';
import { renderRoutes } from './scaffold/routes';

export async function addUiExtensions() {
const projectSpinner = spinner();
Expand Down Expand Up @@ -44,16 +46,14 @@ export async function addUiExtensions() {
}
installSpinner.stop('Dependencies installed');

const scaffolder = new Scaffolder();
scaffolder.addFile(renderProviders, 'providers.ts');
scaffolder.addFile(renderRoutes, 'routes.ts');
const pluginDir = pluginClass.getSourceFile().getDirectory().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'));
routesFile.move(path.join(pluginDir, 'ui', 'routes.ts'));

log.success('Created UI extension scaffold');

const pluginDir = pluginClass.getSourceFile().getDirectory().getPath();
scaffolder.createScaffold({
dir: path.join(pluginDir, 'ui'),
context: {},
});
const vendureConfig = getVendureConfig(project);
if (!vendureConfig) {
log.warning(
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default [
// Add your providers here
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default [
// Add your custom routes here
];
2 changes: 1 addition & 1 deletion packages/cli/src/shared/shared-prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export async function getCustomEntityName(cancelledMessage: string) {
const entityName = await text({
message: 'What is the name of the custom entity?',
initialValue: '',
placeholder: '',
validate: input => {
if (!input) {
return 'The custom entity name cannot be empty';
Expand All @@ -34,6 +33,7 @@ export async function selectPluginClass(project: Project, cancelledMessage: stri
value: c,
label: c.getName() as string,
})),
maxItems: 10,
});
if (isCancel(targetPlugin)) {
cancel(cancelledMessage);
Expand Down
24 changes: 20 additions & 4 deletions packages/cli/src/utilities/ast-utils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { log } from '@clack/prompts';
import fs from 'fs-extra';
import path from 'node:path';
import { Node, ObjectLiteralExpression, Project, SourceFile, VariableDeclaration } from 'ts-morph';
import {
Node,
ObjectLiteralExpression,
Project,
ProjectOptions,
SourceFile,
VariableDeclaration,
} from 'ts-morph';

import { defaultManipulationSettings } from '../constants';

export function getTsMorphProject() {
export function getTsMorphProject(options: ProjectOptions = {}) {
const tsConfigPath = path.join(process.cwd(), 'tsconfig.json');
if (!fs.existsSync(tsConfigPath)) {
throw new Error('No tsconfig.json found in current directory');
Expand All @@ -15,6 +23,7 @@ export function getTsMorphProject() {
compilerOptions: {
skipLibCheck: true,
},
...options,
});
}

Expand Down Expand Up @@ -110,9 +119,16 @@ export function getRelativeImportPath(locations: { from: SourceFile; to: SourceF
);
}

export function createSourceFileFromTemplate(project: Project, templatePath: string) {
export function createFile(project: Project, templatePath: string) {
const template = fs.readFileSync(templatePath, 'utf-8');
return project.createSourceFile('temp.ts', template);
try {
return project.createSourceFile(path.join('/.vendure-cli-temp/', templatePath), template, {
overwrite: true,
});
} catch (e: any) {
log.error(e.message);
process.exit(1);
}
}

export function kebabize(str: string) {
Expand Down

0 comments on commit 6ba362c

Please sign in to comment.