From f585cb42cdd17e00268e60b744849c28d81235a2 Mon Sep 17 00:00:00 2001 From: Boshen Date: Wed, 23 Sep 2026 06:54:41 +0000 Subject: [PATCH] feat(napi): add threadless WASI builds (#26898) ## Summary - add threadless `wasm32-wasip1` builds and generated loaders for parser, minify, transform, transform-react, and transform-relay - use the threadless bindings for browser and Cloudflare Workers while preserving threaded WASI and WebContainer fallbacks - build and publish the new target in CI and release workflows Closes #25271 ## Testing - native NAPI builds and package tests: 1,091 passed - parser Chromium browser tests passed - local workerd smoke tests passed for all five packages - deployed Workers using local package tarballs passed for all five packages and reported `wasm32-wasip1` - packed main-package file lists match the published `0.150.0` packages - `just fmt`, `just check`, `just lint`, `just doc`, `just ast`, targeted Oxlint, typos, and `git diff --check` passed - `just ready` reaches three unrelated Oxlint ANSI-color snapshot mismatches in this non-interactive environment; no pending snapshots remain ## AI assistance Codex was used to help implement and test this change. The contributor remains responsible for reviewing and understanding the changes before merge. --- .github/workflows/ci.yml | 3 + .github/workflows/reusable_release_napi.yml | 4 + napi/minify/browser.js | 2 +- napi/minify/index.js | 50 +- napi/minify/minify.wasip1-browser.js | 990 +++++++++++ napi/minify/minify.wasip1-deferred.d.ts | 83 + napi/minify/minify.wasip1-deferred.js | 1488 +++++++++++++++++ napi/minify/minify.wasip1.cjs | 1209 ++++++++++++++ napi/minify/minify.wasip1.d.cts | 353 ++++ napi/minify/package.json | 3 + napi/minify/scripts/patch.js | 4 + napi/parser/napi.wasm.json | 2 +- napi/parser/package.json | 3 + napi/parser/scripts/build-browser-bundle.js | 8 +- napi/parser/scripts/patch.js | 4 + napi/parser/src-js/bindings.js | 50 +- napi/parser/src-js/browser.js | 2 +- napi/parser/src-js/parser.wasip1-browser.js | 995 +++++++++++ .../parser/src-js/parser.wasip1-deferred.d.ts | 83 + napi/parser/src-js/parser.wasip1-deferred.js | 1488 +++++++++++++++++ napi/parser/src-js/parser.wasip1.cjs | 1214 ++++++++++++++ napi/parser/src-js/parser.wasip1.d.cts | 301 ++++ napi/parser/src-js/wasm.js | 4 +- napi/parser/vitest.config.browser.ts | 2 +- napi/transform-react/browser.js | 2 +- napi/transform-react/index.js | 50 +- napi/transform-react/package.json | 3 + napi/transform-react/scripts/patch.js | 4 + .../transform-react.wasip1-browser.js | 989 +++++++++++ .../transform-react.wasip1-deferred.d.ts | 83 + .../transform-react.wasip1-deferred.js | 1488 +++++++++++++++++ .../transform-react.wasip1.cjs | 1208 +++++++++++++ .../transform-react.wasip1.d.cts | 342 ++++ napi/transform-relay/browser.js | 2 +- napi/transform-relay/index.js | 50 +- napi/transform-relay/package.json | 3 + napi/transform-relay/scripts/patch.js | 4 + .../transform-relay.wasip1-browser.js | 989 +++++++++++ .../transform-relay.wasip1-deferred.d.ts | 83 + .../transform-relay.wasip1-deferred.js | 1488 +++++++++++++++++ .../transform-relay.wasip1.cjs | 1208 +++++++++++++ .../transform-relay.wasip1.d.cts | 120 ++ napi/transform/browser.js | 2 +- napi/transform/index.js | 50 +- napi/transform/package.json | 3 + napi/transform/scripts/patch.js | 4 + napi/transform/transform.wasip1-browser.js | 994 +++++++++++ napi/transform/transform.wasip1-deferred.d.ts | 83 + napi/transform/transform.wasip1-deferred.js | 1488 +++++++++++++++++ napi/transform/transform.wasip1.cjs | 1213 ++++++++++++++ napi/transform/transform.wasip1.d.cts | 695 ++++++++ oxfmtrc.jsonc | 1 + oxlintrc.json | 2 + package.json | 1 + 54 files changed, 20979 insertions(+), 18 deletions(-) create mode 100644 napi/minify/minify.wasip1-browser.js create mode 100644 napi/minify/minify.wasip1-deferred.d.ts create mode 100644 napi/minify/minify.wasip1-deferred.js create mode 100644 napi/minify/minify.wasip1.cjs create mode 100644 napi/minify/minify.wasip1.d.cts create mode 100644 napi/parser/src-js/parser.wasip1-browser.js create mode 100644 napi/parser/src-js/parser.wasip1-deferred.d.ts create mode 100644 napi/parser/src-js/parser.wasip1-deferred.js create mode 100644 napi/parser/src-js/parser.wasip1.cjs create mode 100644 napi/parser/src-js/parser.wasip1.d.cts create mode 100644 napi/transform-react/transform-react.wasip1-browser.js create mode 100644 napi/transform-react/transform-react.wasip1-deferred.d.ts create mode 100644 napi/transform-react/transform-react.wasip1-deferred.js create mode 100644 napi/transform-react/transform-react.wasip1.cjs create mode 100644 napi/transform-react/transform-react.wasip1.d.cts create mode 100644 napi/transform-relay/transform-relay.wasip1-browser.js create mode 100644 napi/transform-relay/transform-relay.wasip1-deferred.d.ts create mode 100644 napi/transform-relay/transform-relay.wasip1-deferred.js create mode 100644 napi/transform-relay/transform-relay.wasip1.cjs create mode 100644 napi/transform-relay/transform-relay.wasip1.d.cts create mode 100644 napi/transform/transform.wasip1-browser.js create mode 100644 napi/transform/transform.wasip1-deferred.d.ts create mode 100644 napi/transform/transform.wasip1-deferred.js create mode 100644 napi/transform/transform.wasip1.cjs create mode 100644 napi/transform/transform.wasip1.d.cts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 271f7be049..6b16b9002b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -183,8 +183,10 @@ jobs: - name: Build if: steps.filter.outputs.changed == 'true' run: | + rustup target add wasm32-wasip1 rustup target add wasm32-wasip1-threads pnpm run build-wasm-dev + pnpm run build-wasip1-dev git diff --exit-code # Must commit everything - name: Test wasi in browser if: steps.filter.outputs.changed == 'true' @@ -194,6 +196,7 @@ jobs: pnpm exec playwright install chromium pnpm run build-wasi pnpm run build-wasm-dev + pnpm run build-wasip1-dev pnpm run build-npm-dir pnpm run test-browser pnpm build-browser-bundle --npmDir npm-dir diff --git a/.github/workflows/reusable_release_napi.yml b/.github/workflows/reusable_release_napi.yml index 32818a7c89..f64ca73e96 100644 --- a/.github/workflows/reusable_release_napi.yml +++ b/.github/workflows/reusable_release_napi.yml @@ -80,6 +80,10 @@ jobs: target: wasm32-wasip1-threads build: pnpm build-wasm + - os: ubuntu-latest + target: wasm32-wasip1 + build: pnpm build-wasip1 + - os: ubuntu-latest target: s390x-unknown-linux-gnu build: | diff --git a/napi/minify/browser.js b/napi/minify/browser.js index 7f75f05478..26e69216f1 100644 --- a/napi/minify/browser.js +++ b/napi/minify/browser.js @@ -1 +1 @@ -export * from '@oxc-minify/binding-wasm32-wasi' +export * from '@oxc-minify/binding-wasm32-wasip1' diff --git a/napi/minify/index.js b/napi/minify/index.js index 5ba7c72b38..67716e1fad 100644 --- a/napi/minify/index.js +++ b/napi/minify/index.js @@ -565,7 +565,7 @@ function createLoadErrorChain(errors) { // // NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict // WASI loading. It never crosses into another flavor or falls back to native. -const __napiWasiFlavors = ['wasm32-wasi'] +const __napiWasiFlavors = ['wasm32-wasi', 'wasm32-wasip1'] const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR const __napiWasiFlavorRequested = typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0 @@ -655,6 +655,27 @@ if (!nativeBinding || forceWasi) { loadErrors.push(candidateError) } } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasip1')) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('./minify.wasip1.cjs', false, ['./minify.wasm32-wasip1.debug.wasm', './minify.wasm32-wasip1.wasm']) + candidateFailed = candidateError !== null + if (!candidateFailed) { + wasiBinding = require('./minify.wasip1.cjs') + nativeBinding = wasiBinding + __napiLoadedBindingTarget = 'wasm32-wasip1' + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasi')) { let candidateError = null let candidateFailed = false @@ -682,6 +703,33 @@ if (!nativeBinding || forceWasi) { loadErrors.push(candidateError) } } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasip1')) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('@oxc-minify/binding-wasm32-wasip1', true, undefined) + candidateFailed = candidateError !== null + if (!candidateFailed) { + if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + const bindingPackageVersion = require('@oxc-minify/binding-wasm32-wasip1/package.json').version + if (bindingPackageVersion !== '0.151.0') { + throw new Error(`WASI binding package version mismatch, expected 0.151.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + } + wasiBinding = require('@oxc-minify/binding-wasm32-wasip1') + nativeBinding = wasiBinding + __napiLoadedBindingTarget = 'wasm32-wasip1' + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } if ( !wasiBindingLoaded && forceWasi && diff --git a/napi/minify/minify.wasip1-browser.js b/napi/minify/minify.wasip1-browser.js new file mode 100644 index 0000000000..81a76fca8e --- /dev/null +++ b/napi/minify/minify.wasip1-browser.js @@ -0,0 +1,990 @@ +import { + emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin as __emnapiTSFNPlugin, + instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync, + WASI as __WASI, +} from '@napi-rs/wasm-runtime' +import { createContext as __emnapiCreateContext } from '@emnapi/runtime' + + +export const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +const __wasi = new __WASI({ + version: 'preview1', +}) + +const __wasmUrl = new URL('./minify.wasm32-wasip1.wasm', import.meta.url).href +const __wasmResponse = await globalThis.fetch(__wasmUrl) +if (!__wasmResponse.ok) { + throw new Error( + 'Failed to fetch WASI module ' + + __wasmUrl + + ': ' + + __wasmResponse.status + + ' ' + + (__wasmResponse.statusText || 'Unknown Status'), + ) +} +const __wasmFile = await __wasmResponse.arrayBuffer() + +const __wasmMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, +}) +let __emnapiContext + +const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose') +const __wasiWorkers = new Set() +// The thread manager has to be reachable *before* anything that can throw +// during load or registration. Initialization can fail after the pool has +// already spawned workers, and the rollback still has to mark their +// terminations as expected — but `__napiModule` is assigned only when +// instantiation RETURNS, so on exactly that path it is still undefined. A +// plugin factory runs while the emnapi module is being created, before the +// wasm is loaded and before any registration function runs, and its context +// carries the very same manager instance. +let __wasiThreadManager + +function __captureWasiThreadManager(context) { + if (context && context.PThread) { + __wasiThreadManager = context.PThread + } + return {} +} + +function __getWasiThreadManager() { + const manager = + __wasiThreadManager !== undefined + ? __wasiThreadManager + : __napiModule + ? __napiModule.PThread + : undefined + if (manager && typeof manager.terminateWorker === 'function') { + return manager + } + return undefined +} +let __napiInstance +let __emnapiContextDestroyed = false +let __emnapiContextDestroyPromise +let __emnapiWasmEnvCleanupPrepared = false +let __emnapiWasmEnvCleanupPreparing = false +let __emnapiWasmEnvCleanupRan = false +let __emnapiWasmEnvCleanupDrained = false +let __emnapiWasmEnvCleanupDrainPromise +let __wasiDisposed = false +let __wasiAsyncWorkDrainPromise +let __wasiDisposePromise +let __completeWasiDisposal = function () {} +// Overridden by loader flavors that have a last-resort reclaim for a rollback +// that stopped short of destroying the context. See +// `__rollbackWasiInitialization`. +let __retainWasiRollbackForRetry = function () {} + +function __isThenable(value) { + return ( + value !== null && + (typeof value === 'object' || typeof value === 'function') && + typeof value.then === 'function' + ) +} + +function __createCleanupError(errors, message) { + if (errors.length === 1) { + return errors[0] + } + const __AggregateError = globalThis.AggregateError + if (typeof __AggregateError === 'function') { + return new __AggregateError(errors, message) + } + const error = new Error(message) + error.errors = errors + return error +} + +function __attachCleanupErrors(error, cleanupErrors) { + if (cleanupErrors.length === 0) { + return error + } + const cleanupError = __createCleanupError( + cleanupErrors, + 'WASI binding cleanup failed', + ) + try { + if ( + error && + (typeof error === 'object' || typeof error === 'function') + ) { + if (error.cause === undefined) { + error.cause = cleanupError + if (error.cause === cleanupError) { + return error + } + } + if (Array.isArray(error.cleanupErrors)) { + error.cleanupErrors.push(cleanupError) + return error + } else { + const attachedCleanupErrors = [cleanupError] + error.cleanupErrors = attachedCleanupErrors + if (error.cleanupErrors === attachedCleanupErrors) { + return error + } + } + } + } catch {} + const aggregate = __createCleanupError( + [error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + try { + aggregate.cause = error + } catch {} + return aggregate +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __isPreparingWasmEnvCleanup() { + return __emnapiWasmEnvCleanupPreparing +} + +function __prepareWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) { + return + } + const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup + if (typeof prepare === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __emnapiWasmEnvCleanupPreparing = true + try { + prepare() + } finally { + __emnapiWasmEnvCleanupPreparing = false + } + __emnapiWasmEnvCleanupRan = true + } + __emnapiWasmEnvCleanupPrepared = true +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (callback) { + setImmediate(callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (callback) { + const channel = new __MessageChannel() + channel.port1.onmessage = function () { + channel.port1.onmessage = null + try { + channel.port1.close() + } catch {} + try { + channel.port2.close() + } catch {} + callback() + } + channel.port2.postMessage(null) + } + } + return function (callback) { + setTimeout(callback, 0) + } +})() + +// A real, *referenced* timer, for waits that must let the whole host make +// progress between looks — the async-work drain polls the addon rather than +// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there +// would spin the loop instead of yielding it. Falls back to the macrotask +// scheduler on a host without timers. +function __scheduleTimer(callback, delay) { + const setTimer = globalThis.setTimeout + if (typeof setTimer !== 'function') { + __scheduleMacrotask(callback) + return + } + try { + setTimer(callback, delay) + } catch { + __scheduleMacrotask(callback) + } +} + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + * + * The "already drained" flag is set only once a wait has actually finished. + * Scheduling a macrotask can fail — a host-provided or patched `setImmediate` + * that throws is enough — and a disposal that rejects stays retryable, so + * marking the drain complete up front would make the retry skip it and destroy + * the context with the barrier's settlements still queued. + * + * A wait that runs out of turns with the counter still nonzero rejects with + * `ERR_NAPI_WASI_CLEANUP_PENDING` for the same reason: at that point + * "finished" is indistinguishable from the stranding above, and destroying + * would discard the very settlement the wait was for. The rejection leaves the + * flag unset and disposal retryable. + */ +function __drainWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupDrained || !__emnapiWasmEnvCleanupRan) { + return + } + if (__emnapiWasmEnvCleanupDrainPromise) { + return __emnapiWasmEnvCleanupDrainPromise + } + const pending = __napiInstance?.exports?.napi_wasm_env_cleanup_pending + const observable = typeof pending === 'function' + if (observable) { + let queued + try { + queued = pending() + } catch { + __emnapiWasmEnvCleanupDrained = true + return + } + if (!queued) { + __emnapiWasmEnvCleanupDrained = true + return + } + } + const limit = observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + const drainPromise = (async () => { + let queued = 0 + for (let turn = 0; turn < limit; turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!observable) { + continue + } + try { + queued = pending() + } catch { + return + } + if (!queued) { + return + } + } + if (!observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: the drained flag stays + // unset, dispose() (and the rollback) decline to destroy, and a later + // dispose() runs the drain again — by which time the queue has usually been + // delivered. A counter that is somehow stuck nonzero therefore costs each + // attempt at most another bounded wait and a rejection, never a stranded + // promise; the process-exit teardown still reclaims the context. + const drainError = new Error( + 'the wasm environment still reports ' + + queued + + ' queued settlement(s) after ' + + limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw drainError + })().then( + (value) => { + // Set only when the wait actually finished AND the queue was seen empty + // (or is unobservable): a drain that timed out with settlements still + // queued rejects above and must stay repeatable. + __emnapiWasmEnvCleanupDrained = true + __emnapiWasmEnvCleanupDrainPromise = undefined + return value + }, + (error) => { + __emnapiWasmEnvCleanupDrainPromise = undefined + throw error + }, + ) + __emnapiWasmEnvCleanupDrainPromise = drainPromise + return drainPromise +} + +function __destroyEmnapiContext() { + if (__emnapiContextDestroyed || __emnapiContext === undefined) { + __emnapiContextDestroyed = true + return + } + if (__emnapiContextDestroyPromise) { + return __emnapiContextDestroyPromise + } + + __prepareWasmEnvCleanup() + const result = __emnapiContext.destroy() + if (!__isThenable(result)) { + __emnapiContextDestroyed = true + return + } + + const destroyPromise = Promise.resolve(result).then( + (value) => { + __emnapiContextDestroyed = true + return value + }, + (error) => { + __emnapiContextDestroyPromise = undefined + throw error + }, + ) + __emnapiContextDestroyPromise = destroyPromise + return destroyPromise +} + +/** + * Holds the event loop open until `work` settles. + * + * Nothing else can: the pool workers are deliberately unreferenced so an idle + * binding cannot keep a process alive, and referencing them again for the + * termination does not hold either — emnapi unreferences a worker the moment it + * reports `async-thread-ready`, which for a worker that was still starting + * lands *after* the termination began. Without a handle of its own, an + * `await dispose()` with nothing else pending exits the process with its + * promise unsettled, and everything after the `await` is skipped. + * + * The timer is cleared as soon as the work settles, so this never outlives the + * disposal that asked for it. + */ +function __keepEventLoopAliveUntil(work) { + const setTimer = globalThis.setInterval + const clearTimer = globalThis.clearInterval + if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') { + return work + } + let timer + try { + timer = setTimer(function () {}, 50) + } catch { + return work + } + const release = function () { + try { + clearTimer(timer) + } catch {} + } + return work.then( + (value) => { + release() + return value + }, + (error) => { + release() + throw error + }, + ) +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this addon's outstanding `napi_async_work` before the teardown that + * would strand it. + * + * `napi_prepare_wasm_env_cleanup` does not cover async work, and nothing about + * it is observable from JavaScript: the threadless archive resolves + * `napi_*_async_work` through the `@emnapi/core` plugins, but the threaded one + * links the C `async_work.c` on the uv threadpool, so there the wasm neither + * imports nor exports those symbols and the only brackets a loader could watch + * (`_emnapi_ctx_*_waiting_request_counter`) are shared with threadsafe + * functions. The addon is the one place both flavors go through, so it answers + * for both, through the same kind of handshake the settlement drain uses: + * + * - `napi_wasm_cancel_pending_async_work()` cancels what no thread has + * started. Those completion callbacks run with `napi_cancelled`, which + * napi-rs turns into a promise rejected with an `AbortError`. + * - `napi_wasm_async_work_pending()` counts what is still owed a completion + * callback. Work already executing refuses cancellation and stays counted + * until it finishes normally — which it can, because this runs before the + * barrier, before `Context.destroy()` and before anything is terminated. + * + * Both exports are optional: an addon built against a napi crate that predates + * them drains nothing and keeps the previous behavior, exactly as the + * `napi_wasm_env_cleanup_pending` handshake degrades. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. The promise it returns otherwise never + * rejects. + * + * The wait has no deadline, and that is the point: giving up would destroy the + * environment with a completion callback still owed, which is the stranding + * this exists to prevent. A task whose `execute` never returns already keeps an + * *undisposed* process alive in exactly the same way, so disposal inherits that + * rather than inventing a bound it cannot honor. + * + * Safe to call from inside a completion callback, which is reachable: settling + * a task runs addon code that can re-enter JavaScript — a setter on the value + * being handed back, a threadsafe-function callback — and that JavaScript can + * call `dispose()`. Two things make it terminate rather than wait on itself: + * + * - The addon keeps a work registered until its completion callback + * *finishes*, so the count read here is at least one and this takes the + * polling path instead of declaring the environment drained and tearing it + * down from inside the frame that is still settling a promise. + * - The poll is a timer, so it cannot run until the callback has returned to + * the host — by which time that work has left the registry. The count the + * next poll reads is the one taken after the callback finished. + * + * `__disposeWasiBinding` hands every caller the same in-flight promise, so the + * nested call joins this disposal rather than starting a second one. + */ +function __drainWasiAsyncWork() { + if (__wasiAsyncWorkDrainPromise !== undefined) { + return __wasiAsyncWorkDrainPromise + } + const exports = __napiInstance?.exports + const pending = exports?.napi_wasm_async_work_pending + const cancelPending = exports?.napi_wasm_cancel_pending_async_work + if (typeof pending !== 'function' || typeof cancelPending !== 'function') { + return + } + + const readPending = () => { + try { + return pending() + } catch (error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition — + // there is nothing left to wait for, and refusing to dispose would only + // keep a dead instance and its stuck counter alive. Best-effort here is + // the honest answer, and it is what disposal did before this drain + // existed. + // + // Only a trap. Anything else means the export is not what this loader + // thinks it is, which is a defect worth surfacing rather than disposing + // over. + if (error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw error + } + } + + if (!readPending()) { + return + } + try { + cancelPending() + } catch { + // Cancellation is an optimization: it bounds the wait by the work already + // executing. Failing it only means waiting for the whole queue instead. + } + if (!readPending()) { + return + } + + const drainPromise = __keepEventLoopAliveUntil( + (async () => { + while (readPending()) { + await new Promise((resolve) => { + __scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })(), + ).then( + () => { + __wasiAsyncWorkDrainPromise = undefined + }, + (error) => { + // A wait that could not run is not a wait that finished. The only way + // here is a host whose timers and macrotask primitives all refuse, and + // the work is still outstanding — reporting success would destroy the + // environment over it, which is the stranding this exists to prevent. + // Reject instead: disposal stays retryable, and the context is not + // destroyed. Clearing the memo first is what makes the retry re-run this. + __wasiAsyncWorkDrainPromise = undefined + throw error + }, + ) + __wasiAsyncWorkDrainPromise = drainPromise + return drainPromise +} + +/** + * `@emnapi/wasi-threads` counts a worker exit as expected only when its own + * thread manager performed the termination. A bare `worker.terminate()` reaches + * the manager's `exit` listener instead, which reports + * `worker (tid = N) sent an error! ... stopped with exit code 1` and rethrows + * inside the emit — aborting the `once('exit')` that backs the terminate + * promise, so disposal never settles and the process dies with an uncaught + * exception. Mark the termination through the manager first. + * + * The manager comes from `__getWasiThreadManager`, not from `__napiModule`: + * the initialization rollback runs on the one path where instantiation never + * returned, so `__napiModule` is still undefined there while the workers it + * spawned are already registered and loaded. + * + * Not `terminateAllThreads()`: that one recreates the pool it just shut down. + */ +function __terminateWasiWorkers() { + const cleanupErrors = [] + const pending = [] + const threadManager = __getWasiThreadManager() + + for (const worker of __wasiWorkers) { + let result + try { + if (threadManager) { + threadManager.terminateWorker(worker) + // `terminateWorker` leaves behind a reporter that logs every message + // still queued on the port, which Node flushes on exit. Nothing is + // listening for those any more. + worker.onmessage = undefined + } + result = worker.terminate() + } catch (error) { + cleanupErrors.push(error) + continue + } + if (__isThenable(result)) { + pending.push( + Promise.resolve(result).then( + () => { + __wasiWorkers.delete(worker) + }, + (error) => { + cleanupErrors.push(error) + }, + ), + ) + } else { + __wasiWorkers.delete(worker) + } + } + + const finish = () => { + if (cleanupErrors.length > 0) { + throw __createCleanupError( + cleanupErrors, + 'Failed to terminate WASI workers', + ) + } + } + return pending.length > 0 + ? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish) + : finish() +} + +function __finishWasiDisposal() { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult).then(__completeWasiDisposal) + } + return __completeWasiDisposal() +} + +function __continueWasiDisposal() { + const destroyResult = __destroyEmnapiContext() + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult).then(__finishWasiDisposal) + } + return __finishWasiDisposal() +} + +function __cleanUpWasmEnvForWasiDisposal() { + // Run the pre-teardown barrier, then let the settlements it queued actually + // reach JavaScript, and only then destroy the environment. Doing these two + // back to back is what strands them. + __prepareWasmEnvCleanup() + const drainResult = __drainWasmEnvCleanup() + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then(__continueWasiDisposal) + } + return __continueWasiDisposal() +} + +function __startWasiDisposal() { + // Outstanding `napi_async_work` goes first, while the environment is still + // completely live: the completion callbacks run addon code, and everything + // after this point takes that away from them — the barrier shuts the async + // runtime down, `Context.destroy()` stops JavaScript calls, and terminating + // the pool threads removes what would have reported the work finished. + const asyncWorkResult = __drainWasiAsyncWork() + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __cleanUpWasmEnvForWasiDisposal, + ) + } + return __cleanUpWasmEnvForWasiDisposal() +} + +/** + * Disposes this generated WASI binding. + * + * Access this function with: + * binding[Symbol.for('napi.rs.wasi.dispose')]() + */ +function __disposeWasiBinding() { + if (__wasiDisposePromise) { + return __wasiDisposePromise + } + if (__wasiDisposed) { + return Promise.resolve() + } + + let resolveDispose + let rejectDispose + const disposePromise = new Promise((resolve, reject) => { + resolveDispose = resolve + rejectDispose = reject + }) + __wasiDisposePromise = disposePromise + + let result + try { + result = __startWasiDisposal() + } catch (error) { + __wasiDisposePromise = undefined + rejectDispose(error) + return disposePromise + } + + Promise.resolve(result).then( + (value) => { + __wasiDisposed = true + resolveDispose(value) + }, + (error) => { + __wasiDisposePromise = undefined + rejectDispose(error) + }, + ) + return disposePromise +} + +function __publishWasiDispose(exports) { + Object.defineProperty(exports, __wasiDisposeSymbol, { + configurable: false, + enumerable: false, + value: __disposeWasiBinding, + writable: false, + }) +} + +function __finishWasiInitializationRollback(cleanupErrors) { + let workerResult + try { + workerResult = __terminateWasiWorkers() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return cleanupErrors + } + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => cleanupErrors) + } + return cleanupErrors +} + +function __destroyContextForWasiRollback(cleanupErrors) { + let destroyResult + try { + destroyResult = __destroyEmnapiContext() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return __finishWasiInitializationRollback(cleanupErrors) + } + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => __finishWasiInitializationRollback(cleanupErrors)) + } + return __finishWasiInitializationRollback(cleanupErrors) +} + +/** + * Leaves a rollback that could not reach the queued settlements undestroyed, and + * hands it to whatever this flavor has that can still reclaim it. + */ +function __retainFailedWasiRollback(cleanupErrors) { + try { + __retainWasiRollbackForRetry() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + } + return cleanupErrors +} + +/** + * Initialization can fail *after* registration has already run, and registration + * runs with a live environment: a module-init hook can start async work and then + * return an error, and the promise it created may already have escaped into + * JavaScript. The barrier cancels that work and *queues* the settlement, so this + * path needs the same drain the ordinary disposal does — destroying without + * yielding discards the queue with a null env and strands the promise. + * + * Stays synchronous when nothing is queued, which covers every failure before + * `beforeInit`: there is no instance to run the barrier on, so nothing to drain. + * + * A barrier or drain that did *not* finish stops the rollback short of + * destroying, which is what `dispose()` already does — a rejected drain there + * never reaches `__continueWasiDisposal`. Destroying anyway is the worse of the + * two trades, and not because of what it saves: + * + * - It cannot deliver the settlements. `Context.destroy()` runs the + * threadsafe function's cleanup hook, which drains the queue with a null env + * and discards it, so a promise that already escaped into JavaScript hangs + * forever with nothing left that could ever settle it. + * - It saves less than it looks. `Context.destroy()` stops JavaScript calls + * and runs cleanup hooks; it does not free the wasm instance or its Memory, + * which this module's scope holds either way. What stopping short retains is + * the emnapi context's bookkeeping and its un-run cleanup hooks. + * - Retry is not theoretical. A rollback that records a cleanup error is + * already kept in the process-wide registry above, so re-`require()`ing this + * file replays it instead of re-instantiating — and the `6e15de6f` flag fix + * means the replay drains again rather than skipping it. Destroying first is + * what makes that retained record useless. + * + * The residual cost is honest: the CJS flavor hands the context to its + * `process.on('exit')` teardown, so a process that never retries still reclaims + * it on the way out. The ESM browser flavor has no equivalent — a module that + * throws while evaluating is permanently errored, so re-importing rethrows + * without re-running this file — and there the context stays until the realm + * goes away. That is the deliberate choice: a hung promise is a silent liveness + * bug with no upper bound, while the retained bookkeeping is bounded by the page. + */ +function __rollbackWasiInitialization() { + // The environment teardown this rollback performs, kept nested so it cannot + // be reached without the async-work drain below running first. + function __rollbackWasmEnvForWasiInitialization() { + const cleanupErrors = [] + let drainResult + let settlementsUnreached = false + try { + __prepareWasmEnvCleanup() + drainResult = __drainWasmEnvCleanup() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + settlementsUnreached = true + } + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then( + () => __destroyContextForWasiRollback(cleanupErrors), + (cleanupError) => { + cleanupErrors.push(cleanupError) + return __retainFailedWasiRollback(cleanupErrors) + }, + ) + } + if (settlementsUnreached) { + return __retainFailedWasiRollback(cleanupErrors) + } + return __destroyContextForWasiRollback(cleanupErrors) + } + + // Same reason as `__startWasiDisposal`: a module-init hook can start async + // work before the load goes on to fail, and this rollback tears down exactly + // what those completions need. Settle them while everything is still live, + // before the barrier and the teardown above take that away. + // + // A drain that could not finish leaves async work possibly outstanding, and + // destroying the context over it would strand exactly what this rollback is + // there to settle. Stop short and retain instead — the same trade + // `__rollbackWasmEnvForWasiInitialization` makes for the settlement drain, so + // the context stays reclaimable by a retry or by this flavor's own + // last-resort teardown. + const __retainAfterAsyncWorkDrainFailure = (cleanupError) => + __retainFailedWasiRollback([cleanupError]) + let asyncWorkResult + try { + asyncWorkResult = __drainWasiAsyncWork() + } catch (cleanupError) { + return __retainAfterAsyncWorkDrainFailure(cleanupError) + } + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __rollbackWasmEnvForWasiInitialization, + __retainAfterAsyncWorkDrainFailure, + ) + } + return __rollbackWasmEnvForWasiInitialization() +} + +let __wasiModule +let __napiModule + +try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareWasmEnvCleanup, + __isPreparingWasmEnvCleanup, + ) + __emnapiContext.suppressDestroy() + + ;({ + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, + } = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [ + __captureWasiThreadManager, + __emnapiAsyncWorkPlugin, + __emnapiTSFNPlugin, + ], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + __publishWasiDispose(__napiModule.exports) + // The default export hands out this object; a named module export does not + // travel with it, so carry the marker on the binding itself too. After the + // host install, which hands the same object to addon-provided registration + // functions that may put anything on it, and inside this `try`, so a claimed + // name fails the load through the rollback below rather than past it. + __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) +} catch (error) { + const cleanupErrors = await __rollbackWasiInitialization() + throw __attachCleanupErrors(error, cleanupErrors) +} +export default __napiModule.exports +export const LegalCommentsMode = __napiModule.exports.LegalCommentsMode +export const minify = __napiModule.exports.minify +export const minifySync = __napiModule.exports.minifySync +export const Severity = __napiModule.exports.Severity diff --git a/napi/minify/minify.wasip1-deferred.d.ts b/napi/minify/minify.wasip1-deferred.d.ts new file mode 100644 index 0000000000..75ab618d51 --- /dev/null +++ b/napi/minify/minify.wasip1-deferred.d.ts @@ -0,0 +1,83 @@ +export type WasiBinding = typeof import('./minify.wasip1.cjs') + +export type WasiModuleInput = + | WebAssembly.Module + | PromiseLike + +/** Run the instance on a linear memory the caller allocated. */ +export interface WasiCallerMemoryOptions { + /** + * A caller-allocated linear memory for this instance. It must be unshared + * and created in this loader's own realm — the WASI and emnapi layers + * underneath identify a Memory with a realm-local `instanceof`, so one from + * a `node:vm` context or another frame is rejected. It is single-use: once + * a validated initialization attempt has begun, the same Memory cannot be + * passed again — including after that attempt failed, and after the instance + * was disposed. + */ + memory: WebAssembly.Memory + /** Not available beside `memory`: the loader allocates neither. */ + initialMemoryPages?: never + /** Not available beside `memory`: the loader allocates neither. */ + maximumMemoryPages?: never +} + +/** Let the loader allocate the linear memory, optionally sized. */ +export interface WasiAllocatedMemoryOptions { + /** Not available beside the page counts: they size the loader's own Memory. */ + memory?: never + /** @default WASM_MEMORY.initialPages */ + initialMemoryPages?: number + /** @default WASM_MEMORY.maximumPages */ + maximumMemoryPages?: number +} + +/** + * Either memory form, never a mix of the two: the loader throws a TypeError + * on `memory` beside a page count. `{}` and an omitted argument select the + * loader defaults. + */ +export type WasiInstanceOptions = + | WasiCallerMemoryOptions + | WasiAllocatedMemoryOptions + +export interface WasiRuntimeStats { + /** Instances created by this evaluated loader module, not process-wide. */ + createdInstances: number + /** Created instances whose dispose() has not completed. */ + liveInstances: number + /** Declared initial address space, not committed memory. */ + declaredInitialMemoryBytes: number +} + +export interface WasiInstance { + readonly exports: WasiBinding + /** This instance's linear memory. Claimed, so it cannot start another one. */ + readonly memory: WebAssembly.Memory + /** Current linear-memory size; 0 once dispose() has completed. */ + readonly memoryBytes: number + readonly disposed: boolean + dispose(): Promise +} + +/** The memory descriptor compiled into this loader. */ +export const WASM_MEMORY: Readonly<{ + initialPages: number + maximumPages: number + pageBytes: number + initialBytes: number + maximumBytes: number +}> + +export function getDeferredRuntimeStats(): Readonly + +export function instantiate(wasmInput: WasiModuleInput): Promise +export function createInstance( + wasmInput: WasiModuleInput, + options?: WasiInstanceOptions, +): Promise +/** Dispose the singleton and retry retained failed-initialization cleanup. */ +export function dispose(): Promise + +/** The WASI flavor this deferred loader instantiates. */ +export declare const __napiBindingTarget: 'wasm32-wasip1' diff --git a/napi/minify/minify.wasip1-deferred.js b/napi/minify/minify.wasip1-deferred.js new file mode 100644 index 0000000000..b39672b06c --- /dev/null +++ b/napi/minify/minify.wasip1-deferred.js @@ -0,0 +1,1488 @@ +import { + emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin as __emnapiTSFNPlugin, + instantiateNapiModule as __emnapiInstantiateNapiModule, + WASI as __WASI, +} from '@napi-rs/wasm-runtime' +import { createContext as __emnapiCreateContext } from '@emnapi/runtime' + + +export const WASM_MEMORY = Object.freeze({ + initialPages: 1024, + maximumPages: 65536, + pageBytes: 65536, + initialBytes: 1024 * 65536, + maximumBytes: 65536 * 65536, +}) + +let __createdInstances = 0 +let __liveInstances = 0 + +/** + * Counters for instances created by THIS module evaluation, not process-wide: + * a second bundled copy of this loader keeps its own. Only successfully + * created instances are counted, and `liveInstances` drops when an instance's + * `dispose()` resolves. + * + * `declaredInitialMemoryBytes` is declared address space, not a host's + * committed-memory metric; pair it with host telemetry rather than treating it + * as a quota. + */ +export function getDeferredRuntimeStats() { + return Object.freeze({ + createdInstances: __createdInstances, + liveInstances: __liveInstances, + declaredInitialMemoryBytes: WASM_MEMORY.initialBytes, + }) +} + +const __arrayBufferByteLengthGetter = Object.getOwnPropertyDescriptor( + ArrayBuffer.prototype, + 'byteLength', +).get +const __memoryBufferGetter = Object.getOwnPropertyDescriptor( + WebAssembly.Memory.prototype, + 'buffer', +).get +// One managed initialization per Memory, success or failure: an attempt that +// throws may already have written into linear memory, so the bytes are not a +// clean slate for a second instance. Module-local, like the counters above. +const __claimedMemories = new WeakSet() + +function __resolveInstanceMemory(__options) { + const __provided = __options == null ? undefined : __options.memory + if (__provided === undefined || __provided === null) { + // Page counts are handed to the engine unvalidated: it already rejects a + // negative, over-4GiB or below-maximum value with a precise message, and a + // second set of bounds here would only drift from it. + const __allocated = new WebAssembly.Memory({ + initial: + __options != null && __options.initialMemoryPages !== undefined + ? __options.initialMemoryPages + : WASM_MEMORY.initialPages, + maximum: + __options != null && __options.maximumMemoryPages !== undefined + ? __options.maximumMemoryPages + : WASM_MEMORY.maximumPages, + }) + // Claimed like a caller-provided one. The handle publishes it as + // `instance.memory`, so handing it back to `createInstance()` is as easy + // as passing your own twice, and it would put two live instances on one + // linear memory: each initialization rewrites the emnapi/WASI state the + // other is still running on. + __claimedMemories.add(__allocated) + return __allocated + } + if ( + __options.initialMemoryPages !== undefined || + __options.maximumMemoryPages !== undefined + ) { + throw new TypeError( + 'Pass either memory or initialMemoryPages/maximumMemoryPages, not both', + ) + } + let __buffer + try { + // Brand check: the getter throws for anything that is not a genuine + // WebAssembly.Memory, including a cross-realm look-alike object. + __buffer = Reflect.apply(__memoryBufferGetter, __provided, []) + } catch { + throw new TypeError('memory must be an unshared WebAssembly.Memory') + } + try { + // Throws for a SharedArrayBuffer. This loader has no threads, and shared + // growth does not detach: external views handed to the addon would + // silently outlive the bytes they describe. + Reflect.apply(__arrayBufferByteLengthGetter, __buffer, []) + } catch { + throw new TypeError( + 'The deferred loader requires an unshared WebAssembly.Memory', + ) + } + // The intrinsic getters above accept a genuine Memory from ANY realm, but + // the loader's dependencies do not: `WASI.setMemory` in + // `@napi-rs/wasm-runtime` and emnapi identify a Memory with a realm-local + // `instanceof`. A Memory built in another realm (a `node:vm` context, a + // same-origin iframe) would pass every check here and only fail deep inside + // initialization. Reject it up front, and before the claim below, so the + // caller keeps it usable in the realm that made it. + if (!(__provided instanceof WebAssembly.Memory)) { + throw new TypeError( + 'memory must be a WebAssembly.Memory created in the same realm as this loader', + ) + } + if (__claimedMemories.has(__provided)) { + throw new TypeError( + 'This WebAssembly.Memory has already been used for a deferred initialization attempt and cannot be reused, including after a failed initialization or a disposal', + ) + } + // Last step, after every check: a rejected option bag must leave the Memory + // unclaimed, or a caller could not fix the call and retry with it. + __claimedMemories.add(__provided) + return __provided +} + +export const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +/** + * Deferred, workerd-safe instantiation: no top-level I/O, no compile-from-bytes. + * Accepts ONLY a precompiled WebAssembly.Module, or a Promise resolving to one + * (e.g. `import mod from './minify.wasm32-wasip1.wasm'` under a CompiledWasm + * module rule / wrangler module import). Byte buffers, URLs and Response + * objects are rejected: they require dynamic Wasm compilation, which + * Cloudflare Workers disallows. + */ +async function __resolveModule(__wasmInput) { + const __module = await __wasmInput + // Brand check, not `instanceof`: `WebAssembly.Module.imports` throws unless + // its argument is a genuine WebAssembly.Module, so prototype-spoofed byte + // buffers are rejected while cross-realm Module instances are accepted. + try { + WebAssembly.Module.imports(__module) + } catch { + throw new TypeError( + "instantiate() and createInstance() expect a precompiled WebAssembly.Module (or a Promise resolving to one), " + + "e.g. import mod from './minify.wasm32-wasip1.wasm' under a CompiledWasm module rule / wrangler module import. " + + "Byte buffers, URLs and Response objects require dynamic Wasm compilation, which Cloudflare Workers disallows.", + ) + } + return __module +} + +let __normalizedModules + +function __rememberNormalizedModule(__module, __normalizedModule) { + if (!__normalizedModules) { + __normalizedModules = new WeakMap() + } + __normalizedModules.set(__module, __normalizedModule) + return __normalizedModule +} + +async function __normalizeModuleForEmnapi(__module) { + if (__module instanceof WebAssembly.Module) { + return __module + } + if (__normalizedModules) { + const __normalizedModule = __normalizedModules.get(__module) + if (__normalizedModule) { + return __normalizedModule + } + } + // @emnapi/core currently performs realm-local `instanceof` checks after + // accepting the module. Structured cloning preserves compiled code without + // compiling bytes and produces a Module owned by the current realm. + if (typeof structuredClone === 'function') { + try { + const __normalizedModule = structuredClone(__module) + if (__normalizedModule instanceof WebAssembly.Module) { + return __rememberNormalizedModule(__module, __normalizedModule) + } + } catch {} + } + // MessageChannel uses the same structured-clone semantics and covers older + // browser/Node hosts that expose it but not the structuredClone function. + if (typeof MessageChannel === 'function') { + let __channel + try { + __channel = new MessageChannel() + const __normalizedModule = await new Promise((resolve, reject) => { + __channel.port1.onmessage = (event) => resolve(event.data) + __channel.port1.onmessageerror = () => + reject(new TypeError('Failed to clone WebAssembly.Module')) + try { + __channel.port2.postMessage(__module) + } catch (error) { + reject(error) + } + }) + if (__normalizedModule instanceof WebAssembly.Module) { + return __rememberNormalizedModule(__module, __normalizedModule) + } + } catch { + } finally { + try { + __channel?.port1.close() + } catch {} + try { + __channel?.port2.close() + } catch {} + } + } + // Last-resort compatibility for genuine, extensible foreign Modules. + try { + Object.setPrototypeOf(__module, WebAssembly.Module.prototype) + } catch {} + if (__module instanceof WebAssembly.Module) { + return __module + } + throw new TypeError( + 'This host cannot normalize a cross-realm WebAssembly.Module; ' + + 'provide structuredClone or MessageChannel support.', + ) +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __captureEmnapiAutoDestroyListener(__process) { + if ( + !__process || + typeof __process.prependListener !== 'function' || + typeof __process.removeListener !== 'function' + ) { + return + } + let __autoDestroyListener + const __captureListener = (__event, __listener) => { + if (__event === 'beforeExit' && __autoDestroyListener === undefined) { + __autoDestroyListener = __listener + } + } + try { + // Run before existing newListener hooks so a hook that registers its own + // beforeExit listener cannot be mistaken for emnapi's registration. + __process.prependListener('newListener', __captureListener) + } catch { + return + } + return () => { + try { + __process.removeListener('newListener', __captureListener) + } catch {} + if (__autoDestroyListener !== undefined) { + try { + __process.removeListener('beforeExit', __autoDestroyListener) + } catch {} + } + } +} + +function __attachCleanupError(__error, __cleanupError) { + try { + if ( + __error && + (typeof __error === 'object' || typeof __error === 'function') && + __error.cause === undefined + ) { + __error.cause = __cleanupError + } + } catch {} +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (__callback) { + setImmediate(__callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (__callback) { + const __channel = new __MessageChannel() + __channel.port1.onmessage = function () { + __channel.port1.onmessage = null + try { + __channel.port1.close() + } catch {} + try { + __channel.port2.close() + } catch {} + __callback() + } + __channel.port2.postMessage(null) + } + } + return function (__callback) { + setTimeout(__callback, 0) + } +})() + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + */ +function __drainWasmEnvCleanup(__instance) { + const __pending = __instance?.exports.napi_wasm_env_cleanup_pending + const __observable = typeof __pending === 'function' + if (__observable) { + let __queued + try { + __queued = __pending() + } catch { + return + } + if (!__queued) { + return + } + } + const __limit = __observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + return (async () => { + let __queued = 0 + for (let __turn = 0; __turn < __limit; __turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!__observable) { + continue + } + try { + __queued = __pending() + } catch { + return + } + if (!__queued) { + return + } + } + if (!__observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: `__prepareForDisposal` + // leaves its drained flag unset, dispose() (and the instantiation-failure + // path) decline to destroy, and a later dispose() runs the drain again — by + // which time the queue has usually been delivered. A counter that is + // somehow stuck nonzero therefore costs each attempt at most another + // bounded wait and a rejection, never a stranded promise; the managed + // beforeExit destroyer still reclaims the context. + const __drainError = new Error( + 'the wasm environment still reports ' + + __queued + + ' queued settlement(s) after ' + + __limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + __drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw __drainError + })() +} + +// A real, *referenced* timer for the async-work wait below, which polls the +// addon rather than interleaving with the @emnapi/core dispatch: a zero-delay +// macrotask there would spin the loop instead of yielding it. Falls back to the +// macrotask scheduler on a host without timers. +function __scheduleTimer(__callback, __delay) { + const __setTimer = globalThis.setTimeout + if (typeof __setTimer !== 'function') { + __scheduleMacrotask(__callback) + return + } + try { + __setTimer(__callback, __delay) + } catch { + __scheduleMacrotask(__callback) + } +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this instance's outstanding `napi_async_work` before the teardown + * that would strand it. + * + * The same hole the eager loaders had, and the same fix: the environment + * cleanup barrier covers promise settlements queued on the threadsafe-function + * queue and says nothing about `napi_async_work`, so destroying the context + * with a work still outstanding leaves its completion callback with nowhere to + * run and its promise unsettled forever. + * + * This flavor is threadless, so `compute` runs on the JavaScript thread inside + * the macrotask that dequeued it: while this is running, any outstanding work + * is queued rather than executing, and `napi_wasm_cancel_pending_async_work` + * can take all of it. The poll is still what decides when the drain is done — + * cancellation delivers those completions from a later macrotask, not + * synchronously. + * + * Per instance, from that instance's own exports: two instances of this loader + * have separate registries and must not wait on each other. + * + * Both exports are optional, so an addon built against a napi crate that + * predates them keeps the previous behavior. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. No deadline: giving up would destroy the + * environment with a completion callback still owed. + */ +function __drainInstanceAsyncWork(__instance) { + const __exports = __instance?.exports + const __pending = __exports?.napi_wasm_async_work_pending + const __cancelPending = __exports?.napi_wasm_cancel_pending_async_work + if (typeof __pending !== 'function' || typeof __cancelPending !== 'function') { + return + } + + const __readPending = () => { + try { + return __pending() + } catch (__error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition and + // best-effort is the honest answer. Anything else means the export is not + // what this loader thinks it is — a defect worth surfacing. + if (__error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw __error + } + } + + if (!__readPending()) { + return + } + try { + __cancelPending() + } catch { + // Cancellation only bounds the wait. Failing it means waiting for the queue + // to run instead, which the poll below already does. + } + if (!__readPending()) { + return + } + + return (async () => { + while (__readPending()) { + await new Promise((__resolve) => { + __scheduleTimer(__resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })() +} + +function __createLifecycleReentryError(__operation) { + const __error = new Error( + __operation + + '() cannot run while an emnapi Context.destroy() call is still active; await the original cleanup promise instead.', + ) + __error.code = 'ERR_NAPI_WASI_LIFECYCLE_REENTRY' + return __error +} + +const __managedEmnapiContextDestroyers = new Set() +let __managedCleanupProcess +let __managedBeforeExitListener +let __managedDestroyPromise +let __managedDestroyersInFlight +let __managedBeforeExitRegistrationRetryCount = 0 +let __managedBeforeExitRegistrationRetryScheduled = false +let __moduleLifecycleDestroyDepth = 0 + +function __removeManagedEmnapiCleanupListeners() { + const __process = __managedCleanupProcess + const __beforeExitListener = __managedBeforeExitListener + __managedCleanupProcess = undefined + __managedBeforeExitListener = undefined + __managedBeforeExitRegistrationRetryCount = 0 + if (__process && __beforeExitListener) { + try { + __process.removeListener('beforeExit', __beforeExitListener) + } catch {} + } +} + +function __scheduleManagedBeforeExitListenerRegistration() { + if ( + !__managedCleanupProcess || + __managedBeforeExitListener || + __managedEmnapiContextDestroyers.size === 0 || + __managedBeforeExitRegistrationRetryScheduled || + __managedBeforeExitRegistrationRetryCount >= 3 + ) { + return + } + __managedBeforeExitRegistrationRetryScheduled = true + __managedBeforeExitRegistrationRetryCount++ + queueMicrotask(() => { + __managedBeforeExitRegistrationRetryScheduled = false + if ( + !__managedCleanupProcess || + __managedBeforeExitListener || + __managedEmnapiContextDestroyers.size === 0 + ) { + return + } + try { + __registerManagedBeforeExitListener() + } catch {} + }) +} + +function __registerManagedBeforeExitListener() { + if (!__managedCleanupProcess || __managedBeforeExitListener) { + return + } + try { + __managedCleanupProcess.once( + 'beforeExit', + __destroyManagedEmnapiContextsBeforeExit, + ) + } catch (error) { + __scheduleManagedBeforeExitListenerRegistration() + throw error + } + __managedBeforeExitListener = __destroyManagedEmnapiContextsBeforeExit + __managedBeforeExitRegistrationRetryCount = 0 +} + +function __settleManagedEmnapiContextDestroy(__promise) { + if (__managedDestroyPromise === __promise) { + __managedDestroyPromise = undefined + __managedDestroyersInFlight = undefined + } + if (__managedEmnapiContextDestroyers.size === 0) { + __removeManagedEmnapiCleanupListeners() + return + } + try { + __registerManagedBeforeExitListener() + } catch {} +} + +function __destroyManagedEmnapiContexts(__excludedDestroyers) { + if (__managedDestroyPromise) { + return __managedDestroyPromise + } + const __destroyers = Array.from(__managedEmnapiContextDestroyers).filter( + (__destroy) => !__excludedDestroyers?.has(__destroy), + ) + if (__destroyers.length === 0) { + return Promise.resolve() + } + let __resolveDestroy + let __rejectDestroy + const __promise = new Promise((resolve, reject) => { + __resolveDestroy = resolve + __rejectDestroy = reject + }) + __managedDestroyPromise = __promise + __managedDestroyersInFlight = new Set(__destroyers) + void Promise.all( + __destroyers.map((__destroy) => { + try { + return Promise.resolve(__destroy()).then( + () => ({ failed: false }), + (error) => ({ failed: true, error }), + ) + } catch (error) { + return { failed: true, error } + } + }), + ).then((__results) => { + let __primaryError + let __failed = false + for (const __result of __results) { + if (!__result.failed) { + continue + } + if (!__failed) { + __failed = true + __primaryError = __result.error + } else { + __attachCleanupError(__primaryError, __result.error) + } + } + if (__failed) { + __rejectDestroy(__primaryError) + } else { + __resolveDestroy() + } + }, __rejectDestroy) + void __promise.then( + () => { + __settleManagedEmnapiContextDestroy(__promise) + }, + () => { + __settleManagedEmnapiContextDestroy(__promise) + }, + ) + return __promise +} + +async function __drainManagedEmnapiContexts(__excludedDestroyers) { + const __attemptedDestroyers = new Set(__excludedDestroyers) + let __primaryError + let __failed = false + while (true) { + let __promise = __managedDestroyPromise + let __destroyers = __managedDestroyersInFlight + if (!__promise) { + __promise = __destroyManagedEmnapiContexts(__attemptedDestroyers) + __destroyers = __managedDestroyersInFlight + if (!__destroyers) { + break + } + } + for (const __destroy of __destroyers) { + __attemptedDestroyers.add(__destroy) + } + try { + await __promise + } catch (error) { + if (!__failed) { + __failed = true + __primaryError = error + } else { + __attachCleanupError(__primaryError, error) + } + } + } + if (__failed) { + throw __primaryError + } +} + +function __destroyManagedEmnapiContextsBeforeExit() { + // A once listener is consumed before Node invokes it, including when another + // cleanup batch is still pending. + __managedBeforeExitListener = undefined + if (__managedDestroyPromise) { + return + } + void __destroyManagedEmnapiContexts().catch((error) => { + queueMicrotask(() => { + throw error + }) + }) +} + +function __registerManagedEmnapiContext(__process, __destroy) { + __managedEmnapiContextDestroyers.add(__destroy) + if ( + !__managedCleanupProcess && + __process && + typeof __process.once === 'function' && + typeof __process.removeListener === 'function' + ) { + __managedCleanupProcess = __process + } + let __registered = true + return () => { + if (!__registered) { + return + } + __registered = false + __managedEmnapiContextDestroyers.delete(__destroy) + if (__managedEmnapiContextDestroyers.size === 0) { + __removeManagedEmnapiCleanupListeners() + } + } +} + +async function __createManagedEmnapiContext( + __prepareEnvCleanup, + __isPreparingEnvCleanup, +) { + const __process = + typeof process === 'object' && process !== null ? process : undefined + const __finishAutoDestroyCapture = + __captureEmnapiAutoDestroyListener(__process) + let __emnapiContext + let __contextInitializationError + let __contextInitializationFailed = false + try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareEnvCleanup, + __isPreparingEnvCleanup, + ) + // emnapi 2.x still registers an unconditional process.once('beforeExit') + // auto-destroy listener on Node hosts, and suppressDestroy() only + // neutralizes its callback without removing it. This loader must stay + // side-effect free per instance, so the listener is captured and removed; + // suppressDestroy() remains the safety net when removal is unavailable. + __emnapiContext.suppressDestroy() + } catch (error) { + __contextInitializationError = error + __contextInitializationFailed = true + } finally { + // Remove only the exact emnapi callback captured above. + __finishAutoDestroyCapture?.() + } + if (__emnapiContext === undefined) { + throw __contextInitializationError + } + let __disposed = false + let __destroying = false + let __destroyPromise + let __cleanupRegistered = false + let __unregisterCleanup + const __destroy = (__blocksModuleLifecycle = false) => { + if (__disposed) { + return + } + if (__destroying) { + throw __createLifecycleReentryError('dispose') + } + if (__destroyPromise) { + return __destroyPromise + } + __destroying = true + let __result + const __finishDestroyInvocation = () => { + __destroying = false + } + const __finishModuleLifecycleDestroy = () => { + if (__blocksModuleLifecycle) { + __blocksModuleLifecycle = false + __moduleLifecycleDestroyDepth-- + } + } + if (__blocksModuleLifecycle) { + __moduleLifecycleDestroyDepth++ + } + try { + // Context.destroy() disables JS before cleanup hooks run, so settle + // runtime-owned promises while this environment can still call JS. + __prepareEnvCleanup?.() + if (__isPreparingEnvCleanup?.()) { + // Reached from inside the barrier, so `Context.destroy()` below would + // hit the wrapper's in-flight no-op. Recording that as a completed + // destroy is what makes the frame that *did* start the barrier skip the + // real one afterwards, leaving the context retained with its cleanup + // hooks unrun. Refuse instead: nothing is flagged, the context stays + // registered for managed beforeExit cleanup, and a later destroy still + // works. dispose() coalesces reentrancy before it can get here, so this + // is the backstop for any other caller that manages to. + throw __createLifecycleReentryError('dispose') + } + __result = __emnapiContext.destroy() + } catch (error) { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + throw error + } + let __then + try { + if ( + __result !== null && + (typeof __result === 'object' || typeof __result === 'function') + ) { + __then = __result.then + } + } catch (error) { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + throw error + } + if (typeof __then === 'function') { + let __resolveResult + let __rejectResult + const __resultPromise = new Promise((resolve, reject) => { + __resolveResult = resolve + __rejectResult = reject + }) + const __promise = __resultPromise.then( + (value) => { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __disposed = true + __destroyPromise = undefined + __unregisterCleanup?.() + return value + }, + (error) => { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __destroyPromise = undefined + throw error + }, + ) + __destroyPromise = __promise + try { + Reflect.apply(__then, __result, [__resolveResult, __rejectResult]) + } catch (error) { + __rejectResult(error) + } + return __promise + } + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __disposed = true + __unregisterCleanup?.() + } + const __destroyForModuleLifecycle = () => __destroy(true) + const __registerCleanup = ( + __beforeExitDestroy = __destroyForModuleLifecycle, + ) => { + if (__cleanupRegistered || __disposed) { + return + } + __unregisterCleanup = __registerManagedEmnapiContext( + __process, + __beforeExitDestroy, + ) + __cleanupRegistered = true + __registerManagedBeforeExitListener() + } + if (__contextInitializationFailed) { + let __registrationError + let __registrationFailed = false + try { + __registerCleanup() + } catch (error) { + __attachCleanupError(__contextInitializationError, error) + __registrationError = error + __registrationFailed = true + } + try { + await __destroyForModuleLifecycle() + } catch (error) { + __attachCleanupError( + __registrationFailed + ? __registrationError + : __contextInitializationError, + error, + ) + try { + __registerManagedBeforeExitListener() + } catch {} + } + throw __contextInitializationError + } + return { + context: __emnapiContext, + destroy: __destroy, + destroyForModuleLifecycle: __destroyForModuleLifecycle, + registerCleanup: __registerCleanup, + } +} + +async function __createInstance( + __wasmInput, + __options, + __beforeExitDestroy, + __onManagedDestroyer, +) { + const __module = await __resolveModule(__wasmInput) + const __emnapiModule = await __normalizeModuleForEmnapi(__module) + const __wasi = new __WASI({ + version: 'preview1', + }) + // The wasm module is linked with `--import-memory`, so a Memory must be + // provided. It is resolved here in function scope (workerd bans global scope + // allocation) and is never shared (no threads, no SharedArrayBuffer). + // Resolve it before the emnapi context so a rejected option bag or a host + // memory-limit failure cannot leak a context that never reaches + // instantiation. + const __wasmMemory = __resolveInstanceMemory(__options) + let __lifecycleState = 'pending' + let __destroyEmnapiContext + let __destroyOwnedContext + let __destroyManagedOwnedContext + let __napiInstance + let __wasmEnvCleanupRan = false + let __wasmEnvCleanupPrepared = false + let __wasmEnvCleanupPreparing = false + let __wasmEnvCleanupDrained = false + let __wasmEnvCleanupDrainPromise + const __isPreparingEnvCleanup = () => __wasmEnvCleanupPreparing + const __prepareEnvCleanup = () => { + if (__wasmEnvCleanupPrepared || __wasmEnvCleanupPreparing) { + return + } + const __prepareWasmEnvCleanup = + __napiInstance?.exports.napi_prepare_wasm_env_cleanup + if (typeof __prepareWasmEnvCleanup === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __wasmEnvCleanupPreparing = true + try { + __prepareWasmEnvCleanup() + } finally { + __wasmEnvCleanupPreparing = false + } + __wasmEnvCleanupRan = true + } + __wasmEnvCleanupPrepared = true + } + // The barrier + settlement drain, hoisted out of the context destroyer so the + // drain can yield without widening the destroyer's reentry window. Both + // yielding paths run it — dispose() and the initialization-failure rollback. + // The destroyer still runs the barrier itself (idempotently) for the one path + // that cannot yield: managed beforeExit cleanup of an instance whose rollback + // is being retried. + // + // "Drained" is recorded only once a wait has actually finished. Scheduling a + // macrotask can fail — a host-provided or patched `setImmediate` that throws + // is enough — and dispose() stays retryable after it rejects, so marking the + // drain complete up front would make the retry skip it and destroy the context + // with the barrier's settlements still queued. + const __prepareForDisposal = () => { + if (__wasmEnvCleanupDrained) { + return + } + if (__wasmEnvCleanupDrainPromise) { + return __wasmEnvCleanupDrainPromise + } + __prepareEnvCleanup() + if (!__wasmEnvCleanupRan) { + return + } + const __drained = __drainWasmEnvCleanup(__napiInstance) + if (!__drained || typeof __drained.then !== 'function') { + __wasmEnvCleanupDrained = true + return + } + const __tracked = __drained.then( + (__value) => { + __wasmEnvCleanupDrained = true + __wasmEnvCleanupDrainPromise = undefined + return __value + }, + (__error) => { + __wasmEnvCleanupDrainPromise = undefined + throw __error + }, + ) + __wasmEnvCleanupDrainPromise = __tracked + return __tracked + } + let __disposed = false + const __runInstanceDisposal = async () => { + if (__lifecycleState !== 'failed') { + __lifecycleState = 'disposal' + } + // Outstanding async work first, while the environment is still completely + // live: its completion callbacks run addon code, and the barrier and + // `Context.destroy()` below each take that away. Undefined unless work is + // outstanding. + const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance) + if (__asyncWorkDrained) { + await __asyncWorkDrained + } + // Then settle what the barrier cancelled, before the environment stops + // accepting JavaScript calls. Undefined unless something is queued, so + // an idle disposal is not delayed by a single turn. + const __drained = __prepareForDisposal() + if (__drained) { + await __drained + } + const __result = await (__beforeExitDestroy + ? __destroyManagedOwnedContext() + : __destroyOwnedContext()) + // Only a completed destroy retires the instance; a throw above leaves + // the counter untouched so a retried dispose() cannot double-decrement. + if (!__disposed) { + __disposed = true + __liveInstances -= 1 + } + return __result + } + let __instanceDisposePromise + /** + * The disposal frame runs the barrier, and the barrier settles the promises it + * cancels synchronously — so a promise hook firing inside it can call this + * same instance's dispose() again while the first call is still in its drain. + * That nested call finds the barrier flagged in flight, prepares nothing, + * drains nothing, and falls straight through to the context destroyer, whose + * `Context.destroy()` hits the wrapper's in-flight no-op. It would record a + * destruction that never happened, and the outer frame would then skip the + * real one: both disposals resolve, no cleanup hook runs, the context stays + * retained. + * + * Memoize before any of that starts, exactly like the eager loaders' + * `__disposeWasiBinding`, so there is only ever one disposal frame per + * instance and a reentrant caller awaits it instead of racing it. Cleared on + * rejection: a drain that failed has to stay retryable. + */ + const __disposeInstance = () => { + if (__instanceDisposePromise) { + return __instanceDisposePromise + } + let __resolveDispose + let __rejectDispose + const __disposePromise = new Promise((__resolve, __reject) => { + __resolveDispose = __resolve + __rejectDispose = __reject + }) + __instanceDisposePromise = __disposePromise + __runInstanceDisposal().then(__resolveDispose, (__error) => { + __instanceDisposePromise = undefined + __rejectDispose(__error) + }) + return __disposePromise + } + const __destroyBeforeExit = __beforeExitDestroy + ? async () => { + if (__lifecycleState === 'failed') { + await __destroyManagedOwnedContext() + return + } + __lifecycleState = 'disposal' + try { + await __beforeExitDestroy() + } catch (error) { + if (__lifecycleState !== 'failed') { + throw error + } + // The singleton's initialization rejection is already observable + // through instantiate() and dispose(). Managed beforeExit cleanup + // owns only context destruction, including retrying a failed rollback. + await __destroyManagedOwnedContext() + } + } + : undefined + const { + context: __emnapiContext, + destroy, + destroyForModuleLifecycle, + registerCleanup: __registerCleanup, + } = await __createManagedEmnapiContext( + __prepareEnvCleanup, + __isPreparingEnvCleanup, + ) + __destroyEmnapiContext = destroy + __destroyOwnedContext = () => __destroyEmnapiContext() + __destroyManagedOwnedContext = destroyForModuleLifecycle + try { + if (__destroyBeforeExit) { + __onManagedDestroyer(__destroyBeforeExit) + await __registerCleanup(__destroyBeforeExit) + } + let __napiModule + ;({ + instance: __napiInstance, + napiModule: __napiModule, + } = await __emnapiInstantiateNapiModule(__emnapiModule, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + // `instantiate()` and `createInstance().exports` hand out this object; a + // named module export does not travel with it. After the instance host + // install, which hands the same object to addon-provided registration + // functions that may put anything on it, and inside this `try`, so a + // claimed name flips `__lifecycleState` to 'failed' and tears the instance + // down rather than escaping a half-built one. + __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) + if (__lifecycleState === 'pending') { + __lifecycleState = 'succeeded' + } + __createdInstances += 1 + __liveInstances += 1 + return { + exports: __napiModule.exports, + get memory() { + return __wasmMemory + }, + get memoryBytes() { + // The Memory outlives the environment, so this stays readable after a + // FAILED dispose() (which leaves the instance undisposed and + // retryable). It reports 0 only once disposal has actually completed. + return __disposed ? 0 : __wasmMemory.buffer.byteLength + }, + get disposed() { + return __disposed + }, + dispose: __disposeInstance, + } + } catch (error) { + __lifecycleState = 'failed' + // Instantiation can fail *after* registration has run, and registration runs + // with a live environment: a module-init hook can start async work and then + // return an error, and the promise it created may already have escaped into + // JavaScript. Settle what the barrier cancels before the context is + // destroyed, exactly like dispose() does; destroying without yielding + // discards the queue with a null env. Undefined unless something is queued, + // so a failure before beforeInit costs no extra turn. + let __settlementsUnreached = false + try { + // Registration can start async work too, and this path destroys the same + // environment its completions need. A drain that cannot finish leaves the + // work outstanding, so it counts as settlements unreached and stops the + // rollback short of destroying, exactly like a failed barrier drain. + const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance) + if (__asyncWorkDrained) { + await __asyncWorkDrained + } + const __drained = __prepareForDisposal() + if (__drained) { + await __drained + } + } catch (drainError) { + __attachCleanupError(error, drainError) + __settlementsUnreached = true + } + let __registrationError + let __registrationFailed = false + if (!__beforeExitDestroy) { + try { + // Independent instances are caller-owned while pending and after + // success. Register only failed rollback so cleanup remains retryable. + await __registerCleanup() + } catch (registrationError) { + __attachCleanupError(error, registrationError) + __registrationError = registrationError + __registrationFailed = true + } + } + if (__settlementsUnreached) { + // The barrier or the drain did not finish, so the settlements it queued + // are still in the threadsafe-function queue. Destroying now runs the + // cleanup hook that drains that queue with a null env and discards it, + // stranding a promise that already escaped into JavaScript — with nothing + // left that could ever settle it. dispose() refuses to destroy for exactly + // this reason (a rejected drain there never reaches the destroy), so this + // path refuses too. + // + // Nothing leaks. The registration just above — and, for the singleton, the + // one made before instantiation — leaves this context in + // `__managedEmnapiContextDestroyers`, so beforeExit destroys it and + // dispose() can retry it. Only the destruction is deferred, and the turns + // that pass in the meantime are exactly what the queue needed. + try { + __registerManagedBeforeExitListener() + } catch {} + throw error + } + try { + await __destroyManagedOwnedContext() + } catch (disposeError) { + // Initialization is the primary failure. Preserve it even if cleanup + // also fails, while retaining the cleanup error when the value is + // extensible and has no existing cause. + __attachCleanupError( + __registrationFailed ? __registrationError : error, + disposeError, + ) + try { + __registerManagedBeforeExitListener() + } catch {} + } + throw error + } +} + +/** + * Create an independent instance. Call and await dispose() when the instance + * is no longer needed so emnapi cleanup hooks run deterministically. + * + * The optional second argument selects this instance's linear memory: either + * `memory` (an unshared, single-use WebAssembly.Memory you allocated) or + * `initialMemoryPages` / `maximumMemoryPages`, never both. Omitted, the + * loader allocates WASM_MEMORY.initialPages..WASM_MEMORY.maximumPages. + * + * A provided Memory must come from this loader's own realm: the WASI and + * emnapi layers underneath identify one with a realm-local `instanceof`, so a + * Memory built in a `node:vm` context or another frame is rejected. Every + * Memory an instance runs on is single-use, the loader-allocated one included: + * `instance.memory` cannot be recycled into a second `createInstance()`. + */ +export async function createInstance(__wasmInput, __options) { + return __createInstance(__wasmInput, __options) +} + +let __defaultModulePromise +let __defaultInstancePromise +let __defaultDisposePromise +let __defaultDisposalStarted = false +const __defaultManagedDestroyers = new WeakMap() +let __moduleDisposePromise + +/** + * Instantiate a module-local singleton. Concurrent and repeated calls + * with the same module share one instance and one Memory allocation. + */ +export function instantiate(__wasmInput) { + const __modulePromise = __resolveModule(__wasmInput) + if (__moduleLifecycleDestroyDepth !== 0) { + void __modulePromise.catch(() => {}) + return Promise.reject(__createLifecycleReentryError('instantiate')) + } + if (__moduleDisposePromise) { + void __modulePromise.catch(() => {}) + return __moduleDisposePromise.then(() => instantiate(__modulePromise)) + } + if (__defaultDisposalStarted) { + // Observe rejected input immediately, but preserve lifecycle ordering and + // error precedence by instantiating only after disposal succeeds. A failed + // disposal retains the old instance only so its cleanup can be retried. + void __modulePromise.catch(() => {}) + const __disposePromise = __defaultDisposePromise ?? dispose() + return __disposePromise.then(() => instantiate(__modulePromise)) + } + if (!__defaultInstancePromise) { + __defaultModulePromise = __modulePromise + const __instancePromise = __modulePromise.then((__module) => + __createInstance( + __module, + undefined, + __disposeDefaultInstance, + (__managedDestroyer) => { + __defaultManagedDestroyers.set( + __instancePromise, + __managedDestroyer, + ) + }, + ), + ) + __defaultInstancePromise = __instancePromise + void __instancePromise.catch(() => { + if (__defaultInstancePromise === __instancePromise) { + __defaultInstancePromise = undefined + __defaultModulePromise = undefined + } + }) + return __instancePromise.then((__instance) => __instance.exports) + } + const __defaultModulePromiseForCall = __defaultModulePromise + const __defaultInstancePromiseForCall = __defaultInstancePromise + return Promise.all([__defaultModulePromiseForCall, __modulePromise]).then( + async ([__defaultModule, __module]) => { + if (__defaultModule !== __module) { + throw new Error( + 'instantiate() already owns a different WebAssembly.Module; call dispose() first or use createInstance() for independent instances.', + ) + } + return (await __defaultInstancePromiseForCall).exports + }, + ) +} + +async function __disposeDefaultInstance(__onDestroy) { + if (__defaultDisposePromise) { + return __defaultDisposePromise + } + const __instancePromise = __defaultInstancePromise + if (!__instancePromise) { + __defaultDisposalStarted = false + return + } + __defaultDisposalStarted = true + const __disposePromise = (async () => { + let __instance + try { + __instance = await __instancePromise + } catch (error) { + const __managedDestroyer = + __defaultManagedDestroyers.get(__instancePromise) + if (__managedDestroyer) { + __onDestroy?.(__managedDestroyer) + } + __defaultManagedDestroyers.delete(__instancePromise) + throw error + } + const __managedDestroyer = + __defaultManagedDestroyers.get(__instancePromise) + if (__managedDestroyer) { + __onDestroy?.(__managedDestroyer) + } + await __instance.dispose() + if (__defaultInstancePromise === __instancePromise) { + __defaultInstancePromise = undefined + __defaultModulePromise = undefined + __defaultDisposalStarted = false + } + __defaultManagedDestroyers.delete(__instancePromise) + })() + __defaultDisposePromise = __disposePromise + try { + await __disposePromise + } finally { + if (__defaultDisposePromise === __disposePromise) { + __defaultDisposePromise = undefined + } + } +} + +async function __dispose() { + let __defaultDisposeError + let __defaultDisposeFailed = false + let __attemptedDefaultDestroyer + try { + await __disposeDefaultInstance((__destroyer) => { + __attemptedDefaultDestroyer = __destroyer + }) + } catch (error) { + __defaultDisposeError = error + __defaultDisposeFailed = true + } + const __excludedDestroyers = new Set() + if (__defaultDisposeFailed && __attemptedDefaultDestroyer) { + __excludedDestroyers.add(__attemptedDefaultDestroyer) + } + try { + await __drainManagedEmnapiContexts(__excludedDestroyers) + } catch (error) { + if (!__defaultDisposeFailed) { + throw error + } + if (error !== __defaultDisposeError) { + __attachCleanupError(__defaultDisposeError, error) + } + } + if (__defaultDisposeFailed) { + throw __defaultDisposeError + } +} + +/** + * Dispose the singleton created by instantiate(). A later call may create a + * fresh instance, including from a different module. This also retries cleanup + * retained after a failed initialization rollback. + */ +export function dispose() { + if (__moduleLifecycleDestroyDepth !== 0) { + return Promise.reject(__createLifecycleReentryError('dispose')) + } + if (__moduleDisposePromise) { + return __moduleDisposePromise + } + let __resolveDispose + let __rejectDispose + const __promise = new Promise((resolve, reject) => { + __resolveDispose = resolve + __rejectDispose = reject + }) + __moduleDisposePromise = __promise + void __dispose().then(__resolveDispose, __rejectDispose) + void __promise.then( + () => { + if (__moduleDisposePromise === __promise) { + __moduleDisposePromise = undefined + } + }, + () => { + if (__moduleDisposePromise === __promise) { + __moduleDisposePromise = undefined + } + }, + ) + return __promise +} diff --git a/napi/minify/minify.wasip1.cjs b/napi/minify/minify.wasip1.cjs new file mode 100644 index 0000000000..7ff372f2bf --- /dev/null +++ b/napi/minify/minify.wasip1.cjs @@ -0,0 +1,1209 @@ +// napi-rs-artifact-metadata:{"version":2,"rootEntry":"index.js","exports":["LegalCommentsMode","minify","minifySync","Severity"],"managedRootEntries":["browser.js","index.js","minify.wasm","minify.debug.wasm"]} +/* eslint-disable */ +/* auto-generated by NAPI-RS */ + +const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +const __nodeFs = require('node:fs') +const __nodePath = require('node:path') +const { WASI: __nodeWASI } = require('node:wasi') + +const { + emnapiAsyncWorkPlugin: __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin: __emnapiTSFNPlugin, + instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync, +} = require('@napi-rs/wasm-runtime') +const { createContext: __emnapiCreateContext } = require('@emnapi/runtime') + +const __cwd = process.cwd() +const __rootDir = __nodePath.parse(__cwd).root +const __hostRoot = + process.platform === 'android' ? __cwd : __rootDir + +const __wasi = new __nodeWASI({ + version: 'preview1', + env: process.env, + preopens: { + [__rootDir]: __hostRoot, + [__hostRoot]: __hostRoot, + }, +}) + +const __wasmMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, +}) + +let __wasmFilePath = __nodePath.join(__dirname, 'minify.wasm32-wasip1.wasm') +const __wasmDebugFilePath = __nodePath.join(__dirname, 'minify.wasm32-wasip1.debug.wasm') + +if (__nodeFs.existsSync(__wasmDebugFilePath)) { + __wasmFilePath = __wasmDebugFilePath +} else if (!__nodeFs.existsSync(__wasmFilePath)) { + const __wasiPackageEntry = require.resolve('@oxc-minify/binding-wasm32-wasip1') + const __packagedWasmFilePath = __nodePath.join( + __nodePath.dirname(__wasiPackageEntry), + 'minify.wasm32-wasip1.wasm', + ) + if (!__nodeFs.existsSync(__packagedWasmFilePath)) { + throw new Error( + '@oxc-minify/binding-wasm32-wasip1 is installed but is missing minify.wasm32-wasip1.wasm.', + ) + } + __wasmFilePath = __packagedWasmFilePath +} + +const __wasmFile = __nodeFs.readFileSync(__wasmFilePath) +let __emnapiContext + +const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose') +const __wasiWorkers = new Set() +// The thread manager has to be reachable *before* anything that can throw +// during load or registration. Initialization can fail after the pool has +// already spawned workers, and the rollback still has to mark their +// terminations as expected — but `__napiModule` is assigned only when +// instantiation RETURNS, so on exactly that path it is still undefined. A +// plugin factory runs while the emnapi module is being created, before the +// wasm is loaded and before any registration function runs, and its context +// carries the very same manager instance. +let __wasiThreadManager + +function __captureWasiThreadManager(context) { + if (context && context.PThread) { + __wasiThreadManager = context.PThread + } + return {} +} + +function __getWasiThreadManager() { + const manager = + __wasiThreadManager !== undefined + ? __wasiThreadManager + : __napiModule + ? __napiModule.PThread + : undefined + if (manager && typeof manager.terminateWorker === 'function') { + return manager + } + return undefined +} +let __napiInstance +let __emnapiContextDestroyed = false +let __emnapiContextDestroyPromise +let __emnapiWasmEnvCleanupPrepared = false +let __emnapiWasmEnvCleanupPreparing = false +let __emnapiWasmEnvCleanupRan = false +let __emnapiWasmEnvCleanupDrained = false +let __emnapiWasmEnvCleanupDrainPromise +let __wasiDisposed = false +let __wasiAsyncWorkDrainPromise +let __wasiDisposePromise +let __completeWasiDisposal = function () {} +// Overridden by loader flavors that have a last-resort reclaim for a rollback +// that stopped short of destroying the context. See +// `__rollbackWasiInitialization`. +let __retainWasiRollbackForRetry = function () {} + +function __isThenable(value) { + return ( + value !== null && + (typeof value === 'object' || typeof value === 'function') && + typeof value.then === 'function' + ) +} + +function __createCleanupError(errors, message) { + if (errors.length === 1) { + return errors[0] + } + const __AggregateError = globalThis.AggregateError + if (typeof __AggregateError === 'function') { + return new __AggregateError(errors, message) + } + const error = new Error(message) + error.errors = errors + return error +} + +function __attachCleanupErrors(error, cleanupErrors) { + if (cleanupErrors.length === 0) { + return error + } + const cleanupError = __createCleanupError( + cleanupErrors, + 'WASI binding cleanup failed', + ) + try { + if ( + error && + (typeof error === 'object' || typeof error === 'function') + ) { + if (error.cause === undefined) { + error.cause = cleanupError + if (error.cause === cleanupError) { + return error + } + } + if (Array.isArray(error.cleanupErrors)) { + error.cleanupErrors.push(cleanupError) + return error + } else { + const attachedCleanupErrors = [cleanupError] + error.cleanupErrors = attachedCleanupErrors + if (error.cleanupErrors === attachedCleanupErrors) { + return error + } + } + } + } catch {} + const aggregate = __createCleanupError( + [error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + try { + aggregate.cause = error + } catch {} + return aggregate +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __isPreparingWasmEnvCleanup() { + return __emnapiWasmEnvCleanupPreparing +} + +function __prepareWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) { + return + } + const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup + if (typeof prepare === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __emnapiWasmEnvCleanupPreparing = true + try { + prepare() + } finally { + __emnapiWasmEnvCleanupPreparing = false + } + __emnapiWasmEnvCleanupRan = true + } + __emnapiWasmEnvCleanupPrepared = true +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (callback) { + setImmediate(callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (callback) { + const channel = new __MessageChannel() + channel.port1.onmessage = function () { + channel.port1.onmessage = null + try { + channel.port1.close() + } catch {} + try { + channel.port2.close() + } catch {} + callback() + } + channel.port2.postMessage(null) + } + } + return function (callback) { + setTimeout(callback, 0) + } +})() + +// A real, *referenced* timer, for waits that must let the whole host make +// progress between looks — the async-work drain polls the addon rather than +// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there +// would spin the loop instead of yielding it. Falls back to the macrotask +// scheduler on a host without timers. +function __scheduleTimer(callback, delay) { + const setTimer = globalThis.setTimeout + if (typeof setTimer !== 'function') { + __scheduleMacrotask(callback) + return + } + try { + setTimer(callback, delay) + } catch { + __scheduleMacrotask(callback) + } +} + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + * + * The "already drained" flag is set only once a wait has actually finished. + * Scheduling a macrotask can fail — a host-provided or patched `setImmediate` + * that throws is enough — and a disposal that rejects stays retryable, so + * marking the drain complete up front would make the retry skip it and destroy + * the context with the barrier's settlements still queued. + * + * A wait that runs out of turns with the counter still nonzero rejects with + * `ERR_NAPI_WASI_CLEANUP_PENDING` for the same reason: at that point + * "finished" is indistinguishable from the stranding above, and destroying + * would discard the very settlement the wait was for. The rejection leaves the + * flag unset and disposal retryable. + */ +function __drainWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupDrained || !__emnapiWasmEnvCleanupRan) { + return + } + if (__emnapiWasmEnvCleanupDrainPromise) { + return __emnapiWasmEnvCleanupDrainPromise + } + const pending = __napiInstance?.exports?.napi_wasm_env_cleanup_pending + const observable = typeof pending === 'function' + if (observable) { + let queued + try { + queued = pending() + } catch { + __emnapiWasmEnvCleanupDrained = true + return + } + if (!queued) { + __emnapiWasmEnvCleanupDrained = true + return + } + } + const limit = observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + const drainPromise = (async () => { + let queued = 0 + for (let turn = 0; turn < limit; turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!observable) { + continue + } + try { + queued = pending() + } catch { + return + } + if (!queued) { + return + } + } + if (!observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: the drained flag stays + // unset, dispose() (and the rollback) decline to destroy, and a later + // dispose() runs the drain again — by which time the queue has usually been + // delivered. A counter that is somehow stuck nonzero therefore costs each + // attempt at most another bounded wait and a rejection, never a stranded + // promise; the process-exit teardown still reclaims the context. + const drainError = new Error( + 'the wasm environment still reports ' + + queued + + ' queued settlement(s) after ' + + limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw drainError + })().then( + (value) => { + // Set only when the wait actually finished AND the queue was seen empty + // (or is unobservable): a drain that timed out with settlements still + // queued rejects above and must stay repeatable. + __emnapiWasmEnvCleanupDrained = true + __emnapiWasmEnvCleanupDrainPromise = undefined + return value + }, + (error) => { + __emnapiWasmEnvCleanupDrainPromise = undefined + throw error + }, + ) + __emnapiWasmEnvCleanupDrainPromise = drainPromise + return drainPromise +} + +function __destroyEmnapiContext() { + if (__emnapiContextDestroyed || __emnapiContext === undefined) { + __emnapiContextDestroyed = true + return + } + if (__emnapiContextDestroyPromise) { + return __emnapiContextDestroyPromise + } + + __prepareWasmEnvCleanup() + const result = __emnapiContext.destroy() + if (!__isThenable(result)) { + __emnapiContextDestroyed = true + return + } + + const destroyPromise = Promise.resolve(result).then( + (value) => { + __emnapiContextDestroyed = true + return value + }, + (error) => { + __emnapiContextDestroyPromise = undefined + throw error + }, + ) + __emnapiContextDestroyPromise = destroyPromise + return destroyPromise +} + +/** + * Holds the event loop open until `work` settles. + * + * Nothing else can: the pool workers are deliberately unreferenced so an idle + * binding cannot keep a process alive, and referencing them again for the + * termination does not hold either — emnapi unreferences a worker the moment it + * reports `async-thread-ready`, which for a worker that was still starting + * lands *after* the termination began. Without a handle of its own, an + * `await dispose()` with nothing else pending exits the process with its + * promise unsettled, and everything after the `await` is skipped. + * + * The timer is cleared as soon as the work settles, so this never outlives the + * disposal that asked for it. + */ +function __keepEventLoopAliveUntil(work) { + const setTimer = globalThis.setInterval + const clearTimer = globalThis.clearInterval + if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') { + return work + } + let timer + try { + timer = setTimer(function () {}, 50) + } catch { + return work + } + const release = function () { + try { + clearTimer(timer) + } catch {} + } + return work.then( + (value) => { + release() + return value + }, + (error) => { + release() + throw error + }, + ) +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this addon's outstanding `napi_async_work` before the teardown that + * would strand it. + * + * `napi_prepare_wasm_env_cleanup` does not cover async work, and nothing about + * it is observable from JavaScript: the threadless archive resolves + * `napi_*_async_work` through the `@emnapi/core` plugins, but the threaded one + * links the C `async_work.c` on the uv threadpool, so there the wasm neither + * imports nor exports those symbols and the only brackets a loader could watch + * (`_emnapi_ctx_*_waiting_request_counter`) are shared with threadsafe + * functions. The addon is the one place both flavors go through, so it answers + * for both, through the same kind of handshake the settlement drain uses: + * + * - `napi_wasm_cancel_pending_async_work()` cancels what no thread has + * started. Those completion callbacks run with `napi_cancelled`, which + * napi-rs turns into a promise rejected with an `AbortError`. + * - `napi_wasm_async_work_pending()` counts what is still owed a completion + * callback. Work already executing refuses cancellation and stays counted + * until it finishes normally — which it can, because this runs before the + * barrier, before `Context.destroy()` and before anything is terminated. + * + * Both exports are optional: an addon built against a napi crate that predates + * them drains nothing and keeps the previous behavior, exactly as the + * `napi_wasm_env_cleanup_pending` handshake degrades. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. The promise it returns otherwise never + * rejects. + * + * The wait has no deadline, and that is the point: giving up would destroy the + * environment with a completion callback still owed, which is the stranding + * this exists to prevent. A task whose `execute` never returns already keeps an + * *undisposed* process alive in exactly the same way, so disposal inherits that + * rather than inventing a bound it cannot honor. + * + * Safe to call from inside a completion callback, which is reachable: settling + * a task runs addon code that can re-enter JavaScript — a setter on the value + * being handed back, a threadsafe-function callback — and that JavaScript can + * call `dispose()`. Two things make it terminate rather than wait on itself: + * + * - The addon keeps a work registered until its completion callback + * *finishes*, so the count read here is at least one and this takes the + * polling path instead of declaring the environment drained and tearing it + * down from inside the frame that is still settling a promise. + * - The poll is a timer, so it cannot run until the callback has returned to + * the host — by which time that work has left the registry. The count the + * next poll reads is the one taken after the callback finished. + * + * `__disposeWasiBinding` hands every caller the same in-flight promise, so the + * nested call joins this disposal rather than starting a second one. + */ +function __drainWasiAsyncWork() { + if (__wasiAsyncWorkDrainPromise !== undefined) { + return __wasiAsyncWorkDrainPromise + } + const exports = __napiInstance?.exports + const pending = exports?.napi_wasm_async_work_pending + const cancelPending = exports?.napi_wasm_cancel_pending_async_work + if (typeof pending !== 'function' || typeof cancelPending !== 'function') { + return + } + + const readPending = () => { + try { + return pending() + } catch (error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition — + // there is nothing left to wait for, and refusing to dispose would only + // keep a dead instance and its stuck counter alive. Best-effort here is + // the honest answer, and it is what disposal did before this drain + // existed. + // + // Only a trap. Anything else means the export is not what this loader + // thinks it is, which is a defect worth surfacing rather than disposing + // over. + if (error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw error + } + } + + if (!readPending()) { + return + } + try { + cancelPending() + } catch { + // Cancellation is an optimization: it bounds the wait by the work already + // executing. Failing it only means waiting for the whole queue instead. + } + if (!readPending()) { + return + } + + const drainPromise = __keepEventLoopAliveUntil( + (async () => { + while (readPending()) { + await new Promise((resolve) => { + __scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })(), + ).then( + () => { + __wasiAsyncWorkDrainPromise = undefined + }, + (error) => { + // A wait that could not run is not a wait that finished. The only way + // here is a host whose timers and macrotask primitives all refuse, and + // the work is still outstanding — reporting success would destroy the + // environment over it, which is the stranding this exists to prevent. + // Reject instead: disposal stays retryable, and the context is not + // destroyed. Clearing the memo first is what makes the retry re-run this. + __wasiAsyncWorkDrainPromise = undefined + throw error + }, + ) + __wasiAsyncWorkDrainPromise = drainPromise + return drainPromise +} + +/** + * `@emnapi/wasi-threads` counts a worker exit as expected only when its own + * thread manager performed the termination. A bare `worker.terminate()` reaches + * the manager's `exit` listener instead, which reports + * `worker (tid = N) sent an error! ... stopped with exit code 1` and rethrows + * inside the emit — aborting the `once('exit')` that backs the terminate + * promise, so disposal never settles and the process dies with an uncaught + * exception. Mark the termination through the manager first. + * + * The manager comes from `__getWasiThreadManager`, not from `__napiModule`: + * the initialization rollback runs on the one path where instantiation never + * returned, so `__napiModule` is still undefined there while the workers it + * spawned are already registered and loaded. + * + * Not `terminateAllThreads()`: that one recreates the pool it just shut down. + */ +function __terminateWasiWorkers() { + const cleanupErrors = [] + const pending = [] + const threadManager = __getWasiThreadManager() + + for (const worker of __wasiWorkers) { + let result + try { + if (threadManager) { + threadManager.terminateWorker(worker) + // `terminateWorker` leaves behind a reporter that logs every message + // still queued on the port, which Node flushes on exit. Nothing is + // listening for those any more. + worker.onmessage = undefined + } + result = worker.terminate() + } catch (error) { + cleanupErrors.push(error) + continue + } + if (__isThenable(result)) { + pending.push( + Promise.resolve(result).then( + () => { + __wasiWorkers.delete(worker) + }, + (error) => { + cleanupErrors.push(error) + }, + ), + ) + } else { + __wasiWorkers.delete(worker) + } + } + + const finish = () => { + if (cleanupErrors.length > 0) { + throw __createCleanupError( + cleanupErrors, + 'Failed to terminate WASI workers', + ) + } + } + return pending.length > 0 + ? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish) + : finish() +} + +function __finishWasiDisposal() { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult).then(__completeWasiDisposal) + } + return __completeWasiDisposal() +} + +function __continueWasiDisposal() { + const destroyResult = __destroyEmnapiContext() + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult).then(__finishWasiDisposal) + } + return __finishWasiDisposal() +} + +function __cleanUpWasmEnvForWasiDisposal() { + // Run the pre-teardown barrier, then let the settlements it queued actually + // reach JavaScript, and only then destroy the environment. Doing these two + // back to back is what strands them. + __prepareWasmEnvCleanup() + const drainResult = __drainWasmEnvCleanup() + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then(__continueWasiDisposal) + } + return __continueWasiDisposal() +} + +function __startWasiDisposal() { + // Outstanding `napi_async_work` goes first, while the environment is still + // completely live: the completion callbacks run addon code, and everything + // after this point takes that away from them — the barrier shuts the async + // runtime down, `Context.destroy()` stops JavaScript calls, and terminating + // the pool threads removes what would have reported the work finished. + const asyncWorkResult = __drainWasiAsyncWork() + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __cleanUpWasmEnvForWasiDisposal, + ) + } + return __cleanUpWasmEnvForWasiDisposal() +} + +/** + * Disposes this generated WASI binding. + * + * Access this function with: + * binding[Symbol.for('napi.rs.wasi.dispose')]() + */ +function __disposeWasiBinding() { + if (__wasiDisposePromise) { + return __wasiDisposePromise + } + if (__wasiDisposed) { + return Promise.resolve() + } + + let resolveDispose + let rejectDispose + const disposePromise = new Promise((resolve, reject) => { + resolveDispose = resolve + rejectDispose = reject + }) + __wasiDisposePromise = disposePromise + + let result + try { + result = __startWasiDisposal() + } catch (error) { + __wasiDisposePromise = undefined + rejectDispose(error) + return disposePromise + } + + Promise.resolve(result).then( + (value) => { + __wasiDisposed = true + resolveDispose(value) + }, + (error) => { + __wasiDisposePromise = undefined + rejectDispose(error) + }, + ) + return disposePromise +} + +function __publishWasiDispose(exports) { + Object.defineProperty(exports, __wasiDisposeSymbol, { + configurable: false, + enumerable: false, + value: __disposeWasiBinding, + writable: false, + }) +} + +function __finishWasiInitializationRollback(cleanupErrors) { + let workerResult + try { + workerResult = __terminateWasiWorkers() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return cleanupErrors + } + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => cleanupErrors) + } + return cleanupErrors +} + +function __destroyContextForWasiRollback(cleanupErrors) { + let destroyResult + try { + destroyResult = __destroyEmnapiContext() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return __finishWasiInitializationRollback(cleanupErrors) + } + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => __finishWasiInitializationRollback(cleanupErrors)) + } + return __finishWasiInitializationRollback(cleanupErrors) +} + +/** + * Leaves a rollback that could not reach the queued settlements undestroyed, and + * hands it to whatever this flavor has that can still reclaim it. + */ +function __retainFailedWasiRollback(cleanupErrors) { + try { + __retainWasiRollbackForRetry() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + } + return cleanupErrors +} + +/** + * Initialization can fail *after* registration has already run, and registration + * runs with a live environment: a module-init hook can start async work and then + * return an error, and the promise it created may already have escaped into + * JavaScript. The barrier cancels that work and *queues* the settlement, so this + * path needs the same drain the ordinary disposal does — destroying without + * yielding discards the queue with a null env and strands the promise. + * + * Stays synchronous when nothing is queued, which covers every failure before + * `beforeInit`: there is no instance to run the barrier on, so nothing to drain. + * + * A barrier or drain that did *not* finish stops the rollback short of + * destroying, which is what `dispose()` already does — a rejected drain there + * never reaches `__continueWasiDisposal`. Destroying anyway is the worse of the + * two trades, and not because of what it saves: + * + * - It cannot deliver the settlements. `Context.destroy()` runs the + * threadsafe function's cleanup hook, which drains the queue with a null env + * and discards it, so a promise that already escaped into JavaScript hangs + * forever with nothing left that could ever settle it. + * - It saves less than it looks. `Context.destroy()` stops JavaScript calls + * and runs cleanup hooks; it does not free the wasm instance or its Memory, + * which this module's scope holds either way. What stopping short retains is + * the emnapi context's bookkeeping and its un-run cleanup hooks. + * - Retry is not theoretical. A rollback that records a cleanup error is + * already kept in the process-wide registry above, so re-`require()`ing this + * file replays it instead of re-instantiating — and the `6e15de6f` flag fix + * means the replay drains again rather than skipping it. Destroying first is + * what makes that retained record useless. + * + * The residual cost is honest: the CJS flavor hands the context to its + * `process.on('exit')` teardown, so a process that never retries still reclaims + * it on the way out. The ESM browser flavor has no equivalent — a module that + * throws while evaluating is permanently errored, so re-importing rethrows + * without re-running this file — and there the context stays until the realm + * goes away. That is the deliberate choice: a hung promise is a silent liveness + * bug with no upper bound, while the retained bookkeeping is bounded by the page. + */ +function __rollbackWasiInitialization() { + // The environment teardown this rollback performs, kept nested so it cannot + // be reached without the async-work drain below running first. + function __rollbackWasmEnvForWasiInitialization() { + const cleanupErrors = [] + let drainResult + let settlementsUnreached = false + try { + __prepareWasmEnvCleanup() + drainResult = __drainWasmEnvCleanup() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + settlementsUnreached = true + } + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then( + () => __destroyContextForWasiRollback(cleanupErrors), + (cleanupError) => { + cleanupErrors.push(cleanupError) + return __retainFailedWasiRollback(cleanupErrors) + }, + ) + } + if (settlementsUnreached) { + return __retainFailedWasiRollback(cleanupErrors) + } + return __destroyContextForWasiRollback(cleanupErrors) + } + + // Same reason as `__startWasiDisposal`: a module-init hook can start async + // work before the load goes on to fail, and this rollback tears down exactly + // what those completions need. Settle them while everything is still live, + // before the barrier and the teardown above take that away. + // + // A drain that could not finish leaves async work possibly outstanding, and + // destroying the context over it would strand exactly what this rollback is + // there to settle. Stop short and retain instead — the same trade + // `__rollbackWasmEnvForWasiInitialization` makes for the settlement drain, so + // the context stays reclaimable by a retry or by this flavor's own + // last-resort teardown. + const __retainAfterAsyncWorkDrainFailure = (cleanupError) => + __retainFailedWasiRollback([cleanupError]) + let asyncWorkResult + try { + asyncWorkResult = __drainWasiAsyncWork() + } catch (cleanupError) { + return __retainAfterAsyncWorkDrainFailure(cleanupError) + } + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __rollbackWasmEnvForWasiInitialization, + __retainAfterAsyncWorkDrainFailure, + ) + } + return __rollbackWasmEnvForWasiInitialization() +} + +const __wasiRollbackRegistrySymbol = Symbol.for('napi.rs.wasi.rollback.registry.v1') +const __wasiRollbackRegistryKey = + typeof __filename === 'string' ? __filename : __wasmFilePath + +function __getWasiRollbackRegistry() { + const existing = process[__wasiRollbackRegistrySymbol] + if (existing !== undefined) { + if (!(existing instanceof Map)) { + throw new TypeError( + 'The process-wide NAPI-RS WASI rollback registry is invalid', + ) + } + return existing + } + const registry = new Map() + Object.defineProperty(process, __wasiRollbackRegistrySymbol, { + configurable: false, + enumerable: false, + value: registry, + writable: false, + }) + return registry +} + +const __wasiRollbackRegistry = __getWasiRollbackRegistry() + +function __completeWasiInitializationRollback(record, cleanupErrors) { + try { + if (cleanupErrors.length === 0) { + if ( + __wasiRollbackRegistry.get(__wasiRollbackRegistryKey) === record + ) { + __wasiRollbackRegistry.delete(__wasiRollbackRegistryKey) + } + return + } + record.error = __attachCleanupErrors(record.error, cleanupErrors) + } catch (cleanupError) { + try { + record.error = __createCleanupError( + [record.error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + } catch {} + } finally { + record.active = false + record.promise = undefined + } +} + +function __runWasiInitializationRollback(record) { + if (record.active) { + return + } + record.active = true + + let rollbackResult + try { + rollbackResult = record.rollback() + } catch (cleanupError) { + __completeWasiInitializationRollback(record, [cleanupError]) + return + } + + if (!__isThenable(rollbackResult)) { + __completeWasiInitializationRollback(record, rollbackResult) + return + } + + record.promise = Promise.resolve(rollbackResult).then( + (cleanupErrors) => { + __completeWasiInitializationRollback(record, cleanupErrors) + }, + (cleanupError) => { + __completeWasiInitializationRollback(record, [cleanupError]) + }, + ) +} + +const __pendingWasiRollback = __wasiRollbackRegistry.get( + __wasiRollbackRegistryKey, +) +if (__pendingWasiRollback !== undefined) { + __runWasiInitializationRollback(__pendingWasiRollback) + throw __pendingWasiRollback.error +} + +let __wasiModule +let __napiModule +let __wasiExitListenerRegistered = false + +function __removeWasiExitListener() { + if ( + __wasiExitListenerRegistered && + typeof process.removeListener === 'function' + ) { + process.removeListener('exit', __disposeWasiBindingAtExit) + } + __wasiExitListenerRegistered = false +} + +function __disposeWasiBindingAtExit() { + __wasiExitListenerRegistered = false + // An 'exit' handler cannot yield, so it cannot wait for queued promise + // settlements the way __startWasiDisposal does — the process is leaving and + // those promises have no observer left anyway. Run the synchronous teardown + // directly. Every step is idempotent, which also makes this the synchronous + // finish for a disposal that is still waiting for its drain. + try { + __destroyEmnapiContext() + } catch {} + try { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + void Promise.resolve(workerResult).catch(() => {}) + } + } catch {} +} + +function __registerWasiExitListener() { + if ( + !__wasiExitListenerRegistered && + typeof process.once === 'function' + ) { + process.once('exit', __disposeWasiBindingAtExit) + __wasiExitListenerRegistered = true + } +} + +__completeWasiDisposal = __removeWasiExitListener +// A rollback that could not reach the queued settlements keeps the context so +// the registry replay above can retry it. Nothing forces that replay to happen, +// so hand the context to the same synchronous teardown a successful load uses: +// a process that exits without ever retrying still runs the cleanup hooks. The +// handler cannot yield, so it does not settle anything — but by then the process +// is leaving and those promises have no observer left anyway. +__retainWasiRollbackForRetry = __registerWasiExitListener + +function __captureEmnapiAutoDestroyListener() { + if ( + typeof process.prependListener !== 'function' || + typeof process.removeListener !== 'function' + ) { + return + } + let __autoDestroyListener + const __captureListener = (__event, __listener) => { + if (__event === 'beforeExit' && __autoDestroyListener === undefined) { + __autoDestroyListener = __listener + } + } + try { + // Run before existing newListener hooks so a hook that registers its own + // beforeExit listener cannot be mistaken for emnapi's registration. + process.prependListener('newListener', __captureListener) + } catch { + return + } + return () => { + try { + process.removeListener('newListener', __captureListener) + } catch {} + if (__autoDestroyListener !== undefined) { + try { + process.removeListener('beforeExit', __autoDestroyListener) + } catch {} + } + } +} + +try { + const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener() + try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareWasmEnvCleanup, + __isPreparingWasmEnvCleanup, + ) + // emnapi 2.x still registers an unconditional once-listener for + // beforeExit that auto-destroys the context, and suppressDestroy() only + // neutralizes its callback without removing it. This loader owns cleanup + // through its 'exit' listener, so emnapi's listener is captured and + // removed; suppressDestroy() remains the safety net when removal fails. + __emnapiContext.suppressDestroy() + } finally { + // Remove only the exact emnapi callback captured above. + __finishAutoDestroyCapture?.() + } + + ;({ + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, + } = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [ + __captureWasiThreadManager, + __emnapiAsyncWorkPlugin, + __emnapiTSFNPlugin, + ], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + __publishWasiDispose(__napiModule.exports) + // The CommonJS tail below aliases `__napiModule.exports`; a named module + // export does not travel with it, so carry the marker on the binding itself + // too. Three things pin the stamp to exactly this spot: + // - inside this `try`, because the guard throws on a + // `#[napi(module_exports)]` hook that claimed the name, and only the + // catch below tears the environment — context, workers, exit listener — + // back down; + // - after the async runtime host install, which hands this same object to + // addon-provided registration functions that may put anything on it; + // - assigning onto the loader's own `module.exports`, which is still the + // original object here, so an addon accessor with a refusing setter is + // never written through. `cjs-module-lexer` — Node's CJS -> ESM named + // export detection — reads the static `module.exports. =` either + // way, and the later `module.exports = __napiModule.exports` does not + // undo that. + module.exports.__napiBindingTarget = __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) + __registerWasiExitListener() +} catch (error) { + const rollback = { + active: false, + error, + promise: undefined, + rollback: __rollbackWasiInitialization, + } + __wasiRollbackRegistry.set(__wasiRollbackRegistryKey, rollback) + __runWasiInitializationRollback(rollback) + throw rollback.error +} +module.exports = __napiModule.exports +module.exports.LegalCommentsMode = __napiModule.exports.LegalCommentsMode +module.exports.minify = __napiModule.exports.minify +module.exports.minifySync = __napiModule.exports.minifySync +module.exports.Severity = __napiModule.exports.Severity diff --git a/napi/minify/minify.wasip1.d.cts b/napi/minify/minify.wasip1.d.cts new file mode 100644 index 0000000000..03908623cf --- /dev/null +++ b/napi/minify/minify.wasip1.d.cts @@ -0,0 +1,353 @@ +/* auto-generated by NAPI-RS */ +/* eslint-disable */ + +/** The WASI flavor this loader instantiates. */ +export declare const __napiBindingTarget: 'wasm32-wasip1' + +export interface CodegenOptions { + /** + * Remove whitespace. + * + * @default true + */ + removeWhitespace?: boolean + /** + * Escape non-ASCII characters in string literals, untagged template literals, regular + * expression literals and identifier names. + * + * Uses `\uXXXX` for characters up to U+FFFF and `\u{...}` for higher code points. + * Regular expressions use escaped UTF-16 surrogate pairs for higher code points instead; + * escaping changes the observable `RegExp.prototype.source` value. + * + * Code point escapes (`\u{...}`) require ES2015 or later; this option does not provide + * ES5-compatible output. + * + * Non-ASCII characters are left unescaped in tagged template quasis (whose raw text is + * observable), JSX names and text, JSX attribute strings, hashbangs and preserved comments. + * JavaScript expressions inside tagged templates and JSX are escaped normally. + * + * @default false + */ + asciiOnly?: boolean + /** + * How to handle legal comments (comments containing `@license`, `@preserve`, or starting with `//!`/`/*!`). + * + * * `"none"` - Do not preserve any legal comments. + * * `"inline"` - Preserve all legal comments inline. + * * `"eof"` - Move all legal comments to the end of the file. + * * `"external"` - Extract legal comments without linking. + * * `{ linked: "path/to/legal.txt" }` - Extract legal comments and add a link comment to the given path. + * + * @default "none" (when minifying) + */ + legalComments?: 'none' | 'inline' | 'eof' | 'external' | { linked: string } +} + +export interface CompressOptions { + /** + * Set desired EcmaScript standard version for output. + * + * Set `esnext` to enable all target highering. + * + * Example: + * + * * `'es2015'` + * * `['es2020', 'chrome58', 'edge16', 'firefox57', 'node12', 'safari11']` + * + * @default 'esnext' + * + * @see [oxc#target](https://oxc.rs/docs/guide/usage/transformer/lowering#target) + */ + target?: string | Array + /** + * Pass true to discard calls to `console.*`. + * + * @default false + */ + dropConsole?: boolean + /** + * Remove `debugger;` statements. + * + * @default true + */ + dropDebugger?: boolean + /** + * Pass `true` to drop unreferenced functions and variables. + * + * Simple direct variable assignments do not count as references unless set to `keep_assign`. + * @default true + */ + unused?: boolean | 'keep_assign' + /** Keep function / class names. */ + keepNames?: CompressOptionsKeepNames + /** + * Join consecutive var, let and const statements. + * + * @default true + */ + joinVars?: boolean + /** + * Join consecutive simple statements using the comma operator. + * + * `a; b` -> `a, b` + * + * @default true + */ + sequences?: boolean + /** + * Set of label names to drop from the code. + * + * Labeled statements matching these names will be removed during minification. + * + * @default [] + */ + dropLabels?: Array + /** Limit the maximum number of iterations for debugging purpose. */ + maxIterations?: number + /** Treeshake options. */ + treeshake?: TreeShakeOptions +} + +export interface CompressOptionsKeepNames { + /** + * Keep function names so that `Function.prototype.name` is preserved. + * + * This does not guarantee that the `undefined` name is preserved. + * + * @default false + */ + function: boolean + /** + * Keep class names so that `Class.prototype.name` is preserved. + * + * This does not guarantee that the `undefined` name is preserved. + * + * @default false + */ + class: boolean +} + +export interface LegalCommentsLinked { + /** + * Extract legal comments and write them to the given path, with a link + * comment appended to the generated code. + */ + linked: string +} + +export declare const enum LegalCommentsMode { + /** Do not preserve any legal comments. */ + None = 'none', + /** Preserve all legal comments inline. */ + Inline = 'inline', + /** Move all legal comments to the end of the file. */ + Eof = 'eof', + /** Extract legal comments without linking. */ + External = 'external', +} + +export interface MangleOptions { + /** + * Pass `true` to mangle names declared in the top level scope. + * + * @default true for modules and commonjs, otherwise false + */ + toplevel?: boolean + /** + * Preserve `name` property for functions and classes. + * + * @default false + */ + keepNames?: boolean | MangleOptionsKeepNames + /** + * Names that bindings must not be renamed to, and that bindings already + * carrying them keep. Equivalent to terser's `mangle.reserved`. + * + * Pass `['exports', 'module']` when minifying prebuilt CommonJS / UMD files + * that Node consumers `import` directly, so Node's cjs-module-lexer can still + * detect the mangled module's named exports. + * + * @default [] + */ + reserved?: Array + /** Debug mangled names. */ + debug?: boolean +} + +export interface MangleOptionsKeepNames { + /** + * Preserve `name` property for functions. + * + * @default false + */ + function: boolean + /** + * Preserve `name` property for classes. + * + * @default false + */ + class: boolean +} + +export interface ManglePropertiesOptions { + /** + * JavaScript `RegExp` selecting property names to mangle. The source and flags are compiled + * with Rust's regex engine. Flags `i`, `m`, `s`, and `u` are supported. + */ + include: RegExp + /** JavaScript `RegExp` excluding property names selected by `include`. */ + exclude?: RegExp + /** Exact names that are neither mangled nor emitted as automatic output names. */ + reserved?: Array + /** + * Mangle quoted property occurrences in addition to unquoted occurrences. + * + * @default false + */ + quoted?: boolean + /** + * Generate readable `_$name$_`-style output names. + * + * @default false + */ + debug?: boolean + /** + * Stable mappings from original names to output names. `false` reserves an original name. + * Entries that do not match `include`, or that match `exclude`, remain inert but are + * preserved in the returned `mangleCache`. String targets must be `IdentifierName` values + * other than `__proto__`, `constructor`, or `prototype`. The original name `__proto__` is + * always reserved and cannot be used as a cache key. + */ + cache?: Record +} + +/** + * Minify asynchronously. + * + * Note: This function can be slower than `minifySync` due to the overhead of spawning a thread. + */ +export declare function minify(filename: string, sourceText: string, options?: MinifyOptions | undefined | null): Promise + +export interface MinifyOptions { + /** Use when minifying an ES module. */ + module?: boolean + compress?: boolean | CompressOptions + mangle?: boolean | MangleOptions + /** + * Mangle matching property names independently of identifier mangling. Properties owned by + * unminified code, imported module namespaces, globals, or host APIs must be excluded or + * reserved. + */ + mangleProps?: ManglePropertiesOptions + codegen?: boolean | CodegenOptions + sourcemap?: boolean +} + +export interface MinifyResult { + code: string + map?: SourceMap + errors: Array + /** + * Legal comments extracted from the source code. + * Only populated when `codegen.legalComments` is `"linked"` or `"external"`. + */ + legalComments: Array + /** + * Updated property-name cache sorted by original name. Present when `mangleProps` ran on a + * parse without errors. + */ + mangleCache?: Record +} + +/** Minify synchronously. */ +export declare function minifySync(filename: string, sourceText: string, options?: MinifyOptions | undefined | null): MinifyResult + +export interface TreeShakeOptions { + /** + * Whether to respect the pure annotations. + * + * Pure annotations are comments that mark an expression as pure. + * For example: @__PURE__ or #__NO_SIDE_EFFECTS__. + * + * @default true + */ + annotations?: boolean + /** + * Whether to treat this function call as pure. + * + * This function is called for normal function calls, new calls, and + * tagged template calls. + */ + manualPureFunctions?: Array + /** + * Whether property read accesses have side effects. + * + * @default 'always' + */ + propertyReadSideEffects?: boolean | 'always' + /** + * Whether property write accesses (assignments to member expressions) have side effects. + * + * When false, assignments like `obj.prop = value` are considered side-effect-free + * (assuming the object and value expressions themselves are side-effect-free). + * + * @default true + */ + propertyWriteSideEffects?: boolean + /** + * Whether accessing a global variable has side effects. + * + * Accessing a non-existing global variable will throw an error. + * Global variable may be a getter that has side effects. + * + * @default true + */ + unknownGlobalSideEffects?: boolean + /** + * Whether invalid import statements have side effects. + * + * Accessing a non-existing import name will throw an error. + * Also import statements that cannot be resolved will throw an error. + * + * @default true + */ + invalidImportSideEffects?: boolean +} + +export interface Comment { + type: 'Line' | 'Block' + value: string + start: number + end: number +} + +export interface ErrorLabel { + message: string | null + start: number + end: number +} + +export interface OxcError { + severity: Severity + message: string + labels: Array + helpMessage: string | null + codeframe: string | null +} + +export declare const enum Severity { + Error = 'Error', + Warning = 'Warning', + Advice = 'Advice', +} + +export interface SourceMap { + file?: string + mappings: string + names: Array + sourceRoot?: string + sources: Array + sourcesContent?: Array + version: number + x_google_ignoreList?: Array +} diff --git a/napi/minify/package.json b/napi/minify/package.json index 39d59f5149..bef175a557 100644 --- a/napi/minify/package.json +++ b/napi/minify/package.json @@ -44,6 +44,8 @@ "postbuild-dev": "node scripts/patch.js", "build-wasm": "pnpm run build-wasm-dev --release", "build-wasm-dev": "pnpm run build-dev --target wasm32-wasip1-threads --dts minify.wasi.d.cts", + "build-wasip1": "pnpm run build-wasip1-dev --release", + "build-wasip1-dev": "pnpm run build-dev --target wasm32-wasip1 --dts minify.wasip1.d.cts", "test": "vitest run --dir ./test" }, "dependencies": {}, @@ -73,6 +75,7 @@ "riscv64gc-unknown-linux-gnu", "riscv64gc-unknown-linux-musl", "s390x-unknown-linux-gnu", + "wasm32-wasip1", "wasm32-wasip1-threads", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", diff --git a/napi/minify/scripts/patch.js b/napi/minify/scripts/patch.js index 69b640522b..6135892489 100644 --- a/napi/minify/scripts/patch.js +++ b/napi/minify/scripts/patch.js @@ -23,3 +23,7 @@ if (!nativeBinding && globalThis.process?.versions?.["webcontainer"]) { ` + s, ); fs.writeFileSync(path, data); + +const wasip1BrowserPath = pathJoin(packageDir, "minify.wasip1-browser.js"); +const wasip1Browser = fs.readFileSync(wasip1BrowserPath, "utf-8").replaceAll(/[ \t]+$/gmu, ""); +fs.writeFileSync(wasip1BrowserPath, wasip1Browser); diff --git a/napi/parser/napi.wasm.json b/napi/parser/napi.wasm.json index 188e2859e6..9e02e767ac 100644 --- a/napi/parser/napi.wasm.json +++ b/napi/parser/napi.wasm.json @@ -1,3 +1,3 @@ { - "targets": ["wasm32-wasip1-threads"] + "targets": ["wasm32-wasip1", "wasm32-wasip1-threads"] } diff --git a/napi/parser/package.json b/napi/parser/package.json index 3fee55da76..3052284a8d 100644 --- a/napi/parser/package.json +++ b/napi/parser/package.json @@ -68,6 +68,8 @@ "build-wasi": "napi build --config-path napi.wasm.json --target wasm32-wasip1-threads --dts parser.wasi.d.cts --no-dts-header", "build-wasm": "pnpm run build-wasm-dev --release", "build-wasm-dev": "pnpm run build-dev --target wasm32-wasip1-threads --dts parser.wasi.d.cts --no-dts-header", + "build-wasip1": "pnpm run build-wasip1-dev --release", + "build-wasip1-dev": "pnpm run build-dev --target wasm32-wasip1 --dts parser.wasip1.d.cts --no-dts-header", "build-npm-dir": "rm -rf npm-dir && napi create-npm-dirs --config-path napi.wasm.json --npm-dir npm-dir && pnpm napi artifacts --config-path napi.wasm.json --npm-dir npm-dir --output-dir src-js", "build-browser-bundle": "node scripts/build-browser-bundle.js", "test": "pnpm run test-node run", @@ -113,6 +115,7 @@ "riscv64gc-unknown-linux-gnu", "riscv64gc-unknown-linux-musl", "s390x-unknown-linux-gnu", + "wasm32-wasip1", "wasm32-wasip1-threads", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", diff --git a/napi/parser/scripts/build-browser-bundle.js b/napi/parser/scripts/build-browser-bundle.js index df00d4281b..dcba444f9e 100644 --- a/napi/parser/scripts/build-browser-bundle.js +++ b/napi/parser/scripts/build-browser-bundle.js @@ -6,7 +6,7 @@ import { rolldown } from "rolldown"; // Rollup-compatible bundlers reserve `\0`-prefixed IDs for plugin virtual modules, // so Rolldown will not try to resolve these generated `?url` asset modules as files. const NEW_URL_ASSET_PREFIX = "\0new-url-asset:"; -const WASI_BROWSER_BINDING = path.resolve("./src-js/parser.wasi-browser.js"); +const WASIP1_BROWSER_BINDING = path.resolve("./src-js/parser.wasip1-browser.js"); async function main() { const args = parseArgs({ @@ -24,7 +24,7 @@ async function main() { platform: "browser", resolve: { alias: { - "@oxc-parser/binding-wasm32-wasi": WASI_BROWSER_BINDING, + "@oxc-parser/binding-wasm32-wasip1": WASIP1_BROWSER_BINDING, }, }, plugins: [ @@ -54,7 +54,7 @@ async function main() { } if (args.values.npmDir) { - const pkgDir = path.resolve(args.values.npmDir, "wasm32-wasi"); + const pkgDir = path.resolve(args.values.npmDir, "wasm32-wasip1"); // add `browser-bundle.js` to `package.json:files` const pkgFile = path.join(pkgDir, "package.json"); @@ -62,7 +62,7 @@ async function main() { pkg.files.push("browser-bundle.js"); fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2)); - // copy `browser-bundle.js` to `/wasm32-wasi` + // copy `browser-bundle.js` to `/wasm32-wasip1` fs.cpSync("browser-bundle.js", path.join(pkgDir, "browser-bundle.js")); } } diff --git a/napi/parser/scripts/patch.js b/napi/parser/scripts/patch.js index e9a2c02a06..3bebe3fa56 100644 --- a/napi/parser/scripts/patch.js +++ b/napi/parser/scripts/patch.js @@ -28,3 +28,7 @@ export { getBufferOffset, parseRaw, parseRawSync } `; fs.writeFileSync(filename, data); + +const wasip1BrowserFilename = pathJoin(packageDir, "src-js/parser.wasip1-browser.js"); +const wasip1Browser = fs.readFileSync(wasip1BrowserFilename, "utf-8").replaceAll(/[ \t]+$/gmu, ""); +fs.writeFileSync(wasip1BrowserFilename, wasip1Browser); diff --git a/napi/parser/src-js/bindings.js b/napi/parser/src-js/bindings.js index 5176af7986..7701914b68 100644 --- a/napi/parser/src-js/bindings.js +++ b/napi/parser/src-js/bindings.js @@ -565,7 +565,7 @@ function createLoadErrorChain(errors) { // // NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict // WASI loading. It never crosses into another flavor or falls back to native. -const __napiWasiFlavors = ['wasm32-wasi'] +const __napiWasiFlavors = ['wasm32-wasi', 'wasm32-wasip1'] const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR const __napiWasiFlavorRequested = typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0 @@ -655,6 +655,27 @@ if (!nativeBinding || forceWasi) { loadErrors.push(candidateError) } } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasip1')) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('./parser.wasip1.cjs', false, ['./parser.wasm32-wasip1.debug.wasm', './parser.wasm32-wasip1.wasm']) + candidateFailed = candidateError !== null + if (!candidateFailed) { + wasiBinding = require('./parser.wasip1.cjs') + nativeBinding = wasiBinding + __napiLoadedBindingTarget = 'wasm32-wasip1' + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasi')) { let candidateError = null let candidateFailed = false @@ -682,6 +703,33 @@ if (!nativeBinding || forceWasi) { loadErrors.push(candidateError) } } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasip1')) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('@oxc-parser/binding-wasm32-wasip1', true, undefined) + candidateFailed = candidateError !== null + if (!candidateFailed) { + if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + const bindingPackageVersion = require('@oxc-parser/binding-wasm32-wasip1/package.json').version + if (bindingPackageVersion !== '0.151.0') { + throw new Error(`WASI binding package version mismatch, expected 0.151.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + } + wasiBinding = require('@oxc-parser/binding-wasm32-wasip1') + nativeBinding = wasiBinding + __napiLoadedBindingTarget = 'wasm32-wasip1' + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } if ( !wasiBindingLoaded && forceWasi && diff --git a/napi/parser/src-js/browser.js b/napi/parser/src-js/browser.js index 6cd891437b..8a5ae5f126 100644 --- a/napi/parser/src-js/browser.js +++ b/napi/parser/src-js/browser.js @@ -1 +1 @@ -export * from '@oxc-parser/binding-wasm32-wasi' +export * from '@oxc-parser/binding-wasm32-wasip1' diff --git a/napi/parser/src-js/parser.wasip1-browser.js b/napi/parser/src-js/parser.wasip1-browser.js new file mode 100644 index 0000000000..4c6da87736 --- /dev/null +++ b/napi/parser/src-js/parser.wasip1-browser.js @@ -0,0 +1,995 @@ +import { + emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin as __emnapiTSFNPlugin, + instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync, + WASI as __WASI, +} from '@napi-rs/wasm-runtime' +import { createContext as __emnapiCreateContext } from '@emnapi/runtime' + + +export const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +const __wasi = new __WASI({ + version: 'preview1', +}) + +const __wasmUrl = new URL('./parser.wasm32-wasip1.wasm', import.meta.url).href +const __wasmResponse = await globalThis.fetch(__wasmUrl) +if (!__wasmResponse.ok) { + throw new Error( + 'Failed to fetch WASI module ' + + __wasmUrl + + ': ' + + __wasmResponse.status + + ' ' + + (__wasmResponse.statusText || 'Unknown Status'), + ) +} +const __wasmFile = await __wasmResponse.arrayBuffer() + +const __wasmMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, +}) +let __emnapiContext + +const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose') +const __wasiWorkers = new Set() +// The thread manager has to be reachable *before* anything that can throw +// during load or registration. Initialization can fail after the pool has +// already spawned workers, and the rollback still has to mark their +// terminations as expected — but `__napiModule` is assigned only when +// instantiation RETURNS, so on exactly that path it is still undefined. A +// plugin factory runs while the emnapi module is being created, before the +// wasm is loaded and before any registration function runs, and its context +// carries the very same manager instance. +let __wasiThreadManager + +function __captureWasiThreadManager(context) { + if (context && context.PThread) { + __wasiThreadManager = context.PThread + } + return {} +} + +function __getWasiThreadManager() { + const manager = + __wasiThreadManager !== undefined + ? __wasiThreadManager + : __napiModule + ? __napiModule.PThread + : undefined + if (manager && typeof manager.terminateWorker === 'function') { + return manager + } + return undefined +} +let __napiInstance +let __emnapiContextDestroyed = false +let __emnapiContextDestroyPromise +let __emnapiWasmEnvCleanupPrepared = false +let __emnapiWasmEnvCleanupPreparing = false +let __emnapiWasmEnvCleanupRan = false +let __emnapiWasmEnvCleanupDrained = false +let __emnapiWasmEnvCleanupDrainPromise +let __wasiDisposed = false +let __wasiAsyncWorkDrainPromise +let __wasiDisposePromise +let __completeWasiDisposal = function () {} +// Overridden by loader flavors that have a last-resort reclaim for a rollback +// that stopped short of destroying the context. See +// `__rollbackWasiInitialization`. +let __retainWasiRollbackForRetry = function () {} + +function __isThenable(value) { + return ( + value !== null && + (typeof value === 'object' || typeof value === 'function') && + typeof value.then === 'function' + ) +} + +function __createCleanupError(errors, message) { + if (errors.length === 1) { + return errors[0] + } + const __AggregateError = globalThis.AggregateError + if (typeof __AggregateError === 'function') { + return new __AggregateError(errors, message) + } + const error = new Error(message) + error.errors = errors + return error +} + +function __attachCleanupErrors(error, cleanupErrors) { + if (cleanupErrors.length === 0) { + return error + } + const cleanupError = __createCleanupError( + cleanupErrors, + 'WASI binding cleanup failed', + ) + try { + if ( + error && + (typeof error === 'object' || typeof error === 'function') + ) { + if (error.cause === undefined) { + error.cause = cleanupError + if (error.cause === cleanupError) { + return error + } + } + if (Array.isArray(error.cleanupErrors)) { + error.cleanupErrors.push(cleanupError) + return error + } else { + const attachedCleanupErrors = [cleanupError] + error.cleanupErrors = attachedCleanupErrors + if (error.cleanupErrors === attachedCleanupErrors) { + return error + } + } + } + } catch {} + const aggregate = __createCleanupError( + [error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + try { + aggregate.cause = error + } catch {} + return aggregate +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __isPreparingWasmEnvCleanup() { + return __emnapiWasmEnvCleanupPreparing +} + +function __prepareWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) { + return + } + const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup + if (typeof prepare === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __emnapiWasmEnvCleanupPreparing = true + try { + prepare() + } finally { + __emnapiWasmEnvCleanupPreparing = false + } + __emnapiWasmEnvCleanupRan = true + } + __emnapiWasmEnvCleanupPrepared = true +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (callback) { + setImmediate(callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (callback) { + const channel = new __MessageChannel() + channel.port1.onmessage = function () { + channel.port1.onmessage = null + try { + channel.port1.close() + } catch {} + try { + channel.port2.close() + } catch {} + callback() + } + channel.port2.postMessage(null) + } + } + return function (callback) { + setTimeout(callback, 0) + } +})() + +// A real, *referenced* timer, for waits that must let the whole host make +// progress between looks — the async-work drain polls the addon rather than +// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there +// would spin the loop instead of yielding it. Falls back to the macrotask +// scheduler on a host without timers. +function __scheduleTimer(callback, delay) { + const setTimer = globalThis.setTimeout + if (typeof setTimer !== 'function') { + __scheduleMacrotask(callback) + return + } + try { + setTimer(callback, delay) + } catch { + __scheduleMacrotask(callback) + } +} + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + * + * The "already drained" flag is set only once a wait has actually finished. + * Scheduling a macrotask can fail — a host-provided or patched `setImmediate` + * that throws is enough — and a disposal that rejects stays retryable, so + * marking the drain complete up front would make the retry skip it and destroy + * the context with the barrier's settlements still queued. + * + * A wait that runs out of turns with the counter still nonzero rejects with + * `ERR_NAPI_WASI_CLEANUP_PENDING` for the same reason: at that point + * "finished" is indistinguishable from the stranding above, and destroying + * would discard the very settlement the wait was for. The rejection leaves the + * flag unset and disposal retryable. + */ +function __drainWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupDrained || !__emnapiWasmEnvCleanupRan) { + return + } + if (__emnapiWasmEnvCleanupDrainPromise) { + return __emnapiWasmEnvCleanupDrainPromise + } + const pending = __napiInstance?.exports?.napi_wasm_env_cleanup_pending + const observable = typeof pending === 'function' + if (observable) { + let queued + try { + queued = pending() + } catch { + __emnapiWasmEnvCleanupDrained = true + return + } + if (!queued) { + __emnapiWasmEnvCleanupDrained = true + return + } + } + const limit = observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + const drainPromise = (async () => { + let queued = 0 + for (let turn = 0; turn < limit; turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!observable) { + continue + } + try { + queued = pending() + } catch { + return + } + if (!queued) { + return + } + } + if (!observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: the drained flag stays + // unset, dispose() (and the rollback) decline to destroy, and a later + // dispose() runs the drain again — by which time the queue has usually been + // delivered. A counter that is somehow stuck nonzero therefore costs each + // attempt at most another bounded wait and a rejection, never a stranded + // promise; the process-exit teardown still reclaims the context. + const drainError = new Error( + 'the wasm environment still reports ' + + queued + + ' queued settlement(s) after ' + + limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw drainError + })().then( + (value) => { + // Set only when the wait actually finished AND the queue was seen empty + // (or is unobservable): a drain that timed out with settlements still + // queued rejects above and must stay repeatable. + __emnapiWasmEnvCleanupDrained = true + __emnapiWasmEnvCleanupDrainPromise = undefined + return value + }, + (error) => { + __emnapiWasmEnvCleanupDrainPromise = undefined + throw error + }, + ) + __emnapiWasmEnvCleanupDrainPromise = drainPromise + return drainPromise +} + +function __destroyEmnapiContext() { + if (__emnapiContextDestroyed || __emnapiContext === undefined) { + __emnapiContextDestroyed = true + return + } + if (__emnapiContextDestroyPromise) { + return __emnapiContextDestroyPromise + } + + __prepareWasmEnvCleanup() + const result = __emnapiContext.destroy() + if (!__isThenable(result)) { + __emnapiContextDestroyed = true + return + } + + const destroyPromise = Promise.resolve(result).then( + (value) => { + __emnapiContextDestroyed = true + return value + }, + (error) => { + __emnapiContextDestroyPromise = undefined + throw error + }, + ) + __emnapiContextDestroyPromise = destroyPromise + return destroyPromise +} + +/** + * Holds the event loop open until `work` settles. + * + * Nothing else can: the pool workers are deliberately unreferenced so an idle + * binding cannot keep a process alive, and referencing them again for the + * termination does not hold either — emnapi unreferences a worker the moment it + * reports `async-thread-ready`, which for a worker that was still starting + * lands *after* the termination began. Without a handle of its own, an + * `await dispose()` with nothing else pending exits the process with its + * promise unsettled, and everything after the `await` is skipped. + * + * The timer is cleared as soon as the work settles, so this never outlives the + * disposal that asked for it. + */ +function __keepEventLoopAliveUntil(work) { + const setTimer = globalThis.setInterval + const clearTimer = globalThis.clearInterval + if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') { + return work + } + let timer + try { + timer = setTimer(function () {}, 50) + } catch { + return work + } + const release = function () { + try { + clearTimer(timer) + } catch {} + } + return work.then( + (value) => { + release() + return value + }, + (error) => { + release() + throw error + }, + ) +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this addon's outstanding `napi_async_work` before the teardown that + * would strand it. + * + * `napi_prepare_wasm_env_cleanup` does not cover async work, and nothing about + * it is observable from JavaScript: the threadless archive resolves + * `napi_*_async_work` through the `@emnapi/core` plugins, but the threaded one + * links the C `async_work.c` on the uv threadpool, so there the wasm neither + * imports nor exports those symbols and the only brackets a loader could watch + * (`_emnapi_ctx_*_waiting_request_counter`) are shared with threadsafe + * functions. The addon is the one place both flavors go through, so it answers + * for both, through the same kind of handshake the settlement drain uses: + * + * - `napi_wasm_cancel_pending_async_work()` cancels what no thread has + * started. Those completion callbacks run with `napi_cancelled`, which + * napi-rs turns into a promise rejected with an `AbortError`. + * - `napi_wasm_async_work_pending()` counts what is still owed a completion + * callback. Work already executing refuses cancellation and stays counted + * until it finishes normally — which it can, because this runs before the + * barrier, before `Context.destroy()` and before anything is terminated. + * + * Both exports are optional: an addon built against a napi crate that predates + * them drains nothing and keeps the previous behavior, exactly as the + * `napi_wasm_env_cleanup_pending` handshake degrades. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. The promise it returns otherwise never + * rejects. + * + * The wait has no deadline, and that is the point: giving up would destroy the + * environment with a completion callback still owed, which is the stranding + * this exists to prevent. A task whose `execute` never returns already keeps an + * *undisposed* process alive in exactly the same way, so disposal inherits that + * rather than inventing a bound it cannot honor. + * + * Safe to call from inside a completion callback, which is reachable: settling + * a task runs addon code that can re-enter JavaScript — a setter on the value + * being handed back, a threadsafe-function callback — and that JavaScript can + * call `dispose()`. Two things make it terminate rather than wait on itself: + * + * - The addon keeps a work registered until its completion callback + * *finishes*, so the count read here is at least one and this takes the + * polling path instead of declaring the environment drained and tearing it + * down from inside the frame that is still settling a promise. + * - The poll is a timer, so it cannot run until the callback has returned to + * the host — by which time that work has left the registry. The count the + * next poll reads is the one taken after the callback finished. + * + * `__disposeWasiBinding` hands every caller the same in-flight promise, so the + * nested call joins this disposal rather than starting a second one. + */ +function __drainWasiAsyncWork() { + if (__wasiAsyncWorkDrainPromise !== undefined) { + return __wasiAsyncWorkDrainPromise + } + const exports = __napiInstance?.exports + const pending = exports?.napi_wasm_async_work_pending + const cancelPending = exports?.napi_wasm_cancel_pending_async_work + if (typeof pending !== 'function' || typeof cancelPending !== 'function') { + return + } + + const readPending = () => { + try { + return pending() + } catch (error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition — + // there is nothing left to wait for, and refusing to dispose would only + // keep a dead instance and its stuck counter alive. Best-effort here is + // the honest answer, and it is what disposal did before this drain + // existed. + // + // Only a trap. Anything else means the export is not what this loader + // thinks it is, which is a defect worth surfacing rather than disposing + // over. + if (error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw error + } + } + + if (!readPending()) { + return + } + try { + cancelPending() + } catch { + // Cancellation is an optimization: it bounds the wait by the work already + // executing. Failing it only means waiting for the whole queue instead. + } + if (!readPending()) { + return + } + + const drainPromise = __keepEventLoopAliveUntil( + (async () => { + while (readPending()) { + await new Promise((resolve) => { + __scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })(), + ).then( + () => { + __wasiAsyncWorkDrainPromise = undefined + }, + (error) => { + // A wait that could not run is not a wait that finished. The only way + // here is a host whose timers and macrotask primitives all refuse, and + // the work is still outstanding — reporting success would destroy the + // environment over it, which is the stranding this exists to prevent. + // Reject instead: disposal stays retryable, and the context is not + // destroyed. Clearing the memo first is what makes the retry re-run this. + __wasiAsyncWorkDrainPromise = undefined + throw error + }, + ) + __wasiAsyncWorkDrainPromise = drainPromise + return drainPromise +} + +/** + * `@emnapi/wasi-threads` counts a worker exit as expected only when its own + * thread manager performed the termination. A bare `worker.terminate()` reaches + * the manager's `exit` listener instead, which reports + * `worker (tid = N) sent an error! ... stopped with exit code 1` and rethrows + * inside the emit — aborting the `once('exit')` that backs the terminate + * promise, so disposal never settles and the process dies with an uncaught + * exception. Mark the termination through the manager first. + * + * The manager comes from `__getWasiThreadManager`, not from `__napiModule`: + * the initialization rollback runs on the one path where instantiation never + * returned, so `__napiModule` is still undefined there while the workers it + * spawned are already registered and loaded. + * + * Not `terminateAllThreads()`: that one recreates the pool it just shut down. + */ +function __terminateWasiWorkers() { + const cleanupErrors = [] + const pending = [] + const threadManager = __getWasiThreadManager() + + for (const worker of __wasiWorkers) { + let result + try { + if (threadManager) { + threadManager.terminateWorker(worker) + // `terminateWorker` leaves behind a reporter that logs every message + // still queued on the port, which Node flushes on exit. Nothing is + // listening for those any more. + worker.onmessage = undefined + } + result = worker.terminate() + } catch (error) { + cleanupErrors.push(error) + continue + } + if (__isThenable(result)) { + pending.push( + Promise.resolve(result).then( + () => { + __wasiWorkers.delete(worker) + }, + (error) => { + cleanupErrors.push(error) + }, + ), + ) + } else { + __wasiWorkers.delete(worker) + } + } + + const finish = () => { + if (cleanupErrors.length > 0) { + throw __createCleanupError( + cleanupErrors, + 'Failed to terminate WASI workers', + ) + } + } + return pending.length > 0 + ? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish) + : finish() +} + +function __finishWasiDisposal() { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult).then(__completeWasiDisposal) + } + return __completeWasiDisposal() +} + +function __continueWasiDisposal() { + const destroyResult = __destroyEmnapiContext() + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult).then(__finishWasiDisposal) + } + return __finishWasiDisposal() +} + +function __cleanUpWasmEnvForWasiDisposal() { + // Run the pre-teardown barrier, then let the settlements it queued actually + // reach JavaScript, and only then destroy the environment. Doing these two + // back to back is what strands them. + __prepareWasmEnvCleanup() + const drainResult = __drainWasmEnvCleanup() + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then(__continueWasiDisposal) + } + return __continueWasiDisposal() +} + +function __startWasiDisposal() { + // Outstanding `napi_async_work` goes first, while the environment is still + // completely live: the completion callbacks run addon code, and everything + // after this point takes that away from them — the barrier shuts the async + // runtime down, `Context.destroy()` stops JavaScript calls, and terminating + // the pool threads removes what would have reported the work finished. + const asyncWorkResult = __drainWasiAsyncWork() + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __cleanUpWasmEnvForWasiDisposal, + ) + } + return __cleanUpWasmEnvForWasiDisposal() +} + +/** + * Disposes this generated WASI binding. + * + * Access this function with: + * binding[Symbol.for('napi.rs.wasi.dispose')]() + */ +function __disposeWasiBinding() { + if (__wasiDisposePromise) { + return __wasiDisposePromise + } + if (__wasiDisposed) { + return Promise.resolve() + } + + let resolveDispose + let rejectDispose + const disposePromise = new Promise((resolve, reject) => { + resolveDispose = resolve + rejectDispose = reject + }) + __wasiDisposePromise = disposePromise + + let result + try { + result = __startWasiDisposal() + } catch (error) { + __wasiDisposePromise = undefined + rejectDispose(error) + return disposePromise + } + + Promise.resolve(result).then( + (value) => { + __wasiDisposed = true + resolveDispose(value) + }, + (error) => { + __wasiDisposePromise = undefined + rejectDispose(error) + }, + ) + return disposePromise +} + +function __publishWasiDispose(exports) { + Object.defineProperty(exports, __wasiDisposeSymbol, { + configurable: false, + enumerable: false, + value: __disposeWasiBinding, + writable: false, + }) +} + +function __finishWasiInitializationRollback(cleanupErrors) { + let workerResult + try { + workerResult = __terminateWasiWorkers() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return cleanupErrors + } + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => cleanupErrors) + } + return cleanupErrors +} + +function __destroyContextForWasiRollback(cleanupErrors) { + let destroyResult + try { + destroyResult = __destroyEmnapiContext() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return __finishWasiInitializationRollback(cleanupErrors) + } + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => __finishWasiInitializationRollback(cleanupErrors)) + } + return __finishWasiInitializationRollback(cleanupErrors) +} + +/** + * Leaves a rollback that could not reach the queued settlements undestroyed, and + * hands it to whatever this flavor has that can still reclaim it. + */ +function __retainFailedWasiRollback(cleanupErrors) { + try { + __retainWasiRollbackForRetry() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + } + return cleanupErrors +} + +/** + * Initialization can fail *after* registration has already run, and registration + * runs with a live environment: a module-init hook can start async work and then + * return an error, and the promise it created may already have escaped into + * JavaScript. The barrier cancels that work and *queues* the settlement, so this + * path needs the same drain the ordinary disposal does — destroying without + * yielding discards the queue with a null env and strands the promise. + * + * Stays synchronous when nothing is queued, which covers every failure before + * `beforeInit`: there is no instance to run the barrier on, so nothing to drain. + * + * A barrier or drain that did *not* finish stops the rollback short of + * destroying, which is what `dispose()` already does — a rejected drain there + * never reaches `__continueWasiDisposal`. Destroying anyway is the worse of the + * two trades, and not because of what it saves: + * + * - It cannot deliver the settlements. `Context.destroy()` runs the + * threadsafe function's cleanup hook, which drains the queue with a null env + * and discards it, so a promise that already escaped into JavaScript hangs + * forever with nothing left that could ever settle it. + * - It saves less than it looks. `Context.destroy()` stops JavaScript calls + * and runs cleanup hooks; it does not free the wasm instance or its Memory, + * which this module's scope holds either way. What stopping short retains is + * the emnapi context's bookkeeping and its un-run cleanup hooks. + * - Retry is not theoretical. A rollback that records a cleanup error is + * already kept in the process-wide registry above, so re-`require()`ing this + * file replays it instead of re-instantiating — and the `6e15de6f` flag fix + * means the replay drains again rather than skipping it. Destroying first is + * what makes that retained record useless. + * + * The residual cost is honest: the CJS flavor hands the context to its + * `process.on('exit')` teardown, so a process that never retries still reclaims + * it on the way out. The ESM browser flavor has no equivalent — a module that + * throws while evaluating is permanently errored, so re-importing rethrows + * without re-running this file — and there the context stays until the realm + * goes away. That is the deliberate choice: a hung promise is a silent liveness + * bug with no upper bound, while the retained bookkeeping is bounded by the page. + */ +function __rollbackWasiInitialization() { + // The environment teardown this rollback performs, kept nested so it cannot + // be reached without the async-work drain below running first. + function __rollbackWasmEnvForWasiInitialization() { + const cleanupErrors = [] + let drainResult + let settlementsUnreached = false + try { + __prepareWasmEnvCleanup() + drainResult = __drainWasmEnvCleanup() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + settlementsUnreached = true + } + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then( + () => __destroyContextForWasiRollback(cleanupErrors), + (cleanupError) => { + cleanupErrors.push(cleanupError) + return __retainFailedWasiRollback(cleanupErrors) + }, + ) + } + if (settlementsUnreached) { + return __retainFailedWasiRollback(cleanupErrors) + } + return __destroyContextForWasiRollback(cleanupErrors) + } + + // Same reason as `__startWasiDisposal`: a module-init hook can start async + // work before the load goes on to fail, and this rollback tears down exactly + // what those completions need. Settle them while everything is still live, + // before the barrier and the teardown above take that away. + // + // A drain that could not finish leaves async work possibly outstanding, and + // destroying the context over it would strand exactly what this rollback is + // there to settle. Stop short and retain instead — the same trade + // `__rollbackWasmEnvForWasiInitialization` makes for the settlement drain, so + // the context stays reclaimable by a retry or by this flavor's own + // last-resort teardown. + const __retainAfterAsyncWorkDrainFailure = (cleanupError) => + __retainFailedWasiRollback([cleanupError]) + let asyncWorkResult + try { + asyncWorkResult = __drainWasiAsyncWork() + } catch (cleanupError) { + return __retainAfterAsyncWorkDrainFailure(cleanupError) + } + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __rollbackWasmEnvForWasiInitialization, + __retainAfterAsyncWorkDrainFailure, + ) + } + return __rollbackWasmEnvForWasiInitialization() +} + +let __wasiModule +let __napiModule + +try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareWasmEnvCleanup, + __isPreparingWasmEnvCleanup, + ) + __emnapiContext.suppressDestroy() + + ;({ + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, + } = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [ + __captureWasiThreadManager, + __emnapiAsyncWorkPlugin, + __emnapiTSFNPlugin, + ], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + __publishWasiDispose(__napiModule.exports) + // The default export hands out this object; a named module export does not + // travel with it, so carry the marker on the binding itself too. After the + // host install, which hands the same object to addon-provided registration + // functions that may put anything on it, and inside this `try`, so a claimed + // name fails the load through the rollback below rather than past it. + __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) +} catch (error) { + const cleanupErrors = await __rollbackWasiInitialization() + throw __attachCleanupErrors(error, cleanupErrors) +} +export default __napiModule.exports +export const Severity = __napiModule.exports.Severity +export const ParseResult = __napiModule.exports.ParseResult +export const ExportExportNameKind = __napiModule.exports.ExportExportNameKind +export const ExportImportNameKind = __napiModule.exports.ExportImportNameKind +export const ExportLocalNameKind = __napiModule.exports.ExportLocalNameKind +export const ImportNameKind = __napiModule.exports.ImportNameKind +export const parse = __napiModule.exports.parse +export const parseSync = __napiModule.exports.parseSync +export const rawTransferSupported = __napiModule.exports.rawTransferSupported diff --git a/napi/parser/src-js/parser.wasip1-deferred.d.ts b/napi/parser/src-js/parser.wasip1-deferred.d.ts new file mode 100644 index 0000000000..f5f150f775 --- /dev/null +++ b/napi/parser/src-js/parser.wasip1-deferred.d.ts @@ -0,0 +1,83 @@ +export type WasiBinding = typeof import('./parser.wasip1.cjs') + +export type WasiModuleInput = + | WebAssembly.Module + | PromiseLike + +/** Run the instance on a linear memory the caller allocated. */ +export interface WasiCallerMemoryOptions { + /** + * A caller-allocated linear memory for this instance. It must be unshared + * and created in this loader's own realm — the WASI and emnapi layers + * underneath identify a Memory with a realm-local `instanceof`, so one from + * a `node:vm` context or another frame is rejected. It is single-use: once + * a validated initialization attempt has begun, the same Memory cannot be + * passed again — including after that attempt failed, and after the instance + * was disposed. + */ + memory: WebAssembly.Memory + /** Not available beside `memory`: the loader allocates neither. */ + initialMemoryPages?: never + /** Not available beside `memory`: the loader allocates neither. */ + maximumMemoryPages?: never +} + +/** Let the loader allocate the linear memory, optionally sized. */ +export interface WasiAllocatedMemoryOptions { + /** Not available beside the page counts: they size the loader's own Memory. */ + memory?: never + /** @default WASM_MEMORY.initialPages */ + initialMemoryPages?: number + /** @default WASM_MEMORY.maximumPages */ + maximumMemoryPages?: number +} + +/** + * Either memory form, never a mix of the two: the loader throws a TypeError + * on `memory` beside a page count. `{}` and an omitted argument select the + * loader defaults. + */ +export type WasiInstanceOptions = + | WasiCallerMemoryOptions + | WasiAllocatedMemoryOptions + +export interface WasiRuntimeStats { + /** Instances created by this evaluated loader module, not process-wide. */ + createdInstances: number + /** Created instances whose dispose() has not completed. */ + liveInstances: number + /** Declared initial address space, not committed memory. */ + declaredInitialMemoryBytes: number +} + +export interface WasiInstance { + readonly exports: WasiBinding + /** This instance's linear memory. Claimed, so it cannot start another one. */ + readonly memory: WebAssembly.Memory + /** Current linear-memory size; 0 once dispose() has completed. */ + readonly memoryBytes: number + readonly disposed: boolean + dispose(): Promise +} + +/** The memory descriptor compiled into this loader. */ +export const WASM_MEMORY: Readonly<{ + initialPages: number + maximumPages: number + pageBytes: number + initialBytes: number + maximumBytes: number +}> + +export function getDeferredRuntimeStats(): Readonly + +export function instantiate(wasmInput: WasiModuleInput): Promise +export function createInstance( + wasmInput: WasiModuleInput, + options?: WasiInstanceOptions, +): Promise +/** Dispose the singleton and retry retained failed-initialization cleanup. */ +export function dispose(): Promise + +/** The WASI flavor this deferred loader instantiates. */ +export declare const __napiBindingTarget: 'wasm32-wasip1' diff --git a/napi/parser/src-js/parser.wasip1-deferred.js b/napi/parser/src-js/parser.wasip1-deferred.js new file mode 100644 index 0000000000..d4133ebdac --- /dev/null +++ b/napi/parser/src-js/parser.wasip1-deferred.js @@ -0,0 +1,1488 @@ +import { + emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin as __emnapiTSFNPlugin, + instantiateNapiModule as __emnapiInstantiateNapiModule, + WASI as __WASI, +} from '@napi-rs/wasm-runtime' +import { createContext as __emnapiCreateContext } from '@emnapi/runtime' + + +export const WASM_MEMORY = Object.freeze({ + initialPages: 1024, + maximumPages: 65536, + pageBytes: 65536, + initialBytes: 1024 * 65536, + maximumBytes: 65536 * 65536, +}) + +let __createdInstances = 0 +let __liveInstances = 0 + +/** + * Counters for instances created by THIS module evaluation, not process-wide: + * a second bundled copy of this loader keeps its own. Only successfully + * created instances are counted, and `liveInstances` drops when an instance's + * `dispose()` resolves. + * + * `declaredInitialMemoryBytes` is declared address space, not a host's + * committed-memory metric; pair it with host telemetry rather than treating it + * as a quota. + */ +export function getDeferredRuntimeStats() { + return Object.freeze({ + createdInstances: __createdInstances, + liveInstances: __liveInstances, + declaredInitialMemoryBytes: WASM_MEMORY.initialBytes, + }) +} + +const __arrayBufferByteLengthGetter = Object.getOwnPropertyDescriptor( + ArrayBuffer.prototype, + 'byteLength', +).get +const __memoryBufferGetter = Object.getOwnPropertyDescriptor( + WebAssembly.Memory.prototype, + 'buffer', +).get +// One managed initialization per Memory, success or failure: an attempt that +// throws may already have written into linear memory, so the bytes are not a +// clean slate for a second instance. Module-local, like the counters above. +const __claimedMemories = new WeakSet() + +function __resolveInstanceMemory(__options) { + const __provided = __options == null ? undefined : __options.memory + if (__provided === undefined || __provided === null) { + // Page counts are handed to the engine unvalidated: it already rejects a + // negative, over-4GiB or below-maximum value with a precise message, and a + // second set of bounds here would only drift from it. + const __allocated = new WebAssembly.Memory({ + initial: + __options != null && __options.initialMemoryPages !== undefined + ? __options.initialMemoryPages + : WASM_MEMORY.initialPages, + maximum: + __options != null && __options.maximumMemoryPages !== undefined + ? __options.maximumMemoryPages + : WASM_MEMORY.maximumPages, + }) + // Claimed like a caller-provided one. The handle publishes it as + // `instance.memory`, so handing it back to `createInstance()` is as easy + // as passing your own twice, and it would put two live instances on one + // linear memory: each initialization rewrites the emnapi/WASI state the + // other is still running on. + __claimedMemories.add(__allocated) + return __allocated + } + if ( + __options.initialMemoryPages !== undefined || + __options.maximumMemoryPages !== undefined + ) { + throw new TypeError( + 'Pass either memory or initialMemoryPages/maximumMemoryPages, not both', + ) + } + let __buffer + try { + // Brand check: the getter throws for anything that is not a genuine + // WebAssembly.Memory, including a cross-realm look-alike object. + __buffer = Reflect.apply(__memoryBufferGetter, __provided, []) + } catch { + throw new TypeError('memory must be an unshared WebAssembly.Memory') + } + try { + // Throws for a SharedArrayBuffer. This loader has no threads, and shared + // growth does not detach: external views handed to the addon would + // silently outlive the bytes they describe. + Reflect.apply(__arrayBufferByteLengthGetter, __buffer, []) + } catch { + throw new TypeError( + 'The deferred loader requires an unshared WebAssembly.Memory', + ) + } + // The intrinsic getters above accept a genuine Memory from ANY realm, but + // the loader's dependencies do not: `WASI.setMemory` in + // `@napi-rs/wasm-runtime` and emnapi identify a Memory with a realm-local + // `instanceof`. A Memory built in another realm (a `node:vm` context, a + // same-origin iframe) would pass every check here and only fail deep inside + // initialization. Reject it up front, and before the claim below, so the + // caller keeps it usable in the realm that made it. + if (!(__provided instanceof WebAssembly.Memory)) { + throw new TypeError( + 'memory must be a WebAssembly.Memory created in the same realm as this loader', + ) + } + if (__claimedMemories.has(__provided)) { + throw new TypeError( + 'This WebAssembly.Memory has already been used for a deferred initialization attempt and cannot be reused, including after a failed initialization or a disposal', + ) + } + // Last step, after every check: a rejected option bag must leave the Memory + // unclaimed, or a caller could not fix the call and retry with it. + __claimedMemories.add(__provided) + return __provided +} + +export const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +/** + * Deferred, workerd-safe instantiation: no top-level I/O, no compile-from-bytes. + * Accepts ONLY a precompiled WebAssembly.Module, or a Promise resolving to one + * (e.g. `import mod from './parser.wasm32-wasip1.wasm'` under a CompiledWasm + * module rule / wrangler module import). Byte buffers, URLs and Response + * objects are rejected: they require dynamic Wasm compilation, which + * Cloudflare Workers disallows. + */ +async function __resolveModule(__wasmInput) { + const __module = await __wasmInput + // Brand check, not `instanceof`: `WebAssembly.Module.imports` throws unless + // its argument is a genuine WebAssembly.Module, so prototype-spoofed byte + // buffers are rejected while cross-realm Module instances are accepted. + try { + WebAssembly.Module.imports(__module) + } catch { + throw new TypeError( + "instantiate() and createInstance() expect a precompiled WebAssembly.Module (or a Promise resolving to one), " + + "e.g. import mod from './parser.wasm32-wasip1.wasm' under a CompiledWasm module rule / wrangler module import. " + + "Byte buffers, URLs and Response objects require dynamic Wasm compilation, which Cloudflare Workers disallows.", + ) + } + return __module +} + +let __normalizedModules + +function __rememberNormalizedModule(__module, __normalizedModule) { + if (!__normalizedModules) { + __normalizedModules = new WeakMap() + } + __normalizedModules.set(__module, __normalizedModule) + return __normalizedModule +} + +async function __normalizeModuleForEmnapi(__module) { + if (__module instanceof WebAssembly.Module) { + return __module + } + if (__normalizedModules) { + const __normalizedModule = __normalizedModules.get(__module) + if (__normalizedModule) { + return __normalizedModule + } + } + // @emnapi/core currently performs realm-local `instanceof` checks after + // accepting the module. Structured cloning preserves compiled code without + // compiling bytes and produces a Module owned by the current realm. + if (typeof structuredClone === 'function') { + try { + const __normalizedModule = structuredClone(__module) + if (__normalizedModule instanceof WebAssembly.Module) { + return __rememberNormalizedModule(__module, __normalizedModule) + } + } catch {} + } + // MessageChannel uses the same structured-clone semantics and covers older + // browser/Node hosts that expose it but not the structuredClone function. + if (typeof MessageChannel === 'function') { + let __channel + try { + __channel = new MessageChannel() + const __normalizedModule = await new Promise((resolve, reject) => { + __channel.port1.onmessage = (event) => resolve(event.data) + __channel.port1.onmessageerror = () => + reject(new TypeError('Failed to clone WebAssembly.Module')) + try { + __channel.port2.postMessage(__module) + } catch (error) { + reject(error) + } + }) + if (__normalizedModule instanceof WebAssembly.Module) { + return __rememberNormalizedModule(__module, __normalizedModule) + } + } catch { + } finally { + try { + __channel?.port1.close() + } catch {} + try { + __channel?.port2.close() + } catch {} + } + } + // Last-resort compatibility for genuine, extensible foreign Modules. + try { + Object.setPrototypeOf(__module, WebAssembly.Module.prototype) + } catch {} + if (__module instanceof WebAssembly.Module) { + return __module + } + throw new TypeError( + 'This host cannot normalize a cross-realm WebAssembly.Module; ' + + 'provide structuredClone or MessageChannel support.', + ) +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __captureEmnapiAutoDestroyListener(__process) { + if ( + !__process || + typeof __process.prependListener !== 'function' || + typeof __process.removeListener !== 'function' + ) { + return + } + let __autoDestroyListener + const __captureListener = (__event, __listener) => { + if (__event === 'beforeExit' && __autoDestroyListener === undefined) { + __autoDestroyListener = __listener + } + } + try { + // Run before existing newListener hooks so a hook that registers its own + // beforeExit listener cannot be mistaken for emnapi's registration. + __process.prependListener('newListener', __captureListener) + } catch { + return + } + return () => { + try { + __process.removeListener('newListener', __captureListener) + } catch {} + if (__autoDestroyListener !== undefined) { + try { + __process.removeListener('beforeExit', __autoDestroyListener) + } catch {} + } + } +} + +function __attachCleanupError(__error, __cleanupError) { + try { + if ( + __error && + (typeof __error === 'object' || typeof __error === 'function') && + __error.cause === undefined + ) { + __error.cause = __cleanupError + } + } catch {} +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (__callback) { + setImmediate(__callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (__callback) { + const __channel = new __MessageChannel() + __channel.port1.onmessage = function () { + __channel.port1.onmessage = null + try { + __channel.port1.close() + } catch {} + try { + __channel.port2.close() + } catch {} + __callback() + } + __channel.port2.postMessage(null) + } + } + return function (__callback) { + setTimeout(__callback, 0) + } +})() + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + */ +function __drainWasmEnvCleanup(__instance) { + const __pending = __instance?.exports.napi_wasm_env_cleanup_pending + const __observable = typeof __pending === 'function' + if (__observable) { + let __queued + try { + __queued = __pending() + } catch { + return + } + if (!__queued) { + return + } + } + const __limit = __observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + return (async () => { + let __queued = 0 + for (let __turn = 0; __turn < __limit; __turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!__observable) { + continue + } + try { + __queued = __pending() + } catch { + return + } + if (!__queued) { + return + } + } + if (!__observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: `__prepareForDisposal` + // leaves its drained flag unset, dispose() (and the instantiation-failure + // path) decline to destroy, and a later dispose() runs the drain again — by + // which time the queue has usually been delivered. A counter that is + // somehow stuck nonzero therefore costs each attempt at most another + // bounded wait and a rejection, never a stranded promise; the managed + // beforeExit destroyer still reclaims the context. + const __drainError = new Error( + 'the wasm environment still reports ' + + __queued + + ' queued settlement(s) after ' + + __limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + __drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw __drainError + })() +} + +// A real, *referenced* timer for the async-work wait below, which polls the +// addon rather than interleaving with the @emnapi/core dispatch: a zero-delay +// macrotask there would spin the loop instead of yielding it. Falls back to the +// macrotask scheduler on a host without timers. +function __scheduleTimer(__callback, __delay) { + const __setTimer = globalThis.setTimeout + if (typeof __setTimer !== 'function') { + __scheduleMacrotask(__callback) + return + } + try { + __setTimer(__callback, __delay) + } catch { + __scheduleMacrotask(__callback) + } +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this instance's outstanding `napi_async_work` before the teardown + * that would strand it. + * + * The same hole the eager loaders had, and the same fix: the environment + * cleanup barrier covers promise settlements queued on the threadsafe-function + * queue and says nothing about `napi_async_work`, so destroying the context + * with a work still outstanding leaves its completion callback with nowhere to + * run and its promise unsettled forever. + * + * This flavor is threadless, so `compute` runs on the JavaScript thread inside + * the macrotask that dequeued it: while this is running, any outstanding work + * is queued rather than executing, and `napi_wasm_cancel_pending_async_work` + * can take all of it. The poll is still what decides when the drain is done — + * cancellation delivers those completions from a later macrotask, not + * synchronously. + * + * Per instance, from that instance's own exports: two instances of this loader + * have separate registries and must not wait on each other. + * + * Both exports are optional, so an addon built against a napi crate that + * predates them keeps the previous behavior. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. No deadline: giving up would destroy the + * environment with a completion callback still owed. + */ +function __drainInstanceAsyncWork(__instance) { + const __exports = __instance?.exports + const __pending = __exports?.napi_wasm_async_work_pending + const __cancelPending = __exports?.napi_wasm_cancel_pending_async_work + if (typeof __pending !== 'function' || typeof __cancelPending !== 'function') { + return + } + + const __readPending = () => { + try { + return __pending() + } catch (__error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition and + // best-effort is the honest answer. Anything else means the export is not + // what this loader thinks it is — a defect worth surfacing. + if (__error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw __error + } + } + + if (!__readPending()) { + return + } + try { + __cancelPending() + } catch { + // Cancellation only bounds the wait. Failing it means waiting for the queue + // to run instead, which the poll below already does. + } + if (!__readPending()) { + return + } + + return (async () => { + while (__readPending()) { + await new Promise((__resolve) => { + __scheduleTimer(__resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })() +} + +function __createLifecycleReentryError(__operation) { + const __error = new Error( + __operation + + '() cannot run while an emnapi Context.destroy() call is still active; await the original cleanup promise instead.', + ) + __error.code = 'ERR_NAPI_WASI_LIFECYCLE_REENTRY' + return __error +} + +const __managedEmnapiContextDestroyers = new Set() +let __managedCleanupProcess +let __managedBeforeExitListener +let __managedDestroyPromise +let __managedDestroyersInFlight +let __managedBeforeExitRegistrationRetryCount = 0 +let __managedBeforeExitRegistrationRetryScheduled = false +let __moduleLifecycleDestroyDepth = 0 + +function __removeManagedEmnapiCleanupListeners() { + const __process = __managedCleanupProcess + const __beforeExitListener = __managedBeforeExitListener + __managedCleanupProcess = undefined + __managedBeforeExitListener = undefined + __managedBeforeExitRegistrationRetryCount = 0 + if (__process && __beforeExitListener) { + try { + __process.removeListener('beforeExit', __beforeExitListener) + } catch {} + } +} + +function __scheduleManagedBeforeExitListenerRegistration() { + if ( + !__managedCleanupProcess || + __managedBeforeExitListener || + __managedEmnapiContextDestroyers.size === 0 || + __managedBeforeExitRegistrationRetryScheduled || + __managedBeforeExitRegistrationRetryCount >= 3 + ) { + return + } + __managedBeforeExitRegistrationRetryScheduled = true + __managedBeforeExitRegistrationRetryCount++ + queueMicrotask(() => { + __managedBeforeExitRegistrationRetryScheduled = false + if ( + !__managedCleanupProcess || + __managedBeforeExitListener || + __managedEmnapiContextDestroyers.size === 0 + ) { + return + } + try { + __registerManagedBeforeExitListener() + } catch {} + }) +} + +function __registerManagedBeforeExitListener() { + if (!__managedCleanupProcess || __managedBeforeExitListener) { + return + } + try { + __managedCleanupProcess.once( + 'beforeExit', + __destroyManagedEmnapiContextsBeforeExit, + ) + } catch (error) { + __scheduleManagedBeforeExitListenerRegistration() + throw error + } + __managedBeforeExitListener = __destroyManagedEmnapiContextsBeforeExit + __managedBeforeExitRegistrationRetryCount = 0 +} + +function __settleManagedEmnapiContextDestroy(__promise) { + if (__managedDestroyPromise === __promise) { + __managedDestroyPromise = undefined + __managedDestroyersInFlight = undefined + } + if (__managedEmnapiContextDestroyers.size === 0) { + __removeManagedEmnapiCleanupListeners() + return + } + try { + __registerManagedBeforeExitListener() + } catch {} +} + +function __destroyManagedEmnapiContexts(__excludedDestroyers) { + if (__managedDestroyPromise) { + return __managedDestroyPromise + } + const __destroyers = Array.from(__managedEmnapiContextDestroyers).filter( + (__destroy) => !__excludedDestroyers?.has(__destroy), + ) + if (__destroyers.length === 0) { + return Promise.resolve() + } + let __resolveDestroy + let __rejectDestroy + const __promise = new Promise((resolve, reject) => { + __resolveDestroy = resolve + __rejectDestroy = reject + }) + __managedDestroyPromise = __promise + __managedDestroyersInFlight = new Set(__destroyers) + void Promise.all( + __destroyers.map((__destroy) => { + try { + return Promise.resolve(__destroy()).then( + () => ({ failed: false }), + (error) => ({ failed: true, error }), + ) + } catch (error) { + return { failed: true, error } + } + }), + ).then((__results) => { + let __primaryError + let __failed = false + for (const __result of __results) { + if (!__result.failed) { + continue + } + if (!__failed) { + __failed = true + __primaryError = __result.error + } else { + __attachCleanupError(__primaryError, __result.error) + } + } + if (__failed) { + __rejectDestroy(__primaryError) + } else { + __resolveDestroy() + } + }, __rejectDestroy) + void __promise.then( + () => { + __settleManagedEmnapiContextDestroy(__promise) + }, + () => { + __settleManagedEmnapiContextDestroy(__promise) + }, + ) + return __promise +} + +async function __drainManagedEmnapiContexts(__excludedDestroyers) { + const __attemptedDestroyers = new Set(__excludedDestroyers) + let __primaryError + let __failed = false + while (true) { + let __promise = __managedDestroyPromise + let __destroyers = __managedDestroyersInFlight + if (!__promise) { + __promise = __destroyManagedEmnapiContexts(__attemptedDestroyers) + __destroyers = __managedDestroyersInFlight + if (!__destroyers) { + break + } + } + for (const __destroy of __destroyers) { + __attemptedDestroyers.add(__destroy) + } + try { + await __promise + } catch (error) { + if (!__failed) { + __failed = true + __primaryError = error + } else { + __attachCleanupError(__primaryError, error) + } + } + } + if (__failed) { + throw __primaryError + } +} + +function __destroyManagedEmnapiContextsBeforeExit() { + // A once listener is consumed before Node invokes it, including when another + // cleanup batch is still pending. + __managedBeforeExitListener = undefined + if (__managedDestroyPromise) { + return + } + void __destroyManagedEmnapiContexts().catch((error) => { + queueMicrotask(() => { + throw error + }) + }) +} + +function __registerManagedEmnapiContext(__process, __destroy) { + __managedEmnapiContextDestroyers.add(__destroy) + if ( + !__managedCleanupProcess && + __process && + typeof __process.once === 'function' && + typeof __process.removeListener === 'function' + ) { + __managedCleanupProcess = __process + } + let __registered = true + return () => { + if (!__registered) { + return + } + __registered = false + __managedEmnapiContextDestroyers.delete(__destroy) + if (__managedEmnapiContextDestroyers.size === 0) { + __removeManagedEmnapiCleanupListeners() + } + } +} + +async function __createManagedEmnapiContext( + __prepareEnvCleanup, + __isPreparingEnvCleanup, +) { + const __process = + typeof process === 'object' && process !== null ? process : undefined + const __finishAutoDestroyCapture = + __captureEmnapiAutoDestroyListener(__process) + let __emnapiContext + let __contextInitializationError + let __contextInitializationFailed = false + try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareEnvCleanup, + __isPreparingEnvCleanup, + ) + // emnapi 2.x still registers an unconditional process.once('beforeExit') + // auto-destroy listener on Node hosts, and suppressDestroy() only + // neutralizes its callback without removing it. This loader must stay + // side-effect free per instance, so the listener is captured and removed; + // suppressDestroy() remains the safety net when removal is unavailable. + __emnapiContext.suppressDestroy() + } catch (error) { + __contextInitializationError = error + __contextInitializationFailed = true + } finally { + // Remove only the exact emnapi callback captured above. + __finishAutoDestroyCapture?.() + } + if (__emnapiContext === undefined) { + throw __contextInitializationError + } + let __disposed = false + let __destroying = false + let __destroyPromise + let __cleanupRegistered = false + let __unregisterCleanup + const __destroy = (__blocksModuleLifecycle = false) => { + if (__disposed) { + return + } + if (__destroying) { + throw __createLifecycleReentryError('dispose') + } + if (__destroyPromise) { + return __destroyPromise + } + __destroying = true + let __result + const __finishDestroyInvocation = () => { + __destroying = false + } + const __finishModuleLifecycleDestroy = () => { + if (__blocksModuleLifecycle) { + __blocksModuleLifecycle = false + __moduleLifecycleDestroyDepth-- + } + } + if (__blocksModuleLifecycle) { + __moduleLifecycleDestroyDepth++ + } + try { + // Context.destroy() disables JS before cleanup hooks run, so settle + // runtime-owned promises while this environment can still call JS. + __prepareEnvCleanup?.() + if (__isPreparingEnvCleanup?.()) { + // Reached from inside the barrier, so `Context.destroy()` below would + // hit the wrapper's in-flight no-op. Recording that as a completed + // destroy is what makes the frame that *did* start the barrier skip the + // real one afterwards, leaving the context retained with its cleanup + // hooks unrun. Refuse instead: nothing is flagged, the context stays + // registered for managed beforeExit cleanup, and a later destroy still + // works. dispose() coalesces reentrancy before it can get here, so this + // is the backstop for any other caller that manages to. + throw __createLifecycleReentryError('dispose') + } + __result = __emnapiContext.destroy() + } catch (error) { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + throw error + } + let __then + try { + if ( + __result !== null && + (typeof __result === 'object' || typeof __result === 'function') + ) { + __then = __result.then + } + } catch (error) { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + throw error + } + if (typeof __then === 'function') { + let __resolveResult + let __rejectResult + const __resultPromise = new Promise((resolve, reject) => { + __resolveResult = resolve + __rejectResult = reject + }) + const __promise = __resultPromise.then( + (value) => { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __disposed = true + __destroyPromise = undefined + __unregisterCleanup?.() + return value + }, + (error) => { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __destroyPromise = undefined + throw error + }, + ) + __destroyPromise = __promise + try { + Reflect.apply(__then, __result, [__resolveResult, __rejectResult]) + } catch (error) { + __rejectResult(error) + } + return __promise + } + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __disposed = true + __unregisterCleanup?.() + } + const __destroyForModuleLifecycle = () => __destroy(true) + const __registerCleanup = ( + __beforeExitDestroy = __destroyForModuleLifecycle, + ) => { + if (__cleanupRegistered || __disposed) { + return + } + __unregisterCleanup = __registerManagedEmnapiContext( + __process, + __beforeExitDestroy, + ) + __cleanupRegistered = true + __registerManagedBeforeExitListener() + } + if (__contextInitializationFailed) { + let __registrationError + let __registrationFailed = false + try { + __registerCleanup() + } catch (error) { + __attachCleanupError(__contextInitializationError, error) + __registrationError = error + __registrationFailed = true + } + try { + await __destroyForModuleLifecycle() + } catch (error) { + __attachCleanupError( + __registrationFailed + ? __registrationError + : __contextInitializationError, + error, + ) + try { + __registerManagedBeforeExitListener() + } catch {} + } + throw __contextInitializationError + } + return { + context: __emnapiContext, + destroy: __destroy, + destroyForModuleLifecycle: __destroyForModuleLifecycle, + registerCleanup: __registerCleanup, + } +} + +async function __createInstance( + __wasmInput, + __options, + __beforeExitDestroy, + __onManagedDestroyer, +) { + const __module = await __resolveModule(__wasmInput) + const __emnapiModule = await __normalizeModuleForEmnapi(__module) + const __wasi = new __WASI({ + version: 'preview1', + }) + // The wasm module is linked with `--import-memory`, so a Memory must be + // provided. It is resolved here in function scope (workerd bans global scope + // allocation) and is never shared (no threads, no SharedArrayBuffer). + // Resolve it before the emnapi context so a rejected option bag or a host + // memory-limit failure cannot leak a context that never reaches + // instantiation. + const __wasmMemory = __resolveInstanceMemory(__options) + let __lifecycleState = 'pending' + let __destroyEmnapiContext + let __destroyOwnedContext + let __destroyManagedOwnedContext + let __napiInstance + let __wasmEnvCleanupRan = false + let __wasmEnvCleanupPrepared = false + let __wasmEnvCleanupPreparing = false + let __wasmEnvCleanupDrained = false + let __wasmEnvCleanupDrainPromise + const __isPreparingEnvCleanup = () => __wasmEnvCleanupPreparing + const __prepareEnvCleanup = () => { + if (__wasmEnvCleanupPrepared || __wasmEnvCleanupPreparing) { + return + } + const __prepareWasmEnvCleanup = + __napiInstance?.exports.napi_prepare_wasm_env_cleanup + if (typeof __prepareWasmEnvCleanup === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __wasmEnvCleanupPreparing = true + try { + __prepareWasmEnvCleanup() + } finally { + __wasmEnvCleanupPreparing = false + } + __wasmEnvCleanupRan = true + } + __wasmEnvCleanupPrepared = true + } + // The barrier + settlement drain, hoisted out of the context destroyer so the + // drain can yield without widening the destroyer's reentry window. Both + // yielding paths run it — dispose() and the initialization-failure rollback. + // The destroyer still runs the barrier itself (idempotently) for the one path + // that cannot yield: managed beforeExit cleanup of an instance whose rollback + // is being retried. + // + // "Drained" is recorded only once a wait has actually finished. Scheduling a + // macrotask can fail — a host-provided or patched `setImmediate` that throws + // is enough — and dispose() stays retryable after it rejects, so marking the + // drain complete up front would make the retry skip it and destroy the context + // with the barrier's settlements still queued. + const __prepareForDisposal = () => { + if (__wasmEnvCleanupDrained) { + return + } + if (__wasmEnvCleanupDrainPromise) { + return __wasmEnvCleanupDrainPromise + } + __prepareEnvCleanup() + if (!__wasmEnvCleanupRan) { + return + } + const __drained = __drainWasmEnvCleanup(__napiInstance) + if (!__drained || typeof __drained.then !== 'function') { + __wasmEnvCleanupDrained = true + return + } + const __tracked = __drained.then( + (__value) => { + __wasmEnvCleanupDrained = true + __wasmEnvCleanupDrainPromise = undefined + return __value + }, + (__error) => { + __wasmEnvCleanupDrainPromise = undefined + throw __error + }, + ) + __wasmEnvCleanupDrainPromise = __tracked + return __tracked + } + let __disposed = false + const __runInstanceDisposal = async () => { + if (__lifecycleState !== 'failed') { + __lifecycleState = 'disposal' + } + // Outstanding async work first, while the environment is still completely + // live: its completion callbacks run addon code, and the barrier and + // `Context.destroy()` below each take that away. Undefined unless work is + // outstanding. + const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance) + if (__asyncWorkDrained) { + await __asyncWorkDrained + } + // Then settle what the barrier cancelled, before the environment stops + // accepting JavaScript calls. Undefined unless something is queued, so + // an idle disposal is not delayed by a single turn. + const __drained = __prepareForDisposal() + if (__drained) { + await __drained + } + const __result = await (__beforeExitDestroy + ? __destroyManagedOwnedContext() + : __destroyOwnedContext()) + // Only a completed destroy retires the instance; a throw above leaves + // the counter untouched so a retried dispose() cannot double-decrement. + if (!__disposed) { + __disposed = true + __liveInstances -= 1 + } + return __result + } + let __instanceDisposePromise + /** + * The disposal frame runs the barrier, and the barrier settles the promises it + * cancels synchronously — so a promise hook firing inside it can call this + * same instance's dispose() again while the first call is still in its drain. + * That nested call finds the barrier flagged in flight, prepares nothing, + * drains nothing, and falls straight through to the context destroyer, whose + * `Context.destroy()` hits the wrapper's in-flight no-op. It would record a + * destruction that never happened, and the outer frame would then skip the + * real one: both disposals resolve, no cleanup hook runs, the context stays + * retained. + * + * Memoize before any of that starts, exactly like the eager loaders' + * `__disposeWasiBinding`, so there is only ever one disposal frame per + * instance and a reentrant caller awaits it instead of racing it. Cleared on + * rejection: a drain that failed has to stay retryable. + */ + const __disposeInstance = () => { + if (__instanceDisposePromise) { + return __instanceDisposePromise + } + let __resolveDispose + let __rejectDispose + const __disposePromise = new Promise((__resolve, __reject) => { + __resolveDispose = __resolve + __rejectDispose = __reject + }) + __instanceDisposePromise = __disposePromise + __runInstanceDisposal().then(__resolveDispose, (__error) => { + __instanceDisposePromise = undefined + __rejectDispose(__error) + }) + return __disposePromise + } + const __destroyBeforeExit = __beforeExitDestroy + ? async () => { + if (__lifecycleState === 'failed') { + await __destroyManagedOwnedContext() + return + } + __lifecycleState = 'disposal' + try { + await __beforeExitDestroy() + } catch (error) { + if (__lifecycleState !== 'failed') { + throw error + } + // The singleton's initialization rejection is already observable + // through instantiate() and dispose(). Managed beforeExit cleanup + // owns only context destruction, including retrying a failed rollback. + await __destroyManagedOwnedContext() + } + } + : undefined + const { + context: __emnapiContext, + destroy, + destroyForModuleLifecycle, + registerCleanup: __registerCleanup, + } = await __createManagedEmnapiContext( + __prepareEnvCleanup, + __isPreparingEnvCleanup, + ) + __destroyEmnapiContext = destroy + __destroyOwnedContext = () => __destroyEmnapiContext() + __destroyManagedOwnedContext = destroyForModuleLifecycle + try { + if (__destroyBeforeExit) { + __onManagedDestroyer(__destroyBeforeExit) + await __registerCleanup(__destroyBeforeExit) + } + let __napiModule + ;({ + instance: __napiInstance, + napiModule: __napiModule, + } = await __emnapiInstantiateNapiModule(__emnapiModule, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + // `instantiate()` and `createInstance().exports` hand out this object; a + // named module export does not travel with it. After the instance host + // install, which hands the same object to addon-provided registration + // functions that may put anything on it, and inside this `try`, so a + // claimed name flips `__lifecycleState` to 'failed' and tears the instance + // down rather than escaping a half-built one. + __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) + if (__lifecycleState === 'pending') { + __lifecycleState = 'succeeded' + } + __createdInstances += 1 + __liveInstances += 1 + return { + exports: __napiModule.exports, + get memory() { + return __wasmMemory + }, + get memoryBytes() { + // The Memory outlives the environment, so this stays readable after a + // FAILED dispose() (which leaves the instance undisposed and + // retryable). It reports 0 only once disposal has actually completed. + return __disposed ? 0 : __wasmMemory.buffer.byteLength + }, + get disposed() { + return __disposed + }, + dispose: __disposeInstance, + } + } catch (error) { + __lifecycleState = 'failed' + // Instantiation can fail *after* registration has run, and registration runs + // with a live environment: a module-init hook can start async work and then + // return an error, and the promise it created may already have escaped into + // JavaScript. Settle what the barrier cancels before the context is + // destroyed, exactly like dispose() does; destroying without yielding + // discards the queue with a null env. Undefined unless something is queued, + // so a failure before beforeInit costs no extra turn. + let __settlementsUnreached = false + try { + // Registration can start async work too, and this path destroys the same + // environment its completions need. A drain that cannot finish leaves the + // work outstanding, so it counts as settlements unreached and stops the + // rollback short of destroying, exactly like a failed barrier drain. + const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance) + if (__asyncWorkDrained) { + await __asyncWorkDrained + } + const __drained = __prepareForDisposal() + if (__drained) { + await __drained + } + } catch (drainError) { + __attachCleanupError(error, drainError) + __settlementsUnreached = true + } + let __registrationError + let __registrationFailed = false + if (!__beforeExitDestroy) { + try { + // Independent instances are caller-owned while pending and after + // success. Register only failed rollback so cleanup remains retryable. + await __registerCleanup() + } catch (registrationError) { + __attachCleanupError(error, registrationError) + __registrationError = registrationError + __registrationFailed = true + } + } + if (__settlementsUnreached) { + // The barrier or the drain did not finish, so the settlements it queued + // are still in the threadsafe-function queue. Destroying now runs the + // cleanup hook that drains that queue with a null env and discards it, + // stranding a promise that already escaped into JavaScript — with nothing + // left that could ever settle it. dispose() refuses to destroy for exactly + // this reason (a rejected drain there never reaches the destroy), so this + // path refuses too. + // + // Nothing leaks. The registration just above — and, for the singleton, the + // one made before instantiation — leaves this context in + // `__managedEmnapiContextDestroyers`, so beforeExit destroys it and + // dispose() can retry it. Only the destruction is deferred, and the turns + // that pass in the meantime are exactly what the queue needed. + try { + __registerManagedBeforeExitListener() + } catch {} + throw error + } + try { + await __destroyManagedOwnedContext() + } catch (disposeError) { + // Initialization is the primary failure. Preserve it even if cleanup + // also fails, while retaining the cleanup error when the value is + // extensible and has no existing cause. + __attachCleanupError( + __registrationFailed ? __registrationError : error, + disposeError, + ) + try { + __registerManagedBeforeExitListener() + } catch {} + } + throw error + } +} + +/** + * Create an independent instance. Call and await dispose() when the instance + * is no longer needed so emnapi cleanup hooks run deterministically. + * + * The optional second argument selects this instance's linear memory: either + * `memory` (an unshared, single-use WebAssembly.Memory you allocated) or + * `initialMemoryPages` / `maximumMemoryPages`, never both. Omitted, the + * loader allocates WASM_MEMORY.initialPages..WASM_MEMORY.maximumPages. + * + * A provided Memory must come from this loader's own realm: the WASI and + * emnapi layers underneath identify one with a realm-local `instanceof`, so a + * Memory built in a `node:vm` context or another frame is rejected. Every + * Memory an instance runs on is single-use, the loader-allocated one included: + * `instance.memory` cannot be recycled into a second `createInstance()`. + */ +export async function createInstance(__wasmInput, __options) { + return __createInstance(__wasmInput, __options) +} + +let __defaultModulePromise +let __defaultInstancePromise +let __defaultDisposePromise +let __defaultDisposalStarted = false +const __defaultManagedDestroyers = new WeakMap() +let __moduleDisposePromise + +/** + * Instantiate a module-local singleton. Concurrent and repeated calls + * with the same module share one instance and one Memory allocation. + */ +export function instantiate(__wasmInput) { + const __modulePromise = __resolveModule(__wasmInput) + if (__moduleLifecycleDestroyDepth !== 0) { + void __modulePromise.catch(() => {}) + return Promise.reject(__createLifecycleReentryError('instantiate')) + } + if (__moduleDisposePromise) { + void __modulePromise.catch(() => {}) + return __moduleDisposePromise.then(() => instantiate(__modulePromise)) + } + if (__defaultDisposalStarted) { + // Observe rejected input immediately, but preserve lifecycle ordering and + // error precedence by instantiating only after disposal succeeds. A failed + // disposal retains the old instance only so its cleanup can be retried. + void __modulePromise.catch(() => {}) + const __disposePromise = __defaultDisposePromise ?? dispose() + return __disposePromise.then(() => instantiate(__modulePromise)) + } + if (!__defaultInstancePromise) { + __defaultModulePromise = __modulePromise + const __instancePromise = __modulePromise.then((__module) => + __createInstance( + __module, + undefined, + __disposeDefaultInstance, + (__managedDestroyer) => { + __defaultManagedDestroyers.set( + __instancePromise, + __managedDestroyer, + ) + }, + ), + ) + __defaultInstancePromise = __instancePromise + void __instancePromise.catch(() => { + if (__defaultInstancePromise === __instancePromise) { + __defaultInstancePromise = undefined + __defaultModulePromise = undefined + } + }) + return __instancePromise.then((__instance) => __instance.exports) + } + const __defaultModulePromiseForCall = __defaultModulePromise + const __defaultInstancePromiseForCall = __defaultInstancePromise + return Promise.all([__defaultModulePromiseForCall, __modulePromise]).then( + async ([__defaultModule, __module]) => { + if (__defaultModule !== __module) { + throw new Error( + 'instantiate() already owns a different WebAssembly.Module; call dispose() first or use createInstance() for independent instances.', + ) + } + return (await __defaultInstancePromiseForCall).exports + }, + ) +} + +async function __disposeDefaultInstance(__onDestroy) { + if (__defaultDisposePromise) { + return __defaultDisposePromise + } + const __instancePromise = __defaultInstancePromise + if (!__instancePromise) { + __defaultDisposalStarted = false + return + } + __defaultDisposalStarted = true + const __disposePromise = (async () => { + let __instance + try { + __instance = await __instancePromise + } catch (error) { + const __managedDestroyer = + __defaultManagedDestroyers.get(__instancePromise) + if (__managedDestroyer) { + __onDestroy?.(__managedDestroyer) + } + __defaultManagedDestroyers.delete(__instancePromise) + throw error + } + const __managedDestroyer = + __defaultManagedDestroyers.get(__instancePromise) + if (__managedDestroyer) { + __onDestroy?.(__managedDestroyer) + } + await __instance.dispose() + if (__defaultInstancePromise === __instancePromise) { + __defaultInstancePromise = undefined + __defaultModulePromise = undefined + __defaultDisposalStarted = false + } + __defaultManagedDestroyers.delete(__instancePromise) + })() + __defaultDisposePromise = __disposePromise + try { + await __disposePromise + } finally { + if (__defaultDisposePromise === __disposePromise) { + __defaultDisposePromise = undefined + } + } +} + +async function __dispose() { + let __defaultDisposeError + let __defaultDisposeFailed = false + let __attemptedDefaultDestroyer + try { + await __disposeDefaultInstance((__destroyer) => { + __attemptedDefaultDestroyer = __destroyer + }) + } catch (error) { + __defaultDisposeError = error + __defaultDisposeFailed = true + } + const __excludedDestroyers = new Set() + if (__defaultDisposeFailed && __attemptedDefaultDestroyer) { + __excludedDestroyers.add(__attemptedDefaultDestroyer) + } + try { + await __drainManagedEmnapiContexts(__excludedDestroyers) + } catch (error) { + if (!__defaultDisposeFailed) { + throw error + } + if (error !== __defaultDisposeError) { + __attachCleanupError(__defaultDisposeError, error) + } + } + if (__defaultDisposeFailed) { + throw __defaultDisposeError + } +} + +/** + * Dispose the singleton created by instantiate(). A later call may create a + * fresh instance, including from a different module. This also retries cleanup + * retained after a failed initialization rollback. + */ +export function dispose() { + if (__moduleLifecycleDestroyDepth !== 0) { + return Promise.reject(__createLifecycleReentryError('dispose')) + } + if (__moduleDisposePromise) { + return __moduleDisposePromise + } + let __resolveDispose + let __rejectDispose + const __promise = new Promise((resolve, reject) => { + __resolveDispose = resolve + __rejectDispose = reject + }) + __moduleDisposePromise = __promise + void __dispose().then(__resolveDispose, __rejectDispose) + void __promise.then( + () => { + if (__moduleDisposePromise === __promise) { + __moduleDisposePromise = undefined + } + }, + () => { + if (__moduleDisposePromise === __promise) { + __moduleDisposePromise = undefined + } + }, + ) + return __promise +} diff --git a/napi/parser/src-js/parser.wasip1.cjs b/napi/parser/src-js/parser.wasip1.cjs new file mode 100644 index 0000000000..103fc5e0c4 --- /dev/null +++ b/napi/parser/src-js/parser.wasip1.cjs @@ -0,0 +1,1214 @@ +// napi-rs-artifact-metadata:{"version":2,"rootEntry":"bindings.js","exports":["Severity","ParseResult","ExportExportNameKind","ExportImportNameKind","ExportLocalNameKind","ImportNameKind","parse","parseSync","rawTransferSupported"],"managedRootEntries":["browser.js","bindings.js","parser.wasm","parser.debug.wasm"]} +/* eslint-disable */ +/* auto-generated by NAPI-RS */ + +const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +const __nodeFs = require('node:fs') +const __nodePath = require('node:path') +const { WASI: __nodeWASI } = require('node:wasi') + +const { + emnapiAsyncWorkPlugin: __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin: __emnapiTSFNPlugin, + instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync, +} = require('@napi-rs/wasm-runtime') +const { createContext: __emnapiCreateContext } = require('@emnapi/runtime') + +const __cwd = process.cwd() +const __rootDir = __nodePath.parse(__cwd).root +const __hostRoot = + process.platform === 'android' ? __cwd : __rootDir + +const __wasi = new __nodeWASI({ + version: 'preview1', + env: process.env, + preopens: { + [__rootDir]: __hostRoot, + [__hostRoot]: __hostRoot, + }, +}) + +const __wasmMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, +}) + +let __wasmFilePath = __nodePath.join(__dirname, 'parser.wasm32-wasip1.wasm') +const __wasmDebugFilePath = __nodePath.join(__dirname, 'parser.wasm32-wasip1.debug.wasm') + +if (__nodeFs.existsSync(__wasmDebugFilePath)) { + __wasmFilePath = __wasmDebugFilePath +} else if (!__nodeFs.existsSync(__wasmFilePath)) { + const __wasiPackageEntry = require.resolve('@oxc-parser/binding-wasm32-wasip1') + const __packagedWasmFilePath = __nodePath.join( + __nodePath.dirname(__wasiPackageEntry), + 'parser.wasm32-wasip1.wasm', + ) + if (!__nodeFs.existsSync(__packagedWasmFilePath)) { + throw new Error( + '@oxc-parser/binding-wasm32-wasip1 is installed but is missing parser.wasm32-wasip1.wasm.', + ) + } + __wasmFilePath = __packagedWasmFilePath +} + +const __wasmFile = __nodeFs.readFileSync(__wasmFilePath) +let __emnapiContext + +const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose') +const __wasiWorkers = new Set() +// The thread manager has to be reachable *before* anything that can throw +// during load or registration. Initialization can fail after the pool has +// already spawned workers, and the rollback still has to mark their +// terminations as expected — but `__napiModule` is assigned only when +// instantiation RETURNS, so on exactly that path it is still undefined. A +// plugin factory runs while the emnapi module is being created, before the +// wasm is loaded and before any registration function runs, and its context +// carries the very same manager instance. +let __wasiThreadManager + +function __captureWasiThreadManager(context) { + if (context && context.PThread) { + __wasiThreadManager = context.PThread + } + return {} +} + +function __getWasiThreadManager() { + const manager = + __wasiThreadManager !== undefined + ? __wasiThreadManager + : __napiModule + ? __napiModule.PThread + : undefined + if (manager && typeof manager.terminateWorker === 'function') { + return manager + } + return undefined +} +let __napiInstance +let __emnapiContextDestroyed = false +let __emnapiContextDestroyPromise +let __emnapiWasmEnvCleanupPrepared = false +let __emnapiWasmEnvCleanupPreparing = false +let __emnapiWasmEnvCleanupRan = false +let __emnapiWasmEnvCleanupDrained = false +let __emnapiWasmEnvCleanupDrainPromise +let __wasiDisposed = false +let __wasiAsyncWorkDrainPromise +let __wasiDisposePromise +let __completeWasiDisposal = function () {} +// Overridden by loader flavors that have a last-resort reclaim for a rollback +// that stopped short of destroying the context. See +// `__rollbackWasiInitialization`. +let __retainWasiRollbackForRetry = function () {} + +function __isThenable(value) { + return ( + value !== null && + (typeof value === 'object' || typeof value === 'function') && + typeof value.then === 'function' + ) +} + +function __createCleanupError(errors, message) { + if (errors.length === 1) { + return errors[0] + } + const __AggregateError = globalThis.AggregateError + if (typeof __AggregateError === 'function') { + return new __AggregateError(errors, message) + } + const error = new Error(message) + error.errors = errors + return error +} + +function __attachCleanupErrors(error, cleanupErrors) { + if (cleanupErrors.length === 0) { + return error + } + const cleanupError = __createCleanupError( + cleanupErrors, + 'WASI binding cleanup failed', + ) + try { + if ( + error && + (typeof error === 'object' || typeof error === 'function') + ) { + if (error.cause === undefined) { + error.cause = cleanupError + if (error.cause === cleanupError) { + return error + } + } + if (Array.isArray(error.cleanupErrors)) { + error.cleanupErrors.push(cleanupError) + return error + } else { + const attachedCleanupErrors = [cleanupError] + error.cleanupErrors = attachedCleanupErrors + if (error.cleanupErrors === attachedCleanupErrors) { + return error + } + } + } + } catch {} + const aggregate = __createCleanupError( + [error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + try { + aggregate.cause = error + } catch {} + return aggregate +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __isPreparingWasmEnvCleanup() { + return __emnapiWasmEnvCleanupPreparing +} + +function __prepareWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) { + return + } + const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup + if (typeof prepare === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __emnapiWasmEnvCleanupPreparing = true + try { + prepare() + } finally { + __emnapiWasmEnvCleanupPreparing = false + } + __emnapiWasmEnvCleanupRan = true + } + __emnapiWasmEnvCleanupPrepared = true +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (callback) { + setImmediate(callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (callback) { + const channel = new __MessageChannel() + channel.port1.onmessage = function () { + channel.port1.onmessage = null + try { + channel.port1.close() + } catch {} + try { + channel.port2.close() + } catch {} + callback() + } + channel.port2.postMessage(null) + } + } + return function (callback) { + setTimeout(callback, 0) + } +})() + +// A real, *referenced* timer, for waits that must let the whole host make +// progress between looks — the async-work drain polls the addon rather than +// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there +// would spin the loop instead of yielding it. Falls back to the macrotask +// scheduler on a host without timers. +function __scheduleTimer(callback, delay) { + const setTimer = globalThis.setTimeout + if (typeof setTimer !== 'function') { + __scheduleMacrotask(callback) + return + } + try { + setTimer(callback, delay) + } catch { + __scheduleMacrotask(callback) + } +} + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + * + * The "already drained" flag is set only once a wait has actually finished. + * Scheduling a macrotask can fail — a host-provided or patched `setImmediate` + * that throws is enough — and a disposal that rejects stays retryable, so + * marking the drain complete up front would make the retry skip it and destroy + * the context with the barrier's settlements still queued. + * + * A wait that runs out of turns with the counter still nonzero rejects with + * `ERR_NAPI_WASI_CLEANUP_PENDING` for the same reason: at that point + * "finished" is indistinguishable from the stranding above, and destroying + * would discard the very settlement the wait was for. The rejection leaves the + * flag unset and disposal retryable. + */ +function __drainWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupDrained || !__emnapiWasmEnvCleanupRan) { + return + } + if (__emnapiWasmEnvCleanupDrainPromise) { + return __emnapiWasmEnvCleanupDrainPromise + } + const pending = __napiInstance?.exports?.napi_wasm_env_cleanup_pending + const observable = typeof pending === 'function' + if (observable) { + let queued + try { + queued = pending() + } catch { + __emnapiWasmEnvCleanupDrained = true + return + } + if (!queued) { + __emnapiWasmEnvCleanupDrained = true + return + } + } + const limit = observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + const drainPromise = (async () => { + let queued = 0 + for (let turn = 0; turn < limit; turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!observable) { + continue + } + try { + queued = pending() + } catch { + return + } + if (!queued) { + return + } + } + if (!observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: the drained flag stays + // unset, dispose() (and the rollback) decline to destroy, and a later + // dispose() runs the drain again — by which time the queue has usually been + // delivered. A counter that is somehow stuck nonzero therefore costs each + // attempt at most another bounded wait and a rejection, never a stranded + // promise; the process-exit teardown still reclaims the context. + const drainError = new Error( + 'the wasm environment still reports ' + + queued + + ' queued settlement(s) after ' + + limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw drainError + })().then( + (value) => { + // Set only when the wait actually finished AND the queue was seen empty + // (or is unobservable): a drain that timed out with settlements still + // queued rejects above and must stay repeatable. + __emnapiWasmEnvCleanupDrained = true + __emnapiWasmEnvCleanupDrainPromise = undefined + return value + }, + (error) => { + __emnapiWasmEnvCleanupDrainPromise = undefined + throw error + }, + ) + __emnapiWasmEnvCleanupDrainPromise = drainPromise + return drainPromise +} + +function __destroyEmnapiContext() { + if (__emnapiContextDestroyed || __emnapiContext === undefined) { + __emnapiContextDestroyed = true + return + } + if (__emnapiContextDestroyPromise) { + return __emnapiContextDestroyPromise + } + + __prepareWasmEnvCleanup() + const result = __emnapiContext.destroy() + if (!__isThenable(result)) { + __emnapiContextDestroyed = true + return + } + + const destroyPromise = Promise.resolve(result).then( + (value) => { + __emnapiContextDestroyed = true + return value + }, + (error) => { + __emnapiContextDestroyPromise = undefined + throw error + }, + ) + __emnapiContextDestroyPromise = destroyPromise + return destroyPromise +} + +/** + * Holds the event loop open until `work` settles. + * + * Nothing else can: the pool workers are deliberately unreferenced so an idle + * binding cannot keep a process alive, and referencing them again for the + * termination does not hold either — emnapi unreferences a worker the moment it + * reports `async-thread-ready`, which for a worker that was still starting + * lands *after* the termination began. Without a handle of its own, an + * `await dispose()` with nothing else pending exits the process with its + * promise unsettled, and everything after the `await` is skipped. + * + * The timer is cleared as soon as the work settles, so this never outlives the + * disposal that asked for it. + */ +function __keepEventLoopAliveUntil(work) { + const setTimer = globalThis.setInterval + const clearTimer = globalThis.clearInterval + if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') { + return work + } + let timer + try { + timer = setTimer(function () {}, 50) + } catch { + return work + } + const release = function () { + try { + clearTimer(timer) + } catch {} + } + return work.then( + (value) => { + release() + return value + }, + (error) => { + release() + throw error + }, + ) +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this addon's outstanding `napi_async_work` before the teardown that + * would strand it. + * + * `napi_prepare_wasm_env_cleanup` does not cover async work, and nothing about + * it is observable from JavaScript: the threadless archive resolves + * `napi_*_async_work` through the `@emnapi/core` plugins, but the threaded one + * links the C `async_work.c` on the uv threadpool, so there the wasm neither + * imports nor exports those symbols and the only brackets a loader could watch + * (`_emnapi_ctx_*_waiting_request_counter`) are shared with threadsafe + * functions. The addon is the one place both flavors go through, so it answers + * for both, through the same kind of handshake the settlement drain uses: + * + * - `napi_wasm_cancel_pending_async_work()` cancels what no thread has + * started. Those completion callbacks run with `napi_cancelled`, which + * napi-rs turns into a promise rejected with an `AbortError`. + * - `napi_wasm_async_work_pending()` counts what is still owed a completion + * callback. Work already executing refuses cancellation and stays counted + * until it finishes normally — which it can, because this runs before the + * barrier, before `Context.destroy()` and before anything is terminated. + * + * Both exports are optional: an addon built against a napi crate that predates + * them drains nothing and keeps the previous behavior, exactly as the + * `napi_wasm_env_cleanup_pending` handshake degrades. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. The promise it returns otherwise never + * rejects. + * + * The wait has no deadline, and that is the point: giving up would destroy the + * environment with a completion callback still owed, which is the stranding + * this exists to prevent. A task whose `execute` never returns already keeps an + * *undisposed* process alive in exactly the same way, so disposal inherits that + * rather than inventing a bound it cannot honor. + * + * Safe to call from inside a completion callback, which is reachable: settling + * a task runs addon code that can re-enter JavaScript — a setter on the value + * being handed back, a threadsafe-function callback — and that JavaScript can + * call `dispose()`. Two things make it terminate rather than wait on itself: + * + * - The addon keeps a work registered until its completion callback + * *finishes*, so the count read here is at least one and this takes the + * polling path instead of declaring the environment drained and tearing it + * down from inside the frame that is still settling a promise. + * - The poll is a timer, so it cannot run until the callback has returned to + * the host — by which time that work has left the registry. The count the + * next poll reads is the one taken after the callback finished. + * + * `__disposeWasiBinding` hands every caller the same in-flight promise, so the + * nested call joins this disposal rather than starting a second one. + */ +function __drainWasiAsyncWork() { + if (__wasiAsyncWorkDrainPromise !== undefined) { + return __wasiAsyncWorkDrainPromise + } + const exports = __napiInstance?.exports + const pending = exports?.napi_wasm_async_work_pending + const cancelPending = exports?.napi_wasm_cancel_pending_async_work + if (typeof pending !== 'function' || typeof cancelPending !== 'function') { + return + } + + const readPending = () => { + try { + return pending() + } catch (error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition — + // there is nothing left to wait for, and refusing to dispose would only + // keep a dead instance and its stuck counter alive. Best-effort here is + // the honest answer, and it is what disposal did before this drain + // existed. + // + // Only a trap. Anything else means the export is not what this loader + // thinks it is, which is a defect worth surfacing rather than disposing + // over. + if (error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw error + } + } + + if (!readPending()) { + return + } + try { + cancelPending() + } catch { + // Cancellation is an optimization: it bounds the wait by the work already + // executing. Failing it only means waiting for the whole queue instead. + } + if (!readPending()) { + return + } + + const drainPromise = __keepEventLoopAliveUntil( + (async () => { + while (readPending()) { + await new Promise((resolve) => { + __scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })(), + ).then( + () => { + __wasiAsyncWorkDrainPromise = undefined + }, + (error) => { + // A wait that could not run is not a wait that finished. The only way + // here is a host whose timers and macrotask primitives all refuse, and + // the work is still outstanding — reporting success would destroy the + // environment over it, which is the stranding this exists to prevent. + // Reject instead: disposal stays retryable, and the context is not + // destroyed. Clearing the memo first is what makes the retry re-run this. + __wasiAsyncWorkDrainPromise = undefined + throw error + }, + ) + __wasiAsyncWorkDrainPromise = drainPromise + return drainPromise +} + +/** + * `@emnapi/wasi-threads` counts a worker exit as expected only when its own + * thread manager performed the termination. A bare `worker.terminate()` reaches + * the manager's `exit` listener instead, which reports + * `worker (tid = N) sent an error! ... stopped with exit code 1` and rethrows + * inside the emit — aborting the `once('exit')` that backs the terminate + * promise, so disposal never settles and the process dies with an uncaught + * exception. Mark the termination through the manager first. + * + * The manager comes from `__getWasiThreadManager`, not from `__napiModule`: + * the initialization rollback runs on the one path where instantiation never + * returned, so `__napiModule` is still undefined there while the workers it + * spawned are already registered and loaded. + * + * Not `terminateAllThreads()`: that one recreates the pool it just shut down. + */ +function __terminateWasiWorkers() { + const cleanupErrors = [] + const pending = [] + const threadManager = __getWasiThreadManager() + + for (const worker of __wasiWorkers) { + let result + try { + if (threadManager) { + threadManager.terminateWorker(worker) + // `terminateWorker` leaves behind a reporter that logs every message + // still queued on the port, which Node flushes on exit. Nothing is + // listening for those any more. + worker.onmessage = undefined + } + result = worker.terminate() + } catch (error) { + cleanupErrors.push(error) + continue + } + if (__isThenable(result)) { + pending.push( + Promise.resolve(result).then( + () => { + __wasiWorkers.delete(worker) + }, + (error) => { + cleanupErrors.push(error) + }, + ), + ) + } else { + __wasiWorkers.delete(worker) + } + } + + const finish = () => { + if (cleanupErrors.length > 0) { + throw __createCleanupError( + cleanupErrors, + 'Failed to terminate WASI workers', + ) + } + } + return pending.length > 0 + ? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish) + : finish() +} + +function __finishWasiDisposal() { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult).then(__completeWasiDisposal) + } + return __completeWasiDisposal() +} + +function __continueWasiDisposal() { + const destroyResult = __destroyEmnapiContext() + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult).then(__finishWasiDisposal) + } + return __finishWasiDisposal() +} + +function __cleanUpWasmEnvForWasiDisposal() { + // Run the pre-teardown barrier, then let the settlements it queued actually + // reach JavaScript, and only then destroy the environment. Doing these two + // back to back is what strands them. + __prepareWasmEnvCleanup() + const drainResult = __drainWasmEnvCleanup() + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then(__continueWasiDisposal) + } + return __continueWasiDisposal() +} + +function __startWasiDisposal() { + // Outstanding `napi_async_work` goes first, while the environment is still + // completely live: the completion callbacks run addon code, and everything + // after this point takes that away from them — the barrier shuts the async + // runtime down, `Context.destroy()` stops JavaScript calls, and terminating + // the pool threads removes what would have reported the work finished. + const asyncWorkResult = __drainWasiAsyncWork() + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __cleanUpWasmEnvForWasiDisposal, + ) + } + return __cleanUpWasmEnvForWasiDisposal() +} + +/** + * Disposes this generated WASI binding. + * + * Access this function with: + * binding[Symbol.for('napi.rs.wasi.dispose')]() + */ +function __disposeWasiBinding() { + if (__wasiDisposePromise) { + return __wasiDisposePromise + } + if (__wasiDisposed) { + return Promise.resolve() + } + + let resolveDispose + let rejectDispose + const disposePromise = new Promise((resolve, reject) => { + resolveDispose = resolve + rejectDispose = reject + }) + __wasiDisposePromise = disposePromise + + let result + try { + result = __startWasiDisposal() + } catch (error) { + __wasiDisposePromise = undefined + rejectDispose(error) + return disposePromise + } + + Promise.resolve(result).then( + (value) => { + __wasiDisposed = true + resolveDispose(value) + }, + (error) => { + __wasiDisposePromise = undefined + rejectDispose(error) + }, + ) + return disposePromise +} + +function __publishWasiDispose(exports) { + Object.defineProperty(exports, __wasiDisposeSymbol, { + configurable: false, + enumerable: false, + value: __disposeWasiBinding, + writable: false, + }) +} + +function __finishWasiInitializationRollback(cleanupErrors) { + let workerResult + try { + workerResult = __terminateWasiWorkers() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return cleanupErrors + } + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => cleanupErrors) + } + return cleanupErrors +} + +function __destroyContextForWasiRollback(cleanupErrors) { + let destroyResult + try { + destroyResult = __destroyEmnapiContext() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return __finishWasiInitializationRollback(cleanupErrors) + } + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => __finishWasiInitializationRollback(cleanupErrors)) + } + return __finishWasiInitializationRollback(cleanupErrors) +} + +/** + * Leaves a rollback that could not reach the queued settlements undestroyed, and + * hands it to whatever this flavor has that can still reclaim it. + */ +function __retainFailedWasiRollback(cleanupErrors) { + try { + __retainWasiRollbackForRetry() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + } + return cleanupErrors +} + +/** + * Initialization can fail *after* registration has already run, and registration + * runs with a live environment: a module-init hook can start async work and then + * return an error, and the promise it created may already have escaped into + * JavaScript. The barrier cancels that work and *queues* the settlement, so this + * path needs the same drain the ordinary disposal does — destroying without + * yielding discards the queue with a null env and strands the promise. + * + * Stays synchronous when nothing is queued, which covers every failure before + * `beforeInit`: there is no instance to run the barrier on, so nothing to drain. + * + * A barrier or drain that did *not* finish stops the rollback short of + * destroying, which is what `dispose()` already does — a rejected drain there + * never reaches `__continueWasiDisposal`. Destroying anyway is the worse of the + * two trades, and not because of what it saves: + * + * - It cannot deliver the settlements. `Context.destroy()` runs the + * threadsafe function's cleanup hook, which drains the queue with a null env + * and discards it, so a promise that already escaped into JavaScript hangs + * forever with nothing left that could ever settle it. + * - It saves less than it looks. `Context.destroy()` stops JavaScript calls + * and runs cleanup hooks; it does not free the wasm instance or its Memory, + * which this module's scope holds either way. What stopping short retains is + * the emnapi context's bookkeeping and its un-run cleanup hooks. + * - Retry is not theoretical. A rollback that records a cleanup error is + * already kept in the process-wide registry above, so re-`require()`ing this + * file replays it instead of re-instantiating — and the `6e15de6f` flag fix + * means the replay drains again rather than skipping it. Destroying first is + * what makes that retained record useless. + * + * The residual cost is honest: the CJS flavor hands the context to its + * `process.on('exit')` teardown, so a process that never retries still reclaims + * it on the way out. The ESM browser flavor has no equivalent — a module that + * throws while evaluating is permanently errored, so re-importing rethrows + * without re-running this file — and there the context stays until the realm + * goes away. That is the deliberate choice: a hung promise is a silent liveness + * bug with no upper bound, while the retained bookkeeping is bounded by the page. + */ +function __rollbackWasiInitialization() { + // The environment teardown this rollback performs, kept nested so it cannot + // be reached without the async-work drain below running first. + function __rollbackWasmEnvForWasiInitialization() { + const cleanupErrors = [] + let drainResult + let settlementsUnreached = false + try { + __prepareWasmEnvCleanup() + drainResult = __drainWasmEnvCleanup() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + settlementsUnreached = true + } + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then( + () => __destroyContextForWasiRollback(cleanupErrors), + (cleanupError) => { + cleanupErrors.push(cleanupError) + return __retainFailedWasiRollback(cleanupErrors) + }, + ) + } + if (settlementsUnreached) { + return __retainFailedWasiRollback(cleanupErrors) + } + return __destroyContextForWasiRollback(cleanupErrors) + } + + // Same reason as `__startWasiDisposal`: a module-init hook can start async + // work before the load goes on to fail, and this rollback tears down exactly + // what those completions need. Settle them while everything is still live, + // before the barrier and the teardown above take that away. + // + // A drain that could not finish leaves async work possibly outstanding, and + // destroying the context over it would strand exactly what this rollback is + // there to settle. Stop short and retain instead — the same trade + // `__rollbackWasmEnvForWasiInitialization` makes for the settlement drain, so + // the context stays reclaimable by a retry or by this flavor's own + // last-resort teardown. + const __retainAfterAsyncWorkDrainFailure = (cleanupError) => + __retainFailedWasiRollback([cleanupError]) + let asyncWorkResult + try { + asyncWorkResult = __drainWasiAsyncWork() + } catch (cleanupError) { + return __retainAfterAsyncWorkDrainFailure(cleanupError) + } + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __rollbackWasmEnvForWasiInitialization, + __retainAfterAsyncWorkDrainFailure, + ) + } + return __rollbackWasmEnvForWasiInitialization() +} + +const __wasiRollbackRegistrySymbol = Symbol.for('napi.rs.wasi.rollback.registry.v1') +const __wasiRollbackRegistryKey = + typeof __filename === 'string' ? __filename : __wasmFilePath + +function __getWasiRollbackRegistry() { + const existing = process[__wasiRollbackRegistrySymbol] + if (existing !== undefined) { + if (!(existing instanceof Map)) { + throw new TypeError( + 'The process-wide NAPI-RS WASI rollback registry is invalid', + ) + } + return existing + } + const registry = new Map() + Object.defineProperty(process, __wasiRollbackRegistrySymbol, { + configurable: false, + enumerable: false, + value: registry, + writable: false, + }) + return registry +} + +const __wasiRollbackRegistry = __getWasiRollbackRegistry() + +function __completeWasiInitializationRollback(record, cleanupErrors) { + try { + if (cleanupErrors.length === 0) { + if ( + __wasiRollbackRegistry.get(__wasiRollbackRegistryKey) === record + ) { + __wasiRollbackRegistry.delete(__wasiRollbackRegistryKey) + } + return + } + record.error = __attachCleanupErrors(record.error, cleanupErrors) + } catch (cleanupError) { + try { + record.error = __createCleanupError( + [record.error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + } catch {} + } finally { + record.active = false + record.promise = undefined + } +} + +function __runWasiInitializationRollback(record) { + if (record.active) { + return + } + record.active = true + + let rollbackResult + try { + rollbackResult = record.rollback() + } catch (cleanupError) { + __completeWasiInitializationRollback(record, [cleanupError]) + return + } + + if (!__isThenable(rollbackResult)) { + __completeWasiInitializationRollback(record, rollbackResult) + return + } + + record.promise = Promise.resolve(rollbackResult).then( + (cleanupErrors) => { + __completeWasiInitializationRollback(record, cleanupErrors) + }, + (cleanupError) => { + __completeWasiInitializationRollback(record, [cleanupError]) + }, + ) +} + +const __pendingWasiRollback = __wasiRollbackRegistry.get( + __wasiRollbackRegistryKey, +) +if (__pendingWasiRollback !== undefined) { + __runWasiInitializationRollback(__pendingWasiRollback) + throw __pendingWasiRollback.error +} + +let __wasiModule +let __napiModule +let __wasiExitListenerRegistered = false + +function __removeWasiExitListener() { + if ( + __wasiExitListenerRegistered && + typeof process.removeListener === 'function' + ) { + process.removeListener('exit', __disposeWasiBindingAtExit) + } + __wasiExitListenerRegistered = false +} + +function __disposeWasiBindingAtExit() { + __wasiExitListenerRegistered = false + // An 'exit' handler cannot yield, so it cannot wait for queued promise + // settlements the way __startWasiDisposal does — the process is leaving and + // those promises have no observer left anyway. Run the synchronous teardown + // directly. Every step is idempotent, which also makes this the synchronous + // finish for a disposal that is still waiting for its drain. + try { + __destroyEmnapiContext() + } catch {} + try { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + void Promise.resolve(workerResult).catch(() => {}) + } + } catch {} +} + +function __registerWasiExitListener() { + if ( + !__wasiExitListenerRegistered && + typeof process.once === 'function' + ) { + process.once('exit', __disposeWasiBindingAtExit) + __wasiExitListenerRegistered = true + } +} + +__completeWasiDisposal = __removeWasiExitListener +// A rollback that could not reach the queued settlements keeps the context so +// the registry replay above can retry it. Nothing forces that replay to happen, +// so hand the context to the same synchronous teardown a successful load uses: +// a process that exits without ever retrying still runs the cleanup hooks. The +// handler cannot yield, so it does not settle anything — but by then the process +// is leaving and those promises have no observer left anyway. +__retainWasiRollbackForRetry = __registerWasiExitListener + +function __captureEmnapiAutoDestroyListener() { + if ( + typeof process.prependListener !== 'function' || + typeof process.removeListener !== 'function' + ) { + return + } + let __autoDestroyListener + const __captureListener = (__event, __listener) => { + if (__event === 'beforeExit' && __autoDestroyListener === undefined) { + __autoDestroyListener = __listener + } + } + try { + // Run before existing newListener hooks so a hook that registers its own + // beforeExit listener cannot be mistaken for emnapi's registration. + process.prependListener('newListener', __captureListener) + } catch { + return + } + return () => { + try { + process.removeListener('newListener', __captureListener) + } catch {} + if (__autoDestroyListener !== undefined) { + try { + process.removeListener('beforeExit', __autoDestroyListener) + } catch {} + } + } +} + +try { + const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener() + try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareWasmEnvCleanup, + __isPreparingWasmEnvCleanup, + ) + // emnapi 2.x still registers an unconditional once-listener for + // beforeExit that auto-destroys the context, and suppressDestroy() only + // neutralizes its callback without removing it. This loader owns cleanup + // through its 'exit' listener, so emnapi's listener is captured and + // removed; suppressDestroy() remains the safety net when removal fails. + __emnapiContext.suppressDestroy() + } finally { + // Remove only the exact emnapi callback captured above. + __finishAutoDestroyCapture?.() + } + + ;({ + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, + } = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [ + __captureWasiThreadManager, + __emnapiAsyncWorkPlugin, + __emnapiTSFNPlugin, + ], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + __publishWasiDispose(__napiModule.exports) + // The CommonJS tail below aliases `__napiModule.exports`; a named module + // export does not travel with it, so carry the marker on the binding itself + // too. Three things pin the stamp to exactly this spot: + // - inside this `try`, because the guard throws on a + // `#[napi(module_exports)]` hook that claimed the name, and only the + // catch below tears the environment — context, workers, exit listener — + // back down; + // - after the async runtime host install, which hands this same object to + // addon-provided registration functions that may put anything on it; + // - assigning onto the loader's own `module.exports`, which is still the + // original object here, so an addon accessor with a refusing setter is + // never written through. `cjs-module-lexer` — Node's CJS -> ESM named + // export detection — reads the static `module.exports. =` either + // way, and the later `module.exports = __napiModule.exports` does not + // undo that. + module.exports.__napiBindingTarget = __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) + __registerWasiExitListener() +} catch (error) { + const rollback = { + active: false, + error, + promise: undefined, + rollback: __rollbackWasiInitialization, + } + __wasiRollbackRegistry.set(__wasiRollbackRegistryKey, rollback) + __runWasiInitializationRollback(rollback) + throw rollback.error +} +module.exports = __napiModule.exports +module.exports.Severity = __napiModule.exports.Severity +module.exports.ParseResult = __napiModule.exports.ParseResult +module.exports.ExportExportNameKind = __napiModule.exports.ExportExportNameKind +module.exports.ExportImportNameKind = __napiModule.exports.ExportImportNameKind +module.exports.ExportLocalNameKind = __napiModule.exports.ExportLocalNameKind +module.exports.ImportNameKind = __napiModule.exports.ImportNameKind +module.exports.parse = __napiModule.exports.parse +module.exports.parseSync = __napiModule.exports.parseSync +module.exports.rawTransferSupported = __napiModule.exports.rawTransferSupported diff --git a/napi/parser/src-js/parser.wasip1.d.cts b/napi/parser/src-js/parser.wasip1.d.cts new file mode 100644 index 0000000000..75e77530fc --- /dev/null +++ b/napi/parser/src-js/parser.wasip1.d.cts @@ -0,0 +1,301 @@ + +/** The WASI flavor this loader instantiates. */ +export declare const __napiBindingTarget: 'wasm32-wasip1' + +export interface Comment { + type: 'Line' | 'Block' + value: string + start: number + end: number +} + +export interface ErrorLabel { + message: string | null + start: number + end: number +} + +export interface OxcError { + severity: Severity + message: string + labels: Array + helpMessage: string | null + codeframe: string | null +} + +export declare const enum Severity { + Error = 'Error', + Warning = 'Warning', + Advice = 'Advice', +} + +export declare class ParseResult { + get program(): import("@oxc-project/types").Program + get module(): EcmaScriptModule + get comments(): Array + get errors(): Array +} + +export interface DynamicImport { + start: number + end: number + moduleRequest: Span +} + +export interface EcmaScriptModule { + /** + * Has ESM syntax. + * + * i.e. `import` and `export` statements, and `import.meta`. + * + * Dynamic imports `import('foo')` are ignored since they can be used in non-ESM files. + */ + hasModuleSyntax: boolean + /** Import statements. */ + staticImports: Array + /** Export statements. */ + staticExports: Array + /** Dynamic import expressions. */ + dynamicImports: Array + /** Span positions` of `import.meta` */ + importMetas: Array +} + +export interface ExportExportName { + kind: ExportExportNameKind + name: string | null + start: number | null + end: number | null +} + +export declare const enum ExportExportNameKind { + /** `export { name } */ + Name = 'Name', + /** `export default expression` */ + Default = 'Default', + /** `export * from "mod" */ + None = 'None', +} + +export interface ExportImportName { + kind: ExportImportNameKind + name: string | null + start: number | null + end: number | null +} + +export declare const enum ExportImportNameKind { + /** `export { name } */ + Name = 'Name', + /** `export * as ns from "mod"` */ + All = 'All', + /** `export * from "mod"` */ + AllButDefault = 'AllButDefault', + /** Does not have a specifier. */ + None = 'None', +} + +export interface ExportLocalName { + kind: ExportLocalNameKind + name: string | null + start: number | null + end: number | null +} + +export declare const enum ExportLocalNameKind { + /** `export { name } */ + Name = 'Name', + /** `export default expression` */ + Default = 'Default', + /** + * If the exported value is not locally accessible from within the module. + * `export default function () {}` + */ + None = 'None', +} + +export interface ImportName { + kind: ImportNameKind + name: string | null + start: number | null + end: number | null +} + +export declare const enum ImportNameKind { + /** `import { x } from "mod"` */ + Name = 'Name', + /** `import * as ns from "mod"` */ + NamespaceObject = 'NamespaceObject', + /** `import defaultExport from "mod"` */ + Default = 'Default', +} + +/** + * Parse JS/TS source asynchronously on a separate thread. + * + * Note that not all of the workload can happen on a separate thread. + * Parsing on Rust side does happen in a separate thread, but deserialization of the AST to JS objects + * has to happen on current thread. This synchronous deserialization work typically outweighs + * the asynchronous parsing by a factor of between 3 and 20. + * + * i.e. the majority of the workload cannot be parallelized by using this method. + * + * Generally `parseSync` is preferable to use as it does not have the overhead of spawning a thread. + * If you need to parallelize parsing multiple files, it is recommended to use worker threads. + */ +export declare function parse(filename: string, sourceText: string, options?: ParserOptions | undefined | null): Promise + +export interface ParserOptions { + /** Treat the source text as `js`, `jsx`, `ts`, `tsx` or `dts`. */ + lang?: 'js' | 'jsx' | 'ts' | 'tsx' | 'dts' + /** Treat the source text as `script` or `module` code. */ + sourceType?: 'script' | 'module' | 'commonjs' | 'unambiguous' | undefined + /** + * Return an AST which includes TypeScript-related properties, or excludes them. + * + * `'js'` is default for JS / JSX files. + * `'ts'` is default for TS / TSX files. + * The type of the file is determined from `lang` option, or extension of provided `filename`. + */ + astType?: 'js' | 'ts' + /** + * Controls whether the `range` property is included on AST nodes. + * The `range` property is a `[number, number]` which indicates the start/end offsets + * of the node in the file contents. + * + * @default false + */ + range?: boolean + /** + * Emit `ParenthesizedExpression` and `TSParenthesizedType` in AST. + * + * If this option is true, parenthesized expressions are represented by + * (non-standard) `ParenthesizedExpression` and `TSParenthesizedType` nodes that + * have a single `expression` property containing the expression inside parentheses. + * + * @default true + */ + preserveParens?: boolean + /** + * Produce semantic errors with an additional AST pass. + * Semantic errors depend on symbols and scopes, where the parser does not construct. + * This adds a small performance overhead. + * + * @default false + */ + showSemanticErrors?: boolean +} + +/** + * Parse JS/TS source synchronously on current thread. + * + * This is generally preferable over `parse` (async) as it does not have the overhead + * of spawning a thread, and the majority of the workload cannot be parallelized anyway + * (see `parse` documentation for details). + * + * If you need to parallelize parsing multiple files, it is recommended to use worker threads + * with `parseSync` rather than using `parse`. + */ +export declare function parseSync(filename: string, sourceText: string, options?: ParserOptions | undefined | null): ParseResult + +/** Returns `true` if raw transfer is supported on this platform. */ +export declare function rawTransferSupported(): boolean + +export interface Span { + start: number + end: number +} + +export interface StaticExport { + start: number + end: number + entries: Array +} + +export interface StaticExportEntry { + start: number + end: number + moduleRequest: ValueSpan | null + /** The name under which the desired binding is exported by the module`. */ + importName: ExportImportName + /** The name used to export this binding by this module. */ + exportName: ExportExportName + /** The name that is used to locally access the exported value from within the importing module. */ + localName: ExportLocalName + /** + * Whether the export is a TypeScript `export type`. + * + * Examples: + * + * ```ts + * export type * from 'mod'; + * export type * as ns from 'mod'; + * export type { foo }; + * export { type foo }: + * export type { foo } from 'mod'; + * ``` + */ + isType: boolean +} + +export interface StaticImport { + /** Start of import statement. */ + start: number + /** End of import statement. */ + end: number + /** + * Import source. + * + * ```js + * import { foo } from "mod"; + * // ^^^ + * ``` + */ + moduleRequest: ValueSpan + /** + * Import specifiers. + * + * Empty for `import "mod"`. + */ + entries: Array +} + +export interface StaticImportEntry { + /** + * The name under which the desired binding is exported by the module. + * + * ```js + * import { foo } from "mod"; + * // ^^^ + * import { foo as bar } from "mod"; + * // ^^^ + * ``` + */ + importName: ImportName + /** + * The name that is used to locally access the imported value from within the importing module. + * ```js + * import { foo } from "mod"; + * // ^^^ + * import { foo as bar } from "mod"; + * // ^^^ + * ``` + */ + localName: ValueSpan + /** + * Whether this binding is for a TypeScript type-only import. + * + * `true` for the following imports: + * ```ts + * import type { foo } from "mod"; + * import { type foo } from "mod"; + * ``` + */ + isType: boolean +} + +export interface ValueSpan { + value: string + start: number + end: number +} diff --git a/napi/parser/src-js/wasm.js b/napi/parser/src-js/wasm.js index 8f6ca1b5f2..cb3668e959 100644 --- a/napi/parser/src-js/wasm.js +++ b/napi/parser/src-js/wasm.js @@ -1,5 +1,5 @@ -export * from "@oxc-parser/binding-wasm32-wasi"; -import * as bindings from "@oxc-parser/binding-wasm32-wasi"; +export * from "@oxc-parser/binding-wasm32-wasip1"; +import * as bindings from "@oxc-parser/binding-wasm32-wasip1"; import { wrap } from "./wrap.js"; export { default as visitorKeys } from "./generated/visit/keys.js"; diff --git a/napi/parser/vitest.config.browser.ts b/napi/parser/vitest.config.browser.ts index f2f4e56bdc..31df51db10 100644 --- a/napi/parser/vitest.config.browser.ts +++ b/napi/parser/vitest.config.browser.ts @@ -20,7 +20,7 @@ export default defineConfig({ }, resolve: { alias: { - "@oxc-parser/binding-wasm32-wasi": path.resolve("npm-dir/wasm32-wasi"), + "@oxc-parser/binding-wasm32-wasip1": path.resolve("npm-dir/wasm32-wasip1"), }, }, server: { diff --git a/napi/transform-react/browser.js b/napi/transform-react/browser.js index 1c0041e36d..8be17656c0 100644 --- a/napi/transform-react/browser.js +++ b/napi/transform-react/browser.js @@ -1 +1 @@ -export * from '@oxc-transform-react/binding-wasm32-wasi' +export * from '@oxc-transform-react/binding-wasm32-wasip1' diff --git a/napi/transform-react/index.js b/napi/transform-react/index.js index 032eeddaf4..bfde9e1d53 100644 --- a/napi/transform-react/index.js +++ b/napi/transform-react/index.js @@ -565,7 +565,7 @@ function createLoadErrorChain(errors) { // // NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict // WASI loading. It never crosses into another flavor or falls back to native. -const __napiWasiFlavors = ['wasm32-wasi'] +const __napiWasiFlavors = ['wasm32-wasi', 'wasm32-wasip1'] const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR const __napiWasiFlavorRequested = typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0 @@ -655,6 +655,27 @@ if (!nativeBinding || forceWasi) { loadErrors.push(candidateError) } } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasip1')) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('./transform-react.wasip1.cjs', false, ['./transform-react.wasm32-wasip1.debug.wasm', './transform-react.wasm32-wasip1.wasm']) + candidateFailed = candidateError !== null + if (!candidateFailed) { + wasiBinding = require('./transform-react.wasip1.cjs') + nativeBinding = wasiBinding + __napiLoadedBindingTarget = 'wasm32-wasip1' + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasi')) { let candidateError = null let candidateFailed = false @@ -682,6 +703,33 @@ if (!nativeBinding || forceWasi) { loadErrors.push(candidateError) } } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasip1')) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('@oxc-transform-react/binding-wasm32-wasip1', true, undefined) + candidateFailed = candidateError !== null + if (!candidateFailed) { + if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + const bindingPackageVersion = require('@oxc-transform-react/binding-wasm32-wasip1/package.json').version + if (bindingPackageVersion !== '0.151.0') { + throw new Error(`WASI binding package version mismatch, expected 0.151.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + } + wasiBinding = require('@oxc-transform-react/binding-wasm32-wasip1') + nativeBinding = wasiBinding + __napiLoadedBindingTarget = 'wasm32-wasip1' + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } if ( !wasiBindingLoaded && forceWasi && diff --git a/napi/transform-react/package.json b/napi/transform-react/package.json index fbba6b641f..0fcab54976 100644 --- a/napi/transform-react/package.json +++ b/napi/transform-react/package.json @@ -46,6 +46,8 @@ "postbuild-dev": "node scripts/patch.js", "build-wasm": "pnpm run build-wasm-dev --release", "build-wasm-dev": "pnpm run build-dev --target wasm32-wasip1-threads --dts transform-react.wasi.d.cts", + "build-wasip1": "pnpm run build-wasip1-dev --release", + "build-wasip1-dev": "pnpm run build-dev --target wasm32-wasip1 --dts transform-react.wasip1.d.cts", "test": "vitest run --dir ./test" }, "devDependencies": { @@ -74,6 +76,7 @@ "riscv64gc-unknown-linux-gnu", "riscv64gc-unknown-linux-musl", "s390x-unknown-linux-gnu", + "wasm32-wasip1", "wasm32-wasip1-threads", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", diff --git a/napi/transform-react/scripts/patch.js b/napi/transform-react/scripts/patch.js index 1990fac096..5b2b5eadbd 100644 --- a/napi/transform-react/scripts/patch.js +++ b/napi/transform-react/scripts/patch.js @@ -27,3 +27,7 @@ fs.writeFileSync(path, data); const workerPath = pathJoin(packageDir, "wasi-worker-browser.mjs"); const worker = fs.readFileSync(workerPath, "utf-8").replaceAll(/[ \t]+$/gmu, ""); fs.writeFileSync(workerPath, worker); + +const wasip1BrowserPath = pathJoin(packageDir, "transform-react.wasip1-browser.js"); +const wasip1Browser = fs.readFileSync(wasip1BrowserPath, "utf-8").replaceAll(/[ \t]+$/gmu, ""); +fs.writeFileSync(wasip1BrowserPath, wasip1Browser); diff --git a/napi/transform-react/transform-react.wasip1-browser.js b/napi/transform-react/transform-react.wasip1-browser.js new file mode 100644 index 0000000000..f6c603d7a0 --- /dev/null +++ b/napi/transform-react/transform-react.wasip1-browser.js @@ -0,0 +1,989 @@ +import { + emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin as __emnapiTSFNPlugin, + instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync, + WASI as __WASI, +} from '@napi-rs/wasm-runtime' +import { createContext as __emnapiCreateContext } from '@emnapi/runtime' + + +export const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +const __wasi = new __WASI({ + version: 'preview1', +}) + +const __wasmUrl = new URL('./transform-react.wasm32-wasip1.wasm', import.meta.url).href +const __wasmResponse = await globalThis.fetch(__wasmUrl) +if (!__wasmResponse.ok) { + throw new Error( + 'Failed to fetch WASI module ' + + __wasmUrl + + ': ' + + __wasmResponse.status + + ' ' + + (__wasmResponse.statusText || 'Unknown Status'), + ) +} +const __wasmFile = await __wasmResponse.arrayBuffer() + +const __wasmMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, +}) +let __emnapiContext + +const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose') +const __wasiWorkers = new Set() +// The thread manager has to be reachable *before* anything that can throw +// during load or registration. Initialization can fail after the pool has +// already spawned workers, and the rollback still has to mark their +// terminations as expected — but `__napiModule` is assigned only when +// instantiation RETURNS, so on exactly that path it is still undefined. A +// plugin factory runs while the emnapi module is being created, before the +// wasm is loaded and before any registration function runs, and its context +// carries the very same manager instance. +let __wasiThreadManager + +function __captureWasiThreadManager(context) { + if (context && context.PThread) { + __wasiThreadManager = context.PThread + } + return {} +} + +function __getWasiThreadManager() { + const manager = + __wasiThreadManager !== undefined + ? __wasiThreadManager + : __napiModule + ? __napiModule.PThread + : undefined + if (manager && typeof manager.terminateWorker === 'function') { + return manager + } + return undefined +} +let __napiInstance +let __emnapiContextDestroyed = false +let __emnapiContextDestroyPromise +let __emnapiWasmEnvCleanupPrepared = false +let __emnapiWasmEnvCleanupPreparing = false +let __emnapiWasmEnvCleanupRan = false +let __emnapiWasmEnvCleanupDrained = false +let __emnapiWasmEnvCleanupDrainPromise +let __wasiDisposed = false +let __wasiAsyncWorkDrainPromise +let __wasiDisposePromise +let __completeWasiDisposal = function () {} +// Overridden by loader flavors that have a last-resort reclaim for a rollback +// that stopped short of destroying the context. See +// `__rollbackWasiInitialization`. +let __retainWasiRollbackForRetry = function () {} + +function __isThenable(value) { + return ( + value !== null && + (typeof value === 'object' || typeof value === 'function') && + typeof value.then === 'function' + ) +} + +function __createCleanupError(errors, message) { + if (errors.length === 1) { + return errors[0] + } + const __AggregateError = globalThis.AggregateError + if (typeof __AggregateError === 'function') { + return new __AggregateError(errors, message) + } + const error = new Error(message) + error.errors = errors + return error +} + +function __attachCleanupErrors(error, cleanupErrors) { + if (cleanupErrors.length === 0) { + return error + } + const cleanupError = __createCleanupError( + cleanupErrors, + 'WASI binding cleanup failed', + ) + try { + if ( + error && + (typeof error === 'object' || typeof error === 'function') + ) { + if (error.cause === undefined) { + error.cause = cleanupError + if (error.cause === cleanupError) { + return error + } + } + if (Array.isArray(error.cleanupErrors)) { + error.cleanupErrors.push(cleanupError) + return error + } else { + const attachedCleanupErrors = [cleanupError] + error.cleanupErrors = attachedCleanupErrors + if (error.cleanupErrors === attachedCleanupErrors) { + return error + } + } + } + } catch {} + const aggregate = __createCleanupError( + [error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + try { + aggregate.cause = error + } catch {} + return aggregate +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __isPreparingWasmEnvCleanup() { + return __emnapiWasmEnvCleanupPreparing +} + +function __prepareWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) { + return + } + const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup + if (typeof prepare === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __emnapiWasmEnvCleanupPreparing = true + try { + prepare() + } finally { + __emnapiWasmEnvCleanupPreparing = false + } + __emnapiWasmEnvCleanupRan = true + } + __emnapiWasmEnvCleanupPrepared = true +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (callback) { + setImmediate(callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (callback) { + const channel = new __MessageChannel() + channel.port1.onmessage = function () { + channel.port1.onmessage = null + try { + channel.port1.close() + } catch {} + try { + channel.port2.close() + } catch {} + callback() + } + channel.port2.postMessage(null) + } + } + return function (callback) { + setTimeout(callback, 0) + } +})() + +// A real, *referenced* timer, for waits that must let the whole host make +// progress between looks — the async-work drain polls the addon rather than +// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there +// would spin the loop instead of yielding it. Falls back to the macrotask +// scheduler on a host without timers. +function __scheduleTimer(callback, delay) { + const setTimer = globalThis.setTimeout + if (typeof setTimer !== 'function') { + __scheduleMacrotask(callback) + return + } + try { + setTimer(callback, delay) + } catch { + __scheduleMacrotask(callback) + } +} + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + * + * The "already drained" flag is set only once a wait has actually finished. + * Scheduling a macrotask can fail — a host-provided or patched `setImmediate` + * that throws is enough — and a disposal that rejects stays retryable, so + * marking the drain complete up front would make the retry skip it and destroy + * the context with the barrier's settlements still queued. + * + * A wait that runs out of turns with the counter still nonzero rejects with + * `ERR_NAPI_WASI_CLEANUP_PENDING` for the same reason: at that point + * "finished" is indistinguishable from the stranding above, and destroying + * would discard the very settlement the wait was for. The rejection leaves the + * flag unset and disposal retryable. + */ +function __drainWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupDrained || !__emnapiWasmEnvCleanupRan) { + return + } + if (__emnapiWasmEnvCleanupDrainPromise) { + return __emnapiWasmEnvCleanupDrainPromise + } + const pending = __napiInstance?.exports?.napi_wasm_env_cleanup_pending + const observable = typeof pending === 'function' + if (observable) { + let queued + try { + queued = pending() + } catch { + __emnapiWasmEnvCleanupDrained = true + return + } + if (!queued) { + __emnapiWasmEnvCleanupDrained = true + return + } + } + const limit = observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + const drainPromise = (async () => { + let queued = 0 + for (let turn = 0; turn < limit; turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!observable) { + continue + } + try { + queued = pending() + } catch { + return + } + if (!queued) { + return + } + } + if (!observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: the drained flag stays + // unset, dispose() (and the rollback) decline to destroy, and a later + // dispose() runs the drain again — by which time the queue has usually been + // delivered. A counter that is somehow stuck nonzero therefore costs each + // attempt at most another bounded wait and a rejection, never a stranded + // promise; the process-exit teardown still reclaims the context. + const drainError = new Error( + 'the wasm environment still reports ' + + queued + + ' queued settlement(s) after ' + + limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw drainError + })().then( + (value) => { + // Set only when the wait actually finished AND the queue was seen empty + // (or is unobservable): a drain that timed out with settlements still + // queued rejects above and must stay repeatable. + __emnapiWasmEnvCleanupDrained = true + __emnapiWasmEnvCleanupDrainPromise = undefined + return value + }, + (error) => { + __emnapiWasmEnvCleanupDrainPromise = undefined + throw error + }, + ) + __emnapiWasmEnvCleanupDrainPromise = drainPromise + return drainPromise +} + +function __destroyEmnapiContext() { + if (__emnapiContextDestroyed || __emnapiContext === undefined) { + __emnapiContextDestroyed = true + return + } + if (__emnapiContextDestroyPromise) { + return __emnapiContextDestroyPromise + } + + __prepareWasmEnvCleanup() + const result = __emnapiContext.destroy() + if (!__isThenable(result)) { + __emnapiContextDestroyed = true + return + } + + const destroyPromise = Promise.resolve(result).then( + (value) => { + __emnapiContextDestroyed = true + return value + }, + (error) => { + __emnapiContextDestroyPromise = undefined + throw error + }, + ) + __emnapiContextDestroyPromise = destroyPromise + return destroyPromise +} + +/** + * Holds the event loop open until `work` settles. + * + * Nothing else can: the pool workers are deliberately unreferenced so an idle + * binding cannot keep a process alive, and referencing them again for the + * termination does not hold either — emnapi unreferences a worker the moment it + * reports `async-thread-ready`, which for a worker that was still starting + * lands *after* the termination began. Without a handle of its own, an + * `await dispose()` with nothing else pending exits the process with its + * promise unsettled, and everything after the `await` is skipped. + * + * The timer is cleared as soon as the work settles, so this never outlives the + * disposal that asked for it. + */ +function __keepEventLoopAliveUntil(work) { + const setTimer = globalThis.setInterval + const clearTimer = globalThis.clearInterval + if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') { + return work + } + let timer + try { + timer = setTimer(function () {}, 50) + } catch { + return work + } + const release = function () { + try { + clearTimer(timer) + } catch {} + } + return work.then( + (value) => { + release() + return value + }, + (error) => { + release() + throw error + }, + ) +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this addon's outstanding `napi_async_work` before the teardown that + * would strand it. + * + * `napi_prepare_wasm_env_cleanup` does not cover async work, and nothing about + * it is observable from JavaScript: the threadless archive resolves + * `napi_*_async_work` through the `@emnapi/core` plugins, but the threaded one + * links the C `async_work.c` on the uv threadpool, so there the wasm neither + * imports nor exports those symbols and the only brackets a loader could watch + * (`_emnapi_ctx_*_waiting_request_counter`) are shared with threadsafe + * functions. The addon is the one place both flavors go through, so it answers + * for both, through the same kind of handshake the settlement drain uses: + * + * - `napi_wasm_cancel_pending_async_work()` cancels what no thread has + * started. Those completion callbacks run with `napi_cancelled`, which + * napi-rs turns into a promise rejected with an `AbortError`. + * - `napi_wasm_async_work_pending()` counts what is still owed a completion + * callback. Work already executing refuses cancellation and stays counted + * until it finishes normally — which it can, because this runs before the + * barrier, before `Context.destroy()` and before anything is terminated. + * + * Both exports are optional: an addon built against a napi crate that predates + * them drains nothing and keeps the previous behavior, exactly as the + * `napi_wasm_env_cleanup_pending` handshake degrades. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. The promise it returns otherwise never + * rejects. + * + * The wait has no deadline, and that is the point: giving up would destroy the + * environment with a completion callback still owed, which is the stranding + * this exists to prevent. A task whose `execute` never returns already keeps an + * *undisposed* process alive in exactly the same way, so disposal inherits that + * rather than inventing a bound it cannot honor. + * + * Safe to call from inside a completion callback, which is reachable: settling + * a task runs addon code that can re-enter JavaScript — a setter on the value + * being handed back, a threadsafe-function callback — and that JavaScript can + * call `dispose()`. Two things make it terminate rather than wait on itself: + * + * - The addon keeps a work registered until its completion callback + * *finishes*, so the count read here is at least one and this takes the + * polling path instead of declaring the environment drained and tearing it + * down from inside the frame that is still settling a promise. + * - The poll is a timer, so it cannot run until the callback has returned to + * the host — by which time that work has left the registry. The count the + * next poll reads is the one taken after the callback finished. + * + * `__disposeWasiBinding` hands every caller the same in-flight promise, so the + * nested call joins this disposal rather than starting a second one. + */ +function __drainWasiAsyncWork() { + if (__wasiAsyncWorkDrainPromise !== undefined) { + return __wasiAsyncWorkDrainPromise + } + const exports = __napiInstance?.exports + const pending = exports?.napi_wasm_async_work_pending + const cancelPending = exports?.napi_wasm_cancel_pending_async_work + if (typeof pending !== 'function' || typeof cancelPending !== 'function') { + return + } + + const readPending = () => { + try { + return pending() + } catch (error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition — + // there is nothing left to wait for, and refusing to dispose would only + // keep a dead instance and its stuck counter alive. Best-effort here is + // the honest answer, and it is what disposal did before this drain + // existed. + // + // Only a trap. Anything else means the export is not what this loader + // thinks it is, which is a defect worth surfacing rather than disposing + // over. + if (error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw error + } + } + + if (!readPending()) { + return + } + try { + cancelPending() + } catch { + // Cancellation is an optimization: it bounds the wait by the work already + // executing. Failing it only means waiting for the whole queue instead. + } + if (!readPending()) { + return + } + + const drainPromise = __keepEventLoopAliveUntil( + (async () => { + while (readPending()) { + await new Promise((resolve) => { + __scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })(), + ).then( + () => { + __wasiAsyncWorkDrainPromise = undefined + }, + (error) => { + // A wait that could not run is not a wait that finished. The only way + // here is a host whose timers and macrotask primitives all refuse, and + // the work is still outstanding — reporting success would destroy the + // environment over it, which is the stranding this exists to prevent. + // Reject instead: disposal stays retryable, and the context is not + // destroyed. Clearing the memo first is what makes the retry re-run this. + __wasiAsyncWorkDrainPromise = undefined + throw error + }, + ) + __wasiAsyncWorkDrainPromise = drainPromise + return drainPromise +} + +/** + * `@emnapi/wasi-threads` counts a worker exit as expected only when its own + * thread manager performed the termination. A bare `worker.terminate()` reaches + * the manager's `exit` listener instead, which reports + * `worker (tid = N) sent an error! ... stopped with exit code 1` and rethrows + * inside the emit — aborting the `once('exit')` that backs the terminate + * promise, so disposal never settles and the process dies with an uncaught + * exception. Mark the termination through the manager first. + * + * The manager comes from `__getWasiThreadManager`, not from `__napiModule`: + * the initialization rollback runs on the one path where instantiation never + * returned, so `__napiModule` is still undefined there while the workers it + * spawned are already registered and loaded. + * + * Not `terminateAllThreads()`: that one recreates the pool it just shut down. + */ +function __terminateWasiWorkers() { + const cleanupErrors = [] + const pending = [] + const threadManager = __getWasiThreadManager() + + for (const worker of __wasiWorkers) { + let result + try { + if (threadManager) { + threadManager.terminateWorker(worker) + // `terminateWorker` leaves behind a reporter that logs every message + // still queued on the port, which Node flushes on exit. Nothing is + // listening for those any more. + worker.onmessage = undefined + } + result = worker.terminate() + } catch (error) { + cleanupErrors.push(error) + continue + } + if (__isThenable(result)) { + pending.push( + Promise.resolve(result).then( + () => { + __wasiWorkers.delete(worker) + }, + (error) => { + cleanupErrors.push(error) + }, + ), + ) + } else { + __wasiWorkers.delete(worker) + } + } + + const finish = () => { + if (cleanupErrors.length > 0) { + throw __createCleanupError( + cleanupErrors, + 'Failed to terminate WASI workers', + ) + } + } + return pending.length > 0 + ? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish) + : finish() +} + +function __finishWasiDisposal() { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult).then(__completeWasiDisposal) + } + return __completeWasiDisposal() +} + +function __continueWasiDisposal() { + const destroyResult = __destroyEmnapiContext() + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult).then(__finishWasiDisposal) + } + return __finishWasiDisposal() +} + +function __cleanUpWasmEnvForWasiDisposal() { + // Run the pre-teardown barrier, then let the settlements it queued actually + // reach JavaScript, and only then destroy the environment. Doing these two + // back to back is what strands them. + __prepareWasmEnvCleanup() + const drainResult = __drainWasmEnvCleanup() + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then(__continueWasiDisposal) + } + return __continueWasiDisposal() +} + +function __startWasiDisposal() { + // Outstanding `napi_async_work` goes first, while the environment is still + // completely live: the completion callbacks run addon code, and everything + // after this point takes that away from them — the barrier shuts the async + // runtime down, `Context.destroy()` stops JavaScript calls, and terminating + // the pool threads removes what would have reported the work finished. + const asyncWorkResult = __drainWasiAsyncWork() + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __cleanUpWasmEnvForWasiDisposal, + ) + } + return __cleanUpWasmEnvForWasiDisposal() +} + +/** + * Disposes this generated WASI binding. + * + * Access this function with: + * binding[Symbol.for('napi.rs.wasi.dispose')]() + */ +function __disposeWasiBinding() { + if (__wasiDisposePromise) { + return __wasiDisposePromise + } + if (__wasiDisposed) { + return Promise.resolve() + } + + let resolveDispose + let rejectDispose + const disposePromise = new Promise((resolve, reject) => { + resolveDispose = resolve + rejectDispose = reject + }) + __wasiDisposePromise = disposePromise + + let result + try { + result = __startWasiDisposal() + } catch (error) { + __wasiDisposePromise = undefined + rejectDispose(error) + return disposePromise + } + + Promise.resolve(result).then( + (value) => { + __wasiDisposed = true + resolveDispose(value) + }, + (error) => { + __wasiDisposePromise = undefined + rejectDispose(error) + }, + ) + return disposePromise +} + +function __publishWasiDispose(exports) { + Object.defineProperty(exports, __wasiDisposeSymbol, { + configurable: false, + enumerable: false, + value: __disposeWasiBinding, + writable: false, + }) +} + +function __finishWasiInitializationRollback(cleanupErrors) { + let workerResult + try { + workerResult = __terminateWasiWorkers() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return cleanupErrors + } + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => cleanupErrors) + } + return cleanupErrors +} + +function __destroyContextForWasiRollback(cleanupErrors) { + let destroyResult + try { + destroyResult = __destroyEmnapiContext() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return __finishWasiInitializationRollback(cleanupErrors) + } + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => __finishWasiInitializationRollback(cleanupErrors)) + } + return __finishWasiInitializationRollback(cleanupErrors) +} + +/** + * Leaves a rollback that could not reach the queued settlements undestroyed, and + * hands it to whatever this flavor has that can still reclaim it. + */ +function __retainFailedWasiRollback(cleanupErrors) { + try { + __retainWasiRollbackForRetry() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + } + return cleanupErrors +} + +/** + * Initialization can fail *after* registration has already run, and registration + * runs with a live environment: a module-init hook can start async work and then + * return an error, and the promise it created may already have escaped into + * JavaScript. The barrier cancels that work and *queues* the settlement, so this + * path needs the same drain the ordinary disposal does — destroying without + * yielding discards the queue with a null env and strands the promise. + * + * Stays synchronous when nothing is queued, which covers every failure before + * `beforeInit`: there is no instance to run the barrier on, so nothing to drain. + * + * A barrier or drain that did *not* finish stops the rollback short of + * destroying, which is what `dispose()` already does — a rejected drain there + * never reaches `__continueWasiDisposal`. Destroying anyway is the worse of the + * two trades, and not because of what it saves: + * + * - It cannot deliver the settlements. `Context.destroy()` runs the + * threadsafe function's cleanup hook, which drains the queue with a null env + * and discards it, so a promise that already escaped into JavaScript hangs + * forever with nothing left that could ever settle it. + * - It saves less than it looks. `Context.destroy()` stops JavaScript calls + * and runs cleanup hooks; it does not free the wasm instance or its Memory, + * which this module's scope holds either way. What stopping short retains is + * the emnapi context's bookkeeping and its un-run cleanup hooks. + * - Retry is not theoretical. A rollback that records a cleanup error is + * already kept in the process-wide registry above, so re-`require()`ing this + * file replays it instead of re-instantiating — and the `6e15de6f` flag fix + * means the replay drains again rather than skipping it. Destroying first is + * what makes that retained record useless. + * + * The residual cost is honest: the CJS flavor hands the context to its + * `process.on('exit')` teardown, so a process that never retries still reclaims + * it on the way out. The ESM browser flavor has no equivalent — a module that + * throws while evaluating is permanently errored, so re-importing rethrows + * without re-running this file — and there the context stays until the realm + * goes away. That is the deliberate choice: a hung promise is a silent liveness + * bug with no upper bound, while the retained bookkeeping is bounded by the page. + */ +function __rollbackWasiInitialization() { + // The environment teardown this rollback performs, kept nested so it cannot + // be reached without the async-work drain below running first. + function __rollbackWasmEnvForWasiInitialization() { + const cleanupErrors = [] + let drainResult + let settlementsUnreached = false + try { + __prepareWasmEnvCleanup() + drainResult = __drainWasmEnvCleanup() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + settlementsUnreached = true + } + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then( + () => __destroyContextForWasiRollback(cleanupErrors), + (cleanupError) => { + cleanupErrors.push(cleanupError) + return __retainFailedWasiRollback(cleanupErrors) + }, + ) + } + if (settlementsUnreached) { + return __retainFailedWasiRollback(cleanupErrors) + } + return __destroyContextForWasiRollback(cleanupErrors) + } + + // Same reason as `__startWasiDisposal`: a module-init hook can start async + // work before the load goes on to fail, and this rollback tears down exactly + // what those completions need. Settle them while everything is still live, + // before the barrier and the teardown above take that away. + // + // A drain that could not finish leaves async work possibly outstanding, and + // destroying the context over it would strand exactly what this rollback is + // there to settle. Stop short and retain instead — the same trade + // `__rollbackWasmEnvForWasiInitialization` makes for the settlement drain, so + // the context stays reclaimable by a retry or by this flavor's own + // last-resort teardown. + const __retainAfterAsyncWorkDrainFailure = (cleanupError) => + __retainFailedWasiRollback([cleanupError]) + let asyncWorkResult + try { + asyncWorkResult = __drainWasiAsyncWork() + } catch (cleanupError) { + return __retainAfterAsyncWorkDrainFailure(cleanupError) + } + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __rollbackWasmEnvForWasiInitialization, + __retainAfterAsyncWorkDrainFailure, + ) + } + return __rollbackWasmEnvForWasiInitialization() +} + +let __wasiModule +let __napiModule + +try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareWasmEnvCleanup, + __isPreparingWasmEnvCleanup, + ) + __emnapiContext.suppressDestroy() + + ;({ + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, + } = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [ + __captureWasiThreadManager, + __emnapiAsyncWorkPlugin, + __emnapiTSFNPlugin, + ], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + __publishWasiDispose(__napiModule.exports) + // The default export hands out this object; a named module export does not + // travel with it, so carry the marker on the binding itself too. After the + // host install, which hands the same object to addon-provided registration + // functions that may put anything on it, and inside this `try`, so a claimed + // name fails the load through the rollback below rather than past it. + __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) +} catch (error) { + const cleanupErrors = await __rollbackWasiInitialization() + throw __attachCleanupErrors(error, cleanupErrors) +} +export default __napiModule.exports +export const Severity = __napiModule.exports.Severity +export const transform = __napiModule.exports.transform +export const transformSync = __napiModule.exports.transformSync diff --git a/napi/transform-react/transform-react.wasip1-deferred.d.ts b/napi/transform-react/transform-react.wasip1-deferred.d.ts new file mode 100644 index 0000000000..a0602b935a --- /dev/null +++ b/napi/transform-react/transform-react.wasip1-deferred.d.ts @@ -0,0 +1,83 @@ +export type WasiBinding = typeof import('./transform-react.wasip1.cjs') + +export type WasiModuleInput = + | WebAssembly.Module + | PromiseLike + +/** Run the instance on a linear memory the caller allocated. */ +export interface WasiCallerMemoryOptions { + /** + * A caller-allocated linear memory for this instance. It must be unshared + * and created in this loader's own realm — the WASI and emnapi layers + * underneath identify a Memory with a realm-local `instanceof`, so one from + * a `node:vm` context or another frame is rejected. It is single-use: once + * a validated initialization attempt has begun, the same Memory cannot be + * passed again — including after that attempt failed, and after the instance + * was disposed. + */ + memory: WebAssembly.Memory + /** Not available beside `memory`: the loader allocates neither. */ + initialMemoryPages?: never + /** Not available beside `memory`: the loader allocates neither. */ + maximumMemoryPages?: never +} + +/** Let the loader allocate the linear memory, optionally sized. */ +export interface WasiAllocatedMemoryOptions { + /** Not available beside the page counts: they size the loader's own Memory. */ + memory?: never + /** @default WASM_MEMORY.initialPages */ + initialMemoryPages?: number + /** @default WASM_MEMORY.maximumPages */ + maximumMemoryPages?: number +} + +/** + * Either memory form, never a mix of the two: the loader throws a TypeError + * on `memory` beside a page count. `{}` and an omitted argument select the + * loader defaults. + */ +export type WasiInstanceOptions = + | WasiCallerMemoryOptions + | WasiAllocatedMemoryOptions + +export interface WasiRuntimeStats { + /** Instances created by this evaluated loader module, not process-wide. */ + createdInstances: number + /** Created instances whose dispose() has not completed. */ + liveInstances: number + /** Declared initial address space, not committed memory. */ + declaredInitialMemoryBytes: number +} + +export interface WasiInstance { + readonly exports: WasiBinding + /** This instance's linear memory. Claimed, so it cannot start another one. */ + readonly memory: WebAssembly.Memory + /** Current linear-memory size; 0 once dispose() has completed. */ + readonly memoryBytes: number + readonly disposed: boolean + dispose(): Promise +} + +/** The memory descriptor compiled into this loader. */ +export const WASM_MEMORY: Readonly<{ + initialPages: number + maximumPages: number + pageBytes: number + initialBytes: number + maximumBytes: number +}> + +export function getDeferredRuntimeStats(): Readonly + +export function instantiate(wasmInput: WasiModuleInput): Promise +export function createInstance( + wasmInput: WasiModuleInput, + options?: WasiInstanceOptions, +): Promise +/** Dispose the singleton and retry retained failed-initialization cleanup. */ +export function dispose(): Promise + +/** The WASI flavor this deferred loader instantiates. */ +export declare const __napiBindingTarget: 'wasm32-wasip1' diff --git a/napi/transform-react/transform-react.wasip1-deferred.js b/napi/transform-react/transform-react.wasip1-deferred.js new file mode 100644 index 0000000000..4973d51ea0 --- /dev/null +++ b/napi/transform-react/transform-react.wasip1-deferred.js @@ -0,0 +1,1488 @@ +import { + emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin as __emnapiTSFNPlugin, + instantiateNapiModule as __emnapiInstantiateNapiModule, + WASI as __WASI, +} from '@napi-rs/wasm-runtime' +import { createContext as __emnapiCreateContext } from '@emnapi/runtime' + + +export const WASM_MEMORY = Object.freeze({ + initialPages: 1024, + maximumPages: 65536, + pageBytes: 65536, + initialBytes: 1024 * 65536, + maximumBytes: 65536 * 65536, +}) + +let __createdInstances = 0 +let __liveInstances = 0 + +/** + * Counters for instances created by THIS module evaluation, not process-wide: + * a second bundled copy of this loader keeps its own. Only successfully + * created instances are counted, and `liveInstances` drops when an instance's + * `dispose()` resolves. + * + * `declaredInitialMemoryBytes` is declared address space, not a host's + * committed-memory metric; pair it with host telemetry rather than treating it + * as a quota. + */ +export function getDeferredRuntimeStats() { + return Object.freeze({ + createdInstances: __createdInstances, + liveInstances: __liveInstances, + declaredInitialMemoryBytes: WASM_MEMORY.initialBytes, + }) +} + +const __arrayBufferByteLengthGetter = Object.getOwnPropertyDescriptor( + ArrayBuffer.prototype, + 'byteLength', +).get +const __memoryBufferGetter = Object.getOwnPropertyDescriptor( + WebAssembly.Memory.prototype, + 'buffer', +).get +// One managed initialization per Memory, success or failure: an attempt that +// throws may already have written into linear memory, so the bytes are not a +// clean slate for a second instance. Module-local, like the counters above. +const __claimedMemories = new WeakSet() + +function __resolveInstanceMemory(__options) { + const __provided = __options == null ? undefined : __options.memory + if (__provided === undefined || __provided === null) { + // Page counts are handed to the engine unvalidated: it already rejects a + // negative, over-4GiB or below-maximum value with a precise message, and a + // second set of bounds here would only drift from it. + const __allocated = new WebAssembly.Memory({ + initial: + __options != null && __options.initialMemoryPages !== undefined + ? __options.initialMemoryPages + : WASM_MEMORY.initialPages, + maximum: + __options != null && __options.maximumMemoryPages !== undefined + ? __options.maximumMemoryPages + : WASM_MEMORY.maximumPages, + }) + // Claimed like a caller-provided one. The handle publishes it as + // `instance.memory`, so handing it back to `createInstance()` is as easy + // as passing your own twice, and it would put two live instances on one + // linear memory: each initialization rewrites the emnapi/WASI state the + // other is still running on. + __claimedMemories.add(__allocated) + return __allocated + } + if ( + __options.initialMemoryPages !== undefined || + __options.maximumMemoryPages !== undefined + ) { + throw new TypeError( + 'Pass either memory or initialMemoryPages/maximumMemoryPages, not both', + ) + } + let __buffer + try { + // Brand check: the getter throws for anything that is not a genuine + // WebAssembly.Memory, including a cross-realm look-alike object. + __buffer = Reflect.apply(__memoryBufferGetter, __provided, []) + } catch { + throw new TypeError('memory must be an unshared WebAssembly.Memory') + } + try { + // Throws for a SharedArrayBuffer. This loader has no threads, and shared + // growth does not detach: external views handed to the addon would + // silently outlive the bytes they describe. + Reflect.apply(__arrayBufferByteLengthGetter, __buffer, []) + } catch { + throw new TypeError( + 'The deferred loader requires an unshared WebAssembly.Memory', + ) + } + // The intrinsic getters above accept a genuine Memory from ANY realm, but + // the loader's dependencies do not: `WASI.setMemory` in + // `@napi-rs/wasm-runtime` and emnapi identify a Memory with a realm-local + // `instanceof`. A Memory built in another realm (a `node:vm` context, a + // same-origin iframe) would pass every check here and only fail deep inside + // initialization. Reject it up front, and before the claim below, so the + // caller keeps it usable in the realm that made it. + if (!(__provided instanceof WebAssembly.Memory)) { + throw new TypeError( + 'memory must be a WebAssembly.Memory created in the same realm as this loader', + ) + } + if (__claimedMemories.has(__provided)) { + throw new TypeError( + 'This WebAssembly.Memory has already been used for a deferred initialization attempt and cannot be reused, including after a failed initialization or a disposal', + ) + } + // Last step, after every check: a rejected option bag must leave the Memory + // unclaimed, or a caller could not fix the call and retry with it. + __claimedMemories.add(__provided) + return __provided +} + +export const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +/** + * Deferred, workerd-safe instantiation: no top-level I/O, no compile-from-bytes. + * Accepts ONLY a precompiled WebAssembly.Module, or a Promise resolving to one + * (e.g. `import mod from './transform-react.wasm32-wasip1.wasm'` under a CompiledWasm + * module rule / wrangler module import). Byte buffers, URLs and Response + * objects are rejected: they require dynamic Wasm compilation, which + * Cloudflare Workers disallows. + */ +async function __resolveModule(__wasmInput) { + const __module = await __wasmInput + // Brand check, not `instanceof`: `WebAssembly.Module.imports` throws unless + // its argument is a genuine WebAssembly.Module, so prototype-spoofed byte + // buffers are rejected while cross-realm Module instances are accepted. + try { + WebAssembly.Module.imports(__module) + } catch { + throw new TypeError( + "instantiate() and createInstance() expect a precompiled WebAssembly.Module (or a Promise resolving to one), " + + "e.g. import mod from './transform-react.wasm32-wasip1.wasm' under a CompiledWasm module rule / wrangler module import. " + + "Byte buffers, URLs and Response objects require dynamic Wasm compilation, which Cloudflare Workers disallows.", + ) + } + return __module +} + +let __normalizedModules + +function __rememberNormalizedModule(__module, __normalizedModule) { + if (!__normalizedModules) { + __normalizedModules = new WeakMap() + } + __normalizedModules.set(__module, __normalizedModule) + return __normalizedModule +} + +async function __normalizeModuleForEmnapi(__module) { + if (__module instanceof WebAssembly.Module) { + return __module + } + if (__normalizedModules) { + const __normalizedModule = __normalizedModules.get(__module) + if (__normalizedModule) { + return __normalizedModule + } + } + // @emnapi/core currently performs realm-local `instanceof` checks after + // accepting the module. Structured cloning preserves compiled code without + // compiling bytes and produces a Module owned by the current realm. + if (typeof structuredClone === 'function') { + try { + const __normalizedModule = structuredClone(__module) + if (__normalizedModule instanceof WebAssembly.Module) { + return __rememberNormalizedModule(__module, __normalizedModule) + } + } catch {} + } + // MessageChannel uses the same structured-clone semantics and covers older + // browser/Node hosts that expose it but not the structuredClone function. + if (typeof MessageChannel === 'function') { + let __channel + try { + __channel = new MessageChannel() + const __normalizedModule = await new Promise((resolve, reject) => { + __channel.port1.onmessage = (event) => resolve(event.data) + __channel.port1.onmessageerror = () => + reject(new TypeError('Failed to clone WebAssembly.Module')) + try { + __channel.port2.postMessage(__module) + } catch (error) { + reject(error) + } + }) + if (__normalizedModule instanceof WebAssembly.Module) { + return __rememberNormalizedModule(__module, __normalizedModule) + } + } catch { + } finally { + try { + __channel?.port1.close() + } catch {} + try { + __channel?.port2.close() + } catch {} + } + } + // Last-resort compatibility for genuine, extensible foreign Modules. + try { + Object.setPrototypeOf(__module, WebAssembly.Module.prototype) + } catch {} + if (__module instanceof WebAssembly.Module) { + return __module + } + throw new TypeError( + 'This host cannot normalize a cross-realm WebAssembly.Module; ' + + 'provide structuredClone or MessageChannel support.', + ) +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __captureEmnapiAutoDestroyListener(__process) { + if ( + !__process || + typeof __process.prependListener !== 'function' || + typeof __process.removeListener !== 'function' + ) { + return + } + let __autoDestroyListener + const __captureListener = (__event, __listener) => { + if (__event === 'beforeExit' && __autoDestroyListener === undefined) { + __autoDestroyListener = __listener + } + } + try { + // Run before existing newListener hooks so a hook that registers its own + // beforeExit listener cannot be mistaken for emnapi's registration. + __process.prependListener('newListener', __captureListener) + } catch { + return + } + return () => { + try { + __process.removeListener('newListener', __captureListener) + } catch {} + if (__autoDestroyListener !== undefined) { + try { + __process.removeListener('beforeExit', __autoDestroyListener) + } catch {} + } + } +} + +function __attachCleanupError(__error, __cleanupError) { + try { + if ( + __error && + (typeof __error === 'object' || typeof __error === 'function') && + __error.cause === undefined + ) { + __error.cause = __cleanupError + } + } catch {} +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (__callback) { + setImmediate(__callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (__callback) { + const __channel = new __MessageChannel() + __channel.port1.onmessage = function () { + __channel.port1.onmessage = null + try { + __channel.port1.close() + } catch {} + try { + __channel.port2.close() + } catch {} + __callback() + } + __channel.port2.postMessage(null) + } + } + return function (__callback) { + setTimeout(__callback, 0) + } +})() + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + */ +function __drainWasmEnvCleanup(__instance) { + const __pending = __instance?.exports.napi_wasm_env_cleanup_pending + const __observable = typeof __pending === 'function' + if (__observable) { + let __queued + try { + __queued = __pending() + } catch { + return + } + if (!__queued) { + return + } + } + const __limit = __observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + return (async () => { + let __queued = 0 + for (let __turn = 0; __turn < __limit; __turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!__observable) { + continue + } + try { + __queued = __pending() + } catch { + return + } + if (!__queued) { + return + } + } + if (!__observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: `__prepareForDisposal` + // leaves its drained flag unset, dispose() (and the instantiation-failure + // path) decline to destroy, and a later dispose() runs the drain again — by + // which time the queue has usually been delivered. A counter that is + // somehow stuck nonzero therefore costs each attempt at most another + // bounded wait and a rejection, never a stranded promise; the managed + // beforeExit destroyer still reclaims the context. + const __drainError = new Error( + 'the wasm environment still reports ' + + __queued + + ' queued settlement(s) after ' + + __limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + __drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw __drainError + })() +} + +// A real, *referenced* timer for the async-work wait below, which polls the +// addon rather than interleaving with the @emnapi/core dispatch: a zero-delay +// macrotask there would spin the loop instead of yielding it. Falls back to the +// macrotask scheduler on a host without timers. +function __scheduleTimer(__callback, __delay) { + const __setTimer = globalThis.setTimeout + if (typeof __setTimer !== 'function') { + __scheduleMacrotask(__callback) + return + } + try { + __setTimer(__callback, __delay) + } catch { + __scheduleMacrotask(__callback) + } +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this instance's outstanding `napi_async_work` before the teardown + * that would strand it. + * + * The same hole the eager loaders had, and the same fix: the environment + * cleanup barrier covers promise settlements queued on the threadsafe-function + * queue and says nothing about `napi_async_work`, so destroying the context + * with a work still outstanding leaves its completion callback with nowhere to + * run and its promise unsettled forever. + * + * This flavor is threadless, so `compute` runs on the JavaScript thread inside + * the macrotask that dequeued it: while this is running, any outstanding work + * is queued rather than executing, and `napi_wasm_cancel_pending_async_work` + * can take all of it. The poll is still what decides when the drain is done — + * cancellation delivers those completions from a later macrotask, not + * synchronously. + * + * Per instance, from that instance's own exports: two instances of this loader + * have separate registries and must not wait on each other. + * + * Both exports are optional, so an addon built against a napi crate that + * predates them keeps the previous behavior. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. No deadline: giving up would destroy the + * environment with a completion callback still owed. + */ +function __drainInstanceAsyncWork(__instance) { + const __exports = __instance?.exports + const __pending = __exports?.napi_wasm_async_work_pending + const __cancelPending = __exports?.napi_wasm_cancel_pending_async_work + if (typeof __pending !== 'function' || typeof __cancelPending !== 'function') { + return + } + + const __readPending = () => { + try { + return __pending() + } catch (__error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition and + // best-effort is the honest answer. Anything else means the export is not + // what this loader thinks it is — a defect worth surfacing. + if (__error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw __error + } + } + + if (!__readPending()) { + return + } + try { + __cancelPending() + } catch { + // Cancellation only bounds the wait. Failing it means waiting for the queue + // to run instead, which the poll below already does. + } + if (!__readPending()) { + return + } + + return (async () => { + while (__readPending()) { + await new Promise((__resolve) => { + __scheduleTimer(__resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })() +} + +function __createLifecycleReentryError(__operation) { + const __error = new Error( + __operation + + '() cannot run while an emnapi Context.destroy() call is still active; await the original cleanup promise instead.', + ) + __error.code = 'ERR_NAPI_WASI_LIFECYCLE_REENTRY' + return __error +} + +const __managedEmnapiContextDestroyers = new Set() +let __managedCleanupProcess +let __managedBeforeExitListener +let __managedDestroyPromise +let __managedDestroyersInFlight +let __managedBeforeExitRegistrationRetryCount = 0 +let __managedBeforeExitRegistrationRetryScheduled = false +let __moduleLifecycleDestroyDepth = 0 + +function __removeManagedEmnapiCleanupListeners() { + const __process = __managedCleanupProcess + const __beforeExitListener = __managedBeforeExitListener + __managedCleanupProcess = undefined + __managedBeforeExitListener = undefined + __managedBeforeExitRegistrationRetryCount = 0 + if (__process && __beforeExitListener) { + try { + __process.removeListener('beforeExit', __beforeExitListener) + } catch {} + } +} + +function __scheduleManagedBeforeExitListenerRegistration() { + if ( + !__managedCleanupProcess || + __managedBeforeExitListener || + __managedEmnapiContextDestroyers.size === 0 || + __managedBeforeExitRegistrationRetryScheduled || + __managedBeforeExitRegistrationRetryCount >= 3 + ) { + return + } + __managedBeforeExitRegistrationRetryScheduled = true + __managedBeforeExitRegistrationRetryCount++ + queueMicrotask(() => { + __managedBeforeExitRegistrationRetryScheduled = false + if ( + !__managedCleanupProcess || + __managedBeforeExitListener || + __managedEmnapiContextDestroyers.size === 0 + ) { + return + } + try { + __registerManagedBeforeExitListener() + } catch {} + }) +} + +function __registerManagedBeforeExitListener() { + if (!__managedCleanupProcess || __managedBeforeExitListener) { + return + } + try { + __managedCleanupProcess.once( + 'beforeExit', + __destroyManagedEmnapiContextsBeforeExit, + ) + } catch (error) { + __scheduleManagedBeforeExitListenerRegistration() + throw error + } + __managedBeforeExitListener = __destroyManagedEmnapiContextsBeforeExit + __managedBeforeExitRegistrationRetryCount = 0 +} + +function __settleManagedEmnapiContextDestroy(__promise) { + if (__managedDestroyPromise === __promise) { + __managedDestroyPromise = undefined + __managedDestroyersInFlight = undefined + } + if (__managedEmnapiContextDestroyers.size === 0) { + __removeManagedEmnapiCleanupListeners() + return + } + try { + __registerManagedBeforeExitListener() + } catch {} +} + +function __destroyManagedEmnapiContexts(__excludedDestroyers) { + if (__managedDestroyPromise) { + return __managedDestroyPromise + } + const __destroyers = Array.from(__managedEmnapiContextDestroyers).filter( + (__destroy) => !__excludedDestroyers?.has(__destroy), + ) + if (__destroyers.length === 0) { + return Promise.resolve() + } + let __resolveDestroy + let __rejectDestroy + const __promise = new Promise((resolve, reject) => { + __resolveDestroy = resolve + __rejectDestroy = reject + }) + __managedDestroyPromise = __promise + __managedDestroyersInFlight = new Set(__destroyers) + void Promise.all( + __destroyers.map((__destroy) => { + try { + return Promise.resolve(__destroy()).then( + () => ({ failed: false }), + (error) => ({ failed: true, error }), + ) + } catch (error) { + return { failed: true, error } + } + }), + ).then((__results) => { + let __primaryError + let __failed = false + for (const __result of __results) { + if (!__result.failed) { + continue + } + if (!__failed) { + __failed = true + __primaryError = __result.error + } else { + __attachCleanupError(__primaryError, __result.error) + } + } + if (__failed) { + __rejectDestroy(__primaryError) + } else { + __resolveDestroy() + } + }, __rejectDestroy) + void __promise.then( + () => { + __settleManagedEmnapiContextDestroy(__promise) + }, + () => { + __settleManagedEmnapiContextDestroy(__promise) + }, + ) + return __promise +} + +async function __drainManagedEmnapiContexts(__excludedDestroyers) { + const __attemptedDestroyers = new Set(__excludedDestroyers) + let __primaryError + let __failed = false + while (true) { + let __promise = __managedDestroyPromise + let __destroyers = __managedDestroyersInFlight + if (!__promise) { + __promise = __destroyManagedEmnapiContexts(__attemptedDestroyers) + __destroyers = __managedDestroyersInFlight + if (!__destroyers) { + break + } + } + for (const __destroy of __destroyers) { + __attemptedDestroyers.add(__destroy) + } + try { + await __promise + } catch (error) { + if (!__failed) { + __failed = true + __primaryError = error + } else { + __attachCleanupError(__primaryError, error) + } + } + } + if (__failed) { + throw __primaryError + } +} + +function __destroyManagedEmnapiContextsBeforeExit() { + // A once listener is consumed before Node invokes it, including when another + // cleanup batch is still pending. + __managedBeforeExitListener = undefined + if (__managedDestroyPromise) { + return + } + void __destroyManagedEmnapiContexts().catch((error) => { + queueMicrotask(() => { + throw error + }) + }) +} + +function __registerManagedEmnapiContext(__process, __destroy) { + __managedEmnapiContextDestroyers.add(__destroy) + if ( + !__managedCleanupProcess && + __process && + typeof __process.once === 'function' && + typeof __process.removeListener === 'function' + ) { + __managedCleanupProcess = __process + } + let __registered = true + return () => { + if (!__registered) { + return + } + __registered = false + __managedEmnapiContextDestroyers.delete(__destroy) + if (__managedEmnapiContextDestroyers.size === 0) { + __removeManagedEmnapiCleanupListeners() + } + } +} + +async function __createManagedEmnapiContext( + __prepareEnvCleanup, + __isPreparingEnvCleanup, +) { + const __process = + typeof process === 'object' && process !== null ? process : undefined + const __finishAutoDestroyCapture = + __captureEmnapiAutoDestroyListener(__process) + let __emnapiContext + let __contextInitializationError + let __contextInitializationFailed = false + try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareEnvCleanup, + __isPreparingEnvCleanup, + ) + // emnapi 2.x still registers an unconditional process.once('beforeExit') + // auto-destroy listener on Node hosts, and suppressDestroy() only + // neutralizes its callback without removing it. This loader must stay + // side-effect free per instance, so the listener is captured and removed; + // suppressDestroy() remains the safety net when removal is unavailable. + __emnapiContext.suppressDestroy() + } catch (error) { + __contextInitializationError = error + __contextInitializationFailed = true + } finally { + // Remove only the exact emnapi callback captured above. + __finishAutoDestroyCapture?.() + } + if (__emnapiContext === undefined) { + throw __contextInitializationError + } + let __disposed = false + let __destroying = false + let __destroyPromise + let __cleanupRegistered = false + let __unregisterCleanup + const __destroy = (__blocksModuleLifecycle = false) => { + if (__disposed) { + return + } + if (__destroying) { + throw __createLifecycleReentryError('dispose') + } + if (__destroyPromise) { + return __destroyPromise + } + __destroying = true + let __result + const __finishDestroyInvocation = () => { + __destroying = false + } + const __finishModuleLifecycleDestroy = () => { + if (__blocksModuleLifecycle) { + __blocksModuleLifecycle = false + __moduleLifecycleDestroyDepth-- + } + } + if (__blocksModuleLifecycle) { + __moduleLifecycleDestroyDepth++ + } + try { + // Context.destroy() disables JS before cleanup hooks run, so settle + // runtime-owned promises while this environment can still call JS. + __prepareEnvCleanup?.() + if (__isPreparingEnvCleanup?.()) { + // Reached from inside the barrier, so `Context.destroy()` below would + // hit the wrapper's in-flight no-op. Recording that as a completed + // destroy is what makes the frame that *did* start the barrier skip the + // real one afterwards, leaving the context retained with its cleanup + // hooks unrun. Refuse instead: nothing is flagged, the context stays + // registered for managed beforeExit cleanup, and a later destroy still + // works. dispose() coalesces reentrancy before it can get here, so this + // is the backstop for any other caller that manages to. + throw __createLifecycleReentryError('dispose') + } + __result = __emnapiContext.destroy() + } catch (error) { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + throw error + } + let __then + try { + if ( + __result !== null && + (typeof __result === 'object' || typeof __result === 'function') + ) { + __then = __result.then + } + } catch (error) { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + throw error + } + if (typeof __then === 'function') { + let __resolveResult + let __rejectResult + const __resultPromise = new Promise((resolve, reject) => { + __resolveResult = resolve + __rejectResult = reject + }) + const __promise = __resultPromise.then( + (value) => { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __disposed = true + __destroyPromise = undefined + __unregisterCleanup?.() + return value + }, + (error) => { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __destroyPromise = undefined + throw error + }, + ) + __destroyPromise = __promise + try { + Reflect.apply(__then, __result, [__resolveResult, __rejectResult]) + } catch (error) { + __rejectResult(error) + } + return __promise + } + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __disposed = true + __unregisterCleanup?.() + } + const __destroyForModuleLifecycle = () => __destroy(true) + const __registerCleanup = ( + __beforeExitDestroy = __destroyForModuleLifecycle, + ) => { + if (__cleanupRegistered || __disposed) { + return + } + __unregisterCleanup = __registerManagedEmnapiContext( + __process, + __beforeExitDestroy, + ) + __cleanupRegistered = true + __registerManagedBeforeExitListener() + } + if (__contextInitializationFailed) { + let __registrationError + let __registrationFailed = false + try { + __registerCleanup() + } catch (error) { + __attachCleanupError(__contextInitializationError, error) + __registrationError = error + __registrationFailed = true + } + try { + await __destroyForModuleLifecycle() + } catch (error) { + __attachCleanupError( + __registrationFailed + ? __registrationError + : __contextInitializationError, + error, + ) + try { + __registerManagedBeforeExitListener() + } catch {} + } + throw __contextInitializationError + } + return { + context: __emnapiContext, + destroy: __destroy, + destroyForModuleLifecycle: __destroyForModuleLifecycle, + registerCleanup: __registerCleanup, + } +} + +async function __createInstance( + __wasmInput, + __options, + __beforeExitDestroy, + __onManagedDestroyer, +) { + const __module = await __resolveModule(__wasmInput) + const __emnapiModule = await __normalizeModuleForEmnapi(__module) + const __wasi = new __WASI({ + version: 'preview1', + }) + // The wasm module is linked with `--import-memory`, so a Memory must be + // provided. It is resolved here in function scope (workerd bans global scope + // allocation) and is never shared (no threads, no SharedArrayBuffer). + // Resolve it before the emnapi context so a rejected option bag or a host + // memory-limit failure cannot leak a context that never reaches + // instantiation. + const __wasmMemory = __resolveInstanceMemory(__options) + let __lifecycleState = 'pending' + let __destroyEmnapiContext + let __destroyOwnedContext + let __destroyManagedOwnedContext + let __napiInstance + let __wasmEnvCleanupRan = false + let __wasmEnvCleanupPrepared = false + let __wasmEnvCleanupPreparing = false + let __wasmEnvCleanupDrained = false + let __wasmEnvCleanupDrainPromise + const __isPreparingEnvCleanup = () => __wasmEnvCleanupPreparing + const __prepareEnvCleanup = () => { + if (__wasmEnvCleanupPrepared || __wasmEnvCleanupPreparing) { + return + } + const __prepareWasmEnvCleanup = + __napiInstance?.exports.napi_prepare_wasm_env_cleanup + if (typeof __prepareWasmEnvCleanup === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __wasmEnvCleanupPreparing = true + try { + __prepareWasmEnvCleanup() + } finally { + __wasmEnvCleanupPreparing = false + } + __wasmEnvCleanupRan = true + } + __wasmEnvCleanupPrepared = true + } + // The barrier + settlement drain, hoisted out of the context destroyer so the + // drain can yield without widening the destroyer's reentry window. Both + // yielding paths run it — dispose() and the initialization-failure rollback. + // The destroyer still runs the barrier itself (idempotently) for the one path + // that cannot yield: managed beforeExit cleanup of an instance whose rollback + // is being retried. + // + // "Drained" is recorded only once a wait has actually finished. Scheduling a + // macrotask can fail — a host-provided or patched `setImmediate` that throws + // is enough — and dispose() stays retryable after it rejects, so marking the + // drain complete up front would make the retry skip it and destroy the context + // with the barrier's settlements still queued. + const __prepareForDisposal = () => { + if (__wasmEnvCleanupDrained) { + return + } + if (__wasmEnvCleanupDrainPromise) { + return __wasmEnvCleanupDrainPromise + } + __prepareEnvCleanup() + if (!__wasmEnvCleanupRan) { + return + } + const __drained = __drainWasmEnvCleanup(__napiInstance) + if (!__drained || typeof __drained.then !== 'function') { + __wasmEnvCleanupDrained = true + return + } + const __tracked = __drained.then( + (__value) => { + __wasmEnvCleanupDrained = true + __wasmEnvCleanupDrainPromise = undefined + return __value + }, + (__error) => { + __wasmEnvCleanupDrainPromise = undefined + throw __error + }, + ) + __wasmEnvCleanupDrainPromise = __tracked + return __tracked + } + let __disposed = false + const __runInstanceDisposal = async () => { + if (__lifecycleState !== 'failed') { + __lifecycleState = 'disposal' + } + // Outstanding async work first, while the environment is still completely + // live: its completion callbacks run addon code, and the barrier and + // `Context.destroy()` below each take that away. Undefined unless work is + // outstanding. + const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance) + if (__asyncWorkDrained) { + await __asyncWorkDrained + } + // Then settle what the barrier cancelled, before the environment stops + // accepting JavaScript calls. Undefined unless something is queued, so + // an idle disposal is not delayed by a single turn. + const __drained = __prepareForDisposal() + if (__drained) { + await __drained + } + const __result = await (__beforeExitDestroy + ? __destroyManagedOwnedContext() + : __destroyOwnedContext()) + // Only a completed destroy retires the instance; a throw above leaves + // the counter untouched so a retried dispose() cannot double-decrement. + if (!__disposed) { + __disposed = true + __liveInstances -= 1 + } + return __result + } + let __instanceDisposePromise + /** + * The disposal frame runs the barrier, and the barrier settles the promises it + * cancels synchronously — so a promise hook firing inside it can call this + * same instance's dispose() again while the first call is still in its drain. + * That nested call finds the barrier flagged in flight, prepares nothing, + * drains nothing, and falls straight through to the context destroyer, whose + * `Context.destroy()` hits the wrapper's in-flight no-op. It would record a + * destruction that never happened, and the outer frame would then skip the + * real one: both disposals resolve, no cleanup hook runs, the context stays + * retained. + * + * Memoize before any of that starts, exactly like the eager loaders' + * `__disposeWasiBinding`, so there is only ever one disposal frame per + * instance and a reentrant caller awaits it instead of racing it. Cleared on + * rejection: a drain that failed has to stay retryable. + */ + const __disposeInstance = () => { + if (__instanceDisposePromise) { + return __instanceDisposePromise + } + let __resolveDispose + let __rejectDispose + const __disposePromise = new Promise((__resolve, __reject) => { + __resolveDispose = __resolve + __rejectDispose = __reject + }) + __instanceDisposePromise = __disposePromise + __runInstanceDisposal().then(__resolveDispose, (__error) => { + __instanceDisposePromise = undefined + __rejectDispose(__error) + }) + return __disposePromise + } + const __destroyBeforeExit = __beforeExitDestroy + ? async () => { + if (__lifecycleState === 'failed') { + await __destroyManagedOwnedContext() + return + } + __lifecycleState = 'disposal' + try { + await __beforeExitDestroy() + } catch (error) { + if (__lifecycleState !== 'failed') { + throw error + } + // The singleton's initialization rejection is already observable + // through instantiate() and dispose(). Managed beforeExit cleanup + // owns only context destruction, including retrying a failed rollback. + await __destroyManagedOwnedContext() + } + } + : undefined + const { + context: __emnapiContext, + destroy, + destroyForModuleLifecycle, + registerCleanup: __registerCleanup, + } = await __createManagedEmnapiContext( + __prepareEnvCleanup, + __isPreparingEnvCleanup, + ) + __destroyEmnapiContext = destroy + __destroyOwnedContext = () => __destroyEmnapiContext() + __destroyManagedOwnedContext = destroyForModuleLifecycle + try { + if (__destroyBeforeExit) { + __onManagedDestroyer(__destroyBeforeExit) + await __registerCleanup(__destroyBeforeExit) + } + let __napiModule + ;({ + instance: __napiInstance, + napiModule: __napiModule, + } = await __emnapiInstantiateNapiModule(__emnapiModule, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + // `instantiate()` and `createInstance().exports` hand out this object; a + // named module export does not travel with it. After the instance host + // install, which hands the same object to addon-provided registration + // functions that may put anything on it, and inside this `try`, so a + // claimed name flips `__lifecycleState` to 'failed' and tears the instance + // down rather than escaping a half-built one. + __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) + if (__lifecycleState === 'pending') { + __lifecycleState = 'succeeded' + } + __createdInstances += 1 + __liveInstances += 1 + return { + exports: __napiModule.exports, + get memory() { + return __wasmMemory + }, + get memoryBytes() { + // The Memory outlives the environment, so this stays readable after a + // FAILED dispose() (which leaves the instance undisposed and + // retryable). It reports 0 only once disposal has actually completed. + return __disposed ? 0 : __wasmMemory.buffer.byteLength + }, + get disposed() { + return __disposed + }, + dispose: __disposeInstance, + } + } catch (error) { + __lifecycleState = 'failed' + // Instantiation can fail *after* registration has run, and registration runs + // with a live environment: a module-init hook can start async work and then + // return an error, and the promise it created may already have escaped into + // JavaScript. Settle what the barrier cancels before the context is + // destroyed, exactly like dispose() does; destroying without yielding + // discards the queue with a null env. Undefined unless something is queued, + // so a failure before beforeInit costs no extra turn. + let __settlementsUnreached = false + try { + // Registration can start async work too, and this path destroys the same + // environment its completions need. A drain that cannot finish leaves the + // work outstanding, so it counts as settlements unreached and stops the + // rollback short of destroying, exactly like a failed barrier drain. + const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance) + if (__asyncWorkDrained) { + await __asyncWorkDrained + } + const __drained = __prepareForDisposal() + if (__drained) { + await __drained + } + } catch (drainError) { + __attachCleanupError(error, drainError) + __settlementsUnreached = true + } + let __registrationError + let __registrationFailed = false + if (!__beforeExitDestroy) { + try { + // Independent instances are caller-owned while pending and after + // success. Register only failed rollback so cleanup remains retryable. + await __registerCleanup() + } catch (registrationError) { + __attachCleanupError(error, registrationError) + __registrationError = registrationError + __registrationFailed = true + } + } + if (__settlementsUnreached) { + // The barrier or the drain did not finish, so the settlements it queued + // are still in the threadsafe-function queue. Destroying now runs the + // cleanup hook that drains that queue with a null env and discards it, + // stranding a promise that already escaped into JavaScript — with nothing + // left that could ever settle it. dispose() refuses to destroy for exactly + // this reason (a rejected drain there never reaches the destroy), so this + // path refuses too. + // + // Nothing leaks. The registration just above — and, for the singleton, the + // one made before instantiation — leaves this context in + // `__managedEmnapiContextDestroyers`, so beforeExit destroys it and + // dispose() can retry it. Only the destruction is deferred, and the turns + // that pass in the meantime are exactly what the queue needed. + try { + __registerManagedBeforeExitListener() + } catch {} + throw error + } + try { + await __destroyManagedOwnedContext() + } catch (disposeError) { + // Initialization is the primary failure. Preserve it even if cleanup + // also fails, while retaining the cleanup error when the value is + // extensible and has no existing cause. + __attachCleanupError( + __registrationFailed ? __registrationError : error, + disposeError, + ) + try { + __registerManagedBeforeExitListener() + } catch {} + } + throw error + } +} + +/** + * Create an independent instance. Call and await dispose() when the instance + * is no longer needed so emnapi cleanup hooks run deterministically. + * + * The optional second argument selects this instance's linear memory: either + * `memory` (an unshared, single-use WebAssembly.Memory you allocated) or + * `initialMemoryPages` / `maximumMemoryPages`, never both. Omitted, the + * loader allocates WASM_MEMORY.initialPages..WASM_MEMORY.maximumPages. + * + * A provided Memory must come from this loader's own realm: the WASI and + * emnapi layers underneath identify one with a realm-local `instanceof`, so a + * Memory built in a `node:vm` context or another frame is rejected. Every + * Memory an instance runs on is single-use, the loader-allocated one included: + * `instance.memory` cannot be recycled into a second `createInstance()`. + */ +export async function createInstance(__wasmInput, __options) { + return __createInstance(__wasmInput, __options) +} + +let __defaultModulePromise +let __defaultInstancePromise +let __defaultDisposePromise +let __defaultDisposalStarted = false +const __defaultManagedDestroyers = new WeakMap() +let __moduleDisposePromise + +/** + * Instantiate a module-local singleton. Concurrent and repeated calls + * with the same module share one instance and one Memory allocation. + */ +export function instantiate(__wasmInput) { + const __modulePromise = __resolveModule(__wasmInput) + if (__moduleLifecycleDestroyDepth !== 0) { + void __modulePromise.catch(() => {}) + return Promise.reject(__createLifecycleReentryError('instantiate')) + } + if (__moduleDisposePromise) { + void __modulePromise.catch(() => {}) + return __moduleDisposePromise.then(() => instantiate(__modulePromise)) + } + if (__defaultDisposalStarted) { + // Observe rejected input immediately, but preserve lifecycle ordering and + // error precedence by instantiating only after disposal succeeds. A failed + // disposal retains the old instance only so its cleanup can be retried. + void __modulePromise.catch(() => {}) + const __disposePromise = __defaultDisposePromise ?? dispose() + return __disposePromise.then(() => instantiate(__modulePromise)) + } + if (!__defaultInstancePromise) { + __defaultModulePromise = __modulePromise + const __instancePromise = __modulePromise.then((__module) => + __createInstance( + __module, + undefined, + __disposeDefaultInstance, + (__managedDestroyer) => { + __defaultManagedDestroyers.set( + __instancePromise, + __managedDestroyer, + ) + }, + ), + ) + __defaultInstancePromise = __instancePromise + void __instancePromise.catch(() => { + if (__defaultInstancePromise === __instancePromise) { + __defaultInstancePromise = undefined + __defaultModulePromise = undefined + } + }) + return __instancePromise.then((__instance) => __instance.exports) + } + const __defaultModulePromiseForCall = __defaultModulePromise + const __defaultInstancePromiseForCall = __defaultInstancePromise + return Promise.all([__defaultModulePromiseForCall, __modulePromise]).then( + async ([__defaultModule, __module]) => { + if (__defaultModule !== __module) { + throw new Error( + 'instantiate() already owns a different WebAssembly.Module; call dispose() first or use createInstance() for independent instances.', + ) + } + return (await __defaultInstancePromiseForCall).exports + }, + ) +} + +async function __disposeDefaultInstance(__onDestroy) { + if (__defaultDisposePromise) { + return __defaultDisposePromise + } + const __instancePromise = __defaultInstancePromise + if (!__instancePromise) { + __defaultDisposalStarted = false + return + } + __defaultDisposalStarted = true + const __disposePromise = (async () => { + let __instance + try { + __instance = await __instancePromise + } catch (error) { + const __managedDestroyer = + __defaultManagedDestroyers.get(__instancePromise) + if (__managedDestroyer) { + __onDestroy?.(__managedDestroyer) + } + __defaultManagedDestroyers.delete(__instancePromise) + throw error + } + const __managedDestroyer = + __defaultManagedDestroyers.get(__instancePromise) + if (__managedDestroyer) { + __onDestroy?.(__managedDestroyer) + } + await __instance.dispose() + if (__defaultInstancePromise === __instancePromise) { + __defaultInstancePromise = undefined + __defaultModulePromise = undefined + __defaultDisposalStarted = false + } + __defaultManagedDestroyers.delete(__instancePromise) + })() + __defaultDisposePromise = __disposePromise + try { + await __disposePromise + } finally { + if (__defaultDisposePromise === __disposePromise) { + __defaultDisposePromise = undefined + } + } +} + +async function __dispose() { + let __defaultDisposeError + let __defaultDisposeFailed = false + let __attemptedDefaultDestroyer + try { + await __disposeDefaultInstance((__destroyer) => { + __attemptedDefaultDestroyer = __destroyer + }) + } catch (error) { + __defaultDisposeError = error + __defaultDisposeFailed = true + } + const __excludedDestroyers = new Set() + if (__defaultDisposeFailed && __attemptedDefaultDestroyer) { + __excludedDestroyers.add(__attemptedDefaultDestroyer) + } + try { + await __drainManagedEmnapiContexts(__excludedDestroyers) + } catch (error) { + if (!__defaultDisposeFailed) { + throw error + } + if (error !== __defaultDisposeError) { + __attachCleanupError(__defaultDisposeError, error) + } + } + if (__defaultDisposeFailed) { + throw __defaultDisposeError + } +} + +/** + * Dispose the singleton created by instantiate(). A later call may create a + * fresh instance, including from a different module. This also retries cleanup + * retained after a failed initialization rollback. + */ +export function dispose() { + if (__moduleLifecycleDestroyDepth !== 0) { + return Promise.reject(__createLifecycleReentryError('dispose')) + } + if (__moduleDisposePromise) { + return __moduleDisposePromise + } + let __resolveDispose + let __rejectDispose + const __promise = new Promise((resolve, reject) => { + __resolveDispose = resolve + __rejectDispose = reject + }) + __moduleDisposePromise = __promise + void __dispose().then(__resolveDispose, __rejectDispose) + void __promise.then( + () => { + if (__moduleDisposePromise === __promise) { + __moduleDisposePromise = undefined + } + }, + () => { + if (__moduleDisposePromise === __promise) { + __moduleDisposePromise = undefined + } + }, + ) + return __promise +} diff --git a/napi/transform-react/transform-react.wasip1.cjs b/napi/transform-react/transform-react.wasip1.cjs new file mode 100644 index 0000000000..1713119dd6 --- /dev/null +++ b/napi/transform-react/transform-react.wasip1.cjs @@ -0,0 +1,1208 @@ +// napi-rs-artifact-metadata:{"version":2,"rootEntry":"index.js","exports":["Severity","transform","transformSync"],"managedRootEntries":["browser.js","index.js","transform-react.wasm","transform-react.debug.wasm"]} +/* eslint-disable */ +/* auto-generated by NAPI-RS */ + +const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +const __nodeFs = require('node:fs') +const __nodePath = require('node:path') +const { WASI: __nodeWASI } = require('node:wasi') + +const { + emnapiAsyncWorkPlugin: __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin: __emnapiTSFNPlugin, + instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync, +} = require('@napi-rs/wasm-runtime') +const { createContext: __emnapiCreateContext } = require('@emnapi/runtime') + +const __cwd = process.cwd() +const __rootDir = __nodePath.parse(__cwd).root +const __hostRoot = + process.platform === 'android' ? __cwd : __rootDir + +const __wasi = new __nodeWASI({ + version: 'preview1', + env: process.env, + preopens: { + [__rootDir]: __hostRoot, + [__hostRoot]: __hostRoot, + }, +}) + +const __wasmMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, +}) + +let __wasmFilePath = __nodePath.join(__dirname, 'transform-react.wasm32-wasip1.wasm') +const __wasmDebugFilePath = __nodePath.join(__dirname, 'transform-react.wasm32-wasip1.debug.wasm') + +if (__nodeFs.existsSync(__wasmDebugFilePath)) { + __wasmFilePath = __wasmDebugFilePath +} else if (!__nodeFs.existsSync(__wasmFilePath)) { + const __wasiPackageEntry = require.resolve('@oxc-transform-react/binding-wasm32-wasip1') + const __packagedWasmFilePath = __nodePath.join( + __nodePath.dirname(__wasiPackageEntry), + 'transform-react.wasm32-wasip1.wasm', + ) + if (!__nodeFs.existsSync(__packagedWasmFilePath)) { + throw new Error( + '@oxc-transform-react/binding-wasm32-wasip1 is installed but is missing transform-react.wasm32-wasip1.wasm.', + ) + } + __wasmFilePath = __packagedWasmFilePath +} + +const __wasmFile = __nodeFs.readFileSync(__wasmFilePath) +let __emnapiContext + +const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose') +const __wasiWorkers = new Set() +// The thread manager has to be reachable *before* anything that can throw +// during load or registration. Initialization can fail after the pool has +// already spawned workers, and the rollback still has to mark their +// terminations as expected — but `__napiModule` is assigned only when +// instantiation RETURNS, so on exactly that path it is still undefined. A +// plugin factory runs while the emnapi module is being created, before the +// wasm is loaded and before any registration function runs, and its context +// carries the very same manager instance. +let __wasiThreadManager + +function __captureWasiThreadManager(context) { + if (context && context.PThread) { + __wasiThreadManager = context.PThread + } + return {} +} + +function __getWasiThreadManager() { + const manager = + __wasiThreadManager !== undefined + ? __wasiThreadManager + : __napiModule + ? __napiModule.PThread + : undefined + if (manager && typeof manager.terminateWorker === 'function') { + return manager + } + return undefined +} +let __napiInstance +let __emnapiContextDestroyed = false +let __emnapiContextDestroyPromise +let __emnapiWasmEnvCleanupPrepared = false +let __emnapiWasmEnvCleanupPreparing = false +let __emnapiWasmEnvCleanupRan = false +let __emnapiWasmEnvCleanupDrained = false +let __emnapiWasmEnvCleanupDrainPromise +let __wasiDisposed = false +let __wasiAsyncWorkDrainPromise +let __wasiDisposePromise +let __completeWasiDisposal = function () {} +// Overridden by loader flavors that have a last-resort reclaim for a rollback +// that stopped short of destroying the context. See +// `__rollbackWasiInitialization`. +let __retainWasiRollbackForRetry = function () {} + +function __isThenable(value) { + return ( + value !== null && + (typeof value === 'object' || typeof value === 'function') && + typeof value.then === 'function' + ) +} + +function __createCleanupError(errors, message) { + if (errors.length === 1) { + return errors[0] + } + const __AggregateError = globalThis.AggregateError + if (typeof __AggregateError === 'function') { + return new __AggregateError(errors, message) + } + const error = new Error(message) + error.errors = errors + return error +} + +function __attachCleanupErrors(error, cleanupErrors) { + if (cleanupErrors.length === 0) { + return error + } + const cleanupError = __createCleanupError( + cleanupErrors, + 'WASI binding cleanup failed', + ) + try { + if ( + error && + (typeof error === 'object' || typeof error === 'function') + ) { + if (error.cause === undefined) { + error.cause = cleanupError + if (error.cause === cleanupError) { + return error + } + } + if (Array.isArray(error.cleanupErrors)) { + error.cleanupErrors.push(cleanupError) + return error + } else { + const attachedCleanupErrors = [cleanupError] + error.cleanupErrors = attachedCleanupErrors + if (error.cleanupErrors === attachedCleanupErrors) { + return error + } + } + } + } catch {} + const aggregate = __createCleanupError( + [error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + try { + aggregate.cause = error + } catch {} + return aggregate +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __isPreparingWasmEnvCleanup() { + return __emnapiWasmEnvCleanupPreparing +} + +function __prepareWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) { + return + } + const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup + if (typeof prepare === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __emnapiWasmEnvCleanupPreparing = true + try { + prepare() + } finally { + __emnapiWasmEnvCleanupPreparing = false + } + __emnapiWasmEnvCleanupRan = true + } + __emnapiWasmEnvCleanupPrepared = true +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (callback) { + setImmediate(callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (callback) { + const channel = new __MessageChannel() + channel.port1.onmessage = function () { + channel.port1.onmessage = null + try { + channel.port1.close() + } catch {} + try { + channel.port2.close() + } catch {} + callback() + } + channel.port2.postMessage(null) + } + } + return function (callback) { + setTimeout(callback, 0) + } +})() + +// A real, *referenced* timer, for waits that must let the whole host make +// progress between looks — the async-work drain polls the addon rather than +// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there +// would spin the loop instead of yielding it. Falls back to the macrotask +// scheduler on a host without timers. +function __scheduleTimer(callback, delay) { + const setTimer = globalThis.setTimeout + if (typeof setTimer !== 'function') { + __scheduleMacrotask(callback) + return + } + try { + setTimer(callback, delay) + } catch { + __scheduleMacrotask(callback) + } +} + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + * + * The "already drained" flag is set only once a wait has actually finished. + * Scheduling a macrotask can fail — a host-provided or patched `setImmediate` + * that throws is enough — and a disposal that rejects stays retryable, so + * marking the drain complete up front would make the retry skip it and destroy + * the context with the barrier's settlements still queued. + * + * A wait that runs out of turns with the counter still nonzero rejects with + * `ERR_NAPI_WASI_CLEANUP_PENDING` for the same reason: at that point + * "finished" is indistinguishable from the stranding above, and destroying + * would discard the very settlement the wait was for. The rejection leaves the + * flag unset and disposal retryable. + */ +function __drainWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupDrained || !__emnapiWasmEnvCleanupRan) { + return + } + if (__emnapiWasmEnvCleanupDrainPromise) { + return __emnapiWasmEnvCleanupDrainPromise + } + const pending = __napiInstance?.exports?.napi_wasm_env_cleanup_pending + const observable = typeof pending === 'function' + if (observable) { + let queued + try { + queued = pending() + } catch { + __emnapiWasmEnvCleanupDrained = true + return + } + if (!queued) { + __emnapiWasmEnvCleanupDrained = true + return + } + } + const limit = observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + const drainPromise = (async () => { + let queued = 0 + for (let turn = 0; turn < limit; turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!observable) { + continue + } + try { + queued = pending() + } catch { + return + } + if (!queued) { + return + } + } + if (!observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: the drained flag stays + // unset, dispose() (and the rollback) decline to destroy, and a later + // dispose() runs the drain again — by which time the queue has usually been + // delivered. A counter that is somehow stuck nonzero therefore costs each + // attempt at most another bounded wait and a rejection, never a stranded + // promise; the process-exit teardown still reclaims the context. + const drainError = new Error( + 'the wasm environment still reports ' + + queued + + ' queued settlement(s) after ' + + limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw drainError + })().then( + (value) => { + // Set only when the wait actually finished AND the queue was seen empty + // (or is unobservable): a drain that timed out with settlements still + // queued rejects above and must stay repeatable. + __emnapiWasmEnvCleanupDrained = true + __emnapiWasmEnvCleanupDrainPromise = undefined + return value + }, + (error) => { + __emnapiWasmEnvCleanupDrainPromise = undefined + throw error + }, + ) + __emnapiWasmEnvCleanupDrainPromise = drainPromise + return drainPromise +} + +function __destroyEmnapiContext() { + if (__emnapiContextDestroyed || __emnapiContext === undefined) { + __emnapiContextDestroyed = true + return + } + if (__emnapiContextDestroyPromise) { + return __emnapiContextDestroyPromise + } + + __prepareWasmEnvCleanup() + const result = __emnapiContext.destroy() + if (!__isThenable(result)) { + __emnapiContextDestroyed = true + return + } + + const destroyPromise = Promise.resolve(result).then( + (value) => { + __emnapiContextDestroyed = true + return value + }, + (error) => { + __emnapiContextDestroyPromise = undefined + throw error + }, + ) + __emnapiContextDestroyPromise = destroyPromise + return destroyPromise +} + +/** + * Holds the event loop open until `work` settles. + * + * Nothing else can: the pool workers are deliberately unreferenced so an idle + * binding cannot keep a process alive, and referencing them again for the + * termination does not hold either — emnapi unreferences a worker the moment it + * reports `async-thread-ready`, which for a worker that was still starting + * lands *after* the termination began. Without a handle of its own, an + * `await dispose()` with nothing else pending exits the process with its + * promise unsettled, and everything after the `await` is skipped. + * + * The timer is cleared as soon as the work settles, so this never outlives the + * disposal that asked for it. + */ +function __keepEventLoopAliveUntil(work) { + const setTimer = globalThis.setInterval + const clearTimer = globalThis.clearInterval + if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') { + return work + } + let timer + try { + timer = setTimer(function () {}, 50) + } catch { + return work + } + const release = function () { + try { + clearTimer(timer) + } catch {} + } + return work.then( + (value) => { + release() + return value + }, + (error) => { + release() + throw error + }, + ) +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this addon's outstanding `napi_async_work` before the teardown that + * would strand it. + * + * `napi_prepare_wasm_env_cleanup` does not cover async work, and nothing about + * it is observable from JavaScript: the threadless archive resolves + * `napi_*_async_work` through the `@emnapi/core` plugins, but the threaded one + * links the C `async_work.c` on the uv threadpool, so there the wasm neither + * imports nor exports those symbols and the only brackets a loader could watch + * (`_emnapi_ctx_*_waiting_request_counter`) are shared with threadsafe + * functions. The addon is the one place both flavors go through, so it answers + * for both, through the same kind of handshake the settlement drain uses: + * + * - `napi_wasm_cancel_pending_async_work()` cancels what no thread has + * started. Those completion callbacks run with `napi_cancelled`, which + * napi-rs turns into a promise rejected with an `AbortError`. + * - `napi_wasm_async_work_pending()` counts what is still owed a completion + * callback. Work already executing refuses cancellation and stays counted + * until it finishes normally — which it can, because this runs before the + * barrier, before `Context.destroy()` and before anything is terminated. + * + * Both exports are optional: an addon built against a napi crate that predates + * them drains nothing and keeps the previous behavior, exactly as the + * `napi_wasm_env_cleanup_pending` handshake degrades. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. The promise it returns otherwise never + * rejects. + * + * The wait has no deadline, and that is the point: giving up would destroy the + * environment with a completion callback still owed, which is the stranding + * this exists to prevent. A task whose `execute` never returns already keeps an + * *undisposed* process alive in exactly the same way, so disposal inherits that + * rather than inventing a bound it cannot honor. + * + * Safe to call from inside a completion callback, which is reachable: settling + * a task runs addon code that can re-enter JavaScript — a setter on the value + * being handed back, a threadsafe-function callback — and that JavaScript can + * call `dispose()`. Two things make it terminate rather than wait on itself: + * + * - The addon keeps a work registered until its completion callback + * *finishes*, so the count read here is at least one and this takes the + * polling path instead of declaring the environment drained and tearing it + * down from inside the frame that is still settling a promise. + * - The poll is a timer, so it cannot run until the callback has returned to + * the host — by which time that work has left the registry. The count the + * next poll reads is the one taken after the callback finished. + * + * `__disposeWasiBinding` hands every caller the same in-flight promise, so the + * nested call joins this disposal rather than starting a second one. + */ +function __drainWasiAsyncWork() { + if (__wasiAsyncWorkDrainPromise !== undefined) { + return __wasiAsyncWorkDrainPromise + } + const exports = __napiInstance?.exports + const pending = exports?.napi_wasm_async_work_pending + const cancelPending = exports?.napi_wasm_cancel_pending_async_work + if (typeof pending !== 'function' || typeof cancelPending !== 'function') { + return + } + + const readPending = () => { + try { + return pending() + } catch (error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition — + // there is nothing left to wait for, and refusing to dispose would only + // keep a dead instance and its stuck counter alive. Best-effort here is + // the honest answer, and it is what disposal did before this drain + // existed. + // + // Only a trap. Anything else means the export is not what this loader + // thinks it is, which is a defect worth surfacing rather than disposing + // over. + if (error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw error + } + } + + if (!readPending()) { + return + } + try { + cancelPending() + } catch { + // Cancellation is an optimization: it bounds the wait by the work already + // executing. Failing it only means waiting for the whole queue instead. + } + if (!readPending()) { + return + } + + const drainPromise = __keepEventLoopAliveUntil( + (async () => { + while (readPending()) { + await new Promise((resolve) => { + __scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })(), + ).then( + () => { + __wasiAsyncWorkDrainPromise = undefined + }, + (error) => { + // A wait that could not run is not a wait that finished. The only way + // here is a host whose timers and macrotask primitives all refuse, and + // the work is still outstanding — reporting success would destroy the + // environment over it, which is the stranding this exists to prevent. + // Reject instead: disposal stays retryable, and the context is not + // destroyed. Clearing the memo first is what makes the retry re-run this. + __wasiAsyncWorkDrainPromise = undefined + throw error + }, + ) + __wasiAsyncWorkDrainPromise = drainPromise + return drainPromise +} + +/** + * `@emnapi/wasi-threads` counts a worker exit as expected only when its own + * thread manager performed the termination. A bare `worker.terminate()` reaches + * the manager's `exit` listener instead, which reports + * `worker (tid = N) sent an error! ... stopped with exit code 1` and rethrows + * inside the emit — aborting the `once('exit')` that backs the terminate + * promise, so disposal never settles and the process dies with an uncaught + * exception. Mark the termination through the manager first. + * + * The manager comes from `__getWasiThreadManager`, not from `__napiModule`: + * the initialization rollback runs on the one path where instantiation never + * returned, so `__napiModule` is still undefined there while the workers it + * spawned are already registered and loaded. + * + * Not `terminateAllThreads()`: that one recreates the pool it just shut down. + */ +function __terminateWasiWorkers() { + const cleanupErrors = [] + const pending = [] + const threadManager = __getWasiThreadManager() + + for (const worker of __wasiWorkers) { + let result + try { + if (threadManager) { + threadManager.terminateWorker(worker) + // `terminateWorker` leaves behind a reporter that logs every message + // still queued on the port, which Node flushes on exit. Nothing is + // listening for those any more. + worker.onmessage = undefined + } + result = worker.terminate() + } catch (error) { + cleanupErrors.push(error) + continue + } + if (__isThenable(result)) { + pending.push( + Promise.resolve(result).then( + () => { + __wasiWorkers.delete(worker) + }, + (error) => { + cleanupErrors.push(error) + }, + ), + ) + } else { + __wasiWorkers.delete(worker) + } + } + + const finish = () => { + if (cleanupErrors.length > 0) { + throw __createCleanupError( + cleanupErrors, + 'Failed to terminate WASI workers', + ) + } + } + return pending.length > 0 + ? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish) + : finish() +} + +function __finishWasiDisposal() { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult).then(__completeWasiDisposal) + } + return __completeWasiDisposal() +} + +function __continueWasiDisposal() { + const destroyResult = __destroyEmnapiContext() + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult).then(__finishWasiDisposal) + } + return __finishWasiDisposal() +} + +function __cleanUpWasmEnvForWasiDisposal() { + // Run the pre-teardown barrier, then let the settlements it queued actually + // reach JavaScript, and only then destroy the environment. Doing these two + // back to back is what strands them. + __prepareWasmEnvCleanup() + const drainResult = __drainWasmEnvCleanup() + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then(__continueWasiDisposal) + } + return __continueWasiDisposal() +} + +function __startWasiDisposal() { + // Outstanding `napi_async_work` goes first, while the environment is still + // completely live: the completion callbacks run addon code, and everything + // after this point takes that away from them — the barrier shuts the async + // runtime down, `Context.destroy()` stops JavaScript calls, and terminating + // the pool threads removes what would have reported the work finished. + const asyncWorkResult = __drainWasiAsyncWork() + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __cleanUpWasmEnvForWasiDisposal, + ) + } + return __cleanUpWasmEnvForWasiDisposal() +} + +/** + * Disposes this generated WASI binding. + * + * Access this function with: + * binding[Symbol.for('napi.rs.wasi.dispose')]() + */ +function __disposeWasiBinding() { + if (__wasiDisposePromise) { + return __wasiDisposePromise + } + if (__wasiDisposed) { + return Promise.resolve() + } + + let resolveDispose + let rejectDispose + const disposePromise = new Promise((resolve, reject) => { + resolveDispose = resolve + rejectDispose = reject + }) + __wasiDisposePromise = disposePromise + + let result + try { + result = __startWasiDisposal() + } catch (error) { + __wasiDisposePromise = undefined + rejectDispose(error) + return disposePromise + } + + Promise.resolve(result).then( + (value) => { + __wasiDisposed = true + resolveDispose(value) + }, + (error) => { + __wasiDisposePromise = undefined + rejectDispose(error) + }, + ) + return disposePromise +} + +function __publishWasiDispose(exports) { + Object.defineProperty(exports, __wasiDisposeSymbol, { + configurable: false, + enumerable: false, + value: __disposeWasiBinding, + writable: false, + }) +} + +function __finishWasiInitializationRollback(cleanupErrors) { + let workerResult + try { + workerResult = __terminateWasiWorkers() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return cleanupErrors + } + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => cleanupErrors) + } + return cleanupErrors +} + +function __destroyContextForWasiRollback(cleanupErrors) { + let destroyResult + try { + destroyResult = __destroyEmnapiContext() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return __finishWasiInitializationRollback(cleanupErrors) + } + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => __finishWasiInitializationRollback(cleanupErrors)) + } + return __finishWasiInitializationRollback(cleanupErrors) +} + +/** + * Leaves a rollback that could not reach the queued settlements undestroyed, and + * hands it to whatever this flavor has that can still reclaim it. + */ +function __retainFailedWasiRollback(cleanupErrors) { + try { + __retainWasiRollbackForRetry() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + } + return cleanupErrors +} + +/** + * Initialization can fail *after* registration has already run, and registration + * runs with a live environment: a module-init hook can start async work and then + * return an error, and the promise it created may already have escaped into + * JavaScript. The barrier cancels that work and *queues* the settlement, so this + * path needs the same drain the ordinary disposal does — destroying without + * yielding discards the queue with a null env and strands the promise. + * + * Stays synchronous when nothing is queued, which covers every failure before + * `beforeInit`: there is no instance to run the barrier on, so nothing to drain. + * + * A barrier or drain that did *not* finish stops the rollback short of + * destroying, which is what `dispose()` already does — a rejected drain there + * never reaches `__continueWasiDisposal`. Destroying anyway is the worse of the + * two trades, and not because of what it saves: + * + * - It cannot deliver the settlements. `Context.destroy()` runs the + * threadsafe function's cleanup hook, which drains the queue with a null env + * and discards it, so a promise that already escaped into JavaScript hangs + * forever with nothing left that could ever settle it. + * - It saves less than it looks. `Context.destroy()` stops JavaScript calls + * and runs cleanup hooks; it does not free the wasm instance or its Memory, + * which this module's scope holds either way. What stopping short retains is + * the emnapi context's bookkeeping and its un-run cleanup hooks. + * - Retry is not theoretical. A rollback that records a cleanup error is + * already kept in the process-wide registry above, so re-`require()`ing this + * file replays it instead of re-instantiating — and the `6e15de6f` flag fix + * means the replay drains again rather than skipping it. Destroying first is + * what makes that retained record useless. + * + * The residual cost is honest: the CJS flavor hands the context to its + * `process.on('exit')` teardown, so a process that never retries still reclaims + * it on the way out. The ESM browser flavor has no equivalent — a module that + * throws while evaluating is permanently errored, so re-importing rethrows + * without re-running this file — and there the context stays until the realm + * goes away. That is the deliberate choice: a hung promise is a silent liveness + * bug with no upper bound, while the retained bookkeeping is bounded by the page. + */ +function __rollbackWasiInitialization() { + // The environment teardown this rollback performs, kept nested so it cannot + // be reached without the async-work drain below running first. + function __rollbackWasmEnvForWasiInitialization() { + const cleanupErrors = [] + let drainResult + let settlementsUnreached = false + try { + __prepareWasmEnvCleanup() + drainResult = __drainWasmEnvCleanup() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + settlementsUnreached = true + } + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then( + () => __destroyContextForWasiRollback(cleanupErrors), + (cleanupError) => { + cleanupErrors.push(cleanupError) + return __retainFailedWasiRollback(cleanupErrors) + }, + ) + } + if (settlementsUnreached) { + return __retainFailedWasiRollback(cleanupErrors) + } + return __destroyContextForWasiRollback(cleanupErrors) + } + + // Same reason as `__startWasiDisposal`: a module-init hook can start async + // work before the load goes on to fail, and this rollback tears down exactly + // what those completions need. Settle them while everything is still live, + // before the barrier and the teardown above take that away. + // + // A drain that could not finish leaves async work possibly outstanding, and + // destroying the context over it would strand exactly what this rollback is + // there to settle. Stop short and retain instead — the same trade + // `__rollbackWasmEnvForWasiInitialization` makes for the settlement drain, so + // the context stays reclaimable by a retry or by this flavor's own + // last-resort teardown. + const __retainAfterAsyncWorkDrainFailure = (cleanupError) => + __retainFailedWasiRollback([cleanupError]) + let asyncWorkResult + try { + asyncWorkResult = __drainWasiAsyncWork() + } catch (cleanupError) { + return __retainAfterAsyncWorkDrainFailure(cleanupError) + } + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __rollbackWasmEnvForWasiInitialization, + __retainAfterAsyncWorkDrainFailure, + ) + } + return __rollbackWasmEnvForWasiInitialization() +} + +const __wasiRollbackRegistrySymbol = Symbol.for('napi.rs.wasi.rollback.registry.v1') +const __wasiRollbackRegistryKey = + typeof __filename === 'string' ? __filename : __wasmFilePath + +function __getWasiRollbackRegistry() { + const existing = process[__wasiRollbackRegistrySymbol] + if (existing !== undefined) { + if (!(existing instanceof Map)) { + throw new TypeError( + 'The process-wide NAPI-RS WASI rollback registry is invalid', + ) + } + return existing + } + const registry = new Map() + Object.defineProperty(process, __wasiRollbackRegistrySymbol, { + configurable: false, + enumerable: false, + value: registry, + writable: false, + }) + return registry +} + +const __wasiRollbackRegistry = __getWasiRollbackRegistry() + +function __completeWasiInitializationRollback(record, cleanupErrors) { + try { + if (cleanupErrors.length === 0) { + if ( + __wasiRollbackRegistry.get(__wasiRollbackRegistryKey) === record + ) { + __wasiRollbackRegistry.delete(__wasiRollbackRegistryKey) + } + return + } + record.error = __attachCleanupErrors(record.error, cleanupErrors) + } catch (cleanupError) { + try { + record.error = __createCleanupError( + [record.error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + } catch {} + } finally { + record.active = false + record.promise = undefined + } +} + +function __runWasiInitializationRollback(record) { + if (record.active) { + return + } + record.active = true + + let rollbackResult + try { + rollbackResult = record.rollback() + } catch (cleanupError) { + __completeWasiInitializationRollback(record, [cleanupError]) + return + } + + if (!__isThenable(rollbackResult)) { + __completeWasiInitializationRollback(record, rollbackResult) + return + } + + record.promise = Promise.resolve(rollbackResult).then( + (cleanupErrors) => { + __completeWasiInitializationRollback(record, cleanupErrors) + }, + (cleanupError) => { + __completeWasiInitializationRollback(record, [cleanupError]) + }, + ) +} + +const __pendingWasiRollback = __wasiRollbackRegistry.get( + __wasiRollbackRegistryKey, +) +if (__pendingWasiRollback !== undefined) { + __runWasiInitializationRollback(__pendingWasiRollback) + throw __pendingWasiRollback.error +} + +let __wasiModule +let __napiModule +let __wasiExitListenerRegistered = false + +function __removeWasiExitListener() { + if ( + __wasiExitListenerRegistered && + typeof process.removeListener === 'function' + ) { + process.removeListener('exit', __disposeWasiBindingAtExit) + } + __wasiExitListenerRegistered = false +} + +function __disposeWasiBindingAtExit() { + __wasiExitListenerRegistered = false + // An 'exit' handler cannot yield, so it cannot wait for queued promise + // settlements the way __startWasiDisposal does — the process is leaving and + // those promises have no observer left anyway. Run the synchronous teardown + // directly. Every step is idempotent, which also makes this the synchronous + // finish for a disposal that is still waiting for its drain. + try { + __destroyEmnapiContext() + } catch {} + try { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + void Promise.resolve(workerResult).catch(() => {}) + } + } catch {} +} + +function __registerWasiExitListener() { + if ( + !__wasiExitListenerRegistered && + typeof process.once === 'function' + ) { + process.once('exit', __disposeWasiBindingAtExit) + __wasiExitListenerRegistered = true + } +} + +__completeWasiDisposal = __removeWasiExitListener +// A rollback that could not reach the queued settlements keeps the context so +// the registry replay above can retry it. Nothing forces that replay to happen, +// so hand the context to the same synchronous teardown a successful load uses: +// a process that exits without ever retrying still runs the cleanup hooks. The +// handler cannot yield, so it does not settle anything — but by then the process +// is leaving and those promises have no observer left anyway. +__retainWasiRollbackForRetry = __registerWasiExitListener + +function __captureEmnapiAutoDestroyListener() { + if ( + typeof process.prependListener !== 'function' || + typeof process.removeListener !== 'function' + ) { + return + } + let __autoDestroyListener + const __captureListener = (__event, __listener) => { + if (__event === 'beforeExit' && __autoDestroyListener === undefined) { + __autoDestroyListener = __listener + } + } + try { + // Run before existing newListener hooks so a hook that registers its own + // beforeExit listener cannot be mistaken for emnapi's registration. + process.prependListener('newListener', __captureListener) + } catch { + return + } + return () => { + try { + process.removeListener('newListener', __captureListener) + } catch {} + if (__autoDestroyListener !== undefined) { + try { + process.removeListener('beforeExit', __autoDestroyListener) + } catch {} + } + } +} + +try { + const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener() + try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareWasmEnvCleanup, + __isPreparingWasmEnvCleanup, + ) + // emnapi 2.x still registers an unconditional once-listener for + // beforeExit that auto-destroys the context, and suppressDestroy() only + // neutralizes its callback without removing it. This loader owns cleanup + // through its 'exit' listener, so emnapi's listener is captured and + // removed; suppressDestroy() remains the safety net when removal fails. + __emnapiContext.suppressDestroy() + } finally { + // Remove only the exact emnapi callback captured above. + __finishAutoDestroyCapture?.() + } + + ;({ + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, + } = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [ + __captureWasiThreadManager, + __emnapiAsyncWorkPlugin, + __emnapiTSFNPlugin, + ], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + __publishWasiDispose(__napiModule.exports) + // The CommonJS tail below aliases `__napiModule.exports`; a named module + // export does not travel with it, so carry the marker on the binding itself + // too. Three things pin the stamp to exactly this spot: + // - inside this `try`, because the guard throws on a + // `#[napi(module_exports)]` hook that claimed the name, and only the + // catch below tears the environment — context, workers, exit listener — + // back down; + // - after the async runtime host install, which hands this same object to + // addon-provided registration functions that may put anything on it; + // - assigning onto the loader's own `module.exports`, which is still the + // original object here, so an addon accessor with a refusing setter is + // never written through. `cjs-module-lexer` — Node's CJS -> ESM named + // export detection — reads the static `module.exports. =` either + // way, and the later `module.exports = __napiModule.exports` does not + // undo that. + module.exports.__napiBindingTarget = __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) + __registerWasiExitListener() +} catch (error) { + const rollback = { + active: false, + error, + promise: undefined, + rollback: __rollbackWasiInitialization, + } + __wasiRollbackRegistry.set(__wasiRollbackRegistryKey, rollback) + __runWasiInitializationRollback(rollback) + throw rollback.error +} +module.exports = __napiModule.exports +module.exports.Severity = __napiModule.exports.Severity +module.exports.transform = __napiModule.exports.transform +module.exports.transformSync = __napiModule.exports.transformSync diff --git a/napi/transform-react/transform-react.wasip1.d.cts b/napi/transform-react/transform-react.wasip1.d.cts new file mode 100644 index 0000000000..80c6cfceb4 --- /dev/null +++ b/napi/transform-react/transform-react.wasip1.d.cts @@ -0,0 +1,342 @@ +/* auto-generated by NAPI-RS */ +/* eslint-disable */ + +/** The WASI flavor this loader instantiates. */ +export declare const __napiBindingTarget: 'wasm32-wasip1' + +export interface Comment { + type: 'Line' | 'Block' + value: string + start: number + end: number +} + +export interface ErrorLabel { + message: string | null + start: number + end: number +} + +export interface OxcError { + severity: Severity + message: string + labels: Array + helpMessage: string | null + codeframe: string | null +} + +export declare const enum Severity { + Error = 'Error', + Warning = 'Warning', + Advice = 'Advice', +} + +export interface SourceMap { + file?: string + mappings: string + names: Array + sourceRoot?: string + sources: Array + sourcesContent?: Array + version: number + x_google_ignoreList?: Array +} + +/** + * Configure how TSX and JSX are transformed. + * + * @see + */ +export interface JsxOptions { + /** + * Decides which runtime to use. + * + * - 'automatic' - auto-import the correct JSX factories + * - 'classic' - no auto-import + * + * @default 'automatic' + */ + runtime?: 'classic' | 'automatic' + /** + * Emit development-specific information, such as `__source` and `__self`. + * + * @default false + */ + development?: boolean + /** + * Toggles whether or not to throw an error if an XML namespaced tag name + * is used. + * + * Though the JSX spec allows this, it is disabled by default since React's + * JSX does not currently have support for it. + * + * @default true + */ + throwIfNamespace?: boolean + /** + * Mark JSX elements and top-level React method calls as pure for tree shaking. + * + * @default true + */ + pure?: boolean + /** + * Replaces the import source when importing functions. + * + * @default 'react' + */ + importSource?: string + /** + * Replace the function used when compiling JSX expressions. It should be a + * qualified name (e.g. `React.createElement`) or an identifier (e.g. + * `createElement`). + * + * Only used for `classic` {@link runtime}. + * + * @default 'React.createElement' + */ + pragma?: string + /** + * Replace the component used when compiling JSX fragments. It should be a + * valid JSX tag name. + * + * Only used for `classic` {@link runtime}. + * + * @default 'React.Fragment' + */ + pragmaFrag?: string + /** + * Enable React Fast Refresh. + * + * @default false + */ + refresh?: boolean | ReactRefreshOptions +} + +/** Dynamic feature-gating import. */ +export interface ReactCompilerDynamicGating { + source: string +} + +/** + * Partial React Compiler environment configuration. + * + * Unset fields retain compiler defaults. Callback-valued providers and the + * compiler's test-only panic switch are intentionally not exposed. + */ +export interface ReactCompilerEnvironmentOptions { + customMacros?: Array + enableResetCacheOnSourceFileChanges?: boolean + enablePreserveExistingMemoizationGuarantees?: boolean + validatePreserveExistingMemoizationGuarantees?: boolean + /** + * Enable exhaustive manual memo dependency validation. + * + * @default false + */ + validateExhaustiveMemoizationDependencies?: boolean + validateExhaustiveEffectDependencies?: 'off' | 'all' | 'missing-only' | 'extra-only' + enableOptionalDependencies?: boolean + enableNameAnonymousFunctions?: boolean + validateHooksUsage?: boolean + validateRefAccessDuringRender?: boolean + validateNoSetStateInRender?: boolean + enableUseKeyedState?: boolean + validateNoSetStateInEffects?: boolean + validateNoDerivedComputationsInEffects?: boolean + validateNoDerivedComputationsInEffectsExp?: boolean + validateNoJsxInTryStatements?: boolean + validateStaticComponents?: boolean + validateNoCapitalizedCalls?: Array + validateBlocklistedImports?: Array + validateSourceLocations?: boolean + validateNoImpureFunctionsInRender?: boolean + validateNoFreezingKnownMutableFunctions?: boolean + enableAssumeHooksFollowRulesOfReact?: boolean + enableTransitivelyFreezeFunctionExpressions?: boolean + enableFunctionOutlining?: boolean + enableJsxOutlining?: boolean + assertValidMutableRanges?: boolean + enableCustomTypeDefinitionForReanimated?: boolean + enableTreatRefLikeIdentifiersAsRefs?: boolean + enableTreatSetIdentifiersAsStateSetters?: boolean + /** + * Validate that `useMemo` callbacks return a value. + * + * @default false + */ + validateNoVoidUseMemo?: boolean + enableAllowSetStateFromRefsInEffects?: boolean + enableVerboseNoSetStateInEffect?: boolean + enableForest?: boolean +} + +/** Static feature-gating import. */ +export interface ReactCompilerGating { + source: string + importSpecifierName: string +} + +/** Meta-internal React runtime target. */ +export interface ReactCompilerMetaTarget { + kind: 'donotuse_meta_internal' + runtimeModule?: string +} + +/** + * React Compiler options. + * + * Fields mirror `babel-plugin-react-compiler` and `react-compiler-napi`. + */ +export interface ReactCompilerOptions { + /** + * Which functions the compiler attempts to compile. + * + * @default 'infer' + */ + compilationMode?: 'infer' | 'syntax' | 'annotation' | 'all' + /** + * When compiler diagnostics escalate into a hard failure. + * + * @default 'none' + */ + panicThreshold?: 'none' | 'critical_errors' | 'all_errors' + /** + * React runtime target. React 17 and 18 use `react-compiler-runtime`; + * React 19 uses `react/compiler-runtime`. + * + * @default '19' + */ + target?: '17' | '18' | '19' | ReactCompilerMetaTarget + /** Emit both compiled and original functions behind an imported feature gate. */ + gating?: ReactCompilerGating + /** Enable `"use memo if(...)"` directive-driven gating. */ + dynamicGating?: ReactCompilerDynamicGating + /** + * Analyze and report diagnostics without applying compiler output. + * + * @deprecated Prefer `outputMode: "lint"`. + * @default false + */ + noEmit?: boolean + /** Select client, SSR, or lint output. */ + outputMode?: 'client' | 'ssr' | 'lint' + /** + * ESLint rule names whose suppressions opt a function out of compilation. + * Defaults to `react-hooks/exhaustive-deps` and `react-hooks/rules-of-hooks`; + * pass an empty array to disable this behavior. + */ + eslintSuppressionRules?: Array + /** + * Treat Flow suppression comments as opt-outs. + * + * @default true + */ + flowSuppressions?: boolean + /** + * Compile functions carrying `"use no memo"` or `"use no forget"`. + * + * @default false + */ + ignoreUseNoForget?: boolean + /** Additional directives that opt a function out of compilation. */ + customOptOutDirectives?: Array + /** + * Only run the React Compiler when the filename contains one of these strings. + * + * By default, files whose filename contains `node_modules` are skipped. + * Providing this option replaces that default filter. + * + * Function-valued `sources` filters from the Babel plugin are intentionally + * unsupported across the native boundary. + */ + sources?: Array + /** Feature flags and validation settings for compiler passes. */ + environment?: ReactCompilerEnvironmentOptions +} + +/** React Fast Refresh options. */ +export interface ReactRefreshOptions { + /** + * Specify the identifier of the refresh registration variable. + * + * @default `$RefreshReg$` + */ + refreshReg?: string + /** + * Specify the identifier of the refresh signature variable. + * + * @default `$RefreshSig$` + */ + refreshSig?: string + /** + * Emit full hook signatures instead of compact hashes. + * + * @default false + */ + emitFullSignatures?: boolean +} + +/** + * Compile a JavaScript or TypeScript React module asynchronously. + * + * This uses a worker-pool thread and can be slower than `transformSync` for a + * single small module. + */ +export declare function transform(filename: string, sourceText: string, options?: TransformOptions | undefined | null): Promise + +/** + * Options for compiling a JavaScript or TypeScript React module. + * + * `lang`, `sourceType`, and `sourcemap` configure the surrounding Oxc + * parse/codegen pipeline. React Compiler and JSX transforms are configured + * independently. + */ +export interface TransformOptions { + /** Treat the source as `js`, `jsx`, `ts`, `tsx`, or `dts`. */ + lang?: 'js' | 'jsx' | 'ts' | 'tsx' | 'dts' + /** Treat the source as script, module, CommonJS, or infer it from syntax. */ + sourceType?: 'script' | 'module' | 'commonjs' | 'unambiguous' + /** + * Generate a source map. + * + * @default false + */ + sourcemap?: boolean + /** + * Configure how TSX and JSX are transformed, or preserve JSX syntax. + * + * @see + */ + jsx?: 'preserve' | JsxOptions + /** + * Configure React Compiler, or disable it with `false`. + * + * @default true + */ + reactCompiler?: boolean | ReactCompilerOptions +} + +/** Result returned by the React Compiler transform. */ +export interface TransformResult { + /** Whether the transform was aborted without emitting code. */ + fatal: boolean + /** + * Transformed JavaScript code. + * + * This is empty when parsing, semantic analysis, option validation, or the + * React Compiler reports a fatal error. + */ + code: string + /** Source map, populated when `sourcemap` is `true`. */ + map?: SourceMap + /** Parse, semantic, downstream transform, and fatal React Compiler diagnostics. */ + errors: Array +} + +/** + * Compile a JavaScript or TypeScript React module synchronously. + * + * The React Compiler runs first on the pristine AST. TypeScript syntax is + * removed and configured JSX transforms run afterwards. + */ +export declare function transformSync(filename: string, sourceText: string, options?: TransformOptions | undefined | null): TransformResult diff --git a/napi/transform-relay/browser.js b/napi/transform-relay/browser.js index 7626a56fae..c2b37f7b7b 100644 --- a/napi/transform-relay/browser.js +++ b/napi/transform-relay/browser.js @@ -1 +1 @@ -export * from '@oxc-transform-relay/binding-wasm32-wasi' +export * from '@oxc-transform-relay/binding-wasm32-wasip1' diff --git a/napi/transform-relay/index.js b/napi/transform-relay/index.js index 178a82de3c..2a06f6c0c4 100644 --- a/napi/transform-relay/index.js +++ b/napi/transform-relay/index.js @@ -565,7 +565,7 @@ function createLoadErrorChain(errors) { // // NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict // WASI loading. It never crosses into another flavor or falls back to native. -const __napiWasiFlavors = ['wasm32-wasi'] +const __napiWasiFlavors = ['wasm32-wasi', 'wasm32-wasip1'] const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR const __napiWasiFlavorRequested = typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0 @@ -655,6 +655,27 @@ if (!nativeBinding || forceWasi) { loadErrors.push(candidateError) } } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasip1')) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('./transform-relay.wasip1.cjs', false, ['./transform-relay.wasm32-wasip1.debug.wasm', './transform-relay.wasm32-wasip1.wasm']) + candidateFailed = candidateError !== null + if (!candidateFailed) { + wasiBinding = require('./transform-relay.wasip1.cjs') + nativeBinding = wasiBinding + __napiLoadedBindingTarget = 'wasm32-wasip1' + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasi')) { let candidateError = null let candidateFailed = false @@ -682,6 +703,33 @@ if (!nativeBinding || forceWasi) { loadErrors.push(candidateError) } } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasip1')) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('@oxc-transform-relay/binding-wasm32-wasip1', true, undefined) + candidateFailed = candidateError !== null + if (!candidateFailed) { + if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + const bindingPackageVersion = require('@oxc-transform-relay/binding-wasm32-wasip1/package.json').version + if (bindingPackageVersion !== '0.151.0') { + throw new Error(`WASI binding package version mismatch, expected 0.151.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + } + wasiBinding = require('@oxc-transform-relay/binding-wasm32-wasip1') + nativeBinding = wasiBinding + __napiLoadedBindingTarget = 'wasm32-wasip1' + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } if ( !wasiBindingLoaded && forceWasi && diff --git a/napi/transform-relay/package.json b/napi/transform-relay/package.json index e5e19a4bda..0dbbad59d5 100644 --- a/napi/transform-relay/package.json +++ b/napi/transform-relay/package.json @@ -44,6 +44,8 @@ "postbuild-dev": "node scripts/patch.js", "build-wasm": "pnpm run build-wasm-dev --release", "build-wasm-dev": "pnpm run build-dev --target wasm32-wasip1-threads --dts transform-relay.wasi.d.cts", + "build-wasip1": "pnpm run build-wasip1-dev --release", + "build-wasip1-dev": "pnpm run build-dev --target wasm32-wasip1 --dts transform-relay.wasip1.d.cts", "test": "vitest run --dir ./test" }, "devDependencies": { @@ -72,6 +74,7 @@ "riscv64gc-unknown-linux-gnu", "riscv64gc-unknown-linux-musl", "s390x-unknown-linux-gnu", + "wasm32-wasip1", "wasm32-wasip1-threads", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", diff --git a/napi/transform-relay/scripts/patch.js b/napi/transform-relay/scripts/patch.js index d7a9c18547..6debedee97 100644 --- a/napi/transform-relay/scripts/patch.js +++ b/napi/transform-relay/scripts/patch.js @@ -27,3 +27,7 @@ fs.writeFileSync(path, data); const workerPath = pathJoin(packageDir, "wasi-worker-browser.mjs"); const worker = fs.readFileSync(workerPath, "utf-8").replaceAll(/[ \t]+$/gmu, ""); fs.writeFileSync(workerPath, worker); + +const wasip1BrowserPath = pathJoin(packageDir, "transform-relay.wasip1-browser.js"); +const wasip1Browser = fs.readFileSync(wasip1BrowserPath, "utf-8").replaceAll(/[ \t]+$/gmu, ""); +fs.writeFileSync(wasip1BrowserPath, wasip1Browser); diff --git a/napi/transform-relay/transform-relay.wasip1-browser.js b/napi/transform-relay/transform-relay.wasip1-browser.js new file mode 100644 index 0000000000..c3cecf2c02 --- /dev/null +++ b/napi/transform-relay/transform-relay.wasip1-browser.js @@ -0,0 +1,989 @@ +import { + emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin as __emnapiTSFNPlugin, + instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync, + WASI as __WASI, +} from '@napi-rs/wasm-runtime' +import { createContext as __emnapiCreateContext } from '@emnapi/runtime' + + +export const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +const __wasi = new __WASI({ + version: 'preview1', +}) + +const __wasmUrl = new URL('./transform-relay.wasm32-wasip1.wasm', import.meta.url).href +const __wasmResponse = await globalThis.fetch(__wasmUrl) +if (!__wasmResponse.ok) { + throw new Error( + 'Failed to fetch WASI module ' + + __wasmUrl + + ': ' + + __wasmResponse.status + + ' ' + + (__wasmResponse.statusText || 'Unknown Status'), + ) +} +const __wasmFile = await __wasmResponse.arrayBuffer() + +const __wasmMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, +}) +let __emnapiContext + +const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose') +const __wasiWorkers = new Set() +// The thread manager has to be reachable *before* anything that can throw +// during load or registration. Initialization can fail after the pool has +// already spawned workers, and the rollback still has to mark their +// terminations as expected — but `__napiModule` is assigned only when +// instantiation RETURNS, so on exactly that path it is still undefined. A +// plugin factory runs while the emnapi module is being created, before the +// wasm is loaded and before any registration function runs, and its context +// carries the very same manager instance. +let __wasiThreadManager + +function __captureWasiThreadManager(context) { + if (context && context.PThread) { + __wasiThreadManager = context.PThread + } + return {} +} + +function __getWasiThreadManager() { + const manager = + __wasiThreadManager !== undefined + ? __wasiThreadManager + : __napiModule + ? __napiModule.PThread + : undefined + if (manager && typeof manager.terminateWorker === 'function') { + return manager + } + return undefined +} +let __napiInstance +let __emnapiContextDestroyed = false +let __emnapiContextDestroyPromise +let __emnapiWasmEnvCleanupPrepared = false +let __emnapiWasmEnvCleanupPreparing = false +let __emnapiWasmEnvCleanupRan = false +let __emnapiWasmEnvCleanupDrained = false +let __emnapiWasmEnvCleanupDrainPromise +let __wasiDisposed = false +let __wasiAsyncWorkDrainPromise +let __wasiDisposePromise +let __completeWasiDisposal = function () {} +// Overridden by loader flavors that have a last-resort reclaim for a rollback +// that stopped short of destroying the context. See +// `__rollbackWasiInitialization`. +let __retainWasiRollbackForRetry = function () {} + +function __isThenable(value) { + return ( + value !== null && + (typeof value === 'object' || typeof value === 'function') && + typeof value.then === 'function' + ) +} + +function __createCleanupError(errors, message) { + if (errors.length === 1) { + return errors[0] + } + const __AggregateError = globalThis.AggregateError + if (typeof __AggregateError === 'function') { + return new __AggregateError(errors, message) + } + const error = new Error(message) + error.errors = errors + return error +} + +function __attachCleanupErrors(error, cleanupErrors) { + if (cleanupErrors.length === 0) { + return error + } + const cleanupError = __createCleanupError( + cleanupErrors, + 'WASI binding cleanup failed', + ) + try { + if ( + error && + (typeof error === 'object' || typeof error === 'function') + ) { + if (error.cause === undefined) { + error.cause = cleanupError + if (error.cause === cleanupError) { + return error + } + } + if (Array.isArray(error.cleanupErrors)) { + error.cleanupErrors.push(cleanupError) + return error + } else { + const attachedCleanupErrors = [cleanupError] + error.cleanupErrors = attachedCleanupErrors + if (error.cleanupErrors === attachedCleanupErrors) { + return error + } + } + } + } catch {} + const aggregate = __createCleanupError( + [error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + try { + aggregate.cause = error + } catch {} + return aggregate +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __isPreparingWasmEnvCleanup() { + return __emnapiWasmEnvCleanupPreparing +} + +function __prepareWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) { + return + } + const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup + if (typeof prepare === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __emnapiWasmEnvCleanupPreparing = true + try { + prepare() + } finally { + __emnapiWasmEnvCleanupPreparing = false + } + __emnapiWasmEnvCleanupRan = true + } + __emnapiWasmEnvCleanupPrepared = true +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (callback) { + setImmediate(callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (callback) { + const channel = new __MessageChannel() + channel.port1.onmessage = function () { + channel.port1.onmessage = null + try { + channel.port1.close() + } catch {} + try { + channel.port2.close() + } catch {} + callback() + } + channel.port2.postMessage(null) + } + } + return function (callback) { + setTimeout(callback, 0) + } +})() + +// A real, *referenced* timer, for waits that must let the whole host make +// progress between looks — the async-work drain polls the addon rather than +// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there +// would spin the loop instead of yielding it. Falls back to the macrotask +// scheduler on a host without timers. +function __scheduleTimer(callback, delay) { + const setTimer = globalThis.setTimeout + if (typeof setTimer !== 'function') { + __scheduleMacrotask(callback) + return + } + try { + setTimer(callback, delay) + } catch { + __scheduleMacrotask(callback) + } +} + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + * + * The "already drained" flag is set only once a wait has actually finished. + * Scheduling a macrotask can fail — a host-provided or patched `setImmediate` + * that throws is enough — and a disposal that rejects stays retryable, so + * marking the drain complete up front would make the retry skip it and destroy + * the context with the barrier's settlements still queued. + * + * A wait that runs out of turns with the counter still nonzero rejects with + * `ERR_NAPI_WASI_CLEANUP_PENDING` for the same reason: at that point + * "finished" is indistinguishable from the stranding above, and destroying + * would discard the very settlement the wait was for. The rejection leaves the + * flag unset and disposal retryable. + */ +function __drainWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupDrained || !__emnapiWasmEnvCleanupRan) { + return + } + if (__emnapiWasmEnvCleanupDrainPromise) { + return __emnapiWasmEnvCleanupDrainPromise + } + const pending = __napiInstance?.exports?.napi_wasm_env_cleanup_pending + const observable = typeof pending === 'function' + if (observable) { + let queued + try { + queued = pending() + } catch { + __emnapiWasmEnvCleanupDrained = true + return + } + if (!queued) { + __emnapiWasmEnvCleanupDrained = true + return + } + } + const limit = observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + const drainPromise = (async () => { + let queued = 0 + for (let turn = 0; turn < limit; turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!observable) { + continue + } + try { + queued = pending() + } catch { + return + } + if (!queued) { + return + } + } + if (!observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: the drained flag stays + // unset, dispose() (and the rollback) decline to destroy, and a later + // dispose() runs the drain again — by which time the queue has usually been + // delivered. A counter that is somehow stuck nonzero therefore costs each + // attempt at most another bounded wait and a rejection, never a stranded + // promise; the process-exit teardown still reclaims the context. + const drainError = new Error( + 'the wasm environment still reports ' + + queued + + ' queued settlement(s) after ' + + limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw drainError + })().then( + (value) => { + // Set only when the wait actually finished AND the queue was seen empty + // (or is unobservable): a drain that timed out with settlements still + // queued rejects above and must stay repeatable. + __emnapiWasmEnvCleanupDrained = true + __emnapiWasmEnvCleanupDrainPromise = undefined + return value + }, + (error) => { + __emnapiWasmEnvCleanupDrainPromise = undefined + throw error + }, + ) + __emnapiWasmEnvCleanupDrainPromise = drainPromise + return drainPromise +} + +function __destroyEmnapiContext() { + if (__emnapiContextDestroyed || __emnapiContext === undefined) { + __emnapiContextDestroyed = true + return + } + if (__emnapiContextDestroyPromise) { + return __emnapiContextDestroyPromise + } + + __prepareWasmEnvCleanup() + const result = __emnapiContext.destroy() + if (!__isThenable(result)) { + __emnapiContextDestroyed = true + return + } + + const destroyPromise = Promise.resolve(result).then( + (value) => { + __emnapiContextDestroyed = true + return value + }, + (error) => { + __emnapiContextDestroyPromise = undefined + throw error + }, + ) + __emnapiContextDestroyPromise = destroyPromise + return destroyPromise +} + +/** + * Holds the event loop open until `work` settles. + * + * Nothing else can: the pool workers are deliberately unreferenced so an idle + * binding cannot keep a process alive, and referencing them again for the + * termination does not hold either — emnapi unreferences a worker the moment it + * reports `async-thread-ready`, which for a worker that was still starting + * lands *after* the termination began. Without a handle of its own, an + * `await dispose()` with nothing else pending exits the process with its + * promise unsettled, and everything after the `await` is skipped. + * + * The timer is cleared as soon as the work settles, so this never outlives the + * disposal that asked for it. + */ +function __keepEventLoopAliveUntil(work) { + const setTimer = globalThis.setInterval + const clearTimer = globalThis.clearInterval + if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') { + return work + } + let timer + try { + timer = setTimer(function () {}, 50) + } catch { + return work + } + const release = function () { + try { + clearTimer(timer) + } catch {} + } + return work.then( + (value) => { + release() + return value + }, + (error) => { + release() + throw error + }, + ) +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this addon's outstanding `napi_async_work` before the teardown that + * would strand it. + * + * `napi_prepare_wasm_env_cleanup` does not cover async work, and nothing about + * it is observable from JavaScript: the threadless archive resolves + * `napi_*_async_work` through the `@emnapi/core` plugins, but the threaded one + * links the C `async_work.c` on the uv threadpool, so there the wasm neither + * imports nor exports those symbols and the only brackets a loader could watch + * (`_emnapi_ctx_*_waiting_request_counter`) are shared with threadsafe + * functions. The addon is the one place both flavors go through, so it answers + * for both, through the same kind of handshake the settlement drain uses: + * + * - `napi_wasm_cancel_pending_async_work()` cancels what no thread has + * started. Those completion callbacks run with `napi_cancelled`, which + * napi-rs turns into a promise rejected with an `AbortError`. + * - `napi_wasm_async_work_pending()` counts what is still owed a completion + * callback. Work already executing refuses cancellation and stays counted + * until it finishes normally — which it can, because this runs before the + * barrier, before `Context.destroy()` and before anything is terminated. + * + * Both exports are optional: an addon built against a napi crate that predates + * them drains nothing and keeps the previous behavior, exactly as the + * `napi_wasm_env_cleanup_pending` handshake degrades. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. The promise it returns otherwise never + * rejects. + * + * The wait has no deadline, and that is the point: giving up would destroy the + * environment with a completion callback still owed, which is the stranding + * this exists to prevent. A task whose `execute` never returns already keeps an + * *undisposed* process alive in exactly the same way, so disposal inherits that + * rather than inventing a bound it cannot honor. + * + * Safe to call from inside a completion callback, which is reachable: settling + * a task runs addon code that can re-enter JavaScript — a setter on the value + * being handed back, a threadsafe-function callback — and that JavaScript can + * call `dispose()`. Two things make it terminate rather than wait on itself: + * + * - The addon keeps a work registered until its completion callback + * *finishes*, so the count read here is at least one and this takes the + * polling path instead of declaring the environment drained and tearing it + * down from inside the frame that is still settling a promise. + * - The poll is a timer, so it cannot run until the callback has returned to + * the host — by which time that work has left the registry. The count the + * next poll reads is the one taken after the callback finished. + * + * `__disposeWasiBinding` hands every caller the same in-flight promise, so the + * nested call joins this disposal rather than starting a second one. + */ +function __drainWasiAsyncWork() { + if (__wasiAsyncWorkDrainPromise !== undefined) { + return __wasiAsyncWorkDrainPromise + } + const exports = __napiInstance?.exports + const pending = exports?.napi_wasm_async_work_pending + const cancelPending = exports?.napi_wasm_cancel_pending_async_work + if (typeof pending !== 'function' || typeof cancelPending !== 'function') { + return + } + + const readPending = () => { + try { + return pending() + } catch (error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition — + // there is nothing left to wait for, and refusing to dispose would only + // keep a dead instance and its stuck counter alive. Best-effort here is + // the honest answer, and it is what disposal did before this drain + // existed. + // + // Only a trap. Anything else means the export is not what this loader + // thinks it is, which is a defect worth surfacing rather than disposing + // over. + if (error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw error + } + } + + if (!readPending()) { + return + } + try { + cancelPending() + } catch { + // Cancellation is an optimization: it bounds the wait by the work already + // executing. Failing it only means waiting for the whole queue instead. + } + if (!readPending()) { + return + } + + const drainPromise = __keepEventLoopAliveUntil( + (async () => { + while (readPending()) { + await new Promise((resolve) => { + __scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })(), + ).then( + () => { + __wasiAsyncWorkDrainPromise = undefined + }, + (error) => { + // A wait that could not run is not a wait that finished. The only way + // here is a host whose timers and macrotask primitives all refuse, and + // the work is still outstanding — reporting success would destroy the + // environment over it, which is the stranding this exists to prevent. + // Reject instead: disposal stays retryable, and the context is not + // destroyed. Clearing the memo first is what makes the retry re-run this. + __wasiAsyncWorkDrainPromise = undefined + throw error + }, + ) + __wasiAsyncWorkDrainPromise = drainPromise + return drainPromise +} + +/** + * `@emnapi/wasi-threads` counts a worker exit as expected only when its own + * thread manager performed the termination. A bare `worker.terminate()` reaches + * the manager's `exit` listener instead, which reports + * `worker (tid = N) sent an error! ... stopped with exit code 1` and rethrows + * inside the emit — aborting the `once('exit')` that backs the terminate + * promise, so disposal never settles and the process dies with an uncaught + * exception. Mark the termination through the manager first. + * + * The manager comes from `__getWasiThreadManager`, not from `__napiModule`: + * the initialization rollback runs on the one path where instantiation never + * returned, so `__napiModule` is still undefined there while the workers it + * spawned are already registered and loaded. + * + * Not `terminateAllThreads()`: that one recreates the pool it just shut down. + */ +function __terminateWasiWorkers() { + const cleanupErrors = [] + const pending = [] + const threadManager = __getWasiThreadManager() + + for (const worker of __wasiWorkers) { + let result + try { + if (threadManager) { + threadManager.terminateWorker(worker) + // `terminateWorker` leaves behind a reporter that logs every message + // still queued on the port, which Node flushes on exit. Nothing is + // listening for those any more. + worker.onmessage = undefined + } + result = worker.terminate() + } catch (error) { + cleanupErrors.push(error) + continue + } + if (__isThenable(result)) { + pending.push( + Promise.resolve(result).then( + () => { + __wasiWorkers.delete(worker) + }, + (error) => { + cleanupErrors.push(error) + }, + ), + ) + } else { + __wasiWorkers.delete(worker) + } + } + + const finish = () => { + if (cleanupErrors.length > 0) { + throw __createCleanupError( + cleanupErrors, + 'Failed to terminate WASI workers', + ) + } + } + return pending.length > 0 + ? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish) + : finish() +} + +function __finishWasiDisposal() { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult).then(__completeWasiDisposal) + } + return __completeWasiDisposal() +} + +function __continueWasiDisposal() { + const destroyResult = __destroyEmnapiContext() + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult).then(__finishWasiDisposal) + } + return __finishWasiDisposal() +} + +function __cleanUpWasmEnvForWasiDisposal() { + // Run the pre-teardown barrier, then let the settlements it queued actually + // reach JavaScript, and only then destroy the environment. Doing these two + // back to back is what strands them. + __prepareWasmEnvCleanup() + const drainResult = __drainWasmEnvCleanup() + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then(__continueWasiDisposal) + } + return __continueWasiDisposal() +} + +function __startWasiDisposal() { + // Outstanding `napi_async_work` goes first, while the environment is still + // completely live: the completion callbacks run addon code, and everything + // after this point takes that away from them — the barrier shuts the async + // runtime down, `Context.destroy()` stops JavaScript calls, and terminating + // the pool threads removes what would have reported the work finished. + const asyncWorkResult = __drainWasiAsyncWork() + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __cleanUpWasmEnvForWasiDisposal, + ) + } + return __cleanUpWasmEnvForWasiDisposal() +} + +/** + * Disposes this generated WASI binding. + * + * Access this function with: + * binding[Symbol.for('napi.rs.wasi.dispose')]() + */ +function __disposeWasiBinding() { + if (__wasiDisposePromise) { + return __wasiDisposePromise + } + if (__wasiDisposed) { + return Promise.resolve() + } + + let resolveDispose + let rejectDispose + const disposePromise = new Promise((resolve, reject) => { + resolveDispose = resolve + rejectDispose = reject + }) + __wasiDisposePromise = disposePromise + + let result + try { + result = __startWasiDisposal() + } catch (error) { + __wasiDisposePromise = undefined + rejectDispose(error) + return disposePromise + } + + Promise.resolve(result).then( + (value) => { + __wasiDisposed = true + resolveDispose(value) + }, + (error) => { + __wasiDisposePromise = undefined + rejectDispose(error) + }, + ) + return disposePromise +} + +function __publishWasiDispose(exports) { + Object.defineProperty(exports, __wasiDisposeSymbol, { + configurable: false, + enumerable: false, + value: __disposeWasiBinding, + writable: false, + }) +} + +function __finishWasiInitializationRollback(cleanupErrors) { + let workerResult + try { + workerResult = __terminateWasiWorkers() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return cleanupErrors + } + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => cleanupErrors) + } + return cleanupErrors +} + +function __destroyContextForWasiRollback(cleanupErrors) { + let destroyResult + try { + destroyResult = __destroyEmnapiContext() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return __finishWasiInitializationRollback(cleanupErrors) + } + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => __finishWasiInitializationRollback(cleanupErrors)) + } + return __finishWasiInitializationRollback(cleanupErrors) +} + +/** + * Leaves a rollback that could not reach the queued settlements undestroyed, and + * hands it to whatever this flavor has that can still reclaim it. + */ +function __retainFailedWasiRollback(cleanupErrors) { + try { + __retainWasiRollbackForRetry() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + } + return cleanupErrors +} + +/** + * Initialization can fail *after* registration has already run, and registration + * runs with a live environment: a module-init hook can start async work and then + * return an error, and the promise it created may already have escaped into + * JavaScript. The barrier cancels that work and *queues* the settlement, so this + * path needs the same drain the ordinary disposal does — destroying without + * yielding discards the queue with a null env and strands the promise. + * + * Stays synchronous when nothing is queued, which covers every failure before + * `beforeInit`: there is no instance to run the barrier on, so nothing to drain. + * + * A barrier or drain that did *not* finish stops the rollback short of + * destroying, which is what `dispose()` already does — a rejected drain there + * never reaches `__continueWasiDisposal`. Destroying anyway is the worse of the + * two trades, and not because of what it saves: + * + * - It cannot deliver the settlements. `Context.destroy()` runs the + * threadsafe function's cleanup hook, which drains the queue with a null env + * and discards it, so a promise that already escaped into JavaScript hangs + * forever with nothing left that could ever settle it. + * - It saves less than it looks. `Context.destroy()` stops JavaScript calls + * and runs cleanup hooks; it does not free the wasm instance or its Memory, + * which this module's scope holds either way. What stopping short retains is + * the emnapi context's bookkeeping and its un-run cleanup hooks. + * - Retry is not theoretical. A rollback that records a cleanup error is + * already kept in the process-wide registry above, so re-`require()`ing this + * file replays it instead of re-instantiating — and the `6e15de6f` flag fix + * means the replay drains again rather than skipping it. Destroying first is + * what makes that retained record useless. + * + * The residual cost is honest: the CJS flavor hands the context to its + * `process.on('exit')` teardown, so a process that never retries still reclaims + * it on the way out. The ESM browser flavor has no equivalent — a module that + * throws while evaluating is permanently errored, so re-importing rethrows + * without re-running this file — and there the context stays until the realm + * goes away. That is the deliberate choice: a hung promise is a silent liveness + * bug with no upper bound, while the retained bookkeeping is bounded by the page. + */ +function __rollbackWasiInitialization() { + // The environment teardown this rollback performs, kept nested so it cannot + // be reached without the async-work drain below running first. + function __rollbackWasmEnvForWasiInitialization() { + const cleanupErrors = [] + let drainResult + let settlementsUnreached = false + try { + __prepareWasmEnvCleanup() + drainResult = __drainWasmEnvCleanup() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + settlementsUnreached = true + } + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then( + () => __destroyContextForWasiRollback(cleanupErrors), + (cleanupError) => { + cleanupErrors.push(cleanupError) + return __retainFailedWasiRollback(cleanupErrors) + }, + ) + } + if (settlementsUnreached) { + return __retainFailedWasiRollback(cleanupErrors) + } + return __destroyContextForWasiRollback(cleanupErrors) + } + + // Same reason as `__startWasiDisposal`: a module-init hook can start async + // work before the load goes on to fail, and this rollback tears down exactly + // what those completions need. Settle them while everything is still live, + // before the barrier and the teardown above take that away. + // + // A drain that could not finish leaves async work possibly outstanding, and + // destroying the context over it would strand exactly what this rollback is + // there to settle. Stop short and retain instead — the same trade + // `__rollbackWasmEnvForWasiInitialization` makes for the settlement drain, so + // the context stays reclaimable by a retry or by this flavor's own + // last-resort teardown. + const __retainAfterAsyncWorkDrainFailure = (cleanupError) => + __retainFailedWasiRollback([cleanupError]) + let asyncWorkResult + try { + asyncWorkResult = __drainWasiAsyncWork() + } catch (cleanupError) { + return __retainAfterAsyncWorkDrainFailure(cleanupError) + } + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __rollbackWasmEnvForWasiInitialization, + __retainAfterAsyncWorkDrainFailure, + ) + } + return __rollbackWasmEnvForWasiInitialization() +} + +let __wasiModule +let __napiModule + +try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareWasmEnvCleanup, + __isPreparingWasmEnvCleanup, + ) + __emnapiContext.suppressDestroy() + + ;({ + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, + } = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [ + __captureWasiThreadManager, + __emnapiAsyncWorkPlugin, + __emnapiTSFNPlugin, + ], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + __publishWasiDispose(__napiModule.exports) + // The default export hands out this object; a named module export does not + // travel with it, so carry the marker on the binding itself too. After the + // host install, which hands the same object to addon-provided registration + // functions that may put anything on it, and inside this `try`, so a claimed + // name fails the load through the rollback below rather than past it. + __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) +} catch (error) { + const cleanupErrors = await __rollbackWasiInitialization() + throw __attachCleanupErrors(error, cleanupErrors) +} +export default __napiModule.exports +export const Severity = __napiModule.exports.Severity +export const transform = __napiModule.exports.transform +export const transformSync = __napiModule.exports.transformSync diff --git a/napi/transform-relay/transform-relay.wasip1-deferred.d.ts b/napi/transform-relay/transform-relay.wasip1-deferred.d.ts new file mode 100644 index 0000000000..1e91a1ffbe --- /dev/null +++ b/napi/transform-relay/transform-relay.wasip1-deferred.d.ts @@ -0,0 +1,83 @@ +export type WasiBinding = typeof import('./transform-relay.wasip1.cjs') + +export type WasiModuleInput = + | WebAssembly.Module + | PromiseLike + +/** Run the instance on a linear memory the caller allocated. */ +export interface WasiCallerMemoryOptions { + /** + * A caller-allocated linear memory for this instance. It must be unshared + * and created in this loader's own realm — the WASI and emnapi layers + * underneath identify a Memory with a realm-local `instanceof`, so one from + * a `node:vm` context or another frame is rejected. It is single-use: once + * a validated initialization attempt has begun, the same Memory cannot be + * passed again — including after that attempt failed, and after the instance + * was disposed. + */ + memory: WebAssembly.Memory + /** Not available beside `memory`: the loader allocates neither. */ + initialMemoryPages?: never + /** Not available beside `memory`: the loader allocates neither. */ + maximumMemoryPages?: never +} + +/** Let the loader allocate the linear memory, optionally sized. */ +export interface WasiAllocatedMemoryOptions { + /** Not available beside the page counts: they size the loader's own Memory. */ + memory?: never + /** @default WASM_MEMORY.initialPages */ + initialMemoryPages?: number + /** @default WASM_MEMORY.maximumPages */ + maximumMemoryPages?: number +} + +/** + * Either memory form, never a mix of the two: the loader throws a TypeError + * on `memory` beside a page count. `{}` and an omitted argument select the + * loader defaults. + */ +export type WasiInstanceOptions = + | WasiCallerMemoryOptions + | WasiAllocatedMemoryOptions + +export interface WasiRuntimeStats { + /** Instances created by this evaluated loader module, not process-wide. */ + createdInstances: number + /** Created instances whose dispose() has not completed. */ + liveInstances: number + /** Declared initial address space, not committed memory. */ + declaredInitialMemoryBytes: number +} + +export interface WasiInstance { + readonly exports: WasiBinding + /** This instance's linear memory. Claimed, so it cannot start another one. */ + readonly memory: WebAssembly.Memory + /** Current linear-memory size; 0 once dispose() has completed. */ + readonly memoryBytes: number + readonly disposed: boolean + dispose(): Promise +} + +/** The memory descriptor compiled into this loader. */ +export const WASM_MEMORY: Readonly<{ + initialPages: number + maximumPages: number + pageBytes: number + initialBytes: number + maximumBytes: number +}> + +export function getDeferredRuntimeStats(): Readonly + +export function instantiate(wasmInput: WasiModuleInput): Promise +export function createInstance( + wasmInput: WasiModuleInput, + options?: WasiInstanceOptions, +): Promise +/** Dispose the singleton and retry retained failed-initialization cleanup. */ +export function dispose(): Promise + +/** The WASI flavor this deferred loader instantiates. */ +export declare const __napiBindingTarget: 'wasm32-wasip1' diff --git a/napi/transform-relay/transform-relay.wasip1-deferred.js b/napi/transform-relay/transform-relay.wasip1-deferred.js new file mode 100644 index 0000000000..11be048925 --- /dev/null +++ b/napi/transform-relay/transform-relay.wasip1-deferred.js @@ -0,0 +1,1488 @@ +import { + emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin as __emnapiTSFNPlugin, + instantiateNapiModule as __emnapiInstantiateNapiModule, + WASI as __WASI, +} from '@napi-rs/wasm-runtime' +import { createContext as __emnapiCreateContext } from '@emnapi/runtime' + + +export const WASM_MEMORY = Object.freeze({ + initialPages: 1024, + maximumPages: 65536, + pageBytes: 65536, + initialBytes: 1024 * 65536, + maximumBytes: 65536 * 65536, +}) + +let __createdInstances = 0 +let __liveInstances = 0 + +/** + * Counters for instances created by THIS module evaluation, not process-wide: + * a second bundled copy of this loader keeps its own. Only successfully + * created instances are counted, and `liveInstances` drops when an instance's + * `dispose()` resolves. + * + * `declaredInitialMemoryBytes` is declared address space, not a host's + * committed-memory metric; pair it with host telemetry rather than treating it + * as a quota. + */ +export function getDeferredRuntimeStats() { + return Object.freeze({ + createdInstances: __createdInstances, + liveInstances: __liveInstances, + declaredInitialMemoryBytes: WASM_MEMORY.initialBytes, + }) +} + +const __arrayBufferByteLengthGetter = Object.getOwnPropertyDescriptor( + ArrayBuffer.prototype, + 'byteLength', +).get +const __memoryBufferGetter = Object.getOwnPropertyDescriptor( + WebAssembly.Memory.prototype, + 'buffer', +).get +// One managed initialization per Memory, success or failure: an attempt that +// throws may already have written into linear memory, so the bytes are not a +// clean slate for a second instance. Module-local, like the counters above. +const __claimedMemories = new WeakSet() + +function __resolveInstanceMemory(__options) { + const __provided = __options == null ? undefined : __options.memory + if (__provided === undefined || __provided === null) { + // Page counts are handed to the engine unvalidated: it already rejects a + // negative, over-4GiB or below-maximum value with a precise message, and a + // second set of bounds here would only drift from it. + const __allocated = new WebAssembly.Memory({ + initial: + __options != null && __options.initialMemoryPages !== undefined + ? __options.initialMemoryPages + : WASM_MEMORY.initialPages, + maximum: + __options != null && __options.maximumMemoryPages !== undefined + ? __options.maximumMemoryPages + : WASM_MEMORY.maximumPages, + }) + // Claimed like a caller-provided one. The handle publishes it as + // `instance.memory`, so handing it back to `createInstance()` is as easy + // as passing your own twice, and it would put two live instances on one + // linear memory: each initialization rewrites the emnapi/WASI state the + // other is still running on. + __claimedMemories.add(__allocated) + return __allocated + } + if ( + __options.initialMemoryPages !== undefined || + __options.maximumMemoryPages !== undefined + ) { + throw new TypeError( + 'Pass either memory or initialMemoryPages/maximumMemoryPages, not both', + ) + } + let __buffer + try { + // Brand check: the getter throws for anything that is not a genuine + // WebAssembly.Memory, including a cross-realm look-alike object. + __buffer = Reflect.apply(__memoryBufferGetter, __provided, []) + } catch { + throw new TypeError('memory must be an unshared WebAssembly.Memory') + } + try { + // Throws for a SharedArrayBuffer. This loader has no threads, and shared + // growth does not detach: external views handed to the addon would + // silently outlive the bytes they describe. + Reflect.apply(__arrayBufferByteLengthGetter, __buffer, []) + } catch { + throw new TypeError( + 'The deferred loader requires an unshared WebAssembly.Memory', + ) + } + // The intrinsic getters above accept a genuine Memory from ANY realm, but + // the loader's dependencies do not: `WASI.setMemory` in + // `@napi-rs/wasm-runtime` and emnapi identify a Memory with a realm-local + // `instanceof`. A Memory built in another realm (a `node:vm` context, a + // same-origin iframe) would pass every check here and only fail deep inside + // initialization. Reject it up front, and before the claim below, so the + // caller keeps it usable in the realm that made it. + if (!(__provided instanceof WebAssembly.Memory)) { + throw new TypeError( + 'memory must be a WebAssembly.Memory created in the same realm as this loader', + ) + } + if (__claimedMemories.has(__provided)) { + throw new TypeError( + 'This WebAssembly.Memory has already been used for a deferred initialization attempt and cannot be reused, including after a failed initialization or a disposal', + ) + } + // Last step, after every check: a rejected option bag must leave the Memory + // unclaimed, or a caller could not fix the call and retry with it. + __claimedMemories.add(__provided) + return __provided +} + +export const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +/** + * Deferred, workerd-safe instantiation: no top-level I/O, no compile-from-bytes. + * Accepts ONLY a precompiled WebAssembly.Module, or a Promise resolving to one + * (e.g. `import mod from './transform-relay.wasm32-wasip1.wasm'` under a CompiledWasm + * module rule / wrangler module import). Byte buffers, URLs and Response + * objects are rejected: they require dynamic Wasm compilation, which + * Cloudflare Workers disallows. + */ +async function __resolveModule(__wasmInput) { + const __module = await __wasmInput + // Brand check, not `instanceof`: `WebAssembly.Module.imports` throws unless + // its argument is a genuine WebAssembly.Module, so prototype-spoofed byte + // buffers are rejected while cross-realm Module instances are accepted. + try { + WebAssembly.Module.imports(__module) + } catch { + throw new TypeError( + "instantiate() and createInstance() expect a precompiled WebAssembly.Module (or a Promise resolving to one), " + + "e.g. import mod from './transform-relay.wasm32-wasip1.wasm' under a CompiledWasm module rule / wrangler module import. " + + "Byte buffers, URLs and Response objects require dynamic Wasm compilation, which Cloudflare Workers disallows.", + ) + } + return __module +} + +let __normalizedModules + +function __rememberNormalizedModule(__module, __normalizedModule) { + if (!__normalizedModules) { + __normalizedModules = new WeakMap() + } + __normalizedModules.set(__module, __normalizedModule) + return __normalizedModule +} + +async function __normalizeModuleForEmnapi(__module) { + if (__module instanceof WebAssembly.Module) { + return __module + } + if (__normalizedModules) { + const __normalizedModule = __normalizedModules.get(__module) + if (__normalizedModule) { + return __normalizedModule + } + } + // @emnapi/core currently performs realm-local `instanceof` checks after + // accepting the module. Structured cloning preserves compiled code without + // compiling bytes and produces a Module owned by the current realm. + if (typeof structuredClone === 'function') { + try { + const __normalizedModule = structuredClone(__module) + if (__normalizedModule instanceof WebAssembly.Module) { + return __rememberNormalizedModule(__module, __normalizedModule) + } + } catch {} + } + // MessageChannel uses the same structured-clone semantics and covers older + // browser/Node hosts that expose it but not the structuredClone function. + if (typeof MessageChannel === 'function') { + let __channel + try { + __channel = new MessageChannel() + const __normalizedModule = await new Promise((resolve, reject) => { + __channel.port1.onmessage = (event) => resolve(event.data) + __channel.port1.onmessageerror = () => + reject(new TypeError('Failed to clone WebAssembly.Module')) + try { + __channel.port2.postMessage(__module) + } catch (error) { + reject(error) + } + }) + if (__normalizedModule instanceof WebAssembly.Module) { + return __rememberNormalizedModule(__module, __normalizedModule) + } + } catch { + } finally { + try { + __channel?.port1.close() + } catch {} + try { + __channel?.port2.close() + } catch {} + } + } + // Last-resort compatibility for genuine, extensible foreign Modules. + try { + Object.setPrototypeOf(__module, WebAssembly.Module.prototype) + } catch {} + if (__module instanceof WebAssembly.Module) { + return __module + } + throw new TypeError( + 'This host cannot normalize a cross-realm WebAssembly.Module; ' + + 'provide structuredClone or MessageChannel support.', + ) +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __captureEmnapiAutoDestroyListener(__process) { + if ( + !__process || + typeof __process.prependListener !== 'function' || + typeof __process.removeListener !== 'function' + ) { + return + } + let __autoDestroyListener + const __captureListener = (__event, __listener) => { + if (__event === 'beforeExit' && __autoDestroyListener === undefined) { + __autoDestroyListener = __listener + } + } + try { + // Run before existing newListener hooks so a hook that registers its own + // beforeExit listener cannot be mistaken for emnapi's registration. + __process.prependListener('newListener', __captureListener) + } catch { + return + } + return () => { + try { + __process.removeListener('newListener', __captureListener) + } catch {} + if (__autoDestroyListener !== undefined) { + try { + __process.removeListener('beforeExit', __autoDestroyListener) + } catch {} + } + } +} + +function __attachCleanupError(__error, __cleanupError) { + try { + if ( + __error && + (typeof __error === 'object' || typeof __error === 'function') && + __error.cause === undefined + ) { + __error.cause = __cleanupError + } + } catch {} +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (__callback) { + setImmediate(__callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (__callback) { + const __channel = new __MessageChannel() + __channel.port1.onmessage = function () { + __channel.port1.onmessage = null + try { + __channel.port1.close() + } catch {} + try { + __channel.port2.close() + } catch {} + __callback() + } + __channel.port2.postMessage(null) + } + } + return function (__callback) { + setTimeout(__callback, 0) + } +})() + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + */ +function __drainWasmEnvCleanup(__instance) { + const __pending = __instance?.exports.napi_wasm_env_cleanup_pending + const __observable = typeof __pending === 'function' + if (__observable) { + let __queued + try { + __queued = __pending() + } catch { + return + } + if (!__queued) { + return + } + } + const __limit = __observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + return (async () => { + let __queued = 0 + for (let __turn = 0; __turn < __limit; __turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!__observable) { + continue + } + try { + __queued = __pending() + } catch { + return + } + if (!__queued) { + return + } + } + if (!__observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: `__prepareForDisposal` + // leaves its drained flag unset, dispose() (and the instantiation-failure + // path) decline to destroy, and a later dispose() runs the drain again — by + // which time the queue has usually been delivered. A counter that is + // somehow stuck nonzero therefore costs each attempt at most another + // bounded wait and a rejection, never a stranded promise; the managed + // beforeExit destroyer still reclaims the context. + const __drainError = new Error( + 'the wasm environment still reports ' + + __queued + + ' queued settlement(s) after ' + + __limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + __drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw __drainError + })() +} + +// A real, *referenced* timer for the async-work wait below, which polls the +// addon rather than interleaving with the @emnapi/core dispatch: a zero-delay +// macrotask there would spin the loop instead of yielding it. Falls back to the +// macrotask scheduler on a host without timers. +function __scheduleTimer(__callback, __delay) { + const __setTimer = globalThis.setTimeout + if (typeof __setTimer !== 'function') { + __scheduleMacrotask(__callback) + return + } + try { + __setTimer(__callback, __delay) + } catch { + __scheduleMacrotask(__callback) + } +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this instance's outstanding `napi_async_work` before the teardown + * that would strand it. + * + * The same hole the eager loaders had, and the same fix: the environment + * cleanup barrier covers promise settlements queued on the threadsafe-function + * queue and says nothing about `napi_async_work`, so destroying the context + * with a work still outstanding leaves its completion callback with nowhere to + * run and its promise unsettled forever. + * + * This flavor is threadless, so `compute` runs on the JavaScript thread inside + * the macrotask that dequeued it: while this is running, any outstanding work + * is queued rather than executing, and `napi_wasm_cancel_pending_async_work` + * can take all of it. The poll is still what decides when the drain is done — + * cancellation delivers those completions from a later macrotask, not + * synchronously. + * + * Per instance, from that instance's own exports: two instances of this loader + * have separate registries and must not wait on each other. + * + * Both exports are optional, so an addon built against a napi crate that + * predates them keeps the previous behavior. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. No deadline: giving up would destroy the + * environment with a completion callback still owed. + */ +function __drainInstanceAsyncWork(__instance) { + const __exports = __instance?.exports + const __pending = __exports?.napi_wasm_async_work_pending + const __cancelPending = __exports?.napi_wasm_cancel_pending_async_work + if (typeof __pending !== 'function' || typeof __cancelPending !== 'function') { + return + } + + const __readPending = () => { + try { + return __pending() + } catch (__error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition and + // best-effort is the honest answer. Anything else means the export is not + // what this loader thinks it is — a defect worth surfacing. + if (__error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw __error + } + } + + if (!__readPending()) { + return + } + try { + __cancelPending() + } catch { + // Cancellation only bounds the wait. Failing it means waiting for the queue + // to run instead, which the poll below already does. + } + if (!__readPending()) { + return + } + + return (async () => { + while (__readPending()) { + await new Promise((__resolve) => { + __scheduleTimer(__resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })() +} + +function __createLifecycleReentryError(__operation) { + const __error = new Error( + __operation + + '() cannot run while an emnapi Context.destroy() call is still active; await the original cleanup promise instead.', + ) + __error.code = 'ERR_NAPI_WASI_LIFECYCLE_REENTRY' + return __error +} + +const __managedEmnapiContextDestroyers = new Set() +let __managedCleanupProcess +let __managedBeforeExitListener +let __managedDestroyPromise +let __managedDestroyersInFlight +let __managedBeforeExitRegistrationRetryCount = 0 +let __managedBeforeExitRegistrationRetryScheduled = false +let __moduleLifecycleDestroyDepth = 0 + +function __removeManagedEmnapiCleanupListeners() { + const __process = __managedCleanupProcess + const __beforeExitListener = __managedBeforeExitListener + __managedCleanupProcess = undefined + __managedBeforeExitListener = undefined + __managedBeforeExitRegistrationRetryCount = 0 + if (__process && __beforeExitListener) { + try { + __process.removeListener('beforeExit', __beforeExitListener) + } catch {} + } +} + +function __scheduleManagedBeforeExitListenerRegistration() { + if ( + !__managedCleanupProcess || + __managedBeforeExitListener || + __managedEmnapiContextDestroyers.size === 0 || + __managedBeforeExitRegistrationRetryScheduled || + __managedBeforeExitRegistrationRetryCount >= 3 + ) { + return + } + __managedBeforeExitRegistrationRetryScheduled = true + __managedBeforeExitRegistrationRetryCount++ + queueMicrotask(() => { + __managedBeforeExitRegistrationRetryScheduled = false + if ( + !__managedCleanupProcess || + __managedBeforeExitListener || + __managedEmnapiContextDestroyers.size === 0 + ) { + return + } + try { + __registerManagedBeforeExitListener() + } catch {} + }) +} + +function __registerManagedBeforeExitListener() { + if (!__managedCleanupProcess || __managedBeforeExitListener) { + return + } + try { + __managedCleanupProcess.once( + 'beforeExit', + __destroyManagedEmnapiContextsBeforeExit, + ) + } catch (error) { + __scheduleManagedBeforeExitListenerRegistration() + throw error + } + __managedBeforeExitListener = __destroyManagedEmnapiContextsBeforeExit + __managedBeforeExitRegistrationRetryCount = 0 +} + +function __settleManagedEmnapiContextDestroy(__promise) { + if (__managedDestroyPromise === __promise) { + __managedDestroyPromise = undefined + __managedDestroyersInFlight = undefined + } + if (__managedEmnapiContextDestroyers.size === 0) { + __removeManagedEmnapiCleanupListeners() + return + } + try { + __registerManagedBeforeExitListener() + } catch {} +} + +function __destroyManagedEmnapiContexts(__excludedDestroyers) { + if (__managedDestroyPromise) { + return __managedDestroyPromise + } + const __destroyers = Array.from(__managedEmnapiContextDestroyers).filter( + (__destroy) => !__excludedDestroyers?.has(__destroy), + ) + if (__destroyers.length === 0) { + return Promise.resolve() + } + let __resolveDestroy + let __rejectDestroy + const __promise = new Promise((resolve, reject) => { + __resolveDestroy = resolve + __rejectDestroy = reject + }) + __managedDestroyPromise = __promise + __managedDestroyersInFlight = new Set(__destroyers) + void Promise.all( + __destroyers.map((__destroy) => { + try { + return Promise.resolve(__destroy()).then( + () => ({ failed: false }), + (error) => ({ failed: true, error }), + ) + } catch (error) { + return { failed: true, error } + } + }), + ).then((__results) => { + let __primaryError + let __failed = false + for (const __result of __results) { + if (!__result.failed) { + continue + } + if (!__failed) { + __failed = true + __primaryError = __result.error + } else { + __attachCleanupError(__primaryError, __result.error) + } + } + if (__failed) { + __rejectDestroy(__primaryError) + } else { + __resolveDestroy() + } + }, __rejectDestroy) + void __promise.then( + () => { + __settleManagedEmnapiContextDestroy(__promise) + }, + () => { + __settleManagedEmnapiContextDestroy(__promise) + }, + ) + return __promise +} + +async function __drainManagedEmnapiContexts(__excludedDestroyers) { + const __attemptedDestroyers = new Set(__excludedDestroyers) + let __primaryError + let __failed = false + while (true) { + let __promise = __managedDestroyPromise + let __destroyers = __managedDestroyersInFlight + if (!__promise) { + __promise = __destroyManagedEmnapiContexts(__attemptedDestroyers) + __destroyers = __managedDestroyersInFlight + if (!__destroyers) { + break + } + } + for (const __destroy of __destroyers) { + __attemptedDestroyers.add(__destroy) + } + try { + await __promise + } catch (error) { + if (!__failed) { + __failed = true + __primaryError = error + } else { + __attachCleanupError(__primaryError, error) + } + } + } + if (__failed) { + throw __primaryError + } +} + +function __destroyManagedEmnapiContextsBeforeExit() { + // A once listener is consumed before Node invokes it, including when another + // cleanup batch is still pending. + __managedBeforeExitListener = undefined + if (__managedDestroyPromise) { + return + } + void __destroyManagedEmnapiContexts().catch((error) => { + queueMicrotask(() => { + throw error + }) + }) +} + +function __registerManagedEmnapiContext(__process, __destroy) { + __managedEmnapiContextDestroyers.add(__destroy) + if ( + !__managedCleanupProcess && + __process && + typeof __process.once === 'function' && + typeof __process.removeListener === 'function' + ) { + __managedCleanupProcess = __process + } + let __registered = true + return () => { + if (!__registered) { + return + } + __registered = false + __managedEmnapiContextDestroyers.delete(__destroy) + if (__managedEmnapiContextDestroyers.size === 0) { + __removeManagedEmnapiCleanupListeners() + } + } +} + +async function __createManagedEmnapiContext( + __prepareEnvCleanup, + __isPreparingEnvCleanup, +) { + const __process = + typeof process === 'object' && process !== null ? process : undefined + const __finishAutoDestroyCapture = + __captureEmnapiAutoDestroyListener(__process) + let __emnapiContext + let __contextInitializationError + let __contextInitializationFailed = false + try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareEnvCleanup, + __isPreparingEnvCleanup, + ) + // emnapi 2.x still registers an unconditional process.once('beforeExit') + // auto-destroy listener on Node hosts, and suppressDestroy() only + // neutralizes its callback without removing it. This loader must stay + // side-effect free per instance, so the listener is captured and removed; + // suppressDestroy() remains the safety net when removal is unavailable. + __emnapiContext.suppressDestroy() + } catch (error) { + __contextInitializationError = error + __contextInitializationFailed = true + } finally { + // Remove only the exact emnapi callback captured above. + __finishAutoDestroyCapture?.() + } + if (__emnapiContext === undefined) { + throw __contextInitializationError + } + let __disposed = false + let __destroying = false + let __destroyPromise + let __cleanupRegistered = false + let __unregisterCleanup + const __destroy = (__blocksModuleLifecycle = false) => { + if (__disposed) { + return + } + if (__destroying) { + throw __createLifecycleReentryError('dispose') + } + if (__destroyPromise) { + return __destroyPromise + } + __destroying = true + let __result + const __finishDestroyInvocation = () => { + __destroying = false + } + const __finishModuleLifecycleDestroy = () => { + if (__blocksModuleLifecycle) { + __blocksModuleLifecycle = false + __moduleLifecycleDestroyDepth-- + } + } + if (__blocksModuleLifecycle) { + __moduleLifecycleDestroyDepth++ + } + try { + // Context.destroy() disables JS before cleanup hooks run, so settle + // runtime-owned promises while this environment can still call JS. + __prepareEnvCleanup?.() + if (__isPreparingEnvCleanup?.()) { + // Reached from inside the barrier, so `Context.destroy()` below would + // hit the wrapper's in-flight no-op. Recording that as a completed + // destroy is what makes the frame that *did* start the barrier skip the + // real one afterwards, leaving the context retained with its cleanup + // hooks unrun. Refuse instead: nothing is flagged, the context stays + // registered for managed beforeExit cleanup, and a later destroy still + // works. dispose() coalesces reentrancy before it can get here, so this + // is the backstop for any other caller that manages to. + throw __createLifecycleReentryError('dispose') + } + __result = __emnapiContext.destroy() + } catch (error) { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + throw error + } + let __then + try { + if ( + __result !== null && + (typeof __result === 'object' || typeof __result === 'function') + ) { + __then = __result.then + } + } catch (error) { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + throw error + } + if (typeof __then === 'function') { + let __resolveResult + let __rejectResult + const __resultPromise = new Promise((resolve, reject) => { + __resolveResult = resolve + __rejectResult = reject + }) + const __promise = __resultPromise.then( + (value) => { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __disposed = true + __destroyPromise = undefined + __unregisterCleanup?.() + return value + }, + (error) => { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __destroyPromise = undefined + throw error + }, + ) + __destroyPromise = __promise + try { + Reflect.apply(__then, __result, [__resolveResult, __rejectResult]) + } catch (error) { + __rejectResult(error) + } + return __promise + } + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __disposed = true + __unregisterCleanup?.() + } + const __destroyForModuleLifecycle = () => __destroy(true) + const __registerCleanup = ( + __beforeExitDestroy = __destroyForModuleLifecycle, + ) => { + if (__cleanupRegistered || __disposed) { + return + } + __unregisterCleanup = __registerManagedEmnapiContext( + __process, + __beforeExitDestroy, + ) + __cleanupRegistered = true + __registerManagedBeforeExitListener() + } + if (__contextInitializationFailed) { + let __registrationError + let __registrationFailed = false + try { + __registerCleanup() + } catch (error) { + __attachCleanupError(__contextInitializationError, error) + __registrationError = error + __registrationFailed = true + } + try { + await __destroyForModuleLifecycle() + } catch (error) { + __attachCleanupError( + __registrationFailed + ? __registrationError + : __contextInitializationError, + error, + ) + try { + __registerManagedBeforeExitListener() + } catch {} + } + throw __contextInitializationError + } + return { + context: __emnapiContext, + destroy: __destroy, + destroyForModuleLifecycle: __destroyForModuleLifecycle, + registerCleanup: __registerCleanup, + } +} + +async function __createInstance( + __wasmInput, + __options, + __beforeExitDestroy, + __onManagedDestroyer, +) { + const __module = await __resolveModule(__wasmInput) + const __emnapiModule = await __normalizeModuleForEmnapi(__module) + const __wasi = new __WASI({ + version: 'preview1', + }) + // The wasm module is linked with `--import-memory`, so a Memory must be + // provided. It is resolved here in function scope (workerd bans global scope + // allocation) and is never shared (no threads, no SharedArrayBuffer). + // Resolve it before the emnapi context so a rejected option bag or a host + // memory-limit failure cannot leak a context that never reaches + // instantiation. + const __wasmMemory = __resolveInstanceMemory(__options) + let __lifecycleState = 'pending' + let __destroyEmnapiContext + let __destroyOwnedContext + let __destroyManagedOwnedContext + let __napiInstance + let __wasmEnvCleanupRan = false + let __wasmEnvCleanupPrepared = false + let __wasmEnvCleanupPreparing = false + let __wasmEnvCleanupDrained = false + let __wasmEnvCleanupDrainPromise + const __isPreparingEnvCleanup = () => __wasmEnvCleanupPreparing + const __prepareEnvCleanup = () => { + if (__wasmEnvCleanupPrepared || __wasmEnvCleanupPreparing) { + return + } + const __prepareWasmEnvCleanup = + __napiInstance?.exports.napi_prepare_wasm_env_cleanup + if (typeof __prepareWasmEnvCleanup === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __wasmEnvCleanupPreparing = true + try { + __prepareWasmEnvCleanup() + } finally { + __wasmEnvCleanupPreparing = false + } + __wasmEnvCleanupRan = true + } + __wasmEnvCleanupPrepared = true + } + // The barrier + settlement drain, hoisted out of the context destroyer so the + // drain can yield without widening the destroyer's reentry window. Both + // yielding paths run it — dispose() and the initialization-failure rollback. + // The destroyer still runs the barrier itself (idempotently) for the one path + // that cannot yield: managed beforeExit cleanup of an instance whose rollback + // is being retried. + // + // "Drained" is recorded only once a wait has actually finished. Scheduling a + // macrotask can fail — a host-provided or patched `setImmediate` that throws + // is enough — and dispose() stays retryable after it rejects, so marking the + // drain complete up front would make the retry skip it and destroy the context + // with the barrier's settlements still queued. + const __prepareForDisposal = () => { + if (__wasmEnvCleanupDrained) { + return + } + if (__wasmEnvCleanupDrainPromise) { + return __wasmEnvCleanupDrainPromise + } + __prepareEnvCleanup() + if (!__wasmEnvCleanupRan) { + return + } + const __drained = __drainWasmEnvCleanup(__napiInstance) + if (!__drained || typeof __drained.then !== 'function') { + __wasmEnvCleanupDrained = true + return + } + const __tracked = __drained.then( + (__value) => { + __wasmEnvCleanupDrained = true + __wasmEnvCleanupDrainPromise = undefined + return __value + }, + (__error) => { + __wasmEnvCleanupDrainPromise = undefined + throw __error + }, + ) + __wasmEnvCleanupDrainPromise = __tracked + return __tracked + } + let __disposed = false + const __runInstanceDisposal = async () => { + if (__lifecycleState !== 'failed') { + __lifecycleState = 'disposal' + } + // Outstanding async work first, while the environment is still completely + // live: its completion callbacks run addon code, and the barrier and + // `Context.destroy()` below each take that away. Undefined unless work is + // outstanding. + const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance) + if (__asyncWorkDrained) { + await __asyncWorkDrained + } + // Then settle what the barrier cancelled, before the environment stops + // accepting JavaScript calls. Undefined unless something is queued, so + // an idle disposal is not delayed by a single turn. + const __drained = __prepareForDisposal() + if (__drained) { + await __drained + } + const __result = await (__beforeExitDestroy + ? __destroyManagedOwnedContext() + : __destroyOwnedContext()) + // Only a completed destroy retires the instance; a throw above leaves + // the counter untouched so a retried dispose() cannot double-decrement. + if (!__disposed) { + __disposed = true + __liveInstances -= 1 + } + return __result + } + let __instanceDisposePromise + /** + * The disposal frame runs the barrier, and the barrier settles the promises it + * cancels synchronously — so a promise hook firing inside it can call this + * same instance's dispose() again while the first call is still in its drain. + * That nested call finds the barrier flagged in flight, prepares nothing, + * drains nothing, and falls straight through to the context destroyer, whose + * `Context.destroy()` hits the wrapper's in-flight no-op. It would record a + * destruction that never happened, and the outer frame would then skip the + * real one: both disposals resolve, no cleanup hook runs, the context stays + * retained. + * + * Memoize before any of that starts, exactly like the eager loaders' + * `__disposeWasiBinding`, so there is only ever one disposal frame per + * instance and a reentrant caller awaits it instead of racing it. Cleared on + * rejection: a drain that failed has to stay retryable. + */ + const __disposeInstance = () => { + if (__instanceDisposePromise) { + return __instanceDisposePromise + } + let __resolveDispose + let __rejectDispose + const __disposePromise = new Promise((__resolve, __reject) => { + __resolveDispose = __resolve + __rejectDispose = __reject + }) + __instanceDisposePromise = __disposePromise + __runInstanceDisposal().then(__resolveDispose, (__error) => { + __instanceDisposePromise = undefined + __rejectDispose(__error) + }) + return __disposePromise + } + const __destroyBeforeExit = __beforeExitDestroy + ? async () => { + if (__lifecycleState === 'failed') { + await __destroyManagedOwnedContext() + return + } + __lifecycleState = 'disposal' + try { + await __beforeExitDestroy() + } catch (error) { + if (__lifecycleState !== 'failed') { + throw error + } + // The singleton's initialization rejection is already observable + // through instantiate() and dispose(). Managed beforeExit cleanup + // owns only context destruction, including retrying a failed rollback. + await __destroyManagedOwnedContext() + } + } + : undefined + const { + context: __emnapiContext, + destroy, + destroyForModuleLifecycle, + registerCleanup: __registerCleanup, + } = await __createManagedEmnapiContext( + __prepareEnvCleanup, + __isPreparingEnvCleanup, + ) + __destroyEmnapiContext = destroy + __destroyOwnedContext = () => __destroyEmnapiContext() + __destroyManagedOwnedContext = destroyForModuleLifecycle + try { + if (__destroyBeforeExit) { + __onManagedDestroyer(__destroyBeforeExit) + await __registerCleanup(__destroyBeforeExit) + } + let __napiModule + ;({ + instance: __napiInstance, + napiModule: __napiModule, + } = await __emnapiInstantiateNapiModule(__emnapiModule, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + // `instantiate()` and `createInstance().exports` hand out this object; a + // named module export does not travel with it. After the instance host + // install, which hands the same object to addon-provided registration + // functions that may put anything on it, and inside this `try`, so a + // claimed name flips `__lifecycleState` to 'failed' and tears the instance + // down rather than escaping a half-built one. + __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) + if (__lifecycleState === 'pending') { + __lifecycleState = 'succeeded' + } + __createdInstances += 1 + __liveInstances += 1 + return { + exports: __napiModule.exports, + get memory() { + return __wasmMemory + }, + get memoryBytes() { + // The Memory outlives the environment, so this stays readable after a + // FAILED dispose() (which leaves the instance undisposed and + // retryable). It reports 0 only once disposal has actually completed. + return __disposed ? 0 : __wasmMemory.buffer.byteLength + }, + get disposed() { + return __disposed + }, + dispose: __disposeInstance, + } + } catch (error) { + __lifecycleState = 'failed' + // Instantiation can fail *after* registration has run, and registration runs + // with a live environment: a module-init hook can start async work and then + // return an error, and the promise it created may already have escaped into + // JavaScript. Settle what the barrier cancels before the context is + // destroyed, exactly like dispose() does; destroying without yielding + // discards the queue with a null env. Undefined unless something is queued, + // so a failure before beforeInit costs no extra turn. + let __settlementsUnreached = false + try { + // Registration can start async work too, and this path destroys the same + // environment its completions need. A drain that cannot finish leaves the + // work outstanding, so it counts as settlements unreached and stops the + // rollback short of destroying, exactly like a failed barrier drain. + const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance) + if (__asyncWorkDrained) { + await __asyncWorkDrained + } + const __drained = __prepareForDisposal() + if (__drained) { + await __drained + } + } catch (drainError) { + __attachCleanupError(error, drainError) + __settlementsUnreached = true + } + let __registrationError + let __registrationFailed = false + if (!__beforeExitDestroy) { + try { + // Independent instances are caller-owned while pending and after + // success. Register only failed rollback so cleanup remains retryable. + await __registerCleanup() + } catch (registrationError) { + __attachCleanupError(error, registrationError) + __registrationError = registrationError + __registrationFailed = true + } + } + if (__settlementsUnreached) { + // The barrier or the drain did not finish, so the settlements it queued + // are still in the threadsafe-function queue. Destroying now runs the + // cleanup hook that drains that queue with a null env and discards it, + // stranding a promise that already escaped into JavaScript — with nothing + // left that could ever settle it. dispose() refuses to destroy for exactly + // this reason (a rejected drain there never reaches the destroy), so this + // path refuses too. + // + // Nothing leaks. The registration just above — and, for the singleton, the + // one made before instantiation — leaves this context in + // `__managedEmnapiContextDestroyers`, so beforeExit destroys it and + // dispose() can retry it. Only the destruction is deferred, and the turns + // that pass in the meantime are exactly what the queue needed. + try { + __registerManagedBeforeExitListener() + } catch {} + throw error + } + try { + await __destroyManagedOwnedContext() + } catch (disposeError) { + // Initialization is the primary failure. Preserve it even if cleanup + // also fails, while retaining the cleanup error when the value is + // extensible and has no existing cause. + __attachCleanupError( + __registrationFailed ? __registrationError : error, + disposeError, + ) + try { + __registerManagedBeforeExitListener() + } catch {} + } + throw error + } +} + +/** + * Create an independent instance. Call and await dispose() when the instance + * is no longer needed so emnapi cleanup hooks run deterministically. + * + * The optional second argument selects this instance's linear memory: either + * `memory` (an unshared, single-use WebAssembly.Memory you allocated) or + * `initialMemoryPages` / `maximumMemoryPages`, never both. Omitted, the + * loader allocates WASM_MEMORY.initialPages..WASM_MEMORY.maximumPages. + * + * A provided Memory must come from this loader's own realm: the WASI and + * emnapi layers underneath identify one with a realm-local `instanceof`, so a + * Memory built in a `node:vm` context or another frame is rejected. Every + * Memory an instance runs on is single-use, the loader-allocated one included: + * `instance.memory` cannot be recycled into a second `createInstance()`. + */ +export async function createInstance(__wasmInput, __options) { + return __createInstance(__wasmInput, __options) +} + +let __defaultModulePromise +let __defaultInstancePromise +let __defaultDisposePromise +let __defaultDisposalStarted = false +const __defaultManagedDestroyers = new WeakMap() +let __moduleDisposePromise + +/** + * Instantiate a module-local singleton. Concurrent and repeated calls + * with the same module share one instance and one Memory allocation. + */ +export function instantiate(__wasmInput) { + const __modulePromise = __resolveModule(__wasmInput) + if (__moduleLifecycleDestroyDepth !== 0) { + void __modulePromise.catch(() => {}) + return Promise.reject(__createLifecycleReentryError('instantiate')) + } + if (__moduleDisposePromise) { + void __modulePromise.catch(() => {}) + return __moduleDisposePromise.then(() => instantiate(__modulePromise)) + } + if (__defaultDisposalStarted) { + // Observe rejected input immediately, but preserve lifecycle ordering and + // error precedence by instantiating only after disposal succeeds. A failed + // disposal retains the old instance only so its cleanup can be retried. + void __modulePromise.catch(() => {}) + const __disposePromise = __defaultDisposePromise ?? dispose() + return __disposePromise.then(() => instantiate(__modulePromise)) + } + if (!__defaultInstancePromise) { + __defaultModulePromise = __modulePromise + const __instancePromise = __modulePromise.then((__module) => + __createInstance( + __module, + undefined, + __disposeDefaultInstance, + (__managedDestroyer) => { + __defaultManagedDestroyers.set( + __instancePromise, + __managedDestroyer, + ) + }, + ), + ) + __defaultInstancePromise = __instancePromise + void __instancePromise.catch(() => { + if (__defaultInstancePromise === __instancePromise) { + __defaultInstancePromise = undefined + __defaultModulePromise = undefined + } + }) + return __instancePromise.then((__instance) => __instance.exports) + } + const __defaultModulePromiseForCall = __defaultModulePromise + const __defaultInstancePromiseForCall = __defaultInstancePromise + return Promise.all([__defaultModulePromiseForCall, __modulePromise]).then( + async ([__defaultModule, __module]) => { + if (__defaultModule !== __module) { + throw new Error( + 'instantiate() already owns a different WebAssembly.Module; call dispose() first or use createInstance() for independent instances.', + ) + } + return (await __defaultInstancePromiseForCall).exports + }, + ) +} + +async function __disposeDefaultInstance(__onDestroy) { + if (__defaultDisposePromise) { + return __defaultDisposePromise + } + const __instancePromise = __defaultInstancePromise + if (!__instancePromise) { + __defaultDisposalStarted = false + return + } + __defaultDisposalStarted = true + const __disposePromise = (async () => { + let __instance + try { + __instance = await __instancePromise + } catch (error) { + const __managedDestroyer = + __defaultManagedDestroyers.get(__instancePromise) + if (__managedDestroyer) { + __onDestroy?.(__managedDestroyer) + } + __defaultManagedDestroyers.delete(__instancePromise) + throw error + } + const __managedDestroyer = + __defaultManagedDestroyers.get(__instancePromise) + if (__managedDestroyer) { + __onDestroy?.(__managedDestroyer) + } + await __instance.dispose() + if (__defaultInstancePromise === __instancePromise) { + __defaultInstancePromise = undefined + __defaultModulePromise = undefined + __defaultDisposalStarted = false + } + __defaultManagedDestroyers.delete(__instancePromise) + })() + __defaultDisposePromise = __disposePromise + try { + await __disposePromise + } finally { + if (__defaultDisposePromise === __disposePromise) { + __defaultDisposePromise = undefined + } + } +} + +async function __dispose() { + let __defaultDisposeError + let __defaultDisposeFailed = false + let __attemptedDefaultDestroyer + try { + await __disposeDefaultInstance((__destroyer) => { + __attemptedDefaultDestroyer = __destroyer + }) + } catch (error) { + __defaultDisposeError = error + __defaultDisposeFailed = true + } + const __excludedDestroyers = new Set() + if (__defaultDisposeFailed && __attemptedDefaultDestroyer) { + __excludedDestroyers.add(__attemptedDefaultDestroyer) + } + try { + await __drainManagedEmnapiContexts(__excludedDestroyers) + } catch (error) { + if (!__defaultDisposeFailed) { + throw error + } + if (error !== __defaultDisposeError) { + __attachCleanupError(__defaultDisposeError, error) + } + } + if (__defaultDisposeFailed) { + throw __defaultDisposeError + } +} + +/** + * Dispose the singleton created by instantiate(). A later call may create a + * fresh instance, including from a different module. This also retries cleanup + * retained after a failed initialization rollback. + */ +export function dispose() { + if (__moduleLifecycleDestroyDepth !== 0) { + return Promise.reject(__createLifecycleReentryError('dispose')) + } + if (__moduleDisposePromise) { + return __moduleDisposePromise + } + let __resolveDispose + let __rejectDispose + const __promise = new Promise((resolve, reject) => { + __resolveDispose = resolve + __rejectDispose = reject + }) + __moduleDisposePromise = __promise + void __dispose().then(__resolveDispose, __rejectDispose) + void __promise.then( + () => { + if (__moduleDisposePromise === __promise) { + __moduleDisposePromise = undefined + } + }, + () => { + if (__moduleDisposePromise === __promise) { + __moduleDisposePromise = undefined + } + }, + ) + return __promise +} diff --git a/napi/transform-relay/transform-relay.wasip1.cjs b/napi/transform-relay/transform-relay.wasip1.cjs new file mode 100644 index 0000000000..9a08ca748c --- /dev/null +++ b/napi/transform-relay/transform-relay.wasip1.cjs @@ -0,0 +1,1208 @@ +// napi-rs-artifact-metadata:{"version":2,"rootEntry":"index.js","exports":["Severity","transform","transformSync"],"managedRootEntries":["browser.js","index.js","transform-relay.wasm","transform-relay.debug.wasm"]} +/* eslint-disable */ +/* auto-generated by NAPI-RS */ + +const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +const __nodeFs = require('node:fs') +const __nodePath = require('node:path') +const { WASI: __nodeWASI } = require('node:wasi') + +const { + emnapiAsyncWorkPlugin: __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin: __emnapiTSFNPlugin, + instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync, +} = require('@napi-rs/wasm-runtime') +const { createContext: __emnapiCreateContext } = require('@emnapi/runtime') + +const __cwd = process.cwd() +const __rootDir = __nodePath.parse(__cwd).root +const __hostRoot = + process.platform === 'android' ? __cwd : __rootDir + +const __wasi = new __nodeWASI({ + version: 'preview1', + env: process.env, + preopens: { + [__rootDir]: __hostRoot, + [__hostRoot]: __hostRoot, + }, +}) + +const __wasmMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, +}) + +let __wasmFilePath = __nodePath.join(__dirname, 'transform-relay.wasm32-wasip1.wasm') +const __wasmDebugFilePath = __nodePath.join(__dirname, 'transform-relay.wasm32-wasip1.debug.wasm') + +if (__nodeFs.existsSync(__wasmDebugFilePath)) { + __wasmFilePath = __wasmDebugFilePath +} else if (!__nodeFs.existsSync(__wasmFilePath)) { + const __wasiPackageEntry = require.resolve('@oxc-transform-relay/binding-wasm32-wasip1') + const __packagedWasmFilePath = __nodePath.join( + __nodePath.dirname(__wasiPackageEntry), + 'transform-relay.wasm32-wasip1.wasm', + ) + if (!__nodeFs.existsSync(__packagedWasmFilePath)) { + throw new Error( + '@oxc-transform-relay/binding-wasm32-wasip1 is installed but is missing transform-relay.wasm32-wasip1.wasm.', + ) + } + __wasmFilePath = __packagedWasmFilePath +} + +const __wasmFile = __nodeFs.readFileSync(__wasmFilePath) +let __emnapiContext + +const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose') +const __wasiWorkers = new Set() +// The thread manager has to be reachable *before* anything that can throw +// during load or registration. Initialization can fail after the pool has +// already spawned workers, and the rollback still has to mark their +// terminations as expected — but `__napiModule` is assigned only when +// instantiation RETURNS, so on exactly that path it is still undefined. A +// plugin factory runs while the emnapi module is being created, before the +// wasm is loaded and before any registration function runs, and its context +// carries the very same manager instance. +let __wasiThreadManager + +function __captureWasiThreadManager(context) { + if (context && context.PThread) { + __wasiThreadManager = context.PThread + } + return {} +} + +function __getWasiThreadManager() { + const manager = + __wasiThreadManager !== undefined + ? __wasiThreadManager + : __napiModule + ? __napiModule.PThread + : undefined + if (manager && typeof manager.terminateWorker === 'function') { + return manager + } + return undefined +} +let __napiInstance +let __emnapiContextDestroyed = false +let __emnapiContextDestroyPromise +let __emnapiWasmEnvCleanupPrepared = false +let __emnapiWasmEnvCleanupPreparing = false +let __emnapiWasmEnvCleanupRan = false +let __emnapiWasmEnvCleanupDrained = false +let __emnapiWasmEnvCleanupDrainPromise +let __wasiDisposed = false +let __wasiAsyncWorkDrainPromise +let __wasiDisposePromise +let __completeWasiDisposal = function () {} +// Overridden by loader flavors that have a last-resort reclaim for a rollback +// that stopped short of destroying the context. See +// `__rollbackWasiInitialization`. +let __retainWasiRollbackForRetry = function () {} + +function __isThenable(value) { + return ( + value !== null && + (typeof value === 'object' || typeof value === 'function') && + typeof value.then === 'function' + ) +} + +function __createCleanupError(errors, message) { + if (errors.length === 1) { + return errors[0] + } + const __AggregateError = globalThis.AggregateError + if (typeof __AggregateError === 'function') { + return new __AggregateError(errors, message) + } + const error = new Error(message) + error.errors = errors + return error +} + +function __attachCleanupErrors(error, cleanupErrors) { + if (cleanupErrors.length === 0) { + return error + } + const cleanupError = __createCleanupError( + cleanupErrors, + 'WASI binding cleanup failed', + ) + try { + if ( + error && + (typeof error === 'object' || typeof error === 'function') + ) { + if (error.cause === undefined) { + error.cause = cleanupError + if (error.cause === cleanupError) { + return error + } + } + if (Array.isArray(error.cleanupErrors)) { + error.cleanupErrors.push(cleanupError) + return error + } else { + const attachedCleanupErrors = [cleanupError] + error.cleanupErrors = attachedCleanupErrors + if (error.cleanupErrors === attachedCleanupErrors) { + return error + } + } + } + } catch {} + const aggregate = __createCleanupError( + [error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + try { + aggregate.cause = error + } catch {} + return aggregate +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __isPreparingWasmEnvCleanup() { + return __emnapiWasmEnvCleanupPreparing +} + +function __prepareWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) { + return + } + const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup + if (typeof prepare === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __emnapiWasmEnvCleanupPreparing = true + try { + prepare() + } finally { + __emnapiWasmEnvCleanupPreparing = false + } + __emnapiWasmEnvCleanupRan = true + } + __emnapiWasmEnvCleanupPrepared = true +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (callback) { + setImmediate(callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (callback) { + const channel = new __MessageChannel() + channel.port1.onmessage = function () { + channel.port1.onmessage = null + try { + channel.port1.close() + } catch {} + try { + channel.port2.close() + } catch {} + callback() + } + channel.port2.postMessage(null) + } + } + return function (callback) { + setTimeout(callback, 0) + } +})() + +// A real, *referenced* timer, for waits that must let the whole host make +// progress between looks — the async-work drain polls the addon rather than +// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there +// would spin the loop instead of yielding it. Falls back to the macrotask +// scheduler on a host without timers. +function __scheduleTimer(callback, delay) { + const setTimer = globalThis.setTimeout + if (typeof setTimer !== 'function') { + __scheduleMacrotask(callback) + return + } + try { + setTimer(callback, delay) + } catch { + __scheduleMacrotask(callback) + } +} + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + * + * The "already drained" flag is set only once a wait has actually finished. + * Scheduling a macrotask can fail — a host-provided or patched `setImmediate` + * that throws is enough — and a disposal that rejects stays retryable, so + * marking the drain complete up front would make the retry skip it and destroy + * the context with the barrier's settlements still queued. + * + * A wait that runs out of turns with the counter still nonzero rejects with + * `ERR_NAPI_WASI_CLEANUP_PENDING` for the same reason: at that point + * "finished" is indistinguishable from the stranding above, and destroying + * would discard the very settlement the wait was for. The rejection leaves the + * flag unset and disposal retryable. + */ +function __drainWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupDrained || !__emnapiWasmEnvCleanupRan) { + return + } + if (__emnapiWasmEnvCleanupDrainPromise) { + return __emnapiWasmEnvCleanupDrainPromise + } + const pending = __napiInstance?.exports?.napi_wasm_env_cleanup_pending + const observable = typeof pending === 'function' + if (observable) { + let queued + try { + queued = pending() + } catch { + __emnapiWasmEnvCleanupDrained = true + return + } + if (!queued) { + __emnapiWasmEnvCleanupDrained = true + return + } + } + const limit = observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + const drainPromise = (async () => { + let queued = 0 + for (let turn = 0; turn < limit; turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!observable) { + continue + } + try { + queued = pending() + } catch { + return + } + if (!queued) { + return + } + } + if (!observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: the drained flag stays + // unset, dispose() (and the rollback) decline to destroy, and a later + // dispose() runs the drain again — by which time the queue has usually been + // delivered. A counter that is somehow stuck nonzero therefore costs each + // attempt at most another bounded wait and a rejection, never a stranded + // promise; the process-exit teardown still reclaims the context. + const drainError = new Error( + 'the wasm environment still reports ' + + queued + + ' queued settlement(s) after ' + + limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw drainError + })().then( + (value) => { + // Set only when the wait actually finished AND the queue was seen empty + // (or is unobservable): a drain that timed out with settlements still + // queued rejects above and must stay repeatable. + __emnapiWasmEnvCleanupDrained = true + __emnapiWasmEnvCleanupDrainPromise = undefined + return value + }, + (error) => { + __emnapiWasmEnvCleanupDrainPromise = undefined + throw error + }, + ) + __emnapiWasmEnvCleanupDrainPromise = drainPromise + return drainPromise +} + +function __destroyEmnapiContext() { + if (__emnapiContextDestroyed || __emnapiContext === undefined) { + __emnapiContextDestroyed = true + return + } + if (__emnapiContextDestroyPromise) { + return __emnapiContextDestroyPromise + } + + __prepareWasmEnvCleanup() + const result = __emnapiContext.destroy() + if (!__isThenable(result)) { + __emnapiContextDestroyed = true + return + } + + const destroyPromise = Promise.resolve(result).then( + (value) => { + __emnapiContextDestroyed = true + return value + }, + (error) => { + __emnapiContextDestroyPromise = undefined + throw error + }, + ) + __emnapiContextDestroyPromise = destroyPromise + return destroyPromise +} + +/** + * Holds the event loop open until `work` settles. + * + * Nothing else can: the pool workers are deliberately unreferenced so an idle + * binding cannot keep a process alive, and referencing them again for the + * termination does not hold either — emnapi unreferences a worker the moment it + * reports `async-thread-ready`, which for a worker that was still starting + * lands *after* the termination began. Without a handle of its own, an + * `await dispose()` with nothing else pending exits the process with its + * promise unsettled, and everything after the `await` is skipped. + * + * The timer is cleared as soon as the work settles, so this never outlives the + * disposal that asked for it. + */ +function __keepEventLoopAliveUntil(work) { + const setTimer = globalThis.setInterval + const clearTimer = globalThis.clearInterval + if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') { + return work + } + let timer + try { + timer = setTimer(function () {}, 50) + } catch { + return work + } + const release = function () { + try { + clearTimer(timer) + } catch {} + } + return work.then( + (value) => { + release() + return value + }, + (error) => { + release() + throw error + }, + ) +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this addon's outstanding `napi_async_work` before the teardown that + * would strand it. + * + * `napi_prepare_wasm_env_cleanup` does not cover async work, and nothing about + * it is observable from JavaScript: the threadless archive resolves + * `napi_*_async_work` through the `@emnapi/core` plugins, but the threaded one + * links the C `async_work.c` on the uv threadpool, so there the wasm neither + * imports nor exports those symbols and the only brackets a loader could watch + * (`_emnapi_ctx_*_waiting_request_counter`) are shared with threadsafe + * functions. The addon is the one place both flavors go through, so it answers + * for both, through the same kind of handshake the settlement drain uses: + * + * - `napi_wasm_cancel_pending_async_work()` cancels what no thread has + * started. Those completion callbacks run with `napi_cancelled`, which + * napi-rs turns into a promise rejected with an `AbortError`. + * - `napi_wasm_async_work_pending()` counts what is still owed a completion + * callback. Work already executing refuses cancellation and stays counted + * until it finishes normally — which it can, because this runs before the + * barrier, before `Context.destroy()` and before anything is terminated. + * + * Both exports are optional: an addon built against a napi crate that predates + * them drains nothing and keeps the previous behavior, exactly as the + * `napi_wasm_env_cleanup_pending` handshake degrades. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. The promise it returns otherwise never + * rejects. + * + * The wait has no deadline, and that is the point: giving up would destroy the + * environment with a completion callback still owed, which is the stranding + * this exists to prevent. A task whose `execute` never returns already keeps an + * *undisposed* process alive in exactly the same way, so disposal inherits that + * rather than inventing a bound it cannot honor. + * + * Safe to call from inside a completion callback, which is reachable: settling + * a task runs addon code that can re-enter JavaScript — a setter on the value + * being handed back, a threadsafe-function callback — and that JavaScript can + * call `dispose()`. Two things make it terminate rather than wait on itself: + * + * - The addon keeps a work registered until its completion callback + * *finishes*, so the count read here is at least one and this takes the + * polling path instead of declaring the environment drained and tearing it + * down from inside the frame that is still settling a promise. + * - The poll is a timer, so it cannot run until the callback has returned to + * the host — by which time that work has left the registry. The count the + * next poll reads is the one taken after the callback finished. + * + * `__disposeWasiBinding` hands every caller the same in-flight promise, so the + * nested call joins this disposal rather than starting a second one. + */ +function __drainWasiAsyncWork() { + if (__wasiAsyncWorkDrainPromise !== undefined) { + return __wasiAsyncWorkDrainPromise + } + const exports = __napiInstance?.exports + const pending = exports?.napi_wasm_async_work_pending + const cancelPending = exports?.napi_wasm_cancel_pending_async_work + if (typeof pending !== 'function' || typeof cancelPending !== 'function') { + return + } + + const readPending = () => { + try { + return pending() + } catch (error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition — + // there is nothing left to wait for, and refusing to dispose would only + // keep a dead instance and its stuck counter alive. Best-effort here is + // the honest answer, and it is what disposal did before this drain + // existed. + // + // Only a trap. Anything else means the export is not what this loader + // thinks it is, which is a defect worth surfacing rather than disposing + // over. + if (error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw error + } + } + + if (!readPending()) { + return + } + try { + cancelPending() + } catch { + // Cancellation is an optimization: it bounds the wait by the work already + // executing. Failing it only means waiting for the whole queue instead. + } + if (!readPending()) { + return + } + + const drainPromise = __keepEventLoopAliveUntil( + (async () => { + while (readPending()) { + await new Promise((resolve) => { + __scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })(), + ).then( + () => { + __wasiAsyncWorkDrainPromise = undefined + }, + (error) => { + // A wait that could not run is not a wait that finished. The only way + // here is a host whose timers and macrotask primitives all refuse, and + // the work is still outstanding — reporting success would destroy the + // environment over it, which is the stranding this exists to prevent. + // Reject instead: disposal stays retryable, and the context is not + // destroyed. Clearing the memo first is what makes the retry re-run this. + __wasiAsyncWorkDrainPromise = undefined + throw error + }, + ) + __wasiAsyncWorkDrainPromise = drainPromise + return drainPromise +} + +/** + * `@emnapi/wasi-threads` counts a worker exit as expected only when its own + * thread manager performed the termination. A bare `worker.terminate()` reaches + * the manager's `exit` listener instead, which reports + * `worker (tid = N) sent an error! ... stopped with exit code 1` and rethrows + * inside the emit — aborting the `once('exit')` that backs the terminate + * promise, so disposal never settles and the process dies with an uncaught + * exception. Mark the termination through the manager first. + * + * The manager comes from `__getWasiThreadManager`, not from `__napiModule`: + * the initialization rollback runs on the one path where instantiation never + * returned, so `__napiModule` is still undefined there while the workers it + * spawned are already registered and loaded. + * + * Not `terminateAllThreads()`: that one recreates the pool it just shut down. + */ +function __terminateWasiWorkers() { + const cleanupErrors = [] + const pending = [] + const threadManager = __getWasiThreadManager() + + for (const worker of __wasiWorkers) { + let result + try { + if (threadManager) { + threadManager.terminateWorker(worker) + // `terminateWorker` leaves behind a reporter that logs every message + // still queued on the port, which Node flushes on exit. Nothing is + // listening for those any more. + worker.onmessage = undefined + } + result = worker.terminate() + } catch (error) { + cleanupErrors.push(error) + continue + } + if (__isThenable(result)) { + pending.push( + Promise.resolve(result).then( + () => { + __wasiWorkers.delete(worker) + }, + (error) => { + cleanupErrors.push(error) + }, + ), + ) + } else { + __wasiWorkers.delete(worker) + } + } + + const finish = () => { + if (cleanupErrors.length > 0) { + throw __createCleanupError( + cleanupErrors, + 'Failed to terminate WASI workers', + ) + } + } + return pending.length > 0 + ? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish) + : finish() +} + +function __finishWasiDisposal() { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult).then(__completeWasiDisposal) + } + return __completeWasiDisposal() +} + +function __continueWasiDisposal() { + const destroyResult = __destroyEmnapiContext() + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult).then(__finishWasiDisposal) + } + return __finishWasiDisposal() +} + +function __cleanUpWasmEnvForWasiDisposal() { + // Run the pre-teardown barrier, then let the settlements it queued actually + // reach JavaScript, and only then destroy the environment. Doing these two + // back to back is what strands them. + __prepareWasmEnvCleanup() + const drainResult = __drainWasmEnvCleanup() + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then(__continueWasiDisposal) + } + return __continueWasiDisposal() +} + +function __startWasiDisposal() { + // Outstanding `napi_async_work` goes first, while the environment is still + // completely live: the completion callbacks run addon code, and everything + // after this point takes that away from them — the barrier shuts the async + // runtime down, `Context.destroy()` stops JavaScript calls, and terminating + // the pool threads removes what would have reported the work finished. + const asyncWorkResult = __drainWasiAsyncWork() + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __cleanUpWasmEnvForWasiDisposal, + ) + } + return __cleanUpWasmEnvForWasiDisposal() +} + +/** + * Disposes this generated WASI binding. + * + * Access this function with: + * binding[Symbol.for('napi.rs.wasi.dispose')]() + */ +function __disposeWasiBinding() { + if (__wasiDisposePromise) { + return __wasiDisposePromise + } + if (__wasiDisposed) { + return Promise.resolve() + } + + let resolveDispose + let rejectDispose + const disposePromise = new Promise((resolve, reject) => { + resolveDispose = resolve + rejectDispose = reject + }) + __wasiDisposePromise = disposePromise + + let result + try { + result = __startWasiDisposal() + } catch (error) { + __wasiDisposePromise = undefined + rejectDispose(error) + return disposePromise + } + + Promise.resolve(result).then( + (value) => { + __wasiDisposed = true + resolveDispose(value) + }, + (error) => { + __wasiDisposePromise = undefined + rejectDispose(error) + }, + ) + return disposePromise +} + +function __publishWasiDispose(exports) { + Object.defineProperty(exports, __wasiDisposeSymbol, { + configurable: false, + enumerable: false, + value: __disposeWasiBinding, + writable: false, + }) +} + +function __finishWasiInitializationRollback(cleanupErrors) { + let workerResult + try { + workerResult = __terminateWasiWorkers() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return cleanupErrors + } + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => cleanupErrors) + } + return cleanupErrors +} + +function __destroyContextForWasiRollback(cleanupErrors) { + let destroyResult + try { + destroyResult = __destroyEmnapiContext() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return __finishWasiInitializationRollback(cleanupErrors) + } + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => __finishWasiInitializationRollback(cleanupErrors)) + } + return __finishWasiInitializationRollback(cleanupErrors) +} + +/** + * Leaves a rollback that could not reach the queued settlements undestroyed, and + * hands it to whatever this flavor has that can still reclaim it. + */ +function __retainFailedWasiRollback(cleanupErrors) { + try { + __retainWasiRollbackForRetry() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + } + return cleanupErrors +} + +/** + * Initialization can fail *after* registration has already run, and registration + * runs with a live environment: a module-init hook can start async work and then + * return an error, and the promise it created may already have escaped into + * JavaScript. The barrier cancels that work and *queues* the settlement, so this + * path needs the same drain the ordinary disposal does — destroying without + * yielding discards the queue with a null env and strands the promise. + * + * Stays synchronous when nothing is queued, which covers every failure before + * `beforeInit`: there is no instance to run the barrier on, so nothing to drain. + * + * A barrier or drain that did *not* finish stops the rollback short of + * destroying, which is what `dispose()` already does — a rejected drain there + * never reaches `__continueWasiDisposal`. Destroying anyway is the worse of the + * two trades, and not because of what it saves: + * + * - It cannot deliver the settlements. `Context.destroy()` runs the + * threadsafe function's cleanup hook, which drains the queue with a null env + * and discards it, so a promise that already escaped into JavaScript hangs + * forever with nothing left that could ever settle it. + * - It saves less than it looks. `Context.destroy()` stops JavaScript calls + * and runs cleanup hooks; it does not free the wasm instance or its Memory, + * which this module's scope holds either way. What stopping short retains is + * the emnapi context's bookkeeping and its un-run cleanup hooks. + * - Retry is not theoretical. A rollback that records a cleanup error is + * already kept in the process-wide registry above, so re-`require()`ing this + * file replays it instead of re-instantiating — and the `6e15de6f` flag fix + * means the replay drains again rather than skipping it. Destroying first is + * what makes that retained record useless. + * + * The residual cost is honest: the CJS flavor hands the context to its + * `process.on('exit')` teardown, so a process that never retries still reclaims + * it on the way out. The ESM browser flavor has no equivalent — a module that + * throws while evaluating is permanently errored, so re-importing rethrows + * without re-running this file — and there the context stays until the realm + * goes away. That is the deliberate choice: a hung promise is a silent liveness + * bug with no upper bound, while the retained bookkeeping is bounded by the page. + */ +function __rollbackWasiInitialization() { + // The environment teardown this rollback performs, kept nested so it cannot + // be reached without the async-work drain below running first. + function __rollbackWasmEnvForWasiInitialization() { + const cleanupErrors = [] + let drainResult + let settlementsUnreached = false + try { + __prepareWasmEnvCleanup() + drainResult = __drainWasmEnvCleanup() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + settlementsUnreached = true + } + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then( + () => __destroyContextForWasiRollback(cleanupErrors), + (cleanupError) => { + cleanupErrors.push(cleanupError) + return __retainFailedWasiRollback(cleanupErrors) + }, + ) + } + if (settlementsUnreached) { + return __retainFailedWasiRollback(cleanupErrors) + } + return __destroyContextForWasiRollback(cleanupErrors) + } + + // Same reason as `__startWasiDisposal`: a module-init hook can start async + // work before the load goes on to fail, and this rollback tears down exactly + // what those completions need. Settle them while everything is still live, + // before the barrier and the teardown above take that away. + // + // A drain that could not finish leaves async work possibly outstanding, and + // destroying the context over it would strand exactly what this rollback is + // there to settle. Stop short and retain instead — the same trade + // `__rollbackWasmEnvForWasiInitialization` makes for the settlement drain, so + // the context stays reclaimable by a retry or by this flavor's own + // last-resort teardown. + const __retainAfterAsyncWorkDrainFailure = (cleanupError) => + __retainFailedWasiRollback([cleanupError]) + let asyncWorkResult + try { + asyncWorkResult = __drainWasiAsyncWork() + } catch (cleanupError) { + return __retainAfterAsyncWorkDrainFailure(cleanupError) + } + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __rollbackWasmEnvForWasiInitialization, + __retainAfterAsyncWorkDrainFailure, + ) + } + return __rollbackWasmEnvForWasiInitialization() +} + +const __wasiRollbackRegistrySymbol = Symbol.for('napi.rs.wasi.rollback.registry.v1') +const __wasiRollbackRegistryKey = + typeof __filename === 'string' ? __filename : __wasmFilePath + +function __getWasiRollbackRegistry() { + const existing = process[__wasiRollbackRegistrySymbol] + if (existing !== undefined) { + if (!(existing instanceof Map)) { + throw new TypeError( + 'The process-wide NAPI-RS WASI rollback registry is invalid', + ) + } + return existing + } + const registry = new Map() + Object.defineProperty(process, __wasiRollbackRegistrySymbol, { + configurable: false, + enumerable: false, + value: registry, + writable: false, + }) + return registry +} + +const __wasiRollbackRegistry = __getWasiRollbackRegistry() + +function __completeWasiInitializationRollback(record, cleanupErrors) { + try { + if (cleanupErrors.length === 0) { + if ( + __wasiRollbackRegistry.get(__wasiRollbackRegistryKey) === record + ) { + __wasiRollbackRegistry.delete(__wasiRollbackRegistryKey) + } + return + } + record.error = __attachCleanupErrors(record.error, cleanupErrors) + } catch (cleanupError) { + try { + record.error = __createCleanupError( + [record.error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + } catch {} + } finally { + record.active = false + record.promise = undefined + } +} + +function __runWasiInitializationRollback(record) { + if (record.active) { + return + } + record.active = true + + let rollbackResult + try { + rollbackResult = record.rollback() + } catch (cleanupError) { + __completeWasiInitializationRollback(record, [cleanupError]) + return + } + + if (!__isThenable(rollbackResult)) { + __completeWasiInitializationRollback(record, rollbackResult) + return + } + + record.promise = Promise.resolve(rollbackResult).then( + (cleanupErrors) => { + __completeWasiInitializationRollback(record, cleanupErrors) + }, + (cleanupError) => { + __completeWasiInitializationRollback(record, [cleanupError]) + }, + ) +} + +const __pendingWasiRollback = __wasiRollbackRegistry.get( + __wasiRollbackRegistryKey, +) +if (__pendingWasiRollback !== undefined) { + __runWasiInitializationRollback(__pendingWasiRollback) + throw __pendingWasiRollback.error +} + +let __wasiModule +let __napiModule +let __wasiExitListenerRegistered = false + +function __removeWasiExitListener() { + if ( + __wasiExitListenerRegistered && + typeof process.removeListener === 'function' + ) { + process.removeListener('exit', __disposeWasiBindingAtExit) + } + __wasiExitListenerRegistered = false +} + +function __disposeWasiBindingAtExit() { + __wasiExitListenerRegistered = false + // An 'exit' handler cannot yield, so it cannot wait for queued promise + // settlements the way __startWasiDisposal does — the process is leaving and + // those promises have no observer left anyway. Run the synchronous teardown + // directly. Every step is idempotent, which also makes this the synchronous + // finish for a disposal that is still waiting for its drain. + try { + __destroyEmnapiContext() + } catch {} + try { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + void Promise.resolve(workerResult).catch(() => {}) + } + } catch {} +} + +function __registerWasiExitListener() { + if ( + !__wasiExitListenerRegistered && + typeof process.once === 'function' + ) { + process.once('exit', __disposeWasiBindingAtExit) + __wasiExitListenerRegistered = true + } +} + +__completeWasiDisposal = __removeWasiExitListener +// A rollback that could not reach the queued settlements keeps the context so +// the registry replay above can retry it. Nothing forces that replay to happen, +// so hand the context to the same synchronous teardown a successful load uses: +// a process that exits without ever retrying still runs the cleanup hooks. The +// handler cannot yield, so it does not settle anything — but by then the process +// is leaving and those promises have no observer left anyway. +__retainWasiRollbackForRetry = __registerWasiExitListener + +function __captureEmnapiAutoDestroyListener() { + if ( + typeof process.prependListener !== 'function' || + typeof process.removeListener !== 'function' + ) { + return + } + let __autoDestroyListener + const __captureListener = (__event, __listener) => { + if (__event === 'beforeExit' && __autoDestroyListener === undefined) { + __autoDestroyListener = __listener + } + } + try { + // Run before existing newListener hooks so a hook that registers its own + // beforeExit listener cannot be mistaken for emnapi's registration. + process.prependListener('newListener', __captureListener) + } catch { + return + } + return () => { + try { + process.removeListener('newListener', __captureListener) + } catch {} + if (__autoDestroyListener !== undefined) { + try { + process.removeListener('beforeExit', __autoDestroyListener) + } catch {} + } + } +} + +try { + const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener() + try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareWasmEnvCleanup, + __isPreparingWasmEnvCleanup, + ) + // emnapi 2.x still registers an unconditional once-listener for + // beforeExit that auto-destroys the context, and suppressDestroy() only + // neutralizes its callback without removing it. This loader owns cleanup + // through its 'exit' listener, so emnapi's listener is captured and + // removed; suppressDestroy() remains the safety net when removal fails. + __emnapiContext.suppressDestroy() + } finally { + // Remove only the exact emnapi callback captured above. + __finishAutoDestroyCapture?.() + } + + ;({ + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, + } = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [ + __captureWasiThreadManager, + __emnapiAsyncWorkPlugin, + __emnapiTSFNPlugin, + ], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + __publishWasiDispose(__napiModule.exports) + // The CommonJS tail below aliases `__napiModule.exports`; a named module + // export does not travel with it, so carry the marker on the binding itself + // too. Three things pin the stamp to exactly this spot: + // - inside this `try`, because the guard throws on a + // `#[napi(module_exports)]` hook that claimed the name, and only the + // catch below tears the environment — context, workers, exit listener — + // back down; + // - after the async runtime host install, which hands this same object to + // addon-provided registration functions that may put anything on it; + // - assigning onto the loader's own `module.exports`, which is still the + // original object here, so an addon accessor with a refusing setter is + // never written through. `cjs-module-lexer` — Node's CJS -> ESM named + // export detection — reads the static `module.exports. =` either + // way, and the later `module.exports = __napiModule.exports` does not + // undo that. + module.exports.__napiBindingTarget = __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) + __registerWasiExitListener() +} catch (error) { + const rollback = { + active: false, + error, + promise: undefined, + rollback: __rollbackWasiInitialization, + } + __wasiRollbackRegistry.set(__wasiRollbackRegistryKey, rollback) + __runWasiInitializationRollback(rollback) + throw rollback.error +} +module.exports = __napiModule.exports +module.exports.Severity = __napiModule.exports.Severity +module.exports.transform = __napiModule.exports.transform +module.exports.transformSync = __napiModule.exports.transformSync diff --git a/napi/transform-relay/transform-relay.wasip1.d.cts b/napi/transform-relay/transform-relay.wasip1.d.cts new file mode 100644 index 0000000000..a02655261d --- /dev/null +++ b/napi/transform-relay/transform-relay.wasip1.d.cts @@ -0,0 +1,120 @@ +/* auto-generated by NAPI-RS */ +/* eslint-disable */ + +/** The WASI flavor this loader instantiates. */ +export declare const __napiBindingTarget: 'wasm32-wasip1' + +export interface Comment { + type: 'Line' | 'Block' + value: string + start: number + end: number +} + +export interface ErrorLabel { + message: string | null + start: number + end: number +} + +export interface OxcError { + severity: Severity + message: string + labels: Array + helpMessage: string | null + codeframe: string | null +} + +export declare const enum Severity { + Error = 'Error', + Warning = 'Warning', + Advice = 'Advice', +} + +export interface SourceMap { + file?: string + mappings: string + names: Array + sourceRoot?: string + sources: Array + sourcesContent?: Array + version: number + x_google_ignoreList?: Array +} + +/** + * Apply the Relay `graphql` tagged template transform asynchronously. + * + * This uses a worker-pool thread and can be slower than `transformSync` for a + * single small module. + */ +export declare function transform(filename: string, sourceText: string, options?: TransformOptions | undefined | null): Promise + +/** + * Options for the Relay transform. + * + * `lang`, `sourceType`, and `sourcemap` configure the surrounding Oxc + * parse/codegen pipeline; the remaining fields mirror the options of + * `babel-plugin-relay` / `@swc/plugin-relay`. + */ +export interface TransformOptions { + /** Treat the source as `js`, `jsx`, `ts`, `tsx`, or `dts`. */ + lang?: 'js' | 'jsx' | 'ts' | 'tsx' | 'dts' + /** Treat the source as script, module, CommonJS, or infer it from syntax. */ + sourceType?: 'script' | 'module' | 'commonjs' | 'unambiguous' + /** + * Generate a source map. + * + * @default false + */ + sourcemap?: boolean + /** + * Directory `relay-compiler` emits all artifacts to (its + * `artifactDirectory` setting). When set, artifacts are imported via a + * relative path from the file being transformed to this directory; the + * path is computed lexically, so both must either be absolute or relative + * to the same base directory. When unset, artifacts are imported from the + * `__generated__` directory next to the file being transformed. + */ + artifactDirectory?: string + /** + * Artifact language, determining the imported file extension: + * `Name.graphql.ts` for `typescript`, `Name.graphql.js` otherwise. + * + * @default 'javascript' + */ + language?: 'typescript' | 'javascript' | 'flow' + /** + * Emit a hoisted default import per `graphql` tag instead of an inline + * `require()` call. + * + * Defaults to `true`, matching `babel-plugin-relay` since Relay v17. + * `@swc/plugin-relay` and Next.js default to `false`. + * + * @default true + */ + eagerEsModules?: boolean +} + +/** Result returned by the Relay transform. */ +export interface TransformResult { + /** + * Transformed code. + * + * This is empty when parsing, semantic analysis, option validation, or + * the Relay transform reports an error. + */ + code: string + /** Source map, populated when `sourcemap` is `true`. */ + map?: SourceMap + /** Parse, semantic, option validation, and Relay transform diagnostics. */ + errors: Array +} + +/** + * Apply the Relay `graphql` tagged template transform synchronously. + * + * Only `graphql` tags are rewritten; TypeScript and JSX syntax are preserved + * untouched, so the output composes with any downstream toolchain. + */ +export declare function transformSync(filename: string, sourceText: string, options?: TransformOptions | undefined | null): TransformResult diff --git a/napi/transform/browser.js b/napi/transform/browser.js index 7f68e26bb7..11b96999ef 100644 --- a/napi/transform/browser.js +++ b/napi/transform/browser.js @@ -1 +1 @@ -export * from '@oxc-transform/binding-wasm32-wasi' +export * from '@oxc-transform/binding-wasm32-wasip1' diff --git a/napi/transform/index.js b/napi/transform/index.js index 5204fceb5d..3fbdec882d 100644 --- a/napi/transform/index.js +++ b/napi/transform/index.js @@ -565,7 +565,7 @@ function createLoadErrorChain(errors) { // // NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict // WASI loading. It never crosses into another flavor or falls back to native. -const __napiWasiFlavors = ['wasm32-wasi'] +const __napiWasiFlavors = ['wasm32-wasi', 'wasm32-wasip1'] const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR const __napiWasiFlavorRequested = typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0 @@ -655,6 +655,27 @@ if (!nativeBinding || forceWasi) { loadErrors.push(candidateError) } } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasip1')) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('./transform.wasip1.cjs', false, ['./transform.wasm32-wasip1.debug.wasm', './transform.wasm32-wasip1.wasm']) + candidateFailed = candidateError !== null + if (!candidateFailed) { + wasiBinding = require('./transform.wasip1.cjs') + nativeBinding = wasiBinding + __napiLoadedBindingTarget = 'wasm32-wasip1' + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasi')) { let candidateError = null let candidateFailed = false @@ -682,6 +703,33 @@ if (!nativeBinding || forceWasi) { loadErrors.push(candidateError) } } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === 'wasm32-wasip1')) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('@oxc-transform/binding-wasm32-wasip1', true, undefined) + candidateFailed = candidateError !== null + if (!candidateFailed) { + if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + const bindingPackageVersion = require('@oxc-transform/binding-wasm32-wasip1/package.json').version + if (bindingPackageVersion !== '0.151.0') { + throw new Error(`WASI binding package version mismatch, expected 0.151.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } + } + wasiBinding = require('@oxc-transform/binding-wasm32-wasip1') + nativeBinding = wasiBinding + __napiLoadedBindingTarget = 'wasm32-wasip1' + wasiBindingLoaded = true + } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } if ( !wasiBindingLoaded && forceWasi && diff --git a/napi/transform/package.json b/napi/transform/package.json index e8762e789f..920f9066e6 100644 --- a/napi/transform/package.json +++ b/napi/transform/package.json @@ -45,6 +45,8 @@ "postbuild-dev": "node scripts/patch.js", "build-wasm": "pnpm run build-wasm-dev --release", "build-wasm-dev": "pnpm run build-dev --target wasm32-wasip1-threads --dts transform.wasi.d.cts", + "build-wasip1": "pnpm run build-wasip1-dev --release", + "build-wasip1-dev": "pnpm run build-dev --target wasm32-wasip1 --dts transform.wasip1.d.cts", "test": "vitest run --dir ./test" }, "devDependencies": { @@ -73,6 +75,7 @@ "riscv64gc-unknown-linux-gnu", "riscv64gc-unknown-linux-musl", "s390x-unknown-linux-gnu", + "wasm32-wasip1", "wasm32-wasip1-threads", "x86_64-apple-darwin", "x86_64-pc-windows-msvc", diff --git a/napi/transform/scripts/patch.js b/napi/transform/scripts/patch.js index 2968884432..6d6da3fd68 100644 --- a/napi/transform/scripts/patch.js +++ b/napi/transform/scripts/patch.js @@ -23,3 +23,7 @@ if (!nativeBinding && globalThis.process?.versions?.["webcontainer"]) { ` + s, ); fs.writeFileSync(path, data); + +const wasip1BrowserPath = pathJoin(packageDir, "transform.wasip1-browser.js"); +const wasip1Browser = fs.readFileSync(wasip1BrowserPath, "utf-8").replaceAll(/[ \t]+$/gmu, ""); +fs.writeFileSync(wasip1BrowserPath, wasip1Browser); diff --git a/napi/transform/transform.wasip1-browser.js b/napi/transform/transform.wasip1-browser.js new file mode 100644 index 0000000000..096e4c2f73 --- /dev/null +++ b/napi/transform/transform.wasip1-browser.js @@ -0,0 +1,994 @@ +import { + emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin as __emnapiTSFNPlugin, + instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync, + WASI as __WASI, +} from '@napi-rs/wasm-runtime' +import { createContext as __emnapiCreateContext } from '@emnapi/runtime' + + +export const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +const __wasi = new __WASI({ + version: 'preview1', +}) + +const __wasmUrl = new URL('./transform.wasm32-wasip1.wasm', import.meta.url).href +const __wasmResponse = await globalThis.fetch(__wasmUrl) +if (!__wasmResponse.ok) { + throw new Error( + 'Failed to fetch WASI module ' + + __wasmUrl + + ': ' + + __wasmResponse.status + + ' ' + + (__wasmResponse.statusText || 'Unknown Status'), + ) +} +const __wasmFile = await __wasmResponse.arrayBuffer() + +const __wasmMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, +}) +let __emnapiContext + +const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose') +const __wasiWorkers = new Set() +// The thread manager has to be reachable *before* anything that can throw +// during load or registration. Initialization can fail after the pool has +// already spawned workers, and the rollback still has to mark their +// terminations as expected — but `__napiModule` is assigned only when +// instantiation RETURNS, so on exactly that path it is still undefined. A +// plugin factory runs while the emnapi module is being created, before the +// wasm is loaded and before any registration function runs, and its context +// carries the very same manager instance. +let __wasiThreadManager + +function __captureWasiThreadManager(context) { + if (context && context.PThread) { + __wasiThreadManager = context.PThread + } + return {} +} + +function __getWasiThreadManager() { + const manager = + __wasiThreadManager !== undefined + ? __wasiThreadManager + : __napiModule + ? __napiModule.PThread + : undefined + if (manager && typeof manager.terminateWorker === 'function') { + return manager + } + return undefined +} +let __napiInstance +let __emnapiContextDestroyed = false +let __emnapiContextDestroyPromise +let __emnapiWasmEnvCleanupPrepared = false +let __emnapiWasmEnvCleanupPreparing = false +let __emnapiWasmEnvCleanupRan = false +let __emnapiWasmEnvCleanupDrained = false +let __emnapiWasmEnvCleanupDrainPromise +let __wasiDisposed = false +let __wasiAsyncWorkDrainPromise +let __wasiDisposePromise +let __completeWasiDisposal = function () {} +// Overridden by loader flavors that have a last-resort reclaim for a rollback +// that stopped short of destroying the context. See +// `__rollbackWasiInitialization`. +let __retainWasiRollbackForRetry = function () {} + +function __isThenable(value) { + return ( + value !== null && + (typeof value === 'object' || typeof value === 'function') && + typeof value.then === 'function' + ) +} + +function __createCleanupError(errors, message) { + if (errors.length === 1) { + return errors[0] + } + const __AggregateError = globalThis.AggregateError + if (typeof __AggregateError === 'function') { + return new __AggregateError(errors, message) + } + const error = new Error(message) + error.errors = errors + return error +} + +function __attachCleanupErrors(error, cleanupErrors) { + if (cleanupErrors.length === 0) { + return error + } + const cleanupError = __createCleanupError( + cleanupErrors, + 'WASI binding cleanup failed', + ) + try { + if ( + error && + (typeof error === 'object' || typeof error === 'function') + ) { + if (error.cause === undefined) { + error.cause = cleanupError + if (error.cause === cleanupError) { + return error + } + } + if (Array.isArray(error.cleanupErrors)) { + error.cleanupErrors.push(cleanupError) + return error + } else { + const attachedCleanupErrors = [cleanupError] + error.cleanupErrors = attachedCleanupErrors + if (error.cleanupErrors === attachedCleanupErrors) { + return error + } + } + } + } catch {} + const aggregate = __createCleanupError( + [error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + try { + aggregate.cause = error + } catch {} + return aggregate +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __isPreparingWasmEnvCleanup() { + return __emnapiWasmEnvCleanupPreparing +} + +function __prepareWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) { + return + } + const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup + if (typeof prepare === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __emnapiWasmEnvCleanupPreparing = true + try { + prepare() + } finally { + __emnapiWasmEnvCleanupPreparing = false + } + __emnapiWasmEnvCleanupRan = true + } + __emnapiWasmEnvCleanupPrepared = true +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (callback) { + setImmediate(callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (callback) { + const channel = new __MessageChannel() + channel.port1.onmessage = function () { + channel.port1.onmessage = null + try { + channel.port1.close() + } catch {} + try { + channel.port2.close() + } catch {} + callback() + } + channel.port2.postMessage(null) + } + } + return function (callback) { + setTimeout(callback, 0) + } +})() + +// A real, *referenced* timer, for waits that must let the whole host make +// progress between looks — the async-work drain polls the addon rather than +// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there +// would spin the loop instead of yielding it. Falls back to the macrotask +// scheduler on a host without timers. +function __scheduleTimer(callback, delay) { + const setTimer = globalThis.setTimeout + if (typeof setTimer !== 'function') { + __scheduleMacrotask(callback) + return + } + try { + setTimer(callback, delay) + } catch { + __scheduleMacrotask(callback) + } +} + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + * + * The "already drained" flag is set only once a wait has actually finished. + * Scheduling a macrotask can fail — a host-provided or patched `setImmediate` + * that throws is enough — and a disposal that rejects stays retryable, so + * marking the drain complete up front would make the retry skip it and destroy + * the context with the barrier's settlements still queued. + * + * A wait that runs out of turns with the counter still nonzero rejects with + * `ERR_NAPI_WASI_CLEANUP_PENDING` for the same reason: at that point + * "finished" is indistinguishable from the stranding above, and destroying + * would discard the very settlement the wait was for. The rejection leaves the + * flag unset and disposal retryable. + */ +function __drainWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupDrained || !__emnapiWasmEnvCleanupRan) { + return + } + if (__emnapiWasmEnvCleanupDrainPromise) { + return __emnapiWasmEnvCleanupDrainPromise + } + const pending = __napiInstance?.exports?.napi_wasm_env_cleanup_pending + const observable = typeof pending === 'function' + if (observable) { + let queued + try { + queued = pending() + } catch { + __emnapiWasmEnvCleanupDrained = true + return + } + if (!queued) { + __emnapiWasmEnvCleanupDrained = true + return + } + } + const limit = observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + const drainPromise = (async () => { + let queued = 0 + for (let turn = 0; turn < limit; turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!observable) { + continue + } + try { + queued = pending() + } catch { + return + } + if (!queued) { + return + } + } + if (!observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: the drained flag stays + // unset, dispose() (and the rollback) decline to destroy, and a later + // dispose() runs the drain again — by which time the queue has usually been + // delivered. A counter that is somehow stuck nonzero therefore costs each + // attempt at most another bounded wait and a rejection, never a stranded + // promise; the process-exit teardown still reclaims the context. + const drainError = new Error( + 'the wasm environment still reports ' + + queued + + ' queued settlement(s) after ' + + limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw drainError + })().then( + (value) => { + // Set only when the wait actually finished AND the queue was seen empty + // (or is unobservable): a drain that timed out with settlements still + // queued rejects above and must stay repeatable. + __emnapiWasmEnvCleanupDrained = true + __emnapiWasmEnvCleanupDrainPromise = undefined + return value + }, + (error) => { + __emnapiWasmEnvCleanupDrainPromise = undefined + throw error + }, + ) + __emnapiWasmEnvCleanupDrainPromise = drainPromise + return drainPromise +} + +function __destroyEmnapiContext() { + if (__emnapiContextDestroyed || __emnapiContext === undefined) { + __emnapiContextDestroyed = true + return + } + if (__emnapiContextDestroyPromise) { + return __emnapiContextDestroyPromise + } + + __prepareWasmEnvCleanup() + const result = __emnapiContext.destroy() + if (!__isThenable(result)) { + __emnapiContextDestroyed = true + return + } + + const destroyPromise = Promise.resolve(result).then( + (value) => { + __emnapiContextDestroyed = true + return value + }, + (error) => { + __emnapiContextDestroyPromise = undefined + throw error + }, + ) + __emnapiContextDestroyPromise = destroyPromise + return destroyPromise +} + +/** + * Holds the event loop open until `work` settles. + * + * Nothing else can: the pool workers are deliberately unreferenced so an idle + * binding cannot keep a process alive, and referencing them again for the + * termination does not hold either — emnapi unreferences a worker the moment it + * reports `async-thread-ready`, which for a worker that was still starting + * lands *after* the termination began. Without a handle of its own, an + * `await dispose()` with nothing else pending exits the process with its + * promise unsettled, and everything after the `await` is skipped. + * + * The timer is cleared as soon as the work settles, so this never outlives the + * disposal that asked for it. + */ +function __keepEventLoopAliveUntil(work) { + const setTimer = globalThis.setInterval + const clearTimer = globalThis.clearInterval + if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') { + return work + } + let timer + try { + timer = setTimer(function () {}, 50) + } catch { + return work + } + const release = function () { + try { + clearTimer(timer) + } catch {} + } + return work.then( + (value) => { + release() + return value + }, + (error) => { + release() + throw error + }, + ) +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this addon's outstanding `napi_async_work` before the teardown that + * would strand it. + * + * `napi_prepare_wasm_env_cleanup` does not cover async work, and nothing about + * it is observable from JavaScript: the threadless archive resolves + * `napi_*_async_work` through the `@emnapi/core` plugins, but the threaded one + * links the C `async_work.c` on the uv threadpool, so there the wasm neither + * imports nor exports those symbols and the only brackets a loader could watch + * (`_emnapi_ctx_*_waiting_request_counter`) are shared with threadsafe + * functions. The addon is the one place both flavors go through, so it answers + * for both, through the same kind of handshake the settlement drain uses: + * + * - `napi_wasm_cancel_pending_async_work()` cancels what no thread has + * started. Those completion callbacks run with `napi_cancelled`, which + * napi-rs turns into a promise rejected with an `AbortError`. + * - `napi_wasm_async_work_pending()` counts what is still owed a completion + * callback. Work already executing refuses cancellation and stays counted + * until it finishes normally — which it can, because this runs before the + * barrier, before `Context.destroy()` and before anything is terminated. + * + * Both exports are optional: an addon built against a napi crate that predates + * them drains nothing and keeps the previous behavior, exactly as the + * `napi_wasm_env_cleanup_pending` handshake degrades. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. The promise it returns otherwise never + * rejects. + * + * The wait has no deadline, and that is the point: giving up would destroy the + * environment with a completion callback still owed, which is the stranding + * this exists to prevent. A task whose `execute` never returns already keeps an + * *undisposed* process alive in exactly the same way, so disposal inherits that + * rather than inventing a bound it cannot honor. + * + * Safe to call from inside a completion callback, which is reachable: settling + * a task runs addon code that can re-enter JavaScript — a setter on the value + * being handed back, a threadsafe-function callback — and that JavaScript can + * call `dispose()`. Two things make it terminate rather than wait on itself: + * + * - The addon keeps a work registered until its completion callback + * *finishes*, so the count read here is at least one and this takes the + * polling path instead of declaring the environment drained and tearing it + * down from inside the frame that is still settling a promise. + * - The poll is a timer, so it cannot run until the callback has returned to + * the host — by which time that work has left the registry. The count the + * next poll reads is the one taken after the callback finished. + * + * `__disposeWasiBinding` hands every caller the same in-flight promise, so the + * nested call joins this disposal rather than starting a second one. + */ +function __drainWasiAsyncWork() { + if (__wasiAsyncWorkDrainPromise !== undefined) { + return __wasiAsyncWorkDrainPromise + } + const exports = __napiInstance?.exports + const pending = exports?.napi_wasm_async_work_pending + const cancelPending = exports?.napi_wasm_cancel_pending_async_work + if (typeof pending !== 'function' || typeof cancelPending !== 'function') { + return + } + + const readPending = () => { + try { + return pending() + } catch (error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition — + // there is nothing left to wait for, and refusing to dispose would only + // keep a dead instance and its stuck counter alive. Best-effort here is + // the honest answer, and it is what disposal did before this drain + // existed. + // + // Only a trap. Anything else means the export is not what this loader + // thinks it is, which is a defect worth surfacing rather than disposing + // over. + if (error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw error + } + } + + if (!readPending()) { + return + } + try { + cancelPending() + } catch { + // Cancellation is an optimization: it bounds the wait by the work already + // executing. Failing it only means waiting for the whole queue instead. + } + if (!readPending()) { + return + } + + const drainPromise = __keepEventLoopAliveUntil( + (async () => { + while (readPending()) { + await new Promise((resolve) => { + __scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })(), + ).then( + () => { + __wasiAsyncWorkDrainPromise = undefined + }, + (error) => { + // A wait that could not run is not a wait that finished. The only way + // here is a host whose timers and macrotask primitives all refuse, and + // the work is still outstanding — reporting success would destroy the + // environment over it, which is the stranding this exists to prevent. + // Reject instead: disposal stays retryable, and the context is not + // destroyed. Clearing the memo first is what makes the retry re-run this. + __wasiAsyncWorkDrainPromise = undefined + throw error + }, + ) + __wasiAsyncWorkDrainPromise = drainPromise + return drainPromise +} + +/** + * `@emnapi/wasi-threads` counts a worker exit as expected only when its own + * thread manager performed the termination. A bare `worker.terminate()` reaches + * the manager's `exit` listener instead, which reports + * `worker (tid = N) sent an error! ... stopped with exit code 1` and rethrows + * inside the emit — aborting the `once('exit')` that backs the terminate + * promise, so disposal never settles and the process dies with an uncaught + * exception. Mark the termination through the manager first. + * + * The manager comes from `__getWasiThreadManager`, not from `__napiModule`: + * the initialization rollback runs on the one path where instantiation never + * returned, so `__napiModule` is still undefined there while the workers it + * spawned are already registered and loaded. + * + * Not `terminateAllThreads()`: that one recreates the pool it just shut down. + */ +function __terminateWasiWorkers() { + const cleanupErrors = [] + const pending = [] + const threadManager = __getWasiThreadManager() + + for (const worker of __wasiWorkers) { + let result + try { + if (threadManager) { + threadManager.terminateWorker(worker) + // `terminateWorker` leaves behind a reporter that logs every message + // still queued on the port, which Node flushes on exit. Nothing is + // listening for those any more. + worker.onmessage = undefined + } + result = worker.terminate() + } catch (error) { + cleanupErrors.push(error) + continue + } + if (__isThenable(result)) { + pending.push( + Promise.resolve(result).then( + () => { + __wasiWorkers.delete(worker) + }, + (error) => { + cleanupErrors.push(error) + }, + ), + ) + } else { + __wasiWorkers.delete(worker) + } + } + + const finish = () => { + if (cleanupErrors.length > 0) { + throw __createCleanupError( + cleanupErrors, + 'Failed to terminate WASI workers', + ) + } + } + return pending.length > 0 + ? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish) + : finish() +} + +function __finishWasiDisposal() { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult).then(__completeWasiDisposal) + } + return __completeWasiDisposal() +} + +function __continueWasiDisposal() { + const destroyResult = __destroyEmnapiContext() + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult).then(__finishWasiDisposal) + } + return __finishWasiDisposal() +} + +function __cleanUpWasmEnvForWasiDisposal() { + // Run the pre-teardown barrier, then let the settlements it queued actually + // reach JavaScript, and only then destroy the environment. Doing these two + // back to back is what strands them. + __prepareWasmEnvCleanup() + const drainResult = __drainWasmEnvCleanup() + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then(__continueWasiDisposal) + } + return __continueWasiDisposal() +} + +function __startWasiDisposal() { + // Outstanding `napi_async_work` goes first, while the environment is still + // completely live: the completion callbacks run addon code, and everything + // after this point takes that away from them — the barrier shuts the async + // runtime down, `Context.destroy()` stops JavaScript calls, and terminating + // the pool threads removes what would have reported the work finished. + const asyncWorkResult = __drainWasiAsyncWork() + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __cleanUpWasmEnvForWasiDisposal, + ) + } + return __cleanUpWasmEnvForWasiDisposal() +} + +/** + * Disposes this generated WASI binding. + * + * Access this function with: + * binding[Symbol.for('napi.rs.wasi.dispose')]() + */ +function __disposeWasiBinding() { + if (__wasiDisposePromise) { + return __wasiDisposePromise + } + if (__wasiDisposed) { + return Promise.resolve() + } + + let resolveDispose + let rejectDispose + const disposePromise = new Promise((resolve, reject) => { + resolveDispose = resolve + rejectDispose = reject + }) + __wasiDisposePromise = disposePromise + + let result + try { + result = __startWasiDisposal() + } catch (error) { + __wasiDisposePromise = undefined + rejectDispose(error) + return disposePromise + } + + Promise.resolve(result).then( + (value) => { + __wasiDisposed = true + resolveDispose(value) + }, + (error) => { + __wasiDisposePromise = undefined + rejectDispose(error) + }, + ) + return disposePromise +} + +function __publishWasiDispose(exports) { + Object.defineProperty(exports, __wasiDisposeSymbol, { + configurable: false, + enumerable: false, + value: __disposeWasiBinding, + writable: false, + }) +} + +function __finishWasiInitializationRollback(cleanupErrors) { + let workerResult + try { + workerResult = __terminateWasiWorkers() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return cleanupErrors + } + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => cleanupErrors) + } + return cleanupErrors +} + +function __destroyContextForWasiRollback(cleanupErrors) { + let destroyResult + try { + destroyResult = __destroyEmnapiContext() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return __finishWasiInitializationRollback(cleanupErrors) + } + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => __finishWasiInitializationRollback(cleanupErrors)) + } + return __finishWasiInitializationRollback(cleanupErrors) +} + +/** + * Leaves a rollback that could not reach the queued settlements undestroyed, and + * hands it to whatever this flavor has that can still reclaim it. + */ +function __retainFailedWasiRollback(cleanupErrors) { + try { + __retainWasiRollbackForRetry() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + } + return cleanupErrors +} + +/** + * Initialization can fail *after* registration has already run, and registration + * runs with a live environment: a module-init hook can start async work and then + * return an error, and the promise it created may already have escaped into + * JavaScript. The barrier cancels that work and *queues* the settlement, so this + * path needs the same drain the ordinary disposal does — destroying without + * yielding discards the queue with a null env and strands the promise. + * + * Stays synchronous when nothing is queued, which covers every failure before + * `beforeInit`: there is no instance to run the barrier on, so nothing to drain. + * + * A barrier or drain that did *not* finish stops the rollback short of + * destroying, which is what `dispose()` already does — a rejected drain there + * never reaches `__continueWasiDisposal`. Destroying anyway is the worse of the + * two trades, and not because of what it saves: + * + * - It cannot deliver the settlements. `Context.destroy()` runs the + * threadsafe function's cleanup hook, which drains the queue with a null env + * and discards it, so a promise that already escaped into JavaScript hangs + * forever with nothing left that could ever settle it. + * - It saves less than it looks. `Context.destroy()` stops JavaScript calls + * and runs cleanup hooks; it does not free the wasm instance or its Memory, + * which this module's scope holds either way. What stopping short retains is + * the emnapi context's bookkeeping and its un-run cleanup hooks. + * - Retry is not theoretical. A rollback that records a cleanup error is + * already kept in the process-wide registry above, so re-`require()`ing this + * file replays it instead of re-instantiating — and the `6e15de6f` flag fix + * means the replay drains again rather than skipping it. Destroying first is + * what makes that retained record useless. + * + * The residual cost is honest: the CJS flavor hands the context to its + * `process.on('exit')` teardown, so a process that never retries still reclaims + * it on the way out. The ESM browser flavor has no equivalent — a module that + * throws while evaluating is permanently errored, so re-importing rethrows + * without re-running this file — and there the context stays until the realm + * goes away. That is the deliberate choice: a hung promise is a silent liveness + * bug with no upper bound, while the retained bookkeeping is bounded by the page. + */ +function __rollbackWasiInitialization() { + // The environment teardown this rollback performs, kept nested so it cannot + // be reached without the async-work drain below running first. + function __rollbackWasmEnvForWasiInitialization() { + const cleanupErrors = [] + let drainResult + let settlementsUnreached = false + try { + __prepareWasmEnvCleanup() + drainResult = __drainWasmEnvCleanup() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + settlementsUnreached = true + } + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then( + () => __destroyContextForWasiRollback(cleanupErrors), + (cleanupError) => { + cleanupErrors.push(cleanupError) + return __retainFailedWasiRollback(cleanupErrors) + }, + ) + } + if (settlementsUnreached) { + return __retainFailedWasiRollback(cleanupErrors) + } + return __destroyContextForWasiRollback(cleanupErrors) + } + + // Same reason as `__startWasiDisposal`: a module-init hook can start async + // work before the load goes on to fail, and this rollback tears down exactly + // what those completions need. Settle them while everything is still live, + // before the barrier and the teardown above take that away. + // + // A drain that could not finish leaves async work possibly outstanding, and + // destroying the context over it would strand exactly what this rollback is + // there to settle. Stop short and retain instead — the same trade + // `__rollbackWasmEnvForWasiInitialization` makes for the settlement drain, so + // the context stays reclaimable by a retry or by this flavor's own + // last-resort teardown. + const __retainAfterAsyncWorkDrainFailure = (cleanupError) => + __retainFailedWasiRollback([cleanupError]) + let asyncWorkResult + try { + asyncWorkResult = __drainWasiAsyncWork() + } catch (cleanupError) { + return __retainAfterAsyncWorkDrainFailure(cleanupError) + } + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __rollbackWasmEnvForWasiInitialization, + __retainAfterAsyncWorkDrainFailure, + ) + } + return __rollbackWasmEnvForWasiInitialization() +} + +let __wasiModule +let __napiModule + +try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareWasmEnvCleanup, + __isPreparingWasmEnvCleanup, + ) + __emnapiContext.suppressDestroy() + + ;({ + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, + } = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [ + __captureWasiThreadManager, + __emnapiAsyncWorkPlugin, + __emnapiTSFNPlugin, + ], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + __publishWasiDispose(__napiModule.exports) + // The default export hands out this object; a named module export does not + // travel with it, so carry the marker on the binding itself too. After the + // host install, which hands the same object to addon-provided registration + // functions that may put anything on it, and inside this `try`, so a claimed + // name fails the load through the rollback below rather than past it. + __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) +} catch (error) { + const cleanupErrors = await __rollbackWasiInitialization() + throw __attachCleanupErrors(error, cleanupErrors) +} +export default __napiModule.exports +export const Severity = __napiModule.exports.Severity +export const HelperMode = __napiModule.exports.HelperMode +export const isolatedDeclaration = __napiModule.exports.isolatedDeclaration +export const isolatedDeclarationSync = __napiModule.exports.isolatedDeclarationSync +export const moduleRunnerTransform = __napiModule.exports.moduleRunnerTransform +export const moduleRunnerTransformSync = __napiModule.exports.moduleRunnerTransformSync +export const transform = __napiModule.exports.transform +export const transformSync = __napiModule.exports.transformSync diff --git a/napi/transform/transform.wasip1-deferred.d.ts b/napi/transform/transform.wasip1-deferred.d.ts new file mode 100644 index 0000000000..76ad672d3e --- /dev/null +++ b/napi/transform/transform.wasip1-deferred.d.ts @@ -0,0 +1,83 @@ +export type WasiBinding = typeof import('./transform.wasip1.cjs') + +export type WasiModuleInput = + | WebAssembly.Module + | PromiseLike + +/** Run the instance on a linear memory the caller allocated. */ +export interface WasiCallerMemoryOptions { + /** + * A caller-allocated linear memory for this instance. It must be unshared + * and created in this loader's own realm — the WASI and emnapi layers + * underneath identify a Memory with a realm-local `instanceof`, so one from + * a `node:vm` context or another frame is rejected. It is single-use: once + * a validated initialization attempt has begun, the same Memory cannot be + * passed again — including after that attempt failed, and after the instance + * was disposed. + */ + memory: WebAssembly.Memory + /** Not available beside `memory`: the loader allocates neither. */ + initialMemoryPages?: never + /** Not available beside `memory`: the loader allocates neither. */ + maximumMemoryPages?: never +} + +/** Let the loader allocate the linear memory, optionally sized. */ +export interface WasiAllocatedMemoryOptions { + /** Not available beside the page counts: they size the loader's own Memory. */ + memory?: never + /** @default WASM_MEMORY.initialPages */ + initialMemoryPages?: number + /** @default WASM_MEMORY.maximumPages */ + maximumMemoryPages?: number +} + +/** + * Either memory form, never a mix of the two: the loader throws a TypeError + * on `memory` beside a page count. `{}` and an omitted argument select the + * loader defaults. + */ +export type WasiInstanceOptions = + | WasiCallerMemoryOptions + | WasiAllocatedMemoryOptions + +export interface WasiRuntimeStats { + /** Instances created by this evaluated loader module, not process-wide. */ + createdInstances: number + /** Created instances whose dispose() has not completed. */ + liveInstances: number + /** Declared initial address space, not committed memory. */ + declaredInitialMemoryBytes: number +} + +export interface WasiInstance { + readonly exports: WasiBinding + /** This instance's linear memory. Claimed, so it cannot start another one. */ + readonly memory: WebAssembly.Memory + /** Current linear-memory size; 0 once dispose() has completed. */ + readonly memoryBytes: number + readonly disposed: boolean + dispose(): Promise +} + +/** The memory descriptor compiled into this loader. */ +export const WASM_MEMORY: Readonly<{ + initialPages: number + maximumPages: number + pageBytes: number + initialBytes: number + maximumBytes: number +}> + +export function getDeferredRuntimeStats(): Readonly + +export function instantiate(wasmInput: WasiModuleInput): Promise +export function createInstance( + wasmInput: WasiModuleInput, + options?: WasiInstanceOptions, +): Promise +/** Dispose the singleton and retry retained failed-initialization cleanup. */ +export function dispose(): Promise + +/** The WASI flavor this deferred loader instantiates. */ +export declare const __napiBindingTarget: 'wasm32-wasip1' diff --git a/napi/transform/transform.wasip1-deferred.js b/napi/transform/transform.wasip1-deferred.js new file mode 100644 index 0000000000..3f54deb8ec --- /dev/null +++ b/napi/transform/transform.wasip1-deferred.js @@ -0,0 +1,1488 @@ +import { + emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin as __emnapiTSFNPlugin, + instantiateNapiModule as __emnapiInstantiateNapiModule, + WASI as __WASI, +} from '@napi-rs/wasm-runtime' +import { createContext as __emnapiCreateContext } from '@emnapi/runtime' + + +export const WASM_MEMORY = Object.freeze({ + initialPages: 1024, + maximumPages: 65536, + pageBytes: 65536, + initialBytes: 1024 * 65536, + maximumBytes: 65536 * 65536, +}) + +let __createdInstances = 0 +let __liveInstances = 0 + +/** + * Counters for instances created by THIS module evaluation, not process-wide: + * a second bundled copy of this loader keeps its own. Only successfully + * created instances are counted, and `liveInstances` drops when an instance's + * `dispose()` resolves. + * + * `declaredInitialMemoryBytes` is declared address space, not a host's + * committed-memory metric; pair it with host telemetry rather than treating it + * as a quota. + */ +export function getDeferredRuntimeStats() { + return Object.freeze({ + createdInstances: __createdInstances, + liveInstances: __liveInstances, + declaredInitialMemoryBytes: WASM_MEMORY.initialBytes, + }) +} + +const __arrayBufferByteLengthGetter = Object.getOwnPropertyDescriptor( + ArrayBuffer.prototype, + 'byteLength', +).get +const __memoryBufferGetter = Object.getOwnPropertyDescriptor( + WebAssembly.Memory.prototype, + 'buffer', +).get +// One managed initialization per Memory, success or failure: an attempt that +// throws may already have written into linear memory, so the bytes are not a +// clean slate for a second instance. Module-local, like the counters above. +const __claimedMemories = new WeakSet() + +function __resolveInstanceMemory(__options) { + const __provided = __options == null ? undefined : __options.memory + if (__provided === undefined || __provided === null) { + // Page counts are handed to the engine unvalidated: it already rejects a + // negative, over-4GiB or below-maximum value with a precise message, and a + // second set of bounds here would only drift from it. + const __allocated = new WebAssembly.Memory({ + initial: + __options != null && __options.initialMemoryPages !== undefined + ? __options.initialMemoryPages + : WASM_MEMORY.initialPages, + maximum: + __options != null && __options.maximumMemoryPages !== undefined + ? __options.maximumMemoryPages + : WASM_MEMORY.maximumPages, + }) + // Claimed like a caller-provided one. The handle publishes it as + // `instance.memory`, so handing it back to `createInstance()` is as easy + // as passing your own twice, and it would put two live instances on one + // linear memory: each initialization rewrites the emnapi/WASI state the + // other is still running on. + __claimedMemories.add(__allocated) + return __allocated + } + if ( + __options.initialMemoryPages !== undefined || + __options.maximumMemoryPages !== undefined + ) { + throw new TypeError( + 'Pass either memory or initialMemoryPages/maximumMemoryPages, not both', + ) + } + let __buffer + try { + // Brand check: the getter throws for anything that is not a genuine + // WebAssembly.Memory, including a cross-realm look-alike object. + __buffer = Reflect.apply(__memoryBufferGetter, __provided, []) + } catch { + throw new TypeError('memory must be an unshared WebAssembly.Memory') + } + try { + // Throws for a SharedArrayBuffer. This loader has no threads, and shared + // growth does not detach: external views handed to the addon would + // silently outlive the bytes they describe. + Reflect.apply(__arrayBufferByteLengthGetter, __buffer, []) + } catch { + throw new TypeError( + 'The deferred loader requires an unshared WebAssembly.Memory', + ) + } + // The intrinsic getters above accept a genuine Memory from ANY realm, but + // the loader's dependencies do not: `WASI.setMemory` in + // `@napi-rs/wasm-runtime` and emnapi identify a Memory with a realm-local + // `instanceof`. A Memory built in another realm (a `node:vm` context, a + // same-origin iframe) would pass every check here and only fail deep inside + // initialization. Reject it up front, and before the claim below, so the + // caller keeps it usable in the realm that made it. + if (!(__provided instanceof WebAssembly.Memory)) { + throw new TypeError( + 'memory must be a WebAssembly.Memory created in the same realm as this loader', + ) + } + if (__claimedMemories.has(__provided)) { + throw new TypeError( + 'This WebAssembly.Memory has already been used for a deferred initialization attempt and cannot be reused, including after a failed initialization or a disposal', + ) + } + // Last step, after every check: a rejected option bag must leave the Memory + // unclaimed, or a caller could not fix the call and retry with it. + __claimedMemories.add(__provided) + return __provided +} + +export const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +/** + * Deferred, workerd-safe instantiation: no top-level I/O, no compile-from-bytes. + * Accepts ONLY a precompiled WebAssembly.Module, or a Promise resolving to one + * (e.g. `import mod from './transform.wasm32-wasip1.wasm'` under a CompiledWasm + * module rule / wrangler module import). Byte buffers, URLs and Response + * objects are rejected: they require dynamic Wasm compilation, which + * Cloudflare Workers disallows. + */ +async function __resolveModule(__wasmInput) { + const __module = await __wasmInput + // Brand check, not `instanceof`: `WebAssembly.Module.imports` throws unless + // its argument is a genuine WebAssembly.Module, so prototype-spoofed byte + // buffers are rejected while cross-realm Module instances are accepted. + try { + WebAssembly.Module.imports(__module) + } catch { + throw new TypeError( + "instantiate() and createInstance() expect a precompiled WebAssembly.Module (or a Promise resolving to one), " + + "e.g. import mod from './transform.wasm32-wasip1.wasm' under a CompiledWasm module rule / wrangler module import. " + + "Byte buffers, URLs and Response objects require dynamic Wasm compilation, which Cloudflare Workers disallows.", + ) + } + return __module +} + +let __normalizedModules + +function __rememberNormalizedModule(__module, __normalizedModule) { + if (!__normalizedModules) { + __normalizedModules = new WeakMap() + } + __normalizedModules.set(__module, __normalizedModule) + return __normalizedModule +} + +async function __normalizeModuleForEmnapi(__module) { + if (__module instanceof WebAssembly.Module) { + return __module + } + if (__normalizedModules) { + const __normalizedModule = __normalizedModules.get(__module) + if (__normalizedModule) { + return __normalizedModule + } + } + // @emnapi/core currently performs realm-local `instanceof` checks after + // accepting the module. Structured cloning preserves compiled code without + // compiling bytes and produces a Module owned by the current realm. + if (typeof structuredClone === 'function') { + try { + const __normalizedModule = structuredClone(__module) + if (__normalizedModule instanceof WebAssembly.Module) { + return __rememberNormalizedModule(__module, __normalizedModule) + } + } catch {} + } + // MessageChannel uses the same structured-clone semantics and covers older + // browser/Node hosts that expose it but not the structuredClone function. + if (typeof MessageChannel === 'function') { + let __channel + try { + __channel = new MessageChannel() + const __normalizedModule = await new Promise((resolve, reject) => { + __channel.port1.onmessage = (event) => resolve(event.data) + __channel.port1.onmessageerror = () => + reject(new TypeError('Failed to clone WebAssembly.Module')) + try { + __channel.port2.postMessage(__module) + } catch (error) { + reject(error) + } + }) + if (__normalizedModule instanceof WebAssembly.Module) { + return __rememberNormalizedModule(__module, __normalizedModule) + } + } catch { + } finally { + try { + __channel?.port1.close() + } catch {} + try { + __channel?.port2.close() + } catch {} + } + } + // Last-resort compatibility for genuine, extensible foreign Modules. + try { + Object.setPrototypeOf(__module, WebAssembly.Module.prototype) + } catch {} + if (__module instanceof WebAssembly.Module) { + return __module + } + throw new TypeError( + 'This host cannot normalize a cross-realm WebAssembly.Module; ' + + 'provide structuredClone or MessageChannel support.', + ) +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __captureEmnapiAutoDestroyListener(__process) { + if ( + !__process || + typeof __process.prependListener !== 'function' || + typeof __process.removeListener !== 'function' + ) { + return + } + let __autoDestroyListener + const __captureListener = (__event, __listener) => { + if (__event === 'beforeExit' && __autoDestroyListener === undefined) { + __autoDestroyListener = __listener + } + } + try { + // Run before existing newListener hooks so a hook that registers its own + // beforeExit listener cannot be mistaken for emnapi's registration. + __process.prependListener('newListener', __captureListener) + } catch { + return + } + return () => { + try { + __process.removeListener('newListener', __captureListener) + } catch {} + if (__autoDestroyListener !== undefined) { + try { + __process.removeListener('beforeExit', __autoDestroyListener) + } catch {} + } + } +} + +function __attachCleanupError(__error, __cleanupError) { + try { + if ( + __error && + (typeof __error === 'object' || typeof __error === 'function') && + __error.cause === undefined + ) { + __error.cause = __cleanupError + } + } catch {} +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (__callback) { + setImmediate(__callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (__callback) { + const __channel = new __MessageChannel() + __channel.port1.onmessage = function () { + __channel.port1.onmessage = null + try { + __channel.port1.close() + } catch {} + try { + __channel.port2.close() + } catch {} + __callback() + } + __channel.port2.postMessage(null) + } + } + return function (__callback) { + setTimeout(__callback, 0) + } +})() + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + */ +function __drainWasmEnvCleanup(__instance) { + const __pending = __instance?.exports.napi_wasm_env_cleanup_pending + const __observable = typeof __pending === 'function' + if (__observable) { + let __queued + try { + __queued = __pending() + } catch { + return + } + if (!__queued) { + return + } + } + const __limit = __observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + return (async () => { + let __queued = 0 + for (let __turn = 0; __turn < __limit; __turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!__observable) { + continue + } + try { + __queued = __pending() + } catch { + return + } + if (!__queued) { + return + } + } + if (!__observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: `__prepareForDisposal` + // leaves its drained flag unset, dispose() (and the instantiation-failure + // path) decline to destroy, and a later dispose() runs the drain again — by + // which time the queue has usually been delivered. A counter that is + // somehow stuck nonzero therefore costs each attempt at most another + // bounded wait and a rejection, never a stranded promise; the managed + // beforeExit destroyer still reclaims the context. + const __drainError = new Error( + 'the wasm environment still reports ' + + __queued + + ' queued settlement(s) after ' + + __limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + __drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw __drainError + })() +} + +// A real, *referenced* timer for the async-work wait below, which polls the +// addon rather than interleaving with the @emnapi/core dispatch: a zero-delay +// macrotask there would spin the loop instead of yielding it. Falls back to the +// macrotask scheduler on a host without timers. +function __scheduleTimer(__callback, __delay) { + const __setTimer = globalThis.setTimeout + if (typeof __setTimer !== 'function') { + __scheduleMacrotask(__callback) + return + } + try { + __setTimer(__callback, __delay) + } catch { + __scheduleMacrotask(__callback) + } +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this instance's outstanding `napi_async_work` before the teardown + * that would strand it. + * + * The same hole the eager loaders had, and the same fix: the environment + * cleanup barrier covers promise settlements queued on the threadsafe-function + * queue and says nothing about `napi_async_work`, so destroying the context + * with a work still outstanding leaves its completion callback with nowhere to + * run and its promise unsettled forever. + * + * This flavor is threadless, so `compute` runs on the JavaScript thread inside + * the macrotask that dequeued it: while this is running, any outstanding work + * is queued rather than executing, and `napi_wasm_cancel_pending_async_work` + * can take all of it. The poll is still what decides when the drain is done — + * cancellation delivers those completions from a later macrotask, not + * synchronously. + * + * Per instance, from that instance's own exports: two instances of this loader + * have separate registries and must not wait on each other. + * + * Both exports are optional, so an addon built against a napi crate that + * predates them keeps the previous behavior. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. No deadline: giving up would destroy the + * environment with a completion callback still owed. + */ +function __drainInstanceAsyncWork(__instance) { + const __exports = __instance?.exports + const __pending = __exports?.napi_wasm_async_work_pending + const __cancelPending = __exports?.napi_wasm_cancel_pending_async_work + if (typeof __pending !== 'function' || typeof __cancelPending !== 'function') { + return + } + + const __readPending = () => { + try { + return __pending() + } catch (__error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition and + // best-effort is the honest answer. Anything else means the export is not + // what this loader thinks it is — a defect worth surfacing. + if (__error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw __error + } + } + + if (!__readPending()) { + return + } + try { + __cancelPending() + } catch { + // Cancellation only bounds the wait. Failing it means waiting for the queue + // to run instead, which the poll below already does. + } + if (!__readPending()) { + return + } + + return (async () => { + while (__readPending()) { + await new Promise((__resolve) => { + __scheduleTimer(__resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })() +} + +function __createLifecycleReentryError(__operation) { + const __error = new Error( + __operation + + '() cannot run while an emnapi Context.destroy() call is still active; await the original cleanup promise instead.', + ) + __error.code = 'ERR_NAPI_WASI_LIFECYCLE_REENTRY' + return __error +} + +const __managedEmnapiContextDestroyers = new Set() +let __managedCleanupProcess +let __managedBeforeExitListener +let __managedDestroyPromise +let __managedDestroyersInFlight +let __managedBeforeExitRegistrationRetryCount = 0 +let __managedBeforeExitRegistrationRetryScheduled = false +let __moduleLifecycleDestroyDepth = 0 + +function __removeManagedEmnapiCleanupListeners() { + const __process = __managedCleanupProcess + const __beforeExitListener = __managedBeforeExitListener + __managedCleanupProcess = undefined + __managedBeforeExitListener = undefined + __managedBeforeExitRegistrationRetryCount = 0 + if (__process && __beforeExitListener) { + try { + __process.removeListener('beforeExit', __beforeExitListener) + } catch {} + } +} + +function __scheduleManagedBeforeExitListenerRegistration() { + if ( + !__managedCleanupProcess || + __managedBeforeExitListener || + __managedEmnapiContextDestroyers.size === 0 || + __managedBeforeExitRegistrationRetryScheduled || + __managedBeforeExitRegistrationRetryCount >= 3 + ) { + return + } + __managedBeforeExitRegistrationRetryScheduled = true + __managedBeforeExitRegistrationRetryCount++ + queueMicrotask(() => { + __managedBeforeExitRegistrationRetryScheduled = false + if ( + !__managedCleanupProcess || + __managedBeforeExitListener || + __managedEmnapiContextDestroyers.size === 0 + ) { + return + } + try { + __registerManagedBeforeExitListener() + } catch {} + }) +} + +function __registerManagedBeforeExitListener() { + if (!__managedCleanupProcess || __managedBeforeExitListener) { + return + } + try { + __managedCleanupProcess.once( + 'beforeExit', + __destroyManagedEmnapiContextsBeforeExit, + ) + } catch (error) { + __scheduleManagedBeforeExitListenerRegistration() + throw error + } + __managedBeforeExitListener = __destroyManagedEmnapiContextsBeforeExit + __managedBeforeExitRegistrationRetryCount = 0 +} + +function __settleManagedEmnapiContextDestroy(__promise) { + if (__managedDestroyPromise === __promise) { + __managedDestroyPromise = undefined + __managedDestroyersInFlight = undefined + } + if (__managedEmnapiContextDestroyers.size === 0) { + __removeManagedEmnapiCleanupListeners() + return + } + try { + __registerManagedBeforeExitListener() + } catch {} +} + +function __destroyManagedEmnapiContexts(__excludedDestroyers) { + if (__managedDestroyPromise) { + return __managedDestroyPromise + } + const __destroyers = Array.from(__managedEmnapiContextDestroyers).filter( + (__destroy) => !__excludedDestroyers?.has(__destroy), + ) + if (__destroyers.length === 0) { + return Promise.resolve() + } + let __resolveDestroy + let __rejectDestroy + const __promise = new Promise((resolve, reject) => { + __resolveDestroy = resolve + __rejectDestroy = reject + }) + __managedDestroyPromise = __promise + __managedDestroyersInFlight = new Set(__destroyers) + void Promise.all( + __destroyers.map((__destroy) => { + try { + return Promise.resolve(__destroy()).then( + () => ({ failed: false }), + (error) => ({ failed: true, error }), + ) + } catch (error) { + return { failed: true, error } + } + }), + ).then((__results) => { + let __primaryError + let __failed = false + for (const __result of __results) { + if (!__result.failed) { + continue + } + if (!__failed) { + __failed = true + __primaryError = __result.error + } else { + __attachCleanupError(__primaryError, __result.error) + } + } + if (__failed) { + __rejectDestroy(__primaryError) + } else { + __resolveDestroy() + } + }, __rejectDestroy) + void __promise.then( + () => { + __settleManagedEmnapiContextDestroy(__promise) + }, + () => { + __settleManagedEmnapiContextDestroy(__promise) + }, + ) + return __promise +} + +async function __drainManagedEmnapiContexts(__excludedDestroyers) { + const __attemptedDestroyers = new Set(__excludedDestroyers) + let __primaryError + let __failed = false + while (true) { + let __promise = __managedDestroyPromise + let __destroyers = __managedDestroyersInFlight + if (!__promise) { + __promise = __destroyManagedEmnapiContexts(__attemptedDestroyers) + __destroyers = __managedDestroyersInFlight + if (!__destroyers) { + break + } + } + for (const __destroy of __destroyers) { + __attemptedDestroyers.add(__destroy) + } + try { + await __promise + } catch (error) { + if (!__failed) { + __failed = true + __primaryError = error + } else { + __attachCleanupError(__primaryError, error) + } + } + } + if (__failed) { + throw __primaryError + } +} + +function __destroyManagedEmnapiContextsBeforeExit() { + // A once listener is consumed before Node invokes it, including when another + // cleanup batch is still pending. + __managedBeforeExitListener = undefined + if (__managedDestroyPromise) { + return + } + void __destroyManagedEmnapiContexts().catch((error) => { + queueMicrotask(() => { + throw error + }) + }) +} + +function __registerManagedEmnapiContext(__process, __destroy) { + __managedEmnapiContextDestroyers.add(__destroy) + if ( + !__managedCleanupProcess && + __process && + typeof __process.once === 'function' && + typeof __process.removeListener === 'function' + ) { + __managedCleanupProcess = __process + } + let __registered = true + return () => { + if (!__registered) { + return + } + __registered = false + __managedEmnapiContextDestroyers.delete(__destroy) + if (__managedEmnapiContextDestroyers.size === 0) { + __removeManagedEmnapiCleanupListeners() + } + } +} + +async function __createManagedEmnapiContext( + __prepareEnvCleanup, + __isPreparingEnvCleanup, +) { + const __process = + typeof process === 'object' && process !== null ? process : undefined + const __finishAutoDestroyCapture = + __captureEmnapiAutoDestroyListener(__process) + let __emnapiContext + let __contextInitializationError + let __contextInitializationFailed = false + try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareEnvCleanup, + __isPreparingEnvCleanup, + ) + // emnapi 2.x still registers an unconditional process.once('beforeExit') + // auto-destroy listener on Node hosts, and suppressDestroy() only + // neutralizes its callback without removing it. This loader must stay + // side-effect free per instance, so the listener is captured and removed; + // suppressDestroy() remains the safety net when removal is unavailable. + __emnapiContext.suppressDestroy() + } catch (error) { + __contextInitializationError = error + __contextInitializationFailed = true + } finally { + // Remove only the exact emnapi callback captured above. + __finishAutoDestroyCapture?.() + } + if (__emnapiContext === undefined) { + throw __contextInitializationError + } + let __disposed = false + let __destroying = false + let __destroyPromise + let __cleanupRegistered = false + let __unregisterCleanup + const __destroy = (__blocksModuleLifecycle = false) => { + if (__disposed) { + return + } + if (__destroying) { + throw __createLifecycleReentryError('dispose') + } + if (__destroyPromise) { + return __destroyPromise + } + __destroying = true + let __result + const __finishDestroyInvocation = () => { + __destroying = false + } + const __finishModuleLifecycleDestroy = () => { + if (__blocksModuleLifecycle) { + __blocksModuleLifecycle = false + __moduleLifecycleDestroyDepth-- + } + } + if (__blocksModuleLifecycle) { + __moduleLifecycleDestroyDepth++ + } + try { + // Context.destroy() disables JS before cleanup hooks run, so settle + // runtime-owned promises while this environment can still call JS. + __prepareEnvCleanup?.() + if (__isPreparingEnvCleanup?.()) { + // Reached from inside the barrier, so `Context.destroy()` below would + // hit the wrapper's in-flight no-op. Recording that as a completed + // destroy is what makes the frame that *did* start the barrier skip the + // real one afterwards, leaving the context retained with its cleanup + // hooks unrun. Refuse instead: nothing is flagged, the context stays + // registered for managed beforeExit cleanup, and a later destroy still + // works. dispose() coalesces reentrancy before it can get here, so this + // is the backstop for any other caller that manages to. + throw __createLifecycleReentryError('dispose') + } + __result = __emnapiContext.destroy() + } catch (error) { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + throw error + } + let __then + try { + if ( + __result !== null && + (typeof __result === 'object' || typeof __result === 'function') + ) { + __then = __result.then + } + } catch (error) { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + throw error + } + if (typeof __then === 'function') { + let __resolveResult + let __rejectResult + const __resultPromise = new Promise((resolve, reject) => { + __resolveResult = resolve + __rejectResult = reject + }) + const __promise = __resultPromise.then( + (value) => { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __disposed = true + __destroyPromise = undefined + __unregisterCleanup?.() + return value + }, + (error) => { + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __destroyPromise = undefined + throw error + }, + ) + __destroyPromise = __promise + try { + Reflect.apply(__then, __result, [__resolveResult, __rejectResult]) + } catch (error) { + __rejectResult(error) + } + return __promise + } + __finishDestroyInvocation() + __finishModuleLifecycleDestroy() + __disposed = true + __unregisterCleanup?.() + } + const __destroyForModuleLifecycle = () => __destroy(true) + const __registerCleanup = ( + __beforeExitDestroy = __destroyForModuleLifecycle, + ) => { + if (__cleanupRegistered || __disposed) { + return + } + __unregisterCleanup = __registerManagedEmnapiContext( + __process, + __beforeExitDestroy, + ) + __cleanupRegistered = true + __registerManagedBeforeExitListener() + } + if (__contextInitializationFailed) { + let __registrationError + let __registrationFailed = false + try { + __registerCleanup() + } catch (error) { + __attachCleanupError(__contextInitializationError, error) + __registrationError = error + __registrationFailed = true + } + try { + await __destroyForModuleLifecycle() + } catch (error) { + __attachCleanupError( + __registrationFailed + ? __registrationError + : __contextInitializationError, + error, + ) + try { + __registerManagedBeforeExitListener() + } catch {} + } + throw __contextInitializationError + } + return { + context: __emnapiContext, + destroy: __destroy, + destroyForModuleLifecycle: __destroyForModuleLifecycle, + registerCleanup: __registerCleanup, + } +} + +async function __createInstance( + __wasmInput, + __options, + __beforeExitDestroy, + __onManagedDestroyer, +) { + const __module = await __resolveModule(__wasmInput) + const __emnapiModule = await __normalizeModuleForEmnapi(__module) + const __wasi = new __WASI({ + version: 'preview1', + }) + // The wasm module is linked with `--import-memory`, so a Memory must be + // provided. It is resolved here in function scope (workerd bans global scope + // allocation) and is never shared (no threads, no SharedArrayBuffer). + // Resolve it before the emnapi context so a rejected option bag or a host + // memory-limit failure cannot leak a context that never reaches + // instantiation. + const __wasmMemory = __resolveInstanceMemory(__options) + let __lifecycleState = 'pending' + let __destroyEmnapiContext + let __destroyOwnedContext + let __destroyManagedOwnedContext + let __napiInstance + let __wasmEnvCleanupRan = false + let __wasmEnvCleanupPrepared = false + let __wasmEnvCleanupPreparing = false + let __wasmEnvCleanupDrained = false + let __wasmEnvCleanupDrainPromise + const __isPreparingEnvCleanup = () => __wasmEnvCleanupPreparing + const __prepareEnvCleanup = () => { + if (__wasmEnvCleanupPrepared || __wasmEnvCleanupPreparing) { + return + } + const __prepareWasmEnvCleanup = + __napiInstance?.exports.napi_prepare_wasm_env_cleanup + if (typeof __prepareWasmEnvCleanup === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __wasmEnvCleanupPreparing = true + try { + __prepareWasmEnvCleanup() + } finally { + __wasmEnvCleanupPreparing = false + } + __wasmEnvCleanupRan = true + } + __wasmEnvCleanupPrepared = true + } + // The barrier + settlement drain, hoisted out of the context destroyer so the + // drain can yield without widening the destroyer's reentry window. Both + // yielding paths run it — dispose() and the initialization-failure rollback. + // The destroyer still runs the barrier itself (idempotently) for the one path + // that cannot yield: managed beforeExit cleanup of an instance whose rollback + // is being retried. + // + // "Drained" is recorded only once a wait has actually finished. Scheduling a + // macrotask can fail — a host-provided or patched `setImmediate` that throws + // is enough — and dispose() stays retryable after it rejects, so marking the + // drain complete up front would make the retry skip it and destroy the context + // with the barrier's settlements still queued. + const __prepareForDisposal = () => { + if (__wasmEnvCleanupDrained) { + return + } + if (__wasmEnvCleanupDrainPromise) { + return __wasmEnvCleanupDrainPromise + } + __prepareEnvCleanup() + if (!__wasmEnvCleanupRan) { + return + } + const __drained = __drainWasmEnvCleanup(__napiInstance) + if (!__drained || typeof __drained.then !== 'function') { + __wasmEnvCleanupDrained = true + return + } + const __tracked = __drained.then( + (__value) => { + __wasmEnvCleanupDrained = true + __wasmEnvCleanupDrainPromise = undefined + return __value + }, + (__error) => { + __wasmEnvCleanupDrainPromise = undefined + throw __error + }, + ) + __wasmEnvCleanupDrainPromise = __tracked + return __tracked + } + let __disposed = false + const __runInstanceDisposal = async () => { + if (__lifecycleState !== 'failed') { + __lifecycleState = 'disposal' + } + // Outstanding async work first, while the environment is still completely + // live: its completion callbacks run addon code, and the barrier and + // `Context.destroy()` below each take that away. Undefined unless work is + // outstanding. + const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance) + if (__asyncWorkDrained) { + await __asyncWorkDrained + } + // Then settle what the barrier cancelled, before the environment stops + // accepting JavaScript calls. Undefined unless something is queued, so + // an idle disposal is not delayed by a single turn. + const __drained = __prepareForDisposal() + if (__drained) { + await __drained + } + const __result = await (__beforeExitDestroy + ? __destroyManagedOwnedContext() + : __destroyOwnedContext()) + // Only a completed destroy retires the instance; a throw above leaves + // the counter untouched so a retried dispose() cannot double-decrement. + if (!__disposed) { + __disposed = true + __liveInstances -= 1 + } + return __result + } + let __instanceDisposePromise + /** + * The disposal frame runs the barrier, and the barrier settles the promises it + * cancels synchronously — so a promise hook firing inside it can call this + * same instance's dispose() again while the first call is still in its drain. + * That nested call finds the barrier flagged in flight, prepares nothing, + * drains nothing, and falls straight through to the context destroyer, whose + * `Context.destroy()` hits the wrapper's in-flight no-op. It would record a + * destruction that never happened, and the outer frame would then skip the + * real one: both disposals resolve, no cleanup hook runs, the context stays + * retained. + * + * Memoize before any of that starts, exactly like the eager loaders' + * `__disposeWasiBinding`, so there is only ever one disposal frame per + * instance and a reentrant caller awaits it instead of racing it. Cleared on + * rejection: a drain that failed has to stay retryable. + */ + const __disposeInstance = () => { + if (__instanceDisposePromise) { + return __instanceDisposePromise + } + let __resolveDispose + let __rejectDispose + const __disposePromise = new Promise((__resolve, __reject) => { + __resolveDispose = __resolve + __rejectDispose = __reject + }) + __instanceDisposePromise = __disposePromise + __runInstanceDisposal().then(__resolveDispose, (__error) => { + __instanceDisposePromise = undefined + __rejectDispose(__error) + }) + return __disposePromise + } + const __destroyBeforeExit = __beforeExitDestroy + ? async () => { + if (__lifecycleState === 'failed') { + await __destroyManagedOwnedContext() + return + } + __lifecycleState = 'disposal' + try { + await __beforeExitDestroy() + } catch (error) { + if (__lifecycleState !== 'failed') { + throw error + } + // The singleton's initialization rejection is already observable + // through instantiate() and dispose(). Managed beforeExit cleanup + // owns only context destruction, including retrying a failed rollback. + await __destroyManagedOwnedContext() + } + } + : undefined + const { + context: __emnapiContext, + destroy, + destroyForModuleLifecycle, + registerCleanup: __registerCleanup, + } = await __createManagedEmnapiContext( + __prepareEnvCleanup, + __isPreparingEnvCleanup, + ) + __destroyEmnapiContext = destroy + __destroyOwnedContext = () => __destroyEmnapiContext() + __destroyManagedOwnedContext = destroyForModuleLifecycle + try { + if (__destroyBeforeExit) { + __onManagedDestroyer(__destroyBeforeExit) + await __registerCleanup(__destroyBeforeExit) + } + let __napiModule + ;({ + instance: __napiInstance, + napiModule: __napiModule, + } = await __emnapiInstantiateNapiModule(__emnapiModule, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + // `instantiate()` and `createInstance().exports` hand out this object; a + // named module export does not travel with it. After the instance host + // install, which hands the same object to addon-provided registration + // functions that may put anything on it, and inside this `try`, so a + // claimed name flips `__lifecycleState` to 'failed' and tears the instance + // down rather than escaping a half-built one. + __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) + if (__lifecycleState === 'pending') { + __lifecycleState = 'succeeded' + } + __createdInstances += 1 + __liveInstances += 1 + return { + exports: __napiModule.exports, + get memory() { + return __wasmMemory + }, + get memoryBytes() { + // The Memory outlives the environment, so this stays readable after a + // FAILED dispose() (which leaves the instance undisposed and + // retryable). It reports 0 only once disposal has actually completed. + return __disposed ? 0 : __wasmMemory.buffer.byteLength + }, + get disposed() { + return __disposed + }, + dispose: __disposeInstance, + } + } catch (error) { + __lifecycleState = 'failed' + // Instantiation can fail *after* registration has run, and registration runs + // with a live environment: a module-init hook can start async work and then + // return an error, and the promise it created may already have escaped into + // JavaScript. Settle what the barrier cancels before the context is + // destroyed, exactly like dispose() does; destroying without yielding + // discards the queue with a null env. Undefined unless something is queued, + // so a failure before beforeInit costs no extra turn. + let __settlementsUnreached = false + try { + // Registration can start async work too, and this path destroys the same + // environment its completions need. A drain that cannot finish leaves the + // work outstanding, so it counts as settlements unreached and stops the + // rollback short of destroying, exactly like a failed barrier drain. + const __asyncWorkDrained = __drainInstanceAsyncWork(__napiInstance) + if (__asyncWorkDrained) { + await __asyncWorkDrained + } + const __drained = __prepareForDisposal() + if (__drained) { + await __drained + } + } catch (drainError) { + __attachCleanupError(error, drainError) + __settlementsUnreached = true + } + let __registrationError + let __registrationFailed = false + if (!__beforeExitDestroy) { + try { + // Independent instances are caller-owned while pending and after + // success. Register only failed rollback so cleanup remains retryable. + await __registerCleanup() + } catch (registrationError) { + __attachCleanupError(error, registrationError) + __registrationError = registrationError + __registrationFailed = true + } + } + if (__settlementsUnreached) { + // The barrier or the drain did not finish, so the settlements it queued + // are still in the threadsafe-function queue. Destroying now runs the + // cleanup hook that drains that queue with a null env and discards it, + // stranding a promise that already escaped into JavaScript — with nothing + // left that could ever settle it. dispose() refuses to destroy for exactly + // this reason (a rejected drain there never reaches the destroy), so this + // path refuses too. + // + // Nothing leaks. The registration just above — and, for the singleton, the + // one made before instantiation — leaves this context in + // `__managedEmnapiContextDestroyers`, so beforeExit destroys it and + // dispose() can retry it. Only the destruction is deferred, and the turns + // that pass in the meantime are exactly what the queue needed. + try { + __registerManagedBeforeExitListener() + } catch {} + throw error + } + try { + await __destroyManagedOwnedContext() + } catch (disposeError) { + // Initialization is the primary failure. Preserve it even if cleanup + // also fails, while retaining the cleanup error when the value is + // extensible and has no existing cause. + __attachCleanupError( + __registrationFailed ? __registrationError : error, + disposeError, + ) + try { + __registerManagedBeforeExitListener() + } catch {} + } + throw error + } +} + +/** + * Create an independent instance. Call and await dispose() when the instance + * is no longer needed so emnapi cleanup hooks run deterministically. + * + * The optional second argument selects this instance's linear memory: either + * `memory` (an unshared, single-use WebAssembly.Memory you allocated) or + * `initialMemoryPages` / `maximumMemoryPages`, never both. Omitted, the + * loader allocates WASM_MEMORY.initialPages..WASM_MEMORY.maximumPages. + * + * A provided Memory must come from this loader's own realm: the WASI and + * emnapi layers underneath identify one with a realm-local `instanceof`, so a + * Memory built in a `node:vm` context or another frame is rejected. Every + * Memory an instance runs on is single-use, the loader-allocated one included: + * `instance.memory` cannot be recycled into a second `createInstance()`. + */ +export async function createInstance(__wasmInput, __options) { + return __createInstance(__wasmInput, __options) +} + +let __defaultModulePromise +let __defaultInstancePromise +let __defaultDisposePromise +let __defaultDisposalStarted = false +const __defaultManagedDestroyers = new WeakMap() +let __moduleDisposePromise + +/** + * Instantiate a module-local singleton. Concurrent and repeated calls + * with the same module share one instance and one Memory allocation. + */ +export function instantiate(__wasmInput) { + const __modulePromise = __resolveModule(__wasmInput) + if (__moduleLifecycleDestroyDepth !== 0) { + void __modulePromise.catch(() => {}) + return Promise.reject(__createLifecycleReentryError('instantiate')) + } + if (__moduleDisposePromise) { + void __modulePromise.catch(() => {}) + return __moduleDisposePromise.then(() => instantiate(__modulePromise)) + } + if (__defaultDisposalStarted) { + // Observe rejected input immediately, but preserve lifecycle ordering and + // error precedence by instantiating only after disposal succeeds. A failed + // disposal retains the old instance only so its cleanup can be retried. + void __modulePromise.catch(() => {}) + const __disposePromise = __defaultDisposePromise ?? dispose() + return __disposePromise.then(() => instantiate(__modulePromise)) + } + if (!__defaultInstancePromise) { + __defaultModulePromise = __modulePromise + const __instancePromise = __modulePromise.then((__module) => + __createInstance( + __module, + undefined, + __disposeDefaultInstance, + (__managedDestroyer) => { + __defaultManagedDestroyers.set( + __instancePromise, + __managedDestroyer, + ) + }, + ), + ) + __defaultInstancePromise = __instancePromise + void __instancePromise.catch(() => { + if (__defaultInstancePromise === __instancePromise) { + __defaultInstancePromise = undefined + __defaultModulePromise = undefined + } + }) + return __instancePromise.then((__instance) => __instance.exports) + } + const __defaultModulePromiseForCall = __defaultModulePromise + const __defaultInstancePromiseForCall = __defaultInstancePromise + return Promise.all([__defaultModulePromiseForCall, __modulePromise]).then( + async ([__defaultModule, __module]) => { + if (__defaultModule !== __module) { + throw new Error( + 'instantiate() already owns a different WebAssembly.Module; call dispose() first or use createInstance() for independent instances.', + ) + } + return (await __defaultInstancePromiseForCall).exports + }, + ) +} + +async function __disposeDefaultInstance(__onDestroy) { + if (__defaultDisposePromise) { + return __defaultDisposePromise + } + const __instancePromise = __defaultInstancePromise + if (!__instancePromise) { + __defaultDisposalStarted = false + return + } + __defaultDisposalStarted = true + const __disposePromise = (async () => { + let __instance + try { + __instance = await __instancePromise + } catch (error) { + const __managedDestroyer = + __defaultManagedDestroyers.get(__instancePromise) + if (__managedDestroyer) { + __onDestroy?.(__managedDestroyer) + } + __defaultManagedDestroyers.delete(__instancePromise) + throw error + } + const __managedDestroyer = + __defaultManagedDestroyers.get(__instancePromise) + if (__managedDestroyer) { + __onDestroy?.(__managedDestroyer) + } + await __instance.dispose() + if (__defaultInstancePromise === __instancePromise) { + __defaultInstancePromise = undefined + __defaultModulePromise = undefined + __defaultDisposalStarted = false + } + __defaultManagedDestroyers.delete(__instancePromise) + })() + __defaultDisposePromise = __disposePromise + try { + await __disposePromise + } finally { + if (__defaultDisposePromise === __disposePromise) { + __defaultDisposePromise = undefined + } + } +} + +async function __dispose() { + let __defaultDisposeError + let __defaultDisposeFailed = false + let __attemptedDefaultDestroyer + try { + await __disposeDefaultInstance((__destroyer) => { + __attemptedDefaultDestroyer = __destroyer + }) + } catch (error) { + __defaultDisposeError = error + __defaultDisposeFailed = true + } + const __excludedDestroyers = new Set() + if (__defaultDisposeFailed && __attemptedDefaultDestroyer) { + __excludedDestroyers.add(__attemptedDefaultDestroyer) + } + try { + await __drainManagedEmnapiContexts(__excludedDestroyers) + } catch (error) { + if (!__defaultDisposeFailed) { + throw error + } + if (error !== __defaultDisposeError) { + __attachCleanupError(__defaultDisposeError, error) + } + } + if (__defaultDisposeFailed) { + throw __defaultDisposeError + } +} + +/** + * Dispose the singleton created by instantiate(). A later call may create a + * fresh instance, including from a different module. This also retries cleanup + * retained after a failed initialization rollback. + */ +export function dispose() { + if (__moduleLifecycleDestroyDepth !== 0) { + return Promise.reject(__createLifecycleReentryError('dispose')) + } + if (__moduleDisposePromise) { + return __moduleDisposePromise + } + let __resolveDispose + let __rejectDispose + const __promise = new Promise((resolve, reject) => { + __resolveDispose = resolve + __rejectDispose = reject + }) + __moduleDisposePromise = __promise + void __dispose().then(__resolveDispose, __rejectDispose) + void __promise.then( + () => { + if (__moduleDisposePromise === __promise) { + __moduleDisposePromise = undefined + } + }, + () => { + if (__moduleDisposePromise === __promise) { + __moduleDisposePromise = undefined + } + }, + ) + return __promise +} diff --git a/napi/transform/transform.wasip1.cjs b/napi/transform/transform.wasip1.cjs new file mode 100644 index 0000000000..96619b1f53 --- /dev/null +++ b/napi/transform/transform.wasip1.cjs @@ -0,0 +1,1213 @@ +// napi-rs-artifact-metadata:{"version":2,"rootEntry":"index.js","exports":["Severity","HelperMode","isolatedDeclaration","isolatedDeclarationSync","moduleRunnerTransform","moduleRunnerTransformSync","transform","transformSync"],"managedRootEntries":["browser.js","index.js","transform.wasm","transform.debug.wasm"]} +/* eslint-disable */ +/* auto-generated by NAPI-RS */ + +const __napiBindingTarget = 'wasm32-wasip1' +function __napiStampBindingTarget(exportsObject, target) { + if ( + Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget') + ) { + if (exportsObject.__napiBindingTarget === target) { + // Already ours: the root entry aliases the object it loaded, so a WASI + // fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that + // is a generated loader — arrives already stamped with this same value. + return target + } + const error = new Error( + '`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].', + ) + error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT' + throw error + } + if (!Object.isExtensible(exportsObject)) { + // A `#[napi(module_exports)]` hook may seal or freeze this object + // (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata, + // never a reason to fail an otherwise successful load, so the stamp is + // skipped. What a consumer still sees then follows the entry point: the + // browser and deferred loaders declare `__napiBindingTarget` at module + // level and go on reporting it, while the CommonJS entries hand back this + // very object as `module.exports`, so there the value is absent. + return target + } + try { + // [[Define]], not [[Set]]: an ordinary assignment walks the prototype + // chain, so an inherited accessor could swallow the value or throw and + // fail an otherwise successful load. The descriptor is what a successful + // assignment would have produced. + Object.defineProperty(exportsObject, '__napiBindingTarget', { + configurable: true, + enumerable: true, + value: target, + writable: true, + }) + } catch { + // Same rule as the non-extensible skip above: reporting the artifact is + // metadata, never a reason to fail an otherwise successful load. An exotic + // object (a Proxy whose defineProperty trap refuses) is skipped, not + // thrown over. + } + // The CommonJS loaders assign this return value so `cjs-module-lexer` — and + // therefore Node's CJS -> ESM named export detection — can see + // `__napiBindingTarget` statically. + return target +} + +const __nodeFs = require('node:fs') +const __nodePath = require('node:path') +const { WASI: __nodeWASI } = require('node:wasi') + +const { + emnapiAsyncWorkPlugin: __emnapiAsyncWorkPlugin, + emnapiTSFNPlugin: __emnapiTSFNPlugin, + instantiateNapiModuleSync: __emnapiInstantiateNapiModuleSync, +} = require('@napi-rs/wasm-runtime') +const { createContext: __emnapiCreateContext } = require('@emnapi/runtime') + +const __cwd = process.cwd() +const __rootDir = __nodePath.parse(__cwd).root +const __hostRoot = + process.platform === 'android' ? __cwd : __rootDir + +const __wasi = new __nodeWASI({ + version: 'preview1', + env: process.env, + preopens: { + [__rootDir]: __hostRoot, + [__hostRoot]: __hostRoot, + }, +}) + +const __wasmMemory = new WebAssembly.Memory({ + initial: 4000, + maximum: 65536, +}) + +let __wasmFilePath = __nodePath.join(__dirname, 'transform.wasm32-wasip1.wasm') +const __wasmDebugFilePath = __nodePath.join(__dirname, 'transform.wasm32-wasip1.debug.wasm') + +if (__nodeFs.existsSync(__wasmDebugFilePath)) { + __wasmFilePath = __wasmDebugFilePath +} else if (!__nodeFs.existsSync(__wasmFilePath)) { + const __wasiPackageEntry = require.resolve('@oxc-transform/binding-wasm32-wasip1') + const __packagedWasmFilePath = __nodePath.join( + __nodePath.dirname(__wasiPackageEntry), + 'transform.wasm32-wasip1.wasm', + ) + if (!__nodeFs.existsSync(__packagedWasmFilePath)) { + throw new Error( + '@oxc-transform/binding-wasm32-wasip1 is installed but is missing transform.wasm32-wasip1.wasm.', + ) + } + __wasmFilePath = __packagedWasmFilePath +} + +const __wasmFile = __nodeFs.readFileSync(__wasmFilePath) +let __emnapiContext + +const __wasiDisposeSymbol = Symbol.for('napi.rs.wasi.dispose') +const __wasiWorkers = new Set() +// The thread manager has to be reachable *before* anything that can throw +// during load or registration. Initialization can fail after the pool has +// already spawned workers, and the rollback still has to mark their +// terminations as expected — but `__napiModule` is assigned only when +// instantiation RETURNS, so on exactly that path it is still undefined. A +// plugin factory runs while the emnapi module is being created, before the +// wasm is loaded and before any registration function runs, and its context +// carries the very same manager instance. +let __wasiThreadManager + +function __captureWasiThreadManager(context) { + if (context && context.PThread) { + __wasiThreadManager = context.PThread + } + return {} +} + +function __getWasiThreadManager() { + const manager = + __wasiThreadManager !== undefined + ? __wasiThreadManager + : __napiModule + ? __napiModule.PThread + : undefined + if (manager && typeof manager.terminateWorker === 'function') { + return manager + } + return undefined +} +let __napiInstance +let __emnapiContextDestroyed = false +let __emnapiContextDestroyPromise +let __emnapiWasmEnvCleanupPrepared = false +let __emnapiWasmEnvCleanupPreparing = false +let __emnapiWasmEnvCleanupRan = false +let __emnapiWasmEnvCleanupDrained = false +let __emnapiWasmEnvCleanupDrainPromise +let __wasiDisposed = false +let __wasiAsyncWorkDrainPromise +let __wasiDisposePromise +let __completeWasiDisposal = function () {} +// Overridden by loader flavors that have a last-resort reclaim for a rollback +// that stopped short of destroying the context. See +// `__rollbackWasiInitialization`. +let __retainWasiRollbackForRetry = function () {} + +function __isThenable(value) { + return ( + value !== null && + (typeof value === 'object' || typeof value === 'function') && + typeof value.then === 'function' + ) +} + +function __createCleanupError(errors, message) { + if (errors.length === 1) { + return errors[0] + } + const __AggregateError = globalThis.AggregateError + if (typeof __AggregateError === 'function') { + return new __AggregateError(errors, message) + } + const error = new Error(message) + error.errors = errors + return error +} + +function __attachCleanupErrors(error, cleanupErrors) { + if (cleanupErrors.length === 0) { + return error + } + const cleanupError = __createCleanupError( + cleanupErrors, + 'WASI binding cleanup failed', + ) + try { + if ( + error && + (typeof error === 'object' || typeof error === 'function') + ) { + if (error.cause === undefined) { + error.cause = cleanupError + if (error.cause === cleanupError) { + return error + } + } + if (Array.isArray(error.cleanupErrors)) { + error.cleanupErrors.push(cleanupError) + return error + } else { + const attachedCleanupErrors = [cleanupError] + error.cleanupErrors = attachedCleanupErrors + if (error.cleanupErrors === attachedCleanupErrors) { + return error + } + } + } + } catch {} + const aggregate = __createCleanupError( + [error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + try { + aggregate.cause = error + } catch {} + return aggregate +} + +function __wrapEmnapiContextDestroyForSettlement( + context, + prepareEnvCleanup, + isPreparingEnvCleanup, +) { + let destroy + try { + destroy = context.destroy + } catch { + return context + } + if (typeof destroy !== 'function') { + return context + } + try { + Object.defineProperty(context, 'destroy', { + configurable: true, + enumerable: false, + writable: true, + value: function () { + // Reentered from a promise hook that fired inside the barrier: the + // frame running it destroys as soon as it returns. + if (isPreparingEnvCleanup?.()) { + return + } + prepareEnvCleanup?.() + return Reflect.apply(destroy, this, arguments) + }, + }) + } catch {} + return context +} + +function __isPreparingWasmEnvCleanup() { + return __emnapiWasmEnvCleanupPreparing +} + +function __prepareWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) { + return + } + const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup + if (typeof prepare === 'function') { + // The addon settles the promises it cancels synchronously, under a + // non-reentrant lifecycle mutex: anything a promise hook calls from in + // here must not reach this export again. + __emnapiWasmEnvCleanupPreparing = true + try { + prepare() + } finally { + __emnapiWasmEnvCleanupPreparing = false + } + __emnapiWasmEnvCleanupRan = true + } + __emnapiWasmEnvCleanupPrepared = true +} + +// Mirror the primitive @emnapi/core schedules its threadsafe-function dispatch +// on, so the drain turns below interleave with that dispatch instead of racing +// ahead of it on a faster queue. +const __scheduleMacrotask = (function () { + if (typeof setImmediate === 'function') { + return function (callback) { + setImmediate(callback) + } + } + const __MessageChannel = globalThis.MessageChannel + if (typeof __MessageChannel === 'function') { + return function (callback) { + const channel = new __MessageChannel() + channel.port1.onmessage = function () { + channel.port1.onmessage = null + try { + channel.port1.close() + } catch {} + try { + channel.port2.close() + } catch {} + callback() + } + channel.port2.postMessage(null) + } + } + return function (callback) { + setTimeout(callback, 0) + } +})() + +// A real, *referenced* timer, for waits that must let the whole host make +// progress between looks — the async-work drain polls the addon rather than +// interleaving with the @emnapi/core dispatch, so a zero-delay macrotask there +// would spin the loop instead of yielding it. Falls back to the macrotask +// scheduler on a host without timers. +function __scheduleTimer(callback, delay) { + const setTimer = globalThis.setTimeout + if (typeof setTimer !== 'function') { + __scheduleMacrotask(callback) + return + } + try { + setTimer(callback, delay) + } catch { + __scheduleMacrotask(callback) + } +} + +// Turns to wait for while the addon still reports queued settlements. Reaching +// zero is the only success. A counter still nonzero at this bound rejects the +// disposal as retryable (`ERR_NAPI_WASI_CLEANUP_PENDING`) rather than +// destroying the context over a still-queued settlement — the wait stays +// bounded either way. +const __WASM_ENV_CLEANUP_DRAIN_TURNS = 128 +// Without `napi_wasm_env_cleanup_pending` the queue is not observable. Fall +// back to the number of turns @emnapi/core needs to coalesce and dispatch a +// call made on this thread (two), plus a margin. +const __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS = 4 + +/** + * `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the + * tasks it cancelled: `napi_call_threadsafe_function` appends to the + * threadsafe-function queue, and @emnapi/core dispatches that queue from a + * macrotask — two coalescing turns later, even for a call made on this very + * thread. `Context.destroy()` then runs the threadsafe function's cleanup hook, + * which drains the queue with a null env and *discards* whatever is still in it. + * + * So destroying without yielding first strands exactly the promises the barrier + * exists to settle. Yield real event-loop turns until the addon reports the + * queue empty; microtask checkpoints cannot help, no number of them lets a + * macrotask run. + * + * Returns nothing when there is nothing to wait for, which keeps disposal + * synchronous in the common case. + * + * The "already drained" flag is set only once a wait has actually finished. + * Scheduling a macrotask can fail — a host-provided or patched `setImmediate` + * that throws is enough — and a disposal that rejects stays retryable, so + * marking the drain complete up front would make the retry skip it and destroy + * the context with the barrier's settlements still queued. + * + * A wait that runs out of turns with the counter still nonzero rejects with + * `ERR_NAPI_WASI_CLEANUP_PENDING` for the same reason: at that point + * "finished" is indistinguishable from the stranding above, and destroying + * would discard the very settlement the wait was for. The rejection leaves the + * flag unset and disposal retryable. + */ +function __drainWasmEnvCleanup() { + if (__emnapiWasmEnvCleanupDrained || !__emnapiWasmEnvCleanupRan) { + return + } + if (__emnapiWasmEnvCleanupDrainPromise) { + return __emnapiWasmEnvCleanupDrainPromise + } + const pending = __napiInstance?.exports?.napi_wasm_env_cleanup_pending + const observable = typeof pending === 'function' + if (observable) { + let queued + try { + queued = pending() + } catch { + __emnapiWasmEnvCleanupDrained = true + return + } + if (!queued) { + __emnapiWasmEnvCleanupDrained = true + return + } + } + const limit = observable + ? __WASM_ENV_CLEANUP_DRAIN_TURNS + : __WASM_ENV_CLEANUP_BLIND_DRAIN_TURNS + const drainPromise = (async () => { + let queued = 0 + for (let turn = 0; turn < limit; turn++) { + await new Promise((resolve) => { + __scheduleMacrotask(resolve) + }) + if (!observable) { + continue + } + try { + queued = pending() + } catch { + return + } + if (!queued) { + return + } + } + if (!observable) { + // Blind wait: without `napi_wasm_env_cleanup_pending` the bound IS the + // contract — there is nothing to consult, so finishing the turns is + // finishing the drain. + return + } + // The counter is still nonzero after every turn the bound allows. The wait + // stays bounded — but claiming success here would be indistinguishable from + // the stranding this drain exists to prevent: disposal would go on to + // destroy the context, whose cleanup hook discards the still-queued + // settlement with a null env, and the promise it was for hangs forever. + // Reject instead, as a retryable cleanup failure: the drained flag stays + // unset, dispose() (and the rollback) decline to destroy, and a later + // dispose() runs the drain again — by which time the queue has usually been + // delivered. A counter that is somehow stuck nonzero therefore costs each + // attempt at most another bounded wait and a rejection, never a stranded + // promise; the process-exit teardown still reclaims the context. + const drainError = new Error( + 'the wasm environment still reports ' + + queued + + ' queued settlement(s) after ' + + limit + + ' event-loop turns; the context was not destroyed - retry dispose() to wait for the queue again', + ) + drainError.code = 'ERR_NAPI_WASI_CLEANUP_PENDING' + throw drainError + })().then( + (value) => { + // Set only when the wait actually finished AND the queue was seen empty + // (or is unobservable): a drain that timed out with settlements still + // queued rejects above and must stay repeatable. + __emnapiWasmEnvCleanupDrained = true + __emnapiWasmEnvCleanupDrainPromise = undefined + return value + }, + (error) => { + __emnapiWasmEnvCleanupDrainPromise = undefined + throw error + }, + ) + __emnapiWasmEnvCleanupDrainPromise = drainPromise + return drainPromise +} + +function __destroyEmnapiContext() { + if (__emnapiContextDestroyed || __emnapiContext === undefined) { + __emnapiContextDestroyed = true + return + } + if (__emnapiContextDestroyPromise) { + return __emnapiContextDestroyPromise + } + + __prepareWasmEnvCleanup() + const result = __emnapiContext.destroy() + if (!__isThenable(result)) { + __emnapiContextDestroyed = true + return + } + + const destroyPromise = Promise.resolve(result).then( + (value) => { + __emnapiContextDestroyed = true + return value + }, + (error) => { + __emnapiContextDestroyPromise = undefined + throw error + }, + ) + __emnapiContextDestroyPromise = destroyPromise + return destroyPromise +} + +/** + * Holds the event loop open until `work` settles. + * + * Nothing else can: the pool workers are deliberately unreferenced so an idle + * binding cannot keep a process alive, and referencing them again for the + * termination does not hold either — emnapi unreferences a worker the moment it + * reports `async-thread-ready`, which for a worker that was still starting + * lands *after* the termination began. Without a handle of its own, an + * `await dispose()` with nothing else pending exits the process with its + * promise unsettled, and everything after the `await` is skipped. + * + * The timer is cleared as soon as the work settles, so this never outlives the + * disposal that asked for it. + */ +function __keepEventLoopAliveUntil(work) { + const setTimer = globalThis.setInterval + const clearTimer = globalThis.clearInterval + if (typeof setTimer !== 'function' || typeof clearTimer !== 'function') { + return work + } + let timer + try { + timer = setTimer(function () {}, 50) + } catch { + return work + } + const release = function () { + try { + clearTimer(timer) + } catch {} + } + return work.then( + (value) => { + release() + return value + }, + (error) => { + release() + throw error + }, + ) +} + +// How often to re-read `napi_wasm_async_work_pending` while waiting. The wait +// ends when the addon reports zero, so this only decides how promptly disposal +// notices — not how long it waits. +const __WASI_ASYNC_WORK_POLL_INTERVAL_MS = 1 + +/** + * Settles this addon's outstanding `napi_async_work` before the teardown that + * would strand it. + * + * `napi_prepare_wasm_env_cleanup` does not cover async work, and nothing about + * it is observable from JavaScript: the threadless archive resolves + * `napi_*_async_work` through the `@emnapi/core` plugins, but the threaded one + * links the C `async_work.c` on the uv threadpool, so there the wasm neither + * imports nor exports those symbols and the only brackets a loader could watch + * (`_emnapi_ctx_*_waiting_request_counter`) are shared with threadsafe + * functions. The addon is the one place both flavors go through, so it answers + * for both, through the same kind of handshake the settlement drain uses: + * + * - `napi_wasm_cancel_pending_async_work()` cancels what no thread has + * started. Those completion callbacks run with `napi_cancelled`, which + * napi-rs turns into a promise rejected with an `AbortError`. + * - `napi_wasm_async_work_pending()` counts what is still owed a completion + * callback. Work already executing refuses cancellation and stays counted + * until it finishes normally — which it can, because this runs before the + * barrier, before `Context.destroy()` and before anything is terminated. + * + * Both exports are optional: an addon built against a napi crate that predates + * them drains nothing and keeps the previous behavior, exactly as the + * `napi_wasm_env_cleanup_pending` handshake degrades. + * + * Returns nothing when there is nothing outstanding, which keeps disposal + * synchronous in the common case. The promise it returns otherwise never + * rejects. + * + * The wait has no deadline, and that is the point: giving up would destroy the + * environment with a completion callback still owed, which is the stranding + * this exists to prevent. A task whose `execute` never returns already keeps an + * *undisposed* process alive in exactly the same way, so disposal inherits that + * rather than inventing a bound it cannot honor. + * + * Safe to call from inside a completion callback, which is reachable: settling + * a task runs addon code that can re-enter JavaScript — a setter on the value + * being handed back, a threadsafe-function callback — and that JavaScript can + * call `dispose()`. Two things make it terminate rather than wait on itself: + * + * - The addon keeps a work registered until its completion callback + * *finishes*, so the count read here is at least one and this takes the + * polling path instead of declaring the environment drained and tearing it + * down from inside the frame that is still settling a promise. + * - The poll is a timer, so it cannot run until the callback has returned to + * the host — by which time that work has left the registry. The count the + * next poll reads is the one taken after the callback finished. + * + * `__disposeWasiBinding` hands every caller the same in-flight promise, so the + * nested call joins this disposal rather than starting a second one. + */ +function __drainWasiAsyncWork() { + if (__wasiAsyncWorkDrainPromise !== undefined) { + return __wasiAsyncWorkDrainPromise + } + const exports = __napiInstance?.exports + const pending = exports?.napi_wasm_async_work_pending + const cancelPending = exports?.napi_wasm_cancel_pending_async_work + if (typeof pending !== 'function' || typeof cancelPending !== 'function') { + return + } + + const readPending = () => { + try { + return pending() + } catch (error) { + // A trap is the only way this call fails: it reads a counter and cannot + // allocate or call back into JavaScript. A trapped instance can no longer + // run anything, so its outstanding work is unreachable by definition — + // there is nothing left to wait for, and refusing to dispose would only + // keep a dead instance and its stuck counter alive. Best-effort here is + // the honest answer, and it is what disposal did before this drain + // existed. + // + // Only a trap. Anything else means the export is not what this loader + // thinks it is, which is a defect worth surfacing rather than disposing + // over. + if (error instanceof globalThis.WebAssembly.RuntimeError) { + return 0 + } + throw error + } + } + + if (!readPending()) { + return + } + try { + cancelPending() + } catch { + // Cancellation is an optimization: it bounds the wait by the work already + // executing. Failing it only means waiting for the whole queue instead. + } + if (!readPending()) { + return + } + + const drainPromise = __keepEventLoopAliveUntil( + (async () => { + while (readPending()) { + await new Promise((resolve) => { + __scheduleTimer(resolve, __WASI_ASYNC_WORK_POLL_INTERVAL_MS) + }) + } + })(), + ).then( + () => { + __wasiAsyncWorkDrainPromise = undefined + }, + (error) => { + // A wait that could not run is not a wait that finished. The only way + // here is a host whose timers and macrotask primitives all refuse, and + // the work is still outstanding — reporting success would destroy the + // environment over it, which is the stranding this exists to prevent. + // Reject instead: disposal stays retryable, and the context is not + // destroyed. Clearing the memo first is what makes the retry re-run this. + __wasiAsyncWorkDrainPromise = undefined + throw error + }, + ) + __wasiAsyncWorkDrainPromise = drainPromise + return drainPromise +} + +/** + * `@emnapi/wasi-threads` counts a worker exit as expected only when its own + * thread manager performed the termination. A bare `worker.terminate()` reaches + * the manager's `exit` listener instead, which reports + * `worker (tid = N) sent an error! ... stopped with exit code 1` and rethrows + * inside the emit — aborting the `once('exit')` that backs the terminate + * promise, so disposal never settles and the process dies with an uncaught + * exception. Mark the termination through the manager first. + * + * The manager comes from `__getWasiThreadManager`, not from `__napiModule`: + * the initialization rollback runs on the one path where instantiation never + * returned, so `__napiModule` is still undefined there while the workers it + * spawned are already registered and loaded. + * + * Not `terminateAllThreads()`: that one recreates the pool it just shut down. + */ +function __terminateWasiWorkers() { + const cleanupErrors = [] + const pending = [] + const threadManager = __getWasiThreadManager() + + for (const worker of __wasiWorkers) { + let result + try { + if (threadManager) { + threadManager.terminateWorker(worker) + // `terminateWorker` leaves behind a reporter that logs every message + // still queued on the port, which Node flushes on exit. Nothing is + // listening for those any more. + worker.onmessage = undefined + } + result = worker.terminate() + } catch (error) { + cleanupErrors.push(error) + continue + } + if (__isThenable(result)) { + pending.push( + Promise.resolve(result).then( + () => { + __wasiWorkers.delete(worker) + }, + (error) => { + cleanupErrors.push(error) + }, + ), + ) + } else { + __wasiWorkers.delete(worker) + } + } + + const finish = () => { + if (cleanupErrors.length > 0) { + throw __createCleanupError( + cleanupErrors, + 'Failed to terminate WASI workers', + ) + } + } + return pending.length > 0 + ? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish) + : finish() +} + +function __finishWasiDisposal() { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult).then(__completeWasiDisposal) + } + return __completeWasiDisposal() +} + +function __continueWasiDisposal() { + const destroyResult = __destroyEmnapiContext() + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult).then(__finishWasiDisposal) + } + return __finishWasiDisposal() +} + +function __cleanUpWasmEnvForWasiDisposal() { + // Run the pre-teardown barrier, then let the settlements it queued actually + // reach JavaScript, and only then destroy the environment. Doing these two + // back to back is what strands them. + __prepareWasmEnvCleanup() + const drainResult = __drainWasmEnvCleanup() + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then(__continueWasiDisposal) + } + return __continueWasiDisposal() +} + +function __startWasiDisposal() { + // Outstanding `napi_async_work` goes first, while the environment is still + // completely live: the completion callbacks run addon code, and everything + // after this point takes that away from them — the barrier shuts the async + // runtime down, `Context.destroy()` stops JavaScript calls, and terminating + // the pool threads removes what would have reported the work finished. + const asyncWorkResult = __drainWasiAsyncWork() + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __cleanUpWasmEnvForWasiDisposal, + ) + } + return __cleanUpWasmEnvForWasiDisposal() +} + +/** + * Disposes this generated WASI binding. + * + * Access this function with: + * binding[Symbol.for('napi.rs.wasi.dispose')]() + */ +function __disposeWasiBinding() { + if (__wasiDisposePromise) { + return __wasiDisposePromise + } + if (__wasiDisposed) { + return Promise.resolve() + } + + let resolveDispose + let rejectDispose + const disposePromise = new Promise((resolve, reject) => { + resolveDispose = resolve + rejectDispose = reject + }) + __wasiDisposePromise = disposePromise + + let result + try { + result = __startWasiDisposal() + } catch (error) { + __wasiDisposePromise = undefined + rejectDispose(error) + return disposePromise + } + + Promise.resolve(result).then( + (value) => { + __wasiDisposed = true + resolveDispose(value) + }, + (error) => { + __wasiDisposePromise = undefined + rejectDispose(error) + }, + ) + return disposePromise +} + +function __publishWasiDispose(exports) { + Object.defineProperty(exports, __wasiDisposeSymbol, { + configurable: false, + enumerable: false, + value: __disposeWasiBinding, + writable: false, + }) +} + +function __finishWasiInitializationRollback(cleanupErrors) { + let workerResult + try { + workerResult = __terminateWasiWorkers() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return cleanupErrors + } + if (__isThenable(workerResult)) { + return Promise.resolve(workerResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => cleanupErrors) + } + return cleanupErrors +} + +function __destroyContextForWasiRollback(cleanupErrors) { + let destroyResult + try { + destroyResult = __destroyEmnapiContext() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + return __finishWasiInitializationRollback(cleanupErrors) + } + if (__isThenable(destroyResult)) { + return Promise.resolve(destroyResult) + .catch((cleanupError) => { + cleanupErrors.push(cleanupError) + }) + .then(() => __finishWasiInitializationRollback(cleanupErrors)) + } + return __finishWasiInitializationRollback(cleanupErrors) +} + +/** + * Leaves a rollback that could not reach the queued settlements undestroyed, and + * hands it to whatever this flavor has that can still reclaim it. + */ +function __retainFailedWasiRollback(cleanupErrors) { + try { + __retainWasiRollbackForRetry() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + } + return cleanupErrors +} + +/** + * Initialization can fail *after* registration has already run, and registration + * runs with a live environment: a module-init hook can start async work and then + * return an error, and the promise it created may already have escaped into + * JavaScript. The barrier cancels that work and *queues* the settlement, so this + * path needs the same drain the ordinary disposal does — destroying without + * yielding discards the queue with a null env and strands the promise. + * + * Stays synchronous when nothing is queued, which covers every failure before + * `beforeInit`: there is no instance to run the barrier on, so nothing to drain. + * + * A barrier or drain that did *not* finish stops the rollback short of + * destroying, which is what `dispose()` already does — a rejected drain there + * never reaches `__continueWasiDisposal`. Destroying anyway is the worse of the + * two trades, and not because of what it saves: + * + * - It cannot deliver the settlements. `Context.destroy()` runs the + * threadsafe function's cleanup hook, which drains the queue with a null env + * and discards it, so a promise that already escaped into JavaScript hangs + * forever with nothing left that could ever settle it. + * - It saves less than it looks. `Context.destroy()` stops JavaScript calls + * and runs cleanup hooks; it does not free the wasm instance or its Memory, + * which this module's scope holds either way. What stopping short retains is + * the emnapi context's bookkeeping and its un-run cleanup hooks. + * - Retry is not theoretical. A rollback that records a cleanup error is + * already kept in the process-wide registry above, so re-`require()`ing this + * file replays it instead of re-instantiating — and the `6e15de6f` flag fix + * means the replay drains again rather than skipping it. Destroying first is + * what makes that retained record useless. + * + * The residual cost is honest: the CJS flavor hands the context to its + * `process.on('exit')` teardown, so a process that never retries still reclaims + * it on the way out. The ESM browser flavor has no equivalent — a module that + * throws while evaluating is permanently errored, so re-importing rethrows + * without re-running this file — and there the context stays until the realm + * goes away. That is the deliberate choice: a hung promise is a silent liveness + * bug with no upper bound, while the retained bookkeeping is bounded by the page. + */ +function __rollbackWasiInitialization() { + // The environment teardown this rollback performs, kept nested so it cannot + // be reached without the async-work drain below running first. + function __rollbackWasmEnvForWasiInitialization() { + const cleanupErrors = [] + let drainResult + let settlementsUnreached = false + try { + __prepareWasmEnvCleanup() + drainResult = __drainWasmEnvCleanup() + } catch (cleanupError) { + cleanupErrors.push(cleanupError) + settlementsUnreached = true + } + if (__isThenable(drainResult)) { + return Promise.resolve(drainResult).then( + () => __destroyContextForWasiRollback(cleanupErrors), + (cleanupError) => { + cleanupErrors.push(cleanupError) + return __retainFailedWasiRollback(cleanupErrors) + }, + ) + } + if (settlementsUnreached) { + return __retainFailedWasiRollback(cleanupErrors) + } + return __destroyContextForWasiRollback(cleanupErrors) + } + + // Same reason as `__startWasiDisposal`: a module-init hook can start async + // work before the load goes on to fail, and this rollback tears down exactly + // what those completions need. Settle them while everything is still live, + // before the barrier and the teardown above take that away. + // + // A drain that could not finish leaves async work possibly outstanding, and + // destroying the context over it would strand exactly what this rollback is + // there to settle. Stop short and retain instead — the same trade + // `__rollbackWasmEnvForWasiInitialization` makes for the settlement drain, so + // the context stays reclaimable by a retry or by this flavor's own + // last-resort teardown. + const __retainAfterAsyncWorkDrainFailure = (cleanupError) => + __retainFailedWasiRollback([cleanupError]) + let asyncWorkResult + try { + asyncWorkResult = __drainWasiAsyncWork() + } catch (cleanupError) { + return __retainAfterAsyncWorkDrainFailure(cleanupError) + } + if (__isThenable(asyncWorkResult)) { + return Promise.resolve(asyncWorkResult).then( + __rollbackWasmEnvForWasiInitialization, + __retainAfterAsyncWorkDrainFailure, + ) + } + return __rollbackWasmEnvForWasiInitialization() +} + +const __wasiRollbackRegistrySymbol = Symbol.for('napi.rs.wasi.rollback.registry.v1') +const __wasiRollbackRegistryKey = + typeof __filename === 'string' ? __filename : __wasmFilePath + +function __getWasiRollbackRegistry() { + const existing = process[__wasiRollbackRegistrySymbol] + if (existing !== undefined) { + if (!(existing instanceof Map)) { + throw new TypeError( + 'The process-wide NAPI-RS WASI rollback registry is invalid', + ) + } + return existing + } + const registry = new Map() + Object.defineProperty(process, __wasiRollbackRegistrySymbol, { + configurable: false, + enumerable: false, + value: registry, + writable: false, + }) + return registry +} + +const __wasiRollbackRegistry = __getWasiRollbackRegistry() + +function __completeWasiInitializationRollback(record, cleanupErrors) { + try { + if (cleanupErrors.length === 0) { + if ( + __wasiRollbackRegistry.get(__wasiRollbackRegistryKey) === record + ) { + __wasiRollbackRegistry.delete(__wasiRollbackRegistryKey) + } + return + } + record.error = __attachCleanupErrors(record.error, cleanupErrors) + } catch (cleanupError) { + try { + record.error = __createCleanupError( + [record.error, cleanupError], + 'WASI binding initialization and cleanup failed', + ) + } catch {} + } finally { + record.active = false + record.promise = undefined + } +} + +function __runWasiInitializationRollback(record) { + if (record.active) { + return + } + record.active = true + + let rollbackResult + try { + rollbackResult = record.rollback() + } catch (cleanupError) { + __completeWasiInitializationRollback(record, [cleanupError]) + return + } + + if (!__isThenable(rollbackResult)) { + __completeWasiInitializationRollback(record, rollbackResult) + return + } + + record.promise = Promise.resolve(rollbackResult).then( + (cleanupErrors) => { + __completeWasiInitializationRollback(record, cleanupErrors) + }, + (cleanupError) => { + __completeWasiInitializationRollback(record, [cleanupError]) + }, + ) +} + +const __pendingWasiRollback = __wasiRollbackRegistry.get( + __wasiRollbackRegistryKey, +) +if (__pendingWasiRollback !== undefined) { + __runWasiInitializationRollback(__pendingWasiRollback) + throw __pendingWasiRollback.error +} + +let __wasiModule +let __napiModule +let __wasiExitListenerRegistered = false + +function __removeWasiExitListener() { + if ( + __wasiExitListenerRegistered && + typeof process.removeListener === 'function' + ) { + process.removeListener('exit', __disposeWasiBindingAtExit) + } + __wasiExitListenerRegistered = false +} + +function __disposeWasiBindingAtExit() { + __wasiExitListenerRegistered = false + // An 'exit' handler cannot yield, so it cannot wait for queued promise + // settlements the way __startWasiDisposal does — the process is leaving and + // those promises have no observer left anyway. Run the synchronous teardown + // directly. Every step is idempotent, which also makes this the synchronous + // finish for a disposal that is still waiting for its drain. + try { + __destroyEmnapiContext() + } catch {} + try { + const workerResult = __terminateWasiWorkers() + if (__isThenable(workerResult)) { + void Promise.resolve(workerResult).catch(() => {}) + } + } catch {} +} + +function __registerWasiExitListener() { + if ( + !__wasiExitListenerRegistered && + typeof process.once === 'function' + ) { + process.once('exit', __disposeWasiBindingAtExit) + __wasiExitListenerRegistered = true + } +} + +__completeWasiDisposal = __removeWasiExitListener +// A rollback that could not reach the queued settlements keeps the context so +// the registry replay above can retry it. Nothing forces that replay to happen, +// so hand the context to the same synchronous teardown a successful load uses: +// a process that exits without ever retrying still runs the cleanup hooks. The +// handler cannot yield, so it does not settle anything — but by then the process +// is leaving and those promises have no observer left anyway. +__retainWasiRollbackForRetry = __registerWasiExitListener + +function __captureEmnapiAutoDestroyListener() { + if ( + typeof process.prependListener !== 'function' || + typeof process.removeListener !== 'function' + ) { + return + } + let __autoDestroyListener + const __captureListener = (__event, __listener) => { + if (__event === 'beforeExit' && __autoDestroyListener === undefined) { + __autoDestroyListener = __listener + } + } + try { + // Run before existing newListener hooks so a hook that registers its own + // beforeExit listener cannot be mistaken for emnapi's registration. + process.prependListener('newListener', __captureListener) + } catch { + return + } + return () => { + try { + process.removeListener('newListener', __captureListener) + } catch {} + if (__autoDestroyListener !== undefined) { + try { + process.removeListener('beforeExit', __autoDestroyListener) + } catch {} + } + } +} + +try { + const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener() + try { + __emnapiContext = __wrapEmnapiContextDestroyForSettlement( + __emnapiCreateContext({ autoDestroy: false }), + __prepareWasmEnvCleanup, + __isPreparingWasmEnvCleanup, + ) + // emnapi 2.x still registers an unconditional once-listener for + // beforeExit that auto-destroys the context, and suppressDestroy() only + // neutralizes its callback without removing it. This loader owns cleanup + // through its 'exit' listener, so emnapi's listener is captured and + // removed; suppressDestroy() remains the safety net when removal fails. + __emnapiContext.suppressDestroy() + } finally { + // Remove only the exact emnapi callback captured above. + __finishAutoDestroyCapture?.() + } + + ;({ + instance: __napiInstance, + module: __wasiModule, + napiModule: __napiModule, + } = __emnapiInstantiateNapiModuleSync(__wasmFile, { + context: __emnapiContext, + asyncWorkPoolSize: 0, + plugins: [ + __captureWasiThreadManager, + __emnapiAsyncWorkPlugin, + __emnapiTSFNPlugin, + ], + wasi: __wasi, + overwriteImports(importObject) { + importObject.env = { + ...importObject.env, + ...importObject.napi, + ...importObject.emnapi, + memory: __wasmMemory, + } + return importObject + }, + beforeInit({ instance }) { + __napiInstance = instance + for (const name of Object.keys(instance.exports)) { + if (name.startsWith('__napi_register__')) { + instance.exports[name]() + } + } + }, + })) + __publishWasiDispose(__napiModule.exports) + // The CommonJS tail below aliases `__napiModule.exports`; a named module + // export does not travel with it, so carry the marker on the binding itself + // too. Three things pin the stamp to exactly this spot: + // - inside this `try`, because the guard throws on a + // `#[napi(module_exports)]` hook that claimed the name, and only the + // catch below tears the environment — context, workers, exit listener — + // back down; + // - after the async runtime host install, which hands this same object to + // addon-provided registration functions that may put anything on it; + // - assigning onto the loader's own `module.exports`, which is still the + // original object here, so an addon accessor with a refusing setter is + // never written through. `cjs-module-lexer` — Node's CJS -> ESM named + // export detection — reads the static `module.exports. =` either + // way, and the later `module.exports = __napiModule.exports` does not + // undo that. + module.exports.__napiBindingTarget = __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget) + __registerWasiExitListener() +} catch (error) { + const rollback = { + active: false, + error, + promise: undefined, + rollback: __rollbackWasiInitialization, + } + __wasiRollbackRegistry.set(__wasiRollbackRegistryKey, rollback) + __runWasiInitializationRollback(rollback) + throw rollback.error +} +module.exports = __napiModule.exports +module.exports.Severity = __napiModule.exports.Severity +module.exports.HelperMode = __napiModule.exports.HelperMode +module.exports.isolatedDeclaration = __napiModule.exports.isolatedDeclaration +module.exports.isolatedDeclarationSync = __napiModule.exports.isolatedDeclarationSync +module.exports.moduleRunnerTransform = __napiModule.exports.moduleRunnerTransform +module.exports.moduleRunnerTransformSync = __napiModule.exports.moduleRunnerTransformSync +module.exports.transform = __napiModule.exports.transform +module.exports.transformSync = __napiModule.exports.transformSync diff --git a/napi/transform/transform.wasip1.d.cts b/napi/transform/transform.wasip1.d.cts new file mode 100644 index 0000000000..ffc52c88e0 --- /dev/null +++ b/napi/transform/transform.wasip1.d.cts @@ -0,0 +1,695 @@ +/* auto-generated by NAPI-RS */ +/* eslint-disable */ + +/** The WASI flavor this loader instantiates. */ +export declare const __napiBindingTarget: 'wasm32-wasip1' + +export interface Comment { + type: 'Line' | 'Block' + value: string + start: number + end: number +} + +export interface ErrorLabel { + message: string | null + start: number + end: number +} + +export interface OxcError { + severity: Severity + message: string + labels: Array + helpMessage: string | null + codeframe: string | null +} + +export declare const enum Severity { + Error = 'Error', + Warning = 'Warning', + Advice = 'Advice', +} + +export interface SourceMap { + file?: string + mappings: string + names: Array + sourceRoot?: string + sources: Array + sourcesContent?: Array + version: number + x_google_ignoreList?: Array +} + +export interface ArrowFunctionsOptions { + /** + * This option enables the following: + * * Wrap the generated function in .bind(this) and keeps uses of this inside the function as-is, instead of using a renamed this. + * * Add a runtime check to ensure the functions are not instantiated. + * * Add names to arrow functions. + * + * @default false + */ + spec?: boolean +} + +export interface CompilerAssumptions { + ignoreFunctionLength?: boolean + noDocumentAll?: boolean + objectRestNoSymbols?: boolean + pureGetters?: boolean + /** + * When using public class fields, assume that they don't shadow any getter in the current class, + * in its subclasses or in its superclass. Thus, it's safe to assign them rather than using + * `Object.defineProperty`. + * + * For example: + * + * Input: + * ```js + * class Test { + * field = 2; + * + * static staticField = 3; + * } + * ``` + * + * When `set_public_class_fields` is `true`, the output will be: + * ```js + * class Test { + * constructor() { + * this.field = 2; + * } + * } + * Test.staticField = 3; + * ``` + * + * Otherwise, the output will be: + * ```js + * import _defineProperty from "@oxc-project/runtime/helpers/defineProperty"; + * class Test { + * constructor() { + * _defineProperty(this, "field", 2); + * } + * } + * _defineProperty(Test, "staticField", 3); + * ``` + * + * NOTE: For TypeScript, if you wanted behavior is equivalent to `useDefineForClassFields: false`, you should + * set both `set_public_class_fields` and [`crate::TypeScriptOptions::remove_class_fields_without_initializer`] + * to `true`. + */ + setPublicClassFields?: boolean +} + +export interface DecoratorOptions { + /** + * Enables experimental support for decorators, which is a version of decorators that predates the TC39 standardization process. + * + * Decorators are a language feature which hasn’t yet been fully ratified into the JavaScript specification. + * This means that the implementation version in TypeScript may differ from the implementation in JavaScript when it it decided by TC39. + * + * @see https://www.typescriptlang.org/tsconfig/#experimentalDecorators + * @default false + */ + legacy?: boolean + /** + * Enables emitting decorator metadata. + * + * This option the same as [emitDecoratorMetadata](https://www.typescriptlang.org/tsconfig/#emitDecoratorMetadata) + * in TypeScript, and it only works when `legacy` is true. + * + * @see https://www.typescriptlang.org/tsconfig/#emitDecoratorMetadata + * @default false + */ + emitDecoratorMetadata?: boolean + /** + * Aligns nullable-union `design:type` emission with `--strictNullChecks`. + * + * When `true` (default), `T | null` and `T | undefined` emit `Object`, matching tsc strict. + * When `false`, `null` and `undefined` are elided from the union so the underlying + * primitive constructor is emitted, matching tsc with `--strictNullChecks=false` + * and `babel-plugin-transform-typescript-metadata`. + * + * @see https://www.typescriptlang.org/tsconfig/#strictNullChecks + * @default true + */ + strictNullChecks?: boolean +} + +export interface Es2015Options { + /** Transform arrow functions into function expressions. */ + arrowFunction?: ArrowFunctionsOptions +} + +export declare const enum HelperMode { + /** + * Runtime mode (default): Helper functions are imported from a runtime package. + * + * Example: + * + * ```js + * import helperName from "@oxc-project/runtime/helpers/helperName"; + * helperName(...arguments); + * ``` + */ + Runtime = 'Runtime', + /** + * External mode: Helper functions are accessed from a global `babelHelpers` object. + * + * Example: + * + * ```js + * babelHelpers.helperName(...arguments); + * ``` + */ + External = 'External', +} + +export interface Helpers { + mode?: HelperMode +} + +/** + * TypeScript Isolated Declarations for Standalone DTS Emit (async) + * + * Note: This function can be slower than `isolatedDeclarationSync` due to the overhead of spawning a thread. + */ +export declare function isolatedDeclaration(filename: string, sourceText: string, options?: IsolatedDeclarationsOptions | undefined | null): Promise + +export interface IsolatedDeclarationsOptions { + /** + * Do not emit declarations for code that has an @internal annotation in its JSDoc comment. + * This is an internal compiler option; use at your own risk, because the compiler does not check that the result is valid. + * + * Default: `false` + * + * See + */ + stripInternal?: boolean + sourcemap?: boolean +} + +export interface IsolatedDeclarationsResult { + code: string + map?: SourceMap + errors: Array +} + +/** TypeScript Isolated Declarations for Standalone DTS Emit */ +export declare function isolatedDeclarationSync(filename: string, sourceText: string, options?: IsolatedDeclarationsOptions | undefined | null): IsolatedDeclarationsResult + +/** + * Configure how TSX and JSX are transformed. + * + * @see {@link https://oxc.rs/docs/guide/usage/transformer/jsx} + */ +export interface JsxOptions { + /** + * Decides which runtime to use. + * + * - 'automatic' - auto-import the correct JSX factories + * - 'classic' - no auto-import + * + * @default 'automatic' + */ + runtime?: 'classic' | 'automatic' + /** + * Emit development-specific information, such as `__source` and `__self`. + * + * @default false + */ + development?: boolean + /** + * Toggles whether or not to throw an error if an XML namespaced tag name + * is used. + * + * Though the JSX spec allows this, it is disabled by default since React's + * JSX does not currently have support for it. + * + * @default true + */ + throwIfNamespace?: boolean + /** + * Mark JSX elements and top-level React method calls as pure for tree shaking. + * + * @default true + */ + pure?: boolean + /** + * Replaces the import source when importing functions. + * + * @default 'react' + */ + importSource?: string + /** + * Replace the function used when compiling JSX expressions. It should be a + * qualified name (e.g. `React.createElement`) or an identifier (e.g. + * `createElement`). + * + * Only used for `classic` {@link runtime}. + * + * @default 'React.createElement' + */ + pragma?: string + /** + * Replace the component used when compiling JSX fragments. It should be a + * valid JSX tag name. + * + * Only used for `classic` {@link runtime}. + * + * @default 'React.Fragment' + */ + pragmaFrag?: string + /** + * Enable React Fast Refresh . + * + * Conforms to the implementation in {@link https://github.com/facebook/react/tree/v18.3.1/packages/react-refresh} + * + * @default false + */ + refresh?: boolean | ReactRefreshOptions +} + +/** + * Transform JavaScript code to a Vite Node runnable module. + * + * @param filename The name of the file being transformed. + * @param sourceText the source code itself + * @param options The options for the transformation. See {@link + * ModuleRunnerTransformOptions} for more information. + * + * @returns an object containing the transformed code, source maps, and any + * errors that occurred during parsing or transformation. + * + * Note: This function can be slower than `moduleRunnerTransformSync` due to the overhead of spawning a thread. + * + * @deprecated Only works for Vite. + */ +export declare function moduleRunnerTransform(filename: string, sourceText: string, options?: ModuleRunnerTransformOptions | undefined | null): Promise + +export interface ModuleRunnerTransformOptions { + /** + * Enable source map generation. + * + * When `true`, the `sourceMap` field of transform result objects will be populated. + * + * @default false + * + * @see {@link SourceMap} + */ + sourcemap?: boolean +} + +export interface ModuleRunnerTransformResult { + /** + * The transformed code. + * + * If parsing failed, this will be an empty string. + */ + code: string + /** + * The source map for the transformed code. + * + * This will be set if {@link TransformOptions#sourcemap} is `true`. + */ + map?: SourceMap + deps: Array + dynamicDeps: Array + /** + * Parse and transformation errors. + * + * Oxc's parser recovers from common syntax errors, meaning that + * transformed code may still be available even if there are errors in this + * list. + */ + errors: Array +} + +/** @deprecated Only works for Vite. */ +export declare function moduleRunnerTransformSync(filename: string, sourceText: string, options?: ModuleRunnerTransformOptions | undefined | null): ModuleRunnerTransformResult + +export interface PluginsOptions { + styledComponents?: StyledComponentsOptions + taggedTemplateEscape?: boolean +} + +export interface ReactRefreshOptions { + /** + * Specify the identifier of the refresh registration variable. + * + * @default `$RefreshReg$`. + */ + refreshReg?: string + /** + * Specify the identifier of the refresh signature variable. + * + * @default `$RefreshSig$`. + */ + refreshSig?: string + emitFullSignatures?: boolean +} + +/** + * Configure how styled-components are transformed. + * + * @see {@link https://oxc.rs/docs/guide/usage/transformer/plugins#styled-components} + */ +export interface StyledComponentsOptions { + /** + * Enhances the attached CSS class name on each component with richer output to help + * identify your components in the DOM without React DevTools. + * + * @default true + */ + displayName?: boolean + /** + * Controls whether the `displayName` of a component will be prefixed with the filename + * to make the component name as unique as possible. + * + * @default true + */ + fileName?: boolean + /** + * Adds a unique identifier to every styled component to avoid checksum mismatches + * due to different class generation on the client and server during server-side rendering. + * + * @default true + */ + ssr?: boolean + /** + * Transpiles styled-components tagged template literals to a smaller representation + * than what Babel normally creates, helping to reduce bundle size. + * + * Disabled by default because Oxc does not down-level template literals, so this + * transform only increases output size. + * + * @default false + */ + transpileTemplateLiterals?: boolean + /** + * Minifies CSS content by removing all whitespace and comments from your CSS, + * keeping valuable bytes out of your bundles. + * + * @default true + */ + minify?: boolean + /** + * Enables transformation of JSX `css` prop when using styled-components. + * + * **Note: This feature is not yet implemented in oxc.** + * + * @default true + */ + cssProp?: boolean + /** + * Enables "pure annotation" to aid dead code elimination by bundlers. + * + * @default false + */ + pure?: boolean + /** + * Adds a namespace prefix to component identifiers to ensure class names are unique. + * + * Example: With `namespace: "my-app"`, generates `componentId: "my-app__sc-3rfj0a-1"` + */ + namespace?: string + /** + * List of file names that are considered meaningless for component naming purposes. + * + * When the `fileName` option is enabled and a component is in a file with a name + * from this list, the directory name will be used instead of the file name for + * the component's display name. + * + * @default `["index"]` + */ + meaninglessFileNames?: Array + /** + * Import paths to be considered as styled-components imports at the top level. + * + * **Note: This feature is not yet implemented in oxc.** + */ + topLevelImportPaths?: Array +} + +/** + * Transpile a JavaScript or TypeScript into a target ECMAScript version, asynchronously. + * + * Note: This function can be slower than `transform` due to the overhead of spawning a thread. + * + * @param filename The name of the file being transformed. If this is a + * relative path, consider setting the {@link TransformOptions#cwd} option. + * @param sourceText the source code itself + * @param options The options for the transformation. See {@link + * TransformOptions} for more information. + * + * @returns a promise that resolves to an object containing the transformed code, + * source maps, and any errors that occurred during parsing or transformation. + */ +export declare function transform(filename: string, sourceText: string, options?: TransformOptions | undefined | null): Promise + +/** + * Options for transforming a JavaScript or TypeScript file. + * + * Options are listed in evaluation order: the source is parsed (`lang`, + * `sourceType`), declarations are emitted (`typescript.declaration`), then + * transforms run (`typescript`, `decorator`, `plugins`, + * `jsx`, `target`), followed by the `inject` and `define` plugins, and + * finally codegen (`sourcemap`). `helpers` configures the runtime helpers + * the transforms emit. + * + * @see {@link transform} + */ +export interface TransformOptions { + /** Treat the source text as `js`, `jsx`, `ts`, `tsx`, or `dts`. */ + lang?: 'js' | 'jsx' | 'ts' | 'tsx' | 'dts' + /** Treat the source text as `script` or `module` code. */ + sourceType?: 'script' | 'module' | 'commonjs' | 'unambiguous' | undefined + /** + * The current working directory. Used to resolve relative paths in other + * options. + */ + cwd?: string + /** Set assumptions in order to produce smaller output. */ + assumptions?: CompilerAssumptions + /** + * Configure how TypeScript is transformed. + * + * `typescript.declaration` is evaluated before all transforms. + * + * @see {@link https://oxc.rs/docs/guide/usage/transformer/typescript} + */ + typescript?: TypeScriptOptions + /** Decorator plugin */ + decorator?: DecoratorOptions + /** + * Third-party plugins to use. + * @see {@link https://oxc.rs/docs/guide/usage/transformer/plugins} + */ + plugins?: PluginsOptions + /** + * Configure how TSX and JSX are transformed. + * @see {@link https://oxc.rs/docs/guide/usage/transformer/jsx} + */ + jsx?: 'preserve' | JsxOptions + /** + * Sets the target environment for the generated JavaScript. + * + * The lowest target is `es2015`. + * + * Example: + * + * * `'es2015'` + * * `['es2020', 'chrome58', 'edge16', 'firefox57', 'node12', 'safari11']` + * + * @default `esnext` (No transformation) + * + * @see {@link https://oxc.rs/docs/guide/usage/transformer/lowering#target} + */ + target?: string | Array + /** Behaviour for runtime helpers. */ + helpers?: Helpers + /** + * Inject Plugin + * + * Runs after all transforms. + * + * @see {@link https://oxc.rs/docs/guide/usage/transformer/global-variable-replacement#inject} + */ + inject?: Record + /** + * Define Plugin + * + * Runs after the inject plugin. + * + * @see {@link https://oxc.rs/docs/guide/usage/transformer/global-variable-replacement#define} + */ + define?: Record + /** + * Enable source map generation. + * + * When `true`, the `sourceMap` field of transform result objects will be populated. + * + * @default false + * + * @see {@link SourceMap} + */ + sourcemap?: boolean +} + +export interface TransformResult { + /** + * The transformed code. + * + * If parsing failed, this will be an empty string. + */ + code: string + /** + * The source map for the transformed code. + * + * This will be set if {@link TransformOptions#sourcemap} is `true`. + */ + map?: SourceMap + /** + * The `.d.ts` declaration file for the transformed code. Declarations are + * only generated if `declaration` is set to `true` and a TypeScript file + * is provided. + * + * If parsing failed and `declaration` is set, this will be an empty string. + * + * @see {@link TypeScriptOptions#declaration} + * @see [declaration tsconfig option](https://www.typescriptlang.org/tsconfig/#declaration) + */ + declaration?: string + /** + * Declaration source map. Only generated if both + * {@link TypeScriptOptions#declaration declaration} and + * {@link TransformOptions#sourcemap sourcemap} are set to `true`. + */ + declarationMap?: SourceMap + /** + * Helpers used. + * + * @internal + * + * Example: + * + * ```text + * { "_objectSpread": "@oxc-project/runtime/helpers/objectSpread2" } + * ``` + */ + helpersUsed: Record + /** + * Parse and transformation errors. + * + * Oxc's parser recovers from common syntax errors, meaning that + * transformed code may still be available even if there are errors in this + * list. + */ + errors: Array +} + +/** + * Transpile a JavaScript or TypeScript into a target ECMAScript version. + * + * @param filename The name of the file being transformed. If this is a + * relative path, consider setting the {@link TransformOptions#cwd} option.. + * @param sourceText the source code itself + * @param options The options for the transformation. See {@link + * TransformOptions} for more information. + * + * @returns an object containing the transformed code, source maps, and any + * errors that occurred during parsing or transformation. + */ +export declare function transformSync(filename: string, sourceText: string, options?: TransformOptions | undefined | null): TransformResult + +export interface TypeScriptOptions { + jsxPragma?: string + jsxPragmaFrag?: string + onlyRemoveTypeImports?: boolean + allowNamespaces?: boolean + /** + * When enabled, type-only class fields are only removed if they are prefixed with the declare modifier: + * + * @deprecated + * + * Allowing `declare` fields is built-in support in Oxc without any option. If you want to remove class fields + * without initializer, you can use `remove_class_fields_without_initializer: true` instead. + */ + allowDeclareFields?: boolean + /** + * When enabled, class fields without initializers are removed. + * + * For example: + * ```ts + * class Foo { + * x: number; + * y: number = 0; + * } + * ``` + * // transform into + * ```js + * class Foo { + * x: number; + * } + * ``` + * + * The option is used to align with the behavior of TypeScript's `useDefineForClassFields: false` option. + * When you want to enable this, you also need to set [`crate::CompilerAssumptions::set_public_class_fields`] + * to `true`. The `set_public_class_fields: true` + `remove_class_fields_without_initializer: true` is + * equivalent to `useDefineForClassFields: false` in TypeScript. + * + * When `set_public_class_fields` is true and class-properties plugin is enabled, the above example transforms into: + * + * ```js + * class Foo { + * constructor() { + * this.y = 0; + * } + * } + * ``` + * + * Defaults to `false`. + */ + removeClassFieldsWithoutInitializer?: boolean + /** + * When true, optimize const enums by inlining their values at usage sites + * and removing the enum declaration. + * + * @default false + */ + optimizeConstEnums?: boolean + /** + * When true, optimize regular (non-const) enums by inlining their member + * accesses at usage sites when the member value is statically known. + * + * Non-exported enum declarations are also removed when all members are + * evaluable and no references to the enum as a runtime value exist + * (e.g., `console.log(Foo)`, `typeof Foo`, or passing the enum as an argument). + * + * @default false + */ + optimizeEnums?: boolean + /** + * Also generate a `.d.ts` declaration file for TypeScript files. + * + * The source file must be compliant with all + * [`isolatedDeclarations`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html#isolated-declarations) + * requirements. + * + * @default false + */ + declaration?: IsolatedDeclarationsOptions + /** + * Rewrite or remove TypeScript import/export declaration extensions. + * + * - When set to `rewrite`, it will change `.ts`, `.mts`, `.cts` extensions to `.js`, `.mjs`, `.cjs` respectively. + * - When set to `remove`, it will remove `.ts`/`.mts`/`.cts`/`.tsx` extension entirely. + * - When set to `true`, it's equivalent to `rewrite`. + * - When set to `false` or omitted, no changes will be made to the extensions. + * + * @default false + */ + rewriteImportExtensions?: 'rewrite' | 'remove' | boolean +} diff --git a/oxfmtrc.jsonc b/oxfmtrc.jsonc index 1b8a1fa3c1..16fa45607a 100644 --- a/oxfmtrc.jsonc +++ b/oxfmtrc.jsonc @@ -40,6 +40,7 @@ "napi/{parser,transform,transform-react,transform-relay,minify,playground}/**/*.wasi.d.cts", "napi/{parser,transform,transform-react,transform-relay,minify,playground}/**/*.wasi-browser.js", "napi/{parser,transform,transform-react,transform-relay,minify,playground}/**/*.wasi.cjs", + "napi/{parser,transform,transform-react,transform-relay,minify}/**/*.wasip1*", "napi/{parser,transform,transform-react,transform-relay,minify,playground}/**/wasi-worker-browser.mjs", "napi/{parser,transform,transform-react,transform-relay,minify,playground}/**/wasi-worker.mjs", "napi/{parser,transform,transform-react,transform-relay,minify,playground}/**/browser.js", diff --git a/oxlintrc.json b/oxlintrc.json index 8341c8272d..aedef874f2 100644 --- a/oxlintrc.json +++ b/oxlintrc.json @@ -106,10 +106,12 @@ "*.d.cts", "napi/*/*.wasi.cjs", "napi/*/*.wasi-browser.js", + "napi/*/*.wasip1*", "napi/*/wasi-worker.mjs", "napi/*/wasi-worker-browser.mjs", "napi/*/src-js/*.wasi.cjs", "napi/*/src-js/*.wasi-browser.js", + "napi/*/src-js/*.wasip1*", "napi/*/src-js/wasi-worker.mjs", "napi/*/src-js/wasi-worker-browser.mjs", "npm/oxlint-plugins/**" diff --git a/package.json b/package.json index d54ecd9a21..41cca12c76 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "build-dev": "pnpm --workspace-concurrency=1 --filter \"./napi/*\" --filter \"./apps/*\" --filter \"./packages/*\" build-dev", "build-test": "pnpm --workspace-concurrency=1 --filter \"./napi/*\" --filter \"./apps/*\" --filter \"./packages/*\" build-test", "build-wasm-dev": "pnpm --workspace-concurrency=1 --filter \"./napi/*\" build-wasm-dev", + "build-wasip1-dev": "pnpm --workspace-concurrency=1 --filter oxc-parser --filter oxc-minify --filter oxc-transform --filter oxc-transform-react --filter oxc-transform-relay build-wasip1-dev", "test": "pnpm --workspace-concurrency=1 --filter \"./napi/*\" --filter \"./apps/*\" --filter \"./packages/*\" test", "fmt": "oxfmt -c oxfmtrc.jsonc", "lint": "oxlint -c oxlintrc.json --deny-warnings --report-unused-disable-directives"