@servicetitan/hammer-token 2.5.0 → 3.0.0
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/CHANGELOG.md +56 -0
- package/README.md +332 -0
- package/build/web/core/component-variables.scss +1088 -131
- package/build/web/core/component.d.ts +558 -0
- package/build/web/core/component.js +6685 -249
- package/build/web/core/component.scss +557 -69
- package/build/web/core/css-utils/a2-border.css +47 -45
- package/build/web/core/css-utils/a2-color.css +443 -227
- package/build/web/core/css-utils/a2-font.css +0 -2
- package/build/web/core/css-utils/a2-spacing.css +476 -478
- package/build/web/core/css-utils/a2-utils.css +992 -772
- package/build/web/core/css-utils/border.css +47 -45
- package/build/web/core/css-utils/color.css +443 -227
- package/build/web/core/css-utils/font.css +0 -2
- package/build/web/core/css-utils/spacing.css +476 -478
- package/build/web/core/css-utils/utils.css +992 -772
- package/build/web/core/index.d.ts +6 -0
- package/build/web/core/index.js +1 -1
- package/build/web/core/primitive-variables.scss +148 -65
- package/build/web/core/primitive.d.ts +209 -0
- package/build/web/core/primitive.js +779 -61
- package/build/web/core/primitive.scss +207 -124
- package/build/web/core/semantic-variables.scss +363 -239
- package/build/web/core/semantic.d.ts +221 -0
- package/build/web/core/semantic.js +1613 -347
- package/build/web/core/semantic.scss +219 -137
- package/build/web/index.d.ts +3 -4
- package/build/web/index.js +0 -1
- package/build/web/types.d.ts +17 -0
- package/config.js +121 -497
- package/eslint.config.mjs +11 -1
- package/package.json +15 -5
- package/src/global/primitive/breakpoint.tokens.json +54 -0
- package/src/global/primitive/color.tokens.json +1092 -0
- package/src/global/primitive/duration.tokens.json +44 -0
- package/src/global/primitive/font.tokens.json +151 -0
- package/src/global/primitive/radius.tokens.json +94 -0
- package/src/global/primitive/size.tokens.json +174 -0
- package/src/global/primitive/transition.tokens.json +32 -0
- package/src/theme/core/background.tokens.json +1312 -0
- package/src/theme/core/border.tokens.json +192 -0
- package/src/theme/core/chart.tokens.json +982 -0
- package/src/theme/core/component/ai-mark.tokens.json +20 -0
- package/src/theme/core/component/alert.tokens.json +261 -0
- package/src/theme/core/component/announcement.tokens.json +460 -0
- package/src/theme/core/component/avatar.tokens.json +137 -0
- package/src/theme/core/component/badge.tokens.json +42 -0
- package/src/theme/core/component/breadcrumb.tokens.json +42 -0
- package/src/theme/core/component/button-toggle.tokens.json +428 -0
- package/src/theme/core/component/button.tokens.json +941 -0
- package/src/theme/core/component/calendar.tokens.json +391 -0
- package/src/theme/core/component/card.tokens.json +107 -0
- package/src/theme/core/component/checkbox.tokens.json +631 -0
- package/src/theme/core/component/chip.tokens.json +169 -0
- package/src/theme/core/component/combobox.tokens.json +269 -0
- package/src/theme/core/component/details.tokens.json +152 -0
- package/src/theme/core/component/dialog.tokens.json +87 -0
- package/src/theme/core/component/divider.tokens.json +23 -0
- package/src/theme/core/component/dnd.tokens.json +208 -0
- package/src/theme/core/component/drawer.tokens.json +61 -0
- package/src/theme/core/component/drilldown.tokens.json +61 -0
- package/src/theme/core/component/edit-card.tokens.json +381 -0
- package/src/theme/core/component/field-label.tokens.json +42 -0
- package/src/theme/core/component/field-message.tokens.json +74 -0
- package/src/theme/core/component/icon.tokens.json +42 -0
- package/src/theme/core/component/link.tokens.json +108 -0
- package/src/theme/core/component/list-view.tokens.json +82 -0
- package/src/theme/core/component/listbox.tokens.json +283 -0
- package/src/theme/core/component/menu.tokens.json +230 -0
- package/src/theme/core/component/overflow.tokens.json +84 -0
- package/src/theme/core/component/page.tokens.json +377 -0
- package/src/theme/core/component/pagination.tokens.json +63 -0
- package/src/theme/core/component/popover.tokens.json +122 -0
- package/src/theme/core/component/progress-bar.tokens.json +133 -0
- package/src/theme/core/component/radio.tokens.json +631 -0
- package/src/theme/core/component/segmented-control.tokens.json +175 -0
- package/src/theme/core/component/select-card.tokens.json +943 -0
- package/src/theme/core/component/side-nav.tokens.json +349 -0
- package/src/theme/core/component/skeleton.tokens.json +42 -0
- package/src/theme/core/component/spinner.tokens.json +96 -0
- package/src/theme/core/component/status-icon.tokens.json +164 -0
- package/src/theme/core/component/stepper.tokens.json +484 -0
- package/src/theme/core/component/switch.tokens.json +285 -0
- package/src/theme/core/component/tab.tokens.json +192 -0
- package/src/theme/core/component/text-field.tokens.json +160 -0
- package/src/theme/core/component/text.tokens.json +59 -0
- package/src/theme/core/component/toast.tokens.json +343 -0
- package/src/theme/core/component/toolbar.tokens.json +114 -0
- package/src/theme/core/component/tooltip.tokens.json +61 -0
- package/src/theme/core/focus.tokens.json +56 -0
- package/src/theme/core/foreground.tokens.json +416 -0
- package/src/theme/core/gradient.tokens.json +41 -0
- package/src/theme/core/opacity.tokens.json +25 -0
- package/src/theme/core/shadow.tokens.json +81 -0
- package/src/theme/core/status.tokens.json +74 -0
- package/src/theme/core/typography.tokens.json +163 -0
- package/src/utils/__tests__/css-utils-format-utils.test.js +312 -0
- package/src/utils/__tests__/sd-build-configs.test.js +306 -0
- package/src/utils/__tests__/sd-formats.test.js +950 -0
- package/src/utils/__tests__/sd-transforms.test.js +336 -0
- package/src/utils/__tests__/token-helpers.test.js +1160 -0
- package/src/utils/copy-css-utils-cli.js +13 -1
- package/src/utils/css-utils-format-utils.js +105 -176
- package/src/utils/figma/__tests__/sync-gradient.test.js +561 -0
- package/src/utils/figma/__tests__/token-conversion.test.js +117 -0
- package/src/utils/figma/__tests__/token-resolution.test.js +231 -0
- package/src/utils/figma/auth.js +355 -0
- package/src/utils/figma/constants.js +22 -0
- package/src/utils/figma/errors.js +80 -0
- package/src/utils/figma/figma-api.js +1069 -0
- package/src/utils/figma/get-token.js +348 -0
- package/src/utils/figma/sync-components.js +909 -0
- package/src/utils/figma/sync-main.js +692 -0
- package/src/utils/figma/sync-orchestration.js +683 -0
- package/src/utils/figma/sync-primitives.js +230 -0
- package/src/utils/figma/sync-semantic.js +1056 -0
- package/src/utils/figma/token-conversion.js +340 -0
- package/src/utils/figma/token-parsing.js +186 -0
- package/src/utils/figma/token-resolution.js +569 -0
- package/src/utils/figma/utils.js +199 -0
- package/src/utils/sd-build-configs.js +305 -0
- package/src/utils/sd-formats.js +965 -0
- package/src/utils/sd-transforms.js +165 -0
- package/src/utils/token-helpers.js +848 -0
- package/tsconfig.json +18 -0
- package/vitest.config.js +17 -0
- package/.turbo/turbo-build.log +0 -37
- package/build/web/core/raw.js +0 -229
- package/src/global/primitive/breakpoint.js +0 -19
- package/src/global/primitive/color.js +0 -231
- package/src/global/primitive/duration.js +0 -16
- package/src/global/primitive/font.js +0 -60
- package/src/global/primitive/radius.js +0 -31
- package/src/global/primitive/size.js +0 -55
- package/src/global/primitive/transition.js +0 -16
- package/src/theme/core/background.js +0 -170
- package/src/theme/core/border.js +0 -103
- package/src/theme/core/charts.js +0 -439
- package/src/theme/core/component/button.js +0 -708
- package/src/theme/core/component/checkbox.js +0 -405
- package/src/theme/core/focus.js +0 -35
- package/src/theme/core/foreground.js +0 -148
- package/src/theme/core/overlay.js +0 -137
- package/src/theme/core/shadow.js +0 -29
- package/src/theme/core/status.js +0 -49
- package/src/theme/core/typography.js +0 -82
- package/type/types.ts +0 -341
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sync Orchestration
|
|
5
|
+
*
|
|
6
|
+
* Main orchestration logic for syncing themes to Figma.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
loadThemeTokenFiles,
|
|
11
|
+
parseTokenFile,
|
|
12
|
+
flattenTokenTree,
|
|
13
|
+
pathToVariableName,
|
|
14
|
+
isComponentToken,
|
|
15
|
+
} = require("./token-parsing");
|
|
16
|
+
const { resolvePrimitiveReferences } = require("./token-resolution");
|
|
17
|
+
const { figmaRequest } = require("./figma-api");
|
|
18
|
+
const { tokenValuesDiffer } = require("./figma-api");
|
|
19
|
+
const { isShadowSizeToken } = require("./token-conversion");
|
|
20
|
+
const { buildPrimitiveVariableRequest } = require("./sync-primitives");
|
|
21
|
+
const { buildSemanticVariableRequest } = require("./sync-semantic");
|
|
22
|
+
const { buildComponentVariableRequest } = require("./sync-components");
|
|
23
|
+
const {
|
|
24
|
+
getOrCreateCollection,
|
|
25
|
+
getOrCreateExtendedCollection,
|
|
26
|
+
getOrCreateModes,
|
|
27
|
+
} = require("./figma-api");
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sync a single theme to Figma
|
|
31
|
+
* @param {Object} config - Configuration object
|
|
32
|
+
* @param {string} themeName - Theme name (e.g., "core", "test")
|
|
33
|
+
* @param {Map} coreTokens - Core theme tokens for comparison (null for core theme)
|
|
34
|
+
* @param {string} collectionName - Collection name to use
|
|
35
|
+
* @param {Object} [options] - Sync options
|
|
36
|
+
* @param {boolean} [options.dryRun=false] - If true, validate without making changes
|
|
37
|
+
* @param {boolean} [options.verbose=false] - Enable verbose logging
|
|
38
|
+
* @returns {Promise<Object>} Stats object
|
|
39
|
+
*/
|
|
40
|
+
async function syncTheme(
|
|
41
|
+
config,
|
|
42
|
+
themeName,
|
|
43
|
+
coreTokens = null,
|
|
44
|
+
collectionName = null,
|
|
45
|
+
options = {},
|
|
46
|
+
) {
|
|
47
|
+
const { dryRun = false, verbose = false } = options;
|
|
48
|
+
const isExtended = themeName !== "core" && coreTokens !== null;
|
|
49
|
+
const actualCollectionName = collectionName || config.collectionName;
|
|
50
|
+
|
|
51
|
+
if (isExtended) {
|
|
52
|
+
console.log(`\n🎨 Processing extended theme: ${themeName}`);
|
|
53
|
+
console.log(`📦 Collection: ${actualCollectionName}\n`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Load token files for this theme
|
|
57
|
+
const tokenFiles = loadThemeTokenFiles(themeName);
|
|
58
|
+
if (tokenFiles.length === 0) {
|
|
59
|
+
console.log(` ⚠️ No token files found for theme "${themeName}"`);
|
|
60
|
+
return { created: 0, updated: 0, skipped: 0, errors: 0, errorMessages: [] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Parse tokens
|
|
64
|
+
const allTokens = [];
|
|
65
|
+
for (const filePath of tokenFiles) {
|
|
66
|
+
const tokenData = parseTokenFile(filePath);
|
|
67
|
+
const flattened = flattenTokenTree(tokenData, [], [], filePath);
|
|
68
|
+
allTokens.push(...flattened);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build token registry
|
|
72
|
+
const tokenRegistry = new Map();
|
|
73
|
+
for (const tokenData of allTokens) {
|
|
74
|
+
const variableName = pathToVariableName(tokenData.path);
|
|
75
|
+
tokenRegistry.set(variableName, {
|
|
76
|
+
...tokenData,
|
|
77
|
+
variableName,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Resolve primitive references
|
|
82
|
+
const resolvedTokens = resolvePrimitiveReferences(tokenRegistry);
|
|
83
|
+
|
|
84
|
+
// Filter tokens for extended themes (only keep ones that differ from core)
|
|
85
|
+
let tokensToSync = resolvedTokens;
|
|
86
|
+
if (isExtended) {
|
|
87
|
+
const filteredTokens = new Map();
|
|
88
|
+
for (const [tokenPath, tokenData] of resolvedTokens.entries()) {
|
|
89
|
+
const coreToken = coreTokens.get(tokenPath);
|
|
90
|
+
if (!coreToken) {
|
|
91
|
+
// Token doesn't exist in core - include it
|
|
92
|
+
filteredTokens.set(tokenPath, tokenData);
|
|
93
|
+
} else {
|
|
94
|
+
// Compare values
|
|
95
|
+
const lightDiffers = tokenValuesDiffer(
|
|
96
|
+
tokenData.resolvedLight,
|
|
97
|
+
coreToken.resolvedLight,
|
|
98
|
+
);
|
|
99
|
+
const darkDiffers = tokenValuesDiffer(
|
|
100
|
+
tokenData.resolvedDark,
|
|
101
|
+
coreToken.resolvedDark,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (lightDiffers || darkDiffers) {
|
|
105
|
+
filteredTokens.set(tokenPath, tokenData);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
tokensToSync = filteredTokens;
|
|
110
|
+
console.log(
|
|
111
|
+
` Found ${tokensToSync.size} tokens that differ from core (out of ${resolvedTokens.size} total)\n`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Get or create collection
|
|
116
|
+
// Optimize: Cache variables data to avoid redundant API calls
|
|
117
|
+
let collection;
|
|
118
|
+
let coreCollectionId = null;
|
|
119
|
+
let cachedVariablesData = null;
|
|
120
|
+
|
|
121
|
+
if (isExtended) {
|
|
122
|
+
// For extended themes, create an extended collection
|
|
123
|
+
// First, we need the core collection ID
|
|
124
|
+
cachedVariablesData = await figmaRequest(config, "/variables/local");
|
|
125
|
+
const collections = cachedVariablesData.meta?.variableCollections
|
|
126
|
+
? Object.values(cachedVariablesData.meta.variableCollections)
|
|
127
|
+
: [];
|
|
128
|
+
const coreCollection = collections.find(
|
|
129
|
+
(coll) => coll.name === config.collectionName && !coll.isExtension,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (!coreCollection) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Core collection "${config.collectionName}" not found. Please sync core theme first.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
coreCollectionId = coreCollection.id;
|
|
139
|
+
collection = await getOrCreateExtendedCollection(
|
|
140
|
+
config,
|
|
141
|
+
coreCollectionId,
|
|
142
|
+
actualCollectionName,
|
|
143
|
+
);
|
|
144
|
+
} else {
|
|
145
|
+
// For core theme, create regular collection
|
|
146
|
+
const collectionConfig = {
|
|
147
|
+
...config,
|
|
148
|
+
collectionName: actualCollectionName,
|
|
149
|
+
};
|
|
150
|
+
console.log(" 📥 Loading collection and variables...");
|
|
151
|
+
collection = await getOrCreateCollection(collectionConfig);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Get or create modes (extended collections inherit modes from parent)
|
|
155
|
+
const modes = await getOrCreateModes(
|
|
156
|
+
{ ...config, collectionName: actualCollectionName },
|
|
157
|
+
collection.id,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Get existing variables (reuse cached data if available, otherwise fetch)
|
|
161
|
+
if (!cachedVariablesData) {
|
|
162
|
+
cachedVariablesData = await figmaRequest(config, "/variables/local");
|
|
163
|
+
}
|
|
164
|
+
const allExistingVariables = cachedVariablesData.meta?.variables
|
|
165
|
+
? Object.values(cachedVariablesData.meta.variables)
|
|
166
|
+
: [];
|
|
167
|
+
|
|
168
|
+
// For extended collections, get variables from parent collection (they inherit)
|
|
169
|
+
// For core collections, get variables from the collection itself
|
|
170
|
+
let existingVariables;
|
|
171
|
+
if (isExtended && coreCollectionId) {
|
|
172
|
+
// Extended collections inherit variables from parent - get parent's variables
|
|
173
|
+
existingVariables = allExistingVariables.filter(
|
|
174
|
+
(v) => v.variableCollectionId === coreCollectionId,
|
|
175
|
+
);
|
|
176
|
+
} else {
|
|
177
|
+
// Core collection - get its own variables
|
|
178
|
+
existingVariables = allExistingVariables.filter(
|
|
179
|
+
(v) => v.variableCollectionId === collection.id,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// For extended collections, we only create variable mode value overrides (not new variables)
|
|
184
|
+
// For core collections, we create both variables and values
|
|
185
|
+
if (isExtended) {
|
|
186
|
+
// Extended collection: only create overrides for tokens that differ
|
|
187
|
+
// Variables are inherited from parent, so we only need variableModeValues
|
|
188
|
+
// Use parent collection's variables but target the extended collection for overrides
|
|
189
|
+
const tempIdMapForOverrides = new Map();
|
|
190
|
+
|
|
191
|
+
// Build semantic variable request - it will find existing variables from parent
|
|
192
|
+
// Note: For extended collections, we only want to create overrides, not new variables
|
|
193
|
+
// So we filter tokensToSync to only include tokens that have corresponding variables in parent
|
|
194
|
+
// Separate semantic and component tokens
|
|
195
|
+
const semanticTokensWithParent = new Map();
|
|
196
|
+
const componentTokensWithParent = new Map();
|
|
197
|
+
|
|
198
|
+
// Optimize: Create a Map for O(1) variable lookups
|
|
199
|
+
const variableNameMap = new Map();
|
|
200
|
+
for (const v of existingVariables) {
|
|
201
|
+
if (v.name) variableNameMap.set(v.name, v);
|
|
202
|
+
if (v.key && v.key !== v.name) variableNameMap.set(v.key, v);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const [tokenPath, tokenData] of tokensToSync.entries()) {
|
|
206
|
+
const isComponent = isComponentToken(tokenData.sourceFilePath);
|
|
207
|
+
const variableName = isComponent
|
|
208
|
+
? `component/${tokenPath}`
|
|
209
|
+
: `semantic/${tokenPath}`;
|
|
210
|
+
|
|
211
|
+
// Check if variable exists in parent (including shadow size components)
|
|
212
|
+
// Optimize: Use Map lookup instead of array find
|
|
213
|
+
const parentVar =
|
|
214
|
+
variableNameMap.get(variableName) || variableNameMap.get(tokenPath);
|
|
215
|
+
|
|
216
|
+
// For shadow size tokens, check for x, y, blur components
|
|
217
|
+
if (!parentVar && isShadowSizeToken(tokenPath) && !isComponent) {
|
|
218
|
+
const hasX = variableNameMap.get(`${variableName}/x`);
|
|
219
|
+
const hasY = variableNameMap.get(`${variableName}/y`);
|
|
220
|
+
const hasBlur = variableNameMap.get(`${variableName}/blur`);
|
|
221
|
+
if (hasX && hasY && hasBlur) {
|
|
222
|
+
semanticTokensWithParent.set(tokenPath, tokenData);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (parentVar) {
|
|
228
|
+
if (isComponent) {
|
|
229
|
+
componentTokensWithParent.set(tokenPath, tokenData);
|
|
230
|
+
} else {
|
|
231
|
+
semanticTokensWithParent.set(tokenPath, tokenData);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Use semantic tokens for semantic request
|
|
237
|
+
const tokensWithParentVariables = semanticTokensWithParent;
|
|
238
|
+
|
|
239
|
+
if (
|
|
240
|
+
tokensWithParentVariables.size === 0 &&
|
|
241
|
+
componentTokensWithParent.size === 0
|
|
242
|
+
) {
|
|
243
|
+
console.log(
|
|
244
|
+
` ℹ️ No tokens to override (all match core or variables not found in parent)\n`,
|
|
245
|
+
);
|
|
246
|
+
return {
|
|
247
|
+
created: 0,
|
|
248
|
+
updated: 0,
|
|
249
|
+
skipped: 0,
|
|
250
|
+
errors: 0,
|
|
251
|
+
errorMessages: [],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// For extended themes, merge core tokens with extended tokens for reference resolution
|
|
256
|
+
// Extended tokens may reference primitives from core theme
|
|
257
|
+
const allTokensForRefs = new Map(coreTokens);
|
|
258
|
+
for (const [tokenPath, tokenData] of resolvedTokens.entries()) {
|
|
259
|
+
allTokensForRefs.set(tokenPath, tokenData);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const semanticRequest = buildSemanticVariableRequest(
|
|
263
|
+
tokensWithParentVariables,
|
|
264
|
+
existingVariables, // Parent collection variables (inherited)
|
|
265
|
+
collection.id, // Extended collection ID (for overrides)
|
|
266
|
+
modes,
|
|
267
|
+
tempIdMapForOverrides,
|
|
268
|
+
allTokensForRefs, // Full resolved tokens (core + extended) for reference resolution
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// For extended collections, only send variableModeValues (overrides), not variable creation
|
|
272
|
+
// Extended collections inherit variables, so we can't create new ones
|
|
273
|
+
// Filter out any variableChanges (shouldn't happen, but just in case)
|
|
274
|
+
if (semanticRequest.variableChanges.length > 0) {
|
|
275
|
+
console.warn(
|
|
276
|
+
` ⚠️ Warning: ${semanticRequest.variableChanges.length} variable creation requests filtered out (extended collections inherit variables)`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (semanticRequest.variableModeValues.length > 0) {
|
|
281
|
+
if (dryRun) {
|
|
282
|
+
if (verbose) {
|
|
283
|
+
console.log(
|
|
284
|
+
` [DRY RUN] Would update ${semanticRequest.variableModeValues.length} variable mode values`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
await figmaRequest(config, "/variables", {
|
|
289
|
+
method: "POST",
|
|
290
|
+
body: JSON.stringify({
|
|
291
|
+
variableModeValues: semanticRequest.variableModeValues,
|
|
292
|
+
}),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Also handle component tokens for extended collections (only overrides)
|
|
298
|
+
if (componentTokensWithParent.size > 0) {
|
|
299
|
+
// For extended themes, merge core tokens with extended tokens for reference resolution
|
|
300
|
+
// Extended tokens may reference primitives from core theme
|
|
301
|
+
const allTokensForRefs = new Map(coreTokens);
|
|
302
|
+
for (const [tokenPath, tokenData] of resolvedTokens.entries()) {
|
|
303
|
+
allTokensForRefs.set(tokenPath, tokenData);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const componentRequest = buildComponentVariableRequest(
|
|
307
|
+
componentTokensWithParent,
|
|
308
|
+
existingVariables, // Parent collection variables
|
|
309
|
+
collection.id, // Extended collection ID
|
|
310
|
+
modes,
|
|
311
|
+
tempIdMapForOverrides,
|
|
312
|
+
allTokensForRefs, // Full resolved tokens (core + extended) for reference resolution
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// Only send overrides, not variable creation
|
|
316
|
+
if (componentRequest.variableChanges.length > 0) {
|
|
317
|
+
console.warn(
|
|
318
|
+
` ⚠️ Warning: ${componentRequest.variableChanges.length} component variable creation requests filtered out (extended collections inherit variables)`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (componentRequest.variableModeValues.length > 0) {
|
|
323
|
+
if (dryRun) {
|
|
324
|
+
if (verbose) {
|
|
325
|
+
console.log(
|
|
326
|
+
` [DRY RUN] Would update ${componentRequest.variableModeValues.length} component variable mode values`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
await figmaRequest(config, "/variables", {
|
|
331
|
+
method: "POST",
|
|
332
|
+
body: JSON.stringify({
|
|
333
|
+
variableModeValues: componentRequest.variableModeValues,
|
|
334
|
+
}),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const totalErrors =
|
|
340
|
+
semanticRequest.stats.errors + componentRequest.stats.errors;
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
created: 0, // Extended collections don't create new variables
|
|
344
|
+
updated: semanticRequest.stats.updated + componentRequest.stats.updated,
|
|
345
|
+
skipped: semanticRequest.stats.skipped + componentRequest.stats.skipped,
|
|
346
|
+
deleted: semanticRequest.stats.deleted + componentRequest.stats.deleted,
|
|
347
|
+
errors: totalErrors,
|
|
348
|
+
errorMessages: [
|
|
349
|
+
...(semanticRequest.errors || []),
|
|
350
|
+
...(componentRequest.errors || []),
|
|
351
|
+
],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
created: 0, // Extended collections don't create new variables
|
|
357
|
+
updated: semanticRequest.stats.updated,
|
|
358
|
+
skipped: semanticRequest.stats.skipped,
|
|
359
|
+
deleted: semanticRequest.stats.deleted,
|
|
360
|
+
errors: semanticRequest.stats.errors,
|
|
361
|
+
errorMessages: semanticRequest.errors || [],
|
|
362
|
+
};
|
|
363
|
+
} else {
|
|
364
|
+
// Core collection: create variables and values normally
|
|
365
|
+
// Filter existing variables to this collection
|
|
366
|
+
const collectionVariables = existingVariables.filter(
|
|
367
|
+
(v) => v.variableCollectionId === collection.id,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Build and send primitive variables
|
|
371
|
+
const tempIdMap = new Map();
|
|
372
|
+
const primitiveRequest = buildPrimitiveVariableRequest(
|
|
373
|
+
tokensToSync,
|
|
374
|
+
collectionVariables,
|
|
375
|
+
collection.id,
|
|
376
|
+
modes,
|
|
377
|
+
tempIdMap,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// Declare response in outer scope so it's accessible later
|
|
381
|
+
let response = null;
|
|
382
|
+
|
|
383
|
+
if (
|
|
384
|
+
primitiveRequest.variableChanges.length > 0 ||
|
|
385
|
+
primitiveRequest.variableModeValues.length > 0
|
|
386
|
+
) {
|
|
387
|
+
const requestBody = {};
|
|
388
|
+
if (primitiveRequest.variableChanges.length > 0) {
|
|
389
|
+
requestBody.variables = primitiveRequest.variableChanges;
|
|
390
|
+
}
|
|
391
|
+
if (primitiveRequest.variableModeValues.length > 0) {
|
|
392
|
+
requestBody.variableModeValues = primitiveRequest.variableModeValues;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (dryRun) {
|
|
396
|
+
if (verbose) {
|
|
397
|
+
console.log(
|
|
398
|
+
` [DRY RUN] Would ${primitiveRequest.variableChanges.length > 0 ? `create/update/delete ${primitiveRequest.variableChanges.length} variables` : ""}${primitiveRequest.stats.deleted > 0 ? ` (${primitiveRequest.stats.deleted} deleted)` : ""} ${primitiveRequest.variableModeValues.length > 0 ? `update ${primitiveRequest.variableModeValues.length} mode values` : ""}`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
console.log(" 🔄 Syncing primitives...");
|
|
403
|
+
response = await figmaRequest(config, "/variables", {
|
|
404
|
+
method: "POST",
|
|
405
|
+
body: JSON.stringify(requestBody),
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Update tempIdMap with real IDs from Figma's response
|
|
409
|
+
// Figma returns tempIdToRealId mapping for variables that were created
|
|
410
|
+
if (
|
|
411
|
+
response.meta?.tempIdToRealId &&
|
|
412
|
+
primitiveRequest.variableChanges.length > 0
|
|
413
|
+
) {
|
|
414
|
+
const tempIdToRealId = response.meta.tempIdToRealId;
|
|
415
|
+
|
|
416
|
+
// Update tempIdMap: replace temp IDs with real IDs for all entries
|
|
417
|
+
for (const [key, tempId] of tempIdMap.entries()) {
|
|
418
|
+
if (tempIdToRealId[tempId]) {
|
|
419
|
+
const realId = tempIdToRealId[tempId];
|
|
420
|
+
tempIdMap.set(key, realId);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Reload variables for semantic token references (only if primitives were created/updated)
|
|
428
|
+
// Optimize: Only reload if we actually created/updated variables
|
|
429
|
+
let updatedCollectionVariables = collectionVariables;
|
|
430
|
+
if (primitiveRequest.variableChanges.length > 0 && !dryRun) {
|
|
431
|
+
const updatedVariablesData = await figmaRequest(
|
|
432
|
+
config,
|
|
433
|
+
"/variables/local",
|
|
434
|
+
);
|
|
435
|
+
const updatedExistingVariables = updatedVariablesData.meta?.variables
|
|
436
|
+
? Object.values(updatedVariablesData.meta.variables)
|
|
437
|
+
: [];
|
|
438
|
+
updatedCollectionVariables = updatedExistingVariables.filter(
|
|
439
|
+
(v) => v.variableCollectionId === collection.id,
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
// Fallback: If tempIdToRealId wasn't in response, update tempIdMap from reloaded variables
|
|
443
|
+
// Match variables by name to find real IDs for temp IDs
|
|
444
|
+
if (
|
|
445
|
+
!response?.meta?.tempIdToRealId ||
|
|
446
|
+
Object.keys(response?.meta?.tempIdToRealId || {}).length === 0
|
|
447
|
+
) {
|
|
448
|
+
// Create a map of variable names to IDs from reloaded variables
|
|
449
|
+
const varNameToIdMap = new Map();
|
|
450
|
+
for (const v of updatedCollectionVariables) {
|
|
451
|
+
if (v.name) varNameToIdMap.set(v.name, v.id);
|
|
452
|
+
if (v.key && v.key !== v.name) varNameToIdMap.set(v.key, v.id);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Update tempIdMap: find variables by name and replace temp IDs with real IDs
|
|
456
|
+
// Also update entries keyed by token path (without prefix) by looking up the variable name
|
|
457
|
+
for (const [key, tempId] of tempIdMap.entries()) {
|
|
458
|
+
if (tempId.startsWith("temp_var_")) {
|
|
459
|
+
let realId = null;
|
|
460
|
+
|
|
461
|
+
// Check if key is a variable name (starts with primitive/, semantic/, component/)
|
|
462
|
+
if (
|
|
463
|
+
key.includes("/") &&
|
|
464
|
+
(key.startsWith("primitive/") ||
|
|
465
|
+
key.startsWith("semantic/") ||
|
|
466
|
+
key.startsWith("component/"))
|
|
467
|
+
) {
|
|
468
|
+
realId = varNameToIdMap.get(key);
|
|
469
|
+
} else {
|
|
470
|
+
// Key might be a token path (without prefix) - try to find it by constructing variable names
|
|
471
|
+
// Try primitive/ prefix first (for primitive tokens)
|
|
472
|
+
const primitiveVarName = `primitive/${key}`;
|
|
473
|
+
realId = varNameToIdMap.get(primitiveVarName);
|
|
474
|
+
if (!realId) {
|
|
475
|
+
// Try semantic/ prefix
|
|
476
|
+
const semanticVarName = `semantic/${key}`;
|
|
477
|
+
realId = varNameToIdMap.get(semanticVarName);
|
|
478
|
+
}
|
|
479
|
+
if (!realId) {
|
|
480
|
+
// Try component/ prefix
|
|
481
|
+
const componentVarName = `component/${key}`;
|
|
482
|
+
realId = varNameToIdMap.get(componentVarName);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (realId) {
|
|
487
|
+
tempIdMap.set(key, realId);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Build and send semantic variables
|
|
495
|
+
const semanticRequest = buildSemanticVariableRequest(
|
|
496
|
+
tokensToSync,
|
|
497
|
+
updatedCollectionVariables,
|
|
498
|
+
collection.id,
|
|
499
|
+
modes,
|
|
500
|
+
tempIdMap,
|
|
501
|
+
resolvedTokens, // Full resolved tokens for reference resolution
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
let semanticResponse = null;
|
|
505
|
+
if (
|
|
506
|
+
semanticRequest.variableChanges.length > 0 ||
|
|
507
|
+
semanticRequest.variableModeValues.length > 0
|
|
508
|
+
) {
|
|
509
|
+
const requestBody = {};
|
|
510
|
+
if (semanticRequest.variableChanges.length > 0) {
|
|
511
|
+
requestBody.variables = semanticRequest.variableChanges;
|
|
512
|
+
}
|
|
513
|
+
if (semanticRequest.variableModeValues.length > 0) {
|
|
514
|
+
requestBody.variableModeValues = semanticRequest.variableModeValues;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (dryRun) {
|
|
518
|
+
if (verbose) {
|
|
519
|
+
console.log(
|
|
520
|
+
` [DRY RUN] Would ${semanticRequest.variableChanges.length > 0 ? `create/update/delete ${semanticRequest.variableChanges.length} semantic variables` : ""}${semanticRequest.stats.deleted > 0 ? ` (${semanticRequest.stats.deleted} deleted)` : ""} ${semanticRequest.variableModeValues.length > 0 ? `update ${semanticRequest.variableModeValues.length} mode values` : ""}`,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
console.log(" 🔄 Syncing semantic tokens...");
|
|
525
|
+
semanticResponse = await figmaRequest(config, "/variables", {
|
|
526
|
+
method: "POST",
|
|
527
|
+
body: JSON.stringify(requestBody),
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// Update tempIdMap with real IDs from Figma's response for semantic variables
|
|
531
|
+
if (
|
|
532
|
+
semanticResponse.meta?.tempIdToRealId &&
|
|
533
|
+
semanticRequest.variableChanges.length > 0
|
|
534
|
+
) {
|
|
535
|
+
const tempIdToRealId = semanticResponse.meta.tempIdToRealId;
|
|
536
|
+
|
|
537
|
+
// Update tempIdMap: replace temp IDs with real IDs for all entries
|
|
538
|
+
for (const [key, tempId] of tempIdMap.entries()) {
|
|
539
|
+
if (tempIdToRealId[tempId]) {
|
|
540
|
+
const realId = tempIdToRealId[tempId];
|
|
541
|
+
tempIdMap.set(key, realId);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Reload variables again for component token references (only if semantic variables were created/updated)
|
|
549
|
+
// Optimize: Only reload if we actually created/updated variables
|
|
550
|
+
let finalCollectionVariables = updatedCollectionVariables;
|
|
551
|
+
if (semanticRequest.variableChanges.length > 0 && !dryRun) {
|
|
552
|
+
console.log(" 📥 Reloading variables...");
|
|
553
|
+
const finalVariablesData = await figmaRequest(config, "/variables/local");
|
|
554
|
+
const finalExistingVariables = finalVariablesData.meta?.variables
|
|
555
|
+
? Object.values(finalVariablesData.meta.variables)
|
|
556
|
+
: [];
|
|
557
|
+
finalCollectionVariables = finalExistingVariables.filter(
|
|
558
|
+
(v) => v.variableCollectionId === collection.id,
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
// Fallback: If tempIdToRealId wasn't in response, update tempIdMap from reloaded variables
|
|
562
|
+
if (
|
|
563
|
+
!semanticResponse?.meta?.tempIdToRealId ||
|
|
564
|
+
Object.keys(semanticResponse.meta.tempIdToRealId).length === 0
|
|
565
|
+
) {
|
|
566
|
+
// Create a map of variable names to IDs from reloaded variables
|
|
567
|
+
const varNameToIdMap = new Map();
|
|
568
|
+
for (const v of finalCollectionVariables) {
|
|
569
|
+
if (v.name) varNameToIdMap.set(v.name, v.id);
|
|
570
|
+
if (v.key && v.key !== v.name) varNameToIdMap.set(v.key, v.id);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Update tempIdMap: find variables by name and replace temp IDs with real IDs
|
|
574
|
+
// Also update entries keyed by token path (without prefix) by looking up the variable name
|
|
575
|
+
for (const [key, tempId] of tempIdMap.entries()) {
|
|
576
|
+
if (tempId.startsWith("temp_var_")) {
|
|
577
|
+
let realId = null;
|
|
578
|
+
|
|
579
|
+
// Check if key is a variable name (starts with semantic/, component/, primitive/)
|
|
580
|
+
if (
|
|
581
|
+
key.includes("/") &&
|
|
582
|
+
(key.startsWith("semantic/") ||
|
|
583
|
+
key.startsWith("component/") ||
|
|
584
|
+
key.startsWith("primitive/"))
|
|
585
|
+
) {
|
|
586
|
+
realId = varNameToIdMap.get(key);
|
|
587
|
+
} else {
|
|
588
|
+
// Key might be a token path (without prefix) - try to find it by constructing variable names
|
|
589
|
+
// Try semantic/ prefix first (most common for component token references)
|
|
590
|
+
const semanticVarName = `semantic/${key}`;
|
|
591
|
+
realId = varNameToIdMap.get(semanticVarName);
|
|
592
|
+
if (!realId) {
|
|
593
|
+
// Try component/ prefix
|
|
594
|
+
const componentVarName = `component/${key}`;
|
|
595
|
+
realId = varNameToIdMap.get(componentVarName);
|
|
596
|
+
}
|
|
597
|
+
if (!realId) {
|
|
598
|
+
// Try primitive/ prefix
|
|
599
|
+
const primitiveVarName = `primitive/${key}`;
|
|
600
|
+
realId = varNameToIdMap.get(primitiveVarName);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (realId) {
|
|
605
|
+
tempIdMap.set(key, realId);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Build and send component variables
|
|
613
|
+
const componentRequest = buildComponentVariableRequest(
|
|
614
|
+
tokensToSync,
|
|
615
|
+
finalCollectionVariables,
|
|
616
|
+
collection.id,
|
|
617
|
+
modes,
|
|
618
|
+
tempIdMap,
|
|
619
|
+
resolvedTokens, // Full resolved tokens for reference resolution
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
if (
|
|
623
|
+
componentRequest.variableChanges.length > 0 ||
|
|
624
|
+
componentRequest.variableModeValues.length > 0
|
|
625
|
+
) {
|
|
626
|
+
const requestBody = {};
|
|
627
|
+
if (componentRequest.variableChanges.length > 0) {
|
|
628
|
+
requestBody.variables = componentRequest.variableChanges;
|
|
629
|
+
}
|
|
630
|
+
if (componentRequest.variableModeValues.length > 0) {
|
|
631
|
+
requestBody.variableModeValues = componentRequest.variableModeValues;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (dryRun) {
|
|
635
|
+
if (verbose) {
|
|
636
|
+
console.log(
|
|
637
|
+
` [DRY RUN] Would ${componentRequest.variableChanges.length > 0 ? `create/update/delete ${componentRequest.variableChanges.length} component variables` : ""}${componentRequest.stats.deleted > 0 ? ` (${componentRequest.stats.deleted} deleted)` : ""} ${componentRequest.variableModeValues.length > 0 ? `update ${componentRequest.variableModeValues.length} mode values` : ""}`,
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
console.log(" 🔄 Syncing component tokens...");
|
|
642
|
+
await figmaRequest(config, "/variables", {
|
|
643
|
+
method: "POST",
|
|
644
|
+
body: JSON.stringify(requestBody),
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const totalErrors =
|
|
650
|
+
primitiveRequest.stats.errors +
|
|
651
|
+
semanticRequest.stats.errors +
|
|
652
|
+
componentRequest.stats.errors;
|
|
653
|
+
|
|
654
|
+
return {
|
|
655
|
+
created:
|
|
656
|
+
primitiveRequest.stats.created +
|
|
657
|
+
semanticRequest.stats.created +
|
|
658
|
+
componentRequest.stats.created,
|
|
659
|
+
updated:
|
|
660
|
+
primitiveRequest.stats.updated +
|
|
661
|
+
semanticRequest.stats.updated +
|
|
662
|
+
componentRequest.stats.updated,
|
|
663
|
+
skipped:
|
|
664
|
+
primitiveRequest.stats.skipped +
|
|
665
|
+
semanticRequest.stats.skipped +
|
|
666
|
+
componentRequest.stats.skipped,
|
|
667
|
+
deleted:
|
|
668
|
+
primitiveRequest.stats.deleted +
|
|
669
|
+
semanticRequest.stats.deleted +
|
|
670
|
+
componentRequest.stats.deleted,
|
|
671
|
+
errors: totalErrors,
|
|
672
|
+
errorMessages: [
|
|
673
|
+
...(primitiveRequest.errors || []),
|
|
674
|
+
...(semanticRequest.errors || []),
|
|
675
|
+
...(componentRequest.errors || []),
|
|
676
|
+
],
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
module.exports = {
|
|
682
|
+
syncTheme,
|
|
683
|
+
};
|