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.
- package/README.md +20 -24
- package/dist/index.mjs +160 -20
- 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
|
|
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
|
|
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
|
|
45
|
+
lockfile-subset chalk --dry-run
|
|
42
46
|
```
|
|
43
47
|
|
|
44
|
-
This generates a minimal `package.json` and
|
|
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`
|
|
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` +
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 /
|
|
143
|
-
lockfile-subset prisma sharp
|
|
144
|
-
lockfile-subset prisma sharp -
|
|
145
|
-
lockfile-subset
|
|
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 =
|
|
279
|
+
const { projectPath, type } = resolveLockfile(args.lockfile);
|
|
163
280
|
const outputDir = resolve(args.output);
|
|
164
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
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
|
}
|