Skip to content

Commit

Permalink
fix(design): way to sort variable to refer existing variables
Browse files Browse the repository at this point in the history
  • Loading branch information
kpanot committed Oct 16, 2024
1 parent b512051 commit e6bdb4f
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
getSassStyleContentUpdater,
getSassTokenDefinitionRenderer,
getSassTokenValueRenderer,
getTokenSorterByName,
getTokenSorterByRef,
type SassTokenDefinitionRendererOptions,
type SassTokenValueRendererOptions,
type TokenKeyRenderer,
Expand Down Expand Up @@ -122,8 +124,23 @@ export const getStyleRendererOptions = (tokenVariableNameRenderer: TokenKeyRende
}
})(options.variableType || options.language);

/** Sorting strategy of variables based on selected language */
const tokenListTransforms = ((language) => {
switch (language) {
case 'scss':
case 'sass': {
return [getTokenSorterByName, getTokenSorterByRef];
}
case 'css':
default: {
return [getTokenSorterByName];
}
}
})(options.variableType || options.language);

/** Option to be used by the style renderer */
return {
tokenListTransforms,
writeFile: writeFileWithLogger,
styleContentUpdater,
determineFileToUpdate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { promises as fs } from 'node:fs';
import { resolve } from 'node:path';
import type { DesignTokenSpecification } from '../design-token-specification.interface';
import type { DesignTokenVariableSet } from '../parsers';
import { compareVariableByName, computeFileToUpdatePath, renderDesignTokens } from './design-token-style.renderer';
import { computeFileToUpdatePath, getTokenSorterByName, getTokenSorterByRef, renderDesignTokens } from './design-token-style.renderer';

describe('Design Token Renderer', () => {
let exampleVariable!: DesignTokenSpecification;
Expand Down Expand Up @@ -78,7 +78,7 @@ describe('Design Token Renderer', () => {
expect(writeFile).toHaveBeenCalledTimes(2);
});

describe('the comparator', () => {
describe('the transformers sorting by name', () => {
const firstVariable = '--example-color';
const lastVariable = '--example-wrong-ref';

Expand Down Expand Up @@ -113,13 +113,47 @@ describe('Design Token Renderer', () => {
readFile,
existsFile,
determineFileToUpdate,
variableSortComparator: (a, b) => -compareVariableByName(a, b)
tokenListTransforms: [(list) => (vars) => getTokenSorterByName(list)(vars).reverse()]
});

const contentToTest = result['styles.scss'];

expect(contentToTest.indexOf(firstVariable)).toBeGreaterThan(contentToTest.indexOf(lastVariable));
});

test('should execute the transform functions in given order', async () => {
const result: any = {};
const writeFile = jest.fn().mockImplementation((filename, content) => { result[filename] = content; });
const readFile = jest.fn().mockReturnValue('');
const existsFile = jest.fn().mockReturnValue(true);
const determineFileToUpdate = jest.fn().mockImplementation(computeFileToUpdatePath('.'));
const tokenListTransform = jest.fn().mockReturnValue([]);
await renderDesignTokens(designTokens, {
writeFile,
readFile,
existsFile,
determineFileToUpdate,
tokenListTransforms: [() => tokenListTransform, () => tokenListTransform]
});

expect(tokenListTransform).toHaveBeenCalledTimes(2 * 2); // twice on 2 files
expect(tokenListTransform).not.toHaveBeenNthCalledWith(1, []);
expect(tokenListTransform).toHaveBeenNthCalledWith(2, []);
expect(tokenListTransform).not.toHaveBeenNthCalledWith(3, []);
expect(tokenListTransform).toHaveBeenNthCalledWith(4, []);
});
});
});

describe('getTokenSorterByRef', () => {
it('should sort properly variables with multiple refs', () => {
const list = Array.from(designTokens.values());
const sortedTokens = getTokenSorterByRef(designTokens)(list);

expect(list.findIndex(({ tokenReferenceName }) => tokenReferenceName === 'example.post-ref'))
.toBeLessThan(list.findIndex(({ tokenReferenceName }) => tokenReferenceName === 'example.var1'));
expect(sortedTokens.findIndex(({ tokenReferenceName }) => tokenReferenceName === 'example.post-ref'))
.toBeGreaterThan(sortedTokens.findIndex(({ tokenReferenceName }) => tokenReferenceName === 'example.var1'));
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getCssTokenDefinitionRenderer } from './css/design-token-definition.ren
import { getCssStyleContentUpdater } from './css/design-token-updater.renderers';
import { existsSync, promises as fs } from 'node:fs';
import { isAbsolute, resolve } from 'node:path';
import type { DesignTokenRendererOptions } from './design-token.renderer.interface';
import type { DesignTokenRendererOptions, TokenListTransform } from './design-token.renderer.interface';

/**
* Retrieve the function that determines which file to update for a given token
Expand All @@ -22,9 +22,62 @@ export const computeFileToUpdatePath = (root = process.cwd(), defaultFile = 'sty
* Compare Token variable by name
* @param a first token variable
* @param b second token variable
* @deprecated use {@link getTokenSorterByName} instead. Will be removed in v13.
*/
export const compareVariableByName = (a: DesignTokenVariableStructure, b: DesignTokenVariableStructure): number => a.getKey().localeCompare(b.getKey());

/**
* Compare Token variable by name
* @param _variableSet Complete set of the parsed Design Token
*/
export const getTokenSorterByName: TokenListTransform = (_variableSet) => (tokens) => tokens.sort((a, b) => a.getKey().localeCompare(b.getKey()));


/**
* Compare Token variable by reference to write theme in reference order
* @param variableSet Complete set of the parsed Design Token
*/
export const getTokenSorterByRef: TokenListTransform = (variableSet) => {
/**
* Get the maximum number of reference levels for a Design Token, following its references
* @param token Design Token
* @param level Value of the current level
* @param mem Memory of the visited node
*/
const getReferenceLevel = (token: DesignTokenVariableStructure, level = 0, mem: DesignTokenVariableStructure[] = []): number => {
const children = token.getReferencesNode(variableSet);
if (children.length === 0 || mem.includes(token)) {
return level;
} else {
level++;
mem = [...mem, token];
return Math.max(...children.map((child) => getReferenceLevel(child, level, mem)));
}
};

return (tokens) => {
const limit = Math.max(...tokens.map((t) => getReferenceLevel(t)));
let sortToken = [...tokens];
for (let i = 0; i < limit + 1; i++) {
let hasChanged = false;
sortToken = sortToken.reduce((acc, token, idx) => {
const firstRef = sortToken.findIndex((t) => t.getReferences(variableSet).includes(token.tokenReferenceName));
if (firstRef > -1 && firstRef < idx) {
acc.splice(idx, 1);
acc.splice(firstRef, 0, token);
hasChanged = true;
}
return acc;
}, sortToken);
if (!hasChanged) {
break;
}
}

return sortToken;
};
};

/**
* Process the parsed Design Token variables and render them according to the given options and renderers
* @param variableSet Complete list of the parsed Design Token
Expand All @@ -47,17 +100,28 @@ export const renderDesignTokens = async (variableSet: DesignTokenVariableSet, op
const determineFileToUpdate = options?.determineFileToUpdate || computeFileToUpdatePath();
const tokenDefinitionRenderer = options?.tokenDefinitionRenderer || getCssTokenDefinitionRenderer();
const styleContentUpdater = options?.styleContentUpdater || getCssStyleContentUpdater();
const updates = Array.from(variableSet.values())
.sort(options?.variableSortComparator || compareVariableByName)
const tokenPerFile = Array.from(variableSet.values())
.reduce((acc, designToken) => {
const filePath = determineFileToUpdate(designToken);
const variable = tokenDefinitionRenderer(designToken, variableSet);
acc[filePath] ||= [];
if (variable) {
acc[filePath].push(variable);
}
acc[filePath].push(designToken);
return acc;
}, {} as Record<string, string[]>);
}, {} as Record<string, DesignTokenVariableStructure[]>);

const updates = Object.fromEntries(
Object.entries(tokenPerFile)
.map(([file, designTokens]) => {
designTokens = options?.variableSortComparator ? designTokens.sort(options.variableSortComparator) : designTokens;
designTokens = (options?.tokenListTransforms?.map((transform) => transform(variableSet)) || [getTokenSorterByName(variableSet)])
.reduce((acc, transform) => transform(acc), designTokens);
return [
file,
designTokens
.map((designToken) => tokenDefinitionRenderer(designToken, variableSet))
.filter((variable): variable is string => !!variable)
];
})
);

await Promise.all(
Object.entries(updates).map(async ([file, vars]) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DesignTokenVariableStructure } from '../parsers';
import type { DesignTokenVariableSet, DesignTokenVariableStructure } from '../parsers';
import type { Logger } from '@o3r/core';

/**
Expand All @@ -17,18 +17,29 @@ export type DesignContentFileUpdater = (variables: string[], file: string, style
*/
export type TokenDefinitionRenderer = (tokenStructure: DesignTokenVariableStructure, variableSet: Map<string, DesignTokenVariableStructure>) => string | undefined;

/**
* Function defining the way the variable should be sorted before being generated
*/
export type TokenListTransform = (variableSet: DesignTokenVariableSet) => (tokens: DesignTokenVariableStructure[]) => DesignTokenVariableStructure[];

/**
* Options of the Design Token Renderer value
*/
export interface DesignTokenRendererOptions {
/**
* Comparator to sort variable before rendering
* @default {@see compareVariableByName}
* @param a first Design Token
* @param b second Design Token
* @deprecated Use {@link tokenListTransform} instead. Will be removed in v13
*/
variableSortComparator?: (a: DesignTokenVariableStructure, b: DesignTokenVariableStructure) => number;

/**
* List of tokens transform function. The transformation will be apply per file.
* Note: the order of the outputted array will be kept when generating the code
*/
tokenListTransforms?: TokenListTransform[];

/** Custom Style Content updated function */
styleContentUpdater?: DesignContentFileUpdater;

Expand Down
3 changes: 3 additions & 0 deletions packages/@o3r/design/testing/mocks/design-token-theme.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"$schema": "../../schemas/design-token.schema.json",
"example": {
"post-ref": {
"$value": "{example.var1}"
},
"var1": {
"$type": "color",
"$value": "#000"
Expand Down

0 comments on commit e6bdb4f

Please sign in to comment.