diff --git a/scripts/doctor.mjs b/scripts/doctor.mjs index 2ce4930..02a6b98 100755 --- a/scripts/doctor.mjs +++ b/scripts/doctor.mjs @@ -15,6 +15,7 @@ import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; +import { execFileSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const HERE = path.dirname(fileURLToPath(import.meta.url)); @@ -47,6 +48,24 @@ const SERVER_CORE = path.join(BACK, 'core'); // «Demo Panel» в нём норма, а не недоделка. Узнаётся по каталогу core/ рядом. const IS_TEMPLATE = fs.existsSync(path.join(PANEL, 'core', 'browser')); +// Ядро попадает в панель одним из двух способов, и проверки у них разные: +// +// subtree — панель лежит в git-репозитории продукта, ядро подтянуто +// `git subtree add` и потому отслеживается git. Обновляется +// `git subtree pull`; update.sh здесь не нужен и вреден — он +// сносит каталог целиком и затёр бы поддерево. +// +// copy — ядро раскладывает update.sh из соседнего ma7engine. Так устроен +// сам шаблон, и так живёт панель вне git. +function coreIsTracked(dir) { + try { + const out = execFileSync('git', ['ls-files', '--', dir], + { cwd: PANEL, stdio: ['ignore', 'pipe', 'ignore'] }); + return out.toString().trim() !== ''; + } catch { return false; } +} +const MODE = !IS_TEMPLATE && coreIsTracked(path.join(FRONT, 'src/core')) ? 'subtree' : 'copy'; + // ─── накопитель результатов ───────────────────────────────────────────────── const errors = [], warns = [], skips = [], oks = []; @@ -71,20 +90,33 @@ if (!fs.existsSync(FRONT)) { } const updateSh = path.join(FRONT, 'scripts/update.sh'); -if (fs.existsSync(updateSh)) ok('update.sh на месте'); +if (MODE === 'subtree') { + if (fs.existsSync(updateSh)) { + err('в subtree-панели остался frontend/scripts/update.sh', + 'он сносит каталог ядра целиком (rm -rf) и затрёт подтянутое поддерево — ' + + 'обновление здесь делается git subtree pull'); + } else ok('ядро приходит через git subtree (update.sh не нужен)'); +} else if (fs.existsSync(updateSh)) ok('update.sh на месте'); else err('нет frontend/scripts/update.sh', 'без него ядро в панели не обновляется'); const frontPkgText = need(path.join(FRONT, 'package.json'), 'фронт'); const frontPkg = frontPkgText ? JSON.parse(frontPkgText) : null; if (frontPkg) { const s = frontPkg.scripts ?? {}; - if (/update\.sh/.test(s.update ?? '')) ok('скрипт update ведёт на scripts/update.sh'); - else err('frontend/package.json: скрипт "update" не зовёт scripts/update.sh'); + if (MODE === 'subtree') { + const stale = ['update', 'predev', 'prebuild'].filter(h => /update/.test(s[h] ?? '')); + if (stale.length) err(`frontend/package.json: остались хуки копирования (${stale.join(', ')})`, + 'ядро приходит поддеревом — эти скрипты его затрут'); + else ok('в package.json нет хуков копирования ядра'); + } else { + if (/update\.sh/.test(s.update ?? '')) ok('скрипт update ведёт на scripts/update.sh'); + else err('frontend/package.json: скрипт "update" не зовёт scripts/update.sh'); - for (const hook of ['predev', 'prebuild']) { - if (/update/.test(s[hook] ?? '')) ok(`${hook} обновляет ядро`); - else err(`frontend/package.json: нет "${hook}": "npm run update"`, - 'без него dev и build идут на устаревшей копии ядра'); + for (const hook of ['predev', 'prebuild']) { + if (/update/.test(s[hook] ?? '')) ok(`${hook} обновляет ядро`); + else err(`frontend/package.json: нет "${hook}": "npm run update"`, + 'без него dev и build идут на устаревшей копии ядра'); + } } } @@ -330,7 +362,7 @@ if (!IS_TEMPLATE) { // ─── вывод ────────────────────────────────────────────────────────────────── const rel = path.relative(process.cwd(), PANEL) || '.'; -console.log(`doctor: ${rel}${IS_TEMPLATE ? ' (шаблон ma7engine)' : ''}\n`); +console.log(`doctor: ${rel}${IS_TEMPLATE ? ' (шаблон ma7engine)' : ` (ядро: ${MODE})`}\n`); for (const m of oks) console.log(` ok ${m}`); for (const m of skips) console.log(` — ${m}`); diff --git a/scripts/new-panel.mjs b/scripts/new-panel.mjs index 47ac599..41683ff 100755 --- a/scripts/new-panel.mjs +++ b/scripts/new-panel.mjs @@ -76,7 +76,15 @@ function edit(relPath, pairs) { fs.writeFileSync(file, text); } -edit('frontend/package.json', [['"ma7engine-example-frontend"', `"${slug}-frontend"`]]); +edit('frontend/package.json', [ + ['"ma7engine-example-frontend"', `"${slug}-frontend"`], + // В панели ядро приезжает через git subtree, а не копированием: хуки + // predev/prebuild и сам update.sh здесь не нужны и вредны — update.sh + // сносит каталог целиком (rm -rf) и затёр бы подтянутое поддерево. + [` "predev": "npm run update",\n`, ''], + [` "update": "sh scripts/update.sh",\n`, ''], + [` "prebuild": "npm run update",\n`, ''], +]); // Заголовок вкладки браузера — не то же, что branding.name в App.tsx: его видно // до того, как React смонтируется, и в закладках. edit('frontend/index.html', [['Demo Panel', `${display}`]]); @@ -193,6 +201,13 @@ for (const f of ['Dockerfile', '.dockerignore']) { fs.copyFileSync(path.join(ROOT, f), path.join(target, f)); } +// Доктор берётся из scripts/, а не тот, что мог приехать внутри frontend/: +// в шаблоне frontend/scripts/doctor.mjs — вендоренная копия, и она бывает +// старше исходника. Панель должна получить актуальные проверки. +fs.mkdirSync(path.join(target, 'frontend/scripts'), { recursive: true }); +fs.copyFileSync(path.join(ROOT, 'scripts/doctor.mjs'), + path.join(target, 'frontend/scripts/doctor.mjs')); + fs.mkdirSync(path.join(target, 'scripts'), { recursive: true }); const dev = fs.readFileSync(path.join(ROOT, 'scripts/demo.sh'), 'utf8') .replace('# Поднимает демо-панель целиком: бэкенд в фоне, Vite на переднем плане.', @@ -208,19 +223,15 @@ const dev = fs.readFileSync(path.join(ROOT, 'scripts/demo.sh'), 'utf8') fs.writeFileSync(path.join(target, 'scripts/dev.sh'), dev, { mode: 0o755 }); // ─── ядро ─────────────────────────────────────────────────────────────────── -// Копия ядра кладётся сразу: в панели она закоммиченный исходник, и панель -// должна собираться с первого коммита, а не после того, как кто-то вспомнит -// про `npm run update`. Путь к ядру известен — это мы сами. -try { - execFileSync('sh', [path.join(target, 'frontend/scripts/update.sh')], - { env: { ...process.env, MA7ENGINE: ROOT }, stdio: 'pipe' }); - console.log('new-panel: ядро скопировано в frontend/src/core и backend/core'); -} catch (e) { - console.error('new-panel: не удалось разложить ядро — запусти `npm run update` во фронте\n' + - String(e.stderr ?? e.message)); -} +// Ядро сюда не копируется: его приносит `git subtree add` — командами ниже. +// Копия и поддерево не уживаются, поэтому update.sh в панель не едет вовсе. +fs.rmSync(path.join(target, 'frontend/scripts/update.sh'), { force: true }); // ─── что дальше ───────────────────────────────────────────────────────────── +// Панель — надстройка: она обычно лежит подкаталогом продуктового репозитория, +// и prefix для subtree считается от его корня. Угадать корень отсюда нельзя, +// поэтому в подсказке путь даётся относительно каталога панели. +const prefix = `${path.basename(target)}/`; console.log(` new-panel: панель ${display} создана в ${target} @@ -232,5 +243,12 @@ new-panel: панель ${display} создана в ${target} Пароль и секрет обязательны — без них ядро не даст процессу подняться: ADMIN_PASS=... ADMIN_JWT_SECRET=... + Ядро приносит git subtree — из каталога панели в её репозитории: + git subtree add --prefix=${prefix}frontend/src/core ${ROOT} dist/browser --squash + git subtree add --prefix=${prefix}backend/core ${ROOT} dist/server --squash + + Дальше обновляется тем же git subtree pull. Копировать ядро руками не нужно — + и не стоит: поддерево умеет merge и покажет конфликт, а копия молча затрёт. + Проверить, что панель собрана по стандарту: cd ${target}/frontend && npm run doctor`); diff --git a/scripts/release.sh b/scripts/release.sh index 33fc1a6..d02c818 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -31,9 +31,14 @@ if [ -n "$(git status --porcelain)" ]; then exit 1 fi +# `git subtree split -b <ветка>` умеет только создать ветку и отказывается +# двигать существующую, если новый коммит ей не потомок, — а после любого +# reset/rebase в ядре это ровно так. Поэтому split считает коммит, а ветка +# переставляется принудительно: ветки производные, своей истории у них нет. split() { # split <каталог> <ветка> printf 'release: %s → %s\n' "$1" "$2" - git subtree split --prefix="$1" -b "$2" -q + rev=$(git subtree split --prefix="$1" -q) + git branch -f "$2" "$rev" printf ' %s\n' "$(git log -1 --format='%h %s' "$2")" }