@lzear/repo-lint 4.2.2 → 4.3.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 +267 -104
- package/dist/index.d.ts +27 -1
- package/dist/index.js +561 -96
- package/package.json +26 -17
package/dist/index.js
CHANGED
|
@@ -1,149 +1,548 @@
|
|
|
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) => {
|
|
18
78
|
try {
|
|
19
|
-
|
|
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;
|
|
357
|
+
try {
|
|
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 auditCommand = (pm) => {
|
|
427
|
+
switch (pm.name) {
|
|
428
|
+
case "npm":
|
|
429
|
+
return ["npm", "audit", "--omit", "dev", "--audit-level", "high"];
|
|
430
|
+
case "pnpm":
|
|
431
|
+
return ["pnpm", "audit", "--prod", "--audit-level", "high"];
|
|
432
|
+
case "bun":
|
|
433
|
+
return ["bun", "audit"];
|
|
434
|
+
case "yarn":
|
|
435
|
+
return pm.version?.startsWith("1.") ? ["yarn", "audit", "--level", "high"] : [
|
|
436
|
+
"yarn",
|
|
437
|
+
"npm",
|
|
438
|
+
"audit",
|
|
439
|
+
"--environment",
|
|
440
|
+
"production",
|
|
441
|
+
"--severity",
|
|
442
|
+
"high"
|
|
443
|
+
];
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
var DEP_FIELDS = [
|
|
447
|
+
"dependencies",
|
|
448
|
+
"devDependencies",
|
|
449
|
+
"optionalDependencies",
|
|
450
|
+
"peerDependencies"
|
|
451
|
+
];
|
|
452
|
+
var NON_REGISTRY_RANGE_RE = /^(?:workspace:|file:|link:|portal:|catalog:|git|https?:)/;
|
|
453
|
+
var collectEntry = (dependencies, name, range) => {
|
|
454
|
+
if (typeof range !== "string" || NON_REGISTRY_RANGE_RE.test(range)) return;
|
|
455
|
+
const alias = /^npm:(.+)@([^@]+)$/.exec(range);
|
|
456
|
+
const target = alias?.[1] ?? name;
|
|
457
|
+
const targetRange = alias?.[2] ?? range;
|
|
458
|
+
if (!dependencies.has(target)) dependencies.set(target, targetRange);
|
|
459
|
+
};
|
|
460
|
+
var collectFromPackage = (dependencies, packageDir) => {
|
|
461
|
+
const package_ = readPackage(packageDir);
|
|
462
|
+
for (const field of DEP_FIELDS) {
|
|
463
|
+
const section = package_?.[field];
|
|
464
|
+
if (typeof section !== "object" || section === null) continue;
|
|
465
|
+
for (const [name, range] of Object.entries(section))
|
|
466
|
+
collectEntry(dependencies, name, range);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
var collectDependencies = (dir) => {
|
|
470
|
+
const dependencies = /* @__PURE__ */ new Map();
|
|
471
|
+
for (const packageDir of [dir, ...getWorkspaceDirectories(dir)])
|
|
472
|
+
collectFromPackage(dependencies, packageDir);
|
|
473
|
+
return dependencies;
|
|
474
|
+
};
|
|
475
|
+
var findDeprecated = async (name, range) => {
|
|
476
|
+
const response = await fetch(
|
|
477
|
+
`https://registry.npmjs.org/${name.replace("/", "%2F")}`,
|
|
478
|
+
{
|
|
479
|
+
headers: { accept: "application/vnd.npm.install-v1+json" },
|
|
480
|
+
signal: AbortSignal.timeout(1e4)
|
|
481
|
+
}
|
|
482
|
+
);
|
|
483
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
484
|
+
const data = await response.json();
|
|
485
|
+
const latest = data["dist-tags"]?.latest;
|
|
486
|
+
const resolved = latest && satisfies(latest, range) ? latest : maxSatisfying(Object.keys(data.versions), range);
|
|
487
|
+
if (!resolved) return null;
|
|
488
|
+
const deprecated = data.versions[resolved]?.deprecated;
|
|
489
|
+
return deprecated ? `${name}@${resolved} \u2014 ${deprecated.slice(0, 120)}` : null;
|
|
85
490
|
};
|
|
86
491
|
var LOCAL_CHECKS = [
|
|
87
492
|
{
|
|
88
493
|
id: "readme-exists",
|
|
89
|
-
desc: "README
|
|
90
|
-
type: "local",
|
|
91
|
-
check: (dir) => existsSync(path.join(dir, "README.md"))
|
|
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",
|
|
494
|
+
desc: "README",
|
|
103
495
|
type: "local",
|
|
104
|
-
|
|
105
|
-
check: (dir) => readmeIncludes(dir, "project/badge/Coverage/")
|
|
496
|
+
check: (dir) => existsSync2(path2.join(dir, "README.md"))
|
|
106
497
|
},
|
|
107
498
|
{
|
|
108
499
|
id: "readme-npm-badge",
|
|
109
|
-
desc: "
|
|
500
|
+
desc: "npm badge",
|
|
110
501
|
type: "local",
|
|
111
502
|
publishedOnly: true,
|
|
112
503
|
check: (dir) => readmeIncludes(dir, "shields.io/npm/v/")
|
|
113
504
|
},
|
|
114
505
|
{
|
|
115
|
-
id: "codacy
|
|
116
|
-
desc: "
|
|
506
|
+
id: "codacy",
|
|
507
|
+
desc: "Codacy",
|
|
117
508
|
type: "local",
|
|
118
509
|
publishedOnly: true,
|
|
119
|
-
check: (dir) =>
|
|
510
|
+
check: (dir) => {
|
|
511
|
+
const missing = [
|
|
512
|
+
readmeIncludes(dir, "project/badge/Grade/") ? null : "grade badge missing in README",
|
|
513
|
+
readmeIncludes(dir, "project/badge/Coverage/") ? null : "coverage badge missing in README",
|
|
514
|
+
existsSync2(path2.join(dir, ".codacy.yml")) ? null : ".codacy.yml missing"
|
|
515
|
+
].filter((m) => m !== null);
|
|
516
|
+
if (missing.length === 0) return true;
|
|
517
|
+
return { pass: false, detail: missing.join("\n") };
|
|
518
|
+
}
|
|
120
519
|
},
|
|
121
520
|
{
|
|
122
521
|
id: "license",
|
|
123
|
-
desc: "LICENSE
|
|
522
|
+
desc: "LICENSE",
|
|
124
523
|
type: "local",
|
|
125
524
|
publishedOnly: true,
|
|
126
|
-
check: (dir) =>
|
|
525
|
+
check: (dir) => existsSync2(path2.join(dir, "LICENSE"))
|
|
127
526
|
},
|
|
128
527
|
{
|
|
129
528
|
id: "ci-workflow",
|
|
130
|
-
desc: "CI workflow
|
|
529
|
+
desc: "CI workflow",
|
|
131
530
|
type: "local",
|
|
132
531
|
publishedOnly: true,
|
|
133
|
-
check: (dir) =>
|
|
532
|
+
check: (dir) => existsSync2(path2.join(dir, ".github/workflows/ci.yml"))
|
|
134
533
|
},
|
|
135
534
|
{
|
|
136
535
|
id: "renovate",
|
|
137
|
-
desc: "renovate.json
|
|
536
|
+
desc: "renovate.json",
|
|
138
537
|
type: "local",
|
|
139
|
-
check: (dir) =>
|
|
538
|
+
check: (dir) => existsSync2(path2.join(dir, "renovate.json"))
|
|
140
539
|
},
|
|
141
540
|
{
|
|
142
541
|
id: "pkg-publint",
|
|
143
|
-
desc: "publint
|
|
542
|
+
desc: "publint",
|
|
144
543
|
type: "local",
|
|
145
544
|
publishedOnly: true,
|
|
146
|
-
check: (dir) =>
|
|
545
|
+
check: (dir) => eachPublishedPackage(dir, async (pkgDir) => {
|
|
147
546
|
const { messages } = await publint({ pkgDir });
|
|
148
547
|
const failures = messages.filter(
|
|
149
548
|
(m) => m.type === "error" || m.type === "warning"
|
|
@@ -157,13 +556,13 @@ var LOCAL_CHECKS = [
|
|
|
157
556
|
},
|
|
158
557
|
{
|
|
159
558
|
id: "pkg-attw",
|
|
160
|
-
desc: "attw
|
|
559
|
+
desc: "attw",
|
|
161
560
|
type: "local",
|
|
162
561
|
check: (dir) => {
|
|
163
562
|
const bin = findBin("attw", _dirname);
|
|
164
563
|
if (!bin) return { pass: false, detail: "attw not available" };
|
|
165
|
-
return
|
|
166
|
-
const r =
|
|
564
|
+
return eachPublishedPackage(dir, (pkgDir) => {
|
|
565
|
+
const r = spawnSync2(
|
|
167
566
|
process.execPath,
|
|
168
567
|
[bin, "--pack", "--profile", "esm-only"],
|
|
169
568
|
{
|
|
@@ -179,12 +578,12 @@ var LOCAL_CHECKS = [
|
|
|
179
578
|
},
|
|
180
579
|
{
|
|
181
580
|
id: "pkg-knip",
|
|
182
|
-
desc: "knip
|
|
581
|
+
desc: "knip",
|
|
183
582
|
type: "local",
|
|
184
583
|
check: (dir) => {
|
|
185
584
|
const bin = findBin("knip", _dirname);
|
|
186
585
|
if (!bin) return { pass: false, detail: "knip not available" };
|
|
187
|
-
const r =
|
|
586
|
+
const r = spawnSync2(process.execPath, [bin], {
|
|
188
587
|
cwd: dir,
|
|
189
588
|
encoding: "utf8",
|
|
190
589
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -197,28 +596,90 @@ var LOCAL_CHECKS = [
|
|
|
197
596
|
Run: npx knip` };
|
|
198
597
|
}
|
|
199
598
|
},
|
|
599
|
+
{
|
|
600
|
+
id: "monorepo-lint",
|
|
601
|
+
desc: "sherif (workspaces)",
|
|
602
|
+
type: "local",
|
|
603
|
+
check: (dir) => {
|
|
604
|
+
const package_ = readPackage(dir);
|
|
605
|
+
if (!package_ || getWorkspacePatterns(package_).length === 0)
|
|
606
|
+
return { pass: true, detail: "not a monorepo" };
|
|
607
|
+
const bin = findBin("sherif", _dirname);
|
|
608
|
+
if (!bin) return { pass: false, detail: "sherif not available" };
|
|
609
|
+
const r = spawnSync2(process.execPath, [bin], {
|
|
610
|
+
cwd: dir,
|
|
611
|
+
encoding: "utf8",
|
|
612
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
613
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
614
|
+
});
|
|
615
|
+
if (r.status === 0) return true;
|
|
616
|
+
const detail = (r.stdout + r.stderr).trim().split("\n").slice(0, 20).join("\n");
|
|
617
|
+
return { pass: false, detail };
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
id: "deps-audit",
|
|
622
|
+
desc: "audit",
|
|
623
|
+
type: "local",
|
|
624
|
+
check: (dir) => {
|
|
625
|
+
const pm = detectPackageManager(dir);
|
|
626
|
+
const [command, ...arguments_] = auditCommand(pm);
|
|
627
|
+
const r = spawnSync2(command, arguments_, {
|
|
628
|
+
cwd: dir,
|
|
629
|
+
encoding: "utf8",
|
|
630
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
631
|
+
});
|
|
632
|
+
if (r.error) return { pass: false, detail: String(r.error) };
|
|
633
|
+
if (r.status === 0) return true;
|
|
634
|
+
const detail = (r.stdout + r.stderr).trim().split("\n").slice(0, 20).join("\n");
|
|
635
|
+
return { pass: false, detail: detail || `audit exited ${r.status}` };
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
id: "deps-deprecated",
|
|
640
|
+
desc: "no deprecated deps",
|
|
641
|
+
type: "local",
|
|
642
|
+
check: async (dir) => {
|
|
643
|
+
const dependencies = collectDependencies(dir);
|
|
644
|
+
const failures = [];
|
|
645
|
+
await Promise.all(
|
|
646
|
+
[...dependencies].map(async ([name, range]) => {
|
|
647
|
+
try {
|
|
648
|
+
const deprecated = await findDeprecated(name, range);
|
|
649
|
+
if (deprecated) failures.push(deprecated);
|
|
650
|
+
} catch (error) {
|
|
651
|
+
failures.push(`${name} \u2014 check failed: ${String(error)}`);
|
|
652
|
+
}
|
|
653
|
+
})
|
|
654
|
+
);
|
|
655
|
+
if (failures.length === 0) return true;
|
|
656
|
+
return {
|
|
657
|
+
pass: false,
|
|
658
|
+
detail: failures.toSorted((a, b) => a.localeCompare(b)).join("\n")
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
},
|
|
200
662
|
{
|
|
201
663
|
id: "deps-fresh",
|
|
202
|
-
desc: "
|
|
664
|
+
desc: "deps up to date",
|
|
203
665
|
type: "local",
|
|
204
666
|
check: async (dir) => {
|
|
205
|
-
const
|
|
206
|
-
const
|
|
667
|
+
const package_ = readPackage(dir);
|
|
668
|
+
const hasWorkspaces2 = Array.isArray(package_?.workspaces) && package_.workspaces.length > 0;
|
|
207
669
|
try {
|
|
208
|
-
const result = await
|
|
209
|
-
packageFile:
|
|
210
|
-
...
|
|
670
|
+
const result = await ncuRun2({
|
|
671
|
+
packageFile: path2.join(dir, "package.json"),
|
|
672
|
+
...hasWorkspaces2 && { workspaces: true },
|
|
211
673
|
silent: true
|
|
212
674
|
});
|
|
213
|
-
const entries =
|
|
675
|
+
const entries = hasWorkspaces2 ? Object.values(
|
|
214
676
|
result
|
|
215
677
|
).flatMap((x) => Object.entries(x)) : Object.entries(result);
|
|
216
678
|
if (entries.length === 0) return true;
|
|
217
679
|
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
680
|
return { pass: false, detail: `${lines}
|
|
220
681
|
|
|
221
|
-
Run:
|
|
682
|
+
Run: forge update` };
|
|
222
683
|
} catch (error) {
|
|
223
684
|
return { pass: false, detail: String(error) };
|
|
224
685
|
}
|
|
@@ -227,7 +688,7 @@ Run: ${cmd}` };
|
|
|
227
688
|
];
|
|
228
689
|
var listSecrets = (repo) => {
|
|
229
690
|
try {
|
|
230
|
-
const result =
|
|
691
|
+
const result = spawnSync2(
|
|
231
692
|
"gh",
|
|
232
693
|
["secret", "list", "--repo", repo, "--json", "name"],
|
|
233
694
|
{
|
|
@@ -244,7 +705,7 @@ var listSecrets = (repo) => {
|
|
|
244
705
|
var REMOTE_CHECKS = [
|
|
245
706
|
{
|
|
246
707
|
id: "secret-codacy-token",
|
|
247
|
-
desc: "
|
|
708
|
+
desc: "Codacy secret",
|
|
248
709
|
type: "remote",
|
|
249
710
|
publishedOnly: true,
|
|
250
711
|
check: (repo) => {
|
|
@@ -265,19 +726,21 @@ var checkLocal = async (options = {}) => {
|
|
|
265
726
|
stdio: ["ignore", "pipe", "ignore"]
|
|
266
727
|
}).trim();
|
|
267
728
|
const match = /github\.com[:/](.+?)(?:\.git)?$/.exec(remote);
|
|
268
|
-
return match?.[1] ??
|
|
729
|
+
return match?.[1] ?? path3.basename(dir);
|
|
269
730
|
} catch {
|
|
270
731
|
return dir;
|
|
271
732
|
}
|
|
272
733
|
})();
|
|
273
|
-
const
|
|
734
|
+
const isPublished = hasPublishedPkg(dir);
|
|
274
735
|
const localResults = await Promise.all(
|
|
275
|
-
LOCAL_CHECKS.filter((c) => !c.publishedOnly ||
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
736
|
+
LOCAL_CHECKS.filter((c) => !c.publishedOnly || isPublished).map(
|
|
737
|
+
async (c) => {
|
|
738
|
+
const raw = await c.check(dir);
|
|
739
|
+
return typeof raw === "boolean" ? { id: c.id, desc: c.desc, pass: raw } : { id: c.id, desc: c.desc, ...raw };
|
|
740
|
+
}
|
|
741
|
+
)
|
|
279
742
|
);
|
|
280
|
-
const remoteResults = skipRemote ? [] : REMOTE_CHECKS.filter((c) => !c.publishedOnly ||
|
|
743
|
+
const remoteResults = skipRemote ? [] : REMOTE_CHECKS.filter((c) => !c.publishedOnly || isPublished).map((c) => ({
|
|
281
744
|
id: c.id,
|
|
282
745
|
desc: c.desc,
|
|
283
746
|
pass: c.check(repo)
|
|
@@ -287,11 +750,11 @@ var checkLocal = async (options = {}) => {
|
|
|
287
750
|
var checkRepo = async (repo, options = {}) => {
|
|
288
751
|
const {
|
|
289
752
|
token,
|
|
290
|
-
baseDir =
|
|
753
|
+
baseDir = path3.join(tmpdir(), "repo-lint"),
|
|
291
754
|
skipRemote = false
|
|
292
755
|
} = options;
|
|
293
|
-
const dir =
|
|
294
|
-
if (
|
|
756
|
+
const dir = path3.join(baseDir, repo.replace("/", "__"));
|
|
757
|
+
if (existsSync3(dir))
|
|
295
758
|
execSync(`git -C ${dir} pull --quiet`, { stdio: "ignore" });
|
|
296
759
|
else {
|
|
297
760
|
const url = token ? `https://${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
|
|
@@ -315,5 +778,7 @@ export {
|
|
|
315
778
|
LOCAL_CHECKS,
|
|
316
779
|
REMOTE_CHECKS,
|
|
317
780
|
checkLocal,
|
|
318
|
-
checkRepo
|
|
781
|
+
checkRepo,
|
|
782
|
+
detectPackageManager,
|
|
783
|
+
runUpdate
|
|
319
784
|
};
|