refactor: enforce frontend view model boundaries
Frontend verify / commits (pull_request) Successful in 28s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 8m19s

This commit is contained in:
2026-09-28 18:48:23 +03:00
parent 5b69003516
commit ca0ad4f824
287 changed files with 8558 additions and 4538 deletions
+116 -9
View File
@@ -16,11 +16,13 @@ import storybook from "eslint-plugin-storybook";
import testingLibrary from "eslint-plugin-testing-library";
import unicorn from "eslint-plugin-unicorn";
import assert from "node:assert/strict";
import { globSync } from "node:fs";
import { test } from "node:test";
import tseslint from "typescript-eslint";
test("ESLint enforces the frontend profile and excludes generated artifacts", async () => {
const eslint = new ESLint();
for (const [filePath, code, rule] of [
[
"src/shared/api/problem-details.ts",
@@ -110,6 +112,7 @@ test("ESLint enforces the frontend profile and excludes generated artifacts", as
rule,
);
}
for (const file of [
"src/shared/api/generated/schema.d.ts",
"packages/ui/dist/index.js",
@@ -117,9 +120,11 @@ test("ESLint enforces the frontend profile and excludes generated artifacts", as
"coverage/index.js",
])
assert.equal(await eslint.isPathIgnored(file), true, file);
const story = await eslint.calculateConfigForFile(
"src/shared/ui/Button.stories.tsx",
);
assert.equal(story.rules["import-x/no-default-export"][0], 0);
assert.ok(story.rules["storybook/default-exports"][0] > 0);
assert.equal(story.rules["react-refresh/only-export-components"], undefined);
@@ -130,6 +135,7 @@ test("all recommended plugin rules remain enabled in their intended scopes", asy
const source = "src/shared/api/problem-details.ts";
const component = "packages/ui/src/EntryCard/EntryCard.tsx";
const unit = "packages/ui/src/EntryCard/EntryCard.test.tsx";
for (const [preset, filePath] of [
[tseslint.configs.recommendedTypeChecked, source],
[[importX.flatConfigs.recommended, importX.flatConfigs.typescript], source],
@@ -147,12 +153,25 @@ test("all recommended plugin rules remain enabled in their intended scopes", asy
[vitest.configs.recommended, unit],
[testingLibrary.configs["flat/react"], unit],
[jestDom.configs["flat/recommended"], unit],
...storybook.configs["flat/recommended"].filter(config => config.rules).map(config => [config, config.name.endsWith("main-rules") ? ".storybook/main.ts" : "src/shared/ui/Button.stories.tsx"]),
...storybook.configs["flat/recommended"]
.filter(config => config.rules)
.map(config => [
config,
config.name.endsWith("main-rules")
? ".storybook/main.ts"
: "src/shared/ui/Button.stories.tsx",
]),
]) {
const configs = [preset].flat();
const expected = Object.assign({}, ...configs.map(config => config.rules));
const expected = Object.assign(
{},
...configs.map(config => config.rules),
);
const config = await eslint.calculateConfigForFile(filePath);
const actual = new Map(Object.entries(config.rules));
for (const [rule, setting] of Object.entries(expected)) {
const severity = Array.isArray(setting) ? setting[0] : setting;
if (severity === "off" || severity === 0 || !rule.includes("/")) continue;
@@ -164,10 +183,12 @@ test("all recommended plugin rules remain enabled in their intended scopes", asy
test("ESLint formatting is idempotent and preserves TSX generic syntax", async () => {
const eslint = new ESLint({ fix: true });
const filePath = "packages/ui/src/EntryCard/EntryCard.tsx";
const [fixed] = await eslint.lintText(
"//identity\nconst identity = <T,>(value: T): T => value;\nexport {identity};",
{ filePath },
);
assert.equal(fixed.fatalErrorCount, 0);
assert.equal(fixed.errorCount, 0);
assert.ok(fixed.output);
@@ -178,10 +199,12 @@ test("ESLint formatting is idempotent and preserves TSX generic syntax", async (
test("ESLint formatting preserves spaces around JSX text and interpolations", async () => {
const eslint = new ESLint({ fix: true });
const [fixed] = await eslint.lintText(
"const Example = () => {\nconst value = 'value';\nreturn <span>Amount: {value} / {value}</span>;\n};\nexport { Example };",
{ filePath: "packages/ui/src/EntryCard/EntryCard.tsx" },
);
assert.equal(fixed.errorCount, 0);
assert.ok(fixed.output?.includes("Amount: {value} / {value}"));
});
@@ -190,11 +213,25 @@ test("ESLint formatting preserves spaces around JSX text and interpolations", as
test("machine codes require enum members without banning ordinary text or Prisma selects", async () => {
const eslint = new ESLint();
const filePath = "src/shared/api/problem-details.ts";
for (const code of ["export function check(code: string) { return code === \"one-off-code\"; }", "export const result = `MOEX_UNAVAILABLE`;", "export const result = { code: \"one-off-code\" };", "export const result = { \"errorCode\": \"one-off-code\" };", "export const result = { code: `dynamic-${Date.now()}` };", "export const result = \"MOEX_UNAVAILABLE\";", "export const result = [\"INSTRUMENT_ID_TYPE_FIGI\"];", "export const result = { OPERATION_TYPE_COUPON: \"income\" };"]) {
for (const code of [
"export function check(code: string) { return code === \"one-off-code\"; }",
"export const result = `MOEX_UNAVAILABLE`;",
"export const result = { code: \"one-off-code\" };",
"export const result = { \"errorCode\": \"one-off-code\" };",
"export const result = { code: `dynamic-${Date.now()}` };",
"export const result = \"MOEX_UNAVAILABLE\";",
"export const result = [\"INSTRUMENT_ID_TYPE_FIGI\"];",
"export const result = { OPERATION_TYPE_COUPON: \"income\" };",
]) {
const [result] = await eslint.lintText(code, { filePath });
assert.equal(result.fatalErrorCount, 0, code);
assert.ok(result.messages.some(({ ruleId }) => ruleId === "no-restricted-syntax"), code);
assert.ok(
result.messages.some(({ ruleId }) => ruleId === "no-restricted-syntax"),
code,
);
}
for (const code of [
"enum Code { Unavailable = \"MOEX_UNAVAILABLE\" } export const result = { code: Code.Unavailable };",
"export const select = { code: true, errorCode: true };",
@@ -202,10 +239,80 @@ test("machine codes require enum members without banning ordinary text or Prisma
]) {
const [result] = await eslint.lintText(code, { filePath });
assert.equal(result.fatalErrorCount, 0, code);
assert.ok(result.messages.every(({ ruleId }) => ruleId !== "no-restricted-syntax"), code);
assert.ok(
result.messages.every(({ ruleId }) => ruleId !== "no-restricted-syntax"),
code,
);
}
const [fixture] = await eslint.lintText("export const result = { code: \"MOEX_UNAVAILABLE\" };", { filePath: "src/shared/api/client.test.ts" });
assert.ok(fixture.messages.every(({ ruleId }) => ruleId !== "no-restricted-syntax"));
const [comparison] = await eslint.lintText("enum Code { Ready = \"ready\" } export function check(code: Code) { return code === \"ready\"; }", { filePath });
assert.ok(comparison.messages.some(({ ruleId }) => ruleId === "@typescript-eslint/no-unsafe-enum-comparison"));
const [fixture] = await eslint.lintText(
"export const result = { code: \"MOEX_UNAVAILABLE\" };",
{ filePath: "src/shared/api/client.test.ts" },
);
assert.ok(
fixture.messages.every(({ ruleId }) => ruleId !== "no-restricted-syntax"),
);
const [comparison] = await eslint.lintText(
"enum Code { Ready = \"ready\" } export function check(code: Code) { return code === \"ready\"; }",
{ filePath },
);
assert.ok(
comparison.messages.some(
({ ruleId }) => ruleId === "@typescript-eslint/no-unsafe-enum-comparison",
),
);
});
test("readability enforces syntax without banning VM and schema patterns", async () => {
const eslint = new ESLint();
const component = "src/entities/bank-card/ui/BankCardForm/BankCardForm.tsx";
const model = "src/entities/income/api/useIncome.ts";
const invalid = [
[component, "const Example = () => <svg><path d=\"M0 0\" /></svg>;", "frontend/readability"],
[component, "const Example = () => <button onClick={() => alert(\"go\")} />;", "no-restricted-syntax"],
[component, "const Example = () => <button title={true ? \"yes\" : \"no\"} />;", "no-restricted-syntax"],
[component, "const Example = () => { const helper = () => 1; return <span>{helper()}</span>; };", "frontend/readability"],
[component, "import { z } from \"zod\"; const schema = z.string();", "frontend/readability"],
[component, "const schema = otherSchema.superRefine(() => {});", "frontend/readability"],
[model, "function useExample(accountId: string | undefined) { return accountId; }", "frontend/readability"],
[model, "type Example = { state: \"one\" } | { state: \"two\" };", "frontend/readability"],
[component, "const Example = () => <div>\n<span />\n<span />\n</div>; export { Example };", "frontend/jsx-spacing"],
[model, `const excessivelyLongDeclarationName = ${"1234567890".repeat(9)};`, "@stylistic/max-len"],
];
for (const [filePath, code, rule] of invalid) {
const [result] = await eslint.lintText(code, { filePath });
assert.equal(result.fatalErrorCount, 0, code);
assert.ok(result.messages.some(message => message.ruleId === rule), `${rule}: ${code}`);
}
for (const [filePath, code] of [
["src/shared/lib/calendar.schema.ts", "import { z } from \"zod\"; const calendar = z.iso.date();"],
["packages/icons/src/svg/AddIcon.tsx", "const Icon = () => <svg><path d=\"M0 0\" /></svg>;"],
[model, "function useExample(accountId?: string) { const onClick = () => accountId; return { onClick }; }"],
[model, "function example(value: string | undefined, required: string) { return value ?? required; }"],
[model, "type Example =\n | { state: \"one\" }\n | { state: \"two\" };"],
[component, "const Example = (props: { onClick: () => void }) => <button onClick={props.onClick} />;"],
]) {
const [result] = await eslint.lintText(code, { filePath });
assert.equal(result.fatalErrorCount, 0, code);
assert.ok(result.messages.every(message => !["frontend/readability", "no-restricted-syntax"].includes(message.ruleId)), code);
}
assert.deepEqual(globSync("src/**/*.svg"), []);
});
test("JSX spacing fixes preserve text interpolation and are stable", async () => {
const eslint = new ESLint({ fix: true });
const filePath = "packages/ui/src/EntryCard/EntryCard.tsx";
const [fixed] = await eslint.lintText("const Example = () => <div>\n<span />\n<span />\n</div>; export { Example };", { filePath });
const output = fixed.output;
assert.ok(output);
const [again] = await eslint.lintText(output, { filePath });
assert.equal(again.output, undefined);
assert.equal(again.errorCount, 0);
});