@effected/lockfiles 0.6.0 → 0.6.2

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/internal/pnpm.js CHANGED
@@ -14,7 +14,8 @@ const PnpmImporter = Schema.Struct({
14
14
  dependencies: PnpmImporterDeps,
15
15
  devDependencies: PnpmImporterDeps,
16
16
  peerDependencies: PnpmImporterDeps,
17
- optionalDependencies: PnpmImporterDeps
17
+ optionalDependencies: PnpmImporterDeps,
18
+ publishDirectory: Schema.optionalKey(Schema.String)
18
19
  });
19
20
  /**
20
21
  * The version gate's own input: `lockfileVersion` and nothing else.
@@ -89,9 +90,23 @@ const parsePnpm = (content) => Effect.gen(function* () {
89
90
  * resolution, which names no instance in this lockfile, from becoming a
90
91
  * plausible lie.
91
92
  *
93
+ * A failed composition gets one second reading: the recorded version ITSELF as
94
+ * an instance id. That is how pnpm spells an `npm:` alias — the dependency
95
+ * `typescript-classic: npm:typescript@^6.0.3` is recorded as
96
+ * `typescript-classic: typescript@6.0.3`, the referenced instance's own key,
97
+ * peer suffix included when one applies. Still compose-then-verify, not a
98
+ * guess: a plain version (`6.0.3`) can never be an instance id, because ids
99
+ * always carry a name, so the reading admits exactly the alias shape.
100
+ *
101
+ * Unlike {@link resolveImporterEdges}, there is no `workspace:`-specifier
102
+ * ancestor walk here — a snapshot body records no specifier, so that evidence
103
+ * does not exist on this path. The publish-directory map needs no specifier:
104
+ * the importer entry's own `publishDirectory` declaration is exact evidence,
105
+ * available to both paths, and is consulted unconditionally.
106
+ *
92
107
  * @internal
93
108
  */
94
- const resolveEdges = (sections, instanceIds) => {
109
+ const resolveEdges = (sections, instanceIds, publishDirTargets) => {
95
110
  const edges = /* @__PURE__ */ new Map();
96
111
  const unnameable = /* @__PURE__ */ new Set();
97
112
  for (const section of sections) {
@@ -101,11 +116,13 @@ const resolveEdges = (sections, instanceIds) => {
101
116
  if (version.startsWith(LINK_PREFIX)) {
102
117
  const target = resolveLinkTarget("", version.slice(LINK_PREFIX.length));
103
118
  if (target !== void 0 && instanceIds.has(target)) edges.set(name, target);
119
+ else if (target !== void 0 && publishDirTargets.has(target)) edges.set(name, publishDirTargets.get(target));
104
120
  else unnameable.add(name);
105
121
  continue;
106
122
  }
107
123
  const bare = `${name}@${version}`;
108
124
  if (instanceIds.has(bare)) edges.set(name, bare);
125
+ else if (instanceIds.has(version)) edges.set(name, version);
109
126
  else unnameable.add(name);
110
127
  }
111
128
  }
@@ -121,6 +138,13 @@ const LINK_PREFIX = "link:";
121
138
  * workspace edge can name the workspace importer's instance id.
122
139
  * Total: a target that walks above the root yields `undefined`.
123
140
  *
141
+ * A target that normalizes to NOTHING — `link:.` recorded root-relative —
142
+ * also yields `undefined` rather than the root importer's ".": the root
143
+ * importer deliberately emits no `ResolvedPackage` row (its path cannot serve
144
+ * as a package name), so mapping the empty target to "." would hand back an id
145
+ * no row carries, and no pnpm output recording that shape has been observed.
146
+ * The edge stays honestly unnameable instead.
147
+ *
124
148
  * @internal
125
149
  */
126
150
  const resolveLinkTarget = (importerPath, target) => {
@@ -135,6 +159,31 @@ const resolveLinkTarget = (importerPath, target) => {
135
159
  }
136
160
  return segments.length === 0 ? void 0 : segments.join("/");
137
161
  };
162
+ /** The specifier protocol pnpm resolves through the workspace itself. @internal */
163
+ const WORKSPACE_PREFIX = "workspace:";
164
+ /**
165
+ * The importer a `link:` target lands INSIDE, longest path first.
166
+ *
167
+ * pnpm's `publishConfig.linkDirectory` makes a workspace link point at the
168
+ * package's publish directory rather than its root — `link:../bundler/dist/dev/pkg`
169
+ * where `packages/bundler` is the importer — so the target itself is a build
170
+ * output that is no importer and never will be. The owning importer is the
171
+ * longest ancestor of the target that IS one; longest-first so a nested importer
172
+ * beats its parent.
173
+ *
174
+ * The root importer (`"."`) is deliberately unreachable here: it is an ancestor
175
+ * of every path in the workspace, so admitting it would resolve every stray link
176
+ * to the root rather than leaving it unnamed.
177
+ *
178
+ * @internal
179
+ */
180
+ const owningImporter = (target, importerPaths) => {
181
+ const segments = target.split("/");
182
+ for (let end = segments.length - 1; end > 0; end--) {
183
+ const candidate = segments.slice(0, end).join("/");
184
+ if (importerPaths.has(candidate)) return candidate;
185
+ }
186
+ };
138
187
  /**
139
188
  * Resolve one importer's outgoing edges. pnpm records `{ specifier, version }`
140
189
  * per importer dependency, where the version is either a registry version (the
@@ -142,9 +191,28 @@ const resolveLinkTarget = (importerPath, target) => {
142
191
  * naming another importer — normalized against this importer's own path. Both
143
192
  * are checked against the lockfile's instance ids before being emitted.
144
193
  *
194
+ * A `link:` target that is no importer gets two further chances. First the
195
+ * publish-directory map: an importer that DECLARES `publishDirectory` has told
196
+ * the lockfile exactly which build directory its links point at, so a target
197
+ * matching a declared publish directory names that importer on evidence, no
198
+ * specifier required — {@link resolveEdges} consults the same map for the same
199
+ * reason. Then, and only under a `workspace:` SPECIFIER, the ancestor walk:
200
+ * pnpm resolved that edge through the workspace, so the target directory is
201
+ * known to belong to a workspace package, and {@link owningImporter} names it.
202
+ * The specifier is what makes the walk a resolution rather than a guess — a
203
+ * hand-written `link:../vendor/stub` specifier names a directory pnpm never
204
+ * claimed was a workspace package, and attributing it to whichever importer
205
+ * happens to contain it would answer with the wrong package's peers instead of
206
+ * admitting it could not name the edge. The snapshot path has no specifier and
207
+ * therefore no walk; the publish-directory map is the evidence it does get.
208
+ *
209
+ * A failed `name@version` composition gets the same alias reading as
210
+ * {@link resolveEdges}: the recorded version itself as an instance id, which is
211
+ * how pnpm spells `npm:` aliases.
212
+ *
145
213
  * @internal
146
214
  */
147
- const resolveImporterEdges = (importer, importerPath, instanceIds) => {
215
+ const resolveImporterEdges = (importer, importerPath, instanceIds, importerPaths, publishDirTargets) => {
148
216
  const edges = /* @__PURE__ */ new Map();
149
217
  const unnameable = /* @__PURE__ */ new Set();
150
218
  for (const group of importerDepGroups(importer)) {
@@ -153,12 +221,14 @@ const resolveImporterEdges = (importer, importerPath, instanceIds) => {
153
221
  if (name === "") continue;
154
222
  if (info.version.startsWith(LINK_PREFIX)) {
155
223
  const target = resolveLinkTarget(importerPath, info.version.slice(5));
156
- if (target !== void 0 && instanceIds.has(target)) edges.set(name, target);
224
+ const owner = target === void 0 ? void 0 : instanceIds.has(target) ? target : publishDirTargets.get(target) ?? (info.specifier.startsWith(WORKSPACE_PREFIX) ? owningImporter(target, importerPaths) : void 0);
225
+ if (owner !== void 0) edges.set(name, owner);
157
226
  else unnameable.add(name);
158
227
  continue;
159
228
  }
160
229
  const bare = `${name}@${info.version}`;
161
230
  if (instanceIds.has(bare)) edges.set(name, bare);
231
+ else if (instanceIds.has(info.version)) edges.set(name, info.version);
162
232
  else unnameable.add(name);
163
233
  }
164
234
  }
@@ -213,6 +283,14 @@ const toFields = (raw) => Effect.gen(function* () {
213
283
  ...raw.packages ? Object.keys(raw.packages) : [],
214
284
  ...Object.keys(raw.importers)
215
285
  ]);
286
+ const importerPaths = new Set(Object.keys(raw.importers).filter((path) => path !== "." && path !== ""));
287
+ const publishDirTargets = /* @__PURE__ */ new Map();
288
+ for (const [importerPath, importer] of Object.entries(raw.importers)) {
289
+ if (importerPath === "") continue;
290
+ if (importer.publishDirectory === void 0) continue;
291
+ const key = resolveLinkTarget(importerPath, importer.publishDirectory);
292
+ if (key !== void 0) publishDirTargets.set(key, importerPath);
293
+ }
216
294
  const emitted = /* @__PURE__ */ new Set();
217
295
  for (const [importerPath, importer] of Object.entries(raw.importers)) {
218
296
  if (importerPath === "." || importerPath === "") continue;
@@ -222,7 +300,7 @@ const toFields = (raw) => Effect.gen(function* () {
222
300
  instanceId: importerPath,
223
301
  isWorkspace: true,
224
302
  relativePath: importerPath,
225
- ...resolveImporterEdges(importer, importerPath, instanceIds)
303
+ ...resolveImporterEdges(importer, importerPath, instanceIds, importerPaths, publishDirTargets)
226
304
  }));
227
305
  }
228
306
  const emit = (instanceId, meta, edges) => Effect.gen(function* () {
@@ -240,7 +318,7 @@ const toFields = (raw) => Effect.gen(function* () {
240
318
  ...integrity !== void 0 ? { integrity } : {},
241
319
  isWorkspace: false,
242
320
  ...peerDeclarations(meta?.peerDependencies, meta?.peerDependenciesMeta),
243
- ...resolveEdges(edges, instanceIds)
321
+ ...resolveEdges(edges, instanceIds, publishDirTargets)
244
322
  }));
245
323
  });
246
324
  if (snapshots) for (const [key, snapshot] of Object.entries(snapshots)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/lockfiles",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": false,
5
5
  "description": "Pure lockfile parsing for bun, npm, pnpm and yarn Berry into one unified Effect schema model, with pure integrity checking against workspace manifests.",
6
6
  "keywords": [
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@effected/jsonc": "^0.7.0",
44
- "@effected/npm": "^0.11.0",
44
+ "@effected/npm": "^0.11.1",
45
45
  "@effected/semver": "^0.5.0",
46
46
  "@effected/yaml": "^0.10.0",
47
47
  "effect": "4.0.0-rc.109"
@@ -5,7 +5,7 @@
5
5
  "toolPackages": [
6
6
  {
7
7
  "packageName": "@microsoft/api-extractor",
8
- "packageVersion": "7.58.12"
8
+ "packageVersion": "7.58.13"
9
9
  }
10
10
  ]
11
11
  }