Skip to content

Commit a60671f

Browse files
committed
fix: regenerate native bindings after NAPI-RS upgrade
1 parent 8b5ac2f commit a60671f

2 files changed

Lines changed: 88 additions & 1 deletion

File tree

‎packages/rstack/binding.cjs‎

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
const { readFileSync } = require('fs')
66
let nativeBinding = null
7+
// Which artifact actually loaded. The WASI fallback chain overwrites it with
8+
// the flavor it resolved; the late native retry below leaves it alone because
9+
// it only runs while no WASI candidate has been loaded.
10+
let __napiLoadedBindingTarget = 'native'
711
const loadErrors = []
812

913
const isMusl = () => {
@@ -62,7 +66,16 @@ const isMuslFromChildProcess = () => {
6266
function requireNative() {
6367
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
6468
try {
65-
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
69+
const overrideBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
70+
// The override may be a generated WASI loader, which already reports its
71+
// own flavor. Adopt it: `module.exports` aliases this object, so claiming
72+
// 'native' would both misreport the artifact and overwrite the loader's
73+
// marker through the alias.
74+
__napiLoadedBindingTarget =
75+
overrideBinding && typeof overrideBinding.__napiBindingTarget === 'string'
76+
? overrideBinding.__napiBindingTarget
77+
: 'native'
78+
return overrideBinding
6679
} catch (err) {
6780
loadErrors.push(err)
6881
}
@@ -627,6 +640,7 @@ if (!nativeBinding || forceWasi) {
627640
if (!candidateFailed) {
628641
wasiBinding = require('./rstack.wasi.cjs')
629642
nativeBinding = wasiBinding
643+
__napiLoadedBindingTarget = 'wasm32-wasi'
630644
wasiBindingLoaded = true
631645
}
632646
} catch (err) {
@@ -653,6 +667,7 @@ if (!nativeBinding || forceWasi) {
653667
}
654668
wasiBinding = require('@rstackjs/cli-wasm32-wasi')
655669
nativeBinding = wasiBinding
670+
__napiLoadedBindingTarget = 'wasm32-wasi'
656671
wasiBindingLoaded = true
657672
}
658673
} catch (err) {
@@ -698,6 +713,70 @@ if (!nativeBinding) {
698713
throw new Error(`Failed to load native binding`)
699714
}
700715

716+
function __napiStampBindingTarget(exportsObject, target) {
717+
if (
718+
Object.prototype.hasOwnProperty.call(exportsObject, '__napiBindingTarget')
719+
) {
720+
if (exportsObject.__napiBindingTarget === target) {
721+
// Already ours: the root entry aliases the object it loaded, so a WASI
722+
// fallback candidate — or a `NAPI_RS_NATIVE_LIBRARY_PATH` override that
723+
// is a generated loader — arrives already stamped with this same value.
724+
return target
725+
}
726+
const error = new Error(
727+
'`__napiBindingTarget` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].',
728+
)
729+
error.code = 'ERR_NAPI_BINDING_TARGET_CONFLICT'
730+
throw error
731+
}
732+
if (!Object.isExtensible(exportsObject)) {
733+
// A `#[napi(module_exports)]` hook may seal or freeze this object
734+
// (`Object::seal` / `Object::freeze`). Reporting the artifact is metadata,
735+
// never a reason to fail an otherwise successful load, so the stamp is
736+
// skipped. What a consumer still sees then follows the entry point: the
737+
// browser and deferred loaders declare `__napiBindingTarget` at module
738+
// level and go on reporting it, while the CommonJS entries hand back this
739+
// very object as `module.exports`, so there the value is absent.
740+
return target
741+
}
742+
try {
743+
// [[Define]], not [[Set]]: an ordinary assignment walks the prototype
744+
// chain, so an inherited accessor could swallow the value or throw and
745+
// fail an otherwise successful load. The descriptor is what a successful
746+
// assignment would have produced.
747+
Object.defineProperty(exportsObject, '__napiBindingTarget', {
748+
configurable: true,
749+
enumerable: true,
750+
value: target,
751+
writable: true,
752+
})
753+
} catch {
754+
// Same rule as the non-extensible skip above: reporting the artifact is
755+
// metadata, never a reason to fail an otherwise successful load. An exotic
756+
// object (a Proxy whose defineProperty trap refuses) is skipped, not
757+
// thrown over.
758+
}
759+
// The CommonJS loaders assign this return value so `cjs-module-lexer` — and
760+
// therefore Node's CJS -> ESM named export detection — can see
761+
// `__napiBindingTarget` statically.
762+
return target
763+
}
764+
// Stamp before the alias, not after. The guard only reads `nativeBinding`
765+
// (`hasOwnProperty` plus a comparison), which is safe against any addon
766+
// accessor; an assignment is not, because a `#[napi(module_exports)]` hook can
767+
// expose a getter reporting this very value and a setter that throws. So the
768+
// assignment lands on the loader's own `module.exports`, still the original
769+
// object here, and the alias below replaces it.
770+
//
771+
// The assignment is what keeps the marker a statically visible CommonJS export:
772+
// `cjs-module-lexer` is Node's CJS -> ESM named export detection, it cannot see
773+
// a bare call, and the later `module.exports = nativeBinding` does not undo the
774+
// detection. The assignment itself always succeeds — its target is this
775+
// loader's own, still extensible `module.exports` — and the alias below then
776+
// discards the value it wrote. What a consumer reads is whatever the guard put
777+
// on `nativeBinding`, so on a frozen binding, where the guard skips, the
778+
// linked import resolves to `undefined`.
779+
module.exports.__napiBindingTarget = __napiStampBindingTarget(nativeBinding, __napiLoadedBindingTarget)
701780
module.exports = nativeBinding
702781
module.exports.GitIgnoreMatcher = nativeBinding.GitIgnoreMatcher
703782
module.exports.IgnoreMatcher = nativeBinding.IgnoreMatcher

‎packages/rstack/binding.d.cts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
/* auto-generated by NAPI-RS */
22
/* eslint-disable */
33

4+
/**
5+
* Which binding artifact the generated loader actually loaded: `'native'` for
6+
* a native addon, otherwise the `platformArchABI` of the WASI flavor. Every
7+
* flavor napi-rs can build is listed, because `NAPI_RS_NATIVE_LIBRARY_PATH`
8+
* can point the loader at a WASI artifact this package does not build itself.
9+
*/
10+
export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'
11+
412
/** JavaScript-facing hierarchy for repository `.gitignore` files. */
513
export declare class GitIgnoreMatcher {
614
/** Creates an empty matcher whose sources can be added during directory traversal. */

0 commit comments

Comments
 (0)