Browse Source

refactor(dashboard): Clean up and improve Vite plugin architecture

Michael Bromley 10 months ago
parent
commit
50194ac013

+ 58 - 76
package-lock.json

@@ -4577,6 +4577,7 @@
     },
     "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
       "version": "1.3.0",
+      "extraneous": true,
       "inBundle": true,
       "license": "MIT",
       "engines": {
@@ -12170,7 +12171,6 @@
       "version": "5.1.4",
       "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
       "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@types/estree": "^1.0.0",
@@ -13498,7 +13498,6 @@
       "version": "1.10.18",
       "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.18.tgz",
       "integrity": "sha512-IUWKD6uQYGRy8w2X9EZrtYg1O3SCijlHbCXzMaHQYc1X7yjijQh4H3IVL9ssZZyVp2ZDfQZu4bD5DWxxvpyjvg==",
-      "devOptional": true,
       "hasInstallScript": true,
       "license": "Apache-2.0",
       "dependencies": {
@@ -13556,7 +13555,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -13573,7 +13571,6 @@
       "cpu": [
         "arm"
       ],
-      "dev": true,
       "license": "Apache-2.0",
       "optional": true,
       "os": [
@@ -13590,7 +13587,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -13607,7 +13603,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -13640,7 +13635,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -13657,7 +13651,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -13674,7 +13667,6 @@
       "cpu": [
         "ia32"
       ],
-      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -13691,7 +13683,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -13708,7 +13699,6 @@
       "cpu": [
         "arm64"
       ],
-      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -13725,7 +13715,6 @@
       "cpu": [
         "x64"
       ],
-      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -13739,14 +13728,12 @@
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
       "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
-      "devOptional": true,
       "license": "Apache-2.0"
     },
     "node_modules/@swc/types": {
       "version": "0.1.17",
       "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz",
       "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==",
-      "devOptional": true,
       "license": "Apache-2.0",
       "dependencies": {
         "@swc/counter": "^0.1.3"
@@ -22060,7 +22047,6 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
       "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/esutils": {
@@ -29052,7 +29038,6 @@
       "version": "0.2.5",
       "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz",
       "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
@@ -33016,7 +33001,6 @@
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
       "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=12"
@@ -39449,7 +39433,6 @@
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.1.tgz",
       "integrity": "sha512-/ZLrPNjChhGx3Z95pxJ4tQgfI6rWqukgYHKflrNB4zAV1izOQuDhkTn55JWeivpBxDCoK7M/TStb2aS/14PS/g==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@rollup/pluginutils": "^5.1.0",
@@ -39464,7 +39447,6 @@
       "version": "1.16.1",
       "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
       "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "acorn": "^8.14.0",
@@ -41033,7 +41015,6 @@
       "version": "0.6.2",
       "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
       "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/webpack/node_modules/ajv": {
@@ -41730,7 +41711,7 @@
     },
     "packages/admin-ui": {
       "name": "@vendure/admin-ui",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "@angular/animations": "^17.2.4",
@@ -41753,7 +41734,7 @@
         "@ng-select/ng-select": "^12.0.7",
         "@ngx-translate/core": "^15.0.0",
         "@ngx-translate/http-loader": "^8.0.0",
-        "@vendure/common": "^3.1.4",
+        "@vendure/common": "^3.1.7",
         "@webcomponents/custom-elements": "^1.6.0",
         "apollo-angular": "^6.0.0",
         "apollo-upload-client": "^18.0.1",
@@ -41824,7 +41805,7 @@
     },
     "packages/admin-ui-plugin": {
       "name": "@vendure/admin-ui-plugin",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "date-fns": "^2.30.0",
@@ -41834,9 +41815,9 @@
       "devDependencies": {
         "@types/express": "^4.17.21",
         "@types/fs-extra": "^11.0.4",
-        "@vendure/admin-ui": "^3.1.4",
-        "@vendure/common": "^3.1.4",
-        "@vendure/core": "^3.1.4",
+        "@vendure/admin-ui": "^3.1.7",
+        "@vendure/common": "^3.1.7",
+        "@vendure/core": "^3.1.7",
         "express": "^4.18.3",
         "rimraf": "^5.0.5",
         "typescript": "5.4.2"
@@ -44216,7 +44197,7 @@
     },
     "packages/asset-server-plugin": {
       "name": "@vendure/asset-server-plugin",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "file-type": "^19.0.0",
@@ -44229,8 +44210,8 @@
         "@types/express": "^4.17.21",
         "@types/fs-extra": "^11.0.4",
         "@types/node-fetch": "^2.6.11",
-        "@vendure/common": "^3.1.4",
-        "@vendure/core": "^3.1.4",
+        "@vendure/common": "^3.1.7",
+        "@vendure/core": "^3.1.7",
         "express": "^4.18.3",
         "node-fetch": "^2.7.0",
         "rimraf": "^5.0.5",
@@ -44337,11 +44318,11 @@
     },
     "packages/cli": {
       "name": "@vendure/cli",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "@clack/prompts": "^0.7.0",
-        "@vendure/common": "^3.1.4",
+        "@vendure/common": "^3.1.7",
         "change-case": "^4.1.2",
         "commander": "^11.0.0",
         "dotenv": "^16.4.5",
@@ -44355,7 +44336,7 @@
         "vendure": "dist/cli.js"
       },
       "devDependencies": {
-        "@vendure/core": "^3.1.4",
+        "@vendure/core": "^3.1.7",
         "typescript": "5.3.3"
       },
       "funding": {
@@ -44390,7 +44371,7 @@
     },
     "packages/common": {
       "name": "@vendure/common",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "devDependencies": {
         "rimraf": "^5.0.5",
@@ -44497,7 +44478,7 @@
     },
     "packages/core": {
       "name": "@vendure/core",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "@apollo/server": "^4.11.2",
@@ -44509,7 +44490,7 @@
         "@nestjs/platform-express": "~10.4.12",
         "@nestjs/terminus": "~10.2.3",
         "@nestjs/typeorm": "~10.0.2",
-        "@vendure/common": "^3.1.4",
+        "@vendure/common": "^3.1.7",
         "bcrypt": "^5.1.1",
         "body-parser": "^1.20.2",
         "cookie-session": "^2.1.0",
@@ -44742,11 +44723,11 @@
     },
     "packages/create": {
       "name": "@vendure/create",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "@clack/prompts": "^0.7.0",
-        "@vendure/common": "^3.1.4",
+        "@vendure/common": "^3.1.7",
         "commander": "^11.0.0",
         "cross-spawn": "^7.0.3",
         "fs-extra": "^11.2.0",
@@ -44764,7 +44745,7 @@
         "@types/fs-extra": "^11.0.4",
         "@types/handlebars": "^4.1.0",
         "@types/semver": "^7.5.8",
-        "@vendure/core": "^3.1.4",
+        "@vendure/core": "^3.1.7",
         "rimraf": "^5.0.5",
         "ts-node": "^10.9.2",
         "typescript": "5.3.3"
@@ -44906,6 +44887,7 @@
         "tailwind-merge": "^3.0.1",
         "tailwindcss": "^4.0.6",
         "tailwindcss-animate": "^1.0.7",
+        "unplugin-swc": "^1.5.1",
         "use-debounce": "^10.0.4"
       },
       "devDependencies": {
@@ -45257,21 +45239,21 @@
       "license": "MIT"
     },
     "packages/dev-server": {
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "@nestjs/axios": "^3.0.2",
-        "@vendure/admin-ui-plugin": "^3.1.4",
-        "@vendure/asset-server-plugin": "^3.1.4",
-        "@vendure/common": "^3.1.4",
-        "@vendure/core": "^3.1.4",
-        "@vendure/elasticsearch-plugin": "^3.1.4",
-        "@vendure/email-plugin": "^3.1.4",
+        "@vendure/admin-ui-plugin": "^3.1.7",
+        "@vendure/asset-server-plugin": "^3.1.7",
+        "@vendure/common": "^3.1.7",
+        "@vendure/core": "^3.1.7",
+        "@vendure/elasticsearch-plugin": "^3.1.7",
+        "@vendure/email-plugin": "^3.1.7",
         "typescript": "5.3.3"
       },
       "devDependencies": {
-        "@vendure/testing": "^3.1.4",
-        "@vendure/ui-devkit": "^3.1.4",
+        "@vendure/testing": "^3.1.7",
+        "@vendure/ui-devkit": "^3.1.7",
         "commander": "^12.0.0",
         "concurrently": "^8.2.2",
         "csv-stringify": "^6.4.6",
@@ -45298,7 +45280,7 @@
     },
     "packages/elasticsearch-plugin": {
       "name": "@vendure/elasticsearch-plugin",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "@elastic/elasticsearch": "~7.9.1",
@@ -45306,8 +45288,8 @@
         "fast-deep-equal": "^3.1.3"
       },
       "devDependencies": {
-        "@vendure/common": "^3.1.4",
-        "@vendure/core": "^3.1.4",
+        "@vendure/common": "^3.1.7",
+        "@vendure/core": "^3.1.7",
         "rimraf": "^5.0.5",
         "typescript": "5.3.3"
       },
@@ -45412,7 +45394,7 @@
     },
     "packages/email-plugin": {
       "name": "@vendure/email-plugin",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "@types/nodemailer": "^6.4.9",
@@ -45428,8 +45410,8 @@
         "@types/express": "^4.17.21",
         "@types/fs-extra": "^11.0.4",
         "@types/mjml": "^4.7.4",
-        "@vendure/common": "^3.1.4",
-        "@vendure/core": "^3.1.4",
+        "@vendure/common": "^3.1.7",
+        "@vendure/core": "^3.1.7",
         "rimraf": "^5.0.5",
         "typescript": "5.3.3"
       },
@@ -45534,14 +45516,14 @@
     },
     "packages/harden-plugin": {
       "name": "@vendure/harden-plugin",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "graphql-query-complexity": "^0.12.0"
       },
       "devDependencies": {
-        "@vendure/common": "^3.1.4",
-        "@vendure/core": "^3.1.4"
+        "@vendure/common": "^3.1.7",
+        "@vendure/core": "^3.1.7"
       },
       "funding": {
         "url": "https://github.com/sponsors/michaelbromley"
@@ -45549,12 +45531,12 @@
     },
     "packages/job-queue-plugin": {
       "name": "@vendure/job-queue-plugin",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "devDependencies": {
         "@google-cloud/pubsub": "^2.8.0",
-        "@vendure/common": "^3.1.4",
-        "@vendure/core": "^3.1.4",
+        "@vendure/common": "^3.1.7",
+        "@vendure/core": "^3.1.7",
         "bullmq": "^5.4.2",
         "ioredis": "^5.3.2",
         "rimraf": "^5.0.5",
@@ -45661,7 +45643,7 @@
     },
     "packages/payments-plugin": {
       "name": "@vendure/payments-plugin",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "currency.js": "2.0.4"
@@ -45670,9 +45652,9 @@
         "@mollie/api-client": "^3.7.0",
         "@types/braintree": "^3.3.11",
         "@types/localtunnel": "2.0.4",
-        "@vendure/common": "^3.1.4",
-        "@vendure/core": "^3.1.4",
-        "@vendure/testing": "^3.1.4",
+        "@vendure/common": "^3.1.7",
+        "@vendure/core": "^3.1.7",
+        "@vendure/testing": "^3.1.7",
         "braintree": "^3.22.0",
         "localtunnel": "2.0.2",
         "nock": "^13.1.4",
@@ -45821,12 +45803,12 @@
     },
     "packages/sentry-plugin": {
       "name": "@vendure/sentry-plugin",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "devDependencies": {
         "@sentry/node": "^7.106.1",
-        "@vendure/common": "^3.1.4",
-        "@vendure/core": "^3.1.4"
+        "@vendure/common": "^3.1.7",
+        "@vendure/core": "^3.1.7"
       },
       "funding": {
         "url": "https://github.com/sponsors/michaelbromley"
@@ -45837,14 +45819,14 @@
     },
     "packages/stellate-plugin": {
       "name": "@vendure/stellate-plugin",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "node-fetch": "^2.7.0"
       },
       "devDependencies": {
-        "@vendure/common": "^3.1.4",
-        "@vendure/core": "^3.1.4"
+        "@vendure/common": "^3.1.7",
+        "@vendure/core": "^3.1.7"
       },
       "funding": {
         "url": "https://github.com/sponsors/michaelbromley"
@@ -45852,11 +45834,11 @@
     },
     "packages/testing": {
       "name": "@vendure/testing",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "@graphql-typed-document-node/core": "^3.2.0",
-        "@vendure/common": "^3.1.4",
+        "@vendure/common": "^3.1.7",
         "faker": "^4.1.0",
         "form-data": "^4.0.0",
         "graphql": "~16.10.0",
@@ -45869,7 +45851,7 @@
         "@types/mysql": "^2.15.26",
         "@types/node-fetch": "^2.6.4",
         "@types/pg": "^8.11.2",
-        "@vendure/core": "^3.1.4",
+        "@vendure/core": "^3.1.7",
         "mysql": "^2.18.1",
         "pg": "^8.11.3",
         "rimraf": "^5.0.5",
@@ -45980,15 +45962,15 @@
     },
     "packages/ui-devkit": {
       "name": "@vendure/ui-devkit",
-      "version": "3.1.4",
+      "version": "3.1.7",
       "license": "GPL-3.0-or-later",
       "dependencies": {
         "@angular-devkit/build-angular": "^17.2.3",
         "@angular/cli": "^17.2.3",
         "@angular/compiler": "^17.2.4",
         "@angular/compiler-cli": "^17.2.4",
-        "@vendure/admin-ui": "^3.1.4",
-        "@vendure/common": "^3.1.4",
+        "@vendure/admin-ui": "^3.1.7",
+        "@vendure/common": "^3.1.7",
         "chalk": "^4.1.0",
         "chokidar": "^3.6.0",
         "fs-extra": "^11.2.0",
@@ -45999,7 +45981,7 @@
         "@rollup/plugin-node-resolve": "^15.2.3",
         "@rollup/plugin-terser": "^0.4.4",
         "@types/fs-extra": "^11.0.4",
-        "@vendure/core": "^3.1.4",
+        "@vendure/core": "^3.1.7",
         "react": "^19.0.0",
         "react-dom": "^19.0.0",
         "rimraf": "^5.0.5",

+ 2 - 3
packages/dashboard/src/main.tsx

@@ -8,9 +8,7 @@ import { RouterProvider, createRouter } from '@tanstack/react-router';
 import '@/framework/defaults.js';
 import { routeTree } from './routeTree.gen';
 import './styles.css';
-import { dashboardExtensions } from 'virtual:dashboard-extensions';
-
-console.log(`Dashboard extensions:`, dashboardExtensions);
+import { runDashboardExtensions } from 'virtual:dashboard-extensions';
 
 // Set up a Router instance
 const router = createRouter({
@@ -43,6 +41,7 @@ function App() {
         dynamicActivate(defaultLocale, () => {
             setI18nLoaded(true);
         });
+        runDashboardExtensions();
     }, []);
     return (
         i18nLoaded && (

+ 1 - 1
packages/dashboard/src/virtual.d.ts

@@ -3,5 +3,5 @@ declare module 'virtual:admin-api-schema' {
     export const schemaInfo: SchemaInfo;
 }
 declare module 'virtual:dashboard-extensions' {
-    export const dashboardExtensions: any;
+    export const runDashboardExtensions: () => Promise<void>;
 }

+ 26 - 10
packages/dashboard/vite/config-loader.ts

@@ -1,9 +1,16 @@
 import { Options, parse, transform } from '@swc/core';
 import { BindingIdentifier, ModuleItem, Pattern, Statement } from '@swc/types';
+import { VendureConfig } from '@vendure/core';
 import fs from 'fs-extra';
 import path from 'path';
 import { pathToFileURL } from 'url';
 
+export interface ConfigLoaderOptions {
+    vendureConfigPath: string;
+    tempDir: string;
+    vendureConfigExport?: string;
+}
+
 /**
  * @description
  * This function compiles the given Vendure config file and any imported relative files (i.e.
@@ -20,11 +27,14 @@ import { pathToFileURL } from 'url';
  * these experimental decorators. The compiled files are then loaded by Vite, which is able to handle the compiled
  * JavaScript output.
  */
-export async function loadVendureConfig(configFilePath: string) {
-    const outputPath = path.join(import.meta.dirname, './.vendure-dashboard-temp');
-    const configFileName = path.basename(configFilePath);
+export async function loadVendureConfig(
+    options: ConfigLoaderOptions,
+): Promise<{ vendureConfig: VendureConfig; exportedSymbolName: string }> {
+    const { vendureConfigPath, vendureConfigExport, tempDir } = options;
+    const outputPath = tempDir;
+    const configFileName = path.basename(vendureConfigPath);
     await fs.remove(outputPath);
-    await compileFile(configFilePath, path.join(import.meta.dirname, './.vendure-dashboard-temp'));
+    await compileFile(vendureConfigPath, path.join(import.meta.dirname, './.vendure-dashboard-temp'));
     const compiledConfigFilePath = pathToFileURL(path.join(outputPath, configFileName)).href.replace(
         /.ts$/,
         '.js',
@@ -34,18 +44,24 @@ export async function loadVendureConfig(configFilePath: string) {
 
     // We need to figure out the symbol exported by the config file by
     // analyzing the AST and finding an export with the type "VendureConfig"
-    const ast = await parse(await fs.readFile(configFilePath, 'utf-8'), {
+    const ast = await parse(await fs.readFile(vendureConfigPath, 'utf-8'), {
         syntax: 'typescript',
         decorators: true,
     });
-    const configExportedSymbolName = findConfigExport(ast.body);
+    const detectedExportedSymbolName = findConfigExport(ast.body);
+    const configExportedSymbolName = detectedExportedSymbolName || vendureConfigExport;
     if (!configExportedSymbolName) {
-        throw new Error(`Could not find a variable exported as VendureConfig`);
-    } else {
-        console.log(`Found config export: ${configExportedSymbolName}`);
+        throw new Error(
+            `Could not find a variable exported as VendureConfig. Please specify the name of the exported variable using the "vendureConfigExport" option.`,
+        );
     }
     const config = await import(compiledConfigFilePath).then(m => m[configExportedSymbolName]);
-    return config;
+    if (!config) {
+        throw new Error(
+            `Could not find a variable exported as VendureConfig with the name "${configExportedSymbolName}".`,
+        );
+    }
+    return { vendureConfig: config, exportedSymbolName: configExportedSymbolName };
 }
 
 /**

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

@@ -21,6 +21,8 @@ import {
 } from 'graphql';
 import { Plugin } from 'vite';
 
+import { ConfigLoaderApi, getConfigLoaderApi } from './vite-plugin-config-loader.js';
+
 export interface SchemaInfo {
     types: {
         [typename: string]: {
@@ -102,28 +104,36 @@ function generateSchemaInfo(schema: GraphQLSchema): SchemaInfo {
 
 const virtualModuleId = 'virtual:admin-api-schema';
 const resolvedVirtualModuleId = `\0${virtualModuleId}`;
-let schemaInfo: SchemaInfo;
-
-export async function adminApiSchemaPlugin(options: { config: VendureConfig }): Promise<Plugin> {
-    resetConfig();
-    await setConfig(options.config ?? {});
-
-    if (!schemaInfo) {
-        const runtimeConfig = await runPluginConfigurations(getConfig() as any);
-        const typesLoader = new GraphQLTypesLoader();
-        const finalSchema = await getFinalVendureSchema({
-            config: runtimeConfig,
-            typePaths: VENDURE_ADMIN_API_TYPE_PATHS,
-            typesLoader,
-            apiType: 'admin',
-            output: 'sdl',
-        });
-        const safeSchema = buildSchema(finalSchema);
-        schemaInfo = generateSchemaInfo(safeSchema);
-    }
+
+export function adminApiSchemaPlugin(): Plugin {
+    let configLoaderApi: ConfigLoaderApi;
+    let schemaInfo: SchemaInfo;
 
     return {
-        name: 'vendure-admin-api-schema',
+        name: 'vendure:admin-api-schema',
+        async configResolved({ plugins }) {
+            configLoaderApi = getConfigLoaderApi(plugins);
+        },
+        async buildStart() {
+            const vendureConfig = await configLoaderApi.getVendureConfig();
+            if (!schemaInfo) {
+                this.info(`Constructing Admin API schema...`);
+                resetConfig();
+                await setConfig(vendureConfig ?? {});
+
+                const runtimeConfig = await runPluginConfigurations(getConfig() as any);
+                const typesLoader = new GraphQLTypesLoader();
+                const finalSchema = await getFinalVendureSchema({
+                    config: runtimeConfig,
+                    typePaths: VENDURE_ADMIN_API_TYPE_PATHS,
+                    typesLoader,
+                    apiType: 'admin',
+                    output: 'sdl',
+                });
+                const safeSchema = buildSchema(finalSchema);
+                schemaInfo = generateSchemaInfo(safeSchema);
+            }
+        },
         resolveId(id) {
             if (id === virtualModuleId) {
                 return resolvedVirtualModuleId;

+ 67 - 0
packages/dashboard/vite/vite-plugin-config-loader.ts

@@ -0,0 +1,67 @@
+import { VendureConfig } from '@vendure/core';
+import { Plugin } from 'vite';
+
+import { ConfigLoaderOptions, loadVendureConfig } from './config-loader.js';
+
+export interface ConfigLoaderApi {
+    getVendureConfig(): Promise<VendureConfig>;
+}
+
+export const configLoaderName = 'vendure:config-loader';
+
+/**
+ * This Vite plugin scans the configured plugins for any dashboard extensions and dynamically
+ * generates an import statement for each one, wrapped up in a `runDashboardExtensions()`
+ * function which can then be imported and executed in the Dashboard app.
+ */
+export function configLoaderPlugin(options: ConfigLoaderOptions): Plugin {
+    let vendureConfig: VendureConfig;
+    let onConfigLoaded = () => {
+        /* */
+    };
+    return {
+        name: configLoaderName,
+        async buildStart() {
+            this.info(`Loading Vendure config...`);
+            try {
+                const result = await loadVendureConfig({
+                    tempDir: options.tempDir,
+                    vendureConfigPath: options.vendureConfigPath,
+                    vendureConfigExport: options.vendureConfigExport,
+                });
+                vendureConfig = result.vendureConfig;
+                this.info(`Vendure config loaded (using export "${result.exportedSymbolName}")`);
+            } catch (e: unknown) {
+                if (e instanceof Error) {
+                    this.error(`Error loading Vendure config: ${e.message}`);
+                }
+            }
+            onConfigLoaded();
+        },
+        api: {
+            getVendureConfig(): Promise<VendureConfig> {
+                if (vendureConfig) {
+                    return Promise.resolve(vendureConfig);
+                } else {
+                    return new Promise<VendureConfig>(resolve => {
+                        onConfigLoaded = () => {
+                            resolve(vendureConfig);
+                        };
+                    });
+                }
+            },
+        } satisfies ConfigLoaderApi,
+    };
+}
+
+/**
+ * Inter-plugin dependencies implemented following the pattern given here:
+ * https://rollupjs.org/plugin-development/#direct-plugin-communication
+ */
+export function getConfigLoaderApi(plugins: readonly Plugin[]): ConfigLoaderApi {
+    const parentPlugin = plugins.find(plugin => plugin.name === configLoaderName);
+    if (!parentPlugin) {
+        throw new Error(`This plugin depends on the "${configLoaderName}" plugin.`);
+    }
+    return parentPlugin.api as ConfigLoaderApi;
+}

+ 39 - 7
packages/dashboard/vite/vite-plugin-dashboard-metadata.ts

@@ -1,26 +1,58 @@
 import { VendureConfig } from '@vendure/core';
 import { getPluginDashboardExtensions } from '@vendure/core';
+import path from 'path';
 import { Plugin } from 'vite';
 
+import { ConfigLoaderApi, getConfigLoaderApi } from './vite-plugin-config-loader.js';
+
 const virtualModuleId = 'virtual:dashboard-extensions';
 const resolvedVirtualModuleId = `\0${virtualModuleId}`;
 
-export async function dashboardMetadataPlugin(options: { config: VendureConfig }): Promise<Plugin> {
+/**
+ * This Vite plugin scans the configured plugins for any dashboard extensions and dynamically
+ * generates an import statement for each one, wrapped up in a `runDashboardExtensions()`
+ * function which can then be imported and executed in the Dashboard app.
+ */
+export function dashboardMetadataPlugin(options: { rootDir: string }): Plugin {
+    let configLoaderApi: ConfigLoaderApi;
+    let vendureConfig: VendureConfig;
     return {
-        name: 'vendure-admin-api-schema',
-        resolveId(id, importer) {
+        name: 'vendure:dashboard-extensions-metadata',
+        configResolved({ plugins }) {
+            configLoaderApi = getConfigLoaderApi(plugins);
+        },
+        resolveId(id) {
             if (id === virtualModuleId) {
                 return resolvedVirtualModuleId;
             }
         },
-        load(id) {
+        async load(id) {
             if (id === resolvedVirtualModuleId) {
-                const extensions = getPluginDashboardExtensions(options.config.plugins ?? []);
-                console.log(`dashboardMetadataPlugin: ${JSON.stringify(extensions)}`);
+                if (!vendureConfig) {
+                    vendureConfig = 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}` };
+                });
+
+                this.info(`Found ${extensionData.length} Dashboard extensions`);
                 return `
-                    export const dashboardExtensions = ${JSON.stringify(extensions)};
+                    export async function runDashboardExtensions() {
+                        ${extensionData.map(extension => `await import('${extension.importPath}');`).join('\n')}
+                    }
                 `;
             }
         },
     };
 }
+
+/**
+ * Converts an import path to a normalized path relative to the rootDir.
+ */
+function normalizeImportPath(rootDir: string, importPath: string): string {
+    const relativePath = path.relative(rootDir, importPath).replace(/\\/g, '/');
+    return relativePath.replace(/\.tsx?$/, '.js');
+}

+ 17 - 0
packages/dashboard/vite/vite-plugin-set-root.ts

@@ -0,0 +1,17 @@
+import path from 'path';
+import { Plugin, UserConfig } from 'vite';
+
+export function setRootPlugin({ packageRoot }: { packageRoot: string }): Plugin {
+    return {
+        name: 'vendure:set-root-plugin',
+        config: (config: UserConfig) => {
+            config.root = packageRoot;
+            config.resolve = {
+                alias: {
+                    '@': path.resolve(packageRoot, './src'),
+                },
+            };
+            return config;
+        },
+    };
+}

+ 46 - 30
packages/dashboard/vite/vite-plugin-vendure-dashboard.ts

@@ -2,52 +2,68 @@ import { lingui } from '@lingui/vite-plugin';
 import tailwindcss from '@tailwindcss/vite';
 import react from '@vitejs/plugin-react';
 import path from 'path';
-import { PluginOption, UserConfig } from 'vite';
-import { compileFile, loadVendureConfig } from './config-loader.js';
+import { PluginOption } from 'vite';
+
 import { adminApiSchemaPlugin } from './vite-plugin-admin-api-schema.js';
+import { configLoaderPlugin } from './vite-plugin-config-loader.js';
 import { dashboardMetadataPlugin } from './vite-plugin-dashboard-metadata.js';
+import { setRootPlugin } from './vite-plugin-set-root.js';
 
 /**
  * @description
- * This is a Vite plugin which configures a set of plugins required to build the Vendure Dashboard.
+ * Options for the {@link vendureDashboardPlugin} Vite plugin.
  */
-export async function vendureDashboardPlugin(options: {
-    vendureConfigPath: string;
+export interface VitePluginVendureDashboardOptions {
+    /**
+     * @description
+     * The path to the Vendure server configuration file.
+     */
+    vendureConfigPath: string | URL;
+    /**
+     * @description
+     * The name of the exported variable from the Vendure server configuration file.
+     * This is only required if the plugin is unable to auto-detect the name of the exported variable.
+     */
     vendureConfigExport?: string;
-}): Promise<PluginOption[]> {
-    // const config = await options.loadConfig();
-    const config = await loadVendureConfig(options.vendureConfigPath);
+}
 
-    const packageRoot = path
-        .join(import.meta.resolve('@vendure/dashboard'), '../..')
-        .replace(/file:[\/\\]+/, '');
+/**
+ * @description
+ * This is a Vite plugin which configures a set of plugins required to build the Vendure Dashboard.
+ */
+export function vendureDashboardPlugin(options: VitePluginVendureDashboardOptions): PluginOption[] {
+    const tempDir = path.join(import.meta.dirname, './.vendure-dashboard-temp');
+    const normalizedVendureConfigPath = getNormalizedVendureConfigPath(options.vendureConfigPath);
+    const packageRoot = getDashboardPackageRoot();
     const linguiConfigPath = path.join(packageRoot, 'lingui.config.js');
-
     process.env.LINGUI_CONFIG = linguiConfigPath;
     return [
-        lingui({
-            configPath: linguiConfigPath,
-        }),
+        lingui(),
         react({
             babel: {
                 plugins: ['@lingui/babel-plugin-lingui-macro'],
             },
         }),
         tailwindcss(),
-        adminApiSchemaPlugin({ config }),
-        dashboardMetadataPlugin({ config }),
-        {
-            name: 'vendure-set-root-plugin',
-            config: (config: UserConfig) => {
-                console.log(`Setting root to ${packageRoot}`);
-                config.root = packageRoot;
-                config.resolve = {
-                    alias: {
-                        '@': path.resolve(packageRoot, './src'),
-                    },
-                };
-                return config;
-            },
-        },
+        configLoaderPlugin({ vendureConfigPath: normalizedVendureConfigPath, tempDir }),
+        setRootPlugin({ packageRoot }),
+        adminApiSchemaPlugin(),
+        dashboardMetadataPlugin({ rootDir: tempDir }),
     ];
 }
+
+/**
+ * @description
+ * Returns the path to the root of the `@vendure/dashboard` package.
+ */
+function getDashboardPackageRoot(): string {
+    return path.join(import.meta.resolve('@vendure/dashboard'), '../..').replace(/file:[\/\\]+/, '');
+}
+
+/**
+ * Get the normalized path to the Vendure config file given either a string or URL.
+ */
+function getNormalizedVendureConfigPath(vendureConfigPath: string | URL): string {
+    const stringPath = typeof vendureConfigPath === 'string' ? vendureConfigPath : vendureConfigPath.href;
+    return stringPath.replace(/^file:[\//]+/, '');
+}

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

@@ -49,7 +49,7 @@ import { AdminUiExtension } from '@vendure/ui-devkit/compiler';
         });
         return config;
     },
-    dashboard: './dashboard/index.tsx',
+    dashboard: path.join(__dirname, './dashboard/index.tsx'),
 })
 export class ReviewsPlugin {
     static uiExtensions: AdminUiExtension = {

+ 1 - 1
packages/dev-server/tsconfig.json

@@ -3,7 +3,7 @@
     "compilerOptions": {
         "module": "NodeNext",
         "sourceMap": true,
-        "jsx": "react",
+        "jsx": "react-jsx",
         "paths": {
             "@vendure/admin-ui/*": ["../admin-ui/package/*"]
         }

+ 3 - 8
packages/dev-server/vite.config.mts

@@ -1,12 +1,7 @@
 import { vendureDashboardPlugin } from '@vendure/dashboard/plugin';
 import { pathToFileURL } from 'url';
-import { defineConfig, UserConfig } from 'vite';
+import { defineConfig } from 'vite';
 
-export default defineConfig(async () => {
-    // TODO: hide this ugly stuff internally so we just need to pass a relative path to the plugin
-    const vendureConfigPath = pathToFileURL('./dev-config.ts').href.replace(/^file:[\//]+/, '');
-
-    return {
-        plugins: [vendureDashboardPlugin({ vendureConfigPath })],
-    } satisfies UserConfig;
+export default defineConfig({
+    plugins: [vendureDashboardPlugin({ vendureConfigPath: pathToFileURL('./dev-config.ts') })],
 });