diff --git a/.changeset/clean-chicken-tan.md b/.changeset/clean-chicken-tan.md new file mode 100644 index 0000000..ab7cd50 --- /dev/null +++ b/.changeset/clean-chicken-tan.md @@ -0,0 +1,6 @@ +--- +'@feature-sliced/steiger-plugin': minor +'steiger': minor +--- + +Add no-ui-in-app rule diff --git a/README.md b/README.md index 3a8eed0..2dc724c 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Currently, Steiger is not extendable with more rules, though that will change in
no-reserved-folder-names
no-segmentless-slices
no-segments-on-sliced-layers
no-ui-in-app
ui
segment on the App layer.public-api
repetitive-naming
segments-by-purpose
ui
segment in app
layer.
+
+Examples of project structures that pass this rule:
+
+```
+📂 shared
+ 📂 ui
+ 📄 index.ts
+📂 pages
+ 📂 home
+ 📂 ui
+ 📄 index.ts
+📂 app
+ 📂 providers
+ 📄 index.ts
+```
+
+Examples of project structures that fail this rule:
+
+```
+📂 shared
+ 📂 ui
+ 📄 index.ts
+📂 pages
+ 📂 home
+ 📂 ui
+ 📄 index.ts
+📂 app
+ 📂 providers
+ 📄 index.ts
+ 📂 ui // ❌
+ 📄 index.ts
+```
+
+## Rationale
+
+It's uncommon to define the `ui` segment on the App layer. The App layer is typically used to combine the application into a single entry point. The UI of your application should already be created on the layers below to avoid mixing up responsibilities. Therefore, the `ui` segment on the App layer is typically a mistake.
+
+For example, context providers are components, but they are not UI. Global styles are technically UI, but they aren't scoped to that segment, so the name `ui` might be a misdirection.
+
+As one possible exception, the `ui` segment can be used on the App layer if the entire application consists of only one page and there is no reason to define the Pages layer.
diff --git a/packages/steiger-plugin-fsd/src/no-ui-in-app/index.spec.ts b/packages/steiger-plugin-fsd/src/no-ui-in-app/index.spec.ts
new file mode 100644
index 0000000..461a79e
--- /dev/null
+++ b/packages/steiger-plugin-fsd/src/no-ui-in-app/index.spec.ts
@@ -0,0 +1,46 @@
+import { expect, it } from 'vitest'
+
+import noUiInApp from './index.js'
+import { joinFromRoot, parseIntoFsdRoot } from '../_lib/prepare-test.js'
+
+it('reports no errors on a project without the "ui" segment on the "app" layer', () => {
+ const root = parseIntoFsdRoot(`
+ 📂 shared
+ 📂 ui
+ 📄 index.ts
+ 📂 pages
+ 📂 home
+ 📂 ui
+ 📄 index.ts
+ 📂 app
+ 📂 providers
+ 📄 index.ts
+ `)
+
+ expect(noUiInApp.check(root)).toEqual({ diagnostics: [] })
+})
+
+it('reports errors on a project with the "ui" segment on the "app" layer', () => {
+ const root = parseIntoFsdRoot(`
+ 📂 shared
+ 📂 ui
+ 📄 index.ts
+ 📂 pages
+ 📂 home
+ 📂 ui
+ 📄 index.ts
+ 📂 app
+ 📂 providers
+ 📄 index.ts
+ 📂 ui
+ 📄 index.ts
+ `)
+
+ const diagnostics = noUiInApp.check(root).diagnostics
+ expect(diagnostics).toEqual([
+ {
+ message: 'Layer "app" should not have "ui" segment.',
+ location: { path: joinFromRoot('app', 'ui') },
+ },
+ ])
+})
diff --git a/packages/steiger-plugin-fsd/src/no-ui-in-app/index.ts b/packages/steiger-plugin-fsd/src/no-ui-in-app/index.ts
new file mode 100644
index 0000000..4a88283
--- /dev/null
+++ b/packages/steiger-plugin-fsd/src/no-ui-in-app/index.ts
@@ -0,0 +1,27 @@
+import type { Diagnostic, Rule } from '@steiger/types'
+import { NAMESPACE } from '../constants.js'
+import { getLayers, getSegments } from '@feature-sliced/filesystem'
+
+const noUiInApp = {
+ name: `${NAMESPACE}/no-ui-in-app`,
+ check(root) {
+ const diagnostics: Arrayno-reserved-folder-names
no-segmentless-slices
no-segments-on-sliced-layers
no-ui-in-app
ui
segment on the App layer.public-api
repetitive-naming
segments-by-purpose