Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"author": "HashiCorp Design Systems <design-systems@hashicorp.com>",
"scripts": {
"build": "rollup --config",
"postbuild": "cp -R dist/styles/@hashicorp/design-system-components.css ../../showcase/public/assets/styles/@hashicorp/ && cp -R dist/styles/@hashicorp/design-system-components-common.css ../../showcase/public/assets/styles/@hashicorp/ && cp -R dist/styles/@hashicorp/design-system-power-select-overrides.css ../../showcase/public/assets/styles/@hashicorp/",
"format": "prettier . --cache --write",
"lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto",
"lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && pnpm run format",
Expand Down Expand Up @@ -425,4 +426,4 @@
"engines": {
"node": ">= 18"
}
}
}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2,996 changes: 1,590 additions & 1,406 deletions packages/tokens/dist/products/css/themed-tokens/with-css-selectors/tokens.css

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

4,261 changes: 2,353 additions & 1,908 deletions packages/tokens/dist/products/css/themed-tokens/with-scss-mixins/tokens.css

Large diffs are not rendered by default.

163 changes: 163 additions & 0 deletions packages/tokens/scripts/build-parts/customFormatCssThemedTokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import { fileHeader, formattedVariables, getReferences, resolveReferences } from 'style-dictionary/utils';
import type { DesignToken, TransformedToken, Dictionary, Config, LocalOptions, FormatFn } from 'style-dictionary/types';

// since we need to differentiate the internal logic depending on the `target`, we need to use
// a high-order/curried function that takes `target` as argument and return a function of type `FormatFn`
export async function customFormatCssThemedTokensFunctionForTarget(target: string): Promise<FormatFn> {

// get the "do not edit directly" header (to use it later inside the curried function)
const header = await fileHeader({});

// this is where we return the "format" function, that depending on the `target` filters the tokens to be written in the "common" file or in the "themed" file
return function ({ dictionary, options }: { dictionary: Dictionary, options: Config & LocalOptions }): string {

// filter out tokens based on kind of `target` and `$modes` existence
const filteredTokens = dictionary.allTokens.filter(token => {
// if it's private we don't want to output the token at all, not in "common" nor in "themed"
if (token.private) {
return false;
} else {
// we have to consider different conditions to decide this should be output in "common" or in "themed"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// we have to consider different conditions to decide this should be output in "common" or in "themed"
// we have to consider different conditions to decide if this should be output in "common" or in "themed"

const isThemed = ('$modes' in token);
const hasReferencesAndHasBeenTransformed = checkIfHasReferencesAndHasBeenTransformed(token, dictionary, options.usesDtcg);
const hasPrivateReferencesWithThemedDescendants = checkIfHasPrivateReferencesWithThemedDescendants(token, dictionary, options.usesDtcg);
if (target === 'common') {
return !isThemed && !hasReferencesAndHasBeenTransformed && !hasPrivateReferencesWithThemedDescendants;
} else {
return isThemed || (!isThemed && (hasReferencesAndHasBeenTransformed || hasPrivateReferencesWithThemedDescendants));
}
}
});

// create a shallow copy of the dictionary with the filtered allTokens
const filteredDictionary = {
...dictionary,
allTokens: filteredTokens
};

// use a custom formatter for the CSS variables
// note: initially we tried to piggyback `StyleDictionary.hooks.formats['css/variables']` but then realized that `formattedVariables` is the one that does all the heavy lifting,
// so we went with a custom implementation to avoid hacks trying to use the hooks
const variables = formattedVariables({
format: 'css',
dictionary: filteredDictionary,
outputReferences: shouldOutputReferences,
formatting: { indentation: ' ' },
usesDtcg: options.usesDtcg,
});

// sort the CSS variables (easier to read and compare)
const sortedVariables = variables.split('\n').sort().join('\n');

// return the content (with the header)
return `${header}:root {\n${sortedVariables}\n}`;
}
}

const shouldOutputReferences = (token: TransformedToken, options: { dictionary: Dictionary, usesDtcg?: boolean }) => {
const { dictionary, usesDtcg } = options;

const originalValue = usesDtcg ? token.original.$value : token.original.value;

// get all the token refs (aliases) that are referenced in its `$value`
// e.g. `"$value": "{foo.bar} {baz}"` has two references (`foo.bar` and `baz`)
const refs = getReferences(originalValue, dictionary.tokens, {
// note: we pass `unfilteredTokens` to ensure we find the refs even if they are filtered out
unfilteredTokens: dictionary.unfilteredTokens,
usesDtcg,
warnImmediately: false,
});

// check whether any of the refs is private
const hasPrivateReferences = refs.some((ref: DesignToken) => ref.private);

// check if the token has references and at the same time its value has been transformed along the way
const hasReferencesAndHasBeenTransformed = checkIfHasReferencesAndHasBeenTransformed(token, dictionary, usesDtcg);

// we output the reference only if none of the conditions above are true
return !hasPrivateReferences && !hasReferencesAndHasBeenTransformed;
}

// inspired by `outputReferencesTransformed` - https://github.com/style-dictionary/style-dictionary/blob/main/lib/utils/references/outputReferencesTransformed.js
const checkIfHasReferencesAndHasBeenTransformed = (token: TransformedToken, dictionary: Dictionary, usesDtcg?: boolean) => {

const value = usesDtcg ? token.$value : token.value;
const originalValue = usesDtcg ? token.original.$value : token.original.value;

const refs = getReferences(originalValue, dictionary.tokens, {
// note: we pass `unfilteredTokens` to ensure we find the refs even if they are filtered out
unfilteredTokens: dictionary.unfilteredTokens,
usesDtcg,
warnImmediately: false,
});
const hasReferences = refs && refs.length > 0;

let isTransformed;

// references can live only in strings (we ignore arrays and objects, for now)
if (typeof originalValue === 'string') {
// Check if the token's value is the same as if we were resolve references on the original value
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Check if the token's value is the same as if we were resolve references on the original value
// Check if the token's value is the same as if we were resolving references on the original value

const transformedValue = resolveReferences(originalValue, dictionary.unfilteredTokens ?? dictionary.tokens, {
usesDtcg,
warnImmediately: false,
})
// This checks whether the token's value has been transformed e.g. transitive transforms.
// see: https://styledictionary.com/reference/utils/references/#resolvereferences
isTransformed = (
// this `value` could be the original one (eg. `#FF0000`, no transformations)
// or the transformed one (eg. `#FF0000`β†’`#FF0000CC` if an `alpha` attribute was applied at token level,
// which triggered the `color/with-alpha` transformation)
value !== transformedValue
);
}
return hasReferences && isTransformed;
}

// checks if a token has any private references that themselves reference themed tokens (they have `$modes`)
const checkIfHasPrivateReferencesWithThemedDescendants = (
token: DesignToken,
dictionary: Dictionary,
usesDtcg?: boolean
): boolean => {
const visited = new Set<string>();

const checkReferences = (currentToken: DesignToken, hasPrivateAncestor = false): boolean => {
const originalValue = usesDtcg ? currentToken.original?.$value : currentToken.original?.value;

const refs = getReferences(originalValue, dictionary.tokens, {
// note: we pass `unfilteredTokens` to ensure we find the refs even if they are filtered out
unfilteredTokens: dictionary.unfilteredTokens,
usesDtcg,
warnImmediately: false,
});

return refs.some((ref: DesignToken) => {
const refKey = ref.path?.join('.') || ref.name;

// skip if already visited (avoid circular references) otherwise store it
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// skip if already visited (avoid circular references) otherwise store it
// skip if already visited (avoid circular references), otherwise store it

Copy link
Contributor

Choose a reason for hiding this comment

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

Btw, not trying to be overly nitpicky, just making recommended slight edits in cases where the text was a bit awkward or confusing to me when reading.

Adding a comma here makes it clearer to read to me:

"Do this, otherwise do that"

if (visited.has(refKey)) {
return false;
} else {
visited.add(refKey);
}

// if we're already in a private chain this token is themed...
if (hasPrivateAncestor && '$modes' in ref) {
return true;
}

// add the current ref to the private reference logical `OR` chain
const isInPrivateChain = ref.private || hasPrivateAncestor;

// Recursively check descendants, passing along the private chain status
return checkReferences(ref, isInPrivateChain);
});
};

return checkReferences(token);
};
24 changes: 24 additions & 0 deletions packages/tokens/scripts/build-parts/customFormatDocsJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import type { Dictionary } from 'style-dictionary/types';

import { cloneDeep } from 'lodash-es';

export function customFormatDocsJsonFunction({ dictionary }: { dictionary: Dictionary}): string {
// Notice: this object shape is used also in the documentation so any updates
// to this format should be reflected in the corresponding type definition.
const output: {}[] = [];
dictionary.allTokens.forEach((token: any) => {
// we remove the "filePath" prop from the token because the orginal file path is irrelevant for us
// (plus its value is an absolute path, so it causes useless diffs in git)
const outputToken = cloneDeep(token);
delete outputToken.filePath;
delete outputToken.isSource;
output.push(outputToken);
});

return JSON.stringify(output, null, 2);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@

import fs from 'fs-extra';
import prettier from 'prettier';
import chalk from 'chalk';

import type { Dictionary, PlatformConfig } from 'style-dictionary';
import { fileHeader } from 'style-dictionary/utils';

import { getSourceFromFileWithRootSelector } from './getSourceFromFileWithRootSelector.ts';

export async function generateThemingCssFiles(_dictionary: Dictionary, config: PlatformConfig): Promise<void> {

const commonSource = await getSourceFromFileWithRootSelector(config, 'default', 'common-tokens.css');
Expand Down Expand Up @@ -92,12 +95,6 @@ export async function generateThemingCssFiles(_dictionary: Dictionary, config: P
}
}

async function getSourceFromFileWithRootSelector(config: PlatformConfig, theme: string, path: string): Promise<string> {
const rawSource = await fs.readFile(`${config.buildPath}themed-tokens/with-root-selector/${theme}/${path}`, 'utf8');
const header = await fileHeader({});
return rawSource.replace(header, '');
}

function getCssVariablesStalenessComment(): string {
let comment = '';
comment += '\n/*\n\n';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import fs from 'fs-extra';

import type { PlatformConfig } from 'style-dictionary';
import { fileHeader } from 'style-dictionary/utils';

export async function getSourceFromFileWithRootSelector(config: PlatformConfig, theme: string, path: string): Promise<string> {
const rawSource = await fs.readFile(`${config.buildPath}themed-tokens/with-root-selector/${theme}/${path}`, 'utf8');
const header = await fileHeader({});
return rawSource.replace(header, '');
}
59 changes: 19 additions & 40 deletions packages/tokens/scripts/build-parts/getStyleDictionaryConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@

import type { Config, DesignToken } from 'style-dictionary/types';

export const targets = ['products', 'devdot', 'marketing', 'cloud-email'];
export const modes = ['default', 'cds-g0', 'cds-g10', 'cds-g90', 'cds-g100'];
export const targets = ['products', 'devdot', 'marketing', 'cloud-email'] as const;
export const modes = ['default', 'cds-g0', 'cds-g10', 'cds-g90', 'cds-g100'] as const;

export type Target = typeof targets[number];
export type Mode = typeof modes[number];

// uncomment this to enable debugging
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this comment be deleted or updated?

const baseConfig: Config = {
// log: {
// warnings: 'warn', // options: warn | error | disabled
// verbosity: 'verbose', // options: default | silent | verbose
// errors: {
// brokenReferences: 'console', // options: throw | console
// },
// }
log: {
warnings: 'warn', // options: warn | error | disabled
verbosity: 'verbose', // options: default | silent | verbose
errors: {
brokenReferences: 'console', // options: throw | console
},
}
};

const excludePrivateTokens = (token: DesignToken) => {
Expand All @@ -43,6 +43,7 @@ export function getStyleDictionaryConfig({ target, mode }: { target: Target, mod
return {
...baseConfig,
source: [
// `src/test/**/*.json`,
`src/carbon-extracted/**/*.json`,
`src/global/**/*.json`,
`src/products/shared/**/*.json`
Expand All @@ -56,39 +57,13 @@ export function getStyleDictionaryConfig({ target, mode }: { target: Target, mod
files: [
{
destination: `themed-tokens/with-root-selector/${mode}/common-tokens.css`,
format: 'css/variables',
options: {
// TODO understand if is better to output references or not for the "common" definitions (almost certainly no) - see: https://hashicorp.atlassian.net/browse/HDS-5669
outputReferences: false,
// outputReferences: (token, { dictionary, usesDtcg }) => {
// // `dictionary` contains `allTokens`, `tokens`, `tokenMap`, `unfilteredTokens`, `unfilteredAllTokens` and `unfilteredTokenMap` props
// // `usesDtcg` tells you whether the Design Token Community Group spec is used with $ prefixes ($value, $type etc.)
// // return true or false
// },
// see: https://styledictionary.com/reference/utils/references/#combining-multiple-outputreference-utility-functions
// outputReferences: (token, options) => outputReferencesFilter(token, options) && outputReferencesTransformed(token, options),
},
filter: (token: DesignToken) => {
return !token.private && !(token.attributes && token.attributes.themeable);
},
// IMPORTANT: filtering, formatting, outputReferences, etc. are done directly in the custom format function
format: 'css/themed-tokens/with-root-selector/common',
},
{
destination: `themed-tokens/with-root-selector/${mode}/themed-tokens.css`,
format: 'css/variables',
options: {
// TODO understand if is better to output references or not for the "themed" definitions (almost certainly no) - see: https://hashicorp.atlassian.net/browse/HDS-5669
outputReferences: false,
// outputReferences: (token, { dictionary, usesDtcg }) => {
// // `dictionary` contains `allTokens`, `tokens`, `tokenMap`, `unfilteredTokens`, `unfilteredAllTokens` and `unfilteredTokenMap` props
// // `usesDtcg` tells you whether the Design Token Community Group spec is used with $ prefixes ($value, $type etc.)
// // return true or false
// },
// see: https://styledictionary.com/reference/utils/references/#combining-multiple-outputreference-utility-functions
// outputReferences: (token, options) => outputReferencesFilter(token, options) && outputReferencesTransformed(token, options),
},
filter: (token: DesignToken) => {
return !token.private && (token.attributes && token.attributes.themeable);
},
// IMPORTANT: filtering, formatting, outputReferences, etc. are done directly in the custom format function
format: 'css/themed-tokens/with-root-selector/themed',
}
],
// this has been registered in the `build` file
Expand Down Expand Up @@ -121,6 +96,7 @@ export function getStyleDictionaryConfig({ target, mode }: { target: Target, mod
return {
...baseConfig,
source: [
// `src/test/**/*.json`,
`src/carbon-extracted/**/*.json`,
`src/global/**/*.json`,
`src/products/shared/**/*.json`
Expand All @@ -138,7 +114,7 @@ export function getStyleDictionaryConfig({ target, mode }: { target: Target, mod
filter: excludePrivateTokens,
}
],
actions: ['generate-css-helpers', 'generate-theming-css-files'],
actions: ['generate-css-helpers', 'validate-theming-css-files', 'generate-theming-css-files'],
},
'docs/json': {
buildPath: 'dist/docs/products/',
Expand Down Expand Up @@ -268,4 +244,7 @@ export function getStyleDictionaryConfig({ target, mode }: { target: Target, mod
}
}
};

// if we reach here, the target is invalid (added to satisfy TypeScript)
throw new Error(`Invalid target: ${target}. Expected one of: ${targets.join(',')}`);
};
Loading
Loading