@napi-rs/cli 3.8.1 → 3.8.3
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/dist/cli.js +66 -16
- package/dist/index.cjs +66 -16
- package/dist/index.d.cts +22 -4
- package/dist/index.d.ts +22 -4
- package/dist/index.js +66 -16
- package/docs/wasi.md +36 -0
- package/package.json +2 -2
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +27 -9
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/pre-publish.spec.ts +141 -0
- package/src/api/__tests__/templates.spec.ts +48 -0
- package/src/api/pre-publish.ts +87 -31
- package/src/api/templates/load-wasi-template.ts +47 -4
- package/src/utils/config.ts +20 -1
package/docs/wasi.md
CHANGED
|
@@ -153,9 +153,45 @@ module runs inside the host process, so `wasm32` or host-OS restrictions would
|
|
|
153
153
|
make npm reject a direct install or skip the optional dependency on otherwise
|
|
154
154
|
supported hosts.
|
|
155
155
|
|
|
156
|
+
Because nothing gates the install, the root package does not declare the WASI
|
|
157
|
+
package in `optionalDependencies` when native targets are also configured. npm
|
|
158
|
+
evaluates every `optionalDependencies` entry independently, so a declared WASI
|
|
159
|
+
package is downloaded by every consumer, including the ones that already
|
|
160
|
+
resolved a native package and will never load the `.wasm` binary. The generated
|
|
161
|
+
binding loader picks WASI at require time instead, and environments without a
|
|
162
|
+
native package are expected to install it on demand.
|
|
163
|
+
|
|
164
|
+
When WASI is the only configured target it is the primary artifact rather than a
|
|
165
|
+
fallback, so it is declared by default. Set `napi.wasm.optionalDependency` to
|
|
166
|
+
override the default in either direction:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"napi": {
|
|
171
|
+
"wasm": {
|
|
172
|
+
"optionalDependency": true
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
156
178
|
`napi.wasm.initialMemory` is measured in 64 KiB WebAssembly pages. The regular
|
|
157
179
|
Node and browser loaders retain the historical 4,000-page (250 MiB) default.
|
|
158
180
|
The deferred `./workerd` loader defaults to 1,024 pages (64 MiB), leaving
|
|
159
181
|
headroom under workerd's 128 MiB isolate limit. An explicit
|
|
160
182
|
`napi.wasm.initialMemory` value applies to every loader, so keep it within the
|
|
161
183
|
target isolate's limit after measuring the addon's actual requirements.
|
|
184
|
+
|
|
185
|
+
Threaded browser loaders always pre-create a pool of wasi-threads workers at
|
|
186
|
+
module initialization, sized as `asyncWorkPoolSize + hardwareConcurrency`
|
|
187
|
+
(logical cores, floored at 2, with a fallback for privacy-fuzzed values),
|
|
188
|
+
and therefore always initialize asynchronously. The `asyncWorkPoolSize`
|
|
189
|
+
reservation is included because emnapi's async-work pool draws its workers
|
|
190
|
+
from the same reuse pool; without it, async work could starve the pool
|
|
191
|
+
before addon thread spawns. This is what allows addon Rust code to spawn
|
|
192
|
+
threads from inside a blocking call: a browser cannot start a worker until
|
|
193
|
+
the blocking thread returns to its event loop, so a thread spawned mid-call
|
|
194
|
+
would never boot and the caller would deadlock waiting for it. With a
|
|
195
|
+
pre-created pool, spawning is only a message to an already-running worker,
|
|
196
|
+
and if the pool is exhausted the fallback allocates a fresh worker that
|
|
197
|
+
boots once the spawning parent returns to its event loop.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@napi-rs/cli",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.3",
|
|
4
4
|
"description": "Cli tools for napi-rs",
|
|
5
5
|
"author": "LongYinan <lynweklm@gmail.com>",
|
|
6
6
|
"homepage": "https://napi.rs/",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"ava": "^8.0.1",
|
|
87
87
|
"empathic": "^2.0.1",
|
|
88
88
|
"env-paths": "^4.0.0",
|
|
89
|
-
"oxc-parser": "^0.
|
|
89
|
+
"oxc-parser": "^0.143.0",
|
|
90
90
|
"prettier": "^3.8.3",
|
|
91
91
|
"tsdown": "^0.22.2",
|
|
92
92
|
"tslib": "^2.8.1"
|
|
@@ -12,7 +12,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
12
12
|
emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin,␊
|
|
13
13
|
emnapiTSFNPlugin as __emnapiTSFNPlugin,␊
|
|
14
14
|
createOnMessage as __wasmCreateOnMessageForFsProxy,␊
|
|
15
|
-
|
|
15
|
+
instantiateNapiModule as __emnapiInstantiateNapiModule,␊
|
|
16
16
|
WASI as __WASI,␊
|
|
17
17
|
} from '@napi-rs/wasm-runtime'␊
|
|
18
18
|
import { createContext as __emnapiCreateContext } from '@emnapi/runtime'␊
|
|
@@ -42,6 +42,11 @@ Generated by [AVA](https://avajs.dev).
|
|
|
42
42
|
maximum: 65536,␊
|
|
43
43
|
shared: true,␊
|
|
44
44
|
})␊
|
|
45
|
+
const __asyncWorkPoolSize = 4␊
|
|
46
|
+
const __workerPoolSize = Math.max(␊
|
|
47
|
+
2,␊
|
|
48
|
+
globalThis.navigator?.hardwareConcurrency ?? 4,␊
|
|
49
|
+
)␊
|
|
45
50
|
␊
|
|
46
51
|
let __emnapiContext␊
|
|
47
52
|
␊
|
|
@@ -314,9 +319,10 @@ Generated by [AVA](https://avajs.dev).
|
|
|
314
319
|
instance: __napiInstance,␊
|
|
315
320
|
module: __wasiModule,␊
|
|
316
321
|
napiModule: __napiModule,␊
|
|
317
|
-
} =
|
|
322
|
+
} = await __emnapiInstantiateNapiModule(__wasmFile, {␊
|
|
318
323
|
context: __emnapiContext,␊
|
|
319
|
-
asyncWorkPoolSize:
|
|
324
|
+
asyncWorkPoolSize: __asyncWorkPoolSize,␊
|
|
325
|
+
reuseWorker: { size: __asyncWorkPoolSize + __workerPoolSize },␊
|
|
320
326
|
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],␊
|
|
321
327
|
wasi: __wasi,␊
|
|
322
328
|
onCreateWorker() {␊
|
|
@@ -361,7 +367,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
361
367
|
emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin,␊
|
|
362
368
|
emnapiTSFNPlugin as __emnapiTSFNPlugin,␊
|
|
363
369
|
createOnMessage as __wasmCreateOnMessageForFsProxy,␊
|
|
364
|
-
|
|
370
|
+
instantiateNapiModule as __emnapiInstantiateNapiModule,␊
|
|
365
371
|
WASI as __WASI,␊
|
|
366
372
|
} from '@napi-rs/wasm-runtime'␊
|
|
367
373
|
import { createContext as __emnapiCreateContext } from '@emnapi/runtime'␊
|
|
@@ -391,6 +397,11 @@ Generated by [AVA](https://avajs.dev).
|
|
|
391
397
|
maximum: 65536,␊
|
|
392
398
|
shared: true,␊
|
|
393
399
|
})␊
|
|
400
|
+
const __asyncWorkPoolSize = 4␊
|
|
401
|
+
const __workerPoolSize = Math.max(␊
|
|
402
|
+
2,␊
|
|
403
|
+
globalThis.navigator?.hardwareConcurrency ?? 4,␊
|
|
404
|
+
)␊
|
|
394
405
|
␊
|
|
395
406
|
let __emnapiContext␊
|
|
396
407
|
␊
|
|
@@ -663,9 +674,10 @@ Generated by [AVA](https://avajs.dev).
|
|
|
663
674
|
instance: __napiInstance,␊
|
|
664
675
|
module: __wasiModule,␊
|
|
665
676
|
napiModule: __napiModule,␊
|
|
666
|
-
} =
|
|
677
|
+
} = await __emnapiInstantiateNapiModule(__wasmFile, {␊
|
|
667
678
|
context: __emnapiContext,␊
|
|
668
|
-
asyncWorkPoolSize:
|
|
679
|
+
asyncWorkPoolSize: __asyncWorkPoolSize,␊
|
|
680
|
+
reuseWorker: { size: __asyncWorkPoolSize + __workerPoolSize },␊
|
|
669
681
|
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],␊
|
|
670
682
|
wasi: __wasi,␊
|
|
671
683
|
onCreateWorker() {␊
|
|
@@ -723,7 +735,7 @@ Generated by [AVA](https://avajs.dev).
|
|
|
723
735
|
emnapiAsyncWorkPlugin as __emnapiAsyncWorkPlugin,␊
|
|
724
736
|
emnapiTSFNPlugin as __emnapiTSFNPlugin,␊
|
|
725
737
|
createOnMessage as __wasmCreateOnMessageForFsProxy,␊
|
|
726
|
-
|
|
738
|
+
instantiateNapiModule as __emnapiInstantiateNapiModule,␊
|
|
727
739
|
WASI as __WASI,␊
|
|
728
740
|
} from '@napi-rs/wasm-runtime'␊
|
|
729
741
|
import { createContext as __emnapiCreateContext } from '@emnapi/runtime'␊
|
|
@@ -759,6 +771,11 @@ Generated by [AVA](https://avajs.dev).
|
|
|
759
771
|
maximum: 65536,␊
|
|
760
772
|
shared: true,␊
|
|
761
773
|
})␊
|
|
774
|
+
const __asyncWorkPoolSize = 4␊
|
|
775
|
+
const __workerPoolSize = Math.max(␊
|
|
776
|
+
2,␊
|
|
777
|
+
globalThis.navigator?.hardwareConcurrency ?? 4,␊
|
|
778
|
+
)␊
|
|
762
779
|
␊
|
|
763
780
|
let __emnapiContext␊
|
|
764
781
|
␊
|
|
@@ -1031,9 +1048,10 @@ Generated by [AVA](https://avajs.dev).
|
|
|
1031
1048
|
instance: __napiInstance,␊
|
|
1032
1049
|
module: __wasiModule,␊
|
|
1033
1050
|
napiModule: __napiModule,␊
|
|
1034
|
-
} =
|
|
1051
|
+
} = await __emnapiInstantiateNapiModule(__wasmFile, {␊
|
|
1035
1052
|
context: __emnapiContext,␊
|
|
1036
|
-
asyncWorkPoolSize:
|
|
1053
|
+
asyncWorkPoolSize: __asyncWorkPoolSize,␊
|
|
1054
|
+
reuseWorker: { size: __asyncWorkPoolSize + __workerPoolSize },␊
|
|
1037
1055
|
plugins: [__emnapiAsyncWorkPlugin, __emnapiTSFNPlugin],␊
|
|
1038
1056
|
wasi: __wasi,␊
|
|
1039
1057
|
onCreateWorker() {␊
|
|
Binary file
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import test from 'ava'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
parseNpmPackFiles,
|
|
5
|
+
resolveRootOptionalDependencies,
|
|
6
|
+
} from '../pre-publish.js'
|
|
7
|
+
import { parseTriple } from '../../utils/index.js'
|
|
8
|
+
|
|
9
|
+
const PACKAGE_NAME = '@scope/pkg'
|
|
10
|
+
const VERSION = '1.2.3'
|
|
11
|
+
|
|
12
|
+
const NATIVE_TARGETS = ['aarch64-apple-darwin', 'x86_64-unknown-linux-gnu'].map(
|
|
13
|
+
parseTriple,
|
|
14
|
+
)
|
|
15
|
+
const WASI_TARGET = parseTriple('wasm32-wasip1-threads')
|
|
16
|
+
const THREADLESS_WASI_TARGET = parseTriple('wasm32-wasip1')
|
|
17
|
+
|
|
18
|
+
const NATIVE_ENTRIES = {
|
|
19
|
+
[`${PACKAGE_NAME}-darwin-arm64`]: VERSION,
|
|
20
|
+
[`${PACKAGE_NAME}-linux-x64-gnu`]: VERSION,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolve(
|
|
24
|
+
targets: Parameters<typeof resolveRootOptionalDependencies>[0]['targets'],
|
|
25
|
+
overrides: Partial<
|
|
26
|
+
Parameters<typeof resolveRootOptionalDependencies>[0]
|
|
27
|
+
> = {},
|
|
28
|
+
) {
|
|
29
|
+
return resolveRootOptionalDependencies({
|
|
30
|
+
existing: undefined,
|
|
31
|
+
managedPackageNames: [PACKAGE_NAME],
|
|
32
|
+
packageName: PACKAGE_NAME,
|
|
33
|
+
targets,
|
|
34
|
+
version: VERSION,
|
|
35
|
+
...overrides,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test('omits the WASI package when native targets are configured', (t) => {
|
|
40
|
+
// The WASI binary is a require-time fallback, not something npm should
|
|
41
|
+
// install on hosts that already resolved a native package.
|
|
42
|
+
t.deepEqual(resolve([...NATIVE_TARGETS, WASI_TARGET]), NATIVE_ENTRIES)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('omits the threadless WASI package too', (t) => {
|
|
46
|
+
t.deepEqual(
|
|
47
|
+
resolve([...NATIVE_TARGETS, THREADLESS_WASI_TARGET]),
|
|
48
|
+
NATIVE_ENTRIES,
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('declares the WASI package when it is the only target', (t) => {
|
|
53
|
+
// With no native package to fall back from, WASI is the primary artifact.
|
|
54
|
+
t.deepEqual(resolve([WASI_TARGET]), {
|
|
55
|
+
[`${PACKAGE_NAME}-wasm32-wasi`]: VERSION,
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('declares every WASI flavor when only WASI targets are configured', (t) => {
|
|
60
|
+
t.deepEqual(resolve([WASI_TARGET, THREADLESS_WASI_TARGET]), {
|
|
61
|
+
[`${PACKAGE_NAME}-wasm32-wasi`]: VERSION,
|
|
62
|
+
[`${PACKAGE_NAME}-wasm32-wasip1`]: VERSION,
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('wasm.optionalDependency=true opts back into declaring WASI', (t) => {
|
|
67
|
+
t.deepEqual(
|
|
68
|
+
resolve([...NATIVE_TARGETS, WASI_TARGET], {
|
|
69
|
+
wasm: { optionalDependency: true },
|
|
70
|
+
}),
|
|
71
|
+
{
|
|
72
|
+
...NATIVE_ENTRIES,
|
|
73
|
+
[`${PACKAGE_NAME}-wasm32-wasi`]: VERSION,
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('wasm.optionalDependency=false opts out even for WASI-only builds', (t) => {
|
|
79
|
+
t.deepEqual(
|
|
80
|
+
resolve([WASI_TARGET], { wasm: { optionalDependency: false } }),
|
|
81
|
+
{},
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('drops a stale WASI entry left over from a previous release', (t) => {
|
|
86
|
+
// Consumers upgrading from a release that did declare the WASI package must
|
|
87
|
+
// not keep the entry, otherwise the regression survives the fix.
|
|
88
|
+
t.deepEqual(
|
|
89
|
+
resolve([...NATIVE_TARGETS, WASI_TARGET], {
|
|
90
|
+
existing: {
|
|
91
|
+
...NATIVE_ENTRIES,
|
|
92
|
+
[`${PACKAGE_NAME}-wasm32-wasi`]: '1.2.2',
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
NATIVE_ENTRIES,
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('preserves unmanaged optionalDependencies', (t) => {
|
|
100
|
+
t.deepEqual(
|
|
101
|
+
resolve([...NATIVE_TARGETS, WASI_TARGET], {
|
|
102
|
+
existing: { 'unrelated-package': '^1.0.0' },
|
|
103
|
+
}),
|
|
104
|
+
{
|
|
105
|
+
'unrelated-package': '^1.0.0',
|
|
106
|
+
...NATIVE_ENTRIES,
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const NPM_PACK_FILES = [
|
|
112
|
+
{ path: './package.json' },
|
|
113
|
+
{ path: 'dist\\index.js' },
|
|
114
|
+
{ path: 42 },
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
test('parses npm 11 pack JSON output', (t) => {
|
|
118
|
+
t.deepEqual(
|
|
119
|
+
[...parseNpmPackFiles(JSON.stringify([{ files: NPM_PACK_FILES }]))],
|
|
120
|
+
['package.json', 'dist/index.js'],
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('parses npm 12 pack JSON output', (t) => {
|
|
125
|
+
t.deepEqual(
|
|
126
|
+
[
|
|
127
|
+
...parseNpmPackFiles(
|
|
128
|
+
JSON.stringify({
|
|
129
|
+
[PACKAGE_NAME]: { files: NPM_PACK_FILES },
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
],
|
|
133
|
+
['package.json', 'dist/index.js'],
|
|
134
|
+
)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('rejects an unexpected npm pack JSON result', (t) => {
|
|
138
|
+
t.throws(() => parseNpmPackFiles(JSON.stringify({ files: [] })), {
|
|
139
|
+
message: 'npm pack returned an unexpected JSON result',
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -13,6 +13,54 @@ test('createWasiBrowserBinding default', (t) => {
|
|
|
13
13
|
t.snapshot(createWasiBrowserBinding('test-wasi'))
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
+
test('createWasiBrowserBinding threaded builds size worker pools from hardwareConcurrency', (t) => {
|
|
17
|
+
const binding = createWasiBrowserBinding(
|
|
18
|
+
'test-wasi',
|
|
19
|
+
4000,
|
|
20
|
+
65536,
|
|
21
|
+
false,
|
|
22
|
+
false, // asyncInit: false — threaded builds still init asynchronously
|
|
23
|
+
false,
|
|
24
|
+
false,
|
|
25
|
+
true,
|
|
26
|
+
)
|
|
27
|
+
t.true(binding.includes('const __asyncWorkPoolSize = 4'))
|
|
28
|
+
t.true(binding.includes('const __workerPoolSize = Math.max('))
|
|
29
|
+
t.true(binding.includes('globalThis.navigator?.hardwareConcurrency ?? 4'))
|
|
30
|
+
// The reuse pool includes the async-work reservation: the async pool
|
|
31
|
+
// draws from the same reuse pool, so without it exhaustion would starve
|
|
32
|
+
// addon thread spawns. No `strict`: at exhaustion the fallback worker
|
|
33
|
+
// boots once the parent returns to its event loop, which is the correct
|
|
34
|
+
// behavior for spawn-and-return workloads.
|
|
35
|
+
t.true(
|
|
36
|
+
binding.includes(
|
|
37
|
+
'reuseWorker: { size: __asyncWorkPoolSize + __workerPoolSize },',
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
t.false(binding.includes('strict'))
|
|
41
|
+
t.true(binding.includes('asyncWorkPoolSize: __asyncWorkPoolSize,'))
|
|
42
|
+
t.true(binding.includes('await __emnapiInstantiateNapiModule('))
|
|
43
|
+
t.false(binding.includes('__emnapiInstantiateNapiModuleSync(__wasmFile'))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('createWasiBrowserBinding threadless keeps sync init and no pool', (t) => {
|
|
47
|
+
const binding = createWasiBrowserBinding(
|
|
48
|
+
'test-wasi',
|
|
49
|
+
4000,
|
|
50
|
+
65536,
|
|
51
|
+
false,
|
|
52
|
+
false,
|
|
53
|
+
false,
|
|
54
|
+
false,
|
|
55
|
+
false,
|
|
56
|
+
)
|
|
57
|
+
t.false(binding.includes('__workerPoolSize'))
|
|
58
|
+
t.false(binding.includes('hardwareConcurrency'))
|
|
59
|
+
t.false(binding.includes('reuseWorker'))
|
|
60
|
+
t.true(binding.includes('asyncWorkPoolSize: 0,'))
|
|
61
|
+
t.true(binding.includes('__emnapiInstantiateNapiModuleSync(__wasmFile'))
|
|
62
|
+
})
|
|
63
|
+
|
|
16
64
|
test('createWasiBrowserBinding with errorEvent', (t) => {
|
|
17
65
|
t.snapshot(
|
|
18
66
|
createWasiBrowserBinding(
|
package/src/api/pre-publish.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
type CommonPackageJsonFields,
|
|
61
61
|
type FileSystemTransactionWrite,
|
|
62
62
|
type Target,
|
|
63
|
+
type UserNapiConfig,
|
|
63
64
|
} from '../utils/index.js'
|
|
64
65
|
|
|
65
66
|
const debug = debugFactory('pre-publish')
|
|
@@ -227,6 +228,61 @@ async function copyOwnedTemporaryPath(source: string, destination: string) {
|
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
function wasiIsOnlyTarget(targets: Target[]) {
|
|
232
|
+
return (
|
|
233
|
+
targets.length > 0 && targets.every((target) => target.platform === 'wasi')
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Build the root package's `optionalDependencies` map.
|
|
239
|
+
*
|
|
240
|
+
* A WASI package is a fallback for hosts that cannot load a `.node` binary, and
|
|
241
|
+
* the generated binding loader selects it at require time rather than npm
|
|
242
|
+
* selecting it at install time. Declaring it as an `optionalDependency`
|
|
243
|
+
* alongside the native packages cannot express "install this only when nothing
|
|
244
|
+
* else matched": npm evaluates every entry independently, so every consumer
|
|
245
|
+
* downloads a `.wasm` binary they will never load.
|
|
246
|
+
*
|
|
247
|
+
* It is therefore only declared when WASI is the only configured target, which
|
|
248
|
+
* makes it the primary artifact rather than a fallback.
|
|
249
|
+
* `napi.wasm.optionalDependency` overrides the default in both directions.
|
|
250
|
+
*
|
|
251
|
+
* See https://github.com/rolldown/rolldown/issues/10556
|
|
252
|
+
*/
|
|
253
|
+
export function resolveRootOptionalDependencies({
|
|
254
|
+
existing,
|
|
255
|
+
managedPackageNames,
|
|
256
|
+
packageName,
|
|
257
|
+
targets,
|
|
258
|
+
version,
|
|
259
|
+
wasm,
|
|
260
|
+
}: {
|
|
261
|
+
existing: unknown
|
|
262
|
+
managedPackageNames: Iterable<string>
|
|
263
|
+
packageName: string
|
|
264
|
+
targets: Target[]
|
|
265
|
+
version: string
|
|
266
|
+
wasm?: UserNapiConfig['wasm']
|
|
267
|
+
}): Record<string, unknown> {
|
|
268
|
+
const optionalDependencies: Record<string, unknown> = {
|
|
269
|
+
...asRecord(existing),
|
|
270
|
+
}
|
|
271
|
+
for (const managedPackageName of managedPackageNames) {
|
|
272
|
+
for (const suffix of MANAGED_OPTIONAL_DEPENDENCY_SUFFIXES) {
|
|
273
|
+
delete optionalDependencies[`${managedPackageName}-${suffix}`]
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const declareWasi = wasm?.optionalDependency ?? wasiIsOnlyTarget(targets)
|
|
277
|
+
for (const target of targets) {
|
|
278
|
+
if (target.platform === 'wasi' && !declareWasi) {
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
optionalDependencies[`${packageName}-${target.platformArchABI}`] = version
|
|
282
|
+
}
|
|
283
|
+
return optionalDependencies
|
|
284
|
+
}
|
|
285
|
+
|
|
230
286
|
export async function prePublish(userOptions: PrePublishOptions) {
|
|
231
287
|
debug('Receive pre-publish options:')
|
|
232
288
|
debug(' %O', userOptions)
|
|
@@ -341,9 +397,6 @@ export async function prePublish(userOptions: PrePublishOptions) {
|
|
|
341
397
|
rootFacade,
|
|
342
398
|
)
|
|
343
399
|
}
|
|
344
|
-
const optionalDependencies = {
|
|
345
|
-
...asRecord(packageJson.optionalDependencies),
|
|
346
|
-
}
|
|
347
400
|
const managedPackageNames = new Set([packageName])
|
|
348
401
|
for (const flavorPackage of rootFacadeReconciliation.managedFlavorPackages) {
|
|
349
402
|
for (const suffix of MANAGED_OPTIONAL_DEPENDENCY_SUFFIXES) {
|
|
@@ -353,22 +406,19 @@ export async function prePublish(userOptions: PrePublishOptions) {
|
|
|
353
406
|
}
|
|
354
407
|
}
|
|
355
408
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
packageJson.engines?.node ?? MINIMUM_WASI_NODE_VERSION,
|
|
370
|
-
)
|
|
371
|
-
: undefined
|
|
409
|
+
const optionalDependencies = resolveRootOptionalDependencies({
|
|
410
|
+
existing: packageJson.optionalDependencies,
|
|
411
|
+
managedPackageNames,
|
|
412
|
+
packageName,
|
|
413
|
+
targets,
|
|
414
|
+
version: packageJson.version,
|
|
415
|
+
wasm,
|
|
416
|
+
})
|
|
417
|
+
const nodeEngine = wasiIsOnlyTarget(targets)
|
|
418
|
+
? restrictWasiNodeEngine(
|
|
419
|
+
packageJson.engines?.node ?? MINIMUM_WASI_NODE_VERSION,
|
|
420
|
+
)
|
|
421
|
+
: undefined
|
|
372
422
|
const rootReleasePlan: RootReleaseMaterializationPlan = {
|
|
373
423
|
packageJson: reconciledPackageJson,
|
|
374
424
|
optionalDependencies,
|
|
@@ -1641,18 +1691,7 @@ function readNpmPackFiles(packageDir: string, packageDescription: string) {
|
|
|
1641
1691
|
maxBuffer: 64 * 1024 * 1024,
|
|
1642
1692
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1643
1693
|
})
|
|
1644
|
-
|
|
1645
|
-
files?: { path?: unknown }[]
|
|
1646
|
-
}[]
|
|
1647
|
-
if (!Array.isArray(packResult) || !Array.isArray(packResult[0]?.files)) {
|
|
1648
|
-
throw new Error('npm pack returned an unexpected JSON result')
|
|
1649
|
-
}
|
|
1650
|
-
return new Set(
|
|
1651
|
-
packResult[0].files
|
|
1652
|
-
.map(({ path }) => path)
|
|
1653
|
-
.filter((path): path is string => typeof path === 'string')
|
|
1654
|
-
.map((path) => path.replaceAll('\\', '/').replace(/^\.\//, '')),
|
|
1655
|
-
)
|
|
1694
|
+
return parseNpmPackFiles(output)
|
|
1656
1695
|
} catch (error) {
|
|
1657
1696
|
throw new Error(
|
|
1658
1697
|
`Failed to validate the ${packageDescription} with npm pack --dry-run. Ensure npm is available and the package can be packed.`,
|
|
@@ -1661,6 +1700,23 @@ function readNpmPackFiles(packageDir: string, packageDescription: string) {
|
|
|
1661
1700
|
}
|
|
1662
1701
|
}
|
|
1663
1702
|
|
|
1703
|
+
export function parseNpmPackFiles(output: string) {
|
|
1704
|
+
const parsedResult: unknown = JSON.parse(output)
|
|
1705
|
+
const packResults = Array.isArray(parsedResult)
|
|
1706
|
+
? parsedResult
|
|
1707
|
+
: Object.values(asRecord(parsedResult) ?? {})
|
|
1708
|
+
const files = asRecord(packResults[0])?.files
|
|
1709
|
+
if (!Array.isArray(files)) {
|
|
1710
|
+
throw new Error('npm pack returned an unexpected JSON result')
|
|
1711
|
+
}
|
|
1712
|
+
return new Set(
|
|
1713
|
+
files
|
|
1714
|
+
.map((file) => asRecord(file)?.path)
|
|
1715
|
+
.filter((path): path is string => typeof path === 'string')
|
|
1716
|
+
.map((path) => path.replaceAll('\\', '/').replace(/^\.\//, '')),
|
|
1717
|
+
)
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1664
1720
|
function validateRootFacadePacklist(
|
|
1665
1721
|
rootDir: string,
|
|
1666
1722
|
rootPackageFiles: string[],
|
|
@@ -271,6 +271,10 @@ export const createWasiBrowserBinding = (
|
|
|
271
271
|
errorEvent = false,
|
|
272
272
|
threads = true,
|
|
273
273
|
) => {
|
|
274
|
+
// Threaded builds always get a pre-created worker pool (see
|
|
275
|
+
// `reuseWorkerOption` below), and pool pre-creation is asynchronous, so
|
|
276
|
+
// they always initialize asynchronously.
|
|
277
|
+
const effectiveAsyncInit = asyncInit || threads
|
|
274
278
|
const fsImport = fs
|
|
275
279
|
? buffer
|
|
276
280
|
? `import { memfs, Buffer } from '@napi-rs/wasm-runtime/fs'`
|
|
@@ -317,17 +321,55 @@ const __wasi = new __WASI({
|
|
|
317
321
|
const emnapiInjectBuffer = buffer
|
|
318
322
|
? ' __emnapiContext.features.Buffer = Buffer\n'
|
|
319
323
|
: ''
|
|
320
|
-
const emnapiInstantiateImport =
|
|
324
|
+
const emnapiInstantiateImport = effectiveAsyncInit
|
|
321
325
|
? `instantiateNapiModule as __emnapiInstantiateNapiModule`
|
|
322
326
|
: `instantiateNapiModuleSync as __emnapiInstantiateNapiModuleSync`
|
|
323
|
-
const emnapiInstantiateCall =
|
|
327
|
+
const emnapiInstantiateCall = effectiveAsyncInit
|
|
324
328
|
? `await __emnapiInstantiateNapiModule`
|
|
325
329
|
: `__emnapiInstantiateNapiModuleSync`
|
|
330
|
+
// The `reuseWorker` pool is what lets addon Rust code spawn threads while
|
|
331
|
+
// the calling thread is blocked inside the wasm call: a browser cannot
|
|
332
|
+
// start a worker until the blocking thread returns to its event loop, so
|
|
333
|
+
// a thread spawned mid-call can never boot and the caller deadlocks
|
|
334
|
+
// waiting for it. With a pre-created pool, spawning is only a message to
|
|
335
|
+
// an already-running worker.
|
|
336
|
+
//
|
|
337
|
+
// Its size comes from `navigator.hardwareConcurrency` at runtime (logical
|
|
338
|
+
// cores, floored at 2, with a fallback for privacy-fuzzed or missing
|
|
339
|
+
// values): a constant undersizes both ends of the range — big desktops
|
|
340
|
+
// leave parallelism on the table, and a fuzzed "2 cores" would
|
|
341
|
+
// oversubscribe.
|
|
342
|
+
//
|
|
343
|
+
// The reuse pool is sized as `__asyncWorkPoolSize + __workerPoolSize`
|
|
344
|
+
// because emnapi's async-work pool draws its workers from the SAME reuse
|
|
345
|
+
// pool: the async reservation must be included or async-work
|
|
346
|
+
// initialization can starve the reuse pool before addon threads spawn.
|
|
347
|
+
//
|
|
348
|
+
// `strict` is deliberately NOT set. Review suggested it so exhaustion
|
|
349
|
+
// errors instead of allocating a fresh worker, but it breaks
|
|
350
|
+
// spawn-and-return workloads (e.g. `testWorkers` in examples/napi, which
|
|
351
|
+
// spawns workers and joins them on a helper thread): at exhaustion their
|
|
352
|
+
// `std::thread::spawn` panics on EAGAIN. Without `strict` the fallback
|
|
353
|
+
// allocates a fresh worker, which boots normally once the spawning
|
|
354
|
+
// parent returns to its event loop — and for joins inside a blocked
|
|
355
|
+
// call, the pre-created pool is what those calls draw from anyway.
|
|
356
|
+
const workerPoolSizeBinding = threads
|
|
357
|
+
? `const __asyncWorkPoolSize = 4
|
|
358
|
+
const __workerPoolSize = Math.max(
|
|
359
|
+
2,
|
|
360
|
+
globalThis.navigator?.hardwareConcurrency ?? 4,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
`
|
|
364
|
+
: ''
|
|
365
|
+
const reuseWorkerOption = threads
|
|
366
|
+
? ` reuseWorker: { size: __asyncWorkPoolSize + __workerPoolSize },\n`
|
|
367
|
+
: ''
|
|
326
368
|
const workerRuntimeImport = threads
|
|
327
369
|
? ` createOnMessage as __wasmCreateOnMessageForFsProxy,\n`
|
|
328
370
|
: ''
|
|
329
371
|
const memoryName = threads ? '__sharedMemory' : '__wasmMemory'
|
|
330
|
-
const asyncWorkPoolOption = ` asyncWorkPoolSize: ${threads ?
|
|
372
|
+
const asyncWorkPoolOption = ` asyncWorkPoolSize: ${threads ? '__asyncWorkPoolSize' : 0},
|
|
331
373
|
`
|
|
332
374
|
// Every build links a "basic" emnapi archive without the C async-work and
|
|
333
375
|
// threadsafe-function implementations (the `emnapi-napi-rs(-mt)` archives shipped by the emnapi package), so the
|
|
@@ -380,7 +422,7 @@ const ${memoryName} = new WebAssembly.Memory({
|
|
|
380
422
|
maximum: ${maximumMemory},
|
|
381
423
|
${threads ? ' shared: true,\n' : ''}\
|
|
382
424
|
})
|
|
383
|
-
|
|
425
|
+
${workerPoolSizeBinding}\
|
|
384
426
|
let __emnapiContext
|
|
385
427
|
${emnapiContextLifecycle}
|
|
386
428
|
let __wasiModule
|
|
@@ -397,6 +439,7 @@ try {
|
|
|
397
439
|
} = ${emnapiInstantiateCall}(__wasmFile, {
|
|
398
440
|
context: __emnapiContext,
|
|
399
441
|
${asyncWorkPoolOption}\
|
|
442
|
+
${reuseWorkerOption}\
|
|
400
443
|
${emnapiPluginOption}\
|
|
401
444
|
wasi: __wasi,
|
|
402
445
|
${workerOption}\
|
package/src/utils/config.ts
CHANGED
|
@@ -72,10 +72,29 @@ export interface UserNapiConfig {
|
|
|
72
72
|
*/
|
|
73
73
|
maximumMemory?: number
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Whether the generated `<packageName>-wasm32-wasi` package is declared as
|
|
77
|
+
* an `optionalDependency` of the root package.
|
|
78
|
+
*
|
|
79
|
+
* When native targets are configured the WASI package is a fallback for
|
|
80
|
+
* hosts that cannot load a `.node` binary, so declaring it would make every
|
|
81
|
+
* consumer download the `.wasm` binary they will never load. It is
|
|
82
|
+
* therefore omitted by default and is expected to be installed on demand by
|
|
83
|
+
* the environments that need it.
|
|
84
|
+
*
|
|
85
|
+
* When WASI is the only configured target it is the primary artifact and is
|
|
86
|
+
* declared by default.
|
|
87
|
+
*
|
|
88
|
+
* Set this explicitly to override either default.
|
|
89
|
+
*
|
|
90
|
+
* @default true when every configured target is WASI, false otherwise
|
|
91
|
+
*/
|
|
92
|
+
optionalDependency?: boolean
|
|
93
|
+
|
|
75
94
|
/**
|
|
76
95
|
* Browser wasm binding configuration
|
|
77
96
|
*/
|
|
78
|
-
browser
|
|
97
|
+
browser?: {
|
|
79
98
|
/**
|
|
80
99
|
* Whether to use fs module in browser
|
|
81
100
|
*/
|