@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
package/src/api/build.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
removeNodeStreamWebTypeImports,
|
|
42
42
|
rewriteUnboundNodeGlobalTypeQueries,
|
|
43
43
|
rewriteTypeImportReferences,
|
|
44
|
+
scanExportedName,
|
|
44
45
|
type Target,
|
|
45
46
|
targetToEnvVar,
|
|
46
47
|
tryInstallCargoBinary,
|
|
@@ -57,7 +58,12 @@ import {
|
|
|
57
58
|
type CargoWorkspaceMetadata,
|
|
58
59
|
} from '../utils/index.js'
|
|
59
60
|
|
|
60
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
assertBindingTargetIdentFree,
|
|
63
|
+
createCjsBinding,
|
|
64
|
+
createEsmBinding,
|
|
65
|
+
NAPI_BINDING_TARGET_EXPORT,
|
|
66
|
+
} from './templates/index.js'
|
|
61
67
|
import {
|
|
62
68
|
createWasiBinding,
|
|
63
69
|
createWasiBrowserBinding,
|
|
@@ -80,6 +86,98 @@ const MANAGED_WASI_FLAVORS = [
|
|
|
80
86
|
},
|
|
81
87
|
] as const
|
|
82
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
|
+
|
|
83
181
|
type OutputKind = 'js' | 'dts' | 'node' | 'exe' | 'wasm'
|
|
84
182
|
type Output = { kind: OutputKind; path: string }
|
|
85
183
|
type WasiBindingMetadata = {
|
|
@@ -97,6 +195,70 @@ type ParsedBuildOptions = Omit<BuildOptions, 'cwd' | 'format'> & {
|
|
|
97
195
|
|
|
98
196
|
export const WASI_ARTIFACT_METADATA_PREFIX = '// napi-rs-artifact-metadata:'
|
|
99
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
|
+
|
|
100
262
|
type CargoConfigFingerprint = readonly [path: string, hash: string]
|
|
101
263
|
|
|
102
264
|
export function getCargoDependencyGraphFingerprint(
|
|
@@ -486,9 +648,11 @@ export function createWasiBrowserEntry(
|
|
|
486
648
|
export function createWasiDeferredBindingTypeDef(
|
|
487
649
|
bindingModuleSpecifier: string,
|
|
488
650
|
hasTypeDef: boolean,
|
|
651
|
+
platformArchABI?: string,
|
|
489
652
|
) {
|
|
490
653
|
const typeDef = createWasiDeferredBrowserBindingTypeDef(
|
|
491
654
|
bindingModuleSpecifier,
|
|
655
|
+
platformArchABI,
|
|
492
656
|
)
|
|
493
657
|
if (hasTypeDef) {
|
|
494
658
|
return typeDef
|
|
@@ -504,6 +668,133 @@ export function createWasiDeferredBindingTypeDef(
|
|
|
504
668
|
return typeDef.replace(rootBindingType, 'Record<string, unknown>')
|
|
505
669
|
}
|
|
506
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
|
+
|
|
507
798
|
export function prepareWasiBindingTypeDef(
|
|
508
799
|
source: string,
|
|
509
800
|
sourcePath: string,
|
|
@@ -527,6 +818,136 @@ export function prepareWasiBindingTypeDef(
|
|
|
527
818
|
return rebaseDeclarationSpecifiers(targetSource, sourcePath, destinationPath)
|
|
528
819
|
}
|
|
529
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
|
+
|
|
530
951
|
export function collectStaleWasiBuildOutputNames(
|
|
531
952
|
binaryName: string,
|
|
532
953
|
buildTarget: Target,
|
|
@@ -1055,6 +1476,12 @@ class Builder {
|
|
|
1055
1476
|
private readonly enableTypeDef: boolean = false
|
|
1056
1477
|
private readonly stagedOutputDestinations = new Map<string, string>()
|
|
1057
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
|
|
1058
1485
|
|
|
1059
1486
|
constructor(
|
|
1060
1487
|
private readonly metadata: CargoWorkspaceMetadata,
|
|
@@ -1314,6 +1741,31 @@ class Builder {
|
|
|
1314
1741
|
return entries
|
|
1315
1742
|
}
|
|
1316
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
|
+
|
|
1317
1769
|
private async readExistingWasiBindingMetadata(wasiTarget: Target) {
|
|
1318
1770
|
const loaderSuffix = wasiLoaderSuffix(wasiTarget.platformArchABI)
|
|
1319
1771
|
const bindingPath = join(
|
|
@@ -2069,7 +2521,20 @@ class Builder {
|
|
|
2069
2521
|
return []
|
|
2070
2522
|
}
|
|
2071
2523
|
|
|
2072
|
-
|
|
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({
|
|
2073
2538
|
typeDefDir,
|
|
2074
2539
|
noDtsHeader: this.options.noDtsHeader,
|
|
2075
2540
|
dtsHeader: this.options.dtsHeader,
|
|
@@ -2079,8 +2544,18 @@ class Builder {
|
|
|
2079
2544
|
runtimeStringEnum:
|
|
2080
2545
|
this.options.runtimeStringEnum ?? this.config.runtimeStringEnum,
|
|
2081
2546
|
cwd: this.options.cwd,
|
|
2547
|
+
declareBindingTarget,
|
|
2082
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
|
+
}
|
|
2083
2557
|
this.typeDefWithTypeImports = dtsWithTypeImports
|
|
2558
|
+
this.typeDefRenderedHeader = header
|
|
2084
2559
|
|
|
2085
2560
|
const typeDefRelativePath = this.options.dts ?? 'index.d.ts'
|
|
2086
2561
|
const finalDest = join(this.finalOutputDir, typeDefRelativePath)
|
|
@@ -2096,7 +2571,13 @@ class Builder {
|
|
|
2096
2571
|
throw new Error(`Failed to write type def file ${dest}`, { cause: e })
|
|
2097
2572
|
}
|
|
2098
2573
|
|
|
2099
|
-
|
|
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) {
|
|
2100
2581
|
this.outputs.push({ kind: 'dts', path: dest })
|
|
2101
2582
|
}
|
|
2102
2583
|
|
|
@@ -2124,10 +2605,13 @@ class Builder {
|
|
|
2124
2605
|
return stagedPath
|
|
2125
2606
|
}
|
|
2126
2607
|
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
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[] {
|
|
2131
2615
|
const declaredWasiTargets = this.config.targets.filter(
|
|
2132
2616
|
(t) => t.platform === 'wasi',
|
|
2133
2617
|
)
|
|
@@ -2143,7 +2627,7 @@ class Builder {
|
|
|
2143
2627
|
) {
|
|
2144
2628
|
declaredWasiTargets.push(this.target)
|
|
2145
2629
|
}
|
|
2146
|
-
|
|
2630
|
+
return [
|
|
2147
2631
|
...new Set(
|
|
2148
2632
|
[
|
|
2149
2633
|
...declaredWasiTargets.filter(wasiTargetHasThreads),
|
|
@@ -2151,6 +2635,13 @@ class Builder {
|
|
|
2151
2635
|
].map((t) => t.platformArchABI),
|
|
2152
2636
|
),
|
|
2153
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()
|
|
2154
2645
|
return writeJsBinding({
|
|
2155
2646
|
platform: this.options.platform,
|
|
2156
2647
|
noJsBinding: this.options.noJsBinding,
|
|
@@ -2253,7 +2744,24 @@ class Builder {
|
|
|
2253
2744
|
metadata: WasiBindingMetadata,
|
|
2254
2745
|
): Promise<Output[]> {
|
|
2255
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
|
+
}
|
|
2256
2760
|
const hasThreads = wasiTargetHasThreads(wasiTarget)
|
|
2761
|
+
const { initialMemory, maximumMemory } = resolveWasmMemory(
|
|
2762
|
+
this.config.wasm,
|
|
2763
|
+
hasThreads,
|
|
2764
|
+
)
|
|
2257
2765
|
const loaderSuffix = wasiLoaderSuffix(wasiTarget.platformArchABI)
|
|
2258
2766
|
// the wasm file stem referenced from inside the loaders
|
|
2259
2767
|
const name = `${this.config.binaryName}.${wasiTarget.platformArchABI}`
|
|
@@ -2269,13 +2777,23 @@ class Builder {
|
|
|
2269
2777
|
dir,
|
|
2270
2778
|
`${this.config.binaryName}.${loaderSuffix}.d.cts`,
|
|
2271
2779
|
)
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
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')
|
|
2279
2797
|
await writeFileAtomic(
|
|
2280
2798
|
bindingPath,
|
|
2281
2799
|
createWasiArtifactMetadata(
|
|
@@ -2288,11 +2806,12 @@ class Builder {
|
|
|
2288
2806
|
createWasiBinding(
|
|
2289
2807
|
name,
|
|
2290
2808
|
this.config.packageName,
|
|
2291
|
-
|
|
2292
|
-
|
|
2809
|
+
initialMemory,
|
|
2810
|
+
maximumMemory,
|
|
2293
2811
|
hasThreads,
|
|
2294
2812
|
wasiTarget.platformArchABI,
|
|
2295
2813
|
`${this.config.binaryName}.${wasiTarget.platformArchABI}`,
|
|
2814
|
+
asyncRuntime,
|
|
2296
2815
|
) +
|
|
2297
2816
|
exportsCode +
|
|
2298
2817
|
'\n',
|
|
@@ -2302,13 +2821,15 @@ class Builder {
|
|
|
2302
2821
|
browserBindingPath,
|
|
2303
2822
|
createWasiBrowserBinding(
|
|
2304
2823
|
name,
|
|
2305
|
-
|
|
2306
|
-
|
|
2824
|
+
initialMemory,
|
|
2825
|
+
maximumMemory,
|
|
2307
2826
|
this.config.wasm?.browser?.fs,
|
|
2308
2827
|
this.config.wasm?.browser?.asyncInit,
|
|
2309
2828
|
this.config.wasm?.browser?.buffer,
|
|
2310
2829
|
this.config.wasm?.browser?.errorEvent,
|
|
2311
2830
|
hasThreads,
|
|
2831
|
+
wasiTarget.platformArchABI,
|
|
2832
|
+
asyncRuntime,
|
|
2312
2833
|
) +
|
|
2313
2834
|
`export default __napiModule.exports\n` +
|
|
2314
2835
|
idents
|
|
@@ -2355,6 +2876,17 @@ export = binding
|
|
|
2355
2876
|
? selectedSourceTypeDef
|
|
2356
2877
|
: removeNodeStreamWebTypeImports(selectedSourceTypeDef)
|
|
2357
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
|
+
)
|
|
2358
2890
|
await writeFileAtomic(bindingTypeDefPath, bindingTypeDef, 'utf8')
|
|
2359
2891
|
const outputs: Output[] = [
|
|
2360
2892
|
{ kind: 'js', path: bindingPath },
|
|
@@ -2392,9 +2924,11 @@ export = binding
|
|
|
2392
2924
|
deferredBindingPath,
|
|
2393
2925
|
createWasiDeferredBrowserBinding(
|
|
2394
2926
|
name,
|
|
2395
|
-
|
|
2396
|
-
|
|
2927
|
+
initialMemory,
|
|
2928
|
+
maximumMemory,
|
|
2397
2929
|
this.config.wasm?.browser?.buffer,
|
|
2930
|
+
wasiTarget.platformArchABI,
|
|
2931
|
+
asyncRuntime,
|
|
2398
2932
|
),
|
|
2399
2933
|
'utf8',
|
|
2400
2934
|
)
|
|
@@ -2403,6 +2937,7 @@ export = binding
|
|
|
2403
2937
|
createWasiDeferredBindingTypeDef(
|
|
2404
2938
|
`./${this.config.binaryName}.${loaderSuffix}.cjs`,
|
|
2405
2939
|
this.enableTypeDef,
|
|
2940
|
+
wasiTarget.platformArchABI,
|
|
2406
2941
|
),
|
|
2407
2942
|
'utf8',
|
|
2408
2943
|
)
|
|
@@ -2578,6 +3113,18 @@ export interface GenerateTypeDefOptions {
|
|
|
2578
3113
|
constEnum?: boolean
|
|
2579
3114
|
runtimeStringEnum?: boolean
|
|
2580
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)
|
|
2581
3128
|
}
|
|
2582
3129
|
|
|
2583
3130
|
/**
|
|
@@ -2592,15 +3139,22 @@ export async function generateTypeDef(
|
|
|
2592
3139
|
exports: string[]
|
|
2593
3140
|
dts: string
|
|
2594
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
|
|
2595
3148
|
}> {
|
|
2596
|
-
if (!(await dirExistsAsync(options.typeDefDir))) {
|
|
2597
|
-
return { exports: [], dts: '', dtsWithTypeImports: '' }
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2600
3149
|
let header = ''
|
|
2601
3150
|
let dts = ''
|
|
2602
3151
|
let exports: string[] = []
|
|
2603
3152
|
|
|
3153
|
+
const declaresBindingTarget = (generated: readonly string[]) =>
|
|
3154
|
+
typeof options.declareBindingTarget === 'function'
|
|
3155
|
+
? options.declareBindingTarget(generated)
|
|
3156
|
+
: Boolean(options.declareBindingTarget)
|
|
3157
|
+
|
|
2604
3158
|
if (!options.noDtsHeader) {
|
|
2605
3159
|
const dtsHeader = options.dtsHeader ?? options.configDtsHeader
|
|
2606
3160
|
const dtsHeaderFile = options.dtsHeaderFile ?? options.configDtsHeaderFile
|
|
@@ -2619,11 +3173,63 @@ export async function generateTypeDef(
|
|
|
2619
3173
|
}
|
|
2620
3174
|
}
|
|
2621
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
|
+
|
|
2622
3228
|
const files = await readdirAsync(options.typeDefDir, { withFileTypes: true })
|
|
2623
3229
|
|
|
2624
3230
|
if (!files.length) {
|
|
2625
3231
|
debug('No type def files found. Skip generating dts file.')
|
|
2626
|
-
return
|
|
3232
|
+
return bindingTargetOnlyTypeDef()
|
|
2627
3233
|
}
|
|
2628
3234
|
|
|
2629
3235
|
const typeDefFiles = files
|
|
@@ -2678,12 +3284,11 @@ export type TypedArray =
|
|
|
2678
3284
|
`
|
|
2679
3285
|
}
|
|
2680
3286
|
|
|
2681
|
-
if (
|
|
2682
|
-
header +=
|
|
2683
|
-
}
|
|
2684
|
-
if (header && !header.endsWith('\n\n')) {
|
|
2685
|
-
header += '\n'
|
|
3287
|
+
if (declaresBindingTarget(exports) && !headerDeclaresBindingTarget) {
|
|
3288
|
+
header += BINDING_TARGET_TYPE_DECLARATION
|
|
2686
3289
|
}
|
|
3290
|
+
|
|
3291
|
+
header = finalizeTypeDefHeader(header)
|
|
2687
3292
|
dts = header + dts
|
|
2688
3293
|
const dtsWithTypeImports = rewriteTypeImportReferences(
|
|
2689
3294
|
header + dtsWithTypeImportMarkers,
|
|
@@ -2695,5 +3300,6 @@ export type TypedArray =
|
|
|
2695
3300
|
exports,
|
|
2696
3301
|
dts,
|
|
2697
3302
|
dtsWithTypeImports,
|
|
3303
|
+
header: renderedHeader,
|
|
2698
3304
|
}
|
|
2699
3305
|
}
|