@napi-rs/cli 3.8.2 → 3.8.4
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 +7 -4
- package/dist/cli.js +527 -25
- package/dist/index.cjs +527 -25
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +527 -25
- package/docs/wasi.md +14 -0
- package/package.json +2 -2
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +795 -18
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/pre-publish.spec.ts +36 -1
- package/src/api/__tests__/templates.spec.ts +133 -1
- package/src/api/pre-publish.ts +18 -12
- package/src/api/templates/load-wasi-template.ts +546 -17
|
Binary file
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import test from 'ava'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
parseNpmPackFiles,
|
|
5
|
+
resolveRootOptionalDependencies,
|
|
6
|
+
} from '../pre-publish.js'
|
|
4
7
|
import { parseTriple } from '../../utils/index.js'
|
|
5
8
|
|
|
6
9
|
const PACKAGE_NAME = '@scope/pkg'
|
|
@@ -104,3 +107,35 @@ test('preserves unmanaged optionalDependencies', (t) => {
|
|
|
104
107
|
},
|
|
105
108
|
)
|
|
106
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
|
+
})
|
|
@@ -2,7 +2,11 @@ import ava, { type ExecutionContext } from 'ava'
|
|
|
2
2
|
import { parseSync } from 'oxc-parser'
|
|
3
3
|
|
|
4
4
|
import { createCjsBinding, createEsmBinding } from '../templates/js-binding.js'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createWasiBinding,
|
|
7
|
+
createWasiBrowserBinding,
|
|
8
|
+
createWasiDeferredBrowserBinding,
|
|
9
|
+
} from '../templates/load-wasi-template.js'
|
|
6
10
|
import { createWasiBrowserWorkerBinding } from '../templates/wasi-worker-template.js'
|
|
7
11
|
|
|
8
12
|
const test = ava
|
|
@@ -13,6 +17,54 @@ test('createWasiBrowserBinding default', (t) => {
|
|
|
13
17
|
t.snapshot(createWasiBrowserBinding('test-wasi'))
|
|
14
18
|
})
|
|
15
19
|
|
|
20
|
+
test('createWasiBrowserBinding threaded builds size worker pools from hardwareConcurrency', (t) => {
|
|
21
|
+
const binding = createWasiBrowserBinding(
|
|
22
|
+
'test-wasi',
|
|
23
|
+
4000,
|
|
24
|
+
65536,
|
|
25
|
+
false,
|
|
26
|
+
false, // asyncInit: false — threaded builds still init asynchronously
|
|
27
|
+
false,
|
|
28
|
+
false,
|
|
29
|
+
true,
|
|
30
|
+
)
|
|
31
|
+
t.true(binding.includes('const __asyncWorkPoolSize = 4'))
|
|
32
|
+
t.true(binding.includes('const __workerPoolSize = Math.max('))
|
|
33
|
+
t.true(binding.includes('globalThis.navigator?.hardwareConcurrency ?? 4'))
|
|
34
|
+
// The reuse pool includes the async-work reservation: the async pool
|
|
35
|
+
// draws from the same reuse pool, so without it exhaustion would starve
|
|
36
|
+
// addon thread spawns. No `strict`: at exhaustion the fallback worker
|
|
37
|
+
// boots once the parent returns to its event loop, which is the correct
|
|
38
|
+
// behavior for spawn-and-return workloads.
|
|
39
|
+
t.true(
|
|
40
|
+
binding.includes(
|
|
41
|
+
'reuseWorker: { size: __asyncWorkPoolSize + __workerPoolSize },',
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
t.false(binding.includes('strict'))
|
|
45
|
+
t.true(binding.includes('asyncWorkPoolSize: __asyncWorkPoolSize,'))
|
|
46
|
+
t.true(binding.includes('await __emnapiInstantiateNapiModule('))
|
|
47
|
+
t.false(binding.includes('__emnapiInstantiateNapiModuleSync(__wasmFile'))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('createWasiBrowserBinding threadless keeps sync init and no pool', (t) => {
|
|
51
|
+
const binding = createWasiBrowserBinding(
|
|
52
|
+
'test-wasi',
|
|
53
|
+
4000,
|
|
54
|
+
65536,
|
|
55
|
+
false,
|
|
56
|
+
false,
|
|
57
|
+
false,
|
|
58
|
+
false,
|
|
59
|
+
false,
|
|
60
|
+
)
|
|
61
|
+
t.false(binding.includes('__workerPoolSize'))
|
|
62
|
+
t.false(binding.includes('hardwareConcurrency'))
|
|
63
|
+
t.false(binding.includes('reuseWorker'))
|
|
64
|
+
t.true(binding.includes('asyncWorkPoolSize: 0,'))
|
|
65
|
+
t.true(binding.includes('__emnapiInstantiateNapiModuleSync(__wasmFile'))
|
|
66
|
+
})
|
|
67
|
+
|
|
16
68
|
test('createWasiBrowserBinding with errorEvent', (t) => {
|
|
17
69
|
t.snapshot(
|
|
18
70
|
createWasiBrowserBinding(
|
|
@@ -152,6 +204,86 @@ for (const { name, code } of cjsBindingCases) {
|
|
|
152
204
|
})
|
|
153
205
|
}
|
|
154
206
|
|
|
207
|
+
// `napi_prepare_wasm_env_cleanup` only *queues* the promise settlements of the
|
|
208
|
+
// tasks it cancels: `napi_call_threadsafe_function` appends to the
|
|
209
|
+
// threadsafe-function queue, and @emnapi/core dispatches that queue from a
|
|
210
|
+
// macrotask two coalescing turns later. `Context.destroy()` then drains the
|
|
211
|
+
// queue with a null env and discards it. Measured against @emnapi/core
|
|
212
|
+
// 2.0.0-alpha.3: zero microtask checkpoints ever deliver the settlement, and it
|
|
213
|
+
// takes exactly two macrotask turns.
|
|
214
|
+
//
|
|
215
|
+
// So a loader that emits the barrier and `destroy()` back to back strands
|
|
216
|
+
// exactly the promises the barrier exists to settle, and it does so silently.
|
|
217
|
+
// `examples/custom-async-runtime` covers the hand-written loader and the two
|
|
218
|
+
// emitted flavors it builds (node cjs threadless, deferred/workerd)
|
|
219
|
+
// behaviorally; the other flavors are never instantiated by any test, so assert
|
|
220
|
+
// the shape here as well.
|
|
221
|
+
const wasiLoaderCases: Array<{ name: string; code: string }> = [
|
|
222
|
+
{ name: 'node cjs', code: createWasiBinding('test', '@scope/test') },
|
|
223
|
+
{
|
|
224
|
+
name: 'node cjs threadless',
|
|
225
|
+
code: createWasiBinding('test', '@scope/test', 4000, 65536, false),
|
|
226
|
+
},
|
|
227
|
+
{ name: 'browser esm', code: createWasiBrowserBinding('test') },
|
|
228
|
+
{ name: 'deferred/workerd', code: createWasiDeferredBrowserBinding('test') },
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
for (const { name, code } of wasiLoaderCases) {
|
|
232
|
+
test(`WASI loader waits for queued settlements before destroy: ${name}`, (t) => {
|
|
233
|
+
t.true(
|
|
234
|
+
code.includes('napi_prepare_wasm_env_cleanup'),
|
|
235
|
+
'loader must run the pre-teardown barrier',
|
|
236
|
+
)
|
|
237
|
+
t.true(
|
|
238
|
+
code.includes('napi_wasm_env_cleanup_pending'),
|
|
239
|
+
'loader must poll napi_wasm_env_cleanup_pending; without it the barrier queues settlements that Context.destroy() then discards',
|
|
240
|
+
)
|
|
241
|
+
t.true(
|
|
242
|
+
code.includes('__drainWasmEnvCleanup'),
|
|
243
|
+
'loader must call the settlement drain on its disposal path',
|
|
244
|
+
)
|
|
245
|
+
t.true(
|
|
246
|
+
code.includes('__scheduleMacrotask'),
|
|
247
|
+
'the drain must yield real macrotask turns; microtask checkpoints never let the emnapi dispatch run',
|
|
248
|
+
)
|
|
249
|
+
// Initialization rollback is the same hazard, one step earlier. Registration
|
|
250
|
+
// runs with a live environment, so a module-init hook can start a task and
|
|
251
|
+
// *then* return an error; the barrier cancels the task and queues its
|
|
252
|
+
// rejection, and a rollback that destroys the context in the same turn
|
|
253
|
+
// discards it — stranding a promise that already escaped into JavaScript.
|
|
254
|
+
t.true(
|
|
255
|
+
initializationRollbackBody(code).includes(
|
|
256
|
+
DRAIN_CALL_BY_ROLLBACK_FLAVOR[
|
|
257
|
+
code.includes(EAGER_ROLLBACK_SIGNATURE) ? 'eager' : 'deferred'
|
|
258
|
+
],
|
|
259
|
+
),
|
|
260
|
+
'initialization rollback must drain queued settlements before destroying the context',
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const EAGER_ROLLBACK_SIGNATURE = 'function __rollbackWasiInitialization() {'
|
|
266
|
+
const DEFERRED_ROLLBACK_SIGNATURE = "__lifecycleState = 'failed'"
|
|
267
|
+
const DRAIN_CALL_BY_ROLLBACK_FLAVOR = {
|
|
268
|
+
eager: '__drainWasmEnvCleanup',
|
|
269
|
+
deferred: '__prepareForDisposal',
|
|
270
|
+
} as const
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The body of a loader's initialization-failure path: `__rollbackWasiInitialization`
|
|
274
|
+
* for the eager loaders, `__createInstance`'s catch for the deferred one.
|
|
275
|
+
* Sliced rather than searched whole-file, so a drain that only runs on the
|
|
276
|
+
* ordinary disposal path cannot satisfy the assertion.
|
|
277
|
+
*/
|
|
278
|
+
function initializationRollbackBody(code: string): string {
|
|
279
|
+
const eagerStart = code.indexOf(EAGER_ROLLBACK_SIGNATURE)
|
|
280
|
+
if (eagerStart !== -1) {
|
|
281
|
+
return code.slice(eagerStart, code.indexOf('\n}', eagerStart))
|
|
282
|
+
}
|
|
283
|
+
const deferredStart = code.indexOf(DEFERRED_ROLLBACK_SIGNATURE)
|
|
284
|
+
return code.slice(deferredStart, code.indexOf('throw error', deferredStart))
|
|
285
|
+
}
|
|
286
|
+
|
|
155
287
|
test('createEsmBinding is Node 12 compatible', (t) => {
|
|
156
288
|
const code = createEsmBinding('test', '@scope/test', ['sum'])
|
|
157
289
|
assertValidJS(t, code, 'esm')
|
package/src/api/pre-publish.ts
CHANGED
|
@@ -1691,18 +1691,7 @@ function readNpmPackFiles(packageDir: string, packageDescription: string) {
|
|
|
1691
1691
|
maxBuffer: 64 * 1024 * 1024,
|
|
1692
1692
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1693
1693
|
})
|
|
1694
|
-
|
|
1695
|
-
files?: { path?: unknown }[]
|
|
1696
|
-
}[]
|
|
1697
|
-
if (!Array.isArray(packResult) || !Array.isArray(packResult[0]?.files)) {
|
|
1698
|
-
throw new Error('npm pack returned an unexpected JSON result')
|
|
1699
|
-
}
|
|
1700
|
-
return new Set(
|
|
1701
|
-
packResult[0].files
|
|
1702
|
-
.map(({ path }) => path)
|
|
1703
|
-
.filter((path): path is string => typeof path === 'string')
|
|
1704
|
-
.map((path) => path.replaceAll('\\', '/').replace(/^\.\//, '')),
|
|
1705
|
-
)
|
|
1694
|
+
return parseNpmPackFiles(output)
|
|
1706
1695
|
} catch (error) {
|
|
1707
1696
|
throw new Error(
|
|
1708
1697
|
`Failed to validate the ${packageDescription} with npm pack --dry-run. Ensure npm is available and the package can be packed.`,
|
|
@@ -1711,6 +1700,23 @@ function readNpmPackFiles(packageDir: string, packageDescription: string) {
|
|
|
1711
1700
|
}
|
|
1712
1701
|
}
|
|
1713
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
|
+
|
|
1714
1720
|
function validateRootFacadePacklist(
|
|
1715
1721
|
rootDir: string,
|
|
1716
1722
|
rootPackageFiles: string[],
|