@qilitt-mickey/vue3-temp-skill 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,405 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * generate-css.mjs —— 契约 → 三份 CSS 的确定性生成器
5
+ *
6
+ * 用法:
7
+ * node generate-css.mjs --project <项目根> [--adapter vue-element-plus] [--out src/styles]
8
+ *
9
+ * 职责(design-apply 第 3/4/5 步的机器执行版,消灭手抄):
10
+ * 1. ui-tokens.css : tokens 族.名 → --族-名 实值 + adapter.roleSupplementVars + adapter.variableBridges
11
+ * 2. ui-roles.css : roles 逐条翻译 host/inner/states/variants + adapter.extraRules 原样追加
12
+ * 3. ui-layout.css : layout.regions 按 adapter.layoutBridge 的挂载类与 skipGeometry 翻译
13
+ *
14
+ * 原则: 契约是唯一事实源;本脚本不做任何视觉判断;同一输入必得同一输出(可复现)。
15
+ */
16
+
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ const SKILL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
22
+
23
+ // ────────────────────────────── 参数
24
+
25
+ function parseArgs(argv) {
26
+ const out = { project: process.cwd(), adapter: null, out: null };
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const a = argv[i];
29
+ if (a === "--project" || a === "-p") out.project = path.resolve(argv[++i]);
30
+ else if (a === "--adapter") out.adapter = argv[++i];
31
+ else if (a === "--out") out.out = argv[++i];
32
+ }
33
+ return out;
34
+ }
35
+
36
+ // ────────────────────────────── 名称转换(与 check-landing.mjs 的 expectedClassName 完全一致)
37
+
38
+ function expectedClassName(roleName) {
39
+ return (
40
+ "role-" +
41
+ roleName
42
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
43
+ .replace(/\./g, "-")
44
+ .replace(/_/g, "-")
45
+ .toLowerCase()
46
+ );
47
+ }
48
+
49
+ function variantClassName(variantName) {
50
+ return variantName
51
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
52
+ .replace(/_/g, "-")
53
+ .toLowerCase();
54
+ }
55
+
56
+ function tokenVar(tokenRef) {
57
+ // "color.brand" → "--color-brand";"space.6" → "--space-6"
58
+ return "--" + tokenRef.replace(/\./g, "-").replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
59
+ }
60
+
61
+ /** 展开值中的 {族.名} 令牌引用 → var(--族-名);字面量原样(色值统一小写,过 prettier 且保证确定性输出) */
62
+ function expandValue(value, tokenIndex) {
63
+ return String(value)
64
+ .replace(/\{([a-zA-Z0-9.]+)\}/g, (m, ref) => {
65
+ const v = tokenVar(ref);
66
+ if (!tokenIndex.has(ref)) {
67
+ // 引用未定义令牌:仍生成 var() 引用(浏览器解析为空时可见),但记入警告
68
+ warnings.push(`令牌引用未在 tokens 定义: {${ref}} → ${v}(请检查契约或补 roleSupplementVars)`);
69
+ }
70
+ return `var(${v})`;
71
+ })
72
+ .replace(/#[0-9a-fA-F]{3,8}\b/g, (c) => c.toLowerCase());
73
+ }
74
+
75
+ /** camelCase 键 → kebab CSS 属性 */
76
+ function cssProp(key) {
77
+ if (key.includes("-")) return key;
78
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
79
+ }
80
+
81
+ const warnings = [];
82
+
83
+ // ────────────────────────────── 读取
84
+
85
+ function loadInputs(args) {
86
+ const designDir = path.join(args.project, ".design");
87
+ const names = fs.readdirSync(designDir).filter((n) => /^visual-contract.*\.json$/.test(n));
88
+ if (!names.length) throw new Error(".design/ 下没有 visual-contract-*.json");
89
+ const contractPath = path.join(designDir, names[0]);
90
+ const contract = JSON.parse(fs.readFileSync(contractPath, "utf-8"));
91
+
92
+ let adapterName = args.adapter;
93
+ if (!adapterName) {
94
+ // 从 mapping/<scene>.json 读取
95
+ const mapPath = path.join(SKILL_DIR, "mapping", `${contract.meta.scene}.json`);
96
+ if (fs.existsSync(mapPath)) {
97
+ adapterName = JSON.parse(fs.readFileSync(mapPath, "utf-8")).adapter;
98
+ }
99
+ }
100
+ if (!adapterName) throw new Error("无法确定 adapter(契约场景无 mapping 且未传 --adapter)");
101
+
102
+ const adapterPath = path.join(SKILL_DIR, "adapters", `${adapterName}.json`);
103
+ if (!fs.existsSync(adapterPath)) throw new Error(`adapter JSON 不存在: ${adapterPath}`);
104
+ const adapter = JSON.parse(fs.readFileSync(adapterPath, "utf-8"));
105
+
106
+ return { contract, adapter, contractPath, adapterName };
107
+ }
108
+
109
+ // ────────────────────────────── ui-tokens.css
110
+
111
+ function genTokens(contract, adapter) {
112
+ const L = [];
113
+ L.push("/**");
114
+ L.push(" * ui-tokens.css —— 视觉契约令牌层(由 generate-css.mjs 生成,禁止手改)");
115
+ L.push(` * 事实源: .design/visual-contract 令牌 + adapter(${adapter.adapter}) 桥接段`);
116
+ L.push(" */");
117
+ L.push("");
118
+ L.push(":root {");
119
+
120
+ const tokenIndex = new Set();
121
+ for (const [family, names] of Object.entries(contract.tokens || {})) {
122
+ L.push(` /* ===== ${family} 族 ===== */`);
123
+ for (const [name, def] of Object.entries(names)) {
124
+ const v = tokenVar(`${family}.${name}`);
125
+ tokenIndex.add(`${family}.${name}`);
126
+ L.push(` ${v}: ${String(def.value).replace(/#[0-9a-fA-F]{3,8}\b/g, (c) => c.toLowerCase())};`);
127
+ }
128
+ L.push("");
129
+ }
130
+
131
+ // 辅助变量(契约 role 声明用到但 tokens 未定义的)
132
+ if (adapter.roleSupplementVars) {
133
+ L.push(" /* ===== 辅助变量(adapter.roleSupplementVars) ===== */");
134
+ for (const [k, v] of Object.entries(adapter.roleSupplementVars)) {
135
+ if (k.startsWith("$")) continue;
136
+ tokenIndex.add(k.replace(/^--/, "").replace(/-/g, ".")); // 宽松
137
+ L.push(` ${k}: ${String(v).replace(/#[0-9a-fA-F]{3,8}\b/g, (c) => c.toLowerCase())};`);
138
+ }
139
+ L.push("");
140
+ }
141
+
142
+ // 桥接段
143
+ L.push(" /* ==========================================================");
144
+ L.push(" * 桥接段(adapter.variableBridges):契约值 → 项目既有变量消费链");
145
+ L.push(" * ========================================================== */");
146
+ const regions = (contract.layout && contract.layout.regions) || {};
147
+ const bridge = adapter.variableBridges || {};
148
+ for (const group of Object.keys(bridge)) {
149
+ if (group.startsWith("$")) continue;
150
+ L.push("");
151
+ L.push(` /* -- ${group} -- */`);
152
+ for (const b of bridge[group]) {
153
+ let value;
154
+ if (b.from && b.from.startsWith("layout:")) {
155
+ // "layout:header.height" → regions.header.height(展开令牌引用)
156
+ const [region, key] = b.from.slice(7).split(".");
157
+ const raw = regions[region] && regions[region][key];
158
+ if (raw == null) {
159
+ warnings.push(`桥接 layout:${region}.${key} 在契约 layout.regions 中不存在,跳过`);
160
+ continue;
161
+ }
162
+ value = expandValue(raw, tokenIndex);
163
+ } else if (b.from && b.from.startsWith("--")) {
164
+ value = `var(${b.from})`;
165
+ } else {
166
+ value = expandValue(b.value, tokenIndex);
167
+ }
168
+ L.push(` ${b.var}: ${value}${b.important ? " !important" : ""};`);
169
+ }
170
+ }
171
+
172
+ L.push("}");
173
+ L.push("");
174
+ return { css: L.join("\n"), tokenIndex };
175
+ }
176
+
177
+ // ────────────────────────────── ui-roles.css
178
+
179
+ function declBlock(decls, indent, tokenIndex) {
180
+ const out = [];
181
+ for (const d of decls) {
182
+ out.push(`${indent}${cssProp(d.prop)}: ${expandValue(d.value, tokenIndex)};`);
183
+ }
184
+ return out;
185
+ }
186
+
187
+ function genRoles(contract, adapter, tokenIndex) {
188
+ const L = [];
189
+ L.push("/**");
190
+ L.push(" * ui-roles.css —— 视觉契约角色层(由 generate-css.mjs 生成,禁止手改)");
191
+ L.push(` * 事实源: .design/visual-contract roles + adapter(${adapter.adapter}) 结构等价转换`);
192
+ L.push(" * 全局文件:禁止 :deep();inner 穿透用普通后代选择器(adapter.innerTargets)");
193
+ L.push(" */");
194
+ L.push("");
195
+
196
+ const roles = contract.roles || {};
197
+ const innerTargets = adapter.innerTargets || {};
198
+ const stateChannels = adapter.stateChannels || {};
199
+ const extraRules = adapter.extraRules || {};
200
+
201
+ // adapter 映射表的键 = 类名去掉 role- 前缀
202
+ const bare = (roleName) => expectedClassName(roleName).replace(/^role-/, "");
203
+ const innerTargetOf = (roleName) => innerTargets[bare(roleName)] || "default";
204
+
205
+ for (const [roleName, role] of Object.entries(roles)) {
206
+ if (roleName.startsWith("$")) continue;
207
+ const cls = "." + expectedClassName(roleName);
208
+ L.push(`/* ===== 角色 ${roleName}${role.label ? " · " + role.label : ""} ===== */`);
209
+
210
+ // host
211
+ if (Array.isArray(role.host) && role.host.length) {
212
+ L.push(`${cls} {`);
213
+ L.push(...declBlock(role.host, " ", tokenIndex));
214
+ L.push("}");
215
+ }
216
+
217
+ // inner
218
+ if (Array.isArray(role.inner) && role.inner.length) {
219
+ const target = innerTargetOf(roleName);
220
+ const sel = target === "default" ? cls : `${cls} ${target}`;
221
+ L.push(`${sel} {`);
222
+ L.push(...declBlock(role.inner, " ", tokenIndex));
223
+ L.push("}");
224
+ }
225
+
226
+ // 结构键 box/flow/align/span/sticky
227
+ const structDecls = [];
228
+ if (role.box) for (const [k, v] of Object.entries(role.box)) structDecls.push({ prop: k, value: v });
229
+ if (role.flow) for (const [k, v] of Object.entries(role.flow)) structDecls.push({ prop: k, value: v });
230
+ if (role.align) for (const [k, v] of Object.entries(role.align)) structDecls.push({ prop: k, value: v });
231
+ if (role.span) for (const [k, v] of Object.entries(role.span)) structDecls.push({ prop: k, value: v });
232
+ if (role.sticky && role.sticky.position) {
233
+ structDecls.push({ prop: "position", value: role.sticky.position });
234
+ if (role.sticky.side) structDecls.push({ prop: role.sticky.side, value: "0" });
235
+ }
236
+ if (structDecls.length) {
237
+ L.push(`${cls} {`);
238
+ L.push(...declBlock(structDecls, " ", tokenIndex));
239
+ L.push("}");
240
+ }
241
+
242
+ // states
243
+ if (role.states) {
244
+ for (const [state, decl] of Object.entries(role.states)) {
245
+ const channel = stateChannels[state] || `:${state}`;
246
+ // 多通道(逗号分隔):作为选择器附加段整体展开(如 ".is-disabled, :disabled")
247
+ for (const ch of channel.split(",").map((c) => c.trim())) {
248
+ const hostSel = ch.startsWith(":") ? `${cls}${ch}` : `${cls}${ch}`;
249
+ if (Array.isArray(decl.host) && decl.host.length) {
250
+ L.push(`${hostSel} {`);
251
+ L.push(...declBlock(decl.host, " ", tokenIndex));
252
+ L.push("}");
253
+ }
254
+ if (Array.isArray(decl.inner) && decl.inner.length) {
255
+ const target = innerTargetOf(roleName);
256
+ const innerSel = target === "default" ? hostSel : `${hostSel} ${target}`;
257
+ L.push(`${innerSel} {`);
258
+ L.push(...declBlock(decl.inner, " ", tokenIndex));
259
+ L.push("}");
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ // variants(整份替换:variant host 覆盖基础 host,inner 同理)
266
+ if (Array.isArray(role.variants)) {
267
+ for (const v of role.variants) {
268
+ const vcls = `${cls}--${variantClassName(v.name)}`;
269
+ const vhost = [];
270
+ if (Array.isArray(role.host)) for (const d of role.host) vhost.push(d);
271
+ if (Array.isArray(v.host)) {
272
+ for (const d of v.host) {
273
+ const i = vhost.findIndex((x) => cssProp(x.prop) === cssProp(d.prop));
274
+ if (i >= 0) vhost[i] = d;
275
+ else vhost.push(d);
276
+ }
277
+ }
278
+ if (vhost.length) {
279
+ L.push(`${vcls} {`);
280
+ L.push(...declBlock(vhost, " ", tokenIndex));
281
+ L.push("}");
282
+ }
283
+ if (Array.isArray(v.inner) && v.inner.length) {
284
+ const target = innerTargetOf(roleName);
285
+ const sel = target === "default" ? vcls : `${vcls} ${target}`;
286
+ L.push(`${sel} {`);
287
+ L.push(...declBlock(v.inner, " ", tokenIndex));
288
+ L.push("}");
289
+ }
290
+ }
291
+ }
292
+
293
+ // extraRules(adapter 的结构等价转换 / 提权段,原样追加——后置覆盖基础翻译)
294
+ const extras = extraRules[bare(roleName)] || extraRules[expectedClassName(roleName)];
295
+ if (extras) {
296
+ L.push(`/* -- adapter 结构等价转换(${adapter.adapter}) -- */`);
297
+ for (const r of extras) {
298
+ const emits = Array.isArray(r.emit) ? r.emit : [r.emit];
299
+ for (const e of emits) L.push(e.trim());
300
+ }
301
+ }
302
+ L.push("");
303
+ }
304
+
305
+ return L.join("\n");
306
+ }
307
+
308
+ // ────────────────────────────── ui-layout.css
309
+
310
+ function genLayout(contract, adapter, tokenIndex) {
311
+ const L = [];
312
+ L.push("/**");
313
+ L.push(" * ui-layout.css —— 视觉契约布局层(由 generate-css.mjs 生成,禁止手改)");
314
+ L.push(` * 事实源: .design/visual-contract layout.regions + adapter(${adapter.adapter}) layoutBridge`);
315
+ L.push(" */");
316
+ L.push("");
317
+
318
+ const bridge = adapter.layoutBridge || {};
319
+ const regions = (contract.layout && contract.layout.regions) || {};
320
+ const areas = new Set((contract.layout?.areas || []).map((a) => a.name));
321
+ const skipGeometry = bridge.skipGeometry || {};
322
+ const regionClasses = bridge.regionClasses || {};
323
+
324
+ // 壳层
325
+ const shell = contract.roles?.["shell.shell"];
326
+ if (shell && bridge.shellClass) {
327
+ L.push(`/* ===== 内容壳(shell.shell) ===== */`);
328
+ L.push(`${bridge.shellClass.join(",\n")} {`);
329
+ L.push(...declBlock(shell.host || [], " ", tokenIndex));
330
+ L.push("}");
331
+ L.push("");
332
+ }
333
+
334
+ for (const [regionName, def] of Object.entries(regions)) {
335
+ if (!areas.has(regionName)) continue; // 未写入 areas 的区域不生成
336
+ const sels = regionClasses[regionName] || [`ui-region-${regionName}`].map((c) => `.${c}`);
337
+ // skipGeometry 用 CSS 属性名;契约键是 camelCase(collapsedWidth)——统一按 cssProp 归一后匹配
338
+ const skip = new Set((skipGeometry[regionName] || []).map((k) => cssProp(k)));
339
+ L.push(`/* ===== 区域 ${regionName}(skip: ${[...skip].join(", ") || "无"}) ===== */`);
340
+ const decls = [];
341
+ const push = (prop, value) => {
342
+ if (skip.has(cssProp(prop))) return;
343
+ decls.push({ prop: cssProp(prop), value: expandValue(value, tokenIndex) });
344
+ };
345
+ if (def.width) push("width", def.width);
346
+ if (def.height) push("height", def.height);
347
+ if (def.minHeight) push("min-height", def.minHeight);
348
+ if (def.padding) push("padding", def.padding);
349
+ if (def.gap) push("gap", def.gap);
350
+ if (def.bg) push("background-color", def.bg);
351
+ if (def.display) push("display", def.display);
352
+ if (def.direction) push("flex-direction", def.direction);
353
+ if (def.alignItems) push("align-items", def.alignItems);
354
+ if (def.justifyContent) push("justify-content", def.justifyContent);
355
+ if (def.position) push("position", def.position);
356
+ if (def.zIndex) push("z-index", def.zIndex);
357
+ // 区域角色同名视觉(如 shell.header 的 border-bottom)从契约 roles 合并(同受 skip 过滤)
358
+ const roleForRegion = contract.roles?.[`shell.${regionName}`];
359
+ if (roleForRegion && Array.isArray(roleForRegion.host)) {
360
+ for (const d of roleForRegion.host) {
361
+ if (skip.has(cssProp(d.prop))) continue;
362
+ if (!decls.some((x) => x.prop === cssProp(d.prop))) {
363
+ decls.push({ prop: cssProp(d.prop), value: expandValue(d.value, tokenIndex) });
364
+ }
365
+ }
366
+ }
367
+ L.push(`${sels.join(",\n")} {`);
368
+ for (const d of decls) L.push(` ${d.prop}: ${d.value};`);
369
+ L.push("}");
370
+ L.push("");
371
+ }
372
+
373
+ return L.join("\n");
374
+ }
375
+
376
+ // ────────────────────────────── 主流程
377
+
378
+ function main() {
379
+ const args = parseArgs(process.argv.slice(2));
380
+ const { contract, adapter, contractPath } = loadInputs(args);
381
+
382
+ const outDir = path.join(args.project, args.out || adapter.output?.dir || "src/styles");
383
+ fs.mkdirSync(outDir, { recursive: true });
384
+
385
+ const { css: tokensCss, tokenIndex } = genTokens(contract, adapter);
386
+ const rolesCss = genRoles(contract, adapter, tokenIndex);
387
+ const layoutCss = genLayout(contract, adapter, tokenIndex);
388
+
389
+ fs.writeFileSync(path.join(outDir, "ui-tokens.css"), tokensCss);
390
+ fs.writeFileSync(path.join(outDir, "ui-roles.css"), rolesCss);
391
+ fs.writeFileSync(path.join(outDir, "ui-layout.css"), layoutCss);
392
+
393
+ console.log(`契约: ${path.relative(args.project, contractPath)}`);
394
+ console.log(`adapter: ${adapter.adapter}`);
395
+ console.log(`生成: ${path.relative(args.project, outDir)}/ui-tokens.css ui-roles.css ui-layout.css`);
396
+ const roleCount = Object.keys(contract.roles || {}).filter((k) => !k.startsWith("$")).length;
397
+ console.log(`角色 ${roleCount} 个全部翻译;令牌族 ${Object.keys(contract.tokens || {}).join("/")}`);
398
+ if (warnings.length) {
399
+ console.log("");
400
+ console.log("警告(不阻塞,但需回查):");
401
+ for (const w of [...new Set(warnings)]) console.log(` - ${w}`);
402
+ }
403
+ }
404
+
405
+ main();