lockfile-subset 1.3.1 → 1.4.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.
package/README.md CHANGED
@@ -46,6 +46,10 @@ lockfile-subset @prisma/client sharp -o /standalone --install
46
46
 
47
47
  # Preview without writing files
48
48
  lockfile-subset chalk --dry-run
49
+
50
+ # esbuild-style wildcards (single "*") match against direct dependencies.
51
+ # Quote them so the shell doesn't expand them.
52
+ lockfile-subset '@aws-sdk/*' sharp
49
53
  ```
50
54
 
51
55
  The lockfile type (npm, pnpm, or yarn) is auto-detected from the project directory. This generates a minimal `package.json` and lockfile in the output directory. Then run `npm ci`, `pnpm install --frozen-lockfile`, or `yarn install --frozen-lockfile` to install exactly those packages.
package/dist/index.mjs CHANGED
@@ -15,6 +15,47 @@ function normalizeWorkspacePath(p) {
15
15
  return p.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
16
16
  }
17
17
  //#endregion
18
+ //#region src/wildcard.ts
19
+ /**
20
+ * esbuild-style wildcard matching for package names. A pattern may contain
21
+ * at most one `*`, which matches any (possibly empty) substring. Patterns
22
+ * without `*` are treated as exact names.
23
+ */
24
+ function matchesPattern(pattern, name) {
25
+ const star = pattern.indexOf("*");
26
+ if (star === -1) return pattern === name;
27
+ if (pattern.indexOf("*", star + 1) !== -1) throw new Error(`Pattern "${pattern}" has more than one "*"; only one wildcard is supported.`);
28
+ const prefix = pattern.slice(0, star);
29
+ const suffix = pattern.slice(star + 1);
30
+ return name.length >= prefix.length + suffix.length && name.startsWith(prefix) && name.endsWith(suffix);
31
+ }
32
+ /**
33
+ * Expand wildcard patterns against the set of available (root-level) package
34
+ * names. Literal patterns pass through unchanged. Wildcard patterns that
35
+ * match nothing throw, so typos surface as errors rather than silent no-ops.
36
+ */
37
+ function expandWildcards(patterns, available) {
38
+ const availableArr = [...available];
39
+ const result = [];
40
+ const seen = /* @__PURE__ */ new Set();
41
+ for (const pattern of patterns) {
42
+ if (!pattern.includes("*")) {
43
+ if (!seen.has(pattern)) {
44
+ seen.add(pattern);
45
+ result.push(pattern);
46
+ }
47
+ continue;
48
+ }
49
+ const matched = availableArr.filter((name) => matchesPattern(pattern, name));
50
+ if (matched.length === 0) throw new Error(`Pattern "${pattern}" did not match any direct dependency.`);
51
+ for (const name of matched) if (!seen.has(name)) {
52
+ seen.add(name);
53
+ result.push(name);
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+ //#endregion
18
59
  //#region src/extract.ts
19
60
  /**
20
61
  * Rewrite a package-lock.json location key so the output is a standalone project.
@@ -46,8 +87,11 @@ async function extractSubset({ projectPath, packageNames, includeOptional = true
46
87
  }
47
88
  startNode = found;
48
89
  }
90
+ const nonDevDirectDeps = [];
91
+ for (const [name, edge] of startNode.edgesOut) if (edge.type !== "dev") nonDevDirectDeps.push(name);
92
+ const resolvedNames = expandWildcards(packageNames, nonDevDirectDeps);
49
93
  const keep = /* @__PURE__ */ new Set();
50
- for (const name of packageNames) {
94
+ for (const name of resolvedNames) {
51
95
  const edge = startNode.edgesOut.get(name);
52
96
  if (!edge?.to) throw new Error(`Package "${name}" not found in lockfile`);
53
97
  if (edge.type === "workspace" || edge.to.isWorkspace) throw new Error(`Package "${name}" resolves to a workspace, not a published package`);
@@ -66,7 +110,7 @@ async function extractSubset({ projectPath, packageNames, includeOptional = true
66
110
  }
67
111
  }
68
112
  const dependencies = {};
69
- for (const name of packageNames) dependencies[name] = startNode.edgesOut.get(name).to.version;
113
+ for (const name of resolvedNames) dependencies[name] = startNode.edgesOut.get(name).to.version;
70
114
  const subsetPackages = {};
71
115
  subsetPackages[""] = {
72
116
  name: "lockfile-subset-output",
@@ -136,9 +180,10 @@ async function extractPnpmSubset({ projectPath, packageNames, includeOptional =
136
180
  version: dep.version
137
181
  };
138
182
  }
183
+ const resolvedNames = expandWildcards(packageNames, Object.keys(rootDeps));
139
184
  const keepSnapshots = /* @__PURE__ */ new Set();
140
185
  const keepPackages = /* @__PURE__ */ new Set();
141
- for (const name of packageNames) {
186
+ for (const name of resolvedNames) {
142
187
  const dep = rootDeps[name];
143
188
  if (!dep) throw new Error(`Package "${name}" not found in pnpm-lock.yaml`);
144
189
  if (dep.version.startsWith("link:")) throw new Error(`Package "${name}" resolves to a workspace (${dep.version}), not a published package`);
@@ -164,13 +209,13 @@ async function extractPnpmSubset({ projectPath, packageNames, includeOptional =
164
209
  }
165
210
  }
166
211
  const dependencies = {};
167
- for (const name of packageNames) dependencies[name] = rootDeps[name].specifier;
212
+ for (const name of resolvedNames) dependencies[name] = rootDeps[name].specifier;
168
213
  const subsetPackages = {};
169
214
  for (const key of keepPackages) if (lockfile.packages[key]) subsetPackages[key] = lockfile.packages[key];
170
215
  const subsetSnapshots = {};
171
216
  for (const key of keepSnapshots) if (lockfile.snapshots[key]) subsetSnapshots[key] = lockfile.snapshots[key];
172
217
  const subsetImporter = { dependencies: {} };
173
- for (const name of packageNames) subsetImporter.dependencies[name] = {
218
+ for (const name of resolvedNames) subsetImporter.dependencies[name] = {
174
219
  specifier: rootDeps[name].specifier,
175
220
  version: rootDeps[name].version
176
221
  };
@@ -215,9 +260,10 @@ function extractV1({ projectPath, packageNames, includeOptional, lockfileContent
215
260
  ...pkgJson.dependencies,
216
261
  ...pkgJson.optionalDependencies
217
262
  };
263
+ const resolvedNames = expandWildcards(packageNames, Object.keys(allDeps));
218
264
  const keepKeys = /* @__PURE__ */ new Set();
219
265
  const collected = [];
220
- for (const name of packageNames) {
266
+ for (const name of resolvedNames) {
221
267
  const range = allDeps[name];
222
268
  if (!range) throw new Error(`Package "${name}" not found in yarn.lock`);
223
269
  if (!lockfile[`${name}@${range}`]) throw new Error(`Package "${name}@${range}" has no lockfile entry. It is likely a workspace dependency, not a published package.`);
@@ -245,7 +291,7 @@ function extractV1({ projectPath, packageNames, includeOptional, lockfileContent
245
291
  const subset = {};
246
292
  for (const key of keepKeys) subset[key] = lockfile[key];
247
293
  const dependencies = {};
248
- for (const name of packageNames) dependencies[name] = allDeps[name];
294
+ for (const name of resolvedNames) dependencies[name] = allDeps[name];
249
295
  const seen = /* @__PURE__ */ new Set();
250
296
  const deduped = collected.filter((c) => {
251
297
  const key = `${c.name}@${c.version}`;
@@ -282,10 +328,11 @@ function extractBerry({ projectPath, packageNames, includeOptional, lockfileCont
282
328
  ...pkgJson.dependencies,
283
329
  ...pkgJson.optionalDependencies
284
330
  };
331
+ const resolvedNames = expandWildcards(packageNames, Object.keys(allDeps));
285
332
  const keepOriginalKeys = /* @__PURE__ */ new Set();
286
333
  const visited = /* @__PURE__ */ new Set();
287
334
  const collected = [];
288
- for (const name of packageNames) {
335
+ for (const name of resolvedNames) {
289
336
  const range = allDeps[name];
290
337
  if (!range) throw new Error(`Package "${name}" not found in yarn.lock`);
291
338
  if (range.startsWith("workspace:")) throw new Error(`Package "${name}" resolves to a workspace, not a published package`);
@@ -314,7 +361,7 @@ function extractBerry({ projectPath, packageNames, includeOptional, lockfileCont
314
361
  }
315
362
  }
316
363
  const dependencies = {};
317
- for (const name of packageNames) dependencies[name] = allDeps[name];
364
+ for (const name of resolvedNames) dependencies[name] = allDeps[name];
318
365
  const lines = [];
319
366
  lines.push("# This file is generated by running \"yarn install\" inside your project.");
320
367
  lines.push("# Manual changes might be lost - proceed with caution!");
@@ -535,7 +582,11 @@ Extract a subset of package-lock.json, pnpm-lock.yaml, or yarn.lock for specifie
535
582
  packages and their transitive dependencies.
536
583
 
537
584
  Arguments:
538
- packages Package names to extract (one or more, space-separated)
585
+ packages Package names to extract (one or more, space-separated).
586
+ esbuild-style wildcards (single "*") are expanded
587
+ against the workspace's direct dependencies, e.g.
588
+ "@aws-sdk/*" or "*-loader". Quote patterns to keep
589
+ the shell from globbing them.
539
590
 
540
591
  Options:
541
592
  --lockfile, -l <path> Path to lockfile (auto-detected by walking up from cwd)
@@ -552,6 +603,7 @@ Examples:
552
603
  lockfile-subset @prisma/client sharp -l /build/package-lock.json
553
604
  lockfile-subset @prisma/client sharp -l pnpm-lock.yaml --install
554
605
  lockfile-subset chalk --dry-run
606
+ lockfile-subset '@aws-sdk/*' sharp
555
607
 
556
608
  Monorepos: cd into the target workspace and run as usual.
557
609
  The lockfile is found by walking up from the current directory, and the
@@ -596,7 +648,8 @@ async function main() {
596
648
  includeOptional: args.includeOptional,
597
649
  workspacePath
598
650
  });
599
- console.log(`Collected ${result.collected.length} packages (${args.packages.length} direct, ${result.collected.length - args.packages.length} transitive)`);
651
+ const directCount = Object.keys(result.packageJson.dependencies).length;
652
+ console.log(`Collected ${result.collected.length} packages (${directCount} direct, ${result.collected.length - directCount} transitive)`);
600
653
  if (args.dryRun) {
601
654
  console.log("\n--- package.json ---");
602
655
  console.log(JSON.stringify(result.packageJson, null, 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lockfile-subset",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
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": {