chore(deps): update dependency @napi-rs/cli to v3.10.0 (#26690)

This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Adoption](https://docs.renovatebot.com/merge-confidence/) |
[Passing](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) | Type |
Update |
|---|---|---|---|---|---|---|---|
| [@napi-rs/cli](https://napi.rs/)
([source](https://redirect.github.com/napi-rs/napi-rs)) | [`3.9.1` →
`3.10.1`](https://renovatebot.com/diffs/npm/@napi-rs%2fcli/3.9.1/3.10.1)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/@napi-rs%2fcli/3.10.1?slim=true)
|
![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@napi-rs%2fcli/3.10.1?slim=true)
|
![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@napi-rs%2fcli/3.9.1/3.10.1?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@napi-rs%2fcli/3.9.1/3.10.1?slim=true)
| pnpm.catalog.default | minor |
| [napi](https://redirect.github.com/napi-rs/napi-rs) | `3.12.4` →
`3.12.5` |
![age](https://developer.mend.io/api/mc/badges/age/crate/napi/3.12.5?slim=true)
|
![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/napi/3.12.5?slim=true)
|
![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/napi/3.12.4/3.12.5?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/napi/3.12.4/3.12.5?slim=true)
| workspace.dependencies | patch |
| [napi-derive](https://redirect.github.com/napi-rs/napi-rs) | `3.6.5` →
`3.6.6` |
![age](https://developer.mend.io/api/mc/badges/age/crate/napi-derive/3.6.6?slim=true)
|
![adoption](https://developer.mend.io/api/mc/badges/adoption/crate/napi-derive/3.6.6?slim=true)
|
![passing](https://developer.mend.io/api/mc/badges/compatibility/crate/napi-derive/3.6.5/3.6.6?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/crate/napi-derive/3.6.5/3.6.6?slim=true)
| workspace.dependencies | patch |

---

### Release Notes

<details>
<summary>napi-rs/napi-rs (@&#8203;napi-rs/cli)</summary>

###
[`v3.10.1`](https://redirect.github.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.10.0...@napi-rs/cli@3.10.1)

[Compare
Source](https://redirect.github.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.10.0...@napi-rs/cli@3.10.1)

###
[`v3.10.0`](https://redirect.github.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.9.1...@napi-rs/cli@3.10.0)

[Compare
Source](https://redirect.github.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.9.1...@napi-rs/cli@3.10.0)

</details>

---

### Configuration

📅 **Schedule**: (in timezone Asia/Shanghai)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/oxc-project/oxc).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC45My4wIiwidXBkYXRlZEluVmVyIjoiNDQuOTMuNCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Cameron Clark <cameron.clark@hey.com>
This commit is contained in:
renovate[bot]andCameron Clark authored and GitHub committed 2026-09-16 05:47:21 +00:00
1 parent f91ad92366
commit 536cea81a4
38 files changed
+2855 -96

No files matched your search

Generated
+4 -4
View File
@@ -1401,9 +1401,9 @@ dependencies = [
[[package]]
name = "napi"
version = "3.12.4"
version = "3.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d2c1d080c842ec1347ebf3cfb9347c15f8c2262f143cc30eea41ddf1c7da720"
checksum = "f0c007d4a8ead952a81887661d41fd56b7e7a70d3d4d921f445fb37a8f6efa1e"
dependencies = [
"bitflags",
"ctor",
@@ -1426,9 +1426,9 @@ checksum = "860e7c40864f95cfb83cde99f9ebadd88ef3d9bdccd7dd2cee0cc96a2dd4ffa7"
[[package]]
name = "napi-derive"
version = "3.6.5"
version = "3.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350057056a30368aa76c11a0d406b0aa61321710be2c36656ab4f6efe0b785a2"
checksum = "e8872852c2d050fc5859749864119bc5d50e3f6ad5957874d61d1a07c76771cb"
dependencies = [
"convert_case",
"ctor",
+8
View File
@@ -1,6 +1,14 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/**
* Which binding artifact the generated loader actually loaded: `'native'` for
* a native addon, otherwise the `platformArchABI` of the WASI flavor. Every
* flavor napi-rs can build is listed, because `NAPI_RS_NATIVE_LIBRARY_PATH`
* can point the loader at a WASI artifact this package does not build itself.
*/
export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'
export interface Comment {
type: 'Line' | 'Block'
value: string
+17 -1
View File
@@ -7,6 +7,10 @@ const require = createRequire(import.meta.url)
const { readFileSync } = require('fs')
let nativeBinding = null
// Which artifact actually loaded. The WASI fallback chain overwrites it with
// the flavor it resolved; the late native retry below leaves it alone because
// it only runs while no WASI candidate has been loaded.
let __napiLoadedBindingTarget = 'native'
const loadErrors = []
const isMusl = () => {
@@ -65,7 +69,16 @@ const isMuslFromChildProcess = () => {
function requireNative() {
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
try {
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
const overrideBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
// The override may be a generated WASI loader, which already reports its
// own flavor. Adopt it: `module.exports` aliases this object, so claiming
// 'native' would both misreport the artifact and overwrite the loader's
// marker through the alias.
__napiLoadedBindingTarget =
overrideBinding && typeof overrideBinding.__napiBindingTarget === 'string'
? overrideBinding.__napiBindingTarget
: 'native'
return overrideBinding
} catch (err) {
loadErrors.push(err)
}
@@ -630,6 +643,7 @@ if (!nativeBinding || forceWasi) {
if (!candidateFailed) {
wasiBinding = require('./oxfmt.wasi.cjs')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -656,6 +670,7 @@ if (!nativeBinding || forceWasi) {
}
wasiBinding = require('@oxfmt/binding-wasm32-wasi')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -706,3 +721,4 @@ export { Severity }
export { format }
export { jsTextToDoc }
export { runCli }
export const __napiBindingTarget = __napiLoadedBindingTarget
+8
View File
@@ -1,6 +1,14 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/**
* Which binding artifact the generated loader actually loaded: `'native'` for
* a native addon, otherwise the `platformArchABI` of the WASI flavor. Every
* flavor napi-rs can build is listed, because `NAPI_RS_NATIVE_LIBRARY_PATH`
* can point the loader at a WASI artifact this package does not build itself.
*/
export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'
export interface Comment {
type: 'Line' | 'Block'
value: string
+17 -1
View File
@@ -7,6 +7,10 @@ const require = createRequire(import.meta.url)
const { readFileSync } = require('fs')
let nativeBinding = null
// Which artifact actually loaded. The WASI fallback chain overwrites it with
// the flavor it resolved; the late native retry below leaves it alone because
// it only runs while no WASI candidate has been loaded.
let __napiLoadedBindingTarget = 'native'
const loadErrors = []
const isMusl = () => {
@@ -65,7 +69,16 @@ const isMuslFromChildProcess = () => {
function requireNative() {
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
try {
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
const overrideBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
// The override may be a generated WASI loader, which already reports its
// own flavor. Adopt it: `module.exports` aliases this object, so claiming
// 'native' would both misreport the artifact and overwrite the loader's
// marker through the alias.
__napiLoadedBindingTarget =
overrideBinding && typeof overrideBinding.__napiBindingTarget === 'string'
? overrideBinding.__napiBindingTarget
: 'native'
return overrideBinding
} catch (err) {
loadErrors.push(err)
}
@@ -630,6 +643,7 @@ if (!nativeBinding || forceWasi) {
if (!candidateFailed) {
wasiBinding = require('./oxlint.wasi.cjs')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -656,6 +670,7 @@ if (!nativeBinding || forceWasi) {
}
wasiBinding = require('@oxlint/binding-wasm32-wasi')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -708,3 +723,4 @@ export { getBufferOffset }
export { lint }
export { parseRawSync }
export { rawTransferSupported }
export const __napiBindingTarget = __napiLoadedBindingTarget
+8
View File
@@ -1,6 +1,14 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/**
* Which binding artifact the generated loader actually loaded: `'native'` for
* a native addon, otherwise the `platformArchABI` of the WASI flavor. Every
* flavor napi-rs can build is listed, because `NAPI_RS_NATIVE_LIBRARY_PATH`
* can point the loader at a WASI artifact this package does not build itself.
*/
export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'
export interface CodegenOptions {
/**
* Remove whitespace.
+17 -1
View File
@@ -7,6 +7,10 @@ const require = createRequire(import.meta.url)
const { readFileSync } = require('fs')
let nativeBinding = null
// Which artifact actually loaded. The WASI fallback chain overwrites it with
// the flavor it resolved; the late native retry below leaves it alone because
// it only runs while no WASI candidate has been loaded.
let __napiLoadedBindingTarget = 'native'
const loadErrors = []
const isMusl = () => {
@@ -65,7 +69,16 @@ const isMuslFromChildProcess = () => {
function requireNative() {
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
try {
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
const overrideBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
// The override may be a generated WASI loader, which already reports its
// own flavor. Adopt it: `module.exports` aliases this object, so claiming
// 'native' would both misreport the artifact and overwrite the loader's
// marker through the alias.
__napiLoadedBindingTarget =
overrideBinding && typeof overrideBinding.__napiBindingTarget === 'string'
? overrideBinding.__napiBindingTarget
: 'native'
return overrideBinding
} catch (err) {
loadErrors.push(err)
}
@@ -630,6 +643,7 @@ if (!nativeBinding || forceWasi) {
if (!candidateFailed) {
wasiBinding = require('./minify.wasi.cjs')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -656,6 +670,7 @@ if (!nativeBinding || forceWasi) {
}
wasiBinding = require('@oxc-minify/binding-wasm32-wasi')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -714,3 +729,4 @@ export { LegalCommentsMode }
export { minify }
export { minifySync }
export { Severity }
export const __napiBindingTarget = __napiLoadedBindingTarget
+208 -5
View File
@@ -8,6 +8,55 @@ import {
import { createContext as __emnapiCreateContext } from '@emnapi/runtime'
export const __napiBindingTarget = 'wasm32-wasi'
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',
@@ -42,10 +91,40 @@ 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
@@ -119,13 +198,58 @@ function __attachCleanupErrors(error, cleanupErrors) {
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) {
if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
return
}
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
if (typeof prepare === 'function') {
prepare()
// 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
@@ -318,13 +442,80 @@ function __destroyEmnapiContext() {
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
},
)
}
/**
* `@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)
@@ -354,7 +545,9 @@ function __terminateWasiWorkers() {
)
}
}
return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
return pending.length > 0
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
: finish()
}
function __finishWasiDisposal() {
@@ -555,7 +748,11 @@ let __wasiModule
let __napiModule
try {
__emnapiContext = __emnapiCreateContext({ autoDestroy: false })
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
__emnapiCreateContext({ autoDestroy: false }),
__prepareWasmEnvCleanup,
__isPreparingWasmEnvCleanup,
)
__emnapiContext.suppressDestroy()
;({
@@ -566,7 +763,7 @@ try {
context: __emnapiContext,
asyncWorkPoolSize: __asyncWorkPoolSize,
reuseWorker: false,
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
wasi: __wasi,
onCreateWorker() {
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
@@ -596,6 +793,12 @@ try {
},
}))
__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)
+223 -5
View File
@@ -2,6 +2,56 @@
/* eslint-disable */
/* auto-generated by NAPI-RS */
const __napiBindingTarget = 'wasm32-wasi'
function __napiStampBindingTarget(exportsObject, target) {
if (
Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget')
) {
if (exportsObject.__napiBindingTarget === target) {
// Already ours: the root entry aliases the object it loaded, so a WASI
// fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that
// is a generated loader — arrives already stamped with this same value.
return target
}
const error = new Error(
'`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].',
)
error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT'
throw error
}
if (!Object.isExtensible(exportsObject)) {
// A `#[napi(module_exports)]` hook may seal or freeze this object
// (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata,
// never a reason to fail an otherwise successful load, so the stamp is
// skipped. What a consumer still sees then follows the entry point: the
// browser and deferred loaders declare `__napiBindingTarget` at module
// level and go on reporting it, while the CommonJS entries hand back this
// very object as `module.exports`, so there the value is absent.
return target
}
try {
// [[Define]], not [[Set]]: an ordinary assignment walks the prototype
// chain, so an inherited accessor could swallow the value or throw and
// fail an otherwise successful load. The descriptor is what a successful
// assignment would have produced.
Object.defineProperty(exportsObject, '__napiBindingTarget', {
configurable: true,
enumerable: true,
value: target,
writable: true,
})
} catch {
// Same rule as the non-extensible skip above: reporting the artifact is
// metadata, never a reason to fail an otherwise successful load. An exotic
// object (a Proxy whose defineProperty trap refuses) is skipped, not
// thrown over.
}
// The CommonJS loaders assign this return value so `cjs-module-lexer` — and
// therefore Node's CJS -> ESM named export detection — can see
// `__napiBindingTarget` statically.
return target
}
const __nodeFs = require('node:fs')
const __nodePath = require('node:path')
const { WASI: __nodeWASI } = require('node:wasi')
@@ -149,10 +199,40 @@ 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
@@ -226,13 +306,58 @@ function __attachCleanupErrors(error, cleanupErrors) {
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) {
if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
return
}
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
if (typeof prepare === 'function') {
prepare()
// 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
@@ -425,13 +550,80 @@ function __destroyEmnapiContext() {
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
},
)
}
/**
* `@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)
@@ -461,7 +653,9 @@ function __terminateWasiWorkers() {
)
}
}
return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
return pending.length > 0
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
: finish()
}
function __finishWasiDisposal() {
@@ -831,7 +1025,11 @@ function __captureEmnapiAutoDestroyListener() {
try {
const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener()
try {
__emnapiContext = __emnapiCreateContext({ autoDestroy: false })
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
__emnapiCreateContext({ autoDestroy: false }),
__prepareWasmEnvCleanup,
__isPreparingWasmEnvCleanup,
)
// emnapi 2.x still registers an unconditional once-listener for
// beforeExit that auto-destroys the context, and suppressDestroy() only
// neutralizes its callback without removing it. This loader owns cleanup
@@ -859,7 +1057,7 @@ try {
}
})(),
reuseWorker: true,
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
wasi: __wasi,
onCreateWorker() {
const worker = __createWasiWorker(__nodePath.join(__dirname, 'wasi-worker.mjs'))
@@ -889,6 +1087,10 @@ try {
}
worker.unref()
// These stubs stay in place for the worker's whole life, disposal
// included: `__keepEventLoopAliveUntil` is what holds the process open
// while a termination is pending, precisely because a worker's own
// references cannot be relied on for it.
}
return worker
},
@@ -911,6 +1113,22 @@ try {
},
}))
__publishWasiDispose(__napiModule.exports)
// The CommonJS tail below aliases `__napiModule.exports`; a named module
// export does not travel with it, so carry the marker on the binding itself
// too. Three things pin the stamp to exactly this spot:
// - inside this `try`, because the guard throws on a
// `#[napi(module_exports)]` hook that claimed the name, and only the
// catch below tears the environment — context, workers, exit listener —
// back down;
// - after the async runtime host install, which hands this same object to
// addon-provided registration functions that may put anything on it;
// - assigning onto the loader's own `module.exports`, which is still the
// original object here, so an addon accessor with a refusing setter is
// never written through. `cjs-module-lexer` — Node's CJS -> ESM named
// export detection — reads the static `module.exports.<name> =` either
// way, and the later `module.exports = __napiModule.exports` does not
// undo that.
module.exports.__napiBindingTarget = __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget)
__registerWasiExitListener()
} catch (error) {
const rollback = {
+3
View File
@@ -1,6 +1,9 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/** The WASI flavor this loader instantiates. */
export declare const __napiBindingTarget: 'wasm32-wasi'
export interface CodegenOptions {
/**
* Remove whitespace.
+17 -1
View File
@@ -7,6 +7,10 @@ const require = createRequire(import.meta.url)
const { readFileSync } = require('fs')
let nativeBinding = null
// Which artifact actually loaded. The WASI fallback chain overwrites it with
// the flavor it resolved; the late native retry below leaves it alone because
// it only runs while no WASI candidate has been loaded.
let __napiLoadedBindingTarget = 'native'
const loadErrors = []
const isMusl = () => {
@@ -65,7 +69,16 @@ const isMuslFromChildProcess = () => {
function requireNative() {
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
try {
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
const overrideBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
// The override may be a generated WASI loader, which already reports its
// own flavor. Adopt it: `module.exports` aliases this object, so claiming
// 'native' would both misreport the artifact and overwrite the loader's
// marker through the alias.
__napiLoadedBindingTarget =
overrideBinding && typeof overrideBinding.__napiBindingTarget === 'string'
? overrideBinding.__napiBindingTarget
: 'native'
return overrideBinding
} catch (err) {
loadErrors.push(err)
}
@@ -630,6 +643,7 @@ if (!nativeBinding || forceWasi) {
if (!candidateFailed) {
wasiBinding = require('./parser.wasi.cjs')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -656,6 +670,7 @@ if (!nativeBinding || forceWasi) {
}
wasiBinding = require('@oxc-parser/binding-wasm32-wasi')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -719,5 +734,6 @@ export { ImportNameKind }
export { parse }
export { parseSync }
export { rawTransferSupported }
export const __napiBindingTarget = __napiLoadedBindingTarget
const { getBufferOffset, parseRaw, parseRawSync } = nativeBinding
export { getBufferOffset, parseRaw, parseRawSync }
+8
View File
@@ -15,6 +15,14 @@ export class Visitor {
visit(program: Program): void;
}
/**
* Which binding artifact the generated loader actually loaded: `'native'` for
* a native addon, otherwise the `platformArchABI` of the WASI flavor. Every
* flavor napi-rs can build is listed, because `NAPI_RS_NATIVE_LIBRARY_PATH`
* can point the loader at a WASI artifact this package does not build itself.
*/
export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'
export interface Comment {
type: 'Line' | 'Block'
value: string
+208 -5
View File
@@ -8,6 +8,55 @@ import {
import { createContext as __emnapiCreateContext } from '@emnapi/runtime'
export const __napiBindingTarget = 'wasm32-wasi'
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',
@@ -42,10 +91,40 @@ 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
@@ -119,13 +198,58 @@ function __attachCleanupErrors(error, cleanupErrors) {
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) {
if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
return
}
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
if (typeof prepare === 'function') {
prepare()
// 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
@@ -318,13 +442,80 @@ function __destroyEmnapiContext() {
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
},
)
}
/**
* `@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)
@@ -354,7 +545,9 @@ function __terminateWasiWorkers() {
)
}
}
return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
return pending.length > 0
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
: finish()
}
function __finishWasiDisposal() {
@@ -555,7 +748,11 @@ let __wasiModule
let __napiModule
try {
__emnapiContext = __emnapiCreateContext({ autoDestroy: false })
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
__emnapiCreateContext({ autoDestroy: false }),
__prepareWasmEnvCleanup,
__isPreparingWasmEnvCleanup,
)
__emnapiContext.suppressDestroy()
;({
@@ -566,7 +763,7 @@ try {
context: __emnapiContext,
asyncWorkPoolSize: __asyncWorkPoolSize,
reuseWorker: false,
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
wasi: __wasi,
onCreateWorker() {
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
@@ -596,6 +793,12 @@ try {
},
}))
__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)
+223 -5
View File
@@ -2,6 +2,56 @@
/* eslint-disable */
/* auto-generated by NAPI-RS */
const __napiBindingTarget = 'wasm32-wasi'
function __napiStampBindingTarget(exportsObject, target) {
if (
Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget')
) {
if (exportsObject.__napiBindingTarget === target) {
// Already ours: the root entry aliases the object it loaded, so a WASI
// fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that
// is a generated loader — arrives already stamped with this same value.
return target
}
const error = new Error(
'`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].',
)
error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT'
throw error
}
if (!Object.isExtensible(exportsObject)) {
// A `#[napi(module_exports)]` hook may seal or freeze this object
// (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata,
// never a reason to fail an otherwise successful load, so the stamp is
// skipped. What a consumer still sees then follows the entry point: the
// browser and deferred loaders declare `__napiBindingTarget` at module
// level and go on reporting it, while the CommonJS entries hand back this
// very object as `module.exports`, so there the value is absent.
return target
}
try {
// [[Define]], not [[Set]]: an ordinary assignment walks the prototype
// chain, so an inherited accessor could swallow the value or throw and
// fail an otherwise successful load. The descriptor is what a successful
// assignment would have produced.
Object.defineProperty(exportsObject, '__napiBindingTarget', {
configurable: true,
enumerable: true,
value: target,
writable: true,
})
} catch {
// Same rule as the non-extensible skip above: reporting the artifact is
// metadata, never a reason to fail an otherwise successful load. An exotic
// object (a Proxy whose defineProperty trap refuses) is skipped, not
// thrown over.
}
// The CommonJS loaders assign this return value so `cjs-module-lexer` — and
// therefore Node's CJS -> ESM named export detection — can see
// `__napiBindingTarget` statically.
return target
}
const __nodeFs = require('node:fs')
const __nodePath = require('node:path')
const { WASI: __nodeWASI } = require('node:wasi')
@@ -149,10 +199,40 @@ 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
@@ -226,13 +306,58 @@ function __attachCleanupErrors(error, cleanupErrors) {
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) {
if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
return
}
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
if (typeof prepare === 'function') {
prepare()
// 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
@@ -425,13 +550,80 @@ function __destroyEmnapiContext() {
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
},
)
}
/**
* `@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)
@@ -461,7 +653,9 @@ function __terminateWasiWorkers() {
)
}
}
return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
return pending.length > 0
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
: finish()
}
function __finishWasiDisposal() {
@@ -831,7 +1025,11 @@ function __captureEmnapiAutoDestroyListener() {
try {
const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener()
try {
__emnapiContext = __emnapiCreateContext({ autoDestroy: false })
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
__emnapiCreateContext({ autoDestroy: false }),
__prepareWasmEnvCleanup,
__isPreparingWasmEnvCleanup,
)
// emnapi 2.x still registers an unconditional once-listener for
// beforeExit that auto-destroys the context, and suppressDestroy() only
// neutralizes its callback without removing it. This loader owns cleanup
@@ -859,7 +1057,7 @@ try {
}
})(),
reuseWorker: true,
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
wasi: __wasi,
onCreateWorker() {
const worker = __createWasiWorker(__nodePath.join(__dirname, 'wasi-worker.mjs'))
@@ -889,6 +1087,10 @@ try {
}
worker.unref()
// These stubs stay in place for the worker's whole life, disposal
// included: `__keepEventLoopAliveUntil` is what holds the process open
// while a termination is pending, precisely because a worker's own
// references cannot be relied on for it.
}
return worker
},
@@ -911,6 +1113,22 @@ try {
},
}))
__publishWasiDispose(__napiModule.exports)
// The CommonJS tail below aliases `__napiModule.exports`; a named module
// export does not travel with it, so carry the marker on the binding itself
// too. Three things pin the stamp to exactly this spot:
// - inside this `try`, because the guard throws on a
// `#[napi(module_exports)]` hook that claimed the name, and only the
// catch below tears the environment — context, workers, exit listener —
// back down;
// - after the async runtime host install, which hands this same object to
// addon-provided registration functions that may put anything on it;
// - assigning onto the loader's own `module.exports`, which is still the
// original object here, so an addon accessor with a refusing setter is
// never written through. `cjs-module-lexer` — Node's CJS -> ESM named
// export detection — reads the static `module.exports.<name> =` either
// way, and the later `module.exports = __napiModule.exports` does not
// undo that.
module.exports.__napiBindingTarget = __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget)
__registerWasiExitListener()
} catch (error) {
const rollback = {
+4
View File
@@ -1,3 +1,7 @@
/** The WASI flavor this loader instantiates. */
export declare const __napiBindingTarget: 'wasm32-wasi'
export interface Comment {
type: 'Line' | 'Block'
value: string
+17 -1
View File
@@ -7,6 +7,10 @@ const require = createRequire(import.meta.url)
const { readFileSync } = require('fs')
let nativeBinding = null
// Which artifact actually loaded. The WASI fallback chain overwrites it with
// the flavor it resolved; the late native retry below leaves it alone because
// it only runs while no WASI candidate has been loaded.
let __napiLoadedBindingTarget = 'native'
const loadErrors = []
const isMusl = () => {
@@ -65,7 +69,16 @@ const isMuslFromChildProcess = () => {
function requireNative() {
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
try {
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
const overrideBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
// The override may be a generated WASI loader, which already reports its
// own flavor. Adopt it: `module.exports` aliases this object, so claiming
// 'native' would both misreport the artifact and overwrite the loader's
// marker through the alias.
__napiLoadedBindingTarget =
overrideBinding && typeof overrideBinding.__napiBindingTarget === 'string'
? overrideBinding.__napiBindingTarget
: 'native'
return overrideBinding
} catch (err) {
loadErrors.push(err)
}
@@ -630,6 +643,7 @@ if (!nativeBinding || forceWasi) {
if (!candidateFailed) {
wasiBinding = require('./playground.wasi.cjs')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -656,6 +670,7 @@ if (!nativeBinding || forceWasi) {
}
wasiBinding = require('@oxc-playground/binding-wasm32-wasi')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -704,3 +719,4 @@ if (!nativeBinding) {
const { Severity, Oxc } = nativeBinding
export { Severity }
export { Oxc }
export const __napiBindingTarget = __napiLoadedBindingTarget
+208 -5
View File
@@ -8,6 +8,55 @@ import {
import { createContext as __emnapiCreateContext } from '@emnapi/runtime'
export const __napiBindingTarget = 'wasm32-wasi'
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',
@@ -42,10 +91,40 @@ 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
@@ -119,13 +198,58 @@ function __attachCleanupErrors(error, cleanupErrors) {
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) {
if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
return
}
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
if (typeof prepare === 'function') {
prepare()
// 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
@@ -318,13 +442,80 @@ function __destroyEmnapiContext() {
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
},
)
}
/**
* `@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)
@@ -354,7 +545,9 @@ function __terminateWasiWorkers() {
)
}
}
return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
return pending.length > 0
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
: finish()
}
function __finishWasiDisposal() {
@@ -555,7 +748,11 @@ let __wasiModule
let __napiModule
try {
__emnapiContext = __emnapiCreateContext({ autoDestroy: false })
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
__emnapiCreateContext({ autoDestroy: false }),
__prepareWasmEnvCleanup,
__isPreparingWasmEnvCleanup,
)
__emnapiContext.suppressDestroy()
;({
@@ -566,7 +763,7 @@ try {
context: __emnapiContext,
asyncWorkPoolSize: __asyncWorkPoolSize,
reuseWorker: false,
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
wasi: __wasi,
onCreateWorker() {
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
@@ -596,6 +793,12 @@ try {
},
}))
__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)
+223 -5
View File
@@ -2,6 +2,56 @@
/* eslint-disable */
/* auto-generated by NAPI-RS */
const __napiBindingTarget = 'wasm32-wasi'
function __napiStampBindingTarget(exportsObject, target) {
if (
Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget')
) {
if (exportsObject.__napiBindingTarget === target) {
// Already ours: the root entry aliases the object it loaded, so a WASI
// fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that
// is a generated loader — arrives already stamped with this same value.
return target
}
const error = new Error(
'`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].',
)
error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT'
throw error
}
if (!Object.isExtensible(exportsObject)) {
// A `#[napi(module_exports)]` hook may seal or freeze this object
// (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata,
// never a reason to fail an otherwise successful load, so the stamp is
// skipped. What a consumer still sees then follows the entry point: the
// browser and deferred loaders declare `__napiBindingTarget` at module
// level and go on reporting it, while the CommonJS entries hand back this
// very object as `module.exports`, so there the value is absent.
return target
}
try {
// [[Define]], not [[Set]]: an ordinary assignment walks the prototype
// chain, so an inherited accessor could swallow the value or throw and
// fail an otherwise successful load. The descriptor is what a successful
// assignment would have produced.
Object.defineProperty(exportsObject, '__napiBindingTarget', {
configurable: true,
enumerable: true,
value: target,
writable: true,
})
} catch {
// Same rule as the non-extensible skip above: reporting the artifact is
// metadata, never a reason to fail an otherwise successful load. An exotic
// object (a Proxy whose defineProperty trap refuses) is skipped, not
// thrown over.
}
// The CommonJS loaders assign this return value so `cjs-module-lexer` — and
// therefore Node's CJS -> ESM named export detection — can see
// `__napiBindingTarget` statically.
return target
}
const __nodeFs = require('node:fs')
const __nodePath = require('node:path')
const { WASI: __nodeWASI } = require('node:wasi')
@@ -149,10 +199,40 @@ 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
@@ -226,13 +306,58 @@ function __attachCleanupErrors(error, cleanupErrors) {
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) {
if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
return
}
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
if (typeof prepare === 'function') {
prepare()
// 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
@@ -425,13 +550,80 @@ function __destroyEmnapiContext() {
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
},
)
}
/**
* `@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)
@@ -461,7 +653,9 @@ function __terminateWasiWorkers() {
)
}
}
return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
return pending.length > 0
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
: finish()
}
function __finishWasiDisposal() {
@@ -831,7 +1025,11 @@ function __captureEmnapiAutoDestroyListener() {
try {
const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener()
try {
__emnapiContext = __emnapiCreateContext({ autoDestroy: false })
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
__emnapiCreateContext({ autoDestroy: false }),
__prepareWasmEnvCleanup,
__isPreparingWasmEnvCleanup,
)
// emnapi 2.x still registers an unconditional once-listener for
// beforeExit that auto-destroys the context, and suppressDestroy() only
// neutralizes its callback without removing it. This loader owns cleanup
@@ -859,7 +1057,7 @@ try {
}
})(),
reuseWorker: true,
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
wasi: __wasi,
onCreateWorker() {
const worker = __createWasiWorker(__nodePath.join(__dirname, 'wasi-worker.mjs'))
@@ -889,6 +1087,10 @@ try {
}
worker.unref()
// These stubs stay in place for the worker's whole life, disposal
// included: `__keepEventLoopAliveUntil` is what holds the process open
// while a termination is pending, precisely because a worker's own
// references cannot be relied on for it.
}
return worker
},
@@ -911,6 +1113,22 @@ try {
},
}))
__publishWasiDispose(__napiModule.exports)
// The CommonJS tail below aliases `__napiModule.exports`; a named module
// export does not travel with it, so carry the marker on the binding itself
// too. Three things pin the stamp to exactly this spot:
// - inside this `try`, because the guard throws on a
// `#[napi(module_exports)]` hook that claimed the name, and only the
// catch below tears the environment — context, workers, exit listener —
// back down;
// - after the async runtime host install, which hands this same object to
// addon-provided registration functions that may put anything on it;
// - assigning onto the loader's own `module.exports`, which is still the
// original object here, so an addon accessor with a refusing setter is
// never written through. `cjs-module-lexer` — Node's CJS -> ESM named
// export detection — reads the static `module.exports.<name> =` either
// way, and the later `module.exports = __napiModule.exports` does not
// undo that.
module.exports.__napiBindingTarget = __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget)
__registerWasiExitListener()
} catch (error) {
const rollback = {
+3
View File
@@ -1,6 +1,9 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/** The WASI flavor this loader instantiates. */
export declare const __napiBindingTarget: 'wasm32-wasi'
export interface Comment {
type: 'Line' | 'Block'
value: string
+8
View File
@@ -1,6 +1,14 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/**
* Which binding artifact the generated loader actually loaded: `'native'` for
* a native addon, otherwise the `platformArchABI` of the WASI flavor. Every
* flavor napi-rs can build is listed, because `NAPI_RS_NATIVE_LIBRARY_PATH`
* can point the loader at a WASI artifact this package does not build itself.
*/
export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'
export interface Comment {
type: 'Line' | 'Block'
value: string
+17 -1
View File
@@ -7,6 +7,10 @@ const require = createRequire(import.meta.url)
const { readFileSync } = require('fs')
let nativeBinding = null
// Which artifact actually loaded. The WASI fallback chain overwrites it with
// the flavor it resolved; the late native retry below leaves it alone because
// it only runs while no WASI candidate has been loaded.
let __napiLoadedBindingTarget = 'native'
const loadErrors = []
const isMusl = () => {
@@ -65,7 +69,16 @@ const isMuslFromChildProcess = () => {
function requireNative() {
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
try {
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
const overrideBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
// The override may be a generated WASI loader, which already reports its
// own flavor. Adopt it: `module.exports` aliases this object, so claiming
// 'native' would both misreport the artifact and overwrite the loader's
// marker through the alias.
__napiLoadedBindingTarget =
overrideBinding && typeof overrideBinding.__napiBindingTarget === 'string'
? overrideBinding.__napiBindingTarget
: 'native'
return overrideBinding
} catch (err) {
loadErrors.push(err)
}
@@ -630,6 +643,7 @@ if (!nativeBinding || forceWasi) {
if (!candidateFailed) {
wasiBinding = require('./transform-react.wasi.cjs')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -656,6 +670,7 @@ if (!nativeBinding || forceWasi) {
}
wasiBinding = require('@oxc-transform-react/binding-wasm32-wasi')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -713,3 +728,4 @@ const { Severity, transform, transformSync } = nativeBinding
export { Severity }
export { transform }
export { transformSync }
export const __napiBindingTarget = __napiLoadedBindingTarget
@@ -8,6 +8,55 @@ import {
import { createContext as __emnapiCreateContext } from '@emnapi/runtime'
export const __napiBindingTarget = 'wasm32-wasi'
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',
@@ -42,10 +91,40 @@ 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
@@ -119,13 +198,58 @@ function __attachCleanupErrors(error, cleanupErrors) {
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) {
if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
return
}
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
if (typeof prepare === 'function') {
prepare()
// 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
@@ -318,13 +442,80 @@ function __destroyEmnapiContext() {
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
},
)
}
/**
* `@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)
@@ -354,7 +545,9 @@ function __terminateWasiWorkers() {
)
}
}
return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
return pending.length > 0
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
: finish()
}
function __finishWasiDisposal() {
@@ -555,7 +748,11 @@ let __wasiModule
let __napiModule
try {
__emnapiContext = __emnapiCreateContext({ autoDestroy: false })
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
__emnapiCreateContext({ autoDestroy: false }),
__prepareWasmEnvCleanup,
__isPreparingWasmEnvCleanup,
)
__emnapiContext.suppressDestroy()
;({
@@ -566,7 +763,7 @@ try {
context: __emnapiContext,
asyncWorkPoolSize: __asyncWorkPoolSize,
reuseWorker: false,
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
wasi: __wasi,
onCreateWorker() {
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
@@ -596,6 +793,12 @@ try {
},
}))
__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)
+223 -5
View File
@@ -2,6 +2,56 @@
/* eslint-disable */
/* auto-generated by NAPI-RS */
const __napiBindingTarget = 'wasm32-wasi'
function __napiStampBindingTarget(exportsObject, target) {
if (
Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget')
) {
if (exportsObject.__napiBindingTarget === target) {
// Already ours: the root entry aliases the object it loaded, so a WASI
// fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that
// is a generated loader — arrives already stamped with this same value.
return target
}
const error = new Error(
'`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].',
)
error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT'
throw error
}
if (!Object.isExtensible(exportsObject)) {
// A `#[napi(module_exports)]` hook may seal or freeze this object
// (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata,
// never a reason to fail an otherwise successful load, so the stamp is
// skipped. What a consumer still sees then follows the entry point: the
// browser and deferred loaders declare `__napiBindingTarget` at module
// level and go on reporting it, while the CommonJS entries hand back this
// very object as `module.exports`, so there the value is absent.
return target
}
try {
// [[Define]], not [[Set]]: an ordinary assignment walks the prototype
// chain, so an inherited accessor could swallow the value or throw and
// fail an otherwise successful load. The descriptor is what a successful
// assignment would have produced.
Object.defineProperty(exportsObject, '__napiBindingTarget', {
configurable: true,
enumerable: true,
value: target,
writable: true,
})
} catch {
// Same rule as the non-extensible skip above: reporting the artifact is
// metadata, never a reason to fail an otherwise successful load. An exotic
// object (a Proxy whose defineProperty trap refuses) is skipped, not
// thrown over.
}
// The CommonJS loaders assign this return value so `cjs-module-lexer` — and
// therefore Node's CJS -> ESM named export detection — can see
// `__napiBindingTarget` statically.
return target
}
const __nodeFs = require('node:fs')
const __nodePath = require('node:path')
const { WASI: __nodeWASI } = require('node:wasi')
@@ -149,10 +199,40 @@ 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
@@ -226,13 +306,58 @@ function __attachCleanupErrors(error, cleanupErrors) {
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) {
if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
return
}
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
if (typeof prepare === 'function') {
prepare()
// 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
@@ -425,13 +550,80 @@ function __destroyEmnapiContext() {
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
},
)
}
/**
* `@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)
@@ -461,7 +653,9 @@ function __terminateWasiWorkers() {
)
}
}
return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
return pending.length > 0
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
: finish()
}
function __finishWasiDisposal() {
@@ -831,7 +1025,11 @@ function __captureEmnapiAutoDestroyListener() {
try {
const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener()
try {
__emnapiContext = __emnapiCreateContext({ autoDestroy: false })
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
__emnapiCreateContext({ autoDestroy: false }),
__prepareWasmEnvCleanup,
__isPreparingWasmEnvCleanup,
)
// emnapi 2.x still registers an unconditional once-listener for
// beforeExit that auto-destroys the context, and suppressDestroy() only
// neutralizes its callback without removing it. This loader owns cleanup
@@ -859,7 +1057,7 @@ try {
}
})(),
reuseWorker: true,
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
wasi: __wasi,
onCreateWorker() {
const worker = __createWasiWorker(__nodePath.join(__dirname, 'wasi-worker.mjs'))
@@ -889,6 +1087,10 @@ try {
}
worker.unref()
// These stubs stay in place for the worker's whole life, disposal
// included: `__keepEventLoopAliveUntil` is what holds the process open
// while a termination is pending, precisely because a worker's own
// references cannot be relied on for it.
}
return worker
},
@@ -911,6 +1113,22 @@ try {
},
}))
__publishWasiDispose(__napiModule.exports)
// The CommonJS tail below aliases `__napiModule.exports`; a named module
// export does not travel with it, so carry the marker on the binding itself
// too. Three things pin the stamp to exactly this spot:
// - inside this `try`, because the guard throws on a
// `#[napi(module_exports)]` hook that claimed the name, and only the
// catch below tears the environment — context, workers, exit listener —
// back down;
// - after the async runtime host install, which hands this same object to
// addon-provided registration functions that may put anything on it;
// - assigning onto the loader's own `module.exports`, which is still the
// original object here, so an addon accessor with a refusing setter is
// never written through. `cjs-module-lexer` — Node's CJS -> ESM named
// export detection — reads the static `module.exports.<name> =` either
// way, and the later `module.exports = __napiModule.exports` does not
// undo that.
module.exports.__napiBindingTarget = __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget)
__registerWasiExitListener()
} catch (error) {
const rollback = {
@@ -1,6 +1,9 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/** The WASI flavor this loader instantiates. */
export declare const __napiBindingTarget: 'wasm32-wasi'
export interface Comment {
type: 'Line' | 'Block'
value: string
+8
View File
@@ -1,6 +1,14 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/**
* Which binding artifact the generated loader actually loaded: `'native'` for
* a native addon, otherwise the `platformArchABI` of the WASI flavor. Every
* flavor napi-rs can build is listed, because `NAPI_RS_NATIVE_LIBRARY_PATH`
* can point the loader at a WASI artifact this package does not build itself.
*/
export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'
export interface Comment {
type: 'Line' | 'Block'
value: string
+17 -1
View File
@@ -7,6 +7,10 @@ const require = createRequire(import.meta.url)
const { readFileSync } = require('fs')
let nativeBinding = null
// Which artifact actually loaded. The WASI fallback chain overwrites it with
// the flavor it resolved; the late native retry below leaves it alone because
// it only runs while no WASI candidate has been loaded.
let __napiLoadedBindingTarget = 'native'
const loadErrors = []
const isMusl = () => {
@@ -65,7 +69,16 @@ const isMuslFromChildProcess = () => {
function requireNative() {
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
try {
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
const overrideBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
// The override may be a generated WASI loader, which already reports its
// own flavor. Adopt it: `module.exports` aliases this object, so claiming
// 'native' would both misreport the artifact and overwrite the loader's
// marker through the alias.
__napiLoadedBindingTarget =
overrideBinding && typeof overrideBinding.__napiBindingTarget === 'string'
? overrideBinding.__napiBindingTarget
: 'native'
return overrideBinding
} catch (err) {
loadErrors.push(err)
}
@@ -630,6 +643,7 @@ if (!nativeBinding || forceWasi) {
if (!candidateFailed) {
wasiBinding = require('./transform-relay.wasi.cjs')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -656,6 +670,7 @@ if (!nativeBinding || forceWasi) {
}
wasiBinding = require('@oxc-transform-relay/binding-wasm32-wasi')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -713,3 +728,4 @@ const { Severity, transform, transformSync } = nativeBinding
export { Severity }
export { transform }
export { transformSync }
export const __napiBindingTarget = __napiLoadedBindingTarget
@@ -8,6 +8,55 @@ import {
import { createContext as __emnapiCreateContext } from '@emnapi/runtime'
export const __napiBindingTarget = 'wasm32-wasi'
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',
@@ -42,10 +91,40 @@ 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
@@ -119,13 +198,58 @@ function __attachCleanupErrors(error, cleanupErrors) {
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) {
if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
return
}
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
if (typeof prepare === 'function') {
prepare()
// 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
@@ -318,13 +442,80 @@ function __destroyEmnapiContext() {
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
},
)
}
/**
* `@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)
@@ -354,7 +545,9 @@ function __terminateWasiWorkers() {
)
}
}
return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
return pending.length > 0
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
: finish()
}
function __finishWasiDisposal() {
@@ -555,7 +748,11 @@ let __wasiModule
let __napiModule
try {
__emnapiContext = __emnapiCreateContext({ autoDestroy: false })
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
__emnapiCreateContext({ autoDestroy: false }),
__prepareWasmEnvCleanup,
__isPreparingWasmEnvCleanup,
)
__emnapiContext.suppressDestroy()
;({
@@ -566,7 +763,7 @@ try {
context: __emnapiContext,
asyncWorkPoolSize: __asyncWorkPoolSize,
reuseWorker: false,
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
wasi: __wasi,
onCreateWorker() {
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
@@ -596,6 +793,12 @@ try {
},
}))
__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)
+223 -5
View File
@@ -2,6 +2,56 @@
/* eslint-disable */
/* auto-generated by NAPI-RS */
const __napiBindingTarget = 'wasm32-wasi'
function __napiStampBindingTarget(exportsObject, target) {
if (
Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget')
) {
if (exportsObject.__napiBindingTarget === target) {
// Already ours: the root entry aliases the object it loaded, so a WASI
// fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that
// is a generated loader — arrives already stamped with this same value.
return target
}
const error = new Error(
'`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].',
)
error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT'
throw error
}
if (!Object.isExtensible(exportsObject)) {
// A `#[napi(module_exports)]` hook may seal or freeze this object
// (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata,
// never a reason to fail an otherwise successful load, so the stamp is
// skipped. What a consumer still sees then follows the entry point: the
// browser and deferred loaders declare `__napiBindingTarget` at module
// level and go on reporting it, while the CommonJS entries hand back this
// very object as `module.exports`, so there the value is absent.
return target
}
try {
// [[Define]], not [[Set]]: an ordinary assignment walks the prototype
// chain, so an inherited accessor could swallow the value or throw and
// fail an otherwise successful load. The descriptor is what a successful
// assignment would have produced.
Object.defineProperty(exportsObject, '__napiBindingTarget', {
configurable: true,
enumerable: true,
value: target,
writable: true,
})
} catch {
// Same rule as the non-extensible skip above: reporting the artifact is
// metadata, never a reason to fail an otherwise successful load. An exotic
// object (a Proxy whose defineProperty trap refuses) is skipped, not
// thrown over.
}
// The CommonJS loaders assign this return value so `cjs-module-lexer` — and
// therefore Node's CJS -> ESM named export detection — can see
// `__napiBindingTarget` statically.
return target
}
const __nodeFs = require('node:fs')
const __nodePath = require('node:path')
const { WASI: __nodeWASI } = require('node:wasi')
@@ -149,10 +199,40 @@ 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
@@ -226,13 +306,58 @@ function __attachCleanupErrors(error, cleanupErrors) {
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) {
if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
return
}
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
if (typeof prepare === 'function') {
prepare()
// 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
@@ -425,13 +550,80 @@ function __destroyEmnapiContext() {
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
},
)
}
/**
* `@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)
@@ -461,7 +653,9 @@ function __terminateWasiWorkers() {
)
}
}
return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
return pending.length > 0
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
: finish()
}
function __finishWasiDisposal() {
@@ -831,7 +1025,11 @@ function __captureEmnapiAutoDestroyListener() {
try {
const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener()
try {
__emnapiContext = __emnapiCreateContext({ autoDestroy: false })
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
__emnapiCreateContext({ autoDestroy: false }),
__prepareWasmEnvCleanup,
__isPreparingWasmEnvCleanup,
)
// emnapi 2.x still registers an unconditional once-listener for
// beforeExit that auto-destroys the context, and suppressDestroy() only
// neutralizes its callback without removing it. This loader owns cleanup
@@ -859,7 +1057,7 @@ try {
}
})(),
reuseWorker: true,
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
wasi: __wasi,
onCreateWorker() {
const worker = __createWasiWorker(__nodePath.join(__dirname, 'wasi-worker.mjs'))
@@ -889,6 +1087,10 @@ try {
}
worker.unref()
// These stubs stay in place for the worker's whole life, disposal
// included: `__keepEventLoopAliveUntil` is what holds the process open
// while a termination is pending, precisely because a worker's own
// references cannot be relied on for it.
}
return worker
},
@@ -911,6 +1113,22 @@ try {
},
}))
__publishWasiDispose(__napiModule.exports)
// The CommonJS tail below aliases `__napiModule.exports`; a named module
// export does not travel with it, so carry the marker on the binding itself
// too. Three things pin the stamp to exactly this spot:
// - inside this `try`, because the guard throws on a
// `#[napi(module_exports)]` hook that claimed the name, and only the
// catch below tears the environment — context, workers, exit listener —
// back down;
// - after the async runtime host install, which hands this same object to
// addon-provided registration functions that may put anything on it;
// - assigning onto the loader's own `module.exports`, which is still the
// original object here, so an addon accessor with a refusing setter is
// never written through. `cjs-module-lexer` — Node's CJS -> ESM named
// export detection — reads the static `module.exports.<name> =` either
// way, and the later `module.exports = __napiModule.exports` does not
// undo that.
module.exports.__napiBindingTarget = __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget)
__registerWasiExitListener()
} catch (error) {
const rollback = {
@@ -1,6 +1,9 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/** The WASI flavor this loader instantiates. */
export declare const __napiBindingTarget: 'wasm32-wasi'
export interface Comment {
type: 'Line' | 'Block'
value: string
+8
View File
@@ -1,6 +1,14 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/**
* Which binding artifact the generated loader actually loaded: `'native'` for
* a native addon, otherwise the `platformArchABI` of the WASI flavor. Every
* flavor napi-rs can build is listed, because `NAPI_RS_NATIVE_LIBRARY_PATH`
* can point the loader at a WASI artifact this package does not build itself.
*/
export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'
export interface Comment {
type: 'Line' | 'Block'
value: string
+17 -1
View File
@@ -7,6 +7,10 @@ const require = createRequire(import.meta.url)
const { readFileSync } = require('fs')
let nativeBinding = null
// Which artifact actually loaded. The WASI fallback chain overwrites it with
// the flavor it resolved; the late native retry below leaves it alone because
// it only runs while no WASI candidate has been loaded.
let __napiLoadedBindingTarget = 'native'
const loadErrors = []
const isMusl = () => {
@@ -65,7 +69,16 @@ const isMuslFromChildProcess = () => {
function requireNative() {
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
try {
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
const overrideBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
// The override may be a generated WASI loader, which already reports its
// own flavor. Adopt it: `module.exports` aliases this object, so claiming
// 'native' would both misreport the artifact and overwrite the loader's
// marker through the alias.
__napiLoadedBindingTarget =
overrideBinding && typeof overrideBinding.__napiBindingTarget === 'string'
? overrideBinding.__napiBindingTarget
: 'native'
return overrideBinding
} catch (err) {
loadErrors.push(err)
}
@@ -630,6 +643,7 @@ if (!nativeBinding || forceWasi) {
if (!candidateFailed) {
wasiBinding = require('./transform.wasi.cjs')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -656,6 +670,7 @@ if (!nativeBinding || forceWasi) {
}
wasiBinding = require('@oxc-transform/binding-wasm32-wasi')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -718,3 +733,4 @@ export { moduleRunnerTransform }
export { moduleRunnerTransformSync }
export { transform }
export { transformSync }
export const __napiBindingTarget = __napiLoadedBindingTarget
+208 -5
View File
@@ -8,6 +8,55 @@ import {
import { createContext as __emnapiCreateContext } from '@emnapi/runtime'
export const __napiBindingTarget = 'wasm32-wasi'
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',
@@ -42,10 +91,40 @@ 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
@@ -119,13 +198,58 @@ function __attachCleanupErrors(error, cleanupErrors) {
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) {
if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
return
}
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
if (typeof prepare === 'function') {
prepare()
// 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
@@ -318,13 +442,80 @@ function __destroyEmnapiContext() {
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
},
)
}
/**
* `@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)
@@ -354,7 +545,9 @@ function __terminateWasiWorkers() {
)
}
}
return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
return pending.length > 0
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
: finish()
}
function __finishWasiDisposal() {
@@ -555,7 +748,11 @@ let __wasiModule
let __napiModule
try {
__emnapiContext = __emnapiCreateContext({ autoDestroy: false })
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
__emnapiCreateContext({ autoDestroy: false }),
__prepareWasmEnvCleanup,
__isPreparingWasmEnvCleanup,
)
__emnapiContext.suppressDestroy()
;({
@@ -566,7 +763,7 @@ try {
context: __emnapiContext,
asyncWorkPoolSize: __asyncWorkPoolSize,
reuseWorker: false,
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
wasi: __wasi,
onCreateWorker() {
const worker = new Worker(new URL('./wasi-worker-browser.mjs', import.meta.url), {
@@ -596,6 +793,12 @@ try {
},
}))
__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)
+223 -5
View File
@@ -2,6 +2,56 @@
/* eslint-disable */
/* auto-generated by NAPI-RS */
const __napiBindingTarget = 'wasm32-wasi'
function __napiStampBindingTarget(exportsObject, target) {
if (
Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget')
) {
if (exportsObject.__napiBindingTarget === target) {
// Already ours: the root entry aliases the object it loaded, so a WASI
// fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that
// is a generated loader — arrives already stamped with this same value.
return target
}
const error = new Error(
'`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].',
)
error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT'
throw error
}
if (!Object.isExtensible(exportsObject)) {
// A `#[napi(module_exports)]` hook may seal or freeze this object
// (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata,
// never a reason to fail an otherwise successful load, so the stamp is
// skipped. What a consumer still sees then follows the entry point: the
// browser and deferred loaders declare `__napiBindingTarget` at module
// level and go on reporting it, while the CommonJS entries hand back this
// very object as `module.exports`, so there the value is absent.
return target
}
try {
// [[Define]], not [[Set]]: an ordinary assignment walks the prototype
// chain, so an inherited accessor could swallow the value or throw and
// fail an otherwise successful load. The descriptor is what a successful
// assignment would have produced.
Object.defineProperty(exportsObject, '__napiBindingTarget', {
configurable: true,
enumerable: true,
value: target,
writable: true,
})
} catch {
// Same rule as the non-extensible skip above: reporting the artifact is
// metadata, never a reason to fail an otherwise successful load. An exotic
// object (a Proxy whose defineProperty trap refuses) is skipped, not
// thrown over.
}
// The CommonJS loaders assign this return value so `cjs-module-lexer` — and
// therefore Node's CJS -> ESM named export detection — can see
// `__napiBindingTarget` statically.
return target
}
const __nodeFs = require('node:fs')
const __nodePath = require('node:path')
const { WASI: __nodeWASI } = require('node:wasi')
@@ -149,10 +199,40 @@ 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
@@ -226,13 +306,58 @@ function __attachCleanupErrors(error, cleanupErrors) {
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) {
if (__emnapiWasmEnvCleanupPrepared || __emnapiWasmEnvCleanupPreparing) {
return
}
const prepare = __napiInstance?.exports?.napi_prepare_wasm_env_cleanup
if (typeof prepare === 'function') {
prepare()
// 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
@@ -425,13 +550,80 @@ function __destroyEmnapiContext() {
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
},
)
}
/**
* `@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)
@@ -461,7 +653,9 @@ function __terminateWasiWorkers() {
)
}
}
return pending.length > 0 ? Promise.all(pending).then(finish) : finish()
return pending.length > 0
? __keepEventLoopAliveUntil(Promise.all(pending)).then(finish)
: finish()
}
function __finishWasiDisposal() {
@@ -831,7 +1025,11 @@ function __captureEmnapiAutoDestroyListener() {
try {
const __finishAutoDestroyCapture = __captureEmnapiAutoDestroyListener()
try {
__emnapiContext = __emnapiCreateContext({ autoDestroy: false })
__emnapiContext = __wrapEmnapiContextDestroyForSettlement(
__emnapiCreateContext({ autoDestroy: false }),
__prepareWasmEnvCleanup,
__isPreparingWasmEnvCleanup,
)
// emnapi 2.x still registers an unconditional once-listener for
// beforeExit that auto-destroys the context, and suppressDestroy() only
// neutralizes its callback without removing it. This loader owns cleanup
@@ -859,7 +1057,7 @@ try {
}
})(),
reuseWorker: true,
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
plugins: [__captureWasiThreadManager, __emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],
wasi: __wasi,
onCreateWorker() {
const worker = __createWasiWorker(__nodePath.join(__dirname, 'wasi-worker.mjs'))
@@ -889,6 +1087,10 @@ try {
}
worker.unref()
// These stubs stay in place for the worker's whole life, disposal
// included: `__keepEventLoopAliveUntil` is what holds the process open
// while a termination is pending, precisely because a worker's own
// references cannot be relied on for it.
}
return worker
},
@@ -911,6 +1113,22 @@ try {
},
}))
__publishWasiDispose(__napiModule.exports)
// The CommonJS tail below aliases `__napiModule.exports`; a named module
// export does not travel with it, so carry the marker on the binding itself
// too. Three things pin the stamp to exactly this spot:
// - inside this `try`, because the guard throws on a
// `#[napi(module_exports)]` hook that claimed the name, and only the
// catch below tears the environment — context, workers, exit listener —
// back down;
// - after the async runtime host install, which hands this same object to
// addon-provided registration functions that may put anything on it;
// - assigning onto the loader's own `module.exports`, which is still the
// original object here, so an addon accessor with a refusing setter is
// never written through. `cjs-module-lexer` — Node's CJS -> ESM named
// export detection — reads the static `module.exports.<name> =` either
// way, and the later `module.exports = __napiModule.exports` does not
// undo that.
module.exports.__napiBindingTarget = __napiStampBindingTarget(__napiModule.exports, __napiBindingTarget)
__registerWasiExitListener()
} catch (error) {
const rollback = {
+3
View File
@@ -1,6 +1,9 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/** The WASI flavor this loader instantiates. */
export declare const __napiBindingTarget: 'wasm32-wasi'
export interface Comment {
type: 'Line' | 'Block'
value: string
+28 -22
View File
@@ -117,8 +117,8 @@ catalogs:
specifier: 2.0.0-alpha.3
version: 2.0.0-alpha.3
'@napi-rs/cli':
specifier: 3.9.1
version: 3.9.1
specifier: 3.10.1
version: 3.10.1
'@napi-rs/wasm-runtime':
specifier: 1.2.4
version: 1.2.4
@@ -161,7 +161,7 @@ importers:
devDependencies:
'@napi-rs/cli':
specifier: 'catalog:'
version: 3.9.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.0.2)(emnapi@2.0.0-alpha.3)
version: 3.10.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.0.2)(emnapi@2.0.0-alpha.3)
oxfmt:
specifier: ^0.67.0
version: 0.67.0(svelte@5.57.0(@typescript-eslint/types@8.69.0))(vite-plus@0.3.0(@arethetypeswrong/core@0.18.5)(@types/node@25.0.2)(@vitest/browser-playwright@4.1.11)(esbuild@0.28.2)(happy-dom@20.0.11)(publint@0.3.24)(svelte@5.57.0(@typescript-eslint/types@8.69.0))(terser@5.44.1)(tsx@4.23.13)(typescript@6.0.3)(vite@8.3.0(@types/node@25.0.2)(esbuild@0.28.2)(terser@5.44.1)(tsx@4.23.13)))
@@ -192,7 +192,7 @@ importers:
version: 0.18.5
'@napi-rs/cli':
specifier: 'catalog:'
version: 3.9.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
version: 3.10.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
'@oxapps/shared':
specifier: workspace:*
version: link:../shared
@@ -240,7 +240,7 @@ importers:
version: 8.0.0
'@napi-rs/cli':
specifier: 'catalog:'
version: 3.9.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
version: 3.10.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
'@oxapps/shared':
specifier: workspace:*
version: link:../shared
@@ -354,7 +354,7 @@ importers:
version: 2.0.0-alpha.3
'@napi-rs/cli':
specifier: 'catalog:'
version: 3.9.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
version: 3.10.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
'@types/node':
specifier: 'catalog:'
version: 24.1.0
@@ -382,7 +382,7 @@ importers:
version: 2.0.0-alpha.3
'@napi-rs/cli':
specifier: 'catalog:'
version: 3.9.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
version: 3.10.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
'@napi-rs/wasm-runtime':
specifier: 'catalog:'
version: 1.2.4(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)
@@ -428,7 +428,7 @@ importers:
version: 2.0.0-alpha.3
'@napi-rs/cli':
specifier: 'catalog:'
version: 3.9.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@25.0.2)(emnapi@2.0.0-alpha.3)
version: 3.10.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@25.0.2)(emnapi@2.0.0-alpha.3)
publint:
specifier: 'catalog:'
version: 0.3.24
@@ -443,7 +443,7 @@ importers:
version: 2.0.0-alpha.3
'@napi-rs/cli':
specifier: 'catalog:'
version: 3.9.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
version: 3.10.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
'@types/node':
specifier: 'catalog:'
version: 24.1.0
@@ -464,7 +464,7 @@ importers:
version: 2.0.0-alpha.3
'@napi-rs/cli':
specifier: 'catalog:'
version: 3.9.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
version: 3.10.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
'@types/node':
specifier: 'catalog:'
version: 24.1.0
@@ -485,7 +485,7 @@ importers:
version: 2.0.0-alpha.3
'@napi-rs/cli':
specifier: 'catalog:'
version: 3.9.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
version: 3.10.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)
'@types/node':
specifier: 'catalog:'
version: 24.1.0
@@ -553,7 +553,7 @@ importers:
devDependencies:
'@napi-rs/cli':
specifier: 'catalog:'
version: 3.9.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.0.2)(emnapi@2.0.0-alpha.3)
version: 3.10.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.0.2)(emnapi@2.0.0-alpha.3)
tasks/compat_data:
devDependencies:
@@ -1480,8 +1480,8 @@ packages:
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
engines: {node: '>=8'}
'@napi-rs/cli@3.9.1':
resolution: {integrity: sha512-2DYBrM2j4n00L+O7fKAS38q19rVQK2HXz5RxSuJgFyZrd9TrdVQFmHOfqvqqsRIRBTZmqiMqBd/yzn4X1a23DQ==}
'@napi-rs/cli@3.10.1':
resolution: {integrity: sha512-9w66AHq2U3vIfj7G0UlVUOENbt6M7XHlHxa+rSbxkVhmCXKY2GIvLGLyYfWz8wBMjwC7dJojNInQhhUJU4hJUw==}
engines: {node: ^20.17.0 || ^22.13.0 || >= 23.5.0}
hasBin: true
peerDependencies:
@@ -4936,6 +4936,10 @@ packages:
resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
engines: {node: '>=12.20.0'}
obug@3.0.0:
resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==}
engines: {node: '>=12.20.0'}
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
@@ -6722,7 +6726,7 @@ snapshots:
'@lukeed/csprng@1.1.0': {}
'@napi-rs/cli@3.9.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)':
'@napi-rs/cli@3.10.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)':
dependencies:
'@inquirer/prompts': 8.5.2(@types/node@24.1.0)
'@napi-rs/cross-toolchain': 1.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
@@ -6732,7 +6736,7 @@ snapshots:
colorette: 2.0.20
es-toolkit: 1.47.0
js-yaml: 5.4.1
obug: 2.1.4
obug: 3.0.0
semver: 7.8.5
typanion: 3.14.0
typescript: 6.0.3
@@ -6754,7 +6758,7 @@ snapshots:
- '@types/node'
- supports-color
'@napi-rs/cli@3.9.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.0.2)(emnapi@2.0.0-alpha.3)':
'@napi-rs/cli@3.10.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@types/node@25.0.2)(emnapi@2.0.0-alpha.3)':
dependencies:
'@inquirer/prompts': 8.5.2(@types/node@25.0.2)
'@napi-rs/cross-toolchain': 1.0.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)
@@ -6764,7 +6768,7 @@ snapshots:
colorette: 2.0.20
es-toolkit: 1.47.0
js-yaml: 5.4.1
obug: 2.1.4
obug: 3.0.0
semver: 7.8.5
typanion: 3.14.0
typescript: 6.0.3
@@ -6786,7 +6790,7 @@ snapshots:
- '@types/node'
- supports-color
'@napi-rs/cli@3.9.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)':
'@napi-rs/cli@3.10.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@24.1.0)(emnapi@2.0.0-alpha.3)':
dependencies:
'@inquirer/prompts': 8.5.2(@types/node@24.1.0)
'@napi-rs/cross-toolchain': 1.0.3(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)
@@ -6796,7 +6800,7 @@ snapshots:
colorette: 2.0.20
es-toolkit: 1.47.0
js-yaml: 5.4.1
obug: 2.1.4
obug: 3.0.0
semver: 7.8.5
typanion: 3.14.0
typescript: 6.0.3
@@ -6818,7 +6822,7 @@ snapshots:
- '@types/node'
- supports-color
'@napi-rs/cli@3.9.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@25.0.2)(emnapi@2.0.0-alpha.3)':
'@napi-rs/cli@3.10.1(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)(@types/node@25.0.2)(emnapi@2.0.0-alpha.3)':
dependencies:
'@inquirer/prompts': 8.5.2(@types/node@25.0.2)
'@napi-rs/cross-toolchain': 1.0.3(@emnapi/core@2.0.0-alpha.3)(@emnapi/runtime@2.0.0-alpha.3)
@@ -6828,7 +6832,7 @@ snapshots:
colorette: 2.0.20
es-toolkit: 1.47.0
js-yaml: 5.4.1
obug: 2.1.4
obug: 3.0.0
semver: 7.8.5
typanion: 3.14.0
typescript: 6.0.3
@@ -9876,6 +9880,8 @@ snapshots:
obug@2.1.4: {}
obug@3.0.0: {}
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
+1 -1
View File
@@ -14,7 +14,7 @@ catalog:
"@emnapi/runtime": 2.0.0-alpha.3
emnapi: 2.0.0-alpha.3
"@arethetypeswrong/core": 0.18.5
"@napi-rs/cli": 3.9.1
"@napi-rs/cli": 3.10.1
"@napi-rs/wasm-runtime": 1.2.4
"@types/node": 24.1.0
cross-env: ^10.1.0
+8
View File
@@ -1,6 +1,14 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
/**
* Which binding artifact the generated loader actually loaded: `'native'` for
* a native addon, otherwise the `platformArchABI` of the WASI flavor. Every
* flavor napi-rs can build is listed, because `NAPI_RS_NATIVE_LIBRARY_PATH`
* can point the loader at a WASI artifact this package does not build itself.
*/
export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'
/**
* Parse `source_text` with `oxc_parser` and print the AST with `oxc_codegen`, including a source map.
*
+17 -1
View File
@@ -7,6 +7,10 @@ const require = createRequire(import.meta.url)
const { readFileSync } = require('fs')
let nativeBinding = null
// Which artifact actually loaded. The WASI fallback chain overwrites it with
// the flavor it resolved; the late native retry below leaves it alone because
// it only runs while no WASI candidate has been loaded.
let __napiLoadedBindingTarget = 'native'
const loadErrors = []
const isMusl = () => {
@@ -65,7 +69,16 @@ const isMuslFromChildProcess = () => {
function requireNative() {
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
try {
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
const overrideBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
// The override may be a generated WASI loader, which already reports its
// own flavor. Adopt it: `module.exports` aliases this object, so claiming
// 'native' would both misreport the artifact and overwrite the loader's
// marker through the alias.
__napiLoadedBindingTarget =
overrideBinding && typeof overrideBinding.__napiBindingTarget === 'string'
? overrideBinding.__napiBindingTarget
: 'native'
return overrideBinding
} catch (err) {
loadErrors.push(err)
}
@@ -630,6 +643,7 @@ if (!nativeBinding || forceWasi) {
if (!candidateFailed) {
wasiBinding = require('./codegen_conformance.wasi.cjs')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -656,6 +670,7 @@ if (!nativeBinding || forceWasi) {
}
wasiBinding = require('oxc-codegen-conformance-wasm32-wasi')
nativeBinding = wasiBinding
__napiLoadedBindingTarget = 'wasm32-wasi'
wasiBindingLoaded = true
}
} catch (err) {
@@ -704,3 +719,4 @@ if (!nativeBinding) {
const { codegen, Severity } = nativeBinding
export { codegen }
export { Severity }
export const __napiBindingTarget = __napiLoadedBindingTarget