@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/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
|
|
200
|
+
desc: "README",
|
|
97
201
|
type: "local",
|
|
98
|
-
check: (dir) =>
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
id: "readme-codacy-grade-badge",
|
|
102
|
-
desc: "README has Codacy grade badge",
|
|
103
|
-
type: "local",
|
|
104
|
-
publishedOnly: true,
|
|
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
|
{
|
|
@@ -251,7 +411,7 @@ var listSecrets = (repo) => {
|
|
|
251
411
|
var REMOTE_CHECKS = [
|
|
252
412
|
{
|
|
253
413
|
id: "secret-codacy-token",
|
|
254
|
-
desc: "
|
|
414
|
+
desc: "Codacy secret",
|
|
255
415
|
type: "remote",
|
|
256
416
|
publishedOnly: true,
|
|
257
417
|
check: (repo) => {
|
|
@@ -272,19 +432,21 @@ var checkLocal = async (options = {}) => {
|
|
|
272
432
|
stdio: ["ignore", "pipe", "ignore"]
|
|
273
433
|
}).trim();
|
|
274
434
|
const match = /github\.com[:/](.+?)(?:\.git)?$/.exec(remote);
|
|
275
|
-
return match?.[1] ??
|
|
435
|
+
return match?.[1] ?? path3.basename(dir);
|
|
276
436
|
} catch {
|
|
277
437
|
return dir;
|
|
278
438
|
}
|
|
279
439
|
})();
|
|
280
|
-
const
|
|
440
|
+
const isPublished = hasPublishedPkg(dir);
|
|
281
441
|
const localResults = await Promise.all(
|
|
282
|
-
LOCAL_CHECKS.filter((c) => !c.publishedOnly ||
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
)
|
|
286
448
|
);
|
|
287
|
-
const remoteResults = skipRemote ? [] : REMOTE_CHECKS.filter((c) => !c.publishedOnly ||
|
|
449
|
+
const remoteResults = skipRemote ? [] : REMOTE_CHECKS.filter((c) => !c.publishedOnly || isPublished).map((c) => ({
|
|
288
450
|
id: c.id,
|
|
289
451
|
desc: c.desc,
|
|
290
452
|
pass: c.check(repo)
|
|
@@ -294,11 +456,11 @@ var checkLocal = async (options = {}) => {
|
|
|
294
456
|
var checkRepo = async (repo, options = {}) => {
|
|
295
457
|
const {
|
|
296
458
|
token,
|
|
297
|
-
baseDir =
|
|
459
|
+
baseDir = path3.join(tmpdir(), "repo-lint"),
|
|
298
460
|
skipRemote = false
|
|
299
461
|
} = options;
|
|
300
|
-
const dir =
|
|
301
|
-
if (
|
|
462
|
+
const dir = path3.join(baseDir, repo.replace("/", "__"));
|
|
463
|
+
if (existsSync3(dir))
|
|
302
464
|
execSync(`git -C ${dir} pull --quiet`, { stdio: "ignore" });
|
|
303
465
|
else {
|
|
304
466
|
const url = token ? `https://${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
|
|
@@ -329,24 +491,24 @@ var { values } = parseArgs({
|
|
|
329
491
|
}
|
|
330
492
|
});
|
|
331
493
|
var printReport = (result) => {
|
|
332
|
-
let
|
|
494
|
+
let isAnyFail = false;
|
|
333
495
|
const pass = result.results.filter((r) => r.pass).length;
|
|
334
496
|
const total = result.results.length;
|
|
335
497
|
for (const r of result.results) {
|
|
336
498
|
console.log(` ${r.pass ? "\u2713" : "\u2717"} ${r.desc}`);
|
|
337
|
-
if (!r.pass)
|
|
499
|
+
if (!r.pass) isAnyFail = true;
|
|
338
500
|
}
|
|
339
501
|
console.log(`
|
|
340
502
|
${pass}/${total} checks passed`);
|
|
341
|
-
return
|
|
503
|
+
return isAnyFail;
|
|
342
504
|
};
|
|
343
505
|
var main = async () => {
|
|
344
|
-
let
|
|
506
|
+
let isAnyFail = false;
|
|
345
507
|
if (values.local) {
|
|
346
508
|
console.log(`
|
|
347
509
|
\u2500\u2500 ${process.cwd()}`);
|
|
348
510
|
const result = await checkLocal({ skipRemote: values["skip-remote"] });
|
|
349
|
-
|
|
511
|
+
isAnyFail = printReport(result);
|
|
350
512
|
} else {
|
|
351
513
|
if (!values.repos) {
|
|
352
514
|
console.error(
|
|
@@ -355,7 +517,8 @@ var main = async () => {
|
|
|
355
517
|
process.exit(1);
|
|
356
518
|
}
|
|
357
519
|
mkdirSync(values.dir, { recursive: true });
|
|
358
|
-
|
|
520
|
+
const repos = values.repos.split(",").map((r) => r.trim());
|
|
521
|
+
for (const repo of repos) {
|
|
359
522
|
console.log(`
|
|
360
523
|
\u2500\u2500 ${repo}`);
|
|
361
524
|
try {
|
|
@@ -364,13 +527,13 @@ var main = async () => {
|
|
|
364
527
|
baseDir: values.dir,
|
|
365
528
|
skipRemote: values["skip-remote"]
|
|
366
529
|
});
|
|
367
|
-
if (printReport(result))
|
|
530
|
+
if (printReport(result)) isAnyFail = true;
|
|
368
531
|
} catch {
|
|
369
532
|
console.log(" \u2717 could not clone repo");
|
|
370
|
-
|
|
533
|
+
isAnyFail = true;
|
|
371
534
|
}
|
|
372
535
|
}
|
|
373
536
|
}
|
|
374
|
-
process.exit(
|
|
537
|
+
process.exit(isAnyFail ? 1 : 0);
|
|
375
538
|
};
|
|
376
|
-
|
|
539
|
+
await main();
|
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,32 @@ declare const LOCAL_CHECKS: LocalCheck[];
|
|
|
22
22
|
declare const REMOTE_CHECKS: RemoteCheck[];
|
|
23
23
|
declare const CHECKS: Check[];
|
|
24
24
|
|
|
25
|
+
type PackageManagerName = 'npm' | 'yarn' | 'pnpm' | 'bun';
|
|
26
|
+
interface PackageManager {
|
|
27
|
+
name: PackageManagerName;
|
|
28
|
+
version?: string;
|
|
29
|
+
source: 'packageManager' | 'lockfile' | 'default';
|
|
30
|
+
}
|
|
31
|
+
interface UpdateResult {
|
|
32
|
+
id: string;
|
|
33
|
+
desc: string;
|
|
34
|
+
pass: boolean;
|
|
35
|
+
changed: boolean;
|
|
36
|
+
detail?: string;
|
|
37
|
+
}
|
|
38
|
+
interface UpdateReport {
|
|
39
|
+
dir: string;
|
|
40
|
+
packageManager: PackageManager;
|
|
41
|
+
results: UpdateResult[];
|
|
42
|
+
}
|
|
43
|
+
interface UpdateOptions {
|
|
44
|
+
dir?: string;
|
|
45
|
+
dry?: boolean;
|
|
46
|
+
install?: boolean;
|
|
47
|
+
}
|
|
48
|
+
declare const detectPackageManager: (dir: string) => PackageManager;
|
|
49
|
+
declare const runUpdate: (options?: UpdateOptions) => Promise<UpdateReport>;
|
|
50
|
+
|
|
25
51
|
interface CheckResult {
|
|
26
52
|
id: string;
|
|
27
53
|
desc: string;
|
|
@@ -45,4 +71,4 @@ interface CheckLocalOptions {
|
|
|
45
71
|
declare const checkLocal: (options?: CheckLocalOptions) => Promise<RepoReport>;
|
|
46
72
|
declare const checkRepo: (repo: string, options?: CheckRepoOptions) => Promise<RepoReport>;
|
|
47
73
|
|
|
48
|
-
export { CHECKS, type Check, type CheckDetail, type CheckLocalOptions, type CheckRepoOptions, type CheckResult, LOCAL_CHECKS, type LocalCheck, REMOTE_CHECKS, type RemoteCheck, type RepoReport, checkLocal, checkRepo };
|
|
74
|
+
export { CHECKS, type Check, type CheckDetail, type CheckLocalOptions, type CheckRepoOptions, type CheckResult, LOCAL_CHECKS, type LocalCheck, type PackageManager, type PackageManagerName, REMOTE_CHECKS, type RemoteCheck, type RepoReport, type UpdateOptions, type UpdateReport, type UpdateResult, checkLocal, checkRepo, detectPackageManager, runUpdate };
|