Files
homelab-upgrade/tools/layer-rules.test.ts
homelab-updaterandClaude Sonnet 5 e4734e8c94 build: TypeScript, ESLint, Vitest, Vite scaffold with layer rule
typescript ~6.0.3 (not 7.x): typescript-eslint 8.70 peers typescript <6.1.0, so 7.x would break lint.
eslint + typescript-eslint: strict type-checked lint; @eslint/js: base rules.
vitest: tests next to code; vite + @preact/preset-vite: frontend bundle; preact: UI.
fastify + @fastify/static: HTTP API/SSE and serving the built UI; zod: validation at boundaries.
@types/node: Node 24 typings. SQLite uses the built-in node:sqlite, so no native dependency.
Layer rule domain -> app -> infra -> http/web lives in tools/layer-rules.ts and is tested.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 23:37:11 +02:00

49 lines
1.9 KiB
TypeScript

import { ESLint } from 'eslint';
import { describe, expect, it } from 'vitest';
import { layerRules } from './layer-rules.ts';
const eslint = new ESLint({ overrideConfigFile: true, overrideConfig: layerRules });
async function violations(filePath: string, importPath: string): Promise<string[]> {
const [result] = await eslint.lintText(`import x from '${importPath}';\nexport { x };\n`, { filePath });
return (result?.messages ?? []).map((message) => message.message);
}
describe('layer rule (domain -> app -> infra -> http/web)', () => {
it.each([
['src/domain/a.ts', '../app/driver.ts'],
['src/domain/a.ts', '../infra/sqlite/open.ts'],
['src/domain/a.ts', '../http/server.ts'],
['src/domain/a.ts', '../web/main.tsx'],
['src/domain/a.ts', 'node:fs'],
['src/app/a.ts', '../infra/sqlite/open.ts'],
['src/app/a.ts', '../http/server.ts'],
['src/app/a.ts', '../web/main.tsx'],
['src/infra/a.ts', '../http/server.ts'],
['src/infra/a.ts', '../web/main.tsx'],
['src/http/a.ts', '../web/main.tsx'],
['src/web/a.tsx', '../app/driver.ts'],
['src/web/a.tsx', '../infra/sqlite/open.ts'],
['src/web/a.tsx', '../http/server.ts'],
['src/web/a.tsx', 'node:fs'],
])('%s must not import %s', async (file, target) => {
expect(await violations(file, target)).toHaveLength(1);
});
it.each([
['src/app/a.ts', '../domain/steps.ts'],
['src/infra/a.ts', '../domain/steps.ts'],
['src/infra/a.ts', '../app/driver.ts'],
['src/infra/a.ts', 'node:sqlite'],
['src/http/a.ts', '../app/driver.ts'],
['src/http/a.ts', '../infra/sqlite/open.ts'],
['src/web/a.tsx', '../domain/steps.ts'],
['src/web/a.tsx', '../http/contract.ts'],
['src/main/a.ts', '../infra/sqlite/open.ts'],
['src/main/a.ts', '../http/server.ts'],
['src/app/a.test.ts', '../infra/sqlite/open.ts'],
])('%s may import %s', async (file, target) => {
expect(await violations(file, target)).toEqual([]);
});
});