lockfile-subset 1.0.2 → 1.1.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 (3) hide show
  1. package/README.md +20 -24
  2. package/dist/index.mjs +160 -20
  3. package/package.json +5 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # lockfile-subset
2
2
 
3
- Extract a subset of `package-lock.json` for specified packages and their transitive dependencies.
3
+ Extract a subset of `package-lock.json` or `pnpm-lock.yaml` for specified packages and their transitive dependencies.
4
4
 
5
5
  ## Why?
6
6
 
@@ -11,8 +11,9 @@ When using bundlers like esbuild with `--external`, you need to ship those exter
11
11
  | Manually copy `node_modules` dirs | Breaks when transitive deps change (e.g., Prisma v6 added new deps) |
12
12
  | `npm install <pkg>` in runner stage | Resolves versions independently — may differ from your lockfile |
13
13
  | `npm ci --omit=dev` | Installs *all* prod dependencies, not just the ones you need |
14
+ | `pnpm deploy` | Only works with workspaces, not arbitrary packages |
14
15
 
15
- **lockfile-subset** solves this by extracting a precise subset from your existing `package-lock.json` — only the packages you specify and their transitive dependencies, with versions exactly matching the original lockfile.
16
+ **lockfile-subset** solves this by extracting a precise subset from your existing lockfile — only the packages you specify and their transitive dependencies, with versions exactly matching the original lockfile.
16
17
 
17
18
  ## Install
18
19
 
@@ -32,16 +33,19 @@ lockfile-subset @prisma/client sharp
32
33
  lockfile-subset @prisma/client sharp -o /standalone
33
34
 
34
35
  # Use a different lockfile path
35
- lockfile-subset @prisma/client sharp --lockfile /build/package-lock.json
36
+ lockfile-subset @prisma/client sharp -l /build/package-lock.json
37
+
38
+ # Use a pnpm lockfile
39
+ lockfile-subset @prisma/client sharp -l pnpm-lock.yaml
36
40
 
37
41
  # Generate + install in one step
38
42
  lockfile-subset @prisma/client sharp -o /standalone --install
39
43
 
40
44
  # Preview without writing files
41
- lockfile-subset prisma --dry-run
45
+ lockfile-subset chalk --dry-run
42
46
  ```
43
47
 
44
- This generates a minimal `package.json` and `package-lock.json` in the output directory. Then run `npm ci` to install exactly those packages at the exact versions from your original lockfile.
48
+ The lockfile type (npm or pnpm) is auto-detected from the project directory. This generates a minimal `package.json` and lockfile in the output directory. Then run `npm ci` or `pnpm install --frozen-lockfile` to install exactly those packages.
45
49
 
46
50
  ### Dockerfile example
47
51
 
@@ -73,35 +77,27 @@ CMD ["node", "dist/index.js"]
73
77
 
74
78
  ### Options
75
79
 
76
- ```
77
- lockfile-subset <packages...> [options]
78
-
79
- Arguments:
80
- packages Package names to extract (space-separated)
81
-
82
- Options:
83
- --lockfile, -l <path> Path to project directory (default: .)
84
- --output, -o <dir> Output directory (default: ./lockfile-subset-output)
85
- --no-optional Exclude optional dependencies
86
- --install Run npm ci after generating the subset
87
- --dry-run Print the result without writing files
88
- --version, -v Show version
89
- --help, -h Show help
90
- ```
80
+ Run `lockfile-subset --help` for the full list of options.
91
81
 
92
82
  ## How it works
93
83
 
94
- 1. Loads your `package-lock.json` using [`@npmcli/arborist`](https://github.com/npm/cli/tree/latest/workspaces/arborist) (npm's own dependency resolver)
84
+ 1. Loads your lockfile (`package-lock.json` via [@npmcli/arborist](https://github.com/npm/cli/tree/latest/workspaces/arborist), or `pnpm-lock.yaml` directly)
95
85
  2. Starting from the specified packages, walks the dependency tree via BFS to collect all transitive dependencies
96
86
  3. Copies the matching entries from the original lockfile — no re-resolution, no version drift
97
- 4. Outputs a minimal `package.json` + `package-lock.json` ready for `npm ci`
87
+ 4. Outputs a minimal `package.json` + lockfile ready for `npm ci` or `pnpm install --frozen-lockfile`
98
88
 
99
89
  Dev dependencies of each package are excluded from traversal. Optional dependencies are included by default (use `--no-optional` to exclude).
100
90
 
91
+ ## Supported lockfile formats
92
+
93
+ | Package manager | Lockfile | Supported versions |
94
+ |---|---|---|
95
+ | npm | `package-lock.json` | v2 (npm 7-8), v3 (npm 9+) |
96
+ | pnpm | `pnpm-lock.yaml` | v9 (pnpm 9-10) |
97
+
101
98
  ## Limitations
102
99
 
103
- - **Lockfile v2/v3 only** — Requires npm 7+ (lockfile v2 or v3). The legacy v1 format (npm 5-6) is not supported.
104
- - **npm only** — pnpm and yarn have different lockfile formats. pnpm users can use `pnpm deploy`; yarn users can use `yarn workspaces focus`.
100
+ - **yarn is not supported** — yarn users can use `yarn workspaces focus`.
105
101
  - **Platform-specific optional deps** — Packages like `sharp` have OS/arch-specific optional dependencies (e.g., `@img/sharp-linux-x64`). If your lockfile was generated on macOS but you run `npm ci` on Linux (e.g., in Docker), those Linux-specific packages may be missing from the lockfile. In that case, generate the lockfile on the target platform, or use `npm install` instead of `npm ci`.
106
102
 
107
103
  ## License
package/dist/index.mjs CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { join, resolve } from "path";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
4
  import { execSync } from "child_process";
4
5
  import { createRequire } from "module";
5
6
  import Arborist from "@npmcli/arborist";
6
- import { mkdirSync, writeFileSync } from "fs";
7
+ import yaml from "js-yaml";
7
8
  //#region src/extract.ts
8
9
  async function extractSubset({ projectPath, packageNames, includeOptional = true }) {
9
10
  const tree = await new Arborist({ path: projectPath }).loadVirtual();
@@ -44,6 +45,7 @@ async function extractSubset({ projectPath, packageNames, includeOptional = true
44
45
  location: node.location
45
46
  }));
46
47
  return {
48
+ type: "npm",
47
49
  packageJson: {
48
50
  name: "lockfile-subset-output",
49
51
  version: "1.0.0",
@@ -60,11 +62,101 @@ async function extractSubset({ projectPath, packageNames, includeOptional = true
60
62
  };
61
63
  }
62
64
  //#endregion
65
+ //#region src/extract-pnpm.ts
66
+ /** Parse "name@version" or "@scope/name@version" into [name, version] */
67
+ function parseSnapshotKey(key) {
68
+ const withoutPeers = key.replace(/\(.*\)$/, "");
69
+ const lastAt = withoutPeers.lastIndexOf("@");
70
+ if (lastAt <= 0) throw new Error(`Invalid snapshot key: ${key}`);
71
+ return {
72
+ name: withoutPeers.slice(0, lastAt),
73
+ version: withoutPeers.slice(lastAt + 1)
74
+ };
75
+ }
76
+ /** Build snapshot key from name and version */
77
+ function snapshotKey(name, version) {
78
+ return `${name}@${version}`;
79
+ }
80
+ async function extractPnpmSubset({ projectPath, packageNames, includeOptional = true }) {
81
+ const content = readFileSync(join(projectPath, "pnpm-lock.yaml"), "utf8");
82
+ const lockfile = yaml.load(content);
83
+ 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");
86
+ 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;
89
+ const keepSnapshots = /* @__PURE__ */ new Set();
90
+ const keepPackages = /* @__PURE__ */ new Set();
91
+ 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)];
95
+ while (queue.length > 0) {
96
+ const current = queue.shift();
97
+ if (keepSnapshots.has(current)) continue;
98
+ keepSnapshots.add(current);
99
+ const parsed = parseSnapshotKey(current);
100
+ keepPackages.add(snapshotKey(parsed.name, parsed.version));
101
+ const snapshot = lockfile.snapshots[current];
102
+ if (!snapshot) continue;
103
+ if (snapshot.dependencies) for (const [depName, depVersion] of Object.entries(snapshot.dependencies)) {
104
+ const depKey = snapshotKey(depName, depVersion);
105
+ if (!keepSnapshots.has(depKey)) queue.push(depKey);
106
+ }
107
+ if (includeOptional && snapshot.optionalDependencies) for (const [depName, depVersion] of Object.entries(snapshot.optionalDependencies)) {
108
+ const depKey = snapshotKey(depName, depVersion);
109
+ if (!keepSnapshots.has(depKey)) queue.push(depKey);
110
+ }
111
+ }
112
+ }
113
+ const dependencies = {};
114
+ for (const name of packageNames) dependencies[name] = parseSnapshotKey(snapshotKey(name, rootDeps[name])).version;
115
+ const subsetPackages = {};
116
+ for (const key of keepPackages) if (lockfile.packages[key]) subsetPackages[key] = lockfile.packages[key];
117
+ const subsetSnapshots = {};
118
+ for (const key of keepSnapshots) if (lockfile.snapshots[key]) subsetSnapshots[key] = lockfile.snapshots[key];
119
+ const subsetImporter = { dependencies: {} };
120
+ for (const name of packageNames) subsetImporter.dependencies[name] = {
121
+ specifier: dependencies[name],
122
+ version: rootDeps[name]
123
+ };
124
+ const collected = [...keepPackages].map((key) => {
125
+ const parsed = parseSnapshotKey(key);
126
+ return {
127
+ name: parsed.name,
128
+ version: parsed.version
129
+ };
130
+ });
131
+ return {
132
+ type: "pnpm",
133
+ packageJson: {
134
+ name: "lockfile-subset-output",
135
+ version: "1.0.0",
136
+ dependencies
137
+ },
138
+ lockfileYaml: {
139
+ lockfileVersion: lockfile.lockfileVersion,
140
+ settings: lockfile.settings,
141
+ importers: { ".": subsetImporter },
142
+ packages: subsetPackages,
143
+ snapshots: subsetSnapshots
144
+ },
145
+ collected
146
+ };
147
+ }
148
+ //#endregion
63
149
  //#region src/write.ts
64
150
  function writeOutput(outputDir, result) {
65
151
  mkdirSync(outputDir, { recursive: true });
66
152
  writeFileSync(join(outputDir, "package.json"), JSON.stringify(result.packageJson, null, 2) + "\n");
67
- writeFileSync(join(outputDir, "package-lock.json"), JSON.stringify(result.lockfileJson, null, 2) + "\n");
153
+ 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, {
155
+ lineWidth: -1,
156
+ noCompatMode: true,
157
+ quotingType: "'",
158
+ forceQuotes: false
159
+ }));
68
160
  }
69
161
  //#endregion
70
162
  //#region src/index.ts
@@ -72,7 +164,7 @@ const { version: VERSION } = createRequire(import.meta.url)("../package.json");
72
164
  function parseArgs(argv) {
73
165
  const args = {
74
166
  packages: [],
75
- lockfile: ".",
167
+ lockfile: "",
76
168
  output: "./lockfile-subset-output",
77
169
  includeOptional: true,
78
170
  install: false,
@@ -120,29 +212,54 @@ function parseArgs(argv) {
120
212
  }
121
213
  return args;
122
214
  }
215
+ function resolveLockfile(lockfilePath) {
216
+ if (!lockfilePath) {
217
+ if (existsSync(resolve("pnpm-lock.yaml"))) return {
218
+ projectPath: resolve("."),
219
+ type: "pnpm"
220
+ };
221
+ if (existsSync(resolve("package-lock.json"))) return {
222
+ projectPath: resolve("."),
223
+ type: "npm"
224
+ };
225
+ throw new Error("No lockfile found in current directory. Expected package-lock.json or pnpm-lock.yaml.");
226
+ }
227
+ const resolved = resolve(lockfilePath);
228
+ const basename = resolved.split("/").pop();
229
+ if (basename === "pnpm-lock.yaml") return {
230
+ projectPath: resolve(resolved, ".."),
231
+ type: "pnpm"
232
+ };
233
+ if (basename === "package-lock.json") return {
234
+ projectPath: resolve(resolved, ".."),
235
+ type: "npm"
236
+ };
237
+ throw new Error(`Invalid lockfile path: ${lockfilePath}. Expected a path to package-lock.json or pnpm-lock.yaml.`);
238
+ }
123
239
  const HELP = `
124
240
  lockfile-subset <packages...> [options]
125
241
 
126
- Extract a subset of package-lock.json for specified packages and their transitive dependencies.
242
+ Extract a subset of package-lock.json or pnpm-lock.yaml for specified packages
243
+ and their transitive dependencies.
127
244
 
128
245
  Arguments:
129
246
  packages Package names to extract (one or more, space-separated)
130
247
 
131
248
  Options:
132
- --lockfile, -l <path> Path to project dir or package-lock.json (default: .)
249
+ --lockfile, -l <path> Path to lockfile (auto-detected from cwd by default)
133
250
  --output, -o <dir> Output directory (default: ./lockfile-subset-output)
134
251
  --no-optional Exclude optional dependencies
135
- --install Run npm ci after generating the subset
252
+ --install Run npm ci / pnpm install --frozen-lockfile after generating
136
253
  --dry-run Print the result without writing files
137
254
  --version, -v Show version
138
255
  --help, -h Show this help
139
256
 
140
257
  Examples:
141
- lockfile-subset prisma sharp
142
- lockfile-subset prisma sharp -o /lambda-standalone
143
- lockfile-subset prisma sharp --lockfile /build/package-lock.json
144
- lockfile-subset prisma sharp -o /lambda-standalone --install
145
- lockfile-subset prisma --dry-run
258
+ lockfile-subset @prisma/client sharp
259
+ lockfile-subset @prisma/client sharp -o /standalone
260
+ lockfile-subset @prisma/client sharp -l /build/package-lock.json
261
+ lockfile-subset @prisma/client sharp -l pnpm-lock.yaml --install
262
+ lockfile-subset chalk --dry-run
146
263
  `.trim();
147
264
  async function main() {
148
265
  const args = parseArgs(process.argv.slice(2));
@@ -159,9 +276,15 @@ async function main() {
159
276
  console.log(HELP);
160
277
  process.exit(1);
161
278
  }
162
- const projectPath = resolve(args.lockfile);
279
+ const { projectPath, type } = resolveLockfile(args.lockfile);
163
280
  const outputDir = resolve(args.output);
164
- const result = await extractSubset({
281
+ let result;
282
+ if (type === "pnpm") result = await extractPnpmSubset({
283
+ projectPath,
284
+ packageNames: args.packages,
285
+ includeOptional: args.includeOptional
286
+ });
287
+ else result = await extractSubset({
165
288
  projectPath,
166
289
  packageNames: args.packages,
167
290
  includeOptional: args.includeOptional
@@ -170,18 +293,35 @@ async function main() {
170
293
  if (args.dryRun) {
171
294
  console.log("\n--- package.json ---");
172
295
  console.log(JSON.stringify(result.packageJson, null, 2));
173
- console.log("\n--- package-lock.json ---");
174
- console.log(JSON.stringify(result.lockfileJson, null, 2));
296
+ if (result.type === "npm") {
297
+ console.log("\n--- package-lock.json ---");
298
+ console.log(JSON.stringify(result.lockfileJson, null, 2));
299
+ } else {
300
+ const yaml = (await import("js-yaml")).default;
301
+ console.log("\n--- pnpm-lock.yaml ---");
302
+ console.log(yaml.dump(result.lockfileYaml, {
303
+ lineWidth: -1,
304
+ noCompatMode: true
305
+ }));
306
+ }
175
307
  return;
176
308
  }
177
309
  writeOutput(outputDir, result);
178
310
  console.log(`Written to ${outputDir}`);
179
311
  if (args.install) {
180
- console.log("Running npm ci...");
181
- execSync("npm ci", {
182
- cwd: outputDir,
183
- stdio: "inherit"
184
- });
312
+ if (type === "pnpm") {
313
+ console.log("Running pnpm install --frozen-lockfile...");
314
+ execSync("pnpm install --frozen-lockfile", {
315
+ cwd: outputDir,
316
+ stdio: "inherit"
317
+ });
318
+ } else {
319
+ console.log("Running npm ci...");
320
+ execSync("npm ci", {
321
+ cwd: outputDir,
322
+ stdio: "inherit"
323
+ });
324
+ }
185
325
  console.log("Done.");
186
326
  }
187
327
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lockfile-subset",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Extract a subset of package-lock.json for specified packages and their transitive dependencies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "keywords": [
18
18
  "npm",
19
+ "pnpm",
19
20
  "lockfile",
20
21
  "package-lock",
21
22
  "subset",
@@ -33,6 +34,7 @@
33
34
  "devDependencies": {
34
35
  "@semantic-release/changelog": "^6.0.3",
35
36
  "@semantic-release/git": "^10.0.1",
37
+ "@types/js-yaml": "^4.0.9",
36
38
  "@types/node": "^25.5.0",
37
39
  "@types/npmcli__arborist": "^6.3.3",
38
40
  "semantic-release": "^25.0.3",
@@ -41,6 +43,7 @@
41
43
  "vitest": "^4.1.0"
42
44
  },
43
45
  "dependencies": {
44
- "@npmcli/arborist": "^9.4.2"
46
+ "@npmcli/arborist": "^9.4.2",
47
+ "js-yaml": "^4.1.1"
45
48
  }
46
49
  }