@mp3wizard/figma-console-mcp 1.25.1 → 1.28.2
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/README.md +53 -35
- package/dist/apps/design-system-dashboard/mcp-app.html +78 -78
- package/dist/apps/token-browser/mcp-app.html +60 -59
- package/dist/cloudflare/core/config.js +0 -8
- package/dist/cloudflare/core/console-monitor.js +3 -3
- package/dist/cloudflare/core/diagnose-tool.js +96 -0
- package/dist/cloudflare/core/figma-tools.js +69 -229
- package/dist/cloudflare/core/identity.js +96 -0
- package/dist/cloudflare/core/tokens/alias-resolver.js +135 -0
- package/dist/cloudflare/core/tokens/config.js +284 -0
- package/dist/cloudflare/core/tokens/figma-converter.js +195 -0
- package/dist/cloudflare/core/tokens/formatters/css-vars.js +329 -0
- package/dist/cloudflare/core/tokens/formatters/dtcg.js +300 -0
- package/dist/cloudflare/core/tokens/formatters/index.js +45 -0
- package/dist/cloudflare/core/tokens/formatters/json.js +187 -0
- package/dist/cloudflare/core/tokens/formatters/less.js +4 -0
- package/dist/cloudflare/core/tokens/formatters/scss.js +252 -0
- package/dist/cloudflare/core/tokens/formatters/stubs.js +13 -0
- package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +207 -0
- package/dist/cloudflare/core/tokens/formatters/tailwind-v3.js +237 -0
- package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +330 -0
- package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +250 -0
- package/dist/cloudflare/core/tokens/formatters/ts-module.js +198 -0
- package/dist/cloudflare/core/tokens/index.js +15 -0
- package/dist/cloudflare/core/tokens/parsers/css-vars.js +4 -0
- package/dist/cloudflare/core/tokens/parsers/dtcg.js +253 -0
- package/dist/cloudflare/core/tokens/parsers/index.js +138 -0
- package/dist/cloudflare/core/tokens/parsers/json.js +7 -0
- package/dist/cloudflare/core/tokens/parsers/scss.js +4 -0
- package/dist/cloudflare/core/tokens/parsers/stubs.js +20 -0
- package/dist/cloudflare/core/tokens/parsers/style-dictionary-v3.js +4 -0
- package/dist/cloudflare/core/tokens/parsers/tailwind-v3.js +4 -0
- package/dist/cloudflare/core/tokens/parsers/tailwind-v4.js +4 -0
- package/dist/cloudflare/core/tokens/parsers/tokens-studio.js +4 -0
- package/dist/cloudflare/core/tokens/schemas.js +148 -0
- package/dist/cloudflare/core/tokens/transforms/color.js +12 -0
- package/dist/cloudflare/core/tokens/transforms/index.js +29 -0
- package/dist/cloudflare/core/tokens/transforms/size.js +7 -0
- package/dist/cloudflare/core/tokens/types.js +18 -0
- package/dist/cloudflare/core/tokens-tools.js +859 -0
- package/dist/cloudflare/core/websocket-server.js +5 -55
- package/dist/cloudflare/index.js +37 -26
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +0 -8
- package/dist/core/config.js.map +1 -1
- package/dist/core/console-monitor.d.ts +2 -2
- package/dist/core/console-monitor.d.ts.map +1 -1
- package/dist/core/console-monitor.js +3 -3
- package/dist/core/console-monitor.js.map +1 -1
- package/dist/core/diagnose-tool.d.ts +33 -0
- package/dist/core/diagnose-tool.d.ts.map +1 -0
- package/dist/core/diagnose-tool.js +97 -0
- package/dist/core/diagnose-tool.js.map +1 -0
- package/dist/core/figma-connector.d.ts +1 -1
- package/dist/core/figma-connector.d.ts.map +1 -1
- package/dist/core/figma-tools.d.ts +1 -2
- package/dist/core/figma-tools.d.ts.map +1 -1
- package/dist/core/figma-tools.js +69 -229
- package/dist/core/figma-tools.js.map +1 -1
- package/dist/core/identity.d.ts +41 -0
- package/dist/core/identity.d.ts.map +1 -0
- package/dist/core/identity.js +97 -0
- package/dist/core/identity.js.map +1 -0
- package/dist/core/tokens/alias-resolver.d.ts +55 -0
- package/dist/core/tokens/alias-resolver.d.ts.map +1 -0
- package/dist/core/tokens/alias-resolver.js +136 -0
- package/dist/core/tokens/alias-resolver.js.map +1 -0
- package/dist/core/tokens/config.d.ts +352 -0
- package/dist/core/tokens/config.d.ts.map +1 -0
- package/dist/core/tokens/config.js +285 -0
- package/dist/core/tokens/config.js.map +1 -0
- package/dist/core/tokens/figma-converter.d.ts +81 -0
- package/dist/core/tokens/figma-converter.d.ts.map +1 -0
- package/dist/core/tokens/figma-converter.js +196 -0
- package/dist/core/tokens/figma-converter.js.map +1 -0
- package/dist/core/tokens/formatters/css-vars.d.ts +24 -0
- package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -0
- package/dist/core/tokens/formatters/css-vars.js +330 -0
- package/dist/core/tokens/formatters/css-vars.js.map +1 -0
- package/dist/core/tokens/formatters/dtcg.d.ts +28 -0
- package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -0
- package/dist/core/tokens/formatters/dtcg.js +301 -0
- package/dist/core/tokens/formatters/dtcg.js.map +1 -0
- package/dist/core/tokens/formatters/index.d.ts +30 -0
- package/dist/core/tokens/formatters/index.d.ts.map +1 -0
- package/dist/core/tokens/formatters/index.js +46 -0
- package/dist/core/tokens/formatters/index.js.map +1 -0
- package/dist/core/tokens/formatters/json.d.ts +37 -0
- package/dist/core/tokens/formatters/json.d.ts.map +1 -0
- package/dist/core/tokens/formatters/json.js +188 -0
- package/dist/core/tokens/formatters/json.js.map +1 -0
- package/dist/core/tokens/formatters/less.d.ts +4 -0
- package/dist/core/tokens/formatters/less.d.ts.map +1 -0
- package/dist/core/tokens/formatters/less.js +5 -0
- package/dist/core/tokens/formatters/less.js.map +1 -0
- package/dist/core/tokens/formatters/scss.d.ts +26 -0
- package/dist/core/tokens/formatters/scss.d.ts.map +1 -0
- package/dist/core/tokens/formatters/scss.js +253 -0
- package/dist/core/tokens/formatters/scss.js.map +1 -0
- package/dist/core/tokens/formatters/stubs.d.ts +9 -0
- package/dist/core/tokens/formatters/stubs.d.ts.map +1 -0
- package/dist/core/tokens/formatters/stubs.js +14 -0
- package/dist/core/tokens/formatters/stubs.js.map +1 -0
- package/dist/core/tokens/formatters/style-dictionary-v3.d.ts +45 -0
- package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -0
- package/dist/core/tokens/formatters/style-dictionary-v3.js +208 -0
- package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -0
- package/dist/core/tokens/formatters/tailwind-v3.d.ts +37 -0
- package/dist/core/tokens/formatters/tailwind-v3.d.ts.map +1 -0
- package/dist/core/tokens/formatters/tailwind-v3.js +238 -0
- package/dist/core/tokens/formatters/tailwind-v3.js.map +1 -0
- package/dist/core/tokens/formatters/tailwind-v4.d.ts +41 -0
- package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -0
- package/dist/core/tokens/formatters/tailwind-v4.js +331 -0
- package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -0
- package/dist/core/tokens/formatters/tokens-studio.d.ts +44 -0
- package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -0
- package/dist/core/tokens/formatters/tokens-studio.js +251 -0
- package/dist/core/tokens/formatters/tokens-studio.js.map +1 -0
- package/dist/core/tokens/formatters/ts-module.d.ts +35 -0
- package/dist/core/tokens/formatters/ts-module.d.ts.map +1 -0
- package/dist/core/tokens/formatters/ts-module.js +199 -0
- package/dist/core/tokens/formatters/ts-module.js.map +1 -0
- package/dist/core/tokens/index.d.ts +17 -0
- package/dist/core/tokens/index.d.ts.map +1 -0
- package/dist/core/tokens/index.js +16 -0
- package/dist/core/tokens/index.js.map +1 -0
- package/dist/core/tokens/parsers/css-vars.d.ts +3 -0
- package/dist/core/tokens/parsers/css-vars.d.ts.map +1 -0
- package/dist/core/tokens/parsers/css-vars.js +5 -0
- package/dist/core/tokens/parsers/css-vars.js.map +1 -0
- package/dist/core/tokens/parsers/dtcg.d.ts +21 -0
- package/dist/core/tokens/parsers/dtcg.d.ts.map +1 -0
- package/dist/core/tokens/parsers/dtcg.js +254 -0
- package/dist/core/tokens/parsers/dtcg.js.map +1 -0
- package/dist/core/tokens/parsers/index.d.ts +37 -0
- package/dist/core/tokens/parsers/index.d.ts.map +1 -0
- package/dist/core/tokens/parsers/index.js +139 -0
- package/dist/core/tokens/parsers/index.js.map +1 -0
- package/dist/core/tokens/parsers/json.d.ts +4 -0
- package/dist/core/tokens/parsers/json.d.ts.map +1 -0
- package/dist/core/tokens/parsers/json.js +8 -0
- package/dist/core/tokens/parsers/json.js.map +1 -0
- package/dist/core/tokens/parsers/scss.d.ts +3 -0
- package/dist/core/tokens/parsers/scss.d.ts.map +1 -0
- package/dist/core/tokens/parsers/scss.js +5 -0
- package/dist/core/tokens/parsers/scss.js.map +1 -0
- package/dist/core/tokens/parsers/stubs.d.ts +15 -0
- package/dist/core/tokens/parsers/stubs.d.ts.map +1 -0
- package/dist/core/tokens/parsers/stubs.js +21 -0
- package/dist/core/tokens/parsers/stubs.js.map +1 -0
- package/dist/core/tokens/parsers/style-dictionary-v3.d.ts +3 -0
- package/dist/core/tokens/parsers/style-dictionary-v3.d.ts.map +1 -0
- package/dist/core/tokens/parsers/style-dictionary-v3.js +5 -0
- package/dist/core/tokens/parsers/style-dictionary-v3.js.map +1 -0
- package/dist/core/tokens/parsers/tailwind-v3.d.ts +3 -0
- package/dist/core/tokens/parsers/tailwind-v3.d.ts.map +1 -0
- package/dist/core/tokens/parsers/tailwind-v3.js +5 -0
- package/dist/core/tokens/parsers/tailwind-v3.js.map +1 -0
- package/dist/core/tokens/parsers/tailwind-v4.d.ts +3 -0
- package/dist/core/tokens/parsers/tailwind-v4.d.ts.map +1 -0
- package/dist/core/tokens/parsers/tailwind-v4.js +5 -0
- package/dist/core/tokens/parsers/tailwind-v4.js.map +1 -0
- package/dist/core/tokens/parsers/tokens-studio.d.ts +3 -0
- package/dist/core/tokens/parsers/tokens-studio.d.ts.map +1 -0
- package/dist/core/tokens/parsers/tokens-studio.js +5 -0
- package/dist/core/tokens/parsers/tokens-studio.js.map +1 -0
- package/dist/core/tokens/schemas.d.ts +152 -0
- package/dist/core/tokens/schemas.d.ts.map +1 -0
- package/dist/core/tokens/schemas.js +149 -0
- package/dist/core/tokens/schemas.js.map +1 -0
- package/dist/core/tokens/transforms/color.d.ts +9 -0
- package/dist/core/tokens/transforms/color.d.ts.map +1 -0
- package/dist/core/tokens/transforms/color.js +13 -0
- package/dist/core/tokens/transforms/color.js.map +1 -0
- package/dist/core/tokens/transforms/index.d.ts +36 -0
- package/dist/core/tokens/transforms/index.d.ts.map +1 -0
- package/dist/core/tokens/transforms/index.js +30 -0
- package/dist/core/tokens/transforms/index.js.map +1 -0
- package/dist/core/tokens/transforms/size.d.ts +7 -0
- package/dist/core/tokens/transforms/size.d.ts.map +1 -0
- package/dist/core/tokens/transforms/size.js +8 -0
- package/dist/core/tokens/transforms/size.js.map +1 -0
- package/dist/core/tokens/types.d.ts +228 -0
- package/dist/core/tokens/types.d.ts.map +1 -0
- package/dist/core/tokens/types.js +19 -0
- package/dist/core/tokens/types.js.map +1 -0
- package/dist/core/tokens-tools.d.ts +42 -0
- package/dist/core/tokens-tools.d.ts.map +1 -0
- package/dist/core/tokens-tools.js +860 -0
- package/dist/core/tokens-tools.js.map +1 -0
- package/dist/core/types/index.d.ts +0 -8
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/core/websocket-connector.d.ts +1 -1
- package/dist/core/websocket-connector.d.ts.map +1 -1
- package/dist/core/websocket-server.d.ts +4 -3
- package/dist/core/websocket-server.d.ts.map +1 -1
- package/dist/core/websocket-server.js +5 -55
- package/dist/core/websocket-server.js.map +1 -1
- package/dist/local.d.ts +0 -12
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +959 -3406
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/code.js +11 -63
- package/figma-desktop-bridge/ui.html +72 -11
- package/package.json +27 -12
- package/figma-desktop-bridge/ui-full.html +0 -1353
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DTCG (Design Tokens Community Group) JSON formatter.
|
|
3
|
+
*
|
|
4
|
+
* Produces W3C-spec DTCG output (https://tr.designtokens.org/format/) with
|
|
5
|
+
* three nuances:
|
|
6
|
+
*
|
|
7
|
+
* 1. Multi-mode tokens. The DTCG v1 spec doesn't natively express modes;
|
|
8
|
+
* we use a separate file per mode (driven by `splitByMode: true`) or a
|
|
9
|
+
* single file with values keyed by mode under a vendor extension when
|
|
10
|
+
* splitByMode is false. The split-file approach is the recommended
|
|
11
|
+
* pattern in the broader DTCG community and is what Style Dictionary
|
|
12
|
+
* v4, Tokens Studio, and Figma's announced native export all use.
|
|
13
|
+
*
|
|
14
|
+
* 2. $extensions["figma-console-mcp"]. We stash Figma variable IDs and
|
|
15
|
+
* last-synced values here for non-destructive round-trip. Other DTCG
|
|
16
|
+
* tools preserve $extensions verbatim.
|
|
17
|
+
*
|
|
18
|
+
* 3. Composite tokens (typography, shadow, gradient) emit DTCG's
|
|
19
|
+
* structured $value form. Aliases emit `"$value": "{path.to.target}"`.
|
|
20
|
+
*
|
|
21
|
+
* This formatter is the canonical output — the format every other
|
|
22
|
+
* formatter (CSS variables today, Tailwind/SCSS/etc. in future minor
|
|
23
|
+
* versions) ultimately derives from.
|
|
24
|
+
*/
|
|
25
|
+
import { FIGMA_MCP_EXTENSION_KEY } from "../types.js";
|
|
26
|
+
import { formatDtcgReference } from "../alias-resolver.js";
|
|
27
|
+
export function formatDtcg(doc, opts) {
|
|
28
|
+
const warnings = [];
|
|
29
|
+
const files = [];
|
|
30
|
+
// Figure out which sets and modes to emit, and how they map to files.
|
|
31
|
+
// Three layout strategies:
|
|
32
|
+
// 1. splitByMode + splitByCollection → one file per (set, mode) pair
|
|
33
|
+
// 2. splitByMode → one file per mode, all sets merged
|
|
34
|
+
// 3. splitByCollection → one file per set, all modes in one tree
|
|
35
|
+
// 4. neither → one file with everything
|
|
36
|
+
const splitByMode = opts.target.splitByMode ?? false;
|
|
37
|
+
const splitByCollection = opts.target.splitByCollection ?? false;
|
|
38
|
+
if (splitByMode && splitByCollection) {
|
|
39
|
+
for (const set of doc.sets) {
|
|
40
|
+
for (const mode of set.modes) {
|
|
41
|
+
const fileTokens = set.tokens
|
|
42
|
+
.map((t) => projectTokenToMode(t, mode, warnings))
|
|
43
|
+
.filter((t) => t !== null);
|
|
44
|
+
files.push({
|
|
45
|
+
path: filenameFor(opts, set, mode),
|
|
46
|
+
content: serializeAsDtcg({ sets: [{ ...set, modes: [mode], tokens: fileTokens }], meta: doc.meta }, warnings, mode),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (splitByMode) {
|
|
52
|
+
const allModes = new Set();
|
|
53
|
+
for (const set of doc.sets)
|
|
54
|
+
for (const m of set.modes)
|
|
55
|
+
allModes.add(m);
|
|
56
|
+
for (const mode of allModes) {
|
|
57
|
+
const fileSets = doc.sets
|
|
58
|
+
.filter((s) => s.modes.includes(mode))
|
|
59
|
+
.map((s) => ({
|
|
60
|
+
...s,
|
|
61
|
+
modes: [mode],
|
|
62
|
+
tokens: s.tokens
|
|
63
|
+
.map((t) => projectTokenToMode(t, mode, warnings))
|
|
64
|
+
.filter((t) => t !== null),
|
|
65
|
+
}));
|
|
66
|
+
files.push({
|
|
67
|
+
path: filenameFor(opts, undefined, mode),
|
|
68
|
+
content: serializeAsDtcg({ sets: fileSets, meta: doc.meta }, warnings, mode),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (splitByCollection) {
|
|
73
|
+
for (const set of doc.sets) {
|
|
74
|
+
files.push({
|
|
75
|
+
path: filenameFor(opts, set),
|
|
76
|
+
content: serializeAsDtcg({ sets: [set], meta: doc.meta }, warnings),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
files.push({
|
|
82
|
+
path: filenameFor(opts),
|
|
83
|
+
content: serializeAsDtcg(doc, warnings),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return { files, warnings };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Project a token's values down to a single mode. Returns null if the token
|
|
90
|
+
* has no value for the requested mode (skip rather than emit a blank).
|
|
91
|
+
*/
|
|
92
|
+
function projectTokenToMode(token, mode, warnings) {
|
|
93
|
+
const value = token.values[mode];
|
|
94
|
+
if (!value) {
|
|
95
|
+
// Token wasn't defined for this mode. Could happen when sets share tokens
|
|
96
|
+
// but only some have multi-mode values. Skip silently — not an error.
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return { ...token, values: { [mode]: value } };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Compute the output filename for a given (set?, mode?) tuple based on the
|
|
103
|
+
* target options.
|
|
104
|
+
*/
|
|
105
|
+
function filenameFor(opts, set, mode) {
|
|
106
|
+
// Caller-specified filename wins.
|
|
107
|
+
if (opts.target.filename)
|
|
108
|
+
return opts.target.filename;
|
|
109
|
+
const parts = [];
|
|
110
|
+
if (set)
|
|
111
|
+
parts.push(slugify(set.name));
|
|
112
|
+
if (mode)
|
|
113
|
+
parts.push(slugify(mode));
|
|
114
|
+
if (parts.length === 0)
|
|
115
|
+
parts.push("tokens");
|
|
116
|
+
return `${parts.join(".")}.tokens.json`;
|
|
117
|
+
}
|
|
118
|
+
function slugify(s) {
|
|
119
|
+
return s
|
|
120
|
+
.trim()
|
|
121
|
+
.toLowerCase()
|
|
122
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
123
|
+
.replace(/^-+|-+$/g, "");
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Serialize a TokenDocument as DTCG JSON. Returns a pretty-printed JSON string
|
|
127
|
+
* with stable key order so git diffs stay minimal across runs.
|
|
128
|
+
*
|
|
129
|
+
* When `fileMode` is provided (splitByMode output), it's stamped into
|
|
130
|
+
* document-level $extensions so the parser can recover which mode this
|
|
131
|
+
* file represents — otherwise the parser sees only `$value` literals and
|
|
132
|
+
* labels them "Default", which breaks round-trip diffs on multi-mode files.
|
|
133
|
+
*/
|
|
134
|
+
function serializeAsDtcg(doc, warnings, fileMode) {
|
|
135
|
+
// Build the nested DTCG group tree by walking every token's path and
|
|
136
|
+
// building groups along the way.
|
|
137
|
+
const tree = {};
|
|
138
|
+
// Document-level $extensions: stash file-level metadata (Figma file key,
|
|
139
|
+
// export timestamp, MCP version, optionally the file's mode for
|
|
140
|
+
// splitByMode output) so round-trip preserves it.
|
|
141
|
+
const mcpDocMeta = {};
|
|
142
|
+
if (doc.meta?.figmaFileKey)
|
|
143
|
+
mcpDocMeta.figmaFileKey = doc.meta.figmaFileKey;
|
|
144
|
+
if (doc.meta?.exportedAt)
|
|
145
|
+
mcpDocMeta.exportedAt = doc.meta.exportedAt;
|
|
146
|
+
if (doc.meta?.mcpVersion)
|
|
147
|
+
mcpDocMeta.mcpVersion = doc.meta.mcpVersion;
|
|
148
|
+
if (fileMode)
|
|
149
|
+
mcpDocMeta.fileMode = fileMode;
|
|
150
|
+
if (Object.keys(mcpDocMeta).length > 0) {
|
|
151
|
+
tree.$extensions = { [FIGMA_MCP_EXTENSION_KEY]: mcpDocMeta };
|
|
152
|
+
}
|
|
153
|
+
for (const set of doc.sets) {
|
|
154
|
+
// Each set lives under a top-level group named after the set. Set-level
|
|
155
|
+
// metadata (Figma collection ID, original name, etc.) goes in that
|
|
156
|
+
// group's $extensions so round-trip recovers the original name even
|
|
157
|
+
// after we slugify it for the JSON key.
|
|
158
|
+
const setKey = setKeyFor(set);
|
|
159
|
+
let setGroup = tree[setKey];
|
|
160
|
+
if (!setGroup) {
|
|
161
|
+
setGroup = {};
|
|
162
|
+
if (set.description)
|
|
163
|
+
setGroup.$description = set.description;
|
|
164
|
+
const mcpMeta = {};
|
|
165
|
+
if (set.meta?.figmaCollectionId) {
|
|
166
|
+
mcpMeta.figmaCollectionId = set.meta.figmaCollectionId;
|
|
167
|
+
}
|
|
168
|
+
// Always stash the original name when it differs from the slug — this
|
|
169
|
+
// is what makes diff matching work after round-trip.
|
|
170
|
+
if (set.name !== setKey) {
|
|
171
|
+
mcpMeta.originalName = set.name;
|
|
172
|
+
}
|
|
173
|
+
if (Object.keys(mcpMeta).length > 0) {
|
|
174
|
+
setGroup.$extensions = { [FIGMA_MCP_EXTENSION_KEY]: mcpMeta };
|
|
175
|
+
}
|
|
176
|
+
tree[setKey] = setGroup;
|
|
177
|
+
}
|
|
178
|
+
for (const token of set.tokens) {
|
|
179
|
+
writeTokenIntoTree(setGroup, token, set.modes, warnings);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return JSON.stringify(sortKeys(tree), null, 2) + "\n";
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Key used for the top-level set group in DTCG. We slugify the set name to
|
|
186
|
+
* keep it a valid JSON key under any consumer's expectations. The original
|
|
187
|
+
* (un-slugged) name is preserved in the set's $extensions so round-trip
|
|
188
|
+
* recovers it.
|
|
189
|
+
*/
|
|
190
|
+
function setKeyFor(set) {
|
|
191
|
+
return slugify(set.name);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Insert a token into the DTCG group tree at the right nested path.
|
|
195
|
+
* Creates intermediate groups as needed.
|
|
196
|
+
*/
|
|
197
|
+
function writeTokenIntoTree(root, token, setModes, warnings) {
|
|
198
|
+
let cursor = root;
|
|
199
|
+
for (let i = 0; i < token.path.length - 1; i++) {
|
|
200
|
+
const segment = token.path[i];
|
|
201
|
+
let next = cursor[segment];
|
|
202
|
+
if (!next || isToken(next)) {
|
|
203
|
+
next = {};
|
|
204
|
+
cursor[segment] = next;
|
|
205
|
+
}
|
|
206
|
+
cursor = next;
|
|
207
|
+
}
|
|
208
|
+
const leafKey = token.path[token.path.length - 1];
|
|
209
|
+
cursor[leafKey] = renderToken(token, setModes, warnings);
|
|
210
|
+
}
|
|
211
|
+
function isToken(node) {
|
|
212
|
+
return "$value" in node;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Convert an internal Token to its DTCG-encoded leaf form.
|
|
216
|
+
*
|
|
217
|
+
* Single-mode token: emits `{ $value, $type, ... }`.
|
|
218
|
+
* Multi-mode token: emits one extension stash with all mode values, because
|
|
219
|
+
* vanilla DTCG doesn't have a native multi-mode encoding. Callers who want
|
|
220
|
+
* one-file-per-mode should set splitByMode at the formatter level.
|
|
221
|
+
*/
|
|
222
|
+
function renderToken(token, setModes, warnings) {
|
|
223
|
+
const result = {
|
|
224
|
+
$value: "",
|
|
225
|
+
$type: token.type,
|
|
226
|
+
};
|
|
227
|
+
if (token.description)
|
|
228
|
+
result.$description = token.description;
|
|
229
|
+
const modeKeys = Object.keys(token.values);
|
|
230
|
+
const isSingleMode = modeKeys.length === 1;
|
|
231
|
+
if (isSingleMode) {
|
|
232
|
+
const onlyValue = token.values[modeKeys[0]];
|
|
233
|
+
result.$value = encodeValue(onlyValue, token, warnings);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// Multi-mode in a single file: pick the first mode as the canonical
|
|
237
|
+
// $value, stash the rest in $extensions for round-trip.
|
|
238
|
+
const primaryMode = setModes[0] in token.values ? setModes[0] : modeKeys[0];
|
|
239
|
+
result.$value = encodeValue(token.values[primaryMode], token, warnings);
|
|
240
|
+
const otherModes = {};
|
|
241
|
+
for (const m of modeKeys) {
|
|
242
|
+
if (m === primaryMode)
|
|
243
|
+
continue;
|
|
244
|
+
otherModes[m] = encodeValue(token.values[m], token, warnings);
|
|
245
|
+
}
|
|
246
|
+
if (Object.keys(otherModes).length > 0) {
|
|
247
|
+
mergeExtension(result, "modes", otherModes);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Preserve any pre-existing extensions (e.g. studio.tokens, our own metadata).
|
|
251
|
+
if (token.extensions) {
|
|
252
|
+
for (const [vendor, payload] of Object.entries(token.extensions)) {
|
|
253
|
+
if (vendor === FIGMA_MCP_EXTENSION_KEY) {
|
|
254
|
+
mergeExtension(result, vendor, payload);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
mergeExtension(result, vendor, payload);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
function encodeValue(value, token, warnings) {
|
|
264
|
+
if (value.reference) {
|
|
265
|
+
return formatDtcgReference(value.reference.replace(/^\{|\}$/g, "").split("."));
|
|
266
|
+
}
|
|
267
|
+
if (value.literal === undefined) {
|
|
268
|
+
warnings.push(`Token ${token.path.join(".")} has neither literal nor reference — emitting empty string.`);
|
|
269
|
+
return "";
|
|
270
|
+
}
|
|
271
|
+
return value.literal;
|
|
272
|
+
}
|
|
273
|
+
function mergeExtension(token, key, payload) {
|
|
274
|
+
token.$extensions ??= {};
|
|
275
|
+
token.$extensions[key] = payload;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Recursively sort object keys for stable serialization (so git diffs only
|
|
279
|
+
* show meaningful changes). $-prefixed keys come first (DTCG convention),
|
|
280
|
+
* then alphabetical.
|
|
281
|
+
*/
|
|
282
|
+
function sortKeys(node) {
|
|
283
|
+
if (node === null || typeof node !== "object" || Array.isArray(node)) {
|
|
284
|
+
return node;
|
|
285
|
+
}
|
|
286
|
+
const obj = node;
|
|
287
|
+
const sorted = {};
|
|
288
|
+
const keys = Object.keys(obj).sort((a, b) => {
|
|
289
|
+
const aDollar = a.startsWith("$");
|
|
290
|
+
const bDollar = b.startsWith("$");
|
|
291
|
+
if (aDollar && !bDollar)
|
|
292
|
+
return -1;
|
|
293
|
+
if (!aDollar && bDollar)
|
|
294
|
+
return 1;
|
|
295
|
+
return a.localeCompare(b);
|
|
296
|
+
});
|
|
297
|
+
for (const k of keys)
|
|
298
|
+
sorted[k] = sortKeys(obj[k]);
|
|
299
|
+
return sorted;
|
|
300
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatter dispatcher. Each formatter converts our canonical internal
|
|
3
|
+
* TokenDocument into a specific output format (DTCG JSON, CSS custom
|
|
4
|
+
* properties, Tailwind v4 @theme, SCSS, etc.).
|
|
5
|
+
*/
|
|
6
|
+
import { formatDtcg } from "./dtcg.js";
|
|
7
|
+
import { formatTokensStudio } from "./tokens-studio.js";
|
|
8
|
+
import { formatCssVars } from "./css-vars.js";
|
|
9
|
+
import { formatTailwindV4 } from "./tailwind-v4.js";
|
|
10
|
+
import { formatTailwindV3 } from "./tailwind-v3.js";
|
|
11
|
+
import { formatScss } from "./scss.js";
|
|
12
|
+
import { formatLess } from "./less.js";
|
|
13
|
+
import { formatTsModule } from "./ts-module.js";
|
|
14
|
+
import { formatJsonFlat, formatJsonNested } from "./json.js";
|
|
15
|
+
import { formatStyleDictionaryV3 } from "./style-dictionary-v3.js";
|
|
16
|
+
export function format(doc, options) {
|
|
17
|
+
switch (options.target.format) {
|
|
18
|
+
case "dtcg":
|
|
19
|
+
return formatDtcg(doc, options);
|
|
20
|
+
case "tokens-studio":
|
|
21
|
+
return formatTokensStudio(doc, options);
|
|
22
|
+
case "css-vars":
|
|
23
|
+
return formatCssVars(doc, options);
|
|
24
|
+
case "tailwind-v4":
|
|
25
|
+
return formatTailwindV4(doc, options);
|
|
26
|
+
case "tailwind-v3":
|
|
27
|
+
return formatTailwindV3(doc, options);
|
|
28
|
+
case "scss":
|
|
29
|
+
return formatScss(doc, options);
|
|
30
|
+
case "less":
|
|
31
|
+
return formatLess(doc, options);
|
|
32
|
+
case "ts-module":
|
|
33
|
+
return formatTsModule(doc, options);
|
|
34
|
+
case "json-flat":
|
|
35
|
+
return formatJsonFlat(doc, options);
|
|
36
|
+
case "json-nested":
|
|
37
|
+
return formatJsonNested(doc, options);
|
|
38
|
+
case "style-dictionary-v3":
|
|
39
|
+
return formatStyleDictionaryV3(doc, options);
|
|
40
|
+
default: {
|
|
41
|
+
const _exhaustive = options.target.format;
|
|
42
|
+
throw new Error(`[figma-console-mcp] Unknown export format: ${_exhaustive}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain JSON formatters — flat and nested.
|
|
3
|
+
*
|
|
4
|
+
* These are dumps without the DTCG `$type`/`$value` envelope, for custom
|
|
5
|
+
* build scripts that just need a key-value map of resolved tokens.
|
|
6
|
+
*
|
|
7
|
+
* Flat shape:
|
|
8
|
+
*
|
|
9
|
+
* {
|
|
10
|
+
* "ds-color-primary": "#4085F2",
|
|
11
|
+
* "ds-spacing-md": "16px",
|
|
12
|
+
* "ds-color-bg--dark": "#0A0A0A"
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* Multi-mode tokens flatten with `--<mode>` suffix (primary mode keeps
|
|
16
|
+
* the bare name; other modes get suffixed).
|
|
17
|
+
*
|
|
18
|
+
* Nested shape:
|
|
19
|
+
*
|
|
20
|
+
* {
|
|
21
|
+
* "color": {
|
|
22
|
+
* "primary": "#4085F2",
|
|
23
|
+
* "brand": { "primary": "#FF00AA" }
|
|
24
|
+
* },
|
|
25
|
+
* "spacing": { "md": "16px" }
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* Multi-mode tokens become objects: `{ Light: "...", Dark: "..." }`.
|
|
29
|
+
*
|
|
30
|
+
* Aliases resolve to the literal value where possible; cross-library
|
|
31
|
+
* aliases get a `null` (caller can decide how to fill those in).
|
|
32
|
+
*/
|
|
33
|
+
import { buildTokenIndex, resolveAliasChain } from "../alias-resolver.js";
|
|
34
|
+
export function formatJsonFlat(doc, opts) {
|
|
35
|
+
const warnings = [];
|
|
36
|
+
const files = [];
|
|
37
|
+
const splitByCollection = opts.target.splitByCollection ?? false;
|
|
38
|
+
const prefix = opts.target.prefix ?? "";
|
|
39
|
+
// Plain JSON has no native alias mechanism — resolve aliases to their
|
|
40
|
+
// literal target so consumers get usable values, not opaque `{ref}` strings.
|
|
41
|
+
const tokenIndex = buildTokenIndex(doc);
|
|
42
|
+
if (splitByCollection) {
|
|
43
|
+
for (const set of doc.sets) {
|
|
44
|
+
files.push({
|
|
45
|
+
path: filenameFor(opts, set, "flat"),
|
|
46
|
+
content: renderFlat([set], prefix, tokenIndex, warnings),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
files.push({
|
|
52
|
+
path: filenameFor(opts, undefined, "flat"),
|
|
53
|
+
content: renderFlat(doc.sets, prefix, tokenIndex, warnings),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return { files, warnings };
|
|
57
|
+
}
|
|
58
|
+
export function formatJsonNested(doc, opts) {
|
|
59
|
+
const warnings = [];
|
|
60
|
+
const files = [];
|
|
61
|
+
const splitByCollection = opts.target.splitByCollection ?? false;
|
|
62
|
+
const tokenIndex = buildTokenIndex(doc);
|
|
63
|
+
if (splitByCollection) {
|
|
64
|
+
for (const set of doc.sets) {
|
|
65
|
+
files.push({
|
|
66
|
+
path: filenameFor(opts, set, "nested"),
|
|
67
|
+
content: renderNested([set], tokenIndex, warnings),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
files.push({
|
|
73
|
+
path: filenameFor(opts, undefined, "nested"),
|
|
74
|
+
content: renderNested(doc.sets, tokenIndex, warnings),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return { files, warnings };
|
|
78
|
+
}
|
|
79
|
+
function filenameFor(opts, set, shape) {
|
|
80
|
+
if (opts.target.filename)
|
|
81
|
+
return opts.target.filename;
|
|
82
|
+
const parts = [];
|
|
83
|
+
if (set)
|
|
84
|
+
parts.push(slugify(set.name));
|
|
85
|
+
parts.push(`tokens.${shape}`);
|
|
86
|
+
return `${parts.join(".")}.json`;
|
|
87
|
+
}
|
|
88
|
+
function slugify(s) {
|
|
89
|
+
return s
|
|
90
|
+
.trim()
|
|
91
|
+
.toLowerCase()
|
|
92
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
93
|
+
.replace(/^-+|-+$/g, "");
|
|
94
|
+
}
|
|
95
|
+
function renderFlat(sets, prefix, tokenIndex, warnings) {
|
|
96
|
+
const out = {};
|
|
97
|
+
for (const set of sets) {
|
|
98
|
+
const primaryMode = pickPrimaryMode(set.modes);
|
|
99
|
+
for (const token of set.tokens) {
|
|
100
|
+
const baseName = `${prefix}${token.path.map(slugify).join("-")}`;
|
|
101
|
+
for (const [modeName, value] of Object.entries(token.values)) {
|
|
102
|
+
const key = modeName === primaryMode
|
|
103
|
+
? baseName
|
|
104
|
+
: `${baseName}--${slugify(modeName)}`;
|
|
105
|
+
const resolved = resolveValue(value, token, modeName, tokenIndex, warnings);
|
|
106
|
+
if (resolved !== undefined)
|
|
107
|
+
out[key] = resolved;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Sort keys for deterministic output.
|
|
112
|
+
const sorted = Object.fromEntries(Object.entries(out).sort(([a], [b]) => a.localeCompare(b)));
|
|
113
|
+
return JSON.stringify(sorted, null, 2) + "\n";
|
|
114
|
+
}
|
|
115
|
+
function renderNested(sets, tokenIndex, warnings) {
|
|
116
|
+
const out = {};
|
|
117
|
+
for (const set of sets) {
|
|
118
|
+
const isMultiMode = set.modes.length > 1;
|
|
119
|
+
for (const token of set.tokens) {
|
|
120
|
+
let cursor = out;
|
|
121
|
+
for (let i = 0; i < token.path.length - 1; i++) {
|
|
122
|
+
const segment = token.path[i];
|
|
123
|
+
if (!cursor[segment] ||
|
|
124
|
+
typeof cursor[segment] !== "object" ||
|
|
125
|
+
Array.isArray(cursor[segment])) {
|
|
126
|
+
cursor[segment] = {};
|
|
127
|
+
}
|
|
128
|
+
cursor = cursor[segment];
|
|
129
|
+
}
|
|
130
|
+
const leafKey = token.path[token.path.length - 1];
|
|
131
|
+
if (isMultiMode) {
|
|
132
|
+
const modeValues = {};
|
|
133
|
+
for (const [modeName, value] of Object.entries(token.values)) {
|
|
134
|
+
const resolved = resolveValue(value, token, modeName, tokenIndex, warnings);
|
|
135
|
+
if (resolved !== undefined)
|
|
136
|
+
modeValues[modeName] = resolved;
|
|
137
|
+
}
|
|
138
|
+
cursor[leafKey] = modeValues;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const [onlyModeName, onlyValue] = Object.entries(token.values)[0] ?? [];
|
|
142
|
+
if (onlyValue) {
|
|
143
|
+
const resolved = resolveValue(onlyValue, token, onlyModeName, tokenIndex, warnings);
|
|
144
|
+
if (resolved !== undefined)
|
|
145
|
+
cursor[leafKey] = resolved;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return JSON.stringify(sortKeys(out), null, 2) + "\n";
|
|
151
|
+
}
|
|
152
|
+
function pickPrimaryMode(modes) {
|
|
153
|
+
return modes.find((m) => /^(default|light|value)$/i.test(m)) ?? modes[0];
|
|
154
|
+
}
|
|
155
|
+
function resolveValue(value, token, mode, tokenIndex, warnings) {
|
|
156
|
+
let effective = value;
|
|
157
|
+
if (value.reference) {
|
|
158
|
+
effective = resolveAliasChain(value, mode, tokenIndex);
|
|
159
|
+
if (!effective) {
|
|
160
|
+
const bare = value.reference.replace(/^\{|\}$/g, "");
|
|
161
|
+
const reason = bare.startsWith("__library:") || bare === "unknown"
|
|
162
|
+
? "cross-library alias"
|
|
163
|
+
: "alias target not found";
|
|
164
|
+
warnings.push(`Skipped ${token.path.join(".")} in JSON — ${reason}: ${value.reference}.`);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!effective || effective.literal === undefined || effective.literal === null) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
if (typeof effective.literal === "number") {
|
|
172
|
+
if (token.type === "dimension")
|
|
173
|
+
return `${effective.literal}px`;
|
|
174
|
+
return effective.literal;
|
|
175
|
+
}
|
|
176
|
+
return effective.literal;
|
|
177
|
+
}
|
|
178
|
+
function sortKeys(obj) {
|
|
179
|
+
if (obj === null || typeof obj !== "object" || Array.isArray(obj))
|
|
180
|
+
return obj;
|
|
181
|
+
const sorted = {};
|
|
182
|
+
const keys = Object.keys(obj).sort();
|
|
183
|
+
for (const k of keys) {
|
|
184
|
+
sorted[k] = sortKeys(obj[k]);
|
|
185
|
+
}
|
|
186
|
+
return sorted;
|
|
187
|
+
}
|