diff --git a/packages/angular/build/package.json b/packages/angular/build/package.json index 6ca438be18ed..9088949b38ad 100644 --- a/packages/angular/build/package.json +++ b/packages/angular/build/package.json @@ -47,7 +47,7 @@ "xxhash-wasm": "1.1.0" }, "optionalDependencies": { - "@oxc-parser/binding-wasm32-wasi": "0.150.0", + "@oxc-parser/binding-wasm32-wasi": "0.156.0", "lmdb": "3.5.6" }, "devDependencies": { 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 778ca55bb814..a84d54eb713b 100644 --- a/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts +++ b/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import '../oxc/oxc-wasi-shim'; import { type DecodedSourceMap, type SourceMapInput } from '@ampproject/remapping'; import { type ɵParsedTranslation, diff --git a/packages/angular/build/src/tools/oxc/oxc-transform.ts b/packages/angular/build/src/tools/oxc/oxc-transform.ts index e0d6ba26a252..89594d9462ef 100644 --- a/packages/angular/build/src/tools/oxc/oxc-transform.ts +++ b/packages/angular/build/src/tools/oxc/oxc-transform.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import './oxc-wasi-shim'; import type { DecodedSourceMap } from '@ampproject/remapping'; import { needsLinking } from '@angular/compiler-cli/linker'; import type { BindingIdentifier, Class, Node } from '@oxc-project/types'; diff --git a/packages/angular/build/src/tools/oxc/oxc-wasi-shim.ts b/packages/angular/build/src/tools/oxc/oxc-wasi-shim.ts new file mode 100644 index 000000000000..3285052be059 --- /dev/null +++ b/packages/angular/build/src/tools/oxc/oxc-wasi-shim.ts @@ -0,0 +1,81 @@ +/** + * @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'; + +// 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. +// +// See: https://github.com/oxc-project/oxc/issues/28717 and https://github.com/napi-rs/napi-rs/issues/3674 +const forceWasi = globalThis.process?.env?.['NAPI_RS_FORCE_WASI']; +if ( + (globalThis.process?.versions?.['webcontainer'] || + forceWasi === 'true' || + forceWasi === 'error') && + !('addonModule' in Object.prototype) +) { + Object.defineProperty(Object.prototype, 'addonModule', { + get() { + return (this as { napiModule?: unknown }).napiModule; + }, + set(value) { + if (this && (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 = require.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); + }; + } +} diff --git a/packages/angular/build/src/tools/oxc/traversal.ts b/packages/angular/build/src/tools/oxc/traversal.ts index f25f4bc78664..7d85e312522a 100644 --- a/packages/angular/build/src/tools/oxc/traversal.ts +++ b/packages/angular/build/src/tools/oxc/traversal.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import './oxc-wasi-shim'; import type { Node } from '@oxc-project/types'; import { visitorKeys } from 'oxc-parser'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1149ea59e045..af5fb6b024d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -594,8 +594,8 @@ importers: version: 5.0.1(@opentelemetry/api@1.10.0)(@types/node@22.20.5)(@vitest/coverage-v8@5.0.1)(jsdom@30.0.1(@noble/hashes@1.8.0))(vite@8.3.5(@types/node@22.20.5)(esbuild@0.28.2)(jiti@2.7.0)(less@4.9.1(supports-color@11.0.0))(sass-embedded@1.104.1)(sass@1.104.1)(terser@5.51.2)(tsx@4.23.15)(yaml@2.9.1)) optionalDependencies: '@oxc-parser/binding-wasm32-wasi': - specifier: 0.150.0 - version: 0.150.0 + specifier: 0.156.0 + version: 0.156.0 lmdb: specifier: 3.5.6 version: 3.5.6 @@ -1811,14 +1811,14 @@ packages: resolution: {integrity: sha512-Xc3VhU02wqZ1HvHRJUwL09HkZSTvidqY5Ya0NXBSYOxAp+Ln9dcJr9fySI+CkONzP3PekQo9WdzCv0PGER/mOA==} engines: {node: '>=14.17.0'} - '@emnapi/core@2.0.0-alpha.3': - resolution: {integrity: sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==} + '@emnapi/core@2.0.0-alpha.6': + resolution: {integrity: sha512-MNVvMV7WtEJMgqaWJMvCyupK7Oi8sZctUIgPRZTsiS3cVvefk8DXPflOU3MSBddtj/rrYHygpYgpHDgwXnI3+w==} - '@emnapi/runtime@2.0.0-alpha.3': - resolution: {integrity: sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==} + '@emnapi/runtime@2.0.0-alpha.6': + resolution: {integrity: sha512-5ZR49ZsZ3cLvu5lkrVj6CMpHRgZjYz7XSlqK23GYYpyEqp749RVuIH0bEUKaGNghMh2pXRhIC3cnVrH28Y1j1Q==} - '@emnapi/wasi-threads@2.0.1': - resolution: {integrity: sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==} + '@emnapi/wasi-threads@2.2.0': + resolution: {integrity: sha512-JP7OYH38Q7+pyTAe14raZHLQIK7BDKbdwji+Xi3pcd/6QkJYQH3SX4wu6EplGrGJWRepoNzedfm0G3B9Z4e9Rw==} '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} @@ -3242,8 +3242,8 @@ packages: cpu: [arm64] os: [openharmony] - '@oxc-parser/binding-wasm32-wasi@0.150.0': - resolution: {integrity: sha512-bTxhIS4YI/Qr6dt5UACoOUNBeGTMr7tjLTjZzmD93OwMg9Qq5Y4RTmfEWUf4h0DM/Ury+3yVdPAoMEqAE+zFnA==} + '@oxc-parser/binding-wasm32-wasi@0.156.0': + resolution: {integrity: sha512-FWs20Zd4XCqpVwiTgKhyXWplIliulfwJMCI+POY2z1YF9IFr91qDLa7Jfqu0f1i95GFOy0GUGx8s6C4EtfFM3g==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} '@oxc-parser/binding-win32-arm64-msvc@0.156.0': @@ -3270,9 +3270,6 @@ packages: '@oxc-project/types@0.156.0': resolution: {integrity: sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==} - '@oxc-project/types@0.156.0': - resolution: {integrity: sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==} - '@parcel/watcher-android-arm64@2.6.0': resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} engines: {node: '>= 10.0.0'} @@ -9755,18 +9752,18 @@ snapshots: '@discoveryjs/json-ext@1.1.0': {} - '@emnapi/core@2.0.0-alpha.3': + '@emnapi/core@2.0.0-alpha.6': dependencies: - '@emnapi/wasi-threads': 2.0.1 + '@emnapi/wasi-threads': 2.2.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@2.0.0-alpha.3': + '@emnapi/runtime@2.0.0-alpha.6': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@2.0.1': + '@emnapi/wasi-threads@2.2.0': dependencies: tslib: 2.8.1 optional: true @@ -10906,10 +10903,10 @@ snapshots: '@napi-rs/nice-win32-x64-msvc': 1.1.1 optional: true - '@napi-rs/wasm-runtime@1.2.6(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)': + '@napi-rs/wasm-runtime@1.2.6(@emnapi/core@2.0.0-alpha.6)(@emnapi/runtime@2.0.0-alpha.6)': dependencies: - '@emnapi/core': 2.0.0-alpha.3 - '@emnapi/runtime': 2.0.0-alpha.3 + '@emnapi/core': 2.0.0-alpha.6 + '@emnapi/runtime': 2.0.0-alpha.6 '@tybys/wasm-util': 0.10.4 optional: true @@ -11118,11 +11115,11 @@ snapshots: '@oxc-parser/binding-openharmony-arm64@0.156.0': optional: true - '@oxc-parser/binding-wasm32-wasi@0.150.0': + '@oxc-parser/binding-wasm32-wasi@0.156.0': dependencies: - '@emnapi/core': 2.0.0-alpha.3 - '@emnapi/runtime': 2.0.0-alpha.3 - '@napi-rs/wasm-runtime': 1.2.6(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3) + '@emnapi/core': 2.0.0-alpha.6 + '@emnapi/runtime': 2.0.0-alpha.6 + '@napi-rs/wasm-runtime': 1.2.6(@emnapi/core@2.0.0-alpha.6)(@emnapi/runtime@2.0.0-alpha.6) optional: true '@oxc-parser/binding-win32-arm64-msvc@0.156.0': @@ -11138,8 +11135,6 @@ snapshots: '@oxc-project/types@0.156.0': {} - '@oxc-project/types@0.156.0': {} - '@parcel/watcher-android-arm64@2.6.0': optional: true diff --git a/tests/e2e/tests/vite/wasi.ts b/tests/e2e/tests/vite/wasi.ts new file mode 100644 index 000000000000..b7ddafb4e82c --- /dev/null +++ b/tests/e2e/tests/vite/wasi.ts @@ -0,0 +1,23 @@ +import assert from 'node:assert'; +import { getGlobalVariable } from '../../utils/env'; +import { ng } from '../../utils/process'; + +export default async function () { + assert( + getGlobalVariable('argv')['esbuild'], + 'This test should not be called in the Webpack suite.', + ); + + const originalForceWasi = process.env['NAPI_RS_FORCE_WASI']; + process.env['NAPI_RS_FORCE_WASI'] = 'error'; + + try { + await ng('build'); + } finally { + if (originalForceWasi === undefined) { + delete process.env['NAPI_RS_FORCE_WASI']; + } else { + process.env['NAPI_RS_FORCE_WASI'] = originalForceWasi; + } + } +}