Skip to content

fix(@angular/build): fall back to Babel linker in WebContainer environments - #34623

Closed
JeanMeche wants to merge 1 commit into
angular:mainfrom
JeanMeche:fix/webcontainer-oxc-fallback
Closed

JeanMeche wants to merge 1 commit into
angular:mainfrom
JeanMeche:fix/webcontainer-oxc-fallback

Conversation

@JeanMeche

Copy link
Copy Markdown
Member

Description

PR #34540 added @oxc-parser/binding-wasm32-wasi as an optional dependency to @angular/build to avoid runtime package manager execution in WebContainer environments. However, the playground and StackBlitz continue to fail.

Root Cause Analysis

  1. Upstream NAPI-RS / Emnapi Version Mismatch (napi-rs/napi-rs#3674):
    Published @oxc-parser/binding-wasm32-wasi releases depend on @napi-rs/wasm-runtime: ~1.2.x, which resolves to 1.2.6. However, @napi-rs/wasm-runtime@1.2.6 declares peer dependencies of @emnapi/core: ^1.7.1 || 2.0.0-beta.2, which rejects the @emnapi/core@2.0.0-alpha.x that the WASI binding was built against.
    Package managers therefore install an isolated @emnapi/core@1.11.3 beneath wasm-runtime, while the binding initializes its context using @emnapi/runtime@2.0.0-alpha.x. Calling createEnv() between these mismatched major versions throws a fatal runtime error:
    TypeError: this.bridge.setLastError is not a function
    
  2. WebContainer Fallback Failure:
    When loading @oxc-parser/binding-wasm32-wasi throws this error, oxc-parser's bindings.js falls back to webcontainer-fallback.cjs, which attempts to shell out to childProcess.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 synchronous require(esm) loader reports as:
    Error [ERR_REQUIRE_CYCLE_MODULE]: Cannot require() ES Module .../oxc-parser/src-js/index.js in a cycle.
    
  3. Eager Worker Module Initialization:
    javascript-transformer-worker.ts had a top-level eager import import { transform as transformWithOxc } from '../oxc/oxc-transform.js'. Even in development (ng serve), where advancedOptimizations is 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):

  1. Default useBabelLinker to true in WebContainer: In WebContainer environments (process.versions.webcontainer), default useBabelLinker to true (while still allowing explicit override via NG_BUILD_BABEL_LINKER). The Babel linker is pure JavaScript and has zero native or WASI dependencies.
  2. Lazy-load oxc-transform: Remove the top-level eager import of oxc-transform.js in javascript-transformer-worker.ts so workers boot without evaluating oxc-parser.
  3. Graceful Fallback: Skip attempting OXC in WebContainer (unless explicitly requested via NG_BUILD_BABEL_LINKER=false), and gracefully fall back to the Babel linker if OXC fails to load.

Fixes #34524

…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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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).

Suggested change
let oxcTransform: typeof import('../oxc/oxc-transform.js').transform | undefined;
let oxcTransformPromise: Promise<typeof import('../oxc/oxc-transform.js').transform | null> | undefined;

Comment on lines +188 to +194
if (shouldRunOxc) {
try {
oxcTransform ??= (await import('../oxc/oxc-transform.js')).transform;
} catch {
oxcLink = false;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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;
    }
  }

Comment on lines +237 to +243
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, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Update the check and invocation to use the locally resolved transform function instead of the module-level oxcTransform variable.

Suggested change
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, {

@alan-agius4

alan-agius4 commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Closing in favor of #34628 which resolves the underlying WASI initialization error directly.

@alan-agius4 alan-agius4 closed this Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

build: ERR_REQUIRE_CYCLE_MODULE when loading oxc-parser in StackBlitz / WebContainer

2 participants