Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions packages/php-wasm/node/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
216 changes: 90 additions & 126 deletions packages/php-wasm/web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,144 +8,108 @@ 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 { 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
import { getExternalModules } from '../../vite-extensions/vite-external-modules';

export default defineConfig(({ command }) => {
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'],
}),
/**
* 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.
*/
plugins: [
viteTsConfigPaths({
root: '../../../',
}),
dts({
entryRoot: 'src',
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
pathsToAliases: false,
}),
viteIgnoreImports({
extensions: ['wasm', 'so', 'dat'],
}),
/*
* 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([
{
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('/');
}
},
// 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$/,
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('/')
);
}
},
// 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$/,
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('/')
);
}
},
// 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$/,
transform: (specifier) =>
`../../../${specifier.split('/').slice(-2).join('/')}`,
},
]),
...viteGlobalExtensions,
],

...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'],
},
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(),
],
},
// 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'],
},

// TODO : move Vitest tests to Playwright tests inside test directory
test: {
globals: true,
environment: 'node',
reporters: ['default'],
sourcemap: true,
rollupOptions: {
// Don't bundle the PHP loaders in the final build. See
// the viteExternalDynamicImports 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'],
},
});
4 changes: 2 additions & 2 deletions packages/php-wasm/web/vite.playwright.config.ts
Original file line number Diff line number Diff line change
@@ -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'],

Expand Down
71 changes: 71 additions & 0 deletions packages/vite-extensions/vite-external-dynamic-imports.ts
Original file line number Diff line number Diff line change
@@ -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<ExternalDynamicImportRule>();

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;