-
Notifications
You must be signed in to change notification settings - Fork 49
π§ͺ [SPIKE] [Project Solar / Phase 1 / Engineering Follow-ups] Fix issues with outputReferences and isThemeable
#3435
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
base: project-solar/phase-1-main-feature-branch
Are you sure you want to change the base?
Changes from all commits
8d72772
27c664e
bba856c
e114570
f794244
febd27c
c8ad226
6136956
7d69865
664c8da
493de99
6ba0355
8a42392
0515037
d75411a
d5315e8
216a2a6
c9d9a20
9dc8ae8
cdb9995
a67986b
e95cfd9
fcf5fe9
3dfe6f6
0489bea
5fac140
8da8036
2cd0c28
2daa246
4398a7e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| 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" | ||||||
| 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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| 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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||||||
| }; | ||||||
| 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 |
|---|---|---|
| @@ -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, ''); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) => { | ||
|
|
@@ -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` | ||
|
|
@@ -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 | ||
|
|
@@ -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` | ||
|
|
@@ -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/', | ||
|
|
@@ -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(',')}`); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.