@howells/lint 0.1.5 → 0.1.7

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/MIGRATIONS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Adoption Notes
2
2
 
3
- Use these notes when replacing an existing ESLint, Prettier, or ad hoc Biome setup with `@howells/lint`.
3
+ Use these notes when replacing an existing ESLint, Prettier, Oxc, or ad hoc Biome setup with `@howells/lint`.
4
4
 
5
5
  ## Primary rule
6
6
 
@@ -19,9 +19,27 @@ If none of these fit cleanly, the likely answer is a new shared preset here, not
19
19
  ## Migration steps
20
20
 
21
21
  1. Add `@howells/lint` as a dev dependency.
22
- 2. Replace `eslint`, `next lint`, `prettier`, or direct `biome` scripts with `howells-lint` and `howells-format`.
23
- 3. Replace the project `biome.json` or `biome.jsonc` with a minimal file that only extends one shared preset.
24
- 4. Remove direct `eslint`, `eslint-config-*`, `eslint-plugin-*`, `prettier`, `@biomejs/biome`, and `ultracite` dependencies once the project is green.
22
+ 2. Pin Node with `.node-version` set to `22.18.0` and `engines.node` set to `>=22.18.0`.
23
+ 3. Replace `eslint`, `next lint`, `prettier`, or direct `biome` scripts with `howells-lint` and `howells-format`.
24
+ 4. Replace the project `biome.json` or `biome.jsonc` with a minimal file that only extends one shared preset.
25
+ 5. Remove direct `eslint`, `eslint-config-*`, `eslint-plugin-*`, `prettier`, `@biomejs/biome`, `oxlint`, `oxfmt`, `oxlint-tsgolint`, and `ultracite` dependencies once the project is green.
26
+
27
+ ## Oxlint/Oxfmt opt-in
28
+
29
+ Biome remains the default migration target. Use Oxlint/Oxfmt only for projects that deliberately choose the Oxc lane.
30
+
31
+ For an Oxlint/Oxfmt project, add `oxlint.config.ts` and `oxfmt.config.ts` using the exports from `@howells/lint`, then use:
32
+
33
+ ```json
34
+ {
35
+ "scripts": {
36
+ "lint": "howells-ox-check .",
37
+ "lint:fix": "howells-ox-fix ."
38
+ }
39
+ }
40
+ ```
41
+
42
+ Do not run Biome and Oxlint/Oxfmt together indefinitely. If both are present during a migration, write down which command is authoritative and remove the other once the migration is green.
25
43
 
26
44
  ## Keep local config thin
27
45
 
@@ -35,7 +53,7 @@ The normal local config should look like this:
35
53
 
36
54
  Acceptable local additions:
37
55
 
38
- - repo-specific file includes or force-ignores that cannot be expressed better in scripts
56
+ - repo-specific file includes or force-ignores for generated files that are unique to one project
39
57
  - one-off rule changes tied to a genuine platform constraint
40
58
  - temporary compatibility shims during migration
41
59
 
package/README.md CHANGED
@@ -1,45 +1,76 @@
1
1
  # `@howells/lint`
2
2
 
3
- Pinned Biome and Ultracite presets for Howells projects.
3
+ Pinned Biome, Oxlint/Oxfmt, and Ultracite presets for Howells projects.
4
4
 
5
5
  The goal is not to invent a second lint philosophy. The goal is to:
6
6
 
7
7
  - pin a single `@biomejs/biome` version
8
+ - pin a single `oxlint` version
9
+ - pin a single `oxfmt` version
8
10
  - pin a single `ultracite` version
9
11
  - pin a single `@manypkg/cli` version for monorepo consistency checks
10
12
  - give every consumer the same small preset matrix
11
13
  - discourage repo-local overrides unless the project has a genuinely unique constraint
12
14
 
13
- ## Install
15
+ Biome is the default toolchain. Oxlint/Oxfmt is offered as an explicit opt-in lane for JavaScript and TypeScript projects that want the Oxc stack's speed and ESLint-style rule coverage.
16
+
17
+ ## Agent Setup Checklist
18
+
19
+ When configuring a project, do this in order:
20
+
21
+ 1. Require Node 22.18.0+ and pnpm in the root `package.json`, and pin `.node-version` to `22.18.0`.
22
+ 2. Install only `@howells/lint` as the direct lint dependency.
23
+ 3. Add a `biome.json` that extends the closest presets.
24
+ 4. Add read-only `lint`, mutating `lint:fix`, and optional `lint:strict` scripts.
25
+ 5. If the project is a monorepo, add root workspace scripts that run `howells-workspace-check`.
26
+ 6. Verify with `pnpm lint` and, when configured, `pnpm lint:strict`.
27
+
28
+ ## Requirements
29
+
30
+ All projects using this package should declare the runtime and package manager explicitly:
31
+
32
+ ```json
33
+ {
34
+ "packageManager": "pnpm@10.23.0",
35
+ "engines": {
36
+ "node": ">=22.18.0"
37
+ }
38
+ }
39
+ ```
40
+
41
+ Also add a root `.node-version` file:
42
+
43
+ ```text
44
+ 22.18.0
45
+ ```
46
+
47
+ Install the shared tooling:
14
48
 
15
49
  ```bash
16
50
  pnpm add -D @howells/lint
17
51
  ```
18
52
 
19
- ## Presets
53
+ Do not add `@biomejs/biome`, `oxlint`, `oxfmt`, `oxlint-tsgolint`, `ultracite`, or `@manypkg/cli` directly unless you are developing this package itself. They are pinned transitively here.
20
54
 
21
- Choose the closest preset instead of starting from a generic base and patching it locally:
55
+ ## Biome Presets
22
56
 
23
- - `@howells/lint/biome/core`
24
- - `@howells/lint/biome/react`
25
- - `@howells/lint/biome/next`
57
+ Choose the closest preset instead of starting from a generic base and patching it locally:
26
58
 
27
- These presets already:
59
+ - `@howells/lint/biome/core` for Node or non-React TypeScript packages
60
+ - `@howells/lint/biome/react` for React packages
61
+ - `@howells/lint/biome/next` for Next.js apps
28
62
 
29
- - pin Biome and Ultracite transitively
30
- - enable VCS ignore file support
31
- - ignore common build output directories
32
- - keep `ignoreUnknown` on so mixed repos do not need defensive local config
33
- - enforce 2-space indentation consistently
34
- - enable Tailwind CSS directives on DOM-oriented presets
63
+ These presets already pin Biome and Ultracite, enable VCS ignore file support, ignore common build output directories, keep `ignoreUnknown` on for mixed repos, enforce 2-space indentation, and enable Tailwind CSS directives on DOM-oriented presets.
35
64
 
36
- ## Usage
65
+ The shared presets exclude generated and output folders seen across Howells projects: `node_modules`, `.next`, `.turbo`, `.vercel`, `dist`, `build`, `coverage`, `out`, `storybook-static`, `playwright-report`, `test-results`, `.source`, `.cache`, `.expo`, `.output`, `.wrangler`, `.svelte-kit`, `.nuxt`, `.vite`, `.vinxi`, `dev-dist`, `tmp`, and `temp`. Keep repo-local excludes only for genuinely project-specific generated files or data directories.
37
66
 
38
67
  Node or non-React TypeScript package:
39
68
 
40
69
  ```json
41
70
  {
42
- "extends": ["@howells/lint/biome/core"]
71
+ "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
72
+ "extends": ["@howells/lint/biome/core"],
73
+ "root": true
43
74
  }
44
75
  ```
45
76
 
@@ -47,7 +78,9 @@ React package:
47
78
 
48
79
  ```json
49
80
  {
50
- "extends": ["@howells/lint/biome/react"]
81
+ "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
82
+ "extends": ["@howells/lint/biome/core", "@howells/lint/biome/react"],
83
+ "root": true
51
84
  }
52
85
  ```
53
86
 
@@ -55,23 +88,69 @@ Next.js app:
55
88
 
56
89
  ```json
57
90
  {
58
- "extends": ["@howells/lint/biome/next"]
91
+ "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
92
+ "extends": [
93
+ "@howells/lint/biome/core",
94
+ "@howells/lint/biome/react",
95
+ "@howells/lint/biome/next"
96
+ ],
97
+ "root": true
59
98
  }
60
99
  ```
61
100
 
62
- ## Binaries
101
+ ## Oxlint/Oxfmt Presets
63
102
 
64
- Installers only need `@howells/lint` as a direct dependency. Use the package binaries instead of adding `@biomejs/biome`, `ultracite`, or `@manypkg/cli` separately:
103
+ Use this lane only when a project deliberately wants Oxlint and Oxfmt instead of Biome for day-to-day linting and formatting. The default `howells-lint` and `howells-format` commands stay on Biome.
65
104
 
66
- - `howells-biome` proxies to the pinned Biome binary
67
- - `howells-ultracite` proxies to the pinned Ultracite binary
68
- - `howells-lint` defaults to `biome check .`
69
- - `howells-lint-strict` runs the high-signal Biome security, correctness, and suspicious lint rules
70
- - `howells-format` defaults to `biome check . --write`
71
- - `howells-workspace-check` validates root workspace hygiene, then runs `manypkg check`
72
- - `howells-workspace-fix` defaults to `manypkg fix`
105
+ Create an `oxlint.config.ts`:
106
+
107
+ ```ts
108
+ import { defineConfig } from "oxlint";
109
+ import core from "@howells/lint/oxlint/core";
110
+
111
+ export default defineConfig({
112
+ extends: [core],
113
+ });
114
+ ```
115
+
116
+ For React or Next.js projects, add the matching presets:
117
+
118
+ ```ts
119
+ import { defineConfig } from "oxlint";
120
+ import core from "@howells/lint/oxlint/core";
121
+ import react from "@howells/lint/oxlint/react";
122
+ import next from "@howells/lint/oxlint/next";
123
+
124
+ export default defineConfig({
125
+ extends: [core, react, next],
126
+ });
127
+ ```
128
+
129
+ Create an `oxfmt.config.ts`:
130
+
131
+ ```ts
132
+ import { defineConfig } from "oxfmt";
133
+ import howells from "@howells/lint/oxfmt";
134
+
135
+ export default defineConfig({
136
+ extends: [howells],
137
+ });
138
+ ```
139
+
140
+ Oxlint type-aware rules are available through the pinned `oxlint-tsgolint` dependency. Enable them in the root Oxlint config when the project is ready for TypeScript 7 / `typescript-go` constraints:
141
+
142
+ ```ts
143
+ export default defineConfig({
144
+ extends: [core],
145
+ options: {
146
+ typeAware: true,
147
+ },
148
+ });
149
+ ```
150
+
151
+ ## Package Scripts
73
152
 
74
- Example scripts:
153
+ Every package or single-package app should use this shape:
75
154
 
76
155
  ```json
77
156
  {
@@ -85,37 +164,79 @@ Example scripts:
85
164
 
86
165
  Keep `lint` non-mutating. Put all `--write` behavior in `lint:fix` or `format` so CI and local checks have the same semantics.
87
166
 
88
- Monorepo root scripts should compose package linting with workspace validation:
167
+ Prefer `howells-lint .` over raw `biome check` or long target lists. Use explicit script targets only when the package has a real scope constraint:
89
168
 
90
169
  ```json
91
170
  {
92
171
  "scripts": {
93
- "lint": "turbo run lint && howells-workspace-check",
94
- "lint:fix": "turbo run lint:fix && howells-workspace-fix",
95
- "lint:strict": "turbo run lint:strict"
172
+ "lint": "howells-lint apps/web packages/ui",
173
+ "lint:fix": "howells-format apps/web packages/ui"
174
+ }
175
+ }
176
+ ```
177
+
178
+ For an Oxlint/Oxfmt project, keep the command names explicit:
179
+
180
+ ```json
181
+ {
182
+ "scripts": {
183
+ "lint": "howells-ox-check .",
184
+ "lint:fix": "howells-ox-fix ."
96
185
  }
97
186
  }
98
187
  ```
99
188
 
100
- `howells-workspace-check` expects workspace roots to declare `packageManager: "pnpm@..."`, require Node 20+ in `engines.node`, and keep `pnpm-workspace.yaml` present when using workspace package directories.
189
+ Use `howells-ox-fix --unsafe .` only when you deliberately want Oxlint's dangerous fixes.
101
190
 
102
- CI should call `pnpm lint` or `pnpm check` so these root checks are not bypassed by a direct `turbo lint` command.
191
+ ## Monorepo Roots
103
192
 
104
- Prefer explicit script targets over config churn when the only difference is scope:
193
+ Use workspace checks only at the monorepo root. Do not add `howells-workspace-check` to individual packages, and do not add it to single-package apps.
194
+
195
+ A monorepo root should have:
105
196
 
106
197
  ```json
107
198
  {
199
+ "packageManager": "pnpm@10.23.0",
200
+ "engines": {
201
+ "node": ">=22.18.0"
202
+ },
108
203
  "scripts": {
109
- "lint": "howells-lint apps/web packages/ui",
110
- "lint:fix": "howells-format apps/web packages/ui"
204
+ "lint": "turbo run lint && howells-workspace-check",
205
+ "lint:fix": "turbo run lint:fix && howells-workspace-fix",
206
+ "lint:strict": "turbo run lint:strict",
207
+ "check": "pnpm lint && pnpm typecheck && pnpm test"
208
+ },
209
+ "devDependencies": {
210
+ "@howells/lint": "^0.1.7"
111
211
  }
112
212
  }
113
213
  ```
114
214
 
215
+ `howells-workspace-check` validates that the root declares `packageManager: "pnpm@..."`, requires Node 22.18.0+ in `engines.node`, pins `.node-version` to `22.18.0`, keeps `pnpm-workspace.yaml` present when workspace package directories exist, and passes `manypkg check`.
216
+
217
+ CI should call `pnpm lint` or `pnpm check` so root workspace checks are not bypassed by a direct `turbo lint` command.
218
+
219
+ ## Binaries
220
+
221
+ Installers only need `@howells/lint` as a direct dependency. Use these package binaries:
222
+
223
+ - `howells-biome` proxies to the pinned Biome binary
224
+ - `howells-ultracite` proxies to the pinned Ultracite binary
225
+ - `howells-lint` defaults to `biome check .`
226
+ - `howells-lint-strict` runs high-signal Biome security, correctness, and suspicious lint rules
227
+ - `howells-format` defaults to `biome check . --write`
228
+ - `howells-oxlint` proxies to the pinned Oxlint binary
229
+ - `howells-oxfmt` proxies to the pinned Oxfmt binary
230
+ - `howells-ox-check` runs `oxfmt --check`, then `oxlint`
231
+ - `howells-ox-fix` runs `oxfmt --write`, then `oxlint --fix`
232
+ - `howells-workspace-check` validates root workspace hygiene, then runs `manypkg check`
233
+ - `howells-workspace-fix` runs `manypkg fix`
234
+
115
235
  ## Rules
116
236
 
117
237
  - Do not add local overrides just to preserve old ESLint behavior.
118
238
  - Do not create local `base`, `shared`, or `custom` Biome wrappers.
239
+ - Do not mix Biome and Oxlint/Oxfmt scripts in the same package unless the project has a deliberate migration plan.
119
240
  - If multiple repos need the same exception, add or adjust a preset here.
120
241
  - If a repo needs framework-specific linting, choose the matching preset instead of layering rules manually.
121
242
  - Prefer inline `biome-ignore` comments for truly isolated exceptions over broad config overrides.
@@ -159,4 +280,6 @@ Add this to `.claude/settings.json` so files are formatted on edit and linted on
159
280
  This package wraps:
160
281
 
161
282
  - [Biome configuration docs](https://biomejs.dev/reference/configuration/)
283
+ - [Oxlint configuration docs](https://oxc.rs/docs/guide/usage/linter/config-file-reference.html)
284
+ - [Oxfmt configuration docs](https://oxc.rs/docs/guide/usage/formatter/config-file-reference)
162
285
  - [Ultracite configuration docs](https://www.ultracite.ai/configuration)
@@ -4,8 +4,8 @@ import { runPackageBin } from "./run-package-bin.mjs";
4
4
 
5
5
  const targets = process.argv.slice(2);
6
6
  const args =
7
- targets.length > 0
8
- ? ["check", "--write", ...targets]
9
- : ["check", "--write", "."];
7
+ targets.length > 0
8
+ ? ["check", "--write", ...targets]
9
+ : ["check", "--write", "."];
10
10
 
11
11
  runPackageBin("@biomejs/biome", "biome", args);
@@ -4,33 +4,33 @@ import { runPackageBin } from "./run-package-bin.mjs";
4
4
 
5
5
  const args = process.argv.slice(2);
6
6
  const biomeCommands = new Set([
7
- "version",
8
- "rage",
9
- "start",
10
- "stop",
11
- "check",
12
- "lint",
13
- "format",
14
- "ci",
15
- "init",
16
- "migrate",
17
- "search",
18
- "explain",
19
- "clean",
20
- "daemon",
21
- "lsp-proxy"
7
+ "version",
8
+ "rage",
9
+ "start",
10
+ "stop",
11
+ "check",
12
+ "lint",
13
+ "format",
14
+ "ci",
15
+ "init",
16
+ "migrate",
17
+ "search",
18
+ "explain",
19
+ "clean",
20
+ "daemon",
21
+ "lsp-proxy",
22
22
  ]);
23
23
  const passthroughOptions = new Set(["--help", "-h", "--version", "-V"]);
24
24
 
25
25
  const resolvedArgs =
26
- args.length === 0
27
- ? ["check", "."]
28
- : biomeCommands.has(args[0])
29
- ? args
30
- : passthroughOptions.has(args[0])
31
- ? args
32
- : args[0].startsWith("-")
33
- ? ["check", ".", ...args]
34
- : ["check", ...args];
26
+ args.length === 0
27
+ ? ["check", "."]
28
+ : biomeCommands.has(args[0])
29
+ ? args
30
+ : passthroughOptions.has(args[0])
31
+ ? args
32
+ : args[0].startsWith("-")
33
+ ? ["check", ".", ...args]
34
+ : ["check", ...args];
35
35
 
36
36
  runPackageBin("@biomejs/biome", "biome", resolvedArgs);
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { resolvePackageBin } from "./run-package-bin.mjs";
5
+
6
+ const args = process.argv.slice(2);
7
+ const targets = args.filter((arg) => !arg.startsWith("-"));
8
+ const oxlintOptions = args.filter((arg) => arg.startsWith("-"));
9
+ const resolvedTargets = targets.length > 0 ? targets : ["."];
10
+
11
+ function run(packageName, binName, commandArgs) {
12
+ const binPath = resolvePackageBin(packageName, binName);
13
+ const result = spawnSync(binPath, commandArgs, {
14
+ stdio: "inherit",
15
+ env: process.env,
16
+ });
17
+
18
+ if (result.error) {
19
+ throw result.error;
20
+ }
21
+
22
+ if ((result.status ?? 1) !== 0) {
23
+ process.exit(result.status ?? 1);
24
+ }
25
+ }
26
+
27
+ run("oxfmt", "oxfmt", ["--check", ...resolvedTargets]);
28
+ run("oxlint", "oxlint", [...oxlintOptions, ...resolvedTargets]);
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { resolvePackageBin } from "./run-package-bin.mjs";
5
+
6
+ const args = process.argv.slice(2);
7
+ const useDangerousFixes = args.includes("--unsafe");
8
+ const filteredArgs = args.filter((arg) => arg !== "--unsafe");
9
+ const targets = filteredArgs.filter((arg) => !arg.startsWith("-"));
10
+ const oxlintOptions = filteredArgs.filter((arg) => arg.startsWith("-"));
11
+ const resolvedTargets = targets.length > 0 ? targets : ["."];
12
+
13
+ function run(packageName, binName, commandArgs) {
14
+ const binPath = resolvePackageBin(packageName, binName);
15
+ const result = spawnSync(binPath, commandArgs, {
16
+ stdio: "inherit",
17
+ env: process.env,
18
+ });
19
+
20
+ if (result.error) {
21
+ throw result.error;
22
+ }
23
+
24
+ if ((result.status ?? 1) !== 0) {
25
+ process.exit(result.status ?? 1);
26
+ }
27
+ }
28
+
29
+ run("oxfmt", "oxfmt", ["--write", ...resolvedTargets]);
30
+ run("oxlint", "oxlint", [
31
+ useDangerousFixes ? "--fix-dangerously" : "--fix",
32
+ ...oxlintOptions,
33
+ ...resolvedTargets,
34
+ ]);
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runPackageBin } from "./run-package-bin.mjs";
4
+
5
+ runPackageBin("oxfmt", "oxfmt", process.argv.slice(2));
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runPackageBin } from "./run-package-bin.mjs";
4
+
5
+ runPackageBin("oxlint", "oxlint", process.argv.slice(2));
@@ -2,67 +2,77 @@
2
2
 
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
- import { dirname, join } from "node:path";
6
5
  import { createRequire } from "node:module";
6
+ import { dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
9
9
  const require = createRequire(import.meta.url);
10
10
  const currentDir = dirname(fileURLToPath(import.meta.url));
11
11
 
12
12
  function resolvePackageJsonPath(packageName) {
13
- try {
14
- return require.resolve(`${packageName}/package.json`);
15
- } catch {
16
- const packageSegments = packageName.split("/");
17
- let searchDir = currentDir;
18
-
19
- while (true) {
20
- const candidate = join(searchDir, "..", "node_modules", ...packageSegments, "package.json");
21
-
22
- if (existsSync(candidate)) {
23
- return candidate;
24
- }
25
-
26
- const parentDir = dirname(searchDir);
27
-
28
- if (parentDir === searchDir) {
29
- break;
30
- }
31
-
32
- searchDir = parentDir;
33
- }
34
- }
35
-
36
- throw new Error(`Could not resolve package.json for package '${packageName}'.`);
13
+ try {
14
+ return require.resolve(`${packageName}/package.json`);
15
+ } catch {
16
+ const packageSegments = packageName.split("/");
17
+ let searchDir = currentDir;
18
+
19
+ while (true) {
20
+ const candidate = join(
21
+ searchDir,
22
+ "..",
23
+ "node_modules",
24
+ ...packageSegments,
25
+ "package.json",
26
+ );
27
+
28
+ if (existsSync(candidate)) {
29
+ return candidate;
30
+ }
31
+
32
+ const parentDir = dirname(searchDir);
33
+
34
+ if (parentDir === searchDir) {
35
+ break;
36
+ }
37
+
38
+ searchDir = parentDir;
39
+ }
40
+ }
41
+
42
+ throw new Error(
43
+ `Could not resolve package.json for package '${packageName}'.`,
44
+ );
37
45
  }
38
46
 
39
- function resolvePackageBin(packageName, binName) {
40
- const packageJsonPath = resolvePackageJsonPath(packageName);
41
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
42
- const packageDir = dirname(packageJsonPath);
43
- const binField = packageJson.bin;
47
+ export function resolvePackageBin(packageName, binName) {
48
+ const packageJsonPath = resolvePackageJsonPath(packageName);
49
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
50
+ const packageDir = dirname(packageJsonPath);
51
+ const binField = packageJson.bin;
44
52
 
45
- if (typeof binField === "string") {
46
- return join(packageDir, binField);
47
- }
53
+ if (typeof binField === "string") {
54
+ return join(packageDir, binField);
55
+ }
48
56
 
49
- if (binField && typeof binField === "object" && binField[binName]) {
50
- return join(packageDir, binField[binName]);
51
- }
57
+ if (binField && typeof binField === "object" && binField[binName]) {
58
+ return join(packageDir, binField[binName]);
59
+ }
52
60
 
53
- throw new Error(`Could not resolve bin '${binName}' for package '${packageName}'.`);
61
+ throw new Error(
62
+ `Could not resolve bin '${binName}' for package '${packageName}'.`,
63
+ );
54
64
  }
55
65
 
56
66
  export function runPackageBin(packageName, binName, args) {
57
- const binPath = resolvePackageBin(packageName, binName);
58
- const result = spawnSync(binPath, args, {
59
- stdio: "inherit",
60
- env: process.env
61
- });
67
+ const binPath = resolvePackageBin(packageName, binName);
68
+ const result = spawnSync(binPath, args, {
69
+ stdio: "inherit",
70
+ env: process.env,
71
+ });
62
72
 
63
- if (result.error) {
64
- throw result.error;
65
- }
73
+ if (result.error) {
74
+ throw result.error;
75
+ }
66
76
 
67
- process.exit(result.status ?? 1);
77
+ process.exit(result.status ?? 1);
68
78
  }
@@ -2,6 +2,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
4
  const workspaceDirs = ["apps", "packages", "services", "workers", "examples"];
5
+ const requiredNodeVersion = "22.18.0";
5
6
 
6
7
  function readRootPackageJson() {
7
8
  const packageJsonPath = join(process.cwd(), "package.json");
@@ -47,22 +48,57 @@ function hasLikelyWorkspaceLayout(packageJson) {
47
48
  );
48
49
  }
49
50
 
50
- function isNode20Engine(range) {
51
+ function isAtLeastRequiredNodeVersion(major, minor, patch = 0) {
52
+ if (major > 22) {
53
+ return true;
54
+ }
55
+
56
+ if (major < 22) {
57
+ return false;
58
+ }
59
+
60
+ if (minor > 18) {
61
+ return true;
62
+ }
63
+
64
+ if (minor < 18) {
65
+ return false;
66
+ }
67
+
68
+ return patch >= 0;
69
+ }
70
+
71
+ function isNode2218Engine(range) {
51
72
  if (typeof range !== "string") {
52
73
  return false;
53
74
  }
54
75
 
55
76
  const normalizedRange = range.replaceAll(/\s+/g, "");
77
+ const match = normalizedRange.match(
78
+ /^(?:>=|\^|~)?(?<major>\d+)(?:\.(?<minor>\d+))?(?:\.(?<patch>\d+))?$/,
79
+ );
56
80
 
57
- if (/(^|[<>=~^|])1[0-9](\.|[^0-9]|$)/.test(normalizedRange)) {
81
+ if (!match?.groups?.major || !match.groups.minor) {
58
82
  return false;
59
83
  }
60
84
 
61
- return />=?20(\.|[^0-9]|$)|\^20(\.|[^0-9]|$)|~20(\.|[^0-9]|$)/.test(
62
- normalizedRange,
85
+ return isAtLeastRequiredNodeVersion(
86
+ Number(match.groups.major),
87
+ Number(match.groups.minor),
88
+ Number(match.groups.patch ?? 0),
63
89
  );
64
90
  }
65
91
 
92
+ function readNodeVersionFile() {
93
+ const nodeVersionPath = join(process.cwd(), ".node-version");
94
+
95
+ if (!existsSync(nodeVersionPath)) {
96
+ return undefined;
97
+ }
98
+
99
+ return readFileSync(nodeVersionPath, "utf8").trim();
100
+ }
101
+
66
102
  export function runWorkspacePreflight() {
67
103
  const { errors, packageJson } = readRootPackageJson();
68
104
 
@@ -76,8 +112,15 @@ export function runWorkspacePreflight() {
76
112
  errors.push("root package.json packageManager must use pnpm");
77
113
  }
78
114
 
79
- if (!isNode20Engine(packageJson.engines?.node)) {
80
- errors.push("root package.json engines.node must require Node 20+");
115
+ if (!isNode2218Engine(packageJson.engines?.node)) {
116
+ errors.push(
117
+ `root package.json engines.node must require Node ${requiredNodeVersion}+`,
118
+ );
119
+ }
120
+
121
+ const nodeVersion = readNodeVersionFile();
122
+ if (nodeVersion !== requiredNodeVersion) {
123
+ errors.push(`root .node-version must be ${requiredNodeVersion}`);
81
124
  }
82
125
 
83
126
  if (
package/biome/core.json CHANGED
@@ -1,27 +1,42 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.4.12/schema.json",
3
- "extends": ["ultracite/biome/core"],
4
- "formatter": {
5
- "indentStyle": "space",
6
- "indentWidth": 2
7
- },
8
- "files": {
9
- "ignoreUnknown": true,
10
- "includes": [
11
- "**",
12
- "!!**/dist",
13
- "!!**/build",
14
- "!!**/.next",
15
- "!!**/.turbo",
16
- "!!**/coverage",
17
- "!!**/storybook-static",
18
- "!!**/.vercel/output",
19
- "!!**/out"
20
- ]
21
- },
22
- "vcs": {
23
- "enabled": true,
24
- "clientKind": "git",
25
- "useIgnoreFile": true
26
- }
2
+ "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
3
+ "extends": ["ultracite/biome/core"],
4
+ "formatter": {
5
+ "indentStyle": "space",
6
+ "indentWidth": 2
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": true,
10
+ "includes": [
11
+ "**",
12
+ "!**/node_modules/**",
13
+ "!**/.next/**",
14
+ "!**/.turbo/**",
15
+ "!**/.vercel/**",
16
+ "!**/dist/**",
17
+ "!**/build/**",
18
+ "!**/coverage/**",
19
+ "!**/out/**",
20
+ "!**/storybook-static/**",
21
+ "!**/playwright-report/**",
22
+ "!**/test-results/**",
23
+ "!**/.source/**",
24
+ "!**/.cache/**",
25
+ "!**/.expo/**",
26
+ "!**/.output/**",
27
+ "!**/.wrangler/**",
28
+ "!**/.svelte-kit/**",
29
+ "!**/.nuxt/**",
30
+ "!**/.vite/**",
31
+ "!**/.vinxi/**",
32
+ "!**/dev-dist/**",
33
+ "!**/tmp/**",
34
+ "!**/temp/**"
35
+ ]
36
+ },
37
+ "vcs": {
38
+ "enabled": true,
39
+ "clientKind": "git",
40
+ "useIgnoreFile": true
41
+ }
27
42
  }
package/biome/next.json CHANGED
@@ -1,9 +1,38 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.4.12/schema.json",
3
- "extends": ["./react.json", "ultracite/biome/next"],
4
- "css": {
5
- "parser": {
6
- "tailwindDirectives": true
7
- }
8
- }
2
+ "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
3
+ "extends": ["./react.json", "ultracite/biome/next"],
4
+ "files": {
5
+ "ignoreUnknown": true,
6
+ "includes": [
7
+ "**",
8
+ "!**/node_modules/**",
9
+ "!**/.next/**",
10
+ "!**/.turbo/**",
11
+ "!**/.vercel/**",
12
+ "!**/dist/**",
13
+ "!**/build/**",
14
+ "!**/coverage/**",
15
+ "!**/out/**",
16
+ "!**/storybook-static/**",
17
+ "!**/playwright-report/**",
18
+ "!**/test-results/**",
19
+ "!**/.source/**",
20
+ "!**/.cache/**",
21
+ "!**/.expo/**",
22
+ "!**/.output/**",
23
+ "!**/.wrangler/**",
24
+ "!**/.svelte-kit/**",
25
+ "!**/.nuxt/**",
26
+ "!**/.vite/**",
27
+ "!**/.vinxi/**",
28
+ "!**/dev-dist/**",
29
+ "!**/tmp/**",
30
+ "!**/temp/**"
31
+ ]
32
+ },
33
+ "css": {
34
+ "parser": {
35
+ "tailwindDirectives": true
36
+ }
37
+ }
9
38
  }
package/biome/react.json CHANGED
@@ -1,9 +1,38 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.4.12/schema.json",
3
- "extends": ["./core.json", "ultracite/biome/react"],
4
- "css": {
5
- "parser": {
6
- "tailwindDirectives": true
7
- }
8
- }
2
+ "$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
3
+ "extends": ["./core.json", "ultracite/biome/react"],
4
+ "files": {
5
+ "ignoreUnknown": true,
6
+ "includes": [
7
+ "**",
8
+ "!**/node_modules/**",
9
+ "!**/.next/**",
10
+ "!**/.turbo/**",
11
+ "!**/.vercel/**",
12
+ "!**/dist/**",
13
+ "!**/build/**",
14
+ "!**/coverage/**",
15
+ "!**/out/**",
16
+ "!**/storybook-static/**",
17
+ "!**/playwright-report/**",
18
+ "!**/test-results/**",
19
+ "!**/.source/**",
20
+ "!**/.cache/**",
21
+ "!**/.expo/**",
22
+ "!**/.output/**",
23
+ "!**/.wrangler/**",
24
+ "!**/.svelte-kit/**",
25
+ "!**/.nuxt/**",
26
+ "!**/.vite/**",
27
+ "!**/.vinxi/**",
28
+ "!**/dev-dist/**",
29
+ "!**/tmp/**",
30
+ "!**/temp/**"
31
+ ]
32
+ },
33
+ "css": {
34
+ "parser": {
35
+ "tailwindDirectives": true
36
+ }
37
+ }
9
38
  }
@@ -0,0 +1,4 @@
1
+ import type { OxfmtConfig } from "oxfmt";
2
+
3
+ declare const config: OxfmtConfig;
4
+ export default config;
@@ -0,0 +1 @@
1
+ export { default } from "ultracite/oxfmt";
@@ -0,0 +1,4 @@
1
+ import type { OxlintConfig } from "oxlint";
2
+
3
+ declare const config: OxlintConfig;
4
+ export default config;
@@ -0,0 +1 @@
1
+ export { default } from "ultracite/oxlint/core";
@@ -0,0 +1,4 @@
1
+ import type { OxlintConfig } from "oxlint";
2
+
3
+ declare const config: OxlintConfig;
4
+ export default config;
@@ -0,0 +1 @@
1
+ export { default } from "ultracite/oxlint/next";
@@ -0,0 +1,4 @@
1
+ import type { OxlintConfig } from "oxlint";
2
+
3
+ declare const config: OxlintConfig;
4
+ export default config;
@@ -0,0 +1 @@
1
+ export { default } from "ultracite/oxlint/react";
package/package.json CHANGED
@@ -1,13 +1,18 @@
1
1
  {
2
2
  "name": "@howells/lint",
3
- "version": "0.1.5",
4
- "description": "Pinned Biome and Ultracite presets for Howells projects.",
3
+ "version": "0.1.7",
4
+ "description": "Pinned Biome, Oxlint/Oxfmt, and Ultracite presets for Howells projects.",
5
5
  "license": "MIT",
6
+ "packageManager": "pnpm@10.23.0",
6
7
  "engines": {
7
- "node": ">=20.19"
8
+ "node": ">=22.18.0"
8
9
  },
9
10
  "files": [
10
11
  "biome/*.json",
12
+ "oxfmt/*.d.mts",
13
+ "oxfmt/*.mjs",
14
+ "oxlint/*.d.mts",
15
+ "oxlint/*.mjs",
11
16
  "bin/*.mjs",
12
17
  "README.md",
13
18
  "MIGRATIONS.md"
@@ -17,19 +22,42 @@
17
22
  "howells-lint": "bin/howells-lint.mjs",
18
23
  "howells-lint-strict": "bin/howells-lint-strict.mjs",
19
24
  "howells-format": "bin/howells-format.mjs",
25
+ "howells-ox-check": "bin/howells-ox-check.mjs",
26
+ "howells-ox-fix": "bin/howells-ox-fix.mjs",
27
+ "howells-oxfmt": "bin/howells-oxfmt.mjs",
28
+ "howells-oxlint": "bin/howells-oxlint.mjs",
20
29
  "howells-ultracite": "bin/howells-ultracite.mjs",
21
30
  "howells-workspace-check": "bin/howells-workspace-check.mjs",
22
31
  "howells-workspace-fix": "bin/howells-workspace-fix.mjs"
23
32
  },
24
33
  "dependencies": {
25
- "@biomejs/biome": "2.4.12",
34
+ "@biomejs/biome": "2.4.15",
26
35
  "@manypkg/cli": "^0.25.1",
27
- "ultracite": "7.6.0"
36
+ "oxfmt": "0.49.0",
37
+ "oxlint": "1.64.0",
38
+ "oxlint-tsgolint": "0.22.1",
39
+ "ultracite": "7.7.0"
28
40
  },
29
41
  "exports": {
30
42
  "./package.json": "./package.json",
31
43
  "./biome/core": "./biome/core.json",
32
44
  "./biome/react": "./biome/react.json",
33
- "./biome/next": "./biome/next.json"
45
+ "./biome/next": "./biome/next.json",
46
+ "./oxfmt": {
47
+ "types": "./oxfmt/index.d.mts",
48
+ "default": "./oxfmt/index.mjs"
49
+ },
50
+ "./oxlint/core": {
51
+ "types": "./oxlint/core.d.mts",
52
+ "default": "./oxlint/core.mjs"
53
+ },
54
+ "./oxlint/react": {
55
+ "types": "./oxlint/react.d.mts",
56
+ "default": "./oxlint/react.mjs"
57
+ },
58
+ "./oxlint/next": {
59
+ "types": "./oxlint/next.d.mts",
60
+ "default": "./oxlint/next.mjs"
61
+ }
34
62
  }
35
63
  }