From 5affb7649c4dcfb9d7ffc93fe8064d92ea7651bf Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 3 Nov 2026 17:50:03 +0000 Subject: [PATCH] refactor(@angular/build): remove oxc WASI shim The upstream issue with @napi-rs/wasm-runtime and @emnapi/core WASI fallback resolution has been fixed in @napi-rs/wasm-runtime@1.2.7 (https://github.com/napi-rs/napi-rs/pull/3680), making the temporary WASI shim no longer necessary. --- packages/angular/build/BUILD.bazel | 1 - .../tools/angular/linker/oxc-ast-host_spec.ts | 6 +- .../src/tools/i18n/i18n-inliner-worker.ts | 4 +- .../build/src/tools/oxc/load-oxc-parser.ts | 124 ------------------ .../src/tools/oxc/load-oxc-parser_spec.ts | 114 ---------------- .../build/src/tools/oxc/oxc-transform.ts | 4 +- .../angular/build/src/tools/oxc/traversal.ts | 6 +- .../angular/build/src/utils/source-map.ts | 11 +- 8 files changed, 18 insertions(+), 252 deletions(-) delete mode 100644 packages/angular/build/src/tools/oxc/load-oxc-parser.ts delete mode 100644 packages/angular/build/src/tools/oxc/load-oxc-parser_spec.ts diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel index 0d1806be151c..d0ab55750e98 100644 --- a/packages/angular/build/BUILD.bazel +++ b/packages/angular/build/BUILD.bazel @@ -89,7 +89,6 @@ ts_project( ":node_modules/@angular/ssr", ":node_modules/@babel/core", ":node_modules/@inquirer/confirm", - ":node_modules/@oxc-parser/binding-wasm32-wasi", ":node_modules/@oxc-project/types", ":node_modules/@parcel/watcher", ":node_modules/@vitejs/plugin-basic-ssl", diff --git a/packages/angular/build/src/tools/angular/linker/oxc-ast-host_spec.ts b/packages/angular/build/src/tools/angular/linker/oxc-ast-host_spec.ts index a73268706cec..cbe0d6a25292 100644 --- a/packages/angular/build/src/tools/angular/linker/oxc-ast-host_spec.ts +++ b/packages/angular/build/src/tools/angular/linker/oxc-ast-host_spec.ts @@ -7,7 +7,7 @@ */ import { FatalLinkerError } from '@angular/compiler-cli/linker'; -import { loadOxcParser } from '../../oxc/load-oxc-parser'; +import { parseSync } from 'oxc-parser'; import { OxcAstHost } from './oxc-ast-host'; describe('OxcAstHost', () => { @@ -18,14 +18,14 @@ describe('OxcAstHost', () => { }); function parseExpression(code: string): unknown { - const { program } = loadOxcParser().parseSync('test.js', `const x = ${code};`, { range: true }); + const { program } = parseSync('test.js', `const x = ${code};`, { range: true }); const stmt = program.body[0] as { declarations: Array<{ init: unknown }> }; return stmt.declarations[0].init; } function parseStatement(code: string): unknown { - const { program } = loadOxcParser().parseSync('test.js', code, { range: true }); + const { program } = parseSync('test.js', code, { range: true }); return program.body[0]; } diff --git a/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts b/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts index 216423c9bb08..778ca55bb814 100644 --- a/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts +++ b/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts @@ -16,7 +16,7 @@ import { } from '@angular/localize'; import { MagicString } from 'magic-string'; import { deserialize } from 'node:v8'; -import { loadOxcParser } from '../oxc/load-oxc-parser'; +import { parseSync } from 'oxc-parser'; import { traversePostOrder } from '../oxc/traversal'; import { createSharedTranslationProxy } from './i18n-translation-reader'; import { loadLocaleData } from './locale-data'; @@ -402,7 +402,7 @@ interface FileLocalizeMetadata { * @returns The extracted localization metadata. */ function extractLocalizeMetadata(filename: string, code: string): FileLocalizeMetadata { - const { program } = loadOxcParser().parseSync(filename, code, { + const { program } = parseSync(filename, code, { sourceType: 'unambiguous', }); diff --git a/packages/angular/build/src/tools/oxc/load-oxc-parser.ts b/packages/angular/build/src/tools/oxc/load-oxc-parser.ts deleted file mode 100644 index 0369535cf522..000000000000 --- a/packages/angular/build/src/tools/oxc/load-oxc-parser.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import { Module, createRequire } from 'node:module'; - -/** - * Synchronous subset of the `oxc-parser` module. - * - * Async `parse()` is intentionally excluded because it spawns internal `wasi-worker.mjs` - * threads that do not execute the WASI compatibility shim and will fail to initialize. - */ -export type OxcParser = Omit; - -let oxcParser: OxcParser | undefined; - -/** - * Lazily loads and caches `oxc-parser`, applying a scoped WebAssembly (WASI) compatibility - * shim when running in WebContainer environments or when NAPI-RS WASI mode is forced. - */ -export function loadOxcParser(): OxcParser { - return (oxcParser ??= initializeOxcParser()); -} - -/** - * Workaround for an upstream incompatibility in `@oxc-parser/binding-wasm32-wasi` when - * falling back to WebAssembly (e.g., inside WebContainers or StackBlitz): - * - * `@oxc-parser/binding-wasm32-wasi` declares a dependency on `@emnapi/core@2.0.0-alpha.6`, - * but depends on `@napi-rs/wasm-runtime@~1.2.6` which expects `@emnapi/core@^1.7.1 || 2.0.0-beta.2`. - * 1. In `@napi-rs/wasm-runtime@1.2.6`, the runtime attempts to assign `initWorker` to - * `emnapi_shared.addonModule`, which is undefined in `alpha.6` where the property was named - * `napiModule`. Aliasing `addonModule` to `this.napiModule` on `Object.prototype` allows the - * WASI runtime to initialize successfully. - * 2. Under npm, `@napi-rs/wasm-runtime@1.2.6`'s peerDependencies cause npm to hoist `@emnapi/core@1.x` - * to top-level `node_modules` while nesting `2.0.0-alpha.6` under `@oxc-parser/binding-wasm32-wasi`, - * causing an ABI mismatch (`this.bridge.setLastError is not a function`). Under pnpm, `oxc-parser` - * does not declare `@oxc-parser/binding-wasm32-wasi` in its own dependencies. Resolving these - * packages relative to `@angular/build` and `@oxc-parser/binding-wasm32-wasi` ensures consistent - * module resolution across package managers. - * - * Because `oxc-parser` evaluates its WASI bindings synchronously during `require('oxc-parser')`, - * `Module._resolveFilename` is restored immediately after loading so other main-thread WASI - * consumers (such as `rolldown` and `vite`) continue resolving their own `@emnapi/*` versions. - * - * See: https://github.com/oxc-project/oxc/issues/28717 and https://github.com/napi-rs/napi-rs/issues/3674 - */ -function initializeOxcParser(): OxcParser { - const localRequire = createRequire(__filename); - const forceWasi = globalThis.process?.env?.['NAPI_RS_FORCE_WASI']; - const shouldInstallWasiShim = Boolean( - globalThis.process?.versions?.['webcontainer'] || - forceWasi === 'true' || - forceWasi === 'error' || - globalThis.process?.env?.['NAPI_RS_WASI_FLAVOR'], - ); - - if (shouldInstallWasiShim) { - if (!('addonModule' in Object.prototype)) { - Object.defineProperty(Object.prototype, 'addonModule', { - get() { - return (this as { napiModule?: unknown } | null | undefined)?.napiModule; - }, - set(value) { - if ( - this && - this !== Object.prototype && - (typeof this === 'object' || typeof this === 'function') - ) { - Object.defineProperty(this, 'addonModule', { - value, - writable: true, - configurable: true, - enumerable: true, - }); - } - }, - configurable: true, - enumerable: false, - }); - } - - const resolvedModules = new Map(); - try { - const wasiBindingPath = localRequire.resolve('@oxc-parser/binding-wasm32-wasi'); - resolvedModules.set('@oxc-parser/binding-wasm32-wasi', wasiBindingPath); - const wasiRequire = createRequire(wasiBindingPath); - resolvedModules.set('@emnapi/core', wasiRequire.resolve('@emnapi/core')); - resolvedModules.set('@emnapi/runtime', wasiRequire.resolve('@emnapi/runtime')); - } catch {} - - if (resolvedModules.size > 0) { - const moduleWithResolve = Module as typeof Module & { - _resolveFilename: ( - request: string, - parent: unknown, - isMain: boolean, - options: unknown, - ) => string; - }; - const origResolveFilename = moduleWithResolve._resolveFilename; - moduleWithResolve._resolveFilename = function (request, parent, isMain, options) { - const resolved = resolvedModules.get(request); - if (resolved !== undefined) { - return resolved; - } - - return origResolveFilename.call(this, request, parent, isMain, options); - }; - - try { - return localRequire('oxc-parser') as OxcParser; - } finally { - moduleWithResolve._resolveFilename = origResolveFilename; - } - } - } - - return localRequire('oxc-parser') as OxcParser; -} diff --git a/packages/angular/build/src/tools/oxc/load-oxc-parser_spec.ts b/packages/angular/build/src/tools/oxc/load-oxc-parser_spec.ts deleted file mode 100644 index 914de016c578..000000000000 --- a/packages/angular/build/src/tools/oxc/load-oxc-parser_spec.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import { Worker } from 'node:worker_threads'; -import { loadOxcParser } from './load-oxc-parser'; - -describe('loadOxcParser', () => { - it('should cache and return the same oxc-parser module instance across calls', () => { - const first = loadOxcParser(); - const second = loadOxcParser(); - - expect(first === second).toBeTrue(); - expect(typeof first.parseSync).toBe('function'); - expect(first.visitorKeys['Program']).toBeDefined(); - }); - - it('should load the WASI binding under NAPI_RS_FORCE_WASI=error and restore Module._resolveFilename', async () => { - const loadOxcParserPath = require.resolve('./load-oxc-parser'); - const sourceMapPath = require.resolve('../../utils/source-map'); - - const result = await new Promise<{ - resolveFilenameRestored: boolean; - wasiBindingLoaded: boolean; - statementCount: number; - commentCount: number; - strippedCode: string; - getterValue: unknown; - ownAssignedValue: unknown; - hasOwnAfterAssign: boolean; - protoGetterPreserved: boolean; - emptyObjectValue: unknown; - }>((resolve, reject) => { - const workerScript = ` - const { Module } = require('node:module'); - const { parentPort, workerData } = require('node:worker_threads'); - const { loadOxcParser } = require(workerData.loadOxcParserPath); - const { removeSourceMappingURL } = require(workerData.sourceMapPath); - - const origResolveFilename = Module._resolveFilename; - const oxc = loadOxcParser(); - const resolveFilenameRestored = Module._resolveFilename === origResolveFilename; - const wasiBindingLoaded = Object.keys(require.cache).some((key) => - key.includes('binding-wasm32-wasi'), - ); - - const parsed = oxc.parseSync('test.js', 'const answer = 42;\\n//# sourceMappingURL=test.map'); - const strippedCode = removeSourceMappingURL( - '//# sourceMappingURL=first.js.map\\nconst x = 1;\\n//# sourceMappingURL=second.js.map', - ); - - const receiver = { napiModule: 'from-napi-module' }; - const getterValue = receiver.addonModule; - receiver.addonModule = 'own-value'; - const hasOwnAfterAssign = Object.hasOwn(receiver, 'addonModule'); - const ownAssignedValue = receiver.addonModule; - - Object.prototype.addonModule = 'polluted'; - const protoGetterPreserved = - typeof Object.getOwnPropertyDescriptor(Object.prototype, 'addonModule')?.get === 'function'; - const emptyObjectValue = {}.addonModule; - (42).addonModule = 'primitive'; - - parentPort.postMessage({ - resolveFilenameRestored, - wasiBindingLoaded, - statementCount: parsed.program.body.length, - commentCount: parsed.comments.length, - strippedCode, - getterValue, - ownAssignedValue, - hasOwnAfterAssign, - protoGetterPreserved, - emptyObjectValue, - }); - `; - - const worker = new Worker(workerScript, { - eval: true, - env: { - ...process.env, - NAPI_RS_FORCE_WASI: 'error', - }, - workerData: { - loadOxcParserPath, - sourceMapPath, - }, - }); - - worker.once('message', resolve); - worker.once('error', reject); - worker.once('exit', (code) => { - if (code !== 0) { - reject(new Error(`Worker exited with non-zero exit code ${code}`)); - } - }); - }); - - expect(result.resolveFilenameRestored).toBeTrue(); - expect(result.wasiBindingLoaded).toBeTrue(); - expect(result.statementCount).toBe(1); - expect(result.commentCount).toBe(1); - expect(result.strippedCode).toBe('\nconst x = 1;\n'); - expect(result.getterValue).toBe('from-napi-module'); - expect(result.hasOwnAfterAssign).toBeTrue(); - expect(result.ownAssignedValue).toBe('own-value'); - expect(result.protoGetterPreserved).toBeTrue(); - expect(result.emptyObjectValue).toBeUndefined(); - }); -}); diff --git a/packages/angular/build/src/tools/oxc/oxc-transform.ts b/packages/angular/build/src/tools/oxc/oxc-transform.ts index 484732488a6d..e0d6ba26a252 100644 --- a/packages/angular/build/src/tools/oxc/oxc-transform.ts +++ b/packages/angular/build/src/tools/oxc/oxc-transform.ts @@ -10,8 +10,8 @@ import type { DecodedSourceMap } from '@ampproject/remapping'; import { needsLinking } from '@angular/compiler-cli/linker'; import type { BindingIdentifier, Class, Node } from '@oxc-project/types'; import { MagicString } from 'magic-string'; +import { parseSync } from 'oxc-parser'; import { OxcLinker } from '../angular/linker/oxc-linker'; -import { loadOxcParser } from './load-oxc-parser'; import { traversePostOrder } from './traversal'; export interface OxcTransformOptions { @@ -261,7 +261,7 @@ function analyzeClassStaticProperties(classNode: Node, code: string): boolean { */ // eslint-disable-next-line max-lines-per-function export function transform(filename: string, code: string, options: OxcTransformOptions) { - const { program } = loadOxcParser().parseSync(filename, code, { range: true }); + const { program } = parseSync(filename, code, { range: true }); const source = new MagicString(code); const shouldLink = options.link && needsLinking(filename, code); diff --git a/packages/angular/build/src/tools/oxc/traversal.ts b/packages/angular/build/src/tools/oxc/traversal.ts index 0e7a42743420..f25f4bc78664 100644 --- a/packages/angular/build/src/tools/oxc/traversal.ts +++ b/packages/angular/build/src/tools/oxc/traversal.ts @@ -7,9 +7,7 @@ */ import type { Node } from '@oxc-project/types'; -import { loadOxcParser } from './load-oxc-parser'; - -let visitorKeys: Record | undefined; +import { visitorKeys } from 'oxc-parser'; /** * Contextual scope information provided to AST visitor callbacks during traversal. @@ -277,7 +275,7 @@ function pushChildNodes(node: Node, stack: (Node | null)[]): void { break; } - const keys = (visitorKeys ??= loadOxcParser().visitorKeys)[node.type]; + const keys = visitorKeys[node.type]; if (!keys) { break; } diff --git a/packages/angular/build/src/utils/source-map.ts b/packages/angular/build/src/utils/source-map.ts index 9b1555f4d051..acc34f551ba8 100644 --- a/packages/angular/build/src/utils/source-map.ts +++ b/packages/angular/build/src/utils/source-map.ts @@ -8,14 +8,16 @@ import type { EncodedSourceMap } from '@ampproject/remapping'; import { existsSync, readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadOxcParser } from '../tools/oxc/load-oxc-parser'; export const SOURCEMAP_COMMENT_PREFIX = '//# sourceMappingURL='; export const SOURCEMAP_COMMENT_BYTES = Buffer.from(SOURCEMAP_COMMENT_PREFIX); const SOURCEMAP_COMMENT_VALUE_PREFIX = SOURCEMAP_COMMENT_PREFIX.slice(2); +let parseSync: typeof import('oxc-parser').parseSync | undefined; + /** * Checks for a single trailing `//# sourceMappingURL=` comment on a raw buffer. * @@ -118,7 +120,12 @@ function removeSourceMappingURLFromString(code: string, skipTrailingCheck = fals } } - const { comments } = loadOxcParser().parseSync('source.js', code, { + if (!parseSync) { + const localRequire = createRequire(__filename); + parseSync = (localRequire('oxc-parser') as typeof import('oxc-parser')).parseSync; + } + + const { comments } = parseSync('source.js', code, { lang: 'js', sourceType: 'unambiguous', });