@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/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@lzear/repo-lint)
|
|
4
4
|
[](../../LICENSE)
|
|
5
5
|
|
|
6
|
-
Checks lzear repos against forge standards. Used internally by `forge check`.
|
|
6
|
+
Checks lzear repos against forge standards and keeps them fresh. Used internally by `forge check` and `forge update`.
|
|
7
7
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
@@ -58,24 +58,41 @@ interface RepoReport {
|
|
|
58
58
|
}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### `runUpdate(options?)`
|
|
62
|
+
|
|
63
|
+
Powers `forge update`. Detects the package manager (npm, yarn, pnpm, bun) and updates dependency ranges (ncu, honoring `.ncurc{,.json,.js,.cjs,.mjs}`), the `packageManager` field, `.nvmrc`/`.node-version` (latest Node LTS), and `.bun-version`, then installs & refreshes the lockfile.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { detectPackageManager, runUpdate } from '@lzear/repo-lint'
|
|
67
|
+
|
|
68
|
+
const report = await runUpdate({ dry: true })
|
|
69
|
+
// { dir, packageManager: { name: 'yarn', version: '4.17.1', … }, results: [...] }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
| Option | Type | Default | Description |
|
|
73
|
+
|-----------|-----------|-----------------|------------------------------------|
|
|
74
|
+
| `dir` | `string` | `process.cwd()` | Repo to update |
|
|
75
|
+
| `dry` | `boolean` | `false` | Report changes without writing |
|
|
76
|
+
| `install` | `boolean` | `true` | Run install/lockfile refresh after |
|
|
77
|
+
|
|
61
78
|
## Checks performed
|
|
62
79
|
|
|
63
|
-
| Check
|
|
64
|
-
|
|
65
|
-
| `readme-exists`
|
|
66
|
-
| `readme-npm-badge`
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `pkg-
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `deps-
|
|
77
|
-
| `
|
|
78
|
-
| `secret-codacy-token`
|
|
80
|
+
| Check | Description |
|
|
81
|
+
|-----------------------|-----------------------------------------------------------|
|
|
82
|
+
| `readme-exists` | `README.md` exists |
|
|
83
|
+
| `readme-npm-badge` | README has an npm badge |
|
|
84
|
+
| `codacy` | Codacy grade & coverage badges in README, `.codacy.yml` |
|
|
85
|
+
| `license` | `LICENSE` exists |
|
|
86
|
+
| `ci-workflow` | a workflow calls the forge reusable CI workflow |
|
|
87
|
+
| `renovate` | `renovate.json` exists |
|
|
88
|
+
| `pkg-publint` | All published packages pass `publint` |
|
|
89
|
+
| `pkg-attw` | All published packages pass `attw` (ESM-only profile) |
|
|
90
|
+
| `pkg-knip` | No unused exports or dependencies (`knip`) |
|
|
91
|
+
| `monorepo-lint` | Workspace consistency (`sherif`, monorepos only) |
|
|
92
|
+
| `deps-audit` | No known vulnerabilities (PM-native `audit`, prod, high+) |
|
|
93
|
+
| `deps-deprecated` | No direct dependency resolves to a deprecated version |
|
|
94
|
+
| `deps-fresh` | All dependencies up to date (`ncu`) |
|
|
95
|
+
| `secret-codacy-token` | GitHub secret `CODACY_PROJECT_TOKEN` is set |
|
|
79
96
|
|
|
80
97
|
## Part of forge
|
|
81
98
|
|
package/dist/bin.js
CHANGED
|
@@ -7,150 +7,265 @@ import { parseArgs } from "util";
|
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
9
|
import { execSync } from "child_process";
|
|
10
|
-
import { existsSync as
|
|
10
|
+
import { existsSync as existsSync3 } from "fs";
|
|
11
11
|
import { tmpdir } from "os";
|
|
12
|
-
import
|
|
12
|
+
import path3 from "path";
|
|
13
13
|
|
|
14
14
|
// src/checks.ts
|
|
15
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
16
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
17
|
+
import path2 from "path";
|
|
18
|
+
import { fileURLToPath } from "url";
|
|
19
|
+
import { run as ncuRun2 } from "npm-check-updates";
|
|
20
|
+
import { publint } from "publint";
|
|
21
|
+
import { maxSatisfying, satisfies } from "semver";
|
|
22
|
+
|
|
23
|
+
// src/update.ts
|
|
15
24
|
import { spawnSync } from "child_process";
|
|
16
|
-
import { existsSync,
|
|
25
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
17
26
|
import path from "path";
|
|
18
|
-
import {
|
|
27
|
+
import { pathToFileURL } from "url";
|
|
19
28
|
import { run as ncuRun } from "npm-check-updates";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
var LOCKFILES = [
|
|
30
|
+
["bun.lock", "bun"],
|
|
31
|
+
["bun.lockb", "bun"],
|
|
32
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
33
|
+
["yarn.lock", "yarn"],
|
|
34
|
+
["package-lock.json", "npm"]
|
|
35
|
+
];
|
|
36
|
+
var PM_FIELD_RE = /^(npm|yarn|pnpm|bun)@([^+\s]+)/;
|
|
37
|
+
var detectPackageManager = (dir) => {
|
|
38
|
+
const packageFile = path.join(dir, "package.json");
|
|
39
|
+
if (existsSync(packageFile))
|
|
40
|
+
try {
|
|
41
|
+
const package_ = JSON.parse(readFileSync(packageFile, "utf8"));
|
|
42
|
+
if (typeof package_.packageManager === "string") {
|
|
43
|
+
const match = PM_FIELD_RE.exec(package_.packageManager);
|
|
44
|
+
if (match?.[1] && match[2])
|
|
45
|
+
return {
|
|
46
|
+
name: match[1],
|
|
47
|
+
version: match[2],
|
|
48
|
+
source: "packageManager"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
for (const [file, name] of LOCKFILES)
|
|
54
|
+
if (existsSync(path.join(dir, file))) return { name, source: "lockfile" };
|
|
55
|
+
return { name: "npm", source: "default" };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/checks.ts
|
|
59
|
+
var _dirname = path2.dirname(fileURLToPath(import.meta.url));
|
|
60
|
+
var readPackage = (dir) => {
|
|
61
|
+
const f = path2.join(dir, "package.json");
|
|
62
|
+
if (!existsSync2(f)) return null;
|
|
25
63
|
try {
|
|
26
|
-
return JSON.parse(
|
|
64
|
+
return JSON.parse(readFileSync2(f, "utf8"));
|
|
27
65
|
} catch {
|
|
28
66
|
return null;
|
|
29
67
|
}
|
|
30
68
|
};
|
|
31
|
-
var getWorkspacePatterns = (
|
|
32
|
-
const ws =
|
|
69
|
+
var getWorkspacePatterns = (package_) => {
|
|
70
|
+
const ws = package_.workspaces;
|
|
33
71
|
if (Array.isArray(ws)) return ws;
|
|
34
|
-
const
|
|
35
|
-
if (Array.isArray(
|
|
72
|
+
const wsPackage = ws;
|
|
73
|
+
if (Array.isArray(wsPackage?.packages)) return wsPackage.packages;
|
|
36
74
|
return [];
|
|
37
75
|
};
|
|
38
76
|
var patternToBase = (rootDir, pattern) => {
|
|
39
77
|
const parts = pattern.split("/");
|
|
40
78
|
if (parts.length === 2 && parts[1] === "*")
|
|
41
|
-
return
|
|
79
|
+
return path2.join(rootDir, parts[0] ?? "");
|
|
42
80
|
if (parts.length === 1 && parts[0] === "*") return rootDir;
|
|
43
81
|
return null;
|
|
44
82
|
};
|
|
45
|
-
var
|
|
46
|
-
const
|
|
47
|
-
if (!
|
|
48
|
-
const
|
|
49
|
-
for (const pattern of getWorkspacePatterns(
|
|
83
|
+
var getWorkspaceDirectories = (rootDir) => {
|
|
84
|
+
const package_ = readPackage(rootDir);
|
|
85
|
+
if (!package_) return [];
|
|
86
|
+
const directories = [];
|
|
87
|
+
for (const pattern of getWorkspacePatterns(package_)) {
|
|
50
88
|
const base = patternToBase(rootDir, pattern);
|
|
51
|
-
if (!base || !
|
|
52
|
-
|
|
53
|
-
|
|
89
|
+
if (!base || !existsSync2(base)) continue;
|
|
90
|
+
const entries = readdirSync(base, { withFileTypes: true });
|
|
91
|
+
for (const entry of entries)
|
|
92
|
+
if (entry.isDirectory()) directories.push(path2.join(base, entry.name));
|
|
54
93
|
}
|
|
55
|
-
return
|
|
94
|
+
return directories;
|
|
56
95
|
};
|
|
57
96
|
var findBin = (name, startDir) => {
|
|
58
97
|
let dir = startDir;
|
|
59
98
|
while (true) {
|
|
60
|
-
const bin =
|
|
61
|
-
if (
|
|
62
|
-
const parent =
|
|
99
|
+
const bin = path2.join(dir, "node_modules", ".bin", name);
|
|
100
|
+
if (existsSync2(bin)) return bin;
|
|
101
|
+
const parent = path2.join(dir, "..");
|
|
63
102
|
if (parent === dir) return null;
|
|
64
103
|
dir = parent;
|
|
65
104
|
}
|
|
66
105
|
};
|
|
67
|
-
var
|
|
68
|
-
const
|
|
69
|
-
if (!
|
|
70
|
-
if (
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
106
|
+
var eachPublishedPackage = async (dir, function_) => {
|
|
107
|
+
const package_ = readPackage(dir);
|
|
108
|
+
if (!package_) return { pass: true };
|
|
109
|
+
if (package_.private === true) {
|
|
110
|
+
const wsDirectories = getWorkspaceDirectories(dir);
|
|
111
|
+
if (wsDirectories.length === 0) return { pass: true };
|
|
73
112
|
const results = await Promise.all(
|
|
74
|
-
|
|
113
|
+
wsDirectories.map((d) => eachPublishedPackage(d, function_))
|
|
75
114
|
);
|
|
76
115
|
const failures = results.filter((r) => !r.pass);
|
|
77
116
|
if (failures.length === 0) return { pass: true };
|
|
78
117
|
const detail = failures.flatMap((r) => r.detail ? [r.detail] : []).join("\n");
|
|
79
118
|
return { pass: false, ...detail && { detail } };
|
|
80
119
|
}
|
|
81
|
-
return
|
|
120
|
+
return function_(dir);
|
|
82
121
|
};
|
|
83
122
|
var hasPublishedPkg = (dir) => {
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
86
|
-
if (
|
|
87
|
-
return
|
|
123
|
+
const package_ = readPackage(dir);
|
|
124
|
+
if (!package_) return false;
|
|
125
|
+
if (package_.private !== true) return true;
|
|
126
|
+
return getWorkspaceDirectories(dir).some((d) => hasPublishedPkg(d));
|
|
88
127
|
};
|
|
89
128
|
var readmeIncludes = (dir, needle) => {
|
|
90
|
-
const f =
|
|
91
|
-
return
|
|
129
|
+
const f = path2.join(dir, "README.md");
|
|
130
|
+
return existsSync2(f) && readFileSync2(f, "utf8").includes(needle);
|
|
131
|
+
};
|
|
132
|
+
var FORGE_WORKFLOW_RE = /uses:\s*lzear\/forge\/\.github\/workflows\/ci\.yml@/;
|
|
133
|
+
var isForgeRepo = (dir) => getWorkspaceDirectories(dir).some(
|
|
134
|
+
(d) => readPackage(d)?.name === "@lzear/forge"
|
|
135
|
+
);
|
|
136
|
+
var callsForgeWorkflow = (dir) => {
|
|
137
|
+
const wfDir = path2.join(dir, ".github", "workflows");
|
|
138
|
+
if (!existsSync2(wfDir)) return false;
|
|
139
|
+
return readdirSync(wfDir).filter((f) => /\.ya?ml$/.test(f)).some(
|
|
140
|
+
(f) => FORGE_WORKFLOW_RE.test(readFileSync2(path2.join(wfDir, f), "utf8"))
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
var auditCommand = (pm) => {
|
|
144
|
+
switch (pm.name) {
|
|
145
|
+
case "npm":
|
|
146
|
+
return ["npm", "audit", "--omit", "dev", "--audit-level", "high"];
|
|
147
|
+
case "pnpm":
|
|
148
|
+
return ["pnpm", "audit", "--prod", "--audit-level", "high"];
|
|
149
|
+
case "bun":
|
|
150
|
+
return ["bun", "audit"];
|
|
151
|
+
case "yarn":
|
|
152
|
+
return pm.version?.startsWith("1.") ? ["yarn", "audit", "--level", "high"] : [
|
|
153
|
+
"yarn",
|
|
154
|
+
"npm",
|
|
155
|
+
"audit",
|
|
156
|
+
"--environment",
|
|
157
|
+
"production",
|
|
158
|
+
"--severity",
|
|
159
|
+
"high"
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
var DEP_FIELDS = [
|
|
164
|
+
"dependencies",
|
|
165
|
+
"devDependencies",
|
|
166
|
+
"optionalDependencies",
|
|
167
|
+
"peerDependencies"
|
|
168
|
+
];
|
|
169
|
+
var NON_REGISTRY_RANGE_RE = /^(?:workspace:|file:|link:|portal:|catalog:|git|https?:)/;
|
|
170
|
+
var collectEntry = (dependencies, name, range) => {
|
|
171
|
+
if (typeof range !== "string" || NON_REGISTRY_RANGE_RE.test(range)) return;
|
|
172
|
+
const alias = /^npm:(.+)@([^@]+)$/.exec(range);
|
|
173
|
+
const target = alias?.[1] ?? name;
|
|
174
|
+
const targetRange = alias?.[2] ?? range;
|
|
175
|
+
if (!dependencies.has(target)) dependencies.set(target, targetRange);
|
|
176
|
+
};
|
|
177
|
+
var collectFromPackage = (dependencies, packageDir) => {
|
|
178
|
+
const package_ = readPackage(packageDir);
|
|
179
|
+
for (const field of DEP_FIELDS) {
|
|
180
|
+
const section = package_?.[field];
|
|
181
|
+
if (typeof section !== "object" || section === null) continue;
|
|
182
|
+
for (const [name, range] of Object.entries(section))
|
|
183
|
+
collectEntry(dependencies, name, range);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
var collectDependencies = (dir) => {
|
|
187
|
+
const dependencies = /* @__PURE__ */ new Map();
|
|
188
|
+
for (const packageDir of [dir, ...getWorkspaceDirectories(dir)])
|
|
189
|
+
collectFromPackage(dependencies, packageDir);
|
|
190
|
+
return dependencies;
|
|
191
|
+
};
|
|
192
|
+
var findDeprecated = async (name, range) => {
|
|
193
|
+
const response = await fetch(
|
|
194
|
+
`https://registry.npmjs.org/${name.replace("/", "%2F")}`,
|
|
195
|
+
{
|
|
196
|
+
headers: { accept: "application/vnd.npm.install-v1+json" },
|
|
197
|
+
signal: AbortSignal.timeout(1e4)
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
201
|
+
const data = await response.json();
|
|
202
|
+
const latest = data["dist-tags"]?.latest;
|
|
203
|
+
const resolved = latest && satisfies(latest, range) ? latest : maxSatisfying(Object.keys(data.versions), range);
|
|
204
|
+
if (!resolved) return null;
|
|
205
|
+
const deprecated = data.versions[resolved]?.deprecated;
|
|
206
|
+
return deprecated ? `${name}@${resolved} \u2014 ${deprecated.slice(0, 120)}` : null;
|
|
92
207
|
};
|
|
93
208
|
var LOCAL_CHECKS = [
|
|
94
209
|
{
|
|
95
210
|
id: "readme-exists",
|
|
96
|
-
desc: "README
|
|
97
|
-
type: "local",
|
|
98
|
-
check: (dir) => existsSync(path.join(dir, "README.md"))
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
id: "readme-codacy-grade-badge",
|
|
102
|
-
desc: "README has Codacy grade badge",
|
|
211
|
+
desc: "README",
|
|
103
212
|
type: "local",
|
|
104
|
-
|
|
105
|
-
check: (dir) => readmeIncludes(dir, "project/badge/Grade/")
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
id: "readme-codacy-coverage-badge",
|
|
109
|
-
desc: "README has Codacy coverage badge",
|
|
110
|
-
type: "local",
|
|
111
|
-
publishedOnly: true,
|
|
112
|
-
check: (dir) => readmeIncludes(dir, "project/badge/Coverage/")
|
|
213
|
+
check: (dir) => existsSync2(path2.join(dir, "README.md"))
|
|
113
214
|
},
|
|
114
215
|
{
|
|
115
216
|
id: "readme-npm-badge",
|
|
116
|
-
desc: "
|
|
217
|
+
desc: "npm badge",
|
|
117
218
|
type: "local",
|
|
118
219
|
publishedOnly: true,
|
|
119
220
|
check: (dir) => readmeIncludes(dir, "shields.io/npm/v/")
|
|
120
221
|
},
|
|
121
222
|
{
|
|
122
|
-
id: "codacy
|
|
123
|
-
desc: "
|
|
223
|
+
id: "codacy",
|
|
224
|
+
desc: "Codacy",
|
|
124
225
|
type: "local",
|
|
125
226
|
publishedOnly: true,
|
|
126
|
-
check: (dir) =>
|
|
227
|
+
check: (dir) => {
|
|
228
|
+
const missing = [
|
|
229
|
+
readmeIncludes(dir, "project/badge/Grade/") ? null : "grade badge missing in README",
|
|
230
|
+
readmeIncludes(dir, "project/badge/Coverage/") ? null : "coverage badge missing in README",
|
|
231
|
+
existsSync2(path2.join(dir, ".codacy.yml")) ? null : ".codacy.yml missing"
|
|
232
|
+
].filter((m) => m !== null);
|
|
233
|
+
if (missing.length === 0) return true;
|
|
234
|
+
return { pass: false, detail: missing.join("\n") };
|
|
235
|
+
}
|
|
127
236
|
},
|
|
128
237
|
{
|
|
129
238
|
id: "license",
|
|
130
|
-
desc: "LICENSE
|
|
239
|
+
desc: "LICENSE",
|
|
131
240
|
type: "local",
|
|
132
241
|
publishedOnly: true,
|
|
133
|
-
check: (dir) =>
|
|
242
|
+
check: (dir) => existsSync2(path2.join(dir, "LICENSE"))
|
|
134
243
|
},
|
|
135
244
|
{
|
|
136
245
|
id: "ci-workflow",
|
|
137
|
-
desc: "CI workflow
|
|
246
|
+
desc: "CI calls forge workflow",
|
|
138
247
|
type: "local",
|
|
139
248
|
publishedOnly: true,
|
|
140
|
-
check: (dir) =>
|
|
249
|
+
check: (dir) => {
|
|
250
|
+
if (callsForgeWorkflow(dir) || isForgeRepo(dir)) return true;
|
|
251
|
+
return {
|
|
252
|
+
pass: false,
|
|
253
|
+
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)"
|
|
254
|
+
};
|
|
255
|
+
}
|
|
141
256
|
},
|
|
142
257
|
{
|
|
143
258
|
id: "renovate",
|
|
144
|
-
desc: "renovate.json
|
|
259
|
+
desc: "renovate.json",
|
|
145
260
|
type: "local",
|
|
146
|
-
check: (dir) =>
|
|
261
|
+
check: (dir) => existsSync2(path2.join(dir, "renovate.json"))
|
|
147
262
|
},
|
|
148
263
|
{
|
|
149
264
|
id: "pkg-publint",
|
|
150
|
-
desc: "publint
|
|
265
|
+
desc: "publint",
|
|
151
266
|
type: "local",
|
|
152
267
|
publishedOnly: true,
|
|
153
|
-
check: (dir) =>
|
|
268
|
+
check: (dir) => eachPublishedPackage(dir, async (pkgDir) => {
|
|
154
269
|
const { messages } = await publint({ pkgDir });
|
|
155
270
|
const failures = messages.filter(
|
|
156
271
|
(m) => m.type === "error" || m.type === "warning"
|
|
@@ -164,13 +279,13 @@ var LOCAL_CHECKS = [
|
|
|
164
279
|
},
|
|
165
280
|
{
|
|
166
281
|
id: "pkg-attw",
|
|
167
|
-
desc: "attw
|
|
282
|
+
desc: "attw",
|
|
168
283
|
type: "local",
|
|
169
284
|
check: (dir) => {
|
|
170
285
|
const bin = findBin("attw", _dirname);
|
|
171
286
|
if (!bin) return { pass: false, detail: "attw not available" };
|
|
172
|
-
return
|
|
173
|
-
const r =
|
|
287
|
+
return eachPublishedPackage(dir, (pkgDir) => {
|
|
288
|
+
const r = spawnSync2(
|
|
174
289
|
process.execPath,
|
|
175
290
|
[bin, "--pack", "--profile", "esm-only"],
|
|
176
291
|
{
|
|
@@ -186,12 +301,12 @@ var LOCAL_CHECKS = [
|
|
|
186
301
|
},
|
|
187
302
|
{
|
|
188
303
|
id: "pkg-knip",
|
|
189
|
-
desc: "knip
|
|
304
|
+
desc: "knip",
|
|
190
305
|
type: "local",
|
|
191
306
|
check: (dir) => {
|
|
192
307
|
const bin = findBin("knip", _dirname);
|
|
193
308
|
if (!bin) return { pass: false, detail: "knip not available" };
|
|
194
|
-
const r =
|
|
309
|
+
const r = spawnSync2(process.execPath, [bin], {
|
|
195
310
|
cwd: dir,
|
|
196
311
|
encoding: "utf8",
|
|
197
312
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -204,17 +319,80 @@ var LOCAL_CHECKS = [
|
|
|
204
319
|
Run: npx knip` };
|
|
205
320
|
}
|
|
206
321
|
},
|
|
322
|
+
{
|
|
323
|
+
id: "monorepo-lint",
|
|
324
|
+
desc: "sherif (workspaces)",
|
|
325
|
+
type: "local",
|
|
326
|
+
check: (dir) => {
|
|
327
|
+
const package_ = readPackage(dir);
|
|
328
|
+
if (!package_ || getWorkspacePatterns(package_).length === 0)
|
|
329
|
+
return { pass: true, detail: "not a monorepo" };
|
|
330
|
+
const bin = findBin("sherif", _dirname);
|
|
331
|
+
if (!bin) return { pass: false, detail: "sherif not available" };
|
|
332
|
+
const r = spawnSync2(process.execPath, [bin], {
|
|
333
|
+
cwd: dir,
|
|
334
|
+
encoding: "utf8",
|
|
335
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
336
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
337
|
+
});
|
|
338
|
+
if (r.status === 0) return true;
|
|
339
|
+
const detail = (r.stdout + r.stderr).trim().split("\n").slice(0, 20).join("\n");
|
|
340
|
+
return { pass: false, detail };
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
id: "deps-audit",
|
|
345
|
+
desc: "audit",
|
|
346
|
+
type: "local",
|
|
347
|
+
check: (dir) => {
|
|
348
|
+
const pm = detectPackageManager(dir);
|
|
349
|
+
const [command, ...arguments_] = auditCommand(pm);
|
|
350
|
+
const r = spawnSync2(command, arguments_, {
|
|
351
|
+
cwd: dir,
|
|
352
|
+
encoding: "utf8",
|
|
353
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
354
|
+
});
|
|
355
|
+
if (r.error) return { pass: false, detail: String(r.error) };
|
|
356
|
+
if (r.status === 0) return true;
|
|
357
|
+
const detail = (r.stdout + r.stderr).trim().split("\n").slice(0, 20).join("\n");
|
|
358
|
+
return { pass: false, detail: detail || `audit exited ${r.status}` };
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
id: "deps-deprecated",
|
|
363
|
+
desc: "no deprecated deps",
|
|
364
|
+
type: "local",
|
|
365
|
+
check: async (dir) => {
|
|
366
|
+
const dependencies = collectDependencies(dir);
|
|
367
|
+
const failures = [];
|
|
368
|
+
await Promise.all(
|
|
369
|
+
[...dependencies].map(async ([name, range]) => {
|
|
370
|
+
try {
|
|
371
|
+
const deprecated = await findDeprecated(name, range);
|
|
372
|
+
if (deprecated) failures.push(deprecated);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
failures.push(`${name} \u2014 check failed: ${String(error)}`);
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
);
|
|
378
|
+
if (failures.length === 0) return true;
|
|
379
|
+
return {
|
|
380
|
+
pass: false,
|
|
381
|
+
detail: failures.toSorted((a, b) => a.localeCompare(b)).join("\n")
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
},
|
|
207
385
|
{
|
|
208
386
|
id: "deps-fresh",
|
|
209
|
-
desc: "
|
|
387
|
+
desc: "deps up to date",
|
|
210
388
|
type: "local",
|
|
211
389
|
check: async (dir) => {
|
|
212
|
-
const
|
|
213
|
-
const hasWorkspaces = Array.isArray(
|
|
390
|
+
const package_ = readPackage(dir);
|
|
391
|
+
const hasWorkspaces = Array.isArray(package_?.workspaces) && package_.workspaces.length > 0;
|
|
214
392
|
try {
|
|
215
|
-
const result = await
|
|
216
|
-
packageFile:
|
|
217
|
-
...hasWorkspaces
|
|
393
|
+
const result = await ncuRun2({
|
|
394
|
+
packageFile: path2.join(dir, "package.json"),
|
|
395
|
+
...hasWorkspaces && { workspaces: true },
|
|
218
396
|
silent: true
|
|
219
397
|
});
|
|
220
398
|
const entries = hasWorkspaces ? Object.values(
|
|
@@ -222,10 +400,9 @@ Run: npx knip` };
|
|
|
222
400
|
).flatMap((x) => Object.entries(x)) : Object.entries(result);
|
|
223
401
|
if (entries.length === 0) return true;
|
|
224
402
|
const lines = entries.map(([k, v]) => `${k} \u2192 ${v}`).join("\n");
|
|
225
|
-
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";
|
|
226
403
|
return { pass: false, detail: `${lines}
|
|
227
404
|
|
|
228
|
-
Run:
|
|
405
|
+
Run: forge update` };
|
|
229
406
|
} catch (error) {
|
|
230
407
|
return { pass: false, detail: String(error) };
|
|
231
408
|
}
|
|
@@ -234,7 +411,7 @@ Run: ${cmd}` };
|
|
|
234
411
|
];
|
|
235
412
|
var listSecrets = (repo) => {
|
|
236
413
|
try {
|
|
237
|
-
const result =
|
|
414
|
+
const result = spawnSync2(
|
|
238
415
|
"gh",
|
|
239
416
|
["secret", "list", "--repo", repo, "--json", "name"],
|
|
240
417
|
{
|
|
@@ -251,7 +428,7 @@ var listSecrets = (repo) => {
|
|
|
251
428
|
var REMOTE_CHECKS = [
|
|
252
429
|
{
|
|
253
430
|
id: "secret-codacy-token",
|
|
254
|
-
desc: "
|
|
431
|
+
desc: "Codacy secret",
|
|
255
432
|
type: "remote",
|
|
256
433
|
publishedOnly: true,
|
|
257
434
|
check: (repo) => {
|
|
@@ -272,19 +449,21 @@ var checkLocal = async (options = {}) => {
|
|
|
272
449
|
stdio: ["ignore", "pipe", "ignore"]
|
|
273
450
|
}).trim();
|
|
274
451
|
const match = /github\.com[:/](.+?)(?:\.git)?$/.exec(remote);
|
|
275
|
-
return match?.[1] ??
|
|
452
|
+
return match?.[1] ?? path3.basename(dir);
|
|
276
453
|
} catch {
|
|
277
454
|
return dir;
|
|
278
455
|
}
|
|
279
456
|
})();
|
|
280
|
-
const
|
|
457
|
+
const isPublished = hasPublishedPkg(dir);
|
|
281
458
|
const localResults = await Promise.all(
|
|
282
|
-
LOCAL_CHECKS.filter((c) => !c.publishedOnly ||
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
459
|
+
LOCAL_CHECKS.filter((c) => !c.publishedOnly || isPublished).map(
|
|
460
|
+
async (c) => {
|
|
461
|
+
const raw = await c.check(dir);
|
|
462
|
+
return typeof raw === "boolean" ? { id: c.id, desc: c.desc, pass: raw } : { id: c.id, desc: c.desc, ...raw };
|
|
463
|
+
}
|
|
464
|
+
)
|
|
286
465
|
);
|
|
287
|
-
const remoteResults = skipRemote ? [] : REMOTE_CHECKS.filter((c) => !c.publishedOnly ||
|
|
466
|
+
const remoteResults = skipRemote ? [] : REMOTE_CHECKS.filter((c) => !c.publishedOnly || isPublished).map((c) => ({
|
|
288
467
|
id: c.id,
|
|
289
468
|
desc: c.desc,
|
|
290
469
|
pass: c.check(repo)
|
|
@@ -294,11 +473,11 @@ var checkLocal = async (options = {}) => {
|
|
|
294
473
|
var checkRepo = async (repo, options = {}) => {
|
|
295
474
|
const {
|
|
296
475
|
token,
|
|
297
|
-
baseDir =
|
|
476
|
+
baseDir = path3.join(tmpdir(), "repo-lint"),
|
|
298
477
|
skipRemote = false
|
|
299
478
|
} = options;
|
|
300
|
-
const dir =
|
|
301
|
-
if (
|
|
479
|
+
const dir = path3.join(baseDir, repo.replace("/", "__"));
|
|
480
|
+
if (existsSync3(dir))
|
|
302
481
|
execSync(`git -C ${dir} pull --quiet`, { stdio: "ignore" });
|
|
303
482
|
else {
|
|
304
483
|
const url = token ? `https://${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
|
|
@@ -329,24 +508,24 @@ var { values } = parseArgs({
|
|
|
329
508
|
}
|
|
330
509
|
});
|
|
331
510
|
var printReport = (result) => {
|
|
332
|
-
let
|
|
511
|
+
let isAnyFail = false;
|
|
333
512
|
const pass = result.results.filter((r) => r.pass).length;
|
|
334
513
|
const total = result.results.length;
|
|
335
514
|
for (const r of result.results) {
|
|
336
515
|
console.log(` ${r.pass ? "\u2713" : "\u2717"} ${r.desc}`);
|
|
337
|
-
if (!r.pass)
|
|
516
|
+
if (!r.pass) isAnyFail = true;
|
|
338
517
|
}
|
|
339
518
|
console.log(`
|
|
340
519
|
${pass}/${total} checks passed`);
|
|
341
|
-
return
|
|
520
|
+
return isAnyFail;
|
|
342
521
|
};
|
|
343
522
|
var main = async () => {
|
|
344
|
-
let
|
|
523
|
+
let isAnyFail = false;
|
|
345
524
|
if (values.local) {
|
|
346
525
|
console.log(`
|
|
347
526
|
\u2500\u2500 ${process.cwd()}`);
|
|
348
527
|
const result = await checkLocal({ skipRemote: values["skip-remote"] });
|
|
349
|
-
|
|
528
|
+
isAnyFail = printReport(result);
|
|
350
529
|
} else {
|
|
351
530
|
if (!values.repos) {
|
|
352
531
|
console.error(
|
|
@@ -355,7 +534,8 @@ var main = async () => {
|
|
|
355
534
|
process.exit(1);
|
|
356
535
|
}
|
|
357
536
|
mkdirSync(values.dir, { recursive: true });
|
|
358
|
-
|
|
537
|
+
const repos = values.repos.split(",").map((r) => r.trim());
|
|
538
|
+
for (const repo of repos) {
|
|
359
539
|
console.log(`
|
|
360
540
|
\u2500\u2500 ${repo}`);
|
|
361
541
|
try {
|
|
@@ -364,13 +544,13 @@ var main = async () => {
|
|
|
364
544
|
baseDir: values.dir,
|
|
365
545
|
skipRemote: values["skip-remote"]
|
|
366
546
|
});
|
|
367
|
-
if (printReport(result))
|
|
547
|
+
if (printReport(result)) isAnyFail = true;
|
|
368
548
|
} catch {
|
|
369
549
|
console.log(" \u2717 could not clone repo");
|
|
370
|
-
|
|
550
|
+
isAnyFail = true;
|
|
371
551
|
}
|
|
372
552
|
}
|
|
373
553
|
}
|
|
374
|
-
process.exit(
|
|
554
|
+
process.exit(isAnyFail ? 1 : 0);
|
|
375
555
|
};
|
|
376
|
-
|
|
556
|
+
await main();
|