lintlane 2.5.0 → 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/LICENSE +21 -0
- package/README.md +210 -512
- package/bin/check-commit.mjs +54 -137
- package/bin/checks.mjs +237 -0
- package/bin/cli.mjs +299 -220
- package/bin/commit-cli.mjs +36 -122
- package/bin/doctor.mjs +328 -0
- package/bin/hook.mjs +156 -0
- package/bin/proc.mjs +132 -0
- package/bin/project.mjs +276 -0
- package/bin/ui-server.mjs +244 -91
- package/package.json +22 -7
- package/presets/commitizen.cjs +4 -0
- package/presets/commitlint.mjs +47 -0
- package/templates/commit-msg +2 -2
- package/templates/pre-commit +2 -2
- package/templates/pre-push +2 -2
- package/templates/prepare-commit-msg +2 -2
- package/ui/index.html +210 -44
- package/bin/ci.mjs +0 -162
- 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-push-check.mjs +0 -48
- package/bin/prepare-commit-msg.mjs +0 -46
package/bin/ui-server.mjs
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { createServer } from "node:http";
|
|
2
5
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
6
|
+
import { resolveChecks, runCiSteps } from "./checks.mjs";
|
|
7
|
+
import { formatReport, runDoctor } from "./doctor.mjs";
|
|
8
|
+
import { runProcess, spawnPrepared } from "./proc.mjs";
|
|
5
9
|
|
|
6
10
|
const MAX_FILES = 200;
|
|
11
|
+
const MAX_BODY = 1024 * 1024;
|
|
7
12
|
|
|
8
13
|
function isInside(root, target) {
|
|
9
14
|
const rel = relative(root, target);
|
|
@@ -16,19 +21,48 @@ function safePath(cwd, file) {
|
|
|
16
21
|
return isInside(cwd, abs) && abs !== cwd ? abs : null;
|
|
17
22
|
}
|
|
18
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
|
+
|
|
39
|
+
const runBiome = (biome, args, options) =>
|
|
40
|
+
runProcess(biome[0], [...biome.slice(1), ...args], options);
|
|
41
|
+
|
|
19
42
|
async function runCheck(cwd, biome) {
|
|
20
43
|
const t0 = performance.now();
|
|
21
44
|
console.log("… running biome check");
|
|
22
|
-
const result = await
|
|
45
|
+
const result = await runBiome(biome, ["check", "--reporter=json", "."], { cwd });
|
|
23
46
|
const out = result.stdout.toString().trim();
|
|
24
47
|
console.log(
|
|
25
48
|
`… biome finished in ${((performance.now() - t0) / 1000).toFixed(1)}s (${(out.length / 1024).toFixed(0)} KB of output)`,
|
|
26
49
|
);
|
|
27
50
|
const start = out.indexOf("{");
|
|
28
51
|
if (start === -1) {
|
|
29
|
-
|
|
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
|
+
);
|
|
30
65
|
}
|
|
31
|
-
const report = JSON.parse(out.slice(start));
|
|
32
66
|
|
|
33
67
|
const byFile = new Map();
|
|
34
68
|
for (const d of report.diagnostics ?? []) {
|
|
@@ -55,6 +89,8 @@ async function runCheck(cwd, biome) {
|
|
|
55
89
|
files,
|
|
56
90
|
totalFiles: byFile.size,
|
|
57
91
|
truncated: byFile.size > MAX_FILES,
|
|
92
|
+
exitCode: result.code,
|
|
93
|
+
ms: Math.round(performance.now() - t0),
|
|
58
94
|
};
|
|
59
95
|
}
|
|
60
96
|
|
|
@@ -63,11 +99,11 @@ async function runCheck(cwd, biome) {
|
|
|
63
99
|
async function getDiff(cwd, biome, file) {
|
|
64
100
|
const abs = safePath(cwd, file);
|
|
65
101
|
if (!abs || !existsSync(abs)) throw new Error(`Invalid file: ${file}`);
|
|
66
|
-
const original = await
|
|
67
|
-
const res = await
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
102
|
+
const original = await readFile(abs, "utf8");
|
|
103
|
+
const res = await runBiome(biome, ["check", "--write", `--stdin-file-path=${file}`], {
|
|
104
|
+
cwd,
|
|
105
|
+
input: original,
|
|
106
|
+
});
|
|
71
107
|
const text = res.stdout.toString();
|
|
72
108
|
const fixed = text.length > 0 ? text : null;
|
|
73
109
|
return { original, fixed, fixable: fixed !== null && fixed !== original };
|
|
@@ -77,107 +113,224 @@ async function applyFixes(cwd, biome, files) {
|
|
|
77
113
|
const paths = files.map((file) => {
|
|
78
114
|
const abs = safePath(cwd, file);
|
|
79
115
|
if (!abs || !existsSync(abs)) throw new Error(`Invalid file: ${file}`);
|
|
80
|
-
return file;
|
|
116
|
+
return file.startsWith("-") ? `./${file}` : file;
|
|
81
117
|
});
|
|
82
118
|
const CHUNK = 100;
|
|
83
119
|
for (let i = 0; i < paths.length; i += CHUNK) {
|
|
84
|
-
await
|
|
120
|
+
await runBiome(biome, ["check", "--write", ...paths.slice(i, i + CHUNK)], { cwd });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
class HttpError extends Error {
|
|
125
|
+
constructor(status, message) {
|
|
126
|
+
super(message);
|
|
127
|
+
this.status = status;
|
|
85
128
|
}
|
|
86
129
|
}
|
|
87
130
|
|
|
131
|
+
function sendJson(res, status, body) {
|
|
132
|
+
const data = JSON.stringify(body);
|
|
133
|
+
res.writeHead(status, {
|
|
134
|
+
"content-type": "application/json; charset=utf-8",
|
|
135
|
+
"cache-control": "no-store",
|
|
136
|
+
});
|
|
137
|
+
res.end(data);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function readJson(req) {
|
|
141
|
+
const chunks = [];
|
|
142
|
+
let size = 0;
|
|
143
|
+
for await (const chunk of req) {
|
|
144
|
+
size += chunk.length;
|
|
145
|
+
if (size > MAX_BODY) throw new HttpError(413, "Request body too large");
|
|
146
|
+
chunks.push(chunk);
|
|
147
|
+
}
|
|
148
|
+
if (size === 0) return {};
|
|
149
|
+
try {
|
|
150
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
151
|
+
} catch {
|
|
152
|
+
throw new HttpError(400, "Invalid JSON");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function openBrowser(address) {
|
|
157
|
+
const [command, args] =
|
|
158
|
+
process.platform === "darwin"
|
|
159
|
+
? ["open", [address]]
|
|
160
|
+
: process.platform === "win32"
|
|
161
|
+
? ["cmd", ["/c", "start", '""', address]]
|
|
162
|
+
: ["xdg-open", [address]];
|
|
163
|
+
try {
|
|
164
|
+
const child = spawnPrepared(command, args, { stdio: "ignore", detached: true });
|
|
165
|
+
child.on("error", () => {});
|
|
166
|
+
child.unref();
|
|
167
|
+
} catch {}
|
|
168
|
+
}
|
|
169
|
+
|
|
88
170
|
export async function startUi({ cwd, biome, kitDir, port, open, pmName, pkg }) {
|
|
89
171
|
let ciBusy = false;
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
172
|
+
let checkBusy = false;
|
|
173
|
+
const token = randomUUID();
|
|
174
|
+
const checksFor = () => resolveChecks({ cwd, biome, pmName, pkg });
|
|
175
|
+
const html = await readFile(join(kitDir, "ui", "index.html"), "utf8");
|
|
176
|
+
|
|
177
|
+
async function handle(req, res) {
|
|
178
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
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
|
+
|
|
186
|
+
if (req.method === "GET" && url.pathname === "/") {
|
|
187
|
+
if (!tokenMatches(url.searchParams.get("t"), token)) {
|
|
188
|
+
res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
|
|
189
|
+
res.end("Forbidden: open the URL printed by `lintlane ui`.");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const nonce = randomBytes(16).toString("base64");
|
|
193
|
+
res.writeHead(200, {
|
|
194
|
+
"content-type": "text/html; charset=utf-8",
|
|
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("; "),
|
|
209
|
+
});
|
|
210
|
+
res.end(html.replace("__TOKEN__", token).replaceAll("<script", `<script nonce="${nonce}"`));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (url.pathname.startsWith("/api/")) {
|
|
215
|
+
if (!tokenMatches(req.headers["x-lintlane-token"], token)) {
|
|
216
|
+
sendJson(res, 401, { error: "Unauthorized" });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (req.method === "POST" && url.pathname === "/api/check") {
|
|
220
|
+
if (checkBusy) {
|
|
221
|
+
sendJson(res, 409, { error: "A check is already running" });
|
|
222
|
+
return;
|
|
104
223
|
}
|
|
105
|
-
|
|
106
|
-
|
|
224
|
+
checkBusy = true;
|
|
225
|
+
try {
|
|
226
|
+
sendJson(res, 200, await runCheck(cwd, biome));
|
|
227
|
+
} finally {
|
|
228
|
+
checkBusy = false;
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (req.method === "POST" && url.pathname === "/api/fix") {
|
|
233
|
+
const { files } = await readJson(req);
|
|
234
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
235
|
+
sendJson(res, 400, { error: "No files given" });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
await applyFixes(cwd, biome, files);
|
|
239
|
+
sendJson(res, 200, { fixed: files.length });
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (req.method === "POST" && url.pathname === "/api/ci/steps") {
|
|
243
|
+
const { steps, problems } = checksFor();
|
|
244
|
+
sendJson(res, 200, {
|
|
245
|
+
pmName,
|
|
246
|
+
problems,
|
|
247
|
+
busy: ciBusy,
|
|
248
|
+
steps: steps.map(({ id, label, display, defaultOn, note }) => ({
|
|
249
|
+
id,
|
|
250
|
+
label,
|
|
251
|
+
display,
|
|
252
|
+
defaultOn,
|
|
253
|
+
note,
|
|
254
|
+
})),
|
|
107
255
|
});
|
|
256
|
+
return;
|
|
108
257
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return
|
|
258
|
+
if (req.method === "POST" && url.pathname === "/api/ci/run") {
|
|
259
|
+
if (ciBusy) {
|
|
260
|
+
sendJson(res, 409, { error: "A CI run is already in progress" });
|
|
261
|
+
return;
|
|
113
262
|
}
|
|
263
|
+
const { steps: ids, stopOnFail } = await readJson(req);
|
|
264
|
+
const known = checksFor().steps;
|
|
265
|
+
const steps = Array.isArray(ids) ? known.filter((step) => ids.includes(step.id)) : [];
|
|
266
|
+
if (steps.length === 0) {
|
|
267
|
+
sendJson(res, 400, { error: "No steps selected" });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
ciBusy = true;
|
|
271
|
+
const controller = new AbortController();
|
|
272
|
+
// The browser's Stop button (or closing the tab) ends the response early.
|
|
273
|
+
res.on("close", () => {
|
|
274
|
+
if (!res.writableFinished) controller.abort();
|
|
275
|
+
});
|
|
276
|
+
res.writeHead(200, { "content-type": "application/x-ndjson", "cache-control": "no-store" });
|
|
114
277
|
try {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
const steps = detectCiSteps({ cwd, biome, pmName, pkg });
|
|
128
|
-
return Response.json({
|
|
129
|
-
pmName,
|
|
130
|
-
steps: steps.map(({ id, label, display, defaultOn, note }) => ({
|
|
131
|
-
id,
|
|
132
|
-
label,
|
|
133
|
-
display,
|
|
134
|
-
defaultOn,
|
|
135
|
-
note,
|
|
136
|
-
})),
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
if (req.method === "POST" && url.pathname === "/api/ci/run") {
|
|
140
|
-
if (ciBusy)
|
|
141
|
-
return Response.json({ error: "A CI run is already in progress" }, { status: 409 });
|
|
142
|
-
const { steps: ids, stopOnFail } = await req.json();
|
|
143
|
-
const known = detectCiSteps({ cwd, biome, pmName, pkg });
|
|
144
|
-
const steps = Array.isArray(ids) ? known.filter((step) => ids.includes(step.id)) : [];
|
|
145
|
-
if (steps.length === 0)
|
|
146
|
-
return Response.json({ error: "No steps selected" }, { status: 400 });
|
|
147
|
-
ciBusy = true;
|
|
148
|
-
const stream = ciStream({
|
|
149
|
-
cwd,
|
|
150
|
-
steps,
|
|
151
|
-
stopOnFail: stopOnFail !== false,
|
|
152
|
-
release: () => {
|
|
153
|
-
ciBusy = false;
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
return new Response(stream, {
|
|
157
|
-
headers: { "content-type": "application/x-ndjson", "cache-control": "no-store" },
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
if (req.method === "POST" && url.pathname === "/api/diff") {
|
|
161
|
-
const { file } = await req.json();
|
|
162
|
-
return Response.json(await getDiff(cwd, biome, file));
|
|
163
|
-
}
|
|
164
|
-
} catch (error) {
|
|
165
|
-
return Response.json({ error: error.message }, { status: 500 });
|
|
278
|
+
await runCiSteps({
|
|
279
|
+
cwd,
|
|
280
|
+
steps,
|
|
281
|
+
stopOnFail: stopOnFail !== false,
|
|
282
|
+
signal: controller.signal,
|
|
283
|
+
send: (event) => {
|
|
284
|
+
if (!res.destroyed) res.write(`${JSON.stringify(event)}\n`);
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
} finally {
|
|
288
|
+
ciBusy = false;
|
|
289
|
+
if (!res.destroyed) res.end();
|
|
166
290
|
}
|
|
291
|
+
return;
|
|
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
|
+
}
|
|
298
|
+
if (req.method === "POST" && url.pathname === "/api/diff") {
|
|
299
|
+
const { file } = await readJson(req);
|
|
300
|
+
sendJson(res, 200, await getDiff(cwd, biome, file));
|
|
301
|
+
return;
|
|
167
302
|
}
|
|
303
|
+
}
|
|
168
304
|
|
|
169
|
-
|
|
170
|
-
|
|
305
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
306
|
+
res.end("Not found");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const server = createServer((req, res) => {
|
|
310
|
+
handle(req, res).catch((error) => {
|
|
311
|
+
if (res.headersSent) {
|
|
312
|
+
res.destroy();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
sendJson(res, error instanceof HttpError ? error.status : 500, { error: error.message });
|
|
316
|
+
});
|
|
171
317
|
});
|
|
172
318
|
|
|
173
|
-
|
|
319
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
320
|
+
server.once("error", (error) => {
|
|
321
|
+
rejectListen(
|
|
322
|
+
error.code === "EADDRINUSE"
|
|
323
|
+
? new Error(`Port ${port} is already in use. Pick another with --port <n>.`)
|
|
324
|
+
: error,
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
server.listen(port, "127.0.0.1", resolveListen);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const address = `http://127.0.0.1:${server.address().port}/?t=${token}`;
|
|
174
331
|
console.log(`✔ Lintlane dashboard running at ${address}`);
|
|
175
332
|
console.log(" Press Ctrl+C to stop.");
|
|
176
333
|
|
|
177
|
-
if (open)
|
|
178
|
-
const opener =
|
|
179
|
-
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
180
|
-
await $`${opener} ${address}`.quiet().nothrow();
|
|
181
|
-
}
|
|
334
|
+
if (open) openBrowser(address);
|
|
182
335
|
return server;
|
|
183
336
|
}
|
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",
|
|
@@ -22,7 +24,9 @@
|
|
|
22
24
|
],
|
|
23
25
|
"scripts": {
|
|
24
26
|
"check": "biome check .",
|
|
25
|
-
"check:fix": "biome check --write ."
|
|
27
|
+
"check:fix": "biome check --write .",
|
|
28
|
+
"test": "node --test \"test/*.test.mjs\"",
|
|
29
|
+
"test:e2e": "LINTLANE_E2E=1 node --test \"test/e2e.test.mjs\""
|
|
26
30
|
},
|
|
27
31
|
"peerDependencies": {
|
|
28
32
|
"@biomejs/biome": ">=2.2.0 <3"
|
|
@@ -34,14 +38,25 @@
|
|
|
34
38
|
"access": "public"
|
|
35
39
|
},
|
|
36
40
|
"engines": {
|
|
37
|
-
"
|
|
41
|
+
"node": ">=22"
|
|
38
42
|
},
|
|
39
43
|
"keywords": [
|
|
40
44
|
"lintlane",
|
|
41
45
|
"biome",
|
|
42
46
|
"react",
|
|
43
47
|
"lint",
|
|
44
|
-
"format"
|
|
48
|
+
"format",
|
|
49
|
+
"lint-staged",
|
|
50
|
+
"husky",
|
|
51
|
+
"commitlint",
|
|
52
|
+
"commitizen"
|
|
45
53
|
],
|
|
46
|
-
"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
|
+
}
|
|
47
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,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 "$@"
|