gendesign/tradein-mvp/frontend/scripts/check-mera-public-isolation.mjs
bot-backend 8e909faca8
All checks were successful
Deploy / changes (push) Successful in 8s
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy / build-frontend (push) Successful in 35s
Deploy Trade-In / build-frontend (push) Successful in 3m32s
Deploy Trade-In / deploy (push) Successful in 1m6s
Deploy / build-backend (push) Successful in 5m42s
Deploy / build-worker (push) Successful in 5m54s
Deploy / deploy (push) Successful in 1m47s
chore(mera-public): isolation guard + favicon + Caddy handle (#2631) (#2644)
2026-08-05 06:20:40 +00:00

158 lines
6.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Guard for issue #2631 / #2545: публичный B2C-лэндинг `/mera-public` должен
* оставаться "пустым по зависимостям" — ни один файл, который Next.js грузит
* СТАТИЧЕСКИ (initial JS для этого route-чанка), не должен транзитивно
* дотягиваться до закрытого контура (useMe / lib/api / lib/sessionId /
* isPathAllowed / RBAC). Никакого test runner (jest/vitest) в проекте нет
* (см. package.json) — это source-level guard вместо test-suite.
*
* НЕ подключён в `.forgejo/workflows/ci-tradein.yml` этим PR (вне scope —
* только `tradein-mvp/frontend/**`). Чтобы он реально гейтил PR, нужен один
* доп. `run: npm run check:mera-public-isolation` шаг в job `frontend-checks`
* рядом с `npm run lint` — отдельным devops/frontend PR.
*
* Как это соотносится с реальным webpack-чанком: `RouteGuard.tsx` намеренно
* подключает `GuardedRoute` через `next/dynamic(() => import(...))` — то есть
* точку разрыва графа модулей (см. шапки RouteGuard.tsx / GuardedRoute.tsx).
* Этот скрипт следует ТОЛЬКО статическим edges (`import ... from "..."` /
* `export ... from "..."` / side-effect `import "..."`) и НИКОГДА не идёт
* внутрь динамического `import(...)` — ровно то же правило, по которому
* webpack решает, что попадёт в один чанк, а что уедет в отдельный.
*
* Запуск: `node scripts/check-mera-public-isolation.mjs` (npm run
* check:mera-public-isolation). Exit 1 + печать цепочки импортов при находке.
*/
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SRC = path.resolve(__dirname, "..", "src");
const RESOLVABLE_EXTS = [".ts", ".tsx", ".js", ".jsx"];
const STATIC_IMPORT_RE = /(?:^|\s)(?:import|export)\s[^;]*?\sfrom\s*["']([^"']+)["']|(?:^|\s)import\s*["']([^"']+)["']/g;
function collectFilesRecursively(dir) {
if (!existsSync(dir)) return [];
const out = [];
for (const entry of readdirSync(dir)) {
const full = path.join(dir, entry);
const st = statSync(full);
if (st.isDirectory()) {
out.push(...collectFilesRecursively(full));
} else if (RESOLVABLE_EXTS.includes(path.extname(full))) {
out.push(full);
}
}
return out;
}
// Всё, что Next.js грузит для /mera-public НЕЗАВИСИМО от dynamic()-разрывов:
// корневой layout (Providers + RouteGuard) + всё поддерево лэндинга.
const ENTRY_FILES = [
path.join(SRC, "app/layout.tsx"),
path.join(SRC, "app/providers.tsx"),
path.join(SRC, "components/auth/RouteGuard.tsx"),
...collectFilesRecursively(path.join(SRC, "app/mera-public")),
];
// Закрытый контур — см. GuardedRoute.tsx шапку. GuardedRoute.tsx сам тоже
// forbidden: он обязан доезжать ТОЛЬКО через next/dynamic, никогда статически.
const FORBIDDEN = [
"lib/useMe.ts",
"lib/api.ts",
"lib/sessionId.ts",
"lib/isPathAllowed.ts",
"components/auth/GuardedRoute.tsx",
].map((p) => path.join(SRC, p));
function resolveSpecifier(spec, fromFile) {
let base;
if (spec.startsWith("@/")) {
base = path.join(SRC, spec.slice(2));
} else if (spec.startsWith(".")) {
base = path.resolve(path.dirname(fromFile), spec);
} else {
return null; // bare specifier — внешний пакет / next/*, не наш граф
}
if (existsSync(base) && statSync(base).isFile()) return base;
for (const ext of RESOLVABLE_EXTS) {
if (existsSync(base + ext)) return base + ext;
}
for (const ext of RESOLVABLE_EXTS) {
const indexed = path.join(base, "index" + ext);
if (existsSync(indexed)) return indexed;
}
return null; // .css / .module.css / картинки / не найдено — лист, не ошибка
}
function staticImportsOf(file) {
const src = readFileSync(file, "utf8");
const specs = [];
let m;
STATIC_IMPORT_RE.lastIndex = 0;
while ((m = STATIC_IMPORT_RE.exec(src)) !== null) {
specs.push(m[1] ?? m[2]);
}
return specs;
}
// BFS от всех entry-файлов по СТАТИЧЕСКИМ рёбрам, с parent-map для отчёта.
const parent = new Map();
const visited = new Set();
const queue = [];
for (const entry of ENTRY_FILES) {
if (!visited.has(entry)) {
visited.add(entry);
queue.push(entry);
}
}
let violation = null;
while (queue.length > 0 && !violation) {
const file = queue.shift();
for (const forbidden of FORBIDDEN) {
if (file === forbidden) {
violation = file;
break;
}
}
if (violation) break;
for (const spec of staticImportsOf(file)) {
const resolved = resolveSpecifier(spec, file);
if (!resolved || visited.has(resolved)) continue;
visited.add(resolved);
parent.set(resolved, file);
queue.push(resolved);
}
}
function chainTo(file) {
const chain = [file];
let cur = file;
while (parent.has(cur)) {
cur = parent.get(cur);
chain.unshift(cur);
}
return chain.map((f) => path.relative(SRC, f)).join("\n -> ");
}
if (violation) {
console.error(
"FAIL: /mera-public публичный бандл статически дотягивается до закрытого контура.\n" +
"Цепочка импортов:\n " +
chainTo(violation) +
"\n\nЕсли модуль реально нужен закрытому RBAC-коду — подключай его через " +
"next/dynamic(() => import(...)), как GuardedRoute в RouteGuard.tsx, а не " +
"статическим import.",
);
process.exit(1);
}
console.log(
`OK: mera-public isolation — ${ENTRY_FILES.length} файлов проверено, закрытый контур не найден.`,
);