Skip to content

Commit

Permalink
fix(cli): Improve plugin generation in monorepos
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed May 2, 2024
1 parent 464e68b commit 40000a4
Showing 1 changed file with 70 additions and 30 deletions.
100 changes: 70 additions & 30 deletions packages/cli/src/commands/add/plugin/create-new-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { cancel, intro, isCancel, log, select, spinner, text } from '@clack/prom
import { constantCase, paramCase, pascalCase } from 'change-case';
import * as fs from 'fs-extra';
import path from 'path';
import { Project, SourceFile } from 'ts-morph';

import { CliCommand, CliCommandReturnVal } from '../../../shared/cli-command';
import { analyzeProject } from '../../../shared/shared-prompts';
import { VendureConfigRef } from '../../../shared/vendure-config-ref';
import { VendurePluginRef } from '../../../shared/vendure-plugin-ref';
import { addImportsToFile, createFile, getTsMorphProject } from '../../../utilities/ast-utils';
import { addImportsToFile, createFile, getPluginClasses } from '../../../utilities/ast-utils';
import { pauseForPromptDisplay } from '../../../utilities/utils';
import { addApiExtensionCommand } from '../api-extension/add-api-extension';
import { addCodegenCommand } from '../codegen/add-codegen';
Expand All @@ -29,6 +31,7 @@ const cancelledMessage = 'Plugin setup cancelled.';
export async function createNewPlugin(): Promise<CliCommandReturnVal> {
const options: GeneratePluginOptions = { name: '', customEntityName: '', pluginDir: '' } as any;
intro('Adding a new Vendure plugin!');
const { project } = await analyzeProject({ cancelledMessage });
if (!options.name) {
const name = await text({
message: 'What is the name of the plugin?',
Expand All @@ -47,7 +50,8 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
options.name = name;
}
}
const pluginDir = getPluginDirName(options.name);
const existingPluginDir = findExistingPluginsDir(project);
const pluginDir = getPluginDirName(options.name, existingPluginDir);
const confirmation = await text({
message: 'Plugin location',
initialValue: pluginDir,
Expand All @@ -65,7 +69,7 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
}

options.pluginDir = confirmation;
const { plugin, project, modifiedSourceFiles } = await generatePlugin(options);
const { plugin, modifiedSourceFiles } = await generatePlugin(project, options);

const configSpinner = spinner();
configSpinner.start('Updating VendureConfig...');
Expand All @@ -89,9 +93,6 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
addCodegenCommand,
];
let allModifiedSourceFiles = [...modifiedSourceFiles];
const pluginClassName = plugin.name;
let workingPlugin = plugin;
let workingProject = project;
while (!done) {
const featureType = await select({
message: `Add features to ${options.name}?`,
Expand All @@ -109,20 +110,11 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
if (featureType === 'no') {
done = true;
} else {
const { project: newProject } = await getTsMorphProject();
workingProject = newProject;
const newPlugin = newProject
.getSourceFile(workingPlugin.getSourceFile().getFilePath())
?.getClass(pluginClassName);
if (!newPlugin) {
throw new Error(`Could not find class "${pluginClassName}" in the new project`);
}
workingPlugin = new VendurePluginRef(newPlugin);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const command = followUpCommands.find(c => c.id === featureType)!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
try {
const result = await command.run({ plugin: new VendurePluginRef(newPlugin) });
const result = await command.run({ plugin });
allModifiedSourceFiles = result.modifiedSourceFiles;
// We format all modified source files and re-load the
// project to avoid issues with the project state
Expand All @@ -133,8 +125,6 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
log.error(`Error adding feature "${command.id}"`);
log.error(e.stack);
}

await workingProject.save();
}
}

Expand All @@ -145,8 +135,9 @@ export async function createNewPlugin(): Promise<CliCommandReturnVal> {
}

export async function generatePlugin(
project: Project,
options: GeneratePluginOptions,
): Promise<CliCommandReturnVal<{ plugin: VendurePluginRef }>> {
): Promise<{ plugin: VendurePluginRef; modifiedSourceFiles: SourceFile[] }> {
const nameWithoutPlugin = options.name.replace(/-?plugin$/i, '');
const normalizedName = nameWithoutPlugin + '-plugin';
const templateContext: NewPluginTemplateContext = {
Expand All @@ -158,7 +149,6 @@ export async function generatePlugin(
const projectSpinner = spinner();
projectSpinner.start('Generating plugin scaffold...');
await pauseForPromptDisplay();
const { project } = await getTsMorphProject({ skipAddingFilesFromTsConfig: false });

const pluginFile = createFile(
project,
Expand All @@ -169,6 +159,8 @@ export async function generatePlugin(
if (!pluginClass) {
throw new Error('Could not find the plugin class in the generated file');
}
pluginFile.getImportDeclaration('./constants.template')?.setModuleSpecifier('./constants');
pluginFile.getImportDeclaration('./types.template')?.setModuleSpecifier('./types');
pluginClass.rename(templateContext.pluginName);

const typesFile = createFile(
Expand All @@ -193,24 +185,72 @@ export async function generatePlugin(
projectSpinner.stop('Generated plugin scaffold');
await project.save();
return {
project,
modifiedSourceFiles: [pluginFile, typesFile, constantsFile],
plugin: new VendurePluginRef(pluginClass),
};
}

function getPluginDirName(name: string) {
function findExistingPluginsDir(project: Project): { prefix: string; suffix: string } | undefined {
const pluginClasses = getPluginClasses(project);
if (pluginClasses.length === 0) {
return;
}
const pluginDirs = pluginClasses.map(c => {
return c.getSourceFile().getDirectoryPath();
});
const prefix = findCommonPath(pluginDirs);
const suffixStartIndex = prefix.length;
const rest = pluginDirs[0].substring(suffixStartIndex).replace(/^\//, '').split('/');
const suffix = rest.length > 1 ? rest.slice(1).join('/') : '';
return { prefix, suffix };
}

function getPluginDirName(
name: string,
existingPluginDirPattern: { prefix: string; suffix: string } | undefined,
) {
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 (existingPluginDirPattern) {
return path.join(
existingPluginDirPattern.prefix,
paramCase(nameWithoutPlugin),
existingPluginDirPattern.suffix,
);
} else {
return path.join(cwd, 'src', 'plugins', paramCase(nameWithoutPlugin));
}
}

if (currentlyInPluginsDir) {
return path.join(cwd, paramCase(nameWithoutPlugin));
function findCommonPath(paths: string[]): string {
if (paths.length === 0) {
return ''; // If no paths provided, return empty string
}
if (currentlyInRootDir) {
return path.join(cwd, 'src', 'plugins', paramCase(nameWithoutPlugin));

// Split each path into segments
const pathSegmentsList = paths.map(p => p.split('/'));

// Find the minimum length of path segments (to avoid out of bounds)
const minLength = Math.min(...pathSegmentsList.map(segments => segments.length));

// Initialize the common path
const commonPath: string[] = [];

// Loop through each segment index up to the minimum length
for (let i = 0; i < minLength; i++) {
// Get the segment at the current index from the first path
const currentSegment = pathSegmentsList[0][i];
// Check if this segment is common across all paths
const isCommon = pathSegmentsList.every(segments => segments[i] === currentSegment);
if (isCommon) {
// If it's common, add this segment to the common path
commonPath.push(currentSegment);
} else {
// If it's not common, break out of the loop
break;
}
}
return path.join(cwd, paramCase(nameWithoutPlugin));

// Join the common path segments back into a string
return commonPath.join('/');
}

0 comments on commit 40000a4

Please sign in to comment.