@ovt2/biome-react-kit 0.0.0-stage → 2.1.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 CHANGED
@@ -1,3 +1,100 @@
1
- # Temporary Holding Version
1
+ # @ovt2/biome-react-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Shared Biome lint/format/check preset for our React apps.
4
+
5
+ ## Setup
6
+
7
+ One command installs Biome and the kit, writes `biome.json`, adds the
8
+ `check`/`check:fix` scripts, and wires up the pre-commit hook and CI workflow:
9
+
10
+ ```sh
11
+ bunx @ovt2/biome-react-kit init
12
+ ```
13
+
14
+ Options:
15
+
16
+ | Flag | Effect |
17
+ | ----------------- | -------------------------------------------------- |
18
+ | `--preset <name>` | `base` (default), `strict`, or `next` |
19
+ | `--no-hooks` | Skip husky + the pre-commit hook |
20
+ | `--no-ci` | Skip the GitHub Actions workflow |
21
+ | `--skip-install` | Print the install commands instead of running them |
22
+ | `-h, --help` | Show usage |
23
+
24
+ `init` never overwrites an existing `biome.json`/`biome.jsonc`, script, hook,
25
+ or workflow file — running it again is safe and only reports what's missing.
26
+
27
+ `init` itself always runs through `bunx` (the script is a Bun executable),
28
+ but it doesn't require your project to use Bun: it detects your package
29
+ manager from the `packageManager` field in `package.json`, or otherwise from
30
+ whichever lockfile is present (`bun.lock`, `package-lock.json`,
31
+ `pnpm-lock.yaml`, `yarn.lock`), and uses that package manager's install
32
+ command and CI setup. It falls back to Bun only when none of those is
33
+ found. The pre-commit hook always calls the locally installed
34
+ `node_modules/.bin/biome`, so it works the same regardless of which package
35
+ manager installed it.
36
+
37
+ ### Manual setup
38
+
39
+ ```sh
40
+ bun add -d -E @biomejs/biome @ovt2/biome-react-kit
41
+ ```
42
+
43
+ Create `biome.json`:
44
+
45
+ ```json
46
+ { "extends": ["@ovt2/biome-react-kit/biome"] }
47
+ ```
48
+
49
+ ## Presets
50
+
51
+ | Name | Extends path | Use case |
52
+ | ------------------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------ |
53
+ | `@ovt2/biome-react-kit/biome` | `./presets/base.json` | Default: recommended rules + React domain, warns on unused code/console. |
54
+ | `@ovt2/biome-react-kit/biome/strict` | `./presets/strict.json` | Base rules raised to errors (unused code, `any`, non-null assertions, console, exhaustive deps). |
55
+ | `@ovt2/biome-react-kit/biome/next` | `./presets/next.json` | Base preset plus the Next.js lint domain. |
56
+
57
+ Presets intentionally don't set `files.includes` — setting it in your own
58
+ `biome.json` would _replace_ the kit's, not merge with it. For Next.js apps,
59
+ add your own ignores:
60
+
61
+ ```json
62
+ {
63
+ "extends": ["@ovt2/biome-react-kit/biome/next"],
64
+ "files": { "includes": ["**", "!**/.next", "!**/out", "!**/next-env.d.ts"] }
65
+ }
66
+ ```
67
+
68
+ Add scripts:
69
+
70
+ ```json
71
+ "check": "biome check .",
72
+ "check:fix": "biome check --write ."
73
+ ```
74
+
75
+ ## Pre-commit hook
76
+
77
+ Run Biome on staged files before every commit so formatting and fixable lint
78
+ issues never reach the repo:
79
+
80
+ ```sh
81
+ bun add -d husky
82
+ bunx husky init
83
+ ```
84
+
85
+ Replace the contents of `.husky/pre-commit` with:
86
+
87
+ ```sh
88
+ ./node_modules/.bin/biome check --write --staged --no-errors-on-unmatched --files-ignore-unknown=true
89
+ git update-index --again
90
+ ```
91
+
92
+ Commits with unfixable errors (e.g. an unsafe-fix-only rule set to error)
93
+ will be blocked until you resolve them.
94
+
95
+ ## Migrating from ESLint/Prettier
96
+
97
+ ```sh
98
+ bunx biome migrate eslint --write
99
+ bunx biome migrate prettier --write
100
+ ```
package/bin/cli.mjs ADDED
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { parseArgs } from "node:util";
5
+ import { $ } from "bun";
6
+
7
+ const KIT_DIR = join(import.meta.dir, "..");
8
+ const KIT_PKG = await Bun.file(join(KIT_DIR, "package.json")).json();
9
+ const KIT_NAME = KIT_PKG.name;
10
+ 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
+ };
66
+
67
+ const HELP = `Usage: bunx ${KIT_NAME} init [options]
68
+
69
+ Options:
70
+ --preset <base|strict|next> Which preset to extend (default: base)
71
+ --no-hooks Skip husky + pre-commit hook setup
72
+ --no-ci Skip the GitHub Actions workflow
73
+ --skip-install Print install commands instead of running them
74
+ -h, --help Show this help
75
+
76
+ The package manager (bun, npm, pnpm, or yarn) is detected from the
77
+ "packageManager" field in package.json or from whichever lockfile is
78
+ present in the current directory. Bun is used as the fallback when
79
+ neither is found.
80
+ `;
81
+
82
+ function fail(message) {
83
+ console.error(`✘ ${message}`);
84
+ process.exit(1);
85
+ }
86
+
87
+ function ok(message) {
88
+ console.log(`✔ ${message}`);
89
+ }
90
+
91
+ function warn(message) {
92
+ console.log(`! ${message}`);
93
+ }
94
+
95
+ function detectPackageManager(cwd, pkg) {
96
+ const declared = pkg.packageManager?.split("@")[0];
97
+ if (declared && declared in PACKAGE_MANAGERS) return declared;
98
+ for (const [name, pm] of Object.entries(PACKAGE_MANAGERS)) {
99
+ if (existsSync(join(cwd, pm.lockfile))) return name;
100
+ }
101
+ return "bun";
102
+ }
103
+
104
+ function binPath(cwd, name) {
105
+ return join(cwd, "node_modules", ".bin", name);
106
+ }
107
+
108
+ const { values, positionals } = parseArgs({
109
+ args: Bun.argv.slice(2),
110
+ options: {
111
+ preset: { type: "string", default: "base" },
112
+ "no-hooks": { type: "boolean", default: false },
113
+ "no-ci": { type: "boolean", default: false },
114
+ "skip-install": { type: "boolean", default: false },
115
+ help: { type: "boolean", short: "h", default: false },
116
+ },
117
+ allowPositionals: true,
118
+ strict: false,
119
+ });
120
+
121
+ if (values.help) {
122
+ console.log(HELP);
123
+ process.exit(0);
124
+ }
125
+
126
+ const command = positionals[0] ?? "init";
127
+ if (command !== "init") {
128
+ fail(`Unknown command "${command}". Usage: bunx ${KIT_NAME} init [options]`);
129
+ }
130
+
131
+ if (!(values.preset in PRESET_PATHS)) {
132
+ fail(
133
+ `Invalid preset "${values.preset}". Valid presets: ${Object.keys(PRESET_PATHS).join(", ")}.`,
134
+ );
135
+ }
136
+
137
+ const cwd = process.cwd();
138
+ const pkgPath = join(cwd, "package.json");
139
+ if (!existsSync(pkgPath)) {
140
+ fail("No package.json found in the current directory.");
141
+ }
142
+
143
+ const pkg = await Bun.file(pkgPath).json();
144
+ const pmName = detectPackageManager(cwd, pkg);
145
+ const pm = PACKAGE_MANAGERS[pmName];
146
+ ok(`Detected package manager: ${pmName}`);
147
+
148
+ const devPackages = ["@biomejs/biome", `${KIT_NAME}@${KIT_VERSION}`];
149
+ if (!values["no-hooks"]) devPackages.push("husky");
150
+ const installArgv = pm.install(devPackages);
151
+ const installCmd = installArgv.join(" ");
152
+
153
+ if (values["skip-install"]) {
154
+ warn(`Skipped install. Run: ${installCmd}`);
155
+ } else {
156
+ try {
157
+ await $`${installArgv}`.cwd(cwd).quiet();
158
+ ok(`Installed ${devPackages.join(", ")}`);
159
+ } catch (error) {
160
+ fail(`Install failed: ${error.message}`);
161
+ }
162
+ }
163
+
164
+ const biomeJsonPath = join(cwd, "biome.json");
165
+ const biomeJsoncPath = join(cwd, "biome.jsonc");
166
+ const extendsSpecifier = `${KIT_NAME}/${PRESET_PATHS[values.preset]}`;
167
+
168
+ if (existsSync(biomeJsonPath) || existsSync(biomeJsoncPath)) {
169
+ warn(`biome.json already exists — add this to its "extends": "${extendsSpecifier}"`);
170
+ } else {
171
+ const config = {
172
+ $schema: "./node_modules/@biomejs/biome/configuration_schema.json",
173
+ extends: [extendsSpecifier],
174
+ };
175
+ if (values.preset === "next") {
176
+ config.files = { includes: ["**", "!**/.next", "!**/out", "!**/next-env.d.ts"] };
177
+ }
178
+ await Bun.write(biomeJsonPath, `${JSON.stringify(config, null, 2)}\n`);
179
+ ok(`Wrote biome.json (extends ${extendsSpecifier})`);
180
+ }
181
+
182
+ pkg.scripts ??= {};
183
+ let pkgChanged = false;
184
+
185
+ if (!pkg.scripts.check) {
186
+ pkg.scripts.check = "biome check .";
187
+ pkgChanged = true;
188
+ ok('Added script "check"');
189
+ } else {
190
+ warn('Script "check" already exists — left untouched');
191
+ }
192
+
193
+ if (!pkg.scripts["check:fix"]) {
194
+ pkg.scripts["check:fix"] = "biome check --write .";
195
+ pkgChanged = true;
196
+ ok('Added script "check:fix"');
197
+ } else {
198
+ warn('Script "check:fix" already exists — left untouched');
199
+ }
200
+
201
+ if (!values["no-hooks"]) {
202
+ if (!pkg.scripts.prepare) {
203
+ pkg.scripts.prepare = "husky";
204
+ pkgChanged = true;
205
+ ok('Added script "prepare"');
206
+ } else if (!pkg.scripts.prepare.includes("husky")) {
207
+ pkg.scripts.prepare = `${pkg.scripts.prepare} && husky`;
208
+ pkgChanged = true;
209
+ ok('Appended husky to existing "prepare" script');
210
+ } else {
211
+ warn('Script "prepare" already runs husky — left untouched');
212
+ }
213
+ }
214
+
215
+ if (pkgChanged) {
216
+ await Bun.write(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
217
+ }
218
+
219
+ if (!values["no-hooks"]) {
220
+ const gitDir = join(cwd, ".git");
221
+ if (!existsSync(gitDir)) {
222
+ warn("No .git directory found — skipping husky hook setup");
223
+ } else {
224
+ const huskyDir = join(cwd, ".husky");
225
+ const hookPath = join(huskyDir, "pre-commit");
226
+ if (existsSync(hookPath)) {
227
+ warn(".husky/pre-commit already exists — left untouched");
228
+ } else {
229
+ await $`mkdir -p ${huskyDir}`.quiet();
230
+ await Bun.write(hookPath, await Bun.file(join(KIT_DIR, "templates", "pre-commit")).text());
231
+ await $`chmod 755 ${hookPath}`.quiet();
232
+ ok("Wrote .husky/pre-commit");
233
+ if (values["skip-install"]) {
234
+ warn("Skipped: run `node_modules/.bin/husky` to finish git hooks setup");
235
+ } else {
236
+ const husky = binPath(cwd, "husky");
237
+ if (existsSync(husky)) {
238
+ try {
239
+ await $`${husky}`.cwd(cwd).quiet();
240
+ ok("Ran husky");
241
+ } catch (error) {
242
+ warn(`husky failed: ${error.message}`);
243
+ }
244
+ } else {
245
+ warn(`husky binary not found at ${husky} — run it manually once installed`);
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ if (!values["no-ci"]) {
253
+ const workflowsDir = join(cwd, ".github", "workflows");
254
+ const ciPath = join(workflowsDir, "biome.yml");
255
+ if (existsSync(ciPath)) {
256
+ warn(".github/workflows/biome.yml already exists — left untouched");
257
+ } else {
258
+ const workflow = `name: Biome
259
+
260
+ on:
261
+ pull_request:
262
+ push:
263
+ branches:
264
+ - main
265
+
266
+ jobs:
267
+ biome:
268
+ runs-on: ubuntu-latest
269
+ steps:
270
+ - uses: actions/checkout@v4
271
+ ${pm.setupSteps.join("\n")}
272
+ - run: ${pm.runCheckCi}
273
+ `;
274
+ await $`mkdir -p ${workflowsDir}`.quiet();
275
+ await Bun.write(ciPath, workflow);
276
+ ok("Wrote .github/workflows/biome.yml");
277
+ }
278
+ }
279
+
280
+ console.log(`\nRun: ${pmName} run check:fix`);
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@ovt2/biome-react-kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.1.0",
4
+ "description": "Shared Biome lint/format/check preset for React apps",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "biome-react-kit": "./bin/cli.mjs"
9
+ },
10
+ "exports": {
11
+ "./biome": "./presets/base.json",
12
+ "./biome/strict": "./presets/strict.json",
13
+ "./biome/next": "./presets/next.json",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "bin",
18
+ "presets",
19
+ "templates",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "check": "biome check .",
24
+ "check:fix": "biome check --write ."
25
+ },
26
+ "peerDependencies": {
27
+ "@biomejs/biome": ">=2.2.0 <3"
28
+ },
29
+ "devDependencies": {
30
+ "@biomejs/biome": "2.6.4"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "engines": {
36
+ "bun": ">=1.2"
37
+ },
38
+ "keywords": [
39
+ "biome",
40
+ "react",
41
+ "lint",
42
+ "format"
43
+ ],
44
+ "author": "Expo2Dev"
45
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.6.4/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "formatter": {
9
+ "enabled": true,
10
+ "indentStyle": "space",
11
+ "indentWidth": 2,
12
+ "lineWidth": 100
13
+ },
14
+ "javascript": {
15
+ "formatter": {
16
+ "quoteStyle": "double",
17
+ "semicolons": "always",
18
+ "trailingCommas": "all"
19
+ }
20
+ },
21
+ "assist": {
22
+ "actions": {
23
+ "source": { "organizeImports": "on" }
24
+ }
25
+ },
26
+ "linter": {
27
+ "enabled": true,
28
+ "domains": {
29
+ "react": "recommended"
30
+ },
31
+ "rules": {
32
+ "preset": "recommended",
33
+ "correctness": {
34
+ "noUnusedImports": "warn",
35
+ "noUnusedVariables": "warn"
36
+ },
37
+ "suspicious": {
38
+ "noConsole": {
39
+ "level": "warn",
40
+ "options": { "allow": ["error", "warn"] }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.6.4/schema.json",
3
+ "extends": ["./base.json"],
4
+ "linter": {
5
+ "domains": {
6
+ "react": "recommended",
7
+ "next": "recommended"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.6.4/schema.json",
3
+ "extends": ["./base.json"],
4
+ "linter": {
5
+ "rules": {
6
+ "correctness": {
7
+ "noUnusedImports": "error",
8
+ "noUnusedVariables": "error",
9
+ "useExhaustiveDependencies": "error"
10
+ },
11
+ "suspicious": {
12
+ "noExplicitAny": "error",
13
+ "noConsole": {
14
+ "level": "error",
15
+ "options": { "allow": ["error", "warn"] }
16
+ }
17
+ },
18
+ "style": {
19
+ "noNonNullAssertion": "error"
20
+ }
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,2 @@
1
+ ./node_modules/.bin/biome check --write --staged --no-errors-on-unmatched --files-ignore-unknown=true
2
+ git update-index --again