Files
aurora-admin/tools/snapshot-site.mjs
T
aurora-admin f1fbfc2ddb
Regression / regression (push) Canceled after 0s
feat(品牌标识): 几何 K 图标(favicon/顶栏标记/theme-color) + 并行会话成果入库
## 品牌标识(本次会话)

起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」,
那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」,
移动端站已是「K」;移动端文档站则完全没有 favicon)。

- 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边;
  描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」
- 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark);
  顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28))
- 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg)
- 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留)
- 移动端 7 个模板补 favicon(此前计数 0)

验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/
theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致;
两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。

## 并行会话成果(本次一并入库)

- 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具
  + IconPreview 预览页 + ICON-SPEC.md 冻结规格
- 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁
- PC 组件:103 个大后台组件 / 组件11 批次
- uni-app:PC 端试点 + 移动端端实现 + 真实编译验证

## 工程

- .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库)
- CHANGELOG 补品牌标识条目
- ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
2026-09-21 10:05:48 +08:00

151 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* snapshot-site.mjs — 把当前构建归档成一个可切换的历史版本文档站。
*
* 产物布局(站点根必须继续叫 site:SITE_BASE 取 pathname 里第一段 '/site/',
* 改名即全站 CSS/data.js 404,见 site/app.js 顶部注释):
*
* /site/ ← 最新版(根站点,永远指当前构建)
* /1.4.1/site/ ← 归档快照(磁盘上就是仓库根的 1.4.1/)
* /1.4.1/frameworks/ /1.4.1/.design_library/ ← 该版本的演示页与令牌依赖
* /1.4.1/index.html /1.4.1/sitemap.xml
*
* 站点根是按 URL 反推的,所以 /1.4.1/site/... 下相对路径(../.design_library、sources/…)
* 全部原样成立,快照内不需要改任何路径。切换入口见 site/app.js 的 loadVersionManifest,
* 清单由 tools/precompute.mjs 生成(只有磁盘上真存在快照的版本才会入清单)。
*
* 用法:
* npm run snapshot:site -- 1.4.1 # 需要构建产物已就绪
* npm run snapshot:site -- 1.4.1 --build # 先跑 build-site.ps1 + precompute 再归档
*
* 注意:归档的是**当前工作区**的构建。仓库里没有历史源码快照、也没有 git tag,
* 所以旧版本无法凭空重建 —— 每次发版后归档一次,才有可切换的历史版本。
*/
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, readFileSync, writeFileSync, statSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { dirname, join, basename } from 'node:path';
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
const SITE_DIR = join(ROOT, 'site');
const argv = process.argv.slice(2);
const flags = new Set(argv.filter((a) => a.startsWith('--')));
const version = argv.find((a) => !a.startsWith('--'));
const fail = (msg) => { console.error('[snapshot] FAIL: ' + msg); process.exit(1); };
const ok = (msg) => console.log('[snapshot] OK: ' + msg);
if (!version) fail('缺少版本号。用法:npm run snapshot:site -- <x.y.z> [--build]');
if (!/^\d+\.\d+\.\d+$/.test(version)) fail(`版本号必须是 x.y.z(收到 ${version})`);
/* 版本号必须与 CHANGELOG / 构建版本对得上,否则清单里会出现对不上的条目 */
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
const changelog = existsSync(join(ROOT, 'CHANGELOG.md')) ? readFileSync(join(ROOT, 'CHANGELOG.md'), 'utf8') : '';
const inChangelog = new RegExp('^## \\[' + version.replace(/\./g, '\\.') + '\\]', 'm').test(changelog);
if (!inChangelog) fail(`CHANGELOG.md 里没有 [${version}] 段 —— 先写变更条目(tools/release.mjs --bump 可提升版本)`);
if (pkg.version !== version && !flags.has('--force-version')) {
fail(`package.json 是 ${pkg.version},与归档版本 ${version} 不一致`);
}
if (flags.has('--build')) {
console.log('[snapshot] 构建站点(build-site.ps1 + precompute)…');
if (process.platform !== 'win32') fail('--build 只能在 Windows 上跑:build-site.ps1 依赖 PowerShell 5.1');
execFileSync('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', 'build-site.ps1'], { cwd: ROOT, stdio: 'inherit' });
execFileSync(process.execPath, [join(ROOT, 'tools', 'precompute.mjs')], { cwd: ROOT, stdio: 'inherit' });
}
for (const f of ['index.html', 'app.js', 'data.js', 'data.json', 'style.css']) {
if (!existsSync(join(SITE_DIR, f))) fail(`缺少构建产物 site/${f} —— 先运行 npm run build:site`);
}
const data = JSON.parse(readFileSync(join(SITE_DIR, 'data.json'), 'utf8'));
const built = String((data.meta && data.meta.version) || '');
/* 正常情况下归档的就是本次构建的版本。--force-version 只用于补档/演练:
历史版本的源码不在仓库里(没有 git tag、没有旧快照),拿当前构建冒充旧版
只适合验证机制,不适合当正式历史版本发布。 */
if (built !== version && !flags.has('--force-version')) {
fail(`构建产物版本是 ${built || '未知'},与归档版本 ${version} 不一致 —— 先 npm run build:site(或加 --build)`);
}
if (built !== version) {
console.warn(`[snapshot] 警告:构建产物是 ${built},却按 ${version} 归档(--force-version)。`);
console.warn('[snapshot] 该快照内容是当前构建,仅用于验证机制,不要当作正式历史版本发布。');
}
/* 快照落在**仓库根**:URL 是 /<x.y.z>/site/…,服务器把 / 映射到仓库根(dev-server 的
ROOT、nginx 的 root),所以磁盘上必须是 <root>/<x.y.z>/site/。
放进 site/ 里会变成 /site/<x.y.z>/… —— 版本菜单点进去就是 404。 */
const dest = join(ROOT, version);
/* 归档 site/ 时排除三类:自身、其它版本目录(否则会把上次快照套进这次),
以及 versions.json —— 版本清单必须只留在站点根:快照自带的那份是归档当时的快照,
之后新归档的版本不会进去,实测会让快照页误判成单版本、角标不可点(回不到最新版)。 */
const skipSelf = (src) => {
const rel = src.slice(SITE_DIR.length + 1).replace(/\\/g, '/');
return rel === version
|| rel === 'versions.json'
|| /^\d+\.\d+\.\d+(?:\/|$)/.test(rel);
};
if (existsSync(dest)) {
if (!flags.has('--force')) {
fail(`${basename(dest)}/ 已存在。确认要覆盖就加 --force(会先删除该目录重建)`);
}
rmSync(dest, { recursive: true, force: true });
console.log(`[snapshot] 已删除旧快照 ${version}/`);
}
mkdirSync(dest, { recursive: true });
/* cpSync 拒绝把目录拷进自己的子目录(Node ≥16.7 的 ERR_FS_CP_EINVAL),
且 site/ 与 dest 是兄弟目录、跨目录搬移可能跨盘 —— 先落到暂存再搬进 dest。 */
const stage = join(ROOT, 'dist-deploy', `.snapshot-stage-${version}`);
rmSync(stage, { recursive: true, force: true });
mkdirSync(stage, { recursive: true });
try {
cpSync(SITE_DIR, join(stage, 'site'), { recursive: true, filter: (src) => !skipSelf(src) });
cpSync(join(ROOT, 'frameworks'), join(stage, 'frameworks'), { recursive: true });
cpSync(join(ROOT, '.design_library'), join(stage, '.design_library'), {
recursive: true,
/* 规范原文与 agent 报告不进快照:站点不读它们,且是仓库内部资料(部署包同样排除) */
filter: (src) => !/(?:^|[\\/])(specs|agent-reports|preview|ui_kits)(?:[\\/]|$)/.test(src)
});
cpSync(join(ROOT, 'index.html'), join(stage, 'index.html'));
if (existsSync(join(ROOT, 'sitemap.xml'))) cpSync(join(ROOT, 'sitemap.xml'), join(stage, 'sitemap.xml'));
for (const entry of readdirSync(stage)) {
cpSync(join(stage, entry), join(dest, entry), { recursive: true });
}
} finally {
rmSync(stage, { recursive: true, force: true });
}
/* 归档标记:给后续版本核对「这份快照到底是哪个版本、什么时候归档的」 */
const now = new Date();
const localDay = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
writeFileSync(
join(dest, 'VERSION.json'),
JSON.stringify({
version,
archivedAt: localDay,
source: 'tools/snapshot-site.mjs',
note: 'archive of the working-tree build at the time of release; paths inside are unmodified'
}, null, 2) + '\n'
);
const size = (p) => {
let total = 0;
for (const e of readdirSync(p, { withFileTypes: true })) {
const f = join(p, e.name);
total += e.isDirectory() ? size(f) : statSync(f).size;
}
return total;
};
const mb = (size(dest) / 1024 / 1024).toFixed(1);
ok(`已归档 v${version} → ${version}/(${mb} MB,URL /${version}/site/)`);
/* 清单必须跟着更新,否则版本菜单里看不到刚归档的版本 */
execFileSync(process.execPath, [join(ROOT, 'tools', 'precompute.mjs')], { cwd: ROOT, stdio: 'inherit' });
const manifest = JSON.parse(readFileSync(join(SITE_DIR, 'versions.json'), 'utf8'));
ok(`site/versions.json 现有 ${manifest.versions.length} 个在线版本:${manifest.versions.map((v) => 'v' + v.version).join(' / ')}`);
console.log('[snapshot] 下一步:node site/dev-server.js 后打开 http://127.0.0.1:3311/site/ 实测切换;');
console.log('[snapshot] 部署走 tools/pack-deploy.mjs(site/ 目录整体打包,快照随包上线)。');