Skip to content

Commit

Permalink
feat(cli): Allow chaining features onto a newly-created plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed Apr 3, 2024
1 parent 6ba362c commit 5b32c59
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 36 deletions.
25 changes: 16 additions & 9 deletions packages/cli/src/commands/add/entity/add-entity.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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: {
Expand All @@ -41,5 +45,8 @@ export async function addEntity() {
addEntityToPlugin(pluginClass, entityFile);

project.saveSync();
outro('✅ Done!');

if (!providedPluginClass) {
outro('✅ Done!');
}
}
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
53 changes: 44 additions & 9 deletions packages/cli/src/commands/add/plugin/create-new-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
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';

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?',
Expand Down Expand Up @@ -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 = {
Expand All @@ -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'));

Expand All @@ -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) {
Expand Down
25 changes: 15 additions & 10 deletions packages/cli/src/commands/add/ui-extensions/add-ui-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -79,7 +82,9 @@ export async function addUiExtensions() {
}

project.saveSync();
outro('✅ Done!');
if (!providedPluginClass) {
outro('✅ Done!');
}
}

function pluginAlreadyHasUiExtensionProp(pluginClass: ClassDeclaration) {
Expand Down
38 changes: 31 additions & 7 deletions packages/cli/src/utilities/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 5b32c59

Please sign in to comment.