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 Forbid subfolders in segments that have the same name as other conventional segments. no-segmentless-slices Forbid slices that don't have any segments. no-segments-on-sliced-layers Forbid segments (like ui, lib, api ...) that appear directly in sliced layer folders (entities, features, ...) + no-ui-in-app Forbid having the ui segment on the App layer. public-api Require slices (and segments on sliceless layers like Shared) to have a public API definition. repetitive-naming Ensure that all entities are named consistently in terms of pluralization. segments-by-purpose Discourage the use of segment names that group code by its essence, and instead encourage grouping by purpose diff --git a/packages/steiger-plugin-fsd/src/index.ts b/packages/steiger-plugin-fsd/src/index.ts index 74de09b..0b2d344 100644 --- a/packages/steiger-plugin-fsd/src/index.ts +++ b/packages/steiger-plugin-fsd/src/index.ts @@ -10,6 +10,7 @@ import noPublicApiSidestep from './no-public-api-sidestep/index.js' import noReservedFolderNames from './no-reserved-folder-names/index.js' import noSegmentlessSlices from './no-segmentless-slices/index.js' import noSegmentsOnSlicedLayers from './no-segments-on-sliced-layers/index.js' +import noUiInApp from './no-ui-in-app/index.js' import publicApi from './public-api/index.js' import repetitiveNaming from './repetitive-naming/index.js' import segmentsByPurpose from './segments-by-purpose/index.js' @@ -28,6 +29,7 @@ const allRules: Array = [ noReservedFolderNames, noSegmentlessSlices, noSegmentsOnSlicedLayers, + noUiInApp, publicApi, repetitiveNaming, segmentsByPurpose, diff --git a/packages/steiger-plugin-fsd/src/no-ui-in-app/README.md b/packages/steiger-plugin-fsd/src/no-ui-in-app/README.md new file mode 100644 index 0000000..6cffbfd --- /dev/null +++ b/packages/steiger-plugin-fsd/src/no-ui-in-app/README.md @@ -0,0 +1,43 @@ +# `no-ui-in-app` + +Forbid having 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: Array = [] + + const layers = getLayers(root) + + if (layers.app !== undefined) { + const segments = getSegments(layers.app) + + if (segments.ui !== undefined) { + diagnostics.push({ + message: 'Layer "app" should not have "ui" segment.', + location: { path: segments.ui.path }, + }) + } + } + + return { diagnostics } + }, +} satisfies Rule + +export default noUiInApp diff --git a/packages/steiger/README.md b/packages/steiger/README.md index 3a8eed0..2dc724c 100644 --- a/packages/steiger/README.md +++ b/packages/steiger/README.md @@ -69,6 +69,7 @@ Currently, Steiger is not extendable with more rules, though that will change in no-reserved-folder-names Forbid subfolders in segments that have the same name as other conventional segments. no-segmentless-slices Forbid slices that don't have any segments. no-segments-on-sliced-layers Forbid segments (like ui, lib, api ...) that appear directly in sliced layer folders (entities, features, ...) + no-ui-in-app Forbid having the ui segment on the App layer. public-api Require slices (and segments on sliceless layers like Shared) to have a public API definition. repetitive-naming Ensure that all entities are named consistently in terms of pluralization. segments-by-purpose Discourage the use of segment names that group code by its essence, and instead encourage grouping by purpose diff --git a/packages/steiger/migrations/convert-config-to-flat.js b/packages/steiger/migrations/convert-config-to-flat.js index 6632c84..6e3272c 100644 --- a/packages/steiger/migrations/convert-config-to-flat.js +++ b/packages/steiger/migrations/convert-config-to-flat.js @@ -63,6 +63,7 @@ const ruleNames = [ 'no-reserved-folder-names', 'no-segmentless-slices', 'no-segments-on-sliced-layers', + 'no-ui-in-app', 'public-api', 'repetitive-naming', 'segments-by-purpose',