@napi-rs/cli 3.9.1 → 3.10.0

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.
@@ -497,6 +497,107 @@ test.serial(
497
497
  },
498
498
  )
499
499
 
500
+ test.serial(
501
+ 'should declare @napi-rs/async-runtime when napi.wasm.asyncRuntime is enabled',
502
+ async (t) => {
503
+ const { tmpDir, packageJsonPath } = t.context
504
+ const registryServer = await startRegistryServer({
505
+ 'dist-tags': {
506
+ latest: '0.1.0',
507
+ },
508
+ })
509
+
510
+ process.env.npm_config_registry = `${registryServer.origin}/npm`
511
+
512
+ const packageJson = {
513
+ name: 'test-async-runtime-dep',
514
+ version: '1.0.0',
515
+ napi: {
516
+ binaryName: 'test-async-runtime-dep',
517
+ targets: ['wasm32-wasip1', 'wasm32-wasip1-threads'],
518
+ wasm: {
519
+ asyncRuntime: true,
520
+ },
521
+ },
522
+ }
523
+
524
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
525
+
526
+ try {
527
+ await createNpmDirs({
528
+ cwd: tmpDir,
529
+ packageJsonPath: 'package.json',
530
+ })
531
+
532
+ for (const packageDir of ['wasm32-wasip1', 'wasm32-wasi']) {
533
+ const scopedPackageJson = JSON.parse(
534
+ await readFile(
535
+ join(tmpDir, 'npm', packageDir, 'package.json'),
536
+ 'utf-8',
537
+ ),
538
+ )
539
+
540
+ t.is(scopedPackageJson.dependencies['@napi-rs/async-runtime'], '^0.1.0')
541
+ }
542
+ t.deepEqual(registryServer.requests.sort(), [
543
+ '/npm/@napi-rs/async-runtime',
544
+ '/npm/@napi-rs/wasm-runtime',
545
+ ])
546
+ } finally {
547
+ await registryServer.close()
548
+ }
549
+ },
550
+ )
551
+
552
+ test.serial(
553
+ 'should not resolve @napi-rs/async-runtime when the flag is unset',
554
+ async (t) => {
555
+ const { tmpDir, packageJsonPath } = t.context
556
+ const registryServer = await startRegistryServer({
557
+ 'dist-tags': {
558
+ latest: '0.1.0',
559
+ },
560
+ })
561
+
562
+ process.env.npm_config_registry = `${registryServer.origin}/npm`
563
+
564
+ const packageJson = {
565
+ name: 'test-async-runtime-dep-off',
566
+ version: '1.0.0',
567
+ napi: {
568
+ binaryName: 'test-async-runtime-dep-off',
569
+ targets: ['wasm32-wasip1', 'wasm32-wasip1-threads'],
570
+ },
571
+ }
572
+
573
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
574
+
575
+ try {
576
+ await createNpmDirs({
577
+ cwd: tmpDir,
578
+ packageJsonPath: 'package.json',
579
+ })
580
+
581
+ for (const packageDir of ['wasm32-wasip1', 'wasm32-wasi']) {
582
+ const scopedPackageJson = JSON.parse(
583
+ await readFile(
584
+ join(tmpDir, 'npm', packageDir, 'package.json'),
585
+ 'utf-8',
586
+ ),
587
+ )
588
+
589
+ t.is(
590
+ scopedPackageJson.dependencies['@napi-rs/async-runtime'],
591
+ undefined,
592
+ )
593
+ }
594
+ t.deepEqual(registryServer.requests, ['/npm/@napi-rs/wasm-runtime'])
595
+ } finally {
596
+ await registryServer.close()
597
+ }
598
+ },
599
+ )
600
+
500
601
  test.serial(
501
602
  'should reject an empty latest dist-tag when resolving wasm runtime metadata',
502
603
  async (t) => {
@@ -1,5 +1,6 @@
1
1
  import { mkdtempSync } from 'node:fs'
2
- import { mkdir, rm, writeFile } from 'node:fs/promises'
2
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
3
+ import { createRequire } from 'node:module'
3
4
  import { tmpdir } from 'node:os'
4
5
  import { dirname, join } from 'node:path'
5
6
 
@@ -12,6 +13,7 @@ import {
12
13
  resolveRootPublisher,
13
14
  rootPublisherRewritesRootExports,
14
15
  sniffRewritingPublisherFromUserAgent,
16
+ validateReleasePackageContents,
15
17
  validateRootFacadePacklist,
16
18
  } from '../pre-publish.js'
17
19
  import { parseTriple } from '../../utils/index.js'
@@ -558,3 +560,153 @@ test('still rejects publishConfig.exports references omitted by npm pack', async
558
560
  )
559
561
  }
560
562
  })
563
+
564
+ // The generated WASI loaders only reach `@napi-rs/async-runtime` when
565
+ // `napi.wasm.asyncRuntime` is on, and `create-npm-dirs` only declares it in the
566
+ // same case. A project that flips the flag without rerunning `create-npm-dirs`
567
+ // therefore publishes a loader importing a package its own manifest never
568
+ // declares, which `npm install` never fetches: the addon then fails at load on
569
+ // every consumer. `pre-publish` has to catch that pair before it ships.
570
+ const requireFromSpec = createRequire(import.meta.url)
571
+ const EMNAPI_VERSION = requireFromSpec('emnapi/package.json').version as string
572
+ const BINARY_NAME = 'pkg'
573
+ const WASI_PLATFORM_ARCH_ABI = 'wasm32-wasi'
574
+ const ASYNC_RUNTIME_PACKAGE = '@napi-rs/async-runtime'
575
+
576
+ interface WasiReleasePackageOptions {
577
+ loadersImportAsyncRuntime: boolean
578
+ declareAsyncRuntime: boolean
579
+ }
580
+
581
+ async function createWasiReleasePackage({
582
+ loadersImportAsyncRuntime,
583
+ declareAsyncRuntime,
584
+ }: WasiReleasePackageOptions) {
585
+ const rootDir = mkdtempSync(join(tmpdir(), 'napi-wasi-release-spec-'))
586
+ const pkgDir = join(rootDir, 'npm', 'wasm32-wasi')
587
+ await mkdir(pkgDir, { recursive: true })
588
+
589
+ const artifact = `${BINARY_NAME}.${WASI_PLATFORM_ARCH_ABI}.wasm`
590
+ const main = `${BINARY_NAME}.wasi.cjs`
591
+ const types = `${BINARY_NAME}.wasi.d.cts`
592
+ const browser = `${BINARY_NAME}.wasi-browser.js`
593
+ const files = [
594
+ artifact,
595
+ main,
596
+ types,
597
+ browser,
598
+ 'wasi-worker.mjs',
599
+ 'wasi-worker-browser.mjs',
600
+ ]
601
+
602
+ const cjsLoader = `const { instantiateNapiModuleSync } = require('@napi-rs/wasm-runtime')
603
+ ${
604
+ loadersImportAsyncRuntime
605
+ ? `const {
606
+ installCurrentThreadHosts: __installCurrentThreadHosts,
607
+ } = require('${ASYNC_RUNTIME_PACKAGE}')
608
+ `
609
+ : ''
610
+ }module.exports = { instantiateNapiModuleSync }
611
+ `
612
+ const browserLoader = `import { instantiateNapiModule } from '@napi-rs/wasm-runtime'
613
+ ${
614
+ loadersImportAsyncRuntime
615
+ ? `import { installCurrentThreadHosts as __installCurrentThreadHosts } from '${ASYNC_RUNTIME_PACKAGE}'
616
+ `
617
+ : ''
618
+ }export default instantiateNapiModule
619
+ `
620
+
621
+ await writeFile(join(pkgDir, artifact), '\0asm')
622
+ await writeFile(join(pkgDir, main), cjsLoader)
623
+ await writeFile(join(pkgDir, types), 'export declare function noop(): void\n')
624
+ await writeFile(join(pkgDir, browser), browserLoader)
625
+ await writeFile(join(pkgDir, 'wasi-worker.mjs'), '// worker\n')
626
+ await writeFile(join(pkgDir, 'wasi-worker-browser.mjs'), '// worker\n')
627
+
628
+ await writeFile(
629
+ join(pkgDir, 'package.json'),
630
+ JSON.stringify({
631
+ name: `${PACKAGE_NAME}-${WASI_PLATFORM_ARCH_ABI}`,
632
+ version: VERSION,
633
+ type: 'module',
634
+ main,
635
+ types,
636
+ browser,
637
+ files,
638
+ dependencies: {
639
+ '@napi-rs/wasm-runtime': '^1.0.0',
640
+ '@emnapi/core': EMNAPI_VERSION,
641
+ '@emnapi/runtime': EMNAPI_VERSION,
642
+ ...(declareAsyncRuntime ? { [ASYNC_RUNTIME_PACKAGE]: '^0.1.0' } : {}),
643
+ },
644
+ }),
645
+ )
646
+
647
+ return { rootDir, pkgDir }
648
+ }
649
+
650
+ function validateWasiReleasePackage(pkgDir: string, rootDir: string) {
651
+ return validateReleasePackageContents({
652
+ pkgDir,
653
+ rootDir,
654
+ packageName: PACKAGE_NAME,
655
+ binaryName: BINARY_NAME,
656
+ target: WASI_TARGET,
657
+ requireDirectBufferDependency: false,
658
+ })
659
+ }
660
+
661
+ test('rejects a WASI release package whose loaders import @napi-rs/async-runtime without declaring it', async (t) => {
662
+ const { rootDir, pkgDir } = await createWasiReleasePackage({
663
+ loadersImportAsyncRuntime: true,
664
+ declareAsyncRuntime: false,
665
+ })
666
+ t.teardown(() => rm(rootDir, { recursive: true, force: true }))
667
+
668
+ await t.throwsAsync(() => validateWasiReleasePackage(pkgDir, rootDir), {
669
+ message: `Release package ${PACKAGE_NAME}-${WASI_PLATFORM_ARCH_ABI} must declare dependency ${ASYNC_RUNTIME_PACKAGE}`,
670
+ })
671
+ })
672
+
673
+ test('accepts a WASI release package that declares the @napi-rs/async-runtime its loaders import', async (t) => {
674
+ const { rootDir, pkgDir } = await createWasiReleasePackage({
675
+ loadersImportAsyncRuntime: true,
676
+ declareAsyncRuntime: true,
677
+ })
678
+ t.teardown(() => rm(rootDir, { recursive: true, force: true }))
679
+
680
+ await t.notThrowsAsync(() => validateWasiReleasePackage(pkgDir, rootDir))
681
+ })
682
+
683
+ test('rejects a declared @napi-rs/async-runtime that no WASI loader imports', async (t) => {
684
+ // Mirrors the `buffer` precedent: the dependency is required when a loader
685
+ // imports it and forbidden when none does, so a flavor never carries an
686
+ // install every consumer pays for and nothing loads.
687
+ const { rootDir, pkgDir } = await createWasiReleasePackage({
688
+ loadersImportAsyncRuntime: false,
689
+ declareAsyncRuntime: true,
690
+ })
691
+ t.teardown(() => rm(rootDir, { recursive: true, force: true }))
692
+
693
+ await t.throwsAsync(() => validateWasiReleasePackage(pkgDir, rootDir), {
694
+ message: `Release package ${PACKAGE_NAME}-${WASI_PLATFORM_ARCH_ABI} must omit ${ASYNC_RUNTIME_PACKAGE} when its loaders do not import it`,
695
+ })
696
+ })
697
+
698
+ test('rejects an invalid @napi-rs/async-runtime range', async (t) => {
699
+ const { rootDir, pkgDir } = await createWasiReleasePackage({
700
+ loadersImportAsyncRuntime: true,
701
+ declareAsyncRuntime: true,
702
+ })
703
+ t.teardown(() => rm(rootDir, { recursive: true, force: true }))
704
+ const packageJsonPath = join(pkgDir, 'package.json')
705
+ const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'))
706
+ packageJson.dependencies[ASYNC_RUNTIME_PACKAGE] = 'not a range'
707
+ await writeFile(packageJsonPath, JSON.stringify(packageJson))
708
+
709
+ await t.throwsAsync(() => validateWasiReleasePackage(pkgDir, rootDir), {
710
+ message: `Release package ${PACKAGE_NAME}-${WASI_PLATFORM_ARCH_ABI} has invalid ${ASYNC_RUNTIME_PACKAGE} dependency not a range`,
711
+ })
712
+ })