Просмотр исходного кода

fix(dashboard): Improve compatibility of config loading

We switch from SWC to tsc to compile the vendure config. This is because SWC
has trouble with certain common Vendure patterns, such as translatable entities.
This means compilation is slightly slower but more reliable.
Michael Bromley 9 месяцев назад
Родитель
Сommit
24cff0bc81

+ 18 - 2
package-lock.json

@@ -4088,7 +4088,6 @@
     },
     "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
       "version": "1.3.0",
-      "extraneous": true,
       "inBundle": true,
       "license": "MIT",
       "engines": {
@@ -14000,6 +13999,7 @@
       "version": "5.1.4",
       "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
       "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
+      "devOptional": true,
       "license": "MIT",
       "dependencies": {
         "@types/estree": "^1.0.0",
@@ -15446,6 +15446,7 @@
       "version": "1.11.13",
       "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.13.tgz",
       "integrity": "sha512-9BXdYz12Wl0zWmZ80PvtjBWeg2ncwJ9L5WJzjhN6yUTZWEV/AwAdVdJnIEp4pro3WyKmAaMxcVOSbhuuOZco5g==",
+      "devOptional": true,
       "hasInstallScript": true,
       "license": "Apache-2.0",
       "dependencies": {
@@ -15503,6 +15504,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -15519,6 +15521,7 @@
       "cpu": [
         "arm"
       ],
+      "dev": true,
       "license": "Apache-2.0",
       "optional": true,
       "os": [
@@ -15535,6 +15538,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -15551,6 +15555,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -15583,6 +15588,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -15599,6 +15605,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -15615,6 +15622,7 @@
       "cpu": [
         "ia32"
       ],
+      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -15631,6 +15639,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -15647,6 +15656,7 @@
       "cpu": [
         "arm64"
       ],
+      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -15663,6 +15673,7 @@
       "cpu": [
         "x64"
       ],
+      "dev": true,
       "license": "Apache-2.0 AND MIT",
       "optional": true,
       "os": [
@@ -15676,12 +15687,14 @@
       "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.20",
       "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.20.tgz",
       "integrity": "sha512-/rlIpxwKrhz4BIplXf6nsEHtqlhzuNN34/k3kMAXH4/lvVoA3cdq+60aqVNnyvw2uITEaCi0WV3pxBe4dQqoXQ==",
+      "devOptional": true,
       "license": "Apache-2.0",
       "dependencies": {
         "@swc/counter": "^0.1.3"
@@ -24418,6 +24431,7 @@
       "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==",
+      "devOptional": true,
       "license": "MIT"
     },
     "node_modules/esutils": {
@@ -31580,6 +31594,7 @@
       "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"
@@ -42794,6 +42809,7 @@
       "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",
@@ -42808,6 +42824,7 @@
       "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",
@@ -47218,7 +47235,6 @@
         "tailwindcss": "^4.0.6",
         "tailwindcss-animate": "^1.0.7",
         "tw-animate-css": "^1.2.4",
-        "unplugin-swc": "^1.5.1",
         "vite": "^6.1.0",
         "zod": "^3.24.2"
       },

+ 0 - 1
packages/dashboard/package.json

@@ -111,7 +111,6 @@
         "tailwindcss": "^4.0.6",
         "tailwindcss-animate": "^1.0.7",
         "tw-animate-css": "^1.2.4",
-        "unplugin-swc": "^1.5.1",
         "vite": "^6.1.0",
         "zod": "^3.24.2"
     },

+ 81 - 84
packages/dashboard/vite/config-loader.ts

@@ -1,8 +1,7 @@
-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 * as ts from 'typescript';
 import { pathToFileURL } from 'url';
 
 export interface ConfigLoaderOptions {
@@ -23,9 +22,9 @@ export interface ConfigLoaderOptions {
  * internally uses esbuild to temporarily compile that TypeScript code. Unfortunately, esbuild does not support
  * these experimental decorators, errors will be thrown as soon as e.g. a TypeORM column decorator is encountered.
  *
- * To work around this, we compile the Vendure config file and all its imports using SWC, which does support
- * these experimental decorators. The compiled files are then loaded by Vite, which is able to handle the compiled
- * JavaScript output.
+ * To work around this, we compile the Vendure config file and all its imports using the TypeScript compiler,
+ * 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,
@@ -45,11 +44,13 @@ export async function loadVendureConfig(
 
     // 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(vendureConfigPath, 'utf-8'), {
-        syntax: 'typescript',
-        decorators: true,
-    });
-    const detectedExportedSymbolName = findConfigExport(ast.body);
+    const sourceFile = ts.createSourceFile(
+        vendureConfigPath,
+        await fs.readFile(vendureConfigPath, 'utf-8'),
+        ts.ScriptTarget.Latest,
+        true,
+    );
+    const detectedExportedSymbolName = findConfigExport(sourceFile);
     const configExportedSymbolName = detectedExportedSymbolName || vendureConfigExport;
     if (!configExportedSymbolName) {
         throw new Error(
@@ -68,31 +69,33 @@ export async function loadVendureConfig(
 /**
  * Given the AST of a TypeScript file, finds the name of the variable exported as VendureConfig.
  */
-function findConfigExport(statements: ModuleItem[]): string | undefined {
-    for (const statement of statements) {
-        if (statement.type === 'ExportDeclaration') {
-            if (statement.declaration.type === 'VariableDeclaration') {
-                for (const declaration of statement.declaration.declarations) {
-                    if (isBindingIdentifier(declaration.id)) {
-                        const typeRef = declaration.id.typeAnnotation?.typeAnnotation;
-                        if (typeRef?.type === 'TsTypeReference') {
-                            if (
-                                typeRef.typeName.type === 'Identifier' &&
-                                typeRef.typeName.value === 'VendureConfig'
-                            ) {
-                                return declaration.id.value;
+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);
     }
-    return undefined;
-}
 
-function isBindingIdentifier(id: Pattern): id is BindingIdentifier {
-    return id.type === 'Identifier' && !!(id as BindingIdentifier).typeAnnotation;
+    visit(sourceFile);
+    return exportedSymbolName;
 }
 
 export async function compileFile(
@@ -100,6 +103,7 @@ export async function compileFile(
     inputPath: string,
     outputDir: string,
     compiledFiles = new Set<string>(),
+    isRoot = true,
 ): Promise<void> {
     if (compiledFiles.has(inputPath)) {
         return;
@@ -112,70 +116,63 @@ export async function compileFile(
     // Read the source file
     const source = await fs.readFile(inputPath, 'utf-8');
 
-    // Transform config
-    const config: Options = {
-        filename: inputPath,
-        sourceMaps: true,
-        jsc: {
-            parser: {
-                syntax: 'typescript',
-                tsx: false,
-                decorators: true,
-            },
-            target: 'es2020',
-            loose: false,
-            transform: {
-                legacyDecorator: true,
-                decoratorMetadata: true,
-            },
-        },
-        module: {
-            type: 'commonjs',
-            strict: true,
-            strictMode: true,
-            lazy: false,
-            noInterop: false,
-        },
-    };
-
-    // Transform the code using SWC
-    const result = await transform(source, config);
-
-    // Generate output file path
-    const relativePath = path.relative(inputRootDir, inputPath);
-    const outputPath = path.join(outputDir, relativePath).replace(/\.ts$/, '.js');
-
-    // Ensure the subdirectory for the output file exists
-    await fs.ensureDir(path.dirname(outputPath));
-
-    // Write the transformed code
-    await fs.writeFile(outputPath, result.code);
-
-    // Write source map if available
-    if (result.map) {
-        await fs.writeFile(`${outputPath}.map`, JSON.stringify(result.map));
-    }
-
     // Parse the source to find relative imports
-    const ast = await parse(source, { syntax: 'typescript', decorators: true });
+    const sourceFile = ts.createSourceFile(inputPath, source, ts.ScriptTarget.Latest, true);
+
     const importPaths = new Set<string>();
 
-    function collectImports(node: any) {
-        if (node.type === 'ImportDeclaration' && node.source.value.startsWith('.')) {
-            const importPath = path.resolve(path.dirname(inputPath), node.source.value);
-            importPaths.add(importPath + '.ts');
-        }
-        for (const key in node) {
-            if (node[key] && typeof node[key] === 'object') {
-                collectImports(node[key]);
+    function collectImports(node: ts.Node) {
+        if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
+            const importPath = node.moduleSpecifier.text;
+            if (importPath.startsWith('.')) {
+                const resolvedPath = path.resolve(path.dirname(inputPath), importPath);
+                importPaths.add(resolvedPath + '.ts');
             }
         }
+        ts.forEachChild(node, collectImports);
     }
 
-    collectImports(ast);
+    collectImports(sourceFile);
 
-    // Recursively compile all relative imports
+    // Recursively collect all files that need to be compiled
     for (const importPath of importPaths) {
-        await compileFile(inputRootDir, importPath, outputDir, compiledFiles);
+        await compileFile(inputRootDir, importPath, outputDir, compiledFiles, false);
+    }
+
+    // If this is the root file (the one that started the compilation),
+    // transpile all files
+    if (isRoot) {
+        const allFiles = Array.from(compiledFiles);
+        for (const file of allFiles) {
+            const fileSource = await fs.readFile(file, 'utf-8');
+            const result = ts.transpileModule(fileSource, {
+                compilerOptions: {
+                    target: ts.ScriptTarget.ES2020,
+                    module: ts.ModuleKind.CommonJS,
+                    experimentalDecorators: true,
+                    emitDecoratorMetadata: true,
+                    sourceMap: true,
+                    esModuleInterop: true,
+                    skipLibCheck: true,
+                    forceConsistentCasingInFileNames: true,
+                },
+                fileName: file,
+            });
+
+            // Generate output file path
+            const relativePath = path.relative(inputRootDir, file);
+            const outputPath = path.join(outputDir, relativePath).replace(/\.ts$/, '.js');
+
+            // Ensure the subdirectory for the output file exists
+            await fs.ensureDir(path.dirname(outputPath));
+
+            // Write the transpiled code
+            await fs.writeFile(outputPath, result.outputText);
+
+            // Write source map if available
+            if (result.sourceMapText) {
+                await fs.writeFile(`${outputPath}.map`, result.sourceMapText);
+            }
+        }
     }
 }

+ 9 - 2
packages/dashboard/vite/vite-plugin-config-loader.ts

@@ -19,15 +19,22 @@ export function configLoaderPlugin(options: ConfigLoaderOptions): Plugin {
     return {
         name: configLoaderName,
         async buildStart() {
-            this.info(`Loading Vendure config...`);
+            this.info(
+                `Loading Vendure config. This can take a short while depending on the size of your project...`,
+            );
             try {
+                const startTime = Date.now();
                 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}")`);
+                const endTime = Date.now();
+                const duration = endTime - startTime;
+                this.info(
+                    `Vendure config loaded (using export "${result.exportedSymbolName}") in ${duration}ms`,
+                );
             } catch (e: unknown) {
                 if (e instanceof Error) {
                     this.error(`Error loading Vendure config: ${e.message}`);