diff --git a/.gitignore b/.gitignore index f6d82ee..b6bb6b9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ tests/shot-*.png tools/fonts-src/ tools/.venv/ +# node_modules (tests/ — puppeteer-core, tools/ — terser+csso для minify.mjs) +node_modules/ + # Служебное .opencode/ .po-handoff.md diff --git a/AGENTS.md b/AGENTS.md index 3d852a8..8ec70b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,7 @@ Проходит тот же геймплей «вживую» в браузере (не headless) и пишет `orca-*.png` в каталог запуска. Если Orca app не запущен — пропустить и НЕ блокировать (главный рубеж — пункт 2). Подробности и подводные камни: `docs/BROWSER-TESTING.md`. 4. Скриншоты: `node tests/gen-screenshots.js [каталог-сборки] [папка-вывода] [язык]` — 15 кадров × 3 вьюпорта (1280×720 / 360×640@2x / 800×[450@1.6x](mailto:450@1.6x); кадры sea/ocean — этап 43, swamp/swamp-night — этап 45, aquarium — этап 46), 24-битный PNG без альфа. Без аргументов — русские в `screenshots-wiki/`, с `en` — английские в `screenshots-wiki-en/`. README ссылается на `screenshots-wiki/mobp-*` (мобильный портрет). Emoji рисует встроенный шрифт (раздел «Встроенный emoji-шрифт») — системные emoji-шрифты для скриншотов больше не нужны. 5. Публикация в Яндекс Играх: `node tools/pack.mjs` (сборка dist/fishing-yandex.zip + самопроверка 5 пунктов) → `node tests/pack-smoke.js` (smoke играбельности КОПИИ из архива, 6 чек) → загрузить zip в консоль. Доки: раздел README «Публикация в Яндекс Играх», запись Этап 27 в DEVLOG. +6. Релизная (уменьшенная) сборка: `node tools/minify.mjs` (зависимости: `npm i --prefix tools` — terser+csso) → `dist/index.release.html` (−35% к размеру) + самопроверка 6 пунктов (размер, node --check, маркеры EMOJI-FONT/FISH-SVG, набор id, видимый текст DOM, base64 шрифта). Методы: terser с `mangle.toplevel:false` (глобалы из буллета «Подводные камни» — «Тесты гоняют игру через внутренние глобалы» — держатся ИМЯМИ, поэтому артефакт можно прогнать тем же tests/test.js), csso в каждом `'; + }); +} + +// ---------- 3. FISH-SVG: без комментов, без whitespace между тегами ---------- +const SVG_RE = /()([\s\S]*?)()/; +const svgM = out.match(SVG_RE); +if (!svgM) { + console.error('minify: не найден блок FISH-SVG (маркеры BEGIN/END)'); + process.exit(1); +} +const svgInner = svgM[2].replace(//g, '').replace(/>\s+<'); +out = out.replace(svgM[0], () => svgM[1] + svgInner + svgM[3]); + +// ---------- 4. Прочие HTML-комменты (кроме маркеров EMOJI-FONT/FISH-SVG) ---------- +// Нюанс: whitespace идёт ВНУТРИ отрицательного lookахeda (`[^>]*`), а не `\s*` после +// `[ \t]*\n?/g, ''); + +// ---------- 5. Whitespace DOM: только между блок-тегами ---------- +const BLOCK = /^(div|body|section|header|footer|main|nav|ul|ol|li|table|canvas|svg)$/i; +{ + let prevOpen = ''; // имя последнего открытого (и ещё не закрытого… упрощённо: последнего встреченного) тега + let outDom = ''; + let i = 0; + while (i < out.length) { + const lt = out.indexOf('<', i); + if (lt < 0) { outDom += out.slice(i); break; } + outDom += out.slice(i, lt); + const gt = out.indexOf('>', lt); + if (gt < 0) { outDom += out.slice(lt); break; } + const tag = out.slice(lt, gt + 1); + if (tag.startsWith(' i && BLOCK.test(prevOpen)) { + if (j >= out.length) eat = true; + else if (out[j] === '<') { + if (out.startsWith('', j)); + const nname = ((head.match(/^<\/?([a-zA-Z][a-zA-Z0-9]*)/) || [])[1] || '').toLowerCase(); + eat = BLOCK.test(nname) || head.startsWith(' { failed = true; console.error(`FAIL (${code}) ${msg}`); }; +const pass = (code, msg) => console.log(`PASS (${code}) ${msg}`); + +const srcBytes = Buffer.byteLength(src, 'utf8'); +const outBytes = Buffer.byteLength(out, 'utf8'); + +// (a) стало меньше +if (outBytes < srcBytes) pass('a', `размер: ${fmt(srcBytes)} → ${fmt(outBytes)} (−${(100 * (1 - outBytes / srcBytes)).toFixed(1)}%)`); +else fail('a', `минификат (${fmt(outBytes)}) не меньше оригинала (${fmt(srcBytes)})`); + +// (b) синтаксис JS из ВЫХОДНОГО файла (паттерн tools/check.mjs) +{ + const blocks = []; + for (const m of out.matchAll(/]*)>([\s\S]*?)<\/script>/gi)) { + if (/\bsrc\s*=/i.test(m[1])) continue; + if (m[2].trim()) blocks.push(m[2]); + } + if (!blocks.length) fail('b', 'в выходном файле не найдено