Skip to content

Commit 281d7fb

Browse files
authored
Fix bug where aliases to remote variables could not be updated when syncing tokens to Figma (#23)
For the `sync_tokens_to_figma.ts` script, if a tokens file has aliases to variables in remote collections, e.g. `"$value": "{name_of_variable}"`, update those alias references when needed. This issue was brought up in #20. The previous behavior was that if a tokens file has aliases to variables in a remote collection, those aliases would cause a 400 response from the `POST /v1/files/:file_key/variables` endpoint, as the script was sending up invalid variable ids inside the alias objects. Now, the script will match up token values with remote variable names and update aliases when needed. Note: we are limited by the variables API in that we cannot alias variables that aren't already consumed by current file. Demo: Here is a demo of the `sync_tokens_to_figma.ts` script updating a Figma file that has a Semantic variables collection with aliases to variables published from another file. https://github.com/figma/variables-github-action-example/assets/250513/592fddbc-38fe-485c-89ca-688b1777b64f ## Test Plan - In one file, create and publish a variable collection - In another file, create a variable collection with variables that alias to variables from the file above - Run `npm run sync-figma-to-tokens` to export the variables from the second file into a tokens file - Run `npm run sync-tokens-to-figma` on the exported tokens file to sync the tokens back to Figma. The result is that we should noop instead of getting a 400 error from the REST API.
1 parent e881303 commit 281d7fb

File tree

2 files changed

+198
-21
lines changed

2 files changed

+198
-21
lines changed

src/token_import.test.ts

Lines changed: 192 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,7 @@ describe('generatePostVariablesPayload', () => {
629629
})
630630
})
631631

632-
it('ignores remote collections and variables', () => {
632+
it('noops if tokens happen to match remote collections and variables', () => {
633633
const localVariablesResponse: GetLocalVariablesResponse = {
634634
status: 200,
635635
error: false,
@@ -712,17 +712,198 @@ describe('generatePostVariablesPayload', () => {
712712

713713
const result = generatePostVariablesPayload(tokensByFile, localVariablesResponse)
714714

715-
// Since all existing collections and variables are remote, result should be equivalent to an initial sync
716-
expect(result).toEqual(
717-
generatePostVariablesPayload(tokensByFile, {
718-
status: 200,
719-
error: false,
720-
meta: {
721-
variableCollections: {},
722-
variables: {},
715+
expect(result).toEqual({
716+
variableCollections: [],
717+
variableModes: [],
718+
variables: [],
719+
variableModeValues: [],
720+
})
721+
})
722+
723+
it('throws on attempted modifications to remote variables', () => {
724+
const localVariablesResponse: GetLocalVariablesResponse = {
725+
status: 200,
726+
error: false,
727+
meta: {
728+
variableCollections: {
729+
'VariableCollectionId:1:1': {
730+
id: 'VariableCollectionId:1:1',
731+
name: 'collection',
732+
modes: [{ modeId: '1:0', name: 'mode1' }],
733+
defaultModeId: '1:0',
734+
remote: true,
735+
key: 'variableKey',
736+
hiddenFromPublishing: false,
737+
variableIds: ['VariableID:2:1', 'VariableID:2:2'],
738+
},
723739
},
724-
}),
725-
)
740+
variables: {
741+
'VariableID:2:1': {
742+
id: 'VariableID:2:1',
743+
name: 'var1',
744+
key: 'variable_key',
745+
variableCollectionId: 'VariableCollectionId:1:1',
746+
resolvedType: 'STRING',
747+
valuesByMode: {
748+
'1:0': 'hello world!',
749+
},
750+
remote: true,
751+
description: '',
752+
hiddenFromPublishing: false,
753+
scopes: ['ALL_SCOPES'],
754+
codeSyntax: {},
755+
},
756+
'VariableID:2:2': {
757+
id: 'VariableID:2:2',
758+
name: 'var2',
759+
key: 'variable_key2',
760+
variableCollectionId: 'VariableCollectionId:1:1',
761+
resolvedType: 'STRING',
762+
valuesByMode: {
763+
'1:0': { type: 'VARIABLE_ALIAS', id: 'VariableID:2:1' },
764+
},
765+
remote: true,
766+
description: '',
767+
hiddenFromPublishing: false,
768+
scopes: ['ALL_SCOPES'],
769+
codeSyntax: {},
770+
},
771+
},
772+
},
773+
}
774+
775+
const tokensByFile: FlattenedTokensByFile = {
776+
'collection.mode1.json': {
777+
var1: {
778+
$type: 'string',
779+
$value: 'hello world!',
780+
$description: '',
781+
$extensions: {
782+
'com.figma': {
783+
hiddenFromPublishing: true, // modification
784+
scopes: ['ALL_SCOPES'],
785+
codeSyntax: {},
786+
},
787+
},
788+
},
789+
var2: {
790+
$type: 'string',
791+
$value: '{var1}',
792+
$description: '',
793+
$extensions: {
794+
'com.figma': {
795+
hiddenFromPublishing: false,
796+
scopes: ['ALL_SCOPES'],
797+
codeSyntax: {},
798+
},
799+
},
800+
},
801+
},
802+
}
803+
804+
expect(() => {
805+
generatePostVariablesPayload(tokensByFile, localVariablesResponse)
806+
}).toThrowError(`Cannot update remote variable "var1" in collection "collection"`)
807+
})
808+
809+
it('updates aliases to remote variables', () => {
810+
const localVariablesResponse: GetLocalVariablesResponse = {
811+
status: 200,
812+
error: false,
813+
meta: {
814+
variableCollections: {
815+
'VariableCollectionId:1:1': {
816+
id: 'VariableCollectionId:1:1',
817+
name: 'primitives',
818+
modes: [{ modeId: '1:0', name: 'mode1' }],
819+
defaultModeId: '1:0',
820+
remote: true,
821+
key: 'variableCollectionKey1',
822+
hiddenFromPublishing: false,
823+
variableIds: ['VariableID:1:2', 'VariableID:1:3'],
824+
},
825+
'VariableCollectionId:2:1': {
826+
id: 'VariableCollectionId:2:1',
827+
name: 'tokens',
828+
modes: [{ modeId: '2:0', name: 'mode1' }],
829+
defaultModeId: '2:0',
830+
remote: false,
831+
key: 'variableCollectionKey2',
832+
hiddenFromPublishing: false,
833+
variableIds: ['VariableID:2:1'],
834+
},
835+
},
836+
variables: {
837+
// 2 color variables in the primitives collection
838+
'VariableID:1:2': {
839+
id: 'VariableID:1:2',
840+
name: 'gray/100',
841+
key: 'variableKey1',
842+
variableCollectionId: 'VariableCollectionId:1:1',
843+
resolvedType: 'COLOR',
844+
valuesByMode: {
845+
'1:0': { r: 0.98, g: 0.98, b: 0.98, a: 1 },
846+
},
847+
remote: true,
848+
description: 'light gray',
849+
hiddenFromPublishing: false,
850+
scopes: ['ALL_SCOPES'],
851+
codeSyntax: {},
852+
},
853+
'VariableID:1:3': {
854+
id: 'VariableID:1:3',
855+
name: 'gray/200',
856+
key: 'variableKey2',
857+
variableCollectionId: 'VariableCollectionId:1:1',
858+
resolvedType: 'COLOR',
859+
valuesByMode: {
860+
'1:0': { r: 0.96, g: 0.96, b: 0.96, a: 1 },
861+
},
862+
remote: true,
863+
description: 'light gray',
864+
hiddenFromPublishing: false,
865+
scopes: ['ALL_SCOPES'],
866+
codeSyntax: {},
867+
},
868+
// 1 color variable in the tokens collection
869+
'VariableID:2:1': {
870+
id: 'VariableID:2:1',
871+
name: 'surface/surface-brand',
872+
key: 'variableKey3',
873+
variableCollectionId: 'VariableCollectionId:2:1',
874+
resolvedType: 'COLOR',
875+
valuesByMode: {
876+
'2:0': { type: 'VARIABLE_ALIAS', id: 'VariableID:1:2' },
877+
},
878+
remote: false,
879+
description: 'light gray',
880+
hiddenFromPublishing: false,
881+
scopes: ['ALL_SCOPES'],
882+
codeSyntax: {},
883+
},
884+
},
885+
},
886+
}
887+
888+
const tokensByFile: FlattenedTokensByFile = {
889+
'tokens.mode1.json': {
890+
// Change the alias to point to the other variable in the primitives collection
891+
'surface/surface-brand': { $type: 'color', $value: '{gray.200}' },
892+
},
893+
}
894+
895+
const result = generatePostVariablesPayload(tokensByFile, localVariablesResponse)
896+
897+
expect(result.variableCollections).toEqual([])
898+
expect(result.variableModes).toEqual([])
899+
expect(result.variables).toEqual([])
900+
expect(result.variableModeValues).toEqual([
901+
{
902+
variableId: 'VariableID:2:1',
903+
modeId: '2:0',
904+
value: { type: 'VARIABLE_ALIAS', id: 'VariableID:1:3' },
905+
},
906+
])
726907
})
727908

728909
it('throws on unsupported token types', async () => {

src/token_import.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,6 @@ export function generatePostVariablesPayload(
242242
} = {}
243243

244244
Object.values(localVariables.meta.variableCollections).forEach((collection) => {
245-
// Skip over remote collections because we can't modify them
246-
if (collection.remote) {
247-
return
248-
}
249-
250245
if (localVariableCollectionsByName[collection.name]) {
251246
throw new Error(`Duplicate variable collection in file: ${collection.name}`)
252247
}
@@ -255,11 +250,6 @@ export function generatePostVariablesPayload(
255250
})
256251

257252
Object.values(localVariables.meta.variables).forEach((variable) => {
258-
// Skip over remote variables because we can't modify them
259-
if (variable.remote) {
260-
return
261-
}
262-
263253
if (!localVariablesByCollectionAndName[variable.variableCollectionId]) {
264254
localVariablesByCollectionAndName[variable.variableCollectionId] = {}
265255
}
@@ -350,6 +340,12 @@ export function generatePostVariablesPayload(
350340
...differences,
351341
})
352342
} else if (variable && Object.keys(differences).length > 0) {
343+
if (variable.remote) {
344+
throw new Error(
345+
`Cannot update remote variable "${variable.name}" in collection "${collectionName}"`,
346+
)
347+
}
348+
353349
postVariablesPayload.variables!.push({
354350
action: 'UPDATE',
355351
id: variableId,

0 commit comments

Comments
 (0)