figma-tokens-flattener 1.0.18 → 1.2.18

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 (3) hide show
  1. package/README.md +45 -0
  2. package/index.js +663 -562
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -1,563 +1,664 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const REUSED_KEYS = require('./consts.js');
6
-
7
- /* Color */
8
- function flattenColorGroups(colorGroups) {
9
- const flattened = {};
10
- if (!colorGroups || typeof colorGroups !== 'object') {
11
- console.warn('An object with groups of colors was expected, but received:', colorGroups);
12
- return flattened;
13
- }
14
-
15
- Object.keys(colorGroups).forEach(groupName => {
16
- const shades = colorGroups[groupName];
17
- if (shades && typeof shades === 'object') {
18
- Object.keys(shades).forEach(shadeKey => {
19
- const shadeObj = shades[shadeKey];
20
- if (shadeObj && typeof shadeObj === 'object' && shadeObj.hasOwnProperty('value')) {
21
- const flatKey = `${groupName}.${shadeKey}`;
22
- flattened[flatKey] = shadeObj.value.toLowerCase();
23
- } else {
24
- console.warn(`The wrong structure for the color ${groupName}.${shadeKey}:`, shadeObj);
25
- }
26
- })
27
- } else {
28
- console.warn(`The wrong structure for a group of colors ${groupName}:`, shades);
29
- }
30
- })
31
- return flattened;
32
- }
33
-
34
- /* Seed */
35
- function flattenSeedTokens(seedTokens) {
36
- const flattened = {};
37
- if (!seedTokens || typeof seedTokens !== 'object') {
38
- console.warn('An object with seed tokens was expected, but received:', seedTokens);
39
- return flattened;
40
- }
41
-
42
-
43
- Object.keys(seedTokens).forEach(tokenName => {
44
- const tokenObj = seedTokens[tokenName];
45
-
46
- if (!tokenObj || typeof tokenObj !== 'object') {
47
- console.warn(`Incorrect structure for the seed token ${tokenName}:`, tokenObj);
48
- return;
49
- }
50
-
51
- let valueToUse;
52
-
53
- if (Object.hasOwn(tokenObj, 'value') && typeof tokenObj.value === 'object' && tokenObj.value !== null && Object.hasOwn(tokenObj.value, 'style')) {
54
- valueToUse = tokenObj.value.style;
55
- }
56
-
57
- else if (Object.hasOwn(tokenObj, 'value')) {
58
- valueToUse = tokenObj.value;
59
- }
60
-
61
- else if (Object.hasOwn(tokenObj, 'style') && tokenObj.style && typeof tokenObj.style === 'object' && Object.hasOwn(tokenObj.style, 'value')) {
62
- valueToUse = tokenObj.style.value;
63
- }
64
-
65
- else {
66
- console.warn(`Unsupported structure for the seed token ${tokenName}:`, tokenObj);
67
- return;
68
- }
69
-
70
- if (typeof valueToUse === 'string' && !isNaN(valueToUse) && !isNaN(parseFloat(valueToUse))) {
71
- if (Number.isInteger(parseFloat(valueToUse))) {
72
- valueToUse = parseInt(valueToUse, 10);
73
- } else {
74
- valueToUse = parseFloat(valueToUse);
75
- }
76
- }
77
-
78
- flattened[tokenName] = valueToUse;
79
- })
80
-
81
- return flattened;
82
- }
83
-
84
- /* Map */
85
- function hexToRgb(hex) {
86
- if (typeof hex !== 'string') return hex;
87
- const hexMatch = hex.trim().toLowerCase().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
88
- if (!hexMatch) return hex;
89
- let hexValue = hexMatch[1];
90
- if (hexValue.length === 3) {
91
- hexValue = hexValue.split('').map(ch => ch + ch).join('');
92
- }
93
- const r = parseInt(hexValue.slice(0, 2), 16);
94
- const g = parseInt(hexValue.slice(2, 4), 16);
95
- const b = parseInt(hexValue.slice(4, 6), 16);
96
- return `${r}, ${g}, ${b}`;
97
- }
98
-
99
- function flattenMapTokens(value, seedContext) {
100
- if (typeof value !== 'string') return value;
101
- const str = value.trim();
102
-
103
- const NAME = '[a-zA-Z_][\\w.-]*';
104
- const TOKEN_RE = new RegExp('(\\{(' + NAME + ')\\})|\\$(' + NAME + ')', 'g');
105
- const SINGLE_TOKEN_BRACED = new RegExp('^\\{(' + NAME + ')\\}$');
106
- const RGBA_RE = /^rgba\s*\((.*)\)\s*$/i;
107
-
108
- const isHex = (colorString) => typeof colorString === 'string' && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(colorString);
109
-
110
- const getTokenValue = (name) =>
111
- Object.prototype.hasOwnProperty.call(seedContext, name) ? seedContext[name] : undefined;
112
-
113
- // {token} — return the token value as it is
114
- const tokenMatch = str.match(SINGLE_TOKEN_BRACED);
115
- if (tokenMatch) {
116
- const tokenValue = getTokenValue(tokenMatch[1]);
117
- return tokenValue !== undefined ? tokenValue : str;
118
- }
119
-
120
- // rgba
121
- const rgbaMatch = str.match(RGBA_RE);
122
- if (rgbaMatch) {
123
- const inside = rgbaMatch[1];
124
- const replaced = inside.replace(TOKEN_RE, (match, _g1, nameBraced) => {
125
- const name = nameBraced;
126
- const tokenValue = getTokenValue(name);
127
- if (tokenValue === undefined) return match;
128
- if (isHex(tokenValue)) return String(hexToRgb(tokenValue)); // "r, g, b"
129
- return String(tokenValue);
130
- });
131
- return 'rgba(' + replaced + ')';
132
- }
133
-
134
- // An attempt to calculate an arithmetic expression
135
- let hadUnknown = false;
136
- let hadNonNumeric = false;
137
-
138
- const expressionWithResolvedTokens = str.replace(TOKEN_RE, (match, _g1, tokenName) => {
139
- const tokenValue = getTokenValue(tokenName);
140
- if (tokenValue === undefined) {
141
- hadUnknown = true;
142
- return match;
143
- }
144
- if (typeof tokenValue === 'number') return String(tokenValue);
145
-
146
- const numericValue = typeof tokenValue === 'string' ? Number(tokenValue) : NaN;
147
- if (Number.isFinite(numericValue)) return String(numericValue);
148
-
149
- hadNonNumeric = true;
150
- return match;
151
- })
152
-
153
- if (!hadUnknown && !hadNonNumeric) {
154
- if (/[^0-9+\-*/().\s]/.test(expressionWithResolvedTokens)) {
155
- return str;
156
- }
157
- try {
158
- const result = Function('"use strict"; return (' + expressionWithResolvedTokens + ');')();
159
- return Number.isFinite(result) ? result : str;
160
- } catch {
161
- return str;
162
- }
163
- }
164
-
165
- // Universal substitution of tokens in any string
166
- const generic = str.replace(TOKEN_RE, (match, g1, tokenName) => {
167
- const tokenValue = getTokenValue(tokenName);
168
- if (tokenValue === undefined) return match;
169
- if (isHex(tokenValue)) return 'rgb(' + hexToRgb(tokenValue) + ')';
170
- return String(tokenValue);
171
- });
172
-
173
- return generic;
174
- }
175
-
176
- function flattenMapTokensWrapper(mapTokens, seedContext) {
177
- const flattened = {};
178
- if (!mapTokens || typeof mapTokens !== 'object') {
179
- console.warn('An object with map tokens was expected, but received:', mapTokens);
180
- return flattened;
181
- }
182
-
183
- Object.keys(mapTokens).forEach(tokenName => {
184
- const tokenContent = mapTokens[tokenName];
185
-
186
- if (tokenContent && typeof tokenContent === 'object' && tokenContent.hasOwnProperty('value')) {
187
- const rawValue = tokenContent.value;
188
-
189
- const computedValue = flattenMapTokens(rawValue, seedContext);
190
- flattened[tokenName] = computedValue;
191
- } else {
192
- console.warn(`Incorrect structure for the map token ${tokenName}:`, tokenContent);
193
- }
194
- })
195
- return flattened;
196
- }
197
-
198
- /* Alias */
199
-
200
- function processSingleShadow(shadowDef, contextTokens) {
201
- if (!shadowDef || typeof shadowDef !== 'object') {
202
- return undefined;
203
- }
204
-
205
- if (typeof shadowDef.x === 'undefined' || typeof shadowDef.y === 'undefined' || typeof shadowDef.color === 'undefined') {
206
- console.warn('Invalid shadow structure, missing x, y or color:', shadowDef);
207
- return undefined;
208
- }
209
-
210
- const x = flattenMapTokens(shadowDef.x, contextTokens);
211
- const y = flattenMapTokens(shadowDef.y, contextTokens);
212
- const blur = flattenMapTokens(shadowDef.blur || '0', contextTokens);
213
- const spread = flattenMapTokens(shadowDef.spread || '0', contextTokens);
214
- const color = flattenMapTokens(shadowDef.color, contextTokens);
215
-
216
- if (typeof x !== 'number' || typeof y !== 'number' || typeof blur !== 'number' || typeof spread !== 'number' || typeof color !== 'string') {
217
- console.warn(`Invalid value type in shadow: x=${x}(${typeof x}), y=${y}(${typeof y}), blur=${blur}(${typeof blur}), spread=${spread}(${typeof spread}), color=${color}(${typeof color})`);
218
- return undefined;
219
- }
220
-
221
- // Forming a line for one shadow
222
- return `${x}px ${y}px ${blur}px ${spread}px ${color}`;
223
- }
224
-
225
- function processBoxShadow(shadowData, contextTokens) {
226
- let shadowsArray;
227
-
228
- if (Array.isArray(shadowData)) {
229
- // This is an array of shadows
230
- shadowsArray = shadowData;
231
- } else if (typeof shadowData === 'object' && shadowData !== null) {
232
- // This is a single shadow object
233
- shadowsArray = [shadowData];
234
- } else {
235
- console.warn('Expected array or shadow object, received::', shadowData);
236
- return undefined;
237
- }
238
-
239
- const processedShadows = [];
240
- for (const shadowDef of shadowsArray) {
241
- const shadowString = processSingleShadow(shadowDef, contextTokens);
242
- if (shadowString) {
243
- processedShadows.push(shadowString);
244
- }
245
- }
246
-
247
- if (processedShadows.length === 0) {
248
- return undefined;
249
- }
250
-
251
- // Combining all the shadow lines into one boxShadow line
252
- return processedShadows.join(', ');
253
- }
254
-
255
- function flattenAliasTokens(aliasTokens, contextTokens) {
256
- const flattened = {};
257
- if (!aliasTokens || typeof aliasTokens !== 'object') {
258
- console.warn('An object with alias tokens was expected, but received:', aliasTokens);
259
- return flattened;
260
- }
261
-
262
- Object.keys(aliasTokens).forEach((tokenName) => {
263
- const tokenContent = aliasTokens[tokenName];
264
-
265
- if (tokenName[0] === tokenName[0].toUpperCase()) {
266
- return;
267
- }
268
-
269
- if (tokenContent && typeof tokenContent === 'object' && Object.hasOwn(tokenContent, 'value')) {
270
- if (
271
- (Array.isArray(tokenContent.value) && tokenContent.value.length > 0) ||
272
- (typeof tokenContent.value === 'object' && tokenContent.value !== null && Object.hasOwn(tokenContent.value, 'x'))
273
- ) {
274
- const boxShadowValue = processBoxShadow(tokenContent.value, contextTokens);
275
- if (boxShadowValue !== undefined) {
276
- flattened[tokenName] = boxShadowValue;
277
- } else {
278
- console.warn(`${tokenName}: The boxShadow structure could not be processed.`, tokenContent.value);
279
- }
280
- }
281
- else {
282
- const rawValue = tokenContent.value;
283
- const processedValue = flattenMapTokens(rawValue, contextTokens);
284
- flattened[tokenName] = processedValue;
285
- }
286
- } else {
287
- console.warn(`Unsupported structure for alias token ${tokenName}:`, tokenContent);
288
- }
289
- });
290
-
291
- return flattened;
292
- }
293
-
294
- function checkAndResolveVarValues(contextTokens) {
295
- const resolved = {};
296
-
297
- Object.keys(contextTokens).forEach(tokenName => {
298
- const TOKEN_RE = /(\{([\w.-]+)\})|\$([\w.-]+)/g;
299
- const currentValue = contextTokens[tokenName];
300
-
301
- // Checking whether the value is a string containing a raw token.
302
- if (typeof currentValue === 'string' && TOKEN_RE.test(currentValue)) {
303
- const recomputedValue = flattenMapTokens(currentValue, contextTokens);
304
-
305
- // If the value has changed after repeated calculation, update it.
306
- if (recomputedValue !== currentValue) {
307
- resolved[tokenName] = recomputedValue;
308
- }
309
- }
310
- })
311
- return resolved;
312
- };
313
-
314
- /* DefaultValues */
315
- function flattenDefaultValueTokens(defaultTokens) {
316
- if (!defaultTokens || typeof defaultTokens !== 'object') return {};
317
-
318
- const resolved = {};
319
-
320
- // 1) Basic numeric values
321
- for (const [name, token] of Object.entries(defaultTokens)) {
322
- if (token && typeof token === 'object' && Object.prototype.hasOwnProperty.call(token, 'value')) {
323
- const val = token.value;
324
- if (typeof val === 'number' && Number.isFinite(val)) {
325
- resolved[name] = val;
326
- }
327
- }
328
- }
329
-
330
- const extractRefKey = (value) => {
331
- if (typeof value !== 'string') return null;
332
- const tokenMatch = value.match(/^\s*\{([^}]+)\}\s*$/);
333
- return tokenMatch ? tokenMatch[1].trim() : null;
334
- };
335
-
336
- // 2) Iterative resolution of "{key}" links
337
- const maxRounds = Object.keys(defaultTokens).length + 5; // защита от циклов
338
- for (let round = 0; round < maxRounds; round++) {
339
- let changed = false;
340
-
341
- for (const [name, token] of Object.entries(defaultTokens)) {
342
- if (!token || typeof token !== 'object' || !Object.prototype.hasOwnProperty.call(token, 'value')) continue;
343
-
344
- const val = token.value;
345
-
346
- if (typeof val === 'number' && Number.isFinite(val)) {
347
- if (resolved[name] !== val) {
348
- resolved[name] = val;
349
- changed = true;
350
- }
351
- continue;
352
- }
353
-
354
- const refKey = extractRefKey(val);
355
- if (!refKey) continue;
356
-
357
- if (Object.prototype.hasOwnProperty.call(resolved, refKey)) {
358
- const num = resolved[refKey];
359
- if (typeof num === 'number' && Number.isFinite(num) && resolved[name] !== num) {
360
- resolved[name] = num;
361
- changed = true;
362
- }
363
- }
364
- }
365
-
366
- if (!changed) break;
367
- }
368
-
369
- // 3) Returning numeric values only
370
- const numericTokens = {};
371
- for (const [tokenName, tokenValue] of Object.entries(resolved)) {
372
- if (typeof tokenValue === 'number' && Number.isFinite(tokenValue)) numericTokens[tokenName] = tokenValue;
373
- }
374
- return numericTokens;
375
- }
376
-
377
- /* Components */
378
- function flattenComponentsTokens(componentsTokens, contextTokens) {
379
- const flattened = {};
380
- if (!componentsTokens || typeof componentsTokens !== 'object') {
381
- console.warn('An object with components tokens was expected, but received:', componentsTokens);
382
- return flattened;
383
- }
384
-
385
- Object.keys(componentsTokens).forEach(componentName => {
386
- const componentTokens = componentsTokens[componentName];
387
- if (!componentTokens || typeof componentTokens !== 'object') {
388
- console.warn(`Incorrect structure for the component ${componentName}:`, componentTokens);
389
- return;
390
- }
391
-
392
- // Processing tokens for one component
393
- const processedComponentTokens = {};
394
- Object.keys(componentTokens).forEach(tokenName => {
395
- const tokenDefinition = componentTokens[tokenName];
396
-
397
- if (tokenDefinition && typeof tokenDefinition === 'object') {
398
- let rawValueToProcess;
399
-
400
- if (Object.hasOwn(tokenDefinition, 'value') && typeof tokenDefinition.value === 'object' && tokenDefinition.value !== null && Object.hasOwn(tokenDefinition.value, 'style')) {
401
- rawValueToProcess = tokenDefinition.value.style;
402
- }
403
- else if (Object.hasOwn(tokenDefinition, 'value')) {
404
- rawValueToProcess = tokenDefinition.value;
405
- }
406
- const processedValue = flattenMapTokens(rawValueToProcess, contextTokens);
407
- processedComponentTokens[tokenName] = processedValue;
408
- } else {
409
- console.warn(`Unsupported token structure ${componentName}.${tokenName}:`, tokenDefinition);
410
- }
411
- })
412
-
413
- // Adding the processed component tokens to the final object
414
- flattened[componentName] = processedComponentTokens;
415
- })
416
-
417
- return flattened;
418
- }
419
-
420
- function addReusedTokens(lightTokens, darkTokens) {
421
- const reusedCollection = {};
422
-
423
- Object.keys(lightTokens).forEach((key) => {
424
- const value = lightTokens[key];
425
-
426
- if (typeof value === 'number' || REUSED_KEYS.includes(key)) {
427
- reusedCollection[key] = value;
428
- }
429
-
430
- });
431
-
432
- // darkTokens will take precedence in case of a key match.
433
- return { ...darkTokens, ...reusedCollection };
434
- }
435
-
436
- function flatten() {
437
- const configFilePath = path.resolve('./figma-tokens-flattener-config.json');
438
- let config = {};
439
-
440
- try {
441
- const configContent = fs.readFileSync(configFilePath, 'utf-8');
442
- config = JSON.parse(configContent);
443
- } catch (configError) {
444
- if (configError.code === 'ENOENT') {
445
- console.log('The configuration file is figma-tokens-flattener-config.json was not found. We use the path - the root directory.');
446
- } else {
447
- console.error('Error when reading or parsing the configuration file:', configError.message);
448
- // Continue with an empty configuration, by default
449
- }
450
- }
451
-
452
- const inputFilePath = path.resolve(config.source?.tokensFile || './tokens.json');
453
- const outputDir = path.resolve(config.target?.jsonsDir || './'); // Save it to the current default directory
454
-
455
- const baseKeys = ['colors', 'seed', 'map', 'alias', 'components']; // The keys we need from the original token
456
-
457
- try {
458
- const fileContent = fs.readFileSync(inputFilePath, 'utf-8');
459
- const allTokensData = JSON.parse(fileContent);
460
-
461
- let lightTokens = {};
462
- let darkTokens = {};
463
-
464
- for (const baseKey of baseKeys) {
465
- const lightFullKey = `light/${baseKey}`;
466
- const darkFullKey = `dark/${baseKey}`;
467
- const lightDefaultKey = `Default/Light`;
468
- const darkDefaultKey = `Default/Dark`;
469
-
470
- //Processing of light tokens
471
- if (allTokensData.hasOwnProperty(lightFullKey)) {
472
- // Special processing transformation of each collection into a flat structure
473
-
474
- if (baseKey === 'colors') {
475
- const flattenedColors = flattenColorGroups(allTokensData[lightFullKey]);
476
- lightTokens = { ...lightTokens, ...flattenedColors }; // Объединяем с существующими токенами
477
- }
478
- else if (baseKey === 'seed') {
479
- const flattenedSeeds = flattenSeedTokens(allTokensData[lightFullKey]);
480
- lightTokens = { ...lightTokens, ...flattenedSeeds };
481
- }
482
- else if (baseKey === 'map') {
483
- const flattenedMaps = flattenMapTokensWrapper(allTokensData[lightFullKey], lightTokens);
484
- lightTokens = { ...lightTokens, ...flattenedMaps };
485
- }
486
- else if (baseKey === 'alias') {
487
- const flattenedAliases = flattenAliasTokens(allTokensData[lightFullKey], lightTokens);
488
- lightTokens = { ...lightTokens, ...flattenedAliases };
489
- const resolved = checkAndResolveVarValues(lightTokens);
490
- lightTokens = { ...lightTokens, ...resolved };
491
- }
492
- else if (baseKey === 'components') {
493
- // We add the remaining default values. They may have nesting, so we put everything in a flat structure.
494
- const flattenDefaultValues = flattenDefaultValueTokens(allTokensData[lightDefaultKey]);
495
- lightTokens = { ...flattenDefaultValues, ...lightTokens };
496
-
497
- const flattenedComponents = flattenComponentsTokens(allTokensData[lightFullKey], lightTokens);
498
- lightTokens = { ...lightTokens, components: flattenedComponents };
499
- }
500
- } else {
501
- console.warn(`Collection not found, collection key: ${lightFullKey}`);
502
- }
503
-
504
- //Processing of dark tokens
505
- if (allTokensData.hasOwnProperty(darkFullKey)) {
506
-
507
- if (baseKey === 'colors') {
508
- const flattenedColors = flattenColorGroups(allTokensData[darkFullKey]);
509
- darkTokens = { ...darkTokens, ...flattenedColors };
510
- } else if (baseKey === 'seed') {
511
- const flattenedSeeds = flattenSeedTokens(allTokensData[darkFullKey]);
512
- darkTokens = { ...darkTokens, ...flattenedSeeds };
513
- }
514
- else if (baseKey === 'map') {
515
- const flattenedMaps = flattenMapTokensWrapper(allTokensData[darkFullKey], darkTokens);
516
- darkTokens = { ...darkTokens, ...flattenedMaps };
517
- }
518
- else if (baseKey === 'alias') {
519
- const flattenedAliases = flattenAliasTokens(allTokensData[darkFullKey], darkTokens);
520
- darkTokens = { ...darkTokens, ...flattenedAliases };
521
- const resolved = checkAndResolveVarValues(darkTokens);
522
- darkTokens = { ...darkTokens, ...resolved };
523
- }
524
- else if (baseKey === 'components') {
525
- // We add the remaining default values. They may have nesting, so we put everything in a flat structure.
526
- const flattenDefaultValues = flattenDefaultValueTokens(allTokensData[darkDefaultKey]);
527
- darkTokens = { ...flattenDefaultValues, ...darkTokens };
528
-
529
- // The tokens of the light theme contain numeric values, while the dark theme does not contain them to avoid duplication.
530
- // Need to add these values, and some lines (shadows, focus, etc.) because only the light theme has them too.
531
- darkTokens = addReusedTokens(lightTokens, darkTokens);
532
-
533
- const flattenedComponents = flattenComponentsTokens(allTokensData[darkFullKey], darkTokens);
534
- darkTokens = { ...darkTokens, components: flattenedComponents };
535
- }
536
- } else {
537
- console.warn(`Collection not found, collection key: ${darkFullKey}`);
538
- }
539
- }
540
-
541
- const lightOutputPath = path.join(outputDir, 'light.json');
542
- const darkOutputPath = path.join(outputDir, 'dark.json');
543
-
544
- console.log(`Saving light and dark tokens in: ${outputDir}`);
545
- fs.writeFileSync(lightOutputPath, JSON.stringify(lightTokens, null, 2));
546
-
547
- fs.writeFileSync(darkOutputPath, JSON.stringify(darkTokens, null, 2));
548
-
549
- console.log('\nReady! Light files.json and dark.json files have been created.');
550
-
551
- } catch (error) {
552
- if (error.code === 'ENOENT') {
553
- console.error(`Error: The tokens file.json was not found on the path ${inputFilePath}`);
554
- } else if (error instanceof SyntaxError) {
555
- console.error('Error: The contents of the tokens file.json is not valid JSON.');
556
- console.error(error.message);
557
- } else {
558
- console.error('Error when reading or parsing the tokens.json file:', error.message);
559
- }
560
- }
561
- };
562
-
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const REUSED_KEYS = require('./consts.js');
6
+
7
+ /* Color */
8
+ function flattenColorGroups(colorGroups, variablesMap) {
9
+ const flattened = {};
10
+ if (!colorGroups || typeof colorGroups !== 'object') {
11
+ console.warn('An object with groups of colors was expected, but received:', colorGroups);
12
+ return flattened;
13
+ }
14
+
15
+ Object.keys(colorGroups).forEach(groupName => {
16
+ const shades = colorGroups[groupName];
17
+ if (shades && typeof shades === 'object') {
18
+ Object.keys(shades).forEach(shadeKey => {
19
+ const shadeObj = shades[shadeKey];
20
+ if (shadeObj && typeof shadeObj === 'object' && shadeObj.hasOwnProperty('value')) {
21
+ const flatKey = `${groupName}.${shadeKey}`;
22
+ let resolvedValue = resolveVariableReference(shadeObj.value, variablesMap);
23
+ flattened[flatKey] = resolvedValue.toLowerCase();
24
+ } else {
25
+ console.warn(`The wrong structure for the color ${groupName}.${shadeKey}:`, shadeObj);
26
+ }
27
+ })
28
+ } else {
29
+ console.warn(`The wrong structure for a group of colors ${groupName}:`, shades);
30
+ }
31
+ })
32
+ return flattened;
33
+ }
34
+
35
+ /* Seed */
36
+
37
+ function flattenSeedTokens(seedTokens, variablesMap) {
38
+ const flattened = {};
39
+ if (!seedTokens || typeof seedTokens !== 'object') {
40
+ console.warn('An object with seed tokens was expected, but received:', seedTokens);
41
+ return flattened;
42
+ }
43
+
44
+ for (const tokenName in seedTokens) {
45
+ const tokenObj = seedTokens[tokenName];
46
+
47
+ if (!tokenObj || typeof tokenObj !== 'object') {
48
+ console.warn(`Incorrect structure for the seed token ${tokenName}:`, tokenObj);
49
+ continue;
50
+ }
51
+
52
+ let valueToUse;
53
+
54
+ // Checking the token structure
55
+ // Format 1: {"value": {"style": {... } } }
56
+ if (tokenObj.hasOwnProperty('value') && typeof tokenObj.value === 'object' && tokenObj.value !== null && tokenObj.value.hasOwnProperty('style')) {
57
+ valueToUse = tokenObj.value.style;
58
+ }
59
+ // Format 2: {"value":"..." }
60
+ else if (tokenObj.hasOwnProperty('value')) {
61
+ valueToUse = tokenObj.value;
62
+ }
63
+ // Format 3: {"style": {"value":"..." } }
64
+ else if (tokenObj.hasOwnProperty('style') && tokenObj.style && typeof tokenObj.style === 'object' && tokenObj.style.hasOwnProperty('value')) {
65
+ valueToUse = tokenObj.style.value;
66
+ }
67
+ else {
68
+ console.warn(`Unsupported structure for the seed token ${tokenName}:`, tokenObj);
69
+ continue;
70
+ }
71
+
72
+ if (valueToUse && typeof valueToUse === 'object' && valueToUse.hasOwnProperty('value')) {
73
+ valueToUse = valueToUse.value;
74
+ }
75
+
76
+ // Checking for a link and substituting a value
77
+ if (typeof valueToUse === 'string' && valueToUse.startsWith('{') && valueToUse.endsWith('}')) {
78
+ valueToUse = resolveVariableReference(valueToUse, variablesMap);
79
+ }
80
+
81
+ // Converting a string number to a number
82
+ if (typeof valueToUse === 'string' && !isNaN(valueToUse) && !isNaN(parseFloat(valueToUse))) {
83
+ if (Number.isInteger(parseFloat(valueToUse))) {
84
+ valueToUse = parseInt(valueToUse, 10);
85
+ } else {
86
+ valueToUse = parseFloat(valueToUse);
87
+ }
88
+ }
89
+
90
+ flattened[tokenName] = valueToUse;
91
+ }
92
+ return flattened;
93
+ }
94
+
95
+ /* Map */
96
+ function hexToRgb(hex) {
97
+ if (typeof hex !== 'string') return hex;
98
+ const hexMatch = hex.trim().toLowerCase().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
99
+ if (!hexMatch) return hex;
100
+ let hexValue = hexMatch[1];
101
+ if (hexValue.length === 3) {
102
+ hexValue = hexValue.split('').map(ch => ch + ch).join('');
103
+ }
104
+ const r = parseInt(hexValue.slice(0, 2), 16);
105
+ const g = parseInt(hexValue.slice(2, 4), 16);
106
+ const b = parseInt(hexValue.slice(4, 6), 16);
107
+ return `${r}, ${g}, ${b}`;
108
+ }
109
+
110
+ function flattenMapTokens(value, contextTokens, variablesMap) { // Добавлен параметр variablesMap
111
+ if (typeof value !== 'string') return value;
112
+ const str = value.trim();
113
+
114
+ // Checking whether the string is a reference to an external variable (for example, {Testname.core.color.100} or {Testname.core.color.100 00})
115
+ const externalReferenceMatch = str.match(/^(\{([A-Za-z_][\w.%-]*\.[\w.%-]*\.[\w.%-\s]+(?:\.[\w.%-\s]*)*)\})$/);
116
+ if (externalReferenceMatch && variablesMap) {
117
+ const fullReference = externalReferenceMatch[1];
118
+ // Calling resolveVariableReference to substitute a value from a variablesMap
119
+ const resolvedValue = resolveVariableReference(fullReference, variablesMap);
120
+
121
+ return resolvedValue;
122
+ }
123
+
124
+ // If this is not an external link, we process it as usual using contextTokens
125
+ const NAME = '[a-zA-Z_][\\w.-]*';
126
+ const TOKEN_RE = new RegExp('(\\{(' + NAME + ')\\})|\\$(' + NAME + ')', 'g');
127
+ const SINGLE_TOKEN_BRACED = new RegExp('^\\{(' + NAME + ')\\}$');
128
+ const RGBA_RE = /^rgba\s*\((.*)\)\s*$/i;
129
+
130
+ const isHex = (colorString) => typeof colorString === 'string' && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(colorString);
131
+
132
+ const getTokenValue = (name) =>
133
+ Object.prototype.hasOwnProperty.call(contextTokens, name) ? contextTokens[name] : undefined;
134
+
135
+ const tokenMatch = str.match(SINGLE_TOKEN_BRACED);
136
+ if (tokenMatch) {
137
+ const tokenValue = getTokenValue(tokenMatch[1]);
138
+ return tokenValue !== undefined ? tokenValue : str;
139
+ }
140
+
141
+ // rgba
142
+ const rgbaMatch = str.match(RGBA_RE);
143
+ if (rgbaMatch) {
144
+ const inside = rgbaMatch[1];
145
+ const replaced = inside.replace(TOKEN_RE, (match, _g1, nameBraced) => {
146
+ const name = nameBraced;
147
+ const tokenValue = getTokenValue(name);
148
+ if (tokenValue === undefined) return match;
149
+ if (isHex(tokenValue)) return String(hexToRgb(tokenValue));
150
+ return String(tokenValue);
151
+ });
152
+ return 'rgba(' + replaced + ')';
153
+ }
154
+
155
+ // Attempt to calculate an arithmetic expression (from contextTokens)
156
+ let hadUnknown = false;
157
+ let hadNonNumeric = false;
158
+
159
+ const expressionWithResolvedTokens = str.replace(TOKEN_RE, (match, _g1, tokenName) => {
160
+ const tokenValue = getTokenValue(tokenName);
161
+ if (tokenValue === undefined) {
162
+ hadUnknown = true;
163
+ return match;
164
+ }
165
+ if (typeof tokenValue === 'number') return String(tokenValue);
166
+
167
+ const numericValue = typeof tokenValue === 'string' ? Number(tokenValue) : NaN;
168
+ if (Number.isFinite(numericValue)) return String(numericValue);
169
+
170
+ hadNonNumeric = true;
171
+ return match;
172
+ });
173
+
174
+ if (!hadUnknown && !hadNonNumeric) {
175
+ if (/[^0-9+\-*/().\s]/.test(expressionWithResolvedTokens)) {
176
+ return str;
177
+ }
178
+ try {
179
+ const result = Function('"use strict"; return (' + expressionWithResolvedTokens + ');')();
180
+ return Number.isFinite(result) ? result : str;
181
+ } catch {
182
+ return str;
183
+ }
184
+ }
185
+
186
+ // Universal substitution of tokens in any string
187
+ const generic = str.replace(TOKEN_RE, (match, g1, tokenName) => {
188
+ const tokenValue = getTokenValue(tokenName);
189
+ if (tokenValue === undefined) return match;
190
+ if (isHex(tokenValue)) return 'rgb(' + hexToRgb(tokenValue) + ')';
191
+ return String(tokenValue);
192
+ });
193
+
194
+ return generic;
195
+ }
196
+
197
+ function flattenMapTokensWrapper(mapTokens, contextTokens, variablesMap) {
198
+ const flattened = {};
199
+ if (!mapTokens || typeof mapTokens !== 'object') {
200
+ console.warn('An object with map tokens was expected, but received:', mapTokens);
201
+ return flattened;
202
+ }
203
+
204
+ for (const tokenName in mapTokens) {
205
+ const tokenContent = mapTokens[tokenName];
206
+
207
+ // Check that the object has the expected structure { "value": "..."}
208
+ if (tokenContent && typeof tokenContent === 'object' && tokenContent.hasOwnProperty('value')) {
209
+ const rawValue = tokenContent.value;
210
+ const processedValue = flattenMapTokens(rawValue, contextTokens, variablesMap);
211
+ flattened[tokenName] = processedValue;
212
+ } else {
213
+ console.warn(`Incorrect structure for the map token ${tokenName}:`, tokenContent);
214
+ }
215
+ }
216
+ return flattened;
217
+ }
218
+
219
+ /* Alias */
220
+
221
+ function processSingleShadow(shadowDef, contextTokens) {
222
+ if (!shadowDef || typeof shadowDef !== 'object') {
223
+ return undefined;
224
+ }
225
+
226
+ if (typeof shadowDef.x === 'undefined' || typeof shadowDef.y === 'undefined' || typeof shadowDef.color === 'undefined') {
227
+ console.warn('Invalid shadow structure, missing x, y or color:', shadowDef);
228
+ return undefined;
229
+ }
230
+
231
+ const x = flattenMapTokens(shadowDef.x, contextTokens);
232
+ const y = flattenMapTokens(shadowDef.y, contextTokens);
233
+ const blur = flattenMapTokens(shadowDef.blur || '0', contextTokens);
234
+ const spread = flattenMapTokens(shadowDef.spread || '0', contextTokens);
235
+ const color = flattenMapTokens(shadowDef.color, contextTokens);
236
+
237
+ if (typeof x !== 'number' || typeof y !== 'number' || typeof blur !== 'number' || typeof spread !== 'number' || typeof color !== 'string') {
238
+ console.warn(`Invalid value type in shadow: x=${x}(${typeof x}), y=${y}(${typeof y}), blur=${blur}(${typeof blur}), spread=${spread}(${typeof spread}), color=${color}(${typeof color})`);
239
+ return undefined;
240
+ }
241
+
242
+ // Forming a line for one shadow
243
+ return `${x}px ${y}px ${blur}px ${spread}px ${color}`;
244
+ }
245
+
246
+ function processBoxShadow(shadowData, contextTokens) {
247
+ let shadowsArray;
248
+
249
+ if (Array.isArray(shadowData)) {
250
+ // This is an array of shadows
251
+ shadowsArray = shadowData;
252
+ } else if (typeof shadowData === 'object' && shadowData !== null) {
253
+ // This is a single shadow object
254
+ shadowsArray = [shadowData];
255
+ } else {
256
+ console.warn('Expected array or shadow object, received::', shadowData);
257
+ return undefined;
258
+ }
259
+
260
+ const processedShadows = [];
261
+ for (const shadowDef of shadowsArray) {
262
+ const shadowString = processSingleShadow(shadowDef, contextTokens);
263
+ if (shadowString) {
264
+ processedShadows.push(shadowString);
265
+ }
266
+ }
267
+
268
+ if (processedShadows.length === 0) {
269
+ return undefined;
270
+ }
271
+
272
+ // Combining all the shadow lines into one boxShadow line
273
+ return processedShadows.join(', ');
274
+ }
275
+
276
+ function flattenAliasTokens(aliasTokens, contextTokens, variablesMap) {
277
+ const flattened = {};
278
+ if (!aliasTokens || typeof aliasTokens !== 'object') {
279
+ console.warn('An object with alias tokens was expected, but received:', aliasTokens);
280
+ return flattened;
281
+ }
282
+
283
+ Object.keys(aliasTokens).forEach((tokenName) => {
284
+ const tokenContent = aliasTokens[tokenName];
285
+
286
+ if (tokenName[0] === tokenName[0].toUpperCase()) {
287
+ return;
288
+ }
289
+
290
+ if (tokenContent && typeof tokenContent === 'object' && Object.hasOwn(tokenContent, 'value')) {
291
+ if (
292
+ (Array.isArray(tokenContent.value) && tokenContent.value.length > 0) ||
293
+ (typeof tokenContent.value === 'object' && tokenContent.value !== null && Object.hasOwn(tokenContent.value, 'x'))
294
+ ) {
295
+ const boxShadowValue = processBoxShadow(tokenContent.value, contextTokens);
296
+ if (boxShadowValue !== undefined) {
297
+ flattened[tokenName] = boxShadowValue;
298
+ } else {
299
+ console.warn(`${tokenName}: The boxShadow structure could not be processed.`, tokenContent.value);
300
+ }
301
+ }
302
+ else {
303
+ const rawValue = tokenContent.value;
304
+ const processedValue = flattenMapTokens(rawValue, contextTokens, variablesMap);
305
+ flattened[tokenName] = processedValue;
306
+ }
307
+ } else {
308
+ console.warn(`Unsupported structure for alias token ${tokenName}:`, tokenContent);
309
+ }
310
+ });
311
+
312
+ return flattened;
313
+ }
314
+
315
+ function checkAndResolveVarValues(contextTokens) {
316
+ const resolved = {};
317
+
318
+ Object.keys(contextTokens).forEach(tokenName => {
319
+ const TOKEN_RE = /(\{([\w.-]+)\})|\$([\w.-]+)/g;
320
+ const currentValue = contextTokens[tokenName];
321
+
322
+ // Checking whether the value is a string containing a raw token.
323
+ if (typeof currentValue === 'string' && TOKEN_RE.test(currentValue)) {
324
+ const recomputedValue = flattenMapTokens(currentValue, contextTokens);
325
+
326
+ // If the value has changed after repeated calculation, update it.
327
+ if (recomputedValue !== currentValue) {
328
+ resolved[tokenName] = recomputedValue;
329
+ }
330
+ }
331
+ })
332
+ return resolved;
333
+ };
334
+
335
+ /* DefaultValues */
336
+ function flattenDefaultValueTokens(defaultTokens) {
337
+ if (!defaultTokens || typeof defaultTokens !== 'object') return {};
338
+
339
+ const resolved = {};
340
+
341
+ // 1) Basic numeric values
342
+ for (const [name, token] of Object.entries(defaultTokens)) {
343
+ if (token && typeof token === 'object' && Object.prototype.hasOwnProperty.call(token, 'value')) {
344
+ const val = token.value;
345
+ if (typeof val === 'number' && Number.isFinite(val)) {
346
+ resolved[name] = val;
347
+ }
348
+ }
349
+ }
350
+
351
+ const extractRefKey = (value) => {
352
+ if (typeof value !== 'string') return null;
353
+ const tokenMatch = value.match(/^\s*\{([^}]+)\}\s*$/);
354
+ return tokenMatch ? tokenMatch[1].trim() : null;
355
+ };
356
+
357
+ // 2) Iterative resolution of "{key}" links
358
+ const maxRounds = Object.keys(defaultTokens).length + 5; // защита от циклов
359
+ for (let round = 0; round < maxRounds; round++) {
360
+ let changed = false;
361
+
362
+ for (const [name, token] of Object.entries(defaultTokens)) {
363
+ if (!token || typeof token !== 'object' || !Object.prototype.hasOwnProperty.call(token, 'value')) continue;
364
+
365
+ const val = token.value;
366
+
367
+ if (typeof val === 'number' && Number.isFinite(val)) {
368
+ if (resolved[name] !== val) {
369
+ resolved[name] = val;
370
+ changed = true;
371
+ }
372
+ continue;
373
+ }
374
+
375
+ const refKey = extractRefKey(val);
376
+ if (!refKey) continue;
377
+
378
+ if (Object.prototype.hasOwnProperty.call(resolved, refKey)) {
379
+ const num = resolved[refKey];
380
+ if (typeof num === 'number' && Number.isFinite(num) && resolved[name] !== num) {
381
+ resolved[name] = num;
382
+ changed = true;
383
+ }
384
+ }
385
+ }
386
+
387
+ if (!changed) break;
388
+ }
389
+
390
+ // 3) Returning numeric values only
391
+ const numericTokens = {};
392
+ for (const [tokenName, tokenValue] of Object.entries(resolved)) {
393
+ if (typeof tokenValue === 'number' && Number.isFinite(tokenValue)) numericTokens[tokenName] = tokenValue;
394
+ }
395
+ return numericTokens;
396
+ }
397
+
398
+ /* Components */
399
+ function flattenComponentsTokens(componentsTokens, contextTokens) {
400
+ const flattened = {};
401
+ if (!componentsTokens || typeof componentsTokens !== 'object') {
402
+ console.warn('An object with components tokens was expected, but received:', componentsTokens);
403
+ return flattened;
404
+ }
405
+
406
+ Object.keys(componentsTokens).forEach(componentName => {
407
+ const componentTokens = componentsTokens[componentName];
408
+ if (!componentTokens || typeof componentTokens !== 'object') {
409
+ console.warn(`Incorrect structure for the component ${componentName}:`, componentTokens);
410
+ return;
411
+ }
412
+
413
+ // Processing tokens for one component
414
+ const processedComponentTokens = {};
415
+ Object.keys(componentTokens).forEach(tokenName => {
416
+ const tokenDefinition = componentTokens[tokenName];
417
+
418
+ if (tokenDefinition && typeof tokenDefinition === 'object') {
419
+ let rawValueToProcess;
420
+
421
+ if (Object.hasOwn(tokenDefinition, 'value') && typeof tokenDefinition.value === 'object' && tokenDefinition.value !== null && Object.hasOwn(tokenDefinition.value, 'style')) {
422
+ rawValueToProcess = tokenDefinition.value.style;
423
+ }
424
+ else if (Object.hasOwn(tokenDefinition, 'value')) {
425
+ rawValueToProcess = tokenDefinition.value;
426
+ }
427
+ const processedValue = flattenMapTokens(rawValueToProcess, contextTokens);
428
+ processedComponentTokens[tokenName] = processedValue;
429
+ } else {
430
+ console.warn(`Unsupported token structure ${componentName}.${tokenName}:`, tokenDefinition);
431
+ }
432
+ })
433
+
434
+ // Adding the processed component tokens to the final object
435
+ flattened[componentName] = processedComponentTokens;
436
+ })
437
+
438
+ return flattened;
439
+ }
440
+
441
+ function addReusedTokens(lightTokens, darkTokens) {
442
+ const reusedCollection = {};
443
+
444
+ Object.keys(lightTokens).forEach((key) => {
445
+ const value = lightTokens[key];
446
+
447
+ if (typeof value === 'number' || REUSED_KEYS.includes(key)) {
448
+ reusedCollection[key] = value;
449
+ }
450
+
451
+ });
452
+
453
+ // darkTokens will take precedence in case of a key match.
454
+ return { ...darkTokens, ...reusedCollection };
455
+ }
456
+
457
+ /* We get the values of the variable file, if there is one */
458
+ function loadVariableFiles(tokensFilePath) {
459
+ const tokensDir = path.dirname(tokensFilePath);
460
+ const allFiles = fs.readdirSync(tokensDir);
461
+ const variableFiles = allFiles.filter(file => file.endsWith('_variable.json'));
462
+ const varFileName = variableFiles[0];
463
+
464
+ let map = {};
465
+
466
+ if (varFileName) {
467
+ const varFilePath = path.join(tokensDir, varFileName);
468
+ const varFileContent = fs.readFileSync(varFilePath, 'utf-8');
469
+ map = JSON.parse(varFileContent);
470
+ }
471
+
472
+ return { variablesMap: Object.keys(map).length > 0 ? map : null, varFileName };
473
+ }
474
+
475
+ function resolveVariableReference(value, variablesMap) {
476
+ if (typeof value !== 'string' || !value.startsWith('{') || !value.endsWith('}')) {
477
+ return value;
478
+ }
479
+
480
+ const reference = value.slice(1, -1);
481
+
482
+ const dotCount = (reference.match(/\./g) || []).length;
483
+ if (dotCount < 2) {
484
+ // Link to styles within tokens.json is not made up of variables
485
+ return value;
486
+ }
487
+
488
+ if (variablesMap && typeof variablesMap === 'object') {
489
+ const keys = reference.split(".");
490
+
491
+ // Check if the first level (file name) exists
492
+ let currentLevel = variablesMap[keys[0]];
493
+ if (currentLevel === undefined) {
494
+ console.warn(`The path "${reference}" was not found in the variables for the reference: ${value}. Level 0 (${keys[0]}) does not exist.`);
495
+ return value;
496
+ }
497
+
498
+ // Going through the rest of the keys
499
+ for (let i = 1; i < keys.length; i++) { // We start with 1, because 0 is the file name
500
+ const key = keys[i];
501
+ if (currentLevel && typeof currentLevel === 'object' && Object.prototype.hasOwnProperty.call(currentLevel, key)) {
502
+ currentLevel = currentLevel[key]; // Going down to the level below
503
+ } else {
504
+ console.warn(`The path "${reference}" was not found in the variables for the reference: ${value}. The ${i} (${key}) level does not exist.`);
505
+ return value; // The path was not found, we return it as it is.
506
+ }
507
+ }
508
+
509
+ // Checking if the final object has the 'value' property.
510
+ if (currentLevel && typeof currentLevel === 'object' && Object.prototype.hasOwnProperty.call(currentLevel, 'value')) {
511
+ if (typeof currentLevel.value === 'string' && currentLevel.value.startsWith('{') && currentLevel.value.endsWith('}')) {
512
+ return resolveVariableReference(currentLevel.value, variablesMap);
513
+ }
514
+ return currentLevel.value;
515
+ } else {
516
+ console.warn(`The final object along the path "${reference}" does not contain the 'value' property. Link: ${value}.`);
517
+ // Let's check if the final level is just a string/number (and not an object with {value: ...})
518
+ if (currentLevel !== null && currentLevel !== undefined && typeof currentLevel !== 'object') {
519
+ console.log(`resolveVariableReference: the value for '${link}' does not have the value of 'value', taking into account:`, currentLevel);
520
+ return currentLevel;
521
+ }
522
+ return value; // Incorrect structure, we return it as it is
523
+ }
524
+
525
+ } else {
526
+ // variablesMap is not an object or is null/undefined
527
+ console.warn(`variablesMap is not an object. Link: ${value}.`);
528
+ return value;
529
+ }
530
+ }
531
+
532
+ function flatten() {
533
+ const configFilePath = path.resolve('./figma-tokens-flattener-config.json');
534
+ let config = {};
535
+
536
+ try {
537
+ const configContent = fs.readFileSync(configFilePath, 'utf-8');
538
+ config = JSON.parse(configContent);
539
+ } catch (configError) {
540
+ if (configError.code === 'ENOENT') {
541
+ console.log('The configuration file is figma-tokens-flattener-config.json was not found. We use the path - the root directory.');
542
+ } else {
543
+ console.error('Error when reading or parsing the configuration file:', configError.message);
544
+ // Continue with an empty configuration, by default
545
+ }
546
+ }
547
+
548
+ const inputFilePath = path.resolve(config.source?.tokensFile || './tokens.json');
549
+ const outputDir = path.resolve(config.target?.jsonsDir || './'); // Save it to the current default directory
550
+ const { variablesMap, varFileName } = loadVariableFiles(inputFilePath); // A custom styles file that is referenced from the main file, full file name
551
+
552
+
553
+ const baseKeys = ['colors', 'seed', 'map', 'alias', 'components']; // The keys we need from the original token
554
+
555
+ try {
556
+ const fileContent = fs.readFileSync(inputFilePath, 'utf-8');
557
+ const allTokensData = JSON.parse(fileContent);
558
+
559
+ let lightTokens = {};
560
+ let darkTokens = {};
561
+
562
+ for (const baseKey of baseKeys) {
563
+ const lightFullKey = `light/${baseKey}`;
564
+ const darkFullKey = `dark/${baseKey}`;
565
+ const lightDefaultKey = `Default/Light`;
566
+ const darkDefaultKey = `Default/Dark`;
567
+
568
+ //Processing of light tokens
569
+ if (allTokensData.hasOwnProperty(lightFullKey)) {
570
+ // Special processing transformation of each collection into a flat structure
571
+
572
+ if (baseKey === 'colors') {
573
+ const flattenedColors = flattenColorGroups(allTokensData[lightFullKey], variablesMap);
574
+ lightTokens = { ...lightTokens, ...flattenedColors }; // Combining it with existing tokens
575
+ }
576
+ else if (baseKey === 'seed') {
577
+ const flattenedSeeds = flattenSeedTokens(allTokensData[lightFullKey], variablesMap);
578
+ lightTokens = { ...lightTokens, ...flattenedSeeds };
579
+ }
580
+ else if (baseKey === 'map') {
581
+ const flattenedMaps = flattenMapTokensWrapper(allTokensData[lightFullKey], lightTokens, variablesMap);
582
+ lightTokens = { ...lightTokens, ...flattenedMaps };
583
+ }
584
+ else if (baseKey === 'alias') {
585
+ const flattenedAliases = flattenAliasTokens(allTokensData[lightFullKey], lightTokens, variablesMap);
586
+ lightTokens = { ...lightTokens, ...flattenedAliases };
587
+ const resolved = checkAndResolveVarValues(lightTokens);
588
+ lightTokens = { ...lightTokens, ...resolved };
589
+ }
590
+ else if (baseKey === 'components') {
591
+ // We add the remaining default values. They may have nesting, so we put everything in a flat structure.
592
+ const flattenDefaultValues = flattenDefaultValueTokens(allTokensData[lightDefaultKey]);
593
+ lightTokens = { ...flattenDefaultValues, ...lightTokens };
594
+
595
+ const flattenedComponents = flattenComponentsTokens(allTokensData[lightFullKey], lightTokens);
596
+ lightTokens = { ...lightTokens, components: flattenedComponents };
597
+ }
598
+ } else {
599
+ console.warn(`Collection not found, collection key: ${lightFullKey}`);
600
+ }
601
+
602
+ //Processing of dark tokens
603
+ if (!variablesMap) {
604
+ if (allTokensData.hasOwnProperty(darkFullKey)) {
605
+ if (baseKey === 'colors') {
606
+ const flattenedColors = flattenColorGroups(allTokensData[darkFullKey], variablesMap);
607
+ darkTokens = { ...darkTokens, ...flattenedColors };
608
+ } else if (baseKey === 'seed') {
609
+ const flattenedSeeds = flattenSeedTokens(allTokensData[darkFullKey]);
610
+ darkTokens = { ...darkTokens, ...flattenedSeeds };
611
+ }
612
+ else if (baseKey === 'map') {
613
+ const flattenedMaps = flattenMapTokensWrapper(allTokensData[darkFullKey], darkTokens);
614
+ darkTokens = { ...darkTokens, ...flattenedMaps };
615
+ }
616
+ else if (baseKey === 'alias') {
617
+ const flattenedAliases = flattenAliasTokens(allTokensData[darkFullKey], darkTokens);
618
+ darkTokens = { ...darkTokens, ...flattenedAliases };
619
+ const resolved = checkAndResolveVarValues(darkTokens);
620
+ darkTokens = { ...darkTokens, ...resolved };
621
+ }
622
+ else if (baseKey === 'components') {
623
+ // We add the remaining default values. They may have nesting, so we put everything in a flat structure.
624
+ const flattenDefaultValues = flattenDefaultValueTokens(allTokensData[darkDefaultKey]);
625
+ darkTokens = { ...flattenDefaultValues, ...darkTokens };
626
+
627
+ // The tokens of the light theme contain numeric values, while the dark theme does not contain them to avoid duplication.
628
+ // Need to add these values, and some lines (shadows, focus, etc.) because only the light theme has them too.
629
+ darkTokens = addReusedTokens(lightTokens, darkTokens);
630
+
631
+ const flattenedComponents = flattenComponentsTokens(allTokensData[darkFullKey], darkTokens);
632
+ darkTokens = { ...darkTokens, components: flattenedComponents };
633
+ }
634
+ } else {
635
+ console.warn(`Collection not found, collection key: ${darkFullKey}`);
636
+ }
637
+ }
638
+ }
639
+ const lightOutputPath = path.join(outputDir, variablesMap ? varFileName.replace('_variable.json', '.json') : 'light.json');
640
+ const darkOutputPath = path.join(outputDir, 'dark.json');
641
+
642
+ console.log(`Saving in: ${outputDir}`);
643
+ fs.writeFileSync(lightOutputPath, JSON.stringify(lightTokens, null, 2));
644
+
645
+ if (!variablesMap) {
646
+ fs.writeFileSync(darkOutputPath, JSON.stringify(darkTokens, null, 2));
647
+
648
+ }
649
+
650
+ console.log('\nReady!');
651
+
652
+ } catch (error) {
653
+ if (error.code === 'ENOENT') {
654
+ console.error(`Error: The tokens file.json was not found on the path ${inputFilePath}`);
655
+ } else if (error instanceof SyntaxError) {
656
+ console.error('Error: The contents of the tokens file.json is not valid JSON.');
657
+ console.error(error.message);
658
+ } else {
659
+ console.error('Error when reading or parsing the tokens.json file:', error.message);
660
+ }
661
+ }
662
+ };
663
+
563
664
  flatten();