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/cli.mjs
CHANGED
|
@@ -1,68 +1,31 @@
|
|
|
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 { resolveChecks } from "./checks.mjs";
|
|
8
|
+
import { binPath, resolveBiome, runInherit, runProcess } from "./proc.mjs";
|
|
9
|
+
import {
|
|
10
|
+
COMMITIZEN_PATH,
|
|
11
|
+
classifyHook,
|
|
12
|
+
detectPackageManager,
|
|
13
|
+
detectPreset,
|
|
14
|
+
hasCommitlintConfig,
|
|
15
|
+
hasLintStagedConfig,
|
|
16
|
+
installCommand,
|
|
17
|
+
LINT_STAGED_CONFIG,
|
|
18
|
+
PRESET_PATHS,
|
|
19
|
+
renderWorkflow,
|
|
20
|
+
runnerCommand,
|
|
21
|
+
} from "./project.mjs";
|
|
22
|
+
|
|
23
|
+
const KIT_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
24
|
+
const readJson = async (path) => JSON.parse(await readFile(path, "utf8"));
|
|
25
|
+
const KIT_PKG = await readJson(join(KIT_DIR, "package.json"));
|
|
9
26
|
const KIT_NAME = KIT_PKG.name;
|
|
10
27
|
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
|
-
};
|
|
28
|
+
const RUNNER = runnerCommand();
|
|
66
29
|
|
|
67
30
|
const BIOME_SUBCOMMANDS = {
|
|
68
31
|
check: (paths) => ["check", ...paths],
|
|
@@ -70,29 +33,21 @@ const BIOME_SUBCOMMANDS = {
|
|
|
70
33
|
ci: (paths) => ["ci", "--reporter=github", ...paths],
|
|
71
34
|
};
|
|
72
35
|
|
|
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]
|
|
36
|
+
const HELP = `Usage: ${RUNNER} ${KIT_NAME} <command> [options]
|
|
85
37
|
|
|
86
38
|
Commands:
|
|
87
39
|
init [options] Set up Biome, scripts, hooks, and CI in the current project
|
|
88
40
|
check [paths] Run biome check (defaults to .)
|
|
89
41
|
check:fix [paths] Run biome check --write (defaults to .)
|
|
90
42
|
ci [paths] Run biome ci --reporter=github (defaults to .)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
--
|
|
94
|
-
|
|
95
|
-
|
|
43
|
+
run [checks] Run the project's checks (Biome, typecheck, build, tests) in
|
|
44
|
+
order, the same list CI and the dashboard use. Flags:
|
|
45
|
+
--keep-going (don't stop at the first failure), --list
|
|
46
|
+
doctor [--json] Diagnose the setup: tools, hooks, configs, CI. Read-only
|
|
47
|
+
commit [options] Guided Conventional Commit (alias: c), run by Commitizen;
|
|
48
|
+
git flags go after --
|
|
49
|
+
check-commit Validate commit messages with commitlint (--message,
|
|
50
|
+
--commit-msg-file, --rev-range, -d)
|
|
96
51
|
ui [options] Open the local dashboard: run checks, review errors and
|
|
97
52
|
proposed fixes as a diff, apply fixes per file
|
|
98
53
|
|
|
@@ -105,6 +60,7 @@ init options:
|
|
|
105
60
|
--no-hooks Skip husky + pre-commit hook setup
|
|
106
61
|
--no-commit-lint Skip the commit-message hooks (commit-msg, prepare-commit-msg, pre-push)
|
|
107
62
|
--no-ci Skip the GitHub Actions workflow
|
|
63
|
+
--dry-run Show everything init would do; change nothing
|
|
108
64
|
--skip-install Print install commands instead of running them
|
|
109
65
|
-h, --help Show this help
|
|
110
66
|
|
|
@@ -116,13 +72,14 @@ auto-selected — pass --preset strict explicitly when you want it.
|
|
|
116
72
|
|
|
117
73
|
check / check:fix / ci run the Biome binary already installed in this
|
|
118
74
|
project's node_modules — you never need to call biome yourself. The
|
|
119
|
-
package manager (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
\`
|
|
75
|
+
package manager (npm, pnpm, yarn, or bun) used by \`init\` is detected from
|
|
76
|
+
the "packageManager" field in package.json, then from whichever lockfile is
|
|
77
|
+
present, then from the tool you launched lintlane with (npx, pnpm dlx, yarn
|
|
78
|
+
dlx, bunx). npm is used when none of those apply.
|
|
79
|
+
|
|
80
|
+
After \`init\` the \`lintlane\` command is installed in your project, for
|
|
81
|
+
example: \`npx lintlane check\`, \`pnpm exec lintlane check\`,
|
|
82
|
+
\`yarn lintlane check\`, \`bunx lintlane check\`.
|
|
126
83
|
`;
|
|
127
84
|
|
|
128
85
|
function fail(message) {
|
|
@@ -138,32 +95,9 @@ function warn(message) {
|
|
|
138
95
|
console.log(`! ${message}`);
|
|
139
96
|
}
|
|
140
97
|
|
|
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
98
|
// Everything after a bare "--" belongs to the subcommand (e.g. `commit -- -a -S`
|
|
165
99
|
// hands "-a -S" to git commit), so it never reaches our own option parsing.
|
|
166
|
-
const rawArgs =
|
|
100
|
+
const rawArgs = process.argv.slice(2);
|
|
167
101
|
const doubleDash = rawArgs.indexOf("--");
|
|
168
102
|
const ownArgs = doubleDash < 0 ? rawArgs : rawArgs.slice(0, doubleDash);
|
|
169
103
|
const passthroughArgs = doubleDash < 0 ? [] : rawArgs.slice(doubleDash + 1);
|
|
@@ -176,6 +110,10 @@ const { values, positionals } = parseArgs({
|
|
|
176
110
|
"no-ci": { type: "boolean", default: false },
|
|
177
111
|
"no-commit-lint": { type: "boolean", default: false },
|
|
178
112
|
"skip-install": { type: "boolean", default: false },
|
|
113
|
+
"dry-run": { type: "boolean", default: false },
|
|
114
|
+
json: { type: "boolean", default: false },
|
|
115
|
+
"keep-going": { type: "boolean", default: false },
|
|
116
|
+
list: { type: "boolean", default: false },
|
|
179
117
|
port: { type: "string" },
|
|
180
118
|
"no-open": { type: "boolean", default: false },
|
|
181
119
|
help: { type: "boolean", short: "h", default: false },
|
|
@@ -195,18 +133,13 @@ if (values.help && !["commit", "c", "check-commit"].includes(command)) {
|
|
|
195
133
|
}
|
|
196
134
|
|
|
197
135
|
if (command in BIOME_SUBCOMMANDS) {
|
|
198
|
-
const biome =
|
|
199
|
-
if (!
|
|
200
|
-
fail(`Biome isn't installed in this project. Run:
|
|
136
|
+
const biome = resolveBiome(cwd);
|
|
137
|
+
if (!biome) {
|
|
138
|
+
fail(`Biome isn't installed in this project. Run: ${RUNNER} ${KIT_NAME} init`);
|
|
201
139
|
}
|
|
202
140
|
const extraPaths = positionals.slice(1);
|
|
203
141
|
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);
|
|
142
|
+
process.exit(await runInherit(biome[0], [...biome.slice(1), ...biomeArgs], { cwd }));
|
|
210
143
|
}
|
|
211
144
|
|
|
212
145
|
if (command === "commit" || command === "c") {
|
|
@@ -219,29 +152,98 @@ if (command === "check-commit") {
|
|
|
219
152
|
process.exit(await main(commandArgs, cwd));
|
|
220
153
|
}
|
|
221
154
|
|
|
155
|
+
if (command === "doctor") {
|
|
156
|
+
const { formatReport, runDoctor } = await import("./doctor.mjs");
|
|
157
|
+
const report = await runDoctor({ cwd, kitDir: KIT_DIR });
|
|
158
|
+
console.log(values.json ? JSON.stringify(report, null, 2) : formatReport(report));
|
|
159
|
+
process.exit(report.ok ? 0 : 1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (command === "hook") {
|
|
163
|
+
const { runHook } = await import("./hook.mjs");
|
|
164
|
+
const at = rawArgs.indexOf("hook");
|
|
165
|
+
const [name, ...hookArgs] = rawArgs.slice(at + 1);
|
|
166
|
+
process.exit(await runHook(name, hookArgs, cwd));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (command === "run") {
|
|
170
|
+
const biome = resolveBiome(cwd);
|
|
171
|
+
if (!biome) fail(`Biome isn't installed in this project. Run: ${RUNNER} ${KIT_NAME} init`);
|
|
172
|
+
const pkg = existsSync(join(cwd, "package.json"))
|
|
173
|
+
? await readJson(join(cwd, "package.json"))
|
|
174
|
+
: {};
|
|
175
|
+
const { steps, problems } = resolveChecks({
|
|
176
|
+
cwd,
|
|
177
|
+
biome,
|
|
178
|
+
pmName: detectPackageManager(cwd, pkg).name,
|
|
179
|
+
pkg,
|
|
180
|
+
});
|
|
181
|
+
for (const problem of problems) warn(problem);
|
|
182
|
+
if (values.list) {
|
|
183
|
+
for (const step of steps) {
|
|
184
|
+
console.log(`${step.id.padEnd(10)} ${step.defaultOn ? "on " : "off"} ${step.display}`);
|
|
185
|
+
}
|
|
186
|
+
process.exit(0);
|
|
187
|
+
}
|
|
188
|
+
const wanted = positionals.slice(1);
|
|
189
|
+
const unknown = wanted.filter((id) => !steps.some((step) => step.id === id));
|
|
190
|
+
if (unknown.length)
|
|
191
|
+
fail(`Unknown check: ${unknown.join(", ")}. Available: ${steps.map((st) => st.id).join(", ")}`);
|
|
192
|
+
const chosen = wanted.length
|
|
193
|
+
? steps.filter((step) => wanted.includes(step.id))
|
|
194
|
+
: steps.filter((step) => step.defaultOn);
|
|
195
|
+
if (chosen.length === 0) fail("No checks to run.");
|
|
196
|
+
const { runCiSteps } = await import("./checks.mjs");
|
|
197
|
+
const results = [];
|
|
198
|
+
const byId = new Map(steps.map((step) => [step.id, step]));
|
|
199
|
+
await runCiSteps({
|
|
200
|
+
cwd,
|
|
201
|
+
steps: chosen,
|
|
202
|
+
stopOnFail: !values["keep-going"],
|
|
203
|
+
send: (event) => {
|
|
204
|
+
if (event.type === "start")
|
|
205
|
+
console.log(`▶ ${byId.get(event.id).label} (${byId.get(event.id).display})`);
|
|
206
|
+
else if (event.type === "log") process.stdout.write(event.text);
|
|
207
|
+
else if (event.type === "end") {
|
|
208
|
+
results.push(event);
|
|
209
|
+
console.log(
|
|
210
|
+
`${event.code === 0 ? "✔" : "✘"} ${byId.get(event.id).label} (${(event.ms / 1000).toFixed(1)}s)`,
|
|
211
|
+
);
|
|
212
|
+
} else if (event.type === "skip") console.log(`- skipped ${byId.get(event.id).label}`);
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
process.exit(
|
|
216
|
+
results.every((result) => result.code === 0) && results.length === chosen.length ? 0 : 1,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
222
220
|
if (command === "ui") {
|
|
223
|
-
const biome =
|
|
224
|
-
if (!
|
|
225
|
-
fail(`Biome isn't installed in this project. Run:
|
|
221
|
+
const biome = resolveBiome(cwd);
|
|
222
|
+
if (!biome) {
|
|
223
|
+
fail(`Biome isn't installed in this project. Run: ${RUNNER} ${KIT_NAME} init`);
|
|
226
224
|
}
|
|
227
225
|
const { startUi } = await import("./ui-server.mjs");
|
|
228
226
|
const uiPkgPath = join(cwd, "package.json");
|
|
229
|
-
const uiPkg = existsSync(uiPkgPath) ? await
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
227
|
+
const uiPkg = existsSync(uiPkgPath) ? await readJson(uiPkgPath) : {};
|
|
228
|
+
try {
|
|
229
|
+
await startUi({
|
|
230
|
+
cwd,
|
|
231
|
+
biome,
|
|
232
|
+
pkg: uiPkg,
|
|
233
|
+
pmName: detectPackageManager(cwd, uiPkg).name,
|
|
234
|
+
kitDir: KIT_DIR,
|
|
235
|
+
port: values.port === undefined ? 4321 : Number(values.port),
|
|
236
|
+
open: !values["no-open"],
|
|
237
|
+
});
|
|
238
|
+
} catch (error) {
|
|
239
|
+
fail(error.message);
|
|
240
|
+
}
|
|
239
241
|
await new Promise(() => {});
|
|
240
242
|
}
|
|
241
243
|
|
|
242
244
|
if (command !== "init") {
|
|
243
245
|
fail(
|
|
244
|
-
`Unknown command "${command}". Usage:
|
|
246
|
+
`Unknown command "${command}". Usage: ${RUNNER} ${KIT_NAME} <init|check|check:fix|ci|run|doctor|commit|check-commit|ui> [options]`,
|
|
245
247
|
);
|
|
246
248
|
}
|
|
247
249
|
|
|
@@ -256,10 +258,9 @@ if (!existsSync(pkgPath)) {
|
|
|
256
258
|
fail("No package.json found in the current directory.");
|
|
257
259
|
}
|
|
258
260
|
|
|
259
|
-
|
|
260
|
-
const pmName = detectPackageManager(cwd, pkg);
|
|
261
|
-
|
|
262
|
-
ok(`Detected package manager: ${pmName}`);
|
|
261
|
+
let pkg = await readJson(pkgPath);
|
|
262
|
+
const { name: pmName, source: pmSource } = detectPackageManager(cwd, pkg);
|
|
263
|
+
ok(`Detected package manager: ${pmName} (${pmSource})`);
|
|
263
264
|
|
|
264
265
|
let preset = values.preset;
|
|
265
266
|
if (preset === undefined) {
|
|
@@ -272,20 +273,53 @@ if (preset === undefined) {
|
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
275
|
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
276
|
+
const dryRun = values["dry-run"];
|
|
277
|
+
if (dryRun) console.log("Dry run: nothing will be changed.\n");
|
|
278
|
+
|
|
279
|
+
// Every change goes through act(): in a dry run it only says what it would do.
|
|
280
|
+
async function act(message, fn) {
|
|
281
|
+
if (dryRun) {
|
|
282
|
+
console.log(`+ would ${message}`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
await fn();
|
|
286
|
+
ok(message[0].toUpperCase() + message.slice(1));
|
|
287
|
+
}
|
|
279
288
|
|
|
280
|
-
|
|
281
|
-
|
|
289
|
+
// LINTLANE_INSTALL_SPEC lets you install lintlane from somewhere other than the
|
|
290
|
+
// registry (a tarball from `npm pack`, a local folder) when testing a build.
|
|
291
|
+
const kitSpec = process.env.LINTLANE_INSTALL_SPEC || `${KIT_NAME}@${KIT_VERSION}`;
|
|
292
|
+
const declared = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
293
|
+
// Only install what's missing, so an existing Biome or husky version is never bumped.
|
|
294
|
+
const devPackages = [];
|
|
295
|
+
if (!("@biomejs/biome" in declared)) devPackages.push("@biomejs/biome");
|
|
296
|
+
if (process.env.LINTLANE_INSTALL_SPEC || declared[KIT_NAME] !== KIT_VERSION)
|
|
297
|
+
devPackages.push(kitSpec);
|
|
298
|
+
if (!values["no-hooks"] && !("husky" in declared)) devPackages.push("husky");
|
|
299
|
+
|
|
300
|
+
if (devPackages.length === 0) {
|
|
301
|
+
ok("Dependencies already installed");
|
|
302
|
+
} else if (values["skip-install"] || dryRun) {
|
|
303
|
+
const argv = installCommand(pmName, devPackages).join(" ");
|
|
304
|
+
if (dryRun) console.log(`+ would run: ${argv}`);
|
|
305
|
+
else warn(`Skipped install. Run: ${argv}`);
|
|
282
306
|
} else {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
307
|
+
const installArgv = installCommand(pmName, devPackages);
|
|
308
|
+
const install = await runProcess(installArgv[0], installArgv.slice(1), { cwd });
|
|
309
|
+
if (install.code !== 0) {
|
|
310
|
+
const reason =
|
|
311
|
+
install.error?.code === "ENOENT"
|
|
312
|
+
? `"${installArgv[0]}" was not found. Install it, or run again with --skip-install and install the packages yourself:\n ${installArgv.join(" ")}`
|
|
313
|
+
: (install.stderr.toString().trim() || install.stdout.toString().trim())
|
|
314
|
+
.split("\n")
|
|
315
|
+
.slice(-8)
|
|
316
|
+
.join("\n");
|
|
317
|
+
fail(`Install failed (${installArgv.join(" ")}):\n${reason}`);
|
|
288
318
|
}
|
|
319
|
+
ok(`Installed ${devPackages.join(", ")}`);
|
|
320
|
+
// The install just rewrote package.json (devDependencies). Re-read it, or the
|
|
321
|
+
// scripts we add below would be written over a stale copy and erase those entries.
|
|
322
|
+
pkg = await readJson(pkgPath);
|
|
289
323
|
}
|
|
290
324
|
|
|
291
325
|
const biomeJsonPath = join(cwd, "biome.json");
|
|
@@ -293,7 +327,7 @@ const biomeJsoncPath = join(cwd, "biome.jsonc");
|
|
|
293
327
|
const extendsSpecifier = `${KIT_NAME}/${PRESET_PATHS[preset]}`;
|
|
294
328
|
|
|
295
329
|
if (existsSync(biomeJsonPath) || existsSync(biomeJsoncPath)) {
|
|
296
|
-
warn(`biome.json already exists —
|
|
330
|
+
warn(`biome.json already exists — make sure its "extends" includes "${extendsSpecifier}"`);
|
|
297
331
|
} else {
|
|
298
332
|
const config = {
|
|
299
333
|
$schema: "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
@@ -302,45 +336,86 @@ if (existsSync(biomeJsonPath) || existsSync(biomeJsoncPath)) {
|
|
|
302
336
|
if (preset === "next") {
|
|
303
337
|
config.files = { includes: ["**", "!**/.next", "!**/out", "!**/next-env.d.ts"] };
|
|
304
338
|
}
|
|
305
|
-
await
|
|
306
|
-
|
|
339
|
+
await act(`write biome.json (extends ${extendsSpecifier})`, () =>
|
|
340
|
+
writeFile(biomeJsonPath, `${JSON.stringify(config, null, 2)}\n`),
|
|
341
|
+
);
|
|
307
342
|
}
|
|
308
343
|
|
|
344
|
+
// package.json: scripts, lint-staged, Commitizen. Only ever adds what's missing.
|
|
309
345
|
pkg.scripts ??= {};
|
|
310
|
-
|
|
346
|
+
const pkgEdits = [];
|
|
347
|
+
const addScript = (name, value) => {
|
|
348
|
+
if (!pkg.scripts[name]) {
|
|
349
|
+
pkgEdits.push(() => {
|
|
350
|
+
pkg.scripts[name] = value;
|
|
351
|
+
});
|
|
352
|
+
return `add script "${name}"`;
|
|
353
|
+
}
|
|
354
|
+
warn(`Script "${name}" already exists — left untouched`);
|
|
355
|
+
return null;
|
|
356
|
+
};
|
|
357
|
+
const pending = [
|
|
358
|
+
addScript("check", "biome check ."),
|
|
359
|
+
addScript("check:fix", "biome check --write ."),
|
|
360
|
+
];
|
|
311
361
|
|
|
312
|
-
if (!
|
|
313
|
-
pkg.scripts.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
362
|
+
if (!values["no-hooks"]) {
|
|
363
|
+
if (!pkg.scripts.prepare) pending.push(addScript("prepare", "husky"));
|
|
364
|
+
else if (!pkg.scripts.prepare.includes("husky")) {
|
|
365
|
+
pkgEdits.push(() => {
|
|
366
|
+
pkg.scripts.prepare = `${pkg.scripts.prepare} && husky`;
|
|
367
|
+
});
|
|
368
|
+
pending.push('append husky to the existing "prepare" script');
|
|
369
|
+
} else {
|
|
370
|
+
warn('Script "prepare" already runs husky — left untouched');
|
|
371
|
+
}
|
|
319
372
|
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
373
|
+
if (hasLintStagedConfig(cwd, pkg)) {
|
|
374
|
+
warn("lint-staged is already configured — left untouched");
|
|
375
|
+
} else {
|
|
376
|
+
pkgEdits.push(() => {
|
|
377
|
+
pkg["lint-staged"] = LINT_STAGED_CONFIG;
|
|
378
|
+
});
|
|
379
|
+
pending.push('add "lint-staged" config (Biome on staged files)');
|
|
380
|
+
}
|
|
326
381
|
}
|
|
327
382
|
|
|
328
|
-
if (!values["no-
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
pkgChanged = true;
|
|
332
|
-
ok('Added script "prepare"');
|
|
333
|
-
} else if (!pkg.scripts.prepare.includes("husky")) {
|
|
334
|
-
pkg.scripts.prepare = `${pkg.scripts.prepare} && husky`;
|
|
335
|
-
pkgChanged = true;
|
|
336
|
-
ok('Appended husky to existing "prepare" script');
|
|
383
|
+
if (!values["no-commit-lint"]) {
|
|
384
|
+
if (pkg.config?.commitizen?.path) {
|
|
385
|
+
warn("Commitizen is already configured — left untouched");
|
|
337
386
|
} else {
|
|
338
|
-
|
|
387
|
+
pkgEdits.push(() => {
|
|
388
|
+
pkg.config = {
|
|
389
|
+
...pkg.config,
|
|
390
|
+
commitizen: { ...pkg.config?.commitizen, path: COMMITIZEN_PATH },
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
pending.push('add "config.commitizen" (guided commits)');
|
|
339
394
|
}
|
|
340
395
|
}
|
|
341
396
|
|
|
342
|
-
|
|
343
|
-
|
|
397
|
+
for (const message of pending.filter(Boolean)) {
|
|
398
|
+
if (dryRun) console.log(`+ would ${message} in package.json`);
|
|
399
|
+
}
|
|
400
|
+
if (pkgEdits.length > 0 && !dryRun) {
|
|
401
|
+
for (const edit of pkgEdits) edit();
|
|
402
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
403
|
+
for (const message of pending.filter(Boolean))
|
|
404
|
+
ok(`${message[0].toUpperCase()}${message.slice(1)}`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// commitlint config
|
|
408
|
+
if (!values["no-commit-lint"]) {
|
|
409
|
+
if (hasCommitlintConfig(cwd, pkg)) {
|
|
410
|
+
warn("commitlint is already configured — left untouched");
|
|
411
|
+
} else {
|
|
412
|
+
await act("write commitlint.config.mjs", () =>
|
|
413
|
+
writeFile(
|
|
414
|
+
join(cwd, "commitlint.config.mjs"),
|
|
415
|
+
'export { default } from "lintlane/commitlint";\n',
|
|
416
|
+
),
|
|
417
|
+
);
|
|
418
|
+
}
|
|
344
419
|
}
|
|
345
420
|
|
|
346
421
|
if (!values["no-hooks"]) {
|
|
@@ -355,15 +430,34 @@ if (!values["no-hooks"]) {
|
|
|
355
430
|
|
|
356
431
|
for (const hook of hooks) {
|
|
357
432
|
const hookPath = join(huskyDir, hook);
|
|
358
|
-
|
|
359
|
-
|
|
433
|
+
const template = await readFile(join(KIT_DIR, "templates", hook), "utf8");
|
|
434
|
+
const writeHook = async () => {
|
|
435
|
+
await mkdir(huskyDir, { recursive: true });
|
|
436
|
+
await writeFile(hookPath, template);
|
|
437
|
+
await chmod(hookPath, 0o755);
|
|
438
|
+
wroteHook = true;
|
|
439
|
+
};
|
|
440
|
+
if (!existsSync(hookPath)) {
|
|
441
|
+
await act(`write .husky/${hook}`, writeHook);
|
|
360
442
|
continue;
|
|
361
443
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
444
|
+
const state = classifyHook(hook, await readFile(hookPath, "utf8"), template);
|
|
445
|
+
if (state === "current") {
|
|
446
|
+
ok(`.husky/${hook} is up to date`);
|
|
447
|
+
} else if (state === "legacy") {
|
|
448
|
+
// Untouched hook from an older lintlane; the files it called are gone.
|
|
449
|
+
await act(`upgrade .husky/${hook} (the old one is from an earlier lintlane)`, writeHook);
|
|
450
|
+
} else if (state === "stale" || state === "unsafe") {
|
|
451
|
+
warn(
|
|
452
|
+
`.husky/${hook} was edited and still calls files lintlane no longer ships` +
|
|
453
|
+
(state === "unsafe"
|
|
454
|
+
? " (and `git update-index --again` can stage changes you didn't stage)"
|
|
455
|
+
: "") +
|
|
456
|
+
`. Replace its contents with node_modules/lintlane/templates/${hook}.`,
|
|
457
|
+
);
|
|
458
|
+
} else {
|
|
459
|
+
warn(`.husky/${hook} already exists — left untouched`);
|
|
460
|
+
}
|
|
367
461
|
}
|
|
368
462
|
|
|
369
463
|
if (wroteHook) {
|
|
@@ -372,16 +466,15 @@ if (!values["no-hooks"]) {
|
|
|
372
466
|
} else {
|
|
373
467
|
const husky = binPath(cwd, "husky");
|
|
374
468
|
if (existsSync(husky)) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
} catch (error) {
|
|
379
|
-
warn(`husky failed: ${error.message}`);
|
|
380
|
-
}
|
|
469
|
+
const ran = await runProcess(husky, [], { cwd });
|
|
470
|
+
if (ran.code === 0) ok("Ran husky");
|
|
471
|
+
else warn(`husky failed: ${(ran.stderr.toString() || ran.error?.message || "").trim()}`);
|
|
381
472
|
} else {
|
|
382
473
|
warn(`husky binary not found at ${husky} — run it manually once installed`);
|
|
383
474
|
}
|
|
384
475
|
}
|
|
476
|
+
} else if (dryRun) {
|
|
477
|
+
console.log("+ would run husky to point git at .husky");
|
|
385
478
|
}
|
|
386
479
|
}
|
|
387
480
|
}
|
|
@@ -390,39 +483,25 @@ if (!values["no-ci"]) {
|
|
|
390
483
|
const workflowsDir = join(cwd, ".github", "workflows");
|
|
391
484
|
const ciPath = join(workflowsDir, "biome.yml");
|
|
392
485
|
if (existsSync(ciPath)) {
|
|
393
|
-
warn(
|
|
486
|
+
warn(
|
|
487
|
+
".github/workflows/biome.yml already exists — left untouched (run `lintlane doctor` to compare it with your checks)",
|
|
488
|
+
);
|
|
394
489
|
} else {
|
|
395
|
-
// Catches commits that skipped the local hooks (--no-verify, GUI clients, fresh clones).
|
|
396
490
|
const withCommitCheck = !values["no-commit-lint"] && !values["no-hooks"];
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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);
|
|
424
|
-
ok("Wrote .github/workflows/biome.yml");
|
|
491
|
+
const biomeArgv = resolveBiome(cwd) ?? ["biome"];
|
|
492
|
+
const { steps: checks } = resolveChecks({ cwd, biome: biomeArgv, pmName, pkg });
|
|
493
|
+
await act("write .github/workflows/biome.yml", async () => {
|
|
494
|
+
await mkdir(workflowsDir, { recursive: true });
|
|
495
|
+
await writeFile(
|
|
496
|
+
ciPath,
|
|
497
|
+
renderWorkflow({ pmName, kitName: KIT_NAME, withCommitCheck, checks }),
|
|
498
|
+
);
|
|
499
|
+
});
|
|
425
500
|
}
|
|
426
501
|
}
|
|
427
502
|
|
|
428
|
-
console.log(
|
|
503
|
+
console.log(
|
|
504
|
+
dryRun
|
|
505
|
+
? "\nDry run finished. Run again without --dry-run to apply."
|
|
506
|
+
: `\nRun: ${pmName} run check:fix (and \`${RUNNER} ${KIT_NAME} doctor\` to verify the setup)`,
|
|
507
|
+
);
|