lintlane 2.5.0 → 3.0.1
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 +223 -97
- package/bin/ci.mjs +82 -76
- package/bin/cli.mjs +102 -180
- package/bin/pre-commit.mjs +55 -0
- package/bin/proc.mjs +113 -0
- package/bin/project.mjs +180 -0
- package/bin/staged.mjs +333 -0
- package/bin/ui-server.mjs +175 -89
- package/package.json +5 -3
- package/templates/pre-commit +5 -2
package/bin/ci.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3
|
+
import { binPath, spawnPrepared } from "./proc.mjs";
|
|
3
4
|
|
|
4
5
|
const MAX_LOG_CHARS = 2_000_000;
|
|
5
6
|
|
|
@@ -30,6 +31,7 @@ function firstScript(scripts, names) {
|
|
|
30
31
|
* and the browser only ever sends step ids back — never command lines.
|
|
31
32
|
*/
|
|
32
33
|
export function detectCiSteps({ cwd, biome, pmName, pkg }) {
|
|
34
|
+
// `biome` is an argv prefix: [path] or [node, script] (see resolveBiome).
|
|
33
35
|
const scripts = pkg?.scripts ?? {};
|
|
34
36
|
const run = RUN_SCRIPT[pmName] ?? RUN_SCRIPT.npm;
|
|
35
37
|
const steps = [];
|
|
@@ -45,7 +47,7 @@ export function detectCiSteps({ cwd, biome, pmName, pkg }) {
|
|
|
45
47
|
steps.push({
|
|
46
48
|
id: "biome",
|
|
47
49
|
label: "Biome CI (lint + format)",
|
|
48
|
-
cmd: [biome, "ci", "--colors=off", "."],
|
|
50
|
+
cmd: [...biome, "ci", "--colors=off", "."],
|
|
49
51
|
display: "biome ci .",
|
|
50
52
|
defaultOn: true,
|
|
51
53
|
note: "The same check the GitHub workflow runs.",
|
|
@@ -59,14 +61,11 @@ export function detectCiSteps({ cwd, biome, pmName, pkg }) {
|
|
|
59
61
|
cmd: run(typecheck),
|
|
60
62
|
defaultOn: true,
|
|
61
63
|
});
|
|
62
|
-
} else if (
|
|
63
|
-
existsSync(join(cwd, "tsconfig.json")) &&
|
|
64
|
-
existsSync(join(cwd, "node_modules", ".bin", "tsc"))
|
|
65
|
-
) {
|
|
64
|
+
} else if (existsSync(`${cwd}/tsconfig.json`) && existsSync(binPath(cwd, "tsc"))) {
|
|
66
65
|
steps.push({
|
|
67
66
|
id: "typecheck",
|
|
68
67
|
label: "Typecheck",
|
|
69
|
-
cmd: [
|
|
68
|
+
cmd: [binPath(cwd, "tsc"), "--noEmit"],
|
|
70
69
|
display: "tsc --noEmit",
|
|
71
70
|
defaultOn: true,
|
|
72
71
|
});
|
|
@@ -83,80 +82,87 @@ export function detectCiSteps({ cwd, biome, pmName, pkg }) {
|
|
|
83
82
|
return steps.map((step) => ({ ...step, display: step.display ?? step.cmd.join(" ") }));
|
|
84
83
|
}
|
|
85
84
|
|
|
85
|
+
// Kill the step and everything it started (a build tool's child processes too).
|
|
86
|
+
function killTree(child) {
|
|
87
|
+
if (!child || child.exitCode !== null) return;
|
|
88
|
+
try {
|
|
89
|
+
if (process.platform === "win32") child.kill();
|
|
90
|
+
else process.kill(-child.pid, "SIGTERM");
|
|
91
|
+
} catch {
|
|
92
|
+
try {
|
|
93
|
+
child.kill();
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
86
98
|
/**
|
|
87
|
-
* Run steps one after another
|
|
99
|
+
* Run steps one after another, reporting events through `send`:
|
|
88
100
|
* start / log / end / skip / done
|
|
89
|
-
*
|
|
101
|
+
* Aborting `signal` (the browser's Stop button, or the page closing) kills the
|
|
102
|
+
* running step. Resolves when the run is over; never rejects.
|
|
90
103
|
*/
|
|
91
|
-
export function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
for (const step of steps) {
|
|
105
|
-
if (cancelled) break;
|
|
106
|
-
if (failed && stopOnFail) {
|
|
107
|
-
send({ type: "skip", id: step.id });
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
104
|
+
export async function runCiSteps({ cwd, steps, stopOnFail, send, signal }) {
|
|
105
|
+
let child = null;
|
|
106
|
+
const onAbort = () => killTree(child);
|
|
107
|
+
signal?.addEventListener("abort", onAbort);
|
|
108
|
+
let failed = false;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
for (const step of steps) {
|
|
112
|
+
if (signal?.aborted) return;
|
|
113
|
+
if (failed && stopOnFail) {
|
|
114
|
+
send({ type: "skip", id: step.id });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
110
117
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
send({ type: "log", id: step.id, text: "\n… output truncated …\n" });
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
proc = Bun.spawn(step.cmd, {
|
|
133
|
-
cwd,
|
|
134
|
-
env: { ...process.env, CI: "true", NO_COLOR: "1", FORCE_COLOR: "0" },
|
|
135
|
-
stdin: "ignore",
|
|
136
|
-
stdout: "pipe",
|
|
137
|
-
stderr: "pipe",
|
|
138
|
-
});
|
|
139
|
-
await Promise.all([pump(proc.stdout), pump(proc.stderr)]);
|
|
140
|
-
code = await proc.exited;
|
|
141
|
-
} catch (error) {
|
|
142
|
-
send({ type: "log", id: step.id, text: `\n${error.message}\n` });
|
|
143
|
-
code = 127;
|
|
118
|
+
send({ type: "start", id: step.id });
|
|
119
|
+
const t0 = performance.now();
|
|
120
|
+
let sent = 0;
|
|
121
|
+
let truncated = false;
|
|
122
|
+
|
|
123
|
+
const pump = (stream) => {
|
|
124
|
+
const decoder = new StringDecoder("utf8");
|
|
125
|
+
stream.on("data", (chunk) => {
|
|
126
|
+
const text = decoder.write(chunk);
|
|
127
|
+
if (sent < MAX_LOG_CHARS) {
|
|
128
|
+
sent += text.length;
|
|
129
|
+
send({ type: "log", id: step.id, text });
|
|
130
|
+
} else if (!truncated) {
|
|
131
|
+
truncated = true;
|
|
132
|
+
send({ type: "log", id: step.id, text: "\n… output truncated …\n" });
|
|
144
133
|
}
|
|
145
|
-
|
|
134
|
+
});
|
|
135
|
+
};
|
|
146
136
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
137
|
+
const code = await new Promise((resolve) => {
|
|
138
|
+
try {
|
|
139
|
+
child = spawnPrepared(step.cmd[0], step.cmd.slice(1), {
|
|
140
|
+
cwd,
|
|
141
|
+
env: { ...process.env, CI: "true", NO_COLOR: "1", FORCE_COLOR: "0" },
|
|
142
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
143
|
+
detached: process.platform !== "win32", // own process group, so Stop can kill the tree
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
send({ type: "log", id: step.id, text: `\n${error.message}\n` });
|
|
147
|
+
resolve(127);
|
|
148
|
+
return;
|
|
150
149
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
150
|
+
pump(child.stdout);
|
|
151
|
+
pump(child.stderr);
|
|
152
|
+
child.on("error", (error) => {
|
|
153
|
+
send({ type: "log", id: step.id, text: `\n${error.message}\n` });
|
|
154
|
+
resolve(127);
|
|
155
|
+
});
|
|
156
|
+
child.on("close", (exitCode, killedBy) => resolve(exitCode ?? (killedBy ? 143 : 1)));
|
|
157
|
+
});
|
|
158
|
+
child = null;
|
|
159
|
+
|
|
160
|
+
if (signal?.aborted) return;
|
|
161
|
+
if (code !== 0) failed = true;
|
|
162
|
+
send({ type: "end", id: step.id, code, ms: Math.round(performance.now() - t0) });
|
|
163
|
+
}
|
|
164
|
+
send({ type: "done" });
|
|
165
|
+
} finally {
|
|
166
|
+
signal?.removeEventListener("abort", onAbort);
|
|
167
|
+
}
|
|
162
168
|
}
|
package/bin/cli.mjs
CHANGED
|
@@ -1,68 +1,27 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import {
|
|
3
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
4
6
|
import { parseArgs } from "node:util";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import { binPath, resolveBiome, runInherit, runProcess } from "./proc.mjs";
|
|
8
|
+
import {
|
|
9
|
+
detectPackageManager,
|
|
10
|
+
detectPreset,
|
|
11
|
+
hasUnsafeRestaging,
|
|
12
|
+
installCommand,
|
|
13
|
+
isLegacyPreCommit,
|
|
14
|
+
PRESET_PATHS,
|
|
15
|
+
renderWorkflow,
|
|
16
|
+
runnerCommand,
|
|
17
|
+
} from "./project.mjs";
|
|
18
|
+
|
|
19
|
+
const KIT_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
|
+
const readJson = async (path) => JSON.parse(await readFile(path, "utf8"));
|
|
21
|
+
const KIT_PKG = await readJson(join(KIT_DIR, "package.json"));
|
|
9
22
|
const KIT_NAME = KIT_PKG.name;
|
|
10
23
|
const KIT_VERSION = KIT_PKG.version;
|
|
11
|
-
|
|
12
|
-
const PRESET_PATHS = {
|
|
13
|
-
base: "biome",
|
|
14
|
-
strict: "biome/strict",
|
|
15
|
-
next: "biome/next",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const PACKAGE_MANAGERS = {
|
|
19
|
-
bun: {
|
|
20
|
-
lockfile: "bun.lock",
|
|
21
|
-
install: (pkgs) => ["bun", "add", "--dev", "--exact", ...pkgs],
|
|
22
|
-
runCheckCi: "bunx biome ci --reporter=github .",
|
|
23
|
-
setupSteps: [
|
|
24
|
-
" - uses: oven-sh/setup-bun@v2",
|
|
25
|
-
" - run: bun install --frozen-lockfile",
|
|
26
|
-
],
|
|
27
|
-
},
|
|
28
|
-
npm: {
|
|
29
|
-
lockfile: "package-lock.json",
|
|
30
|
-
install: (pkgs) => ["npm", "install", "--save-dev", "--save-exact", ...pkgs],
|
|
31
|
-
runCheckCi: "npx biome ci --reporter=github .",
|
|
32
|
-
setupSteps: [
|
|
33
|
-
" - uses: actions/setup-node@v4",
|
|
34
|
-
" with:",
|
|
35
|
-
" node-version: 20",
|
|
36
|
-
" cache: npm",
|
|
37
|
-
" - run: npm ci",
|
|
38
|
-
],
|
|
39
|
-
},
|
|
40
|
-
pnpm: {
|
|
41
|
-
lockfile: "pnpm-lock.yaml",
|
|
42
|
-
install: (pkgs) => ["pnpm", "add", "--save-dev", "--save-exact", ...pkgs],
|
|
43
|
-
runCheckCi: "pnpm exec biome ci --reporter=github .",
|
|
44
|
-
setupSteps: [
|
|
45
|
-
" - uses: pnpm/action-setup@v4",
|
|
46
|
-
" - uses: actions/setup-node@v4",
|
|
47
|
-
" with:",
|
|
48
|
-
" node-version: 20",
|
|
49
|
-
" cache: pnpm",
|
|
50
|
-
" - run: pnpm install --frozen-lockfile",
|
|
51
|
-
],
|
|
52
|
-
},
|
|
53
|
-
yarn: {
|
|
54
|
-
lockfile: "yarn.lock",
|
|
55
|
-
install: (pkgs) => ["yarn", "add", "--dev", "--exact", ...pkgs],
|
|
56
|
-
runCheckCi: "yarn biome ci --reporter=github .",
|
|
57
|
-
setupSteps: [
|
|
58
|
-
" - uses: actions/setup-node@v4",
|
|
59
|
-
" with:",
|
|
60
|
-
" node-version: 20",
|
|
61
|
-
" cache: yarn",
|
|
62
|
-
" - run: yarn install --frozen-lockfile",
|
|
63
|
-
],
|
|
64
|
-
},
|
|
65
|
-
};
|
|
24
|
+
const RUNNER = runnerCommand();
|
|
66
25
|
|
|
67
26
|
const BIOME_SUBCOMMANDS = {
|
|
68
27
|
check: (paths) => ["check", ...paths],
|
|
@@ -70,18 +29,7 @@ const BIOME_SUBCOMMANDS = {
|
|
|
70
29
|
ci: (paths) => ["ci", "--reporter=github", ...paths],
|
|
71
30
|
};
|
|
72
31
|
|
|
73
|
-
|
|
74
|
-
// lint domain — the others are still React under the hood, so they map to
|
|
75
|
-
// the "base" preset, but we name what we found instead of guessing silently.
|
|
76
|
-
const FRAMEWORK_DETECTORS = [
|
|
77
|
-
{ label: "Next.js", preset: "next", deps: ["next"] },
|
|
78
|
-
{ label: "TanStack Start", preset: "base", deps: ["@tanstack/react-start", "@tanstack/start"] },
|
|
79
|
-
{ label: "React Router (framework mode)", preset: "base", deps: ["@react-router/dev"] },
|
|
80
|
-
{ label: "Remix", preset: "base", deps: ["@remix-run/react", "@remix-run/node"] },
|
|
81
|
-
{ label: "React", preset: "base", deps: ["react"] },
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
const HELP = `Usage: bunx ${KIT_NAME} <command> [options]
|
|
32
|
+
const HELP = `Usage: ${RUNNER} ${KIT_NAME} <command> [options]
|
|
85
33
|
|
|
86
34
|
Commands:
|
|
87
35
|
init [options] Set up Biome, scripts, hooks, and CI in the current project
|
|
@@ -116,13 +64,14 @@ auto-selected — pass --preset strict explicitly when you want it.
|
|
|
116
64
|
|
|
117
65
|
check / check:fix / ci run the Biome binary already installed in this
|
|
118
66
|
project's node_modules — you never need to call biome yourself. The
|
|
119
|
-
package manager (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
\`
|
|
67
|
+
package manager (npm, pnpm, yarn, or bun) used by \`init\` is detected from
|
|
68
|
+
the "packageManager" field in package.json, then from whichever lockfile is
|
|
69
|
+
present, then from the tool you launched lintlane with (npx, pnpm dlx, yarn
|
|
70
|
+
dlx, bunx). npm is used when none of those apply.
|
|
71
|
+
|
|
72
|
+
After \`init\` the \`lintlane\` command is installed in your project, for
|
|
73
|
+
example: \`npx lintlane check\`, \`pnpm exec lintlane check\`,
|
|
74
|
+
\`yarn lintlane check\`, \`bunx lintlane check\`.
|
|
126
75
|
`;
|
|
127
76
|
|
|
128
77
|
function fail(message) {
|
|
@@ -138,32 +87,9 @@ function warn(message) {
|
|
|
138
87
|
console.log(`! ${message}`);
|
|
139
88
|
}
|
|
140
89
|
|
|
141
|
-
function detectPackageManager(cwd, pkg) {
|
|
142
|
-
const declared = pkg.packageManager?.split("@")[0];
|
|
143
|
-
if (declared && declared in PACKAGE_MANAGERS) return declared;
|
|
144
|
-
for (const [name, pm] of Object.entries(PACKAGE_MANAGERS)) {
|
|
145
|
-
if (existsSync(join(cwd, pm.lockfile))) return name;
|
|
146
|
-
}
|
|
147
|
-
return "bun";
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function detectPreset(pkg) {
|
|
151
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
152
|
-
for (const detector of FRAMEWORK_DETECTORS) {
|
|
153
|
-
if (detector.deps.some((dep) => dep in deps)) {
|
|
154
|
-
return { preset: detector.preset, label: detector.label };
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return { preset: "base", label: null };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function binPath(cwd, name) {
|
|
161
|
-
return join(cwd, "node_modules", ".bin", name);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
90
|
// Everything after a bare "--" belongs to the subcommand (e.g. `commit -- -a -S`
|
|
165
91
|
// hands "-a -S" to git commit), so it never reaches our own option parsing.
|
|
166
|
-
const rawArgs =
|
|
92
|
+
const rawArgs = process.argv.slice(2);
|
|
167
93
|
const doubleDash = rawArgs.indexOf("--");
|
|
168
94
|
const ownArgs = doubleDash < 0 ? rawArgs : rawArgs.slice(0, doubleDash);
|
|
169
95
|
const passthroughArgs = doubleDash < 0 ? [] : rawArgs.slice(doubleDash + 1);
|
|
@@ -195,18 +121,13 @@ if (values.help && !["commit", "c", "check-commit"].includes(command)) {
|
|
|
195
121
|
}
|
|
196
122
|
|
|
197
123
|
if (command in BIOME_SUBCOMMANDS) {
|
|
198
|
-
const biome =
|
|
199
|
-
if (!
|
|
200
|
-
fail(`Biome isn't installed in this project. Run:
|
|
124
|
+
const biome = resolveBiome(cwd);
|
|
125
|
+
if (!biome) {
|
|
126
|
+
fail(`Biome isn't installed in this project. Run: ${RUNNER} ${KIT_NAME} init`);
|
|
201
127
|
}
|
|
202
128
|
const extraPaths = positionals.slice(1);
|
|
203
129
|
const biomeArgs = BIOME_SUBCOMMANDS[command](extraPaths.length ? extraPaths : ["."]);
|
|
204
|
-
|
|
205
|
-
await $`${biome} ${biomeArgs}`.cwd(cwd);
|
|
206
|
-
} catch (error) {
|
|
207
|
-
process.exit(error.exitCode ?? 1);
|
|
208
|
-
}
|
|
209
|
-
process.exit(0);
|
|
130
|
+
process.exit(await runInherit(biome[0], [...biome.slice(1), ...biomeArgs], { cwd }));
|
|
210
131
|
}
|
|
211
132
|
|
|
212
133
|
if (command === "commit" || command === "c") {
|
|
@@ -220,28 +141,32 @@ if (command === "check-commit") {
|
|
|
220
141
|
}
|
|
221
142
|
|
|
222
143
|
if (command === "ui") {
|
|
223
|
-
const biome =
|
|
224
|
-
if (!
|
|
225
|
-
fail(`Biome isn't installed in this project. Run:
|
|
144
|
+
const biome = resolveBiome(cwd);
|
|
145
|
+
if (!biome) {
|
|
146
|
+
fail(`Biome isn't installed in this project. Run: ${RUNNER} ${KIT_NAME} init`);
|
|
226
147
|
}
|
|
227
148
|
const { startUi } = await import("./ui-server.mjs");
|
|
228
149
|
const uiPkgPath = join(cwd, "package.json");
|
|
229
|
-
const uiPkg = existsSync(uiPkgPath) ? await
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
150
|
+
const uiPkg = existsSync(uiPkgPath) ? await readJson(uiPkgPath) : {};
|
|
151
|
+
try {
|
|
152
|
+
await startUi({
|
|
153
|
+
cwd,
|
|
154
|
+
biome,
|
|
155
|
+
pkg: uiPkg,
|
|
156
|
+
pmName: detectPackageManager(cwd, uiPkg).name,
|
|
157
|
+
kitDir: KIT_DIR,
|
|
158
|
+
port: values.port === undefined ? 4321 : Number(values.port),
|
|
159
|
+
open: !values["no-open"],
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
fail(error.message);
|
|
163
|
+
}
|
|
239
164
|
await new Promise(() => {});
|
|
240
165
|
}
|
|
241
166
|
|
|
242
167
|
if (command !== "init") {
|
|
243
168
|
fail(
|
|
244
|
-
`Unknown command "${command}". Usage:
|
|
169
|
+
`Unknown command "${command}". Usage: ${RUNNER} ${KIT_NAME} <init|check|check:fix|ci|commit|check-commit|ui> [options]`,
|
|
245
170
|
);
|
|
246
171
|
}
|
|
247
172
|
|
|
@@ -256,10 +181,9 @@ if (!existsSync(pkgPath)) {
|
|
|
256
181
|
fail("No package.json found in the current directory.");
|
|
257
182
|
}
|
|
258
183
|
|
|
259
|
-
|
|
260
|
-
const pmName = detectPackageManager(cwd, pkg);
|
|
261
|
-
|
|
262
|
-
ok(`Detected package manager: ${pmName}`);
|
|
184
|
+
let pkg = await readJson(pkgPath);
|
|
185
|
+
const { name: pmName, source: pmSource } = detectPackageManager(cwd, pkg);
|
|
186
|
+
ok(`Detected package manager: ${pmName} (${pmSource})`);
|
|
263
187
|
|
|
264
188
|
let preset = values.preset;
|
|
265
189
|
if (preset === undefined) {
|
|
@@ -272,20 +196,31 @@ if (preset === undefined) {
|
|
|
272
196
|
}
|
|
273
197
|
}
|
|
274
198
|
|
|
275
|
-
|
|
199
|
+
// LINTLANE_INSTALL_SPEC lets you install lintlane from somewhere other than the
|
|
200
|
+
// registry (a tarball from `npm pack`, a local folder) when testing a build.
|
|
201
|
+
const kitSpec = process.env.LINTLANE_INSTALL_SPEC || `${KIT_NAME}@${KIT_VERSION}`;
|
|
202
|
+
const devPackages = ["@biomejs/biome", kitSpec];
|
|
276
203
|
if (!values["no-hooks"]) devPackages.push("husky");
|
|
277
|
-
const installArgv =
|
|
278
|
-
const installCmd = installArgv.join(" ");
|
|
204
|
+
const installArgv = installCommand(pmName, devPackages);
|
|
279
205
|
|
|
280
206
|
if (values["skip-install"]) {
|
|
281
|
-
warn(`Skipped install. Run: ${
|
|
207
|
+
warn(`Skipped install. Run: ${installArgv.join(" ")}`);
|
|
282
208
|
} else {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
209
|
+
const install = await runProcess(installArgv[0], installArgv.slice(1), { cwd });
|
|
210
|
+
if (install.code !== 0) {
|
|
211
|
+
const reason =
|
|
212
|
+
install.error?.code === "ENOENT"
|
|
213
|
+
? `"${installArgv[0]}" was not found. Install it, or run again with --skip-install and install the packages yourself:\n ${installArgv.join(" ")}`
|
|
214
|
+
: (install.stderr.toString().trim() || install.stdout.toString().trim())
|
|
215
|
+
.split("\n")
|
|
216
|
+
.slice(-8)
|
|
217
|
+
.join("\n");
|
|
218
|
+
fail(`Install failed (${installArgv.join(" ")}):\n${reason}`);
|
|
288
219
|
}
|
|
220
|
+
ok(`Installed ${devPackages.join(", ")}`);
|
|
221
|
+
// The install just rewrote package.json (devDependencies). Re-read it, or the
|
|
222
|
+
// scripts we add below would be written over a stale copy and erase those entries.
|
|
223
|
+
pkg = await readJson(pkgPath);
|
|
289
224
|
}
|
|
290
225
|
|
|
291
226
|
const biomeJsonPath = join(cwd, "biome.json");
|
|
@@ -302,7 +237,7 @@ if (existsSync(biomeJsonPath) || existsSync(biomeJsoncPath)) {
|
|
|
302
237
|
if (preset === "next") {
|
|
303
238
|
config.files = { includes: ["**", "!**/.next", "!**/out", "!**/next-env.d.ts"] };
|
|
304
239
|
}
|
|
305
|
-
await
|
|
240
|
+
await writeFile(biomeJsonPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
306
241
|
ok(`Wrote biome.json (extends ${extendsSpecifier})`);
|
|
307
242
|
}
|
|
308
243
|
|
|
@@ -340,7 +275,7 @@ if (!values["no-hooks"]) {
|
|
|
340
275
|
}
|
|
341
276
|
|
|
342
277
|
if (pkgChanged) {
|
|
343
|
-
await
|
|
278
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
344
279
|
}
|
|
345
280
|
|
|
346
281
|
if (!values["no-hooks"]) {
|
|
@@ -355,13 +290,29 @@ if (!values["no-hooks"]) {
|
|
|
355
290
|
|
|
356
291
|
for (const hook of hooks) {
|
|
357
292
|
const hookPath = join(huskyDir, hook);
|
|
293
|
+
const template = await readFile(join(KIT_DIR, "templates", hook), "utf8");
|
|
358
294
|
if (existsSync(hookPath)) {
|
|
359
|
-
|
|
295
|
+
const existing = await readFile(hookPath, "utf8");
|
|
296
|
+
if (hook === "pre-commit" && isLegacyPreCommit(existing)) {
|
|
297
|
+
// The pre-2.6 hook re-staged files with `git update-index --again`, which also
|
|
298
|
+
// stages unstaged hunks of partially staged files. It's ours and untouched, so replace it.
|
|
299
|
+
await writeFile(hookPath, template);
|
|
300
|
+
await chmod(hookPath, 0o755);
|
|
301
|
+
ok("Replaced .husky/pre-commit (the old one could stage changes you hadn't staged)");
|
|
302
|
+
wroteHook = true;
|
|
303
|
+
} else if (hook === "pre-commit" && hasUnsafeRestaging(existing)) {
|
|
304
|
+
warn(
|
|
305
|
+
".husky/pre-commit runs `git update-index --again`, which can stage changes you didn't stage. " +
|
|
306
|
+
"Replace its contents with node_modules/lintlane/templates/pre-commit (or delete it and re-run init).",
|
|
307
|
+
);
|
|
308
|
+
} else {
|
|
309
|
+
warn(`.husky/${hook} already exists — left untouched`);
|
|
310
|
+
}
|
|
360
311
|
continue;
|
|
361
312
|
}
|
|
362
|
-
await
|
|
363
|
-
await
|
|
364
|
-
await
|
|
313
|
+
await mkdir(huskyDir, { recursive: true });
|
|
314
|
+
await writeFile(hookPath, template);
|
|
315
|
+
await chmod(hookPath, 0o755);
|
|
365
316
|
ok(`Wrote .husky/${hook}`);
|
|
366
317
|
wroteHook = true;
|
|
367
318
|
}
|
|
@@ -372,12 +323,9 @@ if (!values["no-hooks"]) {
|
|
|
372
323
|
} else {
|
|
373
324
|
const husky = binPath(cwd, "husky");
|
|
374
325
|
if (existsSync(husky)) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
} catch (error) {
|
|
379
|
-
warn(`husky failed: ${error.message}`);
|
|
380
|
-
}
|
|
326
|
+
const ran = await runProcess(husky, [], { cwd });
|
|
327
|
+
if (ran.code === 0) ok("Ran husky");
|
|
328
|
+
else warn(`husky failed: ${(ran.stderr.toString() || ran.error?.message || "").trim()}`);
|
|
381
329
|
} else {
|
|
382
330
|
warn(`husky binary not found at ${husky} — run it manually once installed`);
|
|
383
331
|
}
|
|
@@ -392,35 +340,9 @@ if (!values["no-ci"]) {
|
|
|
392
340
|
if (existsSync(ciPath)) {
|
|
393
341
|
warn(".github/workflows/biome.yml already exists — left untouched");
|
|
394
342
|
} else {
|
|
395
|
-
// Catches commits that skipped the local hooks (--no-verify, GUI clients, fresh clones).
|
|
396
343
|
const withCommitCheck = !values["no-commit-lint"] && !values["no-hooks"];
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
? [
|
|
400
|
-
"",
|
|
401
|
-
" - name: Check commit messages",
|
|
402
|
-
" if: github.event_name == 'pull_request'",
|
|
403
|
-
" run: node node_modules/lintlane/bin/check-commit.mjs --rev-range origin/${{ github.base_ref }}..HEAD",
|
|
404
|
-
].join("\n")
|
|
405
|
-
: "";
|
|
406
|
-
const workflow = `name: Biome
|
|
407
|
-
|
|
408
|
-
on:
|
|
409
|
-
pull_request:
|
|
410
|
-
push:
|
|
411
|
-
branches:
|
|
412
|
-
- main
|
|
413
|
-
|
|
414
|
-
jobs:
|
|
415
|
-
biome:
|
|
416
|
-
runs-on: ubuntu-latest
|
|
417
|
-
steps:
|
|
418
|
-
- uses: actions/checkout@v4${commitCheckCheckout}
|
|
419
|
-
${pm.setupSteps.join("\n")}${commitCheckStep}
|
|
420
|
-
- run: ${pm.runCheckCi}
|
|
421
|
-
`;
|
|
422
|
-
await $`mkdir -p ${workflowsDir}`.quiet();
|
|
423
|
-
await Bun.write(ciPath, workflow);
|
|
344
|
+
await mkdir(workflowsDir, { recursive: true });
|
|
345
|
+
await writeFile(ciPath, renderWorkflow({ pmName, kitName: KIT_NAME, withCommitCheck }));
|
|
424
346
|
ok("Wrote .github/workflows/biome.yml");
|
|
425
347
|
}
|
|
426
348
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// git `pre-commit` hook entry: formats and lints the staged files with Biome
|
|
3
|
+
// without ever staging anything the developer didn't stage (see staged.mjs).
|
|
4
|
+
//
|
|
5
|
+
// node pre-commit.mjs run the checks (what the hook does)
|
|
6
|
+
// node pre-commit.mjs --restore recover working files after an interrupted run
|
|
7
|
+
import { resolveBiome, runProcess } from "./proc.mjs";
|
|
8
|
+
import { restoreFromBackup, runStagedChecks, StaleBackupError } from "./staged.mjs";
|
|
9
|
+
|
|
10
|
+
const out = (text) => process.stdout.write(text);
|
|
11
|
+
const err = (text) => process.stderr.write(text);
|
|
12
|
+
|
|
13
|
+
const top = await runProcess("git", ["rev-parse", "--show-toplevel"]);
|
|
14
|
+
if (top.code !== 0) {
|
|
15
|
+
err("lintlane: not inside a git repository.\n");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const cwd = top.stdout.toString().trim();
|
|
19
|
+
process.chdir(cwd);
|
|
20
|
+
|
|
21
|
+
if (process.argv.includes("--restore")) {
|
|
22
|
+
const { restored, kept, found, backupDir } = await restoreFromBackup(cwd);
|
|
23
|
+
if (!found) out("lintlane: nothing to restore.\n");
|
|
24
|
+
for (const path of restored) out(`restored ${path}\n`);
|
|
25
|
+
for (const path of kept) err(`kept ${path}: it changed since the backup (see ${backupDir})\n`);
|
|
26
|
+
process.exit(kept.length > 0 ? 1 : 0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const biome = resolveBiome(cwd);
|
|
30
|
+
if (!biome) {
|
|
31
|
+
err("lintlane: Biome isn't installed. Run your package manager's install, then commit again.\n");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result = await runStagedChecks({
|
|
37
|
+
cwd,
|
|
38
|
+
biome,
|
|
39
|
+
colors: process.stderr.isTTY ? "force" : "off",
|
|
40
|
+
log: { out, err },
|
|
41
|
+
});
|
|
42
|
+
for (const warning of result.warnings) err(`lintlane: ${warning}\n`);
|
|
43
|
+
process.exit(result.ok ? 0 : 1);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error instanceof StaleBackupError) {
|
|
46
|
+
err(
|
|
47
|
+
`\nlintlane: an earlier run was interrupted before it could restore your files.\n` +
|
|
48
|
+
` Your working copies are saved in ${error.backupDir}\n` +
|
|
49
|
+
` Put them back with: node node_modules/lintlane/bin/pre-commit.mjs --restore\n\n`,
|
|
50
|
+
);
|
|
51
|
+
} else {
|
|
52
|
+
err(`lintlane: ${error.message}\n`);
|
|
53
|
+
}
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|