@qilitt-mickey/vue3-temp-skill 1.1.74 → 1.1.76
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.
- package/SKILL.md +137 -137
- package/mapping/basic/color.json +35 -35
- package/mapping/basic/radius.json +17 -17
- package/mapping/basic/semantic-values.json +72 -72
- package/mapping/basic/spacing.json +21 -21
- package/mapping/basic/typography.json +21 -21
- package/mapping/component/button.json +90 -90
- package/mapping/component/card.json +31 -31
- package/mapping/component/input.json +42 -42
- package/mapping/component/overlay.json +79 -79
- package/mapping/component/pagination.json +34 -34
- package/mapping/component/table.json +42 -42
- package/mapping/component/tag.json +48 -48
- package/mapping/layout/breakpoint.json +20 -20
- package/mapping/scenes/admin/content.json +497 -494
- package/mapping/scenes/admin/layout.json +577 -574
- package/package.json +1 -1
- package/parser/fixtures/changeset-legacy.json +11 -11
- package/parser/fixtures/changeset-neg1.json +8 -8
- package/parser/fixtures/changeset-neg2.json +10 -10
- package/parser/fixtures/changeset-neg3.json +22 -22
- package/parser/fixtures/changeset-neg5-g8-stub.json +26 -26
- package/parser/fixtures/changeset-neg7-g10-dims.json +57 -57
- package/parser/fixtures/changeset-neg9-g6-token-chain.json +34 -0
- package/parser/fixtures/changeset-pos-g6-token-chain.json +35 -0
- package/parser/fixtures/changeset-pos-theme-nometa.json +260 -260
- package/parser/fixtures/changeset-registry-gap.json +29 -29
- package/parser/fixtures/fake-project/src/styles/app.scss +5 -5
- package/parser/fixtures/fake-project/src/views/list.vue +13 -13
- package/parser/fixtures/registry-bad.json +11 -11
- package/parser/fixtures/registry-g6-token-chain.json +10 -0
- package/parser/fixtures/verify/actual-clean.json +35 -35
- package/parser/fixtures/verify/actual.json +33 -33
- package/parser/fixtures/verify/mockup-g6-content.html +12 -0
- package/parser/fixtures/verify/mockup-neutral.html +26 -26
- package/parser/fixtures/verify/mockup-nometa.html +26 -26
- package/parser/fixtures/verify/snapshot-latest.json +102 -102
- package/parser/index.js +32 -1
- package/parser/loader.js +66 -66
- package/parser/validator.js +206 -206
- package/references/auth-pages.md +217 -217
- package/references/base-code-dict.md +56 -56
- package/references/data-compare.md +509 -509
- package/references/data-writeback.md +124 -124
- package/references/design-apply.md +167 -167
- package/references/detail-page.md +119 -119
- package/references/download-export.md +97 -97
- package/references/feedback-loading.md +104 -104
- package/references/feedback-ui.md +134 -134
- package/references/i18n-localization.md +230 -230
- package/references/layout-theme.md +2 -0
- package/references/project-inventory.md +438 -438
- package/references/rich-text.md +94 -94
- package/references/seamless-scroll.md +47 -47
- package/references/steps/step0-preflight.md +58 -58
- package/references/steps/step1-match.md +5 -2
- package/references/steps/step3-style.md +38 -33
- package/references/steps/step3b-shell.md +2 -0
- package/references/steps/step3c-priority.md +26 -26
- package/references/steps/step4-l3.md +14 -14
- package/references/steps/step5-gates.md +65 -63
- package/references/steps/step6-verify.md +77 -77
- package/references/verify-captcha.md +119 -119
- package/scripts/check.mjs +24 -0
- package/scripts/design-audit.mjs +208 -3
package/parser/loader.js
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* loader.js — mapping/ 目录读取与合并
|
|
3
|
-
*
|
|
4
|
-
* 递归遍历 mapping/ 下所有 .json 文件,按目录层级合并为完整映射数据集。
|
|
5
|
-
* 合并顺序(后加载覆盖先加载,同 ID 时场景级扩展全局定义):
|
|
6
|
-
* basic/ → layout/ → component/ → scenes/<scene>/
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
10
|
-
import path from "node:path";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
-
|
|
13
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
export const MAPPING_ROOT = path.resolve(__dirname, "..", "mapping");
|
|
15
|
-
|
|
16
|
-
/** 递归收集 JSON 文件(按目录名字典序,保证合并顺序稳定) */
|
|
17
|
-
function collectJsonFiles(dir) {
|
|
18
|
-
const out = [];
|
|
19
|
-
for (const name of readdirSync(dir).sort()) {
|
|
20
|
-
if (name.startsWith("_") || name.startsWith("."))
|
|
21
|
-
continue;
|
|
22
|
-
const full = path.join(dir, name);
|
|
23
|
-
const st = statSync(full);
|
|
24
|
-
if (st.isDirectory())
|
|
25
|
-
out.push(...collectJsonFiles(full));
|
|
26
|
-
else if (name.endsWith(".json"))
|
|
27
|
-
out.push(full);
|
|
28
|
-
}
|
|
29
|
-
return out;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* 构建映射数据集。
|
|
34
|
-
* @returns {{ files: Record<string, object>, fileCount: number, ids: Record<string, {file, meta}> }}
|
|
35
|
-
*/
|
|
36
|
-
export function buildMappingDataset() {
|
|
37
|
-
const files = {};
|
|
38
|
-
const idSource = {};
|
|
39
|
-
|
|
40
|
-
for (const filePath of collectJsonFiles(MAPPING_ROOT)) {
|
|
41
|
-
const rel = path.relative(MAPPING_ROOT, filePath).replaceAll("\\", "/");
|
|
42
|
-
let data;
|
|
43
|
-
try {
|
|
44
|
-
data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
45
|
-
}
|
|
46
|
-
catch (e) {
|
|
47
|
-
throw new Error(`[mapping/${rel}] JSON 解析失败:${e.message}`);
|
|
48
|
-
}
|
|
49
|
-
files[rel] = data;
|
|
50
|
-
|
|
51
|
-
if (data.mappings) {
|
|
52
|
-
for (const id of Object.keys(data.mappings)) {
|
|
53
|
-
if (id.startsWith("$"))
|
|
54
|
-
continue;
|
|
55
|
-
if (idSource[id])
|
|
56
|
-
console.warn(` ⚠ 语义 ID「${id}」在 ${idSource[id].file} 与 ${rel} 中重复,后者覆盖`);
|
|
57
|
-
idSource[id] = { file: rel, meta: data.meta || {} };
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return { files, fileCount: Object.keys(files).length, idSource };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** 供 validator 使用的别名(保持独立可测) */
|
|
66
|
-
export { buildMappingDataset as buildMappingDatasetForValidator };
|
|
1
|
+
/**
|
|
2
|
+
* loader.js — mapping/ 目录读取与合并
|
|
3
|
+
*
|
|
4
|
+
* 递归遍历 mapping/ 下所有 .json 文件,按目录层级合并为完整映射数据集。
|
|
5
|
+
* 合并顺序(后加载覆盖先加载,同 ID 时场景级扩展全局定义):
|
|
6
|
+
* basic/ → layout/ → component/ → scenes/<scene>/
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
export const MAPPING_ROOT = path.resolve(__dirname, "..", "mapping");
|
|
15
|
+
|
|
16
|
+
/** 递归收集 JSON 文件(按目录名字典序,保证合并顺序稳定) */
|
|
17
|
+
function collectJsonFiles(dir) {
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const name of readdirSync(dir).sort()) {
|
|
20
|
+
if (name.startsWith("_") || name.startsWith("."))
|
|
21
|
+
continue;
|
|
22
|
+
const full = path.join(dir, name);
|
|
23
|
+
const st = statSync(full);
|
|
24
|
+
if (st.isDirectory())
|
|
25
|
+
out.push(...collectJsonFiles(full));
|
|
26
|
+
else if (name.endsWith(".json"))
|
|
27
|
+
out.push(full);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 构建映射数据集。
|
|
34
|
+
* @returns {{ files: Record<string, object>, fileCount: number, ids: Record<string, {file, meta}> }}
|
|
35
|
+
*/
|
|
36
|
+
export function buildMappingDataset() {
|
|
37
|
+
const files = {};
|
|
38
|
+
const idSource = {};
|
|
39
|
+
|
|
40
|
+
for (const filePath of collectJsonFiles(MAPPING_ROOT)) {
|
|
41
|
+
const rel = path.relative(MAPPING_ROOT, filePath).replaceAll("\\", "/");
|
|
42
|
+
let data;
|
|
43
|
+
try {
|
|
44
|
+
data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
throw new Error(`[mapping/${rel}] JSON 解析失败:${e.message}`);
|
|
48
|
+
}
|
|
49
|
+
files[rel] = data;
|
|
50
|
+
|
|
51
|
+
if (data.mappings) {
|
|
52
|
+
for (const id of Object.keys(data.mappings)) {
|
|
53
|
+
if (id.startsWith("$"))
|
|
54
|
+
continue;
|
|
55
|
+
if (idSource[id])
|
|
56
|
+
console.warn(` ⚠ 语义 ID「${id}」在 ${idSource[id].file} 与 ${rel} 中重复,后者覆盖`);
|
|
57
|
+
idSource[id] = { file: rel, meta: data.meta || {} };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { files, fileCount: Object.keys(files).length, idSource };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 供 validator 使用的别名(保持独立可测) */
|
|
66
|
+
export { buildMappingDataset as buildMappingDatasetForValidator };
|
package/parser/validator.js
CHANGED
|
@@ -1,206 +1,206 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* validator.js — 映射规则校验器
|
|
3
|
-
*
|
|
4
|
-
* 对 mapping/ 目录合并后的映射数据集做全量校验:
|
|
5
|
-
* 1. 字段完整性(必须字段存在且类型正确)
|
|
6
|
-
* 2. 枚举合法性(type/forms 值域)
|
|
7
|
-
* 3. 层级一致性(children 引用的 ID 必须存在于同级或同场景映射集)
|
|
8
|
-
* 4. 三字段结构:anchors(kind/layer 枚举 + target 必填)、
|
|
9
|
-
* preconditions(非空字符串数组)、priority(strategy 枚举)
|
|
10
|
-
*
|
|
11
|
-
* 说明:selector / anchors.target 的「挂载点存在性」需要在**目标项目源码**里查证,
|
|
12
|
-
* 本文件只做映射数据的自洽校验;存在性检查由 index.js 的 audit 子命令完成。
|
|
13
|
-
*
|
|
14
|
-
* 校验不通过:抛出带文件定位的错误并 exit 1(阻断构建)。
|
|
15
|
-
*
|
|
16
|
-
* 用法(由 index.js 调用,也可独立执行):
|
|
17
|
-
* node parser/validator.js # 校验 mapping/ 全目录
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { buildMappingDataset } from "./loader.js";
|
|
21
|
-
|
|
22
|
-
/** 映射条目允许的 type 枚举 */
|
|
23
|
-
const TYPE_ENUM = ["module", "component", "element", "global_component", "global_token"];
|
|
24
|
-
|
|
25
|
-
/** 必须字段及其类型 */
|
|
26
|
-
const REQUIRED_FIELDS = {
|
|
27
|
-
type: "string",
|
|
28
|
-
style_scope: "string",
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/** 语义 ID 格式:小写字母数字 + 下划线 + 点分层级,最多 4 段 */
|
|
32
|
-
const ID_PATTERN = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){1,3}$/;
|
|
33
|
-
|
|
34
|
-
/** anchors 承载通道类型枚举(class = 选择器锚点 / css_var = CSS 变量锚点 / attr = 属性锚点) */
|
|
35
|
-
const ANCHOR_KINDS = ["class", "css_var", "attr"];
|
|
36
|
-
|
|
37
|
-
/** 落点层级枚举:host = 宿主盒;inner = 内层文本盒(专治「值对了、位置不对」) */
|
|
38
|
-
const LAYER_ENUM = ["host", "inner"];
|
|
39
|
-
|
|
40
|
-
/** 覆写优先级策略枚举(design-apply 提权依据) */
|
|
41
|
-
const PRIORITY_ENUM = ["scope_qualified", "self_plus_important", "deep_qualified"];
|
|
42
|
-
|
|
43
|
-
export function validateMappingDataset(dataset) {
|
|
44
|
-
const errors = [];
|
|
45
|
-
const warnings = [];
|
|
46
|
-
|
|
47
|
-
for (const [file, data] of Object.entries(dataset.files)) {
|
|
48
|
-
// meta 校验
|
|
49
|
-
if (!data.meta) {
|
|
50
|
-
errors.push(`[${file}] 缺少 meta 节点`);
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
for (const key of ["layer", "version", "tech_stack"]) {
|
|
54
|
-
if (!data.meta[key])
|
|
55
|
-
errors.push(`[${file}] meta 缺少 ${key}`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// tokens 型文件(basic/layout 域):token 值必须是非空对象
|
|
59
|
-
if (data.tokens) {
|
|
60
|
-
for (const [field, def] of Object.entries(data.tokens)) {
|
|
61
|
-
if (field.startsWith("$"))
|
|
62
|
-
continue;
|
|
63
|
-
if (typeof def !== "object" || def === null)
|
|
64
|
-
errors.push(`[${file}] tokens.${field} 必须为对象`);
|
|
65
|
-
else if (!def.token && !def.value && !def.hook)
|
|
66
|
-
errors.push(`[${file}] tokens.${field} 缺少 token/value 定义`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// mappings 型文件(component/scenes 域):条目字段校验
|
|
71
|
-
if (data.mappings) {
|
|
72
|
-
for (const [id, entry] of Object.entries(data.mappings)) {
|
|
73
|
-
if (id.startsWith("$"))
|
|
74
|
-
continue;
|
|
75
|
-
|
|
76
|
-
// ID 命名规范
|
|
77
|
-
if (!ID_PATTERN.test(id))
|
|
78
|
-
errors.push(`[${file}] 语义 ID「${id}」不符合命名规范(小写下划线、2~4 段点分层级)`);
|
|
79
|
-
|
|
80
|
-
// 必须字段
|
|
81
|
-
for (const [field, kind] of Object.entries(REQUIRED_FIELDS)) {
|
|
82
|
-
if (typeof entry[field] !== kind)
|
|
83
|
-
errors.push(`[${file}] ${id} 缺少 ${field} 或类型非 ${kind}`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// type 枚举
|
|
87
|
-
if (entry.type && !TYPE_ENUM.includes(entry.type))
|
|
88
|
-
errors.push(`[${file}] ${id}.type「${entry.type}」不在枚举 ${TYPE_ENUM.join("/")} 内`);
|
|
89
|
-
|
|
90
|
-
// css_vars / custom_style_map 必须为对象(可空对象)
|
|
91
|
-
for (const field of ["css_vars", "custom_style_map"]) {
|
|
92
|
-
if (entry[field] !== undefined && typeof entry[field] !== "object")
|
|
93
|
-
errors.push(`[${file}] ${id}.${field} 必须为对象`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// css_vars 值必须以 -- 开头(EP/项目 CSS 变量约定)
|
|
97
|
-
if (entry.css_vars) {
|
|
98
|
-
for (const [k, v] of Object.entries(entry.css_vars)) {
|
|
99
|
-
if (typeof v === "string" && !v.trim().startsWith("--"))
|
|
100
|
-
errors.push(`[${file}] ${id}.css_vars.${k} 值「${v}」应为 CSS 变量名(-- 开头)`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// anchors( §3.2):条目可承载的全部通道,含落点层级 layer
|
|
105
|
-
if (entry.anchors !== undefined) {
|
|
106
|
-
if (!Array.isArray(entry.anchors)) {
|
|
107
|
-
errors.push(`[${file}] ${id}.anchors 必须为数组`);
|
|
108
|
-
}
|
|
109
|
-
else {
|
|
110
|
-
let hasClassAnchor = false;
|
|
111
|
-
for (const [i, a] of entry.anchors.entries()) {
|
|
112
|
-
const at = `${id}.anchors[${i}]`;
|
|
113
|
-
if (!a || typeof a !== "object") {
|
|
114
|
-
errors.push(`[${file}] ${at} 必须为对象`);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
if (!ANCHOR_KINDS.includes(a.kind))
|
|
118
|
-
errors.push(`[${file}] ${at}.kind「${a.kind}」不在枚举 ${ANCHOR_KINDS.join("/")} 内`);
|
|
119
|
-
if (a.kind === "class")
|
|
120
|
-
hasClassAnchor = true;
|
|
121
|
-
if (a.layer !== undefined && !LAYER_ENUM.includes(a.layer))
|
|
122
|
-
errors.push(`[${file}] ${at}.layer「${a.layer}」不在枚举 ${LAYER_ENUM.join("/")} 内`);
|
|
123
|
-
if (typeof a.target !== "string" || a.target.trim() === "")
|
|
124
|
-
errors.push(`[${file}] ${at}.target 必填且为非空字符串`);
|
|
125
|
-
else if (a.kind === "css_var" && !a.target.trim().startsWith("--"))
|
|
126
|
-
errors.push(`[${file}] ${at}.target「${a.target}」为 css_var 锚点时应为 CSS 变量名(-- 开头)`);
|
|
127
|
-
}
|
|
128
|
-
if (entry.anchors.length > 0 && !hasClassAnchor)
|
|
129
|
-
warnings.push(`[${file}] ${id}.anchors 无 class 锚点,样式类字段可能找不到落点选择器`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// preconditions( §3.2):几何 / 取值成立的宿主前提,非空字符串数组
|
|
134
|
-
if (entry.preconditions !== undefined) {
|
|
135
|
-
if (!Array.isArray(entry.preconditions) || entry.preconditions.length === 0) {
|
|
136
|
-
errors.push(`[${file}] ${id}.preconditions 必须为非空字符串数组`);
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
for (const [i, p] of entry.preconditions.entries()) {
|
|
140
|
-
if (typeof p !== "string" || p.trim() === "")
|
|
141
|
-
errors.push(`[${file}] ${id}.preconditions[${i}] 必须为非空字符串`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// priority( §4.6.4):覆写优先级策略
|
|
147
|
-
if (entry.priority !== undefined) {
|
|
148
|
-
if (typeof entry.priority !== "object" || entry.priority === null) {
|
|
149
|
-
errors.push(`[${file}] ${id}.priority 必须为对象`);
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
if (!PRIORITY_ENUM.includes(entry.priority.strategy))
|
|
153
|
-
errors.push(`[${file}] ${id}.priority.strategy「${entry.priority.strategy}」不在枚举 ${PRIORITY_ENUM.join("/")} 内`);
|
|
154
|
-
if (entry.priority.reason !== undefined && typeof entry.priority.reason !== "string")
|
|
155
|
-
errors.push(`[${file}] ${id}.priority.reason 必须为字符串`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// 跨文件一致性:children 引用必须可解析(同场景合并字典内查找)
|
|
163
|
-
const mergedIndex = buildMergedIndex(dataset);
|
|
164
|
-
for (const [file, data] of Object.entries(dataset.files)) {
|
|
165
|
-
if (!data.mappings)
|
|
166
|
-
continue;
|
|
167
|
-
for (const [id, entry] of Object.entries(data.mappings)) {
|
|
168
|
-
if (id.startsWith("$") || !Array.isArray(entry.children))
|
|
169
|
-
continue;
|
|
170
|
-
for (const childId of entry.children) {
|
|
171
|
-
if (!mergedIndex[childId])
|
|
172
|
-
warnings.push(`[${file}] ${id}.children 引用的「${childId}」未在当前映射集中定义(可能是待补点位)`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return { errors, warnings };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** 合并全部 mappings 形成扁平索引(跨文件) */
|
|
181
|
-
export function buildMergedIndex(dataset) {
|
|
182
|
-
const index = {};
|
|
183
|
-
for (const data of Object.values(dataset.files)) {
|
|
184
|
-
if (!data.mappings)
|
|
185
|
-
continue;
|
|
186
|
-
// 同 ID 后加载覆盖先加载(scenes 覆盖 component 全局定义,属预期扩展机制)
|
|
187
|
-
Object.assign(index, data.mappings);
|
|
188
|
-
}
|
|
189
|
-
return index;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ---- CLI 独立执行 ----
|
|
193
|
-
if (process.argv[1] && process.argv[1].endsWith("validator.js")) {
|
|
194
|
-
const dataset = buildMappingDataset();
|
|
195
|
-
const { errors, warnings } = validateMappingDataset(dataset);
|
|
196
|
-
for (const w of warnings)
|
|
197
|
-
console.warn(` ⚠ ${w}`);
|
|
198
|
-
if (errors.length > 0) {
|
|
199
|
-
console.error(`\n✖ 映射表校验失败(${errors.length} 项):`);
|
|
200
|
-
for (const e of errors)
|
|
201
|
-
console.error(` ✘ ${e}`);
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
204
|
-
const count = Object.keys(buildMergedIndex(dataset)).length;
|
|
205
|
-
console.log(`✔ 映射表校验通过:${dataset.fileCount} 个文件,${count} 条语义映射,${warnings.length} 条警告`);
|
|
206
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* validator.js — 映射规则校验器
|
|
3
|
+
*
|
|
4
|
+
* 对 mapping/ 目录合并后的映射数据集做全量校验:
|
|
5
|
+
* 1. 字段完整性(必须字段存在且类型正确)
|
|
6
|
+
* 2. 枚举合法性(type/forms 值域)
|
|
7
|
+
* 3. 层级一致性(children 引用的 ID 必须存在于同级或同场景映射集)
|
|
8
|
+
* 4. 三字段结构:anchors(kind/layer 枚举 + target 必填)、
|
|
9
|
+
* preconditions(非空字符串数组)、priority(strategy 枚举)
|
|
10
|
+
*
|
|
11
|
+
* 说明:selector / anchors.target 的「挂载点存在性」需要在**目标项目源码**里查证,
|
|
12
|
+
* 本文件只做映射数据的自洽校验;存在性检查由 index.js 的 audit 子命令完成。
|
|
13
|
+
*
|
|
14
|
+
* 校验不通过:抛出带文件定位的错误并 exit 1(阻断构建)。
|
|
15
|
+
*
|
|
16
|
+
* 用法(由 index.js 调用,也可独立执行):
|
|
17
|
+
* node parser/validator.js # 校验 mapping/ 全目录
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { buildMappingDataset } from "./loader.js";
|
|
21
|
+
|
|
22
|
+
/** 映射条目允许的 type 枚举 */
|
|
23
|
+
const TYPE_ENUM = ["module", "component", "element", "global_component", "global_token"];
|
|
24
|
+
|
|
25
|
+
/** 必须字段及其类型 */
|
|
26
|
+
const REQUIRED_FIELDS = {
|
|
27
|
+
type: "string",
|
|
28
|
+
style_scope: "string",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** 语义 ID 格式:小写字母数字 + 下划线 + 点分层级,最多 4 段 */
|
|
32
|
+
const ID_PATTERN = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){1,3}$/;
|
|
33
|
+
|
|
34
|
+
/** anchors 承载通道类型枚举(class = 选择器锚点 / css_var = CSS 变量锚点 / attr = 属性锚点) */
|
|
35
|
+
const ANCHOR_KINDS = ["class", "css_var", "attr"];
|
|
36
|
+
|
|
37
|
+
/** 落点层级枚举:host = 宿主盒;inner = 内层文本盒(专治「值对了、位置不对」) */
|
|
38
|
+
const LAYER_ENUM = ["host", "inner"];
|
|
39
|
+
|
|
40
|
+
/** 覆写优先级策略枚举(design-apply 提权依据) */
|
|
41
|
+
const PRIORITY_ENUM = ["scope_qualified", "self_plus_important", "deep_qualified"];
|
|
42
|
+
|
|
43
|
+
export function validateMappingDataset(dataset) {
|
|
44
|
+
const errors = [];
|
|
45
|
+
const warnings = [];
|
|
46
|
+
|
|
47
|
+
for (const [file, data] of Object.entries(dataset.files)) {
|
|
48
|
+
// meta 校验
|
|
49
|
+
if (!data.meta) {
|
|
50
|
+
errors.push(`[${file}] 缺少 meta 节点`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
for (const key of ["layer", "version", "tech_stack"]) {
|
|
54
|
+
if (!data.meta[key])
|
|
55
|
+
errors.push(`[${file}] meta 缺少 ${key}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// tokens 型文件(basic/layout 域):token 值必须是非空对象
|
|
59
|
+
if (data.tokens) {
|
|
60
|
+
for (const [field, def] of Object.entries(data.tokens)) {
|
|
61
|
+
if (field.startsWith("$"))
|
|
62
|
+
continue;
|
|
63
|
+
if (typeof def !== "object" || def === null)
|
|
64
|
+
errors.push(`[${file}] tokens.${field} 必须为对象`);
|
|
65
|
+
else if (!def.token && !def.value && !def.hook)
|
|
66
|
+
errors.push(`[${file}] tokens.${field} 缺少 token/value 定义`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// mappings 型文件(component/scenes 域):条目字段校验
|
|
71
|
+
if (data.mappings) {
|
|
72
|
+
for (const [id, entry] of Object.entries(data.mappings)) {
|
|
73
|
+
if (id.startsWith("$"))
|
|
74
|
+
continue;
|
|
75
|
+
|
|
76
|
+
// ID 命名规范
|
|
77
|
+
if (!ID_PATTERN.test(id))
|
|
78
|
+
errors.push(`[${file}] 语义 ID「${id}」不符合命名规范(小写下划线、2~4 段点分层级)`);
|
|
79
|
+
|
|
80
|
+
// 必须字段
|
|
81
|
+
for (const [field, kind] of Object.entries(REQUIRED_FIELDS)) {
|
|
82
|
+
if (typeof entry[field] !== kind)
|
|
83
|
+
errors.push(`[${file}] ${id} 缺少 ${field} 或类型非 ${kind}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// type 枚举
|
|
87
|
+
if (entry.type && !TYPE_ENUM.includes(entry.type))
|
|
88
|
+
errors.push(`[${file}] ${id}.type「${entry.type}」不在枚举 ${TYPE_ENUM.join("/")} 内`);
|
|
89
|
+
|
|
90
|
+
// css_vars / custom_style_map 必须为对象(可空对象)
|
|
91
|
+
for (const field of ["css_vars", "custom_style_map"]) {
|
|
92
|
+
if (entry[field] !== undefined && typeof entry[field] !== "object")
|
|
93
|
+
errors.push(`[${file}] ${id}.${field} 必须为对象`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// css_vars 值必须以 -- 开头(EP/项目 CSS 变量约定)
|
|
97
|
+
if (entry.css_vars) {
|
|
98
|
+
for (const [k, v] of Object.entries(entry.css_vars)) {
|
|
99
|
+
if (typeof v === "string" && !v.trim().startsWith("--"))
|
|
100
|
+
errors.push(`[${file}] ${id}.css_vars.${k} 值「${v}」应为 CSS 变量名(-- 开头)`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// anchors( §3.2):条目可承载的全部通道,含落点层级 layer
|
|
105
|
+
if (entry.anchors !== undefined) {
|
|
106
|
+
if (!Array.isArray(entry.anchors)) {
|
|
107
|
+
errors.push(`[${file}] ${id}.anchors 必须为数组`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
let hasClassAnchor = false;
|
|
111
|
+
for (const [i, a] of entry.anchors.entries()) {
|
|
112
|
+
const at = `${id}.anchors[${i}]`;
|
|
113
|
+
if (!a || typeof a !== "object") {
|
|
114
|
+
errors.push(`[${file}] ${at} 必须为对象`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!ANCHOR_KINDS.includes(a.kind))
|
|
118
|
+
errors.push(`[${file}] ${at}.kind「${a.kind}」不在枚举 ${ANCHOR_KINDS.join("/")} 内`);
|
|
119
|
+
if (a.kind === "class")
|
|
120
|
+
hasClassAnchor = true;
|
|
121
|
+
if (a.layer !== undefined && !LAYER_ENUM.includes(a.layer))
|
|
122
|
+
errors.push(`[${file}] ${at}.layer「${a.layer}」不在枚举 ${LAYER_ENUM.join("/")} 内`);
|
|
123
|
+
if (typeof a.target !== "string" || a.target.trim() === "")
|
|
124
|
+
errors.push(`[${file}] ${at}.target 必填且为非空字符串`);
|
|
125
|
+
else if (a.kind === "css_var" && !a.target.trim().startsWith("--"))
|
|
126
|
+
errors.push(`[${file}] ${at}.target「${a.target}」为 css_var 锚点时应为 CSS 变量名(-- 开头)`);
|
|
127
|
+
}
|
|
128
|
+
if (entry.anchors.length > 0 && !hasClassAnchor)
|
|
129
|
+
warnings.push(`[${file}] ${id}.anchors 无 class 锚点,样式类字段可能找不到落点选择器`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// preconditions( §3.2):几何 / 取值成立的宿主前提,非空字符串数组
|
|
134
|
+
if (entry.preconditions !== undefined) {
|
|
135
|
+
if (!Array.isArray(entry.preconditions) || entry.preconditions.length === 0) {
|
|
136
|
+
errors.push(`[${file}] ${id}.preconditions 必须为非空字符串数组`);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
for (const [i, p] of entry.preconditions.entries()) {
|
|
140
|
+
if (typeof p !== "string" || p.trim() === "")
|
|
141
|
+
errors.push(`[${file}] ${id}.preconditions[${i}] 必须为非空字符串`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// priority( §4.6.4):覆写优先级策略
|
|
147
|
+
if (entry.priority !== undefined) {
|
|
148
|
+
if (typeof entry.priority !== "object" || entry.priority === null) {
|
|
149
|
+
errors.push(`[${file}] ${id}.priority 必须为对象`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
if (!PRIORITY_ENUM.includes(entry.priority.strategy))
|
|
153
|
+
errors.push(`[${file}] ${id}.priority.strategy「${entry.priority.strategy}」不在枚举 ${PRIORITY_ENUM.join("/")} 内`);
|
|
154
|
+
if (entry.priority.reason !== undefined && typeof entry.priority.reason !== "string")
|
|
155
|
+
errors.push(`[${file}] ${id}.priority.reason 必须为字符串`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 跨文件一致性:children 引用必须可解析(同场景合并字典内查找)
|
|
163
|
+
const mergedIndex = buildMergedIndex(dataset);
|
|
164
|
+
for (const [file, data] of Object.entries(dataset.files)) {
|
|
165
|
+
if (!data.mappings)
|
|
166
|
+
continue;
|
|
167
|
+
for (const [id, entry] of Object.entries(data.mappings)) {
|
|
168
|
+
if (id.startsWith("$") || !Array.isArray(entry.children))
|
|
169
|
+
continue;
|
|
170
|
+
for (const childId of entry.children) {
|
|
171
|
+
if (!mergedIndex[childId])
|
|
172
|
+
warnings.push(`[${file}] ${id}.children 引用的「${childId}」未在当前映射集中定义(可能是待补点位)`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { errors, warnings };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** 合并全部 mappings 形成扁平索引(跨文件) */
|
|
181
|
+
export function buildMergedIndex(dataset) {
|
|
182
|
+
const index = {};
|
|
183
|
+
for (const data of Object.values(dataset.files)) {
|
|
184
|
+
if (!data.mappings)
|
|
185
|
+
continue;
|
|
186
|
+
// 同 ID 后加载覆盖先加载(scenes 覆盖 component 全局定义,属预期扩展机制)
|
|
187
|
+
Object.assign(index, data.mappings);
|
|
188
|
+
}
|
|
189
|
+
return index;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---- CLI 独立执行 ----
|
|
193
|
+
if (process.argv[1] && process.argv[1].endsWith("validator.js")) {
|
|
194
|
+
const dataset = buildMappingDataset();
|
|
195
|
+
const { errors, warnings } = validateMappingDataset(dataset);
|
|
196
|
+
for (const w of warnings)
|
|
197
|
+
console.warn(` ⚠ ${w}`);
|
|
198
|
+
if (errors.length > 0) {
|
|
199
|
+
console.error(`\n✖ 映射表校验失败(${errors.length} 项):`);
|
|
200
|
+
for (const e of errors)
|
|
201
|
+
console.error(` ✘ ${e}`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
const count = Object.keys(buildMergedIndex(dataset)).length;
|
|
205
|
+
console.log(`✔ 映射表校验通过:${dataset.fileCount} 个文件,${count} 条语义映射,${warnings.length} 条警告`);
|
|
206
|
+
}
|