@dobroslavlabs/oxlint
v0.1.4
Published
Custom Oxlint JS plugin and Ultracite-style presets for JS/TS, React, Effect, Elysia, Bun, Zod, and TanStack Router
Downloads
138
Maintainers
Readme
@dobroslavlabs/oxlint
Custom Oxlint JS plugin + Ultracite-style presets for JS/TS, React/JSX, Effect, Elysia, Bun, and Zod.
- Package:
@dobroslavlabs/oxlint(0.1.3) - Repo: github.com/dobroslavlabs/oxlint
- Author: Dobroslav Radosavljevic
- Rule IDs:
dobroslavlabs/<rule> - Changelog: CHANGELOG.md
Quick start
bun add -d oxlint @dobroslavlabs/oxlint// oxlint.config.ts
import { defineConfig } from "oxlint";
import recommended from "@dobroslavlabs/oxlint/recommended";
export default defineConfig({
extends: [recommended],
});Or pick surfaces:
import { defineConfig } from "oxlint";
import core from "@dobroslavlabs/oxlint/core"; // JS/TS
import react from "@dobroslavlabs/oxlint/react"; // JSX/TSX + hooks
import effect from "@dobroslavlabs/oxlint/effect"; // Effect (opt-in)
import elysia from "@dobroslavlabs/oxlint/elysia"; // Elysia (opt-in)
import bun from "@dobroslavlabs/oxlint/bun"; // Bun (opt-in)
import zod from "@dobroslavlabs/oxlint/zod"; // Zod (opt-in)
import tanstackRouter from "@dobroslavlabs/oxlint/tanstack-router"; // TanStack Router (opt-in)
export default defineConfig({
extends: [core, react, effect, elysia, bun, zod, tanstackRouter],
});Presets
| Import | Scope | Enables |
| --- | --- | --- |
| @dobroslavlabs/oxlint/recommended | core + react | Default JS/TS + React set |
| @dobroslavlabs/oxlint/core | all files | Plain JS/TS rules |
| @dobroslavlabs/oxlint/react | React/JSX (+ hooks) | Component / JSX / hook rules |
| @dobroslavlabs/oxlint/effect | all files | Effect / tagged-error rules (opt-in) |
| @dobroslavlabs/oxlint/elysia | all files | ElysiaJS best-practice rules (opt-in) |
| @dobroslavlabs/oxlint/bun | all files | Prefer Bun-native APIs (opt-in) |
| @dobroslavlabs/oxlint/zod | all files | Zod schema rules (opt-in) |
| @dobroslavlabs/oxlint/tanstack-router | all files | TanStack Router typed-nav rules (opt-in) |
| @dobroslavlabs/oxlint | — | Plugin only; enable dobroslavlabs/* yourself |
Each preset sets jsPlugins: ["@dobroslavlabs/oxlint"] and turns on the matching rules. /effect, /elysia, /bun, /zod, and /tanstack-router are not included in /recommended.
Rules
Core
| Rule | Summary |
| --- | --- |
| require-jsdoc | Descriptive JSDoc on complex functions (defaults: statements ≥4, params ≥3, complexity ≥3) |
| no-double-type-assertion | Ban nested / as unknown as assertions |
| no-response-json-type-assertion | Ban asserting any *.json() call output |
| no-dom-target-type-assertion | Ban casting event.target / e.target to DOM elements |
| no-direct-process-env | Ban process.env (allows NODE_ENV, VITEST, JEST_WORKER_ID by default) |
| no-index-barrels | Ban index.* barrel files (allow for package entrypoints) |
| no-index-imports | Ban imports with an explicit /index path segment |
| no-pure-re-export-files | Ban non-index files that only re-export |
| no-forbidden-imports | Configurable import denylist (paths / patterns) — no-op until configured |
Most core rules accept { allow?: string[] } (path substrings — use distinctive fragments). no-direct-process-env also takes allowKeys ([] bans all keys; does not fall back to defaults).
Notes: no-index-imports only flags explicit /index paths (directory imports like ./components are allowed).
"dobroslavlabs/require-jsdoc": ["error", { minStatements: 5, minParams: 4 }],
"dobroslavlabs/no-index-barrels": ["error", { allow: ["src/index.ts"] }],
"dobroslavlabs/no-direct-process-env": ["error", { allowKeys: ["NODE_ENV", "CI"] }],
"dobroslavlabs/no-forbidden-imports": [
"error",
{
paths: [{ name: "lucide-react", message: "Use Hugeicons instead." }],
patterns: [{ regex: "^@unpic/react", message: "Use the shared Image component." }],
},
],React
| Rule | Summary |
| --- | --- |
| no-native-html | Ban high-confidence native HTML; suggest UI components by name |
| no-react-namespace | Ban import React / import * as React and React.* access |
| component-file-name-match | First primary exported component matches kebab file basename |
| no-multi-component-files | One module-level primary component per file (*Impl / *Provider / *Context ignored) |
| hook-file-name-match | Hook in use-*.ts(x) matches basename |
| no-multi-hook-files | One hook per use-*.ts(x) file |
| no-jsx-module-constants | No module-level JSX const |
| no-jsx-local-constants-in-components | No local JSX const inside components |
| no-jsx-variable-reassignment-in-components | No JSX via local reassignment |
| no-jsx-iife-in-components | No JSX-returning IIFEs in components |
| no-render-helper-functions-in-components | No nested JSX-returning helpers in components |
React JSX rules are scoped to **/*.{jsx,tsx} in the react preset. Hook rules and no-react-namespace also apply to plain .ts (React types / hooks). Most accept { allow?: string[] }.
Limits (v1): class components and forwardRef wrappers are not covered by file-shape / JSX-hygiene rules. Secondary named exports after the first primary component are ignored by component-file-name-match.
no-native-html default ban list:button · a · input · textarea · select · option · optgroup · label · img · video · audio · picture · source · iframe · dialog · details · summary · progress · ul · ol · li · table · thead · tbody · tfoot · tr · th · td · caption · hr
"dobroslavlabs/no-native-html": ["error", { tags: ["button", "a"] }],Effect (opt-in — @dobroslavlabs/oxlint/effect)
| Rule | Summary |
| --- | --- |
| no-run-promise-in-domain | Ban runPromise* outside boundary paths (defaults: /routes/, /runtime/, /cli/, /main.ts, …) |
| no-run-promise-in-effect-tests | Ban runPromise* in tests — prefer @effect/vitest it.effect |
| no-run-effect-inside-effect | Ban runners inside Effect.gen / Effect.fn |
| no-try-catch-in-effect-gen | Ban try/catch inside gen/fn — use Effect.try / catchTag |
| prefer-return-yield-star | Prefer return yield* for the terminal yield in a gen/fn body |
| no-outdated-effect-api | Ban curated Effect v3 APIs (Effect.catchAll, Context.Tag, …) |
| no-effect-run-sync-at-module-scope | Ban top-level runSync / Effect.runSync / *.runSync |
| no-singleton-service-export | Ban export const fooService = new FooService() |
| require-tagged-error-class | Prefer Schema.TaggedErrorClass over extends Error / Schema.TaggedError |
| require-tagged-error-message | Require message on TaggedErrorClass()(tag, schema) schemas |
| no-effect-wrapper-helpers | Ban unwrap / fromResult bridges (narrow filenames: from-nullable, effect-result, …) |
| no-multiple-effect-provide | Prefer one composed Layer over many Effect.provide in a pipe chain |
| prefer-effect-fn | Prefer Effect.fn("Name") over bare module-scope Effect.gen (skips program/main/boundaries) |
| no-tagged-error-instanceof | Ban instanceof SomeError for tagged/domain errors (use catchTag) |
| no-generic-error-in-fail | Ban new Error(...) in Effect.fail / Result.err (configurable methods) |
Most accept { allow?: string[] }. Pair with @effect/language-service for type-aware checks (floating Effect, full outdated API catalog, etc.).
import effect from "@dobroslavlabs/oxlint/effect";
export default defineConfig({
extends: [recommended, effect],
});Zod (opt-in — @dobroslavlabs/oxlint/zod)
| Rule | Summary |
| --- | --- |
| zod-modern-format-validators | Prefer z.email() / z.url() / z.uuid() over z.string().email() style |
| zod-schema-naming | Exported z.* schema defs must be PascalCase *Schema |
zod-schema-naming flags definitional z.object() / z.string() etc., not UserSchema.pick() or makeUserSchema().
import zod from "@dobroslavlabs/oxlint/zod";
export default defineConfig({
extends: [recommended, zod],
});Elysia (opt-in — @dobroslavlabs/oxlint/elysia)
Rules only run in files that import from elysia.
| Rule | Summary |
| --- | --- |
| no-context-param | Ban typing handler params as Context — destructure fields |
| prefer-status-helper | Prefer status(code, value) over set.status in handlers/hooks |
| require-route-schema | Require body/query/params/… on post/put/patch (honors .guard() / .group()) |
| require-plugin-name | Require { name } on exported new Elysia() plugins (skips .listen / entry/modules paths) |
| no-functional-plugin-callback | Prefer new Elysia() over .use((app) => …) |
| no-controller-context-class | Ban class methods typed with Context |
| prefer-throw-status | Prefer status() / throw status() over throw new Error / string throws |
| no-cookie-undefined-check | Check cookie.x.value, not cookie.x as optional |
| require-response-schema | When handler uses bare status(), declare a response schema |
import elysia from "@dobroslavlabs/oxlint/elysia";
export default defineConfig({
extends: [recommended, elysia],
});Bun (opt-in — @dobroslavlabs/oxlint/bun)
For Bun apps. Prefer Bun-native APIs over Node fallbacks (docs: Bun.file / Bun.write / Bun.spawn / bun:sqlite). Does not ban node:fs directory APIs (readdir, mkdir, …). Use { allow } for isomorphic packages.
| Rule | Summary |
| --- | --- |
| prefer-bun-file-read | fs.readFile* → Bun.file(…).text() / .json() / .bytes() |
| prefer-bun-write | fs.writeFile* → Bun.write (not appendFile) |
| prefer-bun-spawn | child_process.spawn/fork/execFile* → Bun.spawn / Bun.spawnSync |
| prefer-import-meta-paths | __dirname / __filename → import.meta.dir / import.meta.path |
| no-node-http-server | Ban http(s).createServer — use Bun.serve or Elysia/Hono |
| prefer-bun-sqlite | Ban better-sqlite3 / sqlite3 / node:sqlite → bun:sqlite |
| prefer-bun-sqlite-strict | Prefer new Database(…, { strict: true }) |
| prefer-bun-shell | execa / child_process.exec* shell strings → $`…` |
| prefer-response-json | JSON.stringify + end/send/Response → Response.json |
| no-bun-sleep-sync-in-async | No Bun.sleepSync inside async — use await Bun.sleep |
import bun from "@dobroslavlabs/oxlint/bun";
export default defineConfig({
extends: [recommended, bun],
});TanStack Router (opt-in — @dobroslavlabs/oxlint/tanstack-router)
Super-strict typed navigation. Rules only run in files that import from @tanstack/react-router, @tanstack/solid-router, or Start re-exports. Prefer string-literal to + params/search, or typed linkOptions (spread / pass-through).
| Rule | Summary |
| --- | --- |
| no-dynamic-router-to | Ban non-literal to (templates with ${}, concat, variables) on Link / Navigate / navigate / redirect / linkOptions / router.navigate |
| no-router-href | Ban href except external URL literals (http(s):, mailto:, tel:, //) |
| no-relative-router-to-without-from | Relative to (., .., non-/ paths) requires from |
| no-router-type-assertion | Ban casts / annotations / generics that bypass router inference on to/from and hook returns |
| require-router-hook-from | Bare useNavigate / useParams / useSearch / … need from or strict: false (Route.useX() OK) |
| no-imperative-location-navigation | Ban location / history / router.history for in-app navigation |
import tanstackRouter from "@dobroslavlabs/oxlint/tanstack-router";
export default defineConfig({
extends: [recommended, tanstackRouter],
});Develop
bun install
bun run test # Vitest + Oxlint RuleTester (Node 22+)
bun run typecheck # strict TypeScript 7
bun run fmt # oxfmt
bun run lint # build + ultracite check (dogfoods this package)
bun run build # tsdown → dist (publint + attw + unused)
bun run check # build → typecheck → test → fmt:check → lintStack: Bun · TypeScript 7 · Ultracite (oxlint + oxfmt) · tsdown · Vitest · @oxlint/plugins
CI (.github/workflows/ci.yml) runs check on main push/PR. It does not publish.
Editor: Oxc VS Code extension (format + fix on save — import sort via oxfmt, unused-import removal via oxlint).
Publish (local only)
bun run login # npm login --auth-type=web (browser / 2FA)
bun run pack:dry # check + npm pack --dry-run
bun run release # npm publish --access publicprepublishOnly runs check; prepack runs build. See CHANGELOG.md for release notes.
