@napi-rs/cli 3.10.7 → 3.10.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +235 -20
- package/dist/index.cjs +235 -20
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +235 -20
- package/package.json +2 -2
- package/src/api/__tests__/artifacts.spec.ts +1644 -1
- package/src/api/artifacts.ts +547 -21
- package/src/utils/__tests__/__snapshots__/target.spec.ts.md +7 -0
- package/src/utils/__tests__/__snapshots__/target.spec.ts.snap +0 -0
- package/src/utils/misc.ts +2 -2
- package/src/utils/target.ts +1 -0
package/src/api/artifacts.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'
|
|
1
|
+
import { mkdtemp, realpath, rm, stat, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
3
|
import {
|
|
4
4
|
dirname,
|
|
@@ -18,9 +18,11 @@ import {
|
|
|
18
18
|
} from '../def/artifacts.js'
|
|
19
19
|
import {
|
|
20
20
|
AVAILABLE_TARGETS,
|
|
21
|
+
canonicalizeManagedPackagePath,
|
|
21
22
|
commitFileSystemTransaction,
|
|
22
23
|
debugFactory,
|
|
23
24
|
fileExists,
|
|
25
|
+
managedPackagePathIsWithin,
|
|
24
26
|
parseTriple,
|
|
25
27
|
readFileAsync,
|
|
26
28
|
readNapiConfig,
|
|
@@ -233,15 +235,25 @@ async function collectArtifactsUnlocked(
|
|
|
233
235
|
throw error
|
|
234
236
|
}
|
|
235
237
|
}
|
|
238
|
+
const previouslyManagedRootEntries = await collectManagedRootEntries(
|
|
239
|
+
packageRoot,
|
|
240
|
+
binaryName,
|
|
241
|
+
wasiSources,
|
|
242
|
+
)
|
|
243
|
+
|
|
236
244
|
await addArtifactRootEntry({
|
|
237
245
|
pendingWrites,
|
|
238
246
|
packageRoot,
|
|
239
247
|
packageMain: packageJson.main,
|
|
248
|
+
packageExports: packageJson.exports,
|
|
249
|
+
publishConfig: packageJson.publishConfig,
|
|
250
|
+
previouslyManagedRootEntries,
|
|
240
251
|
binaryName,
|
|
241
252
|
targets,
|
|
242
253
|
artifactsByIdentity,
|
|
243
254
|
wasiTargets,
|
|
244
255
|
wasiSources,
|
|
256
|
+
requireArtifactSourceRootEntry: options.buildOutputDir !== undefined,
|
|
245
257
|
})
|
|
246
258
|
|
|
247
259
|
for (const target of wasiTargets) {
|
|
@@ -276,7 +288,7 @@ async function collectArtifactsUnlocked(
|
|
|
276
288
|
binaryName,
|
|
277
289
|
targets,
|
|
278
290
|
pendingWrites,
|
|
279
|
-
|
|
291
|
+
previouslyManagedRootEntries,
|
|
280
292
|
protectedSourcePaths,
|
|
281
293
|
)
|
|
282
294
|
|
|
@@ -470,7 +482,7 @@ async function addWasiRootEntry(
|
|
|
470
482
|
`WASI artifact source ${source.dir} is incomplete: missing required root entry ${rootEntry}`,
|
|
471
483
|
)
|
|
472
484
|
}
|
|
473
|
-
const destination =
|
|
485
|
+
const destination = resolvePackageRootEntry(
|
|
474
486
|
packageRoot,
|
|
475
487
|
sourcePath.relative,
|
|
476
488
|
'WASI root entry destination',
|
|
@@ -487,25 +499,283 @@ async function addArtifactRootEntry({
|
|
|
487
499
|
pendingWrites,
|
|
488
500
|
packageRoot,
|
|
489
501
|
packageMain,
|
|
502
|
+
packageExports,
|
|
503
|
+
publishConfig,
|
|
504
|
+
previouslyManagedRootEntries,
|
|
490
505
|
binaryName,
|
|
491
506
|
targets,
|
|
492
507
|
artifactsByIdentity,
|
|
493
508
|
wasiTargets,
|
|
494
509
|
wasiSources,
|
|
510
|
+
requireArtifactSourceRootEntry,
|
|
495
511
|
}: {
|
|
496
512
|
pendingWrites: Map<string, PendingWrite>
|
|
497
513
|
packageRoot: string
|
|
498
514
|
packageMain: string | undefined
|
|
515
|
+
packageExports: unknown
|
|
516
|
+
publishConfig: { main?: string; exports?: unknown } | undefined
|
|
517
|
+
previouslyManagedRootEntries: string[]
|
|
499
518
|
binaryName: string
|
|
500
519
|
targets: Target[]
|
|
501
520
|
artifactsByIdentity: Map<string, string[]>
|
|
502
521
|
wasiTargets: Target[]
|
|
503
522
|
wasiSources: Map<string, WasiArtifactSource>
|
|
523
|
+
requireArtifactSourceRootEntry: boolean
|
|
504
524
|
}) {
|
|
505
525
|
const nativeTargets = targets.filter((target) => target.platform !== 'wasi')
|
|
506
|
-
const rootCandidates = packageRootEntryCandidates(packageMain)
|
|
507
526
|
|
|
527
|
+
// The WASI loader metadata records which file was generated as the package
|
|
528
|
+
// root entry when the loaders were built (`--js` on `napi build`, e.g.
|
|
529
|
+
// `binding.js`). That name can differ from `package.json#main`, which may
|
|
530
|
+
// point at a handwritten wrapper like `index.js`; both files must survive
|
|
531
|
+
// reconciliation, so the recorded entry is a root candidate too.
|
|
532
|
+
const wasiDeclaredRootEntries = new Map<string, string | null>()
|
|
533
|
+
for (const target of wasiTargets) {
|
|
534
|
+
const source = wasiSources.get(target.platformArchABI)
|
|
535
|
+
if (!source) {
|
|
536
|
+
continue
|
|
537
|
+
}
|
|
538
|
+
const bindingPath = source.files.get(
|
|
539
|
+
`${binaryName}.${wasiLoaderSuffix(target.platformArchABI)}.cjs`,
|
|
540
|
+
)
|
|
541
|
+
if (!bindingPath) {
|
|
542
|
+
continue
|
|
543
|
+
}
|
|
544
|
+
const metadata = parseWasiArtifactMetadata(
|
|
545
|
+
await readFileAsync(bindingPath, 'utf8'),
|
|
546
|
+
bindingPath,
|
|
547
|
+
)
|
|
548
|
+
if (metadata === undefined) {
|
|
549
|
+
continue
|
|
550
|
+
}
|
|
551
|
+
wasiDeclaredRootEntries.set(
|
|
552
|
+
target.platformArchABI,
|
|
553
|
+
metadata.rootEntry === null
|
|
554
|
+
? null
|
|
555
|
+
: resolveArtifactRelativePath(
|
|
556
|
+
packageRoot,
|
|
557
|
+
metadata.rootEntry,
|
|
558
|
+
'WASI root entry',
|
|
559
|
+
).relative,
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
// All flavors must declare the same root entry: their loaders are
|
|
563
|
+
// interchangeable to consumers, and a divergence means the artifacts came
|
|
564
|
+
// from builds with different `--js` options that cannot be spliced into a
|
|
565
|
+
// coherent package.
|
|
566
|
+
const declaredRootEntryContracts = new Set(wasiDeclaredRootEntries.values())
|
|
567
|
+
if (declaredRootEntryContracts.size > 1) {
|
|
568
|
+
throw new Error(
|
|
569
|
+
`WASI loader metadata declares conflicting root entries: ${[
|
|
570
|
+
...wasiDeclaredRootEntries,
|
|
571
|
+
]
|
|
572
|
+
.map(([abi, entry]) => `${abi}: ${entry ?? 'none'}`)
|
|
573
|
+
.join(', ')}`,
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
const wasiRootEntries = new Set(
|
|
577
|
+
[...wasiDeclaredRootEntries.values()].filter(
|
|
578
|
+
(entry): entry is string => entry !== null,
|
|
579
|
+
),
|
|
580
|
+
)
|
|
581
|
+
// Resolve `main` against the effective post-reconciliation file set: the
|
|
582
|
+
// package root plus the native artifact directories, whose contents move
|
|
583
|
+
// to the root during this transaction. A clean package root with the
|
|
584
|
+
// resolved entry beside the `.node` still counts as shared, so the WASI
|
|
585
|
+
// loader copy never occupies the package entry path.
|
|
586
|
+
const nativeArtifactDirs = [
|
|
587
|
+
...new Set(
|
|
588
|
+
nativeTargets
|
|
589
|
+
.map(
|
|
590
|
+
(target) =>
|
|
591
|
+
artifactsByIdentity.get(artifactName(binaryName, target))?.[0],
|
|
592
|
+
)
|
|
593
|
+
.filter((path): path is string => path !== undefined)
|
|
594
|
+
.map(dirname),
|
|
595
|
+
),
|
|
596
|
+
]
|
|
597
|
+
// Previously managed root entries the new loader contract no longer
|
|
598
|
+
// declares are deleted by this transaction, so they cannot satisfy Node's
|
|
599
|
+
// `main` lookup: an old `dist.js` must not shadow a declared
|
|
600
|
+
// `dist/index.js` that outlives it.
|
|
601
|
+
const normalizeRootEntry = (entry: string) => {
|
|
602
|
+
try {
|
|
603
|
+
return resolveArtifactRelativePath(packageRoot, entry, 'root entry')
|
|
604
|
+
.relative
|
|
605
|
+
} catch {
|
|
606
|
+
return null
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const dyingManagedPaths = new Set(
|
|
610
|
+
previouslyManagedRootEntries
|
|
611
|
+
.map(normalizeRootEntry)
|
|
612
|
+
.filter(
|
|
613
|
+
(entry): entry is string =>
|
|
614
|
+
entry !== null && !wasiRootEntries.has(entry),
|
|
615
|
+
),
|
|
616
|
+
)
|
|
617
|
+
const sharedRootEntryPaths = new Set<string>()
|
|
618
|
+
for (const main of [packageMain, publishConfig?.main]) {
|
|
619
|
+
if (typeof main !== 'string') {
|
|
620
|
+
continue
|
|
621
|
+
}
|
|
622
|
+
for (const path of await packageMainResolutionPaths(
|
|
623
|
+
packageRoot,
|
|
624
|
+
main,
|
|
625
|
+
nativeArtifactDirs,
|
|
626
|
+
dyingManagedPaths,
|
|
627
|
+
)) {
|
|
628
|
+
sharedRootEntryPaths.add(path)
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (packageMain === undefined) {
|
|
632
|
+
// With no `main` field at all, Node's implicit default is `index.js`.
|
|
633
|
+
sharedRootEntryPaths.add('index.js')
|
|
634
|
+
}
|
|
635
|
+
// Consumers resolve `exports["."]` ahead of `main`, and publishers may
|
|
636
|
+
// swap either field through `publishConfig`, so every declared entry
|
|
637
|
+
// target counts as shared with the native root entry — including
|
|
638
|
+
// auxiliary conditions like `types`, whose targets still name the
|
|
639
|
+
// published declaration file.
|
|
640
|
+
const auxiliaryEntryPaths = new Set<string>()
|
|
641
|
+
for (const field of [packageExports, publishConfig?.exports]) {
|
|
642
|
+
const { runtime, auxiliary } = packageExportTargets(field)
|
|
643
|
+
for (const target of [...runtime, ...auxiliary]) {
|
|
644
|
+
const normalized = normalizeRootEntry(target)
|
|
645
|
+
if (normalized !== null) {
|
|
646
|
+
sharedRootEntryPaths.add(normalized)
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
for (const target of auxiliary) {
|
|
650
|
+
const normalized = normalizeRootEntry(target)
|
|
651
|
+
if (normalized !== null) {
|
|
652
|
+
auxiliaryEntryPaths.add(normalized)
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
// Native root-entry scan candidates are a narrower set than the shared
|
|
657
|
+
// paths: auxiliary export targets like `types` name the published file
|
|
658
|
+
// but cannot satisfy a runtime `require`, so they must not consume the
|
|
659
|
+
// single root copy. Declared shared entries come first because the
|
|
660
|
+
// metadata's own rootEntry name is the path consumers resolve, and they
|
|
661
|
+
// are admitted regardless of extension since `--js` accepts arbitrary
|
|
662
|
+
// filenames.
|
|
663
|
+
const rootCandidates = [
|
|
664
|
+
...new Set([
|
|
665
|
+
...[...wasiRootEntries].filter((entry) =>
|
|
666
|
+
sharedRootEntryPaths.has(entry),
|
|
667
|
+
),
|
|
668
|
+
...[...sharedRootEntryPaths].filter(
|
|
669
|
+
(path) => !auxiliaryEntryPaths.has(path),
|
|
670
|
+
),
|
|
671
|
+
...packageRootEntryCandidates(packageMain),
|
|
672
|
+
...(typeof publishConfig?.main === 'string'
|
|
673
|
+
? packageRootEntryCandidates(publishConfig.main)
|
|
674
|
+
: []),
|
|
675
|
+
]),
|
|
676
|
+
]
|
|
677
|
+
|
|
678
|
+
// The root loader below is taken from the threaded WASI target when both
|
|
679
|
+
// flavors are configured, so a distinct root entry must come from that
|
|
680
|
+
// target's source first: the generated entry embeds flavor-specific
|
|
681
|
+
// loader references, and a threadless copy next to the threaded root
|
|
682
|
+
// loader publishes a mismatched pair.
|
|
683
|
+
const rootWasiTarget =
|
|
684
|
+
wasiTargets.find((target) => wasiTargetHasThreads(target)) ?? wasiTargets[0]
|
|
685
|
+
const orderedWasiTargets = [
|
|
686
|
+
...(rootWasiTarget ? [rootWasiTarget] : []),
|
|
687
|
+
...wasiTargets.filter((target) => target !== rootWasiTarget),
|
|
688
|
+
].filter(
|
|
689
|
+
(target, index, list) =>
|
|
690
|
+
list.findIndex(
|
|
691
|
+
(other) => other.platformArchABI === target.platformArchABI,
|
|
692
|
+
) === index,
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
// A metadata-declared root entry whose name is not a package `main`
|
|
696
|
+
// resolution (e.g. `binding.js` next to a handwritten `index.js`) is a
|
|
697
|
+
// file `napi build` generated alongside the loader; reconciliation must
|
|
698
|
+
// keep it. It is resolved before the native scan so a stale copy beside
|
|
699
|
+
// the `.node` artifacts cannot claim the destination, preferring in
|
|
700
|
+
// order: the WASI artifact source, native artifact dirs, then the
|
|
701
|
+
// existing package-root file below. Entries that share a `main` path skip
|
|
702
|
+
// this path entirely: their WASI-source copy is the WASI loader
|
|
703
|
+
// (`*.wasi.cjs` chain), which must never replace the native root loader.
|
|
704
|
+
// Destinations are compared after normalization since `main` values like
|
|
705
|
+
// `./binding.js` and metadata entries like `binding.js` alias the same
|
|
706
|
+
// package-root path.
|
|
707
|
+
const distinctWasiRootEntries = [...wasiRootEntries].filter(
|
|
708
|
+
(entry) => !sharedRootEntryPaths.has(entry),
|
|
709
|
+
)
|
|
710
|
+
const wasiSourceResolvedEntries = new Set<string>()
|
|
711
|
+
if (nativeTargets.length > 0) {
|
|
712
|
+
for (const entry of distinctWasiRootEntries) {
|
|
713
|
+
const destination = resolvePackageRootEntry(
|
|
714
|
+
packageRoot,
|
|
715
|
+
entry,
|
|
716
|
+
'WASI root entry destination',
|
|
717
|
+
).absolute
|
|
718
|
+
let resolved = false
|
|
719
|
+
for (const target of orderedWasiTargets) {
|
|
720
|
+
if (wasiDeclaredRootEntries.get(target.platformArchABI) !== entry) {
|
|
721
|
+
continue
|
|
722
|
+
}
|
|
723
|
+
const source = wasiSources.get(target.platformArchABI)
|
|
724
|
+
if (!source) {
|
|
725
|
+
continue
|
|
726
|
+
}
|
|
727
|
+
const sourcePath = resolveArtifactRelativePath(
|
|
728
|
+
source.dir,
|
|
729
|
+
entry,
|
|
730
|
+
'WASI root entry',
|
|
731
|
+
)
|
|
732
|
+
if (!(await fileExists(sourcePath.absolute))) {
|
|
733
|
+
continue
|
|
734
|
+
}
|
|
735
|
+
addPendingWrite(
|
|
736
|
+
pendingWrites,
|
|
737
|
+
destination,
|
|
738
|
+
sourcePath.absolute,
|
|
739
|
+
await readFileAsync(sourcePath.absolute),
|
|
740
|
+
)
|
|
741
|
+
wasiSourceResolvedEntries.add(entry)
|
|
742
|
+
resolved = true
|
|
743
|
+
break
|
|
744
|
+
}
|
|
745
|
+
if (resolved) {
|
|
746
|
+
continue
|
|
747
|
+
}
|
|
748
|
+
for (const target of nativeTargets) {
|
|
749
|
+
const artifactPath = artifactsByIdentity.get(
|
|
750
|
+
artifactName(binaryName, target),
|
|
751
|
+
)?.[0]
|
|
752
|
+
if (!artifactPath) {
|
|
753
|
+
continue
|
|
754
|
+
}
|
|
755
|
+
const sourcePath = resolveArtifactRelativePath(
|
|
756
|
+
dirname(artifactPath),
|
|
757
|
+
entry,
|
|
758
|
+
'native-adjacent WASI root entry',
|
|
759
|
+
)
|
|
760
|
+
if (!(await fileExists(sourcePath.absolute))) {
|
|
761
|
+
continue
|
|
762
|
+
}
|
|
763
|
+
addPendingWrite(
|
|
764
|
+
pendingWrites,
|
|
765
|
+
destination,
|
|
766
|
+
sourcePath.absolute,
|
|
767
|
+
await readFileAsync(sourcePath.absolute),
|
|
768
|
+
)
|
|
769
|
+
break
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
let copiedNativeRoot = false
|
|
508
775
|
for (const target of nativeTargets) {
|
|
776
|
+
if (copiedNativeRoot) {
|
|
777
|
+
break
|
|
778
|
+
}
|
|
509
779
|
const artifactPath = artifactsByIdentity.get(
|
|
510
780
|
artifactName(binaryName, target),
|
|
511
781
|
)?.[0]
|
|
@@ -519,32 +789,45 @@ async function addArtifactRootEntry({
|
|
|
519
789
|
candidate,
|
|
520
790
|
'native root entry',
|
|
521
791
|
)
|
|
522
|
-
if (!(await
|
|
792
|
+
if (!(await regularFileExists(source.absolute))) {
|
|
523
793
|
continue
|
|
524
794
|
}
|
|
525
|
-
const destination =
|
|
795
|
+
const destination = resolvePackageRootEntry(
|
|
526
796
|
packageRoot,
|
|
527
|
-
|
|
797
|
+
candidate,
|
|
528
798
|
'native root entry destination',
|
|
529
799
|
)
|
|
800
|
+
if (pendingWrites.has(destination.absolute)) {
|
|
801
|
+
continue
|
|
802
|
+
}
|
|
530
803
|
addPendingWrite(
|
|
531
804
|
pendingWrites,
|
|
532
805
|
destination.absolute,
|
|
533
806
|
source.absolute,
|
|
534
807
|
await readFileAsync(source.absolute),
|
|
535
808
|
)
|
|
536
|
-
|
|
809
|
+
copiedNativeRoot = true
|
|
810
|
+
break
|
|
537
811
|
}
|
|
538
812
|
}
|
|
539
813
|
|
|
540
814
|
if (nativeTargets.length > 0) {
|
|
541
|
-
|
|
542
|
-
|
|
815
|
+
// Every root entry candidate that already exists in the package stays
|
|
816
|
+
// managed: keeping it out of pendingWrites would mark it as a stale
|
|
817
|
+
// managed destination and delete it. This must run even when a root entry
|
|
818
|
+
// was copied from an artifact dir above, since a WASI-only candidate
|
|
819
|
+
// such as `binding.js` never sits next to the `.node` artifacts.
|
|
820
|
+
let keptExisting = false
|
|
821
|
+
for (const candidate of [...rootCandidates, ...wasiRootEntries]) {
|
|
822
|
+
const existing = resolvePackageRootEntry(
|
|
543
823
|
packageRoot,
|
|
544
824
|
candidate,
|
|
545
825
|
'existing native root entry',
|
|
546
826
|
)
|
|
547
|
-
if (
|
|
827
|
+
if (
|
|
828
|
+
pendingWrites.has(existing.absolute) ||
|
|
829
|
+
!(await regularFileExists(existing.absolute))
|
|
830
|
+
) {
|
|
548
831
|
continue
|
|
549
832
|
}
|
|
550
833
|
addPendingWrite(
|
|
@@ -553,19 +836,54 @@ async function addArtifactRootEntry({
|
|
|
553
836
|
existing.absolute,
|
|
554
837
|
await readFileAsync(existing.absolute),
|
|
555
838
|
)
|
|
839
|
+
keptExisting = true
|
|
840
|
+
}
|
|
841
|
+
// A declared root entry that no artifact source or existing file
|
|
842
|
+
// resolved would publish a package whose entry point does not exist.
|
|
843
|
+
// Fail loudly like addWasiRootEntry instead of silently dropping it.
|
|
844
|
+
for (const entry of wasiRootEntries) {
|
|
845
|
+
const destination = resolvePackageRootEntry(
|
|
846
|
+
packageRoot,
|
|
847
|
+
entry,
|
|
848
|
+
'WASI root entry destination',
|
|
849
|
+
).absolute
|
|
850
|
+
// With an explicit --build-output-dir the WASI output is authoritative:
|
|
851
|
+
// a distinct declared entry it does not contain cannot be satisfied by
|
|
852
|
+
// a possibly stale package-root or native-adjacent copy, the same way
|
|
853
|
+
// the WASI-only path already rejects an incomplete source.
|
|
854
|
+
if (
|
|
855
|
+
requireArtifactSourceRootEntry &&
|
|
856
|
+
distinctWasiRootEntries.includes(entry) &&
|
|
857
|
+
!wasiSourceResolvedEntries.has(entry)
|
|
858
|
+
) {
|
|
859
|
+
throw new Error(
|
|
860
|
+
`WASI loader metadata declares root entry ${entry}, but the configured build output directory does not contain it`,
|
|
861
|
+
)
|
|
862
|
+
}
|
|
863
|
+
// Only distinct entries must already be pending. Entries shared with
|
|
864
|
+
// the package entry may legitimately be absent, in which case the
|
|
865
|
+
// WASI root fallback below publishes the loader copy.
|
|
866
|
+
if (
|
|
867
|
+
distinctWasiRootEntries.includes(entry) &&
|
|
868
|
+
!pendingWrites.has(destination)
|
|
869
|
+
) {
|
|
870
|
+
throw new Error(
|
|
871
|
+
`WASI loader metadata declares root entry ${entry}, but it was found in neither the artifact sources nor the package root`,
|
|
872
|
+
)
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (copiedNativeRoot || keptExisting) {
|
|
556
876
|
return
|
|
557
877
|
}
|
|
558
878
|
}
|
|
559
879
|
|
|
560
|
-
|
|
561
|
-
wasiTargets.find((target) => wasiTargetHasThreads(target)) ?? wasiTargets[0]
|
|
562
|
-
if (rootTarget) {
|
|
880
|
+
if (rootWasiTarget) {
|
|
563
881
|
await addWasiRootEntry(
|
|
564
882
|
pendingWrites,
|
|
565
883
|
packageRoot,
|
|
566
884
|
binaryName,
|
|
567
|
-
|
|
568
|
-
wasiSources.get(
|
|
885
|
+
rootWasiTarget,
|
|
886
|
+
wasiSources.get(rootWasiTarget.platformArchABI)!,
|
|
569
887
|
packageMain,
|
|
570
888
|
)
|
|
571
889
|
}
|
|
@@ -578,6 +896,185 @@ function packageRootEntryCandidates(packageMain: string | undefined) {
|
|
|
578
896
|
]
|
|
579
897
|
}
|
|
580
898
|
|
|
899
|
+
async function regularFileExists(path: string) {
|
|
900
|
+
try {
|
|
901
|
+
return (await stat(path)).isFile()
|
|
902
|
+
} catch {
|
|
903
|
+
return false
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Package-root-relative paths a `main` value resolves to as the native root
|
|
908
|
+
// entry, following Node's CommonJS lookup order: the specifier itself, the
|
|
909
|
+
// LOAD_AS_FILE extension variants, then LOAD_AS_DIRECTORY — a nested
|
|
910
|
+
// `package.json#main` or `index.*` inside the directory. Existence is
|
|
911
|
+
// probed in the package root and in `additionalDirs` (the native artifact
|
|
912
|
+
// directories), since artifact files move to the package root in the same
|
|
913
|
+
// transaction. Only the specifier itself is always shared: ESM importers
|
|
914
|
+
// resolve `main` literally, so it names the package entry even when no
|
|
915
|
+
// file exists. Every other candidate is shared only when it is the first
|
|
916
|
+
// existing file in Node's order; an earlier match shadows the later
|
|
917
|
+
// spellings, so they must not suppress a WASI-declared root entry of the
|
|
918
|
+
// same name. With no `main`, the implicit default is `index.js`.
|
|
919
|
+
async function packageMainResolutionPaths(
|
|
920
|
+
packageRoot: string,
|
|
921
|
+
packageMain: string | undefined,
|
|
922
|
+
additionalDirs: string[] = [],
|
|
923
|
+
excludedPaths: ReadonlySet<string> = new Set(),
|
|
924
|
+
) {
|
|
925
|
+
const paths = new Set<string>()
|
|
926
|
+
const normalizeCandidate = (entry: string) => {
|
|
927
|
+
try {
|
|
928
|
+
return resolveArtifactRelativePath(packageRoot, entry, 'package main')
|
|
929
|
+
.relative
|
|
930
|
+
} catch {
|
|
931
|
+
// A `main` that escapes the package root cannot alias a managed root
|
|
932
|
+
// entry, so it contributes no shared names.
|
|
933
|
+
return null
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const enqueue = (entry: string) => {
|
|
937
|
+
const normalized = normalizeCandidate(entry)
|
|
938
|
+
if (normalized !== null) {
|
|
939
|
+
paths.add(normalized)
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const candidateExists = async (entry: string) => {
|
|
943
|
+
// A package-root file scheduled for deletion cannot satisfy the
|
|
944
|
+
// lookup, and a pending write is the only root change this transaction
|
|
945
|
+
// introduces; artifact directories publish their files unchanged.
|
|
946
|
+
const normalized = normalizeCandidate(entry)
|
|
947
|
+
if (normalized === null || excludedPaths.has(normalized)) {
|
|
948
|
+
return false
|
|
949
|
+
}
|
|
950
|
+
for (const dir of [packageRoot, ...additionalDirs]) {
|
|
951
|
+
if (await regularFileExists(join(dir, entry))) {
|
|
952
|
+
return true
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return false
|
|
956
|
+
}
|
|
957
|
+
if (packageMain === undefined) {
|
|
958
|
+
enqueue('index.js')
|
|
959
|
+
return paths
|
|
960
|
+
}
|
|
961
|
+
// LOAD_AS_FILE: the specifier itself, then the extension variants.
|
|
962
|
+
const visitAsFile = async (specifier: string): Promise<boolean> => {
|
|
963
|
+
for (const candidate of [
|
|
964
|
+
specifier,
|
|
965
|
+
`${specifier}.js`,
|
|
966
|
+
`${specifier}.json`,
|
|
967
|
+
`${specifier}.node`,
|
|
968
|
+
]) {
|
|
969
|
+
if (await candidateExists(candidate)) {
|
|
970
|
+
enqueue(candidate)
|
|
971
|
+
return true
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return false
|
|
975
|
+
}
|
|
976
|
+
const visitAsIndex = async (directory: string): Promise<boolean> => {
|
|
977
|
+
for (const extension of ['js', 'json', 'node']) {
|
|
978
|
+
const index = join(directory, `index.${extension}`)
|
|
979
|
+
if (await candidateExists(index)) {
|
|
980
|
+
enqueue(index)
|
|
981
|
+
return true
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return false
|
|
985
|
+
}
|
|
986
|
+
const visit = async (specifier: string): Promise<boolean> => {
|
|
987
|
+
// The exact specifier is shared even when it does not exist: ESM
|
|
988
|
+
// importers resolve `main` literally, so it still names the package
|
|
989
|
+
// entry.
|
|
990
|
+
enqueue(specifier)
|
|
991
|
+
if (await candidateExists(specifier)) {
|
|
992
|
+
return true
|
|
993
|
+
}
|
|
994
|
+
for (const extension of ['.js', '.json', '.node']) {
|
|
995
|
+
const candidate = `${specifier}${extension}`
|
|
996
|
+
if (await candidateExists(candidate)) {
|
|
997
|
+
enqueue(candidate)
|
|
998
|
+
return true
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
// LOAD_AS_DIRECTORY: a nested package.json main is tried as a file and
|
|
1002
|
+
// as an index-bearing directory once — Node does not recurse into
|
|
1003
|
+
// further manifests — then the specifier's own index.* wins.
|
|
1004
|
+
const manifest = join(specifier, 'package.json')
|
|
1005
|
+
if (await regularFileExists(join(packageRoot, manifest))) {
|
|
1006
|
+
let nestedMain: unknown
|
|
1007
|
+
try {
|
|
1008
|
+
nestedMain = JSON.parse(
|
|
1009
|
+
await readFileAsync(join(packageRoot, manifest), 'utf8'),
|
|
1010
|
+
)?.main
|
|
1011
|
+
} catch {
|
|
1012
|
+
nestedMain = undefined
|
|
1013
|
+
}
|
|
1014
|
+
if (typeof nestedMain === 'string' && nestedMain.length > 0) {
|
|
1015
|
+
const nested = join(specifier, nestedMain)
|
|
1016
|
+
if ((await visitAsFile(nested)) || (await visitAsIndex(nested))) {
|
|
1017
|
+
return true
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return visitAsIndex(specifier)
|
|
1022
|
+
}
|
|
1023
|
+
// When the configured `main` fails to resolve entirely, Node falls back
|
|
1024
|
+
// to `index.*` at the package root.
|
|
1025
|
+
if (!(await visit(packageMain))) {
|
|
1026
|
+
await visitAsIndex('')
|
|
1027
|
+
}
|
|
1028
|
+
return paths
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Literal file targets under `exports["."]`, split into runtime targets
|
|
1032
|
+
// that `require`/`import` can load and auxiliary targets guarded only by
|
|
1033
|
+
// non-runtime conditions such as `types`. The top level is a subpath map
|
|
1034
|
+
// only when a key starts with `.`; a condition-only map like
|
|
1035
|
+
// `{ node: ..., default: ... }` or a fallback array describes the root
|
|
1036
|
+
// entry directly. Nested conditions and arrays are flattened; anything
|
|
1037
|
+
// that is not a string contributes nothing.
|
|
1038
|
+
function packageExportTargets(exportsField: unknown): {
|
|
1039
|
+
runtime: string[]
|
|
1040
|
+
auxiliary: string[]
|
|
1041
|
+
} {
|
|
1042
|
+
const runtime = new Set<string>()
|
|
1043
|
+
const auxiliary = new Set<string>()
|
|
1044
|
+
const collect = (value: unknown, isAuxiliary: boolean) => {
|
|
1045
|
+
if (typeof value === 'string') {
|
|
1046
|
+
;(isAuxiliary ? auxiliary : runtime).add(value)
|
|
1047
|
+
return
|
|
1048
|
+
}
|
|
1049
|
+
if (Array.isArray(value)) {
|
|
1050
|
+
for (const item of value) {
|
|
1051
|
+
collect(item, isAuxiliary)
|
|
1052
|
+
}
|
|
1053
|
+
return
|
|
1054
|
+
}
|
|
1055
|
+
if (value && typeof value === 'object') {
|
|
1056
|
+
for (const [key, item] of Object.entries(
|
|
1057
|
+
value as Record<string, unknown>,
|
|
1058
|
+
)) {
|
|
1059
|
+
collect(item, isAuxiliary || key === 'types')
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (
|
|
1064
|
+
exportsField &&
|
|
1065
|
+
typeof exportsField === 'object' &&
|
|
1066
|
+
!Array.isArray(exportsField) &&
|
|
1067
|
+
Object.keys(exportsField as Record<string, unknown>).some((key) =>
|
|
1068
|
+
key.startsWith('.'),
|
|
1069
|
+
)
|
|
1070
|
+
) {
|
|
1071
|
+
collect((exportsField as Record<string, unknown>)['.'], false)
|
|
1072
|
+
} else {
|
|
1073
|
+
collect(exportsField, false)
|
|
1074
|
+
}
|
|
1075
|
+
return { runtime: [...runtime], auxiliary: [...auxiliary] }
|
|
1076
|
+
}
|
|
1077
|
+
|
|
581
1078
|
function parseWasiArtifactMetadata(content: string, source: string) {
|
|
582
1079
|
const firstLine = content.split(/\r?\n/, 1)[0]
|
|
583
1080
|
if (!firstLine.startsWith(WASI_ARTIFACT_METADATA_PREFIX)) {
|
|
@@ -671,6 +1168,27 @@ function resolveArtifactRelativePath(
|
|
|
671
1168
|
return { absolute, relative: relativePath }
|
|
672
1169
|
}
|
|
673
1170
|
|
|
1171
|
+
// Root entry names come from `package.json#main` or WASI loader metadata and
|
|
1172
|
+
// may be nested (e.g. `nested/binding.js`). The lexical check in
|
|
1173
|
+
// resolveArtifactRelativePath cannot see symlinked intermediate directories,
|
|
1174
|
+
// and the file-system transaction only guards the workspace boundary, so a
|
|
1175
|
+
// symlinked parent could redirect a pending write into a sibling package.
|
|
1176
|
+
// Canonicalizing the destination's nearest existing ancestor closes that gap.
|
|
1177
|
+
function resolvePackageRootEntry(
|
|
1178
|
+
packageRoot: string,
|
|
1179
|
+
entry: string,
|
|
1180
|
+
description: string,
|
|
1181
|
+
) {
|
|
1182
|
+
const resolved = resolveArtifactRelativePath(packageRoot, entry, description)
|
|
1183
|
+
const canonical = canonicalizeManagedPackagePath(resolved.absolute)
|
|
1184
|
+
if (!managedPackagePathIsWithin(packageRoot, canonical)) {
|
|
1185
|
+
throw new Error(
|
|
1186
|
+
`${description} resolves outside the package root through a symlinked path: ${entry}`,
|
|
1187
|
+
)
|
|
1188
|
+
}
|
|
1189
|
+
return resolved
|
|
1190
|
+
}
|
|
1191
|
+
|
|
674
1192
|
function addPendingWrite(
|
|
675
1193
|
writes: Map<string, PendingWrite>,
|
|
676
1194
|
destination: string,
|
|
@@ -801,11 +1319,19 @@ async function collectStaleManagedDestinations(
|
|
|
801
1319
|
}
|
|
802
1320
|
|
|
803
1321
|
for (const entry of managedRootEntries) {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
entry
|
|
807
|
-
|
|
808
|
-
|
|
1322
|
+
let path: string
|
|
1323
|
+
try {
|
|
1324
|
+
// A stale entry recorded when a different loader was installed can
|
|
1325
|
+
// now resolve through a symlinked directory into a sibling package;
|
|
1326
|
+
// reconciliation must not delete files the package does not own.
|
|
1327
|
+
path = resolvePackageRootEntry(
|
|
1328
|
+
packageRoot,
|
|
1329
|
+
entry,
|
|
1330
|
+
'managed WASI root entry',
|
|
1331
|
+
).absolute
|
|
1332
|
+
} catch {
|
|
1333
|
+
continue
|
|
1334
|
+
}
|
|
809
1335
|
if (!pendingWrites.has(path)) {
|
|
810
1336
|
stalePaths.push(path)
|
|
811
1337
|
}
|
|
@@ -51,6 +51,13 @@ Generated by [AVA](https://avajs.dev).
|
|
|
51
51
|
platformArchABI: 'win32-arm64-msvc',
|
|
52
52
|
triple: 'aarch64-pc-windows-msvc',
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
abi: null,
|
|
56
|
+
arch: 'arm64',
|
|
57
|
+
platform: 'freebsd',
|
|
58
|
+
platformArchABI: 'freebsd-arm64',
|
|
59
|
+
triple: 'aarch64-unknown-freebsd',
|
|
60
|
+
},
|
|
54
61
|
{
|
|
55
62
|
abi: null,
|
|
56
63
|
arch: 'x64',
|