196 lines
7.0 KiB
JSON
196 lines
7.0 KiB
JSON
|
|
{
|
||
|
|
"$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"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|