mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Артефакт один — 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
68 lines
3.1 KiB
JavaScript
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} Б`);
|
|
}
|