From 1ed1a442697e372708539b5790b024952ec860b8 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 9 Dec 2025 12:26:21 +0100 Subject: [PATCH 1/7] Regroup preserve loaders imports Vite plugin inside a vite-extension --- packages/php-wasm/node/vite.config.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/php-wasm/node/vite.config.ts b/packages/php-wasm/node/vite.config.ts index 24ff5870c9..048aca09a3 100644 --- a/packages/php-wasm/node/vite.config.ts +++ b/packages/php-wasm/node/vite.config.ts @@ -71,8 +71,6 @@ export default defineConfig(function () { }, sourcemap: true, rollupOptions: { - // Don't bundle the PHP loaders in the final build. See - // the preserve-php-loaders-imports plugin above. external: getExternalModules(), output: { entryFileNames: '[name].js', From 921dfb0f3d001edc8b68d980a341e7580a82efe4 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 9 Dec 2025 12:48:50 +0100 Subject: [PATCH 2/7] Lint PHP.wasm Web vite config file --- packages/php-wasm/web/vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/php-wasm/web/vite.config.ts b/packages/php-wasm/web/vite.config.ts index 1c2a757415..261efe53d2 100644 --- a/packages/php-wasm/web/vite.config.ts +++ b/packages/php-wasm/web/vite.config.ts @@ -12,7 +12,7 @@ import viteGlobalExtensions from '../../vite-extensions/vite-global-extensions'; // eslint-disable-next-line @nx/enforce-module-boundaries import { getExternalModules } from '../../vite-extensions/vite-external-modules'; -export default defineConfig(({ command }) => { +export default defineConfig(() => { return { cacheDir: '../../../node_modules/.vite/php-wasm', From 737eec8d7bed5502605338ced272ce0660affb5d Mon Sep 17 00:00:00 2001 From: = Date: Tue, 9 Dec 2025 15:43:17 +0100 Subject: [PATCH 3/7] Add a verification step in the extension and comments --- packages/php-wasm/web/vite.config.ts | 123 ++++++------------ .../vite-preserve-loaders-imports.ts | 78 +++++++++++ 2 files changed, 116 insertions(+), 85 deletions(-) create mode 100644 packages/vite-extensions/vite-preserve-loaders-imports.ts diff --git a/packages/php-wasm/web/vite.config.ts b/packages/php-wasm/web/vite.config.ts index 261efe53d2..9cd079f0f3 100644 --- a/packages/php-wasm/web/vite.config.ts +++ b/packages/php-wasm/web/vite.config.ts @@ -8,11 +8,13 @@ import { viteTsConfigPaths } from '../../vite-extensions/vite-ts-config-paths'; // eslint-disable-next-line @nx/enforce-module-boundaries import { viteIgnoreImports } from '../../vite-extensions/vite-ignore-imports'; // eslint-disable-next-line @nx/enforce-module-boundaries +import { vitePreserveLoadersImports } from '../../vite-extensions/vite-preserve-loaders-imports'; +// eslint-disable-next-line @nx/enforce-module-boundaries import viteGlobalExtensions from '../../vite-extensions/vite-global-extensions'; // eslint-disable-next-line @nx/enforce-module-boundaries import { getExternalModules } from '../../vite-extensions/vite-external-modules'; -export default defineConfig(() => { +export default defineConfig(({}) => { return { cacheDir: '../../../node_modules/.vite/php-wasm', @@ -28,93 +30,44 @@ export default defineConfig(() => { viteIgnoreImports({ extensions: ['wasm', 'so', 'dat'], }), - /** - * Vite can't extract static asset in the library mode: - * https://github.com/vitejs/vite/issues/3295 - * - * This workaround replaces the actual php_5_6.js modules paths used - * in the dev mode with their filenames. Then, the filenames are marked - * as external further down in this config. As a result, the final - * bundle contains literal `import('php_5_6.js')` and - * `import('php_5_6.wasm')` statements which allows the consumers to use - * their own loaders. - * - * This keeps the dev mode working AND avoids inlining 5mb of - * wasm via base64 in the final bundle. - */ - { - name: 'preserve-php-loaders-imports', - - resolveDynamicImport(specifier): string | void { - if ( - command === 'build' && - typeof specifier === 'string' && - specifier.match(/php_\d_\d\.js$/) - ) { - /** - * The ../ is weird but necessary to make the final build say - * import("./php/jspi/php_8_2.js") - * and not - * import("php/jspi/php_8_2.js") - * - * The slice(-3) will ensure the 'php/jspi/' - * portion of the path is preserved. - */ - return '../' + specifier.split('/').slice(-3).join('/'); - } + vitePreserveLoadersImports([ + { + regex: /php_\d_\d\.js$/, + /* + * ../ lifts the file up to the dist entryRoot + * web/src/lib/get-php-loader-module.ts > web/src/get-php-loader-module.ts + * + * slice(-3) strips the `public` directory from the path + * web/public/php/jspi/php_8_4.js > ./web/php/jspi/php_8_4.js + */ + transform: (specifier) => + `../${specifier.split('/').slice(-3).join('/')}`, }, - }, - { - name: 'preserve-data-loaders-imports', - - resolveDynamicImport(specifier): string | void { - if ( - command === 'build' && - typeof specifier === 'string' && - specifier.match(/icu\.dat$/) - ) { - /** - * The ../../../ is weird but necessary to make the final build say - * import("./shared/icu.dat") - * and not - * import("shared/icu.dat") - * - * The slice(-2) will ensure the 'shared/' - * portion of the path is preserved. - */ - return ( - '../../../' + - specifier.split('/').slice(-2).join('/') - ); - } + { + regex: /intl\.so$/, + /* + * ../../../ lifts the file up to the dist entryRoot + * web/src/lib/extensions/intl/get-intl-loader-module.ts > web/src/get-intl-loader-module.ts + * + * slice(-6) strips the `public` directory from the path + * web/public/php/jspi/extensions/8_4/intl/intl.so > ./web/php/jspi/extensions/8_4/intl/intl.so + */ + transform: (specifier) => + `../../../${specifier.split('/').slice(-6).join('/')}`, }, - }, - { - name: 'preserve-extension-loaders-imports', - - resolveDynamicImport(specifier): string | void { - if ( - command === 'build' && - typeof specifier === 'string' && - specifier.match(/intl\.so$/) - ) { - /** - * The ../../../ is weird but necessary to make the final build say - * import("./php/{mode}/extensions/intl/{php_version}/intl.so") - * and not - * import("php/{mode}/extensions/intl/{php_version}/intl.so") - * - * The slice(-6) will ensure the 'php/{mode}/extensions/intl/{php_version}' - * portion of the path is preserved. - */ - return ( - '../../../' + - specifier.split('/').slice(-6).join('/') - ); - } + { + regex: /icu\.dat$/, + /* + * ../../../ lifts the file up to the dist entryRoot + * web/src/lib/extensions/intl/with-intl.ts > web/src/with-intl.ts + * + * slice(-2) strips the `public` directory from the path + * web/public/shared/icu.dat > ./web/shared/icu.dat + */ + transform: (specifier) => + `../../../${specifier.split('/').slice(-2).join('/')}`, }, - }, - + ]), ...viteGlobalExtensions, ], diff --git a/packages/vite-extensions/vite-preserve-loaders-imports.ts b/packages/vite-extensions/vite-preserve-loaders-imports.ts new file mode 100644 index 0000000000..e6156bd7f9 --- /dev/null +++ b/packages/vite-extensions/vite-preserve-loaders-imports.ts @@ -0,0 +1,78 @@ +import type { Plugin } from 'vite'; + +export interface PreserveLoadersRule { + regex: RegExp; + transform: (specifier: string) => string; +} + +export function vitePreserveLoadersImports( + rules: PreserveLoadersRule[] +): Plugin { + let command: 'build' | 'serve'; + + const matchedRules = new Set(); + + return { + /** + * Vite can't extract static asset in the library mode: + * https://github.com/vitejs/vite/issues/3295 + * + * This workaround replaces the actual php.js modules paths used + * in the dev mode with their filenames. Then, the filenames are marked + * as external further down in this config. As a result, the final + * bundle contains literal `import('php.js')` and + * `import('php.wasm')` statements which allows the consumers to use + * their own loaders. + * + * This keeps the dev mode working AND avoids inlining 5mb of + * wasm via base64 in the final bundle. + */ + name: 'vite-preserve-loaders-imports', + + configResolved(config) { + command = config.command; + }, + + resolveDynamicImport(specifier) { + if (command !== 'build' || typeof specifier !== 'string') return; + + for (const rule of rules) { + if (new RegExp(rule.regex).test(specifier)) { + matchedRules.add(rule); + /** + * + * Example : transform: specifier => `../${specifier.split('/').slice(-3).join('/')}` + * + * The '../' is weird but necessary to make the final build say + * import("./php/jspi/php.js") + * and not + * import("php/jspi/php.js") + * + * The -3 will ensure the 'php/jspi/' + * portion of the path is preserved. + */ + return rule.transform(specifier); + } + } + + return null; + }, + + buildEnd() { + if (command !== 'build') return; + + const unusedRules = rules.filter((rule) => !matchedRules.has(rule)); + + if (unusedRules.length > 0) { + const details = unusedRules + .map((rule) => `- ${rule.regex}`) + .join('\n'); + + this.error( + `vite-preserve-loaders-imports: The following rules did not match any dynamic imports:\n${details}\n\n` + + `This is likely a misconfiguration or a stale regex.` + ); + } + }, + }; +} From 120ae0791777176db7a3ceb3ca6c864c6c6bcd9e Mon Sep 17 00:00:00 2001 From: = Date: Tue, 9 Dec 2025 15:48:13 +0100 Subject: [PATCH 4/7] Lint PHP.wasm Web vite config file --- packages/php-wasm/web/vite.config.ts | 162 +++++++++++++-------------- 1 file changed, 80 insertions(+), 82 deletions(-) diff --git a/packages/php-wasm/web/vite.config.ts b/packages/php-wasm/web/vite.config.ts index 9cd079f0f3..5ef6150569 100644 --- a/packages/php-wasm/web/vite.config.ts +++ b/packages/php-wasm/web/vite.config.ts @@ -14,91 +14,89 @@ import viteGlobalExtensions from '../../vite-extensions/vite-global-extensions'; // eslint-disable-next-line @nx/enforce-module-boundaries import { getExternalModules } from '../../vite-extensions/vite-external-modules'; -export default defineConfig(({}) => { - return { - cacheDir: '../../../node_modules/.vite/php-wasm', +export default defineConfig({ + cacheDir: '../../../node_modules/.vite/php-wasm', - plugins: [ - viteTsConfigPaths({ - root: '../../../', - }), - dts({ - entryRoot: 'src', - tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'), - pathsToAliases: false, - }), - viteIgnoreImports({ - extensions: ['wasm', 'so', 'dat'], - }), - vitePreserveLoadersImports([ - { - regex: /php_\d_\d\.js$/, - /* - * ../ lifts the file up to the dist entryRoot - * web/src/lib/get-php-loader-module.ts > web/src/get-php-loader-module.ts - * - * slice(-3) strips the `public` directory from the path - * web/public/php/jspi/php_8_4.js > ./web/php/jspi/php_8_4.js - */ - transform: (specifier) => - `../${specifier.split('/').slice(-3).join('/')}`, - }, - { - regex: /intl\.so$/, - /* - * ../../../ lifts the file up to the dist entryRoot - * web/src/lib/extensions/intl/get-intl-loader-module.ts > web/src/get-intl-loader-module.ts - * - * slice(-6) strips the `public` directory from the path - * web/public/php/jspi/extensions/8_4/intl/intl.so > ./web/php/jspi/extensions/8_4/intl/intl.so - */ - transform: (specifier) => - `../../../${specifier.split('/').slice(-6).join('/')}`, - }, - { - regex: /icu\.dat$/, - /* - * ../../../ lifts the file up to the dist entryRoot - * web/src/lib/extensions/intl/with-intl.ts > web/src/with-intl.ts - * - * slice(-2) strips the `public` directory from the path - * web/public/shared/icu.dat > ./web/shared/icu.dat - */ - transform: (specifier) => - `../../../${specifier.split('/').slice(-2).join('/')}`, - }, - ]), - ...viteGlobalExtensions, - ], - - // Configuration for building your library. - // See: https://vitejs.dev/guide/build.html#library-mode - build: { - lib: { - // Could also be a dictionary or array of multiple entry points. - entry: 'src/index.ts', - name: 'php-wasm-web', - fileName: 'index', - formats: ['es', 'cjs'], + plugins: [ + viteTsConfigPaths({ + root: '../../../', + }), + dts({ + entryRoot: 'src', + tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'), + pathsToAliases: false, + }), + viteIgnoreImports({ + extensions: ['wasm', 'so', 'dat'], + }), + vitePreserveLoadersImports([ + { + regex: /php_\d_\d\.js$/, + /* + * ../ lifts the import located file up to the dist entryRoot + * web/src/lib/get-php-loader-module.ts > web/src/get-php-loader-module.ts + * + * slice(-3) strips the `public` directory from the path + * web/public/php/jspi/php_8_4.js > ./web/php/jspi/php_8_4.js + */ + transform: (specifier) => + `../${specifier.split('/').slice(-3).join('/')}`, }, - sourcemap: true, - rollupOptions: { - // Don't bundle the PHP loaders in the final build. See - // the preserve-php-loaders-imports plugin above. - external: [ - /php_\d_\d.js$/, - /icu.dat$/, - /intl.so$/, - ...getExternalModules(), - ], + { + regex: /intl\.so$/, + /* + * ../../../ lifts the import located file to the dist entryRoot + * web/src/lib/extensions/intl/get-intl-loader-module.ts > web/src/get-intl-loader-module.ts + * + * slice(-6) strips the `public` directory from the path + * web/public/php/jspi/extensions/8_4/intl/intl.so > ./web/php/jspi/extensions/8_4/intl/intl.so + */ + transform: (specifier) => + `../../../${specifier.split('/').slice(-6).join('/')}`, }, - }, + { + regex: /icu\.dat$/, + /* + * ../../../ lifts the import located file to the dist entryRoot + * web/src/lib/extensions/intl/with-intl.ts > web/src/with-intl.ts + * + * slice(-2) strips the `public` directory from the path + * web/public/shared/icu.dat > ./web/shared/icu.dat + */ + transform: (specifier) => + `../../../${specifier.split('/').slice(-2).join('/')}`, + }, + ]), + ...viteGlobalExtensions, + ], - // TODO : move Vitest tests to Playwright tests inside test directory - test: { - globals: true, - environment: 'node', - reporters: ['default'], + // Configuration for building your library. + // See: https://vitejs.dev/guide/build.html#library-mode + build: { + lib: { + // Could also be a dictionary or array of multiple entry points. + entry: 'src/index.ts', + name: 'php-wasm-web', + fileName: 'index', + formats: ['es', 'cjs'], }, - }; + sourcemap: true, + rollupOptions: { + // Don't bundle the PHP loaders in the final build. See + // the preserve-php-loaders-imports plugin above. + external: [ + /php_\d_\d.js$/, + /icu.dat$/, + /intl.so$/, + ...getExternalModules(), + ], + }, + }, + + // TODO : move Vitest tests to Playwright tests inside test directory + test: { + globals: true, + environment: 'node', + reporters: ['default'], + }, }); From 80e28d8be411d393165242918bfa9ea2e6799666 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 9 Dec 2025 16:06:34 +0100 Subject: [PATCH 5/7] Lint PHP.wasm Web vite config file --- packages/php-wasm/web/vite.playwright.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/php-wasm/web/vite.playwright.config.ts b/packages/php-wasm/web/vite.playwright.config.ts index 5424a9e86a..989e01ac3a 100644 --- a/packages/php-wasm/web/vite.playwright.config.ts +++ b/packages/php-wasm/web/vite.playwright.config.ts @@ -1,9 +1,9 @@ import { defineConfig, mergeConfig } from 'vite'; import config from './vite.config'; -export default defineConfig((env) => +export default defineConfig(() => mergeConfig( - config(env), + config, defineConfig({ assetsInclude: ['**/*.wasm', '**/*.so', '**/*.dat'], From 4787ad32c983db042a317f4e848b9d5f4baee51b Mon Sep 17 00:00:00 2001 From: = Date: Wed, 10 Dec 2025 08:44:31 +0100 Subject: [PATCH 6/7] Correct and improve comments --- packages/php-wasm/web/vite.config.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/php-wasm/web/vite.config.ts b/packages/php-wasm/web/vite.config.ts index 5ef6150569..54826360f3 100644 --- a/packages/php-wasm/web/vite.config.ts +++ b/packages/php-wasm/web/vite.config.ts @@ -37,7 +37,10 @@ export default defineConfig({ * web/src/lib/get-php-loader-module.ts > web/src/get-php-loader-module.ts * * slice(-3) strips the `public` directory from the path - * web/public/php/jspi/php_8_4.js > ./web/php/jspi/php_8_4.js + * web/public/php/jspi/php_8_4.js > php/jspi/php_8_4.js + * + * yielding + * ../../public/php/jspi/php_8_4.js > ./php/jspi/php_8_4.js */ transform: (specifier) => `../${specifier.split('/').slice(-3).join('/')}`, @@ -46,10 +49,13 @@ export default defineConfig({ regex: /intl\.so$/, /* * ../../../ lifts the import located file to the dist entryRoot - * web/src/lib/extensions/intl/get-intl-loader-module.ts > web/src/get-intl-loader-module.ts + * web/src/lib/extensions/intl/get-intl-extension-module.ts > web/src/get-intl-extension-module.ts * * slice(-6) strips the `public` directory from the path - * web/public/php/jspi/extensions/8_4/intl/intl.so > ./web/php/jspi/extensions/8_4/intl/intl.so + * web/public/php/jspi/extensions/8_4/intl/intl.so > php/jspi/extensions/8_4/intl/intl.so + * + * yielding + * ../../../../public/php/jspi/extensions/intl/8_4/intl.so > ./php/jspi/extensions/intl/8_4/intl.so */ transform: (specifier) => `../../../${specifier.split('/').slice(-6).join('/')}`, @@ -61,7 +67,10 @@ export default defineConfig({ * web/src/lib/extensions/intl/with-intl.ts > web/src/with-intl.ts * * slice(-2) strips the `public` directory from the path - * web/public/shared/icu.dat > ./web/shared/icu.dat + * web/public/shared/icu.dat > shared/icu.dat + * + * yielding + * ../../../../public/shared/icu.dat > ./shared/icu.dat */ transform: (specifier) => `../../../${specifier.split('/').slice(-2).join('/')}`, From 275c78445aae58cb4291b35d75268909f0e18479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 10 Dec 2025 18:16:12 +0100 Subject: [PATCH 7/7] Rename the plugin, update docstrings --- packages/php-wasm/web/vite.config.ts | 70 +++++++++-------- .../vite-external-dynamic-imports.ts | 71 +++++++++++++++++ .../vite-preserve-loaders-imports.ts | 78 ------------------- 3 files changed, 108 insertions(+), 111 deletions(-) create mode 100644 packages/vite-extensions/vite-external-dynamic-imports.ts delete mode 100644 packages/vite-extensions/vite-preserve-loaders-imports.ts diff --git a/packages/php-wasm/web/vite.config.ts b/packages/php-wasm/web/vite.config.ts index 54826360f3..f67b2014d6 100644 --- a/packages/php-wasm/web/vite.config.ts +++ b/packages/php-wasm/web/vite.config.ts @@ -8,7 +8,7 @@ import { viteTsConfigPaths } from '../../vite-extensions/vite-ts-config-paths'; // eslint-disable-next-line @nx/enforce-module-boundaries import { viteIgnoreImports } from '../../vite-extensions/vite-ignore-imports'; // eslint-disable-next-line @nx/enforce-module-boundaries -import { vitePreserveLoadersImports } from '../../vite-extensions/vite-preserve-loaders-imports'; +import { viteExternalDynamicImports } from '../../vite-extensions/vite-external-dynamic-imports'; // eslint-disable-next-line @nx/enforce-module-boundaries import viteGlobalExtensions from '../../vite-extensions/vite-global-extensions'; // eslint-disable-next-line @nx/enforce-module-boundaries @@ -29,49 +29,53 @@ export default defineConfig({ viteIgnoreImports({ extensions: ['wasm', 'so', 'dat'], }), - vitePreserveLoadersImports([ + /* + * These transforms rewrite dynamic import paths so they work from the dist output. + * + * Each transform does two things: + * 1. slice(-N) extracts the path segments we want to keep (strips the 'public' prefix) + * 2. The '../' prefix compensates for the source file's directory depth + * + * Why the '../' prefix? Rollup computes the final import path relative to + * where the source file was located. Since everything gets bundled into + * index.js at the dist root, we need to "climb out" of the source directory + * structure. Rollup then normalizes '../foo' to './foo' in the output. + * + * Example for php_8_4.js: + * Source file: src/lib/get-php-loader-module.ts (2 levels deep: src/lib/) + * Input: '../../public/php/jspi/php_8_4.js' + * slice(-3): 'php/jspi/php_8_4.js' + * With '../': '../php/jspi/php_8_4.js' + * Output: './php/jspi/php_8_4.js' (rollup normalizes for dist root) + */ + viteExternalDynamicImports([ { + // Source: src/lib/get-php-loader-module.ts (1 dir from src/) + // Input: '../../public/php/jspi/php_8_4.js' + // slice(-3): 'php/jspi/php_8_4.js' + // With '../': '../php/jspi/php_8_4.js' + // Output: './php/jspi/php_8_4.js' regex: /php_\d_\d\.js$/, - /* - * ../ lifts the import located file up to the dist entryRoot - * web/src/lib/get-php-loader-module.ts > web/src/get-php-loader-module.ts - * - * slice(-3) strips the `public` directory from the path - * web/public/php/jspi/php_8_4.js > php/jspi/php_8_4.js - * - * yielding - * ../../public/php/jspi/php_8_4.js > ./php/jspi/php_8_4.js - */ transform: (specifier) => `../${specifier.split('/').slice(-3).join('/')}`, }, { + // Source: src/lib/extensions/intl/get-intl-extension-module.ts (3 dirs from src/) + // Input: '../../../../public/php/jspi/extensions/intl/8_4/intl.so' + // slice(-6): 'php/jspi/extensions/intl/8_4/intl.so' + // With '../../../': '../../../php/jspi/extensions/intl/8_4/intl.so' + // Output: './php/jspi/extensions/intl/8_4/intl.so' regex: /intl\.so$/, - /* - * ../../../ lifts the import located file to the dist entryRoot - * web/src/lib/extensions/intl/get-intl-extension-module.ts > web/src/get-intl-extension-module.ts - * - * slice(-6) strips the `public` directory from the path - * web/public/php/jspi/extensions/8_4/intl/intl.so > php/jspi/extensions/8_4/intl/intl.so - * - * yielding - * ../../../../public/php/jspi/extensions/intl/8_4/intl.so > ./php/jspi/extensions/intl/8_4/intl.so - */ transform: (specifier) => `../../../${specifier.split('/').slice(-6).join('/')}`, }, { + // Source: src/lib/extensions/intl/with-intl.ts (3 dirs from src/) + // Input: '../../../../public/shared/icu.dat' + // slice(-2): 'shared/icu.dat' + // With '../../../': '../../../shared/icu.dat' + // Output: './shared/icu.dat' regex: /icu\.dat$/, - /* - * ../../../ lifts the import located file to the dist entryRoot - * web/src/lib/extensions/intl/with-intl.ts > web/src/with-intl.ts - * - * slice(-2) strips the `public` directory from the path - * web/public/shared/icu.dat > shared/icu.dat - * - * yielding - * ../../../../public/shared/icu.dat > ./shared/icu.dat - */ transform: (specifier) => `../../../${specifier.split('/').slice(-2).join('/')}`, }, @@ -92,7 +96,7 @@ export default defineConfig({ sourcemap: true, rollupOptions: { // Don't bundle the PHP loaders in the final build. See - // the preserve-php-loaders-imports plugin above. + // the viteExternalDynamicImports plugin above. external: [ /php_\d_\d.js$/, /icu.dat$/, diff --git a/packages/vite-extensions/vite-external-dynamic-imports.ts b/packages/vite-extensions/vite-external-dynamic-imports.ts new file mode 100644 index 0000000000..dd5c56cd73 --- /dev/null +++ b/packages/vite-extensions/vite-external-dynamic-imports.ts @@ -0,0 +1,71 @@ +import type { Plugin } from 'vite'; + +export interface ExternalDynamicImportRule { + regex: RegExp; + transform: (specifier: string) => string; +} + +/** + * Rewrites dynamic import paths so they resolve correctly from the dist output. + * + * Vite can't extract static assets in library mode (https://github.com/vitejs/vite/issues/3295). + * Without this plugin, dynamic imports like `import('../../public/php/jspi/php_8_4.js')` + * would either be bundled (inlining 5MB+ of WASM as base64) or break entirely. + * + * This plugin works together with rollup's `external` option: + * 1. This plugin rewrites the import paths to be relative to the dist output location + * 2. The `external` option marks these imports as external so they're preserved as + * literal `import()` statements in the bundle + * + * The result is that the final bundle contains imports like `import('./php/jspi/php_8_4.js')` + * which allows consumers to provide their own loaders for these files. + */ +export function viteExternalDynamicImports( + rules: ExternalDynamicImportRule[] +): Plugin { + let command: 'build' | 'serve'; + + const matchedRules = new Set(); + + return { + name: 'vite-external-dynamic-imports', + + configResolved(config) { + command = config.command; + }, + + resolveDynamicImport(specifier) { + if (command !== 'build' || typeof specifier !== 'string') return; + + for (const rule of rules) { + if (new RegExp(rule.regex).test(specifier)) { + matchedRules.add(rule); + return rule.transform(specifier); + } + } + + return null; + }, + + buildEnd() { + if (command !== 'build') return; + + const unusedRules = rules.filter((rule) => !matchedRules.has(rule)); + + if (unusedRules.length > 0) { + const details = unusedRules + .map((rule) => `- ${rule.regex}`) + .join('\n'); + + this.error( + `vite-external-dynamic-imports: The following rules did not match any dynamic imports:\n${details}\n\n` + + `This is likely a misconfiguration or a stale regex.` + ); + } + }, + }; +} + +// Backwards compatibility alias +export const vitePreserveLoadersImports = viteExternalDynamicImports; +export type PreserveLoadersRule = ExternalDynamicImportRule; diff --git a/packages/vite-extensions/vite-preserve-loaders-imports.ts b/packages/vite-extensions/vite-preserve-loaders-imports.ts deleted file mode 100644 index e6156bd7f9..0000000000 --- a/packages/vite-extensions/vite-preserve-loaders-imports.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { Plugin } from 'vite'; - -export interface PreserveLoadersRule { - regex: RegExp; - transform: (specifier: string) => string; -} - -export function vitePreserveLoadersImports( - rules: PreserveLoadersRule[] -): Plugin { - let command: 'build' | 'serve'; - - const matchedRules = new Set(); - - return { - /** - * Vite can't extract static asset in the library mode: - * https://github.com/vitejs/vite/issues/3295 - * - * This workaround replaces the actual php.js modules paths used - * in the dev mode with their filenames. Then, the filenames are marked - * as external further down in this config. As a result, the final - * bundle contains literal `import('php.js')` and - * `import('php.wasm')` statements which allows the consumers to use - * their own loaders. - * - * This keeps the dev mode working AND avoids inlining 5mb of - * wasm via base64 in the final bundle. - */ - name: 'vite-preserve-loaders-imports', - - configResolved(config) { - command = config.command; - }, - - resolveDynamicImport(specifier) { - if (command !== 'build' || typeof specifier !== 'string') return; - - for (const rule of rules) { - if (new RegExp(rule.regex).test(specifier)) { - matchedRules.add(rule); - /** - * - * Example : transform: specifier => `../${specifier.split('/').slice(-3).join('/')}` - * - * The '../' is weird but necessary to make the final build say - * import("./php/jspi/php.js") - * and not - * import("php/jspi/php.js") - * - * The -3 will ensure the 'php/jspi/' - * portion of the path is preserved. - */ - return rule.transform(specifier); - } - } - - return null; - }, - - buildEnd() { - if (command !== 'build') return; - - const unusedRules = rules.filter((rule) => !matchedRules.has(rule)); - - if (unusedRules.length > 0) { - const details = unusedRules - .map((rule) => `- ${rule.regex}`) - .join('\n'); - - this.error( - `vite-preserve-loaders-imports: The following rules did not match any dynamic imports:\n${details}\n\n` + - `This is likely a misconfiguration or a stale regex.` - ); - } - }, - }; -}