Files
aurora-admin/tools/lib/publish-set.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

125 lines
5.6 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.
/* 发布集(publish set)—— 「哪些文件会随部署出去」的唯一实现。
*
* 为什么抽成模块:这份清单有两个消费者,必须同源,否则又会出现"打包器说发了、核对脚本说没发":
* 1. tools/pack-deploy.mjs —— 生成 dist-deploy/ 暂存目录与 tar.gz;
* 2. tools/verify-deploy-consistency.mjs —— 拿它逐字节核对线上快照与工作树。
* 规则真源仍然是 `.dockerignore`(Docker 匹配语义,见下方注释),本模块只解析它 + 枚举目录,
* 不维护第二份排除清单。NEVER 是打包器自身的边界(版本库 / 依赖 / 本地产物),与 .dockerignore 无关。
*/
import fs from 'node:fs';
import path from 'node:path';
/* 部署目录必需文件:与 Dockerfile 的 COPY 对应,且必须留在 /opt 供 docker compose 使用。
注意:它们**始终**随包发布,不受 .dockerignore 影响 —— .dockerignore 只决定
「哪些文件进入镜像构建上下文」,而 docker-compose.yml 这类文件必须留在部署目录里
(compose 从磁盘读取它,不从构建上下文读取)。 */
export const TOP_FILES = ['Dockerfile', '.dockerignore', 'nginx.conf', 'docker-compose.yml', 'sitemap.xml', 'versions.json'];
/* 受 .dockerignore 约束的内容目录(镜像里真正要用的资源) */
export const CONTENT_DIRS = [
'site',
'frameworks',
/* 移动端平台实现 + PC×uni-app 端实现(两轴平台/端模型见 PLATFORMS.md) */
'frameworks-mobile',
'frameworks-uniapp-pc',
'tests',
'.design_library',
];
/* 永不打包:版本库、依赖、本地工具与产物(与 .dockerignore 无关,属于打包器自身边界) */
export const NEVER = [/^\.git(\/|$)/, /^node_modules(\/|$)/, /^\.zcode(\/|$)/, /^\.playwright-mcp(\/|$)/,
/^dist(-deploy)?(\/|$)/, /^\.gitignore$/];
/* 不经过 HTTP 暴露的发布文件:它们是构建/运维配置,nginx 的 web 根里没有。
逐字节核对线上时跳过(核对脚本用;pack-deploy 不关心)。 */
export const NOT_WEB_SERVED = ['Dockerfile', '.dockerignore', 'nginx.conf', 'docker-compose.yml', 'index.html'];
/* ---------- Docker .dockerignore 语义 ----------
* 实测确认:不含 "/" 的模式(如 *.md)只匹配**构建上下文根目录**,不匹配嵌套路径;
* 故一律按「完整相对路径」匹配,其中 * 不跨 "/",** 可跨任意层(含零层)。
* 命中某目录即视为命中其下全部内容。 */
export function patternToRegExp(pat) {
let re = '';
for (let i = 0; i < pat.length; i++) {
const c = pat[i];
if (c === '*') {
if (pat[i + 1] === '*') {
i++;
if (pat[i + 1] === '/') { i++; re += '(?:[^/]*/)*'; }
else re += '.*';
} else re += '[^/]*';
} else if (c === '?') re += '[^/]';
else re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
}
return new RegExp('^' + re + '$');
}
export function loadDockerignore(root) {
const file = path.join(root, '.dockerignore');
if (!fs.existsSync(file)) throw new Error('.dockerignore 不存在——排除规则无法确定,拒绝打包');
const rules = [];
for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
const line = raw.trim();
if (!line || line.startsWith('#')) continue;
const negated = line.startsWith('!');
const body = (negated ? line.slice(1) : line).replace(/^\.\//, '').replace(/\/+$/, '');
if (!body) continue;
rules.push({ negated, re: patternToRegExp(body) });
}
return rules;
}
export function isIgnored(rules, rel) {
const segments = rel.split('/');
let excluded = false;
for (const rule of rules) {
let hit = rule.re.test(rel);
if (!hit) {
for (let i = 1; i < segments.length && !hit; i++) hit = rule.re.test(segments.slice(0, i).join('/'));
}
if (hit) excluded = !rule.negated;
}
return excluded;
}
export function walkTree(absDir, relPrefix, out) {
for (const e of fs.readdirSync(absDir, { withFileTypes: true })) {
const rel = relPrefix ? relPrefix + '/' + e.name : e.name;
if (NEVER.some((re) => re.test(rel))) continue;
if (e.isDirectory()) walkTree(path.join(absDir, e.name), rel, out);
else out.push(rel);
}
}
/* 历史版本文档站快照(tools/snapshot-site.mjs 产出):仓库根的 <x.y.z>/ 目录。
没有快照时这里为空数组,部署包与从前一致(版本菜单此时只有当前版本)。 */
export function versionSnapshotDirs(root) {
return fs.readdirSync(root, { withFileTypes: true })
.filter((e) => e.isDirectory() && /^\d+\.\d+\.\d+$/.test(e.name))
.filter((e) => fs.existsSync(path.join(root, e.name, 'site', 'index.html')))
.map((e) => e.name)
.sort();
}
/**
* 采集发布集。
* @returns {{ kept: string[], candidates: string[], excludedCount: number, versionDirs: string[] }}
* 缺目录 / 缺必需文件 / 缺 .dockerignore 时抛错(调用方按环境问题处理)。
*/
export function collectPublishSet(root) {
const rules = loadDockerignore(root);
const candidates = [];
for (const top of CONTENT_DIRS) {
const abs = path.join(root, top);
if (!fs.existsSync(abs)) throw new Error('缺少部署必需目录:' + top);
walkTree(abs, top, candidates);
}
const versionDirs = versionSnapshotDirs(root);
for (const dir of versionDirs) walkTree(path.join(root, dir), dir, candidates);
for (const f of TOP_FILES) {
if (!fs.existsSync(path.join(root, f))) throw new Error('缺少部署必需文件:' + f);
}
const kept = [...TOP_FILES, ...candidates.filter((f) => !isIgnored(rules, f))].sort();
return { kept, candidates, excludedCount: candidates.length - (kept.length - TOP_FILES.length), versionDirs };
}