Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix aliased variables variables with tests #300

Merged
Merged
2 changes: 1 addition & 1 deletion dist/plugin.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/transformer/standardTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { StandardTokenInterface, StandardTokenTypes, StandardTokenDataInterface,
import roundWithDecimals from '../utilities/roundWithDecimals'
import { tokenExtensions } from './tokenExtensions'
import config from '@config/config'
import { changeNotation } from '@src/utilities/changeNotation'
import { changeNotation } from '../utilities/changeNotation'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had to update syntax so tests would recognize origin of mapping


const lineHeightToDimension = (values): number => {
if (values.lineHeight.unit === 'pixel') {
Expand Down
128 changes: 59 additions & 69 deletions src/utilities/getVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,16 @@ import { tokenCategoryType } from '@typings/tokenCategory'
import { tokenExportKeyType } from '@typings/tokenExportKey'
import { PropertyType } from '@typings/valueTypes'
import { roundRgba } from './convertColor'
import { changeNotation } from './changeNotation'
import { getVariableTypeByValue } from './getVariableTypeByValue'
import roundWithDecimals from './roundWithDecimals'
import handleVariableAlias from './handleVariableAlias'
import processAliasModes from './processAliasModes'
import { Settings } from '@typings/settings'

const extractVariable = (variable, value) => {
const extractVariable = (variable, value, mode) => {
let category: tokenCategoryType = 'color'
let values = {}
if (value.type === 'VARIABLE_ALIAS') {
const resolvedAlias = figma.variables.getVariableById(value.id)
const collection = figma.variables.getVariableCollectionById(resolvedAlias.variableCollectionId)
return {
name: variable.name,
description: variable.description || undefined,
exportKey: tokenTypes.variables.key as tokenExportKeyType,
category: getVariableTypeByValue(Object.values(resolvedAlias.valuesByMode)[0]),
values: `{${collection.name.toLowerCase()}.${changeNotation(resolvedAlias.name, '/', '.')}}`,

// this is being stored so we can properly update the design tokens later to account for all
// modes when using aliases
aliasCollectionName: collection.name.toLowerCase(),
aliasModes: collection.modes
}
return handleVariableAlias(variable, value, mode)
}
switch (variable.resolvedType) {
case 'COLOR':
Expand Down Expand Up @@ -61,62 +48,65 @@ const extractVariable = (variable, value) => {
}
}

const processAliasModes = (variables) => {
return variables.reduce((collector, variable) => {
// nothing needs to be done to variables that have no alias modes, or only one mode
if (!variable.aliasModes || variable.aliasModes.length < 2) {
collector.push(variable)

return collector
}

const { aliasModes, aliasCollectionName } = variable

// this was only added for this function to process that data so before we return the variables, we can remove it
delete variable.aliasModes
delete variable.aliasCollectionName

for (const aliasMode of aliasModes) {
const modeBasedVariable = { ...variable }
modeBasedVariable.values = modeBasedVariable.values.replace(new RegExp(`({${aliasCollectionName}.)`, "i"), `{${aliasCollectionName}.${aliasMode.name}.`)

collector.push(modeBasedVariable)
}

return collector
}, [])
}

export const getVariables = (figma: PluginAPI, settings: Settings) => {
const excludedCollectionIds = figma.variables.getLocalVariableCollections().filter(collection => !['.', '_', ...settings.exclusionPrefix.split(',')].includes(collection.name.charAt(0))).map(collection => collection.id);
const excludedCollectionIds = figma.variables
.getLocalVariableCollections()
.filter(
(collection) =>
!['.', '_', ...settings.exclusionPrefix.split(',')].includes(
collection.name.charAt(0)
)
)
.map((collection) => collection.id)
// get collections
const collections = Object.fromEntries(figma.variables.getLocalVariableCollections().map((collection) => [collection.id, collection]))
const collections = Object.fromEntries(
figma.variables
.getLocalVariableCollections()
.map((collection) => [collection.id, collection])
)
// get variables
const variables = figma.variables.getLocalVariables().filter(variable => excludedCollectionIds.includes(variable.variableCollectionId)).map((variable) => {
// get collection name and modes
const { variableCollectionId } = variable
const { name: collection, modes } = collections[variableCollectionId]
// return each mode value as a separate variable
return Object.entries(variable.valuesByMode).map(([id, value]) => {
// Only add mode if there's more than one
let addMode = settings.modeReference && modes.length > 1
return {
...extractVariable(variable, value),
// name is contstructed from collection, mode and variable name
const variables = figma.variables
.getLocalVariables()
.filter((variable) =>
excludedCollectionIds.includes(variable.variableCollectionId)
)
.map((variable) => {
// get collection name and modes
const { variableCollectionId } = variable
const { name: collection, modes } = collections[variableCollectionId]
// return each mode value as a separate variable
return Object.entries(variable.valuesByMode).map(([id, value]) => {
// Only add mode if there's more than one
const addMode = settings.modeReference && modes.length > 1
return {
...extractVariable(
variable,
value,
modes.find(({ modeId }) => modeId === id)
),
// name is contstructed from collection, mode and variable name

name: addMode ? `${collection}/${modes.find(({ modeId }) => modeId === id).name}/${variable.name}` : `${collection}/${variable.name}`,
// add mnetadata to extensions
extensions: {
[config.key.extensionPluginData]: {
mode: settings.modeReference ? modes.find(({ modeId }) => modeId === id).name : undefined,
collection: collection,
scopes: variable.scopes,
[config.key.extensionVariableStyleId]: variable.id,
exportKey: tokenTypes.variables.key as tokenExportKeyType
name: addMode
? `${collection}/${
modes.find(({ modeId }) => modeId === id).name
}/${variable.name}`
: `${collection}/${variable.name}`,
// add mnetadata to extensions
extensions: {
[config.key.extensionPluginData]: {
mode: settings.modeReference
? modes.find(({ modeId }) => modeId === id).name
: undefined,
collection: collection,
scopes: variable.scopes,
[config.key.extensionVariableStyleId]: variable.id,
exportKey: tokenTypes.variables.key as tokenExportKeyType
}
}
}
}
})
})
})
return settings.modeReference ? processAliasModes(variables.flat()) : variables.flat();
}
return settings.modeReference
? processAliasModes(variables.flat())
: variables.flat()
}
31 changes: 31 additions & 0 deletions src/utilities/handleVariableAlias.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { tokenExportKeyType } from '@typings/tokenExportKey'
import { tokenTypes } from '@config/tokenTypes'

import { getVariableTypeByValue } from '../../src/utilities/getVariableTypeByValue'
import { changeNotation } from '../../src/utilities/changeNotation'

function handleVariableAlias (variable, value, mode) {
const resolvedAlias = figma.variables.getVariableById(value.id)
const collection = figma.variables.getVariableCollectionById(
resolvedAlias.variableCollectionId
)
return {
description: variable.description || undefined,
exportKey: tokenTypes.variables.key as tokenExportKeyType,
category: getVariableTypeByValue(
Object.values(resolvedAlias.valuesByMode)[0]
),
values: `{${collection.name.toLowerCase()}.${changeNotation(
resolvedAlias.name,
'/',
'.'
)}}`,

// this is being stored so we can properly update the design tokens later to account for all
// modes when using aliases
aliasCollectionName: collection.name.toLowerCase(),
aliasMode: mode
}
}

export default handleVariableAlias
29 changes: 29 additions & 0 deletions src/utilities/processAliasModes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const processAliasModes = (variables) => {
return variables.reduce((collector, variable) => {
// only one mode will be passed in if any
if (!variable.aliasMode) {
collector.push(variable)

return collector
}

// alias mode singular because only one is shown
const { aliasMode, aliasCollectionName } = variable

// this was only added for this function to process that data so before we return the variables, we can remove it
delete variable.aliasMode
delete variable.aliasCollectionName

collector.push({
...variable,
values: variable.values.replace(
`{${aliasCollectionName}.`,
`{${aliasCollectionName}.${aliasMode.name}.`
)
})

return collector
}, [])
}

export default processAliasModes
70 changes: 70 additions & 0 deletions tests/unit/handleVariableAlias.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import handleVariableAlias from "../../src/utilities/handleVariableAlias";

import { tokenExportKeyType } from "@typings/tokenExportKey";
import { tokenTypes } from "@config/tokenTypes";

import { getVariableTypeByValue } from "../../src/utilities/getVariableTypeByValue";
import { changeNotation } from "../../src/utilities/changeNotation";

jest.mock("../../src/utilities/getVariableTypeByValue", () => ({
getVariableTypeByValue: jest.fn(),
}));

jest.mock("../../src/utilities/changeNotation", () => ({
changeNotation: jest.fn(),
}));

describe("handleVariableAlias", () => {
beforeEach(() => {
jest.clearAllMocks();
});

beforeAll(() => {
// @ts-ignore
global.figma = {
variables: {
getVariableById: jest.fn(),
getVariableCollectionById: jest.fn(),
},
};
});

it("should return the correct object", () => {
const variable = { description: "test description" };
const value = { id: "test id" };
const resolvedAlias = {
variableCollectionId: "test collection id",
name: "test name",
valuesByMode: { mode1: "value1" },
};
const collection = {
name: "test collection name",
modes: "test modes",
};

// @ts-ignore
global.figma.variables.getVariableById.mockReturnValue(resolvedAlias);

// @ts-ignore
getVariableTypeByValue.mockImplementation(() => "test category");

// @ts-ignore
changeNotation.mockImplementation(() => "test notation");

// @ts-ignore
global.figma.variables.getVariableCollectionById.mockReturnValue(
collection
);

const result = handleVariableAlias(variable, value, 'passedInMode');

expect(result).toEqual({
description: "test description",
exportKey: tokenTypes.variables.key as tokenExportKeyType,
category: "test category",
values: `{test collection name.test notation}`,
aliasCollectionName: "test collection name",
aliasMode: "passedInMode",
});
});
});
44 changes: 44 additions & 0 deletions tests/unit/processAliasModes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import processAliasModes from "../../src/utilities/processAliasModes";

describe("processAliasModes", () => {
it("should return the same variables if they have no alias modes", () => {
const variables = [
{ values: "{color.black}" },
];
const result = processAliasModes(variables);
expect(result).toEqual(variables);
});

it("should remove aliasModes and aliasCollectionName properties from the variables", () => {
const variables = [
{
values: "{collection.}",
aliasMode: "mode1",
aliasCollectionName: "collection",
},
];
const result = processAliasModes(variables);
result.forEach((variable) => {
expect(variable).not.toHaveProperty("aliasMode");
expect(variable).not.toHaveProperty("aliasCollectionName");
});
});

it("should match aliasCollectionName case-insensitively and return the alias collection name", () => {
const variables = [
{
values: "{CollEctIon.}",
aliasMode: "mode1",
aliasCollectionName: "collection",
},
];
const result = processAliasModes(variables);
expect(result).toMatchInlineSnapshot(`
Array [
Object {
"values": "{CollEctIon.}",
},
]
`)
});
});
Loading