// Патч: под словесным знаком «НЕОН» появляется подпись «(ex Shelby)» — // прежнее название бара. Подпись живёт до конца 2026 года: с 1 января 2027 // клиент прячет её сам (useEffect), пересборка не требуется. // Скрипт не идемпотентен: повторный запуск откажется работать. import { readFileSync, writeFileSync, copyFileSync } from "node:fs"; const HEADER = "src/components/Header.tsx"; const FOOTER = "src/components/Footer.tsx"; const die = (msg) => { console.error("PATCH_FAIL: " + msg); process.exit(1); }; // ---------- Header.tsx ---------- let src = readFileSync(HEADER, "utf8"); copyFileSync(HEADER, HEADER + ".bak"); const MARK = "export function Logo("; const first = src.indexOf(MARK); if (first === -1) die("Logo declaration not found in Header.tsx"); if (src.indexOf(MARK, first + 1) !== -1) die("Logo declared more than once"); // конец списка параметров: скобочный баланс от первой ( после MARK let i = src.indexOf("(", first); let parenDepth = 0; for (; i < src.length; i++) { if (src[i] === "(") parenDepth++; else if (src[i] === ")") { parenDepth--; if (parenDepth === 0) { i++; break; } } } // пропускаем пробелы до тела функции while (i < src.length && /\s/.test(src[i])) i++; if (src[i] !== "{") die("Logo body brace not found"); // тело функции: баланс фигурных скобок let depth = 0; let end = -1; for (; i < src.length; i++) { if (src[i] === "{") depth++; else if (src[i] === "}") { depth--; if (depth === 0) { end = i; break; } } } if (end === -1) die("Logo body end not found"); const original = src.slice(first, end + 1); if (!original.includes("") || !original.includes("ОН")) { die("Logo body does not look like the wordmark: " + JSON.stringify(original.slice(0, 240))); } console.log("LOGO_FOUND: " + original.length + " chars, href=" + (original.includes('#home') ? "#home" : "?")); const replacement = `const FORMER_NAME_DEADLINE = new Date("2027-01-01T00:00:00").getTime(); export function Logo({ className }: { className?: string }) { // «(ex Shelby)» — прежнее название бара; подпись видна до конца 2026 года, // после новогодней ночи клиент прячет её сам, пересборка не требуется. // Изначально true, чтобы пререндеренный HTML и первая клиентская отрисовка // совпадали (без hydration-мисматча); эффект убирает надпись после дедлайна. const [showFormerName, setShowFormerName] = useState(true); useEffect(() => { if (Date.now() >= FORMER_NAME_DEADLINE) setShowFormerName(false); }, []); return ( НЕ ОН {showFormerName && ( (ex Shelby) )} ); }`; src = src.slice(0, first) + replacement + src.slice(end + 1); // импорты useState/useEffect, если их ещё нет до объявления Logo const beforeLogo = src.slice(0, src.indexOf("export function Logo")); if (!/\buseEffect\b/.test(beforeLogo)) { if (/import \{[^}]*\} from "react";/.test(src)) { src = src.replace(/import \{([^}]*)\} from "react";/, (m, names) => { const names2 = names .split(",") .map((s) => s.trim()) .filter(Boolean); for (const n of ["useState", "useEffect"]) { if (!names2.includes(n)) names2.push(n); } return `import { ${names2.join(", ")} } from "react";`; }); } else { src = `import { useEffect, useState } from "react";\n` + src; } console.log("REACT_IMPORT_PATCHED"); } else { console.log("REACT_IMPORT_ALREADY_THERE"); } writeFileSync(HEADER, src); console.log("HEADER_PATCHED"); // ---------- Footer.tsx ---------- const footer = readFileSync(FOOTER, "utf8"); if (/