Browse Source

refactor(cli): Create VendureConfigRef, various refactors

Michael Bromley 1 year ago
parent
commit
342861d0f8

+ 3 - 0
packages/cli/src/commands/add/add.ts

@@ -41,6 +41,9 @@ export function registerAddCommand(program: Command) {
                 }
             } catch (e: any) {
                 log.error(e.message as string);
+                if (e.stack) {
+                    log.error(e.stack);
+                }
             }
             process.exit(0);
         });

+ 37 - 13
packages/cli/src/commands/add/codegen/add-codegen.ts

@@ -5,9 +5,9 @@ import { ClassDeclaration, StructureKind, SyntaxKind } from 'ts-morph';
 import { analyzeProject, selectMultiplePluginClasses } from '../../../shared/shared-prompts';
 import { createFile, getRelativeImportPath, getTsMorphProject } from '../../../utilities/ast-utils';
 import { PackageJson } from '../../../utilities/package-utils';
-import { VendurePluginDeclaration } from '../../../utilities/vendure-plugin-declaration';
+import { VendurePluginRef } from '../../../utilities/vendure-plugin-ref';
 
-export async function addCodegen(providedVendurePlugin?: VendurePluginDeclaration) {
+export async function addCodegen(providedVendurePlugin?: VendurePluginRef) {
     const project = await analyzeProject({
         providedVendurePlugin,
         cancelledMessage: 'Add codegen cancelled',
@@ -19,17 +19,24 @@ export async function addCodegen(providedVendurePlugin?: VendurePluginDeclaratio
     const packageJson = new PackageJson(project);
     const installSpinner = spinner();
     installSpinner.start(`Installing dependencies...`);
+    const packagesToInstall = [
+        {
+            pkg: '@graphql-codegen/cli',
+            isDevDependency: true,
+        },
+        {
+            pkg: '@graphql-codegen/typescript',
+            isDevDependency: true,
+        },
+    ];
+    if (plugins.some(p => p.hasUiExtensions())) {
+        packagesToInstall.push({
+            pkg: '@graphql-codegen/client-preset',
+            isDevDependency: true,
+        });
+    }
     try {
-        await packageJson.installPackages([
-            {
-                pkg: '@graphql-codegen/cli',
-                isDevDependency: true,
-            },
-            {
-                pkg: '@graphql-codegen/typescript',
-                isDevDependency: true,
-            },
-        ]);
+        await packageJson.installPackages(packagesToInstall);
     } catch (e: any) {
         log.error(`Failed to install dependencies: ${e.message as string}.`);
     }
@@ -70,6 +77,23 @@ export async function addCodegen(providedVendurePlugin?: VendurePluginDeclaratio
                 initializer: `{ plugins: ['typescript'] }`,
             })
             .formatText();
+
+        if (plugin.hasUiExtensions()) {
+            const uiExtensionsPath = `${path.dirname(relativePluginPath)}/ui`;
+            generatesProp
+                .addProperty({
+                    name: `'${uiExtensionsPath}/gql/'`,
+                    kind: StructureKind.PropertyAssignment,
+                    initializer: `{ 
+                        preset: 'client',
+                        documents: '${uiExtensionsPath}/**/*.ts', 
+                        presetConfig: {
+                            fragmentMasking: false,
+                        },
+                     }`,
+                })
+                .formatText();
+        }
     }
     codegenFile.move(path.join(rootDir.getPath(), 'codegen.ts'));
 
@@ -77,7 +101,7 @@ export async function addCodegen(providedVendurePlugin?: VendurePluginDeclaratio
 
     configSpinner.stop('Configured codegen file');
 
-    project.saveSync();
+    await project.save();
 
     const nextSteps = [
         `You can run codegen by doing the following:`,

+ 3 - 3
packages/cli/src/commands/add/entity/add-entity.ts

@@ -4,7 +4,7 @@ import path from 'path';
 
 import { analyzeProject, getCustomEntityName, selectPlugin } from '../../../shared/shared-prompts';
 import { createFile } from '../../../utilities/ast-utils';
-import { VendurePluginDeclaration } from '../../../utilities/vendure-plugin-declaration';
+import { VendurePluginRef } from '../../../utilities/vendure-plugin-ref';
 
 import { addEntityToPlugin } from './codemods/add-entity-to-plugin/add-entity-to-plugin';
 
@@ -17,7 +17,7 @@ export interface AddEntityTemplateContext {
     };
 }
 
-export async function addEntity(providedVendurePlugin?: VendurePluginDeclaration) {
+export async function addEntity(providedVendurePlugin?: VendurePluginRef) {
     const project = await analyzeProject({ providedVendurePlugin, cancelledMessage });
     const vendurePlugin = providedVendurePlugin ?? (await selectPlugin(project, cancelledMessage));
 
@@ -37,7 +37,7 @@ export async function addEntity(providedVendurePlugin?: VendurePluginDeclaration
 
     addEntityToPlugin(vendurePlugin.classDeclaration, entityFile);
 
-    project.saveSync();
+    await project.save();
 
     if (!providedVendurePlugin) {
         outro('✅  Done!');

+ 25 - 5
packages/cli/src/commands/add/plugin/create-new-plugin.ts

@@ -3,8 +3,10 @@ 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 { VendurePluginDeclaration } from '../../../utilities/vendure-plugin-declaration';
+import { addImportsToFile, createFile, getTsMorphProject } from '../../../utilities/ast-utils';
+import { VendureConfigRef } from '../../../utilities/vendure-config-ref';
+import { VendurePluginRef } from '../../../utilities/vendure-plugin-ref';
+import { addCodegen } from '../codegen/add-codegen';
 import { addEntity } from '../entity/add-entity';
 import { addUiExtensions } from '../ui-extensions/add-ui-extensions';
 
@@ -53,6 +55,18 @@ export async function createNewPlugin() {
     options.pluginDir = confirmation;
     const plugin = await generatePlugin(options);
 
+    const configSpinner = spinner();
+    configSpinner.start('Updating VendureConfig...');
+    await new Promise(resolve => setTimeout(resolve, 100));
+    const vendureConfig = new VendureConfigRef(plugin.classDeclaration.getProject());
+    vendureConfig.addToPluginsArray(`${plugin.name}.init({})`);
+    addImportsToFile(vendureConfig.sourceFile, {
+        moduleSpecifier: plugin.getSourceFile(),
+        namedImports: [plugin.name],
+    });
+    await vendureConfig.sourceFile.getProject().save();
+    configSpinner.stop('Updated VendureConfig');
+
     let done = false;
     while (!done) {
         const featureType = await select({
@@ -61,6 +75,10 @@ export async function createNewPlugin() {
                 { 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' },
+                {
+                    value: 'codegen',
+                    label: '[Plugin: Codegen] Set up GraphQL code generation for this plugin',
+                },
             ],
         });
         if (isCancel(featureType)) {
@@ -72,13 +90,15 @@ export async function createNewPlugin() {
             await addEntity(plugin);
         } else if (featureType === 'uiExtensions') {
             await addUiExtensions(plugin);
+        } else if (featureType === 'codegen') {
+            await addCodegen(plugin);
         }
     }
 
     outro('✅ Plugin setup complete!');
 }
 
-export async function generatePlugin(options: GeneratePluginOptions): Promise<VendurePluginDeclaration> {
+export async function generatePlugin(options: GeneratePluginOptions): Promise<VendurePluginRef> {
     const nameWithoutPlugin = options.name.replace(/-?plugin$/i, '');
     const normalizedName = nameWithoutPlugin + '-plugin';
     const templateContext: NewPluginTemplateContext = {
@@ -115,8 +135,8 @@ export async function generatePlugin(options: GeneratePluginOptions): Promise<Ve
     constantsFile.move(path.join(options.pluginDir, 'constants.ts'));
 
     projectSpinner.stop('Generated plugin scaffold');
-    project.saveSync();
-    return new VendurePluginDeclaration(pluginClass);
+    await project.save();
+    return new VendurePluginRef(pluginClass);
 }
 
 function getPluginDirName(name: string) {

+ 1 - 1
packages/cli/src/commands/add/plugin/templates/types.template.ts

@@ -3,5 +3,5 @@
  * The plugin can be configured using the following options:
  */
 export interface PluginInitOptions {
-    exampleOption: string;
+    exampleOption?: string;
 }

+ 7 - 12
packages/cli/src/commands/add/ui-extensions/add-ui-extensions.ts

@@ -1,21 +1,16 @@
 import { log, note, outro, spinner } from '@clack/prompts';
 import path from 'path';
-import { ClassDeclaration } from 'ts-morph';
 
 import { analyzeProject, selectPlugin } from '../../../shared/shared-prompts';
-import {
-    createFile,
-    getRelativeImportPath,
-    getTsMorphProject,
-    getVendureConfig,
-} from '../../../utilities/ast-utils';
+import { createFile, getRelativeImportPath } from '../../../utilities/ast-utils';
 import { PackageJson } from '../../../utilities/package-utils';
-import { VendurePluginDeclaration } from '../../../utilities/vendure-plugin-declaration';
+import { VendureConfigRef } from '../../../utilities/vendure-config-ref';
+import { VendurePluginRef } from '../../../utilities/vendure-plugin-ref';
 
 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(providedVendurePlugin?: VendurePluginDeclaration) {
+export async function addUiExtensions(providedVendurePlugin?: VendurePluginRef) {
     const project = await analyzeProject({ providedVendurePlugin });
     const vendurePlugin =
         providedVendurePlugin ?? (await selectPlugin(project, 'Add UI extensions cancelled'));
@@ -52,7 +47,7 @@ export async function addUiExtensions(providedVendurePlugin?: VendurePluginDecla
 
     log.success('Created UI extension scaffold');
 
-    const vendureConfig = getVendureConfig(project);
+    const vendureConfig = new VendureConfigRef(project);
     if (!vendureConfig) {
         log.warning(
             `Could not find the VendureConfig declaration in your project. You will need to manually set up the compileUiExtensions function.`,
@@ -60,7 +55,7 @@ export async function addUiExtensions(providedVendurePlugin?: VendurePluginDecla
     } else {
         const pluginClassName = vendurePlugin.name;
         const pluginPath = getRelativeImportPath({
-            to: vendureConfig.getSourceFile(),
+            to: vendureConfig.sourceFile,
             from: vendurePlugin.classDeclaration.getSourceFile(),
         });
         const updated = updateAdminUiPluginInit(vendureConfig, { pluginClassName, pluginPath });
@@ -76,7 +71,7 @@ export async function addUiExtensions(providedVendurePlugin?: VendurePluginDecla
         }
     }
 
-    project.saveSync();
+    await project.save();
     if (!providedVendurePlugin) {
         outro('✅  Done!');
     }

+ 2 - 2
packages/cli/src/commands/add/ui-extensions/codemods/add-ui-extension-static-prop/add-ui-extension-static-prop.spec.ts

@@ -1,4 +1,3 @@
-import fs from 'fs-extra';
 import path from 'path';
 import { Project } from 'ts-morph';
 import { describe, expect, it } from 'vitest';
@@ -6,6 +5,7 @@ import { describe, expect, it } from 'vitest';
 import { defaultManipulationSettings } from '../../../../../constants';
 import { getPluginClasses } from '../../../../../utilities/ast-utils';
 import { expectSourceFileContentToMatch } from '../../../../../utilities/testing-utils';
+import { VendurePluginRef } from '../../../../../utilities/vendure-plugin-ref';
 
 import { addUiExtensionStaticProp } from './add-ui-extension-static-prop';
 
@@ -17,7 +17,7 @@ describe('addUiExtensionStaticProp', () => {
         project.addSourceFileAtPath(path.join(__dirname, 'fixtures', 'no-ui-prop.fixture.ts'));
         const pluginClasses = getPluginClasses(project);
         expect(pluginClasses.length).toBe(1);
-        addUiExtensionStaticProp(pluginClasses[0]);
+        addUiExtensionStaticProp(new VendurePluginRef(pluginClasses[0]));
 
         expectSourceFileContentToMatch(
             pluginClasses[0].getSourceFile(),

+ 2 - 2
packages/cli/src/commands/add/ui-extensions/codemods/add-ui-extension-static-prop/add-ui-extension-static-prop.ts

@@ -2,13 +2,13 @@ import { ClassDeclaration } from 'ts-morph';
 
 import { AdminUiExtensionTypeName } from '../../../../../constants';
 import { addImportsToFile, kebabize } from '../../../../../utilities/ast-utils';
-import { VendurePluginDeclaration } from '../../../../../utilities/vendure-plugin-declaration';
+import { VendurePluginRef } from '../../../../../utilities/vendure-plugin-ref';
 
 /**
  * @description
  * Adds the static `ui` property to the plugin class, and adds the required imports.
  */
-export function addUiExtensionStaticProp(plugin: VendurePluginDeclaration) {
+export function addUiExtensionStaticProp(plugin: VendurePluginRef) {
     const pluginClass = plugin.classDeclaration;
     const adminUiExtensionType = AdminUiExtensionTypeName;
     const extensionId = kebabize(pluginClass.getName() as string).replace(/-plugin$/, '');

+ 5 - 6
packages/cli/src/commands/add/ui-extensions/codemods/update-admin-ui-plugin-init/update-admin-ui-plugin-init.spec.ts

@@ -1,11 +1,10 @@
-import fs from 'fs-extra';
 import path from 'path';
 import { Project } from 'ts-morph';
-import { describe, expect, it } from 'vitest';
+import { describe, it } from 'vitest';
 
 import { defaultManipulationSettings } from '../../../../../constants';
-import { getVendureConfig } from '../../../../../utilities/ast-utils';
 import { expectSourceFileContentToMatch } from '../../../../../utilities/testing-utils';
+import { VendureConfigRef } from '../../../../../utilities/vendure-config-ref';
 
 import { updateAdminUiPluginInit } from './update-admin-ui-plugin-init';
 
@@ -15,7 +14,7 @@ describe('updateAdminUiPluginInit', () => {
             manipulationSettings: defaultManipulationSettings,
         });
         project.addSourceFileAtPath(path.join(__dirname, 'fixtures', 'no-app-prop.fixture.ts'));
-        const vendureConfig = getVendureConfig(project, { checkFileName: false });
+        const vendureConfig = new VendureConfigRef(project, { checkFileName: false });
         updateAdminUiPluginInit(vendureConfig, {
             pluginClassName: 'TestPlugin',
             pluginPath: './plugins/test-plugin/test.plugin',
@@ -28,12 +27,12 @@ describe('updateAdminUiPluginInit', () => {
     });
 
     // TODO: figure out why failing in CI but passing locally
-    it.skip('adds to existing ui extensions array', () => {
+    it('adds to existing ui extensions array', () => {
         const project = new Project({
             manipulationSettings: defaultManipulationSettings,
         });
         project.addSourceFileAtPath(path.join(__dirname, 'fixtures', 'existing-app-prop.fixture.ts'));
-        const vendureConfig = getVendureConfig(project, { checkFileName: false });
+        const vendureConfig = new VendureConfigRef(project, { checkFileName: false });
         updateAdminUiPluginInit(vendureConfig, {
             pluginClassName: 'TestPlugin',
             pluginPath: './plugins/test-plugin/test.plugin',

+ 11 - 12
packages/cli/src/commands/add/ui-extensions/codemods/update-admin-ui-plugin-init/update-admin-ui-plugin-init.ts

@@ -1,19 +1,18 @@
 import { Node, ObjectLiteralExpression, StructureKind, SyntaxKind } from 'ts-morph';
 
 import { addImportsToFile } from '../../../../../utilities/ast-utils';
+import { VendureConfigRef } from '../../../../../utilities/vendure-config-ref';
 
 export function updateAdminUiPluginInit(
-    vendureConfig: ObjectLiteralExpression,
+    vendureConfig: VendureConfigRef,
     options: { pluginClassName: string; pluginPath: string },
 ): boolean {
-    const plugins = vendureConfig
-        .getProperty('plugins')
-        ?.getFirstChildByKind(SyntaxKind.ArrayLiteralExpression)
-        ?.getFirstChildByKind(SyntaxKind.SyntaxList);
-
-    const adminUiPlugin = plugins?.getChildrenOfKind(SyntaxKind.CallExpression).find(c => {
-        return c.getExpression().getText() === 'AdminUiPlugin.init';
-    });
+    const adminUiPlugin = vendureConfig
+        .getPluginsArray()
+        ?.getChildrenOfKind(SyntaxKind.CallExpression)
+        .find(c => {
+            return c.getExpression().getText() === 'AdminUiPlugin.init';
+        });
     if (adminUiPlugin) {
         const initObject = adminUiPlugin
             .getArguments()
@@ -49,19 +48,19 @@ export function updateAdminUiPluginInit(
             }
         }
 
-        addImportsToFile(vendureConfig.getSourceFile(), {
+        addImportsToFile(vendureConfig.sourceFile, {
             moduleSpecifier: '@vendure/ui-devkit/compiler',
             namedImports: ['compileUiExtensions'],
             order: 0,
         });
 
-        addImportsToFile(vendureConfig.getSourceFile(), {
+        addImportsToFile(vendureConfig.sourceFile, {
             moduleSpecifier: 'path',
             namespaceImport: 'path',
             order: 0,
         });
 
-        addImportsToFile(vendureConfig.getSourceFile(), {
+        addImportsToFile(vendureConfig.sourceFile, {
             moduleSpecifier: options.pluginPath,
             namedImports: [options.pluginClassName],
         });

+ 3 - 0
packages/cli/src/constants.ts

@@ -6,3 +6,6 @@ export const defaultManipulationSettings: Partial<ManipulationSettings> = {
 };
 
 export const AdminUiExtensionTypeName = 'AdminUiExtension';
+export const Messages = {
+    NoPluginsFound: `No plugins were found in this project. Create a plugin first by selecting "[Plugin] Add a new plugin"`,
+};

+ 16 - 10
packages/cli/src/shared/shared-prompts.ts

@@ -2,8 +2,9 @@ import { cancel, isCancel, multiselect, select, spinner, text } from '@clack/pro
 import { pascalCase } from 'change-case';
 import { ClassDeclaration, Project } from 'ts-morph';
 
+import { Messages } from '../constants';
 import { getPluginClasses, getTsMorphProject } from '../utilities/ast-utils';
-import { VendurePluginDeclaration } from '../utilities/vendure-plugin-declaration';
+import { VendurePluginRef } from '../utilities/vendure-plugin-ref';
 
 export async function getCustomEntityName(cancelledMessage: string) {
     const entityName = await text({
@@ -27,7 +28,7 @@ export async function getCustomEntityName(cancelledMessage: string) {
 }
 
 export async function analyzeProject(options: {
-    providedVendurePlugin?: VendurePluginDeclaration;
+    providedVendurePlugin?: VendurePluginRef;
     cancelledMessage?: string;
 }) {
     const providedVendurePlugin = options.providedVendurePlugin;
@@ -42,11 +43,12 @@ export async function analyzeProject(options: {
     return project as Project;
 }
 
-export async function selectPlugin(
-    project: Project,
-    cancelledMessage: string,
-): Promise<VendurePluginDeclaration> {
+export async function selectPlugin(project: Project, cancelledMessage: string): Promise<VendurePluginRef> {
     const pluginClasses = getPluginClasses(project);
+    if (pluginClasses.length === 0) {
+        cancel(Messages.NoPluginsFound);
+        process.exit(0);
+    }
     const targetPlugin = await select({
         message: 'To which plugin would you like to add the feature?',
         options: pluginClasses.map(c => ({
@@ -59,14 +61,18 @@ export async function selectPlugin(
         cancel(cancelledMessage);
         process.exit(0);
     }
-    return new VendurePluginDeclaration(targetPlugin as ClassDeclaration);
+    return new VendurePluginRef(targetPlugin as ClassDeclaration);
 }
 
 export async function selectMultiplePluginClasses(
     project: Project,
     cancelledMessage: string,
-): Promise<VendurePluginDeclaration[]> {
+): Promise<VendurePluginRef[]> {
     const pluginClasses = getPluginClasses(project);
+    if (pluginClasses.length === 0) {
+        cancel(Messages.NoPluginsFound);
+        process.exit(0);
+    }
     const selectAll = await select({
         message: 'To which plugin would you like to add the feature?',
         options: [
@@ -85,7 +91,7 @@ export async function selectMultiplePluginClasses(
         process.exit(0);
     }
     if (selectAll === 'all') {
-        return pluginClasses.map(pc => new VendurePluginDeclaration(pc));
+        return pluginClasses.map(pc => new VendurePluginRef(pc));
     }
     const targetPlugins = await multiselect({
         message: 'Select one or more plugins (use ↑, ↓, space to select)',
@@ -98,5 +104,5 @@ export async function selectMultiplePluginClasses(
         cancel(cancelledMessage);
         process.exit(0);
     }
-    return (targetPlugins as ClassDeclaration[]).map(pc => new VendurePluginDeclaration(pc));
+    return (targetPlugins as ClassDeclaration[]).map(pc => new VendurePluginRef(pc));
 }

+ 3 - 54
packages/cli/src/utilities/ast-utils.ts

@@ -1,15 +1,7 @@
 import { log } from '@clack/prompts';
 import fs from 'fs-extra';
 import path from 'node:path';
-import {
-    Directory,
-    Node,
-    ObjectLiteralExpression,
-    Project,
-    ProjectOptions,
-    SourceFile,
-    VariableDeclaration,
-} from 'ts-morph';
+import { Directory, Node, Project, ProjectOptions, SourceFile } from 'ts-morph';
 
 import { defaultManipulationSettings } from '../constants';
 
@@ -44,49 +36,6 @@ export function getPluginClasses(project: Project) {
     return pluginClasses;
 }
 
-export function getVendureConfig(project: Project, options: { checkFileName?: boolean } = {}) {
-    const checkFileName = options.checkFileName ?? true;
-
-    function isVendureConfigVariableDeclaration(v: VariableDeclaration) {
-        return v.getType().getText(v) === 'VendureConfig';
-    }
-
-    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)
-        ?.getChildren()
-        .find(Node.isObjectLiteralExpression) as ObjectLiteralExpression;
-}
-
 export function addImportsToFile(
     sourceFile: SourceFile,
     options: {
@@ -132,7 +81,7 @@ function getModuleSpecifierString(moduleSpecifier: string | SourceFile, sourceFi
     if (typeof moduleSpecifier === 'string') {
         return moduleSpecifier;
     }
-    return getRelativeImportPath({ from: moduleSpecifier, to: sourceFile });
+    return getRelativeImportPath({ from: sourceFile, to: moduleSpecifier });
 }
 
 export function getRelativeImportPath(locations: {
@@ -142,7 +91,7 @@ export function getRelativeImportPath(locations: {
     const fromPath =
         locations.from instanceof SourceFile ? locations.from.getFilePath() : locations.from.getPath();
     const toPath = locations.to instanceof SourceFile ? locations.to.getFilePath() : locations.to.getPath();
-    return convertPathToRelativeImport(path.relative(toPath, fromPath));
+    return convertPathToRelativeImport(path.relative(path.dirname(fromPath), toPath));
 }
 
 export function createFile(project: Project, templatePath: string) {

+ 72 - 0
packages/cli/src/utilities/vendure-config-ref.ts

@@ -0,0 +1,72 @@
+import fs from 'fs-extra';
+import path from 'node:path';
+import {
+    Node,
+    ObjectLiteralExpression,
+    Project,
+    SourceFile,
+    SyntaxKind,
+    VariableDeclaration,
+} from 'ts-morph';
+
+export class VendureConfigRef {
+    readonly sourceFile: SourceFile;
+    readonly configObject: ObjectLiteralExpression;
+
+    constructor(project: Project, options: { checkFileName?: boolean } = {}) {
+        const checkFileName = options.checkFileName ?? true;
+
+        function isVendureConfigVariableDeclaration(v: VariableDeclaration) {
+            return v.getType().getText(v) === 'VendureConfig';
+        }
+
+        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());
+        }
+        if (!vendureConfigFile) {
+            throw new Error('Could not find the VendureConfig declaration in your project.');
+        }
+        this.sourceFile = vendureConfigFile;
+        this.configObject = vendureConfigFile
+            ?.getVariableDeclarations()
+            .find(isVendureConfigVariableDeclaration)
+            ?.getChildren()
+            .find(Node.isObjectLiteralExpression) as ObjectLiteralExpression;
+    }
+
+    getPluginsArray() {
+        return this.configObject
+            .getProperty('plugins')
+            ?.getFirstChildByKind(SyntaxKind.ArrayLiteralExpression);
+    }
+
+    addToPluginsArray(text: string) {
+        this.getPluginsArray()?.addElement(text).formatText();
+    }
+}

+ 5 - 1
packages/cli/src/utilities/vendure-plugin-declaration.ts → packages/cli/src/utilities/vendure-plugin-ref.ts

@@ -2,13 +2,17 @@ import { ClassDeclaration } from 'ts-morph';
 
 import { AdminUiExtensionTypeName } from '../constants';
 
-export class VendurePluginDeclaration {
+export class VendurePluginRef {
     constructor(public classDeclaration: ClassDeclaration) {}
 
     get name(): string {
         return this.classDeclaration.getName() as string;
     }
 
+    getSourceFile() {
+        return this.classDeclaration.getSourceFile();
+    }
+
     getPluginDir() {
         return this.classDeclaration.getSourceFile().getDirectory();
     }