feat: Better support for multiple themes (#2792)

* feat: Better support for multiple themes

This change adds support for two CLI options to the build-tokens
command.

The first, --all-themes makes the build-tokens command process all
themes in the source directory as opposed to specifying all the names
via --theme.

Secondly, --base-paragon-theme allows you to have multiple themes derived
from the same common base. If specified, you can have a derived theme that
still loads tokens from the light theme. For example, you can have a
site theme called 'theme-one' and set the base theme to light so it will
reuse the core light theme tokens and layer on changes from 'theme-one'.

* fixup! feat: Better support for multiple themes

* fixup! fixup! feat: Better support for multiple themes
This commit is contained in:
Kshitij Sobti 2025-11-03 20:51:24 +05:30 committed by GitHub
parent 04e52615ba
commit b46e241866
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 45 additions and 11 deletions

View File

@ -94,7 +94,8 @@ const COMMANDS = {
{
name: '-t, --themes',
description: `Specify themes to include in the token build.
Can be provided as a comma-separated list (e.g., "light,dark") or multiple arguments (e.g., "-t light -t dark").`,
Can be provided as a comma-separated list (e.g., "light,dark") or multiple arguments (e.g., "-t light -t dark").
Cannot be used with --all-themes`,
defaultValue: 'light',
},
{
@ -107,6 +108,16 @@ const COMMANDS = {
description: 'Enable verbose logging.',
defaultValue: false,
},
{
name: '--base-paragon-theme',
description: 'Specify the base theme to use in the token build. For example, to build the "high-contrast" theme on top of the light theme use "--theme high-contrast --base-paragon-theme light".',
defaultValue: 'Same as theme',
},
{
name: '--all-themes',
description: 'Build tokens for all themes in the source directory. Cannot be used with --themes.',
defaultValue: false,
},
],
},
'replace-variables': {

View File

@ -1,3 +1,4 @@
const fs = require('fs');
const path = require('path');
const minimist = require('minimist');
const {
@ -13,17 +14,22 @@ const { createIndexCssFile } = require('../tokens/utils');
* @param {string[]} commandArgs - Command line arguments for building tokens.
* @param {string} [commandArgs.build-dir='./build/'] - The directory where the build output will be placed.
* @param {string} [commandArgs.source] - The source directory containing JSON token files.
* @param {string} [commandArgs.base-paragon-theme] - The base theme to use from Paragon if named differently than
* the theme.
* @param {boolean} [commandArgs.source-tokens-only=false] - Indicates whether to include only source tokens.
* @param {string|string[]} [commandArgs.themes=['light']] - The themes (variants) for which to build tokens.
* @param {boolean} [commandArgs.all-themes] - Indicated whether to process all themes.
*/
async function buildTokensCommand(commandArgs) {
const defaultParams = {
themes: ['light'],
themes: null,
'base-paragon-theme': null,
'build-dir': './build/',
'source-tokens-only': false,
'output-references': true,
'exclude-core': false,
verbose: false,
'all-themes': false,
};
const alias = {
@ -39,19 +45,36 @@ async function buildTokensCommand(commandArgs) {
'output-references': outputReferences,
themes,
verbose,
'base-paragon-theme': baseParagonTheme,
'all-themes': allThemes,
'exclude-core': excludeCore,
} = minimist(
commandArgs,
{
alias,
default: defaultParams,
boolean: ['source-tokens-only', 'output-references', 'exclude-core', 'verbose'],
boolean: ['source-tokens-only', 'output-references', 'exclude-core', 'verbose', 'all-themes'],
},
);
const parsedThemes = Array.isArray(themes) ? themes : themes.split(',').map(t => t.trim());
if (themes !== null && allThemes) {
throw Error('Cannot specify themes with `--themes` when using `--all-themes`.');
}
let themesToProcess = null;
const StyleDictionary = await initializeStyleDictionary({ themes: parsedThemes });
if (allThemes) {
const tokensPath = tokensSource || path.resolve(__dirname, '../tokens/src');
themesToProcess = fs
.readdirSync(`${tokensPath}/themes/`, { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => entry.name);
} else if (Array.isArray(themes)) {
themesToProcess = themes;
} else {
themesToProcess = (themes || 'light').split(',').map(t => t.trim());
}
const StyleDictionary = await initializeStyleDictionary({ themes: themesToProcess });
const coreConfig = {
include: [
@ -100,12 +123,12 @@ async function buildTokensCommand(commandArgs) {
},
};
const getStyleDictionaryConfig = (themeVariant) => ({
const getStyleDictionaryConfig = (themeVariant, baseThemeVariant) => ({
...coreConfig,
include: [
...coreConfig.include,
path.resolve(__dirname, `../tokens/src/themes/${themeVariant}/**/*.json`),
path.resolve(__dirname, `../tokens/src/themes/${themeVariant}/**/*.toml`),
path.resolve(__dirname, `../tokens/src/themes/${baseThemeVariant}/**/*.json`),
path.resolve(__dirname, `../tokens/src/themes/${baseThemeVariant}/**/*.toml`),
],
source: tokensSource
? [
@ -159,10 +182,10 @@ async function buildTokensCommand(commandArgs) {
}
// Add theme variants
for (const themeVariant of parsedThemes) {
const config = getStyleDictionaryConfig(themeVariant);
themesToProcess.forEach(themeVariant => {
const config = getStyleDictionaryConfig(themeVariant, baseParagonTheme || themeVariant);
configs.push({ config, themeVariant });
}
});
// Build tokens for each configuration
await Promise.all(configs.map(async ({ config, themeVariant }) => {