From c6f482e2845f8e13e579ffc858b0e53b9155a47e Mon Sep 17 00:00:00 2001 From: Danny Banks Date: Wed, 15 Dec 2021 15:42:46 -0800 Subject: [PATCH] feat(references): ability to reference other tokens without 'value' (#746) * feat(references): ability to reference other tokens without 'value' * chore(tests): add more tests for dot value --- __integration__/android.test.js | 2 +- __integration__/compose.test.js | 2 +- __integration__/css.test.js | 2 +- __integration__/flutter.test.js | 2 +- __integration__/iOSObjectiveC.test.js | 2 +- __integration__/logging/file.test.js | 12 +- __integration__/outputReferences.test.js | 2 +- __integration__/scss.test.js | 2 +- __integration__/swift.test.js | 2 +- .../tokens/color/{font.json => font.jsonc} | 7 +- __tests__/exportPlatform.test.js | 175 ++++++++++++++++++ lib/utils/resolveObject.js | 11 ++ 12 files changed, 204 insertions(+), 17 deletions(-) rename __integration__/tokens/color/{font.json => font.jsonc} (85%) diff --git a/__integration__/android.test.js b/__integration__/android.test.js index 83f0d2c8b..b554f61e3 100644 --- a/__integration__/android.test.js +++ b/__integration__/android.test.js @@ -18,7 +18,7 @@ const {buildPath} = require('./_constants'); describe('integration', () => { describe('android', () => { StyleDictionary.extend({ - source: [`__integration__/tokens/**/*.json`], + source: [`__integration__/tokens/**/*.json?(c)`], platforms: { android: { transformGroup: `android`, diff --git a/__integration__/compose.test.js b/__integration__/compose.test.js index 6a6b88979..5e5774204 100644 --- a/__integration__/compose.test.js +++ b/__integration__/compose.test.js @@ -18,7 +18,7 @@ const {buildPath} = require('./_constants'); describe('integration', () => { describe('compose', () => { StyleDictionary.extend({ - source: [`__integration__/tokens/**/*.json`], + source: [`__integration__/tokens/**/*.json?(c)`], platforms: { compose: { transformGroup: `compose`, diff --git a/__integration__/css.test.js b/__integration__/css.test.js index cfc4dd50f..86f5b906b 100644 --- a/__integration__/css.test.js +++ b/__integration__/css.test.js @@ -18,7 +18,7 @@ const {buildPath} = require('./_constants'); describe('integration', () => { describe('css', () => { StyleDictionary.extend({ - source: [`__integration__/tokens/**/*.json`], + source: [`__integration__/tokens/**/*.json?(c)`], // Testing proper string interpolation with multiple references here. // This is a CSS/web-specific thing so only including them in this // integration test. diff --git a/__integration__/flutter.test.js b/__integration__/flutter.test.js index f06f5994f..fa7ffe90e 100644 --- a/__integration__/flutter.test.js +++ b/__integration__/flutter.test.js @@ -18,7 +18,7 @@ const {buildPath} = require('./_constants'); describe('integration', () => { describe('flutter', () => { StyleDictionary.extend({ - source: [`__integration__/tokens/**/*.json`], + source: [`__integration__/tokens/**/*.json?(c)`], platforms: { flutter: { transformGroup: `flutter`, diff --git a/__integration__/iOSObjectiveC.test.js b/__integration__/iOSObjectiveC.test.js index 3da74aedd..d1e7b41c8 100644 --- a/__integration__/iOSObjectiveC.test.js +++ b/__integration__/iOSObjectiveC.test.js @@ -18,7 +18,7 @@ const {buildPath} = require('./_constants'); describe('integration', () => { describe('ios objective-c', () => { StyleDictionary.extend({ - source: [`__integration__/tokens/**/*.json`], + source: [`__integration__/tokens/**/*.json?(c)`], platforms: { flutter: { transformGroup: `ios`, diff --git a/__integration__/logging/file.test.js b/__integration__/logging/file.test.js index 5663a41b8..6617d3e54 100644 --- a/__integration__/logging/file.test.js +++ b/__integration__/logging/file.test.js @@ -39,7 +39,7 @@ describe(`integration`, () => { describe(`file`, () => { it(`should warn user empty properties`, () => { StyleDictionary.extend({ - source: [`__integration__/tokens/**/*.json`], + source: [`__integration__/tokens/**/*.json?(c)`], platforms: { css: { transformGroup: `css`, @@ -58,7 +58,7 @@ describe(`integration`, () => { it(`should not warn user of empty properties with log level set to error`, () => { StyleDictionary.extend({ logLevel: `error`, - source: [`__integration__/tokens/**/*.json`], + source: [`__integration__/tokens/**/*.json?(c)`], platforms: { css: { transformGroup: `css`, @@ -75,7 +75,7 @@ describe(`integration`, () => { it(`should warn user of name collisions`, () => { StyleDictionary.extend({ - source: [`__integration__/tokens/**/*.json`], + source: [`__integration__/tokens/**/*.json?(c)`], platforms: { css: { // no name transform means there will be name collisions @@ -95,7 +95,7 @@ describe(`integration`, () => { it(`should not warn user of name collisions with log level set to error`, () => { StyleDictionary.extend({ logLevel: `error`, - source: [`__integration__/tokens/**/*.json`], + source: [`__integration__/tokens/**/*.json?(c)`], platforms: { css: { // no name transform means there will be name collisions @@ -114,7 +114,7 @@ describe(`integration`, () => { it(`should warn user of filtered references`, () => { StyleDictionary.extend({ - source: [`__integration__/tokens/**/*.json`], + source: [`__integration__/tokens/**/*.json?(c)`], platforms: { css: { transformGroup: `css`, @@ -138,7 +138,7 @@ describe(`integration`, () => { it(`should not warn user of filtered references with log level set to error`, () => { StyleDictionary.extend({ logLevel: `error`, - source: [`__integration__/tokens/**/*.json`], + source: [`__integration__/tokens/**/*.json?(c)`], platforms: { css: { transformGroup: `css`, diff --git a/__integration__/outputReferences.test.js b/__integration__/outputReferences.test.js index afa172b64..552c1608e 100644 --- a/__integration__/outputReferences.test.js +++ b/__integration__/outputReferences.test.js @@ -22,7 +22,7 @@ describe('integration', () => { StyleDictionary.extend({ // we are only testing showFileHeader options so we don't need // the full source. - source: [`__integration__/tokens/**/*.json`], + source: [`__integration__/tokens/**/*.json?(c)`], platforms: { css: { transformGroup: 'css', diff --git a/__integration__/scss.test.js b/__integration__/scss.test.js index 4519ec9ab..c013bd8fe 100644 --- a/__integration__/scss.test.js +++ b/__integration__/scss.test.js @@ -19,7 +19,7 @@ const {buildPath} = require('./_constants'); describe(`integration`, () => { describe(`scss`, () => { StyleDictionary.extend({ - source: [`__integration__/tokens/**/*.json`], + source: [`__integration__/tokens/**/*.json?(c)`], platforms: { css: { transformGroup: `scss`, diff --git a/__integration__/swift.test.js b/__integration__/swift.test.js index 44bd170fd..6e5b103e2 100644 --- a/__integration__/swift.test.js +++ b/__integration__/swift.test.js @@ -18,7 +18,7 @@ const {buildPath} = require('./_constants'); describe('integration', () => { describe('swift', () => { StyleDictionary.extend({ - source: [`__integration__/tokens/**/*.json`], + source: [`__integration__/tokens/**/*.json?(c)`], platforms: { flutter: { transformGroup: `ios-swift`, diff --git a/__integration__/tokens/color/font.json b/__integration__/tokens/color/font.jsonc similarity index 85% rename from __integration__/tokens/color/font.json rename to __integration__/tokens/color/font.jsonc index d15b7cc29..cfa6fed10 100644 --- a/__integration__/tokens/color/font.json +++ b/__integration__/tokens/color/font.jsonc @@ -4,17 +4,18 @@ "primary": { "value": "{color.core.neutral.1100.value}" }, "secondary": { "value": "{color.core.neutral.900.value}" }, "tertiary": { "value": "{color.core.neutral.800.value}" }, - + "interactive": { "_": { "value": "{color.brand.primary.value}" }, "hover": { "value": "{color.brand.primary.value}" }, "active": { "value": "{color.brand.secondary.value}" }, "disabled": { "value": "{color.font.tertiary.value}" } }, - + "danger": { "value": "{color.core.red.1000.value}" }, "warning": { "value": "{color.core.orange.1000.value}" }, - "success": { "value": "{color.core.green.1000.value}" } + // make sure references without .value work too + "success": { "value": "{color.core.green.1000}" } } } } \ No newline at end of file diff --git a/__tests__/exportPlatform.test.js b/__tests__/exportPlatform.test.js index 6602f6472..a18a522be 100644 --- a/__tests__/exportPlatform.test.js +++ b/__tests__/exportPlatform.test.js @@ -187,4 +187,179 @@ describe('exportPlatform', () => { }); }); + it('should handle .value and non .value references per the W3C spec', () => { + const tokens = { + colors: { + red: { value: '#f00' }, + error: { value: '{colors.red}' }, + danger: { value: '{colors.error}' }, + alert: { value: '{colors.error.value}' }, + } + } + + const expected = { + colors: { + red: { + value: '#f00', + name: 'colors-red', + path: ['colors','red'], + attributes: { + category: 'colors', + type: 'red' + }, + original: { + value: '#f00' + } + }, + error: { + value: '#f00', + name: 'colors-error', + path: ['colors','error'], + attributes: { + category: 'colors', + type: 'error' + }, + original: { + value: '{colors.red}' + } + }, + danger: { + value: '#f00', + name: 'colors-danger', + path: ['colors','danger'], + attributes: { + category: 'colors', + type: 'danger' + }, + original: { + value: '{colors.error}' + } + }, + alert: { + value: '#f00', + name: 'colors-alert', + path: ['colors','alert'], + attributes: { + category: 'colors', + type: 'alert' + }, + original: { + value: '{colors.error.value}' + } + }, + } + } + + const actual = StyleDictionary.extend({ + tokens, + platforms: { + css: { + transformGroup: `css` + } + } + }).exportPlatform('css'); + expect(actual).toEqual(expected); + }); + + describe('token references without .value', () => { + const tokens = { + color: { + red: { value: '#f00' }, + error: { value: '{color.red}' }, + errorWithValue: { value: '{color.red.value}' }, + danger: { value: '{color.error}' }, + dangerWithValue: { value: '{color.error.value}' }, + dangerErrorValue: { value: '{color.errorWithValue}' } + } + } + + const actual = StyleDictionary.extend({ + tokens, + platforms: { + css: { + transformGroup: 'css' + } + } + }).exportPlatform('css'); + + it('should work if referenced directly', () => { + expect(actual.color.error.value).toEqual('#ff0000'); + }); + it('should work if there is a transitive reference', () => { + expect(actual.color.danger.value).toEqual('#ff0000'); + }); + it('should work if there is a transitive reference with .value', () => { + expect(actual.color.errorWithValue.value).toEqual('#ff0000'); + expect(actual.color.dangerWithValue.value).toEqual('#ff0000'); + expect(actual.color.dangerErrorValue.value).toEqual('#ff0000'); + }); + }); + + describe('non-token references', () => { + const tokens = { + nonTokenColor: 'hsl(10,20%,20%)', + hue: { + red: '10', + green: '120', + blue: '220' + }, + comment: 'hello', + color: { + red: { + // Note having references as part of the value, + // either in an object like this, or in an interpolated + // string like below, requires the use of transitive + // transforms if you want it to be transformed. + value: { + h: '{hue.red}', + s: '100%', + l: '50%' + } + }, + blue: { + value: '{nonTokenColor}', + comment: '{comment}' + }, + green: { + value: 'hsl({hue.green}, 50%, 50%)' + } + } + } + + // making the css/color transform transitive so we can be sure the references + // get resolved properly and transformed. + const transitiveTransform = Object.assign({}, + StyleDictionary.transform['color/css'], + {transitive: true} + ); + + const actual = StyleDictionary.extend({ + tokens, + transform: { + transitiveTransform + }, + platforms: { + css: { + transforms: [ + 'attribute/cti', + 'name/cti/kebab', + 'transitiveTransform' + ] + } + } + }).exportPlatform('css'); + + it('should work if referenced directly', () => { + expect(actual.color.blue.value).toEqual('#3d2c29'); + }); + it('should work if referenced from a non-value', () => { + expect(actual.color.blue.comment).toEqual(tokens.comment); + }); + it('should work if interpolated', () => { + expect(actual.color.green.value).toEqual('#40bf40'); + }); + it('should work if part of an object value', () => { + expect(actual.color.red.value).toEqual('#ff2b00'); + }); + }); }); diff --git a/lib/utils/resolveObject.js b/lib/utils/resolveObject.js index de3495013..2109e4ae5 100644 --- a/lib/utils/resolveObject.js +++ b/lib/utils/resolveObject.js @@ -94,8 +94,19 @@ function compile_value(value, stack) { // Find what the value is referencing const pathName = getPath(variable, options); const context = getName(current_context, options); + const refHasValue = pathName[pathName.length-1] === 'value'; ref = resolveReference(pathName, updated_object); + // If the reference doesn't end in 'value' + // and + // the reference points to someplace that has a `value` attribute + // we should take the '.value' of the reference + // per the W3C draft spec where references do not have .value + // https://design-tokens.github.io/community-group/format/#aliases-references + if (!refHasValue && ref && ref.hasOwnProperty('value')) { + ref = ref.value; + } + if (typeof ref !== 'undefined') { if (typeof ref === 'string') { to_ret = value.replace(match, ref);