Files
aurora-admin/tools/verify-package-import.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

270 lines
12 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.
#!/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);