figma-cache-toolchain 1.4.3 → 1.4.5
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/LICENSE +21 -21
- package/README.md +154 -154
- package/README.owner.md +37 -0
- package/cursor-bootstrap/AGENT-SETUP-PROMPT.md +53 -62
- package/cursor-bootstrap/examples/README.md +5 -3
- package/cursor-bootstrap/figma-cache.config.example.js +238 -138
- package/cursor-bootstrap/rules/01-figma-cache-core.mdc +42 -5
- package/cursor-bootstrap/skills/figma-mcp-local-cache/SKILL.md +10 -5
- package/figma-cache/{README.md → docs/README.md} +44 -20
- package/figma-cache/docs/colleague-guide-zh.md +199 -0
- package/figma-cache/docs/figma-cache-adapter-hint.md +14 -0
- package/figma-cache/docs/quick-start-zh.md +42 -0
- package/figma-cache/figma-cache.js +250 -881
- package/figma-cache/js/backfill-cli.js +50 -0
- package/figma-cache/js/budget-cli.js +108 -0
- package/figma-cache/js/cursor-bootstrap-cli.js +178 -0
- package/figma-cache/js/entry-files.js +149 -0
- package/figma-cache/js/flow-cli.js +227 -0
- package/figma-cache/js/index-store.js +86 -0
- package/figma-cache/js/project-config.js +97 -0
- package/figma-cache/js/upsert-core.js +156 -0
- package/figma-cache/js/validate-cli.js +233 -0
- package/package.json +72 -55
- package/figma-cache/colleague-guide-zh.md +0 -206
- /package/figma-cache/{flow-edge-taxonomy.md → docs/flow-edge-taxonomy.md} +0 -0
- /package/figma-cache/{link-normalization-spec.md → docs/link-normalization-spec.md} +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
|
|
3
|
+
function collectMarkdownFiles(dir, deps) {
|
|
4
|
+
if (!deps.fs.existsSync(dir)) {
|
|
5
|
+
return [];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const output = [];
|
|
9
|
+
const list = deps.fs.readdirSync(dir, { withFileTypes: true });
|
|
10
|
+
list.forEach((entry) => {
|
|
11
|
+
const fullPath = deps.path.join(dir, entry.name);
|
|
12
|
+
if (entry.isDirectory()) {
|
|
13
|
+
output.push(...collectMarkdownFiles(fullPath, deps));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
17
|
+
output.push(fullPath);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return output;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractFigmaUrls(content) {
|
|
24
|
+
const pattern = /https:\/\/www\.figma\.com\/(?:file|design)\/[^\s)\]]+/g;
|
|
25
|
+
return content.match(pattern) || [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function backfillFromIterations(options, deps) {
|
|
29
|
+
const files = collectMarkdownFiles(options.iterationsDir, deps);
|
|
30
|
+
let hit = 0;
|
|
31
|
+
|
|
32
|
+
files.forEach((filePath) => {
|
|
33
|
+
const content = deps.fs.readFileSync(filePath, "utf8");
|
|
34
|
+
const urls = extractFigmaUrls(content);
|
|
35
|
+
urls.forEach((url) => {
|
|
36
|
+
try {
|
|
37
|
+
deps.upsertByUrl(url, { source: "backfill", completeness: [] });
|
|
38
|
+
hit += 1;
|
|
39
|
+
} catch {
|
|
40
|
+
// 忽略无法解析的 URL
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
console.log(`Backfill done. scanned files=${files.length}, urls=${hit}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
backfillFromIterations,
|
|
50
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
|
|
3
|
+
function parsePositiveIntOr(input, fallback) {
|
|
4
|
+
const n = Number(input);
|
|
5
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function buildBudgetReport(options, deps) {
|
|
9
|
+
const index = deps.normalizeIndexShape(deps.readIndex());
|
|
10
|
+
const items = index.items || {};
|
|
11
|
+
const keys = Object.keys(items);
|
|
12
|
+
|
|
13
|
+
const filteredKeys = keys.filter((cacheKey) => {
|
|
14
|
+
if (options.cacheKey && cacheKey !== options.cacheKey) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (options.mcpOnly) {
|
|
18
|
+
const item = items[cacheKey];
|
|
19
|
+
return item && item.source === "figma-mcp";
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const entries = filteredKeys.map((cacheKey) => {
|
|
25
|
+
const item = items[cacheKey];
|
|
26
|
+
const metaAbs = deps.resolveMaybeAbsolutePath(item.paths.meta);
|
|
27
|
+
const nodeDir = deps.path.dirname(metaAbs);
|
|
28
|
+
const mcpRawDir = deps.path.join(nodeDir, "mcp-raw");
|
|
29
|
+
const manifestAbs = deps.path.join(mcpRawDir, "mcp-raw-manifest.json");
|
|
30
|
+
const manifest = deps.safeReadJson(manifestAbs);
|
|
31
|
+
const filesMap = manifest && manifest.files && typeof manifest.files === "object" ? manifest.files : {};
|
|
32
|
+
const fileEntries = Object.entries(filesMap);
|
|
33
|
+
|
|
34
|
+
const mcpRawBytes = fileEntries.reduce((acc, [, fileName]) => {
|
|
35
|
+
const abs = deps.path.join(mcpRawDir, String(fileName));
|
|
36
|
+
return acc + deps.safeFileSize(abs);
|
|
37
|
+
}, 0);
|
|
38
|
+
|
|
39
|
+
const mcpRawFilesCount = fileEntries.length;
|
|
40
|
+
const toolCalls =
|
|
41
|
+
manifest && manifest.toolCalls && typeof manifest.toolCalls === "object"
|
|
42
|
+
? manifest.toolCalls
|
|
43
|
+
: {};
|
|
44
|
+
const toolCallCount = Object.values(toolCalls).reduce((acc, v) => {
|
|
45
|
+
const count = v && typeof v === "object" ? Number(v.count) : 0;
|
|
46
|
+
return acc + (Number.isFinite(count) ? count : 0);
|
|
47
|
+
}, 0);
|
|
48
|
+
|
|
49
|
+
const designContextFile =
|
|
50
|
+
filesMap && Object.prototype.hasOwnProperty.call(filesMap, "get_design_context")
|
|
51
|
+
? String(filesMap.get_design_context)
|
|
52
|
+
: "";
|
|
53
|
+
const designContextChars = designContextFile
|
|
54
|
+
? deps.safeFileSize(deps.path.join(mcpRawDir, designContextFile))
|
|
55
|
+
: 0;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
cacheKey,
|
|
59
|
+
source: item.source || "manual",
|
|
60
|
+
completeness: Array.isArray(item.completeness) ? item.completeness : [],
|
|
61
|
+
hasMcpRawManifest: !!manifest,
|
|
62
|
+
mcpRawFilesCount,
|
|
63
|
+
mcpRawBytes,
|
|
64
|
+
tokenProxyBytes: designContextChars,
|
|
65
|
+
tokenProxyChars: designContextChars,
|
|
66
|
+
toolCallCount,
|
|
67
|
+
toolCalls,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
entries.sort((a, b) => b.mcpRawBytes - a.mcpRawBytes);
|
|
72
|
+
const limit = parsePositiveIntOr(options.limit, entries.length);
|
|
73
|
+
const limitedEntries = entries.slice(0, limit);
|
|
74
|
+
const totals = limitedEntries.reduce(
|
|
75
|
+
(acc, e) => {
|
|
76
|
+
acc.nodes += 1;
|
|
77
|
+
acc.nodesWithMcpRaw += e.hasMcpRawManifest ? 1 : 0;
|
|
78
|
+
acc.mcpRawBytes += e.mcpRawBytes;
|
|
79
|
+
acc.tokenProxyBytes += e.tokenProxyBytes;
|
|
80
|
+
acc.toolCallCount += e.toolCallCount;
|
|
81
|
+
return acc;
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
nodes: 0,
|
|
85
|
+
nodesWithMcpRaw: 0,
|
|
86
|
+
mcpRawBytes: 0,
|
|
87
|
+
tokenProxyBytes: 0,
|
|
88
|
+
toolCallCount: 0,
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
totals.tokenProxyChars = totals.tokenProxyBytes;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
generatedAt: new Date().toISOString(),
|
|
96
|
+
filters: {
|
|
97
|
+
cacheKey: options.cacheKey || null,
|
|
98
|
+
mcpOnly: !!options.mcpOnly,
|
|
99
|
+
limit,
|
|
100
|
+
},
|
|
101
|
+
totals,
|
|
102
|
+
entries: limitedEntries,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
buildBudgetReport,
|
|
108
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
|
|
3
|
+
function readUtf8IfExists(fs, absPath) {
|
|
4
|
+
if (!fs.existsSync(absPath)) {
|
|
5
|
+
return "";
|
|
6
|
+
}
|
|
7
|
+
return fs.readFileSync(absPath, "utf8");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function copyCursorBootstrap(force, deps) {
|
|
11
|
+
const {
|
|
12
|
+
fs,
|
|
13
|
+
path,
|
|
14
|
+
ROOT,
|
|
15
|
+
CACHE_DIR,
|
|
16
|
+
CURSOR_BOOTSTRAP_DIR,
|
|
17
|
+
normalizeSlash,
|
|
18
|
+
readSelfNpmPackageName,
|
|
19
|
+
packageDir,
|
|
20
|
+
} = deps;
|
|
21
|
+
|
|
22
|
+
const pairs = [
|
|
23
|
+
{
|
|
24
|
+
from: path.join("rules", "01-figma-cache-core.mdc"),
|
|
25
|
+
to: path.join(".cursor", "rules", "01-figma-cache-core.mdc"),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
from: path.join("rules", "02-figma-stack-adapter.mdc"),
|
|
29
|
+
to: path.join(".cursor", "rules", "02-figma-stack-adapter.mdc"),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
from: path.join("rules", "figma-local-cache-first.mdc"),
|
|
33
|
+
to: path.join(".cursor", "rules", "figma-local-cache-first.mdc"),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
from: path.join("skills", "figma-mcp-local-cache", "SKILL.md"),
|
|
37
|
+
to: path.join(".cursor", "skills", "figma-mcp-local-cache", "SKILL.md"),
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
if (!fs.existsSync(CURSOR_BOOTSTRAP_DIR)) {
|
|
42
|
+
console.error(
|
|
43
|
+
`[figma-cache] cursor-bootstrap not found at ${normalizeSlash(CURSOR_BOOTSTRAP_DIR)} (broken package install?)`
|
|
44
|
+
);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let copied = 0;
|
|
49
|
+
let skipped = 0;
|
|
50
|
+
pairs.forEach(({ from: relFrom, to: relTo }) => {
|
|
51
|
+
const absFrom = path.join(CURSOR_BOOTSTRAP_DIR, relFrom);
|
|
52
|
+
const absTo = path.join(ROOT, relTo);
|
|
53
|
+
if (!fs.existsSync(absFrom)) {
|
|
54
|
+
console.error(`[figma-cache] missing template file: ${normalizeSlash(absFrom)}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
fs.mkdirSync(path.dirname(absTo), { recursive: true });
|
|
58
|
+
if (fs.existsSync(absTo) && !force) {
|
|
59
|
+
skipped += 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
fs.copyFileSync(absFrom, absTo);
|
|
63
|
+
copied += 1;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const configTemplatePath = path.join(CURSOR_BOOTSTRAP_DIR, "figma-cache.config.example.js");
|
|
67
|
+
const projectConfigPath = path.join(ROOT, "figma-cache.config.js");
|
|
68
|
+
const legacyExamplePath = path.join(ROOT, "figma-cache.config.example.js");
|
|
69
|
+
|
|
70
|
+
if (!fs.existsSync(configTemplatePath)) {
|
|
71
|
+
console.error(`[figma-cache] missing template file: ${normalizeSlash(configTemplatePath)}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const hadProjectConfig = fs.existsSync(projectConfigPath);
|
|
76
|
+
const hadLegacyExample = fs.existsSync(legacyExamplePath);
|
|
77
|
+
const configTemplateBody = fs.readFileSync(configTemplatePath, "utf8");
|
|
78
|
+
|
|
79
|
+
let configAction = "skipped";
|
|
80
|
+
let configSource = "existing";
|
|
81
|
+
if (hadProjectConfig && !force) {
|
|
82
|
+
configAction = "skipped";
|
|
83
|
+
configSource = "existing";
|
|
84
|
+
} else if (!hadProjectConfig && hadLegacyExample && !force) {
|
|
85
|
+
fs.copyFileSync(legacyExamplePath, projectConfigPath);
|
|
86
|
+
configAction = "created";
|
|
87
|
+
configSource = "legacy-example";
|
|
88
|
+
} else {
|
|
89
|
+
fs.writeFileSync(projectConfigPath, configTemplateBody, "utf8");
|
|
90
|
+
configAction = hadProjectConfig ? "overwritten" : "created";
|
|
91
|
+
configSource = "template";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let legacyExampleStatus = "not-found";
|
|
95
|
+
if (fs.existsSync(legacyExamplePath)) {
|
|
96
|
+
const legacyBody = readUtf8IfExists(fs, legacyExamplePath);
|
|
97
|
+
const projectBody = readUtf8IfExists(fs, projectConfigPath);
|
|
98
|
+
const sameAsTemplate = legacyBody === configTemplateBody;
|
|
99
|
+
const sameAsProject = projectBody && legacyBody === projectBody;
|
|
100
|
+
if (sameAsTemplate || sameAsProject) {
|
|
101
|
+
fs.unlinkSync(legacyExamplePath);
|
|
102
|
+
legacyExampleStatus = "deleted";
|
|
103
|
+
} else {
|
|
104
|
+
legacyExampleStatus = "kept-customized";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const agentSrc = path.join(CURSOR_BOOTSTRAP_DIR, "AGENT-SETUP-PROMPT.md");
|
|
109
|
+
const agentDest = path.join(ROOT, "AGENT-SETUP-PROMPT.md");
|
|
110
|
+
if (!fs.existsSync(agentSrc)) {
|
|
111
|
+
console.error(`[figma-cache] missing ${normalizeSlash(agentSrc)}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let agentBody = fs.readFileSync(agentSrc, "utf8");
|
|
116
|
+
const npmPkg = readSelfNpmPackageName();
|
|
117
|
+
agentBody = agentBody.replace(/\{\{NPM_PACKAGE_NAME\}\}/g, npmPkg);
|
|
118
|
+
fs.writeFileSync(agentDest, agentBody, "utf8");
|
|
119
|
+
|
|
120
|
+
const colleagueSrc = path.join(packageDir, "docs", "colleague-guide-zh.md");
|
|
121
|
+
const colleagueDest = path.join(CACHE_DIR, "docs", "colleague-guide-zh.md");
|
|
122
|
+
if (!fs.existsSync(colleagueSrc)) {
|
|
123
|
+
console.error(`[figma-cache] missing ${normalizeSlash(colleagueSrc)} (broken package install?)`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
const colleagueSameFile = path.resolve(colleagueSrc) === path.resolve(colleagueDest);
|
|
127
|
+
if (!colleagueSameFile) {
|
|
128
|
+
fs.mkdirSync(path.dirname(colleagueDest), { recursive: true });
|
|
129
|
+
fs.copyFileSync(colleagueSrc, colleagueDest);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(
|
|
133
|
+
JSON.stringify(
|
|
134
|
+
{
|
|
135
|
+
ok: true,
|
|
136
|
+
root: normalizeSlash(ROOT),
|
|
137
|
+
copied,
|
|
138
|
+
skipped,
|
|
139
|
+
force: !!force,
|
|
140
|
+
hint: skipped
|
|
141
|
+
? "Some template files were skipped (already exist). Re-run with --force to overwrite."
|
|
142
|
+
: "Done.",
|
|
143
|
+
configFile: normalizeSlash(projectConfigPath),
|
|
144
|
+
configAction,
|
|
145
|
+
configSource,
|
|
146
|
+
legacyExampleFile: normalizeSlash(legacyExamplePath),
|
|
147
|
+
legacyExampleStatus,
|
|
148
|
+
agentPromptFile: normalizeSlash(agentDest),
|
|
149
|
+
colleagueGuideFile: normalizeSlash(colleagueDest),
|
|
150
|
+
colleagueGuideSynced: !colleagueSameFile,
|
|
151
|
+
colleagueGuideNote: colleagueSameFile
|
|
152
|
+
? "colleague-guide-zh.md already at package path (toolchain dev tree); no copy."
|
|
153
|
+
: "colleague-guide-zh.md refreshed under FIGMA_CACHE_DIR/docs (default figma-cache/docs/).",
|
|
154
|
+
agentPromptNote:
|
|
155
|
+
"AGENT-SETUP-PROMPT.md is refreshed every run. Next: @ it in Cursor; after Agent finishes, run npm run figma:cache:init (or npx figma-cache init if scripts are missing).",
|
|
156
|
+
npmPackageName: npmPkg,
|
|
157
|
+
},
|
|
158
|
+
null,
|
|
159
|
+
2
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
console.log(
|
|
163
|
+
"\n" +
|
|
164
|
+
"================================================================\n" +
|
|
165
|
+
"下一步(请按顺序):\n" +
|
|
166
|
+
"1) 在 Cursor 对话中输入 @AGENT-SETUP-PROMPT.md,并说明「按该文档执行」\n" +
|
|
167
|
+
" (每次 cursor init 都会刷新该文件;无需再整篇粘贴。)\n" +
|
|
168
|
+
"2) 待 Agent 完成后,在项目根初始化本地缓存索引:\n" +
|
|
169
|
+
" npm run figma:cache:init\n" +
|
|
170
|
+
" 若尚未补全 npm scripts,请改用:npx figma-cache init\n" +
|
|
171
|
+
"================================================================\n"
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
copyCursorBootstrap,
|
|
177
|
+
};
|
|
178
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
|
|
3
|
+
function createEntryFilesService(deps) {
|
|
4
|
+
const {
|
|
5
|
+
fs,
|
|
6
|
+
path,
|
|
7
|
+
resolveMaybeAbsolutePath,
|
|
8
|
+
normalizeCompletenessList,
|
|
9
|
+
completenessAllDimensions,
|
|
10
|
+
runPostEnsureHook,
|
|
11
|
+
} = deps;
|
|
12
|
+
|
|
13
|
+
function ensureFileWithDefault(relativePath, fallbackContent) {
|
|
14
|
+
const absPath = resolveMaybeAbsolutePath(relativePath);
|
|
15
|
+
const dir = path.dirname(absPath);
|
|
16
|
+
if (!fs.existsSync(dir)) {
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
if (!fs.existsSync(absPath)) {
|
|
20
|
+
fs.writeFileSync(absPath, fallbackContent, "utf8");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildCoverageSummary(completeness) {
|
|
25
|
+
const covered = normalizeCompletenessList(completeness);
|
|
26
|
+
const missing = completenessAllDimensions.filter((dim) => !covered.includes(dim));
|
|
27
|
+
return {
|
|
28
|
+
covered,
|
|
29
|
+
missing,
|
|
30
|
+
evidence: {
|
|
31
|
+
layout: covered.includes("layout") ? ["spec.md#layout"] : [],
|
|
32
|
+
text: covered.includes("text") ? ["spec.md#text"] : [],
|
|
33
|
+
tokens: covered.includes("tokens") ? ["spec.md#tokens"] : [],
|
|
34
|
+
interactions: covered.includes("interactions") ? ["state-map.md#interactions"] : [],
|
|
35
|
+
states: covered.includes("states") ? ["state-map.md#states"] : [],
|
|
36
|
+
accessibility: covered.includes("accessibility")
|
|
37
|
+
? ["state-map.md#accessibility"]
|
|
38
|
+
: [],
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildDefaultSpecContent(item) {
|
|
44
|
+
const completeness = normalizeCompletenessList(item.completeness);
|
|
45
|
+
return (
|
|
46
|
+
`# Figma Spec\n\n` +
|
|
47
|
+
`- fileKey: ${item.fileKey}\n` +
|
|
48
|
+
`- scope: ${item.scope}\n` +
|
|
49
|
+
`- nodeId: ${item.nodeId || "N/A"}\n` +
|
|
50
|
+
`- source: ${item.source}\n` +
|
|
51
|
+
`- syncedAt: ${item.syncedAt}\n` +
|
|
52
|
+
`- completeness: ${completeness.join(", ") || "N/A"}\n\n` +
|
|
53
|
+
`## Layout(结构)\n\n` +
|
|
54
|
+
`- TODO: 补充布局结构与关键尺寸。\n\n` +
|
|
55
|
+
`## Text(文案)\n\n` +
|
|
56
|
+
`- TODO: 补充关键文案与语义。\n\n` +
|
|
57
|
+
`## Tokens(变量 / 样式)\n\n` +
|
|
58
|
+
`- TODO: 补充颜色、字体、间距等 token 映射。\n\n` +
|
|
59
|
+
`## Interactions(交互)\n\n` +
|
|
60
|
+
`- TODO: 补充触发条件、状态流转、键盘行为。\n\n` +
|
|
61
|
+
`## States(状态)\n\n` +
|
|
62
|
+
`- TODO: 补充 default / hover / active / focus / disabled 等状态定义。\n\n` +
|
|
63
|
+
`## Accessibility(可访问性)\n\n` +
|
|
64
|
+
`- TODO: 补充 ARIA、焦点顺序、读屏文案和对比度要求。\n`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildDefaultStateMapContent(item) {
|
|
69
|
+
return (
|
|
70
|
+
`# State Map\n\n` +
|
|
71
|
+
`- cacheKey: ${item.fileKey}#${item.nodeId || "__FILE__"}\n` +
|
|
72
|
+
`- completeness: ${normalizeCompletenessList(item.completeness).join(", ") || "N/A"}\n\n` +
|
|
73
|
+
`## Interactions\n\n` +
|
|
74
|
+
`| Trigger | From | To | Notes |\n` +
|
|
75
|
+
`| --- | --- | --- | --- |\n` +
|
|
76
|
+
`| TODO | default | TODO | 补充点击/键盘/失焦行为 |\n\n` +
|
|
77
|
+
`## States\n\n` +
|
|
78
|
+
`| State | Visual | Data | Notes |\n` +
|
|
79
|
+
`| --- | --- | --- | --- |\n` +
|
|
80
|
+
`| default | TODO | TODO | 初始态 |\n` +
|
|
81
|
+
`| hover | TODO | TODO | 悬停态 |\n` +
|
|
82
|
+
`| active | TODO | TODO | 激活态 |\n` +
|
|
83
|
+
`| focus | TODO | TODO | 焦点态 |\n` +
|
|
84
|
+
`| disabled | TODO | TODO | 禁用态 |\n\n` +
|
|
85
|
+
`## Accessibility\n\n` +
|
|
86
|
+
`- TODO: 补充 role / aria-* / tab 顺序 / 键盘行为。\n`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildDefaultRawContent(item) {
|
|
91
|
+
const completeness = normalizeCompletenessList(item.completeness);
|
|
92
|
+
return `${JSON.stringify(
|
|
93
|
+
{
|
|
94
|
+
source: item.source,
|
|
95
|
+
fileKey: item.fileKey,
|
|
96
|
+
nodeId: item.nodeId,
|
|
97
|
+
scope: item.scope,
|
|
98
|
+
syncedAt: item.syncedAt,
|
|
99
|
+
completeness,
|
|
100
|
+
coverageSummary: buildCoverageSummary(completeness),
|
|
101
|
+
interactions: {
|
|
102
|
+
notes: "TODO: 补充点击、键盘、失焦、外部点击等交互规则。",
|
|
103
|
+
},
|
|
104
|
+
states: {
|
|
105
|
+
notes: "TODO: 补充状态矩阵(default/hover/active/focus/disabled)。",
|
|
106
|
+
},
|
|
107
|
+
accessibility: {
|
|
108
|
+
notes: "TODO: 补充 ARIA、焦点管理、读屏文本、无障碍要求。",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
null,
|
|
112
|
+
2
|
|
113
|
+
)}\n`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function ensureEntryFiles(item) {
|
|
117
|
+
ensureFileWithDefault(
|
|
118
|
+
item.paths.meta,
|
|
119
|
+
`${JSON.stringify(
|
|
120
|
+
{
|
|
121
|
+
fileKey: item.fileKey,
|
|
122
|
+
nodeId: item.nodeId,
|
|
123
|
+
scope: item.scope,
|
|
124
|
+
source: item.source,
|
|
125
|
+
syncedAt: item.syncedAt,
|
|
126
|
+
completeness: normalizeCompletenessList(item.completeness),
|
|
127
|
+
},
|
|
128
|
+
null,
|
|
129
|
+
2
|
|
130
|
+
)}\n`
|
|
131
|
+
);
|
|
132
|
+
ensureFileWithDefault(item.paths.spec, buildDefaultSpecContent(item));
|
|
133
|
+
ensureFileWithDefault(item.paths.stateMap, buildDefaultStateMapContent(item));
|
|
134
|
+
ensureFileWithDefault(item.paths.raw, buildDefaultRawContent(item));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ensureEntryFilesAndHook(cacheKey, item) {
|
|
138
|
+
ensureEntryFiles(item);
|
|
139
|
+
runPostEnsureHook(cacheKey, item);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
ensureEntryFilesAndHook,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
createEntryFilesService,
|
|
149
|
+
};
|