#!/usr/bin/env node /** * build-dist.mjs — 产出可分发产物(npm 包 / CDN) * * 设计约束: * - 零运行时依赖:只用 Node 内置能力(fs / path) * - 不依赖 build-site.ps1(那是 Windows 专用的文档站构建) * - 产物自包含:dist/ 里的东西拷走就能用 * * 输入: * - .design_library/kole-ui/colors_and_type.css 设计令牌 * - .design_library/kole-ui/components.css 6 核心组件样式聚合 * - .design_library/kole-ui/css.json 结构化令牌 * - frameworks/*.css 79 个组件样式 * - frameworks/*.html 79 个演示页 * * 输出: * - dist/tokens/tokens.css 设计令牌(可直接 @import) * - dist/tokens/tokens.json W3C DTCG 格式 * - dist/tokens/figma.json Figma Tokens 格式 * - dist/components/.css 单个组件样式 * - dist/components/index.css 全部组件样式聚合 * - dist/components/.html 静态演示页(可独立打开) * - dist/react/index.js React 端聚合入口(可 import) * - dist/react/.jsx 单组件源码(按需 import) * - dist/vue3/index.js Vue 3 端聚合入口(可 import) * - dist/vue3/.vue 单组件 SFC 源码(按需 import) * - dist/vue2/index.js Vue 2 端聚合入口(可 import) * - dist/vue2/.vue 单组件 SFC 源码(按需 import) * - dist/manifest.json 组件清单 + 版本 + 校验和 * - dist/README.md 用法速查 */ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'fs'; import { createHash } from 'crypto'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = join(__dirname, '..'); const LIB = join(ROOT, '.design_library', 'kole-ui'); const SRC = join(ROOT, 'frameworks'); const DIST = join(ROOT, 'dist'); /* ---------- 工具 ---------- */ function ensure(dir) { if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); } function read(p) { return readFileSync(p, 'utf8'); } function write(p, content) { ensure(dirname(p)); // 统一 LF,避免跨平台差异 writeFileSync(p, content.replace(/\r\n/g, '\n')); } function sha256(content) { return createHash('sha256').update(content).digest('hex').slice(0, 16); } /* ---------- 1. 读组件清单 ---------- */ const idxPath = join(LIB, 'components', 'index.json'); if (!existsSync(idxPath)) { console.error('[FATAL] 找不到 components/index.json'); process.exit(1); } const idx = JSON.parse(read(idxPath)); const components = idx.components; const pkg = JSON.parse(read(join(ROOT, 'package.json'))); const version = pkg.version; console.log(`[build-dist] 版本 ${version} / ${components.length} 个组件`); /* ---------- 2. 令牌 ---------- */ const tokenCss = read(join(LIB, 'colors_and_type.css')); // 从令牌文件解析出 name/value/comment(与 build-site.ps1 同一套正则思路) const tokens = []; const TOKEN_RE = /--(kole-[a-z0-9-]+)\s*:\s*([^;]+);(?:\s*\/\*\s*(.*?)\s*\*\/)?/g; let m; while ((m = TOKEN_RE.exec(tokenCss)) !== null) { tokens.push({ name: m[1], value: m[2].trim(), comment: (m[3] || '').trim() }); } write(join(DIST, 'tokens', 'tokens.css'), tokenCss); // W3C DTCG function groupOf(name) { if (/color|brand|text|bg|border|success|warning|error|info|mask/.test(name)) return 'color'; if (/font|type/.test(name)) return 'typography'; if (/radius/.test(name)) return 'radius'; if (/space|gap|padding/.test(name)) return 'spacing'; if (/shadow|elevation/.test(name)) return 'shadow'; if (/duration|ease|motion/.test(name)) return 'motion'; if (/size|height|width|icon/.test(name)) return 'size'; return 'misc'; } const dtcg = {}; for (const t of tokens) { const g = groupOf(t.name); dtcg[g] = dtcg[g] || {}; dtcg[g][t.name] = { $value: t.value, $type: g === 'color' ? 'color' : 'dimension', $description: t.comment || undefined }; } write( join(DIST, 'tokens', 'tokens.json'), JSON.stringify( { $schema: 'https://schemas.design-tokens.org/draft/dtcg.json', $metadata: { name: 'Kole UI', version }, tokens: dtcg }, null, 2 ) ); // Figma Tokens const figma = {}; for (const t of tokens) { const g = groupOf(t.name); figma[t.name] = { value: t.value, type: g === 'color' ? 'color' : g === 'size' ? 'sizing' : g === 'spacing' ? 'spacing' : g === 'radius' ? 'borderRadius' : 'other', description: t.comment || '', }; } write(join(DIST, 'tokens', 'figma.json'), JSON.stringify({ global: figma, $themes: [], $metadata: { tokenSetOrder: ['global'] } }, null, 2)); console.log(`[build-dist] 令牌 ${tokens.length} 个 → dist/tokens/`); /* ---------- 3. 组件样式 ---------- */ const manifest = { name: pkg.name, version, generated: new Date().toISOString().slice(0, 19).replace('T', ' '), tokens: tokens.length, components: [], }; const aggregate = [ '/* Kole UI — 全部组件样式聚合', ` * 版本 ${version} · ${components.length} 个组件`, ' * 用法:在令牌之后引入本文件即可获得全部组件样式', ' */', '', /* 本文件位于 dist/components/,令牌在 dist/tokens/ —— 故必须是 ../tokens/ */ `@import url('../tokens/tokens.css');`, '', ].join('\n'); const parts = [aggregate]; for (const c of components) { const slug = c.slug; const prefix = c.frameworksPrefix; // 组件 CSS const cssPath = join(SRC, `${prefix}.css`); if (existsSync(cssPath)) { const css = read(cssPath); write(join(DIST, 'components', `${slug}.css`), `/* Kole UI · ${c.name} */\n${css}`); parts.push(`/* ---- ${c.name} (${slug}) ---- */`, css, ''); } // 演示页(重写引用,使其在 dist 下能独立打开) const htmlPath = join(SRC, `${prefix}.html`); let hasDemo = false; if (existsSync(htmlPath)) { let html = read(htmlPath); /* dist 的目录结构: dist/components/.html dist/components/.css dist/tokens/tokens.css 因此所有引用都要重写。漏掉文件名是最容易犯的错 —— 会产出「路径对但文件不存在」的坏产物。 */ html = html.replace(/\.\.\/\.design_library\/kole-ui\/colors_and_type\.css/g, '../tokens/tokens.css'); html = html.replace(/\.\.\/\.design_library\/kole-ui\/components\.css/g, './index.css'); html = html.replace(/\.\.\/\.design_library\/kole-ui\//g, '../tokens/'); // 组件自身样式:./.css → ./.css html = html.replace(new RegExp(`(\\./)?${prefix}\\.css`, 'g'), `./${slug}.css`); write(join(DIST, 'components', `${slug}.html`), html); hasDemo = true; } manifest.components.push({ slug, name: c.name, category: c.category, tier: c.tier, css: existsSync(cssPath) ? `components/${slug}.css` : null, demo: hasDemo ? `components/${slug}.html` : null, cssHash: existsSync(cssPath) ? sha256(read(cssPath)) : null, }); } write(join(DIST, 'components', 'index.css'), parts.join('\n')); write(join(DIST, 'manifest.json'), JSON.stringify(manifest, null, 2)); const cssCount = readdirSync(join(DIST, 'components')).filter((f) => f.endsWith('.css')).length; const htmlCount = readdirSync(join(DIST, 'components')).filter((f) => f.endsWith('.html')).length; console.log(`[build-dist] 组件 ${components.length} 个 → dist/components/(${cssCount} css / ${htmlCount} html)`); /* ---------- 3.5 框架端组件入口(可 import)---------- * 目标:让 `import { KoleButton } from 'kole-ui/react'` 这类用法成立。 * 策略(零构建依赖): * - React:直接发 .jsx 源码 + 聚合入口(项目自己的构建链编译 JSX) * - Vue 3:直接发 .vue SFC 源码 + 聚合入口(样式在 SFC 内,随组件走) * - Vue 2:同样发 .vue 源码;另给一份「已编译」的 JS(见下), * 因为 Vue 2 的 .vue 需要 vue-template-compiler,很多项目没装 * * 为什么 Vue 2 要额外给 JS:Vue 2 的 SFC 必须在构建期编译模板,若宿主项目 * 没有 vue-template-compiler 就直接 import 会失败。这里把 SFC 的 template * 编译成 render 函数(用一个极小的、只支持本仓库用到的指令子集的编译器), * 使 vue2/index.js 在不具备 SFC 编译能力的项目里也能用。 */ const FW_PREFIX = new Map(components.map((c) => [c.frameworksPrefix, c])); /* 组件名:Vue2 用 SFC 里的 name(KoleXxx),React 用函数名,Vue3 用文件名推导 */ function pascal(slug, prefix) { return prefix; } const reactImports = []; const vue3Imports = []; const vue2Imports = []; for (const c of components) { const prefix = c.frameworksPrefix; // React const jsxPath = join(SRC, `${prefix}.jsx`); if (existsSync(jsxPath)) { write(join(DIST, 'react', `${prefix}.jsx`), read(jsxPath)); /* JSX 里有 `import './.css'`:必须把同名 CSS 放到同一目录, 否则宿主构建链解析失败(典型症状:组件能 import 但样式全丢)。 */ const jsxCss = join(SRC, `${prefix}.css`); if (existsSync(jsxCss)) write(join(DIST, 'react', `${prefix}.css`), read(jsxCss)); reactImports.push({ prefix, slug: c.slug }); } // Vue 3 const v3Path = join(SRC, `${prefix}.vue3.vue`); if (existsSync(v3Path)) { write(join(DIST, 'vue3', `${prefix}.vue`), read(v3Path)); vue3Imports.push({ prefix, slug: c.slug }); } // Vue 2 const v2Path = join(SRC, `${prefix}.vue2.vue`); if (existsSync(v2Path)) { write(join(DIST, 'vue2', `${prefix}.vue`), read(v2Path)); vue2Imports.push({ prefix, slug: c.slug }); } /* 56 个组件的 Vue 两端用 `