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 未做部分)
245 lines
9.4 KiB
JavaScript
245 lines
9.4 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* verify-uniapp.mjs — uni-app 端的静态门禁(移动端 × uni-app + PC 端 × uni-app)
|
||
*
|
||
* 为什么是静态门禁而不是编译验证:
|
||
* uni-app 的真实编译需要 @dcloudio/vite-plugin-uni(npm 依赖),本仓库零运行时依赖,
|
||
* 且 CI 不装 uni-app 工具链。因此本脚本做**能在零依赖下做到的最强静态检查**:
|
||
* 1. SFC 三段结构完整(template / script / style)
|
||
* 2. <script> 段用 `node --check` 做真实语法校验(ESM 口径)
|
||
* 3. <template> 段做标签配平检查(识别自闭合)
|
||
* 4. 禁 DOM / 浏览器 API(document / window / localStorage / querySelector / PointerEvent)
|
||
* 5. 手势必须用 touch 事件(小程序与 App 端无 PointerEvent)
|
||
* 6. 只用 uni 基础组件(view / text / input / textarea / scroll-view / image …)
|
||
* 7. 类名与令牌前缀按平台隔离(移动端 kole-m- + --kole-m-;PC 端 kole- + --kole-)
|
||
* 8. 移动端用 rpx、PC 端不得用 rpx
|
||
*
|
||
* **未执行**:uni-app 真实编译(H5/小程序/App 三目标构建)。
|
||
* 复现命令(需联网装依赖,写在 ROADMAP S7-P25 的验收步骤里):
|
||
* npx degit dcloudio/uni-preset-vue#vite my-app && cd my-app && npm i \
|
||
* && cp -r <repo>/dist/mobile/uniapp node_modules/kole-ui-mobile/uniapp \
|
||
* && npm run build:h5
|
||
*
|
||
* 零依赖;退出码 0 = 全部通过。
|
||
*/
|
||
import { readFileSync, writeFileSync, mkdtempSync, existsSync } from 'node:fs';
|
||
import { execFileSync } from 'node:child_process';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, join } from 'node:path';
|
||
import { tmpdir } from 'node:os';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = join(__dirname, '..');
|
||
const read = (p) => readFileSync(p, 'utf8').replace(/^\uFEFF/, '');
|
||
|
||
let pass = 0;
|
||
const failures = [];
|
||
function check(ok, id, detail) {
|
||
if (ok) {
|
||
pass++;
|
||
console.log(` PASS ${id}${detail ? ' — ' + detail : ''}`);
|
||
} else {
|
||
failures.push(id + (detail ? ' — ' + detail : ''));
|
||
console.log(` FAIL ${id}${detail ? ' — ' + detail : ''}`);
|
||
}
|
||
}
|
||
|
||
/* ---------- 待检清单 ---------- */
|
||
|
||
const targets = [];
|
||
|
||
const mbIndex = JSON.parse(read(join(ROOT, '.design_library', 'kole-ui-mobile', 'components', 'index.json')));
|
||
for (const c of mbIndex.components) {
|
||
const f = c.files && c.files.uniapp;
|
||
if (f) {
|
||
targets.push({
|
||
platform: 'mobile',
|
||
slug: c.slug,
|
||
path: join(ROOT, 'frameworks-mobile', f),
|
||
rel: `frameworks-mobile/${f}`,
|
||
targets: mbIndex.uniappTargets || [],
|
||
});
|
||
}
|
||
}
|
||
|
||
const pcIndexPath = join(ROOT, '.design_library', 'kole-ui-uniapp', 'index.json');
|
||
if (existsSync(pcIndexPath)) {
|
||
const ui = JSON.parse(read(pcIndexPath));
|
||
for (const c of ui.components) {
|
||
targets.push({
|
||
platform: 'pc',
|
||
slug: c.slug,
|
||
path: join(ROOT, 'frameworks-uniapp-pc', c.file),
|
||
rel: `frameworks-uniapp-pc/${c.file}`,
|
||
targets: ['h5'],
|
||
});
|
||
}
|
||
}
|
||
|
||
console.log('uni-app 端静态门禁');
|
||
console.log(`待检 SFC ${targets.length} 个(移动端 ${targets.filter((t) => t.platform === 'mobile').length} · PC ${targets.filter((t) => t.platform === 'pc').length})`);
|
||
|
||
const TMP = mkdtempSync(join(tmpdir(), 'kole-uniapp-'));
|
||
const UNI_PRIMITIVES = new Set([
|
||
'template', 'view', 'text', 'image', 'input', 'textarea', 'button', 'scroll-view', 'swiper', 'swiper-item',
|
||
'navigator', 'slot', 'checkbox', 'radio', 'switch', 'picker', 'progress', 'rich-text', 'cover-view',
|
||
]);
|
||
|
||
/* 注释剥离:DOM/单位这类检查必须只看**代码**,否则「注释里解释为什么不用 PointerEvent」
|
||
会被判成违规。行注释只剥离「行首(可含缩进)的 //」,避免误伤 https:// 这类值。 */
|
||
function stripComments(src) {
|
||
return src
|
||
.replace(/<!--[\s\S]*?-->/g, ' ')
|
||
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
||
.replace(/^[ \t]*\/\/.*$/gm, ' ');
|
||
}
|
||
|
||
/* ---------- 逐文件检查 ---------- */
|
||
|
||
const problems = {
|
||
exist: [],
|
||
sections: [],
|
||
syntax: [],
|
||
tags: [],
|
||
dom: [],
|
||
gesture: [],
|
||
primitives: [],
|
||
prefix: [],
|
||
units: [],
|
||
};
|
||
|
||
for (const t of targets) {
|
||
if (!existsSync(t.path)) {
|
||
problems.exist.push(t.rel);
|
||
continue;
|
||
}
|
||
const src = read(t.path);
|
||
|
||
/* 1. SFC 三段 */
|
||
const hasTpl = /<template>/.test(src);
|
||
const hasScript = /<script(\s|>)/.test(src);
|
||
const hasStyle = /<style(\s|>)/.test(src);
|
||
if (!hasTpl || !hasScript || !hasStyle) {
|
||
problems.sections.push(`${t.rel}(template=${hasTpl} script=${hasScript} style=${hasStyle})`);
|
||
}
|
||
|
||
/* 2. script 语法(真实 node --check) */
|
||
const scriptMatch = src.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
||
if (scriptMatch) {
|
||
const tmpFile = join(TMP, `${t.platform}-${t.slug}.mjs`);
|
||
writeFileSync(tmpFile, scriptMatch[1], 'utf8');
|
||
try {
|
||
execFileSync(process.execPath, ['--check', tmpFile], { stdio: 'pipe' });
|
||
} catch (e) {
|
||
problems.syntax.push(`${t.rel} → ${String(e.stderr || e.message).split('\n').slice(0, 3).join(' / ')}`);
|
||
}
|
||
}
|
||
|
||
/* 3. template 标签配平 */
|
||
const tplMatch = src.match(/<template>([\s\S]*)<\/template>/);
|
||
if (tplMatch) {
|
||
const stack = [];
|
||
const tagRe = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)((?:"[^"]*"|'[^']*'|[^>"'])*?)(\/?)>/g;
|
||
let m;
|
||
let bad = null;
|
||
while ((m = tagRe.exec(tplMatch[1]))) {
|
||
const closing = m[1] === '/';
|
||
const name = m[2];
|
||
const selfClose = m[4] === '/';
|
||
if (closing) {
|
||
const top = stack.pop();
|
||
if (top !== name) {
|
||
bad = `</${name}> 与 <${top || '空'}> 不匹配`;
|
||
break;
|
||
}
|
||
} else if (!selfClose) {
|
||
stack.push(name);
|
||
}
|
||
}
|
||
if (!bad && stack.length) bad = `未闭合:${stack.join(', ')}`;
|
||
if (bad) problems.tags.push(`${t.rel} → ${bad}`);
|
||
}
|
||
|
||
/* 4. DOM / 浏览器 API(只看代码,注释不计) */
|
||
const code = stripComments(src);
|
||
const DOM_PATTERNS = [
|
||
/\bdocument\./,
|
||
/\bwindow\./,
|
||
/\blocalStorage\b/,
|
||
/\bquerySelector(All)?\b/,
|
||
/\bnew PointerEvent\b/,
|
||
/\baddEventListener\(\s*['"]pointer/,
|
||
/\bgetComputedStyle\b/,
|
||
/\bgetBoundingClientRect\b/,
|
||
];
|
||
DOM_PATTERNS.forEach((re) => {
|
||
if (re.test(code)) problems.dom.push(`${t.rel} → ${re}`);
|
||
});
|
||
|
||
/* 5. 手势必须 touch 事件 */
|
||
const usesPointer = /@pointer(down|move|up|cancel)/.test(code);
|
||
if (usesPointer) problems.gesture.push(`${t.rel} → 使用了 @pointer* 事件(小程序/App 端无 PointerEvent)`);
|
||
const isGesture = /@touch(move|start|end)/.test(code);
|
||
if (isGesture && !/@touch(start|move|end)/.test(code)) {
|
||
problems.gesture.push(`${t.rel} → 手势事件不完整`);
|
||
}
|
||
|
||
/* 6. 只用 uni 基础组件 */
|
||
if (tplMatch) {
|
||
const tags = new Set();
|
||
const re = /<([a-zA-Z][a-zA-Z0-9-]*)/g;
|
||
let m2;
|
||
while ((m2 = re.exec(tplMatch[1]))) tags.add(m2[1]);
|
||
tags.forEach((tag) => {
|
||
if (!UNI_PRIMITIVES.has(tag) && !/^[A-Z]/.test(tag) && !tag.includes('-')) {
|
||
problems.primitives.push(`${t.rel} → <${tag}>`);
|
||
}
|
||
});
|
||
}
|
||
|
||
/* 7. 类名 / 令牌前缀按平台 */
|
||
const cls = src.match(/\.kole-[a-zA-Z0-9_-]+/g) || [];
|
||
if (t.platform === 'mobile') {
|
||
cls.forEach((c) => {
|
||
if (!c.slice(1).startsWith('kole-m-')) problems.prefix.push(`${t.rel} → ${c}`);
|
||
});
|
||
if (!src.includes('--kole-m-')) problems.prefix.push(`${t.rel} → 未使用 --kole-m- 令牌`);
|
||
} else {
|
||
cls.forEach((c) => {
|
||
if (c.slice(1).startsWith('kole-m-')) problems.prefix.push(`${t.rel} → PC 端不得用移动端类 ${c}`);
|
||
});
|
||
}
|
||
|
||
/* 8. 单位策略(rpx 检查只看代码:注释里提到 rpx 不算数) */
|
||
if (t.platform === 'mobile' && !/[0-9]rpx\b/.test(code)) {
|
||
problems.units.push(`${t.rel} → 移动端 uni-app 未使用 rpx`);
|
||
}
|
||
if (t.platform === 'pc' && /[0-9]rpx\b/.test(code)) {
|
||
problems.units.push(`${t.rel} → PC 端不得使用 rpx`);
|
||
}
|
||
}
|
||
|
||
/* ---------- 汇总断言 ---------- */
|
||
|
||
console.log('');
|
||
check(problems.exist.length === 0, `U1 ${targets.length} 个 SFC 文件齐全`, problems.exist.join(', ') || null);
|
||
check(problems.sections.length === 0, 'U2 SFC 三段结构完整', problems.sections.join('; ') || null);
|
||
check(problems.syntax.length === 0, 'U3 <script> 段语法通过 node --check', problems.syntax.join('; ') || null);
|
||
check(problems.tags.length === 0, 'U4 <template> 标签配平', problems.tags.join('; ') || null);
|
||
check(problems.dom.length === 0, 'U5 无 DOM / 浏览器 API 依赖', problems.dom.slice(0, 5).join('; ') || null);
|
||
check(problems.gesture.length === 0, 'U6 手势只用 touch 事件', problems.gesture.join('; ') || null);
|
||
check(problems.primitives.length === 0, 'U7 只用 uni 基础组件', problems.primitives.slice(0, 5).join('; ') || null);
|
||
check(problems.prefix.length === 0, 'U8 类名与令牌前缀按平台隔离', problems.prefix.slice(0, 5).join('; ') || null);
|
||
check(problems.units.length === 0, 'U9 单位策略(移动 rpx / PC px)', problems.units.join('; ') || null);
|
||
|
||
console.log('');
|
||
console.log('未执行:uni-app 真实编译(H5 / 小程序 / App 三目标)—— 需 npm 依赖,见本文件头部复现命令');
|
||
|
||
console.log('\n────────────────────────────');
|
||
if (failures.length) {
|
||
console.error(`[FAIL] ${failures.length} 条 uni-app 断言失败(通过 ${pass} 条):`);
|
||
failures.forEach((f) => console.error(' - ' + f));
|
||
process.exit(1);
|
||
}
|
||
console.log(`[OK] uni-app 门禁全部通过(${pass} 条断言 · ${targets.length} 个 SFC)`);
|