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/action.js
DELETED
|
@@ -1,1144 +0,0 @@
|
|
|
1
|
-
// src/action.ts
|
|
2
|
-
import { appendFileSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
3
|
-
|
|
4
|
-
// src/index.ts
|
|
5
|
-
import { readFileSync } from "fs";
|
|
6
|
-
|
|
7
|
-
// src/core/report.ts
|
|
8
|
-
import { posix as posix2 } from "path";
|
|
9
|
-
|
|
10
|
-
// src/ecosystems/python/parsers/toml.ts
|
|
11
|
-
import { parse } from "smol-toml";
|
|
12
|
-
function parseTomlPackages(content) {
|
|
13
|
-
try {
|
|
14
|
-
const data = parse(content);
|
|
15
|
-
const packages = {};
|
|
16
|
-
for (const pkg of [...data.package ?? [], ...data.packages ?? []]) {
|
|
17
|
-
if (typeof pkg.name === "string" && typeof pkg.version === "string") {
|
|
18
|
-
packages[pkg.name] = pkg.version;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return packages;
|
|
22
|
-
} catch {
|
|
23
|
-
return parseTomlPackagesRegex(content);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
function parseTomlPackagesRegex(content) {
|
|
27
|
-
const packages = {};
|
|
28
|
-
const blocks = content.split(/\[\[packages?\]\]/);
|
|
29
|
-
for (const block of blocks) {
|
|
30
|
-
const nameMatch = block.match(/\nname\s*=\s*"([^"]+)"/);
|
|
31
|
-
const versionMatch = block.match(/\nversion\s*=\s*"([^"]+)"/);
|
|
32
|
-
if (nameMatch && versionMatch) {
|
|
33
|
-
packages[nameMatch[1]] = versionMatch[1];
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return packages;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// src/ecosystems/python/pyproject.ts
|
|
40
|
-
import { parse as parse2 } from "smol-toml";
|
|
41
|
-
function normalizePythonName(name) {
|
|
42
|
-
return name.toLowerCase().replace(/[-_.]+/g, "_");
|
|
43
|
-
}
|
|
44
|
-
function extractPkgName(dep) {
|
|
45
|
-
const match = String(dep).match(/^([\w][\w.-]*)/);
|
|
46
|
-
return match ? normalizePythonName(match[1]) : null;
|
|
47
|
-
}
|
|
48
|
-
function parseDirectDeps(content) {
|
|
49
|
-
const prod = /* @__PURE__ */ new Set();
|
|
50
|
-
const dev = /* @__PURE__ */ new Set();
|
|
51
|
-
let data;
|
|
52
|
-
try {
|
|
53
|
-
data = parse2(content);
|
|
54
|
-
} catch {
|
|
55
|
-
return { prod, dev };
|
|
56
|
-
}
|
|
57
|
-
const project = data["project"];
|
|
58
|
-
const pep517Deps = project?.["dependencies"];
|
|
59
|
-
if (Array.isArray(pep517Deps)) {
|
|
60
|
-
for (const dep of pep517Deps) {
|
|
61
|
-
const name = extractPkgName(dep);
|
|
62
|
-
if (name) prod.add(name);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
const optDeps = project?.["optional-dependencies"];
|
|
66
|
-
if (optDeps && typeof optDeps === "object") {
|
|
67
|
-
for (const group of Object.values(optDeps)) {
|
|
68
|
-
if (Array.isArray(group)) {
|
|
69
|
-
for (const dep of group) {
|
|
70
|
-
const name = extractPkgName(dep);
|
|
71
|
-
if (name && !prod.has(name)) dev.add(name);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const tool = data["tool"];
|
|
77
|
-
const poetry = tool?.["poetry"];
|
|
78
|
-
if (poetry) {
|
|
79
|
-
const poetryDeps = poetry["dependencies"];
|
|
80
|
-
if (poetryDeps) {
|
|
81
|
-
for (const key of Object.keys(poetryDeps)) {
|
|
82
|
-
if (key.toLowerCase() !== "python") prod.add(normalizePythonName(key));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const devDeps = poetry["dev-dependencies"];
|
|
86
|
-
if (devDeps) {
|
|
87
|
-
for (const key of Object.keys(devDeps)) {
|
|
88
|
-
const normalized = normalizePythonName(key);
|
|
89
|
-
if (!prod.has(normalized)) dev.add(normalized);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const groups = poetry["group"];
|
|
93
|
-
if (groups) {
|
|
94
|
-
for (const group of Object.values(groups)) {
|
|
95
|
-
const groupDeps = group["dependencies"];
|
|
96
|
-
if (groupDeps) {
|
|
97
|
-
for (const key of Object.keys(groupDeps)) {
|
|
98
|
-
const normalized = normalizePythonName(key);
|
|
99
|
-
if (!prod.has(normalized)) dev.add(normalized);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
const uv = tool?.["uv"];
|
|
106
|
-
const uvDevDeps = uv?.["dev-dependencies"];
|
|
107
|
-
if (Array.isArray(uvDevDeps)) {
|
|
108
|
-
for (const dep of uvDevDeps) {
|
|
109
|
-
const name = extractPkgName(dep);
|
|
110
|
-
if (name && !prod.has(name)) dev.add(name);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
const depGroups = data["dependency-groups"];
|
|
114
|
-
if (depGroups && typeof depGroups === "object") {
|
|
115
|
-
for (const group of Object.values(depGroups)) {
|
|
116
|
-
if (Array.isArray(group)) {
|
|
117
|
-
for (const entry of group) {
|
|
118
|
-
if (typeof entry === "string") {
|
|
119
|
-
const name = extractPkgName(entry);
|
|
120
|
-
if (name && !prod.has(name)) dev.add(name);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return { prod, dev };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// src/ecosystems/python/index.ts
|
|
130
|
-
var SUPPORTED_LOCKFILES = [
|
|
131
|
-
{ filename: "uv.lock", type: "uv" },
|
|
132
|
-
{ filename: "poetry.lock", type: "poetry" },
|
|
133
|
-
{ filename: "pdm.lock", type: "pdm" },
|
|
134
|
-
{ filename: "pylock.toml", type: "pylock" }
|
|
135
|
-
// PEP 751
|
|
136
|
-
];
|
|
137
|
-
var lockfileTypeMap = new Map(SUPPORTED_LOCKFILES.map((l) => [l.filename, l.type]));
|
|
138
|
-
var pythonEcosystem = {
|
|
139
|
-
name: "python",
|
|
140
|
-
supportedLockfiles: SUPPORTED_LOCKFILES,
|
|
141
|
-
manifestName: "pyproject.toml",
|
|
142
|
-
getLockfileType(filename) {
|
|
143
|
-
return lockfileTypeMap.get(filename);
|
|
144
|
-
},
|
|
145
|
-
parseLockfile(content, _lockfileType) {
|
|
146
|
-
return parseTomlPackages(content);
|
|
147
|
-
},
|
|
148
|
-
parseDirectDeps(manifestContent) {
|
|
149
|
-
return parseDirectDeps(manifestContent);
|
|
150
|
-
},
|
|
151
|
-
normalizeName(name) {
|
|
152
|
-
return normalizePythonName(name);
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
// src/ecosystems/javascript/parsers/npm.ts
|
|
157
|
-
function parseNpmLock(content) {
|
|
158
|
-
const data = JSON.parse(content);
|
|
159
|
-
const version = data.lockfileVersion ?? 1;
|
|
160
|
-
if (version >= 2 && data.packages) {
|
|
161
|
-
return parseV2Packages(data.packages);
|
|
162
|
-
}
|
|
163
|
-
if (data.dependencies) {
|
|
164
|
-
return parseV1Dependencies(data.dependencies);
|
|
165
|
-
}
|
|
166
|
-
return {};
|
|
167
|
-
}
|
|
168
|
-
function parseV2Packages(packages) {
|
|
169
|
-
const result = {};
|
|
170
|
-
for (const [key, pkg] of Object.entries(packages)) {
|
|
171
|
-
if (!key) continue;
|
|
172
|
-
if (!key.startsWith("node_modules/")) continue;
|
|
173
|
-
const segments = key.split("node_modules/");
|
|
174
|
-
if (segments.length > 2) continue;
|
|
175
|
-
const name = key.slice("node_modules/".length);
|
|
176
|
-
const pkgVersion = pkg.version;
|
|
177
|
-
if (pkgVersion && !result[name]) {
|
|
178
|
-
result[name] = pkgVersion;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
return result;
|
|
182
|
-
}
|
|
183
|
-
function parseV1Dependencies(deps, result = {}) {
|
|
184
|
-
for (const [name, pkg] of Object.entries(deps)) {
|
|
185
|
-
if (pkg.version && !result[name]) {
|
|
186
|
-
result[name] = pkg.version;
|
|
187
|
-
}
|
|
188
|
-
if (pkg.dependencies) {
|
|
189
|
-
parseV1Dependencies(pkg.dependencies, result);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return result;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// src/ecosystems/javascript/parsers/yarn.ts
|
|
196
|
-
import { parse as parseYaml } from "yaml";
|
|
197
|
-
function parseYarnLock(content) {
|
|
198
|
-
return isYarnBerry(content) ? parseYarnBerry(content) : parseYarnV1(content);
|
|
199
|
-
}
|
|
200
|
-
function isYarnBerry(content) {
|
|
201
|
-
return content.includes("__metadata:");
|
|
202
|
-
}
|
|
203
|
-
function extractNameFromSpecifier(spec) {
|
|
204
|
-
const trimmed = spec.trim().replace(/^"|"$/g, "");
|
|
205
|
-
if (trimmed.startsWith("@")) {
|
|
206
|
-
const idx = trimmed.indexOf("@", 1);
|
|
207
|
-
return idx > 0 ? trimmed.slice(0, idx) : trimmed;
|
|
208
|
-
}
|
|
209
|
-
const atIdx = trimmed.indexOf("@");
|
|
210
|
-
return atIdx > 0 ? trimmed.slice(0, atIdx) : trimmed;
|
|
211
|
-
}
|
|
212
|
-
function parseYarnV1(content) {
|
|
213
|
-
const packages = {};
|
|
214
|
-
const blocks = content.split(/\n\n+/);
|
|
215
|
-
for (const block of blocks) {
|
|
216
|
-
const trimmed = block.trim();
|
|
217
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
218
|
-
const versionMatch = trimmed.match(/^[ \t]+version "([^"]+)"/m);
|
|
219
|
-
if (!versionMatch) continue;
|
|
220
|
-
const headerLine = trimmed.split("\n")[0].trim().replace(/:$/, "");
|
|
221
|
-
const firstSpecifier = headerLine.split(",")[0].trim().replace(/^"|"$/g, "");
|
|
222
|
-
const name = extractNameFromSpecifier(firstSpecifier);
|
|
223
|
-
if (name && !packages[name]) {
|
|
224
|
-
packages[name] = versionMatch[1];
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return packages;
|
|
228
|
-
}
|
|
229
|
-
function parseYarnBerry(content) {
|
|
230
|
-
const data = parseYaml(content);
|
|
231
|
-
const packages = {};
|
|
232
|
-
for (const [key, value] of Object.entries(data)) {
|
|
233
|
-
if (key === "__metadata") continue;
|
|
234
|
-
if (typeof value !== "object" || !value) continue;
|
|
235
|
-
const entry = value;
|
|
236
|
-
if (entry.linkType === "soft") continue;
|
|
237
|
-
if (!entry.version) continue;
|
|
238
|
-
const cleanKey = key.replace(/^"|"$/g, "");
|
|
239
|
-
const name = extractNameFromBerryKey(cleanKey);
|
|
240
|
-
if (name && !packages[name]) {
|
|
241
|
-
packages[name] = entry.version;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return packages;
|
|
245
|
-
}
|
|
246
|
-
function extractNameFromBerryKey(key) {
|
|
247
|
-
if (key.startsWith("@")) {
|
|
248
|
-
const idx = key.indexOf("@", 1);
|
|
249
|
-
return idx > 0 ? key.slice(0, idx) : key;
|
|
250
|
-
}
|
|
251
|
-
return key.split("@")[0];
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// src/ecosystems/javascript/parsers/pnpm.ts
|
|
255
|
-
import { parse as parseYaml2 } from "yaml";
|
|
256
|
-
function parsePnpmLock(content) {
|
|
257
|
-
const data = parseYaml2(content);
|
|
258
|
-
if (!data?.packages) return {};
|
|
259
|
-
const lockfileVersion = parseLockfileVersion(data.lockfileVersion);
|
|
260
|
-
if (lockfileVersion >= 9) {
|
|
261
|
-
return parsePnpmV9(data.packages);
|
|
262
|
-
}
|
|
263
|
-
return parsePnpmLegacy(data.packages);
|
|
264
|
-
}
|
|
265
|
-
function parseLockfileVersion(v) {
|
|
266
|
-
if (typeof v === "number") return v;
|
|
267
|
-
if (typeof v === "string") return parseFloat(v);
|
|
268
|
-
return 0;
|
|
269
|
-
}
|
|
270
|
-
function parsePnpmV9(packages) {
|
|
271
|
-
const result = {};
|
|
272
|
-
for (const key of Object.keys(packages)) {
|
|
273
|
-
let name, version;
|
|
274
|
-
if (key.startsWith("@")) {
|
|
275
|
-
const atIdx = key.indexOf("@", 1);
|
|
276
|
-
if (atIdx < 0) continue;
|
|
277
|
-
name = key.slice(0, atIdx);
|
|
278
|
-
version = key.slice(atIdx + 1);
|
|
279
|
-
} else {
|
|
280
|
-
const atIdx = key.indexOf("@");
|
|
281
|
-
if (atIdx < 0) continue;
|
|
282
|
-
name = key.slice(0, atIdx);
|
|
283
|
-
version = key.slice(atIdx + 1);
|
|
284
|
-
}
|
|
285
|
-
version = stripVersionSuffix(version);
|
|
286
|
-
if (name && version && !result[name]) {
|
|
287
|
-
result[name] = version;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
return result;
|
|
291
|
-
}
|
|
292
|
-
function parsePnpmLegacy(packages) {
|
|
293
|
-
const result = {};
|
|
294
|
-
for (const key of Object.keys(packages)) {
|
|
295
|
-
const cleaned = key.startsWith("/") ? key.slice(1) : key;
|
|
296
|
-
let name, version;
|
|
297
|
-
if (cleaned.startsWith("@")) {
|
|
298
|
-
const secondSlash = cleaned.indexOf("/", cleaned.indexOf("/") + 1);
|
|
299
|
-
const secondAt = cleaned.indexOf("@", 1);
|
|
300
|
-
if (secondAt > 0 && (secondSlash < 0 || secondAt < secondSlash)) {
|
|
301
|
-
name = cleaned.slice(0, secondAt);
|
|
302
|
-
version = cleaned.slice(secondAt + 1);
|
|
303
|
-
} else if (secondSlash > 0) {
|
|
304
|
-
name = cleaned.slice(0, secondSlash);
|
|
305
|
-
version = cleaned.slice(secondSlash + 1);
|
|
306
|
-
} else {
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
} else {
|
|
310
|
-
const atIdx = cleaned.indexOf("@");
|
|
311
|
-
const slashIdx = cleaned.indexOf("/");
|
|
312
|
-
if (atIdx > 0 && (slashIdx < 0 || atIdx < slashIdx)) {
|
|
313
|
-
name = cleaned.slice(0, atIdx);
|
|
314
|
-
version = cleaned.slice(atIdx + 1);
|
|
315
|
-
} else if (slashIdx > 0) {
|
|
316
|
-
name = cleaned.slice(0, slashIdx);
|
|
317
|
-
version = cleaned.slice(slashIdx + 1);
|
|
318
|
-
} else {
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
version = stripVersionSuffix(version);
|
|
323
|
-
if (name && version && !result[name]) {
|
|
324
|
-
result[name] = version;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return result;
|
|
328
|
-
}
|
|
329
|
-
function stripVersionSuffix(version) {
|
|
330
|
-
return version.split("(")[0].split("_")[0].trim();
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// src/ecosystems/javascript/parsers/bun.ts
|
|
334
|
-
function parseBunLock(content) {
|
|
335
|
-
const data = JSON.parse(content);
|
|
336
|
-
const result = {};
|
|
337
|
-
for (const [name, entry] of Object.entries(data.packages ?? {})) {
|
|
338
|
-
if (!Array.isArray(entry)) continue;
|
|
339
|
-
const nameAtVersion = entry[0];
|
|
340
|
-
if (typeof nameAtVersion !== "string") continue;
|
|
341
|
-
const version = extractVersion(nameAtVersion);
|
|
342
|
-
if (!version || version.startsWith("workspace:")) continue;
|
|
343
|
-
result[name] = version;
|
|
344
|
-
}
|
|
345
|
-
return result;
|
|
346
|
-
}
|
|
347
|
-
function extractVersion(nameAtVersion) {
|
|
348
|
-
if (nameAtVersion.startsWith("@")) {
|
|
349
|
-
const atIdx2 = nameAtVersion.indexOf("@", 1);
|
|
350
|
-
return atIdx2 > 0 ? nameAtVersion.slice(atIdx2 + 1) : "";
|
|
351
|
-
}
|
|
352
|
-
const atIdx = nameAtVersion.indexOf("@");
|
|
353
|
-
return atIdx > 0 ? nameAtVersion.slice(atIdx + 1) : "";
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// src/ecosystems/javascript/package-json.ts
|
|
357
|
-
var PROD_SECTIONS = ["dependencies", "optionalDependencies", "peerDependencies"];
|
|
358
|
-
function normalizeJsName(name) {
|
|
359
|
-
return name.toLowerCase();
|
|
360
|
-
}
|
|
361
|
-
function parseDirectDeps2(content) {
|
|
362
|
-
const prod = /* @__PURE__ */ new Set();
|
|
363
|
-
const dev = /* @__PURE__ */ new Set();
|
|
364
|
-
let data;
|
|
365
|
-
try {
|
|
366
|
-
data = JSON.parse(content);
|
|
367
|
-
} catch {
|
|
368
|
-
return { prod, dev };
|
|
369
|
-
}
|
|
370
|
-
for (const section of PROD_SECTIONS) {
|
|
371
|
-
const deps = data[section];
|
|
372
|
-
if (deps && typeof deps === "object") {
|
|
373
|
-
for (const name of Object.keys(deps)) {
|
|
374
|
-
prod.add(normalizeJsName(name));
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
const devDeps = data["devDependencies"];
|
|
379
|
-
if (devDeps && typeof devDeps === "object") {
|
|
380
|
-
for (const name of Object.keys(devDeps)) {
|
|
381
|
-
const normalized = normalizeJsName(name);
|
|
382
|
-
if (!prod.has(normalized)) dev.add(normalized);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return { prod, dev };
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// src/ecosystems/javascript/index.ts
|
|
389
|
-
var SUPPORTED_LOCKFILES2 = [
|
|
390
|
-
{ filename: "package-lock.json", type: "npm" },
|
|
391
|
-
{ filename: "yarn.lock", type: "yarn" },
|
|
392
|
-
{ filename: "pnpm-lock.yaml", type: "pnpm" },
|
|
393
|
-
{ filename: "bun.lock", type: "bun" }
|
|
394
|
-
];
|
|
395
|
-
var lockfileTypeMap2 = new Map(SUPPORTED_LOCKFILES2.map((l) => [l.filename, l.type]));
|
|
396
|
-
var javascriptEcosystem = {
|
|
397
|
-
name: "javascript",
|
|
398
|
-
supportedLockfiles: SUPPORTED_LOCKFILES2,
|
|
399
|
-
manifestName: "package.json",
|
|
400
|
-
getLockfileType(filename) {
|
|
401
|
-
return lockfileTypeMap2.get(filename);
|
|
402
|
-
},
|
|
403
|
-
parseLockfile(content, lockfileType) {
|
|
404
|
-
switch (lockfileType) {
|
|
405
|
-
case "npm":
|
|
406
|
-
return parseNpmLock(content);
|
|
407
|
-
case "yarn":
|
|
408
|
-
return parseYarnLock(content);
|
|
409
|
-
case "pnpm":
|
|
410
|
-
return parsePnpmLock(content);
|
|
411
|
-
case "bun":
|
|
412
|
-
return parseBunLock(content);
|
|
413
|
-
default:
|
|
414
|
-
return {};
|
|
415
|
-
}
|
|
416
|
-
},
|
|
417
|
-
parseDirectDeps(manifestContent) {
|
|
418
|
-
return parseDirectDeps2(manifestContent);
|
|
419
|
-
},
|
|
420
|
-
normalizeName(name) {
|
|
421
|
-
return normalizeJsName(name);
|
|
422
|
-
}
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
// src/ecosystems/deno/parsers/deno-lock.ts
|
|
426
|
-
function parseDenoLock(content) {
|
|
427
|
-
const data = JSON.parse(content);
|
|
428
|
-
const result = {};
|
|
429
|
-
for (const [key, registry2] of [
|
|
430
|
-
["npm", data.packages?.npm],
|
|
431
|
-
["jsr", data.packages?.jsr]
|
|
432
|
-
]) {
|
|
433
|
-
if (!registry2) continue;
|
|
434
|
-
for (const specifier of Object.keys(registry2)) {
|
|
435
|
-
const { name, version } = splitSpecifier(specifier);
|
|
436
|
-
const resultKey = key === "jsr" ? `jsr:${name}` : name;
|
|
437
|
-
if (name && version && !result[resultKey]) {
|
|
438
|
-
result[resultKey] = version;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return result;
|
|
443
|
-
}
|
|
444
|
-
function splitSpecifier(specifier) {
|
|
445
|
-
if (specifier.startsWith("@")) {
|
|
446
|
-
const atIdx2 = specifier.indexOf("@", 1);
|
|
447
|
-
if (atIdx2 < 0) return { name: specifier, version: "" };
|
|
448
|
-
return { name: specifier.slice(0, atIdx2), version: specifier.slice(atIdx2 + 1) };
|
|
449
|
-
}
|
|
450
|
-
const atIdx = specifier.indexOf("@");
|
|
451
|
-
if (atIdx < 0) return { name: specifier, version: "" };
|
|
452
|
-
return { name: specifier.slice(0, atIdx), version: specifier.slice(atIdx + 1) };
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// src/ecosystems/deno/deno-json.ts
|
|
456
|
-
function normalizeDenoName(name) {
|
|
457
|
-
return name.toLowerCase();
|
|
458
|
-
}
|
|
459
|
-
function parseDirectDeps3(content) {
|
|
460
|
-
const prod = /* @__PURE__ */ new Set();
|
|
461
|
-
let data;
|
|
462
|
-
try {
|
|
463
|
-
data = JSON.parse(content);
|
|
464
|
-
} catch {
|
|
465
|
-
return { prod, dev: /* @__PURE__ */ new Set() };
|
|
466
|
-
}
|
|
467
|
-
const imports = data["imports"];
|
|
468
|
-
if (imports) {
|
|
469
|
-
for (const specifier of Object.values(imports)) {
|
|
470
|
-
const name = extractPackageName(specifier);
|
|
471
|
-
if (name) prod.add(normalizeDenoName(name));
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
const workspace = data["workspace"];
|
|
475
|
-
for (const specifier of workspace?.dependencies ?? []) {
|
|
476
|
-
const name = extractPackageName(specifier);
|
|
477
|
-
if (name) prod.add(normalizeDenoName(name));
|
|
478
|
-
}
|
|
479
|
-
return { prod, dev: /* @__PURE__ */ new Set() };
|
|
480
|
-
}
|
|
481
|
-
function extractPackageName(specifier) {
|
|
482
|
-
const withoutProtocol = specifier.replace(/^(?:npm|jsr|node):/, "");
|
|
483
|
-
if (specifier.startsWith("node:")) return null;
|
|
484
|
-
if (withoutProtocol.startsWith("@")) {
|
|
485
|
-
const atIdx2 = withoutProtocol.indexOf("@", 1);
|
|
486
|
-
return atIdx2 > 0 ? withoutProtocol.slice(0, atIdx2) : withoutProtocol;
|
|
487
|
-
}
|
|
488
|
-
const atIdx = withoutProtocol.indexOf("@");
|
|
489
|
-
return atIdx > 0 ? withoutProtocol.slice(0, atIdx) : withoutProtocol || null;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// src/ecosystems/deno/index.ts
|
|
493
|
-
var SUPPORTED_LOCKFILES3 = [{ filename: "deno.lock", type: "deno" }];
|
|
494
|
-
var denoEcosystem = {
|
|
495
|
-
name: "deno",
|
|
496
|
-
supportedLockfiles: SUPPORTED_LOCKFILES3,
|
|
497
|
-
manifestName: "deno.json",
|
|
498
|
-
getLockfileType(filename) {
|
|
499
|
-
return filename === "deno.lock" ? "deno" : void 0;
|
|
500
|
-
},
|
|
501
|
-
parseLockfile(content, _lockfileType) {
|
|
502
|
-
return parseDenoLock(content);
|
|
503
|
-
},
|
|
504
|
-
parseDirectDeps(manifestContent) {
|
|
505
|
-
return parseDirectDeps3(manifestContent);
|
|
506
|
-
},
|
|
507
|
-
normalizeName(name) {
|
|
508
|
-
return normalizeDenoName(name);
|
|
509
|
-
}
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
// src/ecosystems/index.ts
|
|
513
|
-
var registry = /* @__PURE__ */ new Map();
|
|
514
|
-
function registerEcosystem(ecosystem) {
|
|
515
|
-
registry.set(ecosystem.name, ecosystem);
|
|
516
|
-
}
|
|
517
|
-
function getEcosystemByName(name) {
|
|
518
|
-
return registry.get(name);
|
|
519
|
-
}
|
|
520
|
-
function getEcosystemForLockfile(filename) {
|
|
521
|
-
for (const ecosystem of registry.values()) {
|
|
522
|
-
if (ecosystem.getLockfileType(filename) !== void 0) return ecosystem;
|
|
523
|
-
}
|
|
524
|
-
return void 0;
|
|
525
|
-
}
|
|
526
|
-
function getAllEcosystems() {
|
|
527
|
-
return [...registry.values()];
|
|
528
|
-
}
|
|
529
|
-
registerEcosystem(pythonEcosystem);
|
|
530
|
-
registerEcosystem(javascriptEcosystem);
|
|
531
|
-
registerEcosystem(denoEcosystem);
|
|
532
|
-
|
|
533
|
-
// src/core/discovery.ts
|
|
534
|
-
import { posix } from "path";
|
|
535
|
-
function workspaceFromPath(filePath) {
|
|
536
|
-
const parent = posix.dirname(filePath);
|
|
537
|
-
return parent === "." || parent === "" ? "." : parent;
|
|
538
|
-
}
|
|
539
|
-
function detectLockfileInfo(filePath) {
|
|
540
|
-
const filename = posix.basename(filePath);
|
|
541
|
-
const ecosystem = getEcosystemForLockfile(filename);
|
|
542
|
-
if (!ecosystem) return null;
|
|
543
|
-
const type = ecosystem.getLockfileType(filename);
|
|
544
|
-
if (!type) return null;
|
|
545
|
-
return { path: filePath, type, ecosystemName: ecosystem.name };
|
|
546
|
-
}
|
|
547
|
-
function findAllLockfiles(paths) {
|
|
548
|
-
return paths.flatMap((p) => {
|
|
549
|
-
const info = detectLockfileInfo(p);
|
|
550
|
-
return info ? [info] : [];
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
async function findLockfiles(getFile) {
|
|
554
|
-
const candidates = getAllEcosystems().flatMap(
|
|
555
|
-
(ecosystem) => ecosystem.supportedLockfiles.map(({ filename, type }) => ({
|
|
556
|
-
filename,
|
|
557
|
-
type,
|
|
558
|
-
ecosystemName: ecosystem.name
|
|
559
|
-
}))
|
|
560
|
-
);
|
|
561
|
-
const results = await Promise.all(
|
|
562
|
-
candidates.map(async ({ filename, type, ecosystemName }) => {
|
|
563
|
-
const content = await getFile(filename);
|
|
564
|
-
return content !== null ? { path: filename, type, ecosystemName } : null;
|
|
565
|
-
})
|
|
566
|
-
);
|
|
567
|
-
return results.filter((r) => r !== null);
|
|
568
|
-
}
|
|
569
|
-
function groupByWorkspace(lockfiles) {
|
|
570
|
-
const result = /* @__PURE__ */ new Map();
|
|
571
|
-
for (const lf of lockfiles) {
|
|
572
|
-
const ws = workspaceFromPath(lf.path);
|
|
573
|
-
const existing = result.get(ws) ?? [];
|
|
574
|
-
existing.push(lf);
|
|
575
|
-
result.set(ws, existing);
|
|
576
|
-
}
|
|
577
|
-
return result;
|
|
578
|
-
}
|
|
579
|
-
var LOCKFILE_PRIORITY = {
|
|
580
|
-
"uv.lock": 0,
|
|
581
|
-
"poetry.lock": 1,
|
|
582
|
-
"pdm.lock": 2
|
|
583
|
-
};
|
|
584
|
-
function lockfilePriority(path) {
|
|
585
|
-
return LOCKFILE_PRIORITY[posix.basename(path)] ?? 99;
|
|
586
|
-
}
|
|
587
|
-
function resolveLockfilePair(baseFiles, headFiles) {
|
|
588
|
-
const headByPath = new Map(headFiles.map((f) => [f.path, f]));
|
|
589
|
-
const common = baseFiles.filter((f) => headByPath.has(f.path));
|
|
590
|
-
if (common.length > 0) {
|
|
591
|
-
const chosen = common.sort((a, b) => lockfilePriority(a.path) - lockfilePriority(b.path))[0];
|
|
592
|
-
return {
|
|
593
|
-
basePath: chosen.path,
|
|
594
|
-
baseType: chosen.type,
|
|
595
|
-
headPath: chosen.path,
|
|
596
|
-
headType: headByPath.get(chosen.path).type,
|
|
597
|
-
migrationNote: null,
|
|
598
|
-
ecosystemName: chosen.ecosystemName
|
|
599
|
-
};
|
|
600
|
-
}
|
|
601
|
-
if (baseFiles.length > 0 && headFiles.length > 0) {
|
|
602
|
-
const base = baseFiles[0];
|
|
603
|
-
const head = headFiles[0];
|
|
604
|
-
return {
|
|
605
|
-
basePath: base.path,
|
|
606
|
-
baseType: base.type,
|
|
607
|
-
headPath: head.path,
|
|
608
|
-
headType: head.type,
|
|
609
|
-
migrationNote: `lockfile migration: ${posix.basename(base.path)} (${base.type}) \u2192 ${posix.basename(head.path)} (${head.type})`,
|
|
610
|
-
ecosystemName: head.ecosystemName
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
if (headFiles.length > 0) {
|
|
614
|
-
const head = headFiles[0];
|
|
615
|
-
return {
|
|
616
|
-
basePath: null,
|
|
617
|
-
baseType: null,
|
|
618
|
-
headPath: head.path,
|
|
619
|
-
headType: head.type,
|
|
620
|
-
migrationNote: `new lockfile added: ${posix.basename(head.path)} (${head.type})`,
|
|
621
|
-
ecosystemName: head.ecosystemName
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
if (baseFiles.length > 0) {
|
|
625
|
-
const base = baseFiles[0];
|
|
626
|
-
return {
|
|
627
|
-
basePath: base.path,
|
|
628
|
-
baseType: base.type,
|
|
629
|
-
headPath: null,
|
|
630
|
-
headType: null,
|
|
631
|
-
migrationNote: `lockfile removed: ${posix.basename(base.path)} (${base.type})`,
|
|
632
|
-
ecosystemName: base.ecosystemName
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
return null;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// src/core/diff.ts
|
|
639
|
-
function diffPackages(oldPkgs, newPkgs, directDeps, normalizeName) {
|
|
640
|
-
const allNames = /* @__PURE__ */ new Set([...Object.keys(oldPkgs), ...Object.keys(newPkgs)]);
|
|
641
|
-
const changes = [];
|
|
642
|
-
for (const name of [...allNames].sort()) {
|
|
643
|
-
const inOld = name in oldPkgs;
|
|
644
|
-
const inNew = name in newPkgs;
|
|
645
|
-
if (inOld && inNew && oldPkgs[name] === newPkgs[name]) continue;
|
|
646
|
-
const normalized = normalizeName(name);
|
|
647
|
-
const isProd = directDeps.prod.has(normalized);
|
|
648
|
-
const isDev = directDeps.dev.has(normalized) && !isProd;
|
|
649
|
-
changes.push({
|
|
650
|
-
name,
|
|
651
|
-
change_type: !inOld ? "added" : !inNew ? "removed" : "updated",
|
|
652
|
-
old_version: inOld ? oldPkgs[name] : null,
|
|
653
|
-
new_version: inNew ? newPkgs[name] : null,
|
|
654
|
-
is_direct: isProd || isDev,
|
|
655
|
-
is_dev: isDev
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
return changes;
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// src/core/report.ts
|
|
662
|
-
async function buildLockfileEntry(pair, workspace, getBase, getHead) {
|
|
663
|
-
const ecosystem = getEcosystemByName(pair.ecosystemName);
|
|
664
|
-
if (!ecosystem) return null;
|
|
665
|
-
const manifestPath = ecosystem.manifestName ? workspace === "." ? ecosystem.manifestName : posix2.join(workspace, ecosystem.manifestName) : null;
|
|
666
|
-
const [oldContent, newContent, manifestContent] = await Promise.all([
|
|
667
|
-
pair.basePath ? getBase(pair.basePath) : Promise.resolve(null),
|
|
668
|
-
pair.headPath ? getHead(pair.headPath) : Promise.resolve(null),
|
|
669
|
-
manifestPath ? getHead(manifestPath) : Promise.resolve(null)
|
|
670
|
-
]);
|
|
671
|
-
const oldPkgs = oldContent && pair.baseType ? ecosystem.parseLockfile(oldContent, pair.baseType) : {};
|
|
672
|
-
const newPkgs = newContent && pair.headType ? ecosystem.parseLockfile(newContent, pair.headType) : {};
|
|
673
|
-
const directDeps = manifestContent ? ecosystem.parseDirectDeps(manifestContent) : { prod: /* @__PURE__ */ new Set(), dev: /* @__PURE__ */ new Set() };
|
|
674
|
-
const changes = diffPackages(
|
|
675
|
-
oldPkgs,
|
|
676
|
-
newPkgs,
|
|
677
|
-
directDeps,
|
|
678
|
-
ecosystem.normalizeName.bind(ecosystem)
|
|
679
|
-
);
|
|
680
|
-
const added = changes.filter((c) => c.change_type === "added").length;
|
|
681
|
-
const removed = changes.filter((c) => c.change_type === "removed").length;
|
|
682
|
-
const updated = changes.filter((c) => c.change_type === "updated").length;
|
|
683
|
-
return {
|
|
684
|
-
path: pair.headPath ?? pair.basePath,
|
|
685
|
-
workspace,
|
|
686
|
-
type: pair.headType ?? pair.baseType,
|
|
687
|
-
ecosystem: pair.ecosystemName,
|
|
688
|
-
summary: { added, removed, updated, total_changes: changes.length },
|
|
689
|
-
changes,
|
|
690
|
-
migration: pair.migrationNote ? {
|
|
691
|
-
note: pair.migrationNote,
|
|
692
|
-
base_lockfile: pair.basePath,
|
|
693
|
-
base_lockfile_type: pair.baseType,
|
|
694
|
-
head_lockfile: pair.headPath,
|
|
695
|
-
head_lockfile_type: pair.headType
|
|
696
|
-
} : null
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
async function collectLockfileEntries(options) {
|
|
700
|
-
const { getBase, getHead, allBasePaths, allHeadPaths, lockfile, lockfileType, onNote } = options;
|
|
701
|
-
if (lockfile) {
|
|
702
|
-
const filename = posix2.basename(lockfile);
|
|
703
|
-
const ecosystem = getEcosystemForLockfile(filename);
|
|
704
|
-
if (!ecosystem) throw new Error(`Cannot determine ecosystem for lockfile: ${lockfile}`);
|
|
705
|
-
const type = lockfileType ?? ecosystem.getLockfileType(filename);
|
|
706
|
-
if (!type) throw new Error(`Cannot determine lockfile type for ${lockfile} \u2014 use --type`);
|
|
707
|
-
const ws = posix2.dirname(lockfile);
|
|
708
|
-
const pair = {
|
|
709
|
-
basePath: lockfile,
|
|
710
|
-
baseType: type,
|
|
711
|
-
headPath: lockfile,
|
|
712
|
-
headType: type,
|
|
713
|
-
migrationNote: null,
|
|
714
|
-
ecosystemName: ecosystem.name
|
|
715
|
-
};
|
|
716
|
-
const entry = await buildLockfileEntry(
|
|
717
|
-
pair,
|
|
718
|
-
ws === "." || ws === "" ? "." : ws,
|
|
719
|
-
getBase,
|
|
720
|
-
getHead
|
|
721
|
-
);
|
|
722
|
-
return entry ? [entry] : [];
|
|
723
|
-
}
|
|
724
|
-
let baseAll = findAllLockfiles(allBasePaths);
|
|
725
|
-
let headAll = findAllLockfiles(allHeadPaths);
|
|
726
|
-
if (baseAll.length === 0 && headAll.length === 0) {
|
|
727
|
-
[baseAll, headAll] = await Promise.all([findLockfiles(getBase), findLockfiles(getHead)]);
|
|
728
|
-
}
|
|
729
|
-
const baseByWs = groupByWorkspace(baseAll);
|
|
730
|
-
const headByWs = groupByWorkspace(headAll);
|
|
731
|
-
const allWorkspaces = [.../* @__PURE__ */ new Set([...baseByWs.keys(), ...headByWs.keys()])].sort();
|
|
732
|
-
const entries = await Promise.all(
|
|
733
|
-
allWorkspaces.map(async (ws) => {
|
|
734
|
-
const baseFiles = baseByWs.get(ws) ?? [];
|
|
735
|
-
const headFiles = headByWs.get(ws) ?? [];
|
|
736
|
-
const pair = resolveLockfilePair(baseFiles, headFiles);
|
|
737
|
-
if (!pair) return null;
|
|
738
|
-
if (pair.migrationNote) onNote?.(`[${ws}]: ${pair.migrationNote}`);
|
|
739
|
-
return buildLockfileEntry(pair, ws, getBase, getHead);
|
|
740
|
-
})
|
|
741
|
-
);
|
|
742
|
-
return entries.filter((e) => e !== null);
|
|
743
|
-
}
|
|
744
|
-
function buildDiffReport(lockfiles, baseRef, headRef) {
|
|
745
|
-
const totalAdded = lockfiles.reduce((sum, lf) => sum + lf.summary.added, 0);
|
|
746
|
-
const totalRemoved = lockfiles.reduce((sum, lf) => sum + lf.summary.removed, 0);
|
|
747
|
-
const totalUpdated = lockfiles.reduce((sum, lf) => sum + lf.summary.updated, 0);
|
|
748
|
-
const ecosystems = [...new Set(lockfiles.map((lf) => lf.ecosystem))].sort();
|
|
749
|
-
return {
|
|
750
|
-
schema_version: "1",
|
|
751
|
-
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
752
|
-
base_ref: baseRef,
|
|
753
|
-
head_ref: headRef,
|
|
754
|
-
summary: {
|
|
755
|
-
added: totalAdded,
|
|
756
|
-
removed: totalRemoved,
|
|
757
|
-
updated: totalUpdated,
|
|
758
|
-
total_changes: totalAdded + totalRemoved + totalUpdated,
|
|
759
|
-
ecosystems
|
|
760
|
-
},
|
|
761
|
-
lockfiles
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
// src/sources/git.ts
|
|
766
|
-
import { execFileSync } from "child_process";
|
|
767
|
-
function gitShow(ref, path) {
|
|
768
|
-
try {
|
|
769
|
-
const result = execFileSync("git", ["show", `${ref}:${path}`], {
|
|
770
|
-
encoding: "utf-8",
|
|
771
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
772
|
-
});
|
|
773
|
-
return result || null;
|
|
774
|
-
} catch {
|
|
775
|
-
return null;
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
function gitLsTree(ref) {
|
|
779
|
-
try {
|
|
780
|
-
const result = execFileSync("git", ["ls-tree", "-r", "--name-only", ref], {
|
|
781
|
-
encoding: "utf-8",
|
|
782
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
783
|
-
});
|
|
784
|
-
return result.trim().split("\n").filter(Boolean);
|
|
785
|
-
} catch {
|
|
786
|
-
return [];
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// src/sources/github.ts
|
|
791
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
792
|
-
var API_BASE = "https://api.github.com";
|
|
793
|
-
function token() {
|
|
794
|
-
const t = process.env["GITHUB_TOKEN"];
|
|
795
|
-
if (!t) throw new Error("GITHUB_TOKEN is required for GitHub API access");
|
|
796
|
-
return t;
|
|
797
|
-
}
|
|
798
|
-
function headers(accept = "application/vnd.github+json") {
|
|
799
|
-
return {
|
|
800
|
-
Authorization: `Bearer ${token()}`,
|
|
801
|
-
Accept: accept,
|
|
802
|
-
"X-GitHub-Api-Version": "2022-11-28"
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
async function ghFileAtSha(sha, path, repo) {
|
|
806
|
-
const url = `${API_BASE}/repos/${repo}/contents/${path}?ref=${sha}`;
|
|
807
|
-
const response = await fetch(url, {
|
|
808
|
-
headers: headers("application/vnd.github.raw+json")
|
|
809
|
-
});
|
|
810
|
-
if (!response.ok) return null;
|
|
811
|
-
return response.text();
|
|
812
|
-
}
|
|
813
|
-
async function ghLsTree(sha, repo) {
|
|
814
|
-
const url = `${API_BASE}/repos/${repo}/git/trees/${sha}?recursive=1`;
|
|
815
|
-
const response = await fetch(url, { headers: headers() });
|
|
816
|
-
if (!response.ok) return [];
|
|
817
|
-
const data = await response.json();
|
|
818
|
-
return data.tree.filter((item) => item.type === "blob").map((item) => item.path);
|
|
819
|
-
}
|
|
820
|
-
async function getPrShas(prNumber, repo) {
|
|
821
|
-
const url = `${API_BASE}/repos/${repo}/pulls/${prNumber}`;
|
|
822
|
-
const response = await fetch(url, { headers: headers() });
|
|
823
|
-
if (!response.ok) {
|
|
824
|
-
throw new Error(`GitHub API error ${response.status}: failed to fetch PR #${prNumber}`);
|
|
825
|
-
}
|
|
826
|
-
const data = await response.json();
|
|
827
|
-
return { baseRefOid: data.base.sha, headRefOid: data.head.sha };
|
|
828
|
-
}
|
|
829
|
-
function detectRepo() {
|
|
830
|
-
const fromEnv = process.env["GITHUB_REPOSITORY"];
|
|
831
|
-
if (fromEnv) return fromEnv;
|
|
832
|
-
try {
|
|
833
|
-
const remote = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
834
|
-
encoding: "utf-8",
|
|
835
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
836
|
-
}).trim();
|
|
837
|
-
const match = remote.match(/github\.com[:/]([^/]+\/[^/.]+?)(?:\.git)?$/);
|
|
838
|
-
if (match) return match[1];
|
|
839
|
-
} catch {
|
|
840
|
-
}
|
|
841
|
-
throw new Error(
|
|
842
|
-
"Could not detect GitHub repo \u2014 set GITHUB_REPOSITORY or pass --repo"
|
|
843
|
-
);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// src/index.ts
|
|
847
|
-
async function resolveApiShas(options) {
|
|
848
|
-
if (options.baseSha && options.headSha) {
|
|
849
|
-
return { baseSha: options.baseSha, headSha: options.headSha, repo: options.repo ?? detectRepo() };
|
|
850
|
-
}
|
|
851
|
-
if (options.prNumber) {
|
|
852
|
-
const repo = options.repo ?? detectRepo();
|
|
853
|
-
const { baseRefOid, headRefOid } = await getPrShas(options.prNumber, repo);
|
|
854
|
-
return { baseSha: baseRefOid, headSha: headRefOid, repo };
|
|
855
|
-
}
|
|
856
|
-
return null;
|
|
857
|
-
}
|
|
858
|
-
async function run(options = {}) {
|
|
859
|
-
const { lockfile, lockfileType, onNote } = options;
|
|
860
|
-
if (options.oldFile && options.newFile) {
|
|
861
|
-
const oldPath = options.oldFile;
|
|
862
|
-
const newPath = options.newFile;
|
|
863
|
-
const readLocal = (filePath) => {
|
|
864
|
-
try {
|
|
865
|
-
return readFileSync(filePath, "utf-8");
|
|
866
|
-
} catch {
|
|
867
|
-
return null;
|
|
868
|
-
}
|
|
869
|
-
};
|
|
870
|
-
const getBase2 = (path) => Promise.resolve(path === oldPath ? readLocal(oldPath) : null);
|
|
871
|
-
const getHead2 = (path) => Promise.resolve(path === newPath ? readLocal(newPath) : null);
|
|
872
|
-
const lockfiles2 = await collectLockfileEntries({
|
|
873
|
-
getBase: getBase2,
|
|
874
|
-
getHead: getHead2,
|
|
875
|
-
allBasePaths: [oldPath],
|
|
876
|
-
allHeadPaths: [newPath],
|
|
877
|
-
lockfile: newPath,
|
|
878
|
-
lockfileType,
|
|
879
|
-
onNote
|
|
880
|
-
});
|
|
881
|
-
if (lockfiles2.length === 0) throw new Error("No supported lockfiles found");
|
|
882
|
-
return buildDiffReport(lockfiles2, "local_old", "local_new");
|
|
883
|
-
}
|
|
884
|
-
const apiShas = await resolveApiShas(options);
|
|
885
|
-
if (apiShas) {
|
|
886
|
-
const { baseSha, headSha, repo } = apiShas;
|
|
887
|
-
const getBase2 = (path) => ghFileAtSha(baseSha, path, repo);
|
|
888
|
-
const getHead2 = (path) => ghFileAtSha(headSha, path, repo);
|
|
889
|
-
const [basePaths, headPaths] = await Promise.all([
|
|
890
|
-
ghLsTree(baseSha, repo),
|
|
891
|
-
ghLsTree(headSha, repo)
|
|
892
|
-
]);
|
|
893
|
-
const lockfiles2 = await collectLockfileEntries({
|
|
894
|
-
getBase: getBase2,
|
|
895
|
-
getHead: getHead2,
|
|
896
|
-
allBasePaths: basePaths,
|
|
897
|
-
allHeadPaths: headPaths,
|
|
898
|
-
lockfile,
|
|
899
|
-
lockfileType,
|
|
900
|
-
onNote
|
|
901
|
-
});
|
|
902
|
-
if (lockfiles2.length === 0) throw new Error("No supported lockfiles found");
|
|
903
|
-
return buildDiffReport(lockfiles2, baseSha, headSha);
|
|
904
|
-
}
|
|
905
|
-
const baseRef = options.base ?? "HEAD~1";
|
|
906
|
-
const headRef = options.head ?? "HEAD";
|
|
907
|
-
const getBase = (path) => Promise.resolve(gitShow(baseRef, path));
|
|
908
|
-
const getHead = (path) => Promise.resolve(gitShow(headRef, path));
|
|
909
|
-
const lockfiles = await collectLockfileEntries({
|
|
910
|
-
getBase,
|
|
911
|
-
getHead,
|
|
912
|
-
allBasePaths: gitLsTree(baseRef),
|
|
913
|
-
allHeadPaths: gitLsTree(headRef),
|
|
914
|
-
lockfile,
|
|
915
|
-
lockfileType,
|
|
916
|
-
onNote
|
|
917
|
-
});
|
|
918
|
-
if (lockfiles.length === 0) throw new Error("No supported lockfiles found");
|
|
919
|
-
return buildDiffReport(lockfiles, baseRef, headRef);
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
// src/action/filters.ts
|
|
923
|
-
import { parse as parse3 } from "yaml";
|
|
924
|
-
function applyFilters(filtersYaml, changes) {
|
|
925
|
-
if (!filtersYaml.trim()) return {};
|
|
926
|
-
let config;
|
|
927
|
-
try {
|
|
928
|
-
config = parse3(filtersYaml);
|
|
929
|
-
} catch {
|
|
930
|
-
return {};
|
|
931
|
-
}
|
|
932
|
-
if (!config || typeof config !== "object") return {};
|
|
933
|
-
const changedNames = new Set(changes.map((c) => c.name.toLowerCase()));
|
|
934
|
-
const result = {};
|
|
935
|
-
for (const [groupName, packages] of Object.entries(config)) {
|
|
936
|
-
if (!Array.isArray(packages)) continue;
|
|
937
|
-
result[groupName] = packages.some(
|
|
938
|
-
(pkg) => typeof pkg === "string" && changedNames.has(pkg.toLowerCase())
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
return result;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
// src/action/markdown.ts
|
|
945
|
-
function packageUrl(ecosystem, name) {
|
|
946
|
-
switch (ecosystem) {
|
|
947
|
-
case "python":
|
|
948
|
-
return `https://pypi.org/project/${name}/`;
|
|
949
|
-
case "javascript":
|
|
950
|
-
return `https://www.npmjs.com/package/${encodeURIComponent(name)}`;
|
|
951
|
-
case "deno":
|
|
952
|
-
if (name.startsWith("jsr:")) return `https://jsr.io/${name.slice(4)}`;
|
|
953
|
-
return `https://www.npmjs.com/package/${encodeURIComponent(name)}`;
|
|
954
|
-
default:
|
|
955
|
-
return null;
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
function formatName(change, ecosystem) {
|
|
959
|
-
const url = packageUrl(ecosystem, change.name);
|
|
960
|
-
const linked = url ? `[${change.name}](${url})` : change.name;
|
|
961
|
-
if (change.is_direct && !change.is_dev) return `**${linked}**`;
|
|
962
|
-
if (change.is_dev) return `*${linked}*`;
|
|
963
|
-
return linked;
|
|
964
|
-
}
|
|
965
|
-
function formatLine(change, ecosystem) {
|
|
966
|
-
const name = formatName(change, ecosystem);
|
|
967
|
-
if (change.change_type === "updated") {
|
|
968
|
-
return `- ${name}: \`${change.old_version}\` \u2192 \`${change.new_version}\``;
|
|
969
|
-
}
|
|
970
|
-
if (change.change_type === "added") {
|
|
971
|
-
return `- ${name}: \`${change.new_version}\``;
|
|
972
|
-
}
|
|
973
|
-
return `- ${name}: \`${change.old_version}\``;
|
|
974
|
-
}
|
|
975
|
-
function generateMarkdown(report) {
|
|
976
|
-
const added = [];
|
|
977
|
-
const updated = [];
|
|
978
|
-
const removed = [];
|
|
979
|
-
for (const lf of report.lockfiles) {
|
|
980
|
-
for (const change of lf.changes) {
|
|
981
|
-
const entry = { change, ecosystem: lf.ecosystem };
|
|
982
|
-
if (change.change_type === "added") added.push(entry);
|
|
983
|
-
else if (change.change_type === "updated") updated.push(entry);
|
|
984
|
-
else removed.push(entry);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
const fmt = ({ change, ecosystem }) => formatLine(change, ecosystem);
|
|
988
|
-
const sections = [];
|
|
989
|
-
if (added.length > 0) sections.push(`### Added
|
|
990
|
-
|
|
991
|
-
${added.map(fmt).join("\n")}`);
|
|
992
|
-
if (updated.length > 0) sections.push(`### Changed
|
|
993
|
-
|
|
994
|
-
${updated.map(fmt).join("\n")}`);
|
|
995
|
-
if (removed.length > 0) sections.push(`### Removed
|
|
996
|
-
|
|
997
|
-
${removed.map(fmt).join("\n")}`);
|
|
998
|
-
return sections.join("\n\n");
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// src/action/comment.ts
|
|
1002
|
-
var API_BASE2 = "https://api.github.com";
|
|
1003
|
-
var MARKER = "<!-- depdiff -->";
|
|
1004
|
-
function githubHeaders(token2) {
|
|
1005
|
-
return {
|
|
1006
|
-
Authorization: `Bearer ${token2}`,
|
|
1007
|
-
Accept: "application/vnd.github+json",
|
|
1008
|
-
"Content-Type": "application/json",
|
|
1009
|
-
"X-GitHub-Api-Version": "2022-11-28"
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
async function findExistingComment(prNumber, repo, token2) {
|
|
1013
|
-
const url = `${API_BASE2}/repos/${repo}/issues/${prNumber}/comments?per_page=100`;
|
|
1014
|
-
const response = await fetch(url, { headers: githubHeaders(token2) });
|
|
1015
|
-
if (!response.ok) return null;
|
|
1016
|
-
const comments = await response.json();
|
|
1017
|
-
return comments.find((c) => c.body.includes(MARKER))?.id ?? null;
|
|
1018
|
-
}
|
|
1019
|
-
async function postPrComment(markdown, prNumber, repo) {
|
|
1020
|
-
if (!prNumber) throw new Error("post-comment requires pr-number to be set");
|
|
1021
|
-
if (!repo) throw new Error("post-comment requires repo to be set");
|
|
1022
|
-
const t = process.env["GITHUB_TOKEN"];
|
|
1023
|
-
if (!t) throw new Error("GITHUB_TOKEN is required for post-comment");
|
|
1024
|
-
const body = `${MARKER}
|
|
1025
|
-
|
|
1026
|
-
${markdown}`;
|
|
1027
|
-
const hdrs = githubHeaders(t);
|
|
1028
|
-
const existingId = await findExistingComment(prNumber, repo, t);
|
|
1029
|
-
if (existingId !== null) {
|
|
1030
|
-
await fetch(`${API_BASE2}/repos/${repo}/issues/comments/${existingId}`, {
|
|
1031
|
-
method: "PATCH",
|
|
1032
|
-
headers: hdrs,
|
|
1033
|
-
body: JSON.stringify({ body })
|
|
1034
|
-
});
|
|
1035
|
-
} else {
|
|
1036
|
-
await fetch(`${API_BASE2}/repos/${repo}/issues/${prNumber}/comments`, {
|
|
1037
|
-
method: "POST",
|
|
1038
|
-
headers: hdrs,
|
|
1039
|
-
body: JSON.stringify({ body })
|
|
1040
|
-
});
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// src/action.ts
|
|
1045
|
-
function getInput(name) {
|
|
1046
|
-
return (process.env[`INPUT_${name.replace(/-/g, "_").toUpperCase()}`] ?? "").trim();
|
|
1047
|
-
}
|
|
1048
|
-
function setOutput(name, value) {
|
|
1049
|
-
const outputFile = process.env["GITHUB_OUTPUT"];
|
|
1050
|
-
if (outputFile) {
|
|
1051
|
-
const delimiter = `DEPDIFF_${Math.random().toString(36).slice(2).toUpperCase()}`;
|
|
1052
|
-
appendFileSync(outputFile, `${name}<<${delimiter}
|
|
1053
|
-
${value}
|
|
1054
|
-
${delimiter}
|
|
1055
|
-
`);
|
|
1056
|
-
} else {
|
|
1057
|
-
process.stdout.write(`::set-output name=${name}::${value}
|
|
1058
|
-
`);
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
function logError(message) {
|
|
1062
|
-
process.stdout.write(`::error::${message}
|
|
1063
|
-
`);
|
|
1064
|
-
}
|
|
1065
|
-
function logNotice(message) {
|
|
1066
|
-
process.stdout.write(`::notice::${message}
|
|
1067
|
-
`);
|
|
1068
|
-
}
|
|
1069
|
-
function readEventPayload() {
|
|
1070
|
-
const eventPath = process.env["GITHUB_EVENT_PATH"];
|
|
1071
|
-
if (!eventPath) return null;
|
|
1072
|
-
try {
|
|
1073
|
-
return JSON.parse(readFileSync2(eventPath, "utf-8"));
|
|
1074
|
-
} catch {
|
|
1075
|
-
return null;
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
function detectPrNumber() {
|
|
1079
|
-
const event = readEventPayload();
|
|
1080
|
-
if (!event) return "";
|
|
1081
|
-
const pr = event["pull_request"];
|
|
1082
|
-
const num = pr?.number ?? event["number"];
|
|
1083
|
-
return num != null ? String(num) : "";
|
|
1084
|
-
}
|
|
1085
|
-
var NULL_SHA = "0000000000000000000000000000000000000000";
|
|
1086
|
-
function detectPushShas() {
|
|
1087
|
-
const event = readEventPayload();
|
|
1088
|
-
if (!event) return null;
|
|
1089
|
-
const before = event["before"];
|
|
1090
|
-
const after = event["after"];
|
|
1091
|
-
if (!before || !after || before === NULL_SHA) return null;
|
|
1092
|
-
return { baseSha: before, headSha: after };
|
|
1093
|
-
}
|
|
1094
|
-
(async () => {
|
|
1095
|
-
try {
|
|
1096
|
-
const prNumber = getInput("pr-number") || detectPrNumber();
|
|
1097
|
-
const repo = getInput("repo") || process.env["GITHUB_REPOSITORY"] || "";
|
|
1098
|
-
const pushShas = !prNumber ? detectPushShas() : null;
|
|
1099
|
-
const report = await run({
|
|
1100
|
-
base: getInput("base-ref") || process.env["GITHUB_BASE_REF"],
|
|
1101
|
-
head: getInput("head-ref") || process.env["GITHUB_HEAD_REF"],
|
|
1102
|
-
prNumber: prNumber || void 0,
|
|
1103
|
-
baseSha: pushShas?.baseSha,
|
|
1104
|
-
headSha: pushShas?.headSha,
|
|
1105
|
-
repo: repo || void 0,
|
|
1106
|
-
lockfile: getInput("lockfile") || void 0,
|
|
1107
|
-
lockfileType: getInput("type") || void 0,
|
|
1108
|
-
onNote: logNotice
|
|
1109
|
-
});
|
|
1110
|
-
const json = JSON.stringify(report, null, 2);
|
|
1111
|
-
setOutput("diff", json);
|
|
1112
|
-
const jsonToFile = getInput("json-to-file");
|
|
1113
|
-
if (jsonToFile) writeFileSync(jsonToFile, json);
|
|
1114
|
-
const filtersInput = getInput("filters");
|
|
1115
|
-
if (filtersInput) {
|
|
1116
|
-
const allChanges = report.lockfiles.flatMap((lf) => lf.changes);
|
|
1117
|
-
const filterResults = applyFilters(filtersInput, allChanges);
|
|
1118
|
-
for (const [name, matched] of Object.entries(filterResults)) {
|
|
1119
|
-
setOutput(name, String(matched));
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
const wantsMarkdown = getInput("markdown") === "true";
|
|
1123
|
-
const wantsComment = getInput("post-comment") === "true";
|
|
1124
|
-
if (wantsMarkdown || wantsComment) {
|
|
1125
|
-
const md = generateMarkdown(report);
|
|
1126
|
-
if (wantsMarkdown) {
|
|
1127
|
-
setOutput("markdown", md);
|
|
1128
|
-
const markdownToFile = getInput("markdown-to-file");
|
|
1129
|
-
if (markdownToFile) writeFileSync(markdownToFile, md);
|
|
1130
|
-
}
|
|
1131
|
-
if (wantsComment) {
|
|
1132
|
-
await postPrComment(md, prNumber, repo || void 0);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
const s = report.summary;
|
|
1136
|
-
logNotice(
|
|
1137
|
-
`depdiff: ${s.updated} updated, ${s.added} added, ${s.removed} removed (${report.lockfiles.length} lockfile(s), ecosystems: ${s.ecosystems.join(", ")})`
|
|
1138
|
-
);
|
|
1139
|
-
} catch (err) {
|
|
1140
|
-
logError(err instanceof Error ? err.message : String(err));
|
|
1141
|
-
process.exit(1);
|
|
1142
|
-
}
|
|
1143
|
-
})();
|
|
1144
|
-
//# sourceMappingURL=action.js.map
|