Инструмент: релизная сборка tools/minify.mjs (index.html −35%). (1) Методы: JS — terser compress passes:2 + mangle.toplevel:false (глобалы из буллета «Подводные камни» держатся именами — артефакт прогоняется обычной тестовой суитой); CSS — csso в каждом <style> КРОМЕ блока EMOJI-FONT (base64 woff2 держится побайтово, csso мог бы перенормировать кавычки); FISH-SVG — вырезаны HTML-комменты (~50 КБ описаний анатомии) и whitespace между тегами (в SVG текст-ноды не рендерятся — безопасно); прочие HTML-комменты — кроме маркеров EMOJI-FONT/FISH-SVG BEGIN/END (нельзя: whitespace внутри отрицательного lookaхeda, иначе <!-- EMOJI-FONT:BEGIN съедался бэктреком); whitespace DOM — только между блок-тегами (div/section/… + canvas/svg), inline-текст и пробелы между <b>…</b> и словом не трогаются. (2) Самопроверка 6 пунктов при любом FAIL exit 1: (a) размер, (b) node --check вынутого JS (паттерн tools/check.mjs), (c) маркеры по одному, (d) набор id идентичен (104), (e) видимый текст DOM идентичен, (f) блок EMOJI-FONT побайтово. (3) Артефакт dist/index.release.html: 541029 → 349886 байт (−35,3%), gzip 183366 → 128565; разбивка релиза: JS 112 КБ, SVG 120 КБ, шрифт 93 КБ, CSS 18 КБ, DOM ~5 КБ. (4) Валидация: ПОЛНАЯ тестовая суита на самом артефакте (подставлен под index.html) — 222/222 PASS, exit 0, headless Chromium. (5) Зависимости tools/: terser 5.51.2 + csso 5.0.5 (package.json + package-lock.json, npm i --prefix tools; tools/node_modules не коммитится — .gitignore +node_modules/, заодно прикрыл tests/node_modules). (6) index.html, balance, тесты, скриншоты НЕ тронуты. Доки: README (секция «Релизная (уменьшенная) сборка» в «Публикация в Яндекс Играх»), AGENTS.md (пункт 6 в «Проверка»: команда + методы + обязательный прогон tests/test.js на артефакте перед публикацией).

This commit is contained in:
2026-09-24 19:25:04 +00:00
parent 7437240cae
commit ad079b08f3
6 changed files with 437 additions and 0 deletions
+3
View File
@@ -6,6 +6,9 @@ tests/shot-*.png
tools/fonts-src/ tools/fonts-src/
tools/.venv/ tools/.venv/
# node_modules (tests/ — puppeteer-core, tools/ — terser+csso для minify.mjs)
node_modules/
# Служебное # Служебное
.opencode/ .opencode/
.po-handoff.md .po-handoff.md
+1
View File
@@ -22,6 +22,7 @@
Проходит тот же геймплей «вживую» в браузере (не headless) и пишет `orca-*.png` в каталог запуска. Если Orca app не запущен — пропустить и НЕ блокировать (главный рубеж — пункт 2). Подробности и подводные камни: `docs/BROWSER-TESTING.md`. Проходит тот же геймплей «вживую» в браузере (не 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-шрифты для скриншотов больше не нужны. 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. 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 в каждом `<style>` кроме блока шрифта (держится побайтово), в FISH-SVG удаляются комменты и whitespace между тегами, прочие HTML-комменты — кроме маркеров, whitespace DOM — только между блок-тегами. ПЕРЕД публикацией артефакта: подставить его под `index.html` и прогнать `node tests/test.js` (полную суиту) и/или цепочку пункта 5.
## Подводные камни ## Подводные камни
+6
View File
@@ -68,6 +68,12 @@ node tools/pack.mjs # → dist/fishing-yandex.zip
node tests/pack-smoke.js # перед загрузкой в консоль: smoke упакованной копии (игра из архива, 6 проверок) node tests/pack-smoke.js # перед загрузкой в консоль: smoke упакованной копии (игра из архива, 6 проверок)
``` ```
### Релизная (уменьшенная) сборка
Игра — один файл (~528 КБ), из них ~200 КБ — встроенные SVG-портреты рыб и ~93 КБ — вшитый emoji-шрифт. Для публикации «поменьше» есть минификатор: `node tools/minify.mjs` (зависимости: `npm i --prefix tools` — terser+csso). Он пишет `dist/index.release.html` (−35%: 528→342 КБ, gzip 179→126 КБ) и гоняет самопроверку (6 пунктов: размер, синтаксис JS, маркеры блоков, набор id, видимый текст DOM, base64 шрифта). Что сжимает: JS (terser, имена глобалов НЕ трогаются — артефакт прогоняется обычной тестовой суитой), CSS (csso), комменты и whitespace в SVG-портретах и DOM. Блок emoji-шрифта не трогается (base64 woff2 уже сжат).
Перед публикацией: подставь артефакт под `index.html` и прогони `node tests/test.js` (222 проверки) и/или цепочку выше (pack → pack-smoke).
SDK подхватывается автоматически на площадке; локально (file://) игра работает без него (встроенный стаб). Локальная отладка с SDK: открыть игру с параметром `?sdk=1` и положить `sdk.js` рядом с игрой (или поднять локальный прокси SDK Яндекса). В консоли разработчика (games.yandex.ru): включить монетизацию, опция облачных сейвов — выключена (игра сама хранит сейв в `localStorage`), платформы — без Android TV, `debug-mode=16` — для проверки лоадера. SDK подхватывается автоматически на площадке; локально (file://) игра работает без него (встроенный стаб). Локальная отладка с SDK: открыть игру с параметром `?sdk=1` и положить `sdk.js` рядом с игрой (или поднять локальный прокси SDK Яндекса). В консоли разработчика (games.yandex.ru): включить монетизацию, опция облачных сейвов — выключена (игра сама хранит сейв в `localStorage`), платформы — без Android TV, `debug-mode=16` — для проверки лоадера.
## Тесты ## Тесты
+255
View File
@@ -0,0 +1,255 @@
#!/usr/bin/env node
// Минификация Релизной сборки index.html → dist/index.release.html (для публикации).
//
// Что делает (по частям файла, маркеры блоков не трогаются):
// 1. <script> — terser: compress passes:2, mangle с toplevel:false.
// ВАЖНО: toplevel:false — имена глобалов (state, save, SPECIES, aqOpen, …) НЕ
// переименовываются: их пинит тестовая суита (tests/test.js через page.evaluate)
// и раздел AGENTS.md «Глобалы, которых нельзя переименовывать».
// 2. <style> — csso (каждый блок; EMOJI-FONT блок csso тоже проходит, но base64
// внутри src:url() остаётся побайтово — самопроверка (f) это гарантирует).
// 3. FISH-SVG — удаляются HTML-комменты (русс. описания анатомии рыб, ~50 КБ) и
// whitespace между тегами. В SVG текст-ноды не рендерятся, поэтому `>\s+<`→`><`
// безопасно; path-данные и атрибуты не трогаются.
// 4. Прочие HTML-комменты — удаляются, КРОМЕ маркеров EMOJI-FONT/FISH-SVG
// (BEGIN/END парами — нужны, чтобы блок оставался распознаваемым).
// 5. Whitespace DOM — схлопывается ТОЛЬКО между тегами блок-уровня (div/section/…).
// Inline-текст (в т.ч. пробелы между <b>…</b> и словом) не трогаем — самопроверка (e).
//
// Самопроверка (при любом FAIL — exit 1, артефакт не считается готовым):
// (a) размер выходного файла < входного;
// (b) синтаксис вынутого JS — node --check (тот же паттерн, что в tools/check.mjs);
// (c) маркеры EMOJI-FONT/FISH-SVG BEGIN/END сохранены (по 2 штуки);
// (d) набор id="…" в HTML идентичен оригиналу;
// (e) видимый текст DOM (вне script/style/SVG/комментов) идентичен оригиналу побайтово;
// (f) блок EMOJI-FONT (base64 woff2) идентичен оригиналу побайтово.
//
// Зависимости: terser, csso (npm i --prefix tools; package.json в tools/).
// Запуск: node tools/minify.mjs [путь-к-исходному-html] (по умолчанию index.html в корне репо)
//
// Дальше по желанию:
// - публикация веб-хостинга: положить dist/index.release.html как index.html;
// серверу включить gzip/brotli (тогда по сети пойдёт ещё меньше — см. вывод скрипта);
// - Яндекс Игры: заменить index.html на релизную версию и `node tools/pack.mjs`
// (pack сам проверит JS-синтаксис из архива), либо прогнать tests/test.js на копии.
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { createRequire } from 'node:module';
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gzipSync } from 'node:zlib';
import { minify } from 'terser';
const require = createRequire(import.meta.url);
const csso = require('csso');
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const htmlPath = process.argv[2] || join(root, 'index.html');
const distDir = join(root, 'dist');
const outPath = join(distDir, 'index.release.html');
if (!existsSync(htmlPath)) {
console.error(`minify: не найден ${htmlPath}`);
process.exit(1);
}
const src = readFileSync(htmlPath, 'utf8');
// ---------- 1. JS: terser ----------
const JS_RE = /<script>([\s\S]*?)<\/script>/;
const jsM = src.match(JS_RE);
if (!jsM) {
console.error('minify: не найден <script>-блок с JS');
process.exit(1);
}
let jsMin;
try {
jsMin = (await minify(jsM[1], {
compress: { passes: 2, module: false },
mangle: { toplevel: false }, // глобалы держим — см. шапку
format: { comments: false },
})).code;
} catch (e) {
console.error(`minify: terser не смог минифицировать JS: ${e.message}`);
process.exit(1);
}
let out = src.replace(jsM[1], () => jsMin); // function-замена: спецсимволы в JS-коде не влияют
// ---------- 2. CSS: csso в каждом <style>, КРОМЕ блока EMOJI-FONT ----------
// Блок шрифта — сгенерированный артефакт (tools/emoji-font.py), csso там всё равно
// ничего не сжимает (base64 не трогается), зато мог бы перенормировать кавычки —
// проходим мимо, держим побайтово (самопроверка (f) это ловит).
{
const fb = out.indexOf('EMOJI-FONT:BEGIN');
const fe = out.indexOf('EMOJI-FONT:END');
out = out.replace(/<style>([\s\S]*?)<\/style>/g, (all, css, off) => {
if (fb !== -1 && fe !== -1 && off > fb && off < fe) return all;
return '<style>' + csso.minify(css).css + '</style>';
});
}
// ---------- 3. FISH-SVG: без комментов, без whitespace между тегами ----------
const SVG_RE = /(<!-- FISH-SVG:BEGIN[^>]*-->)([\s\S]*?)(<!-- FISH-SVG:END -->)/;
const svgM = out.match(SVG_RE);
if (!svgM) {
console.error('minify: не найден блок FISH-SVG (маркеры BEGIN/END)');
process.exit(1);
}
const svgInner = svgM[2].replace(/<!--[\s\S]*?-->/g, '').replace(/>\s+</g, '><');
out = out.replace(svgM[0], () => svgM[1] + svgInner + svgM[3]);
// ---------- 4. Прочие HTML-комменты (кроме маркеров EMOJI-FONT/FISH-SVG) ----------
// Нюанс: whitespace идёт ВНУТРИ отрицательного lookахeda (`[^>]*`), а не `\s*` после
// `<!--` — иначе `\s*` откатывалось бы назад и маркер `<!-- EMOJI-FONT:BEGIN…` съедался.
out = out.replace(/<!--(?![^>]*(?:EMOJI-FONT|FISH-SVG):)[\s\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('</')) {
const nm = tag.slice(2, -1).trim().split(/\s/)[0].toLowerCase();
prevOpen = nm;
} else if (!tag.startsWith('<!--')) {
const nm = (tag.match(/^<([a-zA-Z][a-zA-Z0-9]*)/) || [])[1];
if (nm) prevOpen = nm.toLowerCase();
}
outDom += tag;
i = gt + 1;
// whitespace сразу после тега: съедаем, если закрыт блок-тег и дальше блок-тег/конец
let j = i;
while (j < out.length && /[\r\n \t]/.test(out[j])) j++;
let eat = false;
if (j > i && BLOCK.test(prevOpen)) {
if (j >= out.length) eat = true;
else if (out[j] === '<') {
if (out.startsWith('<!', j)) eat = false; // перед комментом не трогаем (маркер!)
else {
const head = out.slice(j, out.indexOf('>', j));
const nname = ((head.match(/^<\/?([a-zA-Z][a-zA-Z0-9]*)/) || [])[1] || '').toLowerCase();
eat = BLOCK.test(nname) || head.startsWith('</');
}
}
}
if (eat) i = j;
}
out = outDom;
}
// ---------- Запись ----------
mkdirSync(distDir, { recursive: true });
writeFileSync(outPath, out);
// ---------- Самопроверка ----------
let failed = false;
const fail = (code, msg) => { 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(/<script\b([^>]*)>([\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', 'в выходном файле не найдено <script>-блоков с JS');
else {
const tmp = join(tmpdir(), `minify-check-${process.pid}`);
mkdirSync(tmp, { recursive: true });
let synOk = true;
try {
blocks.forEach((code, i) => {
const f = join(tmp, `block-${i + 1}.js`);
writeFileSync(f, code);
const r = spawnSync(process.execPath, ['--check', f], { encoding: 'utf8' });
if (r.status !== 0) {
synOk = false;
fail('b', `синтаксис JS в блоке ${i + 1}: ${((r.stderr || r.stdout || '').trim()).split('\n')[0]}`);
}
});
if (synOk) pass('b', `синтаксис JS корректен (node --check, блоков: ${blocks.length})`);
} finally {
rmSync(tmp, { recursive: true, force: true });
}
}
}
// (c) маркеры сохранены
{
const cnt = (re) => (out.match(re) || []).length;
const okC =
cnt(/EMOJI-FONT:BEGIN/g) === 1 && cnt(/EMOJI-FONT:END/g) === 1 &&
cnt(/FISH-SVG:BEGIN/g) === 1 && cnt(/FISH-SVG:END/g) === 1;
if (okC) pass('c', 'маркеры EMOJI-FONT/FISH-SVG BEGIN/END сохранены (по одному)');
else fail('c', `маркеры потеряны: EMOJI-FONT BEGIN/END=${cnt(/EMOJI-FONT:BEGIN/g)}/${cnt(/EMOJI-FONT:END/g)}, FISH-SVG BEGIN/END=${cnt(/FISH-SVG:BEGIN/g)}/${cnt(/FISH-SVG:END/g)}`);
}
// (d) набор id идентичен
{
const ids = (s) => new Set([...s.matchAll(/id="([^"]+)"/g)].map((m) => m[1]));
const a = ids(src), b = ids(out);
const missing = [...a].filter((x) => !b.has(x));
const extra = [...b].filter((x) => !a.has(x));
if (missing.length === 0 && extra.length === 0) pass('d', `набор id идентичен (${a.size} шт)`);
else fail('d', `id расходятся: потеряно=[${missing}] лишнее=[${extra}]`);
}
// (e) видимый текст DOM идентичен (вне script/style/SVG/комментов)
{
const domText = (s) => {
let t = s;
t = t.replace(/<style>[\s\S]*?<\/style>/g, ' ');
t = t.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ');
t = t.replace(/<!--[\s\S]*?-->/g, ' ');
const i = t.search(/FISH-SVG:BEGIN/);
const j = t.indexOf('FISH-SVG:END');
if (i !== -1 && j !== -1) t = t.slice(0, i) + t.slice(j);
t = t.replace(/<[^>]+>/g, ' ');
return t.replace(/\s+/g, ' ').trim();
};
if (domText(src) === domText(out)) pass('e', 'видимый текст DOM идентичен оригиналу');
else fail('e', 'видимый текст DOM ИЗМЕНИЛСЯ (минификация съела текст)');
}
// (f) блок EMOJI-FONT побайтово
{
const font = (s) => {
const i = s.indexOf('EMOJI-FONT:BEGIN');
const j = s.indexOf('EMOJI-FONT:END');
return (i === -1 || j === -1) ? null : s.slice(i, j + 'EMOJI-FONT:END'.length);
};
const a = font(src), b = font(out);
if (a && b && a === b) pass('f', 'блок EMOJI-FONT (base64 woff2) идентичен оригиналу');
else fail('f', 'блок EMOJI-FONT изменён/потерян');
}
// ---------- Итог ----------
if (failed) {
console.error(`minify: FAIL — ${outPath} НЕ готов к публикации (см. пункты выше)`);
process.exit(1);
}
console.log(`minify: OK — ${outPath}`);
console.log(` размер: ${fmt(srcBytes)} → ${fmt(outBytes)} (−${(100 * (1 - outBytes / srcBytes)).toFixed(1)}%)`);
console.log(` gzip (оценка): ${fmt(gzipSync(Buffer.from(src, 'utf8')).length)} → ${fmt(gzipSync(Buffer.from(out, 'utf8')).length)}`);
console.log(' проверка работоспособности: подставь артефакт под index.html и прогони node tests/test.js (и/или tests/pack-smoke.js через pack.mjs)');
function fmt(bytes) {
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' КБ';
return (bytes / (1024 * 1024)).toFixed(2) + ' МБ';
}
+162
View File
@@ -0,0 +1,162 @@
{
"name": "fishing-game-tools",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fishing-game-tools",
"dependencies": {
"csso": "^5.0.5",
"terser": "^5.48.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/acorn": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/css-tree": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.0.28",
"source-map-js": "^1.0.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/csso": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
"license": "MIT",
"dependencies": {
"css-tree": "~2.2.0"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/mdn-data": {
"version": "2.0.28",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
"license": "CC0-1.0"
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/terser": {
"version": "5.51.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz",
"integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==",
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "fishing-game-tools",
"private": true,
"description": "Инструменты сборки: minify.mjs (terser + csso)",
"type": "module",
"dependencies": {
"csso": "^5.0.5",
"terser": "^5.48.0"
}
}