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 未做部分)
270 lines
12 KiB
JavaScript
270 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* verify-package-import.mjs — 验证 dist/ 真的能被 import(不是"文件存在"式检查)
|
||
*
|
||
* 背景:2026-09-19 实测发现 dist/ 只发 CSS 与演示 HTML,package.json 的 main
|
||
* 指向一个 CSS 文件,**没有任何可 import 的组件**;同时 frameworks/ 里存在
|
||
* 「无法编译」的组件(RangeQuickPicker 给 const 重新赋值、CodeInput 的 emit 遮蔽),
|
||
* 而这两类问题都能通过"真编译 + 真挂载"抓出来。
|
||
*
|
||
* 检查分两级:
|
||
* 1) 结构级(零依赖,始终执行):入口文件存在、导出语句与目标文件一一对应、
|
||
* 所有相对 import/`<style src>` 可解析(断链会让用户"import 成功但样式全丢")
|
||
* 2) 编译级(需要可选依赖,缺失则 SKIP):用 esbuild + @vue/compiler-sfc
|
||
* 真编译 79 个 .vue / .jsx,并用 vue/server-renderer 与 react-dom/server
|
||
* 真渲染 KoleButton 断言 DOM 输出
|
||
*
|
||
* 启用编译级检查(可选依赖,不入 package.json,避免拖累"零运行时依赖"):
|
||
* npm i -D esbuild vue @vue/compiler-sfc react react-dom
|
||
* 或指向任意已装好这些依赖的目录(保持仓库 node_modules 干净):
|
||
* KOLE_VERIFY_DEPS_DIR=.zcode/verify-import node tools/verify-package-import.mjs
|
||
*
|
||
* 用法:node tools/verify-package-import.mjs
|
||
* 退出码:0 = 通过(含 SKIP);1 = 有失败项
|
||
*/
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { createRequire } from 'node:module';
|
||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||
|
||
const require = createRequire(import.meta.url);
|
||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||
const ROOT = path.resolve(HERE, '..');
|
||
const DIST = path.join(ROOT, 'dist');
|
||
/* OUT 在下方按 KOLE_VERIFY_DEPS_DIR 决定(需与可选依赖同处一棵 node_modules 树) */
|
||
|
||
const results = [];
|
||
const ok = (name, extra = '') => results.push(['OK ', name, extra]);
|
||
const fail = (name, extra = '') => results.push(['FAIL', name, extra]);
|
||
const skip = (name, extra = '') => results.push(['SKIP', name, extra]);
|
||
|
||
/* ---------- 0. 前置:dist 是否已构建 ---------- */
|
||
if (!fs.existsSync(DIST)) {
|
||
console.error('[verify-package-import] 未找到 dist/,请先运行:node tools/build-dist.mjs');
|
||
process.exit(1);
|
||
}
|
||
|
||
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
||
|
||
/* ---------- 1. 结构级:入口与断链 ---------- */
|
||
const ENDS = ['react', 'vue3', 'vue2'];
|
||
for (const end of ENDS) {
|
||
const dir = path.join(DIST, end);
|
||
const entry = path.join(dir, 'index.js');
|
||
if (!fs.existsSync(entry)) {
|
||
fail(`dist/${end}/index.js 存在`, '入口缺失,无法 import');
|
||
continue;
|
||
}
|
||
const src = fs.readFileSync(entry, 'utf8');
|
||
const exportLines = src.split('\n').filter((l) => l.trim().startsWith('export {'));
|
||
ok(`dist/${end} 入口导出语句数`, String(exportLines.length));
|
||
if (exportLines.length !== 79) fail(`dist/${end} 应导出 79 个组件`, String(exportLines.length));
|
||
|
||
const missing = [];
|
||
for (const l of exportLines) {
|
||
const m = l.match(/from '\.\/(.+)';/);
|
||
if (m && !fs.existsSync(path.join(dir, m[1]))) missing.push(m[1]);
|
||
}
|
||
if (missing.length) fail(`dist/${end} 入口目标全部存在`, `断链 ${missing.length}:${missing.slice(0, 3).join(', ')}`);
|
||
else ok(`dist/${end} 入口目标全部存在`);
|
||
}
|
||
|
||
/* 断链检查:相对 import / @import / url() / <style src>
|
||
注意:先剥离注释——组件源码的文档注释里有用法示例(如 `from './Button'`),
|
||
那不是真实依赖,误报会让这个检查失去意义。 */
|
||
for (const end of ENDS) {
|
||
const dir = path.join(DIST, end);
|
||
if (!fs.existsSync(dir)) continue;
|
||
const broken = [];
|
||
for (const f of fs.readdirSync(dir)) {
|
||
if (!/\.(jsx|vue|js|css)$/.test(f)) continue;
|
||
const raw = fs.readFileSync(path.join(dir, f), 'utf8');
|
||
const src = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||
const specs = [];
|
||
for (const m of src.matchAll(/(?:import\s+['"]|from\s+['"]|@import\s+['"]|url\(\s*['"]?)(\.[^'")]+)/g)) {
|
||
specs.push(m[1]);
|
||
}
|
||
for (const s of specs) {
|
||
const t = path.join(dir, s);
|
||
if (!fs.existsSync(t) && !fs.existsSync(t + '.jsx') && !fs.existsSync(t + '.css')) {
|
||
broken.push(`${f} -> ${s}`);
|
||
}
|
||
}
|
||
}
|
||
if (broken.length) fail(`dist/${end} 相对引用无断链`, `${broken.length} 处:${broken.slice(0, 3).join(' | ')}`);
|
||
else ok(`dist/${end} 相对引用无断链`);
|
||
}
|
||
|
||
/* 用户实际会踩的坑:import 组件但样式丢失。
|
||
抽查 3 个用 <style src> 的组件,确认 CSS 随包发出。 */
|
||
{
|
||
const probe = ['CodeInput', 'AlertModal', 'RangeQuickPicker'];
|
||
const bad = [];
|
||
for (const p of probe) {
|
||
for (const end of ['vue3', 'vue2']) {
|
||
const vue = path.join(DIST, end, `${p}.vue`);
|
||
if (!fs.existsSync(vue)) continue;
|
||
const src = fs.readFileSync(vue, 'utf8');
|
||
const m = src.match(/<style[^>]*src=['"]\.\/([^'"]+)['"]/);
|
||
if (m && !fs.existsSync(path.join(DIST, end, m[1]))) bad.push(`${end}/${p}.vue -> ${m[1]}`);
|
||
}
|
||
}
|
||
if (bad.length) fail('SFC 外部样式随包发布', bad.join(', '));
|
||
else ok('SFC 外部样式随包发布', `抽查 ${probe.length} 个组件`);
|
||
}
|
||
|
||
/* ---------- 2. package.json 入口映射 ---------- */
|
||
{
|
||
const exp = pkg.exports || {};
|
||
const need = ['react', 'vue3', 'vue2'];
|
||
const miss = need.filter((k) => !exp[`./${k}`]);
|
||
if (miss.length) fail('package.json exports 含三端入口', miss.join(', '));
|
||
else ok('package.json exports 含三端入口');
|
||
|
||
const badTarget = [];
|
||
for (const [k, v] of Object.entries(exp)) {
|
||
if (k.includes('*') || k === './package.json') continue;
|
||
const target = typeof v === 'string' ? v : v.default;
|
||
if (target && !fs.existsSync(path.join(ROOT, target))) badTarget.push(`${k} -> ${target}`);
|
||
}
|
||
if (badTarget.length) fail('exports 目标文件存在', badTarget.join(', '));
|
||
else ok('exports 目标文件存在');
|
||
|
||
if (pkg.dependencies && Object.keys(pkg.dependencies).length) {
|
||
fail('零运行时依赖', Object.keys(pkg.dependencies).join(', '));
|
||
} else {
|
||
ok('零运行时依赖');
|
||
}
|
||
const peers = Object.keys(pkg.peerDependencies || {});
|
||
ok('peerDependencies 声明', peers.length ? peers.join(', ') : '(无)');
|
||
}
|
||
|
||
/* ---------- 3. 编译级:真编译 + 真挂载(可选依赖)---------- */
|
||
/* 可选依赖可从 KOLE_VERIFY_DEPS_DIR 指定目录解析,避免为了跑编译级检查
|
||
往仓库 node_modules 里装一堆东西(--no-save 只保护 package.json,
|
||
并不保护 node_modules)。 */
|
||
const DEPS_DIR = process.env.KOLE_VERIFY_DEPS_DIR
|
||
? path.resolve(ROOT, process.env.KOLE_VERIFY_DEPS_DIR)
|
||
: ROOT;
|
||
const depsRequire = DEPS_DIR === ROOT ? require : createRequire(path.join(DEPS_DIR, 'noop.js'));
|
||
|
||
/* bundle 里 vue / react 是 external,运行时仍需解析到它们。
|
||
把产物输出到依赖目录下,import 时才能顺着 node_modules 找到(否则报
|
||
"Cannot find package 'vue' imported from ...")。 */
|
||
const OUT = DEPS_DIR === ROOT
|
||
? path.join(ROOT, '.tmp', 'verify-package-import')
|
||
: path.join(DEPS_DIR, '.verify-out');
|
||
|
||
function tryRequire(name) {
|
||
try { return depsRequire(name); } catch { return null; }
|
||
}
|
||
const esbuild = tryRequire('esbuild');
|
||
const vueSfc = tryRequire('@vue/compiler-sfc');
|
||
const vue = tryRequire('vue');
|
||
const vueServerRenderer = tryRequire('vue/server-renderer');
|
||
const react = tryRequire('react');
|
||
const reactDomServer = tryRequire('react-dom/server');
|
||
|
||
const canVue = esbuild && vueSfc && vue && vueServerRenderer;
|
||
const canReact = esbuild && react && reactDomServer;
|
||
|
||
if (!canVue && !canReact) {
|
||
skip('编译级检查(真编译 + 真挂载)', '未安装可选依赖;启用:npm i -D esbuild vue @vue/compiler-sfc react react-dom');
|
||
} else {
|
||
fs.rmSync(OUT, { recursive: true, force: true });
|
||
fs.mkdirSync(OUT, { recursive: true });
|
||
const asUrl = (p) => pathToFileURL(p).href;
|
||
|
||
const vuePlugin = {
|
||
name: 'vue-sfc',
|
||
setup(build) {
|
||
build.onLoad({ filter: /\.vue$/ }, (args) => {
|
||
const source = fs.readFileSync(args.path, 'utf8');
|
||
const { descriptor, errors } = vueSfc.parse(source, { filename: args.path });
|
||
if (errors.length) throw new Error('SFC parse: ' + errors[0].message);
|
||
const id = Buffer.from(args.path).toString('hex').slice(0, 8);
|
||
const script = vueSfc.compileScript(descriptor, { id });
|
||
const template = descriptor.template
|
||
? vueSfc.compileTemplate({
|
||
source: descriptor.template.content,
|
||
filename: args.path,
|
||
id,
|
||
scoped: descriptor.styles.some((s) => s.scoped),
|
||
compilerOptions: { bindingMetadata: script.bindings },
|
||
})
|
||
: { code: 'export function render(){return null}' };
|
||
if (template.errors && template.errors.length) throw new Error('template: ' + template.errors[0].message);
|
||
return {
|
||
contents: `
|
||
${script.content.replace(/export\s+default/, 'const __sfc_main =')}
|
||
${template.code.replace(/export\s+function\s+render/, 'function __sfc_render')}
|
||
__sfc_main.render = __sfc_render;
|
||
export default __sfc_main;
|
||
`,
|
||
loader: 'js',
|
||
resolveDir: path.dirname(args.path),
|
||
};
|
||
});
|
||
},
|
||
};
|
||
|
||
if (canVue) {
|
||
try {
|
||
await esbuild.build({
|
||
entryPoints: [path.join(DIST, 'vue3', 'index.js')],
|
||
bundle: true, format: 'esm',
|
||
outfile: path.join(OUT, 'vue3-bundle.mjs'),
|
||
plugins: [vuePlugin], external: ['vue'], logLevel: 'silent',
|
||
});
|
||
ok('dist/vue3 入口可编译(79 个 .vue)', fs.statSync(path.join(OUT, 'vue3-bundle.mjs')).size + ' bytes');
|
||
const mod = await import(asUrl(path.join(OUT, 'vue3-bundle.mjs')));
|
||
const names = Object.keys(mod).filter((k) => k.startsWith('Kole'));
|
||
if (names.length === 79) ok('dist/vue3 导出组件数', '79');
|
||
else fail('dist/vue3 应导出 79 个组件', String(names.length));
|
||
|
||
const { createSSRApp, h } = vue;
|
||
const { renderToString } = vueServerRenderer;
|
||
const html = await renderToString(createSSRApp({ render: () => h(mod.KoleButton, { type: 'primary', text: '保存' }) }));
|
||
if (/class="btn[^"]*btn-primary/.test(html) && html.includes('保存')) ok('KoleButton 真实渲染', html.slice(0, 80));
|
||
else fail('KoleButton 渲染结果不符', html.slice(0, 160));
|
||
} catch (e) {
|
||
fail('dist/vue3 编译/渲染', String(e.message).slice(0, 220));
|
||
}
|
||
} else {
|
||
skip('dist/vue3 编译级检查', '缺 esbuild / vue / @vue/compiler-sfc');
|
||
}
|
||
|
||
if (canReact) {
|
||
try {
|
||
await esbuild.build({
|
||
entryPoints: [path.join(DIST, 'react', 'index.js')],
|
||
bundle: true, format: 'esm',
|
||
outfile: path.join(OUT, 'react-bundle.mjs'),
|
||
loader: { '.jsx': 'jsx', '.css': 'css' },
|
||
external: ['react', 'react-dom'], logLevel: 'silent',
|
||
});
|
||
ok('dist/react 入口可编译(79 个 .jsx + CSS)', fs.statSync(path.join(OUT, 'react-bundle.mjs')).size + ' bytes');
|
||
const mod = await import(asUrl(path.join(OUT, 'react-bundle.mjs')));
|
||
const names = Object.keys(mod).filter((k) => k.startsWith('Kole'));
|
||
if (names.length === 79) ok('dist/react 导出组件数', '79');
|
||
else fail('dist/react 应导出 79 个组件', String(names.length));
|
||
|
||
const html = reactDomServer.renderToStaticMarkup(react.createElement(mod.KoleButton, { type: 'primary' }, '保存'));
|
||
if (/class="btn[^"]*btn-primary/.test(html) && html.includes('保存')) ok('KoleButton(React)真实渲染', html.slice(0, 80));
|
||
else fail('KoleButton(React)渲染结果不符', html.slice(0, 160));
|
||
} catch (e) {
|
||
fail('dist/react 编译/渲染', String(e.message).slice(0, 220));
|
||
}
|
||
} else {
|
||
skip('dist/react 编译级检查', '缺 esbuild / react / react-dom');
|
||
}
|
||
}
|
||
|
||
/* ---------- 输出 ---------- */
|
||
console.log('\n===== dist 可 import 性验证 =====');
|
||
for (const [tag, name, extra] of results) console.log(`${tag} ${name}${extra ? ' — ' + extra : ''}`);
|
||
const failed = results.filter((r) => r[0] === 'FAIL').length;
|
||
const skipped = results.filter((r) => r[0] === 'SKIP').length;
|
||
console.log(`\n${failed === 0 ? 'PASS' : failed + ' FAILED'} — ${results.length} checks${skipped ? `(${skipped} 项 SKIP)` : ''}`);
|
||
process.exit(failed === 0 ? 0 : 1);
|