Skip to content

Commit 42ea377

Browse files
authored
[ Vite ] Regroup preserve-*-loaders-imports Vite plugin inside a vite-extension (#3002)
1 parent 517cbfe commit 42ea377

File tree

4 files changed

+163
-130
lines changed

4 files changed

+163
-130
lines changed

packages/php-wasm/node/vite.config.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ export default defineConfig(function () {
7171
},
7272
sourcemap: true,
7373
rollupOptions: {
74-
// Don't bundle the PHP loaders in the final build. See
75-
// the preserve-php-loaders-imports plugin above.
7674
external: getExternalModules(),
7775
output: {
7876
entryFileNames: '[name].js',

packages/php-wasm/web/vite.config.ts

Lines changed: 90 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -8,144 +8,108 @@ import { viteTsConfigPaths } from '../../vite-extensions/vite-ts-config-paths';
88
// eslint-disable-next-line @nx/enforce-module-boundaries
99
import { viteIgnoreImports } from '../../vite-extensions/vite-ignore-imports';
1010
// eslint-disable-next-line @nx/enforce-module-boundaries
11+
import { viteExternalDynamicImports } from '../../vite-extensions/vite-external-dynamic-imports';
12+
// eslint-disable-next-line @nx/enforce-module-boundaries
1113
import viteGlobalExtensions from '../../vite-extensions/vite-global-extensions';
1214
// eslint-disable-next-line @nx/enforce-module-boundaries
1315
import { getExternalModules } from '../../vite-extensions/vite-external-modules';
1416

15-
export default defineConfig(({ command }) => {
16-
return {
17-
cacheDir: '../../../node_modules/.vite/php-wasm',
17+
export default defineConfig({
18+
cacheDir: '../../../node_modules/.vite/php-wasm',
1819

19-
plugins: [
20-
viteTsConfigPaths({
21-
root: '../../../',
22-
}),
23-
dts({
24-
entryRoot: 'src',
25-
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
26-
pathsToAliases: false,
27-
}),
28-
viteIgnoreImports({
29-
extensions: ['wasm', 'so', 'dat'],
30-
}),
31-
/**
32-
* Vite can't extract static asset in the library mode:
33-
* https://github.com/vitejs/vite/issues/3295
34-
*
35-
* This workaround replaces the actual php_5_6.js modules paths used
36-
* in the dev mode with their filenames. Then, the filenames are marked
37-
* as external further down in this config. As a result, the final
38-
* bundle contains literal `import('php_5_6.js')` and
39-
* `import('php_5_6.wasm')` statements which allows the consumers to use
40-
* their own loaders.
41-
*
42-
* This keeps the dev mode working AND avoids inlining 5mb of
43-
* wasm via base64 in the final bundle.
44-
*/
20+
plugins: [
21+
viteTsConfigPaths({
22+
root: '../../../',
23+
}),
24+
dts({
25+
entryRoot: 'src',
26+
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
27+
pathsToAliases: false,
28+
}),
29+
viteIgnoreImports({
30+
extensions: ['wasm', 'so', 'dat'],
31+
}),
32+
/*
33+
* These transforms rewrite dynamic import paths so they work from the dist output.
34+
*
35+
* Each transform does two things:
36+
* 1. slice(-N) extracts the path segments we want to keep (strips the 'public' prefix)
37+
* 2. The '../' prefix compensates for the source file's directory depth
38+
*
39+
* Why the '../' prefix? Rollup computes the final import path relative to
40+
* where the source file was located. Since everything gets bundled into
41+
* index.js at the dist root, we need to "climb out" of the source directory
42+
* structure. Rollup then normalizes '../foo' to './foo' in the output.
43+
*
44+
* Example for php_8_4.js:
45+
* Source file: src/lib/get-php-loader-module.ts (2 levels deep: src/lib/)
46+
* Input: '../../public/php/jspi/php_8_4.js'
47+
* slice(-3): 'php/jspi/php_8_4.js'
48+
* With '../': '../php/jspi/php_8_4.js'
49+
* Output: './php/jspi/php_8_4.js' (rollup normalizes for dist root)
50+
*/
51+
viteExternalDynamicImports([
4552
{
46-
name: 'preserve-php-loaders-imports',
47-
48-
resolveDynamicImport(specifier): string | void {
49-
if (
50-
command === 'build' &&
51-
typeof specifier === 'string' &&
52-
specifier.match(/php_\d_\d\.js$/)
53-
) {
54-
/**
55-
* The ../ is weird but necessary to make the final build say
56-
* import("./php/jspi/php_8_2.js")
57-
* and not
58-
* import("php/jspi/php_8_2.js")
59-
*
60-
* The slice(-3) will ensure the 'php/jspi/'
61-
* portion of the path is preserved.
62-
*/
63-
return '../' + specifier.split('/').slice(-3).join('/');
64-
}
65-
},
53+
// Source: src/lib/get-php-loader-module.ts (1 dir from src/)
54+
// Input: '../../public/php/jspi/php_8_4.js'
55+
// slice(-3): 'php/jspi/php_8_4.js'
56+
// With '../': '../php/jspi/php_8_4.js'
57+
// Output: './php/jspi/php_8_4.js'
58+
regex: /php_\d_\d\.js$/,
59+
transform: (specifier) =>
60+
`../${specifier.split('/').slice(-3).join('/')}`,
6661
},
6762
{
68-
name: 'preserve-data-loaders-imports',
69-
70-
resolveDynamicImport(specifier): string | void {
71-
if (
72-
command === 'build' &&
73-
typeof specifier === 'string' &&
74-
specifier.match(/icu\.dat$/)
75-
) {
76-
/**
77-
* The ../../../ is weird but necessary to make the final build say
78-
* import("./shared/icu.dat")
79-
* and not
80-
* import("shared/icu.dat")
81-
*
82-
* The slice(-2) will ensure the 'shared/'
83-
* portion of the path is preserved.
84-
*/
85-
return (
86-
'../../../' +
87-
specifier.split('/').slice(-2).join('/')
88-
);
89-
}
90-
},
63+
// Source: src/lib/extensions/intl/get-intl-extension-module.ts (3 dirs from src/)
64+
// Input: '../../../../public/php/jspi/extensions/intl/8_4/intl.so'
65+
// slice(-6): 'php/jspi/extensions/intl/8_4/intl.so'
66+
// With '../../../': '../../../php/jspi/extensions/intl/8_4/intl.so'
67+
// Output: './php/jspi/extensions/intl/8_4/intl.so'
68+
regex: /intl\.so$/,
69+
transform: (specifier) =>
70+
`../../../${specifier.split('/').slice(-6).join('/')}`,
9171
},
9272
{
93-
name: 'preserve-extension-loaders-imports',
94-
95-
resolveDynamicImport(specifier): string | void {
96-
if (
97-
command === 'build' &&
98-
typeof specifier === 'string' &&
99-
specifier.match(/intl\.so$/)
100-
) {
101-
/**
102-
* The ../../../ is weird but necessary to make the final build say
103-
* import("./php/{mode}/extensions/intl/{php_version}/intl.so")
104-
* and not
105-
* import("php/{mode}/extensions/intl/{php_version}/intl.so")
106-
*
107-
* The slice(-6) will ensure the 'php/{mode}/extensions/intl/{php_version}'
108-
* portion of the path is preserved.
109-
*/
110-
return (
111-
'../../../' +
112-
specifier.split('/').slice(-6).join('/')
113-
);
114-
}
115-
},
73+
// Source: src/lib/extensions/intl/with-intl.ts (3 dirs from src/)
74+
// Input: '../../../../public/shared/icu.dat'
75+
// slice(-2): 'shared/icu.dat'
76+
// With '../../../': '../../../shared/icu.dat'
77+
// Output: './shared/icu.dat'
78+
regex: /icu\.dat$/,
79+
transform: (specifier) =>
80+
`../../../${specifier.split('/').slice(-2).join('/')}`,
11681
},
82+
]),
83+
...viteGlobalExtensions,
84+
],
11785

118-
...viteGlobalExtensions,
119-
],
120-
121-
// Configuration for building your library.
122-
// See: https://vitejs.dev/guide/build.html#library-mode
123-
build: {
124-
lib: {
125-
// Could also be a dictionary or array of multiple entry points.
126-
entry: 'src/index.ts',
127-
name: 'php-wasm-web',
128-
fileName: 'index',
129-
formats: ['es', 'cjs'],
130-
},
131-
sourcemap: true,
132-
rollupOptions: {
133-
// Don't bundle the PHP loaders in the final build. See
134-
// the preserve-php-loaders-imports plugin above.
135-
external: [
136-
/php_\d_\d.js$/,
137-
/icu.dat$/,
138-
/intl.so$/,
139-
...getExternalModules(),
140-
],
141-
},
86+
// Configuration for building your library.
87+
// See: https://vitejs.dev/guide/build.html#library-mode
88+
build: {
89+
lib: {
90+
// Could also be a dictionary or array of multiple entry points.
91+
entry: 'src/index.ts',
92+
name: 'php-wasm-web',
93+
fileName: 'index',
94+
formats: ['es', 'cjs'],
14295
},
143-
144-
// TODO : move Vitest tests to Playwright tests inside test directory
145-
test: {
146-
globals: true,
147-
environment: 'node',
148-
reporters: ['default'],
96+
sourcemap: true,
97+
rollupOptions: {
98+
// Don't bundle the PHP loaders in the final build. See
99+
// the viteExternalDynamicImports plugin above.
100+
external: [
101+
/php_\d_\d.js$/,
102+
/icu.dat$/,
103+
/intl.so$/,
104+
...getExternalModules(),
105+
],
149106
},
150-
};
107+
},
108+
109+
// TODO : move Vitest tests to Playwright tests inside test directory
110+
test: {
111+
globals: true,
112+
environment: 'node',
113+
reporters: ['default'],
114+
},
151115
});

packages/php-wasm/web/vite.playwright.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { defineConfig, mergeConfig } from 'vite';
22
import config from './vite.config';
33

4-
export default defineConfig((env) =>
4+
export default defineConfig(() =>
55
mergeConfig(
6-
config(env),
6+
config,
77
defineConfig({
88
assetsInclude: ['**/*.wasm', '**/*.so', '**/*.dat'],
99

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { Plugin } from 'vite';
2+
3+
export interface ExternalDynamicImportRule {
4+
regex: RegExp;
5+
transform: (specifier: string) => string;
6+
}
7+
8+
/**
9+
* Rewrites dynamic import paths so they resolve correctly from the dist output.
10+
*
11+
* Vite can't extract static assets in library mode (https://github.com/vitejs/vite/issues/3295).
12+
* Without this plugin, dynamic imports like `import('../../public/php/jspi/php_8_4.js')`
13+
* would either be bundled (inlining 5MB+ of WASM as base64) or break entirely.
14+
*
15+
* This plugin works together with rollup's `external` option:
16+
* 1. This plugin rewrites the import paths to be relative to the dist output location
17+
* 2. The `external` option marks these imports as external so they're preserved as
18+
* literal `import()` statements in the bundle
19+
*
20+
* The result is that the final bundle contains imports like `import('./php/jspi/php_8_4.js')`
21+
* which allows consumers to provide their own loaders for these files.
22+
*/
23+
export function viteExternalDynamicImports(
24+
rules: ExternalDynamicImportRule[]
25+
): Plugin {
26+
let command: 'build' | 'serve';
27+
28+
const matchedRules = new Set<ExternalDynamicImportRule>();
29+
30+
return {
31+
name: 'vite-external-dynamic-imports',
32+
33+
configResolved(config) {
34+
command = config.command;
35+
},
36+
37+
resolveDynamicImport(specifier) {
38+
if (command !== 'build' || typeof specifier !== 'string') return;
39+
40+
for (const rule of rules) {
41+
if (new RegExp(rule.regex).test(specifier)) {
42+
matchedRules.add(rule);
43+
return rule.transform(specifier);
44+
}
45+
}
46+
47+
return null;
48+
},
49+
50+
buildEnd() {
51+
if (command !== 'build') return;
52+
53+
const unusedRules = rules.filter((rule) => !matchedRules.has(rule));
54+
55+
if (unusedRules.length > 0) {
56+
const details = unusedRules
57+
.map((rule) => `- ${rule.regex}`)
58+
.join('\n');
59+
60+
this.error(
61+
`vite-external-dynamic-imports: The following rules did not match any dynamic imports:\n${details}\n\n` +
62+
`This is likely a misconfiguration or a stale regex.`
63+
);
64+
}
65+
},
66+
};
67+
}
68+
69+
// Backwards compatibility alias
70+
export const vitePreserveLoadersImports = viteExternalDynamicImports;
71+
export type PreserveLoadersRule = ExternalDynamicImportRule;

0 commit comments

Comments
 (0)