lockdelta 0.1.0 → 0.1.2
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/action.yml +19 -3
- package/dist/action.cjs +9306 -0
- package/dist/action.cjs.map +1 -0
- package/dist/cli.js +357 -336
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +350 -330
- package/dist/index.js.map +1 -1
- package/package.json +21 -13
- package/dist/action.js +0 -1144
- package/dist/action.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -4,158 +4,151 @@
|
|
|
4
4
|
import { writeFileSync } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
|
-
// src/index.ts
|
|
8
|
-
import { readFileSync } from "fs";
|
|
9
|
-
|
|
10
7
|
// src/core/report.ts
|
|
11
8
|
import { posix as posix2 } from "path";
|
|
12
9
|
|
|
13
|
-
// src/ecosystems/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
const data = parse(content);
|
|
18
|
-
const packages = {};
|
|
19
|
-
for (const pkg of [...data.package ?? [], ...data.packages ?? []]) {
|
|
20
|
-
if (typeof pkg.name === "string" && typeof pkg.version === "string") {
|
|
21
|
-
packages[pkg.name] = pkg.version;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return packages;
|
|
25
|
-
} catch {
|
|
26
|
-
return parseTomlPackagesRegex(content);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
function parseTomlPackagesRegex(content) {
|
|
30
|
-
const packages = {};
|
|
31
|
-
const blocks = content.split(/\[\[packages?\]\]/);
|
|
32
|
-
for (const block of blocks) {
|
|
33
|
-
const nameMatch = block.match(/\nname\s*=\s*"([^"]+)"/);
|
|
34
|
-
const versionMatch = block.match(/\nversion\s*=\s*"([^"]+)"/);
|
|
35
|
-
if (nameMatch && versionMatch) {
|
|
36
|
-
packages[nameMatch[1]] = versionMatch[1];
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return packages;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// src/ecosystems/python/pyproject.ts
|
|
43
|
-
import { parse as parse2 } from "smol-toml";
|
|
44
|
-
function normalizePythonName(name) {
|
|
45
|
-
return name.toLowerCase().replace(/[-_.]+/g, "_");
|
|
46
|
-
}
|
|
47
|
-
function extractPkgName(dep) {
|
|
48
|
-
const match = String(dep).match(/^([\w][\w.-]*)/);
|
|
49
|
-
return match ? normalizePythonName(match[1]) : null;
|
|
10
|
+
// src/ecosystems/deno/deno-json.ts
|
|
11
|
+
function normalizeDenoName(name) {
|
|
12
|
+
return name.toLowerCase();
|
|
50
13
|
}
|
|
51
14
|
function parseDirectDeps(content) {
|
|
52
15
|
const prod = /* @__PURE__ */ new Set();
|
|
53
|
-
const dev = /* @__PURE__ */ new Set();
|
|
54
16
|
let data;
|
|
55
17
|
try {
|
|
56
|
-
data =
|
|
18
|
+
data = JSON.parse(content);
|
|
57
19
|
} catch {
|
|
58
|
-
return { prod, dev };
|
|
59
|
-
}
|
|
60
|
-
const project = data["project"];
|
|
61
|
-
const pep517Deps = project?.["dependencies"];
|
|
62
|
-
if (Array.isArray(pep517Deps)) {
|
|
63
|
-
for (const dep of pep517Deps) {
|
|
64
|
-
const name = extractPkgName(dep);
|
|
65
|
-
if (name) prod.add(name);
|
|
66
|
-
}
|
|
20
|
+
return { prod, dev: /* @__PURE__ */ new Set() };
|
|
67
21
|
}
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
70
|
-
for (const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const name = extractPkgName(dep);
|
|
74
|
-
if (name && !prod.has(name)) dev.add(name);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
22
|
+
const imports = data.imports;
|
|
23
|
+
if (imports) {
|
|
24
|
+
for (const specifier of Object.values(imports)) {
|
|
25
|
+
const name = extractPackageName(specifier);
|
|
26
|
+
if (name) prod.add(normalizeDenoName(name));
|
|
77
27
|
}
|
|
78
28
|
}
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (poetryDeps) {
|
|
84
|
-
for (const key of Object.keys(poetryDeps)) {
|
|
85
|
-
if (key.toLowerCase() !== "python") prod.add(normalizePythonName(key));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const devDeps = poetry["dev-dependencies"];
|
|
89
|
-
if (devDeps) {
|
|
90
|
-
for (const key of Object.keys(devDeps)) {
|
|
91
|
-
const normalized = normalizePythonName(key);
|
|
92
|
-
if (!prod.has(normalized)) dev.add(normalized);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
const groups = poetry["group"];
|
|
96
|
-
if (groups) {
|
|
97
|
-
for (const group of Object.values(groups)) {
|
|
98
|
-
const groupDeps = group["dependencies"];
|
|
99
|
-
if (groupDeps) {
|
|
100
|
-
for (const key of Object.keys(groupDeps)) {
|
|
101
|
-
const normalized = normalizePythonName(key);
|
|
102
|
-
if (!prod.has(normalized)) dev.add(normalized);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
29
|
+
const workspace = data.workspace;
|
|
30
|
+
for (const specifier of workspace?.dependencies ?? []) {
|
|
31
|
+
const name = extractPackageName(specifier);
|
|
32
|
+
if (name) prod.add(normalizeDenoName(name));
|
|
107
33
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
34
|
+
return { prod, dev: /* @__PURE__ */ new Set() };
|
|
35
|
+
}
|
|
36
|
+
function extractPackageName(specifier) {
|
|
37
|
+
const withoutProtocol = specifier.replace(/^(?:npm|jsr|node):/, "");
|
|
38
|
+
if (specifier.startsWith("node:")) return null;
|
|
39
|
+
if (withoutProtocol.startsWith("@")) {
|
|
40
|
+
const atIdx2 = withoutProtocol.indexOf("@", 1);
|
|
41
|
+
return atIdx2 > 0 ? withoutProtocol.slice(0, atIdx2) : withoutProtocol;
|
|
115
42
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
43
|
+
const atIdx = withoutProtocol.indexOf("@");
|
|
44
|
+
return atIdx > 0 ? withoutProtocol.slice(0, atIdx) : withoutProtocol || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/ecosystems/deno/parsers/deno-lock.ts
|
|
48
|
+
function parseDenoLock(content) {
|
|
49
|
+
const data = JSON.parse(content);
|
|
50
|
+
const result = {};
|
|
51
|
+
for (const [key, registry2] of [
|
|
52
|
+
["npm", data.packages?.npm],
|
|
53
|
+
["jsr", data.packages?.jsr]
|
|
54
|
+
]) {
|
|
55
|
+
if (!registry2) continue;
|
|
56
|
+
for (const specifier of Object.keys(registry2)) {
|
|
57
|
+
const { name, version } = splitSpecifier(specifier);
|
|
58
|
+
const resultKey = key === "jsr" ? `jsr:${name}` : name;
|
|
59
|
+
if (name && version && !result[resultKey]) {
|
|
60
|
+
result[resultKey] = version;
|
|
126
61
|
}
|
|
127
62
|
}
|
|
128
63
|
}
|
|
129
|
-
return
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
function splitSpecifier(specifier) {
|
|
67
|
+
if (specifier.startsWith("@")) {
|
|
68
|
+
const atIdx2 = specifier.indexOf("@", 1);
|
|
69
|
+
if (atIdx2 < 0) return { name: specifier, version: "" };
|
|
70
|
+
return { name: specifier.slice(0, atIdx2), version: specifier.slice(atIdx2 + 1) };
|
|
71
|
+
}
|
|
72
|
+
const atIdx = specifier.indexOf("@");
|
|
73
|
+
if (atIdx < 0) return { name: specifier, version: "" };
|
|
74
|
+
return { name: specifier.slice(0, atIdx), version: specifier.slice(atIdx + 1) };
|
|
130
75
|
}
|
|
131
76
|
|
|
132
|
-
// src/ecosystems/
|
|
133
|
-
var SUPPORTED_LOCKFILES = [
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
{ filename: "pdm.lock", type: "pdm" },
|
|
137
|
-
{ filename: "pylock.toml", type: "pylock" }
|
|
138
|
-
// PEP 751
|
|
139
|
-
];
|
|
140
|
-
var lockfileTypeMap = new Map(SUPPORTED_LOCKFILES.map((l) => [l.filename, l.type]));
|
|
141
|
-
var pythonEcosystem = {
|
|
142
|
-
name: "python",
|
|
77
|
+
// src/ecosystems/deno/index.ts
|
|
78
|
+
var SUPPORTED_LOCKFILES = [{ filename: "deno.lock", type: "deno" }];
|
|
79
|
+
var denoEcosystem = {
|
|
80
|
+
name: "deno",
|
|
143
81
|
supportedLockfiles: SUPPORTED_LOCKFILES,
|
|
144
|
-
manifestName: "
|
|
82
|
+
manifestName: "deno.json",
|
|
145
83
|
getLockfileType(filename) {
|
|
146
|
-
return
|
|
84
|
+
return filename === "deno.lock" ? "deno" : void 0;
|
|
147
85
|
},
|
|
148
86
|
parseLockfile(content, _lockfileType) {
|
|
149
|
-
return
|
|
87
|
+
return parseDenoLock(content);
|
|
150
88
|
},
|
|
151
89
|
parseDirectDeps(manifestContent) {
|
|
152
90
|
return parseDirectDeps(manifestContent);
|
|
153
91
|
},
|
|
154
92
|
normalizeName(name) {
|
|
155
|
-
return
|
|
93
|
+
return normalizeDenoName(name);
|
|
156
94
|
}
|
|
157
95
|
};
|
|
158
96
|
|
|
97
|
+
// src/ecosystems/javascript/package-json.ts
|
|
98
|
+
var PROD_SECTIONS = ["dependencies", "optionalDependencies", "peerDependencies"];
|
|
99
|
+
function normalizeJsName(name) {
|
|
100
|
+
return name.toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
function parseDirectDeps2(content) {
|
|
103
|
+
const prod = /* @__PURE__ */ new Set();
|
|
104
|
+
const dev = /* @__PURE__ */ new Set();
|
|
105
|
+
let data;
|
|
106
|
+
try {
|
|
107
|
+
data = JSON.parse(content);
|
|
108
|
+
} catch {
|
|
109
|
+
return { prod, dev };
|
|
110
|
+
}
|
|
111
|
+
for (const section of PROD_SECTIONS) {
|
|
112
|
+
const deps = data[section];
|
|
113
|
+
if (deps && typeof deps === "object") {
|
|
114
|
+
for (const name of Object.keys(deps)) {
|
|
115
|
+
prod.add(normalizeJsName(name));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const devDeps = data.devDependencies;
|
|
120
|
+
if (devDeps && typeof devDeps === "object") {
|
|
121
|
+
for (const name of Object.keys(devDeps)) {
|
|
122
|
+
const normalized = normalizeJsName(name);
|
|
123
|
+
if (!prod.has(normalized)) dev.add(normalized);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { prod, dev };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/ecosystems/javascript/parsers/bun.ts
|
|
130
|
+
function parseBunLock(content) {
|
|
131
|
+
const data = JSON.parse(content);
|
|
132
|
+
const result = {};
|
|
133
|
+
for (const [name, entry] of Object.entries(data.packages ?? {})) {
|
|
134
|
+
if (!Array.isArray(entry)) continue;
|
|
135
|
+
const nameAtVersion = entry[0];
|
|
136
|
+
if (typeof nameAtVersion !== "string") continue;
|
|
137
|
+
const version = extractVersion(nameAtVersion);
|
|
138
|
+
if (!version || version.startsWith("workspace:")) continue;
|
|
139
|
+
result[name] = version;
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
function extractVersion(nameAtVersion) {
|
|
144
|
+
if (nameAtVersion.startsWith("@")) {
|
|
145
|
+
const atIdx2 = nameAtVersion.indexOf("@", 1);
|
|
146
|
+
return atIdx2 > 0 ? nameAtVersion.slice(atIdx2 + 1) : "";
|
|
147
|
+
}
|
|
148
|
+
const atIdx = nameAtVersion.indexOf("@");
|
|
149
|
+
return atIdx > 0 ? nameAtVersion.slice(atIdx + 1) : "";
|
|
150
|
+
}
|
|
151
|
+
|
|
159
152
|
// src/ecosystems/javascript/parsers/npm.ts
|
|
160
153
|
function parseNpmLock(content) {
|
|
161
154
|
const data = JSON.parse(content);
|
|
@@ -195,69 +188,10 @@ function parseV1Dependencies(deps, result = {}) {
|
|
|
195
188
|
return result;
|
|
196
189
|
}
|
|
197
190
|
|
|
198
|
-
// src/ecosystems/javascript/parsers/yarn.ts
|
|
199
|
-
import { parse as parseYaml } from "yaml";
|
|
200
|
-
function parseYarnLock(content) {
|
|
201
|
-
return isYarnBerry(content) ? parseYarnBerry(content) : parseYarnV1(content);
|
|
202
|
-
}
|
|
203
|
-
function isYarnBerry(content) {
|
|
204
|
-
return content.includes("__metadata:");
|
|
205
|
-
}
|
|
206
|
-
function extractNameFromSpecifier(spec) {
|
|
207
|
-
const trimmed = spec.trim().replace(/^"|"$/g, "");
|
|
208
|
-
if (trimmed.startsWith("@")) {
|
|
209
|
-
const idx = trimmed.indexOf("@", 1);
|
|
210
|
-
return idx > 0 ? trimmed.slice(0, idx) : trimmed;
|
|
211
|
-
}
|
|
212
|
-
const atIdx = trimmed.indexOf("@");
|
|
213
|
-
return atIdx > 0 ? trimmed.slice(0, atIdx) : trimmed;
|
|
214
|
-
}
|
|
215
|
-
function parseYarnV1(content) {
|
|
216
|
-
const packages = {};
|
|
217
|
-
const blocks = content.split(/\n\n+/);
|
|
218
|
-
for (const block of blocks) {
|
|
219
|
-
const trimmed = block.trim();
|
|
220
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
221
|
-
const versionMatch = trimmed.match(/^[ \t]+version "([^"]+)"/m);
|
|
222
|
-
if (!versionMatch) continue;
|
|
223
|
-
const headerLine = trimmed.split("\n")[0].trim().replace(/:$/, "");
|
|
224
|
-
const firstSpecifier = headerLine.split(",")[0].trim().replace(/^"|"$/g, "");
|
|
225
|
-
const name = extractNameFromSpecifier(firstSpecifier);
|
|
226
|
-
if (name && !packages[name]) {
|
|
227
|
-
packages[name] = versionMatch[1];
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return packages;
|
|
231
|
-
}
|
|
232
|
-
function parseYarnBerry(content) {
|
|
233
|
-
const data = parseYaml(content);
|
|
234
|
-
const packages = {};
|
|
235
|
-
for (const [key, value] of Object.entries(data)) {
|
|
236
|
-
if (key === "__metadata") continue;
|
|
237
|
-
if (typeof value !== "object" || !value) continue;
|
|
238
|
-
const entry = value;
|
|
239
|
-
if (entry.linkType === "soft") continue;
|
|
240
|
-
if (!entry.version) continue;
|
|
241
|
-
const cleanKey = key.replace(/^"|"$/g, "");
|
|
242
|
-
const name = extractNameFromBerryKey(cleanKey);
|
|
243
|
-
if (name && !packages[name]) {
|
|
244
|
-
packages[name] = entry.version;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return packages;
|
|
248
|
-
}
|
|
249
|
-
function extractNameFromBerryKey(key) {
|
|
250
|
-
if (key.startsWith("@")) {
|
|
251
|
-
const idx = key.indexOf("@", 1);
|
|
252
|
-
return idx > 0 ? key.slice(0, idx) : key;
|
|
253
|
-
}
|
|
254
|
-
return key.split("@")[0];
|
|
255
|
-
}
|
|
256
|
-
|
|
257
191
|
// src/ecosystems/javascript/parsers/pnpm.ts
|
|
258
|
-
import { parse as
|
|
192
|
+
import { parse as parseYaml } from "yaml";
|
|
259
193
|
function parsePnpmLock(content) {
|
|
260
|
-
const data =
|
|
194
|
+
const data = parseYaml(content);
|
|
261
195
|
if (!data?.packages) return {};
|
|
262
196
|
const lockfileVersion = parseLockfileVersion(data.lockfileVersion);
|
|
263
197
|
if (lockfileVersion >= 9) {
|
|
@@ -267,13 +201,14 @@ function parsePnpmLock(content) {
|
|
|
267
201
|
}
|
|
268
202
|
function parseLockfileVersion(v) {
|
|
269
203
|
if (typeof v === "number") return v;
|
|
270
|
-
if (typeof v === "string") return parseFloat(v);
|
|
204
|
+
if (typeof v === "string") return Number.parseFloat(v);
|
|
271
205
|
return 0;
|
|
272
206
|
}
|
|
273
207
|
function parsePnpmV9(packages) {
|
|
274
208
|
const result = {};
|
|
275
209
|
for (const key of Object.keys(packages)) {
|
|
276
|
-
let name
|
|
210
|
+
let name;
|
|
211
|
+
let version;
|
|
277
212
|
if (key.startsWith("@")) {
|
|
278
213
|
const atIdx = key.indexOf("@", 1);
|
|
279
214
|
if (atIdx < 0) continue;
|
|
@@ -296,7 +231,8 @@ function parsePnpmLegacy(packages) {
|
|
|
296
231
|
const result = {};
|
|
297
232
|
for (const key of Object.keys(packages)) {
|
|
298
233
|
const cleaned = key.startsWith("/") ? key.slice(1) : key;
|
|
299
|
-
let name
|
|
234
|
+
let name;
|
|
235
|
+
let version;
|
|
300
236
|
if (cleaned.startsWith("@")) {
|
|
301
237
|
const secondSlash = cleaned.indexOf("/", cleaned.indexOf("/") + 1);
|
|
302
238
|
const secondAt = cleaned.indexOf("@", 1);
|
|
@@ -333,59 +269,63 @@ function stripVersionSuffix(version) {
|
|
|
333
269
|
return version.split("(")[0].split("_")[0].trim();
|
|
334
270
|
}
|
|
335
271
|
|
|
336
|
-
// src/ecosystems/javascript/parsers/
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
for (const [name, entry] of Object.entries(data.packages ?? {})) {
|
|
341
|
-
if (!Array.isArray(entry)) continue;
|
|
342
|
-
const nameAtVersion = entry[0];
|
|
343
|
-
if (typeof nameAtVersion !== "string") continue;
|
|
344
|
-
const version = extractVersion(nameAtVersion);
|
|
345
|
-
if (!version || version.startsWith("workspace:")) continue;
|
|
346
|
-
result[name] = version;
|
|
347
|
-
}
|
|
348
|
-
return result;
|
|
349
|
-
}
|
|
350
|
-
function extractVersion(nameAtVersion) {
|
|
351
|
-
if (nameAtVersion.startsWith("@")) {
|
|
352
|
-
const atIdx2 = nameAtVersion.indexOf("@", 1);
|
|
353
|
-
return atIdx2 > 0 ? nameAtVersion.slice(atIdx2 + 1) : "";
|
|
354
|
-
}
|
|
355
|
-
const atIdx = nameAtVersion.indexOf("@");
|
|
356
|
-
return atIdx > 0 ? nameAtVersion.slice(atIdx + 1) : "";
|
|
272
|
+
// src/ecosystems/javascript/parsers/yarn.ts
|
|
273
|
+
import { parse as parseYaml2 } from "yaml";
|
|
274
|
+
function parseYarnLock(content) {
|
|
275
|
+
return isYarnBerry(content) ? parseYarnBerry(content) : parseYarnV1(content);
|
|
357
276
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
var PROD_SECTIONS = ["dependencies", "optionalDependencies", "peerDependencies"];
|
|
361
|
-
function normalizeJsName(name) {
|
|
362
|
-
return name.toLowerCase();
|
|
277
|
+
function isYarnBerry(content) {
|
|
278
|
+
return content.includes("__metadata:");
|
|
363
279
|
}
|
|
364
|
-
function
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
data = JSON.parse(content);
|
|
370
|
-
} catch {
|
|
371
|
-
return { prod, dev };
|
|
280
|
+
function extractNameFromSpecifier(spec) {
|
|
281
|
+
const trimmed = spec.trim().replace(/^"|"$/g, "");
|
|
282
|
+
if (trimmed.startsWith("@")) {
|
|
283
|
+
const idx = trimmed.indexOf("@", 1);
|
|
284
|
+
return idx > 0 ? trimmed.slice(0, idx) : trimmed;
|
|
372
285
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
286
|
+
const atIdx = trimmed.indexOf("@");
|
|
287
|
+
return atIdx > 0 ? trimmed.slice(0, atIdx) : trimmed;
|
|
288
|
+
}
|
|
289
|
+
function parseYarnV1(content) {
|
|
290
|
+
const packages = {};
|
|
291
|
+
const blocks = content.split(/\n\n+/);
|
|
292
|
+
for (const block of blocks) {
|
|
293
|
+
const trimmed = block.trim();
|
|
294
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
295
|
+
const versionMatch = trimmed.match(/^[ \t]+version "([^"]+)"/m);
|
|
296
|
+
if (!versionMatch) continue;
|
|
297
|
+
const headerLine = trimmed.split("\n")[0].trim().replace(/:$/, "");
|
|
298
|
+
const firstSpecifier = headerLine.split(",")[0].trim().replace(/^"|"$/g, "");
|
|
299
|
+
const name = extractNameFromSpecifier(firstSpecifier);
|
|
300
|
+
if (name && !packages[name]) {
|
|
301
|
+
packages[name] = versionMatch[1];
|
|
379
302
|
}
|
|
380
303
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
304
|
+
return packages;
|
|
305
|
+
}
|
|
306
|
+
function parseYarnBerry(content) {
|
|
307
|
+
const data = parseYaml2(content);
|
|
308
|
+
const packages = {};
|
|
309
|
+
for (const [key, value] of Object.entries(data)) {
|
|
310
|
+
if (key === "__metadata") continue;
|
|
311
|
+
if (typeof value !== "object" || !value) continue;
|
|
312
|
+
const entry = value;
|
|
313
|
+
if (entry.linkType === "soft") continue;
|
|
314
|
+
if (!entry.version) continue;
|
|
315
|
+
const cleanKey = key.replace(/^"|"$/g, "");
|
|
316
|
+
const name = extractNameFromBerryKey(cleanKey);
|
|
317
|
+
if (name && !packages[name]) {
|
|
318
|
+
packages[name] = entry.version;
|
|
386
319
|
}
|
|
387
320
|
}
|
|
388
|
-
return
|
|
321
|
+
return packages;
|
|
322
|
+
}
|
|
323
|
+
function extractNameFromBerryKey(key) {
|
|
324
|
+
if (key.startsWith("@")) {
|
|
325
|
+
const idx = key.indexOf("@", 1);
|
|
326
|
+
return idx > 0 ? key.slice(0, idx) : key;
|
|
327
|
+
}
|
|
328
|
+
return key.split("@")[0];
|
|
389
329
|
}
|
|
390
330
|
|
|
391
331
|
// src/ecosystems/javascript/index.ts
|
|
@@ -395,13 +335,13 @@ var SUPPORTED_LOCKFILES2 = [
|
|
|
395
335
|
{ filename: "pnpm-lock.yaml", type: "pnpm" },
|
|
396
336
|
{ filename: "bun.lock", type: "bun" }
|
|
397
337
|
];
|
|
398
|
-
var
|
|
338
|
+
var lockfileTypeMap = new Map(SUPPORTED_LOCKFILES2.map((l) => [l.filename, l.type]));
|
|
399
339
|
var javascriptEcosystem = {
|
|
400
340
|
name: "javascript",
|
|
401
341
|
supportedLockfiles: SUPPORTED_LOCKFILES2,
|
|
402
342
|
manifestName: "package.json",
|
|
403
343
|
getLockfileType(filename) {
|
|
404
|
-
return
|
|
344
|
+
return lockfileTypeMap.get(filename);
|
|
405
345
|
},
|
|
406
346
|
parseLockfile(content, lockfileType) {
|
|
407
347
|
switch (lockfileType) {
|
|
@@ -425,90 +365,149 @@ var javascriptEcosystem = {
|
|
|
425
365
|
}
|
|
426
366
|
};
|
|
427
367
|
|
|
428
|
-
// src/ecosystems/
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
[
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
for (const specifier of Object.keys(registry2)) {
|
|
438
|
-
const { name, version } = splitSpecifier(specifier);
|
|
439
|
-
const resultKey = key === "jsr" ? `jsr:${name}` : name;
|
|
440
|
-
if (name && version && !result[resultKey]) {
|
|
441
|
-
result[resultKey] = version;
|
|
368
|
+
// src/ecosystems/python/parsers/toml.ts
|
|
369
|
+
import { parse } from "smol-toml";
|
|
370
|
+
function parseTomlPackages(content) {
|
|
371
|
+
try {
|
|
372
|
+
const data = parse(content);
|
|
373
|
+
const packages = {};
|
|
374
|
+
for (const pkg of [...data.package ?? [], ...data.packages ?? []]) {
|
|
375
|
+
if (typeof pkg.name === "string" && typeof pkg.version === "string") {
|
|
376
|
+
packages[pkg.name] = pkg.version;
|
|
442
377
|
}
|
|
443
378
|
}
|
|
379
|
+
return packages;
|
|
380
|
+
} catch {
|
|
381
|
+
return parseTomlPackagesRegex(content);
|
|
444
382
|
}
|
|
445
|
-
return result;
|
|
446
383
|
}
|
|
447
|
-
function
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
384
|
+
function parseTomlPackagesRegex(content) {
|
|
385
|
+
const packages = {};
|
|
386
|
+
const blocks = content.split(/\[\[packages?\]\]/);
|
|
387
|
+
for (const block of blocks) {
|
|
388
|
+
const nameMatch = block.match(/\nname\s*=\s*"([^"]+)"/);
|
|
389
|
+
const versionMatch = block.match(/\nversion\s*=\s*"([^"]+)"/);
|
|
390
|
+
if (nameMatch && versionMatch) {
|
|
391
|
+
packages[nameMatch[1]] = versionMatch[1];
|
|
392
|
+
}
|
|
452
393
|
}
|
|
453
|
-
|
|
454
|
-
if (atIdx < 0) return { name: specifier, version: "" };
|
|
455
|
-
return { name: specifier.slice(0, atIdx), version: specifier.slice(atIdx + 1) };
|
|
394
|
+
return packages;
|
|
456
395
|
}
|
|
457
396
|
|
|
458
|
-
// src/ecosystems/
|
|
459
|
-
|
|
460
|
-
|
|
397
|
+
// src/ecosystems/python/pyproject.ts
|
|
398
|
+
import { parse as parse2 } from "smol-toml";
|
|
399
|
+
function normalizePythonName(name) {
|
|
400
|
+
return name.toLowerCase().replace(/[-_.]+/g, "_");
|
|
401
|
+
}
|
|
402
|
+
function extractPkgName(dep) {
|
|
403
|
+
const match = String(dep).match(/^([\w][\w.-]*)/);
|
|
404
|
+
return match ? normalizePythonName(match[1]) : null;
|
|
461
405
|
}
|
|
462
406
|
function parseDirectDeps3(content) {
|
|
463
407
|
const prod = /* @__PURE__ */ new Set();
|
|
408
|
+
const dev = /* @__PURE__ */ new Set();
|
|
464
409
|
let data;
|
|
465
410
|
try {
|
|
466
|
-
data =
|
|
411
|
+
data = parse2(content);
|
|
467
412
|
} catch {
|
|
468
|
-
return { prod, dev
|
|
413
|
+
return { prod, dev };
|
|
469
414
|
}
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
415
|
+
const project = data.project;
|
|
416
|
+
const pep517Deps = project?.dependencies;
|
|
417
|
+
if (Array.isArray(pep517Deps)) {
|
|
418
|
+
for (const dep of pep517Deps) {
|
|
419
|
+
const name = extractPkgName(dep);
|
|
420
|
+
if (name) prod.add(name);
|
|
475
421
|
}
|
|
476
422
|
}
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
423
|
+
const optDeps = project?.["optional-dependencies"];
|
|
424
|
+
if (optDeps && typeof optDeps === "object") {
|
|
425
|
+
for (const group of Object.values(optDeps)) {
|
|
426
|
+
if (Array.isArray(group)) {
|
|
427
|
+
for (const dep of group) {
|
|
428
|
+
const name = extractPkgName(dep);
|
|
429
|
+
if (name && !prod.has(name)) dev.add(name);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
481
433
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
434
|
+
const tool = data.tool;
|
|
435
|
+
const poetry = tool?.poetry;
|
|
436
|
+
if (poetry) {
|
|
437
|
+
const poetryDeps = poetry.dependencies;
|
|
438
|
+
if (poetryDeps) {
|
|
439
|
+
for (const key of Object.keys(poetryDeps)) {
|
|
440
|
+
if (key.toLowerCase() !== "python") prod.add(normalizePythonName(key));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const devDeps = poetry["dev-dependencies"];
|
|
444
|
+
if (devDeps) {
|
|
445
|
+
for (const key of Object.keys(devDeps)) {
|
|
446
|
+
const normalized = normalizePythonName(key);
|
|
447
|
+
if (!prod.has(normalized)) dev.add(normalized);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const groups = poetry.group;
|
|
451
|
+
if (groups) {
|
|
452
|
+
for (const group of Object.values(groups)) {
|
|
453
|
+
const groupDeps = group.dependencies;
|
|
454
|
+
if (groupDeps) {
|
|
455
|
+
for (const key of Object.keys(groupDeps)) {
|
|
456
|
+
const normalized = normalizePythonName(key);
|
|
457
|
+
if (!prod.has(normalized)) dev.add(normalized);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
490
462
|
}
|
|
491
|
-
const
|
|
492
|
-
|
|
463
|
+
const uv = tool?.uv;
|
|
464
|
+
const uvDevDeps = uv?.["dev-dependencies"];
|
|
465
|
+
if (Array.isArray(uvDevDeps)) {
|
|
466
|
+
for (const dep of uvDevDeps) {
|
|
467
|
+
const name = extractPkgName(dep);
|
|
468
|
+
if (name && !prod.has(name)) dev.add(name);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const depGroups = data["dependency-groups"];
|
|
472
|
+
if (depGroups && typeof depGroups === "object") {
|
|
473
|
+
for (const group of Object.values(depGroups)) {
|
|
474
|
+
if (Array.isArray(group)) {
|
|
475
|
+
for (const entry of group) {
|
|
476
|
+
if (typeof entry === "string") {
|
|
477
|
+
const name = extractPkgName(entry);
|
|
478
|
+
if (name && !prod.has(name)) dev.add(name);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return { prod, dev };
|
|
493
485
|
}
|
|
494
486
|
|
|
495
|
-
// src/ecosystems/
|
|
496
|
-
var SUPPORTED_LOCKFILES3 = [
|
|
497
|
-
|
|
498
|
-
|
|
487
|
+
// src/ecosystems/python/index.ts
|
|
488
|
+
var SUPPORTED_LOCKFILES3 = [
|
|
489
|
+
{ filename: "uv.lock", type: "uv" },
|
|
490
|
+
{ filename: "poetry.lock", type: "poetry" },
|
|
491
|
+
{ filename: "pdm.lock", type: "pdm" },
|
|
492
|
+
{ filename: "pylock.toml", type: "pylock" }
|
|
493
|
+
// PEP 751
|
|
494
|
+
];
|
|
495
|
+
var lockfileTypeMap2 = new Map(SUPPORTED_LOCKFILES3.map((l) => [l.filename, l.type]));
|
|
496
|
+
var pythonEcosystem = {
|
|
497
|
+
name: "python",
|
|
499
498
|
supportedLockfiles: SUPPORTED_LOCKFILES3,
|
|
500
|
-
manifestName: "
|
|
499
|
+
manifestName: "pyproject.toml",
|
|
501
500
|
getLockfileType(filename) {
|
|
502
|
-
return filename
|
|
501
|
+
return lockfileTypeMap2.get(filename);
|
|
503
502
|
},
|
|
504
503
|
parseLockfile(content, _lockfileType) {
|
|
505
|
-
return
|
|
504
|
+
return parseTomlPackages(content);
|
|
506
505
|
},
|
|
507
506
|
parseDirectDeps(manifestContent) {
|
|
508
507
|
return parseDirectDeps3(manifestContent);
|
|
509
508
|
},
|
|
510
509
|
normalizeName(name) {
|
|
511
|
-
return
|
|
510
|
+
return normalizePythonName(name);
|
|
512
511
|
}
|
|
513
512
|
};
|
|
514
513
|
|
|
@@ -533,6 +532,29 @@ registerEcosystem(pythonEcosystem);
|
|
|
533
532
|
registerEcosystem(javascriptEcosystem);
|
|
534
533
|
registerEcosystem(denoEcosystem);
|
|
535
534
|
|
|
535
|
+
// src/core/diff.ts
|
|
536
|
+
function diffPackages(oldPkgs, newPkgs, directDeps, normalizeName) {
|
|
537
|
+
const allNames = /* @__PURE__ */ new Set([...Object.keys(oldPkgs), ...Object.keys(newPkgs)]);
|
|
538
|
+
const changes = [];
|
|
539
|
+
for (const name of [...allNames].sort()) {
|
|
540
|
+
const inOld = name in oldPkgs;
|
|
541
|
+
const inNew = name in newPkgs;
|
|
542
|
+
if (inOld && inNew && oldPkgs[name] === newPkgs[name]) continue;
|
|
543
|
+
const normalized = normalizeName(name);
|
|
544
|
+
const isProd = directDeps.prod.has(normalized);
|
|
545
|
+
const isDev = directDeps.dev.has(normalized) && !isProd;
|
|
546
|
+
changes.push({
|
|
547
|
+
name,
|
|
548
|
+
change_type: !inOld ? "added" : !inNew ? "removed" : "updated",
|
|
549
|
+
old_version: inOld ? oldPkgs[name] : null,
|
|
550
|
+
new_version: inNew ? newPkgs[name] : null,
|
|
551
|
+
is_direct: isProd || isDev,
|
|
552
|
+
is_dev: isDev
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
return changes;
|
|
556
|
+
}
|
|
557
|
+
|
|
536
558
|
// src/core/discovery.ts
|
|
537
559
|
import { posix } from "path";
|
|
538
560
|
function workspaceFromPath(filePath) {
|
|
@@ -596,6 +618,7 @@ function resolveLockfilePair(baseFiles, headFiles) {
|
|
|
596
618
|
basePath: chosen.path,
|
|
597
619
|
baseType: chosen.type,
|
|
598
620
|
headPath: chosen.path,
|
|
621
|
+
// biome-ignore lint/style/noNonNullAssertion: path is guaranteed present (comes from common set)
|
|
599
622
|
headType: headByPath.get(chosen.path).type,
|
|
600
623
|
migrationNote: null,
|
|
601
624
|
ecosystemName: chosen.ecosystemName
|
|
@@ -638,29 +661,6 @@ function resolveLockfilePair(baseFiles, headFiles) {
|
|
|
638
661
|
return null;
|
|
639
662
|
}
|
|
640
663
|
|
|
641
|
-
// src/core/diff.ts
|
|
642
|
-
function diffPackages(oldPkgs, newPkgs, directDeps, normalizeName) {
|
|
643
|
-
const allNames = /* @__PURE__ */ new Set([...Object.keys(oldPkgs), ...Object.keys(newPkgs)]);
|
|
644
|
-
const changes = [];
|
|
645
|
-
for (const name of [...allNames].sort()) {
|
|
646
|
-
const inOld = name in oldPkgs;
|
|
647
|
-
const inNew = name in newPkgs;
|
|
648
|
-
if (inOld && inNew && oldPkgs[name] === newPkgs[name]) continue;
|
|
649
|
-
const normalized = normalizeName(name);
|
|
650
|
-
const isProd = directDeps.prod.has(normalized);
|
|
651
|
-
const isDev = directDeps.dev.has(normalized) && !isProd;
|
|
652
|
-
changes.push({
|
|
653
|
-
name,
|
|
654
|
-
change_type: !inOld ? "added" : !inNew ? "removed" : "updated",
|
|
655
|
-
old_version: inOld ? oldPkgs[name] : null,
|
|
656
|
-
new_version: inNew ? newPkgs[name] : null,
|
|
657
|
-
is_direct: isProd || isDev,
|
|
658
|
-
is_dev: isDev
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
return changes;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
664
|
// src/core/report.ts
|
|
665
665
|
async function buildLockfileEntry(pair, workspace, getBase, getHead) {
|
|
666
666
|
const ecosystem = getEcosystemByName(pair.ecosystemName);
|
|
@@ -793,14 +793,29 @@ function gitLsTree(ref) {
|
|
|
793
793
|
// src/sources/github.ts
|
|
794
794
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
795
795
|
var API_BASE = "https://api.github.com";
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (
|
|
799
|
-
|
|
796
|
+
var cachedToken;
|
|
797
|
+
function resolveToken() {
|
|
798
|
+
if (cachedToken) return cachedToken;
|
|
799
|
+
if (process.env.GITHUB_TOKEN) {
|
|
800
|
+
cachedToken = process.env.GITHUB_TOKEN;
|
|
801
|
+
return cachedToken;
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
const t = execFileSync2("gh", ["auth", "token"], {
|
|
805
|
+
encoding: "utf-8",
|
|
806
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
807
|
+
}).trim();
|
|
808
|
+
if (t) {
|
|
809
|
+
cachedToken = t;
|
|
810
|
+
return cachedToken;
|
|
811
|
+
}
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
814
|
+
throw new Error("No GitHub token found. Set GITHUB_TOKEN or run `gh auth login`.");
|
|
800
815
|
}
|
|
801
816
|
function headers(accept = "application/vnd.github+json") {
|
|
802
817
|
return {
|
|
803
|
-
Authorization: `Bearer ${
|
|
818
|
+
Authorization: `Bearer ${resolveToken()}`,
|
|
804
819
|
Accept: accept,
|
|
805
820
|
"X-GitHub-Api-Version": "2022-11-28"
|
|
806
821
|
};
|
|
@@ -830,7 +845,7 @@ async function getPrShas(prNumber, repo) {
|
|
|
830
845
|
return { baseRefOid: data.base.sha, headRefOid: data.head.sha };
|
|
831
846
|
}
|
|
832
847
|
function detectRepo() {
|
|
833
|
-
const fromEnv = process.env
|
|
848
|
+
const fromEnv = process.env.GITHUB_REPOSITORY;
|
|
834
849
|
if (fromEnv) return fromEnv;
|
|
835
850
|
try {
|
|
836
851
|
const remote = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
@@ -841,15 +856,27 @@ function detectRepo() {
|
|
|
841
856
|
if (match) return match[1];
|
|
842
857
|
} catch {
|
|
843
858
|
}
|
|
844
|
-
throw new Error(
|
|
845
|
-
|
|
846
|
-
|
|
859
|
+
throw new Error("Could not detect GitHub repo \u2014 set GITHUB_REPOSITORY or pass --repo");
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// src/sources/local.ts
|
|
863
|
+
import { readFileSync } from "fs";
|
|
864
|
+
function readLocalFile(path) {
|
|
865
|
+
try {
|
|
866
|
+
return readFileSync(path, "utf-8");
|
|
867
|
+
} catch {
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
847
870
|
}
|
|
848
871
|
|
|
849
872
|
// src/index.ts
|
|
850
873
|
async function resolveApiShas(options) {
|
|
851
874
|
if (options.baseSha && options.headSha) {
|
|
852
|
-
return {
|
|
875
|
+
return {
|
|
876
|
+
baseSha: options.baseSha,
|
|
877
|
+
headSha: options.headSha,
|
|
878
|
+
repo: options.repo ?? detectRepo()
|
|
879
|
+
};
|
|
853
880
|
}
|
|
854
881
|
if (options.prNumber) {
|
|
855
882
|
const repo = options.repo ?? detectRepo();
|
|
@@ -863,15 +890,8 @@ async function run(options = {}) {
|
|
|
863
890
|
if (options.oldFile && options.newFile) {
|
|
864
891
|
const oldPath = options.oldFile;
|
|
865
892
|
const newPath = options.newFile;
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
return readFileSync(filePath, "utf-8");
|
|
869
|
-
} catch {
|
|
870
|
-
return null;
|
|
871
|
-
}
|
|
872
|
-
};
|
|
873
|
-
const getBase2 = (path) => Promise.resolve(path === oldPath ? readLocal(oldPath) : null);
|
|
874
|
-
const getHead2 = (path) => Promise.resolve(path === newPath ? readLocal(newPath) : null);
|
|
893
|
+
const getBase2 = (path) => Promise.resolve(path === oldPath ? readLocalFile(oldPath) : null);
|
|
894
|
+
const getHead2 = (path) => Promise.resolve(path === newPath ? readLocalFile(newPath) : null);
|
|
875
895
|
const lockfiles2 = await collectLockfileEntries({
|
|
876
896
|
getBase: getBase2,
|
|
877
897
|
getHead: getHead2,
|
|
@@ -924,22 +944,22 @@ async function run(options = {}) {
|
|
|
924
944
|
|
|
925
945
|
// src/cli.ts
|
|
926
946
|
var program = new Command();
|
|
927
|
-
program.name("
|
|
947
|
+
program.name("lockdelta").description("Diff dependency lockfiles between git refs, PRs, or local files").version("0.1.0").option(
|
|
928
948
|
"--base <ref>",
|
|
929
949
|
'Base git ref (default: HEAD~1). In CI, reads GITHUB_BASE_REF \u2014 may need "origin/" prefix.',
|
|
930
|
-
process.env
|
|
950
|
+
process.env.GITHUB_BASE_REF
|
|
931
951
|
).option(
|
|
932
952
|
"--head <ref>",
|
|
933
953
|
"Head git ref (default: HEAD). In CI, reads GITHUB_HEAD_REF.",
|
|
934
|
-
process.env
|
|
954
|
+
process.env.GITHUB_HEAD_REF
|
|
935
955
|
).option(
|
|
936
956
|
"--pr <number>",
|
|
937
957
|
"GitHub PR number. Fetches exact SHAs via gh CLI.",
|
|
938
|
-
process.env
|
|
958
|
+
process.env.GITHUB_PR_NUMBER
|
|
939
959
|
).option(
|
|
940
960
|
"--repo <owner/name>",
|
|
941
961
|
"GitHub repo in OWNER/NAME format. Auto-detected if omitted.",
|
|
942
|
-
process.env
|
|
962
|
+
process.env.GITHUB_REPOSITORY
|
|
943
963
|
).option("--lockfile <path>", "Specific lockfile path. Auto-discovers all lockfiles if omitted.").option("--type <type>", "Force lockfile type: uv, poetry, pdm. Only used with --lockfile.").option("--old <path>", "Old lockfile path (local file comparison mode).").option("--new <path>", "New lockfile path (local file comparison mode).").option("--output <path>", "Write JSON report to file instead of stdout.").action(async (opts) => {
|
|
944
964
|
try {
|
|
945
965
|
const report = await run({
|
|
@@ -954,7 +974,8 @@ program.name("depdiff").description("Diff dependency lockfiles between git refs,
|
|
|
954
974
|
onNote: (msg) => process.stderr.write(`Note: ${msg}
|
|
955
975
|
`)
|
|
956
976
|
});
|
|
957
|
-
const json = JSON.stringify(report, null, 2)
|
|
977
|
+
const json = `${JSON.stringify(report, null, 2)}
|
|
978
|
+
`;
|
|
958
979
|
if (opts.output) {
|
|
959
980
|
writeFileSync(opts.output, json, "utf-8");
|
|
960
981
|
process.stderr.write(`Report written to ${opts.output}
|