Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make sure forbidden-imports rule checks files directly inside layers #64

Merged
5 changes: 5 additions & 0 deletions .changeset/quick-eggs-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@feature-sliced/steiger-plugin': patch
---

Make sure forbidden-imports rule checks files directly inside layers
39 changes: 39 additions & 0 deletions packages/steiger-plugin-fsd/src/_lib/index-source-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ export function indexSourceFiles(root: Folder): Record<string, SourceFile> {
}

for (const [layerName, layer] of Object.entries(getLayers(root))) {
// Even though files that are directly inside a layer are not encouraged by the FSD and are forbidden in most cases
// (except for an index/root file for the app layer as an entry point to the application), users can still add them.
// So, we need to index all files directly inside a layer to find errors.
layer.children
.filter((child) => child.type === 'file')
.forEach((file) => walk(file, { layerName: layerName as LayerName, sliceName: null, segmentName: null }))
daniilsapa marked this conversation as resolved.
Show resolved Hide resolved

if (!isSliced(layer)) {
for (const [segmentName, segment] of Object.entries(getSegments(layer))) {
walk(segment, { layerName: layerName as LayerName, sliceName: null, segmentName })
Expand Down Expand Up @@ -70,6 +77,11 @@ if (import.meta.vitest) {
📄 EditorPage.tsx
📄 Editor.tsx
📄 index.ts
📂 app
📂 ui
📄 index.ts
📄 root.ts
📄 index.ts
`)

expect(indexSourceFiles(root)).toEqual({
Expand Down Expand Up @@ -163,6 +175,33 @@ if (import.meta.vitest) {
segmentName: 'ui',
sliceName: null,
},
[joinFromRoot('app', 'ui', 'index.ts')]: {
file: {
path: joinFromRoot('app', 'ui', 'index.ts'),
type: 'file',
},
layerName: 'app',
segmentName: 'ui',
sliceName: null,
},
[joinFromRoot('app', 'root.ts')]: {
file: {
path: joinFromRoot('app', 'root.ts'),
type: 'file',
},
layerName: 'app',
segmentName: 'root',
sliceName: null,
},
[joinFromRoot('app', 'index.ts')]: {
file: {
path: joinFromRoot('app', 'index.ts'),
type: 'file',
},
layerName: 'app',
segmentName: null,
sliceName: null,
},
})
})
}
105 changes: 103 additions & 2 deletions packages/steiger-plugin-fsd/src/_lib/prepare-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,31 @@ import type { FsdRoot } from '@feature-sliced/filesystem'
import type { Folder, File, Diagnostic } from '@steiger/types'
import { vi } from 'vitest'

function findSubfolder(folder: Folder, path: string): Folder {
function checkFolder(folder: Folder): Folder {
if (folder.path === path) {
return folder
}

if (path.startsWith(folder.path)) {
for (const child of folder.children) {
if (child.type === 'folder') {
const result = checkFolder(child)
if (result) {
return result
}
}
}
}

throw new Error(`Path "${path}" not found in the provided file system mock!`)
}

return checkFolder(folder)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: I meant a more simple solution — you pass the root path as the second argument, and whatever file tree you write with emojis is "mounted" to that root. For example:

parseIntoFsdRoot(`
  📂 test
    📄 file
`, "/home/illright");  // the "file" becomes `/home/illright/test/file`

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I agree, the simpler the better, the KISS principle should not be forgotten 😂


/** Parse a multi-line indented string with emojis for files and folders into an FSD root. */
export function parseIntoFsdRoot(fsMarkup: string): FsdRoot {
export function parseIntoFsdRoot(fsMarkup: string, rootPath?: string): FsdRoot {
function parseFolder(lines: Array<string>, path: string): Folder {
const children: Array<Folder | File> = []

Expand All @@ -32,8 +55,9 @@ export function parseIntoFsdRoot(fsMarkup: string): FsdRoot {
.filter(Boolean)
.map((line, _i, lines) => line.slice(lines[0].search(/\S/)))
.filter(Boolean)
const parsedFolder = parseFolder(lines, joinFromRoot())

return parseFolder(lines, joinFromRoot())
return rootPath ? findSubfolder(parsedFolder, rootPath) : parsedFolder
}

export function compareMessages(a: Diagnostic, b: Diagnostic): number {
Expand Down Expand Up @@ -149,4 +173,81 @@ if (import.meta.vitest) {
],
})
})

test('it should return a nested root folder when the optional rootPath argument is passed', () => {
const markup = `
📂 src
📂 entities
📂 users
📂 ui
📄 index.ts
📂 posts
📂 ui
📄 index.ts
📂 shared
📂 ui
📄 index.ts
📄 Button.tsx
`
const root = parseIntoFsdRoot(markup, joinFromRoot('src', 'entities'))

expect(root).toEqual({
type: 'folder',
path: joinFromRoot('src', 'entities'),
children: [
{
type: 'folder',
path: joinFromRoot('src', 'entities', 'users'),
children: [
{
type: 'folder',
path: joinFromRoot('src', 'entities', 'users', 'ui'),
children: [],
},
{
type: 'file',
path: joinFromRoot('src', 'entities', 'users', 'index.ts'),
},
],
},
{
type: 'folder',
path: joinFromRoot('src', 'entities', 'posts'),
children: [
{
type: 'folder',
path: joinFromRoot('src', 'entities', 'posts', 'ui'),
children: [],
},
{
type: 'file',
path: joinFromRoot('src', 'entities', 'posts', 'index.ts'),
},
],
},
],
})
})

test('it should throw an error when the path (from rootPath argument) is not found in the provided file system mock', () => {
const markup = `
📂 src
📂 entities
📂 users
📂 ui
📄 index.ts
📂 posts
📂 ui
📄 index.ts
📂 shared
📂 ui
📄 index.ts
📄 Button.tsx
`
const nonExistentPath = joinFromRoot('src', 'non-existent-folder')

expect(() => parseIntoFsdRoot(markup, nonExistentPath)).toThrowError(
`Path "${nonExistentPath}" not found in the provided file system mock!`,
)
})
}
Loading