Skip to content

Commit 2861fa9

Browse files
JackHowaAnge PICARD
and
Ange PICARD
authored
Fix aliased variables variables with tests (#300)
* fix aliased variables being emmited twice * Run build * Update manifest.json with new design token name and menu formatting * Try to update id to publish new version * Update manifest.json with new name and ID * style: Fix lint * Add handleVariableAlias utility function and test updated * Add processAliasModes utility function and tests * Fix import path in standardTransformer.ts * Remove rename * Re-run build * Fix spacing --------- Co-authored-by: Ange PICARD <[email protected]>
1 parent 82f3bb2 commit 2861fa9

File tree

7 files changed

+235
-71
lines changed

7 files changed

+235
-71
lines changed

dist/plugin.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/transformer/standardTransformer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { StandardTokenInterface, StandardTokenTypes, StandardTokenDataInterface,
44
import roundWithDecimals from '../utilities/roundWithDecimals'
55
import { tokenExtensions } from './tokenExtensions'
66
import config from '@config/config'
7-
import { changeNotation } from '@src/utilities/changeNotation'
7+
import { changeNotation } from '../utilities/changeNotation'
88

99
const lineHeightToDimension = (values): number => {
1010
if (values.lineHeight.unit === 'pixel') {

src/utilities/getVariables.ts

Lines changed: 59 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,16 @@ import { tokenCategoryType } from '@typings/tokenCategory'
44
import { tokenExportKeyType } from '@typings/tokenExportKey'
55
import { PropertyType } from '@typings/valueTypes'
66
import { roundRgba } from './convertColor'
7-
import { changeNotation } from './changeNotation'
8-
import { getVariableTypeByValue } from './getVariableTypeByValue'
97
import roundWithDecimals from './roundWithDecimals'
8+
import handleVariableAlias from './handleVariableAlias'
9+
import processAliasModes from './processAliasModes'
1010
import { Settings } from '@typings/settings'
1111

12-
const extractVariable = (variable, value) => {
12+
const extractVariable = (variable, value, mode) => {
1313
let category: tokenCategoryType = 'color'
1414
let values = {}
1515
if (value.type === 'VARIABLE_ALIAS') {
16-
const resolvedAlias = figma.variables.getVariableById(value.id)
17-
const collection = figma.variables.getVariableCollectionById(resolvedAlias.variableCollectionId)
18-
return {
19-
name: variable.name,
20-
description: variable.description || undefined,
21-
exportKey: tokenTypes.variables.key as tokenExportKeyType,
22-
category: getVariableTypeByValue(Object.values(resolvedAlias.valuesByMode)[0]),
23-
values: `{${collection.name.toLowerCase()}.${changeNotation(resolvedAlias.name, '/', '.')}}`,
24-
25-
// this is being stored so we can properly update the design tokens later to account for all
26-
// modes when using aliases
27-
aliasCollectionName: collection.name.toLowerCase(),
28-
aliasModes: collection.modes
29-
}
16+
return handleVariableAlias(variable, value, mode)
3017
}
3118
switch (variable.resolvedType) {
3219
case 'COLOR':
@@ -61,62 +48,65 @@ const extractVariable = (variable, value) => {
6148
}
6249
}
6350

64-
const processAliasModes = (variables) => {
65-
return variables.reduce((collector, variable) => {
66-
// nothing needs to be done to variables that have no alias modes, or only one mode
67-
if (!variable.aliasModes || variable.aliasModes.length < 2) {
68-
collector.push(variable)
69-
70-
return collector
71-
}
72-
73-
const { aliasModes, aliasCollectionName } = variable
74-
75-
// this was only added for this function to process that data so before we return the variables, we can remove it
76-
delete variable.aliasModes
77-
delete variable.aliasCollectionName
78-
79-
for (const aliasMode of aliasModes) {
80-
const modeBasedVariable = { ...variable }
81-
modeBasedVariable.values = modeBasedVariable.values.replace(new RegExp(`({${aliasCollectionName}.)`, "i"), `{${aliasCollectionName}.${aliasMode.name}.`)
82-
83-
collector.push(modeBasedVariable)
84-
}
85-
86-
return collector
87-
}, [])
88-
}
89-
9051
export const getVariables = (figma: PluginAPI, settings: Settings) => {
91-
const excludedCollectionIds = figma.variables.getLocalVariableCollections().filter(collection => !['.', '_', ...settings.exclusionPrefix.split(',')].includes(collection.name.charAt(0))).map(collection => collection.id);
52+
const excludedCollectionIds = figma.variables
53+
.getLocalVariableCollections()
54+
.filter(
55+
(collection) =>
56+
!['.', '_', ...settings.exclusionPrefix.split(',')].includes(
57+
collection.name.charAt(0)
58+
)
59+
)
60+
.map((collection) => collection.id)
9261
// get collections
93-
const collections = Object.fromEntries(figma.variables.getLocalVariableCollections().map((collection) => [collection.id, collection]))
62+
const collections = Object.fromEntries(
63+
figma.variables
64+
.getLocalVariableCollections()
65+
.map((collection) => [collection.id, collection])
66+
)
9467
// get variables
95-
const variables = figma.variables.getLocalVariables().filter(variable => excludedCollectionIds.includes(variable.variableCollectionId)).map((variable) => {
96-
// get collection name and modes
97-
const { variableCollectionId } = variable
98-
const { name: collection, modes } = collections[variableCollectionId]
99-
// return each mode value as a separate variable
100-
return Object.entries(variable.valuesByMode).map(([id, value]) => {
101-
// Only add mode if there's more than one
102-
let addMode = settings.modeReference && modes.length > 1
103-
return {
104-
...extractVariable(variable, value),
105-
// name is contstructed from collection, mode and variable name
68+
const variables = figma.variables
69+
.getLocalVariables()
70+
.filter((variable) =>
71+
excludedCollectionIds.includes(variable.variableCollectionId)
72+
)
73+
.map((variable) => {
74+
// get collection name and modes
75+
const { variableCollectionId } = variable
76+
const { name: collection, modes } = collections[variableCollectionId]
77+
// return each mode value as a separate variable
78+
return Object.entries(variable.valuesByMode).map(([id, value]) => {
79+
// Only add mode if there's more than one
80+
const addMode = settings.modeReference && modes.length > 1
81+
return {
82+
...extractVariable(
83+
variable,
84+
value,
85+
modes.find(({ modeId }) => modeId === id)
86+
),
87+
// name is contstructed from collection, mode and variable name
10688

107-
name: addMode ? `${collection}/${modes.find(({ modeId }) => modeId === id).name}/${variable.name}` : `${collection}/${variable.name}`,
108-
// add mnetadata to extensions
109-
extensions: {
110-
[config.key.extensionPluginData]: {
111-
mode: settings.modeReference ? modes.find(({ modeId }) => modeId === id).name : undefined,
112-
collection: collection,
113-
scopes: variable.scopes,
114-
[config.key.extensionVariableStyleId]: variable.id,
115-
exportKey: tokenTypes.variables.key as tokenExportKeyType
89+
name: addMode
90+
? `${collection}/${
91+
modes.find(({ modeId }) => modeId === id).name
92+
}/${variable.name}`
93+
: `${collection}/${variable.name}`,
94+
// add mnetadata to extensions
95+
extensions: {
96+
[config.key.extensionPluginData]: {
97+
mode: settings.modeReference
98+
? modes.find(({ modeId }) => modeId === id).name
99+
: undefined,
100+
collection: collection,
101+
scopes: variable.scopes,
102+
[config.key.extensionVariableStyleId]: variable.id,
103+
exportKey: tokenTypes.variables.key as tokenExportKeyType
104+
}
116105
}
117106
}
118-
}
107+
})
119108
})
120-
})
121-
return settings.modeReference ? processAliasModes(variables.flat()) : variables.flat();
122-
}
109+
return settings.modeReference
110+
? processAliasModes(variables.flat())
111+
: variables.flat()
112+
}

src/utilities/handleVariableAlias.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { tokenExportKeyType } from '@typings/tokenExportKey'
2+
import { tokenTypes } from '@config/tokenTypes'
3+
4+
import { getVariableTypeByValue } from '../../src/utilities/getVariableTypeByValue'
5+
import { changeNotation } from '../../src/utilities/changeNotation'
6+
7+
function handleVariableAlias (variable, value, mode) {
8+
const resolvedAlias = figma.variables.getVariableById(value.id)
9+
const collection = figma.variables.getVariableCollectionById(
10+
resolvedAlias.variableCollectionId
11+
)
12+
return {
13+
description: variable.description || undefined,
14+
exportKey: tokenTypes.variables.key as tokenExportKeyType,
15+
category: getVariableTypeByValue(
16+
Object.values(resolvedAlias.valuesByMode)[0]
17+
),
18+
values: `{${collection.name.toLowerCase()}.${changeNotation(
19+
resolvedAlias.name,
20+
'/',
21+
'.'
22+
)}}`,
23+
24+
// this is being stored so we can properly update the design tokens later to account for all
25+
// modes when using aliases
26+
aliasCollectionName: collection.name.toLowerCase(),
27+
aliasMode: mode
28+
}
29+
}
30+
31+
export default handleVariableAlias

src/utilities/processAliasModes.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const processAliasModes = (variables) => {
2+
return variables.reduce((collector, variable) => {
3+
// only one mode will be passed in if any
4+
if (!variable.aliasMode) {
5+
collector.push(variable)
6+
7+
return collector
8+
}
9+
10+
// alias mode singular because only one is shown
11+
const { aliasMode, aliasCollectionName } = variable
12+
13+
// this was only added for this function to process that data so before we return the variables, we can remove it
14+
delete variable.aliasMode
15+
delete variable.aliasCollectionName
16+
17+
collector.push({
18+
...variable,
19+
values: variable.values.replace(
20+
`{${aliasCollectionName}.`,
21+
`{${aliasCollectionName}.${aliasMode.name}.`
22+
)
23+
})
24+
25+
return collector
26+
}, [])
27+
}
28+
29+
export default processAliasModes
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import handleVariableAlias from "../../src/utilities/handleVariableAlias";
2+
3+
import { tokenExportKeyType } from "@typings/tokenExportKey";
4+
import { tokenTypes } from "@config/tokenTypes";
5+
6+
import { getVariableTypeByValue } from "../../src/utilities/getVariableTypeByValue";
7+
import { changeNotation } from "../../src/utilities/changeNotation";
8+
9+
jest.mock("../../src/utilities/getVariableTypeByValue", () => ({
10+
getVariableTypeByValue: jest.fn(),
11+
}));
12+
13+
jest.mock("../../src/utilities/changeNotation", () => ({
14+
changeNotation: jest.fn(),
15+
}));
16+
17+
describe("handleVariableAlias", () => {
18+
beforeEach(() => {
19+
jest.clearAllMocks();
20+
});
21+
22+
beforeAll(() => {
23+
// @ts-ignore
24+
global.figma = {
25+
variables: {
26+
getVariableById: jest.fn(),
27+
getVariableCollectionById: jest.fn(),
28+
},
29+
};
30+
});
31+
32+
it("should return the correct object", () => {
33+
const variable = { description: "test description" };
34+
const value = { id: "test id" };
35+
const resolvedAlias = {
36+
variableCollectionId: "test collection id",
37+
name: "test name",
38+
valuesByMode: { mode1: "value1" },
39+
};
40+
const collection = {
41+
name: "test collection name",
42+
modes: "test modes",
43+
};
44+
45+
// @ts-ignore
46+
global.figma.variables.getVariableById.mockReturnValue(resolvedAlias);
47+
48+
// @ts-ignore
49+
getVariableTypeByValue.mockImplementation(() => "test category");
50+
51+
// @ts-ignore
52+
changeNotation.mockImplementation(() => "test notation");
53+
54+
// @ts-ignore
55+
global.figma.variables.getVariableCollectionById.mockReturnValue(
56+
collection
57+
);
58+
59+
const result = handleVariableAlias(variable, value, 'passedInMode');
60+
61+
expect(result).toEqual({
62+
description: "test description",
63+
exportKey: tokenTypes.variables.key as tokenExportKeyType,
64+
category: "test category",
65+
values: `{test collection name.test notation}`,
66+
aliasCollectionName: "test collection name",
67+
aliasMode: "passedInMode",
68+
});
69+
});
70+
});

tests/unit/processAliasModes.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import processAliasModes from "../../src/utilities/processAliasModes";
2+
3+
describe("processAliasModes", () => {
4+
it("should return the same variables if they have no alias modes", () => {
5+
const variables = [
6+
{ values: "{color.black}" },
7+
];
8+
const result = processAliasModes(variables);
9+
expect(result).toEqual(variables);
10+
});
11+
12+
it("should remove aliasModes and aliasCollectionName properties from the variables", () => {
13+
const variables = [
14+
{
15+
values: "{collection.}",
16+
aliasMode: "mode1",
17+
aliasCollectionName: "collection",
18+
},
19+
];
20+
const result = processAliasModes(variables);
21+
result.forEach((variable) => {
22+
expect(variable).not.toHaveProperty("aliasMode");
23+
expect(variable).not.toHaveProperty("aliasCollectionName");
24+
});
25+
});
26+
27+
it("should match aliasCollectionName case-insensitively and return the alias collection name", () => {
28+
const variables = [
29+
{
30+
values: "{CollEctIon.}",
31+
aliasMode: "mode1",
32+
aliasCollectionName: "collection",
33+
},
34+
];
35+
const result = processAliasModes(variables);
36+
expect(result).toMatchInlineSnapshot(`
37+
Array [
38+
Object {
39+
"values": "{CollEctIon.}",
40+
},
41+
]
42+
`)
43+
});
44+
});

0 commit comments

Comments
 (0)