Ver Fonte

feat(dashboard): Improve support for dashboard extensions

This refactor changes the way we load dashboard extensions, so that
it should work even in scenarios where ts config paths are being used.
Michael Bromley há 9 meses atrás
pai
commit
8763eb7338

+ 1 - 1
packages/dashboard/vite.config.mts

@@ -10,7 +10,7 @@ import { defineConfig } from 'vitest/config';
 export default ({ mode }: { mode: string }) => {
     process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
 
-    const adminApiHost = process.env.VITE_ADMIN_API_HOST || 'http://localhost';
+    const adminApiHost = process.env.VITE_ADMIN_API_HOST || 'http://localhost:3000';
     const adminApiPort = process.env.VITE_ADMIN_API_PORT ? +process.env.VITE_ADMIN_API_PORT : 'auto';
 
     process.env.IS_LOCAL_DEV = adminApiHost.includes('localhost') ? 'true' : 'false';

+ 128 - 0
packages/dashboard/vite/utils/ast-utils.spec.ts

@@ -0,0 +1,128 @@
+import ts from 'typescript';
+import { describe, it, expect } from 'vitest';
+
+import { getPluginInfo, findConfigExport } from './ast-utils.js';
+
+describe('getPluginInfo', () => {
+    it('should return undefined when no plugin class is found', () => {
+        const sourceText = `
+            class NotAPlugin {
+                constructor() {}
+            }
+        `;
+        const sourceFile = ts.createSourceFile('path/to/test.ts', sourceText, ts.ScriptTarget.Latest, true);
+        const result = getPluginInfo(sourceFile);
+        expect(result).toBeUndefined();
+    });
+
+    it('should return plugin info when a valid plugin class is found', () => {
+        const sourceText = `
+            @VendurePlugin({
+                imports: [],
+                providers: []
+            })
+            class TestPlugin {
+                constructor() {}
+            }
+        `;
+        const sourceFile = ts.createSourceFile('path/to/test.ts', sourceText, ts.ScriptTarget.Latest, true);
+        const result = getPluginInfo(sourceFile);
+        expect(result).toEqual({
+            name: 'TestPlugin',
+            path: 'path/to',
+            dashboardEntryPath: undefined,
+        });
+    });
+
+    it('should handle multiple classes but only return the plugin one', () => {
+        const sourceText = `
+            class NotAPlugin {
+                constructor() {}
+            }
+
+            @VendurePlugin({
+                imports: [],
+                providers: []
+            })
+            class TestPlugin {
+                constructor() {}
+            }
+
+            class AnotherClass {
+                constructor() {}
+            }
+        `;
+        const sourceFile = ts.createSourceFile('path/to/test.ts', sourceText, ts.ScriptTarget.Latest, true);
+        const result = getPluginInfo(sourceFile);
+        expect(result).toEqual({
+            name: 'TestPlugin',
+            path: 'path/to',
+            dashboardEntryPath: undefined,
+        });
+    });
+
+    it('should determine the dashboard entry path when it is provided', () => {
+        const sourceText = `
+            @VendurePlugin({
+                imports: [],
+                providers: [],
+                dashboard: './dashboard/index.tsx',
+            })
+            class TestPlugin {
+                constructor() {}
+            }
+        `;
+        const sourceFile = ts.createSourceFile('path/to/test.ts', sourceText, ts.ScriptTarget.Latest, true);
+        const result = getPluginInfo(sourceFile);
+        expect(result).toEqual({
+            name: 'TestPlugin',
+            path: 'path/to',
+            dashboardEntryPath: './dashboard/index.tsx',
+        });
+    });
+});
+
+describe('findConfigExport', () => {
+    it('should return undefined when no VendureConfig export is found', () => {
+        const sourceText = `
+            export const notConfig = {
+                some: 'value'
+            };
+        `;
+        const sourceFile = ts.createSourceFile('path/to/test.ts', sourceText, ts.ScriptTarget.Latest, true);
+        const result = findConfigExport(sourceFile);
+        expect(result).toBeUndefined();
+    });
+
+    it('should find exported variable with VendureConfig type', () => {
+        const sourceText = `
+            import { VendureConfig } from '@vendure/core';
+            
+            export const config: VendureConfig = {
+                authOptions: {
+                    tokenMethod: 'bearer'
+                }
+            };
+        `;
+        const sourceFile = ts.createSourceFile('path/to/test.ts', sourceText, ts.ScriptTarget.Latest, true);
+        const result = findConfigExport(sourceFile);
+        expect(result).toBe('config');
+    });
+
+    it('should find exported variable with VendureConfig type among other exports', () => {
+        const sourceText = `
+            import { VendureConfig } from '@vendure/core';
+            
+            export const otherExport = 'value';
+            export const config: VendureConfig = {
+                authOptions: {
+                    tokenMethod: 'bearer'
+                }
+            };
+            export const anotherExport = 123;
+        `;
+        const sourceFile = ts.createSourceFile('path/to/test.ts', sourceText, ts.ScriptTarget.Latest, true);
+        const result = findConfigExport(sourceFile);
+        expect(result).toBe('config');
+    });
+});

+ 119 - 0
packages/dashboard/vite/utils/ast-utils.ts

@@ -0,0 +1,119 @@
+import path from 'path';
+import ts from 'typescript';
+
+import { PluginInfo } from './config-loader.js';
+
+/**
+ * Get the plugin info from the source file.
+ */
+export function getPluginInfo(sourceFile: ts.SourceFile): PluginInfo | undefined {
+    const classDeclaration = sourceFile.statements.find(statement => {
+        return (
+            statement.kind === ts.SyntaxKind.ClassDeclaration &&
+            statement.getText().includes('@VendurePlugin(')
+        );
+    });
+    if (classDeclaration) {
+        const identifier = classDeclaration.getChildren().find(child => {
+            return child.kind === ts.SyntaxKind.Identifier;
+        });
+        const dashboardEntryPath = classDeclaration
+            .getChildren()
+            .map(child => {
+                if (child.kind === ts.SyntaxKind.SyntaxList) {
+                    const pluginDecorator = child.getChildren().find(_child => {
+                        return _child.kind === ts.SyntaxKind.Decorator;
+                    });
+                    if (pluginDecorator) {
+                        const callExpression = findFirstDescendantOfKind(
+                            pluginDecorator,
+                            ts.SyntaxKind.CallExpression,
+                        );
+                        if (callExpression) {
+                            const objectLiteral = findFirstDescendantOfKind(
+                                callExpression,
+                                ts.SyntaxKind.ObjectLiteralExpression,
+                            );
+                            if (objectLiteral && ts.isObjectLiteralExpression(objectLiteral)) {
+                                // Now find the specific 'dashboard' property
+                                const dashboardProperty = objectLiteral.properties.find(
+                                    prop =>
+                                        ts.isPropertyAssignment(prop) && prop.name?.getText() === 'dashboard',
+                                );
+
+                                if (
+                                    dashboardProperty &&
+                                    ts.isPropertyAssignment(dashboardProperty) &&
+                                    ts.isStringLiteral(dashboardProperty.initializer)
+                                ) {
+                                    const dashboardPath = dashboardProperty.initializer.text;
+                                    return dashboardPath;
+                                }
+                            }
+                        }
+                    }
+                }
+            })
+            .filter(Boolean)?.[0];
+        if (identifier) {
+            return {
+                name: identifier.getText(),
+                pluginPath: path.dirname(sourceFile.fileName),
+                dashboardEntryPath,
+            };
+        }
+    }
+}
+
+/**
+ * Given the AST of a TypeScript file, finds the name of the variable exported as VendureConfig.
+ */
+export function findConfigExport(sourceFile: ts.SourceFile): string | undefined {
+    let exportedSymbolName: string | undefined;
+
+    function visit(node: ts.Node) {
+        if (
+            ts.isVariableStatement(node) &&
+            node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)
+        ) {
+            node.declarationList.declarations.forEach(declaration => {
+                if (ts.isVariableDeclaration(declaration)) {
+                    const typeNode = declaration.type;
+                    if (typeNode && ts.isTypeReferenceNode(typeNode)) {
+                        const typeName = typeNode.typeName;
+                        if (ts.isIdentifier(typeName) && typeName.text === 'VendureConfig') {
+                            if (ts.isIdentifier(declaration.name)) {
+                                exportedSymbolName = declaration.name.text;
+                            }
+                        }
+                    }
+                }
+            });
+        }
+        ts.forEachChild(node, visit);
+    }
+
+    visit(sourceFile);
+    return exportedSymbolName;
+}
+
+function findFirstDescendantOfKind(node: ts.Node, kind: ts.SyntaxKind): ts.Node | undefined {
+    let foundNode: ts.Node | undefined;
+
+    function visit(_node: ts.Node) {
+        if (foundNode) {
+            // Stop searching if we already found it
+            return;
+        }
+        if (_node.kind === kind) {
+            foundNode = _node;
+            return;
+        }
+        // Recursively visit children
+        ts.forEachChild(_node, visit);
+    }
+
+    // Start the traversal from the initial node's children
+    ts.forEachChild(node, visit);
+    return foundNode;
+}

+ 54 - 69
packages/dashboard/vite/config-loader.ts → packages/dashboard/vite/utils/config-loader.ts

@@ -5,12 +5,20 @@ import tsConfigPaths from 'tsconfig-paths';
 import * as ts from 'typescript';
 import { pathToFileURL } from 'url';
 
+import { findConfigExport, getPluginInfo } from './ast-utils.js';
+
 type Logger = {
     info: (message: string) => void;
     warn: (message: string) => void;
     debug: (message: string) => void;
 };
 
+export type PluginInfo = {
+    name: string;
+    pluginPath: string;
+    dashboardEntryPath: string | undefined;
+};
+
 const defaultLogger: Logger = {
     info: (message: string) => {
         /* noop */
@@ -30,6 +38,12 @@ export interface ConfigLoaderOptions {
     logger?: Logger;
 }
 
+export interface LoadVendureConfigResult {
+    vendureConfig: VendureConfig;
+    exportedSymbolName: string;
+    pluginInfo: PluginInfo[];
+}
+
 /**
  * @description
  * This function compiles the given Vendure config file and any imported relative files (i.e.
@@ -46,16 +60,14 @@ export interface ConfigLoaderOptions {
  * which fully supports these experimental decorators. The compiled files are then loaded by Vite, which is able
  * to handle the compiled JavaScript output.
  */
-export async function loadVendureConfig(
-    options: ConfigLoaderOptions,
-): Promise<{ vendureConfig: VendureConfig; exportedSymbolName: string }> {
+export async function loadVendureConfig(options: ConfigLoaderOptions): Promise<LoadVendureConfigResult> {
     const { vendureConfigPath, vendureConfigExport, tempDir } = options;
     const logger = options.logger || defaultLogger;
     const outputPath = tempDir;
     const configFileName = path.basename(vendureConfigPath);
     const inputRootDir = path.dirname(vendureConfigPath);
     await fs.remove(outputPath);
-    await compileFile(inputRootDir, vendureConfigPath, outputPath, logger);
+    const pluginInfo = await compileFile(inputRootDir, vendureConfigPath, outputPath, logger);
     const compiledConfigFilePath = pathToFileURL(path.join(outputPath, configFileName)).href.replace(
         /.ts$/,
         '.js',
@@ -94,39 +106,7 @@ export async function loadVendureConfig(
             `Could not find a variable exported as VendureConfig with the name "${configExportedSymbolName}".`,
         );
     }
-    return { vendureConfig: config, exportedSymbolName: configExportedSymbolName };
-}
-
-/**
- * Given the AST of a TypeScript file, finds the name of the variable exported as VendureConfig.
- */
-function findConfigExport(sourceFile: ts.SourceFile): string | undefined {
-    let exportedSymbolName: string | undefined;
-
-    function visit(node: ts.Node) {
-        if (
-            ts.isVariableStatement(node) &&
-            node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)
-        ) {
-            node.declarationList.declarations.forEach(declaration => {
-                if (ts.isVariableDeclaration(declaration)) {
-                    const typeNode = declaration.type;
-                    if (typeNode && ts.isTypeReferenceNode(typeNode)) {
-                        const typeName = typeNode.typeName;
-                        if (ts.isIdentifier(typeName) && typeName.text === 'VendureConfig') {
-                            if (ts.isIdentifier(declaration.name)) {
-                                exportedSymbolName = declaration.name.text;
-                            }
-                        }
-                    }
-                }
-            });
-        }
-        ts.forEachChild(node, visit);
-    }
-
-    visit(sourceFile);
-    return exportedSymbolName;
+    return { vendureConfig: config, exportedSymbolName: configExportedSymbolName, pluginInfo };
 }
 
 /**
@@ -194,10 +174,11 @@ export async function compileFile(
     logger: Logger = defaultLogger,
     compiledFiles = new Set<string>(),
     isRoot = true,
-): Promise<void> {
+    pluginInfo: PluginInfo[] = [],
+): Promise<PluginInfo[]> {
     const absoluteInputPath = path.resolve(inputPath);
     if (compiledFiles.has(absoluteInputPath)) {
-        return;
+        return pluginInfo;
     }
     compiledFiles.add(absoluteInputPath);
 
@@ -333,13 +314,18 @@ export async function compileFile(
     // Start collecting imports from the source file
     await collectImports(sourceFile);
 
+    const extractedPluginInfo = getPluginInfo(sourceFile);
+    if (extractedPluginInfo) {
+        pluginInfo.push(extractedPluginInfo);
+    }
+
     // Store the tsConfigInfo on the first call if found
     const rootTsConfigInfo = isRoot ? tsConfigInfo : undefined;
 
     // Recursively collect all files that need to be compiled
     for (const importPath of importPaths) {
         // Pass rootTsConfigInfo down, but set isRoot to false
-        await compileFile(inputRootDir, importPath, outputDir, logger, compiledFiles, false);
+        await compileFile(inputRootDir, importPath, outputDir, logger, compiledFiles, false, pluginInfo);
     }
 
     // If this is the root file (the one that started the compilation),
@@ -381,38 +367,10 @@ export async function compileFile(
 
         // Create a Program to represent the compilation context
         const program = ts.createProgram(allFiles, compilerOptions);
-
-        // Perform the compilation and emit files
         logger.info(`Emitting compiled files to ${outputDir}`);
         const emitResult = program.emit();
 
-        // Report diagnostics (errors and warnings)
-        const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);
-
-        let hasEmitErrors = emitResult.emitSkipped;
-        allDiagnostics.forEach(diagnostic => {
-            if (diagnostic.file && diagnostic.start) {
-                const { line, character } = ts.getLineAndCharacterOfPosition(
-                    diagnostic.file,
-                    diagnostic.start,
-                );
-                const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
-                const logFn = diagnostic.category === ts.DiagnosticCategory.Error ? logger.warn : logger.info;
-                logFn(
-                    `TS${diagnostic.code} ${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`,
-                );
-                if (diagnostic.category === ts.DiagnosticCategory.Error) {
-                    hasEmitErrors = true;
-                }
-            } else {
-                const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
-                const logFn = diagnostic.category === ts.DiagnosticCategory.Error ? logger.warn : logger.info;
-                logFn(`TS${diagnostic.code}: ${message}`);
-                if (diagnostic.category === ts.DiagnosticCategory.Error) {
-                    hasEmitErrors = true;
-                }
-            }
-        });
+        const hasEmitErrors = reportDiagnostics(program, emitResult, logger);
 
         if (hasEmitErrors) {
             throw new Error('TypeScript compilation failed with errors.');
@@ -420,4 +378,31 @@ export async function compileFile(
 
         logger.info(`Successfully compiled ${allFiles.length} files to ${outputDir}`);
     }
+    return pluginInfo;
+}
+
+function reportDiagnostics(program: ts.Program, emitResult: ts.EmitResult, logger: Logger) {
+    const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics);
+    let hasEmitErrors = emitResult.emitSkipped;
+    allDiagnostics.forEach(diagnostic => {
+        if (diagnostic.file && diagnostic.start) {
+            const { line, character } = ts.getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start);
+            const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
+            const logFn = diagnostic.category === ts.DiagnosticCategory.Error ? logger.warn : logger.info;
+            logFn(
+                `TS${diagnostic.code} ${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`,
+            );
+            if (diagnostic.category === ts.DiagnosticCategory.Error) {
+                hasEmitErrors = true;
+            }
+        } else {
+            const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
+            const logFn = diagnostic.category === ts.DiagnosticCategory.Error ? logger.warn : logger.info;
+            logFn(`TS${diagnostic.code}: ${message}`);
+            if (diagnostic.category === ts.DiagnosticCategory.Error) {
+                hasEmitErrors = true;
+            }
+        }
+    });
+    return hasEmitErrors;
 }

+ 7 - 1
packages/dashboard/vite/schema-generator.ts → packages/dashboard/vite/utils/schema-generator.ts

@@ -11,14 +11,20 @@ import {
 import { buildSchema } from 'graphql';
 import { GraphQLSchema } from 'graphql';
 
-let schemaPromise: Promise<GraphQLSchema>;
+let schemaPromise: Promise<GraphQLSchema> | undefined;
 
+/**
+ * @description
+ * This function generates a GraphQL schema from the Vendure config.
+ * It is used to generate the schema for the dashboard.
+ */
 export async function generateSchema({
     vendureConfig,
 }: {
     vendureConfig: VendureConfig;
 }): Promise<GraphQLSchema> {
     if (!schemaPromise) {
+        /* eslint-disable-next-line @typescript-eslint/no-misused-promises */
         schemaPromise = new Promise(async (resolve, reject) => {
             resetConfig();
             await setConfig(vendureConfig ?? {});

+ 2 - 2
packages/dashboard/vite/ui-config.ts → packages/dashboard/vite/utils/ui-config.ts

@@ -6,8 +6,8 @@ import {
 import { AdminUiConfig } from '@vendure/common/lib/shared-types';
 import { VendureConfig } from '@vendure/core';
 
-import { defaultAvailableLocales } from './constants.js';
-import { defaultLocale, defaultLanguage, defaultAvailableLanguages } from './constants.js';
+import { defaultAvailableLocales } from '../constants.js';
+import { defaultLocale, defaultLanguage, defaultAvailableLanguages } from '../constants.js';
 
 export function getAdminUiConfig(
     config: VendureConfig,

+ 2 - 2
packages/dashboard/vite/vite-plugin-admin-api-schema.ts

@@ -21,7 +21,7 @@ import {
 } from 'graphql';
 import { Plugin } from 'vite';
 
-import { generateSchema } from './schema-generator.js';
+import { generateSchema } from './utils/schema-generator.js';
 import { ConfigLoaderApi, getConfigLoaderApi } from './vite-plugin-config-loader.js';
 
 export type FieldInfoTuple = readonly [
@@ -61,7 +61,7 @@ export function adminApiSchemaPlugin(): Plugin {
             configLoaderApi = getConfigLoaderApi(plugins);
         },
         async buildStart() {
-            const vendureConfig = await configLoaderApi.getVendureConfig();
+            const { vendureConfig } = await configLoaderApi.getVendureConfig();
             if (!schemaInfo) {
                 const safeSchema = await generateSchema({ vendureConfig });
                 schemaInfo = generateSchemaInfo(safeSchema);

+ 11 - 11
packages/dashboard/vite/vite-plugin-config-loader.ts

@@ -1,10 +1,9 @@
-import { VendureConfig } from '@vendure/core';
 import { Plugin } from 'vite';
 
-import { ConfigLoaderOptions, loadVendureConfig } from './config-loader.js';
+import { ConfigLoaderOptions, loadVendureConfig, LoadVendureConfigResult } from './utils/config-loader.js';
 
 export interface ConfigLoaderApi {
-    getVendureConfig(): Promise<VendureConfig>;
+    getVendureConfig(): Promise<LoadVendureConfigResult>;
 }
 
 export const configLoaderName = 'vendure:config-loader';
@@ -14,7 +13,7 @@ export const configLoaderName = 'vendure:config-loader';
  * makes it available to other plugins via the `ConfigLoaderApi`.
  */
 export function configLoaderPlugin(options: ConfigLoaderOptions): Plugin {
-    let vendureConfig: VendureConfig;
+    let result: LoadVendureConfigResult;
     const onConfigLoaded: Array<() => void> = [];
     return {
         name: configLoaderName,
@@ -24,7 +23,7 @@ export function configLoaderPlugin(options: ConfigLoaderOptions): Plugin {
             );
             try {
                 const startTime = Date.now();
-                const result = await loadVendureConfig({
+                result = await loadVendureConfig({
                     tempDir: options.tempDir,
                     vendureConfigPath: options.vendureConfigPath,
                     vendureConfigExport: options.vendureConfigExport,
@@ -34,9 +33,10 @@ export function configLoaderPlugin(options: ConfigLoaderOptions): Plugin {
                         debug: (message: string) => this.debug(message),
                     },
                 });
-                vendureConfig = result.vendureConfig;
                 const endTime = Date.now();
                 const duration = endTime - startTime;
+                const pluginNames = result.pluginInfo.map(p => p.name).join(', ');
+                this.info(`Found ${result.pluginInfo.length} plugins: ${pluginNames}`);
                 this.info(
                     `Vendure config loaded (using export "${result.exportedSymbolName}") in ${duration}ms`,
                 );
@@ -48,13 +48,13 @@ export function configLoaderPlugin(options: ConfigLoaderOptions): Plugin {
             onConfigLoaded.forEach(fn => fn());
         },
         api: {
-            getVendureConfig(): Promise<VendureConfig> {
-                if (vendureConfig) {
-                    return Promise.resolve(vendureConfig);
+            getVendureConfig(): Promise<LoadVendureConfigResult> {
+                if (result) {
+                    return Promise.resolve(result);
                 } else {
-                    return new Promise<VendureConfig>(resolve => {
+                    return new Promise<LoadVendureConfigResult>(resolve => {
                         onConfigLoaded.push(() => {
-                            resolve(vendureConfig);
+                            resolve(result);
                         });
                     });
                 }

+ 19 - 15
packages/dashboard/vite/vite-plugin-dashboard-metadata.ts

@@ -1,8 +1,7 @@
-import { VendureConfig } from '@vendure/core';
-import { getPluginDashboardExtensions } from '@vendure/core';
 import path from 'path';
 import { Plugin } from 'vite';
 
+import { LoadVendureConfigResult } from './utils/config-loader.js';
 import { ConfigLoaderApi, getConfigLoaderApi } from './vite-plugin-config-loader.js';
 
 const virtualModuleId = 'virtual:dashboard-extensions';
@@ -15,7 +14,7 @@ const resolvedVirtualModuleId = `\0${virtualModuleId}`;
  */
 export function dashboardMetadataPlugin(options: { rootDir: string }): Plugin {
     let configLoaderApi: ConfigLoaderApi;
-    let vendureConfig: VendureConfig;
+    let loadVendureConfigResult: LoadVendureConfigResult;
     return {
         name: 'vendure:dashboard-extensions-metadata',
         configResolved({ plugins }) {
@@ -28,22 +27,27 @@ export function dashboardMetadataPlugin(options: { rootDir: string }): Plugin {
         },
         async load(id) {
             if (id === resolvedVirtualModuleId) {
-                if (!vendureConfig) {
-                    vendureConfig = await configLoaderApi.getVendureConfig();
+                if (!loadVendureConfigResult) {
+                    loadVendureConfigResult = await configLoaderApi.getVendureConfig();
                 }
-                const extensions = getPluginDashboardExtensions(vendureConfig.plugins ?? []);
-                const extensionData: Array<{ importPath: string }> = extensions.map(extension => {
-                    const providedPath = typeof extension === 'string' ? extension : extension.location;
-                    const jsPath = normalizeImportPath(options.rootDir, providedPath);
-                    return { importPath: `./${jsPath}` };
-                });
+                const { pluginInfo } = loadVendureConfigResult;
+                const pluginsWithExtensions =
+                    pluginInfo
+                        ?.map(
+                            ({ dashboardEntryPath, pluginPath }) =>
+                                dashboardEntryPath && path.join(pluginPath, dashboardEntryPath),
+                        )
+                        .filter(x => x != null) ?? [];
 
-                this.info(`Found ${extensionData.length} Dashboard extensions`);
+                this.info(`Found ${pluginsWithExtensions.length} Dashboard extensions`);
                 return `
                     export async function runDashboardExtensions() {
-                        ${extensionData.map(extension => `await import('${extension.importPath}');`).join('\n')}
-                    }
-                `;
+                        ${pluginsWithExtensions
+                            .map(extension => {
+                                return `await import('${extension}');`;
+                            })
+                            .join('\n')}
+                }`;
             }
         },
     };

+ 2 - 2
packages/dashboard/vite/vite-plugin-gql-tada.ts

@@ -4,7 +4,7 @@ import { printSchema } from 'graphql';
 import * as path from 'path';
 import { Plugin } from 'vite';
 
-import { generateSchema } from './schema-generator.js';
+import { generateSchema } from './utils/schema-generator.js';
 import { ConfigLoaderApi, getConfigLoaderApi } from './vite-plugin-config-loader.js';
 
 export function gqlTadaPlugin(options: {
@@ -20,7 +20,7 @@ export function gqlTadaPlugin(options: {
             configLoaderApi = getConfigLoaderApi(plugins);
         },
         async buildStart() {
-            const vendureConfig = await configLoaderApi.getVendureConfig();
+            const { vendureConfig } = await configLoaderApi.getVendureConfig();
             const safeSchema = await generateSchema({ vendureConfig });
 
             const tsConfigContent = {

+ 3 - 2
packages/dashboard/vite/vite-plugin-ui-config.ts

@@ -2,7 +2,7 @@ import { AdminUiConfig, VendureConfig } from '@vendure/core';
 import path from 'path';
 import { Plugin } from 'vite';
 
-import { getAdminUiConfig } from './ui-config.js';
+import { getAdminUiConfig } from './utils/ui-config.js';
 import { ConfigLoaderApi, getConfigLoaderApi } from './vite-plugin-config-loader.js';
 
 const virtualModuleId = 'virtual:vendure-ui-config';
@@ -38,7 +38,8 @@ export function uiConfigPlugin({ adminUiConfig }: UiConfigPluginOptions): Plugin
         async load(id) {
             if (id === resolvedVirtualModuleId) {
                 if (!vendureConfig) {
-                    vendureConfig = await configLoaderApi.getVendureConfig();
+                    const result = await configLoaderApi.getVendureConfig();
+                    vendureConfig = result.vendureConfig;
                 }
 
                 const config = getAdminUiConfig(vendureConfig, adminUiConfig);

+ 3 - 1
packages/dev-server/test-plugins/reviews/dashboard/index.tsx

@@ -18,7 +18,9 @@ export default defineDashboardExtension({
         {
             label: 'Custom Action Bar Item',
             component: props => {
-                return <Button>YOLO swag</Button>;
+                return <Button type="button" onClick={() => {
+                    console.log('Clicked custom action bar item');
+                }}>Test Button</Button>;
             },
             locationId: 'product-detail',
         },

+ 1 - 1
packages/dev-server/test-plugins/reviews/reviews-plugin.ts

@@ -49,7 +49,7 @@ import { ProductReviewTranslation } from './entities/product-review-translation.
         });
         return config;
     },
-    dashboard: './dashboard',
+    dashboard: './dashboard/index.tsx',
 })
 export class ReviewsPlugin {
     static uiExtensions: AdminUiExtension = {