@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
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
pick,
|
|
29
29
|
resolvePackageReconciliationPaths,
|
|
30
30
|
restrictWasiNodeEngine,
|
|
31
|
+
serializeJson,
|
|
31
32
|
wasiLoaderSuffix,
|
|
32
33
|
wasiTargetHasThreads,
|
|
33
34
|
withFileSystemReconciliation,
|
|
@@ -48,6 +49,7 @@ export interface PackageMeta {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const WASM_RUNTIME_PACKAGE_NAME = '@napi-rs/wasm-runtime'
|
|
52
|
+
const ASYNC_RUNTIME_PACKAGE_NAME = '@napi-rs/async-runtime'
|
|
51
53
|
|
|
52
54
|
interface PendingMetadataWrite {
|
|
53
55
|
content: string
|
|
@@ -65,25 +67,25 @@ interface OwnedWasiPackage {
|
|
|
65
67
|
target: Target
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
async function
|
|
70
|
+
async function getLatestPackageVersion(packageName: string) {
|
|
69
71
|
const npmRegistryBase =
|
|
70
72
|
process.env.npm_config_registry?.replace(/\/?$/, '/') ??
|
|
71
73
|
'https://registry.npmjs.org/'
|
|
72
|
-
const packageMetadataUrl = `${npmRegistryBase}${
|
|
74
|
+
const packageMetadataUrl = `${npmRegistryBase}${packageName}`
|
|
73
75
|
let response: Response
|
|
74
76
|
|
|
75
77
|
try {
|
|
76
78
|
response = await fetch(packageMetadataUrl)
|
|
77
79
|
} catch (error) {
|
|
78
80
|
throw new Error(
|
|
79
|
-
`Failed to fetch ${packageMetadataUrl} while resolving ${
|
|
81
|
+
`Failed to fetch ${packageMetadataUrl} while resolving ${packageName}. Check your network connection and npm registry availability.`,
|
|
80
82
|
{ cause: error },
|
|
81
83
|
)
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
if (!response.ok) {
|
|
85
87
|
throw new Error(
|
|
86
|
-
`Failed to fetch ${packageMetadataUrl} while resolving ${
|
|
88
|
+
`Failed to fetch ${packageMetadataUrl} while resolving ${packageName}: npm registry responded with ${response.status} ${response.statusText || 'Unknown Status'}`,
|
|
87
89
|
)
|
|
88
90
|
}
|
|
89
91
|
|
|
@@ -93,7 +95,7 @@ async function getLatestWasmRuntimeVersion() {
|
|
|
93
95
|
packageMeta = (await response.json()) as PackageMeta
|
|
94
96
|
} catch (error) {
|
|
95
97
|
throw new Error(
|
|
96
|
-
`Failed to parse npm registry metadata for ${
|
|
98
|
+
`Failed to parse npm registry metadata for ${packageName} from ${packageMetadataUrl}`,
|
|
97
99
|
{ cause: error },
|
|
98
100
|
)
|
|
99
101
|
}
|
|
@@ -102,7 +104,7 @@ async function getLatestWasmRuntimeVersion() {
|
|
|
102
104
|
|
|
103
105
|
if (typeof latestVersion !== 'string' || latestVersion.trim().length === 0) {
|
|
104
106
|
throw new Error(
|
|
105
|
-
`npm registry metadata for ${
|
|
107
|
+
`npm registry metadata for ${packageName} from ${packageMetadataUrl} did not include a latest dist-tag`,
|
|
106
108
|
)
|
|
107
109
|
}
|
|
108
110
|
|
|
@@ -445,9 +447,15 @@ async function createNpmDirsUnlocked(
|
|
|
445
447
|
),
|
|
446
448
|
)
|
|
447
449
|
).flat()
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
450
|
+
const hasWasmTarget = targets.some((target) => target.arch === 'wasm32')
|
|
451
|
+
const [wasmRuntimeVersion, asyncRuntimeVersion] = await Promise.all([
|
|
452
|
+
hasWasmTarget
|
|
453
|
+
? getLatestPackageVersion(WASM_RUNTIME_PACKAGE_NAME)
|
|
454
|
+
: undefined,
|
|
455
|
+
hasWasmTarget && wasm?.asyncRuntime === true
|
|
456
|
+
? getLatestPackageVersion(ASYNC_RUNTIME_PACKAGE_NAME)
|
|
457
|
+
: undefined,
|
|
458
|
+
])
|
|
451
459
|
const pendingWrites: PendingMetadataWrite[] = []
|
|
452
460
|
|
|
453
461
|
for (const target of targets) {
|
|
@@ -559,6 +567,11 @@ async function createNpmDirsUnlocked(
|
|
|
559
567
|
'@napi-rs/wasm-runtime': `~${wasmRuntimeVersion}`,
|
|
560
568
|
'@emnapi/core': emnapiVersion,
|
|
561
569
|
'@emnapi/runtime': emnapiVersion,
|
|
570
|
+
// The compatibility axis is the host contract version (4), which is
|
|
571
|
+
// stable across a semver major, so a caret range is correct here.
|
|
572
|
+
...(asyncRuntimeVersion
|
|
573
|
+
? { '@napi-rs/async-runtime': `^${asyncRuntimeVersion}` }
|
|
574
|
+
: {}),
|
|
562
575
|
...(wasm?.browser?.buffer === true &&
|
|
563
576
|
(wasm.browser.fs !== true || !wasiTargetHasThreads(target))
|
|
564
577
|
? { buffer: directBufferDependency }
|
|
@@ -574,7 +587,7 @@ async function createNpmDirsUnlocked(
|
|
|
574
587
|
|
|
575
588
|
const targetPackageJson = join(targetDir, 'package.json')
|
|
576
589
|
pendingWrites.push({
|
|
577
|
-
content:
|
|
590
|
+
content: serializeJson(scopedPackageJson),
|
|
578
591
|
destination: targetPackageJson,
|
|
579
592
|
})
|
|
580
593
|
if (wasmModuleTypeDef) {
|
package/src/api/new.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { homedir } from 'node:os'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { promises as fs } from 'node:fs'
|
|
6
6
|
|
|
7
|
-
import { parse as parseToml
|
|
8
|
-
import { load as yamlLoad
|
|
7
|
+
import { parse as parseToml } from '@std/toml'
|
|
8
|
+
import { load as yamlLoad } from 'js-yaml'
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
applyDefaultNewOptions,
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
AVAILABLE_TARGETS,
|
|
16
16
|
CLI_VERSION,
|
|
17
17
|
debugFactory,
|
|
18
|
+
serializeJson,
|
|
19
|
+
serializeToml,
|
|
20
|
+
serializeYaml,
|
|
18
21
|
DEFAULT_TARGETS,
|
|
19
22
|
getWasiTarget,
|
|
20
23
|
mkdirAsync,
|
|
@@ -347,7 +350,7 @@ async function filterTargetsInPackageJson(
|
|
|
347
350
|
}
|
|
348
351
|
}
|
|
349
352
|
|
|
350
|
-
await fs.writeFile(filePath,
|
|
353
|
+
await fs.writeFile(filePath, serializeJson(packageJson))
|
|
351
354
|
await updateGeneratedWasiAttributes(
|
|
352
355
|
path.join(path.dirname(filePath), '.gitattributes'),
|
|
353
356
|
packageJson.napi.binaryName,
|
|
@@ -381,10 +384,10 @@ async function updateGeneratedWasiAttributes(
|
|
|
381
384
|
generatedFiles.add(`${binaryName}.${suffix}-deferred.d.ts`)
|
|
382
385
|
}
|
|
383
386
|
}
|
|
387
|
+
while (lines[lines.length - 1] === '') {
|
|
388
|
+
lines.pop()
|
|
389
|
+
}
|
|
384
390
|
if (generatedFiles.size > 0) {
|
|
385
|
-
while (lines[lines.length - 1] === '') {
|
|
386
|
-
lines.pop()
|
|
387
|
-
}
|
|
388
391
|
lines.push(
|
|
389
392
|
'',
|
|
390
393
|
...[...generatedFiles].map((file) => `${file} linguist-detectable=false`),
|
|
@@ -429,7 +432,7 @@ async function updateCargoTomlTypeDef(
|
|
|
429
432
|
|
|
430
433
|
dependencies['napi-derive'] = dependencyConfig
|
|
431
434
|
|
|
432
|
-
await fs.writeFile(filePath,
|
|
435
|
+
await fs.writeFile(filePath, serializeToml(cargoToml))
|
|
433
436
|
}
|
|
434
437
|
|
|
435
438
|
export async function updateCargoTomlNodeApiVersion(
|
|
@@ -475,7 +478,7 @@ export async function updateCargoTomlNodeApiVersion(
|
|
|
475
478
|
|
|
476
479
|
dependencies.napi = dependencyConfig
|
|
477
480
|
|
|
478
|
-
await fs.writeFile(filePath,
|
|
481
|
+
await fs.writeFile(filePath, serializeToml(cargoToml))
|
|
479
482
|
}
|
|
480
483
|
|
|
481
484
|
async function filterTargetsInGithubActions(
|
|
@@ -683,12 +686,7 @@ async function filterTargetsInGithubActions(
|
|
|
683
686
|
}
|
|
684
687
|
|
|
685
688
|
// Write back the filtered YAML
|
|
686
|
-
|
|
687
|
-
lineWidth: -1,
|
|
688
|
-
noRefs: true,
|
|
689
|
-
sortKeys: false,
|
|
690
|
-
})
|
|
691
|
-
await fs.writeFile(filePath, updatedYaml)
|
|
689
|
+
await fs.writeFile(filePath, serializeYaml(yaml))
|
|
692
690
|
}
|
|
693
691
|
|
|
694
692
|
function processOptions(options: RawNewOptions) {
|
|
@@ -853,10 +851,7 @@ export async function newProject(userOptions: RawNewOptions) {
|
|
|
853
851
|
)
|
|
854
852
|
}
|
|
855
853
|
|
|
856
|
-
await fs.writeFile(
|
|
857
|
-
packageJsonPath,
|
|
858
|
-
JSON.stringify(pkgJson, null, 2) + '\n',
|
|
859
|
-
)
|
|
854
|
+
await fs.writeFile(packageJsonPath, serializeJson(pkgJson))
|
|
860
855
|
} catch (error) {
|
|
861
856
|
throw new Error(`Failed to create project: ${error}`)
|
|
862
857
|
}
|
package/src/api/pre-publish.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
withFileSystemReconciliation,
|
|
59
59
|
AVAILABLE_TARGETS,
|
|
60
60
|
parseTriple,
|
|
61
|
+
serializeJson,
|
|
61
62
|
type CommonPackageJsonFields,
|
|
62
63
|
type FileSystemTransactionWrite,
|
|
63
64
|
type RootPublisher,
|
|
@@ -112,6 +113,10 @@ const wasiRuntimeDependencies = [
|
|
|
112
113
|
'@napi-rs/wasm-runtime',
|
|
113
114
|
'@emnapi/core',
|
|
114
115
|
'@emnapi/runtime',
|
|
116
|
+
// Only the loaders generated for an addon with `napi.wasm.asyncRuntime` set
|
|
117
|
+
// import it. `releasePackageRuntimeImports` matches by prefix, so the package
|
|
118
|
+
// root and any subpath (`@napi-rs/async-runtime/workerd`) both count.
|
|
119
|
+
'@napi-rs/async-runtime',
|
|
115
120
|
'buffer',
|
|
116
121
|
]
|
|
117
122
|
const require = createRequire(import.meta.url)
|
|
@@ -2267,7 +2272,7 @@ async function registerPublicationWorkspaces(
|
|
|
2267
2272
|
} else {
|
|
2268
2273
|
manifest.workspaces = releaseWorkspaces
|
|
2269
2274
|
}
|
|
2270
|
-
await writeFileAtomic(manifestPath,
|
|
2275
|
+
await writeFileAtomic(manifestPath, serializeJson(manifest))
|
|
2271
2276
|
}
|
|
2272
2277
|
|
|
2273
2278
|
async function createPublicationExecutionPackage(
|
|
@@ -2391,10 +2396,7 @@ async function materializeRootReleasePlan(
|
|
|
2391
2396
|
}
|
|
2392
2397
|
}
|
|
2393
2398
|
syncThreadlessWasiRootFacadeManifest(updatedPackageJson, plan.packageJson)
|
|
2394
|
-
await writeFileAtomic(
|
|
2395
|
-
packageJsonPath,
|
|
2396
|
-
JSON.stringify(updatedPackageJson, null, 2),
|
|
2397
|
-
)
|
|
2399
|
+
await writeFileAtomic(packageJsonPath, serializeJson(updatedPackageJson))
|
|
2398
2400
|
|
|
2399
2401
|
const generatedFiles = new Set(plan.facade?.generatedFiles ?? [])
|
|
2400
2402
|
await Promise.all(
|
|
@@ -2492,7 +2494,7 @@ async function stageReleasePackage(
|
|
|
2492
2494
|
const packageJsonPath = join(stagedPkgDir, 'package.json')
|
|
2493
2495
|
const packageJson = JSON.parse(await readFileAsync(packageJsonPath, 'utf8'))
|
|
2494
2496
|
packageJson.version = packageVersion
|
|
2495
|
-
await writeFileAtomic(packageJsonPath,
|
|
2497
|
+
await writeFileAtomic(packageJsonPath, serializeJson(packageJson))
|
|
2496
2498
|
return {
|
|
2497
2499
|
pkgDir: options.pkgDir,
|
|
2498
2500
|
rootDir: options.rootDir,
|
|
@@ -2620,7 +2622,7 @@ export async function commitPrePublishFileSystemTransaction({
|
|
|
2620
2622
|
await commitFileSystemTransaction(transactionRoot, writes, removals)
|
|
2621
2623
|
}
|
|
2622
2624
|
|
|
2623
|
-
async function validateReleasePackageContents({
|
|
2625
|
+
export async function validateReleasePackageContents({
|
|
2624
2626
|
pkgDir,
|
|
2625
2627
|
rootDir,
|
|
2626
2628
|
packageName,
|
|
@@ -2705,10 +2707,7 @@ async function validateReleasePackageContents({
|
|
|
2705
2707
|
if (updateManifest) {
|
|
2706
2708
|
packageFiles.splice(0, packageFiles.length, ...declarationClosure.files)
|
|
2707
2709
|
packageJson.files = packageFiles
|
|
2708
|
-
await writeFileAtomic(
|
|
2709
|
-
packageJsonPath,
|
|
2710
|
-
`${JSON.stringify(packageJson, null, 2)}\n`,
|
|
2711
|
-
)
|
|
2710
|
+
await writeFileAtomic(packageJsonPath, serializeJson(packageJson))
|
|
2712
2711
|
}
|
|
2713
2712
|
|
|
2714
2713
|
const publicFiles = new Set<string>(packageFiles)
|
|
@@ -3016,6 +3015,30 @@ function validateWasiReleasePackageManifest(
|
|
|
3016
3015
|
`Release package ${packageJson.name} must omit buffer when its loaders do not import it`,
|
|
3017
3016
|
)
|
|
3018
3017
|
}
|
|
3018
|
+
|
|
3019
|
+
// `create-npm-dirs` resolves the version from the registry rather than
|
|
3020
|
+
// pinning it here, so only the range's shape can be checked. The require /
|
|
3021
|
+
// forbid pair is what matters: an undeclared import breaks the published
|
|
3022
|
+
// package at load, and a declared dependency no loader imports is dead
|
|
3023
|
+
// weight on every consumer of this flavor.
|
|
3024
|
+
if (packagedRuntimeImports.has('@napi-rs/async-runtime')) {
|
|
3025
|
+
const asyncRuntimeVersion = requireStringDependency(
|
|
3026
|
+
packageJson.name,
|
|
3027
|
+
dependencies,
|
|
3028
|
+
'@napi-rs/async-runtime',
|
|
3029
|
+
)
|
|
3030
|
+
try {
|
|
3031
|
+
new Range(asyncRuntimeVersion)
|
|
3032
|
+
} catch {
|
|
3033
|
+
throw new Error(
|
|
3034
|
+
`Release package ${packageJson.name} has invalid @napi-rs/async-runtime dependency ${asyncRuntimeVersion}`,
|
|
3035
|
+
)
|
|
3036
|
+
}
|
|
3037
|
+
} else if (dependencies['@napi-rs/async-runtime'] !== undefined) {
|
|
3038
|
+
throw new Error(
|
|
3039
|
+
`Release package ${packageJson.name} must omit @napi-rs/async-runtime when its loaders do not import it`,
|
|
3040
|
+
)
|
|
3041
|
+
}
|
|
3019
3042
|
}
|
|
3020
3043
|
|
|
3021
3044
|
function requireStringDependency(
|
package/src/api/rename.ts
CHANGED
|
@@ -12,8 +12,8 @@ import {
|
|
|
12
12
|
sep,
|
|
13
13
|
} from 'node:path'
|
|
14
14
|
|
|
15
|
-
import { parse as parseToml
|
|
16
|
-
import { load as yamlParse
|
|
15
|
+
import { parse as parseToml } from '@std/toml'
|
|
16
|
+
import { load as yamlParse } from 'js-yaml'
|
|
17
17
|
import { isNil, omitBy, pick } from 'es-toolkit'
|
|
18
18
|
|
|
19
19
|
import { applyDefaultRenameOptions, type RenameOptions } from '../def/rename.js'
|
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
readFileAsync,
|
|
24
24
|
resolvePackageReconciliationPaths,
|
|
25
25
|
type Target,
|
|
26
|
+
serializeJson,
|
|
27
|
+
serializeToml,
|
|
28
|
+
serializeYaml,
|
|
26
29
|
wasiLoaderSuffix,
|
|
27
30
|
wasiTargetHasThreads,
|
|
28
31
|
withPackageFileSystemReconciliation,
|
|
@@ -1265,10 +1268,6 @@ function updateNapiConfigRecord(record: JsonRecord, options: RenameOptions) {
|
|
|
1265
1268
|
}
|
|
1266
1269
|
}
|
|
1267
1270
|
|
|
1268
|
-
function serializeJsonLike(content: string, value: unknown) {
|
|
1269
|
-
return `${JSON.stringify(value, null, 2)}${content.endsWith('\n') ? '\n' : ''}`
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
1271
|
function sanitizeCargoPackageName(binaryName: string) {
|
|
1273
1272
|
return binaryName.replace(/[^A-Za-z0-9_]/g, '_').toLowerCase()
|
|
1274
1273
|
}
|
|
@@ -1458,8 +1457,7 @@ async function renameProjectUnlocked(
|
|
|
1458
1457
|
const plan = new RenameTransactionPlan()
|
|
1459
1458
|
plan.addWrite(
|
|
1460
1459
|
packageJsonPath,
|
|
1461
|
-
|
|
1462
|
-
packageJsonContent,
|
|
1460
|
+
serializeJson(
|
|
1463
1461
|
rewritePackageManifest(
|
|
1464
1462
|
packageJsonData,
|
|
1465
1463
|
managedWasiRenames,
|
|
@@ -1477,11 +1475,7 @@ async function renameProjectUnlocked(
|
|
|
1477
1475
|
throw new Error(`NAPI config must contain a JSON object: ${configPath}`)
|
|
1478
1476
|
}
|
|
1479
1477
|
updateNapiConfigRecord(configData, options)
|
|
1480
|
-
plan.addWrite(
|
|
1481
|
-
configPath,
|
|
1482
|
-
serializeJsonLike(configContent, configData),
|
|
1483
|
-
configMode,
|
|
1484
|
-
)
|
|
1478
|
+
plan.addWrite(configPath, serializeJson(configData), configMode)
|
|
1485
1479
|
}
|
|
1486
1480
|
|
|
1487
1481
|
if (binaryNameChanged) {
|
|
@@ -1495,7 +1489,7 @@ async function renameProjectUnlocked(
|
|
|
1495
1489
|
const cargoToml = parseToml(tomlContent) as any
|
|
1496
1490
|
if (cargoToml.package) {
|
|
1497
1491
|
cargoToml.package.name = sanitizeCargoPackageName(newName)
|
|
1498
|
-
plan.addWrite(cargoTomlPath,
|
|
1492
|
+
plan.addWrite(cargoTomlPath, serializeToml(cargoToml), cargoTomlMode)
|
|
1499
1493
|
}
|
|
1500
1494
|
|
|
1501
1495
|
const workflowPath = join(projectRoot, '.github', 'workflows', 'CI.yml')
|
|
@@ -1515,11 +1509,7 @@ async function renameProjectUnlocked(
|
|
|
1515
1509
|
workflowData.env.APP_NAME = newName
|
|
1516
1510
|
plan.addWrite(
|
|
1517
1511
|
canonicalWorkflowPath,
|
|
1518
|
-
|
|
1519
|
-
lineWidth: -1,
|
|
1520
|
-
noRefs: true,
|
|
1521
|
-
sortKeys: false,
|
|
1522
|
-
}),
|
|
1512
|
+
serializeYaml(workflowData),
|
|
1523
1513
|
workflowMode,
|
|
1524
1514
|
)
|
|
1525
1515
|
}
|
|
@@ -1567,8 +1557,7 @@ async function renameProjectUnlocked(
|
|
|
1567
1557
|
`Managed package manifest must contain a JSON object: ${targetPackageJsonPath}`,
|
|
1568
1558
|
)
|
|
1569
1559
|
}
|
|
1570
|
-
const updatedContent =
|
|
1571
|
-
content,
|
|
1560
|
+
const updatedContent = serializeJson(
|
|
1572
1561
|
rewritePackageManifest(
|
|
1573
1562
|
manifest,
|
|
1574
1563
|
managedWasiRenames,
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `__napiBindingTarget` contract, shared by every generated loader.
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately imports nothing: both `js-binding.ts` and
|
|
5
|
+
* `load-wasi-template.ts` depend on it, and `load-wasi-template.ts` otherwise
|
|
6
|
+
* has no top-level imports at all. Keeping the contract at the bottom of the
|
|
7
|
+
* graph is what lets both templates emit the same runtime helper without
|
|
8
|
+
* duplicating its source.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Named export every generated loader uses to report which binding artifact
|
|
13
|
+
* actually loaded: `'native'` for a `.node` addon, otherwise the
|
|
14
|
+
* `platformArchABI` of the WASI flavor (`'wasm32-wasi'`, `'wasm32-wasip1'`).
|
|
15
|
+
*/
|
|
16
|
+
export const NAPI_BINDING_TARGET_EXPORT = '__napiBindingTarget'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `code` on the error the emitted loader throws when the binding it loaded
|
|
20
|
+
* already owns {@link NAPI_BINDING_TARGET_EXPORT}. Named after the other
|
|
21
|
+
* loader-thrown codes (`ERR_NAPI_WASI_LIFECYCLE_REENTRY`,
|
|
22
|
+
* `ERR_NAPI_WASI_CLEANUP_PENDING`, `ERR_NAPI_ASYNC_RUNTIME_BINDING_MISMATCH`)
|
|
23
|
+
* so a consumer can branch on it instead of on the message.
|
|
24
|
+
*/
|
|
25
|
+
export const ERR_NAPI_BINDING_TARGET_CONFLICT =
|
|
26
|
+
'ERR_NAPI_BINDING_TARGET_CONFLICT'
|
|
27
|
+
|
|
28
|
+
/** Name of the runtime helper {@link BINDING_TARGET_STAMP_HELPER} declares. */
|
|
29
|
+
export const NAPI_BINDING_TARGET_STAMP_FN = '__napiStampBindingTarget'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Reject an export of {@link NAPI_BINDING_TARGET_EXPORT} at build time.
|
|
33
|
+
*
|
|
34
|
+
* `idents` is the type-def export list, so this only sees what napi-rs type
|
|
35
|
+
* generation reports. A name attached imperatively by a
|
|
36
|
+
* `#[napi(module_exports)]` hook emits no type-def entry and is invisible here
|
|
37
|
+
* — the same blind spot `typeDefAvailable` documents for the sibling check.
|
|
38
|
+
* {@link BINDING_TARGET_STAMP_HELPER} is what catches those, at load time.
|
|
39
|
+
*
|
|
40
|
+
* What this check is load-bearing for: a duplicated ident would emit a
|
|
41
|
+
* duplicate `export const` in the ESM loader (a syntax error), and would make
|
|
42
|
+
* the CJS loader overwrite its own reported target. It also keeps the generated
|
|
43
|
+
* `.d.ts` free of a duplicate identifier.
|
|
44
|
+
*
|
|
45
|
+
* All three are consequences of emitting something, so callers must only ask
|
|
46
|
+
* when the build emits it: the loader templates check themselves, and the root
|
|
47
|
+
* `.d.ts` is checked behind `bindingTargetDeclarationPredicate`. A build that
|
|
48
|
+
* writes no loader reserves nothing.
|
|
49
|
+
*/
|
|
50
|
+
export function assertBindingTargetIdentFree(idents: string[]): void {
|
|
51
|
+
if (idents.indexOf(NAPI_BINDING_TARGET_EXPORT) !== -1) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`\`${NAPI_BINDING_TARGET_EXPORT}\` is reserved by the generated binding loader. Rename the napi export, e.g. #[napi(js_name = "...")].`,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Runtime helper emitted into every loader that stamps
|
|
60
|
+
* {@link NAPI_BINDING_TARGET_EXPORT} onto an exports object it does not own.
|
|
61
|
+
*
|
|
62
|
+
* Five outcomes, in order:
|
|
63
|
+
*
|
|
64
|
+
* | exports object state | result |
|
|
65
|
+
* | ------------------------------- | ----------------------------------------- |
|
|
66
|
+
* | own property, same value | no-op, returns `target` |
|
|
67
|
+
* | own property, different value | throw, `ERR_NAPI_BINDING_TARGET_CONFLICT` |
|
|
68
|
+
* | non-extensible, no own property | skip, returns `target` |
|
|
69
|
+
* | refuses the definition | skip, returns `target` |
|
|
70
|
+
* | otherwise | stamp, returns `target` |
|
|
71
|
+
*
|
|
72
|
+
* The stamp is `Object.defineProperty`, not an assignment. `hasOwnProperty`
|
|
73
|
+
* above sees own properties only and `Object.isExtensible` only own
|
|
74
|
+
* extensibility, so an ordinary assignment would still walk the prototype chain
|
|
75
|
+
* into an inherited accessor on a user-controlled object: its setter can throw,
|
|
76
|
+
* failing an otherwise successful load, or absorb the write and create nothing,
|
|
77
|
+
* leaving the named export the generated declaration promises resolving to
|
|
78
|
+
* `undefined`. `[[Define]]` consults no prototype, and the descriptor is the one
|
|
79
|
+
* a successful assignment would have produced. The `try` around it covers the
|
|
80
|
+
* one shape that can still refuse — an exotic object such as a `Proxy` whose
|
|
81
|
+
* `defineProperty` trap returns `false` — under the same rule as the
|
|
82
|
+
* non-extensible skip: metadata never fails a load.
|
|
83
|
+
*
|
|
84
|
+
* Every branch that does not throw returns `target`, because the CommonJS emit
|
|
85
|
+
* sites assign the return value —
|
|
86
|
+
* `module.exports.__napiBindingTarget = __napiStampBindingTarget(...)` — rather
|
|
87
|
+
* than calling it as a statement. Node's CJS -> ESM named export detection is
|
|
88
|
+
* `cjs-module-lexer`, a static scanner: it reports `__napiBindingTarget` as a
|
|
89
|
+
* named export only when it can see a `module.exports.<name> =` assignment, and
|
|
90
|
+
* a bare call is invisible to it, so `import { __napiBindingTarget }` from a
|
|
91
|
+
* generated CJS loader stops linking entirely.
|
|
92
|
+
*
|
|
93
|
+
* That assignment always succeeds, and it is never what a consumer reads: its
|
|
94
|
+
* target is the loader's own `module.exports`, an ordinary extensible object,
|
|
95
|
+
* and the alias on the next line replaces it with the binding itself. The value
|
|
96
|
+
* an import resolves to is therefore the one the stamp put on the binding, so
|
|
97
|
+
* when the guard skips — a sealed or frozen exports object — the CommonJS
|
|
98
|
+
* entries report `undefined`. `build.spec.ts` pins exactly that, in `a frozen
|
|
99
|
+
* addon keeps __napiBindingTarget importable, just undefined`. The ESM loaders
|
|
100
|
+
* are unaffected: theirs is a module-level `export const`, not a property of
|
|
101
|
+
* the object they hand out.
|
|
102
|
+
* (`Object.defineProperty` is not an alternative shape for the lexer, whatever
|
|
103
|
+
* it is inside the guard: the lexer matches a literal `module.exports.<name> =`
|
|
104
|
+
* and reports nothing for a data-descriptor `defineProperty` call, so the named
|
|
105
|
+
* import stops linking entirely — measured against Node's own detection.)
|
|
106
|
+
*
|
|
107
|
+
* The assignment target is never the object being stamped. Both CommonJS
|
|
108
|
+
* loaders stamp the addon's exports object but assign onto their own
|
|
109
|
+
* `module.exports`, which they replace with that object afterwards: an addon
|
|
110
|
+
* accessor can report the expected value from a getter and still throw from its
|
|
111
|
+
* setter, and only the guard's `hasOwnProperty`-and-read path is safe to run
|
|
112
|
+
* against it.
|
|
113
|
+
*
|
|
114
|
+
* Placement is the same rule in every loader that stamps a binding object:
|
|
115
|
+
* exactly one stamp, after the async runtime host installation — which hands
|
|
116
|
+
* that object to addon-provided registration functions that may reshape it —
|
|
117
|
+
* and inside the initialization guard that can undo a failed load: the `try`
|
|
118
|
+
* that rolls the WASI environment back, or the one that marks a deferred
|
|
119
|
+
* instance failed.
|
|
120
|
+
*
|
|
121
|
+
* The equal-value short circuit is required, not cosmetic: the root CJS loader
|
|
122
|
+
* aliases the object it loaded, so a `NAPI_RS_NATIVE_LIBRARY_PATH` override
|
|
123
|
+
* that is itself a generated WASI loader — and every WASI fallback candidate —
|
|
124
|
+
* hands back an object already carrying the value about to be stamped.
|
|
125
|
+
*
|
|
126
|
+
* Node 12 compatible (no optional chaining, no nullish coalescing) and valid in
|
|
127
|
+
* both sloppy CJS and strict ESM, because all four loaders emit it verbatim.
|
|
128
|
+
*/
|
|
129
|
+
export const BINDING_TARGET_STAMP_HELPER = `function ${NAPI_BINDING_TARGET_STAMP_FN}(exportsObject, target) {
|
|
130
|
+
if (
|
|
131
|
+
Object.prototype.hasOwnProperty.call(exportsObject, '${NAPI_BINDING_TARGET_EXPORT}')
|
|
132
|
+
) {
|
|
133
|
+
if (exportsObject.${NAPI_BINDING_TARGET_EXPORT} === target) {
|
|
134
|
+
// Already ours: the root entry aliases the object it loaded, so a WASI
|
|
135
|
+
// fallback candidate — or a \`NAPI_RS_NATIVE_LIBRARY_PATH\` override that
|
|
136
|
+
// is a generated loader — arrives already stamped with this same value.
|
|
137
|
+
return target
|
|
138
|
+
}
|
|
139
|
+
const error = new Error(
|
|
140
|
+
'\`${NAPI_BINDING_TARGET_EXPORT}\` is reserved by the generated binding loader, but the loaded binding already exports it. Rename the export, e.g. #[napi(js_name = "...")].',
|
|
141
|
+
)
|
|
142
|
+
error.code = '${ERR_NAPI_BINDING_TARGET_CONFLICT}'
|
|
143
|
+
throw error
|
|
144
|
+
}
|
|
145
|
+
if (!Object.isExtensible(exportsObject)) {
|
|
146
|
+
// A \`#[napi(module_exports)]\` hook may seal or freeze this object
|
|
147
|
+
// (\`Object::seal\` / \`Object::freeze\`). Reporting the artifact is metadata,
|
|
148
|
+
// never a reason to fail an otherwise successful load, so the stamp is
|
|
149
|
+
// skipped. What a consumer still sees then follows the entry point: the
|
|
150
|
+
// browser and deferred loaders declare \`${NAPI_BINDING_TARGET_EXPORT}\` at module
|
|
151
|
+
// level and go on reporting it, while the CommonJS entries hand back this
|
|
152
|
+
// very object as \`module.exports\`, so there the value is absent.
|
|
153
|
+
return target
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
// [[Define]], not [[Set]]: an ordinary assignment walks the prototype
|
|
157
|
+
// chain, so an inherited accessor could swallow the value or throw and
|
|
158
|
+
// fail an otherwise successful load. The descriptor is what a successful
|
|
159
|
+
// assignment would have produced.
|
|
160
|
+
Object.defineProperty(exportsObject, '${NAPI_BINDING_TARGET_EXPORT}', {
|
|
161
|
+
configurable: true,
|
|
162
|
+
enumerable: true,
|
|
163
|
+
value: target,
|
|
164
|
+
writable: true,
|
|
165
|
+
})
|
|
166
|
+
} catch {
|
|
167
|
+
// Same rule as the non-extensible skip above: reporting the artifact is
|
|
168
|
+
// metadata, never a reason to fail an otherwise successful load. An exotic
|
|
169
|
+
// object (a Proxy whose defineProperty trap refuses) is skipped, not
|
|
170
|
+
// thrown over.
|
|
171
|
+
}
|
|
172
|
+
// The CommonJS loaders assign this return value so \`cjs-module-lexer\` — and
|
|
173
|
+
// therefore Node's CJS -> ESM named export detection — can see
|
|
174
|
+
// \`${NAPI_BINDING_TARGET_EXPORT}\` statically.
|
|
175
|
+
return target
|
|
176
|
+
}`
|