76 lines
3.5 KiB
JavaScript
76 lines
3.5 KiB
JavaScript
#!/usr/bin/env node
|
||
// Проверка синтаксиса JS, встроенного в index.html: вынимает содержимое <script>-блоков,
|
||
// прогоняет каждый через node --check во временном файле. Автоматизирует п.1 раздела
|
||
// «Проверка» из AGENTS.md. Чистый Node, без зависимостей.
|
||
// Запуск: node tools/check.mjs [путь-к-html] (по умолчанию index.html в корне репо)
|
||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import { dirname, join } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { spawnSync } from 'node:child_process';
|
||
|
||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||
const htmlPath = process.argv[2] || join(root, 'index.html');
|
||
|
||
let html;
|
||
try {
|
||
html = readFileSync(htmlPath, 'utf8');
|
||
} catch (e) {
|
||
console.error(`check: не удалось прочитать ${htmlPath}: ${e.message}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
// Допустимые типы скриптов с JS (у игры тип не указан); <script src=...> и не-JS блоки пропускаем.
|
||
const JS_TYPES = new Set(['', 'text/javascript', 'application/javascript', 'module']);
|
||
const re = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
||
|
||
const blocks = [];
|
||
for (const m of html.matchAll(re)) {
|
||
const attrs = m[1];
|
||
const code = m[2];
|
||
if (/\bsrc\s*=/i.test(attrs)) continue;
|
||
const type = (attrs.match(/\btype\s*=\s*["']?([^"'\s>]*)/i)?.[1] ?? '').toLowerCase();
|
||
if (!JS_TYPES.has(type)) continue;
|
||
if (!code.trim()) continue;
|
||
// 1-я строка index.html, с которой начинается код блока (сразу после ">").
|
||
const lineInHtml = html.slice(0, m.index + m[0].indexOf(code)).split('\n').length;
|
||
blocks.push({ code, lineInHtml, isModule: type === 'module' });
|
||
}
|
||
|
||
if (!blocks.length) {
|
||
console.error(`check: в ${htmlPath} не найдено <script>-блоков с JS`);
|
||
process.exit(1);
|
||
}
|
||
|
||
const tmp = mkdtempSync(join(tmpdir(), 'htmljs-check-'));
|
||
let failed = false;
|
||
try {
|
||
for (const [i, b] of blocks.entries()) {
|
||
const file = join(tmp, `block-${i + 1}.${b.isModule ? 'mjs' : 'js'}`);
|
||
writeFileSync(file, b.code);
|
||
const r = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8' });
|
||
if (r.status !== 0) {
|
||
failed = true;
|
||
const out = (r.stderr || r.stdout || String(r.error)).trim();
|
||
// node --check пишет в первой строке stderr "<файл>:<строка>" —
|
||
// переводим номер строки в координаты index.html (строки, не столбцы).
|
||
const mm = out.match(new RegExp(`block-${i + 1}\\.(?:mjs|js):(\\d+)`));
|
||
if (mm) {
|
||
const htmlLine = Number(mm[1]) + b.lineInHtml - 1;
|
||
const rest = out.split('\n').slice(1).join('\n');
|
||
console.error(`check: ОШИБКА синтаксиса в ${htmlPath}:${htmlLine} (блок ${i + 1} из ${blocks.length})`);
|
||
if (rest) console.error(rest);
|
||
} else {
|
||
console.error(`check: ОШИБКА синтаксиса в блоке ${i + 1} из ${blocks.length} (${htmlPath})`);
|
||
console.error(out);
|
||
}
|
||
}
|
||
}
|
||
} finally {
|
||
rmSync(tmp, { recursive: true, force: true });
|
||
}
|
||
|
||
if (failed) process.exit(1);
|
||
const jsLines = blocks.reduce((s, b) => s + b.code.split('\n').length, 0);
|
||
console.log(`check: OK — ${htmlPath}: блоков ${blocks.length}, строк JS ${jsLines}, синтаксис корректен`);
|