@napi-rs/cli 3.9.0 → 3.10.0
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 +10359 -8729
- package/dist/index.cjs +10381 -8751
- package/dist/index.d.cts +83 -16
- package/dist/index.d.ts +83 -16
- package/dist/index.js +10359 -8729
- package/docs/wasi.md +318 -4
- package/package.json +5 -6
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +4003 -53
- 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 +2333 -3
- package/src/api/__tests__/create-npm-dirs.spec.ts +105 -3
- package/src/api/__tests__/pre-publish.spec.ts +153 -1
- package/src/api/__tests__/templates.spec.ts +1309 -1
- package/src/api/build.ts +756 -30
- package/src/api/create-npm-dirs.ts +23 -10
- package/src/api/new.ts +13 -18
- package/src/api/pre-publish.ts +34 -11
- package/src/api/rename.ts +10 -21
- package/src/api/templates/binding-target.ts +176 -0
- package/src/api/templates/index.ts +1 -0
- package/src/api/templates/js-binding.ts +54 -10
- package/src/api/templates/load-wasi-template.ts +661 -58
- package/src/api/templates/wasi-worker-template.ts +36 -31
- package/src/commands/build.ts +1 -1
- package/src/utils/__tests__/__snapshots__/typegen.spec.ts.md +18 -28
- package/src/utils/__tests__/__snapshots__/typegen.spec.ts.snap +0 -0
- package/src/utils/__tests__/misc.spec.ts +4 -0
- package/src/utils/__tests__/reconciliation.spec.ts +676 -0
- package/src/utils/__tests__/serialize.spec.ts +55 -0
- package/src/utils/__tests__/target.spec.ts +221 -0
- package/src/utils/__tests__/typegen.spec.ts +115 -0
- package/src/utils/config.ts +50 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/misc.ts +351 -79
- package/src/utils/serialize.ts +47 -0
- package/src/utils/target.ts +150 -1
- package/src/utils/typegen.ts +608 -42
package/src/api/build.ts
CHANGED
|
@@ -41,11 +41,15 @@ import {
|
|
|
41
41
|
removeNodeStreamWebTypeImports,
|
|
42
42
|
rewriteUnboundNodeGlobalTypeQueries,
|
|
43
43
|
rewriteTypeImportReferences,
|
|
44
|
+
scanExportedName,
|
|
44
45
|
type Target,
|
|
45
46
|
targetToEnvVar,
|
|
46
47
|
tryInstallCargoBinary,
|
|
47
48
|
unlinkAsync,
|
|
49
|
+
rustBundledWasiLibc,
|
|
50
|
+
wasiLibcHasNewFutexAbi,
|
|
48
51
|
wasiLoaderSuffix,
|
|
52
|
+
wasiSdkMajorVersion,
|
|
49
53
|
wasiTargetHasThreads,
|
|
50
54
|
writeFileAtomic,
|
|
51
55
|
withFileSystemReconciliation,
|
|
@@ -54,7 +58,12 @@ import {
|
|
|
54
58
|
type CargoWorkspaceMetadata,
|
|
55
59
|
} from '../utils/index.js'
|
|
56
60
|
|
|
57
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
assertBindingTargetIdentFree,
|
|
63
|
+
createCjsBinding,
|
|
64
|
+
createEsmBinding,
|
|
65
|
+
NAPI_BINDING_TARGET_EXPORT,
|
|
66
|
+
} from './templates/index.js'
|
|
58
67
|
import {
|
|
59
68
|
createWasiBinding,
|
|
60
69
|
createWasiBrowserBinding,
|
|
@@ -77,6 +86,98 @@ const MANAGED_WASI_FLAVORS = [
|
|
|
77
86
|
},
|
|
78
87
|
] as const
|
|
79
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Everything a generated loader can report through `__napiBindingTarget`, as a
|
|
91
|
+
* TypeScript literal union. It lists every WASI flavor napi-rs knows instead of
|
|
92
|
+
* the flavors one package builds: `NAPI_RS_NATIVE_LIBRARY_PATH` may point at
|
|
93
|
+
* any generated WASI loader, and the root entry then reports what that loader
|
|
94
|
+
* loaded. A narrower union would make TypeScript reject the branches the
|
|
95
|
+
* override can actually reach.
|
|
96
|
+
*/
|
|
97
|
+
const BINDING_TARGET_TYPE_UNION = [
|
|
98
|
+
"'native'",
|
|
99
|
+
...MANAGED_WASI_FLAVORS.map((flavor) => `'${flavor.platformArchABI}'`),
|
|
100
|
+
].join(' | ')
|
|
101
|
+
|
|
102
|
+
const BINDING_TARGET_TYPE_DECLARATION = `
|
|
103
|
+
/**
|
|
104
|
+
* Which binding artifact the generated loader actually loaded: \`'native'\` for
|
|
105
|
+
* a native addon, otherwise the \`platformArchABI\` of the WASI flavor. Every
|
|
106
|
+
* flavor napi-rs can build is listed, because \`NAPI_RS_NATIVE_LIBRARY_PATH\`
|
|
107
|
+
* can point the loader at a WASI artifact this package does not build itself.
|
|
108
|
+
*/
|
|
109
|
+
export declare const ${NAPI_BINDING_TARGET_EXPORT}: ${BINDING_TARGET_TYPE_UNION}
|
|
110
|
+
`
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The declaration for a file that types one fixed WASI artifact.
|
|
114
|
+
*
|
|
115
|
+
* A flavor's own loaders bake their `platformArchABI` in at generation time
|
|
116
|
+
* and have no override to read — `NAPI_RS_NATIVE_LIBRARY_PATH` exists only in
|
|
117
|
+
* the root loader — so the union {@link BINDING_TARGET_TYPE_DECLARATION}
|
|
118
|
+
* declares is unreachable for them, and declaring it would stop a consumer of
|
|
119
|
+
* a fixed artifact from narrowing. The wording matches the declaration the
|
|
120
|
+
* deferred `./workerd` entry already emits for the same reason.
|
|
121
|
+
*/
|
|
122
|
+
const createBindingTargetFlavorDeclaration = (platformArchABI: string) => `
|
|
123
|
+
/** The WASI flavor this loader instantiates. */
|
|
124
|
+
export declare const ${NAPI_BINDING_TARGET_EXPORT}: '${platformArchABI}'
|
|
125
|
+
`
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* How `source` exports `__napiBindingTarget`, and whether it exports by
|
|
129
|
+
* assignment instead.
|
|
130
|
+
*
|
|
131
|
+
* Only a real top-level export counts, and only the parser can tell one from
|
|
132
|
+
* the other places the name reaches a declaration file: `napi-derive`'s
|
|
133
|
+
* `js_doc` copies doc comments through verbatim, a `--dts-header` may carry a
|
|
134
|
+
* commented-out example of the declaration — or of `export = binding` — and a
|
|
135
|
+
* member of a `declare namespace` or `declare module` block is an export of
|
|
136
|
+
* that block, not of the file. Importing any of those yields TS2305, so
|
|
137
|
+
* treating one as the declaration would leave the generated declaration
|
|
138
|
+
* unwritten. A longer export such as `__napiBindingTargetInfo` is a different
|
|
139
|
+
* name and never counts — `assertBindingTargetIdentFree` only reserves the
|
|
140
|
+
* exact one.
|
|
141
|
+
*
|
|
142
|
+
* {@link scanExportedName} carries the rest of the rule in its `ownsName`
|
|
143
|
+
* result: which export shapes claim the name, and why the ones that live in
|
|
144
|
+
* type space alone let the generated declaration merge with them instead.
|
|
145
|
+
*/
|
|
146
|
+
const bindingTargetExports = (source: string) =>
|
|
147
|
+
scanExportedName(source, NAPI_BINDING_TARGET_EXPORT)
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Every literal a declaration this CLI wrote can name. The root entry's union
|
|
151
|
+
* {@link BINDING_TARGET_TYPE_UNION} and a flavor's own literal
|
|
152
|
+
* ({@link createBindingTargetFlavorDeclaration}) are both spelled out of this
|
|
153
|
+
* set, so a declaration whose type is built only from these is one of ours and
|
|
154
|
+
* may be refreshed in place; anything else belongs to whoever wrote it.
|
|
155
|
+
*
|
|
156
|
+
* A flavor dropped from {@link MANAGED_WASI_FLAVORS} stops being recognised,
|
|
157
|
+
* and its declaration is preserved rather than refreshed: a literal this
|
|
158
|
+
* version can no longer write is indistinguishable from one a `--dts-header`
|
|
159
|
+
* contributed.
|
|
160
|
+
*/
|
|
161
|
+
const MANAGED_BINDING_TARGET_LITERALS = new Set([
|
|
162
|
+
'native',
|
|
163
|
+
...MANAGED_WASI_FLAVORS.map((flavor) => flavor.platformArchABI),
|
|
164
|
+
])
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Whether a declared type is one this CLI wrote. Keyed on the type rather than
|
|
168
|
+
* on the doc comment above it, so rewording a comment cannot strand a
|
|
169
|
+
* declaration this version would otherwise refresh.
|
|
170
|
+
*/
|
|
171
|
+
const isGeneratedBindingTargetType = (type: string) =>
|
|
172
|
+
type
|
|
173
|
+
.split('|')
|
|
174
|
+
.map((member) => member.trim())
|
|
175
|
+
.every(
|
|
176
|
+
(member) =>
|
|
177
|
+
/^'[^']*'$/.test(member) &&
|
|
178
|
+
MANAGED_BINDING_TARGET_LITERALS.has(member.slice(1, -1)),
|
|
179
|
+
)
|
|
180
|
+
|
|
80
181
|
type OutputKind = 'js' | 'dts' | 'node' | 'exe' | 'wasm'
|
|
81
182
|
type Output = { kind: OutputKind; path: string }
|
|
82
183
|
type WasiBindingMetadata = {
|
|
@@ -94,6 +195,70 @@ type ParsedBuildOptions = Omit<BuildOptions, 'cwd' | 'format'> & {
|
|
|
94
195
|
|
|
95
196
|
export const WASI_ARTIFACT_METADATA_PREFIX = '// napi-rs-artifact-metadata:'
|
|
96
197
|
|
|
198
|
+
// The host protocol a binding built with the `napi-async-runtime` crate
|
|
199
|
+
// exposes (contract version 4). See crates/async-runtime/README.md.
|
|
200
|
+
const ASYNC_RUNTIME_HOST_EXPORTS = [
|
|
201
|
+
'getCurrentThreadTaskHostContractVersion',
|
|
202
|
+
'isCurrentThreadHostRegistrationActive',
|
|
203
|
+
'registerCurrentThreadTaskHost',
|
|
204
|
+
'registerTimerHost',
|
|
205
|
+
'reserveCurrentThreadHostRegistration',
|
|
206
|
+
'unregisterCurrentThreadTaskHost',
|
|
207
|
+
'unregisterTimerHost',
|
|
208
|
+
] as const
|
|
209
|
+
|
|
210
|
+
export interface AsyncRuntimeHostContractInput {
|
|
211
|
+
/** The addon's export names, as reported by the type definition pass. */
|
|
212
|
+
idents: string[]
|
|
213
|
+
/** Whether `napi.wasm.asyncRuntime` is on. */
|
|
214
|
+
asyncRuntime: boolean
|
|
215
|
+
/**
|
|
216
|
+
* Whether `idents` is real export metadata. `napi-derive` without its
|
|
217
|
+
* `type-def` feature emits no type definitions, so the ident list is empty
|
|
218
|
+
* for every such addon — including one that does export the whole host
|
|
219
|
+
* contract. An empty list is only evidence when this is `true`.
|
|
220
|
+
*/
|
|
221
|
+
typeDefAvailable: boolean
|
|
222
|
+
packageName: string
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Cross-check `napi.wasm.asyncRuntime` against the addon's export list.
|
|
227
|
+
*
|
|
228
|
+
* Returns the message to throw (`error`) or to log (`warning`), if any. With
|
|
229
|
+
* no type-def metadata there is nothing to check against, so the build says so
|
|
230
|
+
* and defers to the loader's own runtime detection.
|
|
231
|
+
*/
|
|
232
|
+
export function checkAsyncRuntimeHostContract({
|
|
233
|
+
idents,
|
|
234
|
+
asyncRuntime,
|
|
235
|
+
typeDefAvailable,
|
|
236
|
+
packageName,
|
|
237
|
+
}: AsyncRuntimeHostContractInput): { error?: string; warning?: string } {
|
|
238
|
+
if (!typeDefAvailable) {
|
|
239
|
+
if (!asyncRuntime) {
|
|
240
|
+
return {}
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
warning: `napi.wasm.asyncRuntime is enabled but ${packageName} is built without the \`type-def\` feature of \`napi-derive\`, so \`napi build\` has no export list to check the napi-async-runtime host contract against. The generated loaders still verify the contract at load time and fail with ERR_NAPI_ASYNC_RUNTIME_BINDING_MISMATCH if the binding does not expose it.`,
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const missingHostExports = ASYNC_RUNTIME_HOST_EXPORTS.filter(
|
|
247
|
+
(name) => !idents.includes(name),
|
|
248
|
+
)
|
|
249
|
+
if (asyncRuntime && missingHostExports.length > 0) {
|
|
250
|
+
return {
|
|
251
|
+
error: `napi.wasm.asyncRuntime is enabled but ${packageName} does not export the napi-async-runtime host contract. Missing: ${missingHostExports.join(', ')}. Build the addon against the \`napi-async-runtime\` crate (see crates/async-runtime/README.md) or unset napi.wasm.asyncRuntime.`,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!asyncRuntime && missingHostExports.length === 0) {
|
|
255
|
+
return {
|
|
256
|
+
warning: `${packageName} exports the napi-async-runtime host contract but napi.wasm.asyncRuntime is not enabled. The generated WASI loaders will not install the CurrentThread task and timer hosts, so async exports will never make progress unless the host is installed by hand.`,
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return {}
|
|
260
|
+
}
|
|
261
|
+
|
|
97
262
|
type CargoConfigFingerprint = readonly [path: string, hash: string]
|
|
98
263
|
|
|
99
264
|
export function getCargoDependencyGraphFingerprint(
|
|
@@ -223,6 +388,83 @@ export function getTypeDefCacheFolder(options: {
|
|
|
223
388
|
return join(options.targetDir, 'napi-rs', `${options.crateName}-${hash}`)
|
|
224
389
|
}
|
|
225
390
|
|
|
391
|
+
/**
|
|
392
|
+
* Name of the emnapi archive directory built against the wasi-sdk 34 ABI.
|
|
393
|
+
* Only the threaded target has one: the signature change it carries lives in
|
|
394
|
+
* the threads-only futex code.
|
|
395
|
+
*/
|
|
396
|
+
export const EMNAPI_WASI_SDK_34_LINK_DIR = 'wasm32-wasip1-threads-wasi-sdk-34'
|
|
397
|
+
|
|
398
|
+
export interface EmnapiLinkDirSelection {
|
|
399
|
+
/** Directory name under `emnapi/lib` whose archives should be linked. */
|
|
400
|
+
linkDirName: string
|
|
401
|
+
/** Detected wasi-sdk major version, `null` when no wasi-sdk is configured. */
|
|
402
|
+
wasiSdkMajor: number | null
|
|
403
|
+
/** `true` when the toolchain needs the wasi-sdk 34 archives. */
|
|
404
|
+
needsWasiSdk34: boolean
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Pick the emnapi archive directory for a WASI build.
|
|
409
|
+
*
|
|
410
|
+
* wasi-libc removed the unused `int op` parameter from
|
|
411
|
+
* `__wasilibc_futex_wait_atomic_wait` / `__wasilibc_futex_wait_maybe_busy`,
|
|
412
|
+
* and wasi-sdk 34 is the first release shipping the 3-argument signature.
|
|
413
|
+
* Linking the legacy 4-argument archives against a wasi-sdk >= 34 sysroot
|
|
414
|
+
* makes `wasm-ld` report `function signature mismatch` and emit an invalid
|
|
415
|
+
* wasm module, so emnapi publishes a second archive set for the new ABI and
|
|
416
|
+
* the directory has to be chosen by wasi-sdk version, not by target triple.
|
|
417
|
+
*
|
|
418
|
+
* Without `WASI_SDK_PATH`, cargo links through `rust-lld` against the
|
|
419
|
+
* wasi-libc bundled with the Rust standard library, so that archive is
|
|
420
|
+
* probed instead. Rust picked up wasi-sdk 34 in rust-lang/rust#161773, which
|
|
421
|
+
* lands in 1.100, and a toolchain carrying it needs the new archives even
|
|
422
|
+
* though no wasi-sdk is configured.
|
|
423
|
+
*
|
|
424
|
+
* The legacy directory stays the default whenever the ABI cannot be
|
|
425
|
+
* determined, and older emnapi releases ship it alone.
|
|
426
|
+
*/
|
|
427
|
+
export function selectEmnapiLinkDir(
|
|
428
|
+
emnapiLibDir: string,
|
|
429
|
+
wasiTarget: string,
|
|
430
|
+
hasThreads: boolean,
|
|
431
|
+
wasiSdkPath: string | undefined,
|
|
432
|
+
{
|
|
433
|
+
cwd,
|
|
434
|
+
rustWasiLibc,
|
|
435
|
+
}: {
|
|
436
|
+
// The directory Cargo is spawned in. It selects the toolchain, so the
|
|
437
|
+
// sysroot probe has to use it too.
|
|
438
|
+
cwd?: string
|
|
439
|
+
// Omit to resolve the Rust sysroot lazily, so a configured wasi-sdk never
|
|
440
|
+
// pays for a `rustc` invocation. Pass `null` to skip the probe.
|
|
441
|
+
rustWasiLibc?: string | null
|
|
442
|
+
} = {},
|
|
443
|
+
): EmnapiLinkDirSelection {
|
|
444
|
+
const usingWasiSdk = Boolean(wasiSdkPath) && existsSync(wasiSdkPath!)
|
|
445
|
+
const wasiSdkMajor = usingWasiSdk ? wasiSdkMajorVersion(wasiSdkPath!) : null
|
|
446
|
+
// Whichever wasi-libc actually gets linked decides the ABI: the wasi-sdk
|
|
447
|
+
// sysroot when one is configured, otherwise the copy bundled with the Rust
|
|
448
|
+
// standard library. Only the threaded target has a second archive set, so
|
|
449
|
+
// `hasThreads` gates the probe as well as the result — a threadless build
|
|
450
|
+
// must not pay for a `rustc` invocation and an archive read it cannot use.
|
|
451
|
+
const needsWasiSdk34 =
|
|
452
|
+
hasThreads &&
|
|
453
|
+
(usingWasiSdk
|
|
454
|
+
? wasiSdkMajor !== null && wasiSdkMajor >= 34
|
|
455
|
+
: wasiLibcHasNewFutexAbi(
|
|
456
|
+
rustWasiLibc === undefined
|
|
457
|
+
? rustBundledWasiLibc(wasiTarget, cwd)
|
|
458
|
+
: rustWasiLibc,
|
|
459
|
+
) === true)
|
|
460
|
+
const linkDirName =
|
|
461
|
+
needsWasiSdk34 &&
|
|
462
|
+
existsSync(join(emnapiLibDir, EMNAPI_WASI_SDK_34_LINK_DIR))
|
|
463
|
+
? EMNAPI_WASI_SDK_34_LINK_DIR
|
|
464
|
+
: wasiTarget
|
|
465
|
+
return { linkDirName, wasiSdkMajor, needsWasiSdk34 }
|
|
466
|
+
}
|
|
467
|
+
|
|
226
468
|
export function createWasiCompilerFlags(
|
|
227
469
|
wasiSdkPath: string,
|
|
228
470
|
wasiTarget: string,
|
|
@@ -406,9 +648,11 @@ export function createWasiBrowserEntry(
|
|
|
406
648
|
export function createWasiDeferredBindingTypeDef(
|
|
407
649
|
bindingModuleSpecifier: string,
|
|
408
650
|
hasTypeDef: boolean,
|
|
651
|
+
platformArchABI?: string,
|
|
409
652
|
) {
|
|
410
653
|
const typeDef = createWasiDeferredBrowserBindingTypeDef(
|
|
411
654
|
bindingModuleSpecifier,
|
|
655
|
+
platformArchABI,
|
|
412
656
|
)
|
|
413
657
|
if (hasTypeDef) {
|
|
414
658
|
return typeDef
|
|
@@ -424,6 +668,133 @@ export function createWasiDeferredBindingTypeDef(
|
|
|
424
668
|
return typeDef.replace(rootBindingType, 'Record<string, unknown>')
|
|
425
669
|
}
|
|
426
670
|
|
|
671
|
+
/** memory32 tops out at 4 GiB, which is also `--max-memory` in `crates/build/src/wasi.rs`. */
|
|
672
|
+
const WASM32_MAX_MEMORY_PAGES = 65536
|
|
673
|
+
|
|
674
|
+
function validateWasmMemoryPages(
|
|
675
|
+
value: number | undefined,
|
|
676
|
+
key: string,
|
|
677
|
+
): number | undefined {
|
|
678
|
+
if (value === undefined) {
|
|
679
|
+
return undefined
|
|
680
|
+
}
|
|
681
|
+
if (
|
|
682
|
+
!Number.isInteger(value) ||
|
|
683
|
+
value < 1 ||
|
|
684
|
+
value > WASM32_MAX_MEMORY_PAGES
|
|
685
|
+
) {
|
|
686
|
+
throw new Error(
|
|
687
|
+
`${key} must be an integer between 1 and ${WASM32_MAX_MEMORY_PAGES} pages; received ${value}`,
|
|
688
|
+
)
|
|
689
|
+
}
|
|
690
|
+
return value
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export interface ResolvedWasmMemory {
|
|
694
|
+
/** `undefined` keeps each template's own default (4000, or 1024 for `./workerd`). */
|
|
695
|
+
initialMemory?: number
|
|
696
|
+
/** `undefined` keeps the template default (65536). */
|
|
697
|
+
maximumMemory?: number
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Resolve the `WebAssembly.Memory` descriptor the generated loaders embed, for
|
|
702
|
+
* one WASI flavor.
|
|
703
|
+
*
|
|
704
|
+
* The threaded loaders share one `shared: true` memory with every wasi-threads
|
|
705
|
+
* worker, so it is sized for the whole pool. The threadless loaders have a
|
|
706
|
+
* single thread and a plain growable `ArrayBuffer`, so they only need to clear
|
|
707
|
+
* the module's link-time floor (`-zstack-size` plus static data) and grow on
|
|
708
|
+
* demand. `napi.wasm.threadlessInitialMemory` sizes the threadless loaders
|
|
709
|
+
* without shrinking the threaded one; unset, it falls back to
|
|
710
|
+
* `napi.wasm.initialMemory` and nothing changes.
|
|
711
|
+
*/
|
|
712
|
+
export function resolveWasmMemory(
|
|
713
|
+
wasm: NapiConfig['wasm'],
|
|
714
|
+
hasThreads: boolean,
|
|
715
|
+
): ResolvedWasmMemory {
|
|
716
|
+
const initialMemory = validateWasmMemoryPages(
|
|
717
|
+
wasm?.initialMemory,
|
|
718
|
+
'napi.wasm.initialMemory',
|
|
719
|
+
)
|
|
720
|
+
const threadlessInitialMemory = validateWasmMemoryPages(
|
|
721
|
+
wasm?.threadlessInitialMemory,
|
|
722
|
+
'napi.wasm.threadlessInitialMemory',
|
|
723
|
+
)
|
|
724
|
+
const maximumMemory = validateWasmMemoryPages(
|
|
725
|
+
wasm?.maximumMemory,
|
|
726
|
+
'napi.wasm.maximumMemory',
|
|
727
|
+
)
|
|
728
|
+
const ceiling = maximumMemory ?? WASM32_MAX_MEMORY_PAGES
|
|
729
|
+
for (const [key, value] of [
|
|
730
|
+
['napi.wasm.initialMemory', initialMemory],
|
|
731
|
+
['napi.wasm.threadlessInitialMemory', threadlessInitialMemory],
|
|
732
|
+
] as const) {
|
|
733
|
+
if (value !== undefined && value > ceiling) {
|
|
734
|
+
throw new Error(
|
|
735
|
+
`${key} (${value} pages) must not exceed napi.wasm.maximumMemory (${ceiling} pages)`,
|
|
736
|
+
)
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
// The deferred `./workerd` loader is only emitted for threadless flavors
|
|
741
|
+
// (see `writeWasiBindingForTarget`), so it receives this same value.
|
|
742
|
+
initialMemory: hasThreads
|
|
743
|
+
? initialMemory
|
|
744
|
+
: (threadlessInitialMemory ?? initialMemory),
|
|
745
|
+
maximumMemory,
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Normalize a rendered `.d.ts` header. The generated body is concatenated
|
|
751
|
+
* straight onto it, so it always ends with a blank line — and an empty header
|
|
752
|
+
* stays empty, so a file with no header keeps no leading whitespace.
|
|
753
|
+
*/
|
|
754
|
+
function finalizeTypeDefHeader(header: string) {
|
|
755
|
+
let normalized = header
|
|
756
|
+
if (normalized && !normalized.endsWith('\n')) {
|
|
757
|
+
normalized += '\n'
|
|
758
|
+
}
|
|
759
|
+
if (normalized && !normalized.endsWith('\n\n')) {
|
|
760
|
+
normalized += '\n'
|
|
761
|
+
}
|
|
762
|
+
return normalized
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Whether a build's declaration file should declare `__napiBindingTarget`.
|
|
767
|
+
*
|
|
768
|
+
* The export exists only on a generated loader, so the declaration has to
|
|
769
|
+
* follow the loaders a build actually writes. This mirrors both writers:
|
|
770
|
+
*
|
|
771
|
+
* - {@link writeJsBinding} writes the root loader for `--platform` without
|
|
772
|
+
* `--no-js-binding` (`rootLoaderCandidate`), and then only when the build has
|
|
773
|
+
* runtime exports or a WASI flavor to fall back to.
|
|
774
|
+
* - `Builder.writeWasiBinding` writes a flavor's loader set when that flavor is
|
|
775
|
+
* the build target, or when an earlier build of it left metadata on disk
|
|
776
|
+
* (`emitsWasiLoader`). A merely configured flavor is skipped, and a build
|
|
777
|
+
* that skips every flavor emits no loader to carry the export.
|
|
778
|
+
*
|
|
779
|
+
* The export list is only known once typegen has run, and typegen writes the
|
|
780
|
+
* declaration file itself, so the decision comes back as a predicate over that
|
|
781
|
+
* list instead of a flag taken up front.
|
|
782
|
+
*
|
|
783
|
+
* It gates the *reservation* of the name too — `Builder.generateTypeDef` calls
|
|
784
|
+
* {@link assertBindingTargetIdentFree} only when this answers `true`. Declaring
|
|
785
|
+
* the name and forbidding an addon from exporting it are the same question, and
|
|
786
|
+
* a build that emits no loader asks neither.
|
|
787
|
+
*/
|
|
788
|
+
export function bindingTargetDeclarationPredicate(input: {
|
|
789
|
+
rootLoaderCandidate: boolean
|
|
790
|
+
hasWasiFallback: boolean
|
|
791
|
+
emitsWasiLoader: boolean
|
|
792
|
+
}): (exports: readonly string[]) => boolean {
|
|
793
|
+
return (exports) =>
|
|
794
|
+
input.emitsWasiLoader ||
|
|
795
|
+
(input.rootLoaderCandidate && (input.hasWasiFallback || exports.length > 0))
|
|
796
|
+
}
|
|
797
|
+
|
|
427
798
|
export function prepareWasiBindingTypeDef(
|
|
428
799
|
source: string,
|
|
429
800
|
sourcePath: string,
|
|
@@ -447,6 +818,136 @@ export function prepareWasiBindingTypeDef(
|
|
|
447
818
|
return rebaseDeclarationSpecifiers(targetSource, sourcePath, destinationPath)
|
|
448
819
|
}
|
|
449
820
|
|
|
821
|
+
/**
|
|
822
|
+
* Make a declaration file declare the `__napiBindingTarget` its loader exports.
|
|
823
|
+
*
|
|
824
|
+
* `platformArchABI` names the WASI flavor the file types, and the declaration
|
|
825
|
+
* is then that flavor's exact literal; without it the file is the root entry's
|
|
826
|
+
* and keeps the full union, which only the root entry can reach.
|
|
827
|
+
*
|
|
828
|
+
* Two kinds of file arrive here. A WASI declaration derived fresh from the root
|
|
829
|
+
* `index.d.ts` inherits that union and has to be narrowed. A declaration kept
|
|
830
|
+
* from an earlier build — a build that does not target WASI regenerates every
|
|
831
|
+
* declared flavor's loader from the metadata that flavor's own build left
|
|
832
|
+
* behind, and reuses its declaration file verbatim — may predate the export
|
|
833
|
+
* entirely, or carry a union an earlier version of this branch wrote.
|
|
834
|
+
*
|
|
835
|
+
* Either may also carry a declaration nobody here wrote, because a project's
|
|
836
|
+
* own `--dts-header` may declare the export to work around loaders that predate
|
|
837
|
+
* it. So ownership, not mere presence, decides what happens.
|
|
838
|
+
*
|
|
839
|
+
* Three cases, decided in this order:
|
|
840
|
+
*
|
|
841
|
+
* - The declaration the rendered `.d.ts` header owns. `renderedHeader` is that
|
|
842
|
+
* header as this build rendered it; if it declares the export, the project's
|
|
843
|
+
* own declaration wins: nothing is appended, and it is never rewritten.
|
|
844
|
+
* Declaring it a second time would be a TS2451 redeclaration.
|
|
845
|
+
*
|
|
846
|
+
* The header is matched by the text of its declaration against the first
|
|
847
|
+
* declaration in the file, not by a prefix of the file. A WASI declaration is
|
|
848
|
+
* derived through {@link prepareWasiBindingTypeDef}, which rewrites relative
|
|
849
|
+
* import specifiers and, for the threadless flavor, drops `node:stream/web`
|
|
850
|
+
* imports — a header carrying either stops being a literal prefix of the file
|
|
851
|
+
* derived from it, while the declaration statement itself comes through
|
|
852
|
+
* untouched and stays the first one, because those rewrites never reorder
|
|
853
|
+
* statements. On the preserved path the header in hand is this build's, not
|
|
854
|
+
* the one the kept file was written with, and a kept declaration that reads
|
|
855
|
+
* differently is correctly not the header's.
|
|
856
|
+
* - A declaration the header does not own, alone in its statement, whose type
|
|
857
|
+
* is built only out of the literals this CLI writes
|
|
858
|
+
* ({@link isGeneratedBindingTargetType}), is one of ours. Stale — an older
|
|
859
|
+
* union, or the other flavor's literal — it is replaced in place, comment
|
|
860
|
+
* and all, so a file kept from an earlier build ends up saying what the
|
|
861
|
+
* loader written beside it now reports.
|
|
862
|
+
* - Any other export of the name is someone else's and is preserved, with
|
|
863
|
+
* nothing appended beside it. A looser match would rewrite a declaration a
|
|
864
|
+
* `--dts-header` contributed. That covers a statement declaring more names
|
|
865
|
+
* than this one — the block a refresh replaces spans them too, and this CLI
|
|
866
|
+
* writes the declaration alone — and it covers every other way a header can
|
|
867
|
+
* claim the name: as a `function`, `class`, `enum` or instantiated
|
|
868
|
+
* `namespace`, through an `export { target as __napiBindingTarget }` or
|
|
869
|
+
* `export * as __napiBindingTarget from '…'` clause, or through
|
|
870
|
+
* `export import __napiBindingTarget = …`. None of those leaves a
|
|
871
|
+
* declaration here to rewrite, and an export added beside any of them is a
|
|
872
|
+
* TypeScript error (TS2300, TS2323, TS2440 or TS2567 by form).
|
|
873
|
+
*
|
|
874
|
+
* A header that exports the name in type space only — a `type`, an
|
|
875
|
+
* `interface`, a namespace that declares no value — claims nothing: the
|
|
876
|
+
* declaration merges with it, and withholding it would cost a consumer the
|
|
877
|
+
* value the loader really exports — see {@link scanExportedName}.
|
|
878
|
+
*
|
|
879
|
+
* A declaration file that exports by assignment (`export = binding`, what a
|
|
880
|
+
* build without `napi-derive`'s `type-def` feature emits) cannot carry a named
|
|
881
|
+
* export declaration, so it is left alone; its exports are untyped anyway.
|
|
882
|
+
* That, too, is read off the parse — a block comment documenting the CommonJS
|
|
883
|
+
* form is not an export assignment.
|
|
884
|
+
*/
|
|
885
|
+
export function ensureBindingTargetDeclaration(
|
|
886
|
+
typeDef: string,
|
|
887
|
+
platformArchABI?: string,
|
|
888
|
+
renderedHeader?: string,
|
|
889
|
+
) {
|
|
890
|
+
const { exportsByAssignment, ownsName, declarations } =
|
|
891
|
+
bindingTargetExports(typeDef)
|
|
892
|
+
if (exportsByAssignment) {
|
|
893
|
+
return typeDef
|
|
894
|
+
}
|
|
895
|
+
const declaration = platformArchABI
|
|
896
|
+
? createBindingTargetFlavorDeclaration(platformArchABI)
|
|
897
|
+
: BINDING_TARGET_TYPE_DECLARATION
|
|
898
|
+
if (!ownsName) {
|
|
899
|
+
const separator = typeDef.length === 0 || typeDef.endsWith('\n') ? '' : '\n'
|
|
900
|
+
return `${typeDef}${separator}${declaration}`
|
|
901
|
+
}
|
|
902
|
+
// Compared declarator by declarator rather than statement by statement: one
|
|
903
|
+
// statement may declare more names, and rebasing a relative inline import in
|
|
904
|
+
// a sibling's type rewrites the statement without touching this declarator.
|
|
905
|
+
const headerDeclarator = renderedHeader
|
|
906
|
+
? bindingTargetExports(renderedHeader).declarations.map((found) =>
|
|
907
|
+
renderedHeader.slice(found.declaratorStart, found.declaratorEnd),
|
|
908
|
+
)[0]
|
|
909
|
+
: undefined
|
|
910
|
+
const headerOwnsFirst =
|
|
911
|
+
headerDeclarator !== undefined &&
|
|
912
|
+
declarations.length > 0 &&
|
|
913
|
+
typeDef.slice(
|
|
914
|
+
declarations[0].declaratorStart,
|
|
915
|
+
declarations[0].declaratorEnd,
|
|
916
|
+
) === headerDeclarator
|
|
917
|
+
for (const [index, found] of declarations.entries()) {
|
|
918
|
+
if (headerOwnsFirst && index === 0) {
|
|
919
|
+
continue
|
|
920
|
+
}
|
|
921
|
+
if (
|
|
922
|
+
// A statement that declares more than this one name belongs to whoever
|
|
923
|
+
// wrote it whatever its type says: the block a refresh replaces covers
|
|
924
|
+
// those names too, and this CLI has never written one, so a declaration
|
|
925
|
+
// with siblings is not a declaration of ours to refresh.
|
|
926
|
+
found.declaratorCount > 1 ||
|
|
927
|
+
found.type === undefined ||
|
|
928
|
+
!isGeneratedBindingTargetType(found.type)
|
|
929
|
+
) {
|
|
930
|
+
continue
|
|
931
|
+
}
|
|
932
|
+
// Replacing the block instead of appending: a file that carries a union,
|
|
933
|
+
// or the other flavor's literal, has to be narrowed to the flavor it
|
|
934
|
+
// types rather than gaining a second, conflicting declaration. The span
|
|
935
|
+
// covers the doc comment above the statement, so the comment is swapped
|
|
936
|
+
// with it rather than stranded — and the statement's own `;`, which has to
|
|
937
|
+
// come back, or a statement that followed on the same line runs into the
|
|
938
|
+
// replacement (TS1005).
|
|
939
|
+
const terminator =
|
|
940
|
+
typeDef.slice(found.end - 1, found.end) === ';' ? ';' : ''
|
|
941
|
+
return (
|
|
942
|
+
typeDef.slice(0, found.start) +
|
|
943
|
+
declaration.trim() +
|
|
944
|
+
terminator +
|
|
945
|
+
typeDef.slice(found.end)
|
|
946
|
+
)
|
|
947
|
+
}
|
|
948
|
+
return typeDef
|
|
949
|
+
}
|
|
950
|
+
|
|
450
951
|
export function collectStaleWasiBuildOutputNames(
|
|
451
952
|
binaryName: string,
|
|
452
953
|
buildTarget: Target,
|
|
@@ -975,6 +1476,12 @@ class Builder {
|
|
|
975
1476
|
private readonly enableTypeDef: boolean = false
|
|
976
1477
|
private readonly stagedOutputDestinations = new Map<string, string>()
|
|
977
1478
|
private typeDefWithTypeImports: string | undefined
|
|
1479
|
+
/**
|
|
1480
|
+
* The `.d.ts` header this build rendered, so `writeWasiBindingForTarget` can
|
|
1481
|
+
* tell a `__napiBindingTarget` declaration the header owns from one typegen
|
|
1482
|
+
* wrote. `undefined` until typegen runs, and for a build with no typegen.
|
|
1483
|
+
*/
|
|
1484
|
+
private typeDefRenderedHeader: string | undefined
|
|
978
1485
|
|
|
979
1486
|
constructor(
|
|
980
1487
|
private readonly metadata: CargoWorkspaceMetadata,
|
|
@@ -1234,6 +1741,31 @@ class Builder {
|
|
|
1234
1741
|
return entries
|
|
1235
1742
|
}
|
|
1236
1743
|
|
|
1744
|
+
/**
|
|
1745
|
+
* Whether {@link Builder.writeWasiBinding} will actually write a WASI loader
|
|
1746
|
+
* set. A declared WASI target is not enough: a non-WASI build only
|
|
1747
|
+
* regenerates the flavors whose previous loader metadata is still on disk and
|
|
1748
|
+
* skips the rest, so a configured-but-never-built flavor leaves no loader to
|
|
1749
|
+
* carry `__napiBindingTarget`.
|
|
1750
|
+
*
|
|
1751
|
+
* Safe to call from {@link Builder.generateTypeDef}: the metadata probe reads
|
|
1752
|
+
* `finalOutputDir`, which the staging swap never touches, and nothing writes
|
|
1753
|
+
* those files between this probe and `writeWasiBinding`.
|
|
1754
|
+
*/
|
|
1755
|
+
private async willEmitWasiLoader() {
|
|
1756
|
+
if (this.target.platform === 'wasi') {
|
|
1757
|
+
return true
|
|
1758
|
+
}
|
|
1759
|
+
for (const wasiTarget of this.config.targets.filter(
|
|
1760
|
+
(target) => target.platform === 'wasi',
|
|
1761
|
+
)) {
|
|
1762
|
+
if (await this.readExistingWasiBindingMetadata(wasiTarget)) {
|
|
1763
|
+
return true
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
return false
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1237
1769
|
private async readExistingWasiBindingMetadata(wasiTarget: Target) {
|
|
1238
1770
|
const loaderSuffix = wasiLoaderSuffix(wasiTarget.platformArchABI)
|
|
1239
1771
|
const bindingPath = join(
|
|
@@ -1461,16 +1993,40 @@ class Builder {
|
|
|
1461
1993
|
private setWasiEnv() {
|
|
1462
1994
|
const hasThreads = wasiTargetHasThreads(this.target)
|
|
1463
1995
|
const wasiTarget = hasThreads ? 'wasm32-wasip1-threads' : 'wasm32-wasip1'
|
|
1464
|
-
const
|
|
1996
|
+
const emnapiLibDir = join(require.resolve('emnapi'), '..', 'lib')
|
|
1465
1997
|
const emnapiVersion = require('emnapi/package.json').version
|
|
1998
|
+
const { WASI_SDK_PATH } = process.env
|
|
1999
|
+
// `wasiTarget` stays the real target triple, because it is what the clang
|
|
2000
|
+
// driver is told to build for below. The emnapi archive directory is a
|
|
2001
|
+
// separate choice: emnapi ships two archive sets for the threaded target,
|
|
2002
|
+
// one per wasi-libc futex ABI. See `selectEmnapiLinkDir`.
|
|
2003
|
+
const { linkDirName, wasiSdkMajor, needsWasiSdk34 } = selectEmnapiLinkDir(
|
|
2004
|
+
emnapiLibDir,
|
|
2005
|
+
wasiTarget,
|
|
2006
|
+
hasThreads,
|
|
2007
|
+
WASI_SDK_PATH,
|
|
2008
|
+
{ cwd: this.options.cwd },
|
|
2009
|
+
)
|
|
2010
|
+
const emnapi = join(emnapiLibDir, linkDirName)
|
|
1466
2011
|
// Keep this in sync with `emnapi_link_library` in `crates/build/src/wasi.rs`.
|
|
1467
2012
|
const emnapiArchive = join(
|
|
1468
2013
|
emnapi,
|
|
1469
2014
|
hasThreads ? 'libemnapi-napi-rs-mt.a' : 'libemnapi-basic-napi-rs.a',
|
|
1470
2015
|
)
|
|
2016
|
+
const fellBackToLegacy =
|
|
2017
|
+
needsWasiSdk34 && linkDirName !== EMNAPI_WASI_SDK_34_LINK_DIR
|
|
1471
2018
|
if (!existsSync(emnapiArchive)) {
|
|
1472
2019
|
throw new Error(
|
|
1473
|
-
|
|
2020
|
+
fellBackToLegacy
|
|
2021
|
+
? `emnapi@${emnapiVersion} does not ship the ${EMNAPI_WASI_SDK_34_LINK_DIR} archives that wasi-sdk ${wasiSdkMajor} requires, and the ${linkDirName} archive it fell back to is missing too at ${emnapiArchive}. Upgrade emnapi to a version that ships the wasi-sdk 34 archives.`
|
|
2022
|
+
: needsWasiSdk34
|
|
2023
|
+
? `emnapi@${emnapiVersion} is missing the ${linkDirName} archive required by napi-rs at ${emnapiArchive}. wasi-sdk ${wasiSdkMajor} needs those archives, so upgrade emnapi to a version that ships them.`
|
|
2024
|
+
: `emnapi@${emnapiVersion} is missing the ${linkDirName} archive required by napi-rs at ${emnapiArchive}. Install emnapi v2 with support for this target.`,
|
|
2025
|
+
)
|
|
2026
|
+
}
|
|
2027
|
+
if (fellBackToLegacy) {
|
|
2028
|
+
debug.warn(
|
|
2029
|
+
`emnapi@${emnapiVersion} does not ship the ${EMNAPI_WASI_SDK_34_LINK_DIR} archives that wasi-sdk ${wasiSdkMajor} requires. Falling back to ${linkDirName}, which links the legacy wasi-libc futex ABI and may fail with \`wasm-ld: function signature mismatch\`. Upgrade emnapi to fix this.`,
|
|
1474
2030
|
)
|
|
1475
2031
|
}
|
|
1476
2032
|
this.envs.EMNAPI_LINK_DIR = emnapi
|
|
@@ -1499,7 +2055,6 @@ class Builder {
|
|
|
1499
2055
|
`emnapi version mismatch: emnapi@${emnapiVersion}, @emnapi/core@${emnapiCoreVersion}, @emnapi/runtime@${emnapiRuntimeVersion}. Please ensure all emnapi packages are the same version.`,
|
|
1500
2056
|
)
|
|
1501
2057
|
}
|
|
1502
|
-
const { WASI_SDK_PATH } = process.env
|
|
1503
2058
|
|
|
1504
2059
|
if (WASI_SDK_PATH && existsSync(WASI_SDK_PATH)) {
|
|
1505
2060
|
this.envs.CARGO_TARGET_WASM32_WASI_PREVIEW1_THREADS_LINKER = join(
|
|
@@ -1966,7 +2521,20 @@ class Builder {
|
|
|
1966
2521
|
return []
|
|
1967
2522
|
}
|
|
1968
2523
|
|
|
1969
|
-
|
|
2524
|
+
// Declare the loader export only for builds that actually emit a loader:
|
|
2525
|
+
// the native root loader (`--platform` without `--no-js-binding`) or a WASI
|
|
2526
|
+
// flavor loader set this build writes. Whether the root loader is written
|
|
2527
|
+
// also depends on the exports typegen is about to produce, so the decision
|
|
2528
|
+
// travels as a predicate — see `bindingTargetDeclarationPredicate`.
|
|
2529
|
+
const declareBindingTarget = bindingTargetDeclarationPredicate({
|
|
2530
|
+
rootLoaderCandidate:
|
|
2531
|
+
Boolean(this.options.platform) && !this.options.noJsBinding,
|
|
2532
|
+
// the same fallback list `writeJsBinding` is handed
|
|
2533
|
+
hasWasiFallback: this.declaredWasiFlavors().length > 0,
|
|
2534
|
+
emitsWasiLoader: await this.willEmitWasiLoader(),
|
|
2535
|
+
})
|
|
2536
|
+
|
|
2537
|
+
const { exports, dts, dtsWithTypeImports, header } = await generateTypeDef({
|
|
1970
2538
|
typeDefDir,
|
|
1971
2539
|
noDtsHeader: this.options.noDtsHeader,
|
|
1972
2540
|
dtsHeader: this.options.dtsHeader,
|
|
@@ -1976,8 +2544,18 @@ class Builder {
|
|
|
1976
2544
|
runtimeStringEnum:
|
|
1977
2545
|
this.options.runtimeStringEnum ?? this.config.runtimeStringEnum,
|
|
1978
2546
|
cwd: this.options.cwd,
|
|
2547
|
+
declareBindingTarget,
|
|
1979
2548
|
})
|
|
2549
|
+
// The name is reserved exactly when a loader reports it and this file
|
|
2550
|
+
// declares it — one condition, so ask the one predicate. A build that emits
|
|
2551
|
+
// no loader (no `--platform`, or `--no-js`, with no WASI loader set
|
|
2552
|
+
// regenerated) declares nothing, and must keep accepting an addon export of
|
|
2553
|
+
// this name the way every release before the export did.
|
|
2554
|
+
if (declareBindingTarget(exports)) {
|
|
2555
|
+
assertBindingTargetIdentFree(exports)
|
|
2556
|
+
}
|
|
1980
2557
|
this.typeDefWithTypeImports = dtsWithTypeImports
|
|
2558
|
+
this.typeDefRenderedHeader = header
|
|
1981
2559
|
|
|
1982
2560
|
const typeDefRelativePath = this.options.dts ?? 'index.d.ts'
|
|
1983
2561
|
const finalDest = join(this.finalOutputDir, typeDefRelativePath)
|
|
@@ -1993,7 +2571,13 @@ class Builder {
|
|
|
1993
2571
|
throw new Error(`Failed to write type def file ${dest}`, { cause: e })
|
|
1994
2572
|
}
|
|
1995
2573
|
|
|
1996
|
-
|
|
2574
|
+
// The file is written unconditionally above, so track it whenever this
|
|
2575
|
+
// build filled it. Runtime exports are one reason it has content; the
|
|
2576
|
+
// `__napiBindingTarget` declaration is another, and a crate that registers
|
|
2577
|
+
// everything from a `#[napi(module_exports)]` hook has only the second.
|
|
2578
|
+
// `--pipe` (`cli/src/commands/build.ts`) and the `NapiCli.build` return
|
|
2579
|
+
// value see registered outputs only.
|
|
2580
|
+
if (dts.length > 0) {
|
|
1997
2581
|
this.outputs.push({ kind: 'dts', path: dest })
|
|
1998
2582
|
}
|
|
1999
2583
|
|
|
@@ -2021,10 +2605,13 @@ class Builder {
|
|
|
2021
2605
|
return stagedPath
|
|
2022
2606
|
}
|
|
2023
2607
|
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2608
|
+
/**
|
|
2609
|
+
* `platformArchABI`s of every WASI flavor this build's loaders can reference,
|
|
2610
|
+
* in fallback preference order (threaded first). The generated root loader
|
|
2611
|
+
* also lets consumers pin one exact declared flavor with
|
|
2612
|
+
* NAPI_RS_WASI_FLAVOR.
|
|
2613
|
+
*/
|
|
2614
|
+
private declaredWasiFlavors(): string[] {
|
|
2028
2615
|
const declaredWasiTargets = this.config.targets.filter(
|
|
2029
2616
|
(t) => t.platform === 'wasi',
|
|
2030
2617
|
)
|
|
@@ -2040,7 +2627,7 @@ class Builder {
|
|
|
2040
2627
|
) {
|
|
2041
2628
|
declaredWasiTargets.push(this.target)
|
|
2042
2629
|
}
|
|
2043
|
-
|
|
2630
|
+
return [
|
|
2044
2631
|
...new Set(
|
|
2045
2632
|
[
|
|
2046
2633
|
...declaredWasiTargets.filter(wasiTargetHasThreads),
|
|
@@ -2048,6 +2635,13 @@ class Builder {
|
|
|
2048
2635
|
].map((t) => t.platformArchABI),
|
|
2049
2636
|
),
|
|
2050
2637
|
]
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
private async writeJsBinding(idents: string[]) {
|
|
2641
|
+
// No reserved-name check here: it belongs to the decision that emits the
|
|
2642
|
+
// declaration, which lives in `generateTypeDef` above. `writeJsBinding`
|
|
2643
|
+
// runs for every cdylib build, including the ones that write no loader.
|
|
2644
|
+
const wasiFlavors = this.declaredWasiFlavors()
|
|
2051
2645
|
return writeJsBinding({
|
|
2052
2646
|
platform: this.options.platform,
|
|
2053
2647
|
noJsBinding: this.options.noJsBinding,
|
|
@@ -2150,7 +2744,24 @@ class Builder {
|
|
|
2150
2744
|
metadata: WasiBindingMetadata,
|
|
2151
2745
|
): Promise<Output[]> {
|
|
2152
2746
|
const { exports: idents } = metadata
|
|
2747
|
+
const asyncRuntime = this.config.wasm?.asyncRuntime === true
|
|
2748
|
+
const hostContract = checkAsyncRuntimeHostContract({
|
|
2749
|
+
idents,
|
|
2750
|
+
asyncRuntime,
|
|
2751
|
+
typeDefAvailable: this.enableTypeDef,
|
|
2752
|
+
packageName: this.config.packageName,
|
|
2753
|
+
})
|
|
2754
|
+
if (hostContract.error) {
|
|
2755
|
+
throw new Error(hostContract.error)
|
|
2756
|
+
}
|
|
2757
|
+
if (hostContract.warning) {
|
|
2758
|
+
debug.warn(hostContract.warning)
|
|
2759
|
+
}
|
|
2153
2760
|
const hasThreads = wasiTargetHasThreads(wasiTarget)
|
|
2761
|
+
const { initialMemory, maximumMemory } = resolveWasmMemory(
|
|
2762
|
+
this.config.wasm,
|
|
2763
|
+
hasThreads,
|
|
2764
|
+
)
|
|
2154
2765
|
const loaderSuffix = wasiLoaderSuffix(wasiTarget.platformArchABI)
|
|
2155
2766
|
// the wasm file stem referenced from inside the loaders
|
|
2156
2767
|
const name = `${this.config.binaryName}.${wasiTarget.platformArchABI}`
|
|
@@ -2166,13 +2777,23 @@ class Builder {
|
|
|
2166
2777
|
dir,
|
|
2167
2778
|
`${this.config.binaryName}.${loaderSuffix}.d.cts`,
|
|
2168
2779
|
)
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2780
|
+
// A flavor's own export list, not the root type-def's: this is reached only
|
|
2781
|
+
// once `writeWasiBinding` has metadata for the flavor, i.e. only when this
|
|
2782
|
+
// loader set really is written. The root-side check lives in
|
|
2783
|
+
// `Builder.generateTypeDef`, gated on the same predicate as the
|
|
2784
|
+
// declaration — do not re-add an unconditional one here or there.
|
|
2785
|
+
assertBindingTargetIdentFree(idents)
|
|
2786
|
+
// No stamp here. `createWasiBinding` emits the only one, inside the
|
|
2787
|
+
// initialization `try` that rolls the environment back — both the guard and
|
|
2788
|
+
// the assignment can throw, and neither may escape past that boundary. The
|
|
2789
|
+
// assignment there is what `cjs-module-lexer` reads, so this tail stays
|
|
2790
|
+
// purely declarative.
|
|
2791
|
+
const exportsCode = [
|
|
2792
|
+
`module.exports = __napiModule.exports`,
|
|
2793
|
+
...idents.map(
|
|
2794
|
+
(ident) => `module.exports.${ident} = __napiModule.exports.${ident}`,
|
|
2795
|
+
),
|
|
2796
|
+
].join('\n')
|
|
2176
2797
|
await writeFileAtomic(
|
|
2177
2798
|
bindingPath,
|
|
2178
2799
|
createWasiArtifactMetadata(
|
|
@@ -2185,11 +2806,12 @@ class Builder {
|
|
|
2185
2806
|
createWasiBinding(
|
|
2186
2807
|
name,
|
|
2187
2808
|
this.config.packageName,
|
|
2188
|
-
|
|
2189
|
-
|
|
2809
|
+
initialMemory,
|
|
2810
|
+
maximumMemory,
|
|
2190
2811
|
hasThreads,
|
|
2191
2812
|
wasiTarget.platformArchABI,
|
|
2192
2813
|
`${this.config.binaryName}.${wasiTarget.platformArchABI}`,
|
|
2814
|
+
asyncRuntime,
|
|
2193
2815
|
) +
|
|
2194
2816
|
exportsCode +
|
|
2195
2817
|
'\n',
|
|
@@ -2199,13 +2821,15 @@ class Builder {
|
|
|
2199
2821
|
browserBindingPath,
|
|
2200
2822
|
createWasiBrowserBinding(
|
|
2201
2823
|
name,
|
|
2202
|
-
|
|
2203
|
-
|
|
2824
|
+
initialMemory,
|
|
2825
|
+
maximumMemory,
|
|
2204
2826
|
this.config.wasm?.browser?.fs,
|
|
2205
2827
|
this.config.wasm?.browser?.asyncInit,
|
|
2206
2828
|
this.config.wasm?.browser?.buffer,
|
|
2207
2829
|
this.config.wasm?.browser?.errorEvent,
|
|
2208
2830
|
hasThreads,
|
|
2831
|
+
wasiTarget.platformArchABI,
|
|
2832
|
+
asyncRuntime,
|
|
2209
2833
|
) +
|
|
2210
2834
|
`export default __napiModule.exports\n` +
|
|
2211
2835
|
idents
|
|
@@ -2252,6 +2876,17 @@ export = binding
|
|
|
2252
2876
|
? selectedSourceTypeDef
|
|
2253
2877
|
: removeNodeStreamWebTypeImports(selectedSourceTypeDef)
|
|
2254
2878
|
}
|
|
2879
|
+
// This flavor's loaders report a compile-time-fixed identity, so this file
|
|
2880
|
+
// declares that one literal: the root entry's union exists only for
|
|
2881
|
+
// `NAPI_RS_NATIVE_LIBRARY_PATH`, which no flavor loader reads. On the fresh
|
|
2882
|
+
// path that narrows the union inherited from the root `index.d.ts`; on the
|
|
2883
|
+
// preserved path it refreshes a file kept from an earlier build, which may
|
|
2884
|
+
// predate the `__napiBindingTarget` export the loader written above carries.
|
|
2885
|
+
bindingTypeDef = ensureBindingTargetDeclaration(
|
|
2886
|
+
bindingTypeDef,
|
|
2887
|
+
wasiTarget.platformArchABI,
|
|
2888
|
+
this.typeDefRenderedHeader,
|
|
2889
|
+
)
|
|
2255
2890
|
await writeFileAtomic(bindingTypeDefPath, bindingTypeDef, 'utf8')
|
|
2256
2891
|
const outputs: Output[] = [
|
|
2257
2892
|
{ kind: 'js', path: bindingPath },
|
|
@@ -2289,9 +2924,11 @@ export = binding
|
|
|
2289
2924
|
deferredBindingPath,
|
|
2290
2925
|
createWasiDeferredBrowserBinding(
|
|
2291
2926
|
name,
|
|
2292
|
-
|
|
2293
|
-
|
|
2927
|
+
initialMemory,
|
|
2928
|
+
maximumMemory,
|
|
2294
2929
|
this.config.wasm?.browser?.buffer,
|
|
2930
|
+
wasiTarget.platformArchABI,
|
|
2931
|
+
asyncRuntime,
|
|
2295
2932
|
),
|
|
2296
2933
|
'utf8',
|
|
2297
2934
|
)
|
|
@@ -2300,6 +2937,7 @@ export = binding
|
|
|
2300
2937
|
createWasiDeferredBindingTypeDef(
|
|
2301
2938
|
`./${this.config.binaryName}.${loaderSuffix}.cjs`,
|
|
2302
2939
|
this.enableTypeDef,
|
|
2940
|
+
wasiTarget.platformArchABI,
|
|
2303
2941
|
),
|
|
2304
2942
|
'utf8',
|
|
2305
2943
|
)
|
|
@@ -2475,6 +3113,18 @@ export interface GenerateTypeDefOptions {
|
|
|
2475
3113
|
constEnum?: boolean
|
|
2476
3114
|
runtimeStringEnum?: boolean
|
|
2477
3115
|
cwd: string
|
|
3116
|
+
/**
|
|
3117
|
+
* Declare the generated loader's `__napiBindingTarget` export. Set it for
|
|
3118
|
+
* builds that emit a loader; a build that emits none declares nothing. The
|
|
3119
|
+
* declared union covers every artifact a loader can hand back, not only the
|
|
3120
|
+
* targets this package builds — see {@link BINDING_TARGET_TYPE_UNION}.
|
|
3121
|
+
*
|
|
3122
|
+
* Whether the root loader is written depends on the exports this build
|
|
3123
|
+
* produced, which only typegen knows, so a predicate may be passed instead
|
|
3124
|
+
* of a flag; it is called with the generated export list. See
|
|
3125
|
+
* {@link bindingTargetDeclarationPredicate}.
|
|
3126
|
+
*/
|
|
3127
|
+
declareBindingTarget?: boolean | ((exports: readonly string[]) => boolean)
|
|
2478
3128
|
}
|
|
2479
3129
|
|
|
2480
3130
|
/**
|
|
@@ -2489,15 +3139,22 @@ export async function generateTypeDef(
|
|
|
2489
3139
|
exports: string[]
|
|
2490
3140
|
dts: string
|
|
2491
3141
|
dtsWithTypeImports: string
|
|
3142
|
+
/**
|
|
3143
|
+
* The `.d.ts` header as rendered, before anything the generated body needs
|
|
3144
|
+
* was appended to it. `ensureBindingTargetDeclaration` takes it to tell a
|
|
3145
|
+
* declaration the header owns from one this build wrote.
|
|
3146
|
+
*/
|
|
3147
|
+
header: string
|
|
2492
3148
|
}> {
|
|
2493
|
-
if (!(await dirExistsAsync(options.typeDefDir))) {
|
|
2494
|
-
return { exports: [], dts: '', dtsWithTypeImports: '' }
|
|
2495
|
-
}
|
|
2496
|
-
|
|
2497
3149
|
let header = ''
|
|
2498
3150
|
let dts = ''
|
|
2499
3151
|
let exports: string[] = []
|
|
2500
3152
|
|
|
3153
|
+
const declaresBindingTarget = (generated: readonly string[]) =>
|
|
3154
|
+
typeof options.declareBindingTarget === 'function'
|
|
3155
|
+
? options.declareBindingTarget(generated)
|
|
3156
|
+
: Boolean(options.declareBindingTarget)
|
|
3157
|
+
|
|
2501
3158
|
if (!options.noDtsHeader) {
|
|
2502
3159
|
const dtsHeader = options.dtsHeader ?? options.configDtsHeader
|
|
2503
3160
|
const dtsHeaderFile = options.dtsHeaderFile ?? options.configDtsHeaderFile
|
|
@@ -2516,11 +3173,63 @@ export async function generateTypeDef(
|
|
|
2516
3173
|
}
|
|
2517
3174
|
}
|
|
2518
3175
|
|
|
3176
|
+
// The header exactly as the project wrote it, captured before the appends
|
|
3177
|
+
// below grow it, so `ensureBindingTargetDeclaration` can tell a declaration
|
|
3178
|
+
// the header owns from one this build wrote into a file derived from this
|
|
3179
|
+
// one.
|
|
3180
|
+
const renderedHeader = header
|
|
3181
|
+
const headerBindingTarget = bindingTargetExports(renderedHeader)
|
|
3182
|
+
// That header may declare `__napiBindingTarget` itself, as a workaround for
|
|
3183
|
+
// loaders that predate the export. Declaring it again below would be a TS2451
|
|
3184
|
+
// redeclaration, so the header's own wins — the rule
|
|
3185
|
+
// `ensureBindingTargetDeclaration` already applies to a preserved WASI
|
|
3186
|
+
// declaration.
|
|
3187
|
+
//
|
|
3188
|
+
// A header that exports by assignment (`export = binding`, the form a build
|
|
3189
|
+
// without `napi-derive`'s `type-def` feature emits) leaves no room either: an
|
|
3190
|
+
// export assignment cannot sit beside any named export, so appending one is
|
|
3191
|
+
// TS2309 rather than a declaration anybody can use. Its exports are untyped
|
|
3192
|
+
// anyway. `ensureBindingTargetDeclaration` already leaves such a file alone,
|
|
3193
|
+
// and the WASI declaration derived from this one goes through it — reading
|
|
3194
|
+
// the same answer here keeps both ends of that derivation agreeing.
|
|
3195
|
+
const headerDeclaresBindingTarget =
|
|
3196
|
+
headerBindingTarget.ownsName || headerBindingTarget.exportsByAssignment
|
|
3197
|
+
|
|
3198
|
+
// A crate can register every export from a `#[napi(module_exports)]` hook
|
|
3199
|
+
// and emit no `.type` file, and the loader written for it still exports
|
|
3200
|
+
// `__napiBindingTarget`. Declare it, so the declaration file describes the
|
|
3201
|
+
// loader that was written rather than staying empty.
|
|
3202
|
+
const bindingTargetOnlyTypeDef = () => {
|
|
3203
|
+
if (!declaresBindingTarget([])) {
|
|
3204
|
+
return {
|
|
3205
|
+
exports: [],
|
|
3206
|
+
dts: '',
|
|
3207
|
+
dtsWithTypeImports: '',
|
|
3208
|
+
header: renderedHeader,
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
const declarationOnly = finalizeTypeDefHeader(
|
|
3212
|
+
headerDeclaresBindingTarget
|
|
3213
|
+
? header
|
|
3214
|
+
: header + BINDING_TARGET_TYPE_DECLARATION,
|
|
3215
|
+
)
|
|
3216
|
+
return {
|
|
3217
|
+
exports: [],
|
|
3218
|
+
dts: declarationOnly,
|
|
3219
|
+
dtsWithTypeImports: declarationOnly,
|
|
3220
|
+
header: renderedHeader,
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
if (!(await dirExistsAsync(options.typeDefDir))) {
|
|
3225
|
+
return bindingTargetOnlyTypeDef()
|
|
3226
|
+
}
|
|
3227
|
+
|
|
2519
3228
|
const files = await readdirAsync(options.typeDefDir, { withFileTypes: true })
|
|
2520
3229
|
|
|
2521
3230
|
if (!files.length) {
|
|
2522
3231
|
debug('No type def files found. Skip generating dts file.')
|
|
2523
|
-
return
|
|
3232
|
+
return bindingTargetOnlyTypeDef()
|
|
2524
3233
|
}
|
|
2525
3234
|
|
|
2526
3235
|
const typeDefFiles = files
|
|
@@ -2560,10 +3269,26 @@ export declare class ExternalObject<T> {
|
|
|
2560
3269
|
|
|
2561
3270
|
if (dts.indexOf('TypedArray') > -1) {
|
|
2562
3271
|
header += `
|
|
2563
|
-
export type TypedArray =
|
|
3272
|
+
export type TypedArray =
|
|
3273
|
+
| Int8Array
|
|
3274
|
+
| Uint8Array
|
|
3275
|
+
| Uint8ClampedArray
|
|
3276
|
+
| Int16Array
|
|
3277
|
+
| Uint16Array
|
|
3278
|
+
| Int32Array
|
|
3279
|
+
| Uint32Array
|
|
3280
|
+
| Float32Array
|
|
3281
|
+
| Float64Array
|
|
3282
|
+
| BigInt64Array
|
|
3283
|
+
| BigUint64Array
|
|
2564
3284
|
`
|
|
2565
3285
|
}
|
|
2566
3286
|
|
|
3287
|
+
if (declaresBindingTarget(exports) && !headerDeclaresBindingTarget) {
|
|
3288
|
+
header += BINDING_TARGET_TYPE_DECLARATION
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
header = finalizeTypeDefHeader(header)
|
|
2567
3292
|
dts = header + dts
|
|
2568
3293
|
const dtsWithTypeImports = rewriteTypeImportReferences(
|
|
2569
3294
|
header + dtsWithTypeImportMarkers,
|
|
@@ -2575,5 +3300,6 @@ export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array
|
|
|
2575
3300
|
exports,
|
|
2576
3301
|
dts,
|
|
2577
3302
|
dtsWithTypeImports,
|
|
3303
|
+
header: renderedHeader,
|
|
2578
3304
|
}
|
|
2579
3305
|
}
|