Refactor/reacrhitecture to fsd+ #49
+195
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"plugins": ["import"],
|
||||
"categories": {
|
||||
"correctness": "error",
|
||||
"suspicious": "warn",
|
||||
"perf": "warn",
|
||||
"style": "warn",
|
||||
"restriction": "error"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"build",
|
||||
".svelte-kit",
|
||||
".vercel",
|
||||
"*.config.js",
|
||||
"*.config.ts"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"no-debugger": "error",
|
||||
"no-alert": "warn",
|
||||
"import/no-cycle": "error"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user