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 未做部分)
409 lines
18 KiB
JavaScript
409 lines
18 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* verify-mobile-isolation.mjs — 移动端 × PC 端隔离硬门禁
|
||
*
|
||
* 三组断言(任一条失败即 exit 1):
|
||
* A. PC 侧零污染:移动端的东西没有渗进 PC 的任何对外面(目录 / 数据 / 测试 / 分发)
|
||
* B. 移动端自洽:索引声明的 6 端文件齐全、契约字段完整、类名与令牌前缀合规
|
||
* C. 分发隔离:PC 聚合样式无移动端类,移动端产物无 PC 组件类,package.json exports 两套并存
|
||
*
|
||
* 为什么需要这个脚本:本次改动新增了一整个平台(移动端)。历史教训(AGENTS.md §八)
|
||
* 有 6 次「文档声称完成但代码缺失」——隔离这种"没发生的事"必须靠断言证明,
|
||
* 不能靠人眼扫一遍说"看起来没影响"。
|
||
*
|
||
* 零依赖;退出码 0 = 全部通过。
|
||
*/
|
||
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { dirname, join, relative, sep } from 'node:path';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = join(__dirname, '..');
|
||
|
||
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 : ''}`);
|
||
}
|
||
}
|
||
function section(title) {
|
||
console.log('\n' + title);
|
||
}
|
||
|
||
const rel = (p) => relative(ROOT, p).split(sep).join('/');
|
||
const read = (p) => readFileSync(p, 'utf8').replace(/^\uFEFF/, '');
|
||
|
||
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist-deploy', '.playwright-mcp', '.zcode']);
|
||
|
||
/** 递归收集文件(相对仓库根 POSIX 路径) */
|
||
function walk(dir, out = []) {
|
||
if (!existsSync(dir)) return out;
|
||
for (const name of readdirSync(dir)) {
|
||
if (SKIP_DIRS.has(name)) continue;
|
||
const p = join(dir, name);
|
||
const st = statSync(p);
|
||
if (st.isDirectory()) walk(p, out);
|
||
else out.push(p);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function countFilesMatching(re, files = walk(ROOT)) {
|
||
return files.filter((f) => re.test(rel(f)));
|
||
}
|
||
|
||
function readIf(p) {
|
||
return existsSync(p) ? read(p) : null;
|
||
}
|
||
|
||
/* ---------- 基准数据 ---------- */
|
||
|
||
const PC_INDEX = join(ROOT, '.design_library', 'kole-ui', 'components', 'index.json');
|
||
const MB_INDEX = join(ROOT, '.design_library', 'kole-ui-mobile', 'components', 'index.json');
|
||
|
||
const pcIndex = JSON.parse(read(PC_INDEX));
|
||
const mbIndex = JSON.parse(read(MB_INDEX));
|
||
const pcCount = pcIndex.components.length;
|
||
const pcSlugs = new Set(pcIndex.components.map((c) => c.slug));
|
||
const mbSlugs = mbIndex.components.map((c) => c.slug);
|
||
|
||
const allFiles = walk(ROOT);
|
||
|
||
console.log('移动端 × PC 端隔离门禁');
|
||
console.log(`仓库:${ROOT}`);
|
||
console.log(`PC 组件 ${pcIndex.components.length} 个 | 移动端组件 ${mbIndex.components.length} 个 × ${mbIndex.ends.length} 端`);
|
||
|
||
/* ============ A. PC 侧零污染 ============ */
|
||
|
||
section('A · PC 侧零污染');
|
||
|
||
const PC_SURFACES = [
|
||
'frameworks/',
|
||
'site/app.js',
|
||
'site/index.html',
|
||
'site/style.css',
|
||
'site/data.json',
|
||
'site/playground.js',
|
||
'site/components/',
|
||
'.design_library/kole-ui/',
|
||
'tests/_runtime.js',
|
||
'tests/_behaviors.js',
|
||
'tests/index.html',
|
||
'tools/build-dist.mjs',
|
||
'tools/precompute.mjs',
|
||
];
|
||
|
||
const pcSurfaceFiles = allFiles.filter((f) => {
|
||
const r = rel(f);
|
||
return PC_SURFACES.some((s) => (s.endsWith('/') ? r.startsWith(s) : r === s));
|
||
});
|
||
|
||
/* A1:PC 实现文件数 = 索引进度 × 5(数量由索引推导,不写死 —— 组件数增长是正常演进,
|
||
门禁要卡的是「每个组件 5 端齐全」与「没有移动端文件混进来」,不是某个历史数字) */
|
||
const fwFiles = countFilesMatching(/^frameworks\/.+/);
|
||
check(fwFiles.length === pcCount * 5, `A1 frameworks/ = 索引组件数 × 5(${pcCount * 5})`, `实际 ${fwFiles.length}`);
|
||
|
||
/* A2:PC 索引自洽(slug 唯一 + 每个组件都有 frameworksPrefix),且不低于 79 基线 */
|
||
check(
|
||
pcSlugs.size === pcIndex.components.length && pcIndex.components.every((c) => !!c.frameworksPrefix),
|
||
'A2 PC 索引自洽(slug 唯一 / 每项有 frameworksPrefix)',
|
||
`${pcIndex.components.length} 个组件`
|
||
);
|
||
check(pcIndex.components.length >= 79, 'A2b PC 组件数未低于基线 79', `实际 ${pcIndex.components.length}`);
|
||
|
||
/* A3:PC 契约目录 = 契约数 + index.json */
|
||
const pcContracts = countFilesMatching(/^\.design_library\/kole-ui\/components\/.+\.json$/);
|
||
check(pcContracts.length === pcCount + 1, `A3 PC 契约目录 = ${pcCount} 契约 + index.json`, `实际 ${pcContracts.length}`);
|
||
|
||
/* A4:PC 对外数据不含任何移动端痕迹 */
|
||
const dataJson = readIf(join(ROOT, 'site', 'data.json'));
|
||
const dataJs = readIf(join(ROOT, 'site', 'data.js'));
|
||
check(!!dataJson && dataJson.indexOf('kole-m-') < 0, 'A4a site/data.json 无 kole-m- 痕迹');
|
||
check(!!dataJs && dataJs.indexOf('kole-m-') < 0, 'A4b site/data.js 无 kole-m- 痕迹');
|
||
if (dataJson) {
|
||
const parsed = JSON.parse(dataJson);
|
||
check(
|
||
(parsed.components || []).length === pcCount,
|
||
`A4c site/data.json 组件数与索引一致(${pcCount})`,
|
||
`实际 ${(parsed.components || []).length}`
|
||
);
|
||
}
|
||
const dataMobileJson = readIf(join(ROOT, 'site', 'm', 'data.mobile.json'));
|
||
check(!!dataMobileJson, 'A4d site/m/data.mobile.json 存在(移动端自包含数据)');
|
||
|
||
/* A5:PC 的**实现与数据面**零 `kole-m-` 命中。
|
||
判据收窄说明(2026-09-20):PC 侧的**跨平台说明文档**(如 `.design_library/kole-ui/icons/ICON-SPEC.md`)
|
||
会提到移动端令牌(「移动端 --kole-m-icon-size 可被业务覆盖」)—— 那是文档在描述另一端,不是越界实现。
|
||
故这里跳过 `.md` / `.txt` 这类说明性文件,只对实现(css/js/jsx/vue/html/json)与数据面判命中。
|
||
越界实现仍被 A1/A2/A3/A4/A6 与命名空间断言拦住。 */
|
||
const pcImplFiles = pcSurfaceFiles.filter((f) => /\.(css|js|jsx|vue|html|json)$/i.test(rel(f)));
|
||
const mbHits = pcImplFiles.filter((f) => {
|
||
try {
|
||
return read(f).indexOf('kole-m-') >= 0;
|
||
} catch {
|
||
return false;
|
||
}
|
||
});
|
||
check(
|
||
mbHits.length === 0,
|
||
'A5 PC 实现与数据面零 kole-m- 命中',
|
||
mbHits.length ? mbHits.map(rel).join(', ') : `扫描 ${pcImplFiles.length} 个文件(说明性 .md/.txt 不计)`
|
||
);
|
||
|
||
/* A6:PC 面不得引用移动端目录 */
|
||
const crossRefs = pcSurfaceFiles.filter((f) => {
|
||
try {
|
||
const t = read(f);
|
||
return t.includes('frameworks-mobile/') || t.includes('site/m/') || t.includes('kole-ui-mobile');
|
||
} catch {
|
||
return false;
|
||
}
|
||
});
|
||
check(crossRefs.length === 0, 'A6 PC 面零移动端目录引用', crossRefs.length ? crossRefs.map(rel).join(', ') : null);
|
||
|
||
/* A7:PC 组件测试页数与索引一致(_collect/index/template 等不计入) */
|
||
const pcTestPages = countFilesMatching(/^tests\/[a-z0-9]+\.html$/).filter(
|
||
(f) => !['tests/index.html'].includes(rel(f))
|
||
);
|
||
check(pcTestPages.length === pcCount, `A7 PC 测试页 = 索引组件数(${pcCount})`, `实际 ${pcTestPages.length}`);
|
||
|
||
/* A8:slug 不撞名 */
|
||
const overlap = mbSlugs.filter((s) => pcSlugs.has(s));
|
||
check(overlap.length === 0, 'A8 移动端 slug 与 PC 无撞名', overlap.length ? overlap.join(', ') : null);
|
||
|
||
/* A9:PC 分发产物(若已构建)与索引一致 */
|
||
const pcManifest = readIf(join(ROOT, 'dist', 'manifest.json'));
|
||
if (pcManifest) {
|
||
const mf = JSON.parse(pcManifest);
|
||
check((mf.components || []).length === pcCount, `A9 dist/manifest.json = 索引组件数(${pcCount})`, `实际 ${(mf.components || []).length}`);
|
||
} else {
|
||
console.log(' SKIP A9 dist/manifest.json 未构建(先跑 npm run build:dist)');
|
||
}
|
||
|
||
/* A10:PC 回归报告页数与索引一致(若存在) */
|
||
const pcReport = readIf(join(ROOT, 'tests', 'report.json'));
|
||
if (pcReport) {
|
||
const rp = JSON.parse(pcReport);
|
||
check(rp.pages === pcCount, `A10 tests/report.json 页数 = 索引组件数(${pcCount})`, `实际 ${rp.pages}`);
|
||
} else {
|
||
console.log(' SKIP A10 tests/report.json 不存在(先跑 npm run regression)');
|
||
}
|
||
|
||
/* ============ B. 移动端自洽 ============ */
|
||
|
||
section('B · 移动端自洽');
|
||
|
||
/* B1:6 端文件齐全 */
|
||
const missingEnds = [];
|
||
for (const c of mbIndex.components) {
|
||
for (const end of mbIndex.ends) {
|
||
const f = c.files && c.files[end];
|
||
if (!f || !existsSync(join(ROOT, 'frameworks-mobile', f))) missingEnds.push(`${c.slug}/${end}`);
|
||
}
|
||
}
|
||
check(
|
||
missingEnds.length === 0,
|
||
`B1 移动端 ${mbIndex.components.length} × ${mbIndex.ends.length} 端文件齐全`,
|
||
missingEnds.length ? missingEnds.join(', ') : `${mbIndex.components.length * mbIndex.ends.length} 个文件`
|
||
);
|
||
|
||
/* B1b(2026-09-20 新增):**反向断言** —— 磁盘上的实现文件必须都能在索引里找到。
|
||
为什么要它:B1 只查「索引里的组件有没有文件」,不查「磁盘上的文件有没有进索引」。
|
||
于是子 agent 产出的组件在合并索引前**完全不被任何门禁检查**(它们不在索引里),
|
||
属静默缺口 —— 与本仓库历史上「文档声称完成但代码缺失」同一族。
|
||
两个批次子 agent 独立报了这条,故补上。 */
|
||
const indexedPrefixes = new Set(mbIndex.components.map((c) => c.frameworksPrefix));
|
||
const onDiskPrefixes = new Set(
|
||
readdirSync(join(ROOT, 'frameworks-mobile'))
|
||
.filter((f) => /\.(css|html|jsx|uniapp\.vue|vue2\.vue|vue3\.vue)$/.test(f))
|
||
.map((f) => f.split('.')[0])
|
||
);
|
||
const unindexed = [...onDiskPrefixes].filter((p) => !indexedPrefixes.has(p)).sort();
|
||
check(
|
||
unindexed.length === 0,
|
||
`B1b 磁盘实现文件全部已登记(${onDiskPrefixes.size} 个前缀)`,
|
||
unindexed.length ? `未登记:${unindexed.join(', ')}(跑 node tools/merge-mobile-batch.mjs 合并)` : null
|
||
);
|
||
|
||
/* B2:契约字段完整 */
|
||
const CONTRACT_REQUIRED = [
|
||
'slug',
|
||
'name',
|
||
'sourceKind',
|
||
'provenance',
|
||
'variantDimensions',
|
||
'representativeVariants',
|
||
'anatomy',
|
||
'usageHints',
|
||
'doNotInvent',
|
||
'unknowns',
|
||
];
|
||
const badContracts = [];
|
||
for (const c of mbIndex.components) {
|
||
const p = join(ROOT, '.design_library', 'kole-ui-mobile', c.contract);
|
||
if (!existsSync(p)) {
|
||
badContracts.push(`${c.slug}: 缺契约文件`);
|
||
continue;
|
||
}
|
||
const ct = JSON.parse(read(p));
|
||
for (const k of CONTRACT_REQUIRED) {
|
||
if (ct[k] === undefined || ct[k] === null) badContracts.push(`${c.slug}: 缺字段 ${k}`);
|
||
}
|
||
if (ct.provenance !== 'authored-in-repo') badContracts.push(`${c.slug}: provenance 必须为 authored-in-repo`);
|
||
}
|
||
check(badContracts.length === 0, 'B2 移动端契约字段完整且 provenance 诚实', badContracts.join('; ') || null);
|
||
|
||
/* B3:移动端 CSS 只用 kole-m- 类 + 状态类,且无硬编码 hex */
|
||
const mbCssFiles = countFilesMatching(/^frameworks-mobile\/.+\.css$/);
|
||
const cssProblems = [];
|
||
for (const f of mbCssFiles) {
|
||
const t = read(f);
|
||
const cls = t.match(/\.kole-[a-zA-Z0-9_-]+/g) || [];
|
||
cls.forEach((c) => {
|
||
const bare = c.slice(1);
|
||
if (!bare.startsWith('kole-m-')) cssProblems.push(`${rel(f)}: 非移动端前缀类 .${bare}`);
|
||
});
|
||
const stateCls = t.match(/\.is-[a-zA-Z0-9_-]+/g) || [];
|
||
stateCls.forEach((c) => {
|
||
if (!/^\.is-[a-z-]+$/.test(c)) cssProblems.push(`${rel(f)}: 状态类格式异常 ${c}`);
|
||
});
|
||
const hex = t.match(/#[0-9a-fA-F]{3,8}\b/g) || [];
|
||
if (hex.length) cssProblems.push(`${rel(f)}: 硬编码颜色 ${hex.join(' ')}`);
|
||
}
|
||
check(cssProblems.length === 0, `B3 移动端 CSS 前缀与令牌合规(${mbCssFiles.length} 文件)`, cssProblems.slice(0, 6).join('; ') || null);
|
||
|
||
/* B4:移动端 CSS 引用的令牌必须真实存在 */
|
||
const tokenNames = new Set(
|
||
(read(join(ROOT, '.design_library', 'kole-ui', 'colors_and_type.css')).match(/--kole-[a-z0-9-]+/g) || []).concat(
|
||
read(join(ROOT, '.design_library', 'kole-ui-mobile', 'colors_and_type.css')).match(/--kole-m-[a-z0-9-]+/g) || []
|
||
)
|
||
);
|
||
const badTokens = [];
|
||
for (const f of mbCssFiles) {
|
||
(read(f).match(/var\((--kole-[a-z0-9-]+)/g) || []).forEach((v) => {
|
||
const name = v.slice(4);
|
||
if (!/^--kole-(m-[a-z0-9-]+|[a-z0-9-]+)$/.test(name)) badTokens.push(`${rel(f)}: ${name}`);
|
||
});
|
||
}
|
||
check(badTokens.length === 0, 'B4 移动端 CSS 令牌命名合规', badTokens.slice(0, 6).join('; ') || null);
|
||
|
||
/* B5:演示页引用移动端令牌层,不直接引 PC 令牌 */
|
||
const mbHtml = countFilesMatching(/^frameworks-mobile\/.+\.html$/);
|
||
const htmlProblems = [];
|
||
for (const f of mbHtml) {
|
||
const t = read(f);
|
||
if (!t.includes('kole-ui-mobile/colors_and_type.css')) htmlProblems.push(`${rel(f)}: 未引用移动端令牌层`);
|
||
if (t.includes('kole-ui/colors_and_type.css')) htmlProblems.push(`${rel(f)}: 直引 PC 令牌层(应走移动端令牌层)`);
|
||
if (!t.includes('class="demo"') && !t.includes("class='demo'")) htmlProblems.push(`${rel(f)}: 缺 .demo 被测范围容器`);
|
||
}
|
||
check(htmlProblems.length === 0, `B5 移动端演示页令牌与结构合规(${mbHtml.length} 页)`, htmlProblems.slice(0, 6).join('; ') || null);
|
||
|
||
/* B6:演示页声明的 data-behavior 动词必须在移动端行为库里存在 */
|
||
const mbBehaviors = read(join(ROOT, 'tests', 'mobile', '_behaviors.js'));
|
||
const knownVerbs = new Set((mbBehaviors.match(/'([a-z-]+)':\s*function \(ctx, trig, args\)/g) || []).map((s) => s.split("'")[1]));
|
||
const badVerbs = [];
|
||
for (const f of mbHtml) {
|
||
const t = read(f);
|
||
for (const m of t.matchAll(/data-behavior="([a-z][a-z-]*):/g)) {
|
||
if (!knownVerbs.has(m[1])) badVerbs.push(`${rel(f)}: ${m[1]}`);
|
||
}
|
||
}
|
||
check(badVerbs.length === 0, 'B6 演示页行为动词均有实现', badVerbs.join('; ') || `已实现动词 ${knownVerbs.size} 个`);
|
||
|
||
/* B7:生成物齐全 */
|
||
const generated = [
|
||
'site/m/index.html',
|
||
'site/m/style.css',
|
||
'site/m/data.mobile.json',
|
||
'tests/mobile/index.html',
|
||
'tests/mobile/_collect.html',
|
||
'dist/mobile/manifest.json',
|
||
'dist/mobile/tokens/tokens.css',
|
||
'dist/mobile/components/index.css',
|
||
'dist/mobile/uniapp/index.js',
|
||
];
|
||
const missingGen = generated.filter((p) => !existsSync(join(ROOT, p)));
|
||
for (const c of mbIndex.components) {
|
||
if (!existsSync(join(ROOT, 'site', 'm', 'component', c.slug + '.html'))) missingGen.push(`site/m/component/${c.slug}.html`);
|
||
if (!existsSync(join(ROOT, 'tests', 'mobile', c.slug + '.html'))) missingGen.push(`tests/mobile/${c.slug}.html`);
|
||
}
|
||
check(missingGen.length === 0, 'B7 移动端生成物齐全', missingGen.slice(0, 6).join(', ') || `${generated.length + mbIndex.components.length * 2} 项`);
|
||
|
||
/* B8:移动端报告(若存在)与索引一致 */
|
||
const mbReport = readIf(join(ROOT, 'tests', 'mobile-report.json'));
|
||
if (mbReport) {
|
||
const rp = JSON.parse(mbReport);
|
||
check(rp.pages === mbIndex.components.length, `B8 tests/mobile-report.json 页数与索引一致`, `报告 ${rp.pages} / 索引 ${mbIndex.components.length}`);
|
||
} else {
|
||
console.log(' SKIP B8 tests/mobile-report.json 不存在(先跑 npm run regression:mobile)');
|
||
}
|
||
|
||
/* ============ C. 分发隔离 ============ */
|
||
|
||
section('C · 分发隔离');
|
||
|
||
const distPcIndex = readIf(join(ROOT, 'dist', 'components', 'index.css'));
|
||
check(
|
||
!distPcIndex || distPcIndex.indexOf('kole-m-') < 0,
|
||
'C1 PC 样式聚合无移动端类',
|
||
distPcIndex ? null : 'dist/components/index.css 未构建'
|
||
);
|
||
|
||
const distMbIndex = readIf(join(ROOT, 'dist', 'mobile', 'components', 'index.css'));
|
||
check(!!distMbIndex && distMbIndex.indexOf('kole-m-') >= 0, 'C2 移动端样式聚合存在且含 kole-m- 类');
|
||
|
||
const distMbManifest = readIf(join(ROOT, 'dist', 'mobile', 'manifest.json'));
|
||
if (distMbManifest) {
|
||
const mf = JSON.parse(distMbManifest);
|
||
/* 判据(2026-09-20 修正):不再要求「无 PC slug」(同名是允许的),改为
|
||
「slug 集合与移动端索引完全一致」——能同时抓到漏项与串入别的东西。 */
|
||
const mfSlugs = (mf.components || []).map((c) => c.slug).sort().join(',');
|
||
const idxSlugs = mbSlugs.slice().sort().join(',');
|
||
check(mfSlugs === idxSlugs, 'C3 dist/mobile/manifest.json 的 slug 集合与索引一致', mfSlugs === idxSlugs ? `${mfSlugs.split(',').length} 个` : `manifest: ${mfSlugs} / 索引: ${idxSlugs}`);
|
||
const entries = (mf.components || []).flatMap((c) => Object.values(c.sha256 || {}));
|
||
check(entries.every((h) => /^[0-9a-f]{16}$/.test(h)), 'C4 manifest 各端源码校验和齐全');
|
||
} else {
|
||
console.log(' SKIP C3/C4 dist/mobile/manifest.json 未构建');
|
||
}
|
||
|
||
/* C5:unispp PC 列(若已构建)不含移动端类 */
|
||
const distUniappPc = readIf(join(ROOT, 'dist', 'uniapp-pc', 'manifest.json'));
|
||
if (distUniappPc) {
|
||
const mf = JSON.parse(distUniappPc);
|
||
check(mf.platform === 'pc' && mf.end === 'uniapp' && (mf.components || []).length > 0, 'C5 dist/uniapp-pc/manifest.json 平台/端标注正确', `${(mf.components || []).length} 个组件`);
|
||
const srcProblems = [];
|
||
for (const c of mf.components) {
|
||
const t = readIf(join(ROOT, 'frameworks-uniapp-pc', c.file));
|
||
if (t && t.indexOf('kole-m-') >= 0) srcProblems.push(c.file);
|
||
}
|
||
check(srcProblems.length === 0, 'C6 PC × uni-app 端无 kole-m- 前缀', srcProblems.join(', ') || null);
|
||
} else {
|
||
console.log(' SKIP C5/C6 dist/uniapp-pc 未构建(先跑 npm run build:uniapp)');
|
||
}
|
||
|
||
/* C7:package.json exports 两套并存 */
|
||
const pkg = JSON.parse(read(join(ROOT, 'package.json')));
|
||
const exp = pkg.exports || {};
|
||
const needPc = ['./tokens.css', './components/*', './react', './vue3', './vue2'];
|
||
const needMb = ['./mobile', './mobile/tokens.css', './mobile/components/*', './mobile/react', './mobile/vue3', './mobile/vue2', './mobile/uniapp'];
|
||
const missExp = [...needPc, ...needMb].filter((k) => !exp[k]);
|
||
check(missExp.length === 0, 'C7 package.json exports 含 PC 与移动端两套入口', missExp.join(', ') || `${Object.keys(exp).length} 条`);
|
||
const dep = Object.keys(pkg.dependencies || {});
|
||
check(dep.length === 0, 'C8 零运行时依赖保持不变', dep.join(', ') || null);
|
||
|
||
/* ============ 汇总 ============ */
|
||
|
||
console.log('\n────────────────────────────');
|
||
if (failures.length) {
|
||
console.error(`[FAIL] ${failures.length} 条隔离断言失败(通过 ${pass} 条):`);
|
||
failures.forEach((f) => console.error(' - ' + f));
|
||
process.exit(1);
|
||
}
|
||
console.log(`[OK] 隔离门禁全部通过(${pass} 条断言)`);
|