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:
Boshen committed 2026-09-23 06:54:41 +00:00
1 parent 01ff33e9c8
commit f585cb42cd
54 files changed
+20979 -18

No files matched your search

+3
View File
@@ -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
View File
@@ -1 +1 @@
export * from '@oxc-minify/binding-wasm32-wasi'
export * from '@oxc-minify/binding-wasm32-wasip1'
+49 -1
View File
@@ -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 &&
+990
View File
@@ -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
View File
@@ -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
+353
View File
@@ -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>
}
+3
View File
@@ -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",
+4
View File
@@ -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 -1
View File
@@ -1,3 +1,3 @@
{
"targets": ["wasm32-wasip1-threads"]
"targets": ["wasm32-wasip1", "wasm32-wasip1-threads"]
}
+3
View File
@@ -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",
+4 -4
View File
@@ -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"));
}
}
+4
View File
@@ -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);
+49 -1
View File
@@ -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
View File
@@ -1 +1 @@
export * from '@oxc-parser/binding-wasm32-wasi'
export * from '@oxc-parser/binding-wasm32-wasip1'
+995
View File
@@ -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
View File
@@ -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
+301
View File
@@ -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
}
+2 -2
View File
@@ -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";
+1 -1
View File
@@ -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
View File
@@ -1 +1 @@
export * from '@oxc-transform-react/binding-wasm32-wasi'
export * from '@oxc-transform-react/binding-wasm32-wasip1'
+49 -1
View File
@@ -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 &&
+3
View File
@@ -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",
+4
View File
@@ -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
View File
@@ -1 +1 @@
export * from '@oxc-transform-relay/binding-wasm32-wasi'
export * from '@oxc-transform-relay/binding-wasm32-wasip1'
+49 -1
View File
@@ -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 &&
+3
View File
@@ -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",
+4
View File
@@ -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
View File
@@ -1 +1 @@
export * from '@oxc-transform/binding-wasm32-wasi'
export * from '@oxc-transform/binding-wasm32-wasip1'
+49 -1
View File
@@ -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 &&
+3
View File
@@ -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",
+4
View File
@@ -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);
+994
View File
@@ -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
View File
@@ -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
+695
View File
@@ -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
}
+1
View File
@@ -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",
+2
View File
@@ -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/**"
+1
View File
@@ -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"