@napi-rs/cli 3.9.0 → 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.
- package/README.md +17 -4
- package/dist/cli.js +10359 -8729
- package/dist/index.cjs +10381 -8751
- package/dist/index.d.cts +83 -16
- package/dist/index.d.ts +83 -16
- package/dist/index.js +10359 -8729
- package/docs/wasi.md +318 -4
- package/package.json +5 -6
- package/src/api/__tests__/__snapshots__/templates.spec.ts.md +4003 -53
- package/src/api/__tests__/__snapshots__/templates.spec.ts.snap +0 -0
- package/src/api/__tests__/build-regressions.spec.ts +210 -3
- package/src/api/__tests__/build.spec.ts +2333 -3
- package/src/api/__tests__/create-npm-dirs.spec.ts +105 -3
- package/src/api/__tests__/pre-publish.spec.ts +153 -1
- package/src/api/__tests__/templates.spec.ts +1309 -1
- package/src/api/build.ts +756 -30
- package/src/api/create-npm-dirs.ts +23 -10
- package/src/api/new.ts +13 -18
- package/src/api/pre-publish.ts +34 -11
- package/src/api/rename.ts +10 -21
- package/src/api/templates/binding-target.ts +176 -0
- package/src/api/templates/index.ts +1 -0
- package/src/api/templates/js-binding.ts +54 -10
- package/src/api/templates/load-wasi-template.ts +661 -58
- package/src/api/templates/wasi-worker-template.ts +36 -31
- package/src/commands/build.ts +1 -1
- package/src/utils/__tests__/__snapshots__/typegen.spec.ts.md +18 -28
- package/src/utils/__tests__/__snapshots__/typegen.spec.ts.snap +0 -0
- package/src/utils/__tests__/misc.spec.ts +4 -0
- package/src/utils/__tests__/reconciliation.spec.ts +676 -0
- package/src/utils/__tests__/serialize.spec.ts +55 -0
- package/src/utils/__tests__/target.spec.ts +221 -0
- package/src/utils/__tests__/typegen.spec.ts +115 -0
- package/src/utils/config.ts +50 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/misc.ts +351 -79
- package/src/utils/serialize.ts +47 -0
- package/src/utils/target.ts +150 -1
- package/src/utils/typegen.ts +608 -42
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
rmSync,
|
|
6
6
|
writeFileSync,
|
|
7
7
|
} from 'node:fs'
|
|
8
|
-
import { exec } from 'node:child_process'
|
|
8
|
+
import { exec, spawnSync } from 'node:child_process'
|
|
9
9
|
import {
|
|
10
10
|
copyFile,
|
|
11
11
|
mkdir,
|
|
@@ -19,20 +19,29 @@ import { tmpdir } from 'node:os'
|
|
|
19
19
|
import { join, dirname, resolve } from 'node:path'
|
|
20
20
|
import { join as posixJoin, sep as posixSep } from 'node:path/posix'
|
|
21
21
|
import { sep as win32Sep } from 'node:path/win32'
|
|
22
|
-
import { fileURLToPath } from 'node:url'
|
|
22
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
23
23
|
|
|
24
24
|
import ava, { type ExecutionContext, type TestFn } from 'ava'
|
|
25
|
+
import { parseSync } from 'oxc-parser'
|
|
26
|
+
import ts from 'typescript'
|
|
25
27
|
|
|
26
28
|
import {
|
|
29
|
+
bindingTargetDeclarationPredicate,
|
|
27
30
|
buildProject,
|
|
31
|
+
checkAsyncRuntimeHostContract,
|
|
32
|
+
EMNAPI_WASI_SDK_34_LINK_DIR,
|
|
33
|
+
ensureBindingTargetDeclaration,
|
|
28
34
|
generateTypeDef,
|
|
29
35
|
napiCrossToolchainEnvs,
|
|
36
|
+
prepareWasiBindingTypeDef,
|
|
30
37
|
resolveBuildFormat,
|
|
38
|
+
resolveWasmMemory,
|
|
39
|
+
selectEmnapiLinkDir,
|
|
31
40
|
validateCrossCompileFlags,
|
|
32
41
|
validateNapiCrossSupport,
|
|
33
42
|
writeJsBinding,
|
|
34
43
|
} from '../build.js'
|
|
35
|
-
import { getSystemDefaultTarget } from '../../utils/index.js'
|
|
44
|
+
import { getSystemDefaultTarget, scanExportedName } from '../../utils/index.js'
|
|
36
45
|
|
|
37
46
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
38
47
|
const repoRoot = resolve(__dirname, '../../../..')
|
|
@@ -244,6 +253,1963 @@ test('writeJsBinding uses the explicit format independently of the filename', as
|
|
|
244
253
|
t.regex(commonjs, /module\.exports\.sum = nativeBinding\.sum/)
|
|
245
254
|
t.regex(esm, /export \{ sum \}/)
|
|
246
255
|
t.regex(legacyEsm, /export \{ sum \}/)
|
|
256
|
+
t.regex(
|
|
257
|
+
commonjs,
|
|
258
|
+
/module\.exports\.__napiBindingTarget = __napiStampBindingTarget\(nativeBinding, __napiLoadedBindingTarget\)/,
|
|
259
|
+
)
|
|
260
|
+
t.regex(esm, /export const __napiBindingTarget = __napiLoadedBindingTarget/)
|
|
261
|
+
t.regex(
|
|
262
|
+
legacyEsm,
|
|
263
|
+
/export const __napiBindingTarget = __napiLoadedBindingTarget/,
|
|
264
|
+
)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
test('writeJsBinding rejects a napi export named __napiBindingTarget', async (t) => {
|
|
268
|
+
await t.throwsAsync(
|
|
269
|
+
writeJsBinding({
|
|
270
|
+
platform: true,
|
|
271
|
+
idents: ['__napiBindingTarget'],
|
|
272
|
+
binaryName: 'build-integration',
|
|
273
|
+
packageName: 'build-integration',
|
|
274
|
+
version: '0.1.0',
|
|
275
|
+
outputDir: t.context.projectDir,
|
|
276
|
+
}),
|
|
277
|
+
{ message: /reserved by the generated binding loader/ },
|
|
278
|
+
)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
test('the generated loader reports the flavor its library-path override loaded', async (t) => {
|
|
282
|
+
const { projectDir } = t.context
|
|
283
|
+
const overridePath = join(projectDir, 'fake-wasip1.cjs')
|
|
284
|
+
const plainPath = join(projectDir, 'fake-native.cjs')
|
|
285
|
+
await Promise.all([
|
|
286
|
+
// a stand-in for a generated WASI loader: it reports its own flavor
|
|
287
|
+
writeFile(
|
|
288
|
+
overridePath,
|
|
289
|
+
`module.exports = { sum: (a, b) => a + b }\nmodule.exports.__napiBindingTarget = 'wasm32-wasip1'\n`,
|
|
290
|
+
),
|
|
291
|
+
writeFile(plainPath, `module.exports = { sum: (a, b) => a + b }\n`),
|
|
292
|
+
])
|
|
293
|
+
await writeJsBinding({
|
|
294
|
+
platform: true,
|
|
295
|
+
idents: ['sum'],
|
|
296
|
+
binaryName: 'build-integration',
|
|
297
|
+
packageName: 'build-integration',
|
|
298
|
+
version: '0.1.0',
|
|
299
|
+
outputDir: projectDir,
|
|
300
|
+
})
|
|
301
|
+
const rootPath = join(projectDir, 'index.js')
|
|
302
|
+
|
|
303
|
+
const probe = (libraryPath: string) => {
|
|
304
|
+
const result = spawnSync(
|
|
305
|
+
process.execPath,
|
|
306
|
+
[
|
|
307
|
+
'-e',
|
|
308
|
+
// require the override FIRST: the root loader aliases it, so an
|
|
309
|
+
// unconditional assignment would rewrite the override's own marker
|
|
310
|
+
`const override = require(${JSON.stringify(libraryPath)})
|
|
311
|
+
const root = require(${JSON.stringify(rootPath)})
|
|
312
|
+
console.log(
|
|
313
|
+
JSON.stringify({
|
|
314
|
+
root: root.__napiBindingTarget,
|
|
315
|
+
override: override.__napiBindingTarget,
|
|
316
|
+
aliased: root === override,
|
|
317
|
+
sum: root.sum(1, 2),
|
|
318
|
+
}),
|
|
319
|
+
)`,
|
|
320
|
+
],
|
|
321
|
+
{
|
|
322
|
+
encoding: 'utf8',
|
|
323
|
+
env: { ...process.env, NAPI_RS_NATIVE_LIBRARY_PATH: libraryPath },
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
t.is(result.status, 0, `${result.stdout}\n${result.stderr}`)
|
|
327
|
+
return JSON.parse(result.stdout)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
t.deepEqual(probe(overridePath), {
|
|
331
|
+
root: 'wasm32-wasip1',
|
|
332
|
+
override: 'wasm32-wasip1',
|
|
333
|
+
aliased: true,
|
|
334
|
+
sum: 3,
|
|
335
|
+
})
|
|
336
|
+
t.deepEqual(probe(plainPath), {
|
|
337
|
+
root: 'native',
|
|
338
|
+
override: 'native',
|
|
339
|
+
aliased: true,
|
|
340
|
+
sum: 3,
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
// A stand-in for a `#[napi(module_exports)]` hook: it attaches names to the
|
|
345
|
+
// addon's exports object imperatively, so napi-rs type generation never sees
|
|
346
|
+
// them and `assertBindingTargetIdentFree` cannot either. Only the loader can.
|
|
347
|
+
const writeFakePlatformPackage = async (projectDir: string, source: string) => {
|
|
348
|
+
const { platformArchABI } = getSystemDefaultTarget()
|
|
349
|
+
const packageDir = join(
|
|
350
|
+
projectDir,
|
|
351
|
+
'node_modules',
|
|
352
|
+
`build-integration-${platformArchABI}`,
|
|
353
|
+
)
|
|
354
|
+
await mkdir(packageDir, { recursive: true })
|
|
355
|
+
await Promise.all([
|
|
356
|
+
writeFile(
|
|
357
|
+
join(packageDir, 'package.json'),
|
|
358
|
+
`{"name":"build-integration-${platformArchABI}","version":"0.1.0","main":"index.js"}\n`,
|
|
359
|
+
),
|
|
360
|
+
writeFile(join(packageDir, 'index.js'), source),
|
|
361
|
+
])
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const requireRootLoaderInChild = (rootPath: string, body: string) =>
|
|
365
|
+
spawnSync(
|
|
366
|
+
process.execPath,
|
|
367
|
+
['-e', `const binding = require(${JSON.stringify(rootPath)})\n${body}`],
|
|
368
|
+
{ encoding: 'utf8' },
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
test('a module_exports hook may not claim __napiBindingTarget', async (t) => {
|
|
372
|
+
const { projectDir } = t.context
|
|
373
|
+
await writeFakePlatformPackage(
|
|
374
|
+
projectDir,
|
|
375
|
+
`const exportsObject = { sum: (a, b) => a + b }
|
|
376
|
+
exportsObject.__napiBindingTarget = 'addon-owned-value'
|
|
377
|
+
module.exports = exportsObject
|
|
378
|
+
`,
|
|
379
|
+
)
|
|
380
|
+
await writeJsBinding({
|
|
381
|
+
platform: true,
|
|
382
|
+
idents: ['sum'],
|
|
383
|
+
binaryName: 'build-integration',
|
|
384
|
+
packageName: 'build-integration',
|
|
385
|
+
version: '0.1.0',
|
|
386
|
+
outputDir: projectDir,
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
const result = requireRootLoaderInChild(
|
|
390
|
+
join(projectDir, 'index.js'),
|
|
391
|
+
`console.log(binding.__napiBindingTarget)`,
|
|
392
|
+
)
|
|
393
|
+
t.not(result.status, 0, `${result.stdout}\n${result.stderr}`)
|
|
394
|
+
t.regex(result.stderr, /reserved by the generated binding loader/)
|
|
395
|
+
t.regex(result.stderr, /ERR_NAPI_BINDING_TARGET_CONFLICT/)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
test('a zero-ident package still rejects a claimed binding target', async (t) => {
|
|
399
|
+
const { projectDir } = t.context
|
|
400
|
+
// the `!enableTypeDef` shape: no type-def metadata at all, so the build-time
|
|
401
|
+
// assertion has an empty list to check and the loader is the only guard left
|
|
402
|
+
await writeFakePlatformPackage(
|
|
403
|
+
projectDir,
|
|
404
|
+
`const exportsObject = { sum: (a, b) => a + b }
|
|
405
|
+
exportsObject.__napiBindingTarget = 'addon-owned-value'
|
|
406
|
+
module.exports = exportsObject
|
|
407
|
+
`,
|
|
408
|
+
)
|
|
409
|
+
await writeJsBinding({
|
|
410
|
+
platform: true,
|
|
411
|
+
idents: [],
|
|
412
|
+
wasiFlavors: ['wasm32-wasi'],
|
|
413
|
+
binaryName: 'build-integration',
|
|
414
|
+
packageName: 'build-integration',
|
|
415
|
+
version: '0.1.0',
|
|
416
|
+
outputDir: projectDir,
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
const result = requireRootLoaderInChild(
|
|
420
|
+
join(projectDir, 'index.js'),
|
|
421
|
+
`console.log(binding.__napiBindingTarget)`,
|
|
422
|
+
)
|
|
423
|
+
t.not(result.status, 0, `${result.stdout}\n${result.stderr}`)
|
|
424
|
+
t.regex(result.stderr, /ERR_NAPI_BINDING_TARGET_CONFLICT/)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
test('a frozen addon loads without the binding target stamp', async (t) => {
|
|
428
|
+
const { projectDir } = t.context
|
|
429
|
+
// `Object::freeze` in a `#[napi(module_exports)]` hook. Reporting the
|
|
430
|
+
// artifact is metadata; it must never fail an otherwise successful load.
|
|
431
|
+
await writeFakePlatformPackage(
|
|
432
|
+
projectDir,
|
|
433
|
+
`module.exports = Object.freeze({ sum: (a, b) => a + b })\n`,
|
|
434
|
+
)
|
|
435
|
+
await writeJsBinding({
|
|
436
|
+
platform: true,
|
|
437
|
+
idents: ['sum'],
|
|
438
|
+
binaryName: 'build-integration',
|
|
439
|
+
packageName: 'build-integration',
|
|
440
|
+
version: '0.1.0',
|
|
441
|
+
outputDir: projectDir,
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
const result = requireRootLoaderInChild(
|
|
445
|
+
join(projectDir, 'index.js'),
|
|
446
|
+
`console.log(
|
|
447
|
+
JSON.stringify({
|
|
448
|
+
sum: binding.sum(1, 2),
|
|
449
|
+
target: binding.__napiBindingTarget ?? null,
|
|
450
|
+
}),
|
|
451
|
+
)`,
|
|
452
|
+
)
|
|
453
|
+
t.is(result.status, 0, `${result.stdout}\n${result.stderr}`)
|
|
454
|
+
t.deepEqual(JSON.parse(result.stdout), { sum: 3, target: null })
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
// Node's CJS -> ESM named export detection is `cjs-module-lexer`, a static
|
|
458
|
+
// scanner: it reports `__napiBindingTarget` only when it can see
|
|
459
|
+
// `module.exports.__napiBindingTarget =` in the source. A bare guard call is
|
|
460
|
+
// invisible to it, and the import then fails to link at all.
|
|
461
|
+
const importBindingTargetInChild = (rootPath: string) =>
|
|
462
|
+
spawnSync(
|
|
463
|
+
process.execPath,
|
|
464
|
+
[
|
|
465
|
+
'--input-type=module',
|
|
466
|
+
'-e',
|
|
467
|
+
`import { __napiBindingTarget } from ${JSON.stringify(
|
|
468
|
+
pathToFileURL(rootPath).href,
|
|
469
|
+
)}
|
|
470
|
+
console.log(JSON.stringify(__napiBindingTarget))`,
|
|
471
|
+
],
|
|
472
|
+
{ encoding: 'utf8' },
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
test('the CommonJS loader exposes __napiBindingTarget as an ESM named export', async (t) => {
|
|
476
|
+
const { projectDir } = t.context
|
|
477
|
+
await writeFakePlatformPackage(
|
|
478
|
+
projectDir,
|
|
479
|
+
`module.exports = { sum: (a, b) => a + b }\n`,
|
|
480
|
+
)
|
|
481
|
+
await writeJsBinding({
|
|
482
|
+
platform: true,
|
|
483
|
+
idents: ['sum'],
|
|
484
|
+
binaryName: 'build-integration',
|
|
485
|
+
packageName: 'build-integration',
|
|
486
|
+
version: '0.1.0',
|
|
487
|
+
outputDir: projectDir,
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
const result = importBindingTargetInChild(join(projectDir, 'index.js'))
|
|
491
|
+
t.is(result.status, 0, `${result.stdout}\n${result.stderr}`)
|
|
492
|
+
t.is(result.stdout.trim(), '"native"')
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
test('a frozen addon keeps __napiBindingTarget importable, just undefined', async (t) => {
|
|
496
|
+
const { projectDir } = t.context
|
|
497
|
+
// The lexer is static, so the name links either way; the runtime skip is what
|
|
498
|
+
// leaves it undefined. A named import that throws `SyntaxError` at link time
|
|
499
|
+
// would be a much louder break than a missing value.
|
|
500
|
+
await writeFakePlatformPackage(
|
|
501
|
+
projectDir,
|
|
502
|
+
`module.exports = Object.freeze({ sum: (a, b) => a + b })\n`,
|
|
503
|
+
)
|
|
504
|
+
await writeJsBinding({
|
|
505
|
+
platform: true,
|
|
506
|
+
idents: ['sum'],
|
|
507
|
+
binaryName: 'build-integration',
|
|
508
|
+
packageName: 'build-integration',
|
|
509
|
+
version: '0.1.0',
|
|
510
|
+
outputDir: projectDir,
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
const result = importBindingTargetInChild(join(projectDir, 'index.js'))
|
|
514
|
+
t.is(result.status, 0, `${result.stdout}\n${result.stderr}`)
|
|
515
|
+
t.is(result.stdout.trim(), 'undefined')
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
// `Object.create(proto)` in a `#[napi(module_exports)]` hook — napi-rs itself
|
|
519
|
+
// reaches `Object.setPrototypeOf` off the global, so the prototype an addon's
|
|
520
|
+
// exports object carries is not beyond an addon's reach. `hasOwnProperty` does
|
|
521
|
+
// not see an inherited accessor, so an ordinary assignment would reach its
|
|
522
|
+
// setter.
|
|
523
|
+
const inheritedBindingTargetAccessor = (setterBody: string) =>
|
|
524
|
+
`const proto = {}
|
|
525
|
+
Object.defineProperty(proto, '__napiBindingTarget', {
|
|
526
|
+
get() {
|
|
527
|
+
return undefined
|
|
528
|
+
},
|
|
529
|
+
set() {
|
|
530
|
+
${setterBody}
|
|
531
|
+
},
|
|
532
|
+
configurable: true,
|
|
533
|
+
})
|
|
534
|
+
const exportsObject = Object.create(proto)
|
|
535
|
+
exportsObject.sum = (a, b) => a + b
|
|
536
|
+
module.exports = exportsObject
|
|
537
|
+
`
|
|
538
|
+
|
|
539
|
+
const REPORT_BINDING_TARGET = `console.log(
|
|
540
|
+
JSON.stringify({
|
|
541
|
+
sum: binding.sum(1, 2),
|
|
542
|
+
target: binding.__napiBindingTarget ?? null,
|
|
543
|
+
own: Object.prototype.hasOwnProperty.call(binding, '__napiBindingTarget'),
|
|
544
|
+
}),
|
|
545
|
+
)`
|
|
546
|
+
|
|
547
|
+
test('an addon whose prototype carries __napiBindingTarget still loads and reports its target', async (t) => {
|
|
548
|
+
const { projectDir } = t.context
|
|
549
|
+
// The throwing half: an inherited setter that refuses the write would kill an
|
|
550
|
+
// otherwise successful load at the stamp — the same regression the frozen
|
|
551
|
+
// skip above exists to prevent.
|
|
552
|
+
await writeFakePlatformPackage(
|
|
553
|
+
projectDir,
|
|
554
|
+
inheritedBindingTargetAccessor(`throw new Error('addon setter refused')`),
|
|
555
|
+
)
|
|
556
|
+
await writeJsBinding({
|
|
557
|
+
platform: true,
|
|
558
|
+
idents: ['sum'],
|
|
559
|
+
binaryName: 'build-integration',
|
|
560
|
+
packageName: 'build-integration',
|
|
561
|
+
version: '0.1.0',
|
|
562
|
+
outputDir: projectDir,
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
const result = requireRootLoaderInChild(
|
|
566
|
+
join(projectDir, 'index.js'),
|
|
567
|
+
REPORT_BINDING_TARGET,
|
|
568
|
+
)
|
|
569
|
+
t.is(result.status, 0, `${result.stdout}\n${result.stderr}`)
|
|
570
|
+
t.deepEqual(JSON.parse(result.stdout), {
|
|
571
|
+
sum: 3,
|
|
572
|
+
target: 'native',
|
|
573
|
+
own: true,
|
|
574
|
+
})
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
test('an inherited setter does not swallow the binding target', async (t) => {
|
|
578
|
+
const { projectDir } = t.context
|
|
579
|
+
// The absorbing half: the setter accepts the write and creates nothing, so
|
|
580
|
+
// both `require(...).__napiBindingTarget` and the ESM named import resolve to
|
|
581
|
+
// `undefined` while the generated `.d.ts` promises a literal.
|
|
582
|
+
await writeFakePlatformPackage(projectDir, inheritedBindingTargetAccessor(''))
|
|
583
|
+
await writeJsBinding({
|
|
584
|
+
platform: true,
|
|
585
|
+
idents: ['sum'],
|
|
586
|
+
binaryName: 'build-integration',
|
|
587
|
+
packageName: 'build-integration',
|
|
588
|
+
version: '0.1.0',
|
|
589
|
+
outputDir: projectDir,
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
const required = requireRootLoaderInChild(
|
|
593
|
+
join(projectDir, 'index.js'),
|
|
594
|
+
REPORT_BINDING_TARGET,
|
|
595
|
+
)
|
|
596
|
+
t.is(required.status, 0, `${required.stdout}\n${required.stderr}`)
|
|
597
|
+
t.deepEqual(JSON.parse(required.stdout), {
|
|
598
|
+
sum: 3,
|
|
599
|
+
target: 'native',
|
|
600
|
+
own: true,
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
// and the value the lexer-linked named import reads is the one on that same
|
|
604
|
+
// object, so the stamp has to land as an own property for the import to work
|
|
605
|
+
const imported = importBindingTargetInChild(join(projectDir, 'index.js'))
|
|
606
|
+
t.is(imported.status, 0, `${imported.stdout}\n${imported.stderr}`)
|
|
607
|
+
t.is(imported.stdout.trim(), '"native"')
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
test('an exotic binding object never fails the load', async (t) => {
|
|
611
|
+
const { projectDir } = t.context
|
|
612
|
+
// A `Proxy` whose `defineProperty` trap refuses is the one shape the stamp
|
|
613
|
+
// cannot satisfy. Today's assignment is a sloppy-mode no-op there and a bare
|
|
614
|
+
// `Object.defineProperty` would throw, so the skip is what keeps the rule the
|
|
615
|
+
// frozen case states: metadata never fails an otherwise successful load.
|
|
616
|
+
await writeFakePlatformPackage(
|
|
617
|
+
projectDir,
|
|
618
|
+
`module.exports = new Proxy(
|
|
619
|
+
{ sum: (a, b) => a + b },
|
|
620
|
+
{ defineProperty: () => false },
|
|
621
|
+
)
|
|
622
|
+
`,
|
|
623
|
+
)
|
|
624
|
+
await writeJsBinding({
|
|
625
|
+
platform: true,
|
|
626
|
+
idents: ['sum'],
|
|
627
|
+
binaryName: 'build-integration',
|
|
628
|
+
packageName: 'build-integration',
|
|
629
|
+
version: '0.1.0',
|
|
630
|
+
outputDir: projectDir,
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
const result = requireRootLoaderInChild(
|
|
634
|
+
join(projectDir, 'index.js'),
|
|
635
|
+
REPORT_BINDING_TARGET,
|
|
636
|
+
)
|
|
637
|
+
t.is(result.status, 0, `${result.stdout}\n${result.stderr}`)
|
|
638
|
+
t.deepEqual(JSON.parse(result.stdout), {
|
|
639
|
+
sum: 3,
|
|
640
|
+
target: null,
|
|
641
|
+
own: false,
|
|
642
|
+
})
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
const bindingTargetDeclarationOf = (source: string) =>
|
|
646
|
+
source
|
|
647
|
+
.split('\n')
|
|
648
|
+
.find((line) => line.startsWith('export declare const __napiBindingTarget'))
|
|
649
|
+
|
|
650
|
+
test('the declared binding target covers every loadable artifact', async (t) => {
|
|
651
|
+
const { projectDir, typeDefDir } = t.context
|
|
652
|
+
await writeFile(
|
|
653
|
+
join(typeDefDir, 'sum.type'),
|
|
654
|
+
'{"kind":"fn","name":"sum","def":"function sum(a: number, b: number): number"}\n',
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
const { dts } = await generateTypeDef({
|
|
658
|
+
typeDefDir,
|
|
659
|
+
cwd: projectDir,
|
|
660
|
+
declareBindingTarget: true,
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
// Nothing here declares a WASI target, yet NAPI_RS_NATIVE_LIBRARY_PATH may
|
|
664
|
+
// still point the loader at a generated WASI loader, which the root entry
|
|
665
|
+
// then reports. A union of only the configured targets would make TypeScript
|
|
666
|
+
// reject those comparisons.
|
|
667
|
+
t.is(
|
|
668
|
+
bindingTargetDeclarationOf(dts),
|
|
669
|
+
"export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'",
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
const { dts: withoutLoader } = await generateTypeDef({
|
|
673
|
+
typeDefDir,
|
|
674
|
+
cwd: projectDir,
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
t.false(withoutLoader.includes('__napiBindingTarget'))
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
test('a preserved WASI declaration gains the binding target declaration', async (t) => {
|
|
681
|
+
const { projectDir, typeDefDir } = t.context
|
|
682
|
+
await writeFile(
|
|
683
|
+
join(typeDefDir, 'sum.type'),
|
|
684
|
+
'{"kind":"fn","name":"sum","def":"function sum(a: number, b: number): number"}\n',
|
|
685
|
+
)
|
|
686
|
+
const { dts } = await generateTypeDef({
|
|
687
|
+
typeDefDir,
|
|
688
|
+
cwd: projectDir,
|
|
689
|
+
declareBindingTarget: true,
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
// what a WASI build before `__napiBindingTarget` left on disk
|
|
693
|
+
const preserved = `/* auto-generated by NAPI-RS */
|
|
694
|
+
|
|
695
|
+
export declare function sum(a: number, b: number): number
|
|
696
|
+
`
|
|
697
|
+
const refreshed = ensureBindingTargetDeclaration(preserved)
|
|
698
|
+
|
|
699
|
+
t.true(refreshed.startsWith(preserved))
|
|
700
|
+
t.is(bindingTargetDeclarationOf(refreshed), bindingTargetDeclarationOf(dts))
|
|
701
|
+
// a second build must not append a second declaration
|
|
702
|
+
t.is(ensureBindingTargetDeclaration(refreshed), refreshed)
|
|
703
|
+
// an already declared file is left untouched wherever it declares it
|
|
704
|
+
t.is(ensureBindingTargetDeclaration(dts), dts)
|
|
705
|
+
|
|
706
|
+
// a declaration file without type generation exports by assignment, which
|
|
707
|
+
// cannot carry a named export declaration
|
|
708
|
+
const exportAssignment = `declare const binding: Record<string, unknown>
|
|
709
|
+
export = binding
|
|
710
|
+
`
|
|
711
|
+
t.is(ensureBindingTargetDeclaration(exportAssignment), exportAssignment)
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
test('a WASI flavor declaration names only that flavor', async (t) => {
|
|
715
|
+
const { projectDir, typeDefDir } = t.context
|
|
716
|
+
await writeFile(
|
|
717
|
+
join(typeDefDir, 'sum.type'),
|
|
718
|
+
'{"kind":"fn","name":"sum","def":"function sum(a: number, b: number): number"}\n',
|
|
719
|
+
)
|
|
720
|
+
const { dts } = await generateTypeDef({
|
|
721
|
+
typeDefDir,
|
|
722
|
+
cwd: projectDir,
|
|
723
|
+
declareBindingTarget: true,
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
// a flavor's loaders report a compile-time-fixed identity, so the union the
|
|
727
|
+
// root entry inherits from `NAPI_RS_NATIVE_LIBRARY_PATH` is unreachable here
|
|
728
|
+
const threaded = ensureBindingTargetDeclaration(dts, 'wasm32-wasi')
|
|
729
|
+
t.is(
|
|
730
|
+
bindingTargetDeclarationOf(threaded),
|
|
731
|
+
"export declare const __napiBindingTarget: 'wasm32-wasi'",
|
|
732
|
+
)
|
|
733
|
+
const threadless = ensureBindingTargetDeclaration(dts, 'wasm32-wasip1')
|
|
734
|
+
t.is(
|
|
735
|
+
bindingTargetDeclarationOf(threadless),
|
|
736
|
+
"export declare const __napiBindingTarget: 'wasm32-wasip1'",
|
|
737
|
+
)
|
|
738
|
+
// narrowing replaces the inherited union instead of declaring the name twice
|
|
739
|
+
t.is(threaded.split('__napiBindingTarget').length - 1, 1)
|
|
740
|
+
|
|
741
|
+
// a second build must not append a second declaration
|
|
742
|
+
t.is(ensureBindingTargetDeclaration(threadless, 'wasm32-wasip1'), threadless)
|
|
743
|
+
|
|
744
|
+
// a preserved file that never declared it gains the flavor literal, not the
|
|
745
|
+
// union
|
|
746
|
+
const preserved = `/* auto-generated by NAPI-RS */
|
|
747
|
+
|
|
748
|
+
export declare function sum(a: number, b: number): number
|
|
749
|
+
`
|
|
750
|
+
t.is(
|
|
751
|
+
bindingTargetDeclarationOf(
|
|
752
|
+
ensureBindingTargetDeclaration(preserved, 'wasm32-wasi'),
|
|
753
|
+
),
|
|
754
|
+
"export declare const __napiBindingTarget: 'wasm32-wasi'",
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
// the root entry keeps the union (see `NAPI_RS_NATIVE_LIBRARY_PATH`)
|
|
758
|
+
t.is(ensureBindingTargetDeclaration(dts), dts)
|
|
759
|
+
|
|
760
|
+
// a build without `napi-derive`'s `type-def` feature exports by assignment,
|
|
761
|
+
// which cannot carry a named export declaration
|
|
762
|
+
const exportAssignment = `declare const binding: Record<string, unknown>
|
|
763
|
+
export = binding
|
|
764
|
+
`
|
|
765
|
+
t.is(
|
|
766
|
+
ensureBindingTargetDeclaration(exportAssignment, 'wasm32-wasip1'),
|
|
767
|
+
exportAssignment,
|
|
768
|
+
)
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
test('a mention of __napiBindingTarget is not a declaration of it', (t) => {
|
|
772
|
+
// napi-derive copies `js_doc` into the `.d.ts` verbatim, so a doc comment can
|
|
773
|
+
// name the export without declaring it
|
|
774
|
+
const mentionOnly = `/* auto-generated by NAPI-RS */
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Mirrors the loader's \`__napiBindingTarget\` export.
|
|
778
|
+
*/
|
|
779
|
+
export declare function bindingTarget(): string
|
|
780
|
+
`
|
|
781
|
+
t.is(
|
|
782
|
+
bindingTargetDeclarationOf(
|
|
783
|
+
ensureBindingTargetDeclaration(mentionOnly, 'wasm32-wasip1'),
|
|
784
|
+
),
|
|
785
|
+
"export declare const __napiBindingTarget: 'wasm32-wasip1'",
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
// `assertBindingTargetIdentFree` only rejects the exact name, so a longer
|
|
789
|
+
// identifier is a legal export and not this declaration
|
|
790
|
+
const longerIdent = `/* auto-generated by NAPI-RS */
|
|
791
|
+
|
|
792
|
+
export declare const __napiBindingTargetInfo: string
|
|
793
|
+
`
|
|
794
|
+
const refreshed = ensureBindingTargetDeclaration(longerIdent, 'wasm32-wasi')
|
|
795
|
+
t.true(refreshed.startsWith(longerIdent))
|
|
796
|
+
t.true(
|
|
797
|
+
refreshed.includes(
|
|
798
|
+
"export declare const __napiBindingTarget: 'wasm32-wasi'\n",
|
|
799
|
+
),
|
|
800
|
+
)
|
|
801
|
+
})
|
|
802
|
+
|
|
803
|
+
test('a fresh WASI declaration narrows the inherited root union', async (t) => {
|
|
804
|
+
const { projectDir, typeDefDir } = t.context
|
|
805
|
+
await writeFile(
|
|
806
|
+
join(typeDefDir, 'sum.type'),
|
|
807
|
+
'{"kind":"fn","name":"sum","def":"function sum(a: number, b: number): number"}\n',
|
|
808
|
+
)
|
|
809
|
+
const { dts } = await generateTypeDef({
|
|
810
|
+
typeDefDir,
|
|
811
|
+
cwd: projectDir,
|
|
812
|
+
declareBindingTarget: true,
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
// the fresh path derives the flavor declaration from the root `index.d.ts`,
|
|
816
|
+
// which carries the union
|
|
817
|
+
const prepared = prepareWasiBindingTypeDef(
|
|
818
|
+
dts,
|
|
819
|
+
join(projectDir, 'index.d.ts'),
|
|
820
|
+
join(projectDir, 'pkg.wasip1.d.cts'),
|
|
821
|
+
false,
|
|
822
|
+
)
|
|
823
|
+
t.is(
|
|
824
|
+
bindingTargetDeclarationOf(
|
|
825
|
+
ensureBindingTargetDeclaration(prepared, 'wasm32-wasip1'),
|
|
826
|
+
),
|
|
827
|
+
"export declare const __napiBindingTarget: 'wasm32-wasip1'",
|
|
828
|
+
)
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
const ROOT_BINDING_TARGET_DECLARATION =
|
|
832
|
+
"export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'"
|
|
833
|
+
|
|
834
|
+
test('a crate without type defs still declares the binding target', async (t) => {
|
|
835
|
+
const { projectDir, typeDefDir } = t.context
|
|
836
|
+
|
|
837
|
+
// A crate can register every export from a `#[napi(module_exports)]` hook and
|
|
838
|
+
// emit no `.type` file at all. A loader is still written for it, and that
|
|
839
|
+
// loader still exports `__napiBindingTarget`.
|
|
840
|
+
const { exports, dts, dtsWithTypeImports } = await generateTypeDef({
|
|
841
|
+
typeDefDir,
|
|
842
|
+
cwd: projectDir,
|
|
843
|
+
declareBindingTarget: true,
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
t.is(exports.length, 0)
|
|
847
|
+
t.is(bindingTargetDeclarationOf(dts), ROOT_BINDING_TARGET_DECLARATION)
|
|
848
|
+
t.true(dts.startsWith('/* auto-generated by NAPI-RS */'))
|
|
849
|
+
t.is(dtsWithTypeImports, dts)
|
|
850
|
+
})
|
|
851
|
+
|
|
852
|
+
test('a missing type def directory still declares the binding target', async (t) => {
|
|
853
|
+
const { projectDir, typeDefDir } = t.context
|
|
854
|
+
|
|
855
|
+
const { exports, dts, dtsWithTypeImports } = await generateTypeDef({
|
|
856
|
+
typeDefDir: join(typeDefDir, 'missing'),
|
|
857
|
+
cwd: projectDir,
|
|
858
|
+
declareBindingTarget: true,
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
t.is(exports.length, 0)
|
|
862
|
+
t.is(bindingTargetDeclarationOf(dts), ROOT_BINDING_TARGET_DECLARATION)
|
|
863
|
+
t.true(dts.startsWith('/* auto-generated by NAPI-RS */'))
|
|
864
|
+
t.is(dtsWithTypeImports, dts)
|
|
865
|
+
})
|
|
866
|
+
|
|
867
|
+
test('a build that emits no loader declares nothing without type defs', async (t) => {
|
|
868
|
+
const { projectDir, typeDefDir } = t.context
|
|
869
|
+
|
|
870
|
+
const empty = await generateTypeDef({ typeDefDir, cwd: projectDir })
|
|
871
|
+
t.is(empty.dts, '')
|
|
872
|
+
t.is(empty.dtsWithTypeImports, '')
|
|
873
|
+
|
|
874
|
+
const missing = await generateTypeDef({
|
|
875
|
+
typeDefDir: join(typeDefDir, 'missing'),
|
|
876
|
+
cwd: projectDir,
|
|
877
|
+
})
|
|
878
|
+
t.is(missing.dts, '')
|
|
879
|
+
t.is(missing.dtsWithTypeImports, '')
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
test('the declaration-only type def honours the header options', async (t) => {
|
|
883
|
+
const { projectDir, typeDefDir } = t.context
|
|
884
|
+
|
|
885
|
+
const { dts: headerless } = await generateTypeDef({
|
|
886
|
+
typeDefDir,
|
|
887
|
+
cwd: projectDir,
|
|
888
|
+
declareBindingTarget: true,
|
|
889
|
+
noDtsHeader: true,
|
|
890
|
+
})
|
|
891
|
+
t.false(headerless.includes('/* auto-generated by NAPI-RS */'))
|
|
892
|
+
t.is(bindingTargetDeclarationOf(headerless), ROOT_BINDING_TARGET_DECLARATION)
|
|
893
|
+
|
|
894
|
+
const { dts: custom } = await generateTypeDef({
|
|
895
|
+
typeDefDir,
|
|
896
|
+
cwd: projectDir,
|
|
897
|
+
declareBindingTarget: true,
|
|
898
|
+
dtsHeader: '/* custom */\n',
|
|
899
|
+
})
|
|
900
|
+
t.true(custom.startsWith('/* custom */\n'))
|
|
901
|
+
t.is(bindingTargetDeclarationOf(custom), ROOT_BINDING_TARGET_DECLARATION)
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
const countBindingTargetDeclarations = (source: string) =>
|
|
905
|
+
source
|
|
906
|
+
.split('\n')
|
|
907
|
+
.filter((line) =>
|
|
908
|
+
line.startsWith('export declare const __napiBindingTarget'),
|
|
909
|
+
).length
|
|
910
|
+
|
|
911
|
+
test('a header that already declares the binding target is not declared twice', async (t) => {
|
|
912
|
+
const { projectDir, typeDefDir } = t.context
|
|
913
|
+
|
|
914
|
+
// a project that worked around loaders predating the export by declaring it
|
|
915
|
+
// in its own header; a second declaration beside it is a TS2451 redeclaration
|
|
916
|
+
const headerDeclaration =
|
|
917
|
+
"export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'"
|
|
918
|
+
const dtsHeader = `/* auto-generated by NAPI-RS */
|
|
919
|
+
|
|
920
|
+
${headerDeclaration}
|
|
921
|
+
`
|
|
922
|
+
|
|
923
|
+
await writeFile(
|
|
924
|
+
join(typeDefDir, 'sum.type'),
|
|
925
|
+
'{"kind":"fn","name":"sum","def":"function sum(a: number, b: number): number"}\n',
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
const { dts, dtsWithTypeImports, header } = await generateTypeDef({
|
|
929
|
+
typeDefDir,
|
|
930
|
+
cwd: projectDir,
|
|
931
|
+
declareBindingTarget: true,
|
|
932
|
+
dtsHeader,
|
|
933
|
+
})
|
|
934
|
+
t.is(countBindingTargetDeclarations(dts), 1)
|
|
935
|
+
t.is(countBindingTargetDeclarations(dtsWithTypeImports), 1)
|
|
936
|
+
t.is(bindingTargetDeclarationOf(dts), headerDeclaration)
|
|
937
|
+
// the rendered header comes back, so the WASI writer can tell a declaration
|
|
938
|
+
// the header owns from one this build appended
|
|
939
|
+
t.is(header, dtsHeader)
|
|
940
|
+
|
|
941
|
+
// the same header with no `.type` files at all: the declaration-only path
|
|
942
|
+
const emptyTypeDefDir = join(projectDir, 'empty-type-defs')
|
|
943
|
+
await mkdir(emptyTypeDefDir, { recursive: true })
|
|
944
|
+
const { dts: declarationOnly } = await generateTypeDef({
|
|
945
|
+
typeDefDir: emptyTypeDefDir,
|
|
946
|
+
cwd: projectDir,
|
|
947
|
+
declareBindingTarget: true,
|
|
948
|
+
dtsHeader,
|
|
949
|
+
})
|
|
950
|
+
t.is(countBindingTargetDeclarations(declarationOnly), 1)
|
|
951
|
+
|
|
952
|
+
// every route a header reaches the build through
|
|
953
|
+
const headerFile = 'napi-header.d.ts'
|
|
954
|
+
await writeFile(join(projectDir, headerFile), dtsHeader)
|
|
955
|
+
const { dts: fromFile } = await generateTypeDef({
|
|
956
|
+
typeDefDir,
|
|
957
|
+
cwd: projectDir,
|
|
958
|
+
declareBindingTarget: true,
|
|
959
|
+
dtsHeaderFile: headerFile,
|
|
960
|
+
})
|
|
961
|
+
t.is(countBindingTargetDeclarations(fromFile), 1)
|
|
962
|
+
|
|
963
|
+
const { dts: fromConfig } = await generateTypeDef({
|
|
964
|
+
typeDefDir,
|
|
965
|
+
cwd: projectDir,
|
|
966
|
+
declareBindingTarget: true,
|
|
967
|
+
configDtsHeader: dtsHeader,
|
|
968
|
+
})
|
|
969
|
+
t.is(countBindingTargetDeclarations(fromConfig), 1)
|
|
970
|
+
|
|
971
|
+
// the WASI flavor file derived from that root declaration keeps the one
|
|
972
|
+
// declaration instead of gaining a second with a conflicting type
|
|
973
|
+
const wasi = ensureBindingTargetDeclaration(
|
|
974
|
+
prepareWasiBindingTypeDef(
|
|
975
|
+
dts,
|
|
976
|
+
join(projectDir, 'index.d.ts'),
|
|
977
|
+
join(projectDir, 'pkg.wasip1.d.cts'),
|
|
978
|
+
false,
|
|
979
|
+
),
|
|
980
|
+
'wasm32-wasip1',
|
|
981
|
+
dtsHeader,
|
|
982
|
+
)
|
|
983
|
+
t.is(countBindingTargetDeclarations(wasi), 1)
|
|
984
|
+
t.is(bindingTargetDeclarationOf(wasi), headerDeclaration)
|
|
985
|
+
|
|
986
|
+
// a header that declares nothing still gains the union
|
|
987
|
+
const { dts: generated, header: defaultHeader } = await generateTypeDef({
|
|
988
|
+
typeDefDir,
|
|
989
|
+
cwd: projectDir,
|
|
990
|
+
declareBindingTarget: true,
|
|
991
|
+
})
|
|
992
|
+
t.is(bindingTargetDeclarationOf(generated), ROOT_BINDING_TARGET_DECLARATION)
|
|
993
|
+
t.is(countBindingTargetDeclarations(generated), 1)
|
|
994
|
+
t.true(defaultHeader.startsWith('/* auto-generated by NAPI-RS */'))
|
|
995
|
+
|
|
996
|
+
const { header: headerless } = await generateTypeDef({
|
|
997
|
+
typeDefDir,
|
|
998
|
+
cwd: projectDir,
|
|
999
|
+
declareBindingTarget: true,
|
|
1000
|
+
noDtsHeader: true,
|
|
1001
|
+
})
|
|
1002
|
+
t.is(headerless, '')
|
|
1003
|
+
})
|
|
1004
|
+
|
|
1005
|
+
test('an export-assignment header is left alone without type defs', async (t) => {
|
|
1006
|
+
const { tmpDir, projectDir, typeDefDir } = t.context
|
|
1007
|
+
|
|
1008
|
+
// What a package whose header describes the whole module by assignment ships
|
|
1009
|
+
// — the form a build without `napi-derive`'s `type-def` feature emits. An
|
|
1010
|
+
// export assignment cannot sit beside a named export (TS2309), so the
|
|
1011
|
+
// declaration this build would otherwise append has nowhere to go, and the
|
|
1012
|
+
// export it describes is unreachable through `export =` anyway. The same
|
|
1013
|
+
// rule `ensureBindingTargetDeclaration` applies to the derived WASI file.
|
|
1014
|
+
//
|
|
1015
|
+
// Kept out of the export-form table below: every row there is generated with
|
|
1016
|
+
// a `.type` file, and the generated body is itself a named export, so an
|
|
1017
|
+
// `export =` row would carry the header's own TS2309 rather than this
|
|
1018
|
+
// branch's — and that table's consumer imports a named export the form has
|
|
1019
|
+
// none of.
|
|
1020
|
+
const dtsHeader = `declare const binding: { foo(): number }
|
|
1021
|
+
export = binding
|
|
1022
|
+
`
|
|
1023
|
+
|
|
1024
|
+
const { exports, dts, dtsWithTypeImports, header } = await generateTypeDef({
|
|
1025
|
+
typeDefDir,
|
|
1026
|
+
cwd: projectDir,
|
|
1027
|
+
declareBindingTarget: true,
|
|
1028
|
+
dtsHeader,
|
|
1029
|
+
})
|
|
1030
|
+
|
|
1031
|
+
t.is(exports.length, 0)
|
|
1032
|
+
t.is(header, dtsHeader)
|
|
1033
|
+
t.true(dts.startsWith(dtsHeader))
|
|
1034
|
+
t.is(dts.trim(), dtsHeader.trim())
|
|
1035
|
+
t.deepEqual(exportedBindingTargetTypes(dts), [])
|
|
1036
|
+
t.is(countBindingTargetDeclarations(dts), 0)
|
|
1037
|
+
t.is(dtsWithTypeImports, dts)
|
|
1038
|
+
|
|
1039
|
+
const directory = join(tmpDir, 'export-assignment-header')
|
|
1040
|
+
const flavor = ensureBindingTargetDeclaration(
|
|
1041
|
+
prepareWasiBindingTypeDef(
|
|
1042
|
+
dts,
|
|
1043
|
+
join(directory, 'index.d.ts'),
|
|
1044
|
+
join(directory, 'flavor.d.cts'),
|
|
1045
|
+
false,
|
|
1046
|
+
),
|
|
1047
|
+
'wasm32-wasip1',
|
|
1048
|
+
header,
|
|
1049
|
+
)
|
|
1050
|
+
t.true(flavor.startsWith(dtsHeader))
|
|
1051
|
+
t.deepEqual(exportedBindingTargetTypes(flavor), [])
|
|
1052
|
+
|
|
1053
|
+
// TypeScript's own verdict: what this build wrote is worth exactly what the
|
|
1054
|
+
// header alone is worth, which is nothing at all.
|
|
1055
|
+
const codes = await semanticDiagnosticCodes(directory, {
|
|
1056
|
+
'baseline.d.ts': dtsHeader,
|
|
1057
|
+
'index.d.ts': dts,
|
|
1058
|
+
'flavor.d.cts': flavor,
|
|
1059
|
+
})
|
|
1060
|
+
t.deepEqual(codes['baseline.d.ts'], [])
|
|
1061
|
+
t.deepEqual(codes['index.d.ts'], [])
|
|
1062
|
+
t.deepEqual(codes['flavor.d.cts'], [])
|
|
1063
|
+
})
|
|
1064
|
+
|
|
1065
|
+
test('a stale generated binding target declaration is refreshed, a custom one is preserved', (t) => {
|
|
1066
|
+
const preamble = '/* auto-generated by NAPI-RS */\n'
|
|
1067
|
+
|
|
1068
|
+
// what an earlier version of this branch wrote, before `wasm32-wasip1`
|
|
1069
|
+
const stale = `${preamble}
|
|
1070
|
+
/** The artifact this loader loaded. */
|
|
1071
|
+
export declare const __napiBindingTarget: 'native' | 'wasm32-wasi'
|
|
1072
|
+
|
|
1073
|
+
export declare function sum(a: number, b: number): number
|
|
1074
|
+
`
|
|
1075
|
+
const refreshedRoot = ensureBindingTargetDeclaration(stale)
|
|
1076
|
+
t.is(
|
|
1077
|
+
bindingTargetDeclarationOf(refreshedRoot),
|
|
1078
|
+
ROOT_BINDING_TARGET_DECLARATION,
|
|
1079
|
+
)
|
|
1080
|
+
t.is(countBindingTargetDeclarations(refreshedRoot), 1)
|
|
1081
|
+
// the stale block's doc comment is replaced with it, not stranded above it
|
|
1082
|
+
t.false(refreshedRoot.includes('The artifact this loader loaded.'))
|
|
1083
|
+
|
|
1084
|
+
const refreshedFlavor = ensureBindingTargetDeclaration(stale, 'wasm32-wasip1')
|
|
1085
|
+
t.is(
|
|
1086
|
+
bindingTargetDeclarationOf(refreshedFlavor),
|
|
1087
|
+
"export declare const __napiBindingTarget: 'wasm32-wasip1'",
|
|
1088
|
+
)
|
|
1089
|
+
t.is(countBindingTargetDeclarations(refreshedFlavor), 1)
|
|
1090
|
+
|
|
1091
|
+
// a file kept from a build of the other flavor now types this one
|
|
1092
|
+
const wrongFlavor = `${preamble}
|
|
1093
|
+
/** The WASI flavor this loader instantiates. */
|
|
1094
|
+
export declare const __napiBindingTarget: 'wasm32-wasi'
|
|
1095
|
+
`
|
|
1096
|
+
t.is(
|
|
1097
|
+
bindingTargetDeclarationOf(
|
|
1098
|
+
ensureBindingTargetDeclaration(wrongFlavor, 'wasm32-wasip1'),
|
|
1099
|
+
),
|
|
1100
|
+
"export declare const __napiBindingTarget: 'wasm32-wasip1'",
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
// a type this CLI could never have written belongs to whoever wrote it
|
|
1104
|
+
const custom = `${preamble}
|
|
1105
|
+
export declare const __napiBindingTarget: string
|
|
1106
|
+
`
|
|
1107
|
+
t.is(ensureBindingTargetDeclaration(custom, 'wasm32-wasip1'), custom)
|
|
1108
|
+
t.is(ensureBindingTargetDeclaration(custom), custom)
|
|
1109
|
+
|
|
1110
|
+
// and neither is a declaration the build's own header owns, even though it
|
|
1111
|
+
// is spelled out of the literals this CLI writes
|
|
1112
|
+
const headerOwned = `${preamble}
|
|
1113
|
+
export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'
|
|
1114
|
+
`
|
|
1115
|
+
t.is(
|
|
1116
|
+
ensureBindingTargetDeclaration(headerOwned, 'wasm32-wasip1', headerOwned),
|
|
1117
|
+
headerOwned,
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
// refreshing is a fixed point
|
|
1121
|
+
t.is(
|
|
1122
|
+
ensureBindingTargetDeclaration(refreshedFlavor, 'wasm32-wasip1'),
|
|
1123
|
+
refreshedFlavor,
|
|
1124
|
+
)
|
|
1125
|
+
t.is(ensureBindingTargetDeclaration(refreshedRoot), refreshedRoot)
|
|
1126
|
+
})
|
|
1127
|
+
|
|
1128
|
+
const ROOT_BINDING_TARGET_TYPE = "'native' | 'wasm32-wasi' | 'wasm32-wasip1'"
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* The type of every `__napiBindingTarget` a consumer of `source` can import,
|
|
1132
|
+
* in source order, read back with `oxc-parser` — a different parser from the
|
|
1133
|
+
* one `build.ts` uses, so these assertions cannot agree with the code by
|
|
1134
|
+
* sharing its idea of what a declaration is.
|
|
1135
|
+
*/
|
|
1136
|
+
const exportedBindingTargetTypes = (source: string) => {
|
|
1137
|
+
const { program } = parseSync('index.d.ts', source)
|
|
1138
|
+
const types: string[] = []
|
|
1139
|
+
for (const node of program.body) {
|
|
1140
|
+
if (
|
|
1141
|
+
node.type !== 'ExportNamedDeclaration' ||
|
|
1142
|
+
node.declaration?.type !== 'VariableDeclaration'
|
|
1143
|
+
) {
|
|
1144
|
+
continue
|
|
1145
|
+
}
|
|
1146
|
+
for (const declaration of node.declaration.declarations) {
|
|
1147
|
+
if (
|
|
1148
|
+
declaration.id.type !== 'Identifier' ||
|
|
1149
|
+
declaration.id.name !== '__napiBindingTarget'
|
|
1150
|
+
) {
|
|
1151
|
+
continue
|
|
1152
|
+
}
|
|
1153
|
+
const annotation = declaration.id.typeAnnotation
|
|
1154
|
+
types.push(
|
|
1155
|
+
annotation
|
|
1156
|
+
? source.slice(
|
|
1157
|
+
annotation.typeAnnotation.start,
|
|
1158
|
+
annotation.typeAnnotation.end,
|
|
1159
|
+
)
|
|
1160
|
+
: '',
|
|
1161
|
+
)
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return types
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
test('a header-owned binding target declaration survives the WASI transforms', async (t) => {
|
|
1168
|
+
const { projectDir, typeDefDir } = t.context
|
|
1169
|
+
await writeFile(
|
|
1170
|
+
join(typeDefDir, 'sum.type'),
|
|
1171
|
+
'{"kind":"fn","name":"sum","def":"function sum(a: number, b: number): number"}\n',
|
|
1172
|
+
)
|
|
1173
|
+
|
|
1174
|
+
// A header that declares the export itself and imports its own types
|
|
1175
|
+
// relatively. `--dts types/index.d.ts` puts the derived `.d.cts` in another
|
|
1176
|
+
// directory, so `prepareWasiBindingTypeDef` rebases that specifier and the
|
|
1177
|
+
// header stops being a literal prefix of the file derived from it — the
|
|
1178
|
+
// declaration it owns must still be recognised as its own.
|
|
1179
|
+
const rebasedHeader = `/* auto-generated by NAPI-RS */
|
|
1180
|
+
|
|
1181
|
+
import type { Thing } from './thing.js'
|
|
1182
|
+
|
|
1183
|
+
${ROOT_BINDING_TARGET_DECLARATION}
|
|
1184
|
+
export declare function useThing(thing: Thing): void
|
|
1185
|
+
`
|
|
1186
|
+
const { dts: rebasedRoot } = await generateTypeDef({
|
|
1187
|
+
typeDefDir,
|
|
1188
|
+
cwd: projectDir,
|
|
1189
|
+
declareBindingTarget: true,
|
|
1190
|
+
dtsHeader: rebasedHeader,
|
|
1191
|
+
})
|
|
1192
|
+
const rebased = ensureBindingTargetDeclaration(
|
|
1193
|
+
prepareWasiBindingTypeDef(
|
|
1194
|
+
rebasedRoot,
|
|
1195
|
+
join(projectDir, 'types', 'index.d.ts'),
|
|
1196
|
+
join(projectDir, 'pkg.wasip1.d.cts'),
|
|
1197
|
+
false,
|
|
1198
|
+
),
|
|
1199
|
+
'wasm32-wasip1',
|
|
1200
|
+
rebasedHeader,
|
|
1201
|
+
)
|
|
1202
|
+
t.true(rebased.includes("from './types/thing.js'"))
|
|
1203
|
+
t.deepEqual(exportedBindingTargetTypes(rebased), [ROOT_BINDING_TARGET_TYPE])
|
|
1204
|
+
|
|
1205
|
+
// The threadless flavor drops `node:stream/web` type imports so the DOM
|
|
1206
|
+
// globals can take over, which moves the header's declaration just the same.
|
|
1207
|
+
const streamHeader = `/* auto-generated by NAPI-RS */
|
|
1208
|
+
|
|
1209
|
+
import type { ReadableStream } from 'node:stream/web'
|
|
1210
|
+
|
|
1211
|
+
${ROOT_BINDING_TARGET_DECLARATION}
|
|
1212
|
+
export declare function readAll(stream: ReadableStream): void
|
|
1213
|
+
`
|
|
1214
|
+
const { dts: streamRoot } = await generateTypeDef({
|
|
1215
|
+
typeDefDir,
|
|
1216
|
+
cwd: projectDir,
|
|
1217
|
+
declareBindingTarget: true,
|
|
1218
|
+
dtsHeader: streamHeader,
|
|
1219
|
+
})
|
|
1220
|
+
const threadless = ensureBindingTargetDeclaration(
|
|
1221
|
+
prepareWasiBindingTypeDef(
|
|
1222
|
+
streamRoot,
|
|
1223
|
+
join(projectDir, 'index.d.ts'),
|
|
1224
|
+
join(projectDir, 'pkg.wasip1.d.cts'),
|
|
1225
|
+
false,
|
|
1226
|
+
),
|
|
1227
|
+
'wasm32-wasip1',
|
|
1228
|
+
streamHeader,
|
|
1229
|
+
)
|
|
1230
|
+
t.false(threadless.includes('node:stream/web'))
|
|
1231
|
+
t.deepEqual(exportedBindingTargetTypes(threadless), [
|
|
1232
|
+
ROOT_BINDING_TARGET_TYPE,
|
|
1233
|
+
])
|
|
1234
|
+
})
|
|
1235
|
+
|
|
1236
|
+
test('only a top-level export declares the binding target', async (t) => {
|
|
1237
|
+
const { projectDir, typeDefDir } = t.context
|
|
1238
|
+
await writeFile(
|
|
1239
|
+
join(typeDefDir, 'sum.type'),
|
|
1240
|
+
'{"kind":"fn","name":"sum","def":"function sum(a: number, b: number): number"}\n',
|
|
1241
|
+
)
|
|
1242
|
+
|
|
1243
|
+
// A commented-out example of the declaration exports nothing, and a member
|
|
1244
|
+
// of a `declare namespace` or `declare module` block is an export of that
|
|
1245
|
+
// block, not of the file. Taking any of them for the declaration leaves the
|
|
1246
|
+
// generated one unwritten and `import { __napiBindingTarget }` at TS2305.
|
|
1247
|
+
const headers = {
|
|
1248
|
+
'a commented-out example': `/* auto-generated by NAPI-RS */
|
|
1249
|
+
/* Example:
|
|
1250
|
+
${ROOT_BINDING_TARGET_DECLARATION}
|
|
1251
|
+
*/
|
|
1252
|
+
`,
|
|
1253
|
+
'a nested namespace member': `/* auto-generated by NAPI-RS */
|
|
1254
|
+
declare namespace Legacy {
|
|
1255
|
+
export const __napiBindingTarget: string
|
|
1256
|
+
}
|
|
1257
|
+
`,
|
|
1258
|
+
'a module augmentation member': `/* auto-generated by NAPI-RS */
|
|
1259
|
+
declare module 'legacy' {
|
|
1260
|
+
export const __napiBindingTarget: string
|
|
1261
|
+
}
|
|
1262
|
+
`,
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
for (const [what, dtsHeader] of Object.entries(headers)) {
|
|
1266
|
+
const { dts } = await generateTypeDef({
|
|
1267
|
+
typeDefDir,
|
|
1268
|
+
cwd: projectDir,
|
|
1269
|
+
declareBindingTarget: true,
|
|
1270
|
+
dtsHeader,
|
|
1271
|
+
})
|
|
1272
|
+
t.deepEqual(
|
|
1273
|
+
exportedBindingTargetTypes(dts),
|
|
1274
|
+
[ROOT_BINDING_TARGET_TYPE],
|
|
1275
|
+
`${what} still owes the root declaration`,
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
const wasi = ensureBindingTargetDeclaration(
|
|
1279
|
+
prepareWasiBindingTypeDef(
|
|
1280
|
+
dts,
|
|
1281
|
+
join(projectDir, 'index.d.ts'),
|
|
1282
|
+
join(projectDir, 'pkg.wasip1.d.cts'),
|
|
1283
|
+
false,
|
|
1284
|
+
),
|
|
1285
|
+
'wasm32-wasip1',
|
|
1286
|
+
dtsHeader,
|
|
1287
|
+
)
|
|
1288
|
+
t.deepEqual(
|
|
1289
|
+
exportedBindingTargetTypes(wasi),
|
|
1290
|
+
["'wasm32-wasip1'"],
|
|
1291
|
+
`${what} still owes the flavor declaration`,
|
|
1292
|
+
)
|
|
1293
|
+
// whatever the header said is left exactly as it wrote it
|
|
1294
|
+
t.true(wasi.startsWith(dtsHeader), what)
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// The refresh is held to the same rule: a commented-out example of a
|
|
1298
|
+
// declaration this CLI writes is not a declaration to rewrite.
|
|
1299
|
+
const commentedStale = `/* auto-generated by NAPI-RS */
|
|
1300
|
+
|
|
1301
|
+
/* an earlier build wrote:
|
|
1302
|
+
export declare const __napiBindingTarget: 'native' | 'wasm32-wasi'
|
|
1303
|
+
*/
|
|
1304
|
+
|
|
1305
|
+
export declare function sum(a: number, b: number): number
|
|
1306
|
+
`
|
|
1307
|
+
const refreshed = ensureBindingTargetDeclaration(
|
|
1308
|
+
commentedStale,
|
|
1309
|
+
'wasm32-wasip1',
|
|
1310
|
+
)
|
|
1311
|
+
t.true(refreshed.startsWith(commentedStale))
|
|
1312
|
+
t.deepEqual(exportedBindingTargetTypes(refreshed), ["'wasm32-wasip1'"])
|
|
1313
|
+
})
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Syntax errors in a generated declaration file, read back with `oxc-parser`.
|
|
1317
|
+
* A refresh splices text into a file the CLI did not write all of, so what it
|
|
1318
|
+
* leaves behind has to still parse.
|
|
1319
|
+
*/
|
|
1320
|
+
const declarationDiagnostics = (source: string) =>
|
|
1321
|
+
parseSync('index.d.ts', source).errors.map((error) => error.message)
|
|
1322
|
+
|
|
1323
|
+
test('a multi-declarator binding target statement is never rewritten', async (t) => {
|
|
1324
|
+
const { projectDir, typeDefDir } = t.context
|
|
1325
|
+
|
|
1326
|
+
// A statement that declares more names than this one is not one this CLI
|
|
1327
|
+
// wrote — it never writes a sibling — and the block a refresh replaces spans
|
|
1328
|
+
// them, so narrowing it would drop `keepMe` and hand every consumer of that
|
|
1329
|
+
// name a TS2305.
|
|
1330
|
+
const preserved = `/* auto-generated by NAPI-RS */
|
|
1331
|
+
|
|
1332
|
+
export declare const __napiBindingTarget: 'native', keepMe: number
|
|
1333
|
+
|
|
1334
|
+
export declare function sum(a: number, b: number): number
|
|
1335
|
+
`
|
|
1336
|
+
t.is(ensureBindingTargetDeclaration(preserved, 'wasm32-wasip1'), preserved)
|
|
1337
|
+
t.is(ensureBindingTargetDeclaration(preserved), preserved)
|
|
1338
|
+
// it still counts as declared, so nothing is appended beside it either
|
|
1339
|
+
t.deepEqual(exportedBindingTargetTypes(preserved), ["'native'"])
|
|
1340
|
+
|
|
1341
|
+
// A header that declares a sibling whose type carries a relative inline
|
|
1342
|
+
// import. `prepareWasiBindingTypeDef` rebases that specifier, so the
|
|
1343
|
+
// statement stops reading as the header wrote it — the declaration itself
|
|
1344
|
+
// still does, which is what decides ownership.
|
|
1345
|
+
await writeFile(
|
|
1346
|
+
join(typeDefDir, 'sum.type'),
|
|
1347
|
+
'{"kind":"fn","name":"sum","def":"function sum(a: number, b: number): number"}\n',
|
|
1348
|
+
)
|
|
1349
|
+
const dtsHeader = `/* auto-generated by NAPI-RS */
|
|
1350
|
+
|
|
1351
|
+
export declare const __napiBindingTarget: ${ROOT_BINDING_TARGET_TYPE},
|
|
1352
|
+
thing: import('./thing.js').Thing
|
|
1353
|
+
`
|
|
1354
|
+
const { dts } = await generateTypeDef({
|
|
1355
|
+
typeDefDir,
|
|
1356
|
+
cwd: projectDir,
|
|
1357
|
+
declareBindingTarget: true,
|
|
1358
|
+
dtsHeader,
|
|
1359
|
+
})
|
|
1360
|
+
const wasi = ensureBindingTargetDeclaration(
|
|
1361
|
+
prepareWasiBindingTypeDef(
|
|
1362
|
+
dts,
|
|
1363
|
+
join(projectDir, 'types', 'index.d.ts'),
|
|
1364
|
+
join(projectDir, 'pkg.wasip1.d.cts'),
|
|
1365
|
+
false,
|
|
1366
|
+
),
|
|
1367
|
+
'wasm32-wasip1',
|
|
1368
|
+
dtsHeader,
|
|
1369
|
+
)
|
|
1370
|
+
t.true(wasi.includes("thing: import('./types/thing.js').Thing"))
|
|
1371
|
+
t.deepEqual(exportedBindingTargetTypes(wasi), [ROOT_BINDING_TARGET_TYPE])
|
|
1372
|
+
t.deepEqual(declarationDiagnostics(wasi), [])
|
|
1373
|
+
})
|
|
1374
|
+
|
|
1375
|
+
test('a refreshed binding target declaration keeps the statement terminator', (t) => {
|
|
1376
|
+
// The replaced span ends past the statement's own `;`, so a replacement that
|
|
1377
|
+
// drops it runs whatever followed on the same line into the declaration.
|
|
1378
|
+
const sameLine = `/* auto-generated by NAPI-RS */
|
|
1379
|
+
|
|
1380
|
+
export declare const __napiBindingTarget: 'native'; export declare function keepMe(): void;
|
|
1381
|
+
`
|
|
1382
|
+
const refreshed = ensureBindingTargetDeclaration(sameLine, 'wasm32-wasip1')
|
|
1383
|
+
t.deepEqual(declarationDiagnostics(refreshed), [])
|
|
1384
|
+
t.deepEqual(exportedBindingTargetTypes(refreshed), ["'wasm32-wasip1'"])
|
|
1385
|
+
t.true(refreshed.includes('export declare function keepMe(): void;'))
|
|
1386
|
+
|
|
1387
|
+
// and a declaration with no doc comment above it keeps its terminator too
|
|
1388
|
+
const ownLine = `/* auto-generated by NAPI-RS */
|
|
1389
|
+
|
|
1390
|
+
export declare const __napiBindingTarget: 'native';
|
|
1391
|
+
export declare function keepMe(): void;
|
|
1392
|
+
`
|
|
1393
|
+
const refreshedOwnLine = ensureBindingTargetDeclaration(
|
|
1394
|
+
ownLine,
|
|
1395
|
+
'wasm32-wasip1',
|
|
1396
|
+
)
|
|
1397
|
+
t.deepEqual(declarationDiagnostics(refreshedOwnLine), [])
|
|
1398
|
+
t.deepEqual(exportedBindingTargetTypes(refreshedOwnLine), ["'wasm32-wasip1'"])
|
|
1399
|
+
t.true(
|
|
1400
|
+
refreshedOwnLine.includes(
|
|
1401
|
+
"/** The WASI flavor this loader instantiates. */\nexport declare const __napiBindingTarget: 'wasm32-wasip1';\n",
|
|
1402
|
+
),
|
|
1403
|
+
)
|
|
1404
|
+
})
|
|
1405
|
+
|
|
1406
|
+
test('a header that re-exports the binding target under the name owns it', async (t) => {
|
|
1407
|
+
const { projectDir, typeDefDir } = t.context
|
|
1408
|
+
await writeFile(
|
|
1409
|
+
join(typeDefDir, 'sum.type'),
|
|
1410
|
+
'{"kind":"fn","name":"sum","def":"function sum(a: number, b: number): number"}\n',
|
|
1411
|
+
)
|
|
1412
|
+
|
|
1413
|
+
// Declaring the name and exporting it are two statements in these headers,
|
|
1414
|
+
// so there is no `export declare const` to find — but a consumer imports the
|
|
1415
|
+
// name all the same, and a second export of it is a TS2323/TS2484 conflict.
|
|
1416
|
+
// Neither form leaves a declaration this build could rewrite, so both are
|
|
1417
|
+
// preserved exactly as written.
|
|
1418
|
+
const headers = {
|
|
1419
|
+
'an aliased re-export': `/* auto-generated by NAPI-RS */
|
|
1420
|
+
|
|
1421
|
+
declare const target: ${ROOT_BINDING_TARGET_TYPE}
|
|
1422
|
+
export { target as __napiBindingTarget }
|
|
1423
|
+
`,
|
|
1424
|
+
'a plain re-export': `/* auto-generated by NAPI-RS */
|
|
1425
|
+
|
|
1426
|
+
declare const __napiBindingTarget: ${ROOT_BINDING_TARGET_TYPE}
|
|
1427
|
+
export { __napiBindingTarget }
|
|
1428
|
+
`,
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
for (const [what, dtsHeader] of Object.entries(headers)) {
|
|
1432
|
+
const { dts } = await generateTypeDef({
|
|
1433
|
+
typeDefDir,
|
|
1434
|
+
cwd: projectDir,
|
|
1435
|
+
declareBindingTarget: true,
|
|
1436
|
+
dtsHeader,
|
|
1437
|
+
})
|
|
1438
|
+
t.true(dts.startsWith(dtsHeader), what)
|
|
1439
|
+
t.deepEqual(exportedBindingTargetTypes(dts), [], what)
|
|
1440
|
+
t.false(dts.includes('export declare const __napiBindingTarget'), what)
|
|
1441
|
+
t.deepEqual(declarationDiagnostics(dts), [], what)
|
|
1442
|
+
|
|
1443
|
+
// and the flavor file derived from it gains nothing either
|
|
1444
|
+
const wasi = ensureBindingTargetDeclaration(
|
|
1445
|
+
prepareWasiBindingTypeDef(
|
|
1446
|
+
dts,
|
|
1447
|
+
join(projectDir, 'index.d.ts'),
|
|
1448
|
+
join(projectDir, 'pkg.wasip1.d.cts'),
|
|
1449
|
+
false,
|
|
1450
|
+
),
|
|
1451
|
+
'wasm32-wasip1',
|
|
1452
|
+
dtsHeader,
|
|
1453
|
+
)
|
|
1454
|
+
t.false(wasi.includes('export declare const __napiBindingTarget'), what)
|
|
1455
|
+
t.deepEqual(declarationDiagnostics(wasi), [], what)
|
|
1456
|
+
}
|
|
1457
|
+
})
|
|
1458
|
+
|
|
1459
|
+
test('a commented-out export assignment is not an export assignment', (t) => {
|
|
1460
|
+
// A declaration file that really exports by assignment carries no named
|
|
1461
|
+
// export, so nothing is declared in it — but documentation showing that form
|
|
1462
|
+
// inside a block comment must not make an ordinary file look like one.
|
|
1463
|
+
const commented = `/* auto-generated by NAPI-RS */
|
|
1464
|
+
/*
|
|
1465
|
+
export = binding
|
|
1466
|
+
*/
|
|
1467
|
+
|
|
1468
|
+
/** The artifact this loader loaded. */
|
|
1469
|
+
export declare const __napiBindingTarget: ${ROOT_BINDING_TARGET_TYPE}
|
|
1470
|
+
|
|
1471
|
+
export declare function sum(a: number, b: number): number
|
|
1472
|
+
`
|
|
1473
|
+
const refreshed = ensureBindingTargetDeclaration(commented, 'wasm32-wasip1')
|
|
1474
|
+
t.deepEqual(exportedBindingTargetTypes(refreshed), ["'wasm32-wasip1'"])
|
|
1475
|
+
t.true(refreshed.includes('export = binding'))
|
|
1476
|
+
t.deepEqual(declarationDiagnostics(refreshed), [])
|
|
1477
|
+
|
|
1478
|
+
// the same comment in a file that declares nothing still owes a declaration
|
|
1479
|
+
const undeclared = `/* auto-generated by NAPI-RS */
|
|
1480
|
+
/*
|
|
1481
|
+
export = binding
|
|
1482
|
+
*/
|
|
1483
|
+
|
|
1484
|
+
export declare function sum(a: number, b: number): number
|
|
1485
|
+
`
|
|
1486
|
+
t.deepEqual(
|
|
1487
|
+
exportedBindingTargetTypes(
|
|
1488
|
+
ensureBindingTargetDeclaration(undeclared, 'wasm32-wasip1'),
|
|
1489
|
+
),
|
|
1490
|
+
["'wasm32-wasip1'"],
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
// a real top-level export assignment is still left alone
|
|
1494
|
+
const exportAssignment = `declare const binding: Record<string, unknown>
|
|
1495
|
+
export = binding
|
|
1496
|
+
`
|
|
1497
|
+
t.is(
|
|
1498
|
+
ensureBindingTargetDeclaration(exportAssignment, 'wasm32-wasip1'),
|
|
1499
|
+
exportAssignment,
|
|
1500
|
+
)
|
|
1501
|
+
// `export default` is the same node with a different flag and is not one
|
|
1502
|
+
const exportDefault = `/* auto-generated by NAPI-RS */
|
|
1503
|
+
|
|
1504
|
+
declare const binding: Record<string, unknown>
|
|
1505
|
+
export default binding
|
|
1506
|
+
`
|
|
1507
|
+
t.deepEqual(
|
|
1508
|
+
exportedBindingTargetTypes(
|
|
1509
|
+
ensureBindingTargetDeclaration(exportDefault, 'wasm32-wasip1'),
|
|
1510
|
+
),
|
|
1511
|
+
["'wasm32-wasip1'"],
|
|
1512
|
+
)
|
|
1513
|
+
})
|
|
1514
|
+
|
|
1515
|
+
const SEMANTIC_CHECK_OPTIONS: ts.CompilerOptions = {
|
|
1516
|
+
strict: true,
|
|
1517
|
+
noEmit: true,
|
|
1518
|
+
// `.d.ts` files are the ones under test, and `skipLibCheck` would skip every
|
|
1519
|
+
// one of them — including this file. Only the default lib is skipped.
|
|
1520
|
+
skipLibCheck: false,
|
|
1521
|
+
skipDefaultLibCheck: true,
|
|
1522
|
+
target: ts.ScriptTarget.ESNext,
|
|
1523
|
+
module: ts.ModuleKind.ESNext,
|
|
1524
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// One host for every check, so the default lib is read and parsed once instead
|
|
1528
|
+
// of once per row.
|
|
1529
|
+
const semanticCheckHost = (() => {
|
|
1530
|
+
const host = ts.createCompilerHost(SEMANTIC_CHECK_OPTIONS, true)
|
|
1531
|
+
const readSourceFile = host.getSourceFile.bind(host)
|
|
1532
|
+
const libraryFiles = new Map<string, ts.SourceFile | undefined>()
|
|
1533
|
+
host.getSourceFile = (fileName, ...rest) => {
|
|
1534
|
+
if (!fileName.includes('/typescript/lib/')) {
|
|
1535
|
+
return readSourceFile(fileName, ...rest)
|
|
1536
|
+
}
|
|
1537
|
+
if (!libraryFiles.has(fileName)) {
|
|
1538
|
+
libraryFiles.set(fileName, readSourceFile(fileName, ...rest))
|
|
1539
|
+
}
|
|
1540
|
+
return libraryFiles.get(fileName)
|
|
1541
|
+
}
|
|
1542
|
+
return host
|
|
1543
|
+
})()
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* The module the re-export rows below import from. It exports the binding
|
|
1547
|
+
* target name itself, so `export { __napiBindingTarget } from './other.js'`
|
|
1548
|
+
* has something to re-export.
|
|
1549
|
+
*/
|
|
1550
|
+
const SEMANTIC_CHECK_SIBLING = `export declare const thing: 'native'
|
|
1551
|
+
export type Thing = string
|
|
1552
|
+
export declare const __napiBindingTarget: 'native'
|
|
1553
|
+
`
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* TypeScript's own verdict on a set of declaration files written side by side,
|
|
1557
|
+
* as a list of codes per file. Every conflict an extra `__napiBindingTarget`
|
|
1558
|
+
* export causes — TS2300, TS2323, TS2440, TS2567 — is semantic, so
|
|
1559
|
+
* `oxc-parser`'s syntax diagnostics cannot see any of them, and so is the
|
|
1560
|
+
* TS2693 a consumer gets when the export it imports turns out to be a type.
|
|
1561
|
+
*/
|
|
1562
|
+
const semanticDiagnosticCodes = async (
|
|
1563
|
+
directory: string,
|
|
1564
|
+
files: Record<string, string>,
|
|
1565
|
+
) => {
|
|
1566
|
+
await mkdir(directory, { recursive: true })
|
|
1567
|
+
await writeFile(join(directory, 'other.d.ts'), SEMANTIC_CHECK_SIBLING)
|
|
1568
|
+
await Promise.all(
|
|
1569
|
+
Object.entries(files).map(([name, source]) =>
|
|
1570
|
+
writeFile(join(directory, name), source),
|
|
1571
|
+
),
|
|
1572
|
+
)
|
|
1573
|
+
const program = ts.createProgram({
|
|
1574
|
+
rootNames: Object.keys(files).map((name) => join(directory, name)),
|
|
1575
|
+
options: SEMANTIC_CHECK_OPTIONS,
|
|
1576
|
+
host: semanticCheckHost,
|
|
1577
|
+
})
|
|
1578
|
+
|
|
1579
|
+
const diagnostics = ts.getPreEmitDiagnostics(program)
|
|
1580
|
+
return Object.fromEntries(
|
|
1581
|
+
Object.keys(files).map((name) => [
|
|
1582
|
+
name,
|
|
1583
|
+
diagnostics
|
|
1584
|
+
.filter(
|
|
1585
|
+
(diagnostic) =>
|
|
1586
|
+
diagnostic.file?.fileName ===
|
|
1587
|
+
join(directory, name).replaceAll(win32Sep, '/'),
|
|
1588
|
+
)
|
|
1589
|
+
.map((diagnostic) => `TS${diagnostic.code}`),
|
|
1590
|
+
]),
|
|
1591
|
+
)
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/**
|
|
1595
|
+
* `__napiBindingTarget` with its final `t` written as a unicode escape. A
|
|
1596
|
+
* TypeScript identifier may spell any character that way, and the checker
|
|
1597
|
+
* resolves it to the same name — a search of the source text does not.
|
|
1598
|
+
*/
|
|
1599
|
+
const ESCAPED_BINDING_TARGET = '__napiBindingTarge\\u0074'
|
|
1600
|
+
|
|
1601
|
+
/** The same name, spelled plainly. */
|
|
1602
|
+
const BINDING_TARGET_NAME = '__napiBindingTarget'
|
|
1603
|
+
|
|
1604
|
+
/** What makes a header with no export of its own a module. */
|
|
1605
|
+
const LOCAL_MODULE_MARKER = 'export declare const marker: number'
|
|
1606
|
+
|
|
1607
|
+
/** A namespace the rows below alias a member out of. */
|
|
1608
|
+
const LEGACY_NAMESPACE = `declare namespace Legacy {
|
|
1609
|
+
const thing: 'native'
|
|
1610
|
+
}
|
|
1611
|
+
`
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* Every shape a `--dts-header` can export `__napiBindingTarget` through, and
|
|
1615
|
+
* whether that shape claims the name.
|
|
1616
|
+
*
|
|
1617
|
+
* `owns: true` leaves no room for a generated `export declare const` beside it
|
|
1618
|
+
* — TypeScript rejects the pair — so nothing is appended and the header comes
|
|
1619
|
+
* through untouched. `owns: false` is a declaration that lives only in type
|
|
1620
|
+
* space: the const merges with it, and suppressing it there would take typed
|
|
1621
|
+
* access to a real runtime export away (TS2693 at every value use) while
|
|
1622
|
+
* preventing no collision at all.
|
|
1623
|
+
*
|
|
1624
|
+
* `hasType` rows name a type as well, so the consumer below also uses the
|
|
1625
|
+
* import as one.
|
|
1626
|
+
*/
|
|
1627
|
+
const BINDING_TARGET_EXPORT_FORMS: Array<{
|
|
1628
|
+
label: string
|
|
1629
|
+
body: string
|
|
1630
|
+
owns: boolean
|
|
1631
|
+
hasType?: boolean
|
|
1632
|
+
/**
|
|
1633
|
+
* What the consumer of the generated files should report. Defaults to the
|
|
1634
|
+
* header's own verdict when the header owns the name and to nothing when the
|
|
1635
|
+
* declaration is written; a row sets it only where the header alone is not a
|
|
1636
|
+
* comparable module.
|
|
1637
|
+
*/
|
|
1638
|
+
consumer?: 'baseline' | 'clean'
|
|
1639
|
+
}> = [
|
|
1640
|
+
{
|
|
1641
|
+
label: 'export declare const',
|
|
1642
|
+
body: "export declare const __napiBindingTarget: 'native'",
|
|
1643
|
+
owns: true,
|
|
1644
|
+
},
|
|
1645
|
+
{
|
|
1646
|
+
label: 'export declare let',
|
|
1647
|
+
body: "export declare let __napiBindingTarget: 'native'",
|
|
1648
|
+
owns: true,
|
|
1649
|
+
},
|
|
1650
|
+
{
|
|
1651
|
+
label: 'export declare var',
|
|
1652
|
+
body: "export declare var __napiBindingTarget: 'native'",
|
|
1653
|
+
owns: true,
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
label: 'a multi-declarator statement',
|
|
1657
|
+
body: "export declare const __napiBindingTarget: 'native', keepMe: number",
|
|
1658
|
+
owns: true,
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
label: 'export declare function',
|
|
1662
|
+
body: 'export declare function __napiBindingTarget(): void',
|
|
1663
|
+
owns: true,
|
|
1664
|
+
},
|
|
1665
|
+
{
|
|
1666
|
+
label: 'export declare class',
|
|
1667
|
+
body: 'export declare class __napiBindingTarget {}',
|
|
1668
|
+
owns: true,
|
|
1669
|
+
hasType: true,
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
label: 'export declare abstract class',
|
|
1673
|
+
body: 'export declare abstract class __napiBindingTarget {}',
|
|
1674
|
+
owns: true,
|
|
1675
|
+
hasType: true,
|
|
1676
|
+
},
|
|
1677
|
+
{
|
|
1678
|
+
label: 'export declare enum',
|
|
1679
|
+
body: 'export declare enum __napiBindingTarget { A }',
|
|
1680
|
+
owns: true,
|
|
1681
|
+
hasType: true,
|
|
1682
|
+
},
|
|
1683
|
+
{
|
|
1684
|
+
label: 'an instantiated namespace',
|
|
1685
|
+
body: 'export declare namespace __napiBindingTarget { const a: number }',
|
|
1686
|
+
owns: true,
|
|
1687
|
+
},
|
|
1688
|
+
{
|
|
1689
|
+
label: 'an instantiated module',
|
|
1690
|
+
body: 'export declare module __napiBindingTarget { const a: number }',
|
|
1691
|
+
owns: true,
|
|
1692
|
+
},
|
|
1693
|
+
{
|
|
1694
|
+
label: 'a namespace instantiated by a nested one',
|
|
1695
|
+
body: 'export declare namespace __napiBindingTarget {\n namespace Inner {\n const a: number\n }\n}',
|
|
1696
|
+
owns: true,
|
|
1697
|
+
},
|
|
1698
|
+
{
|
|
1699
|
+
label: 'a dotted instantiated namespace',
|
|
1700
|
+
body: 'export declare namespace __napiBindingTarget.Inner { const a: number }',
|
|
1701
|
+
owns: true,
|
|
1702
|
+
},
|
|
1703
|
+
// A namespace is instantiated by an aliased member too, which is exactly
|
|
1704
|
+
// what a hand-written walk over the body kept missing.
|
|
1705
|
+
{
|
|
1706
|
+
label: 'a namespace instantiated by an export import',
|
|
1707
|
+
body: `${LEGACY_NAMESPACE}export declare namespace __napiBindingTarget {\n export import thing = Legacy.thing\n}`,
|
|
1708
|
+
owns: true,
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
label: 'a namespace instantiated by an export clause',
|
|
1712
|
+
body: `${LEGACY_NAMESPACE}export declare namespace __napiBindingTarget {\n export { Legacy }\n}`,
|
|
1713
|
+
owns: true,
|
|
1714
|
+
},
|
|
1715
|
+
{
|
|
1716
|
+
label: 'a namespace instantiated by a nested alias',
|
|
1717
|
+
body: `${LEGACY_NAMESPACE}export declare namespace __napiBindingTarget {\n namespace Inner {\n export import thing = Legacy.thing\n }\n}`,
|
|
1718
|
+
owns: true,
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
label: 'export import name =',
|
|
1722
|
+
body: "declare namespace Legacy {\n const thing: 'native'\n}\nexport import __napiBindingTarget = Legacy.thing",
|
|
1723
|
+
owns: true,
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
label: 'export { local as name }',
|
|
1727
|
+
body: "declare const target: 'native'\nexport { target as __napiBindingTarget }",
|
|
1728
|
+
owns: true,
|
|
1729
|
+
},
|
|
1730
|
+
{
|
|
1731
|
+
label: 'export { name }',
|
|
1732
|
+
body: "declare const __napiBindingTarget: 'native'\nexport { __napiBindingTarget }",
|
|
1733
|
+
owns: true,
|
|
1734
|
+
},
|
|
1735
|
+
{
|
|
1736
|
+
label: 'export { local as name } from',
|
|
1737
|
+
body: "export { thing as __napiBindingTarget } from './other.js'",
|
|
1738
|
+
owns: true,
|
|
1739
|
+
},
|
|
1740
|
+
{
|
|
1741
|
+
label: 'export { name } from',
|
|
1742
|
+
body: "export { __napiBindingTarget } from './other.js'",
|
|
1743
|
+
owns: true,
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
label: 'export type { local as name }',
|
|
1747
|
+
body: 'declare type Target = string\nexport type { Target as __napiBindingTarget }',
|
|
1748
|
+
owns: true,
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
label: 'export type { local as name } from',
|
|
1752
|
+
body: "export type { Thing as __napiBindingTarget } from './other.js'",
|
|
1753
|
+
owns: true,
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
label: 'export * as name from',
|
|
1757
|
+
body: "export * as __napiBindingTarget from './other.js'",
|
|
1758
|
+
owns: true,
|
|
1759
|
+
},
|
|
1760
|
+
{
|
|
1761
|
+
label: 'export type * as name from',
|
|
1762
|
+
body: "export type * as __napiBindingTarget from './other.js'",
|
|
1763
|
+
owns: true,
|
|
1764
|
+
},
|
|
1765
|
+
// type space only: the const merges, and the consumer keeps its value
|
|
1766
|
+
{
|
|
1767
|
+
label: 'export type',
|
|
1768
|
+
body: 'export type __napiBindingTarget = string',
|
|
1769
|
+
owns: false,
|
|
1770
|
+
hasType: true,
|
|
1771
|
+
},
|
|
1772
|
+
{
|
|
1773
|
+
label: 'export interface',
|
|
1774
|
+
body: 'export interface __napiBindingTarget { a: number }',
|
|
1775
|
+
owns: false,
|
|
1776
|
+
hasType: true,
|
|
1777
|
+
},
|
|
1778
|
+
{
|
|
1779
|
+
label: 'a namespace of interfaces',
|
|
1780
|
+
body: 'export declare namespace __napiBindingTarget {\n interface I {\n a: number\n }\n}',
|
|
1781
|
+
owns: false,
|
|
1782
|
+
},
|
|
1783
|
+
{
|
|
1784
|
+
label: 'a namespace of type aliases',
|
|
1785
|
+
body: 'export declare namespace __napiBindingTarget {\n type T = string\n}',
|
|
1786
|
+
owns: false,
|
|
1787
|
+
},
|
|
1788
|
+
{
|
|
1789
|
+
label: 'an empty namespace',
|
|
1790
|
+
body: 'export declare namespace __napiBindingTarget {}',
|
|
1791
|
+
owns: false,
|
|
1792
|
+
},
|
|
1793
|
+
{
|
|
1794
|
+
label: 'a namespace whose nested one is type-only',
|
|
1795
|
+
body: 'export declare namespace __napiBindingTarget {\n namespace Inner {\n interface I {\n a: number\n }\n }\n}',
|
|
1796
|
+
owns: false,
|
|
1797
|
+
},
|
|
1798
|
+
// the control for the three rows above: an alias a namespace keeps to itself
|
|
1799
|
+
// instantiates nothing
|
|
1800
|
+
{
|
|
1801
|
+
label: 'a namespace with an unexported import',
|
|
1802
|
+
body: `${LEGACY_NAMESPACE}export declare namespace __napiBindingTarget {\n import thing = Legacy.thing\n}`,
|
|
1803
|
+
owns: false,
|
|
1804
|
+
},
|
|
1805
|
+
// An escaped spelling is the same name to TypeScript, whatever a text search
|
|
1806
|
+
// of the header says.
|
|
1807
|
+
{
|
|
1808
|
+
label: 'an escaped spelling',
|
|
1809
|
+
body: `export declare const ${ESCAPED_BINDING_TARGET}: 'native'`,
|
|
1810
|
+
owns: true,
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
label: 'an escaped export alias',
|
|
1814
|
+
body: `declare const target: 'native'\nexport { target as ${ESCAPED_BINDING_TARGET} }`,
|
|
1815
|
+
owns: true,
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
label: 'an escaped local binding',
|
|
1819
|
+
body: `declare const ${ESCAPED_BINDING_TARGET}: 'native'\n${LOCAL_MODULE_MARKER}`,
|
|
1820
|
+
owns: true,
|
|
1821
|
+
},
|
|
1822
|
+
// A string-literal export name is not an identifier, so it spells the same
|
|
1823
|
+
// name through the string escapes too — and a string may be broken across
|
|
1824
|
+
// lines with a backslash-newline continuation that no `\u` search finds.
|
|
1825
|
+
{
|
|
1826
|
+
label: 'a hexadecimal escape in a string export name',
|
|
1827
|
+
body: `declare const target: 'native'\nexport { target as "__napiBindingTarg\\x65t" }`,
|
|
1828
|
+
owns: true,
|
|
1829
|
+
},
|
|
1830
|
+
{
|
|
1831
|
+
label: 'a string export name continued across lines',
|
|
1832
|
+
body: `declare const target: 'native'\nexport { target as "__napiBindingTarge\\\nt" }`,
|
|
1833
|
+
owns: true,
|
|
1834
|
+
},
|
|
1835
|
+
// Not every binding that collides is an export. An ambient declaration file
|
|
1836
|
+
// puts most top-level declarations in its export table by itself, but not
|
|
1837
|
+
// all of them, and a file that is not a module has no export table at all.
|
|
1838
|
+
{
|
|
1839
|
+
label: 'a local value binding',
|
|
1840
|
+
body: `declare const __napiBindingTarget: 'native'\n${LOCAL_MODULE_MARKER}`,
|
|
1841
|
+
owns: true,
|
|
1842
|
+
},
|
|
1843
|
+
{
|
|
1844
|
+
label: 'a local function binding',
|
|
1845
|
+
body: `declare function __napiBindingTarget(): void\n${LOCAL_MODULE_MARKER}`,
|
|
1846
|
+
owns: true,
|
|
1847
|
+
},
|
|
1848
|
+
{
|
|
1849
|
+
label: 'a local import alias',
|
|
1850
|
+
body: `${LEGACY_NAMESPACE}import __napiBindingTarget = Legacy.thing\n${LOCAL_MODULE_MARKER}`,
|
|
1851
|
+
owns: true,
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
// A header that is not a module at all. Its `declare const` is a global
|
|
1855
|
+
// the generated export would redeclare (TS2451, TS2395), so nothing is
|
|
1856
|
+
// written — and the generated body then makes the file a module, which by
|
|
1857
|
+
// the ambient declaration file rule turns that same `declare const` into
|
|
1858
|
+
// the export a consumer reads. So the consumer compiles here where it
|
|
1859
|
+
// cannot even import from the header alone (TS2306).
|
|
1860
|
+
label: 'a script header claiming the name',
|
|
1861
|
+
body: "declare const __napiBindingTarget: 'native'",
|
|
1862
|
+
owns: true,
|
|
1863
|
+
consumer: 'clean',
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
label: 'a local type',
|
|
1867
|
+
body: `type __napiBindingTarget = string\n${LOCAL_MODULE_MARKER}`,
|
|
1868
|
+
owns: false,
|
|
1869
|
+
hasType: true,
|
|
1870
|
+
},
|
|
1871
|
+
{
|
|
1872
|
+
label: 'a local interface',
|
|
1873
|
+
body: `interface __napiBindingTarget {\n a: number\n}\n${LOCAL_MODULE_MARKER}`,
|
|
1874
|
+
owns: false,
|
|
1875
|
+
hasType: true,
|
|
1876
|
+
},
|
|
1877
|
+
// The global scope is visible from every file, so a name a header declares
|
|
1878
|
+
// into it comes back from a scope lookup while colliding with nothing the
|
|
1879
|
+
// file itself adds: an export of the name shadows the global instead of
|
|
1880
|
+
// redeclaring it, and a consumer importing the name needs that export
|
|
1881
|
+
// written or it gets TS2305.
|
|
1882
|
+
{
|
|
1883
|
+
label: 'a global augmentation of the name',
|
|
1884
|
+
body: 'export {}\ndeclare global {\n var __napiBindingTarget: string\n}',
|
|
1885
|
+
owns: false,
|
|
1886
|
+
},
|
|
1887
|
+
{
|
|
1888
|
+
label: 'a UMD namespace export of the name',
|
|
1889
|
+
body: `export as namespace __napiBindingTarget\n${LOCAL_MODULE_MARKER}`,
|
|
1890
|
+
owns: false,
|
|
1891
|
+
},
|
|
1892
|
+
{
|
|
1893
|
+
label: 'an export of another name',
|
|
1894
|
+
body: "export declare const __napiBindingTargetInfo: 'native'",
|
|
1895
|
+
owns: false,
|
|
1896
|
+
},
|
|
1897
|
+
{
|
|
1898
|
+
label: 'export default',
|
|
1899
|
+
body: 'declare const binding: Record<string, unknown>\nexport default binding',
|
|
1900
|
+
owns: false,
|
|
1901
|
+
},
|
|
1902
|
+
]
|
|
1903
|
+
|
|
1904
|
+
test('every top-level export of the binding target name owns it', async (t) => {
|
|
1905
|
+
const { tmpDir, projectDir, typeDefDir } = t.context
|
|
1906
|
+
await writeFile(
|
|
1907
|
+
join(typeDefDir, 'sum.type'),
|
|
1908
|
+
'{"kind":"fn","name":"sum","def":"function sum(a: number, b: number): number"}\n',
|
|
1909
|
+
)
|
|
1910
|
+
|
|
1911
|
+
let row = 0
|
|
1912
|
+
for (const {
|
|
1913
|
+
label,
|
|
1914
|
+
body,
|
|
1915
|
+
owns,
|
|
1916
|
+
hasType,
|
|
1917
|
+
consumer: expectedConsumer = owns ? 'baseline' : 'clean',
|
|
1918
|
+
} of BINDING_TARGET_EXPORT_FORMS) {
|
|
1919
|
+
const dtsHeader = `/* auto-generated by NAPI-RS */\n\n${body}\n`
|
|
1920
|
+
const { dts } = await generateTypeDef({
|
|
1921
|
+
typeDefDir,
|
|
1922
|
+
cwd: projectDir,
|
|
1923
|
+
declareBindingTarget: true,
|
|
1924
|
+
dtsHeader,
|
|
1925
|
+
})
|
|
1926
|
+
const directory = join(tmpDir, `export-form-${row++}`)
|
|
1927
|
+
const flavor = ensureBindingTargetDeclaration(
|
|
1928
|
+
prepareWasiBindingTypeDef(
|
|
1929
|
+
dts,
|
|
1930
|
+
join(directory, 'index.d.ts'),
|
|
1931
|
+
join(directory, 'flavor.d.cts'),
|
|
1932
|
+
false,
|
|
1933
|
+
),
|
|
1934
|
+
'wasm32-wasip1',
|
|
1935
|
+
dtsHeader,
|
|
1936
|
+
)
|
|
1937
|
+
|
|
1938
|
+
// the header comes through byte for byte, whatever the build appended
|
|
1939
|
+
t.true(dts.startsWith(dtsHeader), label)
|
|
1940
|
+
|
|
1941
|
+
// What a package consumer writes: import the name and use it as the value
|
|
1942
|
+
// the loader really exports.
|
|
1943
|
+
const consumer = (module: string) =>
|
|
1944
|
+
`import { __napiBindingTarget } from '${module}'\n` +
|
|
1945
|
+
`export const value: string = __napiBindingTarget\n` +
|
|
1946
|
+
(hasType ? `export type Named = __napiBindingTarget\n` : '')
|
|
1947
|
+
|
|
1948
|
+
const codes = await semanticDiagnosticCodes(directory, {
|
|
1949
|
+
'baseline.d.ts': dtsHeader,
|
|
1950
|
+
'baseline-consumer.ts': consumer('./baseline.js'),
|
|
1951
|
+
'index.d.ts': dts,
|
|
1952
|
+
'index-consumer.ts': consumer('./index.js'),
|
|
1953
|
+
'flavor.d.cts': flavor,
|
|
1954
|
+
'flavor-consumer.ts': consumer('./flavor.cjs'),
|
|
1955
|
+
})
|
|
1956
|
+
|
|
1957
|
+
// TypeScript's verdict on the declaration files: whatever the header
|
|
1958
|
+
// itself is worth, the generated root declaration and the flavor
|
|
1959
|
+
// declaration derived from it are worth exactly the same — the build
|
|
1960
|
+
// introduced no semantic diagnostic. Compared against the header rather
|
|
1961
|
+
// than against nothing, because a header may carry one of its own (the
|
|
1962
|
+
// deprecated `module` keyword is TS1540).
|
|
1963
|
+
t.deepEqual(codes['index.d.ts'], codes['baseline.d.ts'], label)
|
|
1964
|
+
t.deepEqual(codes['flavor.d.cts'], codes['baseline.d.ts'], label)
|
|
1965
|
+
|
|
1966
|
+
if (owns) {
|
|
1967
|
+
// nothing added: the file exports exactly what the header exported
|
|
1968
|
+
t.deepEqual(
|
|
1969
|
+
exportedBindingTargetTypes(dts),
|
|
1970
|
+
exportedBindingTargetTypes(dtsHeader),
|
|
1971
|
+
label,
|
|
1972
|
+
)
|
|
1973
|
+
t.true(flavor.startsWith(dtsHeader), label)
|
|
1974
|
+
t.deepEqual(
|
|
1975
|
+
exportedBindingTargetTypes(flavor),
|
|
1976
|
+
exportedBindingTargetTypes(dtsHeader),
|
|
1977
|
+
label,
|
|
1978
|
+
)
|
|
1979
|
+
} else {
|
|
1980
|
+
// the declaration is written, and a consumer can use it as a value —
|
|
1981
|
+
// which is the whole point of writing it
|
|
1982
|
+
t.deepEqual(
|
|
1983
|
+
exportedBindingTargetTypes(dts),
|
|
1984
|
+
[ROOT_BINDING_TARGET_TYPE],
|
|
1985
|
+
label,
|
|
1986
|
+
)
|
|
1987
|
+
t.deepEqual(
|
|
1988
|
+
exportedBindingTargetTypes(flavor),
|
|
1989
|
+
["'wasm32-wasip1'"],
|
|
1990
|
+
label,
|
|
1991
|
+
)
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// What the consumer sees: either exactly what the header alone would give
|
|
1995
|
+
// it — including the failure a type-only re-export clause already causes —
|
|
1996
|
+
// or nothing at all where the declaration was written for it.
|
|
1997
|
+
const expected =
|
|
1998
|
+
expectedConsumer === 'baseline' ? codes['baseline-consumer.ts'] : []
|
|
1999
|
+
t.deepEqual(codes['index-consumer.ts'], expected, label)
|
|
2000
|
+
t.deepEqual(codes['flavor-consumer.ts'], expected, label)
|
|
2001
|
+
}
|
|
2002
|
+
})
|
|
2003
|
+
|
|
2004
|
+
test('a header that cannot spell the binding target skips the checker', (t) => {
|
|
2005
|
+
// Binding the file is what answers ownership, and almost no header needs it
|
|
2006
|
+
// asked: one that contains neither the name nor a backslash anywhere cannot
|
|
2007
|
+
// spell it, and the parse alone settles the question. `checked` reports
|
|
2008
|
+
// whether that shortcut applied, so this holds it in place by observation
|
|
2009
|
+
// rather than by timing anything.
|
|
2010
|
+
const scan = (body: string) =>
|
|
2011
|
+
scanExportedName(
|
|
2012
|
+
`/* auto-generated by NAPI-RS */\n\n${body}\n`,
|
|
2013
|
+
BINDING_TARGET_NAME,
|
|
2014
|
+
)
|
|
2015
|
+
|
|
2016
|
+
const free = scan('export declare function sum(a: number, b: number): number')
|
|
2017
|
+
t.false(free.checked)
|
|
2018
|
+
t.false(free.ownsName)
|
|
2019
|
+
|
|
2020
|
+
const literal = scan(`export declare const ${BINDING_TARGET_NAME}: 'native'`)
|
|
2021
|
+
t.true(literal.checked)
|
|
2022
|
+
t.true(literal.ownsName)
|
|
2023
|
+
|
|
2024
|
+
// the same name written so that no search of the source text finds it
|
|
2025
|
+
const escaped = scan(
|
|
2026
|
+
`export declare const ${ESCAPED_BINDING_TARGET}: 'native'`,
|
|
2027
|
+
)
|
|
2028
|
+
t.true(escaped.checked)
|
|
2029
|
+
t.true(escaped.ownsName)
|
|
2030
|
+
|
|
2031
|
+
// a string-literal export name spells the same name through the string
|
|
2032
|
+
// escapes, which a search for `\u` alone would have walked straight past
|
|
2033
|
+
const hexEscaped = scan(
|
|
2034
|
+
`declare const target: 'native'\nexport { target as "__napiBindingTarg\\x65t" }`,
|
|
2035
|
+
)
|
|
2036
|
+
t.true(hexEscaped.checked)
|
|
2037
|
+
t.true(hexEscaped.ownsName)
|
|
2038
|
+
|
|
2039
|
+
// and so does one broken across lines with a backslash-newline continuation
|
|
2040
|
+
const continued = scan(
|
|
2041
|
+
`declare const target: 'native'\nexport { target as "__napiBindingTarge\\\nt" }`,
|
|
2042
|
+
)
|
|
2043
|
+
t.true(continued.checked)
|
|
2044
|
+
t.true(continued.ownsName)
|
|
2045
|
+
|
|
2046
|
+
// any backslash gives up the shortcut, whatever it turns out to spell:
|
|
2047
|
+
// whether one could have built the name is exactly the question the parse
|
|
2048
|
+
// cannot answer, so it is not guessed at here
|
|
2049
|
+
const unrelatedEscape = scan("export declare const nam\\u0065: 'native'")
|
|
2050
|
+
t.true(unrelatedEscape.checked)
|
|
2051
|
+
t.false(unrelatedEscape.ownsName)
|
|
2052
|
+
|
|
2053
|
+
const unrelatedBackslash = scan("export declare const pattern: '\\\\d+'")
|
|
2054
|
+
t.true(unrelatedBackslash.checked)
|
|
2055
|
+
t.false(unrelatedBackslash.ownsName)
|
|
2056
|
+
})
|
|
2057
|
+
|
|
2058
|
+
test('a WASI flavor without type defs declares its own binding target', async (t) => {
|
|
2059
|
+
const { projectDir, typeDefDir } = t.context
|
|
2060
|
+
|
|
2061
|
+
const { dts } = await generateTypeDef({
|
|
2062
|
+
typeDefDir,
|
|
2063
|
+
cwd: projectDir,
|
|
2064
|
+
declareBindingTarget: true,
|
|
2065
|
+
})
|
|
2066
|
+
|
|
2067
|
+
const prepared = prepareWasiBindingTypeDef(
|
|
2068
|
+
dts,
|
|
2069
|
+
join(projectDir, 'index.d.ts'),
|
|
2070
|
+
join(projectDir, 'pkg.wasip1.d.cts'),
|
|
2071
|
+
false,
|
|
2072
|
+
)
|
|
2073
|
+
t.is(
|
|
2074
|
+
bindingTargetDeclarationOf(
|
|
2075
|
+
ensureBindingTargetDeclaration(prepared, 'wasm32-wasip1'),
|
|
2076
|
+
),
|
|
2077
|
+
"export declare const __napiBindingTarget: 'wasm32-wasip1'",
|
|
2078
|
+
)
|
|
2079
|
+
})
|
|
2080
|
+
|
|
2081
|
+
test('the binding target declaration follows the loaders a build emits', (t) => {
|
|
2082
|
+
// Each row is a build shape, named by what reaches the CLI. `expected` is
|
|
2083
|
+
// whether any loader carrying `__napiBindingTarget` is written for it.
|
|
2084
|
+
const shapes = [
|
|
2085
|
+
{
|
|
2086
|
+
name: 'no --platform, a WASI flavor configured but never built',
|
|
2087
|
+
rootLoaderCandidate: false,
|
|
2088
|
+
hasWasiFallback: true,
|
|
2089
|
+
emitsWasiLoader: false,
|
|
2090
|
+
exports: ['sum'],
|
|
2091
|
+
expected: false,
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
name: 'no --platform, no WASI flavor',
|
|
2095
|
+
rootLoaderCandidate: false,
|
|
2096
|
+
hasWasiFallback: false,
|
|
2097
|
+
emitsWasiLoader: false,
|
|
2098
|
+
exports: ['sum'],
|
|
2099
|
+
expected: false,
|
|
2100
|
+
},
|
|
2101
|
+
{
|
|
2102
|
+
name: '--platform with a WASI flavor configured',
|
|
2103
|
+
rootLoaderCandidate: true,
|
|
2104
|
+
hasWasiFallback: true,
|
|
2105
|
+
emitsWasiLoader: false,
|
|
2106
|
+
exports: ['sum'],
|
|
2107
|
+
expected: true,
|
|
2108
|
+
},
|
|
2109
|
+
{
|
|
2110
|
+
name: '--platform --no-js-binding',
|
|
2111
|
+
rootLoaderCandidate: false,
|
|
2112
|
+
hasWasiFallback: true,
|
|
2113
|
+
emitsWasiLoader: false,
|
|
2114
|
+
exports: ['sum'],
|
|
2115
|
+
expected: false,
|
|
2116
|
+
},
|
|
2117
|
+
{
|
|
2118
|
+
name: '--platform with type defs but zero runtime exports',
|
|
2119
|
+
rootLoaderCandidate: true,
|
|
2120
|
+
hasWasiFallback: false,
|
|
2121
|
+
emitsWasiLoader: false,
|
|
2122
|
+
exports: [],
|
|
2123
|
+
expected: false,
|
|
2124
|
+
},
|
|
2125
|
+
{
|
|
2126
|
+
name: 'no --platform, zero runtime exports',
|
|
2127
|
+
rootLoaderCandidate: false,
|
|
2128
|
+
hasWasiFallback: true,
|
|
2129
|
+
emitsWasiLoader: false,
|
|
2130
|
+
exports: [],
|
|
2131
|
+
expected: false,
|
|
2132
|
+
},
|
|
2133
|
+
{
|
|
2134
|
+
name: 'a native build over WASI loaders an earlier build left behind',
|
|
2135
|
+
rootLoaderCandidate: true,
|
|
2136
|
+
hasWasiFallback: true,
|
|
2137
|
+
emitsWasiLoader: true,
|
|
2138
|
+
exports: ['sum'],
|
|
2139
|
+
expected: true,
|
|
2140
|
+
},
|
|
2141
|
+
{
|
|
2142
|
+
name: '--target wasm32-wasip1 without --platform',
|
|
2143
|
+
rootLoaderCandidate: false,
|
|
2144
|
+
hasWasiFallback: true,
|
|
2145
|
+
emitsWasiLoader: true,
|
|
2146
|
+
exports: [],
|
|
2147
|
+
expected: true,
|
|
2148
|
+
},
|
|
2149
|
+
{
|
|
2150
|
+
name: 'a plain native-only package',
|
|
2151
|
+
rootLoaderCandidate: true,
|
|
2152
|
+
hasWasiFallback: false,
|
|
2153
|
+
emitsWasiLoader: false,
|
|
2154
|
+
exports: ['sum'],
|
|
2155
|
+
expected: true,
|
|
2156
|
+
},
|
|
2157
|
+
] as const
|
|
2158
|
+
|
|
2159
|
+
for (const shape of shapes) {
|
|
2160
|
+
const declares = bindingTargetDeclarationPredicate(shape)
|
|
2161
|
+
t.is(declares(shape.exports), shape.expected, shape.name)
|
|
2162
|
+
}
|
|
2163
|
+
})
|
|
2164
|
+
|
|
2165
|
+
test('the binding target reservation asks the same predicate as the declaration', (t) => {
|
|
2166
|
+
// `Builder.generateTypeDef` reserves `__napiBindingTarget` exactly when it
|
|
2167
|
+
// declares it, so the reservation inherits these answers. The predicate reads
|
|
2168
|
+
// only the *length* of the export list, never the names in it — which is what
|
|
2169
|
+
// lets one decision stand for both.
|
|
2170
|
+
const claimed = ['__napiBindingTarget']
|
|
2171
|
+
const shapes = [
|
|
2172
|
+
{
|
|
2173
|
+
name: 'a plain `napi build` reserves nothing',
|
|
2174
|
+
rootLoaderCandidate: false,
|
|
2175
|
+
hasWasiFallback: false,
|
|
2176
|
+
emitsWasiLoader: false,
|
|
2177
|
+
exports: claimed,
|
|
2178
|
+
expected: false,
|
|
2179
|
+
},
|
|
2180
|
+
{
|
|
2181
|
+
name: '`--platform --no-js` reserves nothing',
|
|
2182
|
+
rootLoaderCandidate: false,
|
|
2183
|
+
hasWasiFallback: true,
|
|
2184
|
+
emitsWasiLoader: false,
|
|
2185
|
+
exports: claimed,
|
|
2186
|
+
expected: false,
|
|
2187
|
+
},
|
|
2188
|
+
{
|
|
2189
|
+
name: '`--platform` writes a root loader, so the name is reserved',
|
|
2190
|
+
rootLoaderCandidate: true,
|
|
2191
|
+
hasWasiFallback: false,
|
|
2192
|
+
emitsWasiLoader: false,
|
|
2193
|
+
exports: claimed,
|
|
2194
|
+
expected: true,
|
|
2195
|
+
},
|
|
2196
|
+
{
|
|
2197
|
+
name: 'a regenerated WASI loader set reserves the name on its own',
|
|
2198
|
+
rootLoaderCandidate: false,
|
|
2199
|
+
hasWasiFallback: false,
|
|
2200
|
+
emitsWasiLoader: true,
|
|
2201
|
+
exports: [],
|
|
2202
|
+
expected: true,
|
|
2203
|
+
},
|
|
2204
|
+
] as const
|
|
2205
|
+
|
|
2206
|
+
for (const shape of shapes) {
|
|
2207
|
+
t.is(
|
|
2208
|
+
bindingTargetDeclarationPredicate(shape)(shape.exports),
|
|
2209
|
+
shape.expected,
|
|
2210
|
+
shape.name,
|
|
2211
|
+
)
|
|
2212
|
+
}
|
|
247
2213
|
})
|
|
248
2214
|
|
|
249
2215
|
test('resolveBuildFormat handles defaults, aliases, and conflicts', (t) => {
|
|
@@ -287,6 +2253,95 @@ test('resolveBuildFormat handles defaults, aliases, and conflicts', (t) => {
|
|
|
287
2253
|
}
|
|
288
2254
|
})
|
|
289
2255
|
|
|
2256
|
+
const ASYNC_RUNTIME_HOST_EXPORTS = [
|
|
2257
|
+
'getCurrentThreadTaskHostContractVersion',
|
|
2258
|
+
'isCurrentThreadHostRegistrationActive',
|
|
2259
|
+
'registerCurrentThreadTaskHost',
|
|
2260
|
+
'registerTimerHost',
|
|
2261
|
+
'reserveCurrentThreadHostRegistration',
|
|
2262
|
+
'unregisterCurrentThreadTaskHost',
|
|
2263
|
+
'unregisterTimerHost',
|
|
2264
|
+
]
|
|
2265
|
+
|
|
2266
|
+
test('checkAsyncRuntimeHostContract reports the exports a binding is missing', (t) => {
|
|
2267
|
+
const { error, warning } = checkAsyncRuntimeHostContract({
|
|
2268
|
+
idents: ['plus100', 'registerTimerHost'],
|
|
2269
|
+
asyncRuntime: true,
|
|
2270
|
+
typeDefAvailable: true,
|
|
2271
|
+
packageName: 'missing-host-exports',
|
|
2272
|
+
})
|
|
2273
|
+
|
|
2274
|
+
t.is(warning, undefined)
|
|
2275
|
+
t.truthy(error)
|
|
2276
|
+
t.regex(error!, /missing-host-exports/)
|
|
2277
|
+
for (const name of ASYNC_RUNTIME_HOST_EXPORTS) {
|
|
2278
|
+
if (name === 'registerTimerHost') {
|
|
2279
|
+
t.notRegex(error!, new RegExp(`Missing:[^.]*\\b${name}\\b`))
|
|
2280
|
+
} else {
|
|
2281
|
+
t.regex(error!, new RegExp(`Missing:[^.]*\\b${name}\\b`))
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
})
|
|
2285
|
+
|
|
2286
|
+
test('checkAsyncRuntimeHostContract warns when the contract is exported but the flag is off', (t) => {
|
|
2287
|
+
const { error, warning } = checkAsyncRuntimeHostContract({
|
|
2288
|
+
idents: [...ASYNC_RUNTIME_HOST_EXPORTS, 'plus100'],
|
|
2289
|
+
asyncRuntime: false,
|
|
2290
|
+
typeDefAvailable: true,
|
|
2291
|
+
packageName: 'flag-is-off',
|
|
2292
|
+
})
|
|
2293
|
+
|
|
2294
|
+
t.is(error, undefined)
|
|
2295
|
+
t.truthy(warning)
|
|
2296
|
+
t.regex(warning!, /napi\.wasm\.asyncRuntime is not enabled/)
|
|
2297
|
+
})
|
|
2298
|
+
|
|
2299
|
+
test('checkAsyncRuntimeHostContract skips the check without type-def metadata', (t) => {
|
|
2300
|
+
const { error, warning } = checkAsyncRuntimeHostContract({
|
|
2301
|
+
idents: [],
|
|
2302
|
+
asyncRuntime: true,
|
|
2303
|
+
typeDefAvailable: false,
|
|
2304
|
+
packageName: 'no-type-def',
|
|
2305
|
+
})
|
|
2306
|
+
|
|
2307
|
+
t.is(error, undefined)
|
|
2308
|
+
t.truthy(warning)
|
|
2309
|
+
t.regex(warning!, /type-def/)
|
|
2310
|
+
t.regex(warning!, /ERR_NAPI_ASYNC_RUNTIME_BINDING_MISMATCH/)
|
|
2311
|
+
|
|
2312
|
+
t.deepEqual(
|
|
2313
|
+
checkAsyncRuntimeHostContract({
|
|
2314
|
+
idents: [],
|
|
2315
|
+
asyncRuntime: false,
|
|
2316
|
+
typeDefAvailable: false,
|
|
2317
|
+
packageName: 'no-type-def',
|
|
2318
|
+
}),
|
|
2319
|
+
{},
|
|
2320
|
+
)
|
|
2321
|
+
})
|
|
2322
|
+
|
|
2323
|
+
test('checkAsyncRuntimeHostContract stays silent when the flag matches the exports', (t) => {
|
|
2324
|
+
t.deepEqual(
|
|
2325
|
+
checkAsyncRuntimeHostContract({
|
|
2326
|
+
idents: [...ASYNC_RUNTIME_HOST_EXPORTS, 'plus100'],
|
|
2327
|
+
asyncRuntime: true,
|
|
2328
|
+
typeDefAvailable: true,
|
|
2329
|
+
packageName: 'all-good',
|
|
2330
|
+
}),
|
|
2331
|
+
{},
|
|
2332
|
+
)
|
|
2333
|
+
|
|
2334
|
+
t.deepEqual(
|
|
2335
|
+
checkAsyncRuntimeHostContract({
|
|
2336
|
+
idents: ['plus100'],
|
|
2337
|
+
asyncRuntime: false,
|
|
2338
|
+
typeDefAvailable: true,
|
|
2339
|
+
packageName: 'all-good',
|
|
2340
|
+
}),
|
|
2341
|
+
{},
|
|
2342
|
+
)
|
|
2343
|
+
})
|
|
2344
|
+
|
|
290
2345
|
test('generateTypeDef preserves deterministic file order', async (t) => {
|
|
291
2346
|
const { projectDir, typeDefDir } = t.context
|
|
292
2347
|
|
|
@@ -1108,3 +3163,278 @@ const isNapiCrossUnsupportedHost =
|
|
|
1108
3163
|
)
|
|
1109
3164
|
},
|
|
1110
3165
|
)
|
|
3166
|
+
|
|
3167
|
+
async function createEmnapiLibDir(root: string, dirNames: string[]) {
|
|
3168
|
+
const emnapiLibDir = join(root, 'emnapi', 'lib')
|
|
3169
|
+
for (const dirName of dirNames) {
|
|
3170
|
+
await mkdir(join(emnapiLibDir, dirName), { recursive: true })
|
|
3171
|
+
}
|
|
3172
|
+
return emnapiLibDir
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
async function createWasiSdkDir(root: string, version: string) {
|
|
3176
|
+
const wasiSdkPath = join(root, `wasi-sdk-${version}`)
|
|
3177
|
+
await mkdir(wasiSdkPath, { recursive: true })
|
|
3178
|
+
await writeFile(join(wasiSdkPath, 'VERSION'), `${version}\n`)
|
|
3179
|
+
return wasiSdkPath
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
async function createWasiLibcArchive(
|
|
3183
|
+
projectDir: string,
|
|
3184
|
+
abi: 'legacy' | 'new',
|
|
3185
|
+
) {
|
|
3186
|
+
const libcPath = join(projectDir, `libc-${abi}.a`)
|
|
3187
|
+
// Only the archive member names matter: wasi-libc moved the futex helpers
|
|
3188
|
+
// into `futex.c` when it dropped the `int op` parameter, and kept `__wait.c`
|
|
3189
|
+
// for other symbols.
|
|
3190
|
+
const members =
|
|
3191
|
+
abi === 'new'
|
|
3192
|
+
? ['__wait.c.obj', 'futex.c.obj']
|
|
3193
|
+
: ['__wait.c.obj', '__wasilibc_busywait.c.obj']
|
|
3194
|
+
await writeFile(libcPath, `!<arch>\n${members.join('\n')}\n`)
|
|
3195
|
+
return libcPath
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
test('selects the wasi-sdk 34 emnapi archives for wasi-sdk >= 34', async (t) => {
|
|
3199
|
+
const { projectDir } = t.context
|
|
3200
|
+
const emnapiLibDir = await createEmnapiLibDir(projectDir, [
|
|
3201
|
+
'wasm32-wasip1',
|
|
3202
|
+
'wasm32-wasip1-threads',
|
|
3203
|
+
EMNAPI_WASI_SDK_34_LINK_DIR,
|
|
3204
|
+
])
|
|
3205
|
+
const wasiSdkPath = await createWasiSdkDir(projectDir, '34.0')
|
|
3206
|
+
|
|
3207
|
+
t.deepEqual(
|
|
3208
|
+
selectEmnapiLinkDir(
|
|
3209
|
+
emnapiLibDir,
|
|
3210
|
+
'wasm32-wasip1-threads',
|
|
3211
|
+
true,
|
|
3212
|
+
wasiSdkPath,
|
|
3213
|
+
),
|
|
3214
|
+
{
|
|
3215
|
+
linkDirName: EMNAPI_WASI_SDK_34_LINK_DIR,
|
|
3216
|
+
wasiSdkMajor: 34,
|
|
3217
|
+
needsWasiSdk34: true,
|
|
3218
|
+
},
|
|
3219
|
+
)
|
|
3220
|
+
})
|
|
3221
|
+
|
|
3222
|
+
test('keeps the legacy emnapi archives for wasi-sdk <= 33', async (t) => {
|
|
3223
|
+
const { projectDir } = t.context
|
|
3224
|
+
const emnapiLibDir = await createEmnapiLibDir(projectDir, [
|
|
3225
|
+
'wasm32-wasip1-threads',
|
|
3226
|
+
EMNAPI_WASI_SDK_34_LINK_DIR,
|
|
3227
|
+
])
|
|
3228
|
+
const wasiSdkPath = await createWasiSdkDir(projectDir, '33.0')
|
|
3229
|
+
|
|
3230
|
+
t.deepEqual(
|
|
3231
|
+
selectEmnapiLinkDir(
|
|
3232
|
+
emnapiLibDir,
|
|
3233
|
+
'wasm32-wasip1-threads',
|
|
3234
|
+
true,
|
|
3235
|
+
wasiSdkPath,
|
|
3236
|
+
),
|
|
3237
|
+
{
|
|
3238
|
+
linkDirName: 'wasm32-wasip1-threads',
|
|
3239
|
+
wasiSdkMajor: 33,
|
|
3240
|
+
needsWasiSdk34: false,
|
|
3241
|
+
},
|
|
3242
|
+
)
|
|
3243
|
+
})
|
|
3244
|
+
|
|
3245
|
+
test('keeps the legacy emnapi archives when the wasi-libc ABI is unknown', async (t) => {
|
|
3246
|
+
const { projectDir } = t.context
|
|
3247
|
+
const emnapiLibDir = await createEmnapiLibDir(projectDir, [
|
|
3248
|
+
'wasm32-wasip1-threads',
|
|
3249
|
+
EMNAPI_WASI_SDK_34_LINK_DIR,
|
|
3250
|
+
])
|
|
3251
|
+
|
|
3252
|
+
t.deepEqual(
|
|
3253
|
+
// `null` stands in for a Rust sysroot that cannot be probed.
|
|
3254
|
+
selectEmnapiLinkDir(
|
|
3255
|
+
emnapiLibDir,
|
|
3256
|
+
'wasm32-wasip1-threads',
|
|
3257
|
+
true,
|
|
3258
|
+
undefined,
|
|
3259
|
+
{ rustWasiLibc: null },
|
|
3260
|
+
),
|
|
3261
|
+
{
|
|
3262
|
+
linkDirName: 'wasm32-wasip1-threads',
|
|
3263
|
+
wasiSdkMajor: null,
|
|
3264
|
+
needsWasiSdk34: false,
|
|
3265
|
+
},
|
|
3266
|
+
)
|
|
3267
|
+
})
|
|
3268
|
+
|
|
3269
|
+
test('keeps the legacy emnapi archives for a pre wasi-sdk 34 Rust toolchain', async (t) => {
|
|
3270
|
+
const { projectDir } = t.context
|
|
3271
|
+
const emnapiLibDir = await createEmnapiLibDir(projectDir, [
|
|
3272
|
+
'wasm32-wasip1-threads',
|
|
3273
|
+
EMNAPI_WASI_SDK_34_LINK_DIR,
|
|
3274
|
+
])
|
|
3275
|
+
const rustWasiLibc = await createWasiLibcArchive(projectDir, 'legacy')
|
|
3276
|
+
|
|
3277
|
+
t.deepEqual(
|
|
3278
|
+
selectEmnapiLinkDir(
|
|
3279
|
+
emnapiLibDir,
|
|
3280
|
+
'wasm32-wasip1-threads',
|
|
3281
|
+
true,
|
|
3282
|
+
undefined,
|
|
3283
|
+
{ rustWasiLibc },
|
|
3284
|
+
),
|
|
3285
|
+
{
|
|
3286
|
+
linkDirName: 'wasm32-wasip1-threads',
|
|
3287
|
+
wasiSdkMajor: null,
|
|
3288
|
+
needsWasiSdk34: false,
|
|
3289
|
+
},
|
|
3290
|
+
)
|
|
3291
|
+
})
|
|
3292
|
+
|
|
3293
|
+
test('selects the wasi-sdk 34 archives for a Rust toolchain that bundles the new wasi-libc', async (t) => {
|
|
3294
|
+
const { projectDir } = t.context
|
|
3295
|
+
const emnapiLibDir = await createEmnapiLibDir(projectDir, [
|
|
3296
|
+
'wasm32-wasip1-threads',
|
|
3297
|
+
EMNAPI_WASI_SDK_34_LINK_DIR,
|
|
3298
|
+
])
|
|
3299
|
+
const rustWasiLibc = await createWasiLibcArchive(projectDir, 'new')
|
|
3300
|
+
|
|
3301
|
+
t.deepEqual(
|
|
3302
|
+
// No wasi-sdk is configured: cargo links Rust's bundled wasi-libc, and
|
|
3303
|
+
// Rust picked up wasi-sdk 34 in rust-lang/rust#161773.
|
|
3304
|
+
selectEmnapiLinkDir(
|
|
3305
|
+
emnapiLibDir,
|
|
3306
|
+
'wasm32-wasip1-threads',
|
|
3307
|
+
true,
|
|
3308
|
+
undefined,
|
|
3309
|
+
{ rustWasiLibc },
|
|
3310
|
+
),
|
|
3311
|
+
{
|
|
3312
|
+
linkDirName: EMNAPI_WASI_SDK_34_LINK_DIR,
|
|
3313
|
+
wasiSdkMajor: null,
|
|
3314
|
+
needsWasiSdk34: true,
|
|
3315
|
+
},
|
|
3316
|
+
)
|
|
3317
|
+
})
|
|
3318
|
+
|
|
3319
|
+
test('never selects the wasi-sdk 34 archives for the threadless target', async (t) => {
|
|
3320
|
+
const { projectDir } = t.context
|
|
3321
|
+
const emnapiLibDir = await createEmnapiLibDir(projectDir, [
|
|
3322
|
+
'wasm32-wasip1',
|
|
3323
|
+
EMNAPI_WASI_SDK_34_LINK_DIR,
|
|
3324
|
+
])
|
|
3325
|
+
const wasiSdkPath = await createWasiSdkDir(projectDir, '34.0')
|
|
3326
|
+
|
|
3327
|
+
t.deepEqual(
|
|
3328
|
+
selectEmnapiLinkDir(emnapiLibDir, 'wasm32-wasip1', false, wasiSdkPath),
|
|
3329
|
+
{
|
|
3330
|
+
linkDirName: 'wasm32-wasip1',
|
|
3331
|
+
wasiSdkMajor: 34,
|
|
3332
|
+
needsWasiSdk34: false,
|
|
3333
|
+
},
|
|
3334
|
+
)
|
|
3335
|
+
})
|
|
3336
|
+
|
|
3337
|
+
test('falls back to the legacy archives when emnapi has no wasi-sdk 34 directory', async (t) => {
|
|
3338
|
+
const { projectDir } = t.context
|
|
3339
|
+
const emnapiLibDir = await createEmnapiLibDir(projectDir, [
|
|
3340
|
+
'wasm32-wasip1-threads',
|
|
3341
|
+
])
|
|
3342
|
+
const wasiSdkPath = await createWasiSdkDir(projectDir, '34.0')
|
|
3343
|
+
|
|
3344
|
+
t.deepEqual(
|
|
3345
|
+
selectEmnapiLinkDir(
|
|
3346
|
+
emnapiLibDir,
|
|
3347
|
+
'wasm32-wasip1-threads',
|
|
3348
|
+
true,
|
|
3349
|
+
wasiSdkPath,
|
|
3350
|
+
),
|
|
3351
|
+
{
|
|
3352
|
+
linkDirName: 'wasm32-wasip1-threads',
|
|
3353
|
+
wasiSdkMajor: 34,
|
|
3354
|
+
needsWasiSdk34: true,
|
|
3355
|
+
},
|
|
3356
|
+
)
|
|
3357
|
+
})
|
|
3358
|
+
|
|
3359
|
+
test('resolveWasmMemory keeps the template defaults when nothing is configured', (t) => {
|
|
3360
|
+
t.deepEqual(resolveWasmMemory(undefined, true), {
|
|
3361
|
+
initialMemory: undefined,
|
|
3362
|
+
maximumMemory: undefined,
|
|
3363
|
+
})
|
|
3364
|
+
t.deepEqual(resolveWasmMemory({}, false), {
|
|
3365
|
+
initialMemory: undefined,
|
|
3366
|
+
maximumMemory: undefined,
|
|
3367
|
+
})
|
|
3368
|
+
})
|
|
3369
|
+
|
|
3370
|
+
test('resolveWasmMemory applies initialMemory to both flavors without the threadless key', (t) => {
|
|
3371
|
+
const wasm = { initialMemory: 16384, maximumMemory: 65536 }
|
|
3372
|
+
t.deepEqual(resolveWasmMemory(wasm, true), {
|
|
3373
|
+
initialMemory: 16384,
|
|
3374
|
+
maximumMemory: 65536,
|
|
3375
|
+
})
|
|
3376
|
+
t.deepEqual(resolveWasmMemory(wasm, false), {
|
|
3377
|
+
initialMemory: 16384,
|
|
3378
|
+
maximumMemory: 65536,
|
|
3379
|
+
})
|
|
3380
|
+
})
|
|
3381
|
+
|
|
3382
|
+
test('resolveWasmMemory gives the threadless flavor its own floor', (t) => {
|
|
3383
|
+
const wasm = { initialMemory: 16384, threadlessInitialMemory: 1027 }
|
|
3384
|
+
t.is(resolveWasmMemory(wasm, true).initialMemory, 16384)
|
|
3385
|
+
t.is(resolveWasmMemory(wasm, false).initialMemory, 1027)
|
|
3386
|
+
// the threadless key alone leaves the threaded loader on its template default
|
|
3387
|
+
t.is(
|
|
3388
|
+
resolveWasmMemory({ threadlessInitialMemory: 1027 }, true).initialMemory,
|
|
3389
|
+
undefined,
|
|
3390
|
+
)
|
|
3391
|
+
})
|
|
3392
|
+
|
|
3393
|
+
test('resolveWasmMemory rejects an initial memory above the maximum', (t) => {
|
|
3394
|
+
t.throws(
|
|
3395
|
+
() =>
|
|
3396
|
+
resolveWasmMemory(
|
|
3397
|
+
{
|
|
3398
|
+
initialMemory: 4000,
|
|
3399
|
+
threadlessInitialMemory: 2048,
|
|
3400
|
+
maximumMemory: 1024,
|
|
3401
|
+
},
|
|
3402
|
+
false,
|
|
3403
|
+
),
|
|
3404
|
+
{
|
|
3405
|
+
message:
|
|
3406
|
+
'napi.wasm.initialMemory (4000 pages) must not exceed napi.wasm.maximumMemory (1024 pages)',
|
|
3407
|
+
},
|
|
3408
|
+
)
|
|
3409
|
+
t.throws(
|
|
3410
|
+
() =>
|
|
3411
|
+
resolveWasmMemory(
|
|
3412
|
+
{ threadlessInitialMemory: 2048, maximumMemory: 1024 },
|
|
3413
|
+
false,
|
|
3414
|
+
),
|
|
3415
|
+
{
|
|
3416
|
+
message:
|
|
3417
|
+
'napi.wasm.threadlessInitialMemory (2048 pages) must not exceed napi.wasm.maximumMemory (1024 pages)',
|
|
3418
|
+
},
|
|
3419
|
+
)
|
|
3420
|
+
// no maximumMemory => the implicit memory32 ceiling still applies
|
|
3421
|
+
t.throws(() => resolveWasmMemory({ threadlessInitialMemory: 65537 }, false), {
|
|
3422
|
+
message: /between 1 and 65536 pages/,
|
|
3423
|
+
})
|
|
3424
|
+
})
|
|
3425
|
+
|
|
3426
|
+
test('resolveWasmMemory rejects non-page values', (t) => {
|
|
3427
|
+
for (const value of [0, -1, 1.5, Number.NaN, 65537]) {
|
|
3428
|
+
t.throws(
|
|
3429
|
+
() => resolveWasmMemory({ threadlessInitialMemory: value }, false),
|
|
3430
|
+
{
|
|
3431
|
+
message:
|
|
3432
|
+
/napi\.wasm\.threadlessInitialMemory must be an integer between 1 and 65536 pages/,
|
|
3433
|
+
},
|
|
3434
|
+
)
|
|
3435
|
+
}
|
|
3436
|
+
t.throws(() => resolveWasmMemory({ maximumMemory: 65537 }, true), {
|
|
3437
|
+
message:
|
|
3438
|
+
/napi\.wasm\.maximumMemory must be an integer between 1 and 65536 pages/,
|
|
3439
|
+
})
|
|
3440
|
+
})
|