lockfile-subset 1.2.1 → 1.3.1

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 +159 -64
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,33 +1,72 @@
1
1
  #!/usr/bin/env node
2
- import { join, resolve } from "path";
2
+ import { basename, dirname, join, relative, resolve, sep } from "path";
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
4
  import { execSync } from "child_process";
5
5
  import { createRequire } from "module";
6
6
  import Arborist from "@npmcli/arborist";
7
7
  import yaml from "js-yaml";
8
+ //#region src/workspace-path.ts
9
+ /**
10
+ * Normalize a workspace/importer path to a forward-slash relative key.
11
+ * Returns "." for the root (empty input, ".", "./").
12
+ */
13
+ function normalizeWorkspacePath(p) {
14
+ if (!p || p === "." || p === "./") return ".";
15
+ return p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
16
+ }
17
+ //#endregion
8
18
  //#region src/extract.ts
9
- async function extractSubset({ projectPath, packageNames, includeOptional = true }) {
19
+ /**
20
+ * Rewrite a package-lock.json location key so the output is a standalone project.
21
+ * Locations inside the chosen workspace get their workspace prefix stripped;
22
+ * hoisted entries at the root stay where they are.
23
+ */
24
+ function rewritePackageLocation(location, workspacePath) {
25
+ if (workspacePath === ".") return location;
26
+ if (location === workspacePath) return "";
27
+ const prefix = workspacePath + "/";
28
+ if (location.startsWith(prefix)) return location.slice(prefix.length);
29
+ return location;
30
+ }
31
+ async function extractSubset({ projectPath, packageNames, includeOptional = true, workspacePath = "." }) {
10
32
  const tree = await new Arborist({ path: projectPath }).loadVirtual();
11
33
  const originalLockfileVersion = tree.meta.originalLockfileVersion;
12
34
  if (originalLockfileVersion < 2) throw new Error(`Lockfile version ${originalLockfileVersion} is not supported. Please upgrade to npm 7+ (lockfile v2/v3) by running: npm install --package-lock-only`);
35
+ const normalizedWorkspace = normalizeWorkspacePath(workspacePath);
36
+ let startNode = tree;
37
+ if (normalizedWorkspace !== ".") {
38
+ let found;
39
+ for (const child of tree.fsChildren) if (child.location === normalizedWorkspace) {
40
+ found = child;
41
+ break;
42
+ }
43
+ if (!found) {
44
+ const available = [...tree.fsChildren].map((c) => c.location).join(", ");
45
+ throw new Error(`Workspace "${normalizedWorkspace}" not found in package-lock.json. Available workspaces: ${available || "(none)"}`);
46
+ }
47
+ startNode = found;
48
+ }
13
49
  const keep = /* @__PURE__ */ new Set();
14
50
  for (const name of packageNames) {
15
- const edge = tree.edgesOut.get(name);
51
+ const edge = startNode.edgesOut.get(name);
16
52
  if (!edge?.to) throw new Error(`Package "${name}" not found in lockfile`);
53
+ if (edge.type === "workspace" || edge.to.isWorkspace) throw new Error(`Package "${name}" resolves to a workspace, not a published package`);
17
54
  const queue = [edge.to];
18
55
  while (queue.length > 0) {
19
56
  const node = queue.shift();
20
57
  if (keep.has(node)) continue;
58
+ if (node.isWorkspace) continue;
21
59
  keep.add(node);
22
60
  for (const e of node.edgesOut.values()) {
23
61
  if (e.type === "dev") continue;
62
+ if (e.type === "workspace") continue;
24
63
  if (e.type === "optional" && !includeOptional) continue;
25
- if (e.to && !keep.has(e.to)) queue.push(e.to);
64
+ if (e.to && !e.to.isWorkspace && !keep.has(e.to)) queue.push(e.to);
26
65
  }
27
66
  }
28
67
  }
29
68
  const dependencies = {};
30
- for (const name of packageNames) dependencies[name] = tree.edgesOut.get(name).to.version;
69
+ for (const name of packageNames) dependencies[name] = startNode.edgesOut.get(name).to.version;
31
70
  const subsetPackages = {};
32
71
  subsetPackages[""] = {
33
72
  name: "lockfile-subset-output",
@@ -36,8 +75,10 @@ async function extractSubset({ projectPath, packageNames, includeOptional = true
36
75
  };
37
76
  const originalPackages = tree.meta.data.packages;
38
77
  for (const node of keep) {
39
- const location = node.location;
40
- if (originalPackages[location]) subsetPackages[location] = originalPackages[location];
78
+ const original = originalPackages[node.location];
79
+ if (!original) continue;
80
+ const rewritten = rewritePackageLocation(node.location, normalizedWorkspace);
81
+ subsetPackages[rewritten] = original;
41
82
  }
42
83
  const collected = [...keep].map((node) => ({
43
84
  name: node.name,
@@ -77,21 +118,31 @@ function parseSnapshotKey(key) {
77
118
  function snapshotKey(name, version) {
78
119
  return `${name}@${version}`;
79
120
  }
80
- async function extractPnpmSubset({ projectPath, packageNames, includeOptional = true }) {
121
+ async function extractPnpmSubset({ projectPath, packageNames, includeOptional = true, workspacePath = "." }) {
81
122
  const content = readFileSync(join(projectPath, "pnpm-lock.yaml"), "utf8");
82
123
  const lockfile = yaml.load(content);
83
124
  if (!lockfile.lockfileVersion || !String(lockfile.lockfileVersion).startsWith("9")) throw new Error(`pnpm lockfile version ${lockfile.lockfileVersion} is not supported. Please upgrade to pnpm 9+ (lockfile v9).`);
84
- const rootImporter = lockfile.importers["."];
85
- if (!rootImporter) throw new Error("No root importer found in pnpm-lock.yaml");
125
+ const importerKey = normalizeWorkspacePath(workspacePath);
126
+ const importer = lockfile.importers[importerKey];
127
+ if (!importer) {
128
+ const available = Object.keys(lockfile.importers).join(", ");
129
+ throw new Error(`Importer "${importerKey}" not found in pnpm-lock.yaml. Available importers: ${available}`);
130
+ }
86
131
  const rootDeps = {};
87
- if (rootImporter.dependencies) for (const [name, info] of Object.entries(rootImporter.dependencies)) rootDeps[name] = info.version;
88
- if (rootImporter.optionalDependencies) for (const [name, info] of Object.entries(rootImporter.optionalDependencies)) rootDeps[name] = info.version;
132
+ for (const info of [importer.dependencies, importer.optionalDependencies]) {
133
+ if (!info) continue;
134
+ for (const [name, dep] of Object.entries(info)) rootDeps[name] = {
135
+ specifier: dep.specifier,
136
+ version: dep.version
137
+ };
138
+ }
89
139
  const keepSnapshots = /* @__PURE__ */ new Set();
90
140
  const keepPackages = /* @__PURE__ */ new Set();
91
141
  for (const name of packageNames) {
92
- const version = rootDeps[name];
93
- if (!version) throw new Error(`Package "${name}" not found in pnpm-lock.yaml`);
94
- const queue = [snapshotKey(name, version)];
142
+ const dep = rootDeps[name];
143
+ if (!dep) throw new Error(`Package "${name}" not found in pnpm-lock.yaml`);
144
+ if (dep.version.startsWith("link:")) throw new Error(`Package "${name}" resolves to a workspace (${dep.version}), not a published package`);
145
+ const queue = [snapshotKey(name, dep.version)];
95
146
  while (queue.length > 0) {
96
147
  const current = queue.shift();
97
148
  if (keepSnapshots.has(current)) continue;
@@ -101,25 +152,27 @@ async function extractPnpmSubset({ projectPath, packageNames, includeOptional =
101
152
  const snapshot = lockfile.snapshots[current];
102
153
  if (!snapshot) continue;
103
154
  if (snapshot.dependencies) for (const [depName, depVersion] of Object.entries(snapshot.dependencies)) {
155
+ if (depVersion.startsWith("link:")) continue;
104
156
  const depKey = snapshotKey(depName, depVersion);
105
157
  if (!keepSnapshots.has(depKey)) queue.push(depKey);
106
158
  }
107
159
  if (includeOptional && snapshot.optionalDependencies) for (const [depName, depVersion] of Object.entries(snapshot.optionalDependencies)) {
160
+ if (depVersion.startsWith("link:")) continue;
108
161
  const depKey = snapshotKey(depName, depVersion);
109
162
  if (!keepSnapshots.has(depKey)) queue.push(depKey);
110
163
  }
111
164
  }
112
165
  }
113
166
  const dependencies = {};
114
- for (const name of packageNames) dependencies[name] = parseSnapshotKey(snapshotKey(name, rootDeps[name])).version;
167
+ for (const name of packageNames) dependencies[name] = rootDeps[name].specifier;
115
168
  const subsetPackages = {};
116
169
  for (const key of keepPackages) if (lockfile.packages[key]) subsetPackages[key] = lockfile.packages[key];
117
170
  const subsetSnapshots = {};
118
171
  for (const key of keepSnapshots) if (lockfile.snapshots[key]) subsetSnapshots[key] = lockfile.snapshots[key];
119
172
  const subsetImporter = { dependencies: {} };
120
173
  for (const name of packageNames) subsetImporter.dependencies[name] = {
121
- specifier: dependencies[name],
122
- version: rootDeps[name]
174
+ specifier: rootDeps[name].specifier,
175
+ version: rootDeps[name].version
123
176
  };
124
177
  const collected = [...keepPackages].map((key) => {
125
178
  const parsed = parseSnapshotKey(key);
@@ -148,14 +201,15 @@ async function extractPnpmSubset({ projectPath, packageNames, includeOptional =
148
201
  //#endregion
149
202
  //#region src/extract-yarn.ts
150
203
  const { parse: parseYarnLockV1, stringify: stringifyYarnLockV1 } = createRequire(import.meta.url)("@yarnpkg/lockfile");
204
+ const OUTPUT_PACKAGE_NAME = "lockfile-subset-output";
151
205
  function detectYarnVersion(content) {
152
206
  return content.includes("# yarn lockfile v1") ? 1 : 2;
153
207
  }
154
- function extractV1({ projectPath, packageNames, includeOptional, lockfileContent }) {
208
+ function extractV1({ projectPath, packageNames, includeOptional, lockfileContent, workspacePath }) {
155
209
  const parsed = parseYarnLockV1(lockfileContent);
156
210
  if (parsed.type !== "success") throw new Error(`Failed to parse yarn.lock: ${parsed.type}`);
157
211
  const lockfile = parsed.object;
158
- const pkgJsonPath = join(projectPath, "package.json");
212
+ const pkgJsonPath = join(projectPath, workspacePath, "package.json");
159
213
  const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
160
214
  const allDeps = {
161
215
  ...pkgJson.dependencies,
@@ -166,6 +220,7 @@ function extractV1({ projectPath, packageNames, includeOptional, lockfileContent
166
220
  for (const name of packageNames) {
167
221
  const range = allDeps[name];
168
222
  if (!range) throw new Error(`Package "${name}" not found in yarn.lock`);
223
+ if (!lockfile[`${name}@${range}`]) throw new Error(`Package "${name}@${range}" has no lockfile entry. It is likely a workspace dependency, not a published package.`);
169
224
  const queue = [`${name}@${range}`];
170
225
  while (queue.length > 0) {
171
226
  const key = queue.shift();
@@ -190,7 +245,7 @@ function extractV1({ projectPath, packageNames, includeOptional, lockfileContent
190
245
  const subset = {};
191
246
  for (const key of keepKeys) subset[key] = lockfile[key];
192
247
  const dependencies = {};
193
- for (const name of packageNames) dependencies[name] = lockfile[`${name}@${allDeps[name]}`].version;
248
+ for (const name of packageNames) dependencies[name] = allDeps[name];
194
249
  const seen = /* @__PURE__ */ new Set();
195
250
  const deduped = collected.filter((c) => {
196
251
  const key = `${c.name}@${c.version}`;
@@ -202,7 +257,7 @@ function extractV1({ projectPath, packageNames, includeOptional, lockfileContent
202
257
  type: "yarn",
203
258
  yarnVersion: 1,
204
259
  packageJson: {
205
- name: "lockfile-subset-output",
260
+ name: OUTPUT_PACKAGE_NAME,
206
261
  version: "1.0.0",
207
262
  dependencies
208
263
  },
@@ -210,7 +265,7 @@ function extractV1({ projectPath, packageNames, includeOptional, lockfileContent
210
265
  collected: deduped
211
266
  };
212
267
  }
213
- function extractBerry({ projectPath, packageNames, includeOptional, lockfileContent }) {
268
+ function extractBerry({ projectPath, packageNames, includeOptional, lockfileContent, workspacePath }) {
214
269
  const lockfile = yaml.load(lockfileContent);
215
270
  const descriptorMap = /* @__PURE__ */ new Map();
216
271
  for (const [compoundKey, entry] of Object.entries(lockfile)) {
@@ -221,7 +276,7 @@ function extractBerry({ projectPath, packageNames, includeOptional, lockfileCont
221
276
  originalKey: compoundKey
222
277
  });
223
278
  }
224
- const pkgJsonPath = join(projectPath, "package.json");
279
+ const pkgJsonPath = join(projectPath, workspacePath, "package.json");
225
280
  const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
226
281
  const allDeps = {
227
282
  ...pkgJson.dependencies,
@@ -233,6 +288,7 @@ function extractBerry({ projectPath, packageNames, includeOptional, lockfileCont
233
288
  for (const name of packageNames) {
234
289
  const range = allDeps[name];
235
290
  if (!range) throw new Error(`Package "${name}" not found in yarn.lock`);
291
+ if (range.startsWith("workspace:")) throw new Error(`Package "${name}" resolves to a workspace, not a published package`);
236
292
  const queue = [`${name}@npm:${range}`];
237
293
  while (queue.length > 0) {
238
294
  const desc = queue.shift();
@@ -246,20 +302,19 @@ function extractBerry({ projectPath, packageNames, includeOptional, lockfileCont
246
302
  version: match.entry.version
247
303
  });
248
304
  if (match.entry.dependencies) for (const [depName, depRange] of Object.entries(match.entry.dependencies)) {
305
+ if (depRange.startsWith("workspace:")) continue;
249
306
  const depDesc = `${depName}@${depRange}`;
250
307
  if (!visited.has(depDesc)) queue.push(depDesc);
251
308
  }
252
309
  if (includeOptional && match.entry.optionalDependencies) for (const [depName, depRange] of Object.entries(match.entry.optionalDependencies)) {
310
+ if (depRange.startsWith("workspace:")) continue;
253
311
  const depDesc = `${depName}@${depRange}`;
254
312
  if (!visited.has(depDesc)) queue.push(depDesc);
255
313
  }
256
314
  }
257
315
  }
258
316
  const dependencies = {};
259
- for (const name of packageNames) {
260
- const descriptor = `${name}@npm:${allDeps[name]}`;
261
- dependencies[name] = descriptorMap.get(descriptor).entry.version;
262
- }
317
+ for (const name of packageNames) dependencies[name] = allDeps[name];
263
318
  const lines = [];
264
319
  lines.push("# This file is generated by running \"yarn install\" inside your project.");
265
320
  lines.push("# Manual changes might be lost - proceed with caution!");
@@ -271,7 +326,22 @@ function extractBerry({ projectPath, packageNames, includeOptional, lockfileCont
271
326
  if (metadata.cacheKey) lines.push(` cacheKey: ${metadata.cacheKey}`);
272
327
  lines.push("");
273
328
  }
274
- for (const originalKey of keepOriginalKeys) {
329
+ const rootWorkspaceKey = `${OUTPUT_PACKAGE_NAME}@workspace:.`;
330
+ const sortedKeys = [...keepOriginalKeys, rootWorkspaceKey].sort();
331
+ for (const originalKey of sortedKeys) {
332
+ if (originalKey === rootWorkspaceKey) {
333
+ lines.push(`"${originalKey}":`);
334
+ lines.push(" version: 0.0.0-use.local");
335
+ lines.push(` resolution: "${originalKey}"`);
336
+ if (Object.keys(dependencies).length > 0) {
337
+ lines.push(" dependencies:");
338
+ for (const [k, v] of Object.entries(dependencies)) lines.push(` ${k}: "npm:${v}"`);
339
+ }
340
+ lines.push(" languageName: unknown");
341
+ lines.push(" linkType: soft");
342
+ lines.push("");
343
+ continue;
344
+ }
275
345
  const entry = lockfile[originalKey];
276
346
  lines.push(`"${originalKey}":`);
277
347
  lines.push(` version: ${entry.version}`);
@@ -312,7 +382,7 @@ function extractBerry({ projectPath, packageNames, includeOptional, lockfileCont
312
382
  type: "yarn",
313
383
  yarnVersion: 2,
314
384
  packageJson: {
315
- name: "lockfile-subset-output",
385
+ name: OUTPUT_PACKAGE_NAME,
316
386
  version: "1.0.0",
317
387
  dependencies
318
388
  },
@@ -328,19 +398,23 @@ function parseDescriptorName(descriptor) {
328
398
  if (lastAt > 0) return descriptor.slice(0, lastAt);
329
399
  return descriptor;
330
400
  }
331
- async function extractYarnSubset({ projectPath, packageNames, includeOptional = true }) {
401
+ async function extractYarnSubset({ projectPath, packageNames, includeOptional = true, workspacePath = "." }) {
332
402
  const lockfileContent = readFileSync(join(projectPath, "yarn.lock"), "utf8");
333
- if (detectYarnVersion(lockfileContent) === 1) return extractV1({
403
+ const version = detectYarnVersion(lockfileContent);
404
+ const normalizedWorkspace = normalizeWorkspacePath(workspacePath);
405
+ if (version === 1) return extractV1({
334
406
  projectPath,
335
407
  packageNames,
336
408
  includeOptional,
337
- lockfileContent
409
+ lockfileContent,
410
+ workspacePath: normalizedWorkspace
338
411
  });
339
412
  else return extractBerry({
340
413
  projectPath,
341
414
  packageNames,
342
415
  includeOptional,
343
- lockfileContent
416
+ lockfileContent,
417
+ workspacePath: normalizedWorkspace
344
418
  });
345
419
  }
346
420
  //#endregion
@@ -411,37 +485,48 @@ function parseArgs(argv) {
411
485
  }
412
486
  return args;
413
487
  }
488
+ const LOCKFILE_BASENAMES = {
489
+ "pnpm-lock.yaml": "pnpm",
490
+ "yarn.lock": "yarn",
491
+ "package-lock.json": "npm"
492
+ };
493
+ /** Walk up from `start` looking for any known lockfile. Returns null if none found. */
494
+ function findLockfileUpwards(start) {
495
+ let dir = start;
496
+ while (true) {
497
+ for (const [name, type] of Object.entries(LOCKFILE_BASENAMES)) if (existsSync(resolve(dir, name))) return {
498
+ projectPath: dir,
499
+ type
500
+ };
501
+ const parent = dirname(dir);
502
+ if (parent === dir) return null;
503
+ dir = parent;
504
+ }
505
+ }
414
506
  function resolveLockfile(lockfilePath) {
415
507
  if (!lockfilePath) {
416
- if (existsSync(resolve("pnpm-lock.yaml"))) return {
417
- projectPath: resolve("."),
418
- type: "pnpm"
419
- };
420
- if (existsSync(resolve("yarn.lock"))) return {
421
- projectPath: resolve("."),
422
- type: "yarn"
423
- };
424
- if (existsSync(resolve("package-lock.json"))) return {
425
- projectPath: resolve("."),
426
- type: "npm"
427
- };
428
- throw new Error("No lockfile found in current directory. Expected package-lock.json, pnpm-lock.yaml, or yarn.lock.");
508
+ const found = findLockfileUpwards(resolve("."));
509
+ if (found) return found;
510
+ throw new Error("No lockfile found in current directory or any parent. Expected package-lock.json, pnpm-lock.yaml, or yarn.lock.");
429
511
  }
430
512
  const resolved = resolve(lockfilePath);
431
- const basename = resolved.split("/").pop();
432
- if (basename === "pnpm-lock.yaml") return {
433
- projectPath: resolve(resolved, ".."),
434
- type: "pnpm"
435
- };
436
- if (basename === "yarn.lock") return {
437
- projectPath: resolve(resolved, ".."),
438
- type: "yarn"
439
- };
440
- if (basename === "package-lock.json") return {
441
- projectPath: resolve(resolved, ".."),
442
- type: "npm"
513
+ const type = LOCKFILE_BASENAMES[basename(resolved)];
514
+ if (!type) throw new Error(`Invalid lockfile path: ${lockfilePath}. Expected a path to package-lock.json, pnpm-lock.yaml, or yarn.lock.`);
515
+ return {
516
+ projectPath: dirname(resolved),
517
+ type
443
518
  };
444
- throw new Error(`Invalid lockfile path: ${lockfilePath}. Expected a path to package-lock.json, pnpm-lock.yaml, or yarn.lock.`);
519
+ }
520
+ /**
521
+ * Resolve the workspace path (relative to projectPath, forward slashes).
522
+ * Inferred from process.cwd() vs the lockfile's project directory: if cwd
523
+ * sits inside a sub-workspace, that path is used; otherwise "." (root).
524
+ */
525
+ function resolveWorkspacePath(projectPath) {
526
+ const rel = relative(projectPath, resolve("."));
527
+ if (rel === "" || rel === ".") return ".";
528
+ if (rel.startsWith("..")) return ".";
529
+ return rel.split(sep).join("/");
445
530
  }
446
531
  const HELP = `
447
532
  lockfile-subset <packages...> [options]
@@ -453,7 +538,7 @@ Arguments:
453
538
  packages Package names to extract (one or more, space-separated)
454
539
 
455
540
  Options:
456
- --lockfile, -l <path> Path to lockfile (auto-detected from cwd by default)
541
+ --lockfile, -l <path> Path to lockfile (auto-detected by walking up from cwd)
457
542
  --output, -o <dir> Output directory (default: ./lockfile-subset-output)
458
543
  --no-optional Exclude optional dependencies
459
544
  --install Run npm ci / pnpm install / yarn install after generating
@@ -467,6 +552,11 @@ Examples:
467
552
  lockfile-subset @prisma/client sharp -l /build/package-lock.json
468
553
  lockfile-subset @prisma/client sharp -l pnpm-lock.yaml --install
469
554
  lockfile-subset chalk --dry-run
555
+
556
+ Monorepos: cd into the target workspace and run as usual.
557
+ The lockfile is found by walking up from the current directory, and the
558
+ sub-workspace is inferred from cwd relative to the lockfile.
559
+ cd apps/web && lockfile-subset next
470
560
  `.trim();
471
561
  async function main() {
472
562
  const args = parseArgs(process.argv.slice(2));
@@ -484,22 +574,27 @@ async function main() {
484
574
  process.exit(1);
485
575
  }
486
576
  const { projectPath, type } = resolveLockfile(args.lockfile);
577
+ const workspacePath = resolveWorkspacePath(projectPath);
487
578
  const outputDir = resolve(args.output);
579
+ if (workspacePath !== ".") console.log(`Using workspace: ${workspacePath} (lockfile root: ${projectPath})`);
488
580
  let result;
489
581
  if (type === "pnpm") result = await extractPnpmSubset({
490
582
  projectPath,
491
583
  packageNames: args.packages,
492
- includeOptional: args.includeOptional
584
+ includeOptional: args.includeOptional,
585
+ workspacePath
493
586
  });
494
587
  else if (type === "yarn") result = await extractYarnSubset({
495
588
  projectPath,
496
589
  packageNames: args.packages,
497
- includeOptional: args.includeOptional
590
+ includeOptional: args.includeOptional,
591
+ workspacePath
498
592
  });
499
593
  else result = await extractSubset({
500
594
  projectPath,
501
595
  packageNames: args.packages,
502
- includeOptional: args.includeOptional
596
+ includeOptional: args.includeOptional,
597
+ workspacePath
503
598
  });
504
599
  console.log(`Collected ${result.collected.length} packages (${args.packages.length} direct, ${result.collected.length - args.packages.length} transitive)`);
505
600
  if (args.dryRun) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lockfile-subset",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
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": {