Regression / regression (push) Canceled after 0s
## 品牌标识(本次会话) 起因:品牌此前没有任何图形标识 —— 唯一 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 未做部分)
125 lines
5.6 KiB
JavaScript
125 lines
5.6 KiB
JavaScript
/* 发布集(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 };
|
||
}
|