chore: guard presentation data boundaries
Frontend verify / commits (pull_request) Successful in 31s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 10m18s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 59s
Frontend verify / verify (push) Successful in 10m53s
Frontend verify / commits (pull_request) Successful in 31s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 10m18s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 59s
Frontend verify / verify (push) Successful in 10m53s
This commit was merged in pull request #45.
This commit is contained in:
@@ -17,6 +17,10 @@
|
||||
CSF сохраняет default meta и прямые story exports; configuration exceptions
|
||||
заданы в [ESLint](../../eslint.config.mjs). Не распространяй их на application code.
|
||||
|
||||
Тип значений формы выводится из её schema, когда input/output совпадают.
|
||||
Общие props определяются у одного owner; DTO и presentation model сохраняют
|
||||
собственные типы при смысловом отличии. Type re-export сохраняет public imports.
|
||||
|
||||
## Читаемость: ESLint и review
|
||||
|
||||
ESLint проверяет ограничение 80 символов (tab = 2), пустые строки между
|
||||
@@ -33,6 +37,10 @@ CSS и Markdown также держи в 80 колонках там, где эт
|
||||
|
||||
Review проверяет то, чего синтаксис не доказывает:
|
||||
|
||||
Обоснованное config exception сопровождается причиной и явным ожиданием в
|
||||
existing lint tests, включая разрешённый и запрещённый примеры. Это позволяет
|
||||
настраивать профиль без случайного ослабления остальных правил.
|
||||
|
||||
- Пустая строка отделяет смысловые шаги: inputs, вычисления, действие, результат.
|
||||
- Имена derived values описывают значение, например `projectedAmount`.
|
||||
- Тернарник остаётся коротким выбором двух значений. Mapping, несколько веток
|
||||
|
||||
@@ -63,6 +63,12 @@ Presentation VM для form-context/UI-only state может использов
|
||||
в каталоге: её граф зависимостей не достигает application data/API.
|
||||
`useTranslation` и декларативный вывод коллекции допустимы во view.
|
||||
|
||||
ESLint ограничивает прямые data/auth/router hook imports в `*.view.tsx`;
|
||||
type imports, error helpers, router components и form context разрешены.
|
||||
Транзитивный fetching через local presentation hook проверяется review;
|
||||
Storybook дополнительно проверяет reachable graph. Новое исключение должно
|
||||
сохранять presentation-only responsibility.
|
||||
|
||||
JSX props получают ссылки на функции, включая `onClick`, render props,
|
||||
`dateParser` и callbacks внутри объектов props. Для React Hook Form предпочитай
|
||||
`useController` в VM вместо inline `Controller.render`. Значение prop с условием
|
||||
|
||||
@@ -185,6 +185,44 @@ export default defineConfig(
|
||||
files: ["src/**/*.tsx", "packages/*/src/**/*.tsx"],
|
||||
ignores: ["**/*.test.tsx", "**/*.stories.tsx", "src/test/**"],
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.view.tsx"],
|
||||
rules: {
|
||||
"no-restricted-imports": ["error", {
|
||||
paths: [
|
||||
{
|
||||
allowTypeImports: true,
|
||||
message: "Query lifecycle belongs to the data VM.",
|
||||
name: "@tanstack/react-query",
|
||||
},
|
||||
{
|
||||
allowTypeImports: true,
|
||||
message: "Transport belongs to shared/api and the data VM.",
|
||||
name: "openapi-fetch",
|
||||
},
|
||||
{
|
||||
allowTypeImports: true,
|
||||
message: "Transport belongs to shared/api and the data VM.",
|
||||
name: "openapi-react-query",
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
allowTypeImports: true,
|
||||
group: ["react-router", "@/entities/*", "@/features/*"],
|
||||
importNamePattern: "^use",
|
||||
message: "Pass route, auth and entity data from the data VM.",
|
||||
},
|
||||
{
|
||||
allowTypeImports: true,
|
||||
group: ["@/shared/api", "@/shared/api/*"],
|
||||
importNamePattern: "^use|^api(?:Query)?Client$",
|
||||
message: "Fetching belongs to the data VM; error helpers are allowed.",
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.{ts,tsx}", "packages/*/src/**/*.{ts,tsx}"],
|
||||
ignores: ["**/*.test.{ts,tsx}", "**/*.stories.tsx", "src/test/**"],
|
||||
|
||||
@@ -338,6 +338,46 @@ test("JSX spacing fixes preserve text interpolation and are stable", async () =>
|
||||
assert.equal(again.errorCount, 0);
|
||||
});
|
||||
|
||||
test("views accept presentation inputs but reject direct data hooks", async () => {
|
||||
const eslint = new ESLint();
|
||||
const filePath = "src/features/instrument-details/ui/InstrumentEvents.view.tsx";
|
||||
|
||||
for (const code of [
|
||||
"import { useStock } from \"@/entities/stock\";",
|
||||
"import { useAuth } from \"@/features/auth\";",
|
||||
"import { useNavigate } from \"react-router\";",
|
||||
"import { apiClient } from \"@/shared/api\";",
|
||||
"import { useQuery } from \"@tanstack/react-query\";",
|
||||
]) {
|
||||
const [result] = await eslint.lintText(code, { filePath });
|
||||
assert.equal(result.fatalErrorCount, 0);
|
||||
assert.ok(result.messages.some(message =>
|
||||
message.ruleId === "no-restricted-imports"));
|
||||
}
|
||||
|
||||
for (const code of [
|
||||
"import { Link } from \"react-router\";",
|
||||
"import { apiErrorCode } from \"@/shared/api\";",
|
||||
"import type { StockPosition } from \"@/entities/stock\";",
|
||||
"import type { QueryKey } from \"@tanstack/react-query\";",
|
||||
"import { useTranslation } from \"react-i18next\";",
|
||||
"import { useFormContext } from \"react-hook-form\";",
|
||||
]) {
|
||||
const [result] = await eslint.lintText(code, { filePath });
|
||||
assert.equal(result.fatalErrorCount, 0);
|
||||
assert.ok(result.messages.every(message =>
|
||||
message.ruleId !== "no-restricted-imports"));
|
||||
}
|
||||
|
||||
const [vm] = await eslint.lintText(
|
||||
"import { useStock } from \"@/entities/stock\";",
|
||||
{ filePath: "src/pages/stock/ui/StockPage/StockPage.vm.ts" },
|
||||
);
|
||||
|
||||
assert.ok(vm.messages.every(message =>
|
||||
message.ruleId !== "no-restricted-imports"));
|
||||
});
|
||||
|
||||
test("compact JSX and contract nulls need no local suppressions", async () => {
|
||||
const eslint = new ESLint({ fix: true });
|
||||
const filePath = "packages/ui/src/EntryCard/EntryCard.tsx";
|
||||
|
||||
Reference in New Issue
Block a user