Repository navigation
Conversation
…nments PR angular#34540 declared `@oxc-parser/binding-wasm32-wasi` as an optional dependency of `@angular/build` to avoid runtime package manager execution inside WebContainer environments. However, upstream WASI bindings for `oxc-parser` currently fail to initialize due to a version and ABI incompatibility between `@napi-rs/wasm-runtime@1.2.6` and `@emnapi/core` (napi-rs/napi-rs#3674). When initialization fails, `oxc-parser` falls back to shelling out to `pnpm i` in `/tmp` from inside the Piscina worker thread, which surfaces as a fatal `ERR_REQUIRE_CYCLE_MODULE` error and crashes `ng serve` and `ng build`. Furthermore, `oxc-transform` was eagerly imported at top-level in `javascript-transformer-worker.ts`, causing worker evaluation to fail immediately upon startup even when OXC was not needed. This change resolves the issue in WebContainer environments: 1. Defaults `useBabelLinker` to `true` when running within WebContainer (`process.versions.webcontainer`), mirroring `useSassEmbedded`. 2. Lazily imports `oxc-transform` in `javascript-transformer-worker.ts` so it is never evaluated during worker module initialization. 3. Skips attempting OXC in WebContainer (unless explicitly requested via `NG_BUILD_BABEL_LINKER=false`), and gracefully falls back to the pure-JS Babel linker if OXC fails to load. Fixes angular#34524
There was a problem hiding this comment.
Code Review
This pull request introduces dynamic importing of the OXC transformer in the JavaScript transformer worker and defaults to using the Babel linker when running within a WebContainer environment. It also adds corresponding unit tests to verify these behaviors. The review feedback points out a critical performance issue where a failed import of the OXC transformer would be repeatedly re-attempted for every file transformation. To address this, the reviewer suggests caching the import attempt as a Promise that resolves to null on failure, allowing subsequent transformations to handle the fallback gracefully without redundant import attempts.
|
|
||
| let remapping: typeof import('@ampproject/remapping').default | undefined; | ||
|
|
||
| let oxcTransform: typeof import('../oxc/oxc-transform.js').transform | undefined; |
There was a problem hiding this comment.
Using a module-level variable oxcTransform that is assigned directly inside the try/catch block can lead to repeated failing import attempts. If the import of oxc-transform.js fails (which is expected in WebContainer environments when the WASM/WASI bindings fail), oxcTransform remains undefined. Consequently, every subsequent file transformation in the worker will re-attempt the import, throwing and catching errors repeatedly. This can severely degrade build performance, especially if the fallback script attempts synchronous process execution. We should instead use a Promise to cache the import attempt (including its failure state).
| let oxcTransform: typeof import('../oxc/oxc-transform.js').transform | undefined; | |
| let oxcTransformPromise: Promise<typeof import('../oxc/oxc-transform.js').transform | null> | undefined; |
| if (shouldRunOxc) { | ||
| try { | ||
| oxcTransform ??= (await import('../oxc/oxc-transform.js')).transform; | ||
| } catch { | ||
| oxcLink = false; | ||
| } | ||
| } |
There was a problem hiding this comment.
Resolve the cached oxcTransformPromise and handle failures gracefully by returning null. This ensures that if the import fails once, subsequent file transformations will immediately receive null from the cached promise without re-attempting the import or throwing new exceptions.
let transform: typeof import('../oxc/oxc-transform.js').transform | null = null;
if (shouldRunOxc) {
oxcTransformPromise ??= import('../oxc/oxc-transform.js')
.then((m) => m.transform)
.catch(() => null);
transform = await oxcTransformPromise;
if (!transform) {
oxcLink = false;
}
}| if (oxcTransform && (oxcLink || advancedOptimizations)) { | ||
| const sideEffectFree = options.sideEffects === false; | ||
| const safeAngularPackage = | ||
| sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename); | ||
| const topLevelSafeMode = !safeAngularPackage; | ||
|
|
||
| const result = transformWithOxc(filename, code, { | ||
| const result = oxcTransform(filename, code, { |
There was a problem hiding this comment.
Update the check and invocation to use the locally resolved transform function instead of the module-level oxcTransform variable.
| if (oxcTransform && (oxcLink || advancedOptimizations)) { | |
| const sideEffectFree = options.sideEffects === false; | |
| const safeAngularPackage = | |
| sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename); | |
| const topLevelSafeMode = !safeAngularPackage; | |
| const result = transformWithOxc(filename, code, { | |
| const result = oxcTransform(filename, code, { | |
| if (transform && (oxcLink || advancedOptimizations)) { | |
| const sideEffectFree = options.sideEffects === false; | |
| const safeAngularPackage = | |
| sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename); | |
| const topLevelSafeMode = !safeAngularPackage; | |
| const result = transform(filename, code, { |
|
Closing in favor of #34628 which resolves the underlying WASI initialization error directly. |
Description
PR #34540 added
@oxc-parser/binding-wasm32-wasias an optional dependency to@angular/buildto avoid runtime package manager execution in WebContainer environments. However, the playground and StackBlitz continue to fail.Root Cause Analysis
Published
@oxc-parser/binding-wasm32-wasireleases depend on@napi-rs/wasm-runtime: ~1.2.x, which resolves to1.2.6. However,@napi-rs/wasm-runtime@1.2.6declares peer dependencies of@emnapi/core: ^1.7.1 || 2.0.0-beta.2, which rejects the@emnapi/core@2.0.0-alpha.xthat the WASI binding was built against.Package managers therefore install an isolated
@emnapi/core@1.11.3beneathwasm-runtime, while the binding initializes its context using@emnapi/runtime@2.0.0-alpha.x. CallingcreateEnv()between these mismatched major versions throws a fatal runtime error:When loading
@oxc-parser/binding-wasm32-wasithrows this error,oxc-parser'sbindings.jsfalls back towebcontainer-fallback.cjs, which attempts to shell out tochildProcess.execFileSync('pnpm', ['i', ...], { cwd: '/tmp/...' })synchronously from inside a Piscina worker thread. This execution fails (and would install the same broken WASI version anyway), which Node.js's synchronousrequire(esm)loader reports as:javascript-transformer-worker.tshad a top-level eager importimport { transform as transformWithOxc } from '../oxc/oxc-transform.js'. Even in development (ng serve), whereadvancedOptimizationsis false, the worker thread crashed on startup before handling any requests.Proposed Solution
Mirror the existing WebContainer detection pattern used for
useSassEmbedded(!process.versions.webcontainer):useBabelLinkertotruein WebContainer: In WebContainer environments (process.versions.webcontainer), defaultuseBabelLinkertotrue(while still allowing explicit override viaNG_BUILD_BABEL_LINKER). The Babel linker is pure JavaScript and has zero native or WASI dependencies.oxc-transform: Remove the top-level eager import ofoxc-transform.jsinjavascript-transformer-worker.tsso workers boot without evaluatingoxc-parser.NG_BUILD_BABEL_LINKER=false), and gracefully fall back to the Babel linker if OXC fails to load.Fixes #34524