lockfile-subset 1.3.0 → 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
  };
@@ -201,6 +246,7 @@ async function extractPnpmSubset({ projectPath, packageNames, includeOptional =
201
246
  //#endregion
202
247
  //#region src/extract-yarn.ts
203
248
  const { parse: parseYarnLockV1, stringify: stringifyYarnLockV1 } = createRequire(import.meta.url)("@yarnpkg/lockfile");
249
+ const OUTPUT_PACKAGE_NAME = "lockfile-subset-output";
204
250
  function detectYarnVersion(content) {
205
251
  return content.includes("# yarn lockfile v1") ? 1 : 2;
206
252
  }
@@ -214,9 +260,10 @@ function extractV1({ projectPath, packageNames, includeOptional, lockfileContent
214
260
  ...pkgJson.dependencies,
215
261
  ...pkgJson.optionalDependencies
216
262
  };
263
+ const resolvedNames = expandWildcards(packageNames, Object.keys(allDeps));
217
264
  const keepKeys = /* @__PURE__ */ new Set();
218
265
  const collected = [];
219
- for (const name of packageNames) {
266
+ for (const name of resolvedNames) {
220
267
  const range = allDeps[name];
221
268
  if (!range) throw new Error(`Package "${name}" not found in yarn.lock`);
222
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.`);
@@ -244,7 +291,7 @@ function extractV1({ projectPath, packageNames, includeOptional, lockfileContent
244
291
  const subset = {};
245
292
  for (const key of keepKeys) subset[key] = lockfile[key];
246
293
  const dependencies = {};
247
- for (const name of packageNames) dependencies[name] = allDeps[name];
294
+ for (const name of resolvedNames) dependencies[name] = allDeps[name];
248
295
  const seen = /* @__PURE__ */ new Set();
249
296
  const deduped = collected.filter((c) => {
250
297
  const key = `${c.name}@${c.version}`;
@@ -256,7 +303,7 @@ function extractV1({ projectPath, packageNames, includeOptional, lockfileContent
256
303
  type: "yarn",
257
304
  yarnVersion: 1,
258
305
  packageJson: {
259
- name: "lockfile-subset-output",
306
+ name: OUTPUT_PACKAGE_NAME,
260
307
  version: "1.0.0",
261
308
  dependencies
262
309
  },
@@ -281,10 +328,11 @@ function extractBerry({ projectPath, packageNames, includeOptional, lockfileCont
281
328
  ...pkgJson.dependencies,
282
329
  ...pkgJson.optionalDependencies
283
330
  };
331
+ const resolvedNames = expandWildcards(packageNames, Object.keys(allDeps));
284
332
  const keepOriginalKeys = /* @__PURE__ */ new Set();
285
333
  const visited = /* @__PURE__ */ new Set();
286
334
  const collected = [];
287
- for (const name of packageNames) {
335
+ for (const name of resolvedNames) {
288
336
  const range = allDeps[name];
289
337
  if (!range) throw new Error(`Package "${name}" not found in yarn.lock`);
290
338
  if (range.startsWith("workspace:")) throw new Error(`Package "${name}" resolves to a workspace, not a published package`);
@@ -313,7 +361,7 @@ function extractBerry({ projectPath, packageNames, includeOptional, lockfileCont
313
361
  }
314
362
  }
315
363
  const dependencies = {};
316
- for (const name of packageNames) dependencies[name] = allDeps[name];
364
+ for (const name of resolvedNames) dependencies[name] = allDeps[name];
317
365
  const lines = [];
318
366
  lines.push("# This file is generated by running \"yarn install\" inside your project.");
319
367
  lines.push("# Manual changes might be lost - proceed with caution!");
@@ -325,7 +373,22 @@ function extractBerry({ projectPath, packageNames, includeOptional, lockfileCont
325
373
  if (metadata.cacheKey) lines.push(` cacheKey: ${metadata.cacheKey}`);
326
374
  lines.push("");
327
375
  }
328
- for (const originalKey of keepOriginalKeys) {
376
+ const rootWorkspaceKey = `${OUTPUT_PACKAGE_NAME}@workspace:.`;
377
+ const sortedKeys = [...keepOriginalKeys, rootWorkspaceKey].sort();
378
+ for (const originalKey of sortedKeys) {
379
+ if (originalKey === rootWorkspaceKey) {
380
+ lines.push(`"${originalKey}":`);
381
+ lines.push(" version: 0.0.0-use.local");
382
+ lines.push(` resolution: "${originalKey}"`);
383
+ if (Object.keys(dependencies).length > 0) {
384
+ lines.push(" dependencies:");
385
+ for (const [k, v] of Object.entries(dependencies)) lines.push(` ${k}: "npm:${v}"`);
386
+ }
387
+ lines.push(" languageName: unknown");
388
+ lines.push(" linkType: soft");
389
+ lines.push("");
390
+ continue;
391
+ }
329
392
  const entry = lockfile[originalKey];
330
393
  lines.push(`"${originalKey}":`);
331
394
  lines.push(` version: ${entry.version}`);
@@ -366,7 +429,7 @@ function extractBerry({ projectPath, packageNames, includeOptional, lockfileCont
366
429
  type: "yarn",
367
430
  yarnVersion: 2,
368
431
  packageJson: {
369
- name: "lockfile-subset-output",
432
+ name: OUTPUT_PACKAGE_NAME,
370
433
  version: "1.0.0",
371
434
  dependencies
372
435
  },
@@ -519,7 +582,11 @@ Extract a subset of package-lock.json, pnpm-lock.yaml, or yarn.lock for specifie
519
582
  packages and their transitive dependencies.
520
583
 
521
584
  Arguments:
522
- 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.
523
590
 
524
591
  Options:
525
592
  --lockfile, -l <path> Path to lockfile (auto-detected by walking up from cwd)
@@ -536,6 +603,7 @@ Examples:
536
603
  lockfile-subset @prisma/client sharp -l /build/package-lock.json
537
604
  lockfile-subset @prisma/client sharp -l pnpm-lock.yaml --install
538
605
  lockfile-subset chalk --dry-run
606
+ lockfile-subset '@aws-sdk/*' sharp
539
607
 
540
608
  Monorepos: cd into the target workspace and run as usual.
541
609
  The lockfile is found by walking up from the current directory, and the
@@ -580,7 +648,8 @@ async function main() {
580
648
  includeOptional: args.includeOptional,
581
649
  workspacePath
582
650
  });
583
- 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)`);
584
653
  if (args.dryRun) {
585
654
  console.log("\n--- package.json ---");
586
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.0",
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": {