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
+10 -1
View File
@@ -14,14 +14,20 @@ const { spawnSync } = require("node:child_process");
test("immutable publication resumes exact digest checks after post-push failure", () => {
const directory = mkdtempSync(nodePath.join(tmpdir(), "wl-image-retry-"));
const workflow = readFileSync(
nodePath.resolve(__dirname, "../.gitea/workflows/image.yml"),
"utf8",
);
const block = workflow
.split(" - name: Publish immutable release tag\n run: |\n", 2)[1]
.split(
" - name: Publish immutable release tag\n run: |\n",
2,
)[1]
.split(" - name:", 1)[0]
.replaceAll(/^ {10}/gm, "");
const double = String.raw`#!${process.execPath}
const fs = require('node:fs');
const args = process.argv.slice(2).join(' ');
@@ -39,6 +45,7 @@ if (args.startsWith('manifest inspect')) {
console.log(args.includes('image.version') ? '1.2.3' : process.env.BAD_REVISION ? 'other' : 'commit');
} else if (args.startsWith('run') && process.env.BAD_SMOKE) process.exit(1);
`;
try {
for (const name of ["docker", "npm"]) {
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames.
@@ -46,6 +53,7 @@ if (args.startsWith('manifest inspect')) {
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames.
chmodSync(nodePath.join(directory, name), EXECUTABLE_MODE);
}
const run = (extra = {}) =>
spawnSync("/bin/sh", ["-ec", block], {
cwd: directory,
@@ -59,6 +67,7 @@ if (args.startsWith('manifest inspect')) {
...extra,
},
});
assert.notEqual(run().status, 0);
const retry = run();
assert.equal(retry.status, 0, retry.stdout + retry.stderr);
+2
View File
@@ -6,6 +6,7 @@ import nodePath from "node:path";
const temporaryDirectory = await mkdtemp(
nodePath.join(tmpdir(), "wealth-ledger-openapi-"),
);
const generatedPath = nodePath.join(temporaryDirectory, "schema.d.ts");
const committedPath = nodePath.resolve("src/shared/api/generated/schema.d.ts");
const cliPath = nodePath.resolve("node_modules/openapi-typescript/bin/cli.js");
@@ -24,6 +25,7 @@ try {
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames.
readFile(generatedPath, "utf8"),
]);
if (committed !== current) {
console.error(
"Generated OpenAPI types are stale. Run `npm run openapi:generate` and commit the result.",
+5 -2
View File
@@ -2,14 +2,16 @@ const assert = require("node:assert/strict");
const { execFileSync } = require("node:child_process");
const { version } = require("../package.json");
// eslint-disable-next-line sonarjs/no-os-command-from-path -- Repository checks use the developer/CI Git installation; arguments never invoke a shell.
const git = (...arguments_) => execFileSync("git", arguments_, { encoding: "utf8" }).trim();
const git = (...arguments_) =>
execFileSync("git", arguments_, { encoding: "utf8" }).trim(); // eslint-disable-line sonarjs/no-os-command-from-path -- Repository checks use the developer/CI Git installation; arguments never invoke a shell.
assert.equal(
git("status", "--porcelain"),
"",
"Commit all changes before preparing a release",
);
const tag_ = git(
"tag",
"--merged",
@@ -20,6 +22,7 @@ const tag_ = git(
)
.split("\n")
.find(tag => /^v\d+\.\d+\.\d+$/.test(tag));
assert.equal(
version,
tag_?.slice(1) ?? "0.0.0",
+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);
});
+14 -5
View File
@@ -20,23 +20,32 @@ test("pre-commit scans the index, redacts findings and fails closed", () => {
const bin = nodePath.join(cwd, "bin");
const marker = nodePath.join(cwd, "downstream");
const token = `ghp_${randomBytes(GITHUB_TOKEN_RANDOM_BYTES).toString("hex")}`;
// eslint-disable-next-line sonarjs/no-os-command-from-path -- Repository checks use the developer/CI Git installation; arguments never invoke a shell.
const git = (...arguments_) => execFileSync("git", arguments_, { cwd, stdio: "pipe" });
const git = (...arguments_) =>
execFileSync("git", arguments_, { cwd, stdio: "pipe" }); // eslint-disable-line sonarjs/no-os-command-from-path -- Repository checks use the developer/CI Git installation; arguments never invoke a shell.
const run = (path = `${bin}${nodePath.delimiter}${process.env.PATH}`) =>
spawnSync("/bin/sh", ["-e", hook], {
cwd,
encoding: "utf8",
env: { ...process.env, PATH: path },
});
try {
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames.
mkdirSync(bin);
for (const command of ["npx", "npm"]) {
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames.
writeFileSync(nodePath.join(bin, command), "#!/bin/sh\necho ran >> downstream\n", {
mode: 0o755,
});
writeFileSync(
nodePath.join(bin, command),
"#!/bin/sh\necho ran >> downstream\n",
{
mode: 0o755,
},
);
}
git("init", "--quiet");
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames.
writeFileSync(nodePath.join(cwd, "sample.txt"), "safe staged content\n");