@napi-rs/cli 3.7.2 → 3.7.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.
Files changed (48) hide show
  1. package/dist/cli.js +240 -64
  2. package/dist/index.cjs +239 -63
  3. package/dist/index.d.cts +5 -5
  4. package/dist/index.d.ts +5 -5
  5. package/dist/index.js +240 -64
  6. package/package.json +3 -3
  7. package/src/api/__tests__/__snapshots__/templates.spec.ts.md +353 -0
  8. package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
  9. package/src/api/__tests__/android-build.spec.ts +113 -0
  10. package/src/api/__tests__/artifacts.spec.ts +95 -0
  11. package/src/api/__tests__/build-regressions.spec.ts +128 -0
  12. package/src/api/__tests__/build.spec.ts +1020 -0
  13. package/src/api/__tests__/create-npm-dirs.spec.ts +561 -0
  14. package/src/api/__tests__/new-node-api-version.spec.ts +87 -0
  15. package/src/api/__tests__/new.spec.ts +527 -0
  16. package/src/api/__tests__/rename.spec.ts +181 -0
  17. package/src/api/__tests__/templates.spec.ts +164 -0
  18. package/src/api/__tests__/universalize.spec.ts +128 -0
  19. package/src/api/artifacts.ts +7 -16
  20. package/src/api/build.ts +354 -111
  21. package/src/api/new.ts +65 -1
  22. package/src/commands/__tests__/new.spec.ts +13 -0
  23. package/src/def/build.ts +6 -6
  24. package/src/utils/__tests__/__fixtures__/napi_type_def +202 -0
  25. package/src/utils/__tests__/__fixtures__/optional-napi-derive/Cargo.toml +3 -0
  26. package/src/utils/__tests__/__fixtures__/optional-napi-derive/main-crate/Cargo.toml +8 -0
  27. package/src/utils/__tests__/__fixtures__/optional-napi-derive/main-crate/src/lib.rs +1 -0
  28. package/src/utils/__tests__/__fixtures__/optional-napi-derive/napi-derive/Cargo.toml +5 -0
  29. package/src/utils/__tests__/__fixtures__/optional-napi-derive/napi-derive/src/lib.rs +1 -0
  30. package/src/utils/__tests__/__fixtures__/optional-napi-derive/with-optional-derive/Cargo.toml +11 -0
  31. package/src/utils/__tests__/__fixtures__/optional-napi-derive/with-optional-derive/src/lib.rs +1 -0
  32. package/src/utils/__tests__/__fixtures__/runtime_string_enum_flag +2 -0
  33. package/src/utils/__tests__/__snapshots__/config.spec.ts.md +97 -0
  34. package/src/utils/__tests__/__snapshots__/config.spec.ts.snap +0 -0
  35. package/src/utils/__tests__/__snapshots__/target.spec.ts.md +180 -0
  36. package/src/utils/__tests__/__snapshots__/target.spec.ts.snap +0 -0
  37. package/src/utils/__tests__/__snapshots__/typegen.spec.ts.md +1197 -0
  38. package/src/utils/__tests__/__snapshots__/typegen.spec.ts.snap +0 -0
  39. package/src/utils/__tests__/__snapshots__/version.spec.ts.md +22 -0
  40. package/src/utils/__tests__/__snapshots__/version.spec.ts.snap +0 -0
  41. package/src/utils/__tests__/config.spec.ts +81 -0
  42. package/src/utils/__tests__/metadata.spec.ts +69 -0
  43. package/src/utils/__tests__/misc.spec.ts +56 -0
  44. package/src/utils/__tests__/target.spec.ts +19 -0
  45. package/src/utils/__tests__/typegen.spec.ts +85 -0
  46. package/src/utils/__tests__/version.spec.ts +7 -0
  47. package/src/utils/metadata.ts +94 -3
  48. package/src/utils/version.ts +2 -0
@@ -0,0 +1,561 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
3
+ import { createServer } from 'node:http'
4
+ import { createRequire } from 'node:module'
5
+ import { tmpdir } from 'node:os'
6
+ import { join } from 'node:path'
7
+
8
+ import ava, { type TestFn } from 'ava'
9
+
10
+ import { createNpmDirs } from '../create-npm-dirs.js'
11
+
12
+ const require = createRequire(import.meta.url)
13
+
14
+ const test = ava as TestFn<{
15
+ tmpDir: string
16
+ packageJsonPath: string
17
+ npmConfigRegistry?: string
18
+ }>
19
+
20
+ test.beforeEach(async (t) => {
21
+ // Create a unique temp directory for tests
22
+ const timestamp = Date.now()
23
+ const random = Math.random().toString(36).substring(7)
24
+ const tmpDir = join(
25
+ tmpdir(),
26
+ 'napi-rs-test',
27
+ `create-npm-dirs-${timestamp}-${random}`,
28
+ )
29
+ const packageJsonPath = join(tmpDir, 'package.json')
30
+
31
+ // Create the directory
32
+ await mkdir(tmpDir, { recursive: true })
33
+
34
+ t.context = {
35
+ tmpDir,
36
+ packageJsonPath,
37
+ npmConfigRegistry: process.env.npm_config_registry,
38
+ }
39
+ })
40
+
41
+ test.afterEach.always(async (t) => {
42
+ if (t.context.npmConfigRegistry === undefined) {
43
+ delete process.env.npm_config_registry
44
+ } else {
45
+ process.env.npm_config_registry = t.context.npmConfigRegistry
46
+ }
47
+
48
+ // Clean up any created directories
49
+ if (existsSync(t.context.tmpDir)) {
50
+ await rm(t.context.tmpDir, { recursive: true, force: true })
51
+ }
52
+ })
53
+
54
+ async function startRegistryServer(
55
+ responseBody: Record<string, unknown> = {
56
+ 'dist-tags': {
57
+ latest: '1.2.3',
58
+ },
59
+ },
60
+ ) {
61
+ const requests: string[] = []
62
+ const server = createServer((req, res) => {
63
+ requests.push(req.url ?? '')
64
+ res.setHeader('content-type', 'application/json')
65
+ res.end(JSON.stringify(responseBody))
66
+ })
67
+
68
+ await new Promise<void>((resolve, reject) => {
69
+ server.once('error', reject)
70
+ server.listen(0, '127.0.0.1', () => resolve())
71
+ })
72
+
73
+ const address = server.address()
74
+
75
+ if (!address || typeof address === 'string') {
76
+ server.close()
77
+ throw new Error('Failed to resolve test registry server address')
78
+ }
79
+
80
+ return {
81
+ requests,
82
+ close: () =>
83
+ new Promise<void>((resolve, reject) => {
84
+ server.close((error) => {
85
+ if (error) {
86
+ reject(error)
87
+ } else {
88
+ resolve()
89
+ }
90
+ })
91
+ }),
92
+ origin: `http://127.0.0.1:${address.port}`,
93
+ }
94
+ }
95
+
96
+ test('should omit exports fields from publishConfig in scoped packages', async (t) => {
97
+ const { tmpDir, packageJsonPath } = t.context
98
+
99
+ // Create a package.json with publishConfig that includes exports field
100
+ const packageJson = {
101
+ name: 'test-package',
102
+ version: '1.0.0',
103
+ publishConfig: {
104
+ registry: 'https://custom-registry.com',
105
+ access: 'public',
106
+ exports: {
107
+ '.': './dist/index.js',
108
+ './package.json': './package.json',
109
+ },
110
+ tag: 'beta',
111
+ },
112
+ napi: {
113
+ binaryName: 'test-package',
114
+ targets: ['x86_64-unknown-linux-gnu'],
115
+ },
116
+ }
117
+
118
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
119
+
120
+ await createNpmDirs({
121
+ cwd: tmpDir,
122
+ packageJsonPath: 'package.json',
123
+ })
124
+
125
+ // Check that the scoped package directory was created
126
+ const scopedDir = join(tmpDir, 'npm', 'linux-x64-gnu')
127
+ t.true(existsSync(scopedDir))
128
+
129
+ // Read the generated package.json for the scoped package
130
+ const scopedPackageJsonPath = join(scopedDir, 'package.json')
131
+ t.true(existsSync(scopedPackageJsonPath))
132
+
133
+ const scopedPackageJson = JSON.parse(
134
+ await readFile(scopedPackageJsonPath, 'utf-8'),
135
+ )
136
+
137
+ // Verify that publishConfig only contains registry and access, not exports
138
+ t.truthy(scopedPackageJson.publishConfig)
139
+ t.is(scopedPackageJson.publishConfig.registry, 'https://custom-registry.com')
140
+ t.is(scopedPackageJson.publishConfig.access, 'public')
141
+ t.is(scopedPackageJson.publishConfig.exports, undefined)
142
+ t.is(scopedPackageJson.publishConfig.tag, undefined)
143
+ })
144
+
145
+ test('should handle package without publishConfig', async (t) => {
146
+ const { tmpDir, packageJsonPath } = t.context
147
+
148
+ // Create a package.json without publishConfig
149
+ const packageJson = {
150
+ name: 'test-package-no-config',
151
+ version: '1.0.0',
152
+ napi: {
153
+ binaryName: 'test-package-no-config',
154
+ targets: ['x86_64-unknown-linux-gnu'],
155
+ },
156
+ }
157
+
158
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
159
+
160
+ await createNpmDirs({
161
+ cwd: tmpDir,
162
+ packageJsonPath: 'package.json',
163
+ })
164
+
165
+ // Check that the scoped package directory was created
166
+ const scopedDir = join(tmpDir, 'npm', 'linux-x64-gnu')
167
+ t.true(existsSync(scopedDir))
168
+
169
+ // Read the generated package.json for the scoped package
170
+ const scopedPackageJsonPath = join(scopedDir, 'package.json')
171
+ const scopedPackageJson = JSON.parse(
172
+ await readFile(scopedPackageJsonPath, 'utf-8'),
173
+ )
174
+
175
+ // Verify that publishConfig is not present when not in source
176
+ t.is(scopedPackageJson.publishConfig, undefined)
177
+ })
178
+
179
+ test('should preserve only registry and access in publishConfig', async (t) => {
180
+ const { tmpDir, packageJsonPath } = t.context
181
+
182
+ // Create a package.json with minimal publishConfig
183
+ const packageJson = {
184
+ name: 'test-package-minimal',
185
+ version: '1.0.0',
186
+ publishConfig: {
187
+ registry: 'https://npm.company.com',
188
+ access: 'restricted',
189
+ },
190
+ napi: {
191
+ binaryName: 'test-package-minimal',
192
+ targets: ['aarch64-apple-darwin'],
193
+ },
194
+ }
195
+
196
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
197
+
198
+ await createNpmDirs({
199
+ cwd: tmpDir,
200
+ packageJsonPath: 'package.json',
201
+ })
202
+
203
+ // Check that the scoped package directory was created
204
+ const scopedDir = join(tmpDir, 'npm', 'darwin-arm64')
205
+ t.true(existsSync(scopedDir))
206
+
207
+ // Read the generated package.json for the scoped package
208
+ const scopedPackageJsonPath = join(scopedDir, 'package.json')
209
+ const scopedPackageJson = JSON.parse(
210
+ await readFile(scopedPackageJsonPath, 'utf-8'),
211
+ )
212
+
213
+ // Verify that publishConfig contains exactly registry and access
214
+ t.truthy(scopedPackageJson.publishConfig)
215
+ t.is(scopedPackageJson.publishConfig.registry, 'https://npm.company.com')
216
+ t.is(scopedPackageJson.publishConfig.access, 'restricted')
217
+ t.is(Object.keys(scopedPackageJson.publishConfig).length, 2)
218
+ })
219
+
220
+ test('should handle WASM targets correctly with publishConfig', async (t) => {
221
+ const { tmpDir, packageJsonPath } = t.context
222
+ const registryServer = await startRegistryServer()
223
+
224
+ // Create a package.json for WASM target
225
+ const packageJson = {
226
+ name: 'test-wasm-package',
227
+ version: '1.0.0',
228
+ publishConfig: {
229
+ registry: `${registryServer.origin}/wasm`,
230
+ access: 'public',
231
+ exports: './wasm.js',
232
+ browser: './browser.js',
233
+ },
234
+ napi: {
235
+ binaryName: 'test-wasm-package',
236
+ targets: ['wasm32-wasi-preview1-threads'],
237
+ },
238
+ }
239
+
240
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
241
+
242
+ try {
243
+ await createNpmDirs({
244
+ cwd: tmpDir,
245
+ packageJsonPath: 'package.json',
246
+ })
247
+
248
+ // Check that the scoped package directory was created
249
+ const scopedDir = join(tmpDir, 'npm', 'wasm32-wasi')
250
+ t.true(existsSync(scopedDir))
251
+
252
+ // Read the generated package.json for the scoped package
253
+ const scopedPackageJsonPath = join(scopedDir, 'package.json')
254
+ const scopedPackageJson = JSON.parse(
255
+ await readFile(scopedPackageJsonPath, 'utf-8'),
256
+ )
257
+
258
+ // Verify that publishConfig is correctly filtered for WASM too
259
+ t.truthy(scopedPackageJson.publishConfig)
260
+ t.is(
261
+ scopedPackageJson.publishConfig.registry,
262
+ `${registryServer.origin}/wasm`,
263
+ )
264
+ t.is(scopedPackageJson.publishConfig.access, 'public')
265
+ t.is(scopedPackageJson.publishConfig.exports, undefined)
266
+ t.is(scopedPackageJson.publishConfig.browser, undefined)
267
+
268
+ // Verify WASM-specific fields are set correctly
269
+ t.truthy(scopedPackageJson.main)
270
+ t.truthy(scopedPackageJson.browser)
271
+ t.truthy(scopedPackageJson.dependencies)
272
+ } finally {
273
+ await registryServer.close()
274
+ }
275
+ })
276
+
277
+ test('should preserve stricter node engine ranges for WASM targets', async (t) => {
278
+ const { tmpDir, packageJsonPath } = t.context
279
+
280
+ const packageJson = {
281
+ name: 'test-wasm-engines',
282
+ version: '1.0.0',
283
+ engines: {
284
+ node: '>=18',
285
+ },
286
+ napi: {
287
+ binaryName: 'test-wasm-engines',
288
+ targets: ['wasm32-wasi-preview1-threads'],
289
+ },
290
+ }
291
+
292
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
293
+
294
+ await createNpmDirs({
295
+ cwd: tmpDir,
296
+ packageJsonPath: 'package.json',
297
+ })
298
+
299
+ const scopedPackageJson = JSON.parse(
300
+ await readFile(join(tmpDir, 'npm', 'wasm32-wasi', 'package.json'), 'utf-8'),
301
+ )
302
+
303
+ t.is(scopedPackageJson.engines.node, '>=18')
304
+ })
305
+
306
+ test('should intersect mixed node engine ranges with the WASI minimum', async (t) => {
307
+ const { tmpDir, packageJsonPath } = t.context
308
+
309
+ const packageJson = {
310
+ name: 'test-wasm-mixed-engines',
311
+ version: '1.0.0',
312
+ engines: {
313
+ node: '>=12 <14 || >=18',
314
+ },
315
+ napi: {
316
+ binaryName: 'test-wasm-mixed-engines',
317
+ targets: ['wasm32-wasi-preview1-threads'],
318
+ },
319
+ }
320
+
321
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
322
+
323
+ await createNpmDirs({
324
+ cwd: tmpDir,
325
+ packageJsonPath: 'package.json',
326
+ })
327
+
328
+ const scopedPackageJson = JSON.parse(
329
+ await readFile(join(tmpDir, 'npm', 'wasm32-wasi', 'package.json'), 'utf-8'),
330
+ )
331
+
332
+ t.is(scopedPackageJson.engines.node, '>=18.0.0')
333
+ })
334
+
335
+ test('should preserve sibling engine constraints when node is missing for WASM targets', async (t) => {
336
+ const { tmpDir, packageJsonPath } = t.context
337
+
338
+ const packageJson = {
339
+ name: 'test-wasm-sibling-engines',
340
+ version: '1.0.0',
341
+ engines: {
342
+ npm: '>=10',
343
+ },
344
+ napi: {
345
+ binaryName: 'test-wasm-sibling-engines',
346
+ targets: ['wasm32-wasi-preview1-threads'],
347
+ },
348
+ }
349
+
350
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
351
+
352
+ await createNpmDirs({
353
+ cwd: tmpDir,
354
+ packageJsonPath: 'package.json',
355
+ })
356
+
357
+ const scopedPackageJson = JSON.parse(
358
+ await readFile(join(tmpDir, 'npm', 'wasm32-wasi', 'package.json'), 'utf-8'),
359
+ )
360
+
361
+ t.deepEqual(scopedPackageJson.engines, {
362
+ npm: '>=10',
363
+ node: '>=14.0.0',
364
+ })
365
+ })
366
+
367
+ test('should replace an exact node engine below the WASI minimum for WASM targets', async (t) => {
368
+ const { tmpDir, packageJsonPath } = t.context
369
+
370
+ const packageJson = {
371
+ name: 'test-wasm-exact-node-engine',
372
+ version: '1.0.0',
373
+ engines: {
374
+ node: '13.0.0',
375
+ },
376
+ napi: {
377
+ binaryName: 'test-wasm-exact-node-engine',
378
+ targets: ['wasm32-wasi-preview1-threads'],
379
+ },
380
+ }
381
+
382
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
383
+
384
+ await createNpmDirs({
385
+ cwd: tmpDir,
386
+ packageJsonPath: 'package.json',
387
+ })
388
+
389
+ const scopedPackageJson = JSON.parse(
390
+ await readFile(join(tmpDir, 'npm', 'wasm32-wasi', 'package.json'), 'utf-8'),
391
+ )
392
+
393
+ t.is(scopedPackageJson.engines.node, '>=14.0.0')
394
+ })
395
+
396
+ test('should drop exact node engine branches below the WASI minimum for WASM targets', async (t) => {
397
+ const { tmpDir, packageJsonPath } = t.context
398
+
399
+ const packageJson = {
400
+ name: 'test-wasm-exact-node-branch',
401
+ version: '1.0.0',
402
+ engines: {
403
+ node: '13.0.0 || >=18',
404
+ },
405
+ napi: {
406
+ binaryName: 'test-wasm-exact-node-branch',
407
+ targets: ['wasm32-wasi-preview1-threads'],
408
+ },
409
+ }
410
+
411
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
412
+
413
+ await createNpmDirs({
414
+ cwd: tmpDir,
415
+ packageJsonPath: 'package.json',
416
+ })
417
+
418
+ const scopedPackageJson = JSON.parse(
419
+ await readFile(join(tmpDir, 'npm', 'wasm32-wasi', 'package.json'), 'utf-8'),
420
+ )
421
+
422
+ t.is(scopedPackageJson.engines.node, '>=18.0.0')
423
+ })
424
+
425
+ test('should set @emnapi/core and @emnapi/runtime versions to match emnapi for WASM targets', async (t) => {
426
+ const { tmpDir, packageJsonPath } = t.context
427
+
428
+ const packageJson = {
429
+ name: 'test-emnapi-versions',
430
+ version: '1.0.0',
431
+ napi: {
432
+ binaryName: 'test-emnapi-versions',
433
+ targets: ['wasm32-wasi-preview1-threads'],
434
+ },
435
+ }
436
+
437
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
438
+
439
+ await createNpmDirs({
440
+ cwd: tmpDir,
441
+ packageJsonPath: 'package.json',
442
+ })
443
+
444
+ const scopedDir = join(tmpDir, 'npm', 'wasm32-wasi')
445
+ const scopedPackageJson = JSON.parse(
446
+ await readFile(join(scopedDir, 'package.json'), 'utf-8'),
447
+ )
448
+
449
+ const emnapiVersion = require('emnapi/package.json').version
450
+ t.is(scopedPackageJson.dependencies['@emnapi/core'], emnapiVersion)
451
+ t.is(scopedPackageJson.dependencies['@emnapi/runtime'], emnapiVersion)
452
+ })
453
+
454
+ test.serial(
455
+ 'should reject an empty latest dist-tag when resolving wasm runtime metadata',
456
+ async (t) => {
457
+ const { tmpDir, packageJsonPath } = t.context
458
+ const registryServer = await startRegistryServer({
459
+ 'dist-tags': {
460
+ latest: ' ',
461
+ },
462
+ })
463
+
464
+ process.env.npm_config_registry = `${registryServer.origin}/npm`
465
+
466
+ const packageJson = {
467
+ name: 'test-wasm-empty-latest-tag',
468
+ version: '1.0.0',
469
+ napi: {
470
+ binaryName: 'test-wasm-empty-latest-tag',
471
+ targets: ['wasm32-wasi-preview1-threads'],
472
+ },
473
+ }
474
+
475
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
476
+
477
+ try {
478
+ const error = await t.throwsAsync(() =>
479
+ createNpmDirs({
480
+ cwd: tmpDir,
481
+ packageJsonPath: 'package.json',
482
+ }),
483
+ )
484
+
485
+ t.regex(error.message, /did not include a latest dist-tag/)
486
+ t.deepEqual(registryServer.requests, ['/npm/@napi-rs/wasm-runtime'])
487
+ } finally {
488
+ await registryServer.close()
489
+ }
490
+ },
491
+ )
492
+
493
+ test.serial(
494
+ 'should ignore publishConfig.registry when resolving wasm runtime metadata',
495
+ async (t) => {
496
+ const { tmpDir, packageJsonPath } = t.context
497
+ const registryServer = await startRegistryServer()
498
+ const envRegistryServer = await startRegistryServer()
499
+
500
+ process.env.npm_config_registry = `${envRegistryServer.origin}/env`
501
+
502
+ const packageJson = {
503
+ name: 'test-wasm-publish-registry',
504
+ version: '1.0.0',
505
+ publishConfig: {
506
+ registry: `${registryServer.origin}/custom`,
507
+ },
508
+ napi: {
509
+ binaryName: 'test-wasm-publish-registry',
510
+ targets: ['wasm32-wasi-preview1-threads'],
511
+ },
512
+ }
513
+
514
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
515
+
516
+ try {
517
+ await createNpmDirs({
518
+ cwd: tmpDir,
519
+ packageJsonPath: 'package.json',
520
+ })
521
+
522
+ t.deepEqual(registryServer.requests, [])
523
+ t.deepEqual(envRegistryServer.requests, ['/env/@napi-rs/wasm-runtime'])
524
+ } finally {
525
+ await registryServer.close()
526
+ await envRegistryServer.close()
527
+ }
528
+ },
529
+ )
530
+
531
+ test.serial(
532
+ 'should resolve wasm runtime metadata from npm_config_registry',
533
+ async (t) => {
534
+ const { tmpDir, packageJsonPath } = t.context
535
+ const registryServer = await startRegistryServer()
536
+
537
+ process.env.npm_config_registry = `${registryServer.origin}/npm`
538
+
539
+ const packageJson = {
540
+ name: 'test-wasm-env-registry',
541
+ version: '1.0.0',
542
+ napi: {
543
+ binaryName: 'test-wasm-env-registry',
544
+ targets: ['wasm32-wasi-preview1-threads'],
545
+ },
546
+ }
547
+
548
+ await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
549
+
550
+ try {
551
+ await createNpmDirs({
552
+ cwd: tmpDir,
553
+ packageJsonPath: 'package.json',
554
+ })
555
+
556
+ t.deepEqual(registryServer.requests, ['/npm/@napi-rs/wasm-runtime'])
557
+ } finally {
558
+ await registryServer.close()
559
+ }
560
+ },
561
+ )
@@ -0,0 +1,87 @@
1
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ import { parse as parseToml } from '@std/toml'
6
+ import test, { type ExecutionContext } from 'ava'
7
+
8
+ import { newProject, updateCargoTomlNodeApiVersion } from '../new.js'
9
+
10
+ async function rewriteNapiDependency(
11
+ t: ExecutionContext,
12
+ dependency: string,
13
+ minNodeApiVersion: number,
14
+ ) {
15
+ const directory = await mkdtemp(join(tmpdir(), 'napi-rs-new-node-api-'))
16
+ t.teardown(() => rm(directory, { recursive: true, force: true }))
17
+
18
+ const cargoTomlPath = join(directory, 'Cargo.toml')
19
+ await writeFile(
20
+ cargoTomlPath,
21
+ `[package]\nname = "fixture"\nversion = "0.0.0"\n\n[dependencies]\nnapi = ${dependency}\n`,
22
+ )
23
+
24
+ await updateCargoTomlNodeApiVersion(cargoTomlPath, minNodeApiVersion)
25
+
26
+ const cargoToml = parseToml(await readFile(cargoTomlPath, 'utf8')) as any
27
+ return cargoToml.dependencies.napi
28
+ }
29
+
30
+ test('sets the default Node-API feature to napi4', async (t) => {
31
+ const napiDependency = await rewriteNapiDependency(t, '"3.0.0"', 4)
32
+
33
+ t.is(napiDependency.version, '3.0.0')
34
+ t.deepEqual(napiDependency.features, ['napi4'])
35
+ t.false('default-features' in napiDependency)
36
+ })
37
+
38
+ test('sets a requested Node-API feature and replaces the template default', async (t) => {
39
+ const napiDependency = await rewriteNapiDependency(
40
+ t,
41
+ '{ version = "3.0.0", features = ["napi4", "serde-json"] }',
42
+ 6,
43
+ )
44
+
45
+ t.deepEqual(napiDependency.features, ['napi6', 'serde-json'])
46
+ t.false('default-features' in napiDependency)
47
+ })
48
+
49
+ test('sets the supported Node-API 10 feature', async (t) => {
50
+ const napiDependency = await rewriteNapiDependency(t, '"3.0.0"', 10)
51
+
52
+ t.deepEqual(napiDependency.features, ['napi10'])
53
+ })
54
+
55
+ test('disables napi4 defaults for lower Node-API versions while retaining dynamic symbol loading', async (t) => {
56
+ const napiDependency = await rewriteNapiDependency(t, '"3.0.0"', 3)
57
+
58
+ t.is(napiDependency['default-features'], false)
59
+ t.deepEqual(napiDependency.features, ['napi3', 'dyn-symbols'])
60
+ })
61
+
62
+ test('preserves an explicitly disabled default feature configuration', async (t) => {
63
+ const napiDependency = await rewriteNapiDependency(
64
+ t,
65
+ '{ version = "3.0.0", default-features = false, features = ["napi8", "serde-json"] }',
66
+ 2,
67
+ )
68
+
69
+ t.is(napiDependency['default-features'], false)
70
+ t.deepEqual(napiDependency.features, ['napi2', 'serde-json'])
71
+ })
72
+
73
+ for (const invalidVersion of [0, 11, 1.5]) {
74
+ test(`rejects invalid Node-API version ${invalidVersion}`, async (t) => {
75
+ const error = await t.throwsAsync(
76
+ newProject({
77
+ path: join(tmpdir(), 'unused-napi-rs-new-project'),
78
+ enableDefaultTargets: true,
79
+ minNodeApiVersion: invalidVersion,
80
+ dryRun: true,
81
+ }),
82
+ { instanceOf: RangeError },
83
+ )
84
+
85
+ t.regex(error.message, /Unsupported Node-API version/)
86
+ })
87
+ }