@seorii/libcollect 0.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/include-deps.mjs +722 -0
- package/package.json +31 -0
package/include-deps.mjs
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import semver from "semver";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_CONCURRENCY = 10;
|
|
10
|
+
const DEFAULT_SOURCE_SECTIONS = ["dependencies", "optionalDependencies", "peerDependencies"];
|
|
11
|
+
const DEFAULT_INCLUDE_TYPES = ["peerDependency", "optionalDependency", "bundledDependency"];
|
|
12
|
+
const REGISTRY_BASE = "https://registry.npmjs.org";
|
|
13
|
+
|
|
14
|
+
function createLimiter(maxConcurrency) {
|
|
15
|
+
let activeCount = 0;
|
|
16
|
+
const queue = [];
|
|
17
|
+
|
|
18
|
+
const next = () => {
|
|
19
|
+
while (activeCount < maxConcurrency && queue.length > 0) {
|
|
20
|
+
const job = queue.shift();
|
|
21
|
+
activeCount += 1;
|
|
22
|
+
|
|
23
|
+
Promise.resolve()
|
|
24
|
+
.then(job.fn)
|
|
25
|
+
.then(job.resolve, job.reject)
|
|
26
|
+
.finally(() => {
|
|
27
|
+
activeCount -= 1;
|
|
28
|
+
next();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return function limit(fn) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
queue.push({ fn, resolve, reject });
|
|
36
|
+
next();
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createRegistryContext(concurrency = DEFAULT_CONCURRENCY) {
|
|
42
|
+
const safeConcurrency =
|
|
43
|
+
Number.isFinite(concurrency) && concurrency > 0 ? Math.floor(concurrency) : DEFAULT_CONCURRENCY;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
concurrency: safeConcurrency,
|
|
47
|
+
fetchLimit: createLimiter(safeConcurrency),
|
|
48
|
+
metaCache: new Map(),
|
|
49
|
+
resolvedCache: new Map(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function fetchJson(url, registryContext) {
|
|
54
|
+
return registryContext.fetchLimit(async () => {
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
headers: { accept: "application/json" },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
throw new Error(`HTTP ${res.status} for ${url}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return res.json();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function getPackageMeta(name, registryContext) {
|
|
68
|
+
if (registryContext.metaCache.has(name)) {
|
|
69
|
+
return registryContext.metaCache.get(name);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const promise = (async () => {
|
|
73
|
+
const encoded = encodeURIComponent(name);
|
|
74
|
+
return fetchJson(`${REGISTRY_BASE}/${encoded}`, registryContext);
|
|
75
|
+
})();
|
|
76
|
+
|
|
77
|
+
registryContext.metaCache.set(name, promise);
|
|
78
|
+
return promise;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseAliasSpec(range) {
|
|
82
|
+
const s = String(range || "").trim();
|
|
83
|
+
const m = s.match(/^npm:((?:@[^/]+\/)?[^@]+)(?:@(.+))?$/);
|
|
84
|
+
if (!m) return null;
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
targetName: m[1],
|
|
88
|
+
targetRange: m[2] || "latest",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isNonRegistrySpec(range) {
|
|
93
|
+
const s = String(range || "").trim();
|
|
94
|
+
if (!s || s === "*" || s === "latest") return false;
|
|
95
|
+
return /^(file:|link:|workspace:|git\+|github:|https?:)/.test(s);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function rangeMentionsPrerelease(range) {
|
|
99
|
+
return /-\w/.test(String(range || ""));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getAllVersions(meta) {
|
|
103
|
+
return Object.keys(meta.versions || {})
|
|
104
|
+
.filter((version) => !!semver.valid(version))
|
|
105
|
+
.sort(semver.rcompare);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function pickVersionFromMeta(meta, wanted) {
|
|
109
|
+
const versions = getAllVersions(meta);
|
|
110
|
+
const distTags = meta["dist-tags"] || {};
|
|
111
|
+
const target = String(wanted || "latest").trim() || "latest";
|
|
112
|
+
|
|
113
|
+
if (target === "latest") {
|
|
114
|
+
return distTags.latest || versions[0] || null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (distTags[target]) {
|
|
118
|
+
return distTags[target];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (meta.versions?.[target]) {
|
|
122
|
+
return target;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!semver.validRange(target)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return semver.maxSatisfying(versions, target, {
|
|
130
|
+
includePrerelease: rangeMentionsPrerelease(target),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function resolveVersion(name, wanted, registryContext) {
|
|
135
|
+
const cacheKey = `${name}@${wanted || "latest"}`;
|
|
136
|
+
if (registryContext.resolvedCache.has(cacheKey)) {
|
|
137
|
+
return registryContext.resolvedCache.get(cacheKey);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const promise = (async () => {
|
|
141
|
+
const alias = parseAliasSpec(wanted);
|
|
142
|
+
if (alias) {
|
|
143
|
+
const aliasResolved = await resolveVersion(
|
|
144
|
+
alias.targetName,
|
|
145
|
+
alias.targetRange,
|
|
146
|
+
registryContext,
|
|
147
|
+
);
|
|
148
|
+
return {
|
|
149
|
+
requestedName: name,
|
|
150
|
+
requestedRange: wanted,
|
|
151
|
+
actualName: alias.targetName,
|
|
152
|
+
version: typeof aliasResolved === "string" ? aliasResolved : aliasResolved.version,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const meta = await getPackageMeta(name, registryContext);
|
|
157
|
+
const resolvedVersion = pickVersionFromMeta(meta, wanted);
|
|
158
|
+
|
|
159
|
+
if (!resolvedVersion) {
|
|
160
|
+
throw new Error(`Cannot resolve version: ${name}@${wanted || "latest"}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return resolvedVersion;
|
|
164
|
+
})();
|
|
165
|
+
|
|
166
|
+
registryContext.resolvedCache.set(cacheKey, promise);
|
|
167
|
+
return promise;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function makeSkippedNode(name, requested, dependencyType, parent, reason) {
|
|
171
|
+
return {
|
|
172
|
+
name,
|
|
173
|
+
requested,
|
|
174
|
+
version: null,
|
|
175
|
+
dependencyType,
|
|
176
|
+
parent,
|
|
177
|
+
skipped: true,
|
|
178
|
+
reason,
|
|
179
|
+
dependencies: [],
|
|
180
|
+
peerDependencies: [],
|
|
181
|
+
bundledDependencies: [],
|
|
182
|
+
optionalDependencies: [],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function buildNodeFromPackage(
|
|
187
|
+
pkgNameForMeta,
|
|
188
|
+
displayName,
|
|
189
|
+
wanted,
|
|
190
|
+
resolvedVersion,
|
|
191
|
+
edgeType,
|
|
192
|
+
parent,
|
|
193
|
+
registryContext,
|
|
194
|
+
visited,
|
|
195
|
+
extra = {},
|
|
196
|
+
) {
|
|
197
|
+
const visitKey = `${pkgNameForMeta}@${resolvedVersion}`;
|
|
198
|
+
|
|
199
|
+
if (visited.has(visitKey)) {
|
|
200
|
+
return {
|
|
201
|
+
name: displayName,
|
|
202
|
+
requested: wanted || "latest",
|
|
203
|
+
version: resolvedVersion,
|
|
204
|
+
dependencyType: edgeType,
|
|
205
|
+
parent,
|
|
206
|
+
actualName: extra.actualName || null,
|
|
207
|
+
aliased: !!extra.aliased,
|
|
208
|
+
circularOrShared: true,
|
|
209
|
+
dependencies: [],
|
|
210
|
+
peerDependencies: [],
|
|
211
|
+
bundledDependencies: [],
|
|
212
|
+
optionalDependencies: [],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
visited.add(visitKey);
|
|
217
|
+
|
|
218
|
+
const meta = await getPackageMeta(pkgNameForMeta, registryContext);
|
|
219
|
+
const pkg = meta.versions?.[resolvedVersion];
|
|
220
|
+
if (!pkg) {
|
|
221
|
+
throw new Error(`Missing version metadata: ${pkgNameForMeta}@${resolvedVersion}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const deps = pkg.dependencies || {};
|
|
225
|
+
const peerDeps = pkg.peerDependencies || {};
|
|
226
|
+
const optionalDeps = pkg.optionalDependencies || {};
|
|
227
|
+
const bundledList = Array.isArray(pkg.bundleDependencies)
|
|
228
|
+
? pkg.bundleDependencies
|
|
229
|
+
: Array.isArray(pkg.bundledDependencies)
|
|
230
|
+
? pkg.bundledDependencies
|
|
231
|
+
: [];
|
|
232
|
+
|
|
233
|
+
const parentRef = `${displayName}@${resolvedVersion}`;
|
|
234
|
+
const walk = (childName, childRange, dependencyType) =>
|
|
235
|
+
walkNode(childName, childRange, dependencyType, parentRef, registryContext, visited);
|
|
236
|
+
|
|
237
|
+
const depNodes = await Promise.all(
|
|
238
|
+
Object.entries(deps).map(([childName, childRange]) =>
|
|
239
|
+
walk(childName, childRange, "dependency"),
|
|
240
|
+
),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const peerNodes = await Promise.all(
|
|
244
|
+
Object.entries(peerDeps).map(([childName, childRange]) =>
|
|
245
|
+
walk(childName, childRange, "peerDependency"),
|
|
246
|
+
),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const optionalNodes = await Promise.all(
|
|
250
|
+
Object.entries(optionalDeps).map(([childName, childRange]) =>
|
|
251
|
+
walk(childName, childRange, "optionalDependency"),
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const bundledNodes = await Promise.all(
|
|
256
|
+
bundledList.map((childName) => {
|
|
257
|
+
const childRange = deps[childName] || optionalDeps[childName] || "latest";
|
|
258
|
+
return walk(childName, childRange, "bundledDependency");
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
name: displayName,
|
|
264
|
+
requested: wanted || "latest",
|
|
265
|
+
version: resolvedVersion,
|
|
266
|
+
actualName: extra.actualName || null,
|
|
267
|
+
aliased: !!extra.aliased,
|
|
268
|
+
description: pkg.description || "",
|
|
269
|
+
dependencyType: edgeType,
|
|
270
|
+
parent,
|
|
271
|
+
dependencies: depNodes,
|
|
272
|
+
peerDependencies: peerNodes,
|
|
273
|
+
bundledDependencies: bundledNodes,
|
|
274
|
+
optionalDependencies: optionalNodes,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function walkNode(name, wanted, edgeType, parent, registryContext, visited) {
|
|
279
|
+
if (edgeType !== "root" && isNonRegistrySpec(wanted)) {
|
|
280
|
+
return makeSkippedNode(name, wanted, edgeType, parent, "non-registry spec");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const alias = parseAliasSpec(wanted);
|
|
284
|
+
if (alias) {
|
|
285
|
+
const resolvedAlias = await resolveVersion(name, wanted, registryContext);
|
|
286
|
+
return buildNodeFromPackage(
|
|
287
|
+
resolvedAlias.actualName,
|
|
288
|
+
name,
|
|
289
|
+
wanted,
|
|
290
|
+
resolvedAlias.version,
|
|
291
|
+
edgeType,
|
|
292
|
+
parent,
|
|
293
|
+
registryContext,
|
|
294
|
+
visited,
|
|
295
|
+
{
|
|
296
|
+
actualName: resolvedAlias.actualName,
|
|
297
|
+
aliased: true,
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const resolvedVersion = await resolveVersion(name, wanted, registryContext);
|
|
303
|
+
return buildNodeFromPackage(
|
|
304
|
+
name,
|
|
305
|
+
name,
|
|
306
|
+
wanted,
|
|
307
|
+
resolvedVersion,
|
|
308
|
+
edgeType,
|
|
309
|
+
parent,
|
|
310
|
+
registryContext,
|
|
311
|
+
visited,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function flattenTree(tree) {
|
|
316
|
+
const out = [];
|
|
317
|
+
const seen = new Set();
|
|
318
|
+
|
|
319
|
+
function visit(node) {
|
|
320
|
+
const key = [
|
|
321
|
+
node.name,
|
|
322
|
+
node.actualName || "",
|
|
323
|
+
node.version || "null",
|
|
324
|
+
node.dependencyType,
|
|
325
|
+
node.parent || "root",
|
|
326
|
+
node.requested || "",
|
|
327
|
+
].join("::");
|
|
328
|
+
|
|
329
|
+
if (seen.has(key)) return;
|
|
330
|
+
seen.add(key);
|
|
331
|
+
|
|
332
|
+
out.push({
|
|
333
|
+
name: node.name,
|
|
334
|
+
actualName: node.actualName || null,
|
|
335
|
+
requested: node.requested,
|
|
336
|
+
version: node.version,
|
|
337
|
+
dependencyType: node.dependencyType,
|
|
338
|
+
parent: node.parent,
|
|
339
|
+
description: node.description || "",
|
|
340
|
+
circularOrShared: !!node.circularOrShared,
|
|
341
|
+
skipped: !!node.skipped,
|
|
342
|
+
reason: node.reason || null,
|
|
343
|
+
aliased: !!node.aliased,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
for (const child of node.dependencies || []) visit(child);
|
|
347
|
+
for (const child of node.peerDependencies || []) visit(child);
|
|
348
|
+
for (const child of node.bundledDependencies || []) visit(child);
|
|
349
|
+
for (const child of node.optionalDependencies || []) visit(child);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
visit(tree);
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function collectNpmDeps(rootName, rootVersion = "latest", options = {}) {
|
|
357
|
+
const registryContext = options.registryContext || createRegistryContext(options.concurrency);
|
|
358
|
+
const visited = new Set();
|
|
359
|
+
const tree = await walkNode(rootName, rootVersion, "root", null, registryContext, visited);
|
|
360
|
+
const flat = flattenTree(tree);
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
root: `${tree.name}@${tree.version}`,
|
|
364
|
+
tree,
|
|
365
|
+
flat,
|
|
366
|
+
options: { concurrency: registryContext.concurrency },
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function sortObject(input) {
|
|
371
|
+
return Object.fromEntries(Object.entries(input).sort(([a], [b]) => a.localeCompare(b)));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function dedupe(items) {
|
|
375
|
+
return [...new Set(items.filter(Boolean))];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function getLibcollectConfig(pkg) {
|
|
379
|
+
const raw = pkg.libcollect && typeof pkg.libcollect === "object" ? pkg.libcollect : {};
|
|
380
|
+
|
|
381
|
+
const sourceSections =
|
|
382
|
+
Array.isArray(raw.sourceSections) && raw.sourceSections.length > 0
|
|
383
|
+
? dedupe(raw.sourceSections)
|
|
384
|
+
: DEFAULT_SOURCE_SECTIONS;
|
|
385
|
+
|
|
386
|
+
const includeDependencyTypes =
|
|
387
|
+
Array.isArray(raw.includeDependencyTypes) && raw.includeDependencyTypes.length > 0
|
|
388
|
+
? dedupe(raw.includeDependencyTypes)
|
|
389
|
+
: DEFAULT_INCLUDE_TYPES;
|
|
390
|
+
|
|
391
|
+
const autoIncludedDependencies = Array.isArray(raw.autoIncludedDependencies)
|
|
392
|
+
? dedupe(raw.autoIncludedDependencies)
|
|
393
|
+
: [];
|
|
394
|
+
|
|
395
|
+
const autoIncludedDependencySpecs =
|
|
396
|
+
raw.autoIncludedDependencySpecs && typeof raw.autoIncludedDependencySpecs === "object"
|
|
397
|
+
? { ...raw.autoIncludedDependencySpecs }
|
|
398
|
+
: {};
|
|
399
|
+
|
|
400
|
+
const concurrency =
|
|
401
|
+
Number.isFinite(raw.concurrency) && raw.concurrency > 0
|
|
402
|
+
? Math.floor(raw.concurrency)
|
|
403
|
+
: DEFAULT_CONCURRENCY;
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
sourceSections,
|
|
407
|
+
includeDependencyTypes,
|
|
408
|
+
autoIncludedDependencies,
|
|
409
|
+
autoIncludedDependencySpecs,
|
|
410
|
+
concurrency,
|
|
411
|
+
exactVersions: raw.exactVersions !== false,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function buildSourceRoots(pkg, config) {
|
|
416
|
+
const previousAuto = new Set(config.autoIncludedDependencies || []);
|
|
417
|
+
const previousAutoSpecs = config.autoIncludedDependencySpecs || {};
|
|
418
|
+
const currentDependencies = { ...pkg.dependencies };
|
|
419
|
+
|
|
420
|
+
const manualDependencies = Object.fromEntries(
|
|
421
|
+
Object.entries(currentDependencies).filter(([name, spec]) => {
|
|
422
|
+
if (!previousAuto.has(name)) return true;
|
|
423
|
+
return previousAutoSpecs[name] !== spec;
|
|
424
|
+
}),
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
const roots = [];
|
|
428
|
+
const seen = new Set();
|
|
429
|
+
|
|
430
|
+
for (const section of config.sourceSections) {
|
|
431
|
+
const sourceMap = section === "dependencies" ? manualDependencies : { ...pkg[section] };
|
|
432
|
+
|
|
433
|
+
for (const [name, range] of Object.entries(sourceMap)) {
|
|
434
|
+
if (seen.has(name)) continue;
|
|
435
|
+
seen.add(name);
|
|
436
|
+
roots.push({ name, range, sourceSection: section });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return { roots, manualDependencies };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function ensureCandidate(candidates, name) {
|
|
444
|
+
if (!candidates.has(name)) {
|
|
445
|
+
candidates.set(name, {
|
|
446
|
+
name,
|
|
447
|
+
actualName: null,
|
|
448
|
+
requestedRanges: new Set(),
|
|
449
|
+
resolvedVersions: new Set(),
|
|
450
|
+
dependencyTypes: new Set(),
|
|
451
|
+
parents: new Set(),
|
|
452
|
+
sourceSections: new Set(),
|
|
453
|
+
aliased: false,
|
|
454
|
+
versionConflict: null,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return candidates.get(name);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function addCandidate(candidates, node, extra = {}) {
|
|
462
|
+
const candidate = ensureCandidate(candidates, node.name);
|
|
463
|
+
|
|
464
|
+
if (node.actualName) candidate.actualName = node.actualName;
|
|
465
|
+
if (extra.actualName) candidate.actualName = extra.actualName;
|
|
466
|
+
if (node.requested) candidate.requestedRanges.add(node.requested);
|
|
467
|
+
if (extra.requestedRange) candidate.requestedRanges.add(extra.requestedRange);
|
|
468
|
+
if (node.version) candidate.resolvedVersions.add(node.version);
|
|
469
|
+
if (extra.resolvedVersion) candidate.resolvedVersions.add(extra.resolvedVersion);
|
|
470
|
+
if (node.dependencyType) candidate.dependencyTypes.add(node.dependencyType);
|
|
471
|
+
if (extra.dependencyType) candidate.dependencyTypes.add(extra.dependencyType);
|
|
472
|
+
if (node.parent) candidate.parents.add(node.parent);
|
|
473
|
+
if (extra.parent) candidate.parents.add(extra.parent);
|
|
474
|
+
if (extra.sourceSection) candidate.sourceSections.add(extra.sourceSection);
|
|
475
|
+
if (node.aliased || extra.aliased) candidate.aliased = true;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function collectCandidates(pkg, config, registryContext) {
|
|
479
|
+
const { roots, manualDependencies } = buildSourceRoots(pkg, config);
|
|
480
|
+
const candidates = new Map();
|
|
481
|
+
const warnings = [];
|
|
482
|
+
const includeTypeSet = new Set(config.includeDependencyTypes);
|
|
483
|
+
|
|
484
|
+
for (const root of roots) {
|
|
485
|
+
if (root.sourceSection !== "dependencies") {
|
|
486
|
+
if (isNonRegistrySpec(root.range)) {
|
|
487
|
+
warnings.push(
|
|
488
|
+
`${root.name}: ${root.sourceSection}에 있는 non-registry spec(${root.range})은 dependencies로 자동 승격하지 않았습니다.`,
|
|
489
|
+
);
|
|
490
|
+
} else {
|
|
491
|
+
const resolvedRoot = await resolveVersion(root.name, root.range, registryContext);
|
|
492
|
+
addCandidate(
|
|
493
|
+
candidates,
|
|
494
|
+
{
|
|
495
|
+
name: root.name,
|
|
496
|
+
requested: root.range,
|
|
497
|
+
version: typeof resolvedRoot === "string" ? resolvedRoot : resolvedRoot.version,
|
|
498
|
+
actualName: typeof resolvedRoot === "string" ? null : resolvedRoot.actualName || null,
|
|
499
|
+
dependencyType: "sourceSection",
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
sourceSection: root.sourceSection,
|
|
503
|
+
aliased: !!(typeof resolvedRoot !== "string" && resolvedRoot.actualName),
|
|
504
|
+
},
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (isNonRegistrySpec(root.range)) {
|
|
510
|
+
warnings.push(
|
|
511
|
+
`${root.name}: root spec(${root.range})는 registry 기반이 아니어서 하위 dep 탐색을 건너뛰었습니다.`,
|
|
512
|
+
);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const result = await collectNpmDeps(root.name, root.range, {
|
|
517
|
+
registryContext,
|
|
518
|
+
concurrency: config.concurrency,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
for (const node of result.flat) {
|
|
522
|
+
if (node.skipped) {
|
|
523
|
+
warnings.push(
|
|
524
|
+
`${node.name}: ${node.parent || "root"}에서 ${node.reason || "skipped"} 되어 자동 포함 대상에서 제외했습니다.`,
|
|
525
|
+
);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!includeTypeSet.has(node.dependencyType)) {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
addCandidate(candidates, node);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return { candidates, warnings, manualDependencies, roots };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function resolvePreferredSpec(name, candidate, pkg, config, registryContext) {
|
|
541
|
+
if (pkg.optionalDependencies?.[name]) return pkg.optionalDependencies[name];
|
|
542
|
+
if (pkg.peerDependencies?.[name]) return pkg.peerDependencies[name];
|
|
543
|
+
|
|
544
|
+
const ranges = [...candidate.requestedRanges].filter(Boolean);
|
|
545
|
+
const aliasRanges = ranges.map(parseAliasSpec).filter(Boolean);
|
|
546
|
+
const targetName = candidate.actualName || aliasRanges[0]?.targetName || name;
|
|
547
|
+
const normalizedRanges = ranges.map((range) => parseAliasSpec(range)?.targetRange || range);
|
|
548
|
+
|
|
549
|
+
const meta = await getPackageMeta(targetName, registryContext);
|
|
550
|
+
const versions = getAllVersions(meta);
|
|
551
|
+
const includePrerelease = normalizedRanges.some(rangeMentionsPrerelease);
|
|
552
|
+
|
|
553
|
+
const commonVersion = versions.find((version) =>
|
|
554
|
+
normalizedRanges.every((range) => {
|
|
555
|
+
if (range === "latest" || range === "*") return true;
|
|
556
|
+
if (meta.versions?.[range]) return version === range;
|
|
557
|
+
if (!semver.validRange(range)) return false;
|
|
558
|
+
return semver.satisfies(version, range, { includePrerelease });
|
|
559
|
+
}),
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
const fallbackVersion =
|
|
563
|
+
commonVersion ||
|
|
564
|
+
[...candidate.resolvedVersions].filter((v) => !!semver.valid(v)).sort(semver.rcompare)[0];
|
|
565
|
+
|
|
566
|
+
if (!fallbackVersion) {
|
|
567
|
+
throw new Error(`자동 포함 버전을 결정할 수 없습니다: ${name}`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (!commonVersion && normalizedRanges.length > 1) {
|
|
571
|
+
candidate.versionConflict = {
|
|
572
|
+
targetName,
|
|
573
|
+
ranges: normalizedRanges,
|
|
574
|
+
picked: fallbackVersion,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (candidate.actualName || aliasRanges.length > 0) {
|
|
579
|
+
return `npm:${targetName}@${fallbackVersion}`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return config.exactVersions ? fallbackVersion : `^${fallbackVersion}`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function diffKeys(beforeObj, afterObj) {
|
|
586
|
+
const before = new Set(Object.keys(beforeObj || {}));
|
|
587
|
+
const after = new Set(Object.keys(afterObj || {}));
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
added: [...after].filter((name) => !before.has(name)).sort(),
|
|
591
|
+
removed: [...before].filter((name) => !after.has(name)).sort(),
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export async function includeDependencies(
|
|
596
|
+
packageJsonPath = path.resolve(process.cwd(), "package.json"),
|
|
597
|
+
) {
|
|
598
|
+
const rawText = await fs.readFile(packageJsonPath, "utf8");
|
|
599
|
+
const pkg = JSON.parse(rawText);
|
|
600
|
+
const config = getLibcollectConfig(pkg);
|
|
601
|
+
const registryContext = createRegistryContext(config.concurrency);
|
|
602
|
+
const beforeDependencies = { ...pkg.dependencies };
|
|
603
|
+
|
|
604
|
+
const { candidates, warnings, manualDependencies, roots } = await collectCandidates(
|
|
605
|
+
pkg,
|
|
606
|
+
config,
|
|
607
|
+
registryContext,
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
const autoIncludedDependencies = {};
|
|
611
|
+
const versionConflictWarnings = [];
|
|
612
|
+
|
|
613
|
+
for (const [name, candidate] of [...candidates.entries()].sort(([a], [b]) =>
|
|
614
|
+
a.localeCompare(b),
|
|
615
|
+
)) {
|
|
616
|
+
if (manualDependencies[name]) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const spec = await resolvePreferredSpec(name, candidate, pkg, config, registryContext);
|
|
621
|
+
autoIncludedDependencies[name] = spec;
|
|
622
|
+
|
|
623
|
+
if (candidate.versionConflict) {
|
|
624
|
+
versionConflictWarnings.push(
|
|
625
|
+
`${name}: ${candidate.versionConflict.ranges.join(" , ")} 를 동시에 만족하는 공통 버전을 찾지 못해 ${candidate.versionConflict.picked} 로 고정했습니다.`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const nextDependencies = sortObject({
|
|
631
|
+
...manualDependencies,
|
|
632
|
+
...autoIncludedDependencies,
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
pkg.dependencies = nextDependencies;
|
|
636
|
+
pkg.libcollect = {
|
|
637
|
+
...(pkg.libcollect && typeof pkg.libcollect === "object" ? pkg.libcollect : {}),
|
|
638
|
+
sourceSections: config.sourceSections,
|
|
639
|
+
includeDependencyTypes: config.includeDependencyTypes,
|
|
640
|
+
autoIncludedDependencies: Object.keys(autoIncludedDependencies).sort(),
|
|
641
|
+
autoIncludedDependencySpecs: sortObject(autoIncludedDependencies),
|
|
642
|
+
concurrency: config.concurrency,
|
|
643
|
+
exactVersions: config.exactVersions,
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
await fs.writeFile(`${packageJsonPath}.bak`, rawText, "utf8");
|
|
647
|
+
await fs.writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
|
|
648
|
+
|
|
649
|
+
const { added, removed } = diffKeys(beforeDependencies, nextDependencies);
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
packageJsonPath,
|
|
653
|
+
backupPath: `${packageJsonPath}.bak`,
|
|
654
|
+
roots,
|
|
655
|
+
added,
|
|
656
|
+
removed,
|
|
657
|
+
warnings: [...warnings, ...versionConflictWarnings],
|
|
658
|
+
autoIncludedCount: Object.keys(autoIncludedDependencies).length,
|
|
659
|
+
dependencyCount: Object.keys(nextDependencies).length,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function parseCliArgs(argv) {
|
|
664
|
+
const args = argv.slice(2);
|
|
665
|
+
const command = args[0] && !args[0].startsWith("-") ? args[0] : "include";
|
|
666
|
+
const flags = new Map();
|
|
667
|
+
|
|
668
|
+
for (const arg of args.slice(command === "include" ? 1 : 0)) {
|
|
669
|
+
if (!arg.startsWith("--")) continue;
|
|
670
|
+
const [key, value] = arg.slice(2).split("=");
|
|
671
|
+
flags.set(key, value ?? "true");
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return { command, flags };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function runCli() {
|
|
678
|
+
const { command } = parseCliArgs(process.argv);
|
|
679
|
+
|
|
680
|
+
if (command !== "include") {
|
|
681
|
+
console.error(`Unknown command: ${command}`);
|
|
682
|
+
process.exitCode = 1;
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
const result = await includeDependencies();
|
|
688
|
+
console.log(
|
|
689
|
+
`[libcollect] roots: ${result.roots.map((x) => `${x.name}@${x.range}`).join(", ")}`,
|
|
690
|
+
);
|
|
691
|
+
console.log(
|
|
692
|
+
`[libcollect] dependencies: ${result.dependencyCount} total, ${result.autoIncludedCount} auto-included`,
|
|
693
|
+
);
|
|
694
|
+
console.log(`[libcollect] added: ${result.added.length}, removed: ${result.removed.length}`);
|
|
695
|
+
console.log(`[libcollect] backup: ${result.backupPath}`);
|
|
696
|
+
|
|
697
|
+
if (result.added.length > 0) {
|
|
698
|
+
console.log(`[libcollect] added deps: ${result.added.join(", ")}`);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (result.removed.length > 0) {
|
|
702
|
+
console.log(`[libcollect] removed deps: ${result.removed.join(", ")}`);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (result.warnings.length > 0) {
|
|
706
|
+
console.warn("[libcollect] warnings:");
|
|
707
|
+
for (const warning of result.warnings) {
|
|
708
|
+
console.warn(` - ${warning}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
} catch (error) {
|
|
712
|
+
console.error(`[libcollect] failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
713
|
+
process.exitCode = 1;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const currentModulePath = fileURLToPath(import.meta.url);
|
|
718
|
+
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
|
|
719
|
+
|
|
720
|
+
if (invokedPath && currentModulePath === invokedPath) {
|
|
721
|
+
await runCli();
|
|
722
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@seorii/libcollect",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"libcollect": "./include-deps.mjs"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"include": "node ./include-deps.mjs include",
|
|
10
|
+
"prepublishOnly": "npm run include"
|
|
11
|
+
},
|
|
12
|
+
"libcollect": {
|
|
13
|
+
"sourceSections": [
|
|
14
|
+
"dependencies",
|
|
15
|
+
"optionalDependencies",
|
|
16
|
+
"peerDependencies"
|
|
17
|
+
],
|
|
18
|
+
"includeDependencyTypes": [
|
|
19
|
+
"peerDependency",
|
|
20
|
+
"optionalDependency",
|
|
21
|
+
"bundledDependency"
|
|
22
|
+
],
|
|
23
|
+
"concurrency": 10,
|
|
24
|
+
"exactVersions": true,
|
|
25
|
+
"autoIncludedDependencies": [],
|
|
26
|
+
"autoIncludedDependencySpecs": {}
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"semver": "^7.7.4"
|
|
30
|
+
}
|
|
31
|
+
}
|