Skip to content

Commit b5dd8e2

Browse files
authored
fix(@angular/build): enable oxc WASI fallback in WebContainer environments
In WebContainer environments (such as StackBlitz and angular.dev playground), native Node.js addons cannot run, requiring oxc-parser to fall back to WebAssembly (WASI).
1 parent 0d622f0 commit b5dd8e2

7 files changed

Lines changed: 129 additions & 27 deletions

File tree

‎packages/angular/build/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"xxhash-wasm": "1.1.0"
4848
},
4949
"optionalDependencies": {
50-
"@oxc-parser/binding-wasm32-wasi": "0.150.0",
50+
"@oxc-parser/binding-wasm32-wasi": "0.156.0",
5151
"lmdb": "3.5.6"
5252
},
5353
"devDependencies": {

‎packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import '../oxc/oxc-wasi-shim';
910
import { type DecodedSourceMap, type SourceMapInput } from '@ampproject/remapping';
1011
import {
1112
type ɵParsedTranslation,

‎packages/angular/build/src/tools/oxc/oxc-transform.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import './oxc-wasi-shim';
910
import type { DecodedSourceMap } from '@ampproject/remapping';
1011
import { needsLinking } from '@angular/compiler-cli/linker';
1112
import type { BindingIdentifier, Class, Node } from '@oxc-project/types';
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { Module, createRequire } from 'node:module';
10+
11+
// Workaround for an upstream incompatibility in `@oxc-parser/binding-wasm32-wasi` when
12+
// falling back to WebAssembly (e.g., inside WebContainers or StackBlitz).
13+
//
14+
// `@oxc-parser/binding-wasm32-wasi` declares a dependency on `@emnapi/core@2.0.0-alpha.6`,
15+
// but depends on `@napi-rs/wasm-runtime@~1.2.6` which expects `@emnapi/core@^1.7.1 || 2.0.0-beta.2`.
16+
// 1. In `@napi-rs/wasm-runtime@1.2.6`, the runtime attempts to assign `initWorker` to
17+
// `emnapi_shared.addonModule`, which is undefined in `alpha.6` where the property was named
18+
// `napiModule`. Aliasing `addonModule` to `this.napiModule` on `Object.prototype` allows the
19+
// WASI runtime to initialize successfully.
20+
// 2. Under npm, `@napi-rs/wasm-runtime@1.2.6`'s peerDependencies cause npm to hoist `@emnapi/core@1.x`
21+
// to top-level `node_modules` while nesting `2.0.0-alpha.6` under `@oxc-parser/binding-wasm32-wasi`,
22+
// causing an ABI mismatch (`this.bridge.setLastError is not a function`). Under pnpm, `oxc-parser`
23+
// does not declare `@oxc-parser/binding-wasm32-wasi` in its own dependencies. Resolving these
24+
// packages relative to `@angular/build` and `@oxc-parser/binding-wasm32-wasi` ensures consistent
25+
// module resolution across package managers.
26+
//
27+
// See: https://github.com/oxc-project/oxc/issues/28717 and https://github.com/napi-rs/napi-rs/issues/3674
28+
const forceWasi = globalThis.process?.env?.['NAPI_RS_FORCE_WASI'];
29+
if (
30+
(globalThis.process?.versions?.['webcontainer'] ||
31+
forceWasi === 'true' ||
32+
forceWasi === 'error') &&
33+
!('addonModule' in Object.prototype)
34+
) {
35+
Object.defineProperty(Object.prototype, 'addonModule', {
36+
get() {
37+
return (this as { napiModule?: unknown }).napiModule;
38+
},
39+
set(value) {
40+
if (this && (typeof this === 'object' || typeof this === 'function')) {
41+
Object.defineProperty(this, 'addonModule', {
42+
value,
43+
writable: true,
44+
configurable: true,
45+
enumerable: true,
46+
});
47+
}
48+
},
49+
configurable: true,
50+
enumerable: false,
51+
});
52+
53+
const resolvedModules = new Map<string, string>();
54+
try {
55+
const wasiBindingPath = require.resolve('@oxc-parser/binding-wasm32-wasi');
56+
resolvedModules.set('@oxc-parser/binding-wasm32-wasi', wasiBindingPath);
57+
const wasiRequire = createRequire(wasiBindingPath);
58+
resolvedModules.set('@emnapi/core', wasiRequire.resolve('@emnapi/core'));
59+
resolvedModules.set('@emnapi/runtime', wasiRequire.resolve('@emnapi/runtime'));
60+
} catch {}
61+
62+
if (resolvedModules.size > 0) {
63+
const moduleWithResolve = Module as typeof Module & {
64+
_resolveFilename: (
65+
request: string,
66+
parent: unknown,
67+
isMain: boolean,
68+
options: unknown,
69+
) => string;
70+
};
71+
const origResolveFilename = moduleWithResolve._resolveFilename;
72+
moduleWithResolve._resolveFilename = function (request, parent, isMain, options) {
73+
const resolved = resolvedModules.get(request);
74+
if (resolved !== undefined) {
75+
return resolved;
76+
}
77+
78+
return origResolveFilename.call(this, request, parent, isMain, options);
79+
};
80+
}
81+
}

‎packages/angular/build/src/tools/oxc/traversal.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import './oxc-wasi-shim';
910
import type { Node } from '@oxc-project/types';
1011
import { visitorKeys } from 'oxc-parser';
1112

‎pnpm-lock.yaml‎

Lines changed: 21 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tests/e2e/tests/vite/wasi.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import assert from 'node:assert';
2+
import { getGlobalVariable } from '../../utils/env';
3+
import { ng } from '../../utils/process';
4+
5+
export default async function () {
6+
assert(
7+
getGlobalVariable('argv')['esbuild'],
8+
'This test should not be called in the Webpack suite.',
9+
);
10+
11+
const originalForceWasi = process.env['NAPI_RS_FORCE_WASI'];
12+
process.env['NAPI_RS_FORCE_WASI'] = 'error';
13+
14+
try {
15+
await ng('build');
16+
} finally {
17+
if (originalForceWasi === undefined) {
18+
delete process.env['NAPI_RS_FORCE_WASI'];
19+
} else {
20+
process.env['NAPI_RS_FORCE_WASI'] = originalForceWasi;
21+
}
22+
}
23+
}

0 commit comments

Comments
 (0)