@lzear/repo-lint 4.2.2 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -17
- package/dist/bin.js +284 -104
- package/dist/index.d.ts +27 -1
- package/dist/index.js +578 -96
- package/package.json +26 -17
package/dist/index.js
CHANGED
|
@@ -1,149 +1,565 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { execSync } from "child_process";
|
|
3
|
-
import { existsSync as
|
|
3
|
+
import { existsSync as existsSync3 } from "fs";
|
|
4
4
|
import { tmpdir } from "os";
|
|
5
|
-
import
|
|
5
|
+
import path3 from "path";
|
|
6
6
|
|
|
7
7
|
// src/checks.ts
|
|
8
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
9
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
10
|
+
import path2 from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
import { run as ncuRun2 } from "npm-check-updates";
|
|
13
|
+
import { publint } from "publint";
|
|
14
|
+
import { maxSatisfying, satisfies } from "semver";
|
|
15
|
+
|
|
16
|
+
// src/update.ts
|
|
8
17
|
import { spawnSync } from "child_process";
|
|
9
|
-
import { existsSync,
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
10
19
|
import path from "path";
|
|
11
|
-
import {
|
|
20
|
+
import { pathToFileURL } from "url";
|
|
12
21
|
import { run as ncuRun } from "npm-check-updates";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
var LOCKFILES = [
|
|
23
|
+
["bun.lock", "bun"],
|
|
24
|
+
["bun.lockb", "bun"],
|
|
25
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
26
|
+
["yarn.lock", "yarn"],
|
|
27
|
+
["package-lock.json", "npm"]
|
|
28
|
+
];
|
|
29
|
+
var PM_FIELD_RE = /^(npm|yarn|pnpm|bun)@([^+\s]+)/;
|
|
30
|
+
var readJsonFile = (file) => {
|
|
31
|
+
const raw = readFileSync(file, "utf8");
|
|
32
|
+
return {
|
|
33
|
+
data: JSON.parse(raw),
|
|
34
|
+
indent: /^[ \t]+/m.exec(raw)?.[0] ?? " ",
|
|
35
|
+
trailingNewline: raw.endsWith("\n")
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
var writeJsonFile = (file, json) => {
|
|
39
|
+
writeFileSync(
|
|
40
|
+
file,
|
|
41
|
+
JSON.stringify(json.data, null, json.indent) + (json.trailingNewline ? "\n" : ""),
|
|
42
|
+
"utf8"
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
var detectPackageManager = (dir) => {
|
|
46
|
+
const packageFile = path.join(dir, "package.json");
|
|
47
|
+
if (existsSync(packageFile))
|
|
48
|
+
try {
|
|
49
|
+
const package_ = JSON.parse(readFileSync(packageFile, "utf8"));
|
|
50
|
+
if (typeof package_.packageManager === "string") {
|
|
51
|
+
const match = PM_FIELD_RE.exec(package_.packageManager);
|
|
52
|
+
if (match?.[1] && match[2])
|
|
53
|
+
return {
|
|
54
|
+
name: match[1],
|
|
55
|
+
version: match[2],
|
|
56
|
+
source: "packageManager"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
for (const [file, name] of LOCKFILES)
|
|
62
|
+
if (existsSync(path.join(dir, file))) return { name, source: "lockfile" };
|
|
63
|
+
return { name: "npm", source: "default" };
|
|
64
|
+
};
|
|
65
|
+
var fetchJson = async (url) => {
|
|
66
|
+
const response = await fetch(url);
|
|
67
|
+
if (!response.ok) throw new Error(`GET ${url} \u2014 HTTP ${response.status}`);
|
|
68
|
+
return await response.json();
|
|
69
|
+
};
|
|
70
|
+
var pmRegistryName = (name, currentMajor) => name === "yarn" && currentMajor >= 2 ? "@yarnpkg%2Fcli-dist" : name;
|
|
71
|
+
var latestPackageVersion = async (name) => {
|
|
72
|
+
const data = await fetchJson(`https://registry.npmjs.org/${name}/latest`);
|
|
73
|
+
if (typeof data.version !== "string")
|
|
74
|
+
throw new Error(`no version in registry response for ${name}`);
|
|
75
|
+
return data.version;
|
|
76
|
+
};
|
|
77
|
+
var hasWorkspaces = (dir) => {
|
|
78
|
+
try {
|
|
79
|
+
const package_ = JSON.parse(
|
|
80
|
+
readFileSync(path.join(dir, "package.json"), "utf8")
|
|
81
|
+
);
|
|
82
|
+
const ws = package_.workspaces;
|
|
83
|
+
if (Array.isArray(ws)) return ws.length > 0;
|
|
84
|
+
const packages = ws?.packages;
|
|
85
|
+
return Array.isArray(packages) && packages.length > 0;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var NCURC_FILES = [
|
|
91
|
+
".ncurc.json",
|
|
92
|
+
".ncurc.js",
|
|
93
|
+
".ncurc.cjs",
|
|
94
|
+
".ncurc.mjs",
|
|
95
|
+
".ncurc"
|
|
96
|
+
];
|
|
97
|
+
var loadNcuRc = async (dir) => {
|
|
98
|
+
for (const name of NCURC_FILES) {
|
|
99
|
+
const file = path.join(dir, name);
|
|
100
|
+
if (!existsSync(file)) continue;
|
|
101
|
+
if (name === ".ncurc" || name.endsWith(".json"))
|
|
102
|
+
return {
|
|
103
|
+
file: name,
|
|
104
|
+
config: JSON.parse(readFileSync(file, "utf8"))
|
|
105
|
+
};
|
|
106
|
+
const module_ = await import(pathToFileURL(file).href);
|
|
107
|
+
return { file: name, config: module_.default ?? module_ };
|
|
108
|
+
}
|
|
109
|
+
return { config: {} };
|
|
110
|
+
};
|
|
111
|
+
var stepDeps = async (dir, isDry) => {
|
|
112
|
+
const base = { id: "deps", desc: "dependency ranges (ncu)" };
|
|
113
|
+
try {
|
|
114
|
+
const isWorkspaces = hasWorkspaces(dir);
|
|
115
|
+
const rc = await loadNcuRc(dir);
|
|
116
|
+
const result = await ncuRun({
|
|
117
|
+
...rc.config,
|
|
118
|
+
packageFile: path.join(dir, "package.json"),
|
|
119
|
+
cwd: dir,
|
|
120
|
+
upgrade: !isDry,
|
|
121
|
+
silent: true,
|
|
122
|
+
dep: ["prod", "dev", "optional", "peer"],
|
|
123
|
+
...isWorkspaces && { workspaces: true, root: true }
|
|
124
|
+
});
|
|
125
|
+
const entries = isWorkspaces ? Object.values(result).flatMap(
|
|
126
|
+
(x) => Object.entries(x)
|
|
127
|
+
) : Object.entries(result);
|
|
128
|
+
const rcNote = rc.file ? ` (using ${rc.file})` : "";
|
|
129
|
+
if (entries.length === 0)
|
|
130
|
+
return {
|
|
131
|
+
...base,
|
|
132
|
+
pass: true,
|
|
133
|
+
changed: false,
|
|
134
|
+
detail: `up to date${rcNote}`
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
...base,
|
|
138
|
+
pass: true,
|
|
139
|
+
changed: true,
|
|
140
|
+
detail: entries.map(([k, v]) => `${k} \u2192 ${v}`).join("\n") + rcNote
|
|
141
|
+
};
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return { ...base, pass: false, changed: false, detail: String(error) };
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
var stepPackageManager = async (dir, isDry) => {
|
|
147
|
+
const base = { id: "package-manager", desc: "packageManager field" };
|
|
148
|
+
try {
|
|
149
|
+
const file = path.join(dir, "package.json");
|
|
150
|
+
const json = readJsonFile(file);
|
|
151
|
+
if (typeof json.data.packageManager !== "string")
|
|
152
|
+
return {
|
|
153
|
+
...base,
|
|
154
|
+
pass: true,
|
|
155
|
+
changed: false,
|
|
156
|
+
detail: "no packageManager field"
|
|
157
|
+
};
|
|
158
|
+
const match = PM_FIELD_RE.exec(json.data.packageManager);
|
|
159
|
+
if (!match)
|
|
160
|
+
return {
|
|
161
|
+
...base,
|
|
162
|
+
pass: true,
|
|
163
|
+
changed: false,
|
|
164
|
+
detail: `unrecognized: ${json.data.packageManager}`
|
|
165
|
+
};
|
|
166
|
+
const [, name, current] = match;
|
|
167
|
+
const major = Number(current.split(".", 1)[0]);
|
|
168
|
+
const latest = await latestPackageVersion(pmRegistryName(name, major));
|
|
169
|
+
if (latest === current)
|
|
170
|
+
return {
|
|
171
|
+
...base,
|
|
172
|
+
pass: true,
|
|
173
|
+
changed: false,
|
|
174
|
+
detail: `${name}@${current} is latest`
|
|
175
|
+
};
|
|
176
|
+
json.data.packageManager = `${name}@${latest}`;
|
|
177
|
+
if (!isDry) writeJsonFile(file, json);
|
|
178
|
+
return {
|
|
179
|
+
...base,
|
|
180
|
+
pass: true,
|
|
181
|
+
changed: true,
|
|
182
|
+
detail: `${name} ${current} \u2192 ${latest}`
|
|
183
|
+
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return { ...base, pass: false, changed: false, detail: String(error) };
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
var updateVersionFile = (file, latest, isDry) => {
|
|
189
|
+
const raw = readFileSync(file, "utf8");
|
|
190
|
+
const current = raw.trim();
|
|
191
|
+
const isVPrefixed = current.startsWith("v");
|
|
192
|
+
const next = isVPrefixed ? `v${latest}` : latest;
|
|
193
|
+
if (current === next)
|
|
194
|
+
return { changed: false, detail: `${path.basename(file)} is latest` };
|
|
195
|
+
if (!isDry)
|
|
196
|
+
writeFileSync(file, next + (raw.endsWith("\n") ? "\n" : ""), "utf8");
|
|
197
|
+
return {
|
|
198
|
+
changed: true,
|
|
199
|
+
detail: `${path.basename(file)} ${current} \u2192 ${next}`
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
var stepNodeVersion = async (dir, isDry) => {
|
|
203
|
+
const base = { id: "node-version", desc: "node version files" };
|
|
204
|
+
const files = [".nvmrc", ".node-version"].map((f) => path.join(dir, f)).filter((f) => existsSync(f));
|
|
205
|
+
if (files.length === 0)
|
|
206
|
+
return { ...base, pass: true, changed: false, detail: "no version file" };
|
|
207
|
+
try {
|
|
208
|
+
const response = await fetch("https://nodejs.org/dist/index.json");
|
|
209
|
+
if (!response.ok)
|
|
210
|
+
throw new Error(
|
|
211
|
+
`GET nodejs.org/dist/index.json \u2014 HTTP ${response.status}`
|
|
212
|
+
);
|
|
213
|
+
const index = await response.json();
|
|
214
|
+
const lts = index.find((v) => v.lts);
|
|
215
|
+
if (!lts) throw new Error("no LTS release found");
|
|
216
|
+
const latest = lts.version.replace(/^v/, "");
|
|
217
|
+
const results = files.map((f) => updateVersionFile(f, latest, isDry));
|
|
218
|
+
return {
|
|
219
|
+
...base,
|
|
220
|
+
pass: true,
|
|
221
|
+
changed: results.some((r) => r.changed),
|
|
222
|
+
detail: results.map((r) => r.detail).join("\n")
|
|
223
|
+
};
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return { ...base, pass: false, changed: false, detail: String(error) };
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
var stepBunVersion = async (dir, isDry) => {
|
|
229
|
+
const base = { id: "bun-version", desc: "bun version file" };
|
|
230
|
+
const file = path.join(dir, ".bun-version");
|
|
231
|
+
if (!existsSync(file))
|
|
232
|
+
return { ...base, pass: true, changed: false, detail: "no version file" };
|
|
233
|
+
try {
|
|
234
|
+
const latest = await latestPackageVersion("bun");
|
|
235
|
+
const result = updateVersionFile(file, latest, isDry);
|
|
236
|
+
return { ...base, pass: true, ...result };
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return { ...base, pass: false, changed: false, detail: String(error) };
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var LICENSE_FILES = ["LICENSE", "LICENSE.md", "LICENSE.txt"];
|
|
242
|
+
var COPYRIGHT_RE = /(copyright(?:\s+\(c\)|\s+©)?\s+)(\d{4})(?:\s*-\s*(\d{4}))?/i;
|
|
243
|
+
var stepLicenseYear = (dir, isDry) => {
|
|
244
|
+
const base = { id: "license-year", desc: "LICENSE copyright year" };
|
|
245
|
+
const file = LICENSE_FILES.map((f) => path.join(dir, f)).find(
|
|
246
|
+
(f) => existsSync(f)
|
|
247
|
+
);
|
|
248
|
+
if (!file)
|
|
249
|
+
return { ...base, pass: true, changed: false, detail: "no license file" };
|
|
250
|
+
const raw = readFileSync(file, "utf8");
|
|
251
|
+
const match = COPYRIGHT_RE.exec(raw);
|
|
252
|
+
if (!match?.[2])
|
|
253
|
+
return {
|
|
254
|
+
...base,
|
|
255
|
+
pass: true,
|
|
256
|
+
changed: false,
|
|
257
|
+
detail: "no copyright year found"
|
|
258
|
+
};
|
|
259
|
+
const year = (/* @__PURE__ */ new Date()).getFullYear();
|
|
260
|
+
const start = match[2];
|
|
261
|
+
const end = Number(match[3] ?? start);
|
|
262
|
+
if (end >= year)
|
|
263
|
+
return {
|
|
264
|
+
...base,
|
|
265
|
+
pass: true,
|
|
266
|
+
changed: false,
|
|
267
|
+
detail: `${start}${match[3] ? "-" + match[3] : ""} is current`
|
|
268
|
+
};
|
|
269
|
+
const current = match[0];
|
|
270
|
+
const next = `${match[1] ?? ""}${start}-${year}`;
|
|
271
|
+
if (!isDry)
|
|
272
|
+
writeFileSync(
|
|
273
|
+
file,
|
|
274
|
+
raw.replace(COPYRIGHT_RE, () => next),
|
|
275
|
+
"utf8"
|
|
276
|
+
);
|
|
277
|
+
return {
|
|
278
|
+
...base,
|
|
279
|
+
pass: true,
|
|
280
|
+
changed: true,
|
|
281
|
+
detail: `${current.trim()} \u2192 ${next.trim()}`
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
var installCommands = (pm) => {
|
|
285
|
+
switch (pm.name) {
|
|
286
|
+
case "bun":
|
|
287
|
+
return [["bun", "update"]];
|
|
288
|
+
case "pnpm":
|
|
289
|
+
return [
|
|
290
|
+
["pnpm", "install"],
|
|
291
|
+
["pnpm", "update", "--recursive"],
|
|
292
|
+
["pnpm", "dedupe"]
|
|
293
|
+
];
|
|
294
|
+
case "yarn":
|
|
295
|
+
return pm.version?.startsWith("1.") ? [
|
|
296
|
+
["yarn", "install"],
|
|
297
|
+
["yarn", "upgrade"]
|
|
298
|
+
] : [
|
|
299
|
+
["yarn", "install"],
|
|
300
|
+
["yarn", "up", "--recursive", "*"],
|
|
301
|
+
["yarn", "dedupe"]
|
|
302
|
+
];
|
|
303
|
+
case "npm":
|
|
304
|
+
return [
|
|
305
|
+
["npm", "install"],
|
|
306
|
+
["npm", "update"],
|
|
307
|
+
["npm", "dedupe"]
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
var stepInstall = (dir, pm) => {
|
|
312
|
+
const base = {
|
|
313
|
+
id: "install",
|
|
314
|
+
desc: `install & refresh lockfile (${pm.name})`
|
|
315
|
+
};
|
|
316
|
+
const lines = [];
|
|
317
|
+
for (const [command, ...arguments_] of installCommands(pm)) {
|
|
318
|
+
lines.push(`$ ${[command, ...arguments_].join(" ")}`);
|
|
319
|
+
const result = spawnSync(command, arguments_, {
|
|
320
|
+
cwd: dir,
|
|
321
|
+
stdio: "inherit"
|
|
322
|
+
});
|
|
323
|
+
if (result.status !== 0)
|
|
324
|
+
return {
|
|
325
|
+
...base,
|
|
326
|
+
pass: false,
|
|
327
|
+
changed: true,
|
|
328
|
+
detail: `${lines.join("\n")}
|
|
329
|
+
exited with ${result.status ?? "signal"}`
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
return { ...base, pass: true, changed: true, detail: lines.join("\n") };
|
|
333
|
+
};
|
|
334
|
+
var runUpdate = async (options = {}) => {
|
|
335
|
+
const {
|
|
336
|
+
dir = process.cwd(),
|
|
337
|
+
dry: isDry = false,
|
|
338
|
+
install: isInstall = true
|
|
339
|
+
} = options;
|
|
340
|
+
const packageManager = detectPackageManager(dir);
|
|
341
|
+
const results = [
|
|
342
|
+
await stepDeps(dir, isDry),
|
|
343
|
+
await stepPackageManager(dir, isDry),
|
|
344
|
+
await stepNodeVersion(dir, isDry),
|
|
345
|
+
await stepBunVersion(dir, isDry),
|
|
346
|
+
stepLicenseYear(dir, isDry)
|
|
347
|
+
];
|
|
348
|
+
if (isInstall && !isDry) results.push(stepInstall(dir, packageManager));
|
|
349
|
+
return { dir, packageManager, results };
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// src/checks.ts
|
|
353
|
+
var _dirname = path2.dirname(fileURLToPath(import.meta.url));
|
|
354
|
+
var readPackage = (dir) => {
|
|
355
|
+
const f = path2.join(dir, "package.json");
|
|
356
|
+
if (!existsSync2(f)) return null;
|
|
18
357
|
try {
|
|
19
|
-
return JSON.parse(
|
|
358
|
+
return JSON.parse(readFileSync2(f, "utf8"));
|
|
20
359
|
} catch {
|
|
21
360
|
return null;
|
|
22
361
|
}
|
|
23
362
|
};
|
|
24
|
-
var getWorkspacePatterns = (
|
|
25
|
-
const ws =
|
|
363
|
+
var getWorkspacePatterns = (package_) => {
|
|
364
|
+
const ws = package_.workspaces;
|
|
26
365
|
if (Array.isArray(ws)) return ws;
|
|
27
|
-
const
|
|
28
|
-
if (Array.isArray(
|
|
366
|
+
const wsPackage = ws;
|
|
367
|
+
if (Array.isArray(wsPackage?.packages)) return wsPackage.packages;
|
|
29
368
|
return [];
|
|
30
369
|
};
|
|
31
370
|
var patternToBase = (rootDir, pattern) => {
|
|
32
371
|
const parts = pattern.split("/");
|
|
33
372
|
if (parts.length === 2 && parts[1] === "*")
|
|
34
|
-
return
|
|
373
|
+
return path2.join(rootDir, parts[0] ?? "");
|
|
35
374
|
if (parts.length === 1 && parts[0] === "*") return rootDir;
|
|
36
375
|
return null;
|
|
37
376
|
};
|
|
38
|
-
var
|
|
39
|
-
const
|
|
40
|
-
if (!
|
|
41
|
-
const
|
|
42
|
-
for (const pattern of getWorkspacePatterns(
|
|
377
|
+
var getWorkspaceDirectories = (rootDir) => {
|
|
378
|
+
const package_ = readPackage(rootDir);
|
|
379
|
+
if (!package_) return [];
|
|
380
|
+
const directories = [];
|
|
381
|
+
for (const pattern of getWorkspacePatterns(package_)) {
|
|
43
382
|
const base = patternToBase(rootDir, pattern);
|
|
44
|
-
if (!base || !
|
|
45
|
-
|
|
46
|
-
|
|
383
|
+
if (!base || !existsSync2(base)) continue;
|
|
384
|
+
const entries = readdirSync(base, { withFileTypes: true });
|
|
385
|
+
for (const entry of entries)
|
|
386
|
+
if (entry.isDirectory()) directories.push(path2.join(base, entry.name));
|
|
47
387
|
}
|
|
48
|
-
return
|
|
388
|
+
return directories;
|
|
49
389
|
};
|
|
50
390
|
var findBin = (name, startDir) => {
|
|
51
391
|
let dir = startDir;
|
|
52
392
|
while (true) {
|
|
53
|
-
const bin =
|
|
54
|
-
if (
|
|
55
|
-
const parent =
|
|
393
|
+
const bin = path2.join(dir, "node_modules", ".bin", name);
|
|
394
|
+
if (existsSync2(bin)) return bin;
|
|
395
|
+
const parent = path2.join(dir, "..");
|
|
56
396
|
if (parent === dir) return null;
|
|
57
397
|
dir = parent;
|
|
58
398
|
}
|
|
59
399
|
};
|
|
60
|
-
var
|
|
61
|
-
const
|
|
62
|
-
if (!
|
|
63
|
-
if (
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
400
|
+
var eachPublishedPackage = async (dir, function_) => {
|
|
401
|
+
const package_ = readPackage(dir);
|
|
402
|
+
if (!package_) return { pass: true };
|
|
403
|
+
if (package_.private === true) {
|
|
404
|
+
const wsDirectories = getWorkspaceDirectories(dir);
|
|
405
|
+
if (wsDirectories.length === 0) return { pass: true };
|
|
66
406
|
const results = await Promise.all(
|
|
67
|
-
|
|
407
|
+
wsDirectories.map((d) => eachPublishedPackage(d, function_))
|
|
68
408
|
);
|
|
69
409
|
const failures = results.filter((r) => !r.pass);
|
|
70
410
|
if (failures.length === 0) return { pass: true };
|
|
71
411
|
const detail = failures.flatMap((r) => r.detail ? [r.detail] : []).join("\n");
|
|
72
412
|
return { pass: false, ...detail && { detail } };
|
|
73
413
|
}
|
|
74
|
-
return
|
|
414
|
+
return function_(dir);
|
|
75
415
|
};
|
|
76
416
|
var hasPublishedPkg = (dir) => {
|
|
77
|
-
const
|
|
78
|
-
if (!
|
|
79
|
-
if (
|
|
80
|
-
return
|
|
417
|
+
const package_ = readPackage(dir);
|
|
418
|
+
if (!package_) return false;
|
|
419
|
+
if (package_.private !== true) return true;
|
|
420
|
+
return getWorkspaceDirectories(dir).some((d) => hasPublishedPkg(d));
|
|
81
421
|
};
|
|
82
422
|
var readmeIncludes = (dir, needle) => {
|
|
83
|
-
const f =
|
|
84
|
-
return
|
|
423
|
+
const f = path2.join(dir, "README.md");
|
|
424
|
+
return existsSync2(f) && readFileSync2(f, "utf8").includes(needle);
|
|
425
|
+
};
|
|
426
|
+
var FORGE_WORKFLOW_RE = /uses:\s*lzear\/forge\/\.github\/workflows\/ci\.yml@/;
|
|
427
|
+
var isForgeRepo = (dir) => getWorkspaceDirectories(dir).some(
|
|
428
|
+
(d) => readPackage(d)?.name === "@lzear/forge"
|
|
429
|
+
);
|
|
430
|
+
var callsForgeWorkflow = (dir) => {
|
|
431
|
+
const wfDir = path2.join(dir, ".github", "workflows");
|
|
432
|
+
if (!existsSync2(wfDir)) return false;
|
|
433
|
+
return readdirSync(wfDir).filter((f) => /\.ya?ml$/.test(f)).some(
|
|
434
|
+
(f) => FORGE_WORKFLOW_RE.test(readFileSync2(path2.join(wfDir, f), "utf8"))
|
|
435
|
+
);
|
|
436
|
+
};
|
|
437
|
+
var auditCommand = (pm) => {
|
|
438
|
+
switch (pm.name) {
|
|
439
|
+
case "npm":
|
|
440
|
+
return ["npm", "audit", "--omit", "dev", "--audit-level", "high"];
|
|
441
|
+
case "pnpm":
|
|
442
|
+
return ["pnpm", "audit", "--prod", "--audit-level", "high"];
|
|
443
|
+
case "bun":
|
|
444
|
+
return ["bun", "audit"];
|
|
445
|
+
case "yarn":
|
|
446
|
+
return pm.version?.startsWith("1.") ? ["yarn", "audit", "--level", "high"] : [
|
|
447
|
+
"yarn",
|
|
448
|
+
"npm",
|
|
449
|
+
"audit",
|
|
450
|
+
"--environment",
|
|
451
|
+
"production",
|
|
452
|
+
"--severity",
|
|
453
|
+
"high"
|
|
454
|
+
];
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
var DEP_FIELDS = [
|
|
458
|
+
"dependencies",
|
|
459
|
+
"devDependencies",
|
|
460
|
+
"optionalDependencies",
|
|
461
|
+
"peerDependencies"
|
|
462
|
+
];
|
|
463
|
+
var NON_REGISTRY_RANGE_RE = /^(?:workspace:|file:|link:|portal:|catalog:|git|https?:)/;
|
|
464
|
+
var collectEntry = (dependencies, name, range) => {
|
|
465
|
+
if (typeof range !== "string" || NON_REGISTRY_RANGE_RE.test(range)) return;
|
|
466
|
+
const alias = /^npm:(.+)@([^@]+)$/.exec(range);
|
|
467
|
+
const target = alias?.[1] ?? name;
|
|
468
|
+
const targetRange = alias?.[2] ?? range;
|
|
469
|
+
if (!dependencies.has(target)) dependencies.set(target, targetRange);
|
|
470
|
+
};
|
|
471
|
+
var collectFromPackage = (dependencies, packageDir) => {
|
|
472
|
+
const package_ = readPackage(packageDir);
|
|
473
|
+
for (const field of DEP_FIELDS) {
|
|
474
|
+
const section = package_?.[field];
|
|
475
|
+
if (typeof section !== "object" || section === null) continue;
|
|
476
|
+
for (const [name, range] of Object.entries(section))
|
|
477
|
+
collectEntry(dependencies, name, range);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
var collectDependencies = (dir) => {
|
|
481
|
+
const dependencies = /* @__PURE__ */ new Map();
|
|
482
|
+
for (const packageDir of [dir, ...getWorkspaceDirectories(dir)])
|
|
483
|
+
collectFromPackage(dependencies, packageDir);
|
|
484
|
+
return dependencies;
|
|
485
|
+
};
|
|
486
|
+
var findDeprecated = async (name, range) => {
|
|
487
|
+
const response = await fetch(
|
|
488
|
+
`https://registry.npmjs.org/${name.replace("/", "%2F")}`,
|
|
489
|
+
{
|
|
490
|
+
headers: { accept: "application/vnd.npm.install-v1+json" },
|
|
491
|
+
signal: AbortSignal.timeout(1e4)
|
|
492
|
+
}
|
|
493
|
+
);
|
|
494
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
495
|
+
const data = await response.json();
|
|
496
|
+
const latest = data["dist-tags"]?.latest;
|
|
497
|
+
const resolved = latest && satisfies(latest, range) ? latest : maxSatisfying(Object.keys(data.versions), range);
|
|
498
|
+
if (!resolved) return null;
|
|
499
|
+
const deprecated = data.versions[resolved]?.deprecated;
|
|
500
|
+
return deprecated ? `${name}@${resolved} \u2014 ${deprecated.slice(0, 120)}` : null;
|
|
85
501
|
};
|
|
86
502
|
var LOCAL_CHECKS = [
|
|
87
503
|
{
|
|
88
504
|
id: "readme-exists",
|
|
89
|
-
desc: "README
|
|
505
|
+
desc: "README",
|
|
90
506
|
type: "local",
|
|
91
|
-
check: (dir) =>
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
id: "readme-codacy-grade-badge",
|
|
95
|
-
desc: "README has Codacy grade badge",
|
|
96
|
-
type: "local",
|
|
97
|
-
publishedOnly: true,
|
|
98
|
-
check: (dir) => readmeIncludes(dir, "project/badge/Grade/")
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
id: "readme-codacy-coverage-badge",
|
|
102
|
-
desc: "README has Codacy coverage badge",
|
|
103
|
-
type: "local",
|
|
104
|
-
publishedOnly: true,
|
|
105
|
-
check: (dir) => readmeIncludes(dir, "project/badge/Coverage/")
|
|
507
|
+
check: (dir) => existsSync2(path2.join(dir, "README.md"))
|
|
106
508
|
},
|
|
107
509
|
{
|
|
108
510
|
id: "readme-npm-badge",
|
|
109
|
-
desc: "
|
|
511
|
+
desc: "npm badge",
|
|
110
512
|
type: "local",
|
|
111
513
|
publishedOnly: true,
|
|
112
514
|
check: (dir) => readmeIncludes(dir, "shields.io/npm/v/")
|
|
113
515
|
},
|
|
114
516
|
{
|
|
115
|
-
id: "codacy
|
|
116
|
-
desc: "
|
|
517
|
+
id: "codacy",
|
|
518
|
+
desc: "Codacy",
|
|
117
519
|
type: "local",
|
|
118
520
|
publishedOnly: true,
|
|
119
|
-
check: (dir) =>
|
|
521
|
+
check: (dir) => {
|
|
522
|
+
const missing = [
|
|
523
|
+
readmeIncludes(dir, "project/badge/Grade/") ? null : "grade badge missing in README",
|
|
524
|
+
readmeIncludes(dir, "project/badge/Coverage/") ? null : "coverage badge missing in README",
|
|
525
|
+
existsSync2(path2.join(dir, ".codacy.yml")) ? null : ".codacy.yml missing"
|
|
526
|
+
].filter((m) => m !== null);
|
|
527
|
+
if (missing.length === 0) return true;
|
|
528
|
+
return { pass: false, detail: missing.join("\n") };
|
|
529
|
+
}
|
|
120
530
|
},
|
|
121
531
|
{
|
|
122
532
|
id: "license",
|
|
123
|
-
desc: "LICENSE
|
|
533
|
+
desc: "LICENSE",
|
|
124
534
|
type: "local",
|
|
125
535
|
publishedOnly: true,
|
|
126
|
-
check: (dir) =>
|
|
536
|
+
check: (dir) => existsSync2(path2.join(dir, "LICENSE"))
|
|
127
537
|
},
|
|
128
538
|
{
|
|
129
539
|
id: "ci-workflow",
|
|
130
|
-
desc: "CI workflow
|
|
540
|
+
desc: "CI calls forge workflow",
|
|
131
541
|
type: "local",
|
|
132
542
|
publishedOnly: true,
|
|
133
|
-
check: (dir) =>
|
|
543
|
+
check: (dir) => {
|
|
544
|
+
if (callsForgeWorkflow(dir) || isForgeRepo(dir)) return true;
|
|
545
|
+
return {
|
|
546
|
+
pass: false,
|
|
547
|
+
detail: "no workflow calls the forge reusable CI \u2014 replace the local ci.yml copy with:\n jobs:\n ci:\n uses: lzear/forge/.github/workflows/ci.yml@<sha> # vX.Y.Z\n(pin to a commit SHA; renovate keeps it fresh)"
|
|
548
|
+
};
|
|
549
|
+
}
|
|
134
550
|
},
|
|
135
551
|
{
|
|
136
552
|
id: "renovate",
|
|
137
|
-
desc: "renovate.json
|
|
553
|
+
desc: "renovate.json",
|
|
138
554
|
type: "local",
|
|
139
|
-
check: (dir) =>
|
|
555
|
+
check: (dir) => existsSync2(path2.join(dir, "renovate.json"))
|
|
140
556
|
},
|
|
141
557
|
{
|
|
142
558
|
id: "pkg-publint",
|
|
143
|
-
desc: "publint
|
|
559
|
+
desc: "publint",
|
|
144
560
|
type: "local",
|
|
145
561
|
publishedOnly: true,
|
|
146
|
-
check: (dir) =>
|
|
562
|
+
check: (dir) => eachPublishedPackage(dir, async (pkgDir) => {
|
|
147
563
|
const { messages } = await publint({ pkgDir });
|
|
148
564
|
const failures = messages.filter(
|
|
149
565
|
(m) => m.type === "error" || m.type === "warning"
|
|
@@ -157,13 +573,13 @@ var LOCAL_CHECKS = [
|
|
|
157
573
|
},
|
|
158
574
|
{
|
|
159
575
|
id: "pkg-attw",
|
|
160
|
-
desc: "attw
|
|
576
|
+
desc: "attw",
|
|
161
577
|
type: "local",
|
|
162
578
|
check: (dir) => {
|
|
163
579
|
const bin = findBin("attw", _dirname);
|
|
164
580
|
if (!bin) return { pass: false, detail: "attw not available" };
|
|
165
|
-
return
|
|
166
|
-
const r =
|
|
581
|
+
return eachPublishedPackage(dir, (pkgDir) => {
|
|
582
|
+
const r = spawnSync2(
|
|
167
583
|
process.execPath,
|
|
168
584
|
[bin, "--pack", "--profile", "esm-only"],
|
|
169
585
|
{
|
|
@@ -179,12 +595,12 @@ var LOCAL_CHECKS = [
|
|
|
179
595
|
},
|
|
180
596
|
{
|
|
181
597
|
id: "pkg-knip",
|
|
182
|
-
desc: "knip
|
|
598
|
+
desc: "knip",
|
|
183
599
|
type: "local",
|
|
184
600
|
check: (dir) => {
|
|
185
601
|
const bin = findBin("knip", _dirname);
|
|
186
602
|
if (!bin) return { pass: false, detail: "knip not available" };
|
|
187
|
-
const r =
|
|
603
|
+
const r = spawnSync2(process.execPath, [bin], {
|
|
188
604
|
cwd: dir,
|
|
189
605
|
encoding: "utf8",
|
|
190
606
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -197,28 +613,90 @@ var LOCAL_CHECKS = [
|
|
|
197
613
|
Run: npx knip` };
|
|
198
614
|
}
|
|
199
615
|
},
|
|
616
|
+
{
|
|
617
|
+
id: "monorepo-lint",
|
|
618
|
+
desc: "sherif (workspaces)",
|
|
619
|
+
type: "local",
|
|
620
|
+
check: (dir) => {
|
|
621
|
+
const package_ = readPackage(dir);
|
|
622
|
+
if (!package_ || getWorkspacePatterns(package_).length === 0)
|
|
623
|
+
return { pass: true, detail: "not a monorepo" };
|
|
624
|
+
const bin = findBin("sherif", _dirname);
|
|
625
|
+
if (!bin) return { pass: false, detail: "sherif not available" };
|
|
626
|
+
const r = spawnSync2(process.execPath, [bin], {
|
|
627
|
+
cwd: dir,
|
|
628
|
+
encoding: "utf8",
|
|
629
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
630
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
631
|
+
});
|
|
632
|
+
if (r.status === 0) return true;
|
|
633
|
+
const detail = (r.stdout + r.stderr).trim().split("\n").slice(0, 20).join("\n");
|
|
634
|
+
return { pass: false, detail };
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
id: "deps-audit",
|
|
639
|
+
desc: "audit",
|
|
640
|
+
type: "local",
|
|
641
|
+
check: (dir) => {
|
|
642
|
+
const pm = detectPackageManager(dir);
|
|
643
|
+
const [command, ...arguments_] = auditCommand(pm);
|
|
644
|
+
const r = spawnSync2(command, arguments_, {
|
|
645
|
+
cwd: dir,
|
|
646
|
+
encoding: "utf8",
|
|
647
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
648
|
+
});
|
|
649
|
+
if (r.error) return { pass: false, detail: String(r.error) };
|
|
650
|
+
if (r.status === 0) return true;
|
|
651
|
+
const detail = (r.stdout + r.stderr).trim().split("\n").slice(0, 20).join("\n");
|
|
652
|
+
return { pass: false, detail: detail || `audit exited ${r.status}` };
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
id: "deps-deprecated",
|
|
657
|
+
desc: "no deprecated deps",
|
|
658
|
+
type: "local",
|
|
659
|
+
check: async (dir) => {
|
|
660
|
+
const dependencies = collectDependencies(dir);
|
|
661
|
+
const failures = [];
|
|
662
|
+
await Promise.all(
|
|
663
|
+
[...dependencies].map(async ([name, range]) => {
|
|
664
|
+
try {
|
|
665
|
+
const deprecated = await findDeprecated(name, range);
|
|
666
|
+
if (deprecated) failures.push(deprecated);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
failures.push(`${name} \u2014 check failed: ${String(error)}`);
|
|
669
|
+
}
|
|
670
|
+
})
|
|
671
|
+
);
|
|
672
|
+
if (failures.length === 0) return true;
|
|
673
|
+
return {
|
|
674
|
+
pass: false,
|
|
675
|
+
detail: failures.toSorted((a, b) => a.localeCompare(b)).join("\n")
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
},
|
|
200
679
|
{
|
|
201
680
|
id: "deps-fresh",
|
|
202
|
-
desc: "
|
|
681
|
+
desc: "deps up to date",
|
|
203
682
|
type: "local",
|
|
204
683
|
check: async (dir) => {
|
|
205
|
-
const
|
|
206
|
-
const
|
|
684
|
+
const package_ = readPackage(dir);
|
|
685
|
+
const hasWorkspaces2 = Array.isArray(package_?.workspaces) && package_.workspaces.length > 0;
|
|
207
686
|
try {
|
|
208
|
-
const result = await
|
|
209
|
-
packageFile:
|
|
210
|
-
...
|
|
687
|
+
const result = await ncuRun2({
|
|
688
|
+
packageFile: path2.join(dir, "package.json"),
|
|
689
|
+
...hasWorkspaces2 && { workspaces: true },
|
|
211
690
|
silent: true
|
|
212
691
|
});
|
|
213
|
-
const entries =
|
|
692
|
+
const entries = hasWorkspaces2 ? Object.values(
|
|
214
693
|
result
|
|
215
694
|
).flatMap((x) => Object.entries(x)) : Object.entries(result);
|
|
216
695
|
if (entries.length === 0) return true;
|
|
217
696
|
const lines = entries.map(([k, v]) => `${k} \u2192 ${v}`).join("\n");
|
|
218
|
-
const cmd = hasWorkspaces ? "yarn dlx npm-check-updates --dep dev,optional,peer,prod,packageManager -u --workspaces && yarn install" : "yarn dlx npm-check-updates --dep dev,optional,peer,prod,packageManager -u && yarn install";
|
|
219
697
|
return { pass: false, detail: `${lines}
|
|
220
698
|
|
|
221
|
-
Run:
|
|
699
|
+
Run: forge update` };
|
|
222
700
|
} catch (error) {
|
|
223
701
|
return { pass: false, detail: String(error) };
|
|
224
702
|
}
|
|
@@ -227,7 +705,7 @@ Run: ${cmd}` };
|
|
|
227
705
|
];
|
|
228
706
|
var listSecrets = (repo) => {
|
|
229
707
|
try {
|
|
230
|
-
const result =
|
|
708
|
+
const result = spawnSync2(
|
|
231
709
|
"gh",
|
|
232
710
|
["secret", "list", "--repo", repo, "--json", "name"],
|
|
233
711
|
{
|
|
@@ -244,7 +722,7 @@ var listSecrets = (repo) => {
|
|
|
244
722
|
var REMOTE_CHECKS = [
|
|
245
723
|
{
|
|
246
724
|
id: "secret-codacy-token",
|
|
247
|
-
desc: "
|
|
725
|
+
desc: "Codacy secret",
|
|
248
726
|
type: "remote",
|
|
249
727
|
publishedOnly: true,
|
|
250
728
|
check: (repo) => {
|
|
@@ -265,19 +743,21 @@ var checkLocal = async (options = {}) => {
|
|
|
265
743
|
stdio: ["ignore", "pipe", "ignore"]
|
|
266
744
|
}).trim();
|
|
267
745
|
const match = /github\.com[:/](.+?)(?:\.git)?$/.exec(remote);
|
|
268
|
-
return match?.[1] ??
|
|
746
|
+
return match?.[1] ?? path3.basename(dir);
|
|
269
747
|
} catch {
|
|
270
748
|
return dir;
|
|
271
749
|
}
|
|
272
750
|
})();
|
|
273
|
-
const
|
|
751
|
+
const isPublished = hasPublishedPkg(dir);
|
|
274
752
|
const localResults = await Promise.all(
|
|
275
|
-
LOCAL_CHECKS.filter((c) => !c.publishedOnly ||
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
753
|
+
LOCAL_CHECKS.filter((c) => !c.publishedOnly || isPublished).map(
|
|
754
|
+
async (c) => {
|
|
755
|
+
const raw = await c.check(dir);
|
|
756
|
+
return typeof raw === "boolean" ? { id: c.id, desc: c.desc, pass: raw } : { id: c.id, desc: c.desc, ...raw };
|
|
757
|
+
}
|
|
758
|
+
)
|
|
279
759
|
);
|
|
280
|
-
const remoteResults = skipRemote ? [] : REMOTE_CHECKS.filter((c) => !c.publishedOnly ||
|
|
760
|
+
const remoteResults = skipRemote ? [] : REMOTE_CHECKS.filter((c) => !c.publishedOnly || isPublished).map((c) => ({
|
|
281
761
|
id: c.id,
|
|
282
762
|
desc: c.desc,
|
|
283
763
|
pass: c.check(repo)
|
|
@@ -287,11 +767,11 @@ var checkLocal = async (options = {}) => {
|
|
|
287
767
|
var checkRepo = async (repo, options = {}) => {
|
|
288
768
|
const {
|
|
289
769
|
token,
|
|
290
|
-
baseDir =
|
|
770
|
+
baseDir = path3.join(tmpdir(), "repo-lint"),
|
|
291
771
|
skipRemote = false
|
|
292
772
|
} = options;
|
|
293
|
-
const dir =
|
|
294
|
-
if (
|
|
773
|
+
const dir = path3.join(baseDir, repo.replace("/", "__"));
|
|
774
|
+
if (existsSync3(dir))
|
|
295
775
|
execSync(`git -C ${dir} pull --quiet`, { stdio: "ignore" });
|
|
296
776
|
else {
|
|
297
777
|
const url = token ? `https://${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
|
|
@@ -315,5 +795,7 @@ export {
|
|
|
315
795
|
LOCAL_CHECKS,
|
|
316
796
|
REMOTE_CHECKS,
|
|
317
797
|
checkLocal,
|
|
318
|
-
checkRepo
|
|
798
|
+
checkRepo,
|
|
799
|
+
detectPackageManager,
|
|
800
|
+
runUpdate
|
|
319
801
|
};
|