Files
houseplan-card/scripts/sh3d-convert/build-page.mjs
T
Claude f6cd99cef4 feat(tools): страница /convert для конвертера Sweet Home 3D, этап 2
Артефакт один — scripts/sh3d-convert/dist/index.html, самодостаточный
файл, как и лендинг: раскладывается копированием, без сборщиков и CDN.
Собирается склейкой тех же модулей, которые проверяют гейты этапа 1;
относительных импортов на странице нет, поэтому она работает из любого
каталога и без сети.

Три обещания страницы проверяются тестами, а не текстом:

1. Ни одного сетевого вызова и ни одной внешней загрузки. Ищутся формы
   ВЫЗОВА, а не имена API: первая редакция теста краснела на собственном
   комментарии, где эти API перечислены как запрещённые. Шрифты
   системные — <link> к шрифтам это внешний запрос при загрузке, а
   обещание «файл не покидает браузер» должно быть буквальным. Заодно
   поэтому на странице нет аналитики.
2. Конвертер внутри страницы даёт тот же результат, что модули
   репозитория: код извлекается из собранного артефакта, прогоняется на
   фикстуре и сравнивается с golden. Склейка, потерявшая модуль,
   краснеет.
3. Собранная страница не отстала от исходников: build-page.mjs --check
   сверяет закоммиченный артефакт с пересборкой.

Плюс полнота переводов: у каждой подписи есть ru и en, словари
совпадают по ключам, у каждого кода отчёта и каждого кода отказа есть
человеческий текст. Иначе пользователь увидит пустую надпись или
`not_zip`.

Отчёт показывается ДО скачивания и по этажам: сколько комнат, стен и
проёмов, какая клетка сетки, и полный список мест, где конвертер принял
решение, — спрямлённая дуга, обрезанная толщина, непривязанный проём,
пропущенный этаж без комнат. Это требование задачи: искать чужие ошибки
в готовом плане из сотен объектов хуже, чем прочитать их заранее.

Гайды (ru/en) получили раздел про импорт из Sweet Home 3D с прямой
записью, что это необязательный ярлык, а не шаг настройки: SCOPE обещает
«no external editors», и обходить это молчанием нельзя.

Свидетели этапа 2, все проверены отрицательным прогоном: добавленный в
страницу fetch, потерянный русский перевод, правка dist руками, склейка
без модуля sh3d.mjs — каждая краснит свой тест.

Гейты: npm test 1871 tests, 1870 pass, 0 fail; pytest без HA 378 passed,
3 skipped.

Осталось владельцу: раскладка на хост (из песочницы SSH недоступен) и
проверка на настоящем файле Sweet Home 3D.

Issue: #446
User-Visible: no
2026-09-04 00:41:40 +03:00

68 lines
3.1 KiB
JavaScript

#!/usr/bin/env node
/**
* Сборка страницы /convert в один самодостаточный файл (#446).
*
* Артефакт один: `dist/index.html`. Так же устроен лендинг, и так же его
* раскладывают на хост — копированием, без сборщиков и без CDN.
*
* Модули склеиваются, а не грузятся по относительным путям: страница обязана
* работать из любого каталога и без сети. Склейка нарочно тупая — снимаются
* строки `import ... from '...'` и префикс `export `. Многострочный импорт
* ломает такую склейку молча, поэтому он ОТКАЗ, а не предупреждение.
*
* node scripts/sh3d-convert/build-page.mjs [--check]
*
* `--check` не пишет файл, а сверяет закоммиченный артефакт с пересборкой:
* этим тест ловит правку страницы или модулей без пересборки.
*/
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ORDER = ['xml.mjs', 'zip.mjs', 'convert.mjs', 'sh3d.mjs', 'page/app.mjs'];
const MARKER = '/* <!--BUNDLE--> */';
const stripModuleSyntax = (source, name) => {
const lines = source.split('\n');
const out = [];
for (const [index, line] of lines.entries()) {
if (/^\s*import\s/.test(line)) {
if (!/;\s*$/.test(line)) {
throw new Error(
`${name}:${index + 1}: многострочный import ломает склейку — запишите его в одну строку`);
}
continue;
}
out.push(line.replace(/^export\s+(?=const|function|class|async|let)/, ''));
}
const remaining = out.find((line) => /^\s*export\s/.test(line));
if (remaining) throw new Error(`${name}: не снят export: ${remaining.trim()}`);
return out.join('\n');
};
export function buildPage() {
const shell = readFileSync(join(HERE, 'page/shell.html'), 'utf8');
if (!shell.includes(MARKER)) throw new Error('в shell.html нет метки вставки');
const bundle = ORDER
.map((name) => `// --- ${name} ---\n${stripModuleSyntax(readFileSync(join(HERE, name), 'utf8'), name)}`)
.join('\n');
return shell.replace(MARKER, `${bundle}\nmount();`);
}
const page = buildPage();
const target = join(HERE, 'dist', 'index.html');
if (process.argv.includes('--check')) {
const committed = readFileSync(target, 'utf8');
if (committed !== page) {
console.error('FAILED: dist/index.html отстал от исходников —'
+ ' пересоберите: node scripts/sh3d-convert/build-page.mjs');
process.exit(1);
}
console.log('ok: dist/index.html совпадает с пересборкой');
} else {
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, page);
console.log(`dist/index.html: ${page.length} Б`);
}