chore(lint): repair oxlint config and enforce FSD boundaries

oxlint was never loading its config: the file was named oxlint.json but
oxlint only auto-discovers .oxlintrc.json/.jsonc, and the `ignore` field
was invalid (should be `ignorePatterns`). So import/no-cycle and every
other rule silently never ran.

- rename oxlint.json -> .oxlintrc.json, fix ignore -> ignorePatterns
- turn off the restriction/style category grab-bags (opt-in, partly
  contradictory); enable wanted rules individually
- add overrides enforcing FSD layer direction and the interior
  ui -> model -> domain law via no-restricted-imports (oxlint has no
  zone rule); import/no-cycle resolves $-aliases via tsconfig discovery
This commit is contained in:
Ilia Mashkov
2026-06-02 23:01:04 +03:00
parent 431fb41a7f
commit deefb51b57
2 changed files with 195 additions and 29 deletions
+195
View File
@@ -0,0 +1,195 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["import"],
"categories": {
"correctness": "error",
"suspicious": "warn",
"perf": "warn",
// style/restriction off: opt-in, contradictory grab-bags. Wanted rules enabled individually below.
"style": "off",
"restriction": "off"
},
"env": {
"browser": true,
"es2021": true
},
"ignorePatterns": [
"node_modules",
"dist",
"build",
".svelte-kit",
".vercel",
"*.config.js",
"*.config.ts"
],
"rules": {
"no-console": "warn",
"no-debugger": "error",
"no-alert": "warn",
// no-cycle resolves $-aliases via tsconfig auto-discovery (no resolver config in oxlint)
"import/no-cycle": "error",
"import/no-duplicates": "warn",
"import/no-unassigned-import": "off", // CSS/side-effect imports are intentional
"no-sequences": "error",
"no-underscore-dangle": "off",
"no-shadow": "warn",
"no-implicit-coercion": "warn",
"no-await-in-loop": "warn",
"no-return-assign": "warn",
"no-new": "warn",
"no-unneeded-ternary": "warn"
},
// FSD boundaries. oxlint has no zone rule, so layer/segment direction is enforced
// with no-restricted-imports patterns scoped per glob. Layer order (high->low):
// app(exempt top shell) > routes > widgets > features > entities > shared.
// A layer bans imports from itself (cross-slice via alias) and every layer above.
// Overrides are LAST-WINS, not merged: a file matching two overrides keeps only the
// last rule config. So the domain override (below) is a self-contained superset, and
// the test/story override (last) fully disables boundary checks for those files.
"overrides": [
// shared = lowest layer: imports nothing above it
{
"files": ["src/shared/**"],
"rules": {
"no-restricted-imports": ["error", {
"patterns": [
{
"group": [
"$app",
"$app/*",
"$routes",
"$routes/*",
"$widgets",
"$widgets/*",
"$features",
"$features/*",
"$entities",
"$entities/*"
],
"message": "FSD layer violation: `shared` is the lowest layer and may not import from any layer above it."
}
]
}]
}
},
// entities: import shared only; no other entity via alias; interior ui<-only-ui
{
"files": ["src/entities/**"],
"rules": {
"no-restricted-imports": ["error", {
"patterns": [
{
"group": ["$app", "$app/*", "$routes", "$routes/*", "$widgets", "$widgets/*", "$features", "$features/*"],
"message": "FSD layer violation: `entities` may only import from `shared`."
},
{
"group": ["$entities", "$entities/*"],
"message": "FSD cross-slice violation: do not import another entity via its alias. Use relative imports inside your own slice; invert the dependency through a higher layer for cross-slice needs."
},
{
"group": ["../ui", "../ui/*", "../../ui/*"],
"message": "FSD segment violation: only `ui` may import `ui`. Interior direction is ui -> model -> domain."
}
]
}]
}
},
// features: import entities/shared only; no other feature via alias
{
"files": ["src/features/**"],
"rules": {
"no-restricted-imports": ["error", {
"patterns": [
{
"group": ["$app", "$app/*", "$routes", "$routes/*", "$widgets", "$widgets/*"],
"message": "FSD layer violation: `features` may only import from `entities` and `shared`."
},
{
"group": ["$features", "$features/*"],
"message": "FSD cross-slice violation: do not import another feature via its alias. Invert the dependency through a higher layer (widget/route)."
},
{
"group": ["../ui", "../ui/*", "../../ui/*"],
"message": "FSD segment violation: only `ui` may import `ui`. Interior direction is ui -> model -> domain."
}
]
}]
}
},
// widgets: import features/entities/shared only; no other widget via alias
{
"files": ["src/widgets/**"],
"rules": {
"no-restricted-imports": ["error", {
"patterns": [
{
"group": ["$app", "$app/*", "$routes", "$routes/*"],
"message": "FSD layer violation: `widgets` may only import from `features`, `entities`, and `shared`."
},
{
"group": ["$widgets", "$widgets/*"],
"message": "FSD cross-slice violation: do not import another widget via its alias. Invert the dependency through the route layer."
},
{
"group": ["../ui", "../ui/*", "../../ui/*"],
"message": "FSD segment violation: only `ui` may import `ui`. Interior direction is ui -> model -> domain."
}
]
}]
}
},
// routes: top of the FSD list, imports any layer below; only app is above it
{
"files": ["src/routes/**"],
"rules": {
"no-restricted-imports": ["error", {
"patterns": [
{ "group": ["$app", "$app/*"], "message": "FSD layer violation: `routes` may not import from `app`." }
]
}]
}
},
// domain (FSD+): pure logic. Imports NO layer (not even shared) and no sibling
// model/ui segment. Superset: wins over the layer override above for these files.
{
"files": ["src/**/domain/**"],
"rules": {
"no-restricted-imports": ["error", {
"patterns": [
{
"group": [
"$app",
"$app/*",
"$routes",
"$routes/*",
"$widgets",
"$widgets/*",
"$features",
"$features/*",
"$entities",
"$entities/*",
"$shared",
"$shared/*"
],
"message": "FSD+ domain isolation: `domain` is pure business logic and may not import any layer (including `shared`). Allowed: relative imports within `domain` and framework-agnostic npm packages."
},
{
"group": ["../model", "../model/*", "../../model/*", "../ui", "../ui/*", "../../ui/*"],
"message": "FSD+ domain isolation: `domain` may not import sibling `model` or `ui` segments. Dependency flows ui -> model -> domain, never back."
}
]
}]
}
},
// tests/stories/fixtures legitimately cross-import (e.g. $entities/Font/testing).
// Must be LAST so last-wins disables boundary checks for them.
{
"files": ["**/*.test.ts", "**/*.spec.ts", "**/*.stories.svelte", "src/**/testing/**"],
"rules": {
"no-restricted-imports": "off"
}
}
]
}