@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.
@@ -1,6 +1,9 @@
1
1
  import test from 'ava'
2
2
 
3
- import { resolveRootOptionalDependencies } from '../pre-publish.js'
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 { createWasiBrowserBinding } from '../templates/load-wasi-template.js'
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')
@@ -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
- const packResult = JSON.parse(output) as {
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[],