@sister.software/oxlint-config
v11.0.0
Published
Sister Software's oxlint config
Readme
@sister.software/oxlint-config
Sister Software's shared oxlint configuration.
Replaces @sister.software/eslint-config. It preserves the two features that mattered most —
platform-layering import boundaries and license-header enforcement — while shedding the
ESLint plugin ecosystem that made major upgrades painful.
Install
yarn add -D @sister.software/oxlint-config oxlintoxlint is a peer dependency. Node >=24 is required (oxlint runs the .ts config and the JS
header plugin via Node).
Usage
Create an oxlint.config.ts (oxlint auto-discovers it):
import { createOxlintConfig } from "@sister.software/oxlint-config"
export default createOxlintConfig({
spdxLicenseIdentifier: "AGPL-3.0",
})createOxlintConfig() returns a complete oxlint config object — there is no extends step.
Options
| Option | Default | Purpose |
| ------------------------ | ------------------------- | ----------------------------------------------------------- |
| packageNamespace | @sister.software | Namespace the runtime-boundary rules apply to. |
| copyrightHolder | Sister Software | @copyright value stamped into file headers. |
| spdxLicenseIdentifier | UNLICENSED | @license value stamped into file headers. |
| author | Teffen Ellis, et al. | @author value stamped into file headers. |
| react | false | Enable oxlint's React plugin. |
| headers | true | Enforce file headers (error severity, auto-fixed). |
| padding | true | Blank line before return/block-like statements. |
| consolePadding | true | Blank line on each side of a console.* call. |
| multilineStatementPadding | true | Blank line on each side of a multi-line statement. |
| braces | true | Braces around single-statement bodies. |
| restrictProcessGlobals | false | Forbid direct process.env / process.argv access. |
| limits | calibrated defaults | Override individual legibility ceilings. |
| testFilePatterns | **/*.test.ts, … | Globs where the size and constant rules switch off. |
| generatedFilePatterns | **/*.gen.ts, … | Globs where only the size ceilings switch off. |
| untypedFilePatterns | **/*.js, **/*.mjs, … | Globs where the multi-line JSDoc requirement switches off. |
| unnamedThresholds | false | Flag numeric literals used as comparison thresholds. |
| constantDocs | false | Require JSDoc on exported / SCREAMING_CASE constants. |
| lengthTruthiness | true | Rewrite explicit length comparisons to truthiness. |
| multilineJSDoc | true | Require JSDoc blocks to span multiple lines. |
| sectionMarkers | true | Enforce the banner → MARK → region → split ladder. |
| ignorePatterns | build output, .yarn | Replace the default ignore list. |
| overrides | {} | Extra config merged last (escape hatch). |
limits takes any subset of the ceilings; unspecified keys keep their calibrated default:
createOxlintConfig({
spdxLicenseIdentifier: "AGPL-3.0",
limits: { maxStatements: 60, complexity: 40 },
})Generated files are exempt from the size ceilings only — every correctness rule still applies, because
generated code ships. Point generatedFilePatterns at your own globs if they differ from the defaults
(**/*.gen.ts(x), **/*.generated.ts, **/generated/**).
Features
Platform layering
Files are matched by their final name segment, and cross-runtime imports are flagged via core
no-restricted-imports:
browser→client,browsernode→server,node,sdkagnostic→shared,commonworker→worker
A *-client file importing a *-server package (or vice versa) is an error; *-shared/*-common
files may not import Node built-ins at all.
License headers
A bundled oxlint JS plugin (sister-software/require-file-header) reports and auto-fixes the
@copyright/@license/@author header, preserving any other JSDoc tags already in the leading
comment block. Run oxlint --fix to stamp headers.
Statement padding
padding-lines (warn, autofixed) puts a blank line before the statements that end a path or a step —
return, continue, break, a bare x++/x-- — and before block-like statements. An interface,
type, enum or namespace with a body counts as block-like: same braces, same weight on the page.
multiline-statement-padding (warn, autofixed) puts one on each side of a statement that spans lines,
so a reader sees where it starts and stops without matching brackets:
const canonical = { const canonical = {
raw, raw,
source, source,
} → }
const aligned = align()
const aligned = align()An exported declaration is padded exactly like the unexported form — both rules look through the
export wrapper.
Neither edge is required at a block boundary, where the brace already separates. console.* calls are
left to the rule below, which groups consecutive ones — this one would split a run that happens to
contain a tall call, and a run of output is one thing.
Console padding
console-padding (warn, autofixed) puts a blank line on each side of a console.* call, so the lines
that produce output stand apart from the lines that compute it:
const total = sum(stats)
console.log(`${total} shards`) const total = sum(stats)
const headers = ["source"]
console.log(headers) → console.log(`${total} shards`)
const headers = ["source"]
console.log(headers)A RUN of calls is one group — no blank lines inside it, one before the first and one after the last, because consecutive calls are one block of output. Neither edge is required where a block boundary already separates them: nothing before a call that opens a block, nothing after one that closes it.
Section markers
Four rules over the comments people use to carve a file into sections. They form one ladder, and each rung is a smaller file than the last:
// ---------------------------------------------------------------
// Second section → // MARK: Second section
// ---------------------------------------------------------------prefer-mark-comment(error, autofixed) — a----banner is three lines of decoration carrying one label. It becomes// MARK: <label>, with a blank line on each side. A banner wrapping more than one line of prose is reported but never collapsed — that prose is content.concise-section-marker(error) — a marker's label is a label, not a paragraph. PastmaxBodyLength(60), the detail belongs in the JSDoc of what it describes, or the file wants splitting and an@fileblock.prefer-region-over-marks(warn) — past one marker, a file has sections, and sections have ENDS.//#region///#endregionfold, so a reader can collapse what they are not reading.max-regions(warn) — pastmaxRegions(10), the sections want to be files. Deliberately high: four well-named regions are usually fine, and splitting can separate a type from its helpers. This is a runaway detector, not a style nudge.
Rungs 3 and 4 warn rather than error: where a section ends, and whether it should move, are judgment calls no fixer gets to make. Both switch off in test files, where the suite's shape is dictated by the code under test.
createOxlintConfig({ sectionMarkers: { maxBodyLength: 80, maxRegions: 6 } })Multi-line JSDoc
multiline-jsdoc (error, autofixed) expands a one-line block:
/** The answer. */ → /**
* The answer.
*/A one-line block reads as an aside; the multi-line form reads as documentation, and it leaves
somewhere to put the second sentence. Only blocks that lead their line are rewritten — one sharing a
line with code is a type cast, and expanding it changes what the line means. Off for
untypedFilePatterns, where a JSDoc block often IS the type annotation.
Legibility tiers
v10 turns on four tiers of upstream rules by default:
- Structural guardrails — nesting depth, function and file size, parameter count, cyclomatic
complexity. The numbers are ceilings, calibrated so considered code stays silent and runaway
generation does not. Tune them with
limits. - Defect classes
correctnessmisses — import cycles, in-place.sort()/.reverse()on arrays that may be shared, hook-order violations,.then()without a return, dead stores. - Test discipline — the vitest plugin, including
expect-expect, which catches a test that runs, passes, and asserts nothing. - Mechanical hygiene — literal form, import discipline, modern-API preference. All autofixable
via
oxlint --fix.
The design record lives in docs/superpowers/specs/2026-07-28-oxlint-legibility-tiers-design.md,
including a register of the rules that were measured and turned down. Check it before enabling
something that looks like an oversight — no-magic-numbers in particular is off on purpose, and
verify-fixtures.mjs fails the build if it or eleven others are switched on.
Named thresholds and documented constants
Two opt-in rules, complements rather than duplicates:
unnamedThresholdsflags a number used as a comparison threshold —if (entries.length > 12). Numbers inside array and object literals are untouched, so a bounding box or a codepoint table stays silent. Radix-prefixed literals are exempt by default:cp >= 0x3040already reads as a codepoint boundary.constantDocsrequires a JSDoc block on module-level constants that are exported orSCREAMING_CASE— the two signals that a constant is public surface or a tuning knob. Function-valued constants are out of scope.
The first says a threshold needs a name; the second says a name needs an explanation. A data table satisfies both by documenting the table, rather than extracting a constant per row.
Write provenance in those blocks — what the value means and where it came from:
/** Entries above this count mean the surface is too ambiguous to disambiguate; set by the 2026-03 sweep. */
const AMBIGUITY_CEILING = 12Not /** The ambiguity ceiling. */, which leaves the reader exactly where they started.
Length checks
The house convention is truthiness — if (items.length), not if (items.length > 0).
sister-software/prefer-length-truthiness enforces it and autofixes, which is why
unicorn/explicit-length-check (which enforces the opposite) is off.
It only rewrites where the value is already coerced to a boolean — a condition, a ternary test, the
operand of !, or a &&/|| inside one of those. Elsewhere the comparison is the value, and
rewriting it would change the type:
if (items.length > 0) { … } // → if (items.length)
if (items.length === 0) { … } // → if (!items.length)
const hasItems = items.length > 0 // untouched — this is a boolean, not a condition
if (items.length === 2) { … } // untouched — not an emptiness checkEscape hatch
Both rules, and every tier rule, take a scoped disable with a stated reason:
// oxlint-disable-next-line max-statements -- generated dispatch table; splitting it hides the shape.Prefer that over a config-level exemption, which silently widens to every future file.
