@sb1/ffe-core 33.2.0 → 100.0.3

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 (45) hide show
  1. package/README.md +8 -8
  2. package/css/colors-semantic-tailwind-4.css +6 -0
  3. package/css/colors-semantic.css +136 -100
  4. package/css/ffe.css +138 -101
  5. package/less/colors-semantic-chevron.less +1 -1
  6. package/less/colors-semantic.less +136 -100
  7. package/less/dimensions.less +1 -0
  8. package/less/variables.less +0 -1
  9. package/lib/semanticColors.js +7 -1
  10. package/lib/semanticColorsTailwind.js +7 -1
  11. package/package.json +6 -4
  12. package/scripts/build-colors/build-colors.cjs +115 -0
  13. package/scripts/build-colors/json/json.cjs +12 -0
  14. package/scripts/build-colors/mobile/android.cjs +99 -0
  15. package/scripts/build-colors/mobile/android.test.js +98 -0
  16. package/scripts/build-colors/mobile/ios.cjs +205 -0
  17. package/scripts/build-colors/web/chevron.cjs +44 -0
  18. package/scripts/build-colors/web/color-names.cjs +24 -0
  19. package/scripts/build-colors/web/css.cjs +78 -0
  20. package/scripts/build-colors/web/js.cjs +44 -0
  21. package/scripts/build-colors/web/tailwind.cjs +36 -0
  22. package/scripts/build-colors/web/tailwind4.cjs +24 -0
  23. package/scripts/build-colors/web/web.cjs +18 -0
  24. package/scripts/build-custom-mq.cjs +45 -0
  25. package/scripts/build.cjs +38 -0
  26. package/scripts/figma-api/color.spec.tsx +126 -0
  27. package/scripts/figma-api/color.ts +52 -0
  28. package/scripts/figma-api/figma_api.ts +41 -0
  29. package/scripts/figma-api/sync_figma_to_tokens.ts +57 -0
  30. package/scripts/figma-api/token_export.spec.tsx +359 -0
  31. package/scripts/figma-api/token_export.ts +99 -0
  32. package/scripts/figma-api/token_import.spec.tsx +1097 -0
  33. package/scripts/figma-api/token_import.ts +430 -0
  34. package/scripts/figma-api/token_types.ts +55 -0
  35. package/scripts/figma-api/utils.ts +11 -0
  36. package/scripts/lib/extractCustomProps.cjs +24 -0
  37. package/scripts/lib/genTypeScript.cjs +7 -0
  38. package/scripts/lib/renderLessVarsToCSSProps.cjs +23 -0
  39. package/scripts/lib/writeToFile.cjs +9 -0
  40. package/scripts/utils.cjs +27 -0
  41. package/tokens/01 Primitive.value.json +4726 -0
  42. package/tokens/02 Semantic.Dark.json +3942 -0
  43. package/tokens/02 Semantic.Light.json +3942 -0
  44. package/tokens/03 Context.Accent.json +2273 -0
  45. package/tokens/03 Context.Default.json +2273 -0
@@ -0,0 +1,1097 @@
1
+ import { GetLocalVariablesResponse } from '@figma/rest-api-spec';
2
+ import {
3
+ FlattenedTokensByFile,
4
+ generatePostVariablesPayload,
5
+ readJsonFiles,
6
+ } from './token_import';
7
+
8
+ jest.mock('fs', () => {
9
+ const MOCK_FILE_INFO: { [fileName: string]: string } = {
10
+ 'tokens/collection1.mode1.json': JSON.stringify({
11
+ spacing: {
12
+ '1': {
13
+ $type: 'number',
14
+ $value: 8,
15
+ $description: '8px spacing',
16
+ },
17
+ '2': {
18
+ $type: 'number',
19
+ $value: 16,
20
+ $description: '16px spacing',
21
+ },
22
+ },
23
+ }),
24
+ 'tokens/collection2.mode1.json': JSON.stringify({
25
+ color: {
26
+ brand: {
27
+ radish: {
28
+ $type: 'color',
29
+ $value: '#ffbe16',
30
+ $description: 'Radish color',
31
+ },
32
+ pear: {
33
+ $type: 'color',
34
+ $value: '#ffbe16',
35
+ $description: 'Pear color',
36
+ },
37
+ },
38
+ },
39
+ }),
40
+ 'tokens/collection3.mode1.json': JSON.stringify({
41
+ token1: { $type: 'string', $value: 'value1' },
42
+ token2: { $type: 'string', $value: 'value2' },
43
+ }),
44
+ 'no_tokens.mode1.json': JSON.stringify({
45
+ foo: 'bar',
46
+ }),
47
+ 'empty_file.mode1.json': '',
48
+ 'file_with_$_keys.mode1.json': JSON.stringify({
49
+ $foo: 'bar',
50
+ token1: { $type: 'string', $value: 'value1' },
51
+ }),
52
+ };
53
+
54
+ return {
55
+ readFileSync: (fpath: string) => {
56
+ if (fpath in MOCK_FILE_INFO) {
57
+ return MOCK_FILE_INFO[fpath];
58
+ }
59
+ return '{}';
60
+ },
61
+ };
62
+ });
63
+
64
+ describe('readJsonFiles', () => {
65
+ it('reads all files and flattens tokens inside', () => {
66
+ const result = readJsonFiles([
67
+ 'tokens/collection1.mode1.json',
68
+ 'tokens/collection2.mode1.json',
69
+ 'tokens/collection3.mode1.json',
70
+ ]);
71
+ expect(result).toEqual({
72
+ 'collection1.mode1.json': {
73
+ 'spacing/1': {
74
+ $type: 'number',
75
+ $value: 8,
76
+ $description: '8px spacing',
77
+ },
78
+ 'spacing/2': {
79
+ $type: 'number',
80
+ $value: 16,
81
+ $description: '16px spacing',
82
+ },
83
+ },
84
+ 'collection2.mode1.json': {
85
+ 'color/brand/radish': {
86
+ $type: 'color',
87
+ $value: '#ffbe16',
88
+ $description: 'Radish color',
89
+ },
90
+ 'color/brand/pear': {
91
+ $type: 'color',
92
+ $value: '#ffbe16',
93
+ $description: 'Pear color',
94
+ },
95
+ },
96
+ 'collection3.mode1.json': {
97
+ token1: { $type: 'string', $value: 'value1' },
98
+ token2: { $type: 'string', $value: 'value2' },
99
+ },
100
+ });
101
+ });
102
+
103
+ it('handles files that do not have any tokens', () => {
104
+ const result = readJsonFiles(['no_tokens.mode1.json']);
105
+ expect(result).toEqual({ 'no_tokens.mode1.json': {} });
106
+ });
107
+
108
+ it('handles duplicate collections and modes', () => {
109
+ expect(() => {
110
+ readJsonFiles([
111
+ 'tokens/collection1.mode1.1.json',
112
+ 'tokens/collection1.mode1.2.json',
113
+ 'tokens/collection1.mode1.3.json',
114
+ ]);
115
+ }).toThrowError(
116
+ 'Duplicate collection and mode in file: tokens/collection1.mode1.2.json',
117
+ );
118
+ });
119
+
120
+ it('handles file names that do not match the expected format', () => {
121
+ expect(() => {
122
+ readJsonFiles([
123
+ 'tokens/collection1.mode1.json',
124
+ 'tokens/collection2.mode1.json',
125
+ 'foo.json',
126
+ ]);
127
+ }).toThrowError(
128
+ 'Invalid tokens file name: foo.json. File names must be in the format: {collectionName}.{modeName}.json',
129
+ );
130
+ });
131
+
132
+ it('ignores keys that start with $', () => {
133
+ const result = readJsonFiles(['file_with_$_keys.mode1.json']);
134
+ expect(result).toEqual({
135
+ 'file_with_$_keys.mode1.json': {
136
+ token1: { $type: 'string', $value: 'value1' },
137
+ },
138
+ });
139
+ });
140
+
141
+ it('handles empty files', () => {
142
+ expect(() => {
143
+ readJsonFiles(['empty_file.mode1.json']);
144
+ }).toThrowError(
145
+ 'Invalid tokens file: empty_file.mode1.json. File is empty.',
146
+ );
147
+ });
148
+ });
149
+
150
+ describe('generatePostVariablesPayload', () => {
151
+ it('does an initial sync', async () => {
152
+ const localVariablesResponse: GetLocalVariablesResponse = {
153
+ status: 200,
154
+ error: false,
155
+ meta: {
156
+ variableCollections: {},
157
+ variables: {},
158
+ },
159
+ };
160
+
161
+ const tokensByFile: FlattenedTokensByFile = {
162
+ 'primitives.mode1.json': {
163
+ 'spacing/1': {
164
+ $type: 'number',
165
+ $value: 8,
166
+ $description: '8px spacing',
167
+ },
168
+ 'spacing/2': { $type: 'number', $value: 16 },
169
+ 'color/brand/radish': {
170
+ $type: 'color',
171
+ $value: '#ffbe16',
172
+ $description: 'Radish color',
173
+ },
174
+ 'color/brand/pear': { $type: 'color', $value: '#ffbe16' },
175
+ },
176
+ 'primitives.mode2.json': {
177
+ 'spacing/1': { $type: 'number', $value: 8 },
178
+ 'spacing/2': { $type: 'number', $value: 16 },
179
+ 'color/brand/radish': { $type: 'color', $value: '#010101' },
180
+ 'color/brand/pear': { $type: 'color', $value: '#010101' },
181
+ },
182
+ 'tokens.mode1.json': {
183
+ 'spacing/spacing-sm': {
184
+ $type: 'number',
185
+ $value: '{spacing.1}',
186
+ },
187
+ 'surface/surface-brand': {
188
+ $type: 'color',
189
+ $value: '{color.brand.radish}',
190
+ },
191
+ },
192
+ 'tokens.mode2.json': {
193
+ 'spacing/spacing-sm': {
194
+ $type: 'number',
195
+ $value: '{spacing.1}',
196
+ },
197
+ 'surface/surface-brand': {
198
+ $type: 'color',
199
+ $value: '{color.brand.pear}',
200
+ },
201
+ },
202
+ };
203
+
204
+ const result = generatePostVariablesPayload(
205
+ tokensByFile,
206
+ localVariablesResponse,
207
+ );
208
+ expect(result.variableCollections).toEqual([
209
+ {
210
+ action: 'CREATE',
211
+ id: 'primitives',
212
+ name: 'primitives',
213
+ initialModeId: 'mode1',
214
+ },
215
+ {
216
+ action: 'CREATE',
217
+ id: 'tokens',
218
+ name: 'tokens',
219
+ initialModeId: 'mode1',
220
+ },
221
+ ]);
222
+
223
+ expect(result.variableModes).toEqual([
224
+ {
225
+ action: 'UPDATE',
226
+ id: 'mode1',
227
+ name: 'mode1',
228
+ variableCollectionId: 'primitives',
229
+ },
230
+ {
231
+ action: 'CREATE',
232
+ id: 'mode2',
233
+ name: 'mode2',
234
+ variableCollectionId: 'primitives',
235
+ },
236
+ {
237
+ action: 'UPDATE',
238
+ id: 'mode1',
239
+ name: 'mode1',
240
+ variableCollectionId: 'tokens',
241
+ },
242
+ {
243
+ action: 'CREATE',
244
+ id: 'mode2',
245
+ name: 'mode2',
246
+ variableCollectionId: 'tokens',
247
+ },
248
+ ]);
249
+
250
+ expect(result.variables).toEqual([
251
+ // variables for the primitives collection
252
+ {
253
+ action: 'CREATE',
254
+ id: 'spacing/1',
255
+ name: 'spacing/1',
256
+ variableCollectionId: 'primitives',
257
+ resolvedType: 'FLOAT',
258
+ description: '8px spacing',
259
+ },
260
+ {
261
+ action: 'CREATE',
262
+ id: 'spacing/2',
263
+ name: 'spacing/2',
264
+ variableCollectionId: 'primitives',
265
+ resolvedType: 'FLOAT',
266
+ },
267
+ {
268
+ action: 'CREATE',
269
+ id: 'color/brand/radish',
270
+ name: 'color/brand/radish',
271
+ variableCollectionId: 'primitives',
272
+ resolvedType: 'COLOR',
273
+ description: 'Radish color',
274
+ },
275
+ {
276
+ action: 'CREATE',
277
+ id: 'color/brand/pear',
278
+ name: 'color/brand/pear',
279
+ variableCollectionId: 'primitives',
280
+ resolvedType: 'COLOR',
281
+ },
282
+
283
+ // variables for the tokens collection
284
+ {
285
+ action: 'CREATE',
286
+ id: 'spacing/spacing-sm',
287
+ name: 'spacing/spacing-sm',
288
+ variableCollectionId: 'tokens',
289
+ resolvedType: 'FLOAT',
290
+ },
291
+ {
292
+ action: 'CREATE',
293
+ id: 'surface/surface-brand',
294
+ name: 'surface/surface-brand',
295
+ variableCollectionId: 'tokens',
296
+ resolvedType: 'COLOR',
297
+ },
298
+ ]);
299
+
300
+ expect(result.variableModeValues).toEqual([
301
+ // primitives, mode1
302
+ { variableId: 'spacing/1', modeId: 'mode1', value: 8 },
303
+ { variableId: 'spacing/2', modeId: 'mode1', value: 16 },
304
+ {
305
+ variableId: 'color/brand/radish',
306
+ modeId: 'mode1',
307
+ value: { r: 1, g: 0.7450980392156863, b: 0.08627450980392157 },
308
+ },
309
+ {
310
+ variableId: 'color/brand/pear',
311
+ modeId: 'mode1',
312
+ value: { r: 1, g: 0.7450980392156863, b: 0.08627450980392157 },
313
+ },
314
+
315
+ // primitives, mode2
316
+ { variableId: 'spacing/1', modeId: 'mode2', value: 8 },
317
+ { variableId: 'spacing/2', modeId: 'mode2', value: 16 },
318
+ {
319
+ variableId: 'color/brand/radish',
320
+ modeId: 'mode2',
321
+ value: {
322
+ r: 0.00392156862745098,
323
+ g: 0.00392156862745098,
324
+ b: 0.00392156862745098,
325
+ },
326
+ },
327
+ {
328
+ variableId: 'color/brand/pear',
329
+ modeId: 'mode2',
330
+ value: {
331
+ r: 0.00392156862745098,
332
+ g: 0.00392156862745098,
333
+ b: 0.00392156862745098,
334
+ },
335
+ },
336
+
337
+ // tokens, mode1
338
+ {
339
+ variableId: 'spacing/spacing-sm',
340
+ modeId: 'mode1',
341
+ value: { type: 'VARIABLE_ALIAS', id: 'spacing/1' },
342
+ },
343
+ {
344
+ variableId: 'surface/surface-brand',
345
+ modeId: 'mode1',
346
+ value: { type: 'VARIABLE_ALIAS', id: 'color/brand/radish' },
347
+ },
348
+
349
+ // tokens, mode2
350
+ {
351
+ variableId: 'spacing/spacing-sm',
352
+ modeId: 'mode2',
353
+ value: { type: 'VARIABLE_ALIAS', id: 'spacing/1' },
354
+ },
355
+ {
356
+ variableId: 'surface/surface-brand',
357
+ modeId: 'mode2',
358
+ value: { type: 'VARIABLE_ALIAS', id: 'color/brand/pear' },
359
+ },
360
+ ]);
361
+ });
362
+
363
+ it('does an in-place update', async () => {
364
+ const localVariablesResponse: GetLocalVariablesResponse = {
365
+ status: 200,
366
+ error: false,
367
+ meta: {
368
+ variableCollections: {
369
+ 'VariableCollectionId:1:1': {
370
+ id: 'VariableCollectionId:1:1',
371
+ name: 'primitives',
372
+ modes: [{ modeId: '1:0', name: 'mode1' }],
373
+ defaultModeId: '1:0',
374
+ remote: false,
375
+ key: 'variableKey',
376
+ hiddenFromPublishing: false,
377
+ variableIds: [
378
+ 'VariableID:2:1',
379
+ 'VariableID:2:2',
380
+ 'VariableID:2:3',
381
+ 'VariableID:2:4',
382
+ ],
383
+ },
384
+ },
385
+ variables: {
386
+ 'VariableID:2:1': {
387
+ id: 'VariableID:2:1',
388
+ name: 'spacing/1',
389
+ key: 'variable_key',
390
+ variableCollectionId: 'VariableCollectionId:1:1',
391
+ resolvedType: 'FLOAT',
392
+ valuesByMode: {
393
+ '1:0': 8,
394
+ },
395
+ remote: false,
396
+ description: '8px spacing',
397
+ hiddenFromPublishing: false,
398
+ scopes: ['ALL_SCOPES'],
399
+ codeSyntax: { WEB: 'web', ANDROID: 'android' },
400
+ },
401
+ 'VariableID:2:2': {
402
+ id: 'VariableID:2:2',
403
+ name: 'spacing/2',
404
+ key: 'variable_key2',
405
+ variableCollectionId: 'VariableCollectionId:1:1',
406
+ resolvedType: 'FLOAT',
407
+ valuesByMode: {
408
+ '1:0': 15, // Different from token value
409
+ },
410
+ remote: false,
411
+ description: 'Another spacing',
412
+ hiddenFromPublishing: false,
413
+ scopes: ['ALL_SCOPES'],
414
+ codeSyntax: { WEB: 'web', ANDROID: 'android' },
415
+ },
416
+ 'VariableID:2:3': {
417
+ id: 'VariableID:2:3',
418
+ name: 'color/brand/radish',
419
+ key: 'variable_key3',
420
+ variableCollectionId: 'VariableCollectionId:1:1',
421
+ resolvedType: 'COLOR',
422
+ valuesByMode: {
423
+ '1:0': {
424
+ r: 1,
425
+ g: 0.7450980392156863,
426
+ b: 0.08627450980392157,
427
+ a: 1,
428
+ },
429
+ },
430
+ remote: false,
431
+ description: 'Radish color',
432
+ hiddenFromPublishing: false,
433
+ scopes: ['ALL_SCOPES'],
434
+ codeSyntax: {},
435
+ },
436
+ 'VariableID:2:4': {
437
+ id: 'VariableID:2:4',
438
+ name: 'color/brand/pear',
439
+ key: 'variable_key4',
440
+ variableCollectionId: 'VariableCollectionId:1:1',
441
+ resolvedType: 'COLOR',
442
+ valuesByMode: {
443
+ // Different from token value
444
+ '1:0': { r: 1, g: 0, b: 0.08627450980392157, a: 1 },
445
+ },
446
+ remote: false,
447
+ description: 'Pear color',
448
+ hiddenFromPublishing: false,
449
+ scopes: ['ALL_SCOPES'],
450
+ codeSyntax: {},
451
+ },
452
+ },
453
+ },
454
+ };
455
+
456
+ const tokensByFile: FlattenedTokensByFile = {
457
+ 'primitives.mode1.json': {
458
+ 'spacing/1': {
459
+ $type: 'number',
460
+ $value: 8,
461
+ $description: '8px spacing',
462
+ $extensions: {
463
+ 'com.figma': {
464
+ hiddenFromPublishing: false,
465
+ scopes: ['ALL_SCOPES'],
466
+ codeSyntax: { WEB: 'web', ANDROID: 'android' },
467
+ },
468
+ },
469
+ },
470
+ 'spacing/2': {
471
+ $type: 'number',
472
+ $value: 16,
473
+ $description: 'changed description',
474
+ $extensions: {
475
+ 'com.figma': {
476
+ hiddenFromPublishing: true,
477
+ scopes: ['TEXT_CONTENT'],
478
+ codeSyntax: { WEB: 'web', ANDROID: 'android_new' },
479
+ },
480
+ },
481
+ },
482
+ 'color/brand/radish': { $type: 'color', $value: '#ffbe16' },
483
+ 'color/brand/pear': { $type: 'color', $value: '#ffbe16' },
484
+ },
485
+ 'primitives.mode2.json': {
486
+ 'spacing/1': { $type: 'number', $value: 8 },
487
+ 'spacing/2': { $type: 'number', $value: 16 },
488
+ 'color/brand/radish': { $type: 'color', $value: '#010101' },
489
+ 'color/brand/pear': { $type: 'color', $value: '#010101' },
490
+ },
491
+ 'tokens.mode1.json': {
492
+ 'spacing/spacing-sm': {
493
+ $type: 'number',
494
+ $value: '{spacing.1}',
495
+ },
496
+ 'surface/surface-brand': {
497
+ $type: 'color',
498
+ $value: '{color.brand.radish}',
499
+ },
500
+ },
501
+ 'tokens.mode2.json': {
502
+ 'spacing/spacing-sm': {
503
+ $type: 'number',
504
+ $value: '{spacing.1}',
505
+ },
506
+ 'surface/surface-brand': {
507
+ $type: 'color',
508
+ $value: '{color.brand.pear}',
509
+ },
510
+ },
511
+ };
512
+
513
+ const result = generatePostVariablesPayload(
514
+ tokensByFile,
515
+ localVariablesResponse,
516
+ );
517
+ expect(result.variableCollections).toEqual([
518
+ {
519
+ action: 'CREATE',
520
+ id: 'tokens',
521
+ name: 'tokens',
522
+ initialModeId: 'mode1',
523
+ },
524
+ ]);
525
+
526
+ expect(result.variableModes).toEqual([
527
+ {
528
+ action: 'CREATE',
529
+ id: 'mode2',
530
+ name: 'mode2',
531
+ variableCollectionId: 'VariableCollectionId:1:1',
532
+ },
533
+ {
534
+ action: 'UPDATE',
535
+ id: 'mode1',
536
+ name: 'mode1',
537
+ variableCollectionId: 'tokens',
538
+ },
539
+ {
540
+ action: 'CREATE',
541
+ id: 'mode2',
542
+ name: 'mode2',
543
+ variableCollectionId: 'tokens',
544
+ },
545
+ ]);
546
+
547
+ expect(result.variables).toEqual([
548
+ {
549
+ action: 'UPDATE',
550
+ id: 'VariableID:2:2',
551
+ description: 'changed description',
552
+ hiddenFromPublishing: true,
553
+ scopes: ['TEXT_CONTENT'],
554
+ codeSyntax: { WEB: 'web', ANDROID: 'android_new' },
555
+ },
556
+ // new variables for the tokens collection
557
+ {
558
+ action: 'CREATE',
559
+ id: 'spacing/spacing-sm',
560
+ name: 'spacing/spacing-sm',
561
+ variableCollectionId: 'tokens',
562
+ resolvedType: 'FLOAT',
563
+ },
564
+ {
565
+ action: 'CREATE',
566
+ id: 'surface/surface-brand',
567
+ name: 'surface/surface-brand',
568
+ variableCollectionId: 'tokens',
569
+ resolvedType: 'COLOR',
570
+ },
571
+ ]);
572
+
573
+ expect(result.variableModeValues).toEqual([
574
+ // primitives, mode1
575
+ { variableId: 'VariableID:2:2', modeId: '1:0', value: 16 },
576
+ {
577
+ variableId: 'VariableID:2:4',
578
+ modeId: '1:0',
579
+ value: { r: 1, g: 0.7450980392156863, b: 0.08627450980392157 },
580
+ },
581
+
582
+ // primitives, mode2
583
+ { variableId: 'VariableID:2:1', modeId: 'mode2', value: 8 },
584
+ { variableId: 'VariableID:2:2', modeId: 'mode2', value: 16 },
585
+ {
586
+ variableId: 'VariableID:2:3',
587
+ modeId: 'mode2',
588
+ value: {
589
+ r: 0.00392156862745098,
590
+ g: 0.00392156862745098,
591
+ b: 0.00392156862745098,
592
+ },
593
+ },
594
+ {
595
+ variableId: 'VariableID:2:4',
596
+ modeId: 'mode2',
597
+ value: {
598
+ r: 0.00392156862745098,
599
+ g: 0.00392156862745098,
600
+ b: 0.00392156862745098,
601
+ },
602
+ },
603
+
604
+ // tokens, mode1
605
+ {
606
+ variableId: 'spacing/spacing-sm',
607
+ modeId: 'mode1',
608
+ value: { type: 'VARIABLE_ALIAS', id: 'VariableID:2:1' },
609
+ },
610
+ {
611
+ variableId: 'surface/surface-brand',
612
+ modeId: 'mode1',
613
+ value: { type: 'VARIABLE_ALIAS', id: 'VariableID:2:3' },
614
+ },
615
+
616
+ // tokens, mode2
617
+ {
618
+ variableId: 'spacing/spacing-sm',
619
+ modeId: 'mode2',
620
+ value: { type: 'VARIABLE_ALIAS', id: 'VariableID:2:1' },
621
+ },
622
+ {
623
+ variableId: 'surface/surface-brand',
624
+ modeId: 'mode2',
625
+ value: { type: 'VARIABLE_ALIAS', id: 'VariableID:2:4' },
626
+ },
627
+ ]);
628
+ });
629
+
630
+ it('noops when everything is already in sync (with aliases)', () => {
631
+ const localVariablesResponse: GetLocalVariablesResponse = {
632
+ status: 200,
633
+ error: false,
634
+ meta: {
635
+ variableCollections: {
636
+ 'VariableCollectionId:1:1': {
637
+ id: 'VariableCollectionId:1:1',
638
+ name: 'collection',
639
+ modes: [{ modeId: '1:0', name: 'mode1' }],
640
+ defaultModeId: '1:0',
641
+ remote: false,
642
+ key: 'variableKey',
643
+ hiddenFromPublishing: false,
644
+ variableIds: ['VariableID:2:1', 'VariableID:2:2'],
645
+ },
646
+ },
647
+ variables: {
648
+ 'VariableID:2:1': {
649
+ id: 'VariableID:2:1',
650
+ name: 'var1',
651
+ key: 'variable_key',
652
+ variableCollectionId: 'VariableCollectionId:1:1',
653
+ resolvedType: 'STRING',
654
+ valuesByMode: {
655
+ '1:0': 'hello world!',
656
+ },
657
+ remote: false,
658
+ description: '',
659
+ hiddenFromPublishing: false,
660
+ scopes: ['ALL_SCOPES'],
661
+ codeSyntax: {},
662
+ },
663
+ 'VariableID:2:2': {
664
+ id: 'VariableID:2:2',
665
+ name: 'var2',
666
+ key: 'variable_key2',
667
+ variableCollectionId: 'VariableCollectionId:1:1',
668
+ resolvedType: 'STRING',
669
+ valuesByMode: {
670
+ '1:0': {
671
+ type: 'VARIABLE_ALIAS',
672
+ id: 'VariableID:2:1',
673
+ },
674
+ },
675
+ remote: false,
676
+ description: '',
677
+ hiddenFromPublishing: false,
678
+ scopes: ['ALL_SCOPES'],
679
+ codeSyntax: {},
680
+ },
681
+ },
682
+ },
683
+ };
684
+
685
+ const tokensByFile: FlattenedTokensByFile = {
686
+ 'collection.mode1.json': {
687
+ var1: {
688
+ $type: 'string',
689
+ $value: 'hello world!',
690
+ $description: '',
691
+ $extensions: {
692
+ 'com.figma': {
693
+ hiddenFromPublishing: false,
694
+ scopes: ['ALL_SCOPES'],
695
+ codeSyntax: {},
696
+ },
697
+ },
698
+ },
699
+ var2: {
700
+ $type: 'string',
701
+ $value: '{var1}',
702
+ $description: '',
703
+ $extensions: {
704
+ 'com.figma': {
705
+ hiddenFromPublishing: false,
706
+ scopes: ['ALL_SCOPES'],
707
+ codeSyntax: {},
708
+ },
709
+ },
710
+ },
711
+ },
712
+ };
713
+
714
+ const result = generatePostVariablesPayload(
715
+ tokensByFile,
716
+ localVariablesResponse,
717
+ );
718
+
719
+ expect(result).toEqual({
720
+ variableCollections: [],
721
+ variableModes: [],
722
+ variables: [],
723
+ variableModeValues: [],
724
+ });
725
+ });
726
+
727
+ it('noops if tokens happen to match remote collections and variables', () => {
728
+ const localVariablesResponse: GetLocalVariablesResponse = {
729
+ status: 200,
730
+ error: false,
731
+ meta: {
732
+ variableCollections: {
733
+ 'VariableCollectionId:1:1': {
734
+ id: 'VariableCollectionId:1:1',
735
+ name: 'collection',
736
+ modes: [{ modeId: '1:0', name: 'mode1' }],
737
+ defaultModeId: '1:0',
738
+ remote: true,
739
+ key: 'variableKey',
740
+ hiddenFromPublishing: false,
741
+ variableIds: ['VariableID:2:1', 'VariableID:2:2'],
742
+ },
743
+ },
744
+ variables: {
745
+ 'VariableID:2:1': {
746
+ id: 'VariableID:2:1',
747
+ name: 'var1',
748
+ key: 'variable_key',
749
+ variableCollectionId: 'VariableCollectionId:1:1',
750
+ resolvedType: 'STRING',
751
+ valuesByMode: {
752
+ '1:0': 'hello world!',
753
+ },
754
+ remote: true,
755
+ description: '',
756
+ hiddenFromPublishing: false,
757
+ scopes: ['ALL_SCOPES'],
758
+ codeSyntax: {},
759
+ },
760
+ 'VariableID:2:2': {
761
+ id: 'VariableID:2:2',
762
+ name: 'var2',
763
+ key: 'variable_key2',
764
+ variableCollectionId: 'VariableCollectionId:1:1',
765
+ resolvedType: 'STRING',
766
+ valuesByMode: {
767
+ '1:0': {
768
+ type: 'VARIABLE_ALIAS',
769
+ id: 'VariableID:2:1',
770
+ },
771
+ },
772
+ remote: true,
773
+ description: '',
774
+ hiddenFromPublishing: false,
775
+ scopes: ['ALL_SCOPES'],
776
+ codeSyntax: {},
777
+ },
778
+ },
779
+ },
780
+ };
781
+
782
+ const tokensByFile: FlattenedTokensByFile = {
783
+ 'collection.mode1.json': {
784
+ var1: {
785
+ $type: 'string',
786
+ $value: 'hello world!',
787
+ $description: '',
788
+ $extensions: {
789
+ 'com.figma': {
790
+ hiddenFromPublishing: false,
791
+ scopes: ['ALL_SCOPES'],
792
+ codeSyntax: {},
793
+ },
794
+ },
795
+ },
796
+ var2: {
797
+ $type: 'string',
798
+ $value: '{var1}',
799
+ $description: '',
800
+ $extensions: {
801
+ 'com.figma': {
802
+ hiddenFromPublishing: false,
803
+ scopes: ['ALL_SCOPES'],
804
+ codeSyntax: {},
805
+ },
806
+ },
807
+ },
808
+ },
809
+ };
810
+
811
+ const result = generatePostVariablesPayload(
812
+ tokensByFile,
813
+ localVariablesResponse,
814
+ );
815
+
816
+ expect(result).toEqual({
817
+ variableCollections: [],
818
+ variableModes: [],
819
+ variables: [],
820
+ variableModeValues: [],
821
+ });
822
+ });
823
+
824
+ it('throws on attempted modifications to remote variables', () => {
825
+ const localVariablesResponse: GetLocalVariablesResponse = {
826
+ status: 200,
827
+ error: false,
828
+ meta: {
829
+ variableCollections: {
830
+ 'VariableCollectionId:1:1': {
831
+ id: 'VariableCollectionId:1:1',
832
+ name: 'collection',
833
+ modes: [{ modeId: '1:0', name: 'mode1' }],
834
+ defaultModeId: '1:0',
835
+ remote: true,
836
+ key: 'variableKey',
837
+ hiddenFromPublishing: false,
838
+ variableIds: ['VariableID:2:1', 'VariableID:2:2'],
839
+ },
840
+ },
841
+ variables: {
842
+ 'VariableID:2:1': {
843
+ id: 'VariableID:2:1',
844
+ name: 'var1',
845
+ key: 'variable_key',
846
+ variableCollectionId: 'VariableCollectionId:1:1',
847
+ resolvedType: 'STRING',
848
+ valuesByMode: {
849
+ '1:0': 'hello world!',
850
+ },
851
+ remote: true,
852
+ description: '',
853
+ hiddenFromPublishing: false,
854
+ scopes: ['ALL_SCOPES'],
855
+ codeSyntax: {},
856
+ },
857
+ 'VariableID:2:2': {
858
+ id: 'VariableID:2:2',
859
+ name: 'var2',
860
+ key: 'variable_key2',
861
+ variableCollectionId: 'VariableCollectionId:1:1',
862
+ resolvedType: 'STRING',
863
+ valuesByMode: {
864
+ '1:0': {
865
+ type: 'VARIABLE_ALIAS',
866
+ id: 'VariableID:2:1',
867
+ },
868
+ },
869
+ remote: true,
870
+ description: '',
871
+ hiddenFromPublishing: false,
872
+ scopes: ['ALL_SCOPES'],
873
+ codeSyntax: {},
874
+ },
875
+ },
876
+ },
877
+ };
878
+
879
+ const tokensByFile: FlattenedTokensByFile = {
880
+ 'collection.mode1.json': {
881
+ var1: {
882
+ $type: 'string',
883
+ $value: 'hello world!',
884
+ $description: '',
885
+ $extensions: {
886
+ 'com.figma': {
887
+ hiddenFromPublishing: true, // modification
888
+ scopes: ['ALL_SCOPES'],
889
+ codeSyntax: {},
890
+ },
891
+ },
892
+ },
893
+ var2: {
894
+ $type: 'string',
895
+ $value: '{var1}',
896
+ $description: '',
897
+ $extensions: {
898
+ 'com.figma': {
899
+ hiddenFromPublishing: false,
900
+ scopes: ['ALL_SCOPES'],
901
+ codeSyntax: {},
902
+ },
903
+ },
904
+ },
905
+ },
906
+ };
907
+
908
+ expect(() => {
909
+ generatePostVariablesPayload(tokensByFile, localVariablesResponse);
910
+ }).toThrowError(
911
+ `Cannot update remote variable "var1" in collection "collection"`,
912
+ );
913
+ });
914
+
915
+ it('updates aliases to remote variables', () => {
916
+ const localVariablesResponse: GetLocalVariablesResponse = {
917
+ status: 200,
918
+ error: false,
919
+ meta: {
920
+ variableCollections: {
921
+ 'VariableCollectionId:1:1': {
922
+ id: 'VariableCollectionId:1:1',
923
+ name: 'primitives',
924
+ modes: [{ modeId: '1:0', name: 'mode1' }],
925
+ defaultModeId: '1:0',
926
+ remote: true,
927
+ key: 'variableCollectionKey1',
928
+ hiddenFromPublishing: false,
929
+ variableIds: ['VariableID:1:2', 'VariableID:1:3'],
930
+ },
931
+ 'VariableCollectionId:2:1': {
932
+ id: 'VariableCollectionId:2:1',
933
+ name: 'tokens',
934
+ modes: [{ modeId: '2:0', name: 'mode1' }],
935
+ defaultModeId: '2:0',
936
+ remote: false,
937
+ key: 'variableCollectionKey2',
938
+ hiddenFromPublishing: false,
939
+ variableIds: ['VariableID:2:1'],
940
+ },
941
+ },
942
+ variables: {
943
+ // 2 color variables in the primitives collection
944
+ 'VariableID:1:2': {
945
+ id: 'VariableID:1:2',
946
+ name: 'gray/100',
947
+ key: 'variableKey1',
948
+ variableCollectionId: 'VariableCollectionId:1:1',
949
+ resolvedType: 'COLOR',
950
+ valuesByMode: {
951
+ '1:0': { r: 0.98, g: 0.98, b: 0.98, a: 1 },
952
+ },
953
+ remote: true,
954
+ description: 'light gray',
955
+ hiddenFromPublishing: false,
956
+ scopes: ['ALL_SCOPES'],
957
+ codeSyntax: {},
958
+ },
959
+ 'VariableID:1:3': {
960
+ id: 'VariableID:1:3',
961
+ name: 'gray/200',
962
+ key: 'variableKey2',
963
+ variableCollectionId: 'VariableCollectionId:1:1',
964
+ resolvedType: 'COLOR',
965
+ valuesByMode: {
966
+ '1:0': { r: 0.96, g: 0.96, b: 0.96, a: 1 },
967
+ },
968
+ remote: true,
969
+ description: 'light gray',
970
+ hiddenFromPublishing: false,
971
+ scopes: ['ALL_SCOPES'],
972
+ codeSyntax: {},
973
+ },
974
+ // 1 color variable in the tokens collection
975
+ 'VariableID:2:1': {
976
+ id: 'VariableID:2:1',
977
+ name: 'surface/surface-brand',
978
+ key: 'variableKey3',
979
+ variableCollectionId: 'VariableCollectionId:2:1',
980
+ resolvedType: 'COLOR',
981
+ valuesByMode: {
982
+ '2:0': {
983
+ type: 'VARIABLE_ALIAS',
984
+ id: 'VariableID:1:2',
985
+ },
986
+ },
987
+ remote: false,
988
+ description: 'light gray',
989
+ hiddenFromPublishing: false,
990
+ scopes: ['ALL_SCOPES'],
991
+ codeSyntax: {},
992
+ },
993
+ },
994
+ },
995
+ };
996
+
997
+ const tokensByFile: FlattenedTokensByFile = {
998
+ 'tokens.mode1.json': {
999
+ // Change the alias to point to the other variable in the primitives collection
1000
+ 'surface/surface-brand': {
1001
+ $type: 'color',
1002
+ $value: '{gray.200}',
1003
+ },
1004
+ },
1005
+ };
1006
+
1007
+ const result = generatePostVariablesPayload(
1008
+ tokensByFile,
1009
+ localVariablesResponse,
1010
+ );
1011
+
1012
+ expect(result.variableCollections).toEqual([]);
1013
+ expect(result.variableModes).toEqual([]);
1014
+ expect(result.variables).toEqual([]);
1015
+ expect(result.variableModeValues).toEqual([
1016
+ {
1017
+ variableId: 'VariableID:2:1',
1018
+ modeId: '2:0',
1019
+ value: { type: 'VARIABLE_ALIAS', id: 'VariableID:1:3' },
1020
+ },
1021
+ ]);
1022
+ });
1023
+
1024
+ it('throws on unsupported token types', async () => {
1025
+ const localVariablesResponse: GetLocalVariablesResponse = {
1026
+ status: 200,
1027
+ error: false,
1028
+ meta: {
1029
+ variableCollections: {},
1030
+ variables: {},
1031
+ },
1032
+ };
1033
+
1034
+ const tokensByFile: any = {
1035
+ 'primitives.mode1.json': {
1036
+ 'font-weight-default': { $type: 'fontWeight', $value: 400 },
1037
+ },
1038
+ };
1039
+
1040
+ expect(() => {
1041
+ generatePostVariablesPayload(tokensByFile, localVariablesResponse);
1042
+ }).toThrowError('Invalid token $type: fontWeight');
1043
+ });
1044
+
1045
+ it('throws on duplicate variable collections in the Figma file', () => {
1046
+ const localVariablesResponse: GetLocalVariablesResponse = {
1047
+ status: 200,
1048
+ error: false,
1049
+ meta: {
1050
+ variableCollections: {
1051
+ 'VariableCollectionId:1:1': {
1052
+ id: 'VariableCollectionId:1:1',
1053
+ name: 'collection',
1054
+ modes: [{ modeId: '1:0', name: 'mode1' }],
1055
+ defaultModeId: '1:0',
1056
+ remote: false,
1057
+ key: 'variableCollectionKey1',
1058
+ hiddenFromPublishing: false,
1059
+ variableIds: [],
1060
+ },
1061
+ 'VariableCollectionId:1:2': {
1062
+ id: 'VariableCollectionId:1:2',
1063
+ name: 'collection',
1064
+ modes: [{ modeId: '2:0', name: 'mode1' }],
1065
+ defaultModeId: '2:0',
1066
+ remote: false,
1067
+ key: 'variableCollectionKey2',
1068
+ hiddenFromPublishing: false,
1069
+ variableIds: [],
1070
+ },
1071
+ },
1072
+ variables: {},
1073
+ },
1074
+ };
1075
+
1076
+ const tokensByFile: FlattenedTokensByFile = {
1077
+ 'collection.mode1.json': {
1078
+ var1: {
1079
+ $type: 'string',
1080
+ $value: 'hello world!',
1081
+ $description: '',
1082
+ $extensions: {
1083
+ 'com.figma': {
1084
+ hiddenFromPublishing: false,
1085
+ scopes: ['ALL_SCOPES'],
1086
+ codeSyntax: {},
1087
+ },
1088
+ },
1089
+ },
1090
+ },
1091
+ };
1092
+
1093
+ expect(() => {
1094
+ generatePostVariablesPayload(tokensByFile, localVariablesResponse);
1095
+ }).toThrowError('Duplicate variable collection in file: collection');
1096
+ });
1097
+ });