diff --git a/examples/kitchen-sink-of-fsd-issues/reference-output-ubuntu-latest.json b/examples/kitchen-sink-of-fsd-issues/reference-output-ubuntu-latest.json deleted file mode 100644 index 53d8ef16..00000000 --- a/examples/kitchen-sink-of-fsd-issues/reference-output-ubuntu-latest.json +++ /dev/null @@ -1,73 +0,0 @@ -[ - { - "message": "Forbidden import from higher layer \"app\".", - "location": { - "path": "/home/runner/work/steiger/steiger/examples/kitchen-sink-of-fsd-issues/src/entities/user/api/getUser.ts" - }, - "ruleName": "fsd/forbidden-imports", - "severity": "error" - }, - { - "message": "Inconsistent pluralization of slice names. Prefer all plural names", - "fixes": [ - { - "type": "rename", - "path": "/home/runner/work/steiger/steiger/examples/kitchen-sink-of-fsd-issues/src/entities/user", - "newName": "users" - } - ], - "location": { - "path": "/home/runner/work/steiger/steiger/examples/kitchen-sink-of-fsd-issues/src/entities" - }, - "ruleName": "fsd/inconsistent-naming", - "severity": "error" - }, - { - "message": "This slice has no references. Consider removing it.", - "location": { - "path": "/home/runner/work/steiger/steiger/examples/kitchen-sink-of-fsd-issues/src/entities/users" - }, - "ruleName": "fsd/insignificant-slice", - "severity": "error" - }, - { - "message": "This slice has no references. Consider removing it.", - "location": { - "path": "/home/runner/work/steiger/steiger/examples/kitchen-sink-of-fsd-issues/src/entities/user" - }, - "ruleName": "fsd/insignificant-slice", - "severity": "error" - }, - { - "message": "Forbidden sidestep of public API when importing from \"@/app/ui/App\".", - "location": { - "path": "/home/runner/work/steiger/steiger/examples/kitchen-sink-of-fsd-issues/src/entities/user/api/getUser.ts" - }, - "ruleName": "fsd/no-public-api-sidestep", - "severity": "error" - }, - { - "message": "Having a folder with the name \"api\" inside a segment could be confusing because that name is commonly used for segments. Consider renaming it.", - "location": { - "path": "/home/runner/work/steiger/steiger/examples/kitchen-sink-of-fsd-issues/src/entities/user/ui/api" - }, - "ruleName": "fsd/no-reserved-folder-names", - "severity": "error" - }, - { - "message": "Layer \"app\" should not have \"ui\" segment.", - "location": { - "path": "/home/runner/work/steiger/steiger/examples/kitchen-sink-of-fsd-issues/src/app/ui" - }, - "ruleName": "fsd/no-ui-in-app", - "severity": "error" - }, - { - "message": "Layer \"processes\" is deprecated, avoid using it", - "location": { - "path": "/home/runner/work/steiger/steiger/examples/kitchen-sink-of-fsd-issues/src/processes" - }, - "ruleName": "fsd/no-processes", - "severity": "error" - } -] diff --git a/integration-tests/eslint.config.mjs b/integration-tests/eslint.config.mjs new file mode 100644 index 00000000..0cab0553 --- /dev/null +++ b/integration-tests/eslint.config.mjs @@ -0,0 +1 @@ +export { default } from '@steiger/eslint-config' diff --git a/integration-tests/package.json b/integration-tests/package.json new file mode 100644 index 00000000..9bd10227 --- /dev/null +++ b/integration-tests/package.json @@ -0,0 +1,42 @@ +{ + "private": true, + "name": "@steiger/integration-tests", + "description": "Integration tests for Steiger", + "version": "0.1.0", + "scripts": { + "test": "vitest run", + "update-windows-snapshots": "node ./scripts/update-windows-snapshots.mjs" + }, + "exports": { + ".": "./src/index.ts" + }, + "type": "module", + "license": "MIT", + "authors": [ + { + "name": "Anton Medvedev", + "email": "unordinarity@yandex.ru", + "url": "https://github.com/unordinarity" + }, + { + "name": "Lev Chelyadinov", + "email": "leva181777@gmail.com", + "url": "https://github.com/illright" + } + ], + "devDependencies": { + "@steiger/eslint-config": "workspace:*", + "@steiger/tsconfig": "workspace:*", + "@total-typescript/ts-reset": "^0.6.1", + "@types/node": "^18.11.9", + "eslint": "^9.16.0", + "get-bin-path": "^11.0.0", + "prettier": "^3.4.2", + "tinyexec": "^0.3.1", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + }, + "dependencies": { + "steiger": "workspace:*" + } +} diff --git a/integration-tests/reset.d.ts b/integration-tests/reset.d.ts new file mode 100644 index 00000000..e3f32cd1 --- /dev/null +++ b/integration-tests/reset.d.ts @@ -0,0 +1,2 @@ +// Do not add any other lines of code to this file! +import '@total-typescript/ts-reset' diff --git a/integration-tests/scripts/update-windows-snapshots.mjs b/integration-tests/scripts/update-windows-snapshots.mjs new file mode 100644 index 00000000..4b151296 --- /dev/null +++ b/integration-tests/scripts/update-windows-snapshots.mjs @@ -0,0 +1,14 @@ +import * as fs from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const snapshotsFolder = join(dirname(fileURLToPath(import.meta.url)), '../tests/__snapshots__') + +// Go through each POSIX snapshot and replace the forward slashes in diagnostic paths with backslashes +for (const file of await fs.readdir(snapshotsFolder)) { + if (file.endsWith('-posix.txt')) { + const windowsSnapshotName = file.slice(0, -'-posix.txt'.length) + '-windows.txt' + const updatedSnapshot = (await fs.readFile(join(snapshotsFolder, file), 'utf8')).replace(/(?<=┌.+)\//gm, '\\') + await fs.writeFile(join(snapshotsFolder, windowsSnapshotName), updatedSnapshot) + } +} diff --git a/integration-tests/tests/__snapshots__/smoke-stderr-posix.txt b/integration-tests/tests/__snapshots__/smoke-stderr-posix.txt new file mode 100644 index 00000000..01ebdfbc --- /dev/null +++ b/integration-tests/tests/__snapshots__/smoke-stderr-posix.txt @@ -0,0 +1,45 @@ + +┌ src/entities/user/api/getUser.ts +✘ Forbidden import from higher layer "app". +│ +└ fsd/forbidden-imports (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/forbidden-imports​) + +┌ src/entities +✘ Inconsistent pluralization of slice names. Prefer all plural names +✔ Auto-fixable +│ +└ fsd/inconsistent-naming (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/inconsistent-naming​) + +┌ src/entities/user +✘ This slice has no references. Consider removing it. +│ +└ fsd/insignificant-slice (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/insignificant-slice​) + +┌ src/entities/users +✘ This slice has no references. Consider removing it. +│ +└ fsd/insignificant-slice (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/insignificant-slice​) + +┌ src/entities/user/api/getUser.ts +✘ Forbidden sidestep of public API when importing from "@/app/ui/App". +│ +└ fsd/no-public-api-sidestep (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/no-public-api-sidestep​) + +┌ src/entities/user/ui/api +✘ Having a folder with the name "api" inside a segment could be confusing because that name is commonly used for segments. Consider renaming it. +│ +└ fsd/no-reserved-folder-names (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/no-reserved-folder-names​) + +┌ src/app/ui +✘ Layer "app" should not have "ui" segment. +│ +└ fsd/no-ui-in-app (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/no-ui-in-app​) + +┌ src/processes +✘ Layer "processes" is deprecated, avoid using it +│ +└ fsd/no-processes (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/no-processes​) + +──────────────────────────────────────────────────────── + Found 8 errors (1 can be fixed automatically with --fix) + diff --git a/integration-tests/tests/__snapshots__/smoke-stderr-windows.txt b/integration-tests/tests/__snapshots__/smoke-stderr-windows.txt new file mode 100644 index 00000000..3f123d62 --- /dev/null +++ b/integration-tests/tests/__snapshots__/smoke-stderr-windows.txt @@ -0,0 +1,45 @@ + +┌ src\entities\user\api\getUser.ts +✘ Forbidden import from higher layer "app". +│ +└ fsd/forbidden-imports (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/forbidden-imports​) + +┌ src\entities +✘ Inconsistent pluralization of slice names. Prefer all plural names +✔ Auto-fixable +│ +└ fsd/inconsistent-naming (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/inconsistent-naming​) + +┌ src\entities\user +✘ This slice has no references. Consider removing it. +│ +└ fsd/insignificant-slice (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/insignificant-slice​) + +┌ src\entities\users +✘ This slice has no references. Consider removing it. +│ +└ fsd/insignificant-slice (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/insignificant-slice​) + +┌ src\entities\user\api\getUser.ts +✘ Forbidden sidestep of public API when importing from "@/app/ui/App". +│ +└ fsd/no-public-api-sidestep (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/no-public-api-sidestep​) + +┌ src\entities\user\ui\api +✘ Having a folder with the name "api" inside a segment could be confusing because that name is commonly used for segments. Consider renaming it. +│ +└ fsd/no-reserved-folder-names (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/no-reserved-folder-names​) + +┌ src\app\ui +✘ Layer "app" should not have "ui" segment. +│ +└ fsd/no-ui-in-app (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/no-ui-in-app​) + +┌ src\processes +✘ Layer "processes" is deprecated, avoid using it +│ +└ fsd/no-processes (​https://github.com/feature-sliced/steiger/tree/master/packages/steiger-plugin-fsd/src/no-processes​) + +──────────────────────────────────────────────────────── + Found 8 errors (1 can be fixed automatically with --fix) + diff --git a/integration-tests/tests/smoke.test.ts b/integration-tests/tests/smoke.test.ts new file mode 100644 index 00000000..f499a304 --- /dev/null +++ b/integration-tests/tests/smoke.test.ts @@ -0,0 +1,24 @@ +import * as fs from 'node:fs/promises' +import os from 'node:os' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { exec } from 'tinyexec' + +import { expect, test } from 'vitest' + +import { getSteigerBinPath } from '../utils/get-bin-path.js' + +const temporaryDirectory = await fs.realpath(os.tmpdir()) +const steiger = await getSteigerBinPath() +const kitchenSinkExample = join(dirname(fileURLToPath(import.meta.url)), '../../examples/kitchen-sink-of-fsd-issues') +const pathPlatform = os.platform() === 'win32' ? 'windows' : 'posix' + +test('basic functionality in the kitchen sink example project', async () => { + const project = join(temporaryDirectory, 'smoke') + await fs.rm(project, { recursive: true, force: true }) + await fs.cp(kitchenSinkExample, project, { recursive: true }) + + const { stderr } = await exec('node', [steiger, 'src'], { nodeOptions: { cwd: project } }) + + await expect(stderr).toMatchFileSnapshot(join('__snapshots__', `smoke-stderr-${pathPlatform}.txt`)) +}) diff --git a/integration-tests/tsconfig.json b/integration-tests/tsconfig.json new file mode 100644 index 00000000..e2b70993 --- /dev/null +++ b/integration-tests/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@steiger/tsconfig/base.json", + "include": ["./tests", "./reset.d.ts", "utils/get-bin-path.ts"], + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + } +} diff --git a/integration-tests/utils/get-bin-path.ts b/integration-tests/utils/get-bin-path.ts new file mode 100644 index 00000000..871b8894 --- /dev/null +++ b/integration-tests/utils/get-bin-path.ts @@ -0,0 +1,22 @@ +import { promises as fs } from 'node:fs' +import * as process from 'node:process' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { getBinPath } from 'get-bin-path' + +/** + * Resolve the full path to the built JS file of the CLI. + * + * Rejects if the file doesn't exist. + */ +export async function getSteigerBinPath() { + const steiger = (await getBinPath({ cwd: join(dirname(fileURLToPath(import.meta.url)), '../../packages/steiger') }))! + try { + await fs.stat(steiger) + } catch { + console.error('Run `npm run build` before running integration tests') + process.exit(1) + } + + return steiger +} diff --git a/packages/steiger/src/app.ts b/packages/steiger/src/app.ts index c44a8246..05687d34 100644 --- a/packages/steiger/src/app.ts +++ b/packages/steiger/src/app.ts @@ -21,12 +21,7 @@ async function runRules({ vfs, rules }: { vfs: Folder; rules: Array }) { const vfsWithoutGlobalIgnores = removeGlobalIgnoreFromVfs(vfs, getGlobalIgnores()) const ruleResults = await Promise.all(rules.map((rule) => runRule(vfsWithoutGlobalIgnores, rule))) - return ruleResults.flatMap((r, ruleResultsIndex) => { - const { diagnostics } = r - if (diagnostics.length === 0) { - return [] - } - + return ruleResults.flatMap(({ diagnostics }, ruleResultsIndex) => { const ruleName = rules[ruleResultsIndex].name const severities = calculateFinalSeverities( vfsWithoutGlobalIgnores, @@ -34,12 +29,14 @@ async function runRules({ vfs, rules }: { vfs: Folder; rules: Array }) { diagnostics.map((d) => d.location.path), ) - return diagnostics.map((d, index) => ({ - ...d, - ruleName, - getRuleDescriptionUrl, - severity: severities[index], - })) + return diagnostics + .sort((a, b) => a.location.path.localeCompare(b.location.path)) + .map((d, index) => ({ + ...d, + ruleName, + getRuleDescriptionUrl, + severity: severities[index], + })) }) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06f679df..585ba0df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,43 @@ importers: specifier: ^2.3.3 version: 2.3.3 + integration-tests: + dependencies: + steiger: + specifier: workspace:* + version: link:../packages/steiger + devDependencies: + '@steiger/eslint-config': + specifier: workspace:* + version: link:../tooling/eslint-config + '@steiger/tsconfig': + specifier: workspace:* + version: link:../tooling/tsconfig + '@total-typescript/ts-reset': + specifier: ^0.6.1 + version: 0.6.1 + '@types/node': + specifier: ^18.11.9 + version: 18.19.67 + eslint: + specifier: ^9.16.0 + version: 9.16.0 + get-bin-path: + specifier: ^11.0.0 + version: 11.0.0 + prettier: + specifier: ^3.4.2 + version: 3.4.2 + tinyexec: + specifier: ^0.3.1 + version: 0.3.1 + typescript: + specifier: ^5.7.2 + version: 5.7.2 + vitest: + specifier: ^2.1.8 + version: 2.1.8(@types/node@18.19.67) + packages/pretty-reporter: dependencies: chalk: @@ -1805,6 +1842,10 @@ packages: resolution: {integrity: sha512-hFM7oivtlgJ3d6XWD6G47l8Wyh/C6vFw5G24Kk1Tbq85yh5gcM8Fne5/lFhiuxB+RT6+SI7I1ThB9lG4FBh3jw==} engines: {node: '>=18'} + get-bin-path@11.0.0: + resolution: {integrity: sha512-hvX/hynZJ6sxeItamADFBZo0WmLoG/qZBlRCLRv+J+oN8USw1ZxefIGJSFPu1GGd3OHWVFJKvCCN970wmpiHzg==} + engines: {node: '>=18.18.0'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -3742,7 +3783,7 @@ snapshots: debug: 4.4.0 globby: 11.1.0 is-glob: 4.0.3 - minimatch: 9.0.4 + minimatch: 9.0.5 semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.7.2) optionalDependencies: @@ -3792,6 +3833,14 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 + '@vitest/mocker@2.1.8(vite@5.4.11(@types/node@18.19.67))': + dependencies: + '@vitest/spy': 2.1.8 + estree-walker: 3.0.3 + magic-string: 0.30.15 + optionalDependencies: + vite: 5.4.11(@types/node@18.19.67) + '@vitest/mocker@2.1.8(vite@5.4.11(@types/node@22.10.1))': dependencies: '@vitest/spy': 2.1.8 @@ -4433,6 +4482,10 @@ snapshots: ast-module-types: 6.0.0 node-source-walk: 7.0.0 + get-bin-path@11.0.0: + dependencies: + escalade: 3.1.2 + get-caller-file@2.0.5: {} get-east-asian-width@1.3.0: {} @@ -5362,6 +5415,24 @@ snapshots: validate-npm-package-name@5.0.1: {} + vite-node@2.1.8(@types/node@18.19.67): + dependencies: + cac: 6.7.14 + debug: 4.4.0 + es-module-lexer: 1.5.4 + pathe: 1.1.2 + vite: 5.4.11(@types/node@18.19.67) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@2.1.8(@types/node@22.10.1): dependencies: cac: 6.7.14 @@ -5380,6 +5451,15 @@ snapshots: - supports-color - terser + vite@5.4.11(@types/node@18.19.67): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.49 + rollup: 4.28.1 + optionalDependencies: + '@types/node': 18.19.67 + fsevents: 2.3.3 + vite@5.4.11(@types/node@22.10.1): dependencies: esbuild: 0.21.5 @@ -5389,6 +5469,41 @@ snapshots: '@types/node': 22.10.1 fsevents: 2.3.3 + vitest@2.1.8(@types/node@18.19.67): + dependencies: + '@vitest/expect': 2.1.8 + '@vitest/mocker': 2.1.8(vite@5.4.11(@types/node@18.19.67)) + '@vitest/pretty-format': 2.1.8 + '@vitest/runner': 2.1.8 + '@vitest/snapshot': 2.1.8 + '@vitest/spy': 2.1.8 + '@vitest/utils': 2.1.8 + chai: 5.1.2 + debug: 4.4.0 + expect-type: 1.1.0 + magic-string: 0.30.15 + pathe: 1.1.2 + std-env: 3.8.0 + tinybench: 2.9.0 + tinyexec: 0.3.1 + tinypool: 1.0.2 + tinyrainbow: 1.2.0 + vite: 5.4.11(@types/node@18.19.67) + vite-node: 2.1.8(@types/node@18.19.67) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 18.19.67 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@2.1.8(@types/node@22.10.1): dependencies: '@vitest/expect': 2.1.8 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7104d705..ca6439c9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - 'packages/*' - 'tooling/*' + - 'integration-tests'