lintlane 3.0.1 → 4.0.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 +204 -632
- package/bin/check-commit.mjs +54 -137
- package/bin/{ci.mjs → checks.mjs} +104 -35
- package/bin/cli.mjs +226 -69
- package/bin/commit-cli.mjs +36 -122
- package/bin/doctor.mjs +328 -0
- package/bin/hook.mjs +156 -0
- package/bin/proc.mjs +20 -1
- package/bin/project.mjs +100 -4
- package/bin/ui-server.mjs +77 -10
- package/package.json +18 -5
- package/presets/commitizen.cjs +4 -0
- package/presets/commitlint.mjs +47 -0
- package/templates/commit-msg +2 -2
- package/templates/pre-commit +2 -5
- package/templates/pre-push +2 -2
- package/templates/prepare-commit-msg +2 -2
- package/ui/index.html +210 -44
- package/bin/commit-config.mjs +0 -24
- package/bin/commit-lint.mjs +0 -143
- package/bin/commit-msg.mjs +0 -18
- package/bin/commit-prompt.mjs +0 -110
- package/bin/pre-commit.mjs +0 -55
- package/bin/pre-push-check.mjs +0 -48
- package/bin/prepare-commit-msg.mjs +0 -46
- package/bin/staged.mjs +0 -333
package/bin/ui-server.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveChecks, runCiSteps } from "./checks.mjs";
|
|
7
|
+
import { formatReport, runDoctor } from "./doctor.mjs";
|
|
7
8
|
import { runProcess, spawnPrepared } from "./proc.mjs";
|
|
8
9
|
|
|
9
10
|
const MAX_FILES = 200;
|
|
@@ -20,6 +21,21 @@ function safePath(cwd, file) {
|
|
|
20
21
|
return isInside(cwd, abs) && abs !== cwd ? abs : null;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function tokenMatches(given, expected) {
|
|
25
|
+
if (typeof given !== "string") return false;
|
|
26
|
+
const a = Buffer.from(given);
|
|
27
|
+
const b = Buffer.from(expected);
|
|
28
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Only answer requests addressed to the loopback name we listen on. A hostile web
|
|
32
|
+
// page can point its own domain at 127.0.0.1 (DNS rebinding); its Host header then
|
|
33
|
+
// names that domain, not us.
|
|
34
|
+
function hostAllowed(hostHeader, port) {
|
|
35
|
+
if (typeof hostHeader !== "string") return false;
|
|
36
|
+
return [`127.0.0.1:${port}`, `localhost:${port}`].includes(hostHeader.toLowerCase());
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
const runBiome = (biome, args, options) =>
|
|
24
40
|
runProcess(biome[0], [...biome.slice(1), ...args], options);
|
|
25
41
|
|
|
@@ -33,9 +49,20 @@ async function runCheck(cwd, biome) {
|
|
|
33
49
|
);
|
|
34
50
|
const start = out.indexOf("{");
|
|
35
51
|
if (start === -1) {
|
|
36
|
-
|
|
52
|
+
const detail = result.error
|
|
53
|
+
? `Couldn't start Biome: ${result.error.message}`
|
|
54
|
+
: result.stderr.toString().trim() ||
|
|
55
|
+
`Biome exited with code ${result.code} and no JSON output`;
|
|
56
|
+
throw new Error(detail);
|
|
57
|
+
}
|
|
58
|
+
let report;
|
|
59
|
+
try {
|
|
60
|
+
report = JSON.parse(out.slice(start));
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Biome's output wasn't valid JSON (exit code ${result.code}). Run \`biome check .\` in a terminal to see the problem.`,
|
|
64
|
+
);
|
|
37
65
|
}
|
|
38
|
-
const report = JSON.parse(out.slice(start));
|
|
39
66
|
|
|
40
67
|
const byFile = new Map();
|
|
41
68
|
for (const d of report.diagnostics ?? []) {
|
|
@@ -62,6 +89,8 @@ async function runCheck(cwd, biome) {
|
|
|
62
89
|
files,
|
|
63
90
|
totalFiles: byFile.size,
|
|
64
91
|
truncated: byFile.size > MAX_FILES,
|
|
92
|
+
exitCode: result.code,
|
|
93
|
+
ms: Math.round(performance.now() - t0),
|
|
65
94
|
};
|
|
66
95
|
}
|
|
67
96
|
|
|
@@ -140,33 +169,64 @@ function openBrowser(address) {
|
|
|
140
169
|
|
|
141
170
|
export async function startUi({ cwd, biome, kitDir, port, open, pmName, pkg }) {
|
|
142
171
|
let ciBusy = false;
|
|
172
|
+
let checkBusy = false;
|
|
143
173
|
const token = randomUUID();
|
|
174
|
+
const checksFor = () => resolveChecks({ cwd, biome, pmName, pkg });
|
|
144
175
|
const html = await readFile(join(kitDir, "ui", "index.html"), "utf8");
|
|
145
176
|
|
|
146
177
|
async function handle(req, res) {
|
|
147
178
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
148
179
|
|
|
180
|
+
if (!hostAllowed(req.headers.host, server.address().port)) {
|
|
181
|
+
res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
|
|
182
|
+
res.end("Forbidden: unexpected Host header.");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
149
186
|
if (req.method === "GET" && url.pathname === "/") {
|
|
150
|
-
if (url.searchParams.get("t")
|
|
187
|
+
if (!tokenMatches(url.searchParams.get("t"), token)) {
|
|
151
188
|
res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
|
|
152
189
|
res.end("Forbidden: open the URL printed by `lintlane ui`.");
|
|
153
190
|
return;
|
|
154
191
|
}
|
|
192
|
+
const nonce = randomBytes(16).toString("base64");
|
|
155
193
|
res.writeHead(200, {
|
|
156
194
|
"content-type": "text/html; charset=utf-8",
|
|
157
195
|
"cache-control": "no-store",
|
|
196
|
+
"x-content-type-options": "nosniff",
|
|
197
|
+
"referrer-policy": "no-referrer",
|
|
198
|
+
"cross-origin-resource-policy": "same-origin",
|
|
199
|
+
"content-security-policy": [
|
|
200
|
+
"default-src 'none'",
|
|
201
|
+
`script-src 'nonce-${nonce}'`,
|
|
202
|
+
"style-src 'unsafe-inline'",
|
|
203
|
+
"connect-src 'self'",
|
|
204
|
+
"img-src 'self' data:",
|
|
205
|
+
"base-uri 'none'",
|
|
206
|
+
"form-action 'none'",
|
|
207
|
+
"frame-ancestors 'none'",
|
|
208
|
+
].join("; "),
|
|
158
209
|
});
|
|
159
|
-
res.end(html.replace("__TOKEN__", token));
|
|
210
|
+
res.end(html.replace("__TOKEN__", token).replaceAll("<script", `<script nonce="${nonce}"`));
|
|
160
211
|
return;
|
|
161
212
|
}
|
|
162
213
|
|
|
163
214
|
if (url.pathname.startsWith("/api/")) {
|
|
164
|
-
if (req.headers["x-lintlane-token"]
|
|
215
|
+
if (!tokenMatches(req.headers["x-lintlane-token"], token)) {
|
|
165
216
|
sendJson(res, 401, { error: "Unauthorized" });
|
|
166
217
|
return;
|
|
167
218
|
}
|
|
168
219
|
if (req.method === "POST" && url.pathname === "/api/check") {
|
|
169
|
-
|
|
220
|
+
if (checkBusy) {
|
|
221
|
+
sendJson(res, 409, { error: "A check is already running" });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
checkBusy = true;
|
|
225
|
+
try {
|
|
226
|
+
sendJson(res, 200, await runCheck(cwd, biome));
|
|
227
|
+
} finally {
|
|
228
|
+
checkBusy = false;
|
|
229
|
+
}
|
|
170
230
|
return;
|
|
171
231
|
}
|
|
172
232
|
if (req.method === "POST" && url.pathname === "/api/fix") {
|
|
@@ -180,9 +240,11 @@ export async function startUi({ cwd, biome, kitDir, port, open, pmName, pkg }) {
|
|
|
180
240
|
return;
|
|
181
241
|
}
|
|
182
242
|
if (req.method === "POST" && url.pathname === "/api/ci/steps") {
|
|
183
|
-
const
|
|
243
|
+
const { steps, problems } = checksFor();
|
|
184
244
|
sendJson(res, 200, {
|
|
185
245
|
pmName,
|
|
246
|
+
problems,
|
|
247
|
+
busy: ciBusy,
|
|
186
248
|
steps: steps.map(({ id, label, display, defaultOn, note }) => ({
|
|
187
249
|
id,
|
|
188
250
|
label,
|
|
@@ -199,7 +261,7 @@ export async function startUi({ cwd, biome, kitDir, port, open, pmName, pkg }) {
|
|
|
199
261
|
return;
|
|
200
262
|
}
|
|
201
263
|
const { steps: ids, stopOnFail } = await readJson(req);
|
|
202
|
-
const known =
|
|
264
|
+
const known = checksFor().steps;
|
|
203
265
|
const steps = Array.isArray(ids) ? known.filter((step) => ids.includes(step.id)) : [];
|
|
204
266
|
if (steps.length === 0) {
|
|
205
267
|
sendJson(res, 400, { error: "No steps selected" });
|
|
@@ -228,6 +290,11 @@ export async function startUi({ cwd, biome, kitDir, port, open, pmName, pkg }) {
|
|
|
228
290
|
}
|
|
229
291
|
return;
|
|
230
292
|
}
|
|
293
|
+
if (req.method === "POST" && url.pathname === "/api/doctor") {
|
|
294
|
+
const report = await runDoctor({ cwd, kitDir });
|
|
295
|
+
sendJson(res, 200, { ...report, text: formatReport(report) });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
231
298
|
if (req.method === "POST" && url.pathname === "/api/diff") {
|
|
232
299
|
const { file } = await readJson(req);
|
|
233
300
|
sendJson(res, 200, await getDiff(cwd, biome, file));
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lintlane",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Code-quality workflow for JavaScript, React and Next.js: orchestrates Biome, lint-staged, Husky, Commitizen and commitlint, with a local dashboard and CI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"./biome": "./presets/base.json",
|
|
12
12
|
"./biome/strict": "./presets/strict.json",
|
|
13
13
|
"./biome/next": "./presets/next.json",
|
|
14
|
-
"./package.json": "./package.json"
|
|
14
|
+
"./package.json": "./package.json",
|
|
15
|
+
"./commitlint": "./presets/commitlint.mjs",
|
|
16
|
+
"./commitizen": "./presets/commitizen.cjs"
|
|
15
17
|
},
|
|
16
18
|
"files": [
|
|
17
19
|
"bin",
|
|
@@ -43,7 +45,18 @@
|
|
|
43
45
|
"biome",
|
|
44
46
|
"react",
|
|
45
47
|
"lint",
|
|
46
|
-
"format"
|
|
48
|
+
"format",
|
|
49
|
+
"lint-staged",
|
|
50
|
+
"husky",
|
|
51
|
+
"commitlint",
|
|
52
|
+
"commitizen"
|
|
47
53
|
],
|
|
48
|
-
"author": "Expo2Dev"
|
|
54
|
+
"author": "Expo2Dev",
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@commitlint/cli": "19.8.1",
|
|
57
|
+
"@commitlint/config-conventional": "19.8.1",
|
|
58
|
+
"commitizen": "4.3.2",
|
|
59
|
+
"cz-conventional-changelog": "3.3.0",
|
|
60
|
+
"lint-staged": "16.4.0"
|
|
61
|
+
}
|
|
49
62
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Commitizen adapter: the standard Conventional Commits prompt, resolved from
|
|
2
|
+
// lintlane's own dependencies so it works under every package manager.
|
|
3
|
+
// package.json: "config": { "commitizen": { "path": "lintlane/presets/commitizen.cjs" } }
|
|
4
|
+
module.exports = require("cz-conventional-changelog");
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Shared commitlint config: Conventional Commits, tuned by the optional
|
|
2
|
+
// "lintlane.commit" block in package.json:
|
|
3
|
+
//
|
|
4
|
+
// "lintlane": { "commit": { "types": ["feat", "fix"], "messageLengthLimit": 72,
|
|
5
|
+
// "allowedPrefixes": ["WIP"] } }
|
|
6
|
+
//
|
|
7
|
+
// A project's commitlint.config.mjs can be just:
|
|
8
|
+
// export { default } from "lintlane/commitlint";
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
|
|
15
|
+
function readOptions() {
|
|
16
|
+
try {
|
|
17
|
+
const pkg = JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf8"));
|
|
18
|
+
const commit = pkg?.lintlane?.commit;
|
|
19
|
+
return commit && typeof commit === "object" ? commit : {};
|
|
20
|
+
} catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const options = readOptions();
|
|
26
|
+
const rules = {};
|
|
27
|
+
|
|
28
|
+
if (Array.isArray(options.types) && options.types.length > 0) {
|
|
29
|
+
rules["type-enum"] = [2, "always", options.types.map((t) => String(Array.isArray(t) ? t[0] : t))];
|
|
30
|
+
} else if (options.types && typeof options.types === "object" && !Array.isArray(options.types)) {
|
|
31
|
+
rules["type-enum"] = [2, "always", Object.keys(options.types)];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const limit = Number(options.messageLengthLimit);
|
|
35
|
+
if (options.messageLengthLimit !== undefined && Number.isFinite(limit) && limit >= 0) {
|
|
36
|
+
rules["header-max-length"] = limit === 0 ? [0] : [2, "always", limit];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const prefixes = Array.isArray(options.allowedPrefixes) ? options.allowedPrefixes.map(String) : [];
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
extends: [require.resolve("@commitlint/config-conventional")],
|
|
43
|
+
rules,
|
|
44
|
+
...(prefixes.length > 0
|
|
45
|
+
? { ignores: [(message) => prefixes.some((prefix) => String(message).startsWith(prefix))] }
|
|
46
|
+
: {}),
|
|
47
|
+
};
|
package/templates/commit-msg
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[ -f node_modules/lintlane/bin/
|
|
2
|
-
node node_modules/lintlane/bin/commit-msg
|
|
1
|
+
[ -f node_modules/lintlane/bin/cli.mjs ] || { echo "lintlane: node_modules/lintlane not found; skipping commit-msg. Run your package manager's install." >&2; exit 0; }
|
|
2
|
+
node node_modules/lintlane/bin/cli.mjs hook commit-msg "$@"
|
package/templates/pre-commit
CHANGED
|
@@ -1,5 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
else
|
|
4
|
-
echo "lintlane: node_modules/lintlane not found; skipping staged-file checks. Run your package manager's install." >&2
|
|
5
|
-
fi
|
|
1
|
+
[ -f node_modules/lintlane/bin/cli.mjs ] || { echo "lintlane: node_modules/lintlane not found; skipping pre-commit. Run your package manager's install." >&2; exit 0; }
|
|
2
|
+
node node_modules/lintlane/bin/cli.mjs hook pre-commit "$@"
|
package/templates/pre-push
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[ -f node_modules/lintlane/bin/
|
|
2
|
-
node node_modules/lintlane/bin/pre-push
|
|
1
|
+
[ -f node_modules/lintlane/bin/cli.mjs ] || { echo "lintlane: node_modules/lintlane not found; skipping pre-push. Run your package manager's install." >&2; exit 0; }
|
|
2
|
+
node node_modules/lintlane/bin/cli.mjs hook pre-push "$@"
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[ -f node_modules/lintlane/bin/prepare-commit-msg.
|
|
2
|
-
node node_modules/lintlane/bin/prepare-commit-msg
|
|
1
|
+
[ -f node_modules/lintlane/bin/cli.mjs ] || { echo "lintlane: node_modules/lintlane not found; skipping prepare-commit-msg. Run your package manager's install." >&2; exit 0; }
|
|
2
|
+
node node_modules/lintlane/bin/cli.mjs hook prepare-commit-msg "$@"
|