adamantite 0.42.0 → 0.43.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
@@ -34,6 +34,8 @@ and Sherif so humans and coding agents can use the same project workflow.
34
34
  through an interactive or non-interactive initializer.
35
35
  - **Setup maintenance**: Assess managed integrations with `doctor`, update managed
36
36
  dependencies, and give humans or agents repair instructions.
37
+ - **Custom rules**: Write project-specific Oxlint rules in `.adamantite/rules/`, one rule
38
+ for each file, and enable all of them with one line.
37
39
  - **Workspace checks**: Find unused code with Knip and dependency inconsistencies with
38
40
  Sherif.
39
41
  - **Agent guidance**: Add a managed Adamantite section to `AGENTS.md`.
@@ -87,8 +89,8 @@ Available setup values:
87
89
 
88
90
  - Scripts: `check`, `fix`, and `analyze`. In a detected monorepo, `analyze` also installs
89
91
  Sherif.
90
- - Presets: `react`, `react-strict`, `react-doctor`, `nextjs`, `vue`, `jest`, `vitest`,
91
- `node`, `strict`, `antislop`, and `shadcn`. `react-doctor` requires `react`.
92
+ - Presets: `react`, `react-strict`, `react-doctor`, `tanstack`, `nextjs`, `vue`, `effect`,
93
+ `jest`, `vitest`, `node`, `strict`, `antislop`, and `shadcn`. `react-doctor` requires `react`.
92
94
  - Editors: `vscode` and `zed`.
93
95
 
94
96
  Presets and TypeScript require the `check` or `fix` script. Editor extension installation
@@ -214,7 +216,34 @@ adamantite doctor
214
216
  ```
215
217
 
216
218
  `update` exits 0 when dependency updates succeed, even if doctor findings remain. Use
217
- `adamantite doctor` as the CI gate.
219
+ `adamantite doctor` as the CI gate. After it installs packages, `update` patches Oxlint and TypeScript again for
220
+ [the effect preset](#the-effect-preset).
221
+
222
+ ### `adamantite prepare`
223
+
224
+ Patch Oxlint and TypeScript for [the effect preset](#the-effect-preset). The `prepare`
225
+ script in `package.json` runs it after each install:
226
+
227
+ ```json
228
+ { "scripts": { "prepare": "adamantite prepare" } }
229
+ ```
230
+
231
+ Without the effect preset, `prepare` does nothing and exits 0. If the patch fails, `prepare`
232
+ fails, so the install fails.
233
+
234
+ ### `adamantite rule`
235
+
236
+ Write and inspect [custom rules](#custom-rules):
237
+
238
+ ```sh
239
+ adamantite rule add no-process-env
240
+ adamantite rule add no-window --dir tooling/lint/rules
241
+ adamantite rule ast src/index.ts
242
+ ```
243
+
244
+ `rule add` writes a rule stub to `.adamantite/rules/`, or to the folder that `--dir` names,
245
+ and an `AGENTS.md` with the authoring guidance when the folder has none. `rule ast` prints the
246
+ ESTree-shaped AST that Oxlint rules visit, so you can read node names off real code.
218
247
 
219
248
  ### Pass arguments to underlying tools
220
249
 
@@ -246,16 +275,20 @@ Adamantite publishes configuration that can also be consumed directly:
246
275
  | `adamantite/lint/react` | React, JSX accessibility, and performance. |
247
276
  | `adamantite/lint/react-strict` | Opinions for React feature code: no state, effect, or memoization hooks outside hook modules. See [the react-strict preset](#the-react-strict-preset). |
248
277
  | `adamantite/lint/react-doctor` | Curated [React Doctor](https://github.com/millionco/react-doctor) rules for React state and effect misuse. See [the react-doctor preset](#the-react-doctor-preset). |
278
+ | `adamantite/lint/tanstack` | Opinions for [TanStack Query](https://tanstack.com/query) with route loaders: queries defined once with `queryOptions()`, and no query data copied into state. See [the tanstack preset](#the-tanstack-preset). |
249
279
  | `adamantite/lint/nextjs` | Next.js rules. |
250
280
  | `adamantite/lint/vue` | Vue rules. |
281
+ | `adamantite/lint/effect` | Curated [@effect/tsgo](https://github.com/Effect-TS/tsgo) rules for Effect misuse, such as effects that never run and unhandled errors. See [the effect preset](#the-effect-preset). |
251
282
  | `adamantite/lint/node` | Node.js rules. |
252
283
  | `adamantite/lint/jest` | Jest rules. |
253
284
  | `adamantite/lint/vitest` | Vitest rules. |
254
285
  | `adamantite/lint/strict` | Framework-neutral opinions: no deep or wide destructuring, and no type assertions outside tests. See [the strict preset](#the-strict-preset). |
255
286
  | `adamantite/lint/antislop` | Vendored [anti-slop](https://github.com/dmmulroy/anti-slop) rules that reject low-evidence, low-signal patterns. Also turns off `typescript/consistent-indexed-object-style` and `unicorn/no-immediate-mutation` from the core preset, which conflict with these rules. |
256
287
  | `adamantite/lint/shadcn` | [@shadcn/lint](https://github.com/shadcn-ui/lint) rules for Tailwind v4 design systems: no restyled components, raw colors, arbitrary values, inline styles, unknown classes, or unreadable class expressions. shadcn/ui is not required. See [the shadcn preset](#the-shadcn-preset). |
288
+ | `adamantite/lint/custom` | A function, `custom()`, that loads the project's own rules from `.adamantite/rules/`. See [custom rules](#custom-rules). |
289
+ | `adamantite/rules` | `defineRule` and the rule and AST types for custom rules. |
257
290
  | `adamantite/format` | Oxfmt configuration. |
258
- | `adamantite/analyze` | Knip configuration. The `ignoreDependencies` named export groups suggested ignore lists; `init` sets `ignoreDependencies.monorepo` in a monorepo. |
291
+ | `adamantite/analyze` | Knip configuration. The `ignoreDependencies` named export groups suggested ignore lists; `init` sets `ignoreDependencies.monorepo` in a monorepo and `ignoreDependencies.effect` with the effect preset. |
259
292
  | `adamantite/typescript` | Strict TypeScript configuration for TS 7+. |
260
293
 
261
294
  When consuming the lint presets directly, hoist the core preset's ignore patterns onto the
@@ -338,6 +371,103 @@ as a hosted product. Read the
338
371
  [license](https://www.npmjs.com/package/oxlint-plugin-react-doctor?activeTab=code) before
339
372
  you select the preset.
340
373
 
374
+ ### The tanstack preset
375
+
376
+ `adamantite/lint/tanstack` holds opinions for TanStack Query (`@tanstack/react-query`) in
377
+ apps that preload queries in route loaders, such as TanStack Router and TanStack Start apps.
378
+ Like `react-strict`, it needs no extra package.
379
+
380
+ - `adamantite/query-from-loader` reports `useQuery`, `useSuspenseQuery`, `useInfiniteQuery`,
381
+ and `useSuspenseInfiniteQuery` calls in route files that take a plain options object or a `queryOptions()`
382
+ call made inside a function. Define each query once with `queryOptions()` at module scope
383
+ or in an exported factory such as `userQuery(id)`, preload it in the route loader with
384
+ `queryClient.ensureQueryData()`, and pass the same options to the hook. For the infinite
385
+ hooks, use `infiniteQueryOptions()` and `queryClient.ensureInfiniteQueryData()`. The rule
386
+ looks through type-only wrappers such as `as const` and `satisfies`. Then a route with
387
+ `preload="intent"` renders from the cache without a request waterfall. An object that
388
+ spreads shared options, such as `{ ...userQuery(id), select }`, stays allowed.
389
+ - `adamantite/no-query-data-in-state` reports `useState` calls whose initial value comes from
390
+ the data of a query in the same component, such as `useState(query.data)` or
391
+ `useState(() => data.user)`. The copy stops updating when the query refetches. Read the
392
+ value from the query result, and keep only the user's edits in state.
393
+
394
+ A route file is a file that calls a route factory from `@tanstack/react-router`, such as
395
+ `createFileRoute`, `createRootRoute`, or `createRoute`. Only a route has a loader to preload
396
+ the query, so components outside route files can define their queries inline.
397
+ `query-from-loader` checks one file at a time. It cannot prove that a loader preloads the
398
+ options, so it moves queries toward shared definitions but does not guarantee the preload.
399
+ The `react-doctor` preset reports other query misuse, such as refetches from effects and
400
+ mutations that do not invalidate queries. See
401
+ [ADR 0008](docs/adr/0008-tanstack-query-options-from-loaders.md).
402
+
403
+ ### The effect preset
404
+
405
+ `adamantite/lint/effect` reports misuse of [Effect](https://effect.website), such as an
406
+ effect that is neither yielded nor assigned (`floating-effect`), an unhandled error or
407
+ requirement (`missing-effect-error`, `missing-effect-context`), and a combinator with a
408
+ simpler replacement (`catch-all-to-map-error`). The rules come from
409
+ [@effect/tsgo](https://github.com/Effect-TS/tsgo), the Effect Language Service for
410
+ TypeScript-Go, and need Oxlint's type-aware mode.
411
+
412
+ The rules are not a JavaScript plugin. They exist only after `effect-tsgo patch --oxlint
413
+ --typescript` replaces the Oxlint, oxlint-tsgolint, and TypeScript binaries in
414
+ `node_modules`. Without the patch, Oxlint stops with `Unknown plugin: 'effecttsgo'`. Each
415
+ install of one of those packages restores the original binaries, so the patch must run again
416
+ after it. Adamantite runs it in these places:
417
+
418
+ - `adamantite prepare`, which the `prepare` script runs after a bare install, such as
419
+ `npm install`, `npm ci`, or a fresh clone in CI.
420
+ - `adamantite init` and `adamantite update`, after they install packages. A named install,
421
+ such as `npm install -D oxlint@1.86.0`, does not run the `prepare` script. If you change one
422
+ of those versions yourself, doctor reports the version drift, and `adamantite update`
423
+ installs the pinned version and patches it.
424
+
425
+ When you select the preset, `adamantite init` does these steps:
426
+
427
+ - Installs the pinned `@effect/tsgo` and runs the patch.
428
+ - Adds `"prepare": "adamantite prepare"` to `package.json`. If the project already has a
429
+ `prepare` script, init rewrites it as `adamantite prepare && (<existing>)`. A command such as
430
+ `cd ..` then cannot move the patch out of the project, and a fallback such as `|| true`
431
+ cannot hide a failed patch.
432
+ - Adds `{ "name": "@effect/language-service", "diagnostics": false }` to
433
+ `compilerOptions.plugins` in `tsconfig.json`. Editors that use the workspace TypeScript
434
+ get Effect quick fixes, refactors, and hovers. Oxlint reports the diagnostics, so the
435
+ language service does not report them a second time.
436
+ - Sets `ignoreDependencies.effect` in `knip.config.ts`, because no package has the name
437
+ `@effect/language-service`.
438
+
439
+ `adamantite doctor` reports each missing part, and `adamantite update` keeps
440
+ `@effect/tsgo` on its pinned version. Each `@effect/tsgo` release supports only some Oxlint,
441
+ oxlint-tsgolint, and TypeScript versions, so Adamantite moves the pins together.
442
+
443
+ A production-only install, such as `npm ci --omit=dev` or `pnpm install --prod`, still runs
444
+ the `prepare` script, but it does not install Adamantite, so the install fails with
445
+ `adamantite: not found`. Add `--ignore-scripts` to those installs. Nothing in a production
446
+ install needs the patch.
447
+
448
+ The rules read their options from the tsconfig entry. For example, `unstable-api-usage`
449
+ reports each use of an API marked `@stability unstable`. To allow a module on purpose, list
450
+ it in `allowedUnstableApis`:
451
+
452
+ ```jsonc
453
+ {
454
+ "compilerOptions": {
455
+ "plugins": [
456
+ {
457
+ "name": "@effect/language-service",
458
+ "diagnostics": false,
459
+ "allowedUnstableApis": ["effect/cli"],
460
+ },
461
+ ],
462
+ },
463
+ }
464
+ ```
465
+
466
+ The preset enables only rules that report Effect code. It does not ban platform APIs, such
467
+ as `Date.now()` or `process.env`, in code that is not Effect code. Inside Effect code, it
468
+ reports them, because `Clock`, `Random`, and `Config` let tests control them. See
469
+ [ADR 0007](docs/adr/0007-effect-preset-patches-oxlint.md) for the criteria.
470
+
341
471
  ### The strict preset
342
472
 
343
473
  `adamantite/lint/strict` holds framework-neutral opinions. Use it with any other preset. Like
@@ -395,6 +525,103 @@ another component directory. See the
395
525
  [@shadcn/lint documentation](https://github.com/shadcn-ui/lint#settings) for settings,
396
526
  contracts, and custom messages.
397
527
 
528
+ ## Custom rules
529
+
530
+ Custom rules are Oxlint rules that your project writes and owns. Each file in
531
+ `.adamantite/rules/` is one rule. `custom()` from `adamantite/lint/custom` loads the folder
532
+ and enables each rule as `error` under the `project` plugin, so `no-process-env.ts` becomes
533
+ `project/no-process-env`. Add a file to add a rule, and delete the file to remove it. A file
534
+ that starts with `_`, such as `_helpers.ts`, is a helper, not a rule.
535
+
536
+ ```text
537
+ .adamantite/rules/
538
+ no-process-env.ts # enabled as project/no-process-env
539
+ _helpers.ts # skipped
540
+ ```
541
+
542
+ ```ts
543
+ // .adamantite/rules/no-process-env.ts
544
+ import { defineRule } from "adamantite/rules"
545
+
546
+ export default defineRule({
547
+ meta: {
548
+ messages: { unexpected: "Read configuration through the config module." },
549
+ type: "problem",
550
+ },
551
+ createOnce(context) {
552
+ return {
553
+ MemberExpression(node) {
554
+ if (node.object.type === "Identifier" && node.object.name === "process") {
555
+ context.report({ messageId: "unexpected", node })
556
+ }
557
+ },
558
+ }
559
+ },
560
+ })
561
+ ```
562
+
563
+ ```ts
564
+ // oxlint.config.ts
565
+ import { defineConfig } from "oxlint"
566
+ import core from "adamantite/lint"
567
+ import custom from "adamantite/lint/custom"
568
+
569
+ export default defineConfig({
570
+ extends: [core, custom()],
571
+ ignorePatterns: core.ignorePatterns,
572
+ })
573
+ ```
574
+
575
+ `custom()` returns an empty config when the folder does not exist, so `adamantite init` adds
576
+ it to every generated config. It takes these options:
577
+
578
+ - `dir`: the rules folder. A relative path resolves from the file that calls `custom()`, not
579
+ from the working directory. Default: `.adamantite/rules`.
580
+ - `name`: the plugin name that prefixes each rule. Default: `project`.
581
+ - `rules`: severity or options for a rule, by rule name without the prefix, such as
582
+ `{ "no-process-env": "warn" }`.
583
+
584
+ Rule files run with Node.js type stripping, so use erasable TypeScript only: no `enum`,
585
+ `namespace` with values, or parameter properties. Rules get no type information. A rule
586
+ that throws prints an error for each file, and the lint run still exits 0. Adamantite owns
587
+ the wiring and the authoring guidance; your project owns the correctness and the tests of
588
+ its rules. To test a rule, use `RuleTester` from `oxlint/plugins-dev`. `adamantite rule add`
589
+ writes the full authoring guidance to `.adamantite/rules/AGENTS.md`.
590
+
591
+ `adamantite doctor` reports a rule file that cannot load, with the reason, a rules folder
592
+ that no `custom()` call loads, and two rules folders that use the same plugin name.
593
+
594
+ ### Custom rules in a monorepo
595
+
596
+ Oxlint rejects two plugins with the same name in one run, so give each rules folder its own
597
+ name. A nested Oxlint config replaces the root config, so a workspace package that needs the
598
+ root rules extends the root config:
599
+
600
+ ```ts
601
+ // packages/web/oxlint.config.ts
602
+ import { defineConfig } from "oxlint"
603
+ import custom from "adamantite/lint/custom"
604
+ import root from "../../oxlint.config.ts"
605
+
606
+ export default defineConfig({ extends: [root, custom({ name: "web" })] })
607
+ ```
608
+
609
+ To share one rules folder across the workspace, keep it in a tooling package and call
610
+ `custom()` there. The relative `dir` resolves from the tooling package, so every config that
611
+ extends it loads the same rules:
612
+
613
+ ```ts
614
+ // tooling/lint/index.ts
615
+ import custom from "adamantite/lint/custom"
616
+
617
+ export default custom({ dir: "rules", name: "acme" })
618
+ ```
619
+
620
+ Run `adamantite rule add <name> --dir tooling/lint/rules` to add a rule to that folder.
621
+ Doctor starts from the Oxlint config of the root and of each workspace package, and follows
622
+ relative imports and imports of workspace packages to find the `custom()` calls. It resolves
623
+ `dir` and `name` when they are string literals.
624
+
398
625
  ## Requirements and boundaries
399
626
 
400
627
  - Adamantite requires Bun 1.0 or later when run with Bun, or Node.js 22.19 or later when