lockfile-subset 1.0.2 → 1.2.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 +388 -20
- package/package.json +9 -3
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,300 @@ 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
|
|
149
|
+
//#region src/extract-yarn.ts
|
|
150
|
+
const { parse: parseYarnLockV1, stringify: stringifyYarnLockV1 } = createRequire(import.meta.url)("@yarnpkg/lockfile");
|
|
151
|
+
function detectYarnVersion(content) {
|
|
152
|
+
return content.includes("# yarn lockfile v1") ? 1 : 2;
|
|
153
|
+
}
|
|
154
|
+
function extractV1({ projectPath, packageNames, includeOptional, lockfileContent }) {
|
|
155
|
+
const parsed = parseYarnLockV1(lockfileContent);
|
|
156
|
+
if (parsed.type !== "success") throw new Error(`Failed to parse yarn.lock: ${parsed.type}`);
|
|
157
|
+
const lockfile = parsed.object;
|
|
158
|
+
const pkgJsonPath = join(projectPath, "package.json");
|
|
159
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
160
|
+
const allDeps = {
|
|
161
|
+
...pkgJson.dependencies,
|
|
162
|
+
...pkgJson.optionalDependencies
|
|
163
|
+
};
|
|
164
|
+
const keepKeys = /* @__PURE__ */ new Set();
|
|
165
|
+
const collected = [];
|
|
166
|
+
for (const name of packageNames) {
|
|
167
|
+
const range = allDeps[name];
|
|
168
|
+
if (!range) throw new Error(`Package "${name}" not found in yarn.lock`);
|
|
169
|
+
const queue = [`${name}@${range}`];
|
|
170
|
+
while (queue.length > 0) {
|
|
171
|
+
const key = queue.shift();
|
|
172
|
+
if (keepKeys.has(key)) continue;
|
|
173
|
+
const entry = lockfile[key];
|
|
174
|
+
if (!entry) continue;
|
|
175
|
+
keepKeys.add(key);
|
|
176
|
+
collected.push({
|
|
177
|
+
name: key.slice(0, key.lastIndexOf("@")),
|
|
178
|
+
version: entry.version
|
|
179
|
+
});
|
|
180
|
+
if (entry.dependencies) for (const [depName, depRange] of Object.entries(entry.dependencies)) {
|
|
181
|
+
const depKey = `${depName}@${depRange}`;
|
|
182
|
+
if (!keepKeys.has(depKey)) queue.push(depKey);
|
|
183
|
+
}
|
|
184
|
+
if (includeOptional && entry.optionalDependencies) for (const [depName, depRange] of Object.entries(entry.optionalDependencies)) {
|
|
185
|
+
const depKey = `${depName}@${depRange}`;
|
|
186
|
+
if (!keepKeys.has(depKey)) queue.push(depKey);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const subset = {};
|
|
191
|
+
for (const key of keepKeys) subset[key] = lockfile[key];
|
|
192
|
+
const dependencies = {};
|
|
193
|
+
for (const name of packageNames) dependencies[name] = lockfile[`${name}@${allDeps[name]}`].version;
|
|
194
|
+
const seen = /* @__PURE__ */ new Set();
|
|
195
|
+
const deduped = collected.filter((c) => {
|
|
196
|
+
const key = `${c.name}@${c.version}`;
|
|
197
|
+
if (seen.has(key)) return false;
|
|
198
|
+
seen.add(key);
|
|
199
|
+
return true;
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
type: "yarn",
|
|
203
|
+
yarnVersion: 1,
|
|
204
|
+
packageJson: {
|
|
205
|
+
name: "lockfile-subset-output",
|
|
206
|
+
version: "1.0.0",
|
|
207
|
+
dependencies
|
|
208
|
+
},
|
|
209
|
+
lockfileContent: stringifyYarnLockV1(subset),
|
|
210
|
+
collected: deduped
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function extractBerry({ projectPath, packageNames, includeOptional, lockfileContent }) {
|
|
214
|
+
const lockfile = yaml.load(lockfileContent);
|
|
215
|
+
const descriptorMap = /* @__PURE__ */ new Map();
|
|
216
|
+
for (const [compoundKey, entry] of Object.entries(lockfile)) {
|
|
217
|
+
if (compoundKey === "__metadata") continue;
|
|
218
|
+
const descriptors = compoundKey.split(", ");
|
|
219
|
+
for (const descriptor of descriptors) descriptorMap.set(descriptor, {
|
|
220
|
+
entry,
|
|
221
|
+
originalKey: compoundKey
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const pkgJsonPath = join(projectPath, "package.json");
|
|
225
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
226
|
+
const allDeps = {
|
|
227
|
+
...pkgJson.dependencies,
|
|
228
|
+
...pkgJson.optionalDependencies
|
|
229
|
+
};
|
|
230
|
+
const keepOriginalKeys = /* @__PURE__ */ new Set();
|
|
231
|
+
const visited = /* @__PURE__ */ new Set();
|
|
232
|
+
const collected = [];
|
|
233
|
+
for (const name of packageNames) {
|
|
234
|
+
const range = allDeps[name];
|
|
235
|
+
if (!range) throw new Error(`Package "${name}" not found in yarn.lock`);
|
|
236
|
+
const queue = [`${name}@npm:${range}`];
|
|
237
|
+
while (queue.length > 0) {
|
|
238
|
+
const desc = queue.shift();
|
|
239
|
+
if (visited.has(desc)) continue;
|
|
240
|
+
visited.add(desc);
|
|
241
|
+
const match = descriptorMap.get(desc);
|
|
242
|
+
if (!match) continue;
|
|
243
|
+
keepOriginalKeys.add(match.originalKey);
|
|
244
|
+
collected.push({
|
|
245
|
+
name: parseDescriptorName(desc),
|
|
246
|
+
version: match.entry.version
|
|
247
|
+
});
|
|
248
|
+
if (match.entry.dependencies) for (const [depName, depRange] of Object.entries(match.entry.dependencies)) {
|
|
249
|
+
const depDesc = `${depName}@${depRange}`;
|
|
250
|
+
if (!visited.has(depDesc)) queue.push(depDesc);
|
|
251
|
+
}
|
|
252
|
+
if (includeOptional && match.entry.optionalDependencies) for (const [depName, depRange] of Object.entries(match.entry.optionalDependencies)) {
|
|
253
|
+
const depDesc = `${depName}@${depRange}`;
|
|
254
|
+
if (!visited.has(depDesc)) queue.push(depDesc);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const dependencies = {};
|
|
259
|
+
for (const name of packageNames) {
|
|
260
|
+
const descriptor = `${name}@npm:${allDeps[name]}`;
|
|
261
|
+
dependencies[name] = descriptorMap.get(descriptor).entry.version;
|
|
262
|
+
}
|
|
263
|
+
const lines = [];
|
|
264
|
+
lines.push("# This file is generated by running \"yarn install\" inside your project.");
|
|
265
|
+
lines.push("# Manual changes might be lost - proceed with caution!");
|
|
266
|
+
lines.push("");
|
|
267
|
+
const metadata = lockfile.__metadata;
|
|
268
|
+
if (metadata) {
|
|
269
|
+
lines.push("__metadata:");
|
|
270
|
+
lines.push(` version: ${metadata.version}`);
|
|
271
|
+
if (metadata.cacheKey) lines.push(` cacheKey: ${metadata.cacheKey}`);
|
|
272
|
+
lines.push("");
|
|
273
|
+
}
|
|
274
|
+
for (const originalKey of keepOriginalKeys) {
|
|
275
|
+
const entry = lockfile[originalKey];
|
|
276
|
+
lines.push(`"${originalKey}":`);
|
|
277
|
+
lines.push(` version: ${entry.version}`);
|
|
278
|
+
lines.push(` resolution: "${entry.resolution}"`);
|
|
279
|
+
if (entry.dependencies && Object.keys(entry.dependencies).length > 0) {
|
|
280
|
+
lines.push(" dependencies:");
|
|
281
|
+
for (const [k, v] of Object.entries(entry.dependencies)) lines.push(` ${k}: "${v}"`);
|
|
282
|
+
}
|
|
283
|
+
if (entry.optionalDependencies && Object.keys(entry.optionalDependencies).length > 0) {
|
|
284
|
+
lines.push(" optionalDependencies:");
|
|
285
|
+
for (const [k, v] of Object.entries(entry.optionalDependencies)) lines.push(` ${k}: "${v}"`);
|
|
286
|
+
}
|
|
287
|
+
if (entry.dependenciesMeta && Object.keys(entry.dependenciesMeta).length > 0) {
|
|
288
|
+
lines.push(" dependenciesMeta:");
|
|
289
|
+
for (const [k, v] of Object.entries(entry.dependenciesMeta)) {
|
|
290
|
+
lines.push(` ${k}:`);
|
|
291
|
+
if (v.optional !== void 0) lines.push(` optional: ${v.optional}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (entry.bin && Object.keys(entry.bin).length > 0) {
|
|
295
|
+
lines.push(" bin:");
|
|
296
|
+
for (const [k, v] of Object.entries(entry.bin)) lines.push(` ${k}: ${v}`);
|
|
297
|
+
}
|
|
298
|
+
if (entry.conditions) lines.push(` conditions: ${entry.conditions}`);
|
|
299
|
+
if (entry.checksum) lines.push(` checksum: ${entry.checksum}`);
|
|
300
|
+
lines.push(` languageName: ${entry.languageName || "node"}`);
|
|
301
|
+
lines.push(` linkType: ${entry.linkType || "hard"}`);
|
|
302
|
+
lines.push("");
|
|
303
|
+
}
|
|
304
|
+
const seen = /* @__PURE__ */ new Set();
|
|
305
|
+
const deduped = collected.filter((c) => {
|
|
306
|
+
const key = `${c.name}@${c.version}`;
|
|
307
|
+
if (seen.has(key)) return false;
|
|
308
|
+
seen.add(key);
|
|
309
|
+
return true;
|
|
310
|
+
});
|
|
311
|
+
return {
|
|
312
|
+
type: "yarn",
|
|
313
|
+
yarnVersion: 2,
|
|
314
|
+
packageJson: {
|
|
315
|
+
name: "lockfile-subset-output",
|
|
316
|
+
version: "1.0.0",
|
|
317
|
+
dependencies
|
|
318
|
+
},
|
|
319
|
+
lockfileContent: lines.join("\n"),
|
|
320
|
+
collected: deduped
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/** Extract package name from a descriptor like "chalk@npm:4.1.2" or "@scope/pkg@npm:^1.0.0" */
|
|
324
|
+
function parseDescriptorName(descriptor) {
|
|
325
|
+
const npmIdx = descriptor.indexOf("@npm:");
|
|
326
|
+
if (npmIdx > 0) return descriptor.slice(0, npmIdx);
|
|
327
|
+
const lastAt = descriptor.lastIndexOf("@");
|
|
328
|
+
if (lastAt > 0) return descriptor.slice(0, lastAt);
|
|
329
|
+
return descriptor;
|
|
330
|
+
}
|
|
331
|
+
async function extractYarnSubset({ projectPath, packageNames, includeOptional = true }) {
|
|
332
|
+
const lockfileContent = readFileSync(join(projectPath, "yarn.lock"), "utf8");
|
|
333
|
+
if (detectYarnVersion(lockfileContent) === 1) return extractV1({
|
|
334
|
+
projectPath,
|
|
335
|
+
packageNames,
|
|
336
|
+
includeOptional,
|
|
337
|
+
lockfileContent
|
|
338
|
+
});
|
|
339
|
+
else return extractBerry({
|
|
340
|
+
projectPath,
|
|
341
|
+
packageNames,
|
|
342
|
+
includeOptional,
|
|
343
|
+
lockfileContent
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
63
347
|
//#region src/write.ts
|
|
64
348
|
function writeOutput(outputDir, result) {
|
|
65
349
|
mkdirSync(outputDir, { recursive: true });
|
|
66
350
|
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");
|
|
351
|
+
if (result.type === "npm") writeFileSync(join(outputDir, "package-lock.json"), JSON.stringify(result.lockfileJson, null, 2) + "\n");
|
|
352
|
+
else if (result.type === "pnpm") writeFileSync(join(outputDir, "pnpm-lock.yaml"), yaml.dump(result.lockfileYaml, {
|
|
353
|
+
lineWidth: -1,
|
|
354
|
+
noCompatMode: true,
|
|
355
|
+
quotingType: "'",
|
|
356
|
+
forceQuotes: false
|
|
357
|
+
}));
|
|
358
|
+
else writeFileSync(join(outputDir, "yarn.lock"), result.lockfileContent);
|
|
68
359
|
}
|
|
69
360
|
//#endregion
|
|
70
361
|
//#region src/index.ts
|
|
@@ -72,7 +363,7 @@ const { version: VERSION } = createRequire(import.meta.url)("../package.json");
|
|
|
72
363
|
function parseArgs(argv) {
|
|
73
364
|
const args = {
|
|
74
365
|
packages: [],
|
|
75
|
-
lockfile: "
|
|
366
|
+
lockfile: "",
|
|
76
367
|
output: "./lockfile-subset-output",
|
|
77
368
|
includeOptional: true,
|
|
78
369
|
install: false,
|
|
@@ -120,29 +411,62 @@ function parseArgs(argv) {
|
|
|
120
411
|
}
|
|
121
412
|
return args;
|
|
122
413
|
}
|
|
414
|
+
function resolveLockfile(lockfilePath) {
|
|
415
|
+
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.");
|
|
429
|
+
}
|
|
430
|
+
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"
|
|
443
|
+
};
|
|
444
|
+
throw new Error(`Invalid lockfile path: ${lockfilePath}. Expected a path to package-lock.json, pnpm-lock.yaml, or yarn.lock.`);
|
|
445
|
+
}
|
|
123
446
|
const HELP = `
|
|
124
447
|
lockfile-subset <packages...> [options]
|
|
125
448
|
|
|
126
|
-
Extract a subset of package-lock.json
|
|
449
|
+
Extract a subset of package-lock.json, pnpm-lock.yaml, or yarn.lock for specified
|
|
450
|
+
packages and their transitive dependencies.
|
|
127
451
|
|
|
128
452
|
Arguments:
|
|
129
453
|
packages Package names to extract (one or more, space-separated)
|
|
130
454
|
|
|
131
455
|
Options:
|
|
132
|
-
--lockfile, -l <path> Path to
|
|
456
|
+
--lockfile, -l <path> Path to lockfile (auto-detected from cwd by default)
|
|
133
457
|
--output, -o <dir> Output directory (default: ./lockfile-subset-output)
|
|
134
458
|
--no-optional Exclude optional dependencies
|
|
135
|
-
--install Run npm ci after generating
|
|
459
|
+
--install Run npm ci / pnpm install / yarn install after generating
|
|
136
460
|
--dry-run Print the result without writing files
|
|
137
461
|
--version, -v Show version
|
|
138
462
|
--help, -h Show this help
|
|
139
463
|
|
|
140
464
|
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
|
|
465
|
+
lockfile-subset @prisma/client sharp
|
|
466
|
+
lockfile-subset @prisma/client sharp -o /standalone
|
|
467
|
+
lockfile-subset @prisma/client sharp -l /build/package-lock.json
|
|
468
|
+
lockfile-subset @prisma/client sharp -l pnpm-lock.yaml --install
|
|
469
|
+
lockfile-subset chalk --dry-run
|
|
146
470
|
`.trim();
|
|
147
471
|
async function main() {
|
|
148
472
|
const args = parseArgs(process.argv.slice(2));
|
|
@@ -159,9 +483,20 @@ async function main() {
|
|
|
159
483
|
console.log(HELP);
|
|
160
484
|
process.exit(1);
|
|
161
485
|
}
|
|
162
|
-
const projectPath =
|
|
486
|
+
const { projectPath, type } = resolveLockfile(args.lockfile);
|
|
163
487
|
const outputDir = resolve(args.output);
|
|
164
|
-
|
|
488
|
+
let result;
|
|
489
|
+
if (type === "pnpm") result = await extractPnpmSubset({
|
|
490
|
+
projectPath,
|
|
491
|
+
packageNames: args.packages,
|
|
492
|
+
includeOptional: args.includeOptional
|
|
493
|
+
});
|
|
494
|
+
else if (type === "yarn") result = await extractYarnSubset({
|
|
495
|
+
projectPath,
|
|
496
|
+
packageNames: args.packages,
|
|
497
|
+
includeOptional: args.includeOptional
|
|
498
|
+
});
|
|
499
|
+
else result = await extractSubset({
|
|
165
500
|
projectPath,
|
|
166
501
|
packageNames: args.packages,
|
|
167
502
|
includeOptional: args.includeOptional
|
|
@@ -170,18 +505,51 @@ async function main() {
|
|
|
170
505
|
if (args.dryRun) {
|
|
171
506
|
console.log("\n--- package.json ---");
|
|
172
507
|
console.log(JSON.stringify(result.packageJson, null, 2));
|
|
173
|
-
|
|
174
|
-
|
|
508
|
+
if (result.type === "npm") {
|
|
509
|
+
console.log("\n--- package-lock.json ---");
|
|
510
|
+
console.log(JSON.stringify(result.lockfileJson, null, 2));
|
|
511
|
+
} else if (result.type === "pnpm") {
|
|
512
|
+
const yaml = (await import("js-yaml")).default;
|
|
513
|
+
console.log("\n--- pnpm-lock.yaml ---");
|
|
514
|
+
console.log(yaml.dump(result.lockfileYaml, {
|
|
515
|
+
lineWidth: -1,
|
|
516
|
+
noCompatMode: true
|
|
517
|
+
}));
|
|
518
|
+
} else {
|
|
519
|
+
console.log("\n--- yarn.lock ---");
|
|
520
|
+
console.log(result.lockfileContent);
|
|
521
|
+
}
|
|
175
522
|
return;
|
|
176
523
|
}
|
|
177
524
|
writeOutput(outputDir, result);
|
|
178
525
|
console.log(`Written to ${outputDir}`);
|
|
179
526
|
if (args.install) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
527
|
+
if (type === "pnpm") {
|
|
528
|
+
console.log("Running pnpm install --frozen-lockfile...");
|
|
529
|
+
execSync("pnpm install --frozen-lockfile", {
|
|
530
|
+
cwd: outputDir,
|
|
531
|
+
stdio: "inherit"
|
|
532
|
+
});
|
|
533
|
+
} else if (type === "yarn") if (result.type === "yarn" && result.yarnVersion === 1) {
|
|
534
|
+
console.log("Running yarn install --frozen-lockfile...");
|
|
535
|
+
execSync("yarn install --frozen-lockfile", {
|
|
536
|
+
cwd: outputDir,
|
|
537
|
+
stdio: "inherit"
|
|
538
|
+
});
|
|
539
|
+
} else {
|
|
540
|
+
console.log("Running yarn install --immutable...");
|
|
541
|
+
execSync("yarn install --immutable", {
|
|
542
|
+
cwd: outputDir,
|
|
543
|
+
stdio: "inherit"
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
console.log("Running npm ci...");
|
|
548
|
+
execSync("npm ci", {
|
|
549
|
+
cwd: outputDir,
|
|
550
|
+
stdio: "inherit"
|
|
551
|
+
});
|
|
552
|
+
}
|
|
185
553
|
console.log("Done.");
|
|
186
554
|
}
|
|
187
555
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lockfile-subset",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Extract a subset of package-lock.json for specified packages and their transitive dependencies",
|
|
3
|
+
"version": "1.2.0",
|
|
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": {
|
|
7
7
|
"lockfile-subset": "./dist/index.mjs"
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"npm",
|
|
19
|
+
"pnpm",
|
|
20
|
+
"yarn",
|
|
19
21
|
"lockfile",
|
|
20
22
|
"package-lock",
|
|
21
23
|
"subset",
|
|
@@ -33,14 +35,18 @@
|
|
|
33
35
|
"devDependencies": {
|
|
34
36
|
"@semantic-release/changelog": "^6.0.3",
|
|
35
37
|
"@semantic-release/git": "^10.0.1",
|
|
38
|
+
"@types/js-yaml": "^4.0.9",
|
|
36
39
|
"@types/node": "^25.5.0",
|
|
37
40
|
"@types/npmcli__arborist": "^6.3.3",
|
|
41
|
+
"@types/yarnpkg__lockfile": "^1.1.9",
|
|
38
42
|
"semantic-release": "^25.0.3",
|
|
39
43
|
"tsdown": "^0.21.4",
|
|
40
44
|
"typescript": "^5.9.3",
|
|
41
45
|
"vitest": "^4.1.0"
|
|
42
46
|
},
|
|
43
47
|
"dependencies": {
|
|
44
|
-
"@npmcli/arborist": "^9.4.2"
|
|
48
|
+
"@npmcli/arborist": "^9.4.2",
|
|
49
|
+
"@yarnpkg/lockfile": "^1.1.0",
|
|
50
|
+
"js-yaml": "^4.1.1"
|
|
45
51
|
}
|
|
46
52
|
}
|