Lime Technology's shared JS/TS code-quality presets. One package, one pinned toolchain, consumed by every LT JavaScript repo so all code is held to the same aggressive bar everywhere.
Bundles and version-pins the whole stack (typescript-eslint, unicorn, sonarjs, import-x, eslint-comments, deslop) so a consumer installs one dependency instead of a dozen drifting ones.
The ESLint config is split into composable concerns. The three ready-made
presets (base / nuxt / worker) just stack the concerns in the right order
and append Prettier last; pull individual concerns when you want finer control.
Presets (start here):
| Export | = concerns | For |
|---|---|---|
eslint/base |
ignores + typescript + quality + testing | Plain TS libs / Node packages |
eslint/worker |
base + cloudflare-workers | Non-Nuxt Workers / services |
eslint/node |
base + Node runtime globals (no Workers builtin bans) | AWS Lambda handlers / Node services & CLIs |
eslint/nuxt |
base + cloudflare-workers + vue + webGUI globals | Nuxt 4 apps on Workers |
Concerns (compose your own):
| Export | Concern |
|---|---|
eslint/typescript |
Type-safety — strict-type-checked + stylistic, unsafe-any / promise / nullish rules |
eslint/quality |
Quality patterns — unicorn (tuned), sonarjs, eslint-comments, deslop, complexity/size budgets, duplication |
eslint/vue |
Vue 3 / Nuxt SFC parsing + auto-import awareness + team conventions |
eslint/react |
React JSX/TSX components + hooks rules + jsx-a11y recommended (curated, anti-slop) |
eslint/cloudflare-workers |
Workers runtime globals + no-Node-builtin guards |
eslint/testing |
Spec/fixture relaxations |
eslint/playwright |
Playwright specs (recommended + curated matcher/diagnostic rules) and configs (mandatory action/navigation timeouts) |
eslint/strict-size |
Opt-in: promotes max-lines + max-lines-per-function from warn to error (append after a preset once the repo is under budget) |
eslint/ignores |
Shared build-artifact ignores |
eslint/globals |
Raw globals maps (Workers + webGUI) |
Non-ESLint:
| Export | Purpose |
|---|---|
prettier |
Shared Prettier config for JS/TS, JSON, Markdown, CSS, and SCSS (single source of truth) |
tsconfig/base.json / nuxt.json / worker.json / node.json |
Extreme-strict tsconfig + framework variants (node.json = ES2022 + Bundler resolution + @types/node, for esbuild-bundled Lambdas) |
knip/base |
Shared knip dead-code baseline |
stylelint/base |
Shared Stylelint baseline (pins stylelint-config-standard); safe on all CSS, including token-source files |
stylelint/design-tokens |
Opt-in: forbids raw color literals (hex / named / rgb() / oklch() / …) so colors flow through tokens; layer onto base |
Type-safety + correctness + duplication + eslint-disable-abuse rules are
error. Complexity / size budgets are warn — you can't refactor an
existing backlog in one PR, so gate new code first and flip to error per repo
once the baseline is under budget. Pure-opinion rules that fight domain naming
(unicorn/name-replacements, abbreviation nagging, etc.) are disabled outright.
Function length is two-tier: the quality concern warns at 50 lines (the
"fits on a screen" nudge), and eslint/strict-size errors at 80 — a hard
stop for runaway functions without red-walling the 50–80 grey zone. Files use one
tier (400, warn → error) since splitting a file is mechanical.
Once a repo is under the size budget, lock it in by appending eslint/strict-size,
which promotes max-lines + max-lines-per-function to error (config/scripts
stay exempt). Cognitive/cyclomatic complexity intentionally stays warn — hard-
gating it rewards extracting nonsense helpers to beat the metric.
import base from "@unraid/js-standards/eslint/base";
import strictSize from "@unraid/js-standards/eslint/strict-size";
export default [...base, ...strictSize];- Type escapes —
no-explicit-any,no-non-null-assertion, unsafeanyassignment/call/return (typescript-eslintstrict-type-checked). - Async bugs — floating & misused promises, needless
await(a common Workers footgun). - Needless code —
no-unnecessary-conditionflags pointless guards on non-nullable values; knip deletes orphaned files/exports/deps. - Sprawl — complexity, cognitive-complexity,
max-lines,max-params, nesting budgets force refactors instead of 300-line functions. - Copy-paste — sonarjs duplicate-string / identical-functions.
- Escape hatches —
eslint-commentsbans blanket / undescribedeslint-disable. - Redundant comments — deslop flags comments that just restate the code.
- Nuxt data fetching (nuxt/vue preset) — reserves
$fetch()for user-triggered requests (event handlers, mutations) and flags data-loading$fetch()— a top-levelawait $fetch()in<script setup>or$fetch()inside a lifecycle hook — in favor ofuseFetch()/useAsyncData()(SSR payload transfer, request dedupe, consistent pending/error state).$fetch()inside function bodies is left alone.
eslint/vue adds Nuxt's own flat config (SFC parsing, auto-import +
generated-component awareness) plus our team conventions, and steers
data-loading $fetch() toward useFetch()/useAsyncData() (see above). It is
already folded into the eslint/nuxt preset; import it directly only when
composing a bespoke stack.
import base from "@unraid/js-standards/eslint/base";
import vue from "@unraid/js-standards/eslint/vue";
export default [...base, ...vue];Because it pulls Nuxt's bundled typescript-eslint / unicorn / import-x, dedupe those to a single version in the consumer (see Gotchas → pnpm overrides).
eslint/react layers React support onto a base/core preset. It registers
@eslint-react/eslint-plugin, eslint-plugin-react-hooks, and
eslint-plugin-jsx-a11y, applying only to component files (.jsx / .tsx).
import base from "@unraid/js-standards/eslint/base";
import react from "@unraid/js-standards/eslint/react";
export default [...base, ...react];The rule set is curated for correct React UI patterns + anti-slop, not
@eslint-react's full noisy recommended: @eslint-react/no-missing-key,
@eslint-react/no-duplicate-key,
@eslint-react/no-nested-component-definitions,
@eslint-react/dom-no-unsafe-target-blank, and
@eslint-react/dom-no-unknown-property are errors;
@eslint-react/no-array-index-key, @eslint-react/jsx-no-useless-fragment, and
@eslint-react/dom-no-dangerously-set-innerhtml are warns; jsx-a11y's
recommended flat config is spread in whole; and react-hooks/rules-of-hooks
(error) + react-hooks/exhaustive-deps (warn) — sourced from
eslint-plugin-react-hooks, not @eslint-react's own equivalents — guard the
classic hook footguns. Component files are exempted from
unicorn/filename-case since React components are PascalCase.
Composition note: unlike the Vue layer, this concern only bundles the three
React plugins (not typescript-eslint / unicorn), so it composes cleanly with the
typescript + quality concerns without the "Cannot redefine plugin" dedupe.
Layer it after the base/core concerns and before prettier.
ESLint 10 native: this concern uses
@eslint-react(peereslint: "*"), which is authored against the modern flat-config + context API and does not call thecontext.getFilename()method ESLint removed in v10. Nosettings.react.versionpin or other workaround is required — the curated rules are AST-based and React-version-independent, so they run on plain.jsxwith no type information.
import playwright from "@unraid/js-standards/eslint/playwright";Two halves — the specs and the config that runs them.
Specs (**/*.{spec,test}.*) get eslint-plugin-playwright's recommended set
(conditional logic in tests, forgotten await on assertions,
page.waitForTimeout sleeps, focused/skipped tests left behind) plus a curated
tier the plugin ships but leaves out of recommended, all at error: matcher
choices that make a failure legible (prefer-to-be, prefer-comparison-matcher,
…), require-to-pass-timeout, no-commented-out-tests, locator quality
(no-raw-locators, no-nth-methods, prefer-native-locators,
no-get-by-title), and structure (require-hook, require-top-level-describe,
no-slowed-test).
This states the target rather than the current state of any one suite. A repo adopting mid-stream downgrades specific rules in its own config, which keeps the exception visible and local instead of hidden in the shared baseline.
For i18n-heavy suites whose action steps should survive translated-copy edits, enable the opt-in stable-action rule on the files Playwright owns:
export default [
...playwright,
{
files: ["e2e/**/*.spec.ts"],
rules: {
"limetech-playwright/prefer-stable-action-locator": "error",
},
},
];The rule flags static copy selectors such as getByText("Save").click() and
getByRole("button", { name: "Save" }).click(), including static hasText
filters, when the locator drives an action. Prefer a stable data-testid for
those targets. Copy and accessible-name locators remain valid for assertions,
role-only locators remain valid, and dynamic scenario data is not treated as UI
copy. The rule is opt-in because third-party surfaces and accessibility-first
suites may intentionally use role/name actions and cannot always add test IDs.
Pure convention stays off — prefer-lowercase-title (style), require-tags
and no-restricted-* (need a project vocabulary), require-soft-assertions
(changes failure semantics), max-expects (an arbitrary budget), and no-hooks
(contradicts require-hook).
The plugin's rules key off bare
test/expectidentifiers. In a repo that also runs Vitest or Jest under that glob, re-scopefileson the spec entry to where Playwright actually owns — otherwise the rules give Playwright advice about a unit test.
Configs (playwright*.config.*) must set use.actionTimeout and
use.navigationTimeout explicitly.
Playwright defaults both to 0, which means no timeout rather than a sensible
one. expect() has its own default, so a config that sets expect.timeout looks
bounded — but that ceiling only covers assertions. A bare .click() or .goto()
on an element that never becomes actionable waits for the entire test timeout.
On a suite with a generous per-test budget that is a silent blackout: no output, no failing assertion, and a final error naming the test rather than the call that hung. Setting both turns it into a fast failure that names the locator.
Prefer framework-native styles first: Vue/Nuxt component CSS belongs in
<style> blocks, shared app styling belongs in the repo's stylesheet/Tailwind
layer, and packageable UI primitives should use the styling API native to that
component system.
When a Worker or server-rendered helper must include page-specific CSS, keep the source as a real stylesheet when the build tool supports it:
- Put page styles in a colocated
.cssfile and import it as raw text when the response needs inline<style>output. - Use a regular stylesheet route/static asset when the app already has an asset pipeline and the extra request/cache boundary is desirable.
- Use SCSS only in repos that already compile SCSS; do not add Sass for one generated page.
- Write normal formatted CSS: one selector per block, one declaration per line,
blank lines between rule groups, and expanded
@mediablocks. - Group design tokens/custom properties at the top, including theme overrides.
- If a build tool cannot import a CSS file, use a named module-level
String.rawtemplate as the fallback. Do not put stylesheet blobs inside render function bodies. - Do not commit minified or one-line CSS blobs unless the file is generated.
import formCss from "./form.css?raw";
export function renderFormHead(): string {
return `<style>${formCss}</style>`;
}Two Stylelint configs enforce the conventions above on hand-authored .css:
stylelint/basepinsstylelint-config-standardso every repo holds CSS to the same correctness bar (valid syntax, no duplicate selectors/properties, modern color-function notation). Formatting stays with Prettier — the standard config is non-stylistic, so they do not fight. Safe on all stylesheets, including the files that define design tokens.stylelint/design-tokenslayers on top for component/feature CSS and forbids raw color literals — hex, named colors, andrgb()/hsl()/oklch()/color()— so every color flows through a token (var(--…)) instead of being hardcoded. This is what keeps theming and light/dark handling in one place; it also catches a raw literal smuggled into avar(--token, #abc)fallback, which otherwise renders the literal when the token is missing instead of failing visibly.
Install Stylelint alongside this package (it is an optional peer) and compose the
configs. Restrict design-tokens to the CSS you hand-write, and leave the files
that define tokens on base only (they legitimately hold raw color values):
// stylelint.config.mjs
import base from "@unraid/js-standards/stylelint/base";
import designTokens from "@unraid/js-standards/stylelint/design-tokens";
export default {
...base,
...designTokens,
rules: { ...base.rules, ...designTokens.rules },
overrides: [
// Token-source files define colors, so keep them on the baseline only.
{ files: ["**/tokens/**/*.css"], rules: { ...base.rules } },
],
};Validating custom-property names against a known token set (catching a reference to a token that does not exist) is a project-level concern — add a repo-local allowlist rule for that. These configs guarantee a value is a token reference rather than a raw literal.
// eslint.config.mjs
import unraid from "@unraid/js-standards/eslint/nuxt";
export default [
...unraid,
{
// repo-specific overrides only
},
];For repos with existing formatting debt, wire format:check to the cleaned
paths first, then broaden it as the repo is formatted. Do not mix Prettier into
lint until the checked paths are under budget.
// tsconfig.json
{ "extends": "@unraid/js-standards/tsconfig/nuxt.json" }// knip.config.js
import base from "@unraid/js-standards/knip/base";
export default { ...base, entry: [...base.entry, "server/index.ts"] };Peer deps the consumer provides: eslint >=10.4, typescript >=5.5, and
(for the Nuxt preset) @nuxt/eslint-config. Oxlint is an optional peer
(oxlint >=1.77) for repositories that enable the fast pre-pass.
These presets are incompatible with repos that compile without
strictNullChecks. There is no supported escape hatch — enable the flag.
Several type-aware rules (no-unnecessary-condition,
prefer-nullish-coalescing, no-unnecessary-boolean-literal-compare,
no-useless-default-assignment, no-unnecessary-type-assertion,
no-unnecessary-type-conversion, sonarjs/different-types-comparison) are
only sound when null/undefined exist in the type system. Without the flag
they treat every optional value as always-present — and their autofixes
still apply, silently rewriting behavior. Observed in production use:
--fix deleted a live destructuring default and stripped as casts the real
typechecker requires, breaking typecheck from a lint run.
How to know you're affected: each of these rules self-reports
This rule requires the 'strictNullChecks' compiler option at position 0:1
of every file. Treat that message as a configuration error and fix the
tsconfig — do not baseline it away with --suppress-all, which is exactly
how it goes unnoticed.
The shared tsconfig/* exports already enable strict mode. Nuxt apps that
override typescript.tsConfig.compilerOptions must keep at least
strictNullChecks: true. Migration is cheaper than it looks: unraid/account
enabled the flag with 161 mechanical fixes, and its lint-suppressions
baseline shrank 36% because most of the "debt" was false findings from the
unsound configuration (unraid/account#1564).
TypeScript 7 (the native Go compiler, "Project Corsa") does not ship the
JavaScript compiler API that type-aware linters depend on — ts.createProgram
is gone. So the ESLint typescript/base concerns' type-aware rules do not
run on TS 7: typescript-eslint won't even install against typescript@7 (its
peer is >=4.8.4 <6.1.0) and crashes if forced. TypeScript 7.1 is expected to
ship a new (and different) programmatic API; until then there are two supported
paths:
1. Side-by-side TypeScript 6.0 (keep typescript-eslint). Per the
TypeScript team's guidance, TS 7 is designed to run alongside a TS 6.0 install
for tools that still need programmatic compiler access. Keep typescript@6 as
the typescript your linter/editor resolve (typescript-eslint's peer already
allows 6.0.x) and build/typecheck with the native compiler
(@typescript/native-preview). The ESLint concerns here work unchanged.
2. Oxlint + oxlint-tsgolint (native TS 7). Oxlint parses TypeScript with
its own Rust parser (no dependency on the typescript package) and gets type
information from tsgolint (built on typescript-go), so it runs natively on
TS 7. Use the oxlint/type-aware preset with oxlint --type-aware (see
Optional: type-aware Oxlint) — the
only linter that fully runs on a pure TS 7 toolchain today.
ESLint keeps only the last no-restricted-imports config for a given file —
two blocks don't merge. The cloudflare-workers concern already sets one (to
block Node builtins). If your repo needs its own restrictions too (e.g. banning
parent-relative imports), combine both into a single block rather than
adding a second one, or the concern's rule is silently overridden:
{
rules: {
"no-restricted-imports": [
"error",
{
paths: [
{ name: "fs", message: "Not available on the Workers runtime." },
// …the rest of the cloudflare-workers paths…
],
patterns: [
{ group: ["../*"], message: "Use #aliases instead of parent-relative imports." },
],
},
],
},
}The same caveat applies to no-restricted-syntax: the vue concern sets it (on
**/*.vue) to steer data-loading $fetch() toward useFetch()/useAsyncData().
If a consuming repo adds its own no-restricted-syntax, fold those selectors in
rather than replacing the block.
The nuxt/vue presets pull Nuxt's bundled typescript-eslint / unicorn /
import-x. Combined with our typescript + quality concerns that can produce
two copies of a plugin and ESLint throws "Cannot redefine plugin". Pin them to
one version in the consumer:
// package.json → pnpm.overrides
"@typescript-eslint/eslint-plugin": "8.62.1",
"@typescript-eslint/parser": "8.62.1",
"@typescript-eslint/utils": "8.62.1",
"eslint-plugin-unicorn": "69.0.0",
"eslint-plugin-import-x": "4.17.1"The package now dogfoods the same workflow it recommends to consumers:
- Oxlint runs first for fast syntax, correctness, suspicious-code, performance, import, promise, TypeScript, Unicorn, and Oxc checks.
- ESLint still runs fully afterward. Keep the full config when you need the Vue template rules, sonarjs cognitive-complexity and duplication, deslop, eslint-comments, and the complete typescript-eslint type-aware set.
- ESLint caching and worker threads are enabled for repeat and large-tree
runs. The cache lives under
.cache/and must not be committed.
The package scripts expose both the combined workflow and each pass separately:
pnpm lint # oxlint, then cached full ESLint
pnpm lint:fast # oxlint only
pnpm lint:eslint # cached full ESLint with worker threads
pnpm lint:eslint:full # full-tree ESLint without the cache
pnpm lint:eslint:errors # errors-only local check; skips warn rules
pnpm lint:eslint:stats # JSON timing data for profilingDo not use lint:eslint:errors for suppression-baseline generation: it skips
warning-level rules. For a deliberate full baseline, run lint:eslint:full
with the repository's reviewed --suppress-all command. Use
--cache-strategy content instead of the default metadata strategy only when
branch switches or generated file mtimes cause unnecessary cache misses.
Install the shared presets and the two linters:
pnpm add -D @unraid/js-standards eslint oxlint typescriptAdd these two small config files at the consumer repository root:
// eslint.config.mjs
import unraid from "@unraid/js-standards/eslint/base";
export default [...unraid];// .oxlintrc.json
{
"extends": ["./node_modules/@unraid/js-standards/src/oxlint/base.json"],
}Then copy the scripts below into package.json. Use the nuxt, worker, or
node ESLint export instead of base when the repository needs that runtime.
Oxlint's extends entries are file paths, which is why the quick-start points
at the package's installed src/oxlint/base.json file.
2. Keep ESLint full by default. Do not add the Oxlint dedupe concern when the ESLint pass is the authoritative complete run:
import unraid from "@unraid/js-standards/eslint/nuxt";
export default [...unraid];3. Optionally dedupe an explicitly optimized ESLint configuration. For very large repositories, append the exported concern last to turn off ESLint rules already covered by the shared Oxlint config. This reduces duplicate work, but it is a different mode from the full ESLint gate:
import unraid from "@unraid/js-standards/eslint/nuxt";
import oxlintDisable from "@unraid/js-standards/eslint/oxlint";
export default [
...unraid,
...oxlintDisable(), // must be last in the optimized mode
];Consumer scripts should mirror the package scripts:
"scripts": {
"lint:oxlint": "oxlint",
"lint:eslint": "eslint . --cache --cache-location .cache/eslint/ --concurrency=auto",
"lint:eslint:full": "eslint . --concurrency=auto",
"lint:fast": "pnpm run lint:oxlint",
"lint": "pnpm run lint:oxlint && pnpm run lint:eslint"
}For pre-commit, pass only staged source files to Oxlint and the cached ESLint
command. Keep the full-tree lint:eslint:full run in CI or pre-push. ESLint's
--stats --format json and Oxlint's --debug timings are diagnostic modes,
not normal lint commands.
If you want fast type-aware feedback too, Oxlint's --type-aware mode runs the
semantic rules (unsafe-any family, floating promises, await-thenable,
no-base-to-string, …) on the Go TypeScript compiler (tsgo) — measured at
~2.8s vs ESLint's 135s, no monorepo crash. Use the oxlint/type-aware preset
(= oxlint/base + pedantic, with the noisiest opt-in rules disabled):
// .oxlintrc.json
{
"extends": ["./node_modules/@unraid/js-standards/src/oxlint/type-aware.json"],
}// package.json — needs the Go backend as a devDep. The version tracks the
// TypeScript release it targets: 7.0.2xxx ↔ TS 7.0.2.
"devDependencies": { "oxlint-tsgolint": "^7" },
"scripts": { "lint:types:fast": "oxlint --type-aware" }Oxlint type-aware went stable (2026-07-22) and now covers 59 of 61 of typescript-eslint's type-aware rules (oxlint 1.77 / tsgolint 7.x, tracking TS 7.0.2) — but never the Vue/sonarjs/deslop rules, which stay ESLint-only.
- On TS ≤ 6, treat it as a fast advisory alongside the authoritative ESLint type-aware gate.
- On TS 7 (see TypeScript 7), it is the
primary type-aware engine —
typescript-eslintcan't run there, so Oxlint--type-awareowns type-safety while ESLint keeps the syntactic + Vue/quality rules.
The faster Rust linter is useful as the first pass, but ESLint remains the complete gate for this stack:
- Our repos are Nuxt/Vue. Oxlint can't fully support
eslint-plugin-vue(Vue uses its own compiler / modified AST, so many rules can't run against SFC templates), and Biome's Vue/Nuxt story is still thin. ESLint has the only mature Vue/Nuxt configs. - The quality-rule value lives in ESLint plugins. unicorn, sonarjs, deslop, and eslint-comments have no Biome/Oxlint equivalent.
- Type-aware rules are the core of this config. typescript-eslint's strict-type-checked remains the complete ESLint type-aware gate; Oxlint's native type-aware mode is available separately for repositories using its TypeScript 7-compatible toolchain.
New rules land warn-first for one minor, then flip to error. Pin an exact
version in consumers; let Renovate open the bump PR so its CI run is the test.
Currently private + GitHub Packages (publishConfig.access: "restricted").
To go public later: set "private": false, access: "public", drop the
registry override, and publish to npm.