diff --git a/packages/steiger-plugin-fsd/src/_lib/prepare-test.ts b/packages/steiger-plugin-fsd/src/_lib/prepare-test.ts index f7a5ea2..7daf1b4 100644 --- a/packages/steiger-plugin-fsd/src/_lib/prepare-test.ts +++ b/packages/steiger-plugin-fsd/src/_lib/prepare-test.ts @@ -1,6 +1,8 @@ -import { join } from 'node:path' +import { join, sep } from 'node:path' +import type { readFileSync, existsSync } from 'node:fs' import type { Folder, File, FsdRoot } from '@feature-sliced/filesystem' import type { Diagnostic } from '../types.js' +import { vi } from 'vitest' /** Parse a multi-line indented string with emojis for files and folders into an FSD root. */ export function parseIntoFsdRoot(fsMarkup: string): FsdRoot { @@ -42,6 +44,27 @@ export function joinFromRoot(...segments: Array) { return join('/', ...segments) } +export function createFsMocks(mockedFiles: Record, original: typeof import('fs')) { + const normalizedMockedFiles = Object.fromEntries( + Object.entries(mockedFiles).map(([path, content]) => [path.replace(/\//g, sep), content]), + ) + + return { + ...original, + readFileSync: vi.fn(((path, options) => { + if (typeof path === 'string' && path in normalizedMockedFiles) { + return normalizedMockedFiles[path as keyof typeof normalizedMockedFiles] + } else { + return original.readFileSync(path, options) + } + }) as typeof readFileSync), + existsSync: vi.fn(((path) => + Object.keys(normalizedMockedFiles).some( + (key) => key === path || key.startsWith(path + sep), + )) as typeof existsSync), + } +} + if (import.meta.vitest) { const { test, expect } = import.meta.vitest diff --git a/packages/steiger-plugin-fsd/src/forbidden-imports/index.spec.ts b/packages/steiger-plugin-fsd/src/forbidden-imports/index.spec.ts index 9a336d0..01264e9 100644 --- a/packages/steiger-plugin-fsd/src/forbidden-imports/index.spec.ts +++ b/packages/steiger-plugin-fsd/src/forbidden-imports/index.spec.ts @@ -1,6 +1,4 @@ import { expect, it, vi } from 'vitest' -import type { readFileSync } from 'node:fs' -import { sep } from 'node:path' import { parseIntoFsdRoot } from '../_lib/prepare-test.js' import forbiddenImports from './index.js' @@ -13,36 +11,28 @@ vi.mock('tsconfck', async (importOriginal) => { }) vi.mock('node:fs', async (importOriginal) => { - const original = await importOriginal() + const originalFs = await importOriginal() + const { createFsMocks } = await import('../_lib/prepare-test.js') - const mockedFiles = { - '/shared/ui/styles.ts': '', - '/shared/ui/Button.tsx': 'import styles from "./styles";', - '/shared/ui/TextField.tsx': 'import styles from "./styles";', - '/shared/ui/index.ts': '', - '/entities/user/ui/UserAvatar.tsx': 'import { Button } from "@/shared/ui"', - '/entities/user/index.ts': '', - '/entities/product/ui/ProductCard.tsx': 'import { UserAvatar } from "@/entities/user"', - '/entities/product/index.ts': '', - '/features/comments/ui/CommentCard.tsx': 'import { styles } from "@/pages/editor"', - '/features/comments/index.ts': '', - '/pages/editor/ui/styles.ts': '', - '/pages/editor/ui/EditorPage.tsx': 'import { Button } from "@/shared/ui"; import { Editor } from "./Editor"', - '/pages/editor/ui/Editor.tsx': 'import { TextField } from "@/shared/ui"', - '/pages/editor/index.ts': '', - } - - return { - ...original, - readFileSync: vi.fn(((path, options) => { - if (typeof path === 'string' && path in mockedFiles) { - return mockedFiles[path as keyof typeof mockedFiles] - } else { - return original.readFileSync(path, options) - } - }) as typeof readFileSync), - existsSync: vi.fn((path) => Object.keys(mockedFiles).some((key) => key === path || key.startsWith(path + sep))), - } + return createFsMocks( + { + '/shared/ui/styles.ts': '', + '/shared/ui/Button.tsx': 'import styles from "./styles";', + '/shared/ui/TextField.tsx': 'import styles from "./styles";', + '/shared/ui/index.ts': '', + '/entities/user/ui/UserAvatar.tsx': 'import { Button } from "@/shared/ui"', + '/entities/user/index.ts': '', + '/entities/product/ui/ProductCard.tsx': 'import { UserAvatar } from "@/entities/user"', + '/entities/product/index.ts': '', + '/features/comments/ui/CommentCard.tsx': 'import { styles } from "@/pages/editor"', + '/features/comments/index.ts': '', + '/pages/editor/ui/styles.ts': '', + '/pages/editor/ui/EditorPage.tsx': 'import { Button } from "@/shared/ui"; import { Editor } from "./Editor"', + '/pages/editor/ui/Editor.tsx': 'import { TextField } from "@/shared/ui"', + '/pages/editor/index.ts': '', + }, + originalFs, + ) }) it('reports no errors on a project with only correct imports', async () => { diff --git a/packages/steiger-plugin-fsd/src/insignificant-slice/index.spec.ts b/packages/steiger-plugin-fsd/src/insignificant-slice/index.spec.ts index 9888ac5..d00a8e1 100644 --- a/packages/steiger-plugin-fsd/src/insignificant-slice/index.spec.ts +++ b/packages/steiger-plugin-fsd/src/insignificant-slice/index.spec.ts @@ -1,6 +1,4 @@ import { expect, it, vi } from 'vitest' -import type { readFileSync } from 'node:fs' -import { sep } from 'node:path' import { compareMessages, parseIntoFsdRoot } from '../_lib/prepare-test.js' import insignificantSlice from './index.js' @@ -13,40 +11,32 @@ vi.mock('tsconfck', async (importOriginal) => { }) vi.mock('node:fs', async (importOriginal) => { - const original = await importOriginal() + const originalFs = await importOriginal() + const { createFsMocks } = await import('../_lib/prepare-test.js') - const mockedFiles = { - '/shared/ui/styles.ts': '', - '/shared/ui/Button.tsx': 'import styles from "./styles";', - '/shared/ui/TextField.tsx': 'import styles from "./styles";', - '/shared/ui/index.ts': '', - '/entities/user/ui/UserAvatar.tsx': 'import { Button } from "@/shared/ui"', - '/entities/user/index.ts': '', - '/entities/product/ui/ProductCard.tsx': '', - '/entities/product/index.ts': '', - '/features/comments/ui/CommentCard.tsx': '', - '/features/comments/index.ts': '', - '/pages/editor/ui/EditorPage.tsx': - 'import { Button } from "@/shared/ui"; import { Editor } from "./Editor"; import { CommentCard } from "@/features/comments"; import { UserAvatar } from "@/entities/user"', - '/pages/editor/ui/Editor.tsx': - 'import { TextField } from "@/shared/ui"; import { UserAvatar } from "@/entities/user"', - '/pages/editor/index.ts': '', - '/pages/settings/ui/SettingsPage.tsx': - 'import { Button } from "@/shared/ui"; import { CommentCard } from "@/features/comments"', - '/pages/settings/index.ts': '', - } - - return { - ...original, - readFileSync: vi.fn(((path, options) => { - if (typeof path === 'string' && path in mockedFiles) { - return mockedFiles[path as keyof typeof mockedFiles] - } else { - return original.readFileSync(path, options) - } - }) as typeof readFileSync), - existsSync: vi.fn((path) => Object.keys(mockedFiles).some((key) => key === path || key.startsWith(path + sep))), - } + return createFsMocks( + { + '/shared/ui/styles.ts': '', + '/shared/ui/Button.tsx': 'import styles from "./styles";', + '/shared/ui/TextField.tsx': 'import styles from "./styles";', + '/shared/ui/index.ts': '', + '/entities/user/ui/UserAvatar.tsx': 'import { Button } from "@/shared/ui"', + '/entities/user/index.ts': '', + '/entities/product/ui/ProductCard.tsx': '', + '/entities/product/index.ts': '', + '/features/comments/ui/CommentCard.tsx': '', + '/features/comments/index.ts': '', + '/pages/editor/ui/EditorPage.tsx': + 'import { Button } from "@/shared/ui"; import { Editor } from "./Editor"; import { CommentCard } from "@/features/comments"; import { UserAvatar } from "@/entities/user"', + '/pages/editor/ui/Editor.tsx': + 'import { TextField } from "@/shared/ui"; import { UserAvatar } from "@/entities/user"', + '/pages/editor/index.ts': '', + '/pages/settings/ui/SettingsPage.tsx': + 'import { Button } from "@/shared/ui"; import { CommentCard } from "@/features/comments"', + '/pages/settings/index.ts': '', + }, + originalFs, + ) }) it('reports no errors on a project with slices only on the Pages layer', async () => { diff --git a/packages/steiger-plugin-fsd/src/no-public-api-sidestep/index.spec.ts b/packages/steiger-plugin-fsd/src/no-public-api-sidestep/index.spec.ts index 8ef1f88..e18dfd6 100644 --- a/packages/steiger-plugin-fsd/src/no-public-api-sidestep/index.spec.ts +++ b/packages/steiger-plugin-fsd/src/no-public-api-sidestep/index.spec.ts @@ -1,6 +1,4 @@ import { expect, it, vi } from 'vitest' -import type { readFileSync } from 'node:fs' -import { sep } from 'node:path' import { parseIntoFsdRoot } from '../_lib/prepare-test.js' import noPublicApiSidestep from './index.js' @@ -13,38 +11,30 @@ vi.mock('tsconfck', async (importOriginal) => { }) vi.mock('node:fs', async (importOriginal) => { - const original = await importOriginal() + const originalFs = await importOriginal() + const { createFsMocks } = await import('../_lib/prepare-test.js') - const mockedFiles = { - '/shared/ui/styles.ts': '', - '/shared/ui/Button.tsx': 'import styles from "./styles";', - '/shared/ui/TextField.tsx': 'import styles from "./styles";', - '/shared/ui/index.ts': '', - '/entities/user/ui/UserAvatar.tsx': 'import { Button } from "@/shared/ui"', - '/entities/user/index.ts': '', - '/entities/product/ui/ProductCard.tsx': 'import { UserAvatar } from "@/entities/user"', - '/entities/product/index.ts': '', - '/features/comments/ui/CommentCard.tsx': 'import { styles } from "@/pages/editor"', - '/features/comments/index.ts': '', - '/pages/editor/ui/styles.ts': '', - '/pages/editor/ui/EditorPage.tsx': 'import { Button } from "@/shared/ui"; import { Editor } from "./Editor"', - '/pages/editor/ui/Editor.tsx': - 'import { TextField } from "@/shared/ui"; import { ProductCard } from "@/entities/product/ui/ProductCard.tsx"', - '/pages/editor/ui/SubmitButton.tsx': 'import { Button } from "@/shared/ui/Button"', - '/pages/editor/index.ts': '', - } - - return { - ...original, - readFileSync: vi.fn(((path, options) => { - if (typeof path === 'string' && path in mockedFiles) { - return mockedFiles[path as keyof typeof mockedFiles] - } else { - return original.readFileSync(path, options) - } - }) as typeof readFileSync), - existsSync: vi.fn((path) => Object.keys(mockedFiles).some((key) => key === path || key.startsWith(path + sep))), - } + return createFsMocks( + { + '/shared/ui/styles.ts': '', + '/shared/ui/Button.tsx': 'import styles from "./styles";', + '/shared/ui/TextField.tsx': 'import styles from "./styles";', + '/shared/ui/index.ts': '', + '/entities/user/ui/UserAvatar.tsx': 'import { Button } from "@/shared/ui"', + '/entities/user/index.ts': '', + '/entities/product/ui/ProductCard.tsx': 'import { UserAvatar } from "@/entities/user"', + '/entities/product/index.ts': '', + '/features/comments/ui/CommentCard.tsx': 'import { styles } from "@/pages/editor"', + '/features/comments/index.ts': '', + '/pages/editor/ui/styles.ts': '', + '/pages/editor/ui/EditorPage.tsx': 'import { Button } from "@/shared/ui"; import { Editor } from "./Editor"', + '/pages/editor/ui/Editor.tsx': + 'import { TextField } from "@/shared/ui"; import { ProductCard } from "@/entities/product/ui/ProductCard.tsx"', + '/pages/editor/ui/SubmitButton.tsx': 'import { Button } from "@/shared/ui/Button"', + '/pages/editor/index.ts': '', + }, + originalFs, + ) }) it('reports no errors on a project without public API sidesteps', async () => {