@lzear/repo-lint 4.2.1 → 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 -114
- package/dist/index.d.ts +27 -1
- package/dist/index.js +561 -106
- 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` | `.github/workflows/ci.yml` exists |
|
|
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,248 @@ 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 auditCommand = (pm) => {
|
|
133
|
+
switch (pm.name) {
|
|
134
|
+
case "npm":
|
|
135
|
+
return ["npm", "audit", "--omit", "dev", "--audit-level", "high"];
|
|
136
|
+
case "pnpm":
|
|
137
|
+
return ["pnpm", "audit", "--prod", "--audit-level", "high"];
|
|
138
|
+
case "bun":
|
|
139
|
+
return ["bun", "audit"];
|
|
140
|
+
case "yarn":
|
|
141
|
+
return pm.version?.startsWith("1.") ? ["yarn", "audit", "--level", "high"] : [
|
|
142
|
+
"yarn",
|
|
143
|
+
"npm",
|
|
144
|
+
"audit",
|
|
145
|
+
"--environment",
|
|
146
|
+
"production",
|
|
147
|
+
"--severity",
|
|
148
|
+
"high"
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
var DEP_FIELDS = [
|
|
153
|
+
"dependencies",
|
|
154
|
+
"devDependencies",
|
|
155
|
+
"optionalDependencies",
|
|
156
|
+
"peerDependencies"
|
|
157
|
+
];
|
|
158
|
+
var NON_REGISTRY_RANGE_RE = /^(?:workspace:|file:|link:|portal:|catalog:|git|https?:)/;
|
|
159
|
+
var collectEntry = (dependencies, name, range) => {
|
|
160
|
+
if (typeof range !== "string" || NON_REGISTRY_RANGE_RE.test(range)) return;
|
|
161
|
+
const alias = /^npm:(.+)@([^@]+)$/.exec(range);
|
|
162
|
+
const target = alias?.[1] ?? name;
|
|
163
|
+
const targetRange = alias?.[2] ?? range;
|
|
164
|
+
if (!dependencies.has(target)) dependencies.set(target, targetRange);
|
|
165
|
+
};
|
|
166
|
+
var collectFromPackage = (dependencies, packageDir) => {
|
|
167
|
+
const package_ = readPackage(packageDir);
|
|
168
|
+
for (const field of DEP_FIELDS) {
|
|
169
|
+
const section = package_?.[field];
|
|
170
|
+
if (typeof section !== "object" || section === null) continue;
|
|
171
|
+
for (const [name, range] of Object.entries(section))
|
|
172
|
+
collectEntry(dependencies, name, range);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var collectDependencies = (dir) => {
|
|
176
|
+
const dependencies = /* @__PURE__ */ new Map();
|
|
177
|
+
for (const packageDir of [dir, ...getWorkspaceDirectories(dir)])
|
|
178
|
+
collectFromPackage(dependencies, packageDir);
|
|
179
|
+
return dependencies;
|
|
180
|
+
};
|
|
181
|
+
var findDeprecated = async (name, range) => {
|
|
182
|
+
const response = await fetch(
|
|
183
|
+
`https://registry.npmjs.org/${name.replace("/", "%2F")}`,
|
|
184
|
+
{
|
|
185
|
+
headers: { accept: "application/vnd.npm.install-v1+json" },
|
|
186
|
+
signal: AbortSignal.timeout(1e4)
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
190
|
+
const data = await response.json();
|
|
191
|
+
const latest = data["dist-tags"]?.latest;
|
|
192
|
+
const resolved = latest && satisfies(latest, range) ? latest : maxSatisfying(Object.keys(data.versions), range);
|
|
193
|
+
if (!resolved) return null;
|
|
194
|
+
const deprecated = data.versions[resolved]?.deprecated;
|
|
195
|
+
return deprecated ? `${name}@${resolved} \u2014 ${deprecated.slice(0, 120)}` : null;
|
|
92
196
|
};
|
|
93
197
|
var LOCAL_CHECKS = [
|
|
94
198
|
{
|
|
95
199
|
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",
|
|
200
|
+
desc: "README",
|
|
103
201
|
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/")
|
|
202
|
+
check: (dir) => existsSync2(path2.join(dir, "README.md"))
|
|
113
203
|
},
|
|
114
204
|
{
|
|
115
205
|
id: "readme-npm-badge",
|
|
116
|
-
desc: "
|
|
206
|
+
desc: "npm badge",
|
|
117
207
|
type: "local",
|
|
118
208
|
publishedOnly: true,
|
|
119
209
|
check: (dir) => readmeIncludes(dir, "shields.io/npm/v/")
|
|
120
210
|
},
|
|
121
211
|
{
|
|
122
|
-
id: "codacy
|
|
123
|
-
desc: "
|
|
212
|
+
id: "codacy",
|
|
213
|
+
desc: "Codacy",
|
|
124
214
|
type: "local",
|
|
125
215
|
publishedOnly: true,
|
|
126
|
-
check: (dir) =>
|
|
216
|
+
check: (dir) => {
|
|
217
|
+
const missing = [
|
|
218
|
+
readmeIncludes(dir, "project/badge/Grade/") ? null : "grade badge missing in README",
|
|
219
|
+
readmeIncludes(dir, "project/badge/Coverage/") ? null : "coverage badge missing in README",
|
|
220
|
+
existsSync2(path2.join(dir, ".codacy.yml")) ? null : ".codacy.yml missing"
|
|
221
|
+
].filter((m) => m !== null);
|
|
222
|
+
if (missing.length === 0) return true;
|
|
223
|
+
return { pass: false, detail: missing.join("\n") };
|
|
224
|
+
}
|
|
127
225
|
},
|
|
128
226
|
{
|
|
129
227
|
id: "license",
|
|
130
|
-
desc: "LICENSE
|
|
228
|
+
desc: "LICENSE",
|
|
131
229
|
type: "local",
|
|
132
230
|
publishedOnly: true,
|
|
133
|
-
check: (dir) =>
|
|
231
|
+
check: (dir) => existsSync2(path2.join(dir, "LICENSE"))
|
|
134
232
|
},
|
|
135
233
|
{
|
|
136
234
|
id: "ci-workflow",
|
|
137
|
-
desc: "CI workflow
|
|
235
|
+
desc: "CI workflow",
|
|
138
236
|
type: "local",
|
|
139
237
|
publishedOnly: true,
|
|
140
|
-
check: (dir) =>
|
|
238
|
+
check: (dir) => existsSync2(path2.join(dir, ".github/workflows/ci.yml"))
|
|
141
239
|
},
|
|
142
240
|
{
|
|
143
241
|
id: "renovate",
|
|
144
|
-
desc: "renovate.json
|
|
242
|
+
desc: "renovate.json",
|
|
145
243
|
type: "local",
|
|
146
|
-
check: (dir) =>
|
|
244
|
+
check: (dir) => existsSync2(path2.join(dir, "renovate.json"))
|
|
147
245
|
},
|
|
148
246
|
{
|
|
149
247
|
id: "pkg-publint",
|
|
150
|
-
desc: "publint
|
|
248
|
+
desc: "publint",
|
|
151
249
|
type: "local",
|
|
152
250
|
publishedOnly: true,
|
|
153
|
-
check: (dir) =>
|
|
251
|
+
check: (dir) => eachPublishedPackage(dir, async (pkgDir) => {
|
|
154
252
|
const { messages } = await publint({ pkgDir });
|
|
155
253
|
const failures = messages.filter(
|
|
156
254
|
(m) => m.type === "error" || m.type === "warning"
|
|
@@ -164,13 +262,13 @@ var LOCAL_CHECKS = [
|
|
|
164
262
|
},
|
|
165
263
|
{
|
|
166
264
|
id: "pkg-attw",
|
|
167
|
-
desc: "attw
|
|
265
|
+
desc: "attw",
|
|
168
266
|
type: "local",
|
|
169
267
|
check: (dir) => {
|
|
170
268
|
const bin = findBin("attw", _dirname);
|
|
171
269
|
if (!bin) return { pass: false, detail: "attw not available" };
|
|
172
|
-
return
|
|
173
|
-
const r =
|
|
270
|
+
return eachPublishedPackage(dir, (pkgDir) => {
|
|
271
|
+
const r = spawnSync2(
|
|
174
272
|
process.execPath,
|
|
175
273
|
[bin, "--pack", "--profile", "esm-only"],
|
|
176
274
|
{
|
|
@@ -186,12 +284,12 @@ var LOCAL_CHECKS = [
|
|
|
186
284
|
},
|
|
187
285
|
{
|
|
188
286
|
id: "pkg-knip",
|
|
189
|
-
desc: "knip
|
|
287
|
+
desc: "knip",
|
|
190
288
|
type: "local",
|
|
191
289
|
check: (dir) => {
|
|
192
290
|
const bin = findBin("knip", _dirname);
|
|
193
291
|
if (!bin) return { pass: false, detail: "knip not available" };
|
|
194
|
-
const r =
|
|
292
|
+
const r = spawnSync2(process.execPath, [bin], {
|
|
195
293
|
cwd: dir,
|
|
196
294
|
encoding: "utf8",
|
|
197
295
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -204,17 +302,80 @@ var LOCAL_CHECKS = [
|
|
|
204
302
|
Run: npx knip` };
|
|
205
303
|
}
|
|
206
304
|
},
|
|
305
|
+
{
|
|
306
|
+
id: "monorepo-lint",
|
|
307
|
+
desc: "sherif (workspaces)",
|
|
308
|
+
type: "local",
|
|
309
|
+
check: (dir) => {
|
|
310
|
+
const package_ = readPackage(dir);
|
|
311
|
+
if (!package_ || getWorkspacePatterns(package_).length === 0)
|
|
312
|
+
return { pass: true, detail: "not a monorepo" };
|
|
313
|
+
const bin = findBin("sherif", _dirname);
|
|
314
|
+
if (!bin) return { pass: false, detail: "sherif not available" };
|
|
315
|
+
const r = spawnSync2(process.execPath, [bin], {
|
|
316
|
+
cwd: dir,
|
|
317
|
+
encoding: "utf8",
|
|
318
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
319
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
320
|
+
});
|
|
321
|
+
if (r.status === 0) return true;
|
|
322
|
+
const detail = (r.stdout + r.stderr).trim().split("\n").slice(0, 20).join("\n");
|
|
323
|
+
return { pass: false, detail };
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
id: "deps-audit",
|
|
328
|
+
desc: "audit",
|
|
329
|
+
type: "local",
|
|
330
|
+
check: (dir) => {
|
|
331
|
+
const pm = detectPackageManager(dir);
|
|
332
|
+
const [command, ...arguments_] = auditCommand(pm);
|
|
333
|
+
const r = spawnSync2(command, arguments_, {
|
|
334
|
+
cwd: dir,
|
|
335
|
+
encoding: "utf8",
|
|
336
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
337
|
+
});
|
|
338
|
+
if (r.error) return { pass: false, detail: String(r.error) };
|
|
339
|
+
if (r.status === 0) return true;
|
|
340
|
+
const detail = (r.stdout + r.stderr).trim().split("\n").slice(0, 20).join("\n");
|
|
341
|
+
return { pass: false, detail: detail || `audit exited ${r.status}` };
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
id: "deps-deprecated",
|
|
346
|
+
desc: "no deprecated deps",
|
|
347
|
+
type: "local",
|
|
348
|
+
check: async (dir) => {
|
|
349
|
+
const dependencies = collectDependencies(dir);
|
|
350
|
+
const failures = [];
|
|
351
|
+
await Promise.all(
|
|
352
|
+
[...dependencies].map(async ([name, range]) => {
|
|
353
|
+
try {
|
|
354
|
+
const deprecated = await findDeprecated(name, range);
|
|
355
|
+
if (deprecated) failures.push(deprecated);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
failures.push(`${name} \u2014 check failed: ${String(error)}`);
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
if (failures.length === 0) return true;
|
|
362
|
+
return {
|
|
363
|
+
pass: false,
|
|
364
|
+
detail: failures.toSorted((a, b) => a.localeCompare(b)).join("\n")
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
},
|
|
207
368
|
{
|
|
208
369
|
id: "deps-fresh",
|
|
209
|
-
desc: "
|
|
370
|
+
desc: "deps up to date",
|
|
210
371
|
type: "local",
|
|
211
372
|
check: async (dir) => {
|
|
212
|
-
const
|
|
213
|
-
const hasWorkspaces = Array.isArray(
|
|
373
|
+
const package_ = readPackage(dir);
|
|
374
|
+
const hasWorkspaces = Array.isArray(package_?.workspaces) && package_.workspaces.length > 0;
|
|
214
375
|
try {
|
|
215
|
-
const result = await
|
|
216
|
-
packageFile:
|
|
217
|
-
...hasWorkspaces
|
|
376
|
+
const result = await ncuRun2({
|
|
377
|
+
packageFile: path2.join(dir, "package.json"),
|
|
378
|
+
...hasWorkspaces && { workspaces: true },
|
|
218
379
|
silent: true
|
|
219
380
|
});
|
|
220
381
|
const entries = hasWorkspaces ? Object.values(
|
|
@@ -222,10 +383,9 @@ Run: npx knip` };
|
|
|
222
383
|
).flatMap((x) => Object.entries(x)) : Object.entries(result);
|
|
223
384
|
if (entries.length === 0) return true;
|
|
224
385
|
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
386
|
return { pass: false, detail: `${lines}
|
|
227
387
|
|
|
228
|
-
Run:
|
|
388
|
+
Run: forge update` };
|
|
229
389
|
} catch (error) {
|
|
230
390
|
return { pass: false, detail: String(error) };
|
|
231
391
|
}
|
|
@@ -234,7 +394,7 @@ Run: ${cmd}` };
|
|
|
234
394
|
];
|
|
235
395
|
var listSecrets = (repo) => {
|
|
236
396
|
try {
|
|
237
|
-
const result =
|
|
397
|
+
const result = spawnSync2(
|
|
238
398
|
"gh",
|
|
239
399
|
["secret", "list", "--repo", repo, "--json", "name"],
|
|
240
400
|
{
|
|
@@ -249,19 +409,9 @@ var listSecrets = (repo) => {
|
|
|
249
409
|
}
|
|
250
410
|
};
|
|
251
411
|
var REMOTE_CHECKS = [
|
|
252
|
-
{
|
|
253
|
-
id: "secret-npm-token",
|
|
254
|
-
desc: "Secret NPM_TOKEN set",
|
|
255
|
-
type: "remote",
|
|
256
|
-
publishedOnly: true,
|
|
257
|
-
check: (repo) => {
|
|
258
|
-
const secrets = listSecrets(repo);
|
|
259
|
-
return secrets?.includes("NPM_TOKEN") ?? false;
|
|
260
|
-
}
|
|
261
|
-
},
|
|
262
412
|
{
|
|
263
413
|
id: "secret-codacy-token",
|
|
264
|
-
desc: "
|
|
414
|
+
desc: "Codacy secret",
|
|
265
415
|
type: "remote",
|
|
266
416
|
publishedOnly: true,
|
|
267
417
|
check: (repo) => {
|
|
@@ -282,19 +432,21 @@ var checkLocal = async (options = {}) => {
|
|
|
282
432
|
stdio: ["ignore", "pipe", "ignore"]
|
|
283
433
|
}).trim();
|
|
284
434
|
const match = /github\.com[:/](.+?)(?:\.git)?$/.exec(remote);
|
|
285
|
-
return match?.[1] ??
|
|
435
|
+
return match?.[1] ?? path3.basename(dir);
|
|
286
436
|
} catch {
|
|
287
437
|
return dir;
|
|
288
438
|
}
|
|
289
439
|
})();
|
|
290
|
-
const
|
|
440
|
+
const isPublished = hasPublishedPkg(dir);
|
|
291
441
|
const localResults = await Promise.all(
|
|
292
|
-
LOCAL_CHECKS.filter((c) => !c.publishedOnly ||
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
442
|
+
LOCAL_CHECKS.filter((c) => !c.publishedOnly || isPublished).map(
|
|
443
|
+
async (c) => {
|
|
444
|
+
const raw = await c.check(dir);
|
|
445
|
+
return typeof raw === "boolean" ? { id: c.id, desc: c.desc, pass: raw } : { id: c.id, desc: c.desc, ...raw };
|
|
446
|
+
}
|
|
447
|
+
)
|
|
296
448
|
);
|
|
297
|
-
const remoteResults = skipRemote ? [] : REMOTE_CHECKS.filter((c) => !c.publishedOnly ||
|
|
449
|
+
const remoteResults = skipRemote ? [] : REMOTE_CHECKS.filter((c) => !c.publishedOnly || isPublished).map((c) => ({
|
|
298
450
|
id: c.id,
|
|
299
451
|
desc: c.desc,
|
|
300
452
|
pass: c.check(repo)
|
|
@@ -304,11 +456,11 @@ var checkLocal = async (options = {}) => {
|
|
|
304
456
|
var checkRepo = async (repo, options = {}) => {
|
|
305
457
|
const {
|
|
306
458
|
token,
|
|
307
|
-
baseDir =
|
|
459
|
+
baseDir = path3.join(tmpdir(), "repo-lint"),
|
|
308
460
|
skipRemote = false
|
|
309
461
|
} = options;
|
|
310
|
-
const dir =
|
|
311
|
-
if (
|
|
462
|
+
const dir = path3.join(baseDir, repo.replace("/", "__"));
|
|
463
|
+
if (existsSync3(dir))
|
|
312
464
|
execSync(`git -C ${dir} pull --quiet`, { stdio: "ignore" });
|
|
313
465
|
else {
|
|
314
466
|
const url = token ? `https://${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
|
|
@@ -339,24 +491,24 @@ var { values } = parseArgs({
|
|
|
339
491
|
}
|
|
340
492
|
});
|
|
341
493
|
var printReport = (result) => {
|
|
342
|
-
let
|
|
494
|
+
let isAnyFail = false;
|
|
343
495
|
const pass = result.results.filter((r) => r.pass).length;
|
|
344
496
|
const total = result.results.length;
|
|
345
497
|
for (const r of result.results) {
|
|
346
498
|
console.log(` ${r.pass ? "\u2713" : "\u2717"} ${r.desc}`);
|
|
347
|
-
if (!r.pass)
|
|
499
|
+
if (!r.pass) isAnyFail = true;
|
|
348
500
|
}
|
|
349
501
|
console.log(`
|
|
350
502
|
${pass}/${total} checks passed`);
|
|
351
|
-
return
|
|
503
|
+
return isAnyFail;
|
|
352
504
|
};
|
|
353
505
|
var main = async () => {
|
|
354
|
-
let
|
|
506
|
+
let isAnyFail = false;
|
|
355
507
|
if (values.local) {
|
|
356
508
|
console.log(`
|
|
357
509
|
\u2500\u2500 ${process.cwd()}`);
|
|
358
510
|
const result = await checkLocal({ skipRemote: values["skip-remote"] });
|
|
359
|
-
|
|
511
|
+
isAnyFail = printReport(result);
|
|
360
512
|
} else {
|
|
361
513
|
if (!values.repos) {
|
|
362
514
|
console.error(
|
|
@@ -365,7 +517,8 @@ var main = async () => {
|
|
|
365
517
|
process.exit(1);
|
|
366
518
|
}
|
|
367
519
|
mkdirSync(values.dir, { recursive: true });
|
|
368
|
-
|
|
520
|
+
const repos = values.repos.split(",").map((r) => r.trim());
|
|
521
|
+
for (const repo of repos) {
|
|
369
522
|
console.log(`
|
|
370
523
|
\u2500\u2500 ${repo}`);
|
|
371
524
|
try {
|
|
@@ -374,13 +527,13 @@ var main = async () => {
|
|
|
374
527
|
baseDir: values.dir,
|
|
375
528
|
skipRemote: values["skip-remote"]
|
|
376
529
|
});
|
|
377
|
-
if (printReport(result))
|
|
530
|
+
if (printReport(result)) isAnyFail = true;
|
|
378
531
|
} catch {
|
|
379
532
|
console.log(" \u2717 could not clone repo");
|
|
380
|
-
|
|
533
|
+
isAnyFail = true;
|
|
381
534
|
}
|
|
382
535
|
}
|
|
383
536
|
}
|
|
384
|
-
process.exit(
|
|
537
|
+
process.exit(isAnyFail ? 1 : 0);
|
|
385
538
|
};
|
|
386
|
-
|
|
539
|
+
await main();
|