@napi-rs/cli 3.10.7 → 3.10.8

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,11 +1,12 @@
1
1
  import { existsSync } from 'node:fs'
2
- import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
2
+ import { mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
 
6
6
  import ava, { type TestFn } from 'ava'
7
7
 
8
8
  import { collectArtifacts } from '../artifacts.js'
9
+ import { WASI_ARTIFACT_METADATA_PREFIX } from '../build.js'
9
10
 
10
11
  const test = ava as TestFn<{ tmpDir: string }>
11
12
 
@@ -110,3 +111,1645 @@ test('resolves a relative WASI build output directory from cwd', async (t) => {
110
111
  ),
111
112
  )
112
113
  })
114
+
115
+ test('keeps the generated WASI root entry when package.json#main is a handwritten wrapper', async (t) => {
116
+ const { tmpDir } = t.context
117
+ const binaryName = 'mixed-entry'
118
+ const packageName = '@napi-rs/mixed-entry'
119
+ const artifactsDir = join(tmpDir, 'artifacts')
120
+
121
+ // Mirrors packages like @node-rs/bcrypt: `main` points at a handwritten
122
+ // `index.js` wrapper while `napi build --js binding.js` generates the
123
+ // loader under a different name recorded as `rootEntry` in the WASI
124
+ // loader metadata.
125
+ await mkdir(artifactsDir, { recursive: true })
126
+ await writeFile(
127
+ join(tmpDir, 'package.json'),
128
+ JSON.stringify({
129
+ name: packageName,
130
+ version: '0.0.0',
131
+ main: 'index.js',
132
+ napi: {
133
+ binaryName,
134
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
135
+ },
136
+ }),
137
+ )
138
+
139
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
140
+ version: 2,
141
+ rootEntry: 'binding.js',
142
+ exports: ['create'],
143
+ managedRootEntries: [
144
+ 'browser.js',
145
+ 'binding.js',
146
+ `${binaryName}.wasm`,
147
+ `${binaryName}.debug.wasm`,
148
+ ],
149
+ })}\nmodule.exports = {}\n`
150
+
151
+ await Promise.all([
152
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
153
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
154
+ writeFile(join(artifactsDir, `${binaryName}.wasi.cjs`), loader),
155
+ writeFile(
156
+ join(artifactsDir, `${binaryName}.wasi.d.cts`),
157
+ 'declare const _default: {}\nexport = _default\n',
158
+ ),
159
+ writeFile(
160
+ join(artifactsDir, `${binaryName}.wasi-browser.js`),
161
+ 'export {}\n',
162
+ ),
163
+ writeFile(join(artifactsDir, 'wasi-worker.mjs'), 'export {}\n'),
164
+ writeFile(join(artifactsDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
165
+ // The generated loader sits at the package root alongside the wrapper,
166
+ // next to neither the `.node` artifact nor `package.json#main`.
167
+ writeFile(join(tmpDir, 'binding.js'), 'module.exports = {}\n'),
168
+ writeFile(join(tmpDir, 'index.js'), 'module.exports = {}\n'),
169
+ ])
170
+
171
+ await collectArtifacts({ cwd: tmpDir })
172
+
173
+ // `binding.js` is a managed root entry that `package.json#main` does not
174
+ // cover; it must be kept rather than swept as a stale destination.
175
+ t.is(
176
+ await readFile(join(tmpDir, 'binding.js'), 'utf8'),
177
+ 'module.exports = {}\n',
178
+ )
179
+ t.is(
180
+ await readFile(join(tmpDir, 'index.js'), 'utf8'),
181
+ 'module.exports = {}\n',
182
+ )
183
+ })
184
+
185
+ test('the WASI artifact source wins over a stale package-root root entry', async (t) => {
186
+ const { tmpDir } = t.context
187
+ const binaryName = 'mixed-entry'
188
+ const packageName = '@napi-rs/mixed-entry'
189
+ const artifactsDir = join(tmpDir, 'artifacts')
190
+ const buildOutputDir = join(tmpDir, 'build-output')
191
+
192
+ // Same layout as above but with --build-output-dir: binaries land in
193
+ // `artifacts`, the generated WASI loader set including `binding.js` lands
194
+ // in `build-output`, and the package root holds a stale `binding.js` next
195
+ // to the handwritten `index.js` wrapper. A second stale copy beside the
196
+ // `.node` artifacts must not suppress the WASI-source copy either.
197
+ await mkdir(artifactsDir, { recursive: true })
198
+ await mkdir(buildOutputDir, { recursive: true })
199
+ await writeFile(
200
+ join(tmpDir, 'package.json'),
201
+ JSON.stringify({
202
+ name: packageName,
203
+ version: '0.0.0',
204
+ main: 'index.js',
205
+ napi: {
206
+ binaryName,
207
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
208
+ },
209
+ }),
210
+ )
211
+
212
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
213
+ version: 2,
214
+ rootEntry: 'binding.js',
215
+ exports: ['create'],
216
+ managedRootEntries: ['browser.js', 'binding.js'],
217
+ })}\nmodule.exports = {}\n`
218
+ const freshBinding = 'module.exports = { fresh: true }\n'
219
+
220
+ await Promise.all([
221
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
222
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
223
+ writeFile(
224
+ join(artifactsDir, 'binding.js'),
225
+ 'module.exports = { stale: "native-adjacent" }\n',
226
+ ),
227
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
228
+ writeFile(
229
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
230
+ 'declare const _default: {}\nexport = _default\n',
231
+ ),
232
+ writeFile(
233
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
234
+ 'export {}\n',
235
+ ),
236
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
237
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
238
+ writeFile(join(buildOutputDir, 'binding.js'), freshBinding),
239
+ writeFile(join(tmpDir, 'binding.js'), 'module.exports = { stale: true }\n'),
240
+ writeFile(join(tmpDir, 'index.js'), 'module.exports = {}\n'),
241
+ ])
242
+
243
+ await collectArtifacts({
244
+ cwd: tmpDir,
245
+ buildOutputDir: 'build-output',
246
+ })
247
+
248
+ // The artifact-source copy of the generated loader must replace the stale
249
+ // package-root file; the handwritten wrapper is untouched.
250
+ t.is(await readFile(join(tmpDir, 'binding.js'), 'utf8'), freshBinding)
251
+ t.is(
252
+ await readFile(join(tmpDir, 'index.js'), 'utf8'),
253
+ 'module.exports = {}\n',
254
+ )
255
+ })
256
+
257
+ test('a WASI-source root entry never replaces the shared native main loader', async (t) => {
258
+ const { tmpDir } = t.context
259
+ const binaryName = 'same-entry'
260
+ const packageName = '@napi-rs/same-entry'
261
+ const artifactsDir = join(tmpDir, 'artifacts')
262
+ const buildOutputDir = join(tmpDir, 'build-output')
263
+
264
+ // The default layout: `main` is the generated loader `index.js`, which the
265
+ // WASI metadata also records as rootEntry. The `index.js` inside the WASI
266
+ // artifact source is the WASI loader (`*.wasi.cjs` chain), so it must lose
267
+ // to the native loader copies even though all three differ.
268
+ await mkdir(artifactsDir, { recursive: true })
269
+ await mkdir(buildOutputDir, { recursive: true })
270
+ await writeFile(
271
+ join(tmpDir, 'package.json'),
272
+ JSON.stringify({
273
+ name: packageName,
274
+ version: '0.0.0',
275
+ main: 'index.js',
276
+ napi: {
277
+ binaryName,
278
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
279
+ },
280
+ }),
281
+ )
282
+
283
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
284
+ version: 2,
285
+ rootEntry: 'index.js',
286
+ exports: ['create'],
287
+ managedRootEntries: ['browser.js', 'index.js'],
288
+ })}\nmodule.exports = {}\n`
289
+
290
+ await Promise.all([
291
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
292
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
293
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
294
+ writeFile(
295
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
296
+ 'declare const _default: {}\nexport = _default\n',
297
+ ),
298
+ writeFile(
299
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
300
+ 'export {}\n',
301
+ ),
302
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
303
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
304
+ writeFile(
305
+ join(buildOutputDir, 'index.js'),
306
+ "module.exports = require('./same-entry.wasi.cjs')\n",
307
+ ),
308
+ writeFile(join(tmpDir, 'index.js'), 'module.exports = { native: true }\n'),
309
+ ])
310
+
311
+ await collectArtifacts({
312
+ cwd: tmpDir,
313
+ buildOutputDir: 'build-output',
314
+ })
315
+
316
+ // The committed native loader survives; the WASI-source copy of the same
317
+ // name must not overwrite it.
318
+ t.is(
319
+ await readFile(join(tmpDir, 'index.js'), 'utf8'),
320
+ 'module.exports = { native: true }\n',
321
+ )
322
+ })
323
+
324
+ test('a package main like ./binding.js aliases the same root destination', async (t) => {
325
+ const { tmpDir } = t.context
326
+ const binaryName = 'alias-entry'
327
+ const packageName = '@napi-rs/alias-entry'
328
+ const artifactsDir = join(tmpDir, 'artifacts')
329
+ const buildOutputDir = join(tmpDir, 'build-output')
330
+
331
+ // `main: "./binding.js"` and metadata `rootEntry: "binding.js"` resolve to
332
+ // the same package-root path, so the entry is the shared native loader and
333
+ // the WASI-source copy must not claim it.
334
+ await mkdir(artifactsDir, { recursive: true })
335
+ await mkdir(buildOutputDir, { recursive: true })
336
+ await writeFile(
337
+ join(tmpDir, 'package.json'),
338
+ JSON.stringify({
339
+ name: packageName,
340
+ version: '0.0.0',
341
+ main: './binding.js',
342
+ napi: {
343
+ binaryName,
344
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
345
+ },
346
+ }),
347
+ )
348
+
349
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
350
+ version: 2,
351
+ rootEntry: 'binding.js',
352
+ exports: ['create'],
353
+ managedRootEntries: ['browser.js', 'binding.js'],
354
+ })}\nmodule.exports = {}\n`
355
+
356
+ await Promise.all([
357
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
358
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
359
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
360
+ writeFile(
361
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
362
+ 'declare const _default: {}\nexport = _default\n',
363
+ ),
364
+ writeFile(
365
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
366
+ 'export {}\n',
367
+ ),
368
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
369
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
370
+ writeFile(
371
+ join(buildOutputDir, 'binding.js'),
372
+ "module.exports = require('./alias-entry.wasi.cjs')\n",
373
+ ),
374
+ writeFile(
375
+ join(tmpDir, 'binding.js'),
376
+ 'module.exports = { native: true }\n',
377
+ ),
378
+ ])
379
+
380
+ await collectArtifacts({
381
+ cwd: tmpDir,
382
+ buildOutputDir: 'build-output',
383
+ })
384
+
385
+ t.is(
386
+ await readFile(join(tmpDir, 'binding.js'), 'utf8'),
387
+ 'module.exports = { native: true }\n',
388
+ )
389
+ })
390
+
391
+ test('a symlinked parent cannot redirect a root entry outside the package root', async (t) => {
392
+ const { tmpDir } = t.context
393
+ const binaryName = 'link-entry'
394
+ const packageName = '@napi-rs/link-entry'
395
+ const artifactsDir = join(tmpDir, 'artifacts')
396
+ const siblingDir = join(tmpDir, '..', `sibling-${Date.now()}`)
397
+
398
+ // `nested/binding.js` passes the lexical `..` check, but `nested` is a
399
+ // symlink to a sibling directory, so the canonical destination leaves the
400
+ // package root even though it stays inside the transaction boundary.
401
+ await mkdir(artifactsDir, { recursive: true })
402
+ await mkdir(siblingDir, { recursive: true })
403
+ await symlink(siblingDir, join(tmpDir, 'nested'))
404
+ await writeFile(
405
+ join(tmpDir, 'package.json'),
406
+ JSON.stringify({
407
+ name: packageName,
408
+ version: '0.0.0',
409
+ main: 'index.js',
410
+ napi: {
411
+ binaryName,
412
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
413
+ },
414
+ }),
415
+ )
416
+
417
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
418
+ version: 2,
419
+ rootEntry: 'nested/binding.js',
420
+ exports: ['create'],
421
+ managedRootEntries: ['browser.js', 'nested/binding.js'],
422
+ })}\nmodule.exports = {}\n`
423
+
424
+ await Promise.all([
425
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
426
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
427
+ writeFile(join(artifactsDir, `${binaryName}.wasi.cjs`), loader),
428
+ writeFile(
429
+ join(artifactsDir, `${binaryName}.wasi.d.cts`),
430
+ 'declare const _default: {}\nexport = _default\n',
431
+ ),
432
+ writeFile(
433
+ join(artifactsDir, `${binaryName}.wasi-browser.js`),
434
+ 'export {}\n',
435
+ ),
436
+ writeFile(join(artifactsDir, 'wasi-worker.mjs'), 'export {}\n'),
437
+ writeFile(join(artifactsDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
438
+ writeFile(join(tmpDir, 'index.js'), 'module.exports = {}\n'),
439
+ ])
440
+
441
+ try {
442
+ await t.throwsAsync(collectArtifacts({ cwd: tmpDir }), {
443
+ message: /resolves outside the package root/,
444
+ })
445
+ t.false(existsSync(join(siblingDir, 'binding.js')))
446
+ } finally {
447
+ await rm(siblingDir, { recursive: true, force: true })
448
+ }
449
+ })
450
+
451
+ test('a WASI-declared index.js resolves from the artifact source when main is a different wrapper', async (t) => {
452
+ const { tmpDir } = t.context
453
+ const binaryName = 'index-entry'
454
+ const packageName = '@napi-rs/index-entry'
455
+ const artifactsDir = join(tmpDir, 'artifacts')
456
+ const buildOutputDir = join(tmpDir, 'build-output')
457
+
458
+ // `main` points at a handwritten `wrapper.js` and the WASI loader metadata
459
+ // declares `index.js` as its generated root entry. `index.js` is only a
460
+ // shared name when it is the effective main; here it is a distinct WASI
461
+ // root entry that must resolve from the build output, not from the stale
462
+ // package-root copy.
463
+ await mkdir(artifactsDir, { recursive: true })
464
+ await mkdir(buildOutputDir, { recursive: true })
465
+ await writeFile(
466
+ join(tmpDir, 'package.json'),
467
+ JSON.stringify({
468
+ name: packageName,
469
+ version: '0.0.0',
470
+ main: 'wrapper.js',
471
+ napi: {
472
+ binaryName,
473
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
474
+ },
475
+ }),
476
+ )
477
+
478
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
479
+ version: 2,
480
+ rootEntry: 'index.js',
481
+ exports: ['create'],
482
+ managedRootEntries: ['browser.js', 'index.js'],
483
+ })}\nmodule.exports = {}\n`
484
+ const freshIndex = "module.exports = require('./index-entry.wasi.cjs')\n"
485
+
486
+ await Promise.all([
487
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
488
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
489
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
490
+ writeFile(
491
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
492
+ 'declare const _default: {}\nexport = _default\n',
493
+ ),
494
+ writeFile(
495
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
496
+ 'export {}\n',
497
+ ),
498
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
499
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
500
+ writeFile(join(buildOutputDir, 'index.js'), freshIndex),
501
+ writeFile(join(tmpDir, 'index.js'), 'module.exports = { stale: true }\n'),
502
+ writeFile(
503
+ join(tmpDir, 'wrapper.js'),
504
+ 'module.exports = { native: true }\n',
505
+ ),
506
+ ])
507
+
508
+ await collectArtifacts({
509
+ cwd: tmpDir,
510
+ buildOutputDir: 'build-output',
511
+ })
512
+
513
+ // The WASI-source copy of the declared `index.js` replaces the stale
514
+ // package-root file; the handwritten wrapper stays untouched.
515
+ t.is(await readFile(join(tmpDir, 'index.js'), 'utf8'), freshIndex)
516
+ t.is(
517
+ await readFile(join(tmpDir, 'wrapper.js'), 'utf8'),
518
+ 'module.exports = { native: true }\n',
519
+ )
520
+ })
521
+
522
+ test('a distinct root entry follows the threaded root loader across WASI flavors', async (t) => {
523
+ const { tmpDir } = t.context
524
+ const binaryName = 'flavor-entry'
525
+ const packageName = '@napi-rs/flavor-entry'
526
+ const nativeDir = join(tmpDir, 'artifacts', 'native')
527
+ const threadlessDir = join(tmpDir, 'artifacts', 'wasip1')
528
+ const threadsDir = join(tmpDir, 'artifacts', 'threads')
529
+
530
+ // Both WASI flavors are configured, the threadless target first. The root
531
+ // loader comes from the threaded target, so the generated `binding.js`
532
+ // must come from the threaded source too: each flavor's copy embeds
533
+ // loader references for its own `*.wasi.cjs` chain.
534
+ await Promise.all([
535
+ mkdir(nativeDir, { recursive: true }),
536
+ mkdir(threadlessDir, { recursive: true }),
537
+ mkdir(threadsDir, { recursive: true }),
538
+ ])
539
+ await writeFile(
540
+ join(tmpDir, 'package.json'),
541
+ JSON.stringify({
542
+ name: packageName,
543
+ version: '0.0.0',
544
+ main: 'index.js',
545
+ napi: {
546
+ binaryName,
547
+ targets: [
548
+ 'x86_64-unknown-linux-gnu',
549
+ 'wasm32-wasip1',
550
+ 'wasm32-wasip1-threads',
551
+ ],
552
+ },
553
+ }),
554
+ )
555
+
556
+ const loaderFor = (rootEntryFlavor: string) =>
557
+ `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
558
+ version: 2,
559
+ rootEntry: 'binding.js',
560
+ exports: ['create'],
561
+ managedRootEntries: ['browser.js', 'binding.js'],
562
+ })}\nmodule.exports = { flavor: '${rootEntryFlavor}' }\n`
563
+
564
+ await Promise.all([
565
+ writeFile(join(nativeDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
566
+ writeFile(
567
+ join(threadsDir, `${binaryName}.wasm32-wasi.wasm`),
568
+ 'threads wasm',
569
+ ),
570
+ writeFile(join(threadsDir, `${binaryName}.wasi.cjs`), loaderFor('threads')),
571
+ writeFile(
572
+ join(threadsDir, `${binaryName}.wasi.d.cts`),
573
+ 'declare const _default: {}\nexport = _default\n',
574
+ ),
575
+ writeFile(join(threadsDir, `${binaryName}.wasi-browser.js`), 'export {}\n'),
576
+ writeFile(join(threadsDir, 'wasi-worker.mjs'), 'export {}\n'),
577
+ writeFile(join(threadsDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
578
+ writeFile(
579
+ join(threadsDir, 'binding.js'),
580
+ "module.exports = require('./flavor-entry.wasi.cjs')\n",
581
+ ),
582
+ writeFile(
583
+ join(threadlessDir, `${binaryName}.wasm32-wasip1.wasm`),
584
+ 'threadless wasm',
585
+ ),
586
+ writeFile(
587
+ join(threadlessDir, `${binaryName}.wasip1.cjs`),
588
+ loaderFor('wasip1'),
589
+ ),
590
+ writeFile(
591
+ join(threadlessDir, `${binaryName}.wasip1.d.cts`),
592
+ 'declare const _default: {}\nexport = _default\n',
593
+ ),
594
+ writeFile(
595
+ join(threadlessDir, `${binaryName}.wasip1-browser.js`),
596
+ 'export {}\n',
597
+ ),
598
+ writeFile(
599
+ join(threadlessDir, `${binaryName}.wasip1-deferred.js`),
600
+ 'export {}\n',
601
+ ),
602
+ writeFile(
603
+ join(threadlessDir, `${binaryName}.wasip1-deferred.d.ts`),
604
+ 'export {}\n',
605
+ ),
606
+ writeFile(
607
+ join(threadlessDir, 'binding.js'),
608
+ "module.exports = require('./flavor-entry.wasip1.cjs')\n",
609
+ ),
610
+ writeFile(join(tmpDir, 'index.js'), 'module.exports = { native: true }\n'),
611
+ ])
612
+
613
+ await collectArtifacts({ cwd: tmpDir })
614
+
615
+ // The published root entry must match the threaded root loader even
616
+ // though the threadless target is configured first.
617
+ t.is(
618
+ await readFile(join(tmpDir, 'binding.js'), 'utf8'),
619
+ "module.exports = require('./flavor-entry.wasi.cjs')\n",
620
+ )
621
+ })
622
+
623
+ test('a main specifier Node resolves to a declared root entry stays shared', async (t) => {
624
+ const { tmpDir } = t.context
625
+ const binaryName = 'extless-entry'
626
+ const packageName = '@napi-rs/extless-entry'
627
+ const artifactsDir = join(tmpDir, 'artifacts')
628
+ const buildOutputDir = join(tmpDir, 'build-output')
629
+
630
+ // CommonJS `main` resolution maps `./binding` to `binding.js`, so the
631
+ // WASI-declared `binding.js` is the native root entry — the WASI-source
632
+ // copy is the WASI loader chain and must never claim that destination.
633
+ await mkdir(artifactsDir, { recursive: true })
634
+ await mkdir(buildOutputDir, { recursive: true })
635
+ await writeFile(
636
+ join(tmpDir, 'package.json'),
637
+ JSON.stringify({
638
+ name: packageName,
639
+ version: '0.0.0',
640
+ main: './binding',
641
+ napi: {
642
+ binaryName,
643
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
644
+ },
645
+ }),
646
+ )
647
+
648
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
649
+ version: 2,
650
+ rootEntry: 'binding.js',
651
+ exports: ['create'],
652
+ managedRootEntries: ['browser.js', 'binding.js'],
653
+ })}\nmodule.exports = {}\n`
654
+
655
+ await Promise.all([
656
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
657
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
658
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
659
+ writeFile(
660
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
661
+ 'declare const _default: {}\nexport = _default\n',
662
+ ),
663
+ writeFile(
664
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
665
+ 'export {}\n',
666
+ ),
667
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
668
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
669
+ writeFile(
670
+ join(buildOutputDir, 'binding.js'),
671
+ "module.exports = require('./extless-entry.wasi.cjs')\n",
672
+ ),
673
+ writeFile(
674
+ join(tmpDir, 'binding.js'),
675
+ 'module.exports = { native: true }\n',
676
+ ),
677
+ ])
678
+
679
+ await collectArtifacts({
680
+ cwd: tmpDir,
681
+ buildOutputDir: 'build-output',
682
+ })
683
+
684
+ t.is(
685
+ await readFile(join(tmpDir, 'binding.js'), 'utf8'),
686
+ 'module.exports = { native: true }\n',
687
+ )
688
+ })
689
+
690
+ test('a directory main keeps its index.js shared with the native root entry', async (t) => {
691
+ const { tmpDir } = t.context
692
+ const binaryName = 'dir-entry'
693
+ const packageName = '@napi-rs/dir-entry'
694
+ const artifactsDir = join(tmpDir, 'artifacts')
695
+ const buildOutputDir = join(tmpDir, 'build-output')
696
+
697
+ // `main: "./dist"` resolves via the directory lookup to `dist/index.js`,
698
+ // so a WASI-declared `dist/index.js` is the native root entry and the
699
+ // WASI-source copy must not claim it.
700
+ await mkdir(artifactsDir, { recursive: true })
701
+ await mkdir(join(buildOutputDir, 'dist'), { recursive: true })
702
+ await mkdir(join(tmpDir, 'dist'), { recursive: true })
703
+ await writeFile(
704
+ join(tmpDir, 'package.json'),
705
+ JSON.stringify({
706
+ name: packageName,
707
+ version: '0.0.0',
708
+ main: './dist',
709
+ napi: {
710
+ binaryName,
711
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
712
+ },
713
+ }),
714
+ )
715
+
716
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
717
+ version: 2,
718
+ rootEntry: 'dist/index.js',
719
+ exports: ['create'],
720
+ managedRootEntries: ['browser.js', 'dist/index.js'],
721
+ })}\nmodule.exports = {}\n`
722
+
723
+ await Promise.all([
724
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
725
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
726
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
727
+ writeFile(
728
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
729
+ 'declare const _default: {}\nexport = _default\n',
730
+ ),
731
+ writeFile(
732
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
733
+ 'export {}\n',
734
+ ),
735
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
736
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
737
+ writeFile(
738
+ join(buildOutputDir, 'dist', 'index.js'),
739
+ "module.exports = require('./dir-entry.wasi.cjs')\n",
740
+ ),
741
+ writeFile(
742
+ join(tmpDir, 'dist', 'index.js'),
743
+ 'module.exports = { native: true }\n',
744
+ ),
745
+ ])
746
+
747
+ await collectArtifacts({
748
+ cwd: tmpDir,
749
+ buildOutputDir: 'build-output',
750
+ })
751
+
752
+ t.is(
753
+ await readFile(join(tmpDir, 'dist', 'index.js'), 'utf8'),
754
+ 'module.exports = { native: true }\n',
755
+ )
756
+ })
757
+
758
+ test('a file shadowing a directory main leaves the declared entry distinct', async (t) => {
759
+ const { tmpDir } = t.context
760
+ const binaryName = 'shadow-entry'
761
+ const packageName = '@napi-rs/shadow-entry'
762
+ const artifactsDir = join(tmpDir, 'artifacts')
763
+ const buildOutputDir = join(tmpDir, 'build-output')
764
+
765
+ // `main: "./dist"` resolves to `dist.js` because the file exists, so the
766
+ // directory lookup never runs and `dist/index.js` is not the package
767
+ // entry. The WASI-declared `dist/index.js` is a distinct generated file
768
+ // and resolves from the build output, replacing the stale root copy.
769
+ await mkdir(artifactsDir, { recursive: true })
770
+ await mkdir(join(buildOutputDir, 'dist'), { recursive: true })
771
+ await mkdir(join(tmpDir, 'dist'), { recursive: true })
772
+ await writeFile(
773
+ join(tmpDir, 'package.json'),
774
+ JSON.stringify({
775
+ name: packageName,
776
+ version: '0.0.0',
777
+ main: './dist',
778
+ napi: {
779
+ binaryName,
780
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
781
+ },
782
+ }),
783
+ )
784
+
785
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
786
+ version: 2,
787
+ rootEntry: 'dist/index.js',
788
+ exports: ['create'],
789
+ managedRootEntries: ['browser.js', 'dist/index.js'],
790
+ })}\nmodule.exports = {}\n`
791
+ const freshIndex = "module.exports = require('./shadow-entry.wasi.cjs')\n"
792
+
793
+ await Promise.all([
794
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
795
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
796
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
797
+ writeFile(
798
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
799
+ 'declare const _default: {}\nexport = _default\n',
800
+ ),
801
+ writeFile(
802
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
803
+ 'export {}\n',
804
+ ),
805
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
806
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
807
+ writeFile(join(buildOutputDir, 'dist', 'index.js'), freshIndex),
808
+ writeFile(join(tmpDir, 'dist.js'), 'module.exports = { native: true }\n'),
809
+ writeFile(
810
+ join(tmpDir, 'dist', 'index.js'),
811
+ 'module.exports = { stale: true }\n',
812
+ ),
813
+ ])
814
+
815
+ await collectArtifacts({
816
+ cwd: tmpDir,
817
+ buildOutputDir: 'build-output',
818
+ })
819
+
820
+ t.is(await readFile(join(tmpDir, 'dist', 'index.js'), 'utf8'), freshIndex)
821
+ t.is(
822
+ await readFile(join(tmpDir, 'dist.js'), 'utf8'),
823
+ 'module.exports = { native: true }\n',
824
+ )
825
+ })
826
+
827
+ test('a self-referential directory main resolves to index.js', async (t) => {
828
+ const { tmpDir } = t.context
829
+ const binaryName = 'self-entry'
830
+ const packageName = '@napi-rs/self-entry'
831
+ const artifactsDir = join(tmpDir, 'artifacts')
832
+
833
+ // `main: "."` names the package directory itself; its own package.json
834
+ // main points back at the same directory, which Node resolves as
835
+ // index.js. Collection must terminate instead of recursing through the
836
+ // same manifest forever.
837
+ await mkdir(artifactsDir, { recursive: true })
838
+ await writeFile(
839
+ join(tmpDir, 'package.json'),
840
+ JSON.stringify({
841
+ name: packageName,
842
+ version: '0.0.0',
843
+ main: '.',
844
+ napi: {
845
+ binaryName,
846
+ targets: ['x86_64-unknown-linux-gnu'],
847
+ },
848
+ }),
849
+ )
850
+
851
+ await Promise.all([
852
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
853
+ writeFile(join(tmpDir, 'index.js'), 'module.exports = { native: true }\n'),
854
+ ])
855
+
856
+ await collectArtifacts({ cwd: tmpDir })
857
+
858
+ t.is(
859
+ await readFile(join(tmpDir, 'index.js'), 'utf8'),
860
+ 'module.exports = { native: true }\n',
861
+ )
862
+ })
863
+
864
+ test('a main resolved through the native artifacts keeps the native loader', async (t) => {
865
+ const { tmpDir } = t.context
866
+ const binaryName = 'clean-entry'
867
+ const packageName = '@napi-rs/clean-entry'
868
+ const artifactsDir = join(tmpDir, 'artifacts')
869
+ const buildOutputDir = join(tmpDir, 'build-output')
870
+
871
+ // The package root has no pre-existing loader: `main: "./binding"`
872
+ // resolves to `binding.js`, which ships inside the native artifacts. The
873
+ // WASI metadata declares the same name, but the resolved entry is the
874
+ // native loader — publishing the WASI-source copy would make `require`
875
+ // of the package main load the WASM build.
876
+ await mkdir(artifactsDir, { recursive: true })
877
+ await mkdir(buildOutputDir, { recursive: true })
878
+ await writeFile(
879
+ join(tmpDir, 'package.json'),
880
+ JSON.stringify({
881
+ name: packageName,
882
+ version: '0.0.0',
883
+ main: './binding',
884
+ napi: {
885
+ binaryName,
886
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
887
+ },
888
+ }),
889
+ )
890
+
891
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
892
+ version: 2,
893
+ rootEntry: 'binding.js',
894
+ exports: ['create'],
895
+ managedRootEntries: ['browser.js', 'binding.js'],
896
+ })}\nmodule.exports = {}\n`
897
+ const nativeBinding =
898
+ "module.exports = require('./clean-entry.linux-x64-gnu.node')\n"
899
+
900
+ await Promise.all([
901
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
902
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
903
+ writeFile(join(artifactsDir, 'binding.js'), nativeBinding),
904
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
905
+ writeFile(
906
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
907
+ 'declare const _default: {}\nexport = _default\n',
908
+ ),
909
+ writeFile(
910
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
911
+ 'export {}\n',
912
+ ),
913
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
914
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
915
+ writeFile(
916
+ join(buildOutputDir, 'binding.js'),
917
+ "module.exports = require('./clean-entry.wasi.cjs')\n",
918
+ ),
919
+ ])
920
+
921
+ await collectArtifacts({
922
+ cwd: tmpDir,
923
+ buildOutputDir: 'build-output',
924
+ })
925
+
926
+ // The resolved main entry comes from the native artifacts, not the
927
+ // WASI-source copy of the same name.
928
+ t.is(await readFile(join(tmpDir, 'binding.js'), 'utf8'), nativeBinding)
929
+ })
930
+
931
+ test('an existing extensionless main shadows the declared extension variant', async (t) => {
932
+ const { tmpDir } = t.context
933
+ const binaryName = 'exact-entry'
934
+ const packageName = '@napi-rs/exact-entry'
935
+ const artifactsDir = join(tmpDir, 'artifacts')
936
+ const buildOutputDir = join(tmpDir, 'build-output')
937
+
938
+ // `main: "./binding"` resolves to the extensionless file `binding`
939
+ // because it exists, so `binding.js` is unreachable as the package
940
+ // entry. The WASI-declared `binding.js` is a distinct generated file and
941
+ // resolves from the build output, replacing the stale root copy.
942
+ await mkdir(artifactsDir, { recursive: true })
943
+ await mkdir(buildOutputDir, { recursive: true })
944
+ await writeFile(
945
+ join(tmpDir, 'package.json'),
946
+ JSON.stringify({
947
+ name: packageName,
948
+ version: '0.0.0',
949
+ main: './binding',
950
+ napi: {
951
+ binaryName,
952
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
953
+ },
954
+ }),
955
+ )
956
+
957
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
958
+ version: 2,
959
+ rootEntry: 'binding.js',
960
+ exports: ['create'],
961
+ managedRootEntries: ['browser.js', 'binding.js'],
962
+ })}\nmodule.exports = {}\n`
963
+ const freshBinding = "module.exports = require('./exact-entry.wasi.cjs')\n"
964
+
965
+ await Promise.all([
966
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
967
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
968
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
969
+ writeFile(
970
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
971
+ 'declare const _default: {}\nexport = _default\n',
972
+ ),
973
+ writeFile(
974
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
975
+ 'export {}\n',
976
+ ),
977
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
978
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
979
+ writeFile(join(buildOutputDir, 'binding.js'), freshBinding),
980
+ writeFile(join(tmpDir, 'binding'), 'module.exports = { exact: true }\n'),
981
+ writeFile(join(tmpDir, 'binding.js'), 'module.exports = { stale: true }\n'),
982
+ ])
983
+
984
+ await collectArtifacts({
985
+ cwd: tmpDir,
986
+ buildOutputDir: 'build-output',
987
+ })
988
+
989
+ t.is(await readFile(join(tmpDir, 'binding.js'), 'utf8'), freshBinding)
990
+ t.is(
991
+ await readFile(join(tmpDir, 'binding'), 'utf8'),
992
+ 'module.exports = { exact: true }\n',
993
+ )
994
+ })
995
+
996
+ test('an exports entry target shares its name with the native root entry', async (t) => {
997
+ const { tmpDir } = t.context
998
+ const binaryName = 'exports-entry'
999
+ const packageName = '@napi-rs/exports-entry'
1000
+ const artifactsDir = join(tmpDir, 'artifacts')
1001
+ const buildOutputDir = join(tmpDir, 'build-output')
1002
+
1003
+ // `exports["."]` resolves ahead of `main`, so `entry.js` is the package
1004
+ // entry even though `main` names a wrapper. The WASI metadata declares
1005
+ // the same name; the native artifact copy must win over the WASI-source
1006
+ // loader chain.
1007
+ await mkdir(artifactsDir, { recursive: true })
1008
+ await mkdir(buildOutputDir, { recursive: true })
1009
+ await writeFile(
1010
+ join(tmpDir, 'package.json'),
1011
+ JSON.stringify({
1012
+ name: packageName,
1013
+ version: '0.0.0',
1014
+ main: 'wrapper.js',
1015
+ exports: { '.': './entry.js' },
1016
+ napi: {
1017
+ binaryName,
1018
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
1019
+ },
1020
+ }),
1021
+ )
1022
+
1023
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
1024
+ version: 2,
1025
+ rootEntry: 'entry.js',
1026
+ exports: ['create'],
1027
+ managedRootEntries: ['browser.js', 'entry.js'],
1028
+ })}\nmodule.exports = {}\n`
1029
+ const nativeEntry =
1030
+ "module.exports = require('./exports-entry.linux-x64-gnu.node')\n"
1031
+
1032
+ await Promise.all([
1033
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
1034
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
1035
+ writeFile(join(artifactsDir, 'entry.js'), nativeEntry),
1036
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
1037
+ writeFile(
1038
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
1039
+ 'declare const _default: {}\nexport = _default\n',
1040
+ ),
1041
+ writeFile(
1042
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
1043
+ 'export {}\n',
1044
+ ),
1045
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
1046
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
1047
+ writeFile(
1048
+ join(buildOutputDir, 'entry.js'),
1049
+ "module.exports = require('./exports-entry.wasi.cjs')\n",
1050
+ ),
1051
+ writeFile(join(tmpDir, 'wrapper.js'), 'module.exports = { wrap: true }\n'),
1052
+ ])
1053
+
1054
+ await collectArtifacts({
1055
+ cwd: tmpDir,
1056
+ buildOutputDir: 'build-output',
1057
+ })
1058
+
1059
+ t.is(await readFile(join(tmpDir, 'entry.js'), 'utf8'), nativeEntry)
1060
+ })
1061
+
1062
+ test('a missing nested main falls back to the directory index', async (t) => {
1063
+ const { tmpDir } = t.context
1064
+ const binaryName = 'nested-miss'
1065
+ const packageName = '@napi-rs/nested-miss'
1066
+ const artifactsDir = join(tmpDir, 'artifacts')
1067
+ const buildOutputDir = join(tmpDir, 'build-output')
1068
+
1069
+ // `main: "./dist"` finds `dist/package.json` whose `main` points at a
1070
+ // file that does not exist. Node then falls back to `dist/index.js`, so
1071
+ // the WASI-declared `dist/index.js` shares the native root entry and the
1072
+ // WASI-source copy must not overwrite the existing native file.
1073
+ await mkdir(artifactsDir, { recursive: true })
1074
+ await mkdir(join(buildOutputDir, 'dist'), { recursive: true })
1075
+ await mkdir(join(tmpDir, 'dist'), { recursive: true })
1076
+ await writeFile(
1077
+ join(tmpDir, 'package.json'),
1078
+ JSON.stringify({
1079
+ name: packageName,
1080
+ version: '0.0.0',
1081
+ main: './dist',
1082
+ napi: {
1083
+ binaryName,
1084
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
1085
+ },
1086
+ }),
1087
+ )
1088
+ await writeFile(
1089
+ join(tmpDir, 'dist', 'package.json'),
1090
+ JSON.stringify({ main: './lib.js' }),
1091
+ )
1092
+
1093
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
1094
+ version: 2,
1095
+ rootEntry: 'dist/index.js',
1096
+ exports: ['create'],
1097
+ managedRootEntries: ['browser.js', 'dist/index.js'],
1098
+ })}\nmodule.exports = {}\n`
1099
+
1100
+ await Promise.all([
1101
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
1102
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
1103
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
1104
+ writeFile(
1105
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
1106
+ 'declare const _default: {}\nexport = _default\n',
1107
+ ),
1108
+ writeFile(
1109
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
1110
+ 'export {}\n',
1111
+ ),
1112
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
1113
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
1114
+ writeFile(
1115
+ join(buildOutputDir, 'dist', 'index.js'),
1116
+ "module.exports = require('./nested-miss.wasi.cjs')\n",
1117
+ ),
1118
+ writeFile(
1119
+ join(tmpDir, 'dist', 'index.js'),
1120
+ 'module.exports = { native: true }\n',
1121
+ ),
1122
+ ])
1123
+
1124
+ await collectArtifacts({
1125
+ cwd: tmpDir,
1126
+ buildOutputDir: 'build-output',
1127
+ })
1128
+
1129
+ t.is(
1130
+ await readFile(join(tmpDir, 'dist', 'index.js'), 'utf8'),
1131
+ 'module.exports = { native: true }\n',
1132
+ )
1133
+ })
1134
+
1135
+ test('a condition-only exports map shares the declared root entry', async (t) => {
1136
+ const { tmpDir } = t.context
1137
+ const binaryName = 'cond-entry'
1138
+ const packageName = '@napi-rs/cond-entry'
1139
+ const artifactsDir = join(tmpDir, 'artifacts')
1140
+ const buildOutputDir = join(tmpDir, 'build-output')
1141
+
1142
+ // A condition-only exports map has no '.' key but still declares the
1143
+ // package entry. The WASI-declared `binding.js` shares the native root
1144
+ // entry, so the WASI-source copy never claims it.
1145
+ await mkdir(artifactsDir, { recursive: true })
1146
+ await mkdir(buildOutputDir, { recursive: true })
1147
+ await writeFile(
1148
+ join(tmpDir, 'package.json'),
1149
+ JSON.stringify({
1150
+ name: packageName,
1151
+ version: '0.0.0',
1152
+ exports: { node: './binding.js', default: './binding.js' },
1153
+ napi: {
1154
+ binaryName,
1155
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
1156
+ },
1157
+ }),
1158
+ )
1159
+
1160
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
1161
+ version: 2,
1162
+ rootEntry: 'binding.js',
1163
+ exports: ['create'],
1164
+ managedRootEntries: ['browser.js', 'binding.js'],
1165
+ })}\nmodule.exports = {}\n`
1166
+ const nativeBinding =
1167
+ "module.exports = require('./cond-entry.linux-x64-gnu.node')\n"
1168
+
1169
+ await Promise.all([
1170
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
1171
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
1172
+ writeFile(join(artifactsDir, 'binding.js'), nativeBinding),
1173
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
1174
+ writeFile(
1175
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
1176
+ 'declare const _default: {}\nexport = _default\n',
1177
+ ),
1178
+ writeFile(
1179
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
1180
+ 'export {}\n',
1181
+ ),
1182
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
1183
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
1184
+ writeFile(
1185
+ join(buildOutputDir, 'binding.js'),
1186
+ "module.exports = require('./cond-entry.wasi.cjs')\n",
1187
+ ),
1188
+ ])
1189
+
1190
+ await collectArtifacts({
1191
+ cwd: tmpDir,
1192
+ buildOutputDir: 'build-output',
1193
+ })
1194
+
1195
+ t.is(await readFile(join(tmpDir, 'binding.js'), 'utf8'), nativeBinding)
1196
+ })
1197
+
1198
+ test('auxiliary export targets do not consume the native root slot', async (t) => {
1199
+ const { tmpDir } = t.context
1200
+ const binaryName = 'typed-entry'
1201
+ const packageName = '@napi-rs/typed-entry'
1202
+ const artifactsDir = join(tmpDir, 'artifacts')
1203
+ const buildOutputDir = join(tmpDir, 'build-output')
1204
+
1205
+ // `types` lists first in the conditional map, but a declaration file is
1206
+ // not a runtime entry. The native scan must still copy `binding.js` from
1207
+ // the artifacts rather than stopping at the `.d.ts`.
1208
+ await mkdir(artifactsDir, { recursive: true })
1209
+ await mkdir(buildOutputDir, { recursive: true })
1210
+ await writeFile(
1211
+ join(tmpDir, 'package.json'),
1212
+ JSON.stringify({
1213
+ name: packageName,
1214
+ version: '0.0.0',
1215
+ main: './binding.js',
1216
+ exports: {
1217
+ '.': {
1218
+ types: './binding.d.ts',
1219
+ default: './binding.js',
1220
+ },
1221
+ },
1222
+ napi: {
1223
+ binaryName,
1224
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
1225
+ },
1226
+ }),
1227
+ )
1228
+
1229
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
1230
+ version: 2,
1231
+ rootEntry: 'binding.js',
1232
+ exports: ['create'],
1233
+ managedRootEntries: ['browser.js', 'binding.js'],
1234
+ })}\nmodule.exports = {}\n`
1235
+ const nativeBinding =
1236
+ "module.exports = require('./typed-entry.linux-x64-gnu.node')\n"
1237
+
1238
+ await Promise.all([
1239
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
1240
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
1241
+ writeFile(join(artifactsDir, 'binding.js'), nativeBinding),
1242
+ writeFile(join(artifactsDir, 'binding.d.ts'), 'declare const x: 1\n'),
1243
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
1244
+ writeFile(
1245
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
1246
+ 'declare const _default: {}\nexport = _default\n',
1247
+ ),
1248
+ writeFile(
1249
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
1250
+ 'export {}\n',
1251
+ ),
1252
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
1253
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
1254
+ writeFile(
1255
+ join(buildOutputDir, 'binding.js'),
1256
+ "module.exports = require('./typed-entry.wasi.cjs')\n",
1257
+ ),
1258
+ ])
1259
+
1260
+ await collectArtifacts({
1261
+ cwd: tmpDir,
1262
+ buildOutputDir: 'build-output',
1263
+ })
1264
+
1265
+ t.is(await readFile(join(tmpDir, 'binding.js'), 'utf8'), nativeBinding)
1266
+ })
1267
+
1268
+ test('a stale managed entry cannot escape the package root through a symlink', async (t) => {
1269
+ const { tmpDir } = t.context
1270
+ const binaryName = 'escape-entry'
1271
+ const packageName = '@napi-rs/escape-entry'
1272
+ const packageDir = join(tmpDir, 'pkg')
1273
+ const siblingDir = join(tmpDir, 'sibling')
1274
+ const artifactsDir = join(tmpDir, 'artifacts')
1275
+ const buildOutputDir = join(packageDir, 'build-output')
1276
+
1277
+ // The old loader at the package root recorded `linked/old.js` as a
1278
+ // managed root entry. `linked` is now a symlink into a sibling package,
1279
+ // so deleting the stale entry must not follow the link.
1280
+ await mkdir(artifactsDir, { recursive: true })
1281
+ await mkdir(buildOutputDir, { recursive: true })
1282
+ await mkdir(siblingDir, { recursive: true })
1283
+ await mkdir(join(packageDir), { recursive: true })
1284
+ await symlink(siblingDir, join(packageDir, 'linked'))
1285
+ await writeFile(join(siblingDir, 'old.js'), 'module.exports = {}\n')
1286
+ await writeFile(
1287
+ join(packageDir, 'package.json'),
1288
+ JSON.stringify({
1289
+ name: packageName,
1290
+ version: '0.0.0',
1291
+ main: 'index.js',
1292
+ napi: {
1293
+ binaryName,
1294
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
1295
+ },
1296
+ }),
1297
+ )
1298
+
1299
+ const oldLoader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
1300
+ version: 2,
1301
+ rootEntry: 'linked/old.js',
1302
+ managedRootEntries: ['browser.js', 'linked/old.js'],
1303
+ })}\nmodule.exports = {}\n`
1304
+ const newLoader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
1305
+ version: 2,
1306
+ rootEntry: 'binding.js',
1307
+ exports: ['create'],
1308
+ managedRootEntries: ['browser.js', 'binding.js'],
1309
+ })}\nmodule.exports = {}\n`
1310
+
1311
+ await Promise.all([
1312
+ writeFile(join(packageDir, `${binaryName}.wasi.cjs`), oldLoader),
1313
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
1314
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
1315
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), newLoader),
1316
+ writeFile(
1317
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
1318
+ 'declare const _default: {}\nexport = _default\n',
1319
+ ),
1320
+ writeFile(
1321
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
1322
+ 'export {}\n',
1323
+ ),
1324
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
1325
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
1326
+ writeFile(join(buildOutputDir, 'binding.js'), 'module.exports = {}\n'),
1327
+ writeFile(
1328
+ join(packageDir, 'index.js'),
1329
+ 'module.exports = { native: true }\n',
1330
+ ),
1331
+ ])
1332
+
1333
+ await collectArtifacts({
1334
+ cwd: packageDir,
1335
+ outputDir: artifactsDir,
1336
+ buildOutputDir: 'build-output',
1337
+ })
1338
+
1339
+ // The sibling file survives: the stale managed entry resolves outside
1340
+ // the package root through the symlink and is skipped.
1341
+ t.is(
1342
+ await readFile(join(siblingDir, 'old.js'), 'utf8'),
1343
+ 'module.exports = {}\n',
1344
+ )
1345
+ })
1346
+
1347
+ test('an extensionless declared main entry is copied from native artifacts', async (t) => {
1348
+ const { tmpDir } = t.context
1349
+ const binaryName = 'bare-entry'
1350
+ const packageName = '@napi-rs/bare-entry'
1351
+ const artifactsDir = join(tmpDir, 'artifacts')
1352
+ const buildOutputDir = join(tmpDir, 'build-output')
1353
+
1354
+ // `main: "./binding"` resolves to the extensionless file `binding`, which
1355
+ // ships in the native artifacts. The WASI metadata declares the same
1356
+ // name, so the entry is shared and the native copy must reach the
1357
+ // package root — filtering it out of the scan because it lacks an
1358
+ // extension would fail collection instead.
1359
+ await mkdir(artifactsDir, { recursive: true })
1360
+ await mkdir(buildOutputDir, { recursive: true })
1361
+ await writeFile(
1362
+ join(tmpDir, 'package.json'),
1363
+ JSON.stringify({
1364
+ name: packageName,
1365
+ version: '0.0.0',
1366
+ main: './binding',
1367
+ napi: {
1368
+ binaryName,
1369
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
1370
+ },
1371
+ }),
1372
+ )
1373
+
1374
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
1375
+ version: 2,
1376
+ rootEntry: 'binding',
1377
+ exports: ['create'],
1378
+ managedRootEntries: ['browser.js', 'binding'],
1379
+ })}\nmodule.exports = {}\n`
1380
+ const nativeBinding =
1381
+ "module.exports = require('./bare-entry.linux-x64-gnu.node')\n"
1382
+
1383
+ await Promise.all([
1384
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
1385
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
1386
+ writeFile(join(artifactsDir, 'binding'), nativeBinding),
1387
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
1388
+ writeFile(
1389
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
1390
+ 'declare const _default: {}\nexport = _default\n',
1391
+ ),
1392
+ writeFile(
1393
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
1394
+ 'export {}\n',
1395
+ ),
1396
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
1397
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
1398
+ writeFile(
1399
+ join(buildOutputDir, 'binding'),
1400
+ "module.exports = require('./bare-entry.wasi.cjs')\n",
1401
+ ),
1402
+ ])
1403
+
1404
+ await collectArtifacts({
1405
+ cwd: tmpDir,
1406
+ buildOutputDir: 'build-output',
1407
+ })
1408
+
1409
+ t.is(await readFile(join(tmpDir, 'binding'), 'utf8'), nativeBinding)
1410
+ })
1411
+
1412
+ test('a dying managed entry does not shadow a declared main resolution', async (t) => {
1413
+ const { tmpDir } = t.context
1414
+ const binaryName = 'moved-entry'
1415
+ const packageName = '@napi-rs/moved-entry'
1416
+ const artifactsDir = join(tmpDir, 'artifacts')
1417
+ const buildOutputDir = join(tmpDir, 'build-output')
1418
+
1419
+ // The previous loader managed `dist.js` at the package root; the new
1420
+ // contract declares `dist/index.js`. The transaction deletes `dist.js`,
1421
+ // so `main: "./dist"` resolves through the directory form and
1422
+ // `dist/index.js` is shared — the WASI-source copy must not overwrite
1423
+ // the native loader that wins after `dist.js` is gone.
1424
+ await mkdir(artifactsDir, { recursive: true })
1425
+ await mkdir(join(buildOutputDir, 'dist'), { recursive: true })
1426
+ await mkdir(join(tmpDir, 'dist'), { recursive: true })
1427
+ await writeFile(
1428
+ join(tmpDir, 'package.json'),
1429
+ JSON.stringify({
1430
+ name: packageName,
1431
+ version: '0.0.0',
1432
+ main: './dist',
1433
+ napi: {
1434
+ binaryName,
1435
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
1436
+ },
1437
+ }),
1438
+ )
1439
+
1440
+ const oldLoader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
1441
+ version: 2,
1442
+ rootEntry: 'dist.js',
1443
+ managedRootEntries: ['browser.js', 'dist.js'],
1444
+ })}\nmodule.exports = {}\n`
1445
+ const newLoader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
1446
+ version: 2,
1447
+ rootEntry: 'dist/index.js',
1448
+ exports: ['create'],
1449
+ managedRootEntries: ['browser.js', 'dist/index.js'],
1450
+ })}\nmodule.exports = {}\n`
1451
+ const nativeIndex = 'module.exports = { native: true }\n'
1452
+
1453
+ await Promise.all([
1454
+ writeFile(join(tmpDir, `${binaryName}.wasi.cjs`), oldLoader),
1455
+ writeFile(join(tmpDir, 'dist.js'), 'module.exports = { old: true }\n'),
1456
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
1457
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
1458
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), newLoader),
1459
+ writeFile(
1460
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
1461
+ 'declare const _default: {}\nexport = _default\n',
1462
+ ),
1463
+ writeFile(
1464
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
1465
+ 'export {}\n',
1466
+ ),
1467
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
1468
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
1469
+ writeFile(
1470
+ join(buildOutputDir, 'dist', 'index.js'),
1471
+ "module.exports = require('./moved-entry.wasi.cjs')\n",
1472
+ ),
1473
+ writeFile(join(tmpDir, 'dist', 'index.js'), nativeIndex),
1474
+ ])
1475
+
1476
+ await collectArtifacts({
1477
+ cwd: tmpDir,
1478
+ buildOutputDir: 'build-output',
1479
+ })
1480
+
1481
+ // The surviving main resolution is the native `dist/index.js`; the stale
1482
+ // managed `dist.js` is removed rather than shadowing it.
1483
+ t.is(await readFile(join(tmpDir, 'dist', 'index.js'), 'utf8'), nativeIndex)
1484
+ t.false(existsSync(join(tmpDir, 'dist.js')))
1485
+ })
1486
+
1487
+ test('an unresolvable main falls back to the package-root index.js', async (t) => {
1488
+ const { tmpDir } = t.context
1489
+ const binaryName = 'fallback-entry'
1490
+ const packageName = '@napi-rs/fallback-entry'
1491
+ const artifactsDir = join(tmpDir, 'artifacts')
1492
+ const buildOutputDir = join(tmpDir, 'build-output')
1493
+
1494
+ // `main` points at a file that exists nowhere, so Node falls back to the
1495
+ // package-root `index.js`. The WASI-declared `index.js` shares that
1496
+ // entry: the native root file survives and the WASI-source copy must
1497
+ // not replace it.
1498
+ await mkdir(artifactsDir, { recursive: true })
1499
+ await mkdir(buildOutputDir, { recursive: true })
1500
+ await writeFile(
1501
+ join(tmpDir, 'package.json'),
1502
+ JSON.stringify({
1503
+ name: packageName,
1504
+ version: '0.0.0',
1505
+ main: './gone.js',
1506
+ napi: {
1507
+ binaryName,
1508
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
1509
+ },
1510
+ }),
1511
+ )
1512
+
1513
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
1514
+ version: 2,
1515
+ rootEntry: 'index.js',
1516
+ exports: ['create'],
1517
+ managedRootEntries: ['browser.js', 'index.js'],
1518
+ })}\nmodule.exports = {}\n`
1519
+ const nativeIndex =
1520
+ "module.exports = require('./fallback-entry.linux-x64-gnu.node')\n"
1521
+
1522
+ await Promise.all([
1523
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
1524
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
1525
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
1526
+ writeFile(
1527
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
1528
+ 'declare const _default: {}\nexport = _default\n',
1529
+ ),
1530
+ writeFile(
1531
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
1532
+ 'export {}\n',
1533
+ ),
1534
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
1535
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
1536
+ writeFile(
1537
+ join(buildOutputDir, 'index.js'),
1538
+ "module.exports = require('./fallback-entry.wasi.cjs')\n",
1539
+ ),
1540
+ writeFile(join(tmpDir, 'index.js'), nativeIndex),
1541
+ ])
1542
+
1543
+ await collectArtifacts({
1544
+ cwd: tmpDir,
1545
+ buildOutputDir: 'build-output',
1546
+ })
1547
+
1548
+ t.is(await readFile(join(tmpDir, 'index.js'), 'utf8'), nativeIndex)
1549
+ })
1550
+
1551
+ test('a shared root entry absent everywhere falls back to the WASI loader', async (t) => {
1552
+ const { tmpDir } = t.context
1553
+ const binaryName = 'fallback-shared'
1554
+ const packageName = '@napi-rs/fallback-shared'
1555
+ const artifactsDir = join(tmpDir, 'artifacts')
1556
+ const buildOutputDir = join(tmpDir, 'build-output')
1557
+
1558
+ // `main: "index.js"` shares its name with the declared root entry, but
1559
+ // no native loader and no package-root copy of `index.js` exist. The
1560
+ // WASI fallback below publishes the loader-generated copy instead of
1561
+ // failing collection.
1562
+ await mkdir(artifactsDir, { recursive: true })
1563
+ await mkdir(buildOutputDir, { recursive: true })
1564
+ await writeFile(
1565
+ join(tmpDir, 'package.json'),
1566
+ JSON.stringify({
1567
+ name: packageName,
1568
+ version: '0.0.0',
1569
+ main: 'index.js',
1570
+ napi: {
1571
+ binaryName,
1572
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
1573
+ },
1574
+ }),
1575
+ )
1576
+
1577
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
1578
+ version: 2,
1579
+ rootEntry: 'index.js',
1580
+ exports: ['create'],
1581
+ managedRootEntries: ['browser.js', 'index.js'],
1582
+ })}\nmodule.exports = {}\n`
1583
+ const wasiIndex = "module.exports = require('./fallback-shared.wasi.cjs')\n"
1584
+
1585
+ await Promise.all([
1586
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
1587
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
1588
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
1589
+ writeFile(
1590
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
1591
+ 'declare const _default: {}\nexport = _default\n',
1592
+ ),
1593
+ writeFile(
1594
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
1595
+ 'export {}\n',
1596
+ ),
1597
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
1598
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
1599
+ writeFile(join(buildOutputDir, 'index.js'), wasiIndex),
1600
+ ])
1601
+
1602
+ await collectArtifacts({
1603
+ cwd: tmpDir,
1604
+ buildOutputDir: 'build-output',
1605
+ })
1606
+
1607
+ t.is(await readFile(join(tmpDir, 'index.js'), 'utf8'), wasiIndex)
1608
+ })
1609
+
1610
+ test('conflicting WASI root entry declarations are rejected', async (t) => {
1611
+ const { tmpDir } = t.context
1612
+ const binaryName = 'skewed-entry'
1613
+ const packageName = '@napi-rs/skewed-entry'
1614
+ const nativeDir = join(tmpDir, 'artifacts', 'native')
1615
+ const threadlessDir = join(tmpDir, 'artifacts', 'wasip1')
1616
+ const threadsDir = join(tmpDir, 'artifacts', 'threads')
1617
+
1618
+ // The flavors were built with different `--js` names, so their root-entry
1619
+ // contracts cannot be spliced into one coherent package. Splicing would
1620
+ // let a stale same-named file in the preferred source satisfy a contract
1621
+ // it never declared.
1622
+ await Promise.all([
1623
+ mkdir(nativeDir, { recursive: true }),
1624
+ mkdir(threadlessDir, { recursive: true }),
1625
+ mkdir(threadsDir, { recursive: true }),
1626
+ ])
1627
+ await writeFile(
1628
+ join(tmpDir, 'package.json'),
1629
+ JSON.stringify({
1630
+ name: packageName,
1631
+ version: '0.0.0',
1632
+ main: 'index.js',
1633
+ napi: {
1634
+ binaryName,
1635
+ targets: [
1636
+ 'x86_64-unknown-linux-gnu',
1637
+ 'wasm32-wasip1',
1638
+ 'wasm32-wasip1-threads',
1639
+ ],
1640
+ },
1641
+ }),
1642
+ )
1643
+
1644
+ const loaderFor = (rootEntry: string) =>
1645
+ `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
1646
+ version: 2,
1647
+ rootEntry,
1648
+ exports: ['create'],
1649
+ managedRootEntries: ['browser.js', rootEntry],
1650
+ })}\nmodule.exports = {}\n`
1651
+
1652
+ await Promise.all([
1653
+ writeFile(join(nativeDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
1654
+ writeFile(
1655
+ join(threadsDir, `${binaryName}.wasm32-wasi.wasm`),
1656
+ 'threads wasm',
1657
+ ),
1658
+ writeFile(
1659
+ join(threadsDir, `${binaryName}.wasi.cjs`),
1660
+ loaderFor('binding.js'),
1661
+ ),
1662
+ writeFile(
1663
+ join(threadsDir, `${binaryName}.wasi.d.cts`),
1664
+ 'declare const _default: {}\nexport = _default\n',
1665
+ ),
1666
+ writeFile(join(threadsDir, `${binaryName}.wasi-browser.js`), 'export {}\n'),
1667
+ writeFile(join(threadsDir, 'wasi-worker.mjs'), 'export {}\n'),
1668
+ writeFile(join(threadsDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
1669
+ writeFile(
1670
+ join(threadlessDir, `${binaryName}.wasm32-wasip1.wasm`),
1671
+ 'threadless wasm',
1672
+ ),
1673
+ writeFile(
1674
+ join(threadlessDir, `${binaryName}.wasip1.cjs`),
1675
+ loaderFor('index.js'),
1676
+ ),
1677
+ writeFile(
1678
+ join(threadlessDir, `${binaryName}.wasip1.d.cts`),
1679
+ 'declare const _default: {}\nexport = _default\n',
1680
+ ),
1681
+ writeFile(
1682
+ join(threadlessDir, `${binaryName}.wasip1-browser.js`),
1683
+ 'export {}\n',
1684
+ ),
1685
+ writeFile(
1686
+ join(threadlessDir, `${binaryName}.wasip1-deferred.js`),
1687
+ 'export {}\n',
1688
+ ),
1689
+ writeFile(
1690
+ join(threadlessDir, `${binaryName}.wasip1-deferred.d.ts`),
1691
+ 'export {}\n',
1692
+ ),
1693
+ writeFile(join(tmpDir, 'index.js'), 'module.exports = { native: true }\n'),
1694
+ ])
1695
+
1696
+ await t.throwsAsync(collectArtifacts({ cwd: tmpDir }), {
1697
+ message: /conflicting root entries/,
1698
+ })
1699
+ })
1700
+
1701
+ test('an explicit build output without the declared root entry is rejected', async (t) => {
1702
+ const { tmpDir } = t.context
1703
+ const binaryName = 'strict-entry'
1704
+ const packageName = '@napi-rs/strict-entry'
1705
+ const artifactsDir = join(tmpDir, 'artifacts')
1706
+ const buildOutputDir = join(tmpDir, 'build-output')
1707
+
1708
+ // --build-output-dir makes the WASI output authoritative: its loader
1709
+ // metadata declares binding.js, the directory does not contain it, and a
1710
+ // stale package-root copy must not paper over the incomplete source.
1711
+ await mkdir(artifactsDir, { recursive: true })
1712
+ await mkdir(buildOutputDir, { recursive: true })
1713
+ await writeFile(
1714
+ join(tmpDir, 'package.json'),
1715
+ JSON.stringify({
1716
+ name: packageName,
1717
+ version: '0.0.0',
1718
+ main: 'index.js',
1719
+ napi: {
1720
+ binaryName,
1721
+ targets: ['x86_64-unknown-linux-gnu', 'wasm32-wasip1-threads'],
1722
+ },
1723
+ }),
1724
+ )
1725
+
1726
+ const loader = `${WASI_ARTIFACT_METADATA_PREFIX}${JSON.stringify({
1727
+ version: 2,
1728
+ rootEntry: 'binding.js',
1729
+ exports: ['create'],
1730
+ managedRootEntries: ['browser.js', 'binding.js'],
1731
+ })}\nmodule.exports = {}\n`
1732
+
1733
+ await Promise.all([
1734
+ writeFile(join(artifactsDir, `${binaryName}.linux-x64-gnu.node`), 'bin'),
1735
+ writeFile(join(artifactsDir, `${binaryName}.wasm32-wasi.wasm`), 'wasm'),
1736
+ writeFile(join(buildOutputDir, `${binaryName}.wasi.cjs`), loader),
1737
+ writeFile(
1738
+ join(buildOutputDir, `${binaryName}.wasi.d.cts`),
1739
+ 'declare const _default: {}\nexport = _default\n',
1740
+ ),
1741
+ writeFile(
1742
+ join(buildOutputDir, `${binaryName}.wasi-browser.js`),
1743
+ 'export {}\n',
1744
+ ),
1745
+ writeFile(join(buildOutputDir, 'wasi-worker.mjs'), 'export {}\n'),
1746
+ writeFile(join(buildOutputDir, 'wasi-worker-browser.mjs'), 'export {}\n'),
1747
+ writeFile(join(tmpDir, 'binding.js'), 'module.exports = { stale: true }\n'),
1748
+ writeFile(join(tmpDir, 'index.js'), 'module.exports = {}\n'),
1749
+ ])
1750
+
1751
+ await t.throwsAsync(
1752
+ collectArtifacts({ cwd: tmpDir, buildOutputDir: 'build-output' }),
1753
+ { message: /does not contain it/ },
1754
+ )
1755
+ })