Skip to content

Commit

Permalink
Merge pull request #15 from qld-gov-au/feature/tokens2.0
Browse files Browse the repository at this point in the history
Design Tokens 2.0
  • Loading branch information
duttonw authored Oct 25, 2024
2 parents 92952ba + c6641e7 commit 2eaa437
Show file tree
Hide file tree
Showing 171 changed files with 42,952 additions and 1,781 deletions.
237 changes: 237 additions & 0 deletions .build/build-tokens.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import StyleDictionary from "style-dictionary";
import { getReferences, usesReferences } from "style-dictionary/utils";
import {
register,
permutateThemes,
} from "@tokens-studio/sd-transforms";
import { promises } from "node:fs";
import { primitiveFilter } from "./sd-filters.mjs";
import {
generateSemanticFiles,
generateComponentFiles,
} from "./sd-file-generators.mjs";
import { copyOriginalFile, emptyDir } from "./dir-and-files.mjs";

register(StyleDictionary);

// list of components that we have tokens for, assume the tokenset path for it is tokens/${comp}.tokens.json
const components = ["button", "card"];

const directory = "src";
const originalSrcIndex = ".build/original/index.ts"
const finalDestIndex = "src/index.ts"
const originalSrcGlobal = ".build/original/global.d.ts"
const finalDestGlobal = "src/global.d.ts"

async function beforeRun() {
// clear
emptyDir(directory);
}

async function afterRun() {
// copy index.ts for dist
copyOriginalFile(originalSrcIndex, finalDestIndex);
copyOriginalFile(originalSrcGlobal, finalDestGlobal);
}

async function run() {
const $themes = JSON.parse(await promises.readFile("tokens/$themes.tokens.json"));
const themes = permutateThemes($themes);
// collect all tokensets for all themes and dedupe
const tokensets = [
...new Set(
Object.values(themes).reduce((acc, sets) => [...acc, ...sets], [])
),
];
// figure out which tokensets are theme-specific
// this is determined by checking if a certain tokenset is used for EVERY theme dimension variant
// if it is, then it is not theme-specific
const themeableSets = tokensets.filter((set) => {
return !Object.values(themes).every((sets) => sets.includes(set));
});

const configs = Object.entries(themes).map(([theme, sets]) => ({
source: sets.map((tokenset) => `tokens/${tokenset}.tokens.json`),
// these are the defaults
log: {
warnings: 'error', // 'warn' | 'error' | 'disabled'
verbosity: 'default', // 'default' | 'silent' | 'verbose'
errors: {
brokenReferences: 'throw', // 'throw' | 'console'
},
},
platforms: {

android: {
transformGroup: 'tokens-studio',
transforms: [
'ts/descriptionToComment',
'ts/size/px',
'ts/opacity',
'ts/size/lineheight',
'ts/resolveMath',
'ts/color/modifiers',
'ts/typography/compose/shorthand',
'ts/typography/fontWeight',
'ts/size/css/letterspacing',
'ts/color/css/hexrgba',
'ts/color/modifiers',
'attribute/themeable'
],
expand: true,
files: [
// primitive tokens, e.g. for application developer
{
destination: "src/android/styles/primitive.tokens.xml",
format: "android/resources",
filter: primitiveFilter,
},
// semantic tokens, e.g. for application developer
...generateSemanticFiles(components, theme, 'android', 'xml'),
// component tokens, e.g. for design system developer
...generateComponentFiles(components, theme, 'android', 'xml'),
],
},
js: {
transformGroup: 'tokens-studio',
transforms: [
'ts/descriptionToComment',
'ts/size/px',
'ts/opacity',
'ts/size/lineheight',
'ts/typography/fontWeight',
'ts/resolveMath',
'ts/size/css/letterspacing',
'ts/color/css/hexrgba',
'ts/color/modifiers',
'attribute/themeable'
],
expand: true,
files: [
// primitive tokens, e.g. for application developer
{
destination: "src/js/styles/primitive.tokens.js",
format: "javascript/es6",
filter: primitiveFilter,
},
// semantic tokens, e.g. for application developer
...generateSemanticFiles(components, theme, 'js', 'js'),
// component tokens, e.g. for design system developer
...generateComponentFiles(components, theme, 'js', 'js'),
],
},
scss: {
transformGroup: 'tokens-studio',
transforms: [
'ts/descriptionToComment',
'ts/size/px',
'ts/opacity',
'ts/size/lineheight',
'ts/typography/fontWeight',
'ts/resolveMath',
'ts/size/css/letterspacing',
'ts/color/css/hexrgba',
'ts/color/modifiers',
'attribute/themeable'
],
expand: true,
files: [
// primitive tokens, e.g. for application developer
{
destination: "src/scss/styles/primitive.tokens.scss",
format: "scss/variables",
filter: primitiveFilter,
},
// semantic tokens, e.g. for application developer
...generateSemanticFiles(components, theme, 'scss', 'scss'),
// component tokens, e.g. for design system developer
...generateComponentFiles(components, theme, 'scss', 'scss'),
],
},

css: {
transformGroup: "tokens-studio",
// transforms: ["attribute/themeable", "name/kebab"],
transforms: [
'ts/descriptionToComment',
'ts/size/px',
'ts/opacity',
'ts/size/lineheight',
'ts/typography/fontWeight',
'ts/resolveMath',
'ts/size/css/letterspacing',
'ts/color/css/hexrgba',
'ts/color/modifiers',
'attribute/themeable',
'name/kebab',
],
expand: true,
files: [
// primitive tokens, e.g. for application developer
{
destination: "src/css/styles/primitive.tokens.css",
format: "css/variables",
filter: primitiveFilter,
},
// semantic tokens, e.g. for application developer
...generateSemanticFiles(components, theme, 'css', 'css'),
// component tokens, e.g. for design system developer
...generateComponentFiles(components, theme, 'css', 'css'),
],
},
},
}));

for (const cfg of configs) {
const sd = new StyleDictionary(cfg);

/**
* This transform checks for each token whether that token's value could change
* due to Tokens Studio theming.
* Any tokenset from Tokens Studio marked as "enabled" in the $themes.json is considered
* a set in which any token could change if the theme changes.
* Any token that is inside such a set or is a reference with a token in that reference chain
* that is inside such a set, is considered "themeable",
* which means it could change by theme switching.
*
* This metadata is applied to the token so we can use it as a way of filtering outputs
* later in the "format" stage.
*/
sd.registerTransform({
name: "attribute/themeable",
type: "attribute",
transform: (token) => {
function isPartOfEnabledSet(token) {
const set = token.filePath
.replace(/^tokens\//g, "")
.replace(/.tokens.json$/g, "");
return themeableSets.includes(set);
}

// Set token to themeable if it's part of an enabled set
if (isPartOfEnabledSet(token)) {
return {
themeable: true,
};
}

// Set token to themeable if it's using a reference and inside the reference chain
// any one of them is from a themeable set
if (usesReferences(token.original.value)) {
const refs = getReferences(token.original.value, sd.tokens);
if (refs.some((ref) => isPartOfEnabledSet(ref))) {
return {
themeable: true,
};
}
}
},
});

await sd.cleanAllPlatforms();
await sd.buildAllPlatforms();
}
}
beforeRun();
run();
afterRun();
44 changes: 44 additions & 0 deletions .build/dir-and-files.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import fs from 'fs';
import path from 'path';
import { copyFile } from 'copy-file';

export const copyOriginalFile = async (originalSrc, finalDest) => {
try {
await copyFile(originalSrc, finalDest);
console.log('file copied');
} catch (err) {
console.log(err);
}
};

export const emptyDir = async (dirPath) => {
try {
// list dir content
const dirContents = fs.readdirSync(dirPath);

for (const fileOrDirPath of dirContents) {
try {
// get full path
const fullPath = path.join(dirPath, fileOrDirPath);
const stat = fs.statSync(fullPath);

if (stat.isDirectory()) {
// it's a sub directory
if (fs.readdirSync(fullPath).length) emptyDir(fullPath);

// if the dir is not empty then remove it's contents too(recursively)
fs.rmdirSync(fullPath);

} else {
// it's a file
fs.unlinkSync(fullPath);
}

} catch (ex) {
console.error(ex.message);
}
}
} catch (err) {
console.log(err);
}
};
18 changes: 18 additions & 0 deletions .build/original/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Please edit original file located in .build/original/global.d.ts

declare module '*.tokens.css' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.tokens.scss' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.tokens.js' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.tokens.xml' {
const classes: { [key: string]: string };
export default classes;
}
8 changes: 8 additions & 0 deletions .build/original/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Please edit original file located in .build/original/index.ts

export default qgdsDesignTokens;
import cssTokensPrimitive from './css/styles/primitive.tokens.css';

function qgdsDesignTokens() {
return JSON.stringify(cssTokensPrimitive);
}
64 changes: 64 additions & 0 deletions .build/sd-file-generators.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { semanticFilter, componentFilter } from "./sd-filters.mjs";

const commonFileOptions = {
android: {
format: "android/resources",
},
js: {
format: "javascript/es6",
},
scss: {
format: "scss/variables",
options: {
selector: ":host",
},
},
css: {
format: "css/variables",
options: {
selector: ":host",
},
}
};

export const generateSemanticFiles = (components, theme, platform, fileExtension) => {
const filesArr = [];
// theme-specific outputs
filesArr.push({
...commonFileOptions[platform],
filter: semanticFilter(components, true),
destination: `src/${platform}/styles/qgds-${theme.toLowerCase()}.tokens.${fileExtension}`,
});

// not theme-specific outputs
filesArr.push({
...commonFileOptions[platform],
filter: semanticFilter(components, false),
destination: `src/${platform}/styles/qgds.tokens.${fileExtension}`,
});

return filesArr;
};

// for each component (currently only button), filter those specific component tokens and output them
// to the component folder where the component source code will live
export const generateComponentFiles = (components, theme, platform, fileExtension) => {
const filesArr = [];

for (const comp of components) {
// theme-specific outputs
filesArr.push({
...commonFileOptions[platform],
filter: componentFilter(comp, true),
destination: `src/${platform}/${comp}/${comp}-${theme.toLowerCase()}.tokens.${fileExtension}`,
});

// not theme-specific outputs
filesArr.push({
...commonFileOptions[platform],
filter: componentFilter(comp, false),
destination: `src/${platform}/${comp}/${comp}.tokens.${fileExtension}`,
});
}
return filesArr;
};
Loading

0 comments on commit 2eaa437

Please sign in to comment.