@teambit/pnpm 1.0.1322 → 1.0.1323

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.
@@ -1,5 +1,5 @@
1
1
  import { type ProjectManifest } from '@pnpm/types';
2
- import { type LockfileFileProjectSnapshot } from '@pnpm/lockfile.types';
2
+ import { type LockfileFile, type LockfileFileProjectSnapshot } from '@pnpm/lockfile.types';
3
3
  import { DependenciesGraph, type PackageAttributes, type DependencyEdge } from '@teambit/objects';
4
4
  import { type CalcDepsGraphOptions, type CalcDepsGraphForComponentOptions, type ComponentIdByPkgName } from '@teambit/dependency-resolver';
5
5
  import { type BitLockfileFile } from './lynx';
@@ -17,6 +17,8 @@ type ResolveFunction = (wantedDependency: {
17
17
  preferredVersions?: Record<string, unknown>;
18
18
  }) => Promise<{
19
19
  resolution?: Record<string, unknown>;
20
+ /** The resolved package's manifest; a registry package's carries its tarball's integrity in `dist`. */
21
+ manifest?: unknown;
20
22
  }>;
21
23
  export declare function init(): Promise<void>;
22
24
  export interface LockfileToGraphCache {
@@ -45,9 +47,26 @@ export declare function createFileVersionReplacer(componentIdByPkgName: Componen
45
47
  * reaches, without crossing the N-API boundary or cloning the full lockfile.
46
48
  */
47
49
  export declare function filterLockfileByImporters(lockfile: BitLockfileFile, importerIds: string[], importersOverride?: Record<string, LockfileFileProjectSnapshot>): BitLockfileFile;
48
- export declare function convertGraphToLockfile(_graph: DependenciesGraph, { manifests, rootDir, resolve, }: {
50
+ export declare function convertGraphToLockfile(_graph: DependenciesGraph, { manifests, rootDir, resolve, onComponentPkgDropped, }: {
49
51
  manifests: Record<string, ProjectManifest>;
50
52
  rootDir: string;
51
53
  resolve: ResolveFunction;
54
+ /** Called for each workspace-component package the lockfile leaves out, with why. */
55
+ onComponentPkgDropped?: (pkgId: string, reason: string) => void;
52
56
  }): Promise<BitLockfileFile>;
57
+ /**
58
+ * The dep path a lockfile ref under `name` points at, or `undefined` for a
59
+ * `link:` ref, which points at no snapshot. An aliased ref
60
+ * (`real-name@1.0.0` under the alias) names the real package, and a `file:`
61
+ * ref a workspace component's own snapshot.
62
+ */
63
+ export declare function refToDepPath(ref: string, name: string): string | undefined;
64
+ /**
65
+ * Set each snapshot's `optional` flag the way pnpm does: on when every path
66
+ * from an importer to it goes through an `optionalDependencies` edge, off
67
+ * otherwise. The graph does not record the flag, and an install that takes
68
+ * the lockfile as it is relies on it: a package reached only optionally may
69
+ * fail to build or to install, one reached otherwise may not.
70
+ */
71
+ export declare function markOptionalOnlyEntries(lockfile: Pick<LockfileFile, 'importers' | 'snapshots'>): void;
53
72
  export {};
@@ -9,6 +9,8 @@ exports.createFileVersionReplacer = createFileVersionReplacer;
9
9
  exports.createLockfileToGraphCache = createLockfileToGraphCache;
10
10
  exports.filterLockfileByImporters = filterLockfileByImporters;
11
11
  exports.init = init;
12
+ exports.markOptionalOnlyEntries = markOptionalOnlyEntries;
13
+ exports.refToDepPath = refToDepPath;
12
14
  function _path() {
13
15
  const data = _interopRequireDefault(require("path"));
14
16
  _path = function () {
@@ -23,13 +25,6 @@ function _lodash() {
23
25
  };
24
26
  return data;
25
27
  }
26
- function _semver() {
27
- const data = _interopRequireDefault(require("semver"));
28
- _semver = function () {
29
- return data;
30
- };
31
- return data;
32
- }
33
28
  function _bitError() {
34
29
  const data = require("@teambit/bit-error");
35
30
  _bitError = function () {
@@ -44,6 +39,13 @@ function _componentPackageVersion() {
44
39
  };
45
40
  return data;
46
41
  }
42
+ function _semver() {
43
+ const data = _interopRequireDefault(require("semver"));
44
+ _semver = function () {
45
+ return data;
46
+ };
47
+ return data;
48
+ }
47
49
  function _objects() {
48
50
  const data = require("@teambit/objects");
49
51
  _objects = function () {
@@ -70,6 +72,23 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
70
72
  * `resolveDependency`). Only the parts this module consumes are typed.
71
73
  */
72
74
 
75
+ /**
76
+ * The integrity a resolution carries: in `resolution` when the resolver
77
+ * reports one, else in the resolved manifest's `dist`, which is where
78
+ * `@pnpm/napi`'s `resolveDependency` reports a registry package's.
79
+ */
80
+ function resolvedIntegrity(resolveResult) {
81
+ const {
82
+ resolution,
83
+ manifest
84
+ } = resolveResult;
85
+ if (resolution != null && typeof resolution.integrity === 'string' && resolution.integrity) {
86
+ return resolution.integrity;
87
+ }
88
+ const dist = manifest?.dist;
89
+ return typeof dist?.integrity === 'string' && dist.integrity ? dist.integrity : undefined;
90
+ }
91
+
73
92
  // @pnpm/deps.path is ESM-only; load it through a .cjs shim so the require()
74
93
  // chain in the build capsule's mocha runner doesn't trip on the transitive ESM
75
94
  // import. Call `init()` once before invoking the public converters; helpers
@@ -563,7 +582,8 @@ function filterLockfileByImporters(lockfile, importerIds, importersOverride) {
563
582
  async function convertGraphToLockfile(_graph, {
564
583
  manifests,
565
584
  rootDir,
566
- resolve
585
+ resolve,
586
+ onComponentPkgDropped
567
587
  }) {
568
588
  await init();
569
589
  const graphString = _graph.serialize();
@@ -644,6 +664,10 @@ async function convertGraphToLockfile(_graph, {
644
664
  }
645
665
  const rootEdge = graph.findRootEdge();
646
666
  if (rootEdge) {
667
+ const workspaceVersions = new Map(Array.from(workspaceComponentDirs(manifests, rootDir).keys()).map(pkgId => {
668
+ const parsed = dp.parse(pkgId);
669
+ return [parsed.name, parsed.version];
670
+ }));
647
671
  for (const [projectDir, manifest] of Object.entries(manifests)) {
648
672
  const projectId = getLockfileImporterId(rootDir, projectDir);
649
673
  lockfile.importers[projectId] = {
@@ -660,7 +684,7 @@ async function convertGraphToLockfile(_graph, {
660
684
  specifier
661
685
  };
662
686
  } else {
663
- const edgeId = rootEdge.neighbours.find(directDep => directDep.name === name && (directDep.specifier === specifier || dp.removeSuffix(directDep.id) === `${name}@${specifier}`))?.id;
687
+ const edgeId = findImporterDepPath(graph, rootEdge, name, specifier, workspaceVersions);
664
688
  if (edgeId) {
665
689
  const parsed = dp.parse(edgeId);
666
690
  const ref = depPathToRef(parsed);
@@ -675,6 +699,10 @@ async function convertGraphToLockfile(_graph, {
675
699
  }
676
700
  }
677
701
  dedupeSnapshotDepsToRootManifest(lockfile, manifests[rootDir]);
702
+ linkWorkspaceComponents(lockfile, manifests, rootDir);
703
+ // Only what the importers reach is worth resolving: a graph holds entries
704
+ // no manifest here declares (an attached extension's dev dependencies, for
705
+ // one), and a registry lookup for them would be wasted or could fail.
678
706
  dropUnreachableLockfileEntries(lockfile);
679
707
  const pkgsToResolve = getPkgsToResolve(lockfile, manifests);
680
708
  const failedWorkspaceComponentPkgs = new Set();
@@ -701,16 +729,15 @@ async function convertGraphToLockfile(_graph, {
701
729
  // auth failures) so real install failures aren't silently masked.
702
730
  if (err.code === 'ERR_PNPM_NO_MATCHING_VERSION' && graph.packages.get(pkgToResolve.pkgId)?.component != null) {
703
731
  failedWorkspaceComponentPkgs.add(pkgToResolve.pkgId);
732
+ onComponentPkgDropped?.(pkgToResolve.pkgId, `the registry has no such version: ${err.message}`);
704
733
  return;
705
734
  }
706
735
  throw err;
707
736
  }
708
- const {
709
- resolution
710
- } = resolveResult;
711
- if (resolution != null && 'integrity' in resolution && resolution.integrity) {
737
+ const integrity = resolvedIntegrity(resolveResult);
738
+ if (integrity) {
712
739
  lockfile.packages[pkgToResolve.pkgId].resolution = {
713
- integrity: resolution.integrity
740
+ integrity
714
741
  };
715
742
  }
716
743
  }
@@ -731,6 +758,9 @@ async function convertGraphToLockfile(_graph, {
731
758
  // validation below — that path means the graph itself is genuinely broken.
732
759
  for (const [pkgId, pkg] of Object.entries(lockfile.packages)) {
733
760
  if ((pkg.resolution == null || Object.keys(pkg.resolution).length === 0) && graph.packages.get(pkgId)?.component != null) {
761
+ if (!failedWorkspaceComponentPkgs.has(pkgId)) {
762
+ onComponentPkgDropped?.(pkgId, 'its resolution has no integrity');
763
+ }
734
764
  failedWorkspaceComponentPkgs.add(pkgId);
735
765
  }
736
766
  }
@@ -738,6 +768,7 @@ async function convertGraphToLockfile(_graph, {
738
768
  scrubPkgsFromLockfile(lockfile, failedWorkspaceComponentPkgs);
739
769
  }
740
770
  dropUnreachableLockfileEntries(lockfile);
771
+ markOptionalOnlyEntries(lockfile);
741
772
  // Validate the generated lockfile
742
773
  for (const [depPath, pkg] of Object.entries(lockfile.packages)) {
743
774
  if (pkg.resolution == null || Object.keys(pkg.resolution).length === 0) {
@@ -760,6 +791,67 @@ async function convertGraphToLockfile(_graph, {
760
791
  return deps;
761
792
  }
762
793
  }
794
+
795
+ /**
796
+ * The dep path a restored importer entry for `name@specifier` points at: the
797
+ * root edge neighbour the manifest's dependency matches, or, when the manifest
798
+ * asks for a version the graph holds only deeper in the tree (an env peer a
799
+ * capsule manifest took from the graph), the snapshot of that version,
800
+ * preferring the peer variant the component's own dependencies were resolved
801
+ * against. Nothing when the graph holds no such version.
802
+ */
803
+ function findImporterDepPath(graph, rootEdge, name, specifier, workspaceVersions) {
804
+ const sameName = rootEdge.neighbours.filter(directDep => directDep.name === name);
805
+ // A merged graph keeps one neighbour per distinct specifier of a package,
806
+ // so the one recorded for this very specifier or version comes first; a
807
+ // range is matched only when none was, and among several resolutions that
808
+ // satisfy it the highest version, which is what a resolution would pick.
809
+ const recorded = sameName.find(directDep => directDep.specifier === specifier) ?? sameName.find(directDep => dp.removeSuffix(directDep.id) === `${name}@${specifier}`);
810
+ if (recorded) return recorded.id;
811
+ const satisfying = sameName.filter(directDep => specifierMatchesDirectDep(name, specifier, directDep, workspaceVersions));
812
+ if (satisfying.length > 0) {
813
+ return satisfying.reduce((highest, directDep) => _semver().default.gt(dp.parse(directDep.id).version, dp.parse(highest.id).version) ? directDep : highest).id;
814
+ }
815
+ const pkgId = `${name}@${specifier}`;
816
+ const candidates = graph.edges.filter(edge => edge.id !== _objects().DependenciesGraph.ROOT_EDGE_ID && (edge.attr?.pkgId ?? dp.removeSuffix(edge.id)) === pkgId).map(edge => edge.id);
817
+ if (candidates.length === 0) return graph.packages.has(pkgId) ? pkgId : undefined;
818
+ const peerIdsOfDirectDeps = new Set();
819
+ const stack = rootEdge.neighbours.map(({
820
+ id
821
+ }) => id);
822
+ while (stack.length > 0) {
823
+ for (const peerId of (0, _objects().splitDependenciesGraphPeerIds)(stack.pop())) {
824
+ peerIdsOfDirectDeps.add(peerId);
825
+ stack.push(peerId);
826
+ }
827
+ }
828
+ return candidates.find(id => peerIdsOfDirectDeps.has(id)) ?? candidates[0];
829
+ }
830
+
831
+ /**
832
+ * Whether the manifest's `specifier` for `name`, which no direct dependency
833
+ * was recorded for, is satisfied by the direct dependency the graph recorded
834
+ * under another specifier: a workspace reference the graph resolved to the
835
+ * version of the workspace component of that name, or a range the resolved
836
+ * version satisfies, read the way pnpm reads ranges, so a prerelease
837
+ * satisfies only a range that names one.
838
+ */
839
+ function specifierMatchesDirectDep(name, specifier, directDep, workspaceVersions) {
840
+ const {
841
+ version
842
+ } = dp.parse(directDep.id);
843
+ if (version == null) return false;
844
+ if (specifier.startsWith('workspace:')) {
845
+ // The entry becomes the workspace component's `file:` record, so only the
846
+ // version the workspace holds can stand for it, and the range, when the
847
+ // reference carries one, has to accept that version.
848
+ if (workspaceVersions.get(name) !== version) return false;
849
+ const range = specifier.slice('workspace:'.length);
850
+ if (range === '' || range === '*' || range === '^' || range === '~') return true;
851
+ return _semver().default.validRange(range) != null && _semver().default.satisfies(version, range);
852
+ }
853
+ return _semver().default.validRange(specifier) != null && _semver().default.satisfies(version, specifier);
854
+ }
763
855
  function getPkgsToResolve(lockfile, manifests) {
764
856
  const pkgsToResolve = [];
765
857
  const pkgsInTheWorkspaces = new Map();
@@ -802,8 +894,8 @@ function dropUnreachableLockfileEntries(lockfile) {
802
894
  if (!snapshot) continue;
803
895
  for (const depType of ['dependencies', 'optionalDependencies']) {
804
896
  for (const [name, ref] of Object.entries(snapshot[depType] ?? {})) {
805
- if (ref.startsWith('link:') || ref.startsWith('file:')) continue;
806
- stack.push(`${name}@${ref}`);
897
+ const subDepPath = refToDepPath(ref, name);
898
+ if (subDepPath != null) stack.push(subDepPath);
807
899
  }
808
900
  }
809
901
  }
@@ -813,8 +905,8 @@ function dropUnreachableLockfileEntries(lockfile) {
813
905
  for (const [name, {
814
906
  version
815
907
  }] of Object.entries(importer[depType] ?? {})) {
816
- if (version.startsWith('link:') || version.startsWith('file:')) continue;
817
- visit(`${name}@${version}`);
908
+ const depPath = refToDepPath(version, name);
909
+ if (depPath != null) visit(depPath);
818
910
  }
819
911
  }
820
912
  }
@@ -832,6 +924,157 @@ function dropUnreachableLockfileEntries(lockfile) {
832
924
  lockfile.bit.depsRequiringBuild = lockfile.bit.depsRequiringBuild.filter(depPath => reachablePackages.has(dp.removeSuffix(depPath)));
833
925
  }
834
926
  }
927
+
928
+ /**
929
+ * The workspace components among `manifests`, keyed by the `name@version`
930
+ * id the graph records them under, with the lockfile-relative directory pnpm
931
+ * records them at. The root project is not a component.
932
+ */
933
+ function workspaceComponentDirs(manifests, rootDir) {
934
+ const dirs = new Map();
935
+ for (const [projectDir, {
936
+ name,
937
+ version
938
+ }] of Object.entries(manifests)) {
939
+ if (!name || !version) continue;
940
+ const relativeDir = getLockfileImporterId(rootDir, projectDir);
941
+ if (relativeDir === '.') continue;
942
+ dirs.set(`${name}@${(0, _componentPackageVersion().snapToSemver)(version)}`, relativeDir);
943
+ }
944
+ return dirs;
945
+ }
946
+
947
+ /**
948
+ * The inverse of `createFileVersionReplacer`: rewrites every `name@version`
949
+ * of a workspace component in `dirs`, wherever it stands in a package id, a
950
+ * dep path or a peer suffix, to the `name@file:<dir>` pnpm records for it.
951
+ */
952
+ function createWorkspaceComponentReplacer(dirs) {
953
+ if (dirs.size === 0) return s => s;
954
+ const ids = Array.from(dirs.keys()).map(_lodash().escapeRegExp);
955
+ // An id starts a dep path or a peer suffix segment, or follows the alias in
956
+ // an aliased dep path, and ends where its own peer suffix or the enclosing
957
+ // segment does.
958
+ const pattern = new RegExp(`(?<=^|[(@])(${ids.join('|')})(?=[()]|$)`, 'g');
959
+ return s => s.replace(pattern, id => `${id.slice(0, id.lastIndexOf('@'))}@file:${dirs.get(id)}`);
960
+ }
961
+
962
+ /**
963
+ * Record the workspace components the graph resolved to, at the version the
964
+ * workspace holds them, the way pnpm does: as `file:` directory packages, with
965
+ * the graph's resolution of their dependencies as their snapshot. pnpm links
966
+ * every dependency on such a version to the workspace component
967
+ * (`linkWorkspacePackages: deep`), transitive ones included, so a registry
968
+ * entry for it would only be resolved again, and pnpm accepts the snapshot
969
+ * while the component's manifest still declares the dependencies it records.
970
+ * Other versions of the same component stay registry packages.
971
+ */
972
+ function linkWorkspaceComponents(lockfile, manifests, rootDir) {
973
+ const dirs = workspaceComponentDirs(manifests, rootDir);
974
+ if (dirs.size === 0) return;
975
+ const replace = createWorkspaceComponentReplacer(dirs);
976
+ const replaceRef = (name, ref) => ref.startsWith('link:') ? ref : replace(`${name}@${ref}`).slice(name.length + 1);
977
+ const packages = {};
978
+ for (const [pkgId, pkg] of Object.entries(lockfile.packages)) {
979
+ const directory = dirs.get(pkgId);
980
+ if (directory == null) {
981
+ packages[pkgId] = pkg;
982
+ continue;
983
+ }
984
+ // The id carries the name and the version, and the directory is the
985
+ // resolution. The rest (peers, engines, bins, ...) describes the package
986
+ // either way.
987
+ packages[replace(pkgId)] = _objectSpread(_objectSpread({}, (0, _lodash().omit)(pkg, ['resolution', 'name', 'version'])), {}, {
988
+ resolution: {
989
+ directory,
990
+ type: 'directory'
991
+ }
992
+ });
993
+ }
994
+ lockfile.packages = packages;
995
+ const snapshots = {};
996
+ for (const [depPath, snapshot] of Object.entries(lockfile.snapshots ?? {})) {
997
+ for (const depType of ['dependencies', 'optionalDependencies']) {
998
+ const deps = snapshot[depType];
999
+ if (!deps) continue;
1000
+ for (const [name, ref] of Object.entries(deps)) {
1001
+ deps[name] = replaceRef(name, ref);
1002
+ }
1003
+ }
1004
+ snapshots[replace(depPath)] = snapshot;
1005
+ }
1006
+ lockfile.snapshots = snapshots;
1007
+ for (const importer of Object.values(lockfile.importers ?? {})) {
1008
+ for (const depType of ['dependencies', 'devDependencies', 'optionalDependencies']) {
1009
+ for (const [name, dep] of Object.entries(importer[depType] ?? {})) {
1010
+ dep.version = replaceRef(name, dep.version);
1011
+ }
1012
+ }
1013
+ }
1014
+ if (lockfile.bit?.depsRequiringBuild) {
1015
+ lockfile.bit.depsRequiringBuild = lockfile.bit.depsRequiringBuild.map(replace);
1016
+ }
1017
+ }
1018
+
1019
+ /**
1020
+ * The dep path a lockfile ref under `name` points at, or `undefined` for a
1021
+ * `link:` ref, which points at no snapshot. An aliased ref
1022
+ * (`real-name@1.0.0` under the alias) names the real package, and a `file:`
1023
+ * ref a workspace component's own snapshot.
1024
+ */
1025
+ function refToDepPath(ref, name) {
1026
+ ensureInitialized();
1027
+ return dp.refToRelative(ref, name) ?? undefined;
1028
+ }
1029
+
1030
+ /**
1031
+ * Set each snapshot's `optional` flag the way pnpm does: on when every path
1032
+ * from an importer to it goes through an `optionalDependencies` edge, off
1033
+ * otherwise. The graph does not record the flag, and an install that takes
1034
+ * the lockfile as it is relies on it: a package reached only optionally may
1035
+ * fail to build or to install, one reached otherwise may not.
1036
+ */
1037
+ function markOptionalOnlyEntries(lockfile) {
1038
+ const snapshots = lockfile.snapshots ?? {};
1039
+ // Walk (dep path, reached optionally): `dependencies` edges keep the
1040
+ // context, `optionalDependencies` edges always enter it.
1041
+ const visited = new Set();
1042
+ const reachedNonOptionally = new Set();
1043
+ const reachedOptionally = new Set();
1044
+ const queue = [];
1045
+ for (const importer of Object.values(lockfile.importers ?? {})) {
1046
+ for (const [depType, optional] of [['dependencies', false], ['devDependencies', false], ['optionalDependencies', true]]) {
1047
+ for (const [name, {
1048
+ version
1049
+ }] of Object.entries(importer[depType] ?? {})) {
1050
+ const depPath = refToDepPath(version, name);
1051
+ if (depPath != null) queue.push([depPath, optional]);
1052
+ }
1053
+ }
1054
+ }
1055
+ while (queue.length > 0) {
1056
+ const [depPath, optional] = queue.pop();
1057
+ const visitKey = `${optional ? 'o' : 'r'}:${depPath}`;
1058
+ if (visited.has(visitKey)) continue;
1059
+ visited.add(visitKey);
1060
+ (optional ? reachedOptionally : reachedNonOptionally).add(depPath);
1061
+ const snapshot = snapshots[depPath];
1062
+ if (!snapshot) continue;
1063
+ for (const [depType, edgeOptional] of [['dependencies', optional], ['optionalDependencies', true]]) {
1064
+ for (const [name, ref] of Object.entries(snapshot[depType] ?? {})) {
1065
+ const subDepPath = refToDepPath(ref, name);
1066
+ if (subDepPath != null) queue.push([subDepPath, edgeOptional]);
1067
+ }
1068
+ }
1069
+ }
1070
+ for (const [depPath, snapshot] of Object.entries(snapshots)) {
1071
+ if (reachedOptionally.has(depPath) && !reachedNonOptionally.has(depPath)) {
1072
+ snapshot.optional = true;
1073
+ } else {
1074
+ delete snapshot.optional;
1075
+ }
1076
+ }
1077
+ }
835
1078
  function dedupeSnapshotDepsToRootManifest(lockfile, rootManifest) {
836
1079
  const overridden = new Set(overriddenPackageNames(lockfile.overrides));
837
1080
  const rootDeps = new Map(Object.entries(nonOptionalDependencies(rootManifest)).filter(([name]) => !overridden.has(name)));