@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.
Files changed (147) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +332 -0
  3. package/build/web/core/component-variables.scss +1088 -131
  4. package/build/web/core/component.d.ts +558 -0
  5. package/build/web/core/component.js +6685 -249
  6. package/build/web/core/component.scss +557 -69
  7. package/build/web/core/css-utils/a2-border.css +47 -45
  8. package/build/web/core/css-utils/a2-color.css +443 -227
  9. package/build/web/core/css-utils/a2-font.css +0 -2
  10. package/build/web/core/css-utils/a2-spacing.css +476 -478
  11. package/build/web/core/css-utils/a2-utils.css +992 -772
  12. package/build/web/core/css-utils/border.css +47 -45
  13. package/build/web/core/css-utils/color.css +443 -227
  14. package/build/web/core/css-utils/font.css +0 -2
  15. package/build/web/core/css-utils/spacing.css +476 -478
  16. package/build/web/core/css-utils/utils.css +992 -772
  17. package/build/web/core/index.d.ts +6 -0
  18. package/build/web/core/index.js +1 -1
  19. package/build/web/core/primitive-variables.scss +148 -65
  20. package/build/web/core/primitive.d.ts +209 -0
  21. package/build/web/core/primitive.js +779 -61
  22. package/build/web/core/primitive.scss +207 -124
  23. package/build/web/core/semantic-variables.scss +363 -239
  24. package/build/web/core/semantic.d.ts +221 -0
  25. package/build/web/core/semantic.js +1613 -347
  26. package/build/web/core/semantic.scss +219 -137
  27. package/build/web/index.d.ts +3 -4
  28. package/build/web/index.js +0 -1
  29. package/build/web/types.d.ts +17 -0
  30. package/config.js +121 -497
  31. package/eslint.config.mjs +11 -1
  32. package/package.json +15 -5
  33. package/src/global/primitive/breakpoint.tokens.json +54 -0
  34. package/src/global/primitive/color.tokens.json +1092 -0
  35. package/src/global/primitive/duration.tokens.json +44 -0
  36. package/src/global/primitive/font.tokens.json +151 -0
  37. package/src/global/primitive/radius.tokens.json +94 -0
  38. package/src/global/primitive/size.tokens.json +174 -0
  39. package/src/global/primitive/transition.tokens.json +32 -0
  40. package/src/theme/core/background.tokens.json +1312 -0
  41. package/src/theme/core/border.tokens.json +192 -0
  42. package/src/theme/core/chart.tokens.json +982 -0
  43. package/src/theme/core/component/ai-mark.tokens.json +20 -0
  44. package/src/theme/core/component/alert.tokens.json +261 -0
  45. package/src/theme/core/component/announcement.tokens.json +460 -0
  46. package/src/theme/core/component/avatar.tokens.json +137 -0
  47. package/src/theme/core/component/badge.tokens.json +42 -0
  48. package/src/theme/core/component/breadcrumb.tokens.json +42 -0
  49. package/src/theme/core/component/button-toggle.tokens.json +428 -0
  50. package/src/theme/core/component/button.tokens.json +941 -0
  51. package/src/theme/core/component/calendar.tokens.json +391 -0
  52. package/src/theme/core/component/card.tokens.json +107 -0
  53. package/src/theme/core/component/checkbox.tokens.json +631 -0
  54. package/src/theme/core/component/chip.tokens.json +169 -0
  55. package/src/theme/core/component/combobox.tokens.json +269 -0
  56. package/src/theme/core/component/details.tokens.json +152 -0
  57. package/src/theme/core/component/dialog.tokens.json +87 -0
  58. package/src/theme/core/component/divider.tokens.json +23 -0
  59. package/src/theme/core/component/dnd.tokens.json +208 -0
  60. package/src/theme/core/component/drawer.tokens.json +61 -0
  61. package/src/theme/core/component/drilldown.tokens.json +61 -0
  62. package/src/theme/core/component/edit-card.tokens.json +381 -0
  63. package/src/theme/core/component/field-label.tokens.json +42 -0
  64. package/src/theme/core/component/field-message.tokens.json +74 -0
  65. package/src/theme/core/component/icon.tokens.json +42 -0
  66. package/src/theme/core/component/link.tokens.json +108 -0
  67. package/src/theme/core/component/list-view.tokens.json +82 -0
  68. package/src/theme/core/component/listbox.tokens.json +283 -0
  69. package/src/theme/core/component/menu.tokens.json +230 -0
  70. package/src/theme/core/component/overflow.tokens.json +84 -0
  71. package/src/theme/core/component/page.tokens.json +377 -0
  72. package/src/theme/core/component/pagination.tokens.json +63 -0
  73. package/src/theme/core/component/popover.tokens.json +122 -0
  74. package/src/theme/core/component/progress-bar.tokens.json +133 -0
  75. package/src/theme/core/component/radio.tokens.json +631 -0
  76. package/src/theme/core/component/segmented-control.tokens.json +175 -0
  77. package/src/theme/core/component/select-card.tokens.json +943 -0
  78. package/src/theme/core/component/side-nav.tokens.json +349 -0
  79. package/src/theme/core/component/skeleton.tokens.json +42 -0
  80. package/src/theme/core/component/spinner.tokens.json +96 -0
  81. package/src/theme/core/component/status-icon.tokens.json +164 -0
  82. package/src/theme/core/component/stepper.tokens.json +484 -0
  83. package/src/theme/core/component/switch.tokens.json +285 -0
  84. package/src/theme/core/component/tab.tokens.json +192 -0
  85. package/src/theme/core/component/text-field.tokens.json +160 -0
  86. package/src/theme/core/component/text.tokens.json +59 -0
  87. package/src/theme/core/component/toast.tokens.json +343 -0
  88. package/src/theme/core/component/toolbar.tokens.json +114 -0
  89. package/src/theme/core/component/tooltip.tokens.json +61 -0
  90. package/src/theme/core/focus.tokens.json +56 -0
  91. package/src/theme/core/foreground.tokens.json +416 -0
  92. package/src/theme/core/gradient.tokens.json +41 -0
  93. package/src/theme/core/opacity.tokens.json +25 -0
  94. package/src/theme/core/shadow.tokens.json +81 -0
  95. package/src/theme/core/status.tokens.json +74 -0
  96. package/src/theme/core/typography.tokens.json +163 -0
  97. package/src/utils/__tests__/css-utils-format-utils.test.js +312 -0
  98. package/src/utils/__tests__/sd-build-configs.test.js +306 -0
  99. package/src/utils/__tests__/sd-formats.test.js +950 -0
  100. package/src/utils/__tests__/sd-transforms.test.js +336 -0
  101. package/src/utils/__tests__/token-helpers.test.js +1160 -0
  102. package/src/utils/copy-css-utils-cli.js +13 -1
  103. package/src/utils/css-utils-format-utils.js +105 -176
  104. package/src/utils/figma/__tests__/sync-gradient.test.js +561 -0
  105. package/src/utils/figma/__tests__/token-conversion.test.js +117 -0
  106. package/src/utils/figma/__tests__/token-resolution.test.js +231 -0
  107. package/src/utils/figma/auth.js +355 -0
  108. package/src/utils/figma/constants.js +22 -0
  109. package/src/utils/figma/errors.js +80 -0
  110. package/src/utils/figma/figma-api.js +1069 -0
  111. package/src/utils/figma/get-token.js +348 -0
  112. package/src/utils/figma/sync-components.js +909 -0
  113. package/src/utils/figma/sync-main.js +692 -0
  114. package/src/utils/figma/sync-orchestration.js +683 -0
  115. package/src/utils/figma/sync-primitives.js +230 -0
  116. package/src/utils/figma/sync-semantic.js +1056 -0
  117. package/src/utils/figma/token-conversion.js +340 -0
  118. package/src/utils/figma/token-parsing.js +186 -0
  119. package/src/utils/figma/token-resolution.js +569 -0
  120. package/src/utils/figma/utils.js +199 -0
  121. package/src/utils/sd-build-configs.js +305 -0
  122. package/src/utils/sd-formats.js +965 -0
  123. package/src/utils/sd-transforms.js +165 -0
  124. package/src/utils/token-helpers.js +848 -0
  125. package/tsconfig.json +18 -0
  126. package/vitest.config.js +17 -0
  127. package/.turbo/turbo-build.log +0 -37
  128. package/build/web/core/raw.js +0 -229
  129. package/src/global/primitive/breakpoint.js +0 -19
  130. package/src/global/primitive/color.js +0 -231
  131. package/src/global/primitive/duration.js +0 -16
  132. package/src/global/primitive/font.js +0 -60
  133. package/src/global/primitive/radius.js +0 -31
  134. package/src/global/primitive/size.js +0 -55
  135. package/src/global/primitive/transition.js +0 -16
  136. package/src/theme/core/background.js +0 -170
  137. package/src/theme/core/border.js +0 -103
  138. package/src/theme/core/charts.js +0 -439
  139. package/src/theme/core/component/button.js +0 -708
  140. package/src/theme/core/component/checkbox.js +0 -405
  141. package/src/theme/core/focus.js +0 -35
  142. package/src/theme/core/foreground.js +0 -148
  143. package/src/theme/core/overlay.js +0 -137
  144. package/src/theme/core/shadow.js +0 -29
  145. package/src/theme/core/status.js +0 -49
  146. package/src/theme/core/typography.js +0 -82
  147. 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
+ };