From fdc28a767ebd62f8b573ea75d1710214607cfe97 Mon Sep 17 00:00:00 2001 From: Raisa Primerova <48605821+RayRedGoose@users.noreply.github.com> Date: Mon, 23 Oct 2023 17:00:47 -0600 Subject: [PATCH] feat: Update configurations to improve DX (#53) - Updated build to replace css from `/dist` - Updated `mergeType` format - Created `formatType` format to generate ts file with inline jsDoc - Updated parser to rename description by comment [category:Infrastructure] Co-authored-by: @alanbsmith --- .gitignore | 3 + .../canvas-tokens-docs/.storybook/preview.ts | 6 +- packages/canvas-tokens-web/package.json | 5 +- packages/canvas-tokens/build.ts | 14 +- .../utils/formatters/formatJS.ts | 52 +++-- .../utils/formatters/formatTypes.ts | 99 ++++++++ .../helpers/formattedObjectInnerValues.ts | 152 ++++++++++++ .../helpers/formattedObjectValue.ts | 100 -------- .../canvas-tokens/utils/formatters/index.ts | 19 +- .../utils/formatters/mergeObjects.ts | 25 +- .../utils/formatters/mergeTypes.ts | 6 +- .../canvas-tokens/utils/spec/formats.spec.ts | 103 +++++---- .../canvas-tokens/utils/spec/helpers.spec.ts | 218 +++++++++--------- .../canvas-tokens/utils/tokenStudioParser.ts | 10 + .../transformers/transformNameToCamelCase.ts | 10 +- 15 files changed, 511 insertions(+), 311 deletions(-) create mode 100644 packages/canvas-tokens/utils/formatters/formatTypes.ts create mode 100644 packages/canvas-tokens/utils/formatters/helpers/formattedObjectInnerValues.ts delete mode 100644 packages/canvas-tokens/utils/formatters/helpers/formattedObjectValue.ts diff --git a/.gitignore b/.gitignore index 80de46e..a3656be 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ # compiled output dist docs +*/canvas-tokens-web/css +*/canvas-tokens-web/less +*/canvas-tokens-web/scss tmp /out-tsc *.tsbuildinfo diff --git a/packages/canvas-tokens-docs/.storybook/preview.ts b/packages/canvas-tokens-docs/.storybook/preview.ts index df43475..b1fb40e 100644 --- a/packages/canvas-tokens-docs/.storybook/preview.ts +++ b/packages/canvas-tokens-docs/.storybook/preview.ts @@ -1,7 +1,7 @@ import {Preview} from '@storybook/react'; -import '@workday/canvas-tokens-web/dist/css/base/_variables.css'; -import '@workday/canvas-tokens-web/dist/css/brand/_variables.css'; -import '@workday/canvas-tokens-web/dist/css/system/_variables.css'; +import '@workday/canvas-tokens-web/css/base/_variables.css'; +import '@workday/canvas-tokens-web/css/brand/_variables.css'; +import '@workday/canvas-tokens-web/css/system/_variables.css'; const preview: Preview = { parameters: { diff --git a/packages/canvas-tokens-web/package.json b/packages/canvas-tokens-web/package.json index 264cc54..d48586e 100644 --- a/packages/canvas-tokens-web/package.json +++ b/packages/canvas-tokens-web/package.json @@ -12,7 +12,10 @@ "clean": "rimraf dist" }, "files": [ - "dist/" + "dist/", + "css/", + "less/", + "scss/" ], "dependencies": {} } diff --git a/packages/canvas-tokens/build.ts b/packages/canvas-tokens/build.ts index eaf97a4..409ab92 100644 --- a/packages/canvas-tokens/build.ts +++ b/packages/canvas-tokens/build.ts @@ -9,10 +9,8 @@ const config = setConfig({ platforms: ['css', 'scss', 'less', 'es6', 'common-js'], levels: ['base', 'brand', 'sys'], platformOptions: { - '*': { - buildPath: '../canvas-tokens-web/dist/', - }, 'css, scss, less': { + buildPath: '../canvas-tokens-web/', transformGroup: 'web', fileName: '{platform}/{level}/_variables', prefix: 'cnvs-', @@ -36,22 +34,22 @@ const config = setConfig({ extensions: ['sass', 'scss'], }, 'es6, common-js': { + buildPath: '../canvas-tokens-web/dist/', transformGroup: 'js', - transforms: ['value/variables', 'name/camel'], + transforms: ['value/flatten-border', 'value/flatten-base-shadow', 'name/camel'], fileName: '{platform}/{level}/index', extensions: ['js', 'd.ts'], modifiers: [ { level: ['base'], extensions: ['js'], - format: 'javascript/{platform}', + format: 'js/{platform}', filterByLevel: true, }, { level: ['base'], extensions: ['d.ts'], - format: 'merge/types', - combineWith: ['javascript/{platform}'], + format: 'ts/inline', filterByLevel: true, }, { @@ -64,7 +62,7 @@ const config = setConfig({ level: ['brand', 'sys'], extensions: ['d.ts'], format: 'merge/types', - combineWith: ['merge/objects', '{platform}/types'], + combineWith: ['merge/objects', 'ts/jsdoc-object'], }, { level: ['base'], diff --git a/packages/canvas-tokens/utils/formatters/formatJS.ts b/packages/canvas-tokens/utils/formatters/formatJS.ts index 654ee4e..501b2ff 100644 --- a/packages/canvas-tokens/utils/formatters/formatJS.ts +++ b/packages/canvas-tokens/utils/formatters/formatJS.ts @@ -8,12 +8,45 @@ import {jsFileHeader} from './helpers/jsFileHeader'; * options can contains `withoutModule` property js module header should not be generated. * @returns file content as a string */ -export const formatToInlineModule: Formatter = ({dictionary, file, options}) => { +export const formatToInlineCommonJSModule: Formatter = ({dictionary, file, options}) => { const headerContent = !options.withoutModule ? jsFileHeader({file}) : formatHelpers.fileHeader({file}); - return dictionary.allTokens.reduce((acc: string, {name, value}) => { - acc += `exports.${name} = "${value}";\n`; + return dictionary.allTokens.reduce((acc: string, {name, path}) => { + const cssVarName = path.join('-'); + acc += `exports.${name} = "--cnvs-${cssVarName}";\n`; + return acc; + }, headerContent); +}; + +/** + * Style Dictionary format function that creates es6 file structure. + * This structure contains separated exports of each token. + * @param {*} FormatterArguments - Style Dictionary formatter object containing `dictionary`, `options`, `file` and `platform` properties. + * options can contains `withoutModule` property js module header should not be generated. + * @returns file content as a string + */ +export const formatToInlineES6Module: Formatter = ({dictionary, file, options}) => { + const headerContent = formatHelpers.fileHeader({file}); + return dictionary.allTokens.reduce((acc: string, {name, path}) => { + const cssVarName = path.join('-'); + acc += `export const ${name} = "--cnvs-${cssVarName}";\n`; + return acc; + }, headerContent); +}; + +/** + * Style Dictionary format function that creates ts file structure. + * This structure contains separated exports of each token with `as const`. + * @param {*} FormatterArguments - Style Dictionary formatter object containing `dictionary`, `options`, `file` and `platform` properties. + * options can contains `withoutModule` property js module header should not be generated. + * @returns file content as a string + */ +export const formatInlineTypes: Formatter = ({dictionary, file}) => { + const headerContent = formatHelpers.fileHeader({file}); + return dictionary.allTokens.reduce((acc: string, {name, path}) => { + const cssVarName = path.join('-'); + acc += `export declare const ${name} = "--cnvs-${cssVarName}" as const;\n`; return acc; }, headerContent); }; @@ -45,19 +78,6 @@ export const formatES6ToObjects: Formatter = ({dictionary, file}) => { }, headerContent); }; -/** - * Style Dictionary format function that create token type definitions. - * @param {*} FormatterArguments - Style Dictionary formatter object containing `dictionary`, `options`, `file` and `platform` properties. - * @returns file content as a string - */ -export const formatES6ToTypes: Formatter = ({dictionary, file}) => { - const headerContent = formatHelpers.fileHeader({file}); - return Object.entries(dictionary.properties).reduce((acc: string, [key, values]) => { - return (acc += - `export declare const ${key} = ` + JSON.stringify(values, null, 2) + ' as const;\n\n'); - }, headerContent); -}; - /** * Style Dictionary format function that create the export index file for es6 folder. * @param {*} FormatterArguments - Style Dictionary formatter object containing `dictionary`, `options`, `file` and `platform` properties. diff --git a/packages/canvas-tokens/utils/formatters/formatTypes.ts b/packages/canvas-tokens/utils/formatters/formatTypes.ts new file mode 100644 index 0000000..0e75010 --- /dev/null +++ b/packages/canvas-tokens/utils/formatters/formatTypes.ts @@ -0,0 +1,99 @@ +import {Formatter, formatHelpers} from 'style-dictionary'; + +/** + * Style Dictionary format function that creates token type definitions with JSDoc. + * @param {*} FormatterArguments - Style Dictionary formatter object containing `dictionary`, `options`, `file` and `platform` properties. + * @returns file content as a string + */ +export const formatJSToTypes: Formatter = ({dictionary, file, options}) => { + const {originalValues} = options; + const headerContent = formatHelpers.fileHeader({file}); + + const placeholders = Object.keys(dictionary.properties) + .map(k => `**${k}**`) + .join('\n'); + + let content = placeholders; + + const replaceInContent: ReplaceFn = (pattern, newValue) => { + content = content.replace(pattern, newValue); + return content; + }; + + recursivelyCreateFileStructure({ + originalValues, + tokens: dictionary.properties, + content, + replaceInContent, + }); + + return headerContent + content; +}; + +const startingText = 'export declare const'; +const endingText = 'as const;\n'; + +type ReplaceFn = (pattern: string, newText: string) => string; + +type HelperArgs = { + originalValues: Record; + tokens: Record; + depth?: number; + content: string; + replaceInContent: ReplaceFn; +}; + +const recursivelyCreateFileStructure = ({ + originalValues, + tokens, + content, + replaceInContent, + depth = 0, +}: HelperArgs) => { + let updatedContent = content; + const entries = Object.entries(tokens); + + entries.forEach(([key, values]) => { + const original = originalValues[key]; + const spaces = ' '.repeat(depth); + const extraSpaces = spaces + ' '; + + if (typeof values === 'string') { + const pxVal = original.value.includes('rem') ? parseFloat(original.value) * 16 : null; + const hasComment = original.comment; + const commentParts = hasComment ? original.comment.split('; ') : []; + const commentText = hasComment + ? `\n${extraSpaces}* ${commentParts.join(`\n${extraSpaces}* `)}\n${extraSpaces}*` + : ''; + const valueText = ` ${original.value}${pxVal ? ` (${pxVal}px)` : ''} `; + const jsDocText = `${spaces}/**${commentText}${valueText}${ + original.comment ? '\n' + extraSpaces : '' + }*/\n`; + const innerText = depth + ? `${spaces}"${key}": "${values}",` + : `${startingText} ${key} = "${values}" ${endingText}`; + const fullInnerText = jsDocText + innerText; + + updatedContent = replaceInContent(`**${key}**`, fullInnerText); + return; + } + + const placeholders = Object.keys(values) + .map(k => `**${k}**`) + .join('\n'); + + const innerText = !depth + ? `${startingText} ${key} = {\n${placeholders}\n} ${endingText}` + : `${spaces}"${key}": {\n${placeholders}\n${spaces}},`; + + updatedContent = replaceInContent(`**${key}**`, innerText); + + recursivelyCreateFileStructure({ + originalValues: original, + tokens: values, + depth: depth + 1, + content: updatedContent, + replaceInContent, + }); + }); +}; diff --git a/packages/canvas-tokens/utils/formatters/helpers/formattedObjectInnerValues.ts b/packages/canvas-tokens/utils/formatters/helpers/formattedObjectInnerValues.ts new file mode 100644 index 0000000..0ac4433 --- /dev/null +++ b/packages/canvas-tokens/utils/formatters/helpers/formattedObjectInnerValues.ts @@ -0,0 +1,152 @@ +import {Dictionary, TransformedToken} from 'style-dictionary'; +import {camelCase} from 'case-anything'; +import * as math from 'mathjs'; +import {isMathExpression, isComposite} from '../../filters'; + +type ChangeValueFunction = (token: TransformedToken) => Record | string; + +interface FormatterHelperArgs { + format: 'sys' | 'brand'; + dictionary: Dictionary; + changeValueFn: ChangeValueFunction; +} + +type CompositeHelper = (args: FormatterHelperArgs) => Record; + +/** + * Style Dictionary format helper that transform dictionary properties object to the level specific object. All keys are changed to camel case and values to CSS variables. + * @param {HelperArgs} HelperArgs - arguments of format helper function, includes properties: + * `dictionary` as tyle Dictionary dictionary object, + * `format`: 'sys' or 'brand' value. + * @returns updated token object with cameled case keys and a css variable name as value. + */ + +export const formattedObjectInnerValues: CompositeHelper = ({ + format, + dictionary, + changeValueFn, +}) => { + const parsed = dictionary.properties[format]; + const filteredTokens = dictionary.allTokens.filter( + ({path: [ctg]}) => ctg !== 'base' && ctg === format + ); + + return filteredTokens.reduce((acc: Record, {path}: TransformedToken) => { + const passedPath = path.slice(1); + + setProperty({ + src: parsed, + output: acc, + path: passedPath, + changeValueFn, + }); + + return acc; + }, {}); +}; + +type RecursionHelperArgs = { + src: Record; + output: Record; + path: string[]; + changeValueFn: ChangeValueFunction; +}; + +/** + * Recursive utility function that iterates through a src object and fills output object with the updated value. + * @param {RecursionHelperArgs} RecursionHelperArgs - arguments of the recursive function: + * `src`: src object containing all token info; + * `output`: 'new object to fill; + * `path`: the path token array to iterate through + * `valueFn` function that changing token value. + */ + +const setProperty = ({src, output, path, changeValueFn}: RecursionHelperArgs): void => { + const [head, ...rest] = path; + const key = camelCase(head); + + // set empty object as value if inner object doesn't exist yet + if (!output[key]) { + output[key] = {}; + } + + // stop iterate if it's end of path and apply function to change token + if (!rest.length) { + const token = src.value ? src : src[head]; + output[key] = changeValueFn(token); + return; + } + + setProperty({ + src: src[head], + output: output[key], + path: rest, + changeValueFn, + }); +}; + +// ** UTILITIES ** + +/** + * Utility function to change token composite values to css var and keys to camel case and regular token value to CSS var name + * @param {Object} token the token object + * @param {Function} getRefs: style dictionary getReferences function + * @returns the updated token composite value + */ +export const changeValuesToCSSVars = ( + token: TransformedToken, + getRefs: Dictionary['getReferences'] +): Record | string => { + const originalValue = token.original.value; + + if (isComposite(token) && typeof originalValue !== 'string') { + return Object.entries(originalValue).reduce( + (acc: Record, [key, value]: string[]) => { + // composite values have single reference + const [ref] = getRefs(value); + const name = camelCase(key); + return {...acc, [name]: `--cnvs-${ref.path.join('-')}`}; + }, + {} + ); + } + + return `--cnvs-${token.path.join('-')}`; +}; + +/** + * Utility function to return token original values + * @param {Object} token the token object + * @param {Function} getRefs: style dictionary getReferences function + * @returns the updated token composite value + */ +type ReturnValues = {value: string; comment?: string}; +export const getOriginalValues = ( + token: TransformedToken +): Record | ReturnValues => { + const {value: tokenValue, comment} = token; + + if (isComposite(token) && typeof tokenValue !== 'string') { + return Object.entries(tokenValue).reduce( + (acc: Record, [key, value]: string[]) => { + const name = camelCase(key); + return { + ...acc, + [name]: {value: resolveMathExpressions(value), comment}, + }; + }, + {} + ); + } + + return {value: resolveMathExpressions(tokenValue), comment}; +}; + +const resolveMathExpressions = (value: string): string => { + if (isMathExpression({value} as TransformedToken)) { + const cleanExpression = value.replace(/rem/g, ''); + return `${math.evaluate(cleanExpression)}rem`; + } + + return value; +}; diff --git a/packages/canvas-tokens/utils/formatters/helpers/formattedObjectValue.ts b/packages/canvas-tokens/utils/formatters/helpers/formattedObjectValue.ts deleted file mode 100644 index 21f0b49..0000000 --- a/packages/canvas-tokens/utils/formatters/helpers/formattedObjectValue.ts +++ /dev/null @@ -1,100 +0,0 @@ -import {Dictionary, TransformedToken} from 'style-dictionary'; -import {camelCase} from 'case-anything'; -import {isComposite} from '../../filters'; - -type HelperArgs = { - format: 'sys' | 'brand'; - dictionary: Dictionary; -}; - -type CompositeHelper = (args: HelperArgs) => Record; - -/** - * Style Dictionary format helper that transform dictionary properties object to the level specific object. All keys are changed to camel case and values to CSS variables. - * @param {HelperArgs} HelperArgs - arguments of format helper function, includes properties: - * `dictionary` as tyle Dictionary dictionary object, - * `format`: 'sys' or 'brand' value. - * @returns updated token object with cameled case keys and a css variable name as value. - */ - -export const formattedObjectValue: CompositeHelper = ({format, dictionary}) => { - const parsed = dictionary.properties[format]; - const filteredTokens = dictionary.allTokens.filter( - ({path: [ctg]}) => ctg !== 'base' && ctg === format - ); - - return filteredTokens.reduce((acc: Record, {path}: TransformedToken) => { - const passedPath = path.slice(1); - - setProperty({ - src: parsed, - output: acc, - path: passedPath, - valueFn: token => { - const originalValue = token.original.value; - // only change composite values - return isComposite(token) && typeof originalValue !== 'string' - ? updateCompositeValues(originalValue, dictionary.getReferences) - : token.value; - }, - }); - - return acc; - }, {}); -}; - -type RecursionHelperArgs = { - src: Record; - output: Record; - path: string[]; - valueFn: Dictionary['getReferences']; -}; - -/** - * Recursive utility function that iterates through a src object and fills output object with the updated value. - * @param {RecursionHelperArgs} RecursionHelperArgs - arguments of the recursive function: - * `src`: src object containing all token info; - * `output`: 'new object to fill; - * `path`: the path token array to iterate through - * `valueFn` function that changing token value. - */ - -const setProperty = ({src, output, path, valueFn}: RecursionHelperArgs): void => { - const [head, ...rest] = path; - const key = camelCase(head); - - // set empty object as value if inner object doesn't exist yet - if (!output[key]) { - output[key] = {}; - } - - // stop iterate if it's end of path and apply function to change token - if (!rest.length) { - output[key] = valueFn(src.value ? src : src[head]); - return; - } - - setProperty({ - src: src[head], - output: output[key], - path: rest, - valueFn, - }); -}; - -/** - * Utility function to change token composite values to css var and keys to camel case - * @param {Object} token the token object - * @param {Function} getRefs: style dictionary getReferences function - * @returns the updated token composite value - */ -const updateCompositeValues = ( - values: Record, - getRefs: Dictionary['getReferences'] -) => - Object.entries(values).reduce((acc: Record, [key, value]: string[]) => { - // composite values have single reference - const [ref] = getRefs(value); - const name = camelCase(key); - return {...acc, [name]: `--cnvs-${ref.path.join('-')}`}; - }, {}); diff --git a/packages/canvas-tokens/utils/formatters/index.ts b/packages/canvas-tokens/utils/formatters/index.ts index 66b5736..ac02114 100644 --- a/packages/canvas-tokens/utils/formatters/index.ts +++ b/packages/canvas-tokens/utils/formatters/index.ts @@ -1,13 +1,15 @@ import {Formatter} from 'style-dictionary'; import { - formatToInlineModule, + formatToInlineCommonJSModule, + formatToInlineES6Module, + formatInlineTypes, formatES6ToObjects, formatCommonToObjects, formatES6Exports, formatCommonJSExports, - formatES6ToTypes, } from './formatJS'; import {formatCSSComposite, formatLessComposite, formatSassComposite} from './formatStyles'; +import {formatJSToTypes} from './formatTypes'; import {mergeObjects} from './mergeObjects'; import {mergeTypes} from './mergeTypes'; import {mergeStyleReferences} from './mergeStyleReferences'; @@ -15,13 +17,18 @@ import {mergeStyleReferences} from './mergeStyleReferences'; export const formats: Record = { // formatter creating the inline common-js file structure // with separated variables of tokens - 'javascript/common-js': formatToInlineModule, + 'js/common-js': formatToInlineCommonJSModule, + // formatter creating the inline es6 file structure + // with separated variables of tokens + 'js/es6': formatToInlineES6Module, + // formatter creating the es6 and common-js inline types including the `as const` + 'ts/inline': formatInlineTypes, // formatter creating the es6 file structure // with tokens united in objects 'es6/objects': formatES6ToObjects, - // formatter creating the es6 and common-js types including the `as const` - 'es6/types': formatES6ToTypes, - 'common-js/types': formatES6ToTypes, + // formatter creating the es6 and common-js types + // including the `as const` and JSDoc description + 'ts/jsdoc-object': formatJSToTypes, // formatter creating the common-js file structure // with tokens united in objects 'common-js/objects': formatCommonToObjects, diff --git a/packages/canvas-tokens/utils/formatters/mergeObjects.ts b/packages/canvas-tokens/utils/formatters/mergeObjects.ts index ad27e91..7ee4f5a 100644 --- a/packages/canvas-tokens/utils/formatters/mergeObjects.ts +++ b/packages/canvas-tokens/utils/formatters/mergeObjects.ts @@ -1,5 +1,9 @@ -import StyleDictionary, {Formatter, Options} from 'style-dictionary'; -import {formattedObjectValue} from './helpers/formattedObjectValue'; +import StyleDictionary, {Formatter, Options, TransformedToken} from 'style-dictionary'; +import { + formattedObjectInnerValues, + changeValuesToCSSVars, + getOriginalValues, +} from './helpers/formattedObjectInnerValues'; interface ExtendedOptions extends Options { formats: string | Formatter[]; @@ -25,17 +29,22 @@ export const mergeObjects: Formatter = ({dictionary, options, ...rest}) => { level, } = options as ExtendedOptions; - const properties = formattedObjectValue({ + const properties = formattedObjectInnerValues({ format: level, - dictionary: { - ...dictionary, - getReferences: value => dictionary.getReferences(value), - }, + dictionary, + changeValueFn: (token: TransformedToken) => + changeValuesToCSSVars(token, value => dictionary.getReferences(value)), + }); + + const originalValues = formattedObjectInnerValues({ + format: level, + dictionary, + changeValueFn: getOriginalValues, }); const params = { dictionary: {...dictionary, properties}, - options, + options: {...options, originalValues}, ...rest, }; diff --git a/packages/canvas-tokens/utils/formatters/mergeTypes.ts b/packages/canvas-tokens/utils/formatters/mergeTypes.ts index 1a8dbe2..2201ec3 100644 --- a/packages/canvas-tokens/utils/formatters/mergeTypes.ts +++ b/packages/canvas-tokens/utils/formatters/mergeTypes.ts @@ -16,10 +16,8 @@ export const mergeTypes: Formatter = params => { const content = StyleDictionary.format[defaultFormat]({ ...params, - options: {...options, formats: restFormats, withoutModule: true}, + options: {...options, formats: restFormats}, }); - return content - .replace(/exports.default = exports./g, 'export default') - .replace(/exports.|export const /g, 'export declare const '); + return content; }; diff --git a/packages/canvas-tokens/utils/spec/formats.spec.ts b/packages/canvas-tokens/utils/spec/formats.spec.ts index 8609cef..2b65e06 100644 --- a/packages/canvas-tokens/utils/spec/formats.spec.ts +++ b/packages/canvas-tokens/utils/spec/formats.spec.ts @@ -15,6 +15,8 @@ jest.mock('style-dictionary', () => ({ const [first] = Object.keys(dictionary.properties); return `exports.${first} = ` + JSON.stringify(dictionary.properties[first], null, 2); }, + 'javascript/types': () => + `export declare const opacity = {\n "disabled": "--cnvs-base-opacity-300"\n}`, 'javascript/common-js': () => `exports.cinnamon100 = "--cnvs-base-palette-cinnamon-100";`, 'css/variables': () => `:root {\n --cnvs-sys-shape-zero: 0rem;\n}`, 'css/composite': () => @@ -40,7 +42,7 @@ describe('formats', () => { { name: 'cinnamon100', value: '#ffefee', - path: [''], + path: ['base', 'palette', 'cinnamon', '100'], filePath: '', isSource: true, original: {value: '#ffefee'}, @@ -48,21 +50,11 @@ describe('formats', () => { { name: 'cinnamon200', value: '#fcc9c5', - path: [''], + path: ['base', 'palette', 'cinnamon', '200'], filePath: '', isSource: true, original: {value: '#fcc9c5'}, }, - { - value: {border: '0.0625rem solid rgba(162,171,180,1)'}, - type: 'composition', - filePath: 'tokens/all.json', - isSource: true, - original: {value: {border: '{sys.line.disabled}'}, type: 'composition'}, - name: 'cnvs-sys-border-input-disabled', - attributes: {}, - path: ['sys', 'border', 'input', 'disabled'], - }, ], getReferences: () => [ { @@ -92,18 +84,40 @@ describe('formats', () => { }; }); - describe('javascript/common-js', () => { + describe('js/common-js', () => { it('should return correct file structure as inline js vars', () => { - const result = formats['javascript/common-js'](defaultArgs); + const result = formats['js/common-js'](defaultArgs); const expected = headerContent + moduleContent + - `exports.cinnamon100 = "#ffefee";\nexports.cinnamon200 = "#fcc9c5";\nexports.cnvs-sys-border-input-disabled = "[object Object]";\n`; + `exports.cinnamon100 = "--cnvs-base-palette-cinnamon-100";\nexports.cinnamon200 = "--cnvs-base-palette-cinnamon-200";\n`; + + expect(result).toBe(expected); + }); + }); + + describe('js/es6', () => { + it('should return correct file structure as inline js vars', () => { + const result = formats['js/es6'](defaultArgs); + const expected = + headerContent + + `export const cinnamon100 = "--cnvs-base-palette-cinnamon-100";\nexport const cinnamon200 = "--cnvs-base-palette-cinnamon-200";\n`; expect(result).toBe(expected); }); }); + describe('ts/inline', () => { + it('should return correct file structure as inline js vars', () => { + const result = formats['ts/inline'](defaultArgs); + const expected = + headerContent + + `export declare const cinnamon100 = "--cnvs-base-palette-cinnamon-100" as const;\nexport declare const cinnamon200 = "--cnvs-base-palette-cinnamon-200" as const;\n`; + + expect(result).toBe(expected); //? + }); + }); + describe('es6/objects', () => { it('should return correct file structure', () => { const result = formats['es6/objects']({ @@ -262,7 +276,7 @@ describe('formats', () => { }); const expected = '.cnvs-sys-border-input-disabled {\n border: @cnvs-sys-line-disabled;\n}'; - expect(result).toBe(expected); //? + expect(result).toBe(expected); }); }); @@ -314,7 +328,7 @@ describe('formats', () => { const result = formats['merge/types']({ ...defaultArgs, options: { - formats: ['javascript/es6'], + formats: ['javascript/types'], level: 'sys', }, dictionary: { @@ -329,31 +343,23 @@ describe('formats', () => { const expected = 'export declare const opacity = {\n "disabled": "--cnvs-base-opacity-300"\n}'; - expect(result).toBe(expected); - }); - - it('should return correct file structure for common-js', () => { - const result = formats['merge/types']({ - ...defaultArgs, - options: { - formats: ['javascript/common-js'], - level: 'sys', - }, - }); - - const expected = 'export declare const cinnamon100 = "--cnvs-base-palette-cinnamon-100";'; - - expect(result).toBe(expected); // + expect(result).toBe(expected); //? }); }); - describe('es6/types', () => { - it('should return correct file structure for es6', () => { - const result = formats['es6/types']({ + describe('ts/jsdoc-object', () => { + it('should return correct file structure with between line JSDoc', () => { + const result = formats['ts/jsdoc-object']({ ...defaultArgs, options: { - formats: ['javascript/es6'], - level: 'sys', + originalValues: { + opacity: { + disabled: { + comment: 'Test JSDoc', + value: '0.4', + }, + }, + }, }, dictionary: { properties: { @@ -366,20 +372,22 @@ describe('formats', () => { const expected = headerContent + - 'export declare const opacity = {\n "disabled": "--cnvs-base-opacity-300"\n} as const;' + - '\n\n'; + 'export declare const opacity = {\n /**\n * Test JSDoc\n * 0.4 \n */\n "disabled": "--cnvs-base-opacity-300",\n} as const;\n'; - expect(result).toBe(expected); + expect(result).toBe(expected); //? }); - }); - describe('common-js/types', () => { - it('should return correct file structure for es6', () => { - const result = formats['es6/types']({ + it('should have one line jsDoc for tokens without comment', () => { + const result = formats['ts/jsdoc-object']({ ...defaultArgs, options: { - formats: ['javascript/common-js'], - level: 'sys', + originalValues: { + opacity: { + disabled: { + value: '0.4', + }, + }, + }, }, dictionary: { properties: { @@ -392,8 +400,7 @@ describe('formats', () => { const expected = headerContent + - 'export declare const opacity = {\n "disabled": "--cnvs-base-opacity-300"\n} as const;' + - '\n\n'; + 'export declare const opacity = {\n /** 0.4 */\n "disabled": "--cnvs-base-opacity-300",\n} as const;\n'; expect(result).toBe(expected); }); diff --git a/packages/canvas-tokens/utils/spec/helpers.spec.ts b/packages/canvas-tokens/utils/spec/helpers.spec.ts index 8b9bff7..d674610 100644 --- a/packages/canvas-tokens/utils/spec/helpers.spec.ts +++ b/packages/canvas-tokens/utils/spec/helpers.spec.ts @@ -1,35 +1,75 @@ import {formattedCompositeStyles} from '../formatters/helpers/formattedCompositeStyles'; -import {formattedObjectValue} from '../formatters/helpers/formattedObjectValue'; +import { + formattedObjectInnerValues, + changeValuesToCSSVars, + getOriginalValues, +} from '../formatters/helpers/formattedObjectInnerValues'; + +const mockCompositeToken = { + value: {color: '{base.pallete.blueberry.400}'}, + comment: 'Use for primary background', + type: 'composition', + filePath: 'tokens/all.json', + isSource: true, + original: { + value: {color: '{base.pallete.blueberry.400}'}, + type: 'color', + }, + name: 'cnvs-sys-color-primary', + attributes: {}, + path: ['sys', 'color', 'primary'], +}; + +const mockMathToken = { + value: '0.25rem * 4', + comment: 'Use for spaces between elements', + type: 'space', + filePath: 'tokens/all.json', + isSource: true, + original: { + value: '{base.unit} * 4', + type: 'space', + }, + name: 'cnvs-sys-space-x', + attributes: {}, + path: ['sys', 'space', 'x'], +}; + +const mockBrandToken = { + value: 'blue', + type: 'color', + filePath: 'tokens/all.json', + isSource: true, + original: { + value: '{base.pallete.blueberry.400}', + type: 'color', + }, + name: 'brandPrimaryBase', + attributes: {}, + path: ['brand', 'primary', 'base'], +}; + +const mockBaseToken = { + value: '--cnvs-base-palette-blueberry-400', + type: 'color', + filePath: 'tokens/all.json', + isSource: true, + original: { + value: 'blue', + type: 'color', + }, + name: 'basePaletteBlueberry400', + attributes: {}, + path: ['base', 'palette', 'blueberry', '400'], +}; const mockDicttionary = { - allTokens: [ - { - value: {border: '0.0625rem solid rgba(162,171,180,1)'}, - type: 'composition', - filePath: 'tokens/all.json', - isSource: true, - original: {value: {border: '{sys.line.disabled}'}, type: 'composition'}, - name: 'cnvs-sys-border-input-disabled', - attributes: {}, - path: ['sys', 'border', 'input', 'disabled'], - }, - { - value: '--cnvs-brand-primary-base', - type: 'fontSizes', - filePath: 'tokens/all.json', - isSource: true, - original: { - value: '{base.blueberry.400}', - type: 'color', - }, - name: 'brandPrimaryBase', - attributes: {}, - path: ['brand', 'primary', 'base'], - }, - ], + allTokens: [mockBrandToken, mockMathToken, mockBaseToken, mockCompositeToken], + properties: {brand: {primary: {base: mockBrandToken}}}, tokens: {sys: {}}, allProperties: [], usesReference: () => true, + getReferences: () => [], }; describe('format helpers', () => { @@ -39,107 +79,61 @@ describe('format helpers', () => { format: str => `@${str}`, }); - const expected = '.cnvs-sys-border-input-disabled {\n border: @sys-line-disabled;\n}'; + const expected = `.cnvs-sys-color-primary {\n color: @base-pallete-blueberry-400;\n}`; expect(result).toStrictEqual(expected); }); it('should transform token objects to new values', () => { - const token = { - value: '--cnvs-brand-primary-base', - type: 'fontSizes', - filePath: 'tokens/all.json', - isSource: true, - original: { - value: '{base.blueberry.400}', - type: 'color', - }, - name: 'brandPrimaryBase', - attributes: {}, - path: ['brand', 'primary', 'base'], - }; - const result = formattedObjectValue({ - dictionary: { - ...mockDicttionary, - allTokens: [token], - properties: { - brand: { - primary: { - base: token, - }, - }, - }, - getReferences: () => [ - { - value: '--cnvs-base-palette-blueberry-400', - type: 'fontSizes', - filePath: 'tokens/all.json', - isSource: true, - original: { - value: '#0875E2', - type: 'color', - }, - name: 'bluberry400', - attributes: {}, - path: ['base', 'palette', '400'], - }, - ], - }, + const result = formattedObjectInnerValues({ + dictionary: mockDicttionary, format: 'brand', + changeValueFn: () => 'testValue', }); - const expected = {primary: {base: '--cnvs-brand-primary-base'}}; + const expected = {primary: {base: 'testValue'}}; + + expect(result).toStrictEqual(expected); //? + }); +}); + +describe('utils to change value', () => { + it('should transform single value token to css variable', () => { + const result = changeValuesToCSSVars(mockBrandToken, () => [mockBaseToken]); + + const expected = '--cnvs-brand-primary-base'; + + expect(result).toStrictEqual(expected); //? + }); + + it('should transform composite tokens into css rule sets', () => { + const result = changeValuesToCSSVars(mockCompositeToken, () => [mockBaseToken]); + + const expected = {color: '--cnvs-base-palette-blueberry-400'}; expect(result).toStrictEqual(expected); }); - it('should transform token objects to new values', () => { - const borderToken = { - value: '--cnvs-sys-border-input-disabled', - type: 'composition', - filePath: 'tokens/all.json', - isSource: true, - original: { - value: '{sys.line.disabled}', - type: 'composition', + it('should transform to original value', () => { + const result = getOriginalValues(mockCompositeToken); + + const expected = { + color: { + comment: 'Use for primary background', + value: '{base.pallete.blueberry.400}', }, - name: 'borderInputDisabled', - attributes: {}, - path: ['sys', 'border', 'input', 'disabled'], }; - const result = formattedObjectValue({ - dictionary: { - ...mockDicttionary, - allTokens: [borderToken], - properties: { - sys: { - border: { - input: { - disabled: borderToken, - }, - }, - }, - }, - getReferences: () => [ - { - value: '--cnv-sys-line-disabled', - type: 'border', - filePath: 'tokens/all.json', - isSource: true, - original: { - value: '1px solid #0875E2', - type: 'color', - }, - name: 'lineDefault', - attributes: {}, - path: ['sys', 'line', 'disabled'], - }, - ], - }, - format: 'sys', - }); - const expected = {border: {input: {disabled: '--cnvs-sys-border-input-disabled'}}}; + expect(result).toStrictEqual(expected); + }); + + it('should transform to original math value', () => { + const result = getOriginalValues(mockMathToken); + + const expected = { + comment: 'Use for spaces between elements', + value: '1rem', + }; expect(result).toStrictEqual(expected); }); diff --git a/packages/canvas-tokens/utils/tokenStudioParser.ts b/packages/canvas-tokens/utils/tokenStudioParser.ts index a99daee..e8af4ae 100644 --- a/packages/canvas-tokens/utils/tokenStudioParser.ts +++ b/packages/canvas-tokens/utils/tokenStudioParser.ts @@ -20,6 +20,7 @@ export const tokenStudioParser = ({contents}: any) => { } const updateTokens = (token: DesignToken) => { + replaceDescriptionByComment(token); transformExtensions(token); transformRefs(token); return token; @@ -36,6 +37,15 @@ const getLevel = (ref: string) => { return !levels.includes(key) && 'base'; }; +const replaceDescriptionByComment = (token: DesignToken) => { + const {description} = token; + if (description) { + delete token.description; + const updated = description.replace(/\n+/g, '; '); + token.comment = updated; + } +}; + const transformRefs = (token: DesignToken) => { const {value} = token; diff --git a/packages/canvas-tokens/utils/transformers/transformNameToCamelCase.ts b/packages/canvas-tokens/utils/transformers/transformNameToCamelCase.ts index a97b6fd..f7ecda7 100644 --- a/packages/canvas-tokens/utils/transformers/transformNameToCamelCase.ts +++ b/packages/canvas-tokens/utils/transformers/transformNameToCamelCase.ts @@ -10,10 +10,10 @@ type Transformer = (token: DesignToken) => string; * @returns updated token name */ export const transformNameToCamelCase: Transformer = token => { - const name = token.path; - const [, ...rest] = name; - const [tokenName, ...restName] = rest; - - const value = tokenName !== 'unit' ? (tokenName === 'palette' ? restName : rest) : name; + const fullName = token.path; + const [_, ...nameWithCategory] = fullName; + const [category, ...tokenName] = nameWithCategory; + const isLowLevelToken = ['unit', 'level'].includes(category); + const value = isLowLevelToken ? fullName : category === 'palette' ? tokenName : nameWithCategory; return camelCase(value.join('-')); };