@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,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokens Studio for Figma JSON formatter.
|
|
3
|
+
*
|
|
4
|
+
* Tokens Studio uses a multi-file layout:
|
|
5
|
+
*
|
|
6
|
+
* $themes.json — array of theme definitions with selectedTokenSets
|
|
7
|
+
* $metadata.json — list of token set names + tokenSetOrder
|
|
8
|
+
* primitives.json — one file per token set
|
|
9
|
+
* semantic.json
|
|
10
|
+
* theme/light.json
|
|
11
|
+
* theme/dark.json
|
|
12
|
+
*
|
|
13
|
+
* Each per-set file holds tokens with the Tokens Studio shape:
|
|
14
|
+
*
|
|
15
|
+
* {
|
|
16
|
+
* "color": {
|
|
17
|
+
* "primary": {
|
|
18
|
+
* "value": "#4085F2",
|
|
19
|
+
* "type": "color",
|
|
20
|
+
* "description": "Primary brand color"
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* Tokens Studio uses bare `value` / `type` (no `$` prefix — same as
|
|
26
|
+
* Style Dictionary v3, which the plugin's format is based on). Aliases
|
|
27
|
+
* use `{path.to.token}` syntax.
|
|
28
|
+
*
|
|
29
|
+
* `$themes.json` carries the Figma collection/mode bindings that make
|
|
30
|
+
* Tokens Studio's "send to Figma" feature work — preserved here for
|
|
31
|
+
* round-trip with Tokens Studio plugin users (notably Altitude).
|
|
32
|
+
*
|
|
33
|
+
* Output strategy:
|
|
34
|
+
* - One file per TokenSet, named after the set (slugified).
|
|
35
|
+
* - Multi-mode sets emit one file per (set, mode) pair (Tokens Studio
|
|
36
|
+
* convention).
|
|
37
|
+
* - `$metadata.json` enumerates the token set names in order.
|
|
38
|
+
* - `$themes.json` builds a theme entry per mode with selectedTokenSets
|
|
39
|
+
* and the figma-console-mcp metadata stamped onto it.
|
|
40
|
+
*/
|
|
41
|
+
import { FIGMA_MCP_EXTENSION_KEY } from "../types.js";
|
|
42
|
+
export function formatTokensStudio(doc, opts) {
|
|
43
|
+
const warnings = [];
|
|
44
|
+
const files = [];
|
|
45
|
+
// Track which (setName, mode) → filename pairs exist so $metadata + $themes
|
|
46
|
+
// can reference them.
|
|
47
|
+
const setNamesByMode = [];
|
|
48
|
+
// 1. Per-set per-mode token files.
|
|
49
|
+
for (const set of doc.sets) {
|
|
50
|
+
for (const mode of set.modes) {
|
|
51
|
+
const fileSetName = set.modes.length > 1 ? `${slugify(set.name)}/${slugify(mode)}` : slugify(set.name);
|
|
52
|
+
const filename = `${fileSetName}.json`;
|
|
53
|
+
setNamesByMode.push({ setName: fileSetName, mode, filename });
|
|
54
|
+
files.push({
|
|
55
|
+
path: filename,
|
|
56
|
+
content: renderSetFile(set, mode, warnings),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// 2. $metadata.json — ordered list of token sets.
|
|
61
|
+
const tokenSetOrder = setNamesByMode.map((e) => e.setName);
|
|
62
|
+
files.push({
|
|
63
|
+
path: "$metadata.json",
|
|
64
|
+
content: JSON.stringify({ tokenSetOrder }, null, 2) + "\n",
|
|
65
|
+
});
|
|
66
|
+
// 3. $themes.json — one theme entry per mode, with figma-console-mcp
|
|
67
|
+
// metadata so the Tokens Studio plugin's Figma sync features know
|
|
68
|
+
// which Figma collection/mode each theme maps to.
|
|
69
|
+
const themes = buildThemes(doc, setNamesByMode);
|
|
70
|
+
files.push({
|
|
71
|
+
path: "$themes.json",
|
|
72
|
+
content: JSON.stringify(themes, null, 2) + "\n",
|
|
73
|
+
});
|
|
74
|
+
return { files, warnings };
|
|
75
|
+
}
|
|
76
|
+
function slugify(s) {
|
|
77
|
+
return s
|
|
78
|
+
.trim()
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
81
|
+
.replace(/^-+|-+$/g, "");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Render a single set's tokens for a specific mode in Tokens Studio's
|
|
85
|
+
* bare-key JSON format.
|
|
86
|
+
*/
|
|
87
|
+
function renderSetFile(set, mode, warnings) {
|
|
88
|
+
const tree = {};
|
|
89
|
+
for (const token of set.tokens) {
|
|
90
|
+
const value = token.values[mode];
|
|
91
|
+
if (!value)
|
|
92
|
+
continue;
|
|
93
|
+
const tsValue = tsValueFor(value, token, warnings);
|
|
94
|
+
if (tsValue === undefined)
|
|
95
|
+
continue;
|
|
96
|
+
// Walk path creating nested groups.
|
|
97
|
+
let cursor = tree;
|
|
98
|
+
for (let i = 0; i < token.path.length - 1; i++) {
|
|
99
|
+
const segment = token.path[i];
|
|
100
|
+
if (!cursor[segment] || typeof cursor[segment] !== "object") {
|
|
101
|
+
cursor[segment] = {};
|
|
102
|
+
}
|
|
103
|
+
cursor = cursor[segment];
|
|
104
|
+
}
|
|
105
|
+
const leafKey = token.path[token.path.length - 1];
|
|
106
|
+
const leaf = {
|
|
107
|
+
value: tsValue,
|
|
108
|
+
type: tsTypeFor(token),
|
|
109
|
+
};
|
|
110
|
+
if (token.description)
|
|
111
|
+
leaf.description = token.description;
|
|
112
|
+
cursor[leafKey] = leaf;
|
|
113
|
+
}
|
|
114
|
+
return JSON.stringify(sortKeys(tree), null, 2) + "\n";
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Build the $themes.json structure. One theme entry per (set, mode) tuple.
|
|
118
|
+
* For multi-mode sets, the theme name combines set + mode.
|
|
119
|
+
*/
|
|
120
|
+
function buildThemes(doc, setNamesByMode) {
|
|
121
|
+
const themes = [];
|
|
122
|
+
// Group by mode so each mode gets one theme entry pulling in the
|
|
123
|
+
// appropriate set files.
|
|
124
|
+
const allModes = new Set();
|
|
125
|
+
for (const e of setNamesByMode)
|
|
126
|
+
allModes.add(e.mode);
|
|
127
|
+
for (const mode of allModes) {
|
|
128
|
+
const selectedTokenSets = {};
|
|
129
|
+
for (const e of setNamesByMode) {
|
|
130
|
+
if (e.mode === mode) {
|
|
131
|
+
selectedTokenSets[e.setName] = "enabled";
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// Other-mode sets are disabled in this theme (Tokens Studio
|
|
135
|
+
// convention: only one mode of each set is "enabled" per theme).
|
|
136
|
+
selectedTokenSets[e.setName] = "source";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Find the Figma collection + mode IDs for this mode (preserved
|
|
140
|
+
// from the export converter's $extensions metadata).
|
|
141
|
+
const figmaCollectionId = findCollectionIdForMode(doc, mode);
|
|
142
|
+
const figmaModeId = findModeId(doc, mode);
|
|
143
|
+
const theme = {
|
|
144
|
+
id: `${slugify(mode)}-${Math.random().toString(36).slice(2, 10)}`,
|
|
145
|
+
name: mode,
|
|
146
|
+
selectedTokenSets,
|
|
147
|
+
};
|
|
148
|
+
// Stash Figma metadata for round-trip with Tokens Studio plugin.
|
|
149
|
+
if (figmaCollectionId)
|
|
150
|
+
theme.$figmaCollectionId = figmaCollectionId;
|
|
151
|
+
if (figmaModeId)
|
|
152
|
+
theme.$figmaModeId = figmaModeId;
|
|
153
|
+
// Also stash our own extension namespace for tools that read it.
|
|
154
|
+
if (figmaCollectionId || figmaModeId) {
|
|
155
|
+
theme.$extensions = {
|
|
156
|
+
[FIGMA_MCP_EXTENSION_KEY]: {
|
|
157
|
+
...(figmaCollectionId ? { figmaCollectionId } : {}),
|
|
158
|
+
...(figmaModeId ? { figmaModeId } : {}),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
themes.push(theme);
|
|
163
|
+
}
|
|
164
|
+
return themes;
|
|
165
|
+
}
|
|
166
|
+
function findCollectionIdForMode(doc, _mode) {
|
|
167
|
+
// The mode-to-collection mapping requires knowing which set the mode
|
|
168
|
+
// belongs to. For simplicity, return the first collection ID we find;
|
|
169
|
+
// multi-collection support requires per-set theme entries which the
|
|
170
|
+
// Tokens Studio plugin does support.
|
|
171
|
+
for (const set of doc.sets) {
|
|
172
|
+
if (set.meta?.figmaCollectionId)
|
|
173
|
+
return set.meta.figmaCollectionId;
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
function findModeId(_doc, _mode) {
|
|
178
|
+
// We don't carry Figma's modeId through the internal model in this
|
|
179
|
+
// version — it's computed lazily during apply via the
|
|
180
|
+
// (collectionId, modeName) lookup map. Future enhancement: stash the
|
|
181
|
+
// modeId in TokenSet.meta during export so it round-trips through
|
|
182
|
+
// Tokens Studio.
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Map DTCG types to Tokens Studio's type names. Tokens Studio uses SD-style
|
|
187
|
+
* type names with some additions (e.g. "sizing", "borderRadius",
|
|
188
|
+
* "boxShadow", "typography", "opacity", "fontFamilies", "fontWeights").
|
|
189
|
+
*/
|
|
190
|
+
function tsTypeFor(token) {
|
|
191
|
+
if (token.type === "color")
|
|
192
|
+
return "color";
|
|
193
|
+
if (token.type === "fontFamily")
|
|
194
|
+
return "fontFamilies";
|
|
195
|
+
if (token.type === "fontWeight")
|
|
196
|
+
return "fontWeights";
|
|
197
|
+
if (token.type === "typography")
|
|
198
|
+
return "typography";
|
|
199
|
+
if (token.type === "shadow")
|
|
200
|
+
return "boxShadow";
|
|
201
|
+
if (token.type === "duration")
|
|
202
|
+
return "time";
|
|
203
|
+
if (token.type === "dimension") {
|
|
204
|
+
const lower = token.path[0]?.toLowerCase() ?? "";
|
|
205
|
+
if (lower.includes("border") || lower.includes("radius"))
|
|
206
|
+
return "borderRadius";
|
|
207
|
+
if (lower.startsWith("space") || lower.startsWith("spacing"))
|
|
208
|
+
return "spacing";
|
|
209
|
+
if (lower.includes("size") || lower.includes("width") || lower.includes("height"))
|
|
210
|
+
return "sizing";
|
|
211
|
+
return "sizing";
|
|
212
|
+
}
|
|
213
|
+
return token.type;
|
|
214
|
+
}
|
|
215
|
+
function tsValueFor(value, token, warnings) {
|
|
216
|
+
if (value.reference) {
|
|
217
|
+
const bare = value.reference.replace(/^\{|\}$/g, "");
|
|
218
|
+
if (bare.startsWith("__library:") || bare === "unknown") {
|
|
219
|
+
warnings.push(`Skipped ${token.path.join(".")} in Tokens Studio — cross-library alias unresolved.`);
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
return `{${bare}}`;
|
|
223
|
+
}
|
|
224
|
+
if (value.literal === undefined || value.literal === null)
|
|
225
|
+
return undefined;
|
|
226
|
+
if (typeof value.literal === "number" && token.type === "dimension") {
|
|
227
|
+
return `${value.literal}px`;
|
|
228
|
+
}
|
|
229
|
+
return value.literal;
|
|
230
|
+
}
|
|
231
|
+
function sortKeys(node) {
|
|
232
|
+
if (node === null || typeof node !== "object" || Array.isArray(node)) {
|
|
233
|
+
return node;
|
|
234
|
+
}
|
|
235
|
+
const obj = node;
|
|
236
|
+
const sorted = {};
|
|
237
|
+
// $-prefixed keys first (matches Tokens Studio convention), then alphabetical.
|
|
238
|
+
const keys = Object.keys(obj).sort((a, b) => {
|
|
239
|
+
const aDollar = a.startsWith("$");
|
|
240
|
+
const bDollar = b.startsWith("$");
|
|
241
|
+
if (aDollar && !bDollar)
|
|
242
|
+
return -1;
|
|
243
|
+
if (!aDollar && bDollar)
|
|
244
|
+
return 1;
|
|
245
|
+
return a.localeCompare(b);
|
|
246
|
+
});
|
|
247
|
+
for (const k of keys)
|
|
248
|
+
sorted[k] = sortKeys(obj[k]);
|
|
249
|
+
return sorted;
|
|
250
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript module formatter.
|
|
3
|
+
*
|
|
4
|
+
* Output shape:
|
|
5
|
+
*
|
|
6
|
+
* // tokens.ts
|
|
7
|
+
* export const tokens = {
|
|
8
|
+
* color: {
|
|
9
|
+
* primary: "#4085F2",
|
|
10
|
+
* brand: { primary: "#FF00AA" }
|
|
11
|
+
* },
|
|
12
|
+
* spacing: {
|
|
13
|
+
* md: "16px"
|
|
14
|
+
* }
|
|
15
|
+
* } as const;
|
|
16
|
+
*
|
|
17
|
+
* export type Tokens = typeof tokens;
|
|
18
|
+
*
|
|
19
|
+
* Multi-mode tokens emit as nested objects keyed by mode name:
|
|
20
|
+
*
|
|
21
|
+
* export const tokens = {
|
|
22
|
+
* mode: {
|
|
23
|
+
* primary: { Light: "#FFFFFF", Dark: "#000000" }
|
|
24
|
+
* }
|
|
25
|
+
* } as const;
|
|
26
|
+
*
|
|
27
|
+
* Aliases get resolved at write time to the literal value (TypeScript
|
|
28
|
+
* can't natively express references). Cross-library aliases get emitted
|
|
29
|
+
* as `null` with a "TODO: cross-library alias unresolved" comment so
|
|
30
|
+
* consumers see the gap.
|
|
31
|
+
*/
|
|
32
|
+
import { buildTokenIndex, resolveAliasChain } from "../alias-resolver.js";
|
|
33
|
+
export function formatTsModule(doc, opts) {
|
|
34
|
+
const warnings = [];
|
|
35
|
+
const files = [];
|
|
36
|
+
const splitByCollection = opts.target.splitByCollection ?? false;
|
|
37
|
+
// splitByMode doesn't make conceptual sense for a TypeScript module
|
|
38
|
+
// (the value is held in-memory at runtime, not selected by CSS cascade).
|
|
39
|
+
// TypeScript modules can't natively express alias references — runtime
|
|
40
|
+
// code reading tokens.color.X gets a string at runtime, no resolution
|
|
41
|
+
// logic. Build a document-wide token index so aliases resolve to their
|
|
42
|
+
// literal target at export time. The index spans every set even when
|
|
43
|
+
// splitByCollection writes single-set files, because aliases often
|
|
44
|
+
// cross set boundaries (semantic → primitives).
|
|
45
|
+
const tokenIndex = buildTokenIndex(doc);
|
|
46
|
+
if (splitByCollection) {
|
|
47
|
+
for (const set of doc.sets) {
|
|
48
|
+
files.push({
|
|
49
|
+
path: filenameFor(opts, set),
|
|
50
|
+
content: renderTsFile([set], opts, tokenIndex, warnings),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
files.push({
|
|
56
|
+
path: filenameFor(opts),
|
|
57
|
+
content: renderTsFile(doc.sets, opts, tokenIndex, warnings),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return { files, warnings };
|
|
61
|
+
}
|
|
62
|
+
function filenameFor(opts, set) {
|
|
63
|
+
if (opts.target.filename)
|
|
64
|
+
return opts.target.filename;
|
|
65
|
+
if (set)
|
|
66
|
+
return `${slugify(set.name)}.tokens.ts`;
|
|
67
|
+
return "tokens.ts";
|
|
68
|
+
}
|
|
69
|
+
function slugify(s) {
|
|
70
|
+
return s
|
|
71
|
+
.trim()
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
74
|
+
.replace(/^-+|-+$/g, "");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Convert a path segment to a valid JS identifier or quoted-key form.
|
|
78
|
+
* Numeric leading chars, hyphens, etc. → quoted string key.
|
|
79
|
+
*/
|
|
80
|
+
function jsKey(segment) {
|
|
81
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(segment))
|
|
82
|
+
return segment;
|
|
83
|
+
return JSON.stringify(segment);
|
|
84
|
+
}
|
|
85
|
+
function renderTsFile(sets, opts, tokenIndex, warnings) {
|
|
86
|
+
const prefix = opts.target.prefix ?? "";
|
|
87
|
+
const lines = [];
|
|
88
|
+
lines.push("/**");
|
|
89
|
+
lines.push(" * Generated by figma-console-mcp — do not edit by hand.");
|
|
90
|
+
lines.push(" * Regenerate with: figma_export_tokens (or your npm tokens:export script)");
|
|
91
|
+
lines.push(" */");
|
|
92
|
+
lines.push("");
|
|
93
|
+
const constName = prefix
|
|
94
|
+
? `${prefix.replace(/-+$/, "").replace(/-(\w)/g, (_, c) => c.toUpperCase())}Tokens`
|
|
95
|
+
: "tokens";
|
|
96
|
+
// Build the nested object structure by walking each set's tokens.
|
|
97
|
+
const tree = {};
|
|
98
|
+
for (const set of sets) {
|
|
99
|
+
for (const token of set.tokens) {
|
|
100
|
+
writeTokenIntoTree(tree, token, set, tokenIndex, warnings);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
lines.push(`export const ${constName} = ${renderTree(tree, 0)} as const;`);
|
|
104
|
+
lines.push("");
|
|
105
|
+
lines.push(`export type ${constName[0].toUpperCase()}${constName.slice(1)} = typeof ${constName};`);
|
|
106
|
+
lines.push("");
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
function writeTokenIntoTree(tree, token, set, tokenIndex, warnings) {
|
|
110
|
+
let cursor = tree;
|
|
111
|
+
for (let i = 0; i < token.path.length - 1; i++) {
|
|
112
|
+
const segment = token.path[i];
|
|
113
|
+
let next = cursor[segment];
|
|
114
|
+
if (!next || isLeaf(next)) {
|
|
115
|
+
next = {};
|
|
116
|
+
cursor[segment] = next;
|
|
117
|
+
}
|
|
118
|
+
cursor = next;
|
|
119
|
+
}
|
|
120
|
+
const leafKey = token.path[token.path.length - 1];
|
|
121
|
+
const isMulti = set.modes.length > 1;
|
|
122
|
+
const resolvedValues = {};
|
|
123
|
+
for (const [modeName, value] of Object.entries(token.values)) {
|
|
124
|
+
let effective = value;
|
|
125
|
+
if (value.reference) {
|
|
126
|
+
effective = resolveAliasChain(value, modeName, tokenIndex);
|
|
127
|
+
if (!effective) {
|
|
128
|
+
const bare = value.reference.replace(/^\{|\}$/g, "");
|
|
129
|
+
const reason = bare.startsWith("__library:") || bare === "unknown"
|
|
130
|
+
? "cross-library alias"
|
|
131
|
+
: "alias target not found";
|
|
132
|
+
warnings.push(`Skipped ${token.path.join(".")} (mode "${modeName}") in TS module — ${reason}: ${value.reference}.`);
|
|
133
|
+
resolvedValues[modeName] = null;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!effective || effective.literal === undefined || effective.literal === null)
|
|
138
|
+
continue;
|
|
139
|
+
resolvedValues[modeName] = formatLiteral(effective.literal, token.type);
|
|
140
|
+
}
|
|
141
|
+
cursor[leafKey] = {
|
|
142
|
+
__leaf: true,
|
|
143
|
+
values: resolvedValues,
|
|
144
|
+
type: token.type,
|
|
145
|
+
isMultiMode: isMulti,
|
|
146
|
+
description: token.description,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function isLeaf(node) {
|
|
150
|
+
return node.__leaf === true;
|
|
151
|
+
}
|
|
152
|
+
function renderTree(tree, indent) {
|
|
153
|
+
const ind = " ".repeat(indent);
|
|
154
|
+
const innerInd = " ".repeat(indent + 1);
|
|
155
|
+
const entries = [];
|
|
156
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
157
|
+
const k = jsKey(key);
|
|
158
|
+
if (isLeaf(value)) {
|
|
159
|
+
const leaf = value;
|
|
160
|
+
if (leaf.isMultiMode) {
|
|
161
|
+
// Emit as { Mode: value } object
|
|
162
|
+
const modeEntries = [];
|
|
163
|
+
for (const [m, v] of Object.entries(leaf.values)) {
|
|
164
|
+
modeEntries.push(`${jsKey(m)}: ${renderValue(v)}`);
|
|
165
|
+
}
|
|
166
|
+
entries.push(`${innerInd}${k}: { ${modeEntries.join(", ")} }`);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// Single-mode: emit value directly
|
|
170
|
+
const onlyValue = Object.values(leaf.values)[0];
|
|
171
|
+
entries.push(`${innerInd}${k}: ${renderValue(onlyValue)}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
entries.push(`${innerInd}${k}: ${renderTree(value, indent + 1)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (entries.length === 0)
|
|
179
|
+
return "{}";
|
|
180
|
+
return `{\n${entries.join(",\n")},\n${ind}}`;
|
|
181
|
+
}
|
|
182
|
+
function renderValue(v) {
|
|
183
|
+
if (v === null)
|
|
184
|
+
return "null /* TODO: cross-library alias unresolved */";
|
|
185
|
+
if (typeof v === "string")
|
|
186
|
+
return JSON.stringify(v);
|
|
187
|
+
if (typeof v === "number" || typeof v === "boolean")
|
|
188
|
+
return String(v);
|
|
189
|
+
return JSON.stringify(v);
|
|
190
|
+
}
|
|
191
|
+
function formatLiteral(value, type) {
|
|
192
|
+
if (typeof value === "number") {
|
|
193
|
+
if (type === "dimension")
|
|
194
|
+
return `${value}px`;
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the token sync engine.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the canonical types, the parser/formatter dispatchers, the
|
|
5
|
+
* config loader, the alias resolver, and the Figma converter. External
|
|
6
|
+
* callers (tools, tests) should import from here rather than reaching into
|
|
7
|
+
* subdirectories.
|
|
8
|
+
*/
|
|
9
|
+
export { FIGMA_MCP_EXTENSION_KEY } from "./types.js";
|
|
10
|
+
export { TokensConfigSchema, loadTokensConfig, findTokensConfig, DEFAULT_TOKENS_CONFIG, buildSuggestedScaffold, resolveOutputTargets, resolveConflictStrategy, } from "./config.js";
|
|
11
|
+
export { ExportTokensInputSchema, ImportTokensInputSchema, ExportFormatSchema, ImportFormatSchema, SyncStrategySchema, ConflictResolutionSchema, } from "./schemas.js";
|
|
12
|
+
export { parse, detectFormat, } from "./parsers/index.js";
|
|
13
|
+
export { format, } from "./formatters/index.js";
|
|
14
|
+
export { buildTokenIndex, resolveReference, resolveAliasChain, validateAliases, formatDtcgReference, parseDtcgReference, } from "./alias-resolver.js";
|
|
15
|
+
export { convertFigmaVariablesToDocument, } from "./figma-converter.js";
|