lockfile-subset 1.1.0 → 1.2.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.
Files changed (2) hide show
  1. package/dist/index.mjs +235 -7
  2. package/package.json +5 -2
package/dist/index.mjs CHANGED
@@ -146,17 +146,216 @@ async function extractPnpmSubset({ projectPath, packageNames, includeOptional =
146
146
  };
147
147
  }
148
148
  //#endregion
149
+ //#region src/extract-yarn.ts
150
+ const { parse: parseYarnLockV1, stringify: stringifyYarnLockV1 } = createRequire(import.meta.url)("@yarnpkg/lockfile");
151
+ function detectYarnVersion(content) {
152
+ return content.includes("# yarn lockfile v1") ? 1 : 2;
153
+ }
154
+ function extractV1({ projectPath, packageNames, includeOptional, lockfileContent }) {
155
+ const parsed = parseYarnLockV1(lockfileContent);
156
+ if (parsed.type !== "success") throw new Error(`Failed to parse yarn.lock: ${parsed.type}`);
157
+ const lockfile = parsed.object;
158
+ const pkgJsonPath = join(projectPath, "package.json");
159
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
160
+ const allDeps = {
161
+ ...pkgJson.dependencies,
162
+ ...pkgJson.optionalDependencies
163
+ };
164
+ const keepKeys = /* @__PURE__ */ new Set();
165
+ const collected = [];
166
+ for (const name of packageNames) {
167
+ const range = allDeps[name];
168
+ if (!range) throw new Error(`Package "${name}" not found in yarn.lock`);
169
+ const queue = [`${name}@${range}`];
170
+ while (queue.length > 0) {
171
+ const key = queue.shift();
172
+ if (keepKeys.has(key)) continue;
173
+ const entry = lockfile[key];
174
+ if (!entry) continue;
175
+ keepKeys.add(key);
176
+ collected.push({
177
+ name: key.slice(0, key.lastIndexOf("@")),
178
+ version: entry.version
179
+ });
180
+ if (entry.dependencies) for (const [depName, depRange] of Object.entries(entry.dependencies)) {
181
+ const depKey = `${depName}@${depRange}`;
182
+ if (!keepKeys.has(depKey)) queue.push(depKey);
183
+ }
184
+ if (includeOptional && entry.optionalDependencies) for (const [depName, depRange] of Object.entries(entry.optionalDependencies)) {
185
+ const depKey = `${depName}@${depRange}`;
186
+ if (!keepKeys.has(depKey)) queue.push(depKey);
187
+ }
188
+ }
189
+ }
190
+ const subset = {};
191
+ for (const key of keepKeys) subset[key] = lockfile[key];
192
+ const dependencies = {};
193
+ for (const name of packageNames) dependencies[name] = lockfile[`${name}@${allDeps[name]}`].version;
194
+ const seen = /* @__PURE__ */ new Set();
195
+ const deduped = collected.filter((c) => {
196
+ const key = `${c.name}@${c.version}`;
197
+ if (seen.has(key)) return false;
198
+ seen.add(key);
199
+ return true;
200
+ });
201
+ return {
202
+ type: "yarn",
203
+ yarnVersion: 1,
204
+ packageJson: {
205
+ name: "lockfile-subset-output",
206
+ version: "1.0.0",
207
+ dependencies
208
+ },
209
+ lockfileContent: stringifyYarnLockV1(subset),
210
+ collected: deduped
211
+ };
212
+ }
213
+ function extractBerry({ projectPath, packageNames, includeOptional, lockfileContent }) {
214
+ const lockfile = yaml.load(lockfileContent);
215
+ const descriptorMap = /* @__PURE__ */ new Map();
216
+ for (const [compoundKey, entry] of Object.entries(lockfile)) {
217
+ if (compoundKey === "__metadata") continue;
218
+ const descriptors = compoundKey.split(", ");
219
+ for (const descriptor of descriptors) descriptorMap.set(descriptor, {
220
+ entry,
221
+ originalKey: compoundKey
222
+ });
223
+ }
224
+ const pkgJsonPath = join(projectPath, "package.json");
225
+ const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
226
+ const allDeps = {
227
+ ...pkgJson.dependencies,
228
+ ...pkgJson.optionalDependencies
229
+ };
230
+ const keepOriginalKeys = /* @__PURE__ */ new Set();
231
+ const visited = /* @__PURE__ */ new Set();
232
+ const collected = [];
233
+ for (const name of packageNames) {
234
+ const range = allDeps[name];
235
+ if (!range) throw new Error(`Package "${name}" not found in yarn.lock`);
236
+ const queue = [`${name}@npm:${range}`];
237
+ while (queue.length > 0) {
238
+ const desc = queue.shift();
239
+ if (visited.has(desc)) continue;
240
+ visited.add(desc);
241
+ const match = descriptorMap.get(desc);
242
+ if (!match) continue;
243
+ keepOriginalKeys.add(match.originalKey);
244
+ collected.push({
245
+ name: parseDescriptorName(desc),
246
+ version: match.entry.version
247
+ });
248
+ if (match.entry.dependencies) for (const [depName, depRange] of Object.entries(match.entry.dependencies)) {
249
+ const depDesc = `${depName}@${depRange}`;
250
+ if (!visited.has(depDesc)) queue.push(depDesc);
251
+ }
252
+ if (includeOptional && match.entry.optionalDependencies) for (const [depName, depRange] of Object.entries(match.entry.optionalDependencies)) {
253
+ const depDesc = `${depName}@${depRange}`;
254
+ if (!visited.has(depDesc)) queue.push(depDesc);
255
+ }
256
+ }
257
+ }
258
+ const dependencies = {};
259
+ for (const name of packageNames) {
260
+ const descriptor = `${name}@npm:${allDeps[name]}`;
261
+ dependencies[name] = descriptorMap.get(descriptor).entry.version;
262
+ }
263
+ const lines = [];
264
+ lines.push("# This file is generated by running \"yarn install\" inside your project.");
265
+ lines.push("# Manual changes might be lost - proceed with caution!");
266
+ lines.push("");
267
+ const metadata = lockfile.__metadata;
268
+ if (metadata) {
269
+ lines.push("__metadata:");
270
+ lines.push(` version: ${metadata.version}`);
271
+ if (metadata.cacheKey) lines.push(` cacheKey: ${metadata.cacheKey}`);
272
+ lines.push("");
273
+ }
274
+ for (const originalKey of keepOriginalKeys) {
275
+ const entry = lockfile[originalKey];
276
+ lines.push(`"${originalKey}":`);
277
+ lines.push(` version: ${entry.version}`);
278
+ lines.push(` resolution: "${entry.resolution}"`);
279
+ if (entry.dependencies && Object.keys(entry.dependencies).length > 0) {
280
+ lines.push(" dependencies:");
281
+ for (const [k, v] of Object.entries(entry.dependencies)) lines.push(` ${k}: "${v}"`);
282
+ }
283
+ if (entry.optionalDependencies && Object.keys(entry.optionalDependencies).length > 0) {
284
+ lines.push(" optionalDependencies:");
285
+ for (const [k, v] of Object.entries(entry.optionalDependencies)) lines.push(` ${k}: "${v}"`);
286
+ }
287
+ if (entry.dependenciesMeta && Object.keys(entry.dependenciesMeta).length > 0) {
288
+ lines.push(" dependenciesMeta:");
289
+ for (const [k, v] of Object.entries(entry.dependenciesMeta)) {
290
+ lines.push(` ${k}:`);
291
+ if (v.optional !== void 0) lines.push(` optional: ${v.optional}`);
292
+ }
293
+ }
294
+ if (entry.bin && Object.keys(entry.bin).length > 0) {
295
+ lines.push(" bin:");
296
+ for (const [k, v] of Object.entries(entry.bin)) lines.push(` ${k}: ${v}`);
297
+ }
298
+ if (entry.conditions) lines.push(` conditions: ${entry.conditions}`);
299
+ if (entry.checksum) lines.push(` checksum: ${entry.checksum}`);
300
+ lines.push(` languageName: ${entry.languageName || "node"}`);
301
+ lines.push(` linkType: ${entry.linkType || "hard"}`);
302
+ lines.push("");
303
+ }
304
+ const seen = /* @__PURE__ */ new Set();
305
+ const deduped = collected.filter((c) => {
306
+ const key = `${c.name}@${c.version}`;
307
+ if (seen.has(key)) return false;
308
+ seen.add(key);
309
+ return true;
310
+ });
311
+ return {
312
+ type: "yarn",
313
+ yarnVersion: 2,
314
+ packageJson: {
315
+ name: "lockfile-subset-output",
316
+ version: "1.0.0",
317
+ dependencies
318
+ },
319
+ lockfileContent: lines.join("\n"),
320
+ collected: deduped
321
+ };
322
+ }
323
+ /** Extract package name from a descriptor like "chalk@npm:4.1.2" or "@scope/pkg@npm:^1.0.0" */
324
+ function parseDescriptorName(descriptor) {
325
+ const npmIdx = descriptor.indexOf("@npm:");
326
+ if (npmIdx > 0) return descriptor.slice(0, npmIdx);
327
+ const lastAt = descriptor.lastIndexOf("@");
328
+ if (lastAt > 0) return descriptor.slice(0, lastAt);
329
+ return descriptor;
330
+ }
331
+ async function extractYarnSubset({ projectPath, packageNames, includeOptional = true }) {
332
+ const lockfileContent = readFileSync(join(projectPath, "yarn.lock"), "utf8");
333
+ if (detectYarnVersion(lockfileContent) === 1) return extractV1({
334
+ projectPath,
335
+ packageNames,
336
+ includeOptional,
337
+ lockfileContent
338
+ });
339
+ else return extractBerry({
340
+ projectPath,
341
+ packageNames,
342
+ includeOptional,
343
+ lockfileContent
344
+ });
345
+ }
346
+ //#endregion
149
347
  //#region src/write.ts
150
348
  function writeOutput(outputDir, result) {
151
349
  mkdirSync(outputDir, { recursive: true });
152
350
  writeFileSync(join(outputDir, "package.json"), JSON.stringify(result.packageJson, null, 2) + "\n");
153
351
  if (result.type === "npm") writeFileSync(join(outputDir, "package-lock.json"), JSON.stringify(result.lockfileJson, null, 2) + "\n");
154
- else writeFileSync(join(outputDir, "pnpm-lock.yaml"), yaml.dump(result.lockfileYaml, {
352
+ else if (result.type === "pnpm") writeFileSync(join(outputDir, "pnpm-lock.yaml"), yaml.dump(result.lockfileYaml, {
155
353
  lineWidth: -1,
156
354
  noCompatMode: true,
157
355
  quotingType: "'",
158
356
  forceQuotes: false
159
357
  }));
358
+ else writeFileSync(join(outputDir, "yarn.lock"), result.lockfileContent);
160
359
  }
161
360
  //#endregion
162
361
  //#region src/index.ts
@@ -218,11 +417,15 @@ function resolveLockfile(lockfilePath) {
218
417
  projectPath: resolve("."),
219
418
  type: "pnpm"
220
419
  };
420
+ if (existsSync(resolve("yarn.lock"))) return {
421
+ projectPath: resolve("."),
422
+ type: "yarn"
423
+ };
221
424
  if (existsSync(resolve("package-lock.json"))) return {
222
425
  projectPath: resolve("."),
223
426
  type: "npm"
224
427
  };
225
- throw new Error("No lockfile found in current directory. Expected package-lock.json or pnpm-lock.yaml.");
428
+ throw new Error("No lockfile found in current directory. Expected package-lock.json, pnpm-lock.yaml, or yarn.lock.");
226
429
  }
227
430
  const resolved = resolve(lockfilePath);
228
431
  const basename = resolved.split("/").pop();
@@ -230,17 +433,21 @@ function resolveLockfile(lockfilePath) {
230
433
  projectPath: resolve(resolved, ".."),
231
434
  type: "pnpm"
232
435
  };
436
+ if (basename === "yarn.lock") return {
437
+ projectPath: resolve(resolved, ".."),
438
+ type: "yarn"
439
+ };
233
440
  if (basename === "package-lock.json") return {
234
441
  projectPath: resolve(resolved, ".."),
235
442
  type: "npm"
236
443
  };
237
- throw new Error(`Invalid lockfile path: ${lockfilePath}. Expected a path to package-lock.json or pnpm-lock.yaml.`);
444
+ throw new Error(`Invalid lockfile path: ${lockfilePath}. Expected a path to package-lock.json, pnpm-lock.yaml, or yarn.lock.`);
238
445
  }
239
446
  const HELP = `
240
447
  lockfile-subset <packages...> [options]
241
448
 
242
- Extract a subset of package-lock.json or pnpm-lock.yaml for specified packages
243
- and their transitive dependencies.
449
+ Extract a subset of package-lock.json, pnpm-lock.yaml, or yarn.lock for specified
450
+ packages and their transitive dependencies.
244
451
 
245
452
  Arguments:
246
453
  packages Package names to extract (one or more, space-separated)
@@ -249,7 +456,7 @@ Options:
249
456
  --lockfile, -l <path> Path to lockfile (auto-detected from cwd by default)
250
457
  --output, -o <dir> Output directory (default: ./lockfile-subset-output)
251
458
  --no-optional Exclude optional dependencies
252
- --install Run npm ci / pnpm install --frozen-lockfile after generating
459
+ --install Run npm ci / pnpm install / yarn install after generating
253
460
  --dry-run Print the result without writing files
254
461
  --version, -v Show version
255
462
  --help, -h Show this help
@@ -284,6 +491,11 @@ async function main() {
284
491
  packageNames: args.packages,
285
492
  includeOptional: args.includeOptional
286
493
  });
494
+ else if (type === "yarn") result = await extractYarnSubset({
495
+ projectPath,
496
+ packageNames: args.packages,
497
+ includeOptional: args.includeOptional
498
+ });
287
499
  else result = await extractSubset({
288
500
  projectPath,
289
501
  packageNames: args.packages,
@@ -296,13 +508,16 @@ async function main() {
296
508
  if (result.type === "npm") {
297
509
  console.log("\n--- package-lock.json ---");
298
510
  console.log(JSON.stringify(result.lockfileJson, null, 2));
299
- } else {
511
+ } else if (result.type === "pnpm") {
300
512
  const yaml = (await import("js-yaml")).default;
301
513
  console.log("\n--- pnpm-lock.yaml ---");
302
514
  console.log(yaml.dump(result.lockfileYaml, {
303
515
  lineWidth: -1,
304
516
  noCompatMode: true
305
517
  }));
518
+ } else {
519
+ console.log("\n--- yarn.lock ---");
520
+ console.log(result.lockfileContent);
306
521
  }
307
522
  return;
308
523
  }
@@ -315,7 +530,20 @@ async function main() {
315
530
  cwd: outputDir,
316
531
  stdio: "inherit"
317
532
  });
533
+ } else if (type === "yarn") if (result.type === "yarn" && result.yarnVersion === 1) {
534
+ console.log("Running yarn install --frozen-lockfile...");
535
+ execSync("yarn install --frozen-lockfile", {
536
+ cwd: outputDir,
537
+ stdio: "inherit"
538
+ });
318
539
  } else {
540
+ console.log("Running yarn install --immutable...");
541
+ execSync("yarn install --immutable", {
542
+ cwd: outputDir,
543
+ stdio: "inherit"
544
+ });
545
+ }
546
+ else {
319
547
  console.log("Running npm ci...");
320
548
  execSync("npm ci", {
321
549
  cwd: outputDir,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lockfile-subset",
3
- "version": "1.1.0",
4
- "description": "Extract a subset of package-lock.json for specified packages and their transitive dependencies",
3
+ "version": "1.2.0",
4
+ "description": "Extract a subset of package-lock.json, pnpm-lock.yaml, or yarn.lock for specified packages and their transitive dependencies",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "lockfile-subset": "./dist/index.mjs"
@@ -17,6 +17,7 @@
17
17
  "keywords": [
18
18
  "npm",
19
19
  "pnpm",
20
+ "yarn",
20
21
  "lockfile",
21
22
  "package-lock",
22
23
  "subset",
@@ -37,6 +38,7 @@
37
38
  "@types/js-yaml": "^4.0.9",
38
39
  "@types/node": "^25.5.0",
39
40
  "@types/npmcli__arborist": "^6.3.3",
41
+ "@types/yarnpkg__lockfile": "^1.1.9",
40
42
  "semantic-release": "^25.0.3",
41
43
  "tsdown": "^0.21.4",
42
44
  "typescript": "^5.9.3",
@@ -44,6 +46,7 @@
44
46
  },
45
47
  "dependencies": {
46
48
  "@npmcli/arborist": "^9.4.2",
49
+ "@yarnpkg/lockfile": "^1.1.0",
47
50
  "js-yaml": "^4.1.1"
48
51
  }
49
52
  }