@napi-rs/cli 3.9.1 → 3.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/dist/cli.js +1594 -180
- package/dist/index.cjs +1594 -180
- package/dist/index.d.cts +69 -2
- package/dist/index.d.ts +69 -2
- package/dist/index.js +1594 -180
- package/docs/wasi.md +335 -4
- package/package.json +3 -3
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +4361 -15
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/build-regressions.spec.ts +210 -3
- package/src/api/__tests__/build.spec.ts +2139 -3
- package/src/api/__tests__/create-npm-dirs.spec.ts +101 -0
- package/src/api/__tests__/pre-publish.spec.ts +153 -1
- package/src/api/__tests__/templates.spec.ts +1352 -0
- package/src/api/build.ts +637 -31
- package/src/api/create-npm-dirs.ts +21 -9
- package/src/api/pre-publish.ts +29 -1
- package/src/api/templates/binding-target.ts +176 -0
- package/src/api/templates/index.ts +1 -0
- package/src/api/templates/js-binding.ts +48 -3
- package/src/api/templates/load-wasi-template.ts +747 -45
- package/src/utils/__tests__/reconciliation.spec.ts +683 -0
- package/src/utils/config.ts +50 -0
- package/src/utils/misc.ts +347 -74
- package/src/utils/typegen.ts +367 -12
|
@@ -49,6 +49,7 @@ export interface PackageMeta {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const WASM_RUNTIME_PACKAGE_NAME = '@napi-rs/wasm-runtime'
|
|
52
|
+
const ASYNC_RUNTIME_PACKAGE_NAME = '@napi-rs/async-runtime'
|
|
52
53
|
|
|
53
54
|
interface PendingMetadataWrite {
|
|
54
55
|
content: string
|
|
@@ -66,25 +67,25 @@ interface OwnedWasiPackage {
|
|
|
66
67
|
target: Target
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
async function
|
|
70
|
+
async function getLatestPackageVersion(packageName: string) {
|
|
70
71
|
const npmRegistryBase =
|
|
71
72
|
process.env.npm_config_registry?.replace(/\/?$/, '/') ??
|
|
72
73
|
'https://registry.npmjs.org/'
|
|
73
|
-
const packageMetadataUrl = `${npmRegistryBase}${
|
|
74
|
+
const packageMetadataUrl = `${npmRegistryBase}${packageName}`
|
|
74
75
|
let response: Response
|
|
75
76
|
|
|
76
77
|
try {
|
|
77
78
|
response = await fetch(packageMetadataUrl)
|
|
78
79
|
} catch (error) {
|
|
79
80
|
throw new Error(
|
|
80
|
-
`Failed to fetch ${packageMetadataUrl} while resolving ${
|
|
81
|
+
`Failed to fetch ${packageMetadataUrl} while resolving ${packageName}. Check your network connection and npm registry availability.`,
|
|
81
82
|
{ cause: error },
|
|
82
83
|
)
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
if (!response.ok) {
|
|
86
87
|
throw new Error(
|
|
87
|
-
`Failed to fetch ${packageMetadataUrl} while resolving ${
|
|
88
|
+
`Failed to fetch ${packageMetadataUrl} while resolving ${packageName}: npm registry responded with ${response.status} ${response.statusText || 'Unknown Status'}`,
|
|
88
89
|
)
|
|
89
90
|
}
|
|
90
91
|
|
|
@@ -94,7 +95,7 @@ async function getLatestWasmRuntimeVersion() {
|
|
|
94
95
|
packageMeta = (await response.json()) as PackageMeta
|
|
95
96
|
} catch (error) {
|
|
96
97
|
throw new Error(
|
|
97
|
-
`Failed to parse npm registry metadata for ${
|
|
98
|
+
`Failed to parse npm registry metadata for ${packageName} from ${packageMetadataUrl}`,
|
|
98
99
|
{ cause: error },
|
|
99
100
|
)
|
|
100
101
|
}
|
|
@@ -103,7 +104,7 @@ async function getLatestWasmRuntimeVersion() {
|
|
|
103
104
|
|
|
104
105
|
if (typeof latestVersion !== 'string' || latestVersion.trim().length === 0) {
|
|
105
106
|
throw new Error(
|
|
106
|
-
`npm registry metadata for ${
|
|
107
|
+
`npm registry metadata for ${packageName} from ${packageMetadataUrl} did not include a latest dist-tag`,
|
|
107
108
|
)
|
|
108
109
|
}
|
|
109
110
|
|
|
@@ -446,9 +447,15 @@ async function createNpmDirsUnlocked(
|
|
|
446
447
|
),
|
|
447
448
|
)
|
|
448
449
|
).flat()
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
450
|
+
const hasWasmTarget = targets.some((target) => target.arch === 'wasm32')
|
|
451
|
+
const [wasmRuntimeVersion, asyncRuntimeVersion] = await Promise.all([
|
|
452
|
+
hasWasmTarget
|
|
453
|
+
? getLatestPackageVersion(WASM_RUNTIME_PACKAGE_NAME)
|
|
454
|
+
: undefined,
|
|
455
|
+
hasWasmTarget && wasm?.asyncRuntime === true
|
|
456
|
+
? getLatestPackageVersion(ASYNC_RUNTIME_PACKAGE_NAME)
|
|
457
|
+
: undefined,
|
|
458
|
+
])
|
|
452
459
|
const pendingWrites: PendingMetadataWrite[] = []
|
|
453
460
|
|
|
454
461
|
for (const target of targets) {
|
|
@@ -560,6 +567,11 @@ async function createNpmDirsUnlocked(
|
|
|
560
567
|
'@napi-rs/wasm-runtime': `~${wasmRuntimeVersion}`,
|
|
561
568
|
'@emnapi/core': emnapiVersion,
|
|
562
569
|
'@emnapi/runtime': emnapiVersion,
|
|
570
|
+
// The compatibility axis is the host contract version (4), which is
|
|
571
|
+
// stable across a semver major, so a caret range is correct here.
|
|
572
|
+
...(asyncRuntimeVersion
|
|
573
|
+
? { '@napi-rs/async-runtime': `^${asyncRuntimeVersion}` }
|
|
574
|
+
: {}),
|
|
563
575
|
...(wasm?.browser?.buffer === true &&
|
|
564
576
|
(wasm.browser.fs !== true || !wasiTargetHasThreads(target))
|
|
565
577
|
? { buffer: directBufferDependency }
|
package/src/api/pre-publish.ts
CHANGED
|
@@ -113,6 +113,10 @@ const wasiRuntimeDependencies = [
|
|
|
113
113
|
'@napi-rs/wasm-runtime',
|
|
114
114
|
'@emnapi/core',
|
|
115
115
|
'@emnapi/runtime',
|
|
116
|
+
// Only the loaders generated for an addon with `napi.wasm.asyncRuntime` set
|
|
117
|
+
// import it. `releasePackageRuntimeImports` matches by prefix, so the package
|
|
118
|
+
// root and any subpath (`@napi-rs/async-runtime/workerd`) both count.
|
|
119
|
+
'@napi-rs/async-runtime',
|
|
116
120
|
'buffer',
|
|
117
121
|
]
|
|
118
122
|
const require = createRequire(import.meta.url)
|
|
@@ -2618,7 +2622,7 @@ export async function commitPrePublishFileSystemTransaction({
|
|
|
2618
2622
|
await commitFileSystemTransaction(transactionRoot, writes, removals)
|
|
2619
2623
|
}
|
|
2620
2624
|
|
|
2621
|
-
async function validateReleasePackageContents({
|
|
2625
|
+
export async function validateReleasePackageContents({
|
|
2622
2626
|
pkgDir,
|
|
2623
2627
|
rootDir,
|
|
2624
2628
|
packageName,
|
|
@@ -3011,6 +3015,30 @@ function validateWasiReleasePackageManifest(
|
|
|
3011
3015
|
`Release package ${packageJson.name} must omit buffer when its loaders do not import it`,
|
|
3012
3016
|
)
|
|
3013
3017
|
}
|
|
3018
|
+
|
|
3019
|
+
// `create-npm-dirs` resolves the version from the registry rather than
|
|
3020
|
+
// pinning it here, so only the range's shape can be checked. The require /
|
|
3021
|
+
// forbid pair is what matters: an undeclared import breaks the published
|
|
3022
|
+
// package at load, and a declared dependency no loader imports is dead
|
|
3023
|
+
// weight on every consumer of this flavor.
|
|
3024
|
+
if (packagedRuntimeImports.has('@napi-rs/async-runtime')) {
|
|
3025
|
+
const asyncRuntimeVersion = requireStringDependency(
|
|
3026
|
+
packageJson.name,
|
|
3027
|
+
dependencies,
|
|
3028
|
+
'@napi-rs/async-runtime',
|
|
3029
|
+
)
|
|
3030
|
+
try {
|
|
3031
|
+
new Range(asyncRuntimeVersion)
|
|
3032
|
+
} catch {
|
|
3033
|
+
throw new Error(
|
|
3034
|
+
`Release package ${packageJson.name} has invalid @napi-rs/async-runtime dependency ${asyncRuntimeVersion}`,
|
|
3035
|
+
)
|
|
3036
|
+
}
|
|
3037
|
+
} else if (dependencies['@napi-rs/async-runtime'] !== undefined) {
|
|
3038
|
+
throw new Error(
|
|
3039
|
+
`Release package ${packageJson.name} must omit @napi-rs/async-runtime when its loaders do not import it`,
|
|
3040
|
+
)
|
|
3041
|
+
}
|
|
3014
3042
|
}
|
|
3015
3043
|
|
|
3016
3044
|
function requireStringDependency(
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `__napiBindingTarget` contract, shared by every generated loader.
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately imports nothing: both `js-binding.ts` and
|
|
5
|
+
* `load-wasi-template.ts` depend on it, and `load-wasi-template.ts` otherwise
|
|
6
|
+
* has no top-level imports at all. Keeping the contract at the bottom of the
|
|
7
|
+
* graph is what lets both templates emit the same runtime helper without
|
|
8
|
+
* duplicating its source.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Named export every generated loader uses to report which binding artifact
|
|
13
|
+
* actually loaded: `'native'` for a `.node` addon, otherwise the
|
|
14
|
+
* `platformArchABI` of the WASI flavor (`'wasm32-wasi'`, `'wasm32-wasip1'`).
|
|
15
|
+
*/
|
|
16
|
+
export const NAPI_BINDING_TARGET_EXPORT = '__napiBindingTarget'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `code` on the error the emitted loader throws when the binding it loaded
|
|
20
|
+
* already owns {@link NAPI_BINDING_TARGET_EXPORT}. Named after the other
|
|
21
|
+
* loader-thrown codes (`ERR_NAPI_WASI_LIFECYCLE_REENTRY`,
|
|
22
|
+
* `ERR_NAPI_WASI_CLEANUP_PENDING`, `ERR_NAPI_ASYNC_RUNTIME_BINDING_MISMATCH`)
|
|
23
|
+
* so a consumer can branch on it instead of on the message.
|
|
24
|
+
*/
|
|
25
|
+
export const ERR_NAPI_BINDING_TARGET_CONFLICT =
|
|
26
|
+
'ERR_NAPI_BINDING_TARGET_CONFLICT'
|
|
27
|
+
|
|
28
|
+
/** Name of the runtime helper {@link BINDING_TARGET_STAMP_HELPER} declares. */
|
|
29
|
+
export const NAPI_BINDING_TARGET_STAMP_FN = '__napiStampBindingTarget'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reject an export of {@link NAPI_BINDING_TARGET_EXPORT} at build time.
|
|
33
|
+
*
|
|
34
|
+
* `idents` is the type-def export list, so this only sees what napi-rs type
|
|
35
|
+
* generation reports. A name attached imperatively by a
|
|
36
|
+
* `#[napi(module_exports)]` hook emits no type-def entry and is invisible here
|
|
37
|
+
* — the same blind spot `typeDefAvailable` documents for the sibling check.
|
|
38
|
+
* {@link BINDING_TARGET_STAMP_HELPER} is what catches those, at load time.
|
|
39
|
+
*
|
|
40
|
+
* What this check is load-bearing for: a duplicated ident would emit a
|
|
41
|
+
* duplicate `export const` in the ESM loader (a syntax error), and would make
|
|
42
|
+
* the CJS loader overwrite its own reported target. It also keeps the generated
|
|
43
|
+
* `.d.ts` free of a duplicate identifier.
|
|
44
|
+
*
|
|
45
|
+
* All three are consequences of emitting something, so callers must only ask
|
|
46
|
+
* when the build emits it: the loader templates check themselves, and the root
|
|
47
|
+
* `.d.ts` is checked behind `bindingTargetDeclarationPredicate`. A build that
|
|
48
|
+
* writes no loader reserves nothing.
|
|
49
|
+
*/
|
|
50
|
+
export function assertBindingTargetIdentFree(idents: string[]): void {
|
|
51
|
+
if (idents.indexOf(NAPI_BINDING_TARGET_EXPORT) !== -1) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`\`${NAPI_BINDING_TARGET_EXPORT}\` is reserved by the generated binding loader. Rename the napi export, e.g. #[napi(js_name = "...")].`,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Runtime helper emitted into every loader that stamps
|
|
60
|
+
* {@link NAPI_BINDING_TARGET_EXPORT} onto an exports object it does not own.
|
|
61
|
+
*
|
|
62
|
+
* Five outcomes, in order:
|
|
63
|
+
*
|
|
64
|
+
* | exports object state | result |
|
|
65
|
+
* | ------------------------------- | ----------------------------------------- |
|
|
66
|
+
* | own property, same value | no-op, returns `target` |
|
|
67
|
+
* | own property, different value | throw, `ERR_NAPI_BINDING_TARGET_CONFLICT` |
|
|
68
|
+
* | non-extensible, no own property | skip, returns `target` |
|
|
69
|
+
* | refuses the definition | skip, returns `target` |
|
|
70
|
+
* | otherwise | stamp, returns `target` |
|
|
71
|
+
*
|
|
72
|
+
* The stamp is `Object.defineProperty`, not an assignment. `hasOwnProperty`
|
|
73
|
+
* above sees own properties only and `Object.isExtensible` only own
|
|
74
|
+
* extensibility, so an ordinary assignment would still walk the prototype chain
|
|
75
|
+
* into an inherited accessor on a user-controlled object: its setter can throw,
|
|
76
|
+
* failing an otherwise successful load, or absorb the write and create nothing,
|
|
77
|
+
* leaving the named export the generated declaration promises resolving to
|
|
78
|
+
* `undefined`. `[[Define]]` consults no prototype, and the descriptor is the one
|
|
79
|
+
* a successful assignment would have produced. The `try` around it covers the
|
|
80
|
+
* one shape that can still refuse — an exotic object such as a `Proxy` whose
|
|
81
|
+
* `defineProperty` trap returns `false` — under the same rule as the
|
|
82
|
+
* non-extensible skip: metadata never fails a load.
|
|
83
|
+
*
|
|
84
|
+
* Every branch that does not throw returns `target`, because the CommonJS emit
|
|
85
|
+
* sites assign the return value —
|
|
86
|
+
* `module.exports.__napiBindingTarget = __napiStampBindingTarget(...)` — rather
|
|
87
|
+
* than calling it as a statement. Node's CJS -> ESM named export detection is
|
|
88
|
+
* `cjs-module-lexer`, a static scanner: it reports `__napiBindingTarget` as a
|
|
89
|
+
* named export only when it can see a `module.exports.<name> =` assignment, and
|
|
90
|
+
* a bare call is invisible to it, so `import { __napiBindingTarget }` from a
|
|
91
|
+
* generated CJS loader stops linking entirely.
|
|
92
|
+
*
|
|
93
|
+
* That assignment always succeeds, and it is never what a consumer reads: its
|
|
94
|
+
* target is the loader's own `module.exports`, an ordinary extensible object,
|
|
95
|
+
* and the alias on the next line replaces it with the binding itself. The value
|
|
96
|
+
* an import resolves to is therefore the one the stamp put on the binding, so
|
|
97
|
+
* when the guard skips — a sealed or frozen exports object — the CommonJS
|
|
98
|
+
* entries report `undefined`. `build.spec.ts` pins exactly that, in `a frozen
|
|
99
|
+
* addon keeps __napiBindingTarget importable, just undefined`. The ESM loaders
|
|
100
|
+
* are unaffected: theirs is a module-level `export const`, not a property of
|
|
101
|
+
* the object they hand out.
|
|
102
|
+
* (`Object.defineProperty` is not an alternative shape for the lexer, whatever
|
|
103
|
+
* it is inside the guard: the lexer matches a literal `module.exports.<name> =`
|
|
104
|
+
* and reports nothing for a data-descriptor `defineProperty` call, so the named
|
|
105
|
+
* import stops linking entirely — measured against Node's own detection.)
|
|
106
|
+
*
|
|
107
|
+
* The assignment target is never the object being stamped. Both CommonJS
|
|
108
|
+
* loaders stamp the addon's exports object but assign onto their own
|
|
109
|
+
* `module.exports`, which they replace with that object afterwards: an addon
|
|
110
|
+
* accessor can report the expected value from a getter and still throw from its
|
|
111
|
+
* setter, and only the guard's `hasOwnProperty`-and-read path is safe to run
|
|
112
|
+
* against it.
|
|
113
|
+
*
|
|
114
|
+
* Placement is the same rule in every loader that stamps a binding object:
|
|
115
|
+
* exactly one stamp, after the async runtime host installation — which hands
|
|
116
|
+
* that object to addon-provided registration functions that may reshape it —
|
|
117
|
+
* and inside the initialization guard that can undo a failed load: the `try`
|
|
118
|
+
* that rolls the WASI environment back, or the one that marks a deferred
|
|
119
|
+
* instance failed.
|
|
120
|
+
*
|
|
121
|
+
* The equal-value short circuit is required, not cosmetic: the root CJS loader
|
|
122
|
+
* aliases the object it loaded, so a `NAPI_RS_NATIVE_LIBRARY_PATH` override
|
|
123
|
+
* that is itself a generated WASI loader — and every WASI fallback candidate —
|
|
124
|
+
* hands back an object already carrying the value about to be stamped.
|
|
125
|
+
*
|
|
126
|
+
* Node 12 compatible (no optional chaining, no nullish coalescing) and valid in
|
|
127
|
+
* both sloppy CJS and strict ESM, because all four loaders emit it verbatim.
|
|
128
|
+
*/
|
|
129
|
+
export const BINDING_TARGET_STAMP_HELPER = `function ${NAPI_BINDING_TARGET_STAMP_FN}(exportsObject, target) {
|
|
130
|
+
if (
|
|
131
|
+
Object.prototype.hasOwnProperty.call(exportsObject, '${NAPI_BINDING_TARGET_EXPORT}')
|
|
132
|
+
) {
|
|
133
|
+
if (exportsObject.${NAPI_BINDING_TARGET_EXPORT} === target) {
|
|
134
|
+
// Already ours: the root entry aliases the object it loaded, so a WASI
|
|
135
|
+
// fallback candidate — or a \`NAPI_RS_NATIVE_LIBRARY_PATH\` override that
|
|
136
|
+
// is a generated loader — arrives already stamped with this same value.
|
|
137
|
+
return target
|
|
138
|
+
}
|
|
139
|
+
const error = new Error(
|
|
140
|
+
'\`${NAPI_BINDING_TARGET_EXPORT}\` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].',
|
|
141
|
+
)
|
|
142
|
+
error.code = '${ERR_NAPI_BINDING_TARGET_CONFLICT}'
|
|
143
|
+
throw error
|
|
144
|
+
}
|
|
145
|
+
if (!Object.isExtensible(exportsObject)) {
|
|
146
|
+
// A \`#[napi(module_exports)]\` hook may seal or freeze this object
|
|
147
|
+
// (\`Object::seal\` / \`Object::freeze\`). Reporting the artifact is metadata,
|
|
148
|
+
// never a reason to fail an otherwise successful load, so the stamp is
|
|
149
|
+
// skipped. What a consumer still sees then follows the entry point: the
|
|
150
|
+
// browser and deferred loaders declare \`${NAPI_BINDING_TARGET_EXPORT}\` at module
|
|
151
|
+
// level and go on reporting it, while the CommonJS entries hand back this
|
|
152
|
+
// very object as \`module.exports\`, so there the value is absent.
|
|
153
|
+
return target
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
// [[Define]], not [[Set]]: an ordinary assignment walks the prototype
|
|
157
|
+
// chain, so an inherited accessor could swallow the value or throw and
|
|
158
|
+
// fail an otherwise successful load. The descriptor is what a successful
|
|
159
|
+
// assignment would have produced.
|
|
160
|
+
Object.defineProperty(exportsObject, '${NAPI_BINDING_TARGET_EXPORT}', {
|
|
161
|
+
configurable: true,
|
|
162
|
+
enumerable: true,
|
|
163
|
+
value: target,
|
|
164
|
+
writable: true,
|
|
165
|
+
})
|
|
166
|
+
} catch {
|
|
167
|
+
// Same rule as the non-extensible skip above: reporting the artifact is
|
|
168
|
+
// metadata, never a reason to fail an otherwise successful load. An exotic
|
|
169
|
+
// object (a Proxy whose defineProperty trap refuses) is skipped, not
|
|
170
|
+
// thrown over.
|
|
171
|
+
}
|
|
172
|
+
// The CommonJS loaders assign this return value so \`cjs-module-lexer\` — and
|
|
173
|
+
// therefore Node's CJS -> ESM named export detection — can see
|
|
174
|
+
// \`${NAPI_BINDING_TARGET_EXPORT}\` statically.
|
|
175
|
+
return target
|
|
176
|
+
}`
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { wasiLoaderSuffix } from '../../utils/index.js'
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
assertBindingTargetIdentFree,
|
|
5
|
+
BINDING_TARGET_STAMP_HELPER,
|
|
6
|
+
NAPI_BINDING_TARGET_EXPORT,
|
|
7
|
+
NAPI_BINDING_TARGET_STAMP_FN,
|
|
8
|
+
} from './binding-target.js'
|
|
9
|
+
|
|
3
10
|
function resolveWasiFlavors(wasiFlavors?: string[]): string[] {
|
|
4
11
|
return wasiFlavors && wasiFlavors.length > 0 ? wasiFlavors : ['wasm32-wasi']
|
|
5
12
|
}
|
|
@@ -62,6 +69,7 @@ function createWasiFallbackChain(
|
|
|
62
69
|
}
|
|
63
70
|
wasiBinding = require('${specifier}')
|
|
64
71
|
nativeBinding = wasiBinding
|
|
72
|
+
__napiLoadedBindingTarget = '${flavor}'
|
|
65
73
|
wasiBindingLoaded = true
|
|
66
74
|
}
|
|
67
75
|
} catch (err) {
|
|
@@ -126,6 +134,7 @@ export function createCjsBinding(
|
|
|
126
134
|
wasiFlavors?: string[],
|
|
127
135
|
localWasiName?: string,
|
|
128
136
|
): string {
|
|
137
|
+
assertBindingTargetIdentFree(idents)
|
|
129
138
|
return `${bindingHeader}
|
|
130
139
|
${createCommonBinding(
|
|
131
140
|
localName,
|
|
@@ -134,6 +143,23 @@ ${createCommonBinding(
|
|
|
134
143
|
wasiFlavors,
|
|
135
144
|
localWasiName,
|
|
136
145
|
)}
|
|
146
|
+
${BINDING_TARGET_STAMP_HELPER}
|
|
147
|
+
// Stamp before the alias, not after. The guard only reads \`nativeBinding\`
|
|
148
|
+
// (\`hasOwnProperty\` plus a comparison), which is safe against any addon
|
|
149
|
+
// accessor; an assignment is not, because a \`#[napi(module_exports)]\` hook can
|
|
150
|
+
// expose a getter reporting this very value and a setter that throws. So the
|
|
151
|
+
// assignment lands on the loader's own \`module.exports\`, still the original
|
|
152
|
+
// object here, and the alias below replaces it.
|
|
153
|
+
//
|
|
154
|
+
// The assignment is what keeps the marker a statically visible CommonJS export:
|
|
155
|
+
// \`cjs-module-lexer\` is Node's CJS -> ESM named export detection, it cannot see
|
|
156
|
+
// a bare call, and the later \`module.exports = nativeBinding\` does not undo the
|
|
157
|
+
// detection. The assignment itself always succeeds — its target is this
|
|
158
|
+
// loader's own, still extensible \`module.exports\` — and the alias below then
|
|
159
|
+
// discards the value it wrote. What a consumer reads is whatever the guard put
|
|
160
|
+
// on \`nativeBinding\`, so on a frozen binding, where the guard skips, the
|
|
161
|
+
// linked import resolves to \`undefined\`.
|
|
162
|
+
module.exports.${NAPI_BINDING_TARGET_EXPORT} = ${NAPI_BINDING_TARGET_STAMP_FN}(nativeBinding, __napiLoadedBindingTarget)
|
|
137
163
|
module.exports = nativeBinding
|
|
138
164
|
${idents
|
|
139
165
|
.map((ident) => `module.exports.${ident} = nativeBinding.${ident}`)
|
|
@@ -149,11 +175,17 @@ export function createEsmBinding(
|
|
|
149
175
|
wasiFlavors?: string[],
|
|
150
176
|
localWasiName?: string,
|
|
151
177
|
): string {
|
|
178
|
+
assertBindingTargetIdentFree(idents)
|
|
179
|
+
// Both branches must carry it, or a zero-ident package silently loses the
|
|
180
|
+
// export.
|
|
181
|
+
const bindingTargetExport = `export const ${NAPI_BINDING_TARGET_EXPORT} = __napiLoadedBindingTarget`
|
|
152
182
|
const exportsCode =
|
|
153
183
|
idents.length > 0
|
|
154
184
|
? `const { ${idents.join(', ')} } = nativeBinding
|
|
155
|
-
${idents.map((ident) => `export { ${ident} }`).join('\n')}
|
|
156
|
-
|
|
185
|
+
${idents.map((ident) => `export { ${ident} }`).join('\n')}
|
|
186
|
+
${bindingTargetExport}`
|
|
187
|
+
: `export default nativeBinding
|
|
188
|
+
${bindingTargetExport}`
|
|
157
189
|
return `${bindingHeader}
|
|
158
190
|
import { createRequire } from 'module'
|
|
159
191
|
const require = createRequire(import.meta.url)
|
|
@@ -213,6 +245,10 @@ ${identLow}}${versionCheck}`
|
|
|
213
245
|
|
|
214
246
|
return `const { readFileSync } = require('fs')
|
|
215
247
|
let nativeBinding = null
|
|
248
|
+
// Which artifact actually loaded. The WASI fallback chain overwrites it with
|
|
249
|
+
// the flavor it resolved; the late native retry below leaves it alone because
|
|
250
|
+
// it only runs while no WASI candidate has been loaded.
|
|
251
|
+
let __napiLoadedBindingTarget = 'native'
|
|
216
252
|
const loadErrors = []
|
|
217
253
|
|
|
218
254
|
const isMusl = () => {
|
|
@@ -271,7 +307,16 @@ const isMuslFromChildProcess = () => {
|
|
|
271
307
|
function requireNative() {
|
|
272
308
|
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
|
|
273
309
|
try {
|
|
274
|
-
|
|
310
|
+
const overrideBinding = require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
|
|
311
|
+
// The override may be a generated WASI loader, which already reports its
|
|
312
|
+
// own flavor. Adopt it: \`module.exports\` aliases this object, so claiming
|
|
313
|
+
// 'native' would both misreport the artifact and overwrite the loader's
|
|
314
|
+
// marker through the alias.
|
|
315
|
+
__napiLoadedBindingTarget =
|
|
316
|
+
overrideBinding && typeof overrideBinding.${NAPI_BINDING_TARGET_EXPORT} === 'string'
|
|
317
|
+
? overrideBinding.${NAPI_BINDING_TARGET_EXPORT}
|
|
318
|
+
: 'native'
|
|
319
|
+
return overrideBinding
|
|
275
320
|
} catch (err) {
|
|
276
321
|
loadErrors.push(err)
|
|
277
322
|
}
|