mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-15 06:39:17 +00:00
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.
This commit is contained in:
54 files changed
+20979
-18
No files matched your search
@@ -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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from '@oxc-minify/binding-wasm32-wasi'
|
||||
export * from '@oxc-minify/binding-wasm32-wasip1'
|
||||
+49
-1
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
export type WasiBinding = typeof import('./minify.wasip1.cjs')
|
||||
|
||||
export type WasiModuleInput =
|
||||
| WebAssembly.Module
|
||||
| PromiseLike<WebAssembly.Module>
|
||||
|
||||
/** 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<void>
|
||||
}
|
||||
|
||||
/** 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<WasiRuntimeStats>
|
||||
|
||||
export function instantiate(wasmInput: WasiModuleInput): Promise<WasiBinding>
|
||||
export function createInstance(
|
||||
wasmInput: WasiModuleInput,
|
||||
options?: WasiInstanceOptions,
|
||||
): Promise<WasiInstance>
|
||||
/** Dispose the singleton and retry retained failed-initialization cleanup. */
|
||||
export function dispose(): Promise<void>
|
||||
|
||||
/** The WASI flavor this deferred loader instantiates. */
|
||||
export declare const __napiBindingTarget: 'wasm32-wasip1'
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -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<string>
|
||||
/**
|
||||
* 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<string>
|
||||
/** 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<string>
|
||||
/** 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<string>
|
||||
/**
|
||||
* 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<string, string | false>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<MinifyResult>
|
||||
|
||||
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<OxcError>
|
||||
/**
|
||||
* Legal comments extracted from the source code.
|
||||
* Only populated when `codegen.legalComments` is `"linked"` or `"external"`.
|
||||
*/
|
||||
legalComments: Array<string>
|
||||
/**
|
||||
* Updated property-name cache sorted by original name. Present when `mangleProps` ran on a
|
||||
* parse without errors.
|
||||
*/
|
||||
mangleCache?: Record<string, string | false>
|
||||
}
|
||||
|
||||
/** 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<string>
|
||||
/**
|
||||
* 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<ErrorLabel>
|
||||
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<string>
|
||||
sourceRoot?: string
|
||||
sources: Array<string>
|
||||
sourcesContent?: Array<string>
|
||||
version: number
|
||||
x_google_ignoreList?: Array<number>
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"targets": ["wasm32-wasip1-threads"]
|
||||
"targets": ["wasm32-wasip1", "wasm32-wasip1-threads"]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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 `<npmDir>/wasm32-wasi`
|
||||
// copy `browser-bundle.js` to `<npmDir>/wasm32-wasip1`
|
||||
fs.cpSync("browser-bundle.js", path.join(pkgDir, "browser-bundle.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);
|
||||
@@ -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 &&
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from '@oxc-parser/binding-wasm32-wasi'
|
||||
export * from '@oxc-parser/binding-wasm32-wasip1'
|
||||
@@ -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
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
export type WasiBinding = typeof import('./parser.wasip1.cjs')
|
||||
|
||||
export type WasiModuleInput =
|
||||
| WebAssembly.Module
|
||||
| PromiseLike<WebAssembly.Module>
|
||||
|
||||
/** 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<void>
|
||||
}
|
||||
|
||||
/** 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<WasiRuntimeStats>
|
||||
|
||||
export function instantiate(wasmInput: WasiModuleInput): Promise<WasiBinding>
|
||||
export function createInstance(
|
||||
wasmInput: WasiModuleInput,
|
||||
options?: WasiInstanceOptions,
|
||||
): Promise<WasiInstance>
|
||||
/** Dispose the singleton and retry retained failed-initialization cleanup. */
|
||||
export function dispose(): Promise<void>
|
||||
|
||||
/** The WASI flavor this deferred loader instantiates. */
|
||||
export declare const __napiBindingTarget: 'wasm32-wasip1'
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -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<ErrorLabel>
|
||||
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<Comment>
|
||||
get errors(): Array<OxcError>
|
||||
}
|
||||
|
||||
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<StaticImport>
|
||||
/** Export statements. */
|
||||
staticExports: Array<StaticExport>
|
||||
/** Dynamic import expressions. */
|
||||
dynamicImports: Array<DynamicImport>
|
||||
/** Span positions` of `import.meta` */
|
||||
importMetas: Array<Span>
|
||||
}
|
||||
|
||||
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<ParseResult>
|
||||
|
||||
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<StaticExportEntry>
|
||||
}
|
||||
|
||||
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<StaticImportEntry>
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from '@oxc-transform-react/binding-wasm32-wasi'
|
||||
export * from '@oxc-transform-react/binding-wasm32-wasip1'
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -0,0 +1,83 @@
|
||||
export type WasiBinding = typeof import('./transform-react.wasip1.cjs')
|
||||
|
||||
export type WasiModuleInput =
|
||||
| WebAssembly.Module
|
||||
| PromiseLike<WebAssembly.Module>
|
||||
|
||||
/** 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<void>
|
||||
}
|
||||
|
||||
/** 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<WasiRuntimeStats>
|
||||
|
||||
export function instantiate(wasmInput: WasiModuleInput): Promise<WasiBinding>
|
||||
export function createInstance(
|
||||
wasmInput: WasiModuleInput,
|
||||
options?: WasiInstanceOptions,
|
||||
): Promise<WasiInstance>
|
||||
/** Dispose the singleton and retry retained failed-initialization cleanup. */
|
||||
export function dispose(): Promise<void>
|
||||
|
||||
/** The WASI flavor this deferred loader instantiates. */
|
||||
export declare const __napiBindingTarget: 'wasm32-wasip1'
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -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<ErrorLabel>
|
||||
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<string>
|
||||
sourceRoot?: string
|
||||
sources: Array<string>
|
||||
sourcesContent?: Array<string>
|
||||
version: number
|
||||
x_google_ignoreList?: Array<number>
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure how TSX and JSX are transformed.
|
||||
*
|
||||
* @see <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.
|
||||
*
|
||||
* @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<string>
|
||||
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<string>
|
||||
validateBlocklistedImports?: Array<string>
|
||||
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<string>
|
||||
/**
|
||||
* 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<string>
|
||||
/**
|
||||
* 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<string>
|
||||
/** 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<TransformResult>
|
||||
|
||||
/**
|
||||
* 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 <https://oxc.rs/docs/guide/usage/transformer/jsx>
|
||||
*/
|
||||
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<OxcError>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1 +1 @@
|
||||
export * from '@oxc-transform-relay/binding-wasm32-wasi'
|
||||
export * from '@oxc-transform-relay/binding-wasm32-wasip1'
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -0,0 +1,83 @@
|
||||
export type WasiBinding = typeof import('./transform-relay.wasip1.cjs')
|
||||
|
||||
export type WasiModuleInput =
|
||||
| WebAssembly.Module
|
||||
| PromiseLike<WebAssembly.Module>
|
||||
|
||||
/** 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<void>
|
||||
}
|
||||
|
||||
/** 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<WasiRuntimeStats>
|
||||
|
||||
export function instantiate(wasmInput: WasiModuleInput): Promise<WasiBinding>
|
||||
export function createInstance(
|
||||
wasmInput: WasiModuleInput,
|
||||
options?: WasiInstanceOptions,
|
||||
): Promise<WasiInstance>
|
||||
/** Dispose the singleton and retry retained failed-initialization cleanup. */
|
||||
export function dispose(): Promise<void>
|
||||
|
||||
/** The WASI flavor this deferred loader instantiates. */
|
||||
export declare const __napiBindingTarget: 'wasm32-wasip1'
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -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<ErrorLabel>
|
||||
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<string>
|
||||
sourceRoot?: string
|
||||
sources: Array<string>
|
||||
sourcesContent?: Array<string>
|
||||
version: number
|
||||
x_google_ignoreList?: Array<number>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<TransformResult>
|
||||
|
||||
/**
|
||||
* 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<OxcError>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1 +1 @@
|
||||
export * from '@oxc-transform/binding-wasm32-wasi'
|
||||
export * from '@oxc-transform/binding-wasm32-wasip1'
|
||||
+49
-1
@@ -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 &&
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
export type WasiBinding = typeof import('./transform.wasip1.cjs')
|
||||
|
||||
export type WasiModuleInput =
|
||||
| WebAssembly.Module
|
||||
| PromiseLike<WebAssembly.Module>
|
||||
|
||||
/** 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<void>
|
||||
}
|
||||
|
||||
/** 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<WasiRuntimeStats>
|
||||
|
||||
export function instantiate(wasmInput: WasiModuleInput): Promise<WasiBinding>
|
||||
export function createInstance(
|
||||
wasmInput: WasiModuleInput,
|
||||
options?: WasiInstanceOptions,
|
||||
): Promise<WasiInstance>
|
||||
/** Dispose the singleton and retry retained failed-initialization cleanup. */
|
||||
export function dispose(): Promise<void>
|
||||
|
||||
/** The WASI flavor this deferred loader instantiates. */
|
||||
export declare const __napiBindingTarget: 'wasm32-wasip1'
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -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<ErrorLabel>
|
||||
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<string>
|
||||
sourceRoot?: string
|
||||
sources: Array<string>
|
||||
sourcesContent?: Array<string>
|
||||
version: number
|
||||
x_google_ignoreList?: Array<number>
|
||||
}
|
||||
|
||||
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<IsolatedDeclarationsResult>
|
||||
|
||||
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 <https://www.typescriptlang.org/tsconfig/#stripInternal>
|
||||
*/
|
||||
stripInternal?: boolean
|
||||
sourcemap?: boolean
|
||||
}
|
||||
|
||||
export interface IsolatedDeclarationsResult {
|
||||
code: string
|
||||
map?: SourceMap
|
||||
errors: Array<OxcError>
|
||||
}
|
||||
|
||||
/** 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<ModuleRunnerTransformResult>
|
||||
|
||||
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<string>
|
||||
dynamicDeps: Array<string>
|
||||
/**
|
||||
* 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<OxcError>
|
||||
}
|
||||
|
||||
/** @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<string>
|
||||
/**
|
||||
* Import paths to be considered as styled-components imports at the top level.
|
||||
*
|
||||
* **Note: This feature is not yet implemented in oxc.**
|
||||
*/
|
||||
topLevelImportPaths?: Array<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<TransformResult>
|
||||
|
||||
/**
|
||||
* 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<string>
|
||||
/** 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<string, string | [string, string]>
|
||||
/**
|
||||
* Define Plugin
|
||||
*
|
||||
* Runs after the inject plugin.
|
||||
*
|
||||
* @see {@link https://oxc.rs/docs/guide/usage/transformer/global-variable-replacement#define}
|
||||
*/
|
||||
define?: Record<string, string>
|
||||
/**
|
||||
* 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<string, string>
|
||||
/**
|
||||
* 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<OxcError>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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/**"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user