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.
@@ -0,0 +1,227 @@
1
+ /* eslint-disable no-console */
2
+
3
+ function slugifyFlowId(name) {
4
+ const raw = String(name || "")
5
+ .trim()
6
+ .toLowerCase()
7
+ .replace(/[^a-z0-9]+/g, "-")
8
+ .replace(/^-+|-+$/g, "");
9
+ return raw || `flow-${Date.now()}`;
10
+ }
11
+
12
+ function ensureFlow(index, flowId, meta, normalizeIndexShape) {
13
+ const normalized = normalizeIndexShape(index);
14
+ normalized.flows = normalized.flows || {};
15
+ if (!normalized.flows[flowId]) {
16
+ normalized.flows[flowId] = {
17
+ id: flowId,
18
+ title: meta && meta.title ? meta.title : flowId,
19
+ description: meta && meta.description ? meta.description : "",
20
+ createdAt: new Date().toISOString(),
21
+ updatedAt: new Date().toISOString(),
22
+ nodes: [],
23
+ edges: [],
24
+ assumptions: [],
25
+ openQuestions: [],
26
+ };
27
+ }
28
+ return normalized.flows[flowId];
29
+ }
30
+
31
+ function upsertFlowNode(index, flowId, cacheKey, normalizeIndexShape) {
32
+ const flow = ensureFlow(index, flowId, {}, normalizeIndexShape);
33
+ if (!flow.nodes.includes(cacheKey)) {
34
+ flow.nodes.push(cacheKey);
35
+ }
36
+ flow.updatedAt = new Date().toISOString();
37
+ }
38
+
39
+ function addFlowEdge(index, flowId, fromKey, toKey, type, note, normalizeIndexShape) {
40
+ const flow = ensureFlow(index, flowId, {}, normalizeIndexShape);
41
+ const edge = {
42
+ id: `${fromKey}->${toKey}:${type}:${Date.now()}`,
43
+ from: fromKey,
44
+ to: toKey,
45
+ type,
46
+ note: note || "",
47
+ createdAt: new Date().toISOString(),
48
+ };
49
+ flow.edges.push(edge);
50
+ flow.updatedAt = new Date().toISOString();
51
+ }
52
+
53
+ function handleFlowCommand(args, deps) {
54
+ const {
55
+ resolveFlowIdFromArgs,
56
+ parseCompletenessFromArgs,
57
+ normalizeIndexShape,
58
+ readIndex,
59
+ writeIndex,
60
+ normalizeFigmaUrl,
61
+ getItem,
62
+ upsertByUrl,
63
+ ensureEntryFilesAndHook,
64
+ } = deps;
65
+
66
+ const sub = args[0];
67
+ const rest = args.slice(1);
68
+
69
+ if (!sub) {
70
+ console.error("Missing flow subcommand");
71
+ process.exit(1);
72
+ }
73
+
74
+ if (sub === "init") {
75
+ const idArg = rest.find((x) => x.startsWith("--id="));
76
+ const titleArg = rest.find((x) => x.startsWith("--title="));
77
+ const descArg = rest.find((x) => x.startsWith("--description="));
78
+ const flowId = idArg ? idArg.split("=")[1] : slugifyFlowId("flow");
79
+ const title = titleArg ? titleArg.split("=").slice(1).join("=") : flowId;
80
+ const description = descArg ? descArg.split("=").slice(1).join("=") : "";
81
+ const index = normalizeIndexShape(readIndex());
82
+ ensureFlow(index, flowId, { title, description }, normalizeIndexShape);
83
+ writeIndex(index);
84
+ console.log(JSON.stringify({ flowId, created: true }, null, 2));
85
+ return;
86
+ }
87
+
88
+ if (sub === "add-node") {
89
+ const flowId = resolveFlowIdFromArgs(rest);
90
+ if (!flowId) {
91
+ console.error("Missing --flow=<flowId> or env FIGMA_DEFAULT_FLOW");
92
+ process.exit(1);
93
+ }
94
+ const url = rest.find((x) => !x.startsWith("--"));
95
+ const ensureArg = rest.includes("--ensure");
96
+ const sourceArg = rest.find((x) => x.startsWith("--source="));
97
+ const source = sourceArg ? sourceArg.split("=")[1] : "manual";
98
+ const { completeness } = parseCompletenessFromArgs(rest);
99
+ const index = normalizeIndexShape(readIndex());
100
+ const normalized = normalizeFigmaUrl(url);
101
+ if (!ensureArg && !getItem(index, normalized.cacheKey)) {
102
+ console.error(
103
+ `Missing cache item for ${normalized.cacheKey}. Run figma:cache:ensure first, or pass --ensure.`
104
+ );
105
+ process.exit(2);
106
+ }
107
+ if (ensureArg) {
108
+ upsertByUrl(url, { source, completeness });
109
+ const refreshed = normalizeIndexShape(readIndex());
110
+ const item = getItem(refreshed, normalized.cacheKey);
111
+ if (item) {
112
+ ensureEntryFilesAndHook(normalized.cacheKey, item);
113
+ }
114
+ Object.assign(index, refreshed);
115
+ }
116
+ upsertFlowNode(index, flowId, normalized.cacheKey, normalizeIndexShape);
117
+ writeIndex(index);
118
+ console.log(
119
+ JSON.stringify(
120
+ { flowId, cacheKey: normalized.cacheKey, added: true, ensured: ensureArg },
121
+ null,
122
+ 2
123
+ )
124
+ );
125
+ return;
126
+ }
127
+
128
+ if (sub === "link") {
129
+ const flowId = resolveFlowIdFromArgs(rest);
130
+ const typeArg = rest.find((x) => x.startsWith("--type="));
131
+ const noteArg = rest.find((x) => x.startsWith("--note="));
132
+ const urls = rest.filter((x) => !x.startsWith("--"));
133
+ if (!flowId) {
134
+ console.error("Missing --flow=<flowId> or env FIGMA_DEFAULT_FLOW");
135
+ process.exit(1);
136
+ }
137
+ if (urls.length < 2) {
138
+ console.error("Missing <fromUrl> <toUrl>");
139
+ process.exit(1);
140
+ }
141
+ const type = typeArg ? typeArg.split("=")[1] : "related";
142
+ const note = noteArg ? noteArg.split("=").slice(1).join("=") : "";
143
+ const from = normalizeFigmaUrl(urls[0]).cacheKey;
144
+ const to = normalizeFigmaUrl(urls[1]).cacheKey;
145
+ const index = normalizeIndexShape(readIndex());
146
+ if (!getItem(index, from) || !getItem(index, to)) {
147
+ console.error("Missing cache item for from/to. Cache urls first with ensure/upsert.");
148
+ process.exit(2);
149
+ }
150
+ upsertFlowNode(index, flowId, from, normalizeIndexShape);
151
+ upsertFlowNode(index, flowId, to, normalizeIndexShape);
152
+ addFlowEdge(index, flowId, from, to, type, note, normalizeIndexShape);
153
+ writeIndex(index);
154
+ console.log(JSON.stringify({ flowId, from, to, type, linked: true }, null, 2));
155
+ return;
156
+ }
157
+
158
+ if (sub === "chain") {
159
+ const flowId = resolveFlowIdFromArgs(rest);
160
+ const typeArg = rest.find((x) => x.startsWith("--type="));
161
+ const type = typeArg ? typeArg.split("=")[1] : "related";
162
+ const urls = rest.filter((x) => !x.startsWith("--"));
163
+ if (!flowId) {
164
+ console.error("Missing --flow=<flowId> or env FIGMA_DEFAULT_FLOW");
165
+ process.exit(1);
166
+ }
167
+ if (urls.length < 2) {
168
+ console.error("Need at least 2 urls");
169
+ process.exit(1);
170
+ }
171
+ const index = normalizeIndexShape(readIndex());
172
+ const keys = urls.map((u) => normalizeFigmaUrl(u).cacheKey);
173
+ keys.forEach((k) => {
174
+ if (!getItem(index, k)) {
175
+ console.error(`Missing cache item for ${k}. Ensure each url is cached first.`);
176
+ process.exit(2);
177
+ }
178
+ });
179
+ keys.forEach((k) => upsertFlowNode(index, flowId, k, normalizeIndexShape));
180
+ for (let i = 0; i < keys.length - 1; i += 1) {
181
+ addFlowEdge(index, flowId, keys[i], keys[i + 1], type, "", normalizeIndexShape);
182
+ }
183
+ writeIndex(index);
184
+ console.log(JSON.stringify({ flowId, chained: keys.length - 1, type }, null, 2));
185
+ return;
186
+ }
187
+
188
+ if (sub === "show") {
189
+ const flowId = resolveFlowIdFromArgs(rest);
190
+ if (!flowId) {
191
+ console.error("Missing --flow=<flowId> or env FIGMA_DEFAULT_FLOW");
192
+ process.exit(1);
193
+ }
194
+ const index = normalizeIndexShape(readIndex());
195
+ const flow = index.flows[flowId];
196
+ console.log(JSON.stringify({ flowId, flow: flow || null }, null, 2));
197
+ return;
198
+ }
199
+
200
+ if (sub === "mermaid") {
201
+ const flowId = resolveFlowIdFromArgs(rest);
202
+ if (!flowId) {
203
+ console.error("Missing --flow=<flowId> or env FIGMA_DEFAULT_FLOW");
204
+ process.exit(1);
205
+ }
206
+ const index = normalizeIndexShape(readIndex());
207
+ const flow = index.flows[flowId];
208
+ if (!flow) {
209
+ console.error(`Unknown flow: ${flowId}`);
210
+ process.exit(1);
211
+ }
212
+ const lines = ["flowchart LR"];
213
+ (flow.edges || []).forEach((edge) => {
214
+ const label = edge.type || "edge";
215
+ lines.push(` ${edge.from} -->|${label}| ${edge.to}`);
216
+ });
217
+ console.log(lines.join("\n"));
218
+ return;
219
+ }
220
+
221
+ console.error(`Unknown flow subcommand: ${sub}`);
222
+ process.exit(1);
223
+ }
224
+
225
+ module.exports = {
226
+ handleFlowCommand,
227
+ };
@@ -0,0 +1,86 @@
1
+ /* eslint-disable no-console */
2
+
3
+ function createIndexStore(options) {
4
+ const {
5
+ fs,
6
+ CACHE_DIR,
7
+ INDEX_PATH,
8
+ SCHEMA_VERSION,
9
+ NORMALIZATION_VERSION,
10
+ } = options;
11
+
12
+ function ensureCacheDir() {
13
+ if (!fs.existsSync(CACHE_DIR)) {
14
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
15
+ }
16
+ }
17
+
18
+ function buildEmptyIndex() {
19
+ return {
20
+ schemaVersion: SCHEMA_VERSION,
21
+ version: 1,
22
+ normalizationVersion: NORMALIZATION_VERSION,
23
+ updatedAt: null,
24
+ flows: {},
25
+ items: {},
26
+ };
27
+ }
28
+
29
+ function normalizeIndexShape(index) {
30
+ if (!index || typeof index !== "object") {
31
+ return buildEmptyIndex();
32
+ }
33
+ if (!index.schemaVersion) {
34
+ index.schemaVersion = SCHEMA_VERSION;
35
+ }
36
+ if (!index.flows || typeof index.flows !== "object") {
37
+ index.flows = {};
38
+ }
39
+ if (!index.items || typeof index.items !== "object") {
40
+ index.items = {};
41
+ }
42
+ if (!index.version) {
43
+ index.version = 1;
44
+ }
45
+ if (!index.normalizationVersion) {
46
+ index.normalizationVersion = NORMALIZATION_VERSION;
47
+ }
48
+ return index;
49
+ }
50
+
51
+ function readIndex() {
52
+ ensureCacheDir();
53
+ if (!fs.existsSync(INDEX_PATH)) {
54
+ return buildEmptyIndex();
55
+ }
56
+ const raw = fs.readFileSync(INDEX_PATH, "utf8");
57
+ return normalizeIndexShape(JSON.parse(raw));
58
+ }
59
+
60
+ function writeIndex(index) {
61
+ const normalized = normalizeIndexShape(index);
62
+ normalized.version = 1;
63
+ normalized.schemaVersion = SCHEMA_VERSION;
64
+ normalized.normalizationVersion = NORMALIZATION_VERSION;
65
+ normalized.updatedAt = new Date().toISOString();
66
+ fs.writeFileSync(INDEX_PATH, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
67
+ }
68
+
69
+ function getItem(index, cacheKey) {
70
+ const normalized = normalizeIndexShape(index);
71
+ return normalized.items && normalized.items[cacheKey] ? normalized.items[cacheKey] : null;
72
+ }
73
+
74
+ return {
75
+ ensureCacheDir,
76
+ buildEmptyIndex,
77
+ normalizeIndexShape,
78
+ readIndex,
79
+ writeIndex,
80
+ getItem,
81
+ };
82
+ }
83
+
84
+ module.exports = {
85
+ createIndexStore,
86
+ };
@@ -0,0 +1,97 @@
1
+ /* eslint-disable no-console */
2
+
3
+ function createProjectConfigService(deps) {
4
+ const {
5
+ fs,
6
+ path,
7
+ ROOT,
8
+ createRequire,
9
+ resolveMaybeAbsolutePath,
10
+ normalizeSlash,
11
+ } = deps;
12
+
13
+ /** @type {object | null} null = not loaded yet; after load always an object (possibly empty) */
14
+ let memoProjectConfig = null;
15
+ /** @type {string | null} */
16
+ let memoProjectConfigPath = null;
17
+
18
+ function loadProjectConfig() {
19
+ if (memoProjectConfig) {
20
+ return memoProjectConfig;
21
+ }
22
+ const candidates = [];
23
+ if (process.env.FIGMA_CACHE_PROJECT_CONFIG) {
24
+ candidates.push(resolveMaybeAbsolutePath(process.env.FIGMA_CACHE_PROJECT_CONFIG));
25
+ }
26
+ candidates.push(path.join(ROOT, "figma-cache.config.js"));
27
+ candidates.push(path.join(ROOT, ".figmacacherc.js"));
28
+
29
+ const requireFromRoot = createRequire(path.join(ROOT, "package.json"));
30
+
31
+ for (const absPath of candidates) {
32
+ if (!fs.existsSync(absPath)) {
33
+ continue;
34
+ }
35
+ try {
36
+ const mod = requireFromRoot(absPath);
37
+ const cfg = mod && mod.default ? mod.default : mod;
38
+ memoProjectConfig = cfg && typeof cfg === "object" ? cfg : {};
39
+ memoProjectConfigPath = absPath;
40
+ return memoProjectConfig;
41
+ } catch (err) {
42
+ console.error(`[figma-cache] project config failed (${absPath}): ${err.message}`);
43
+ }
44
+ }
45
+
46
+ memoProjectConfig = {};
47
+ memoProjectConfigPath = null;
48
+ return memoProjectConfig;
49
+ }
50
+
51
+ function runPostEnsureHook(cacheKey, item) {
52
+ if (!item || !item.paths) {
53
+ return;
54
+ }
55
+ const cfg = loadProjectConfig();
56
+ const hooks = cfg && cfg.hooks;
57
+ if (!hooks || typeof hooks.postEnsure !== "function") {
58
+ return;
59
+ }
60
+ const ctx = {
61
+ cacheKey,
62
+ fileKey: item.fileKey,
63
+ nodeId: item.nodeId == null ? null : item.nodeId,
64
+ scope: item.scope,
65
+ url: item.url == null ? "" : String(item.url),
66
+ source: item.source == null ? "" : String(item.source),
67
+ syncedAt: item.syncedAt == null ? "" : String(item.syncedAt),
68
+ completeness: Array.isArray(item.completeness) ? item.completeness : [],
69
+ paths: {
70
+ raw: item.paths.raw,
71
+ spec: item.paths.spec,
72
+ meta: item.paths.meta,
73
+ stateMap: item.paths.stateMap,
74
+ },
75
+ root: ROOT,
76
+ };
77
+ try {
78
+ hooks.postEnsure(ctx);
79
+ } catch (err) {
80
+ console.error(`[figma-cache] hooks.postEnsure: ${err.message}`);
81
+ }
82
+ }
83
+
84
+ function getProjectConfigPath() {
85
+ return memoProjectConfigPath ? normalizeSlash(memoProjectConfigPath) : null;
86
+ }
87
+
88
+ return {
89
+ loadProjectConfig,
90
+ runPostEnsureHook,
91
+ getProjectConfigPath,
92
+ };
93
+ }
94
+
95
+ module.exports = {
96
+ createProjectConfigService,
97
+ };
@@ -0,0 +1,156 @@
1
+ /* eslint-disable no-console */
2
+
3
+ function createUpsertService(deps) {
4
+ const {
5
+ URL,
6
+ NORMALIZATION_VERSION,
7
+ CACHE_BASE_FOR_STORAGE,
8
+ DEFAULT_COMPLETENESS,
9
+ normalizeCompletenessList,
10
+ normalizeIndexShape,
11
+ readIndex,
12
+ getItem,
13
+ writeIndex,
14
+ } = deps;
15
+
16
+ function resolveCompleteness(extra, oldItem) {
17
+ const normalizedExtra = normalizeCompletenessList(extra && extra.completeness);
18
+ if (
19
+ extra &&
20
+ Object.prototype.hasOwnProperty.call(extra, "completeness") &&
21
+ normalizedExtra.length
22
+ ) {
23
+ return normalizedExtra;
24
+ }
25
+ if (
26
+ extra &&
27
+ Object.prototype.hasOwnProperty.call(extra, "completeness") &&
28
+ Array.isArray(extra.completeness)
29
+ ) {
30
+ return [];
31
+ }
32
+ if (oldItem && Array.isArray(oldItem.completeness) && oldItem.completeness.length) {
33
+ return normalizeCompletenessList(oldItem.completeness);
34
+ }
35
+ return [...DEFAULT_COMPLETENESS];
36
+ }
37
+
38
+ function sanitizeNodeId(nodeId) {
39
+ return String(nodeId).replace(/:/g, "-");
40
+ }
41
+
42
+ function normalizeNodeIdValue(nodeId) {
43
+ const raw = String(nodeId).trim();
44
+ const dashPattern = /^(\d+)-(\d+)$/;
45
+ if (dashPattern.test(raw)) {
46
+ return raw.replace(dashPattern, "$1:$2");
47
+ }
48
+ return raw;
49
+ }
50
+
51
+ function normalizeFigmaUrl(inputUrl) {
52
+ let parsed;
53
+ try {
54
+ parsed = new URL(inputUrl);
55
+ } catch {
56
+ throw new Error(`Invalid URL: ${inputUrl}`);
57
+ }
58
+
59
+ const hostOk = /(^|\.)figma\.com$/i.test(parsed.hostname);
60
+ if (!hostOk) {
61
+ throw new Error(`Non-Figma domain: ${parsed.hostname}`);
62
+ }
63
+
64
+ const parts = parsed.pathname.split("/").filter(Boolean);
65
+ const routeType = parts[0];
66
+ const fileKey = parts[1];
67
+ if (!["file", "design"].includes(routeType) || !fileKey) {
68
+ throw new Error(`Cannot extract fileKey from path: ${parsed.pathname}`);
69
+ }
70
+
71
+ const nodeIdRaw = parsed.searchParams.get("node-id");
72
+ const nodeId = nodeIdRaw ? normalizeNodeIdValue(decodeURIComponent(nodeIdRaw)) : null;
73
+ const isNodeScope = !!nodeId;
74
+ const scope = isNodeScope ? "node" : "file";
75
+ const cacheKey = isNodeScope ? `${fileKey}#${nodeId}` : `${fileKey}#__FILE__`;
76
+
77
+ return {
78
+ fileKey,
79
+ nodeId,
80
+ scope,
81
+ cacheKey,
82
+ normalizedUrl: isNodeScope
83
+ ? `https://www.figma.com/file/${fileKey}/?node-id=${encodeURIComponent(nodeId)}`
84
+ : `https://www.figma.com/file/${fileKey}/`,
85
+ originalUrl: inputUrl,
86
+ normalizationVersion: NORMALIZATION_VERSION,
87
+ };
88
+ }
89
+
90
+ function buildPaths(normalized) {
91
+ if (normalized.scope === "file") {
92
+ return {
93
+ meta: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/meta.json`,
94
+ spec: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/spec.md`,
95
+ stateMap: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/state-map.md`,
96
+ raw: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/raw.json`,
97
+ };
98
+ }
99
+
100
+ const safeNode = sanitizeNodeId(normalized.nodeId);
101
+ return {
102
+ meta: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/nodes/${safeNode}/meta.json`,
103
+ spec: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/nodes/${safeNode}/spec.md`,
104
+ stateMap: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/nodes/${safeNode}/state-map.md`,
105
+ raw: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/nodes/${safeNode}/raw.json`,
106
+ };
107
+ }
108
+
109
+ function buildUpsertItem(normalized, oldItem, extra, syncedAt) {
110
+ const mergedUrls = Array.from(
111
+ new Set([...(oldItem ? oldItem.originalUrls || [] : []), normalized.originalUrl])
112
+ );
113
+ return {
114
+ fileKey: normalized.fileKey,
115
+ nodeId: normalized.nodeId,
116
+ scope: normalized.scope,
117
+ url: normalized.normalizedUrl,
118
+ originalUrls: mergedUrls,
119
+ normalizationVersion: NORMALIZATION_VERSION,
120
+ paths: oldItem && oldItem.paths ? oldItem.paths : buildPaths(normalized),
121
+ syncedAt,
122
+ completeness: resolveCompleteness(extra, oldItem),
123
+ source: (extra && extra.source) || (oldItem && oldItem.source) || "manual",
124
+ };
125
+ }
126
+
127
+ function previewUpsertByUrl(inputUrl, extra) {
128
+ const normalized = normalizeFigmaUrl(inputUrl);
129
+ const index = normalizeIndexShape(readIndex());
130
+ const oldItem = getItem(index, normalized.cacheKey);
131
+ const item = buildUpsertItem(normalized, oldItem, extra, new Date().toISOString());
132
+ return { normalized, item };
133
+ }
134
+
135
+ function upsertByUrl(inputUrl, extra) {
136
+ const normalized = normalizeFigmaUrl(inputUrl);
137
+ const index = normalizeIndexShape(readIndex());
138
+ const oldItem = getItem(index, normalized.cacheKey);
139
+ const item = buildUpsertItem(normalized, oldItem, extra, new Date().toISOString());
140
+
141
+ index.items = index.items || {};
142
+ index.items[normalized.cacheKey] = item;
143
+ writeIndex(index);
144
+ return { normalized, item };
145
+ }
146
+
147
+ return {
148
+ normalizeFigmaUrl,
149
+ previewUpsertByUrl,
150
+ upsertByUrl,
151
+ };
152
+ }
153
+
154
+ module.exports = {
155
+ createUpsertService,
156
+ };