From fa88ffdfa593776d6c694f3b93a1e5f71856e5b3 Mon Sep 17 00:00:00 2001 From: Danny Banks Date: Tue, 11 May 2021 13:00:30 -0700 Subject: [PATCH] fix(references): value object references now work --- .../__snapshots__/objectValues.test.js.snap | 93 ++++++++ __integration__/objectValues.test.js | 211 ++++++++++++++++++ .../utils/reference/getReferences.test.js | 78 +++++++ .../formatHelpers/createPropertyFormatter.js | 49 ++-- lib/common/formatHelpers/sortByReference.js | 4 +- lib/utils/references/getReferences.js | 22 +- 6 files changed, 435 insertions(+), 22 deletions(-) create mode 100644 __integration__/__snapshots__/objectValues.test.js.snap create mode 100644 __integration__/objectValues.test.js create mode 100644 __tests__/utils/reference/getReferences.test.js diff --git a/__integration__/__snapshots__/objectValues.test.js.snap b/__integration__/__snapshots__/objectValues.test.js.snap new file mode 100644 index 000000000..7fc7c7478 --- /dev/null +++ b/__integration__/__snapshots__/objectValues.test.js.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`integration object values css/variables border should match snapshot 1`] = ` +"/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --border-primary: 0.125rem solid #ff0000; +} +" +`; + +exports[`integration object values css/variables border with references should match snapshot 1`] = ` +"/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --border-primary: var(--size-border) solid var(--color-red); +} +" +`; + +exports[`integration object values css/variables hex syntax should match snapshot 1`] = ` +"/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --color-red: #ff0000; + --color-green: #40bf40; +} +" +`; + +exports[`integration object values css/variables hex syntax with references should match snapshot 1`] = ` +"/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --color-red: #ff0000; + --color-green: #40bf40; +} +" +`; + +exports[`integration object values css/variables hsl syntax should match snapshot 1`] = ` +"/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --color-red: #ff0000; + --color-green: hsl(120, 50%, 50%); +} +" +`; + +exports[`integration object values css/variables hsl syntax with references should match snapshot 1`] = ` +"/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --color-red: #ff0000; + --color-green: hsl(120, 50%, 50%); +} +" +`; + +exports[`integration object values scss/variables should match snapshot 1`] = ` +" +// Do not edit directly +// Generated on Sat, 01 Jan 2000 00:00:00 GMT + +$border-primary: 0.125rem solid #ff0000;" +`; + +exports[`integration object values scss/variables with references should match snapshot 1`] = ` +" +// Do not edit directly +// Generated on Sat, 01 Jan 2000 00:00:00 GMT + +$border-primary: $size-border solid $color-red;" +`; diff --git a/__integration__/objectValues.test.js b/__integration__/objectValues.test.js new file mode 100644 index 000000000..addad4e3d --- /dev/null +++ b/__integration__/objectValues.test.js @@ -0,0 +1,211 @@ +/* + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +const fs = require('fs-extra'); +const Color = require('tinycolor2'); +const StyleDictionary = require('../index'); +const {buildPath} = require('./_constants'); + +const options = { + outputReferences: true +} + +describe('integration', () => { + describe('object values', () => { + StyleDictionary.extend({ + properties: { + hue: `120`, + saturation: `50%`, + lightness: `50%`, + color: { + red: { value: "#f00" }, + green: { + value: { + h: "{hue}", + s: "{saturation}", + l: "{lightness}" + } + } + }, + size: { + border: { value: 0.125 } + }, + border: { + primary: { + // getReferences should work on objects like this: + value: { + color: "{color.red.value}", + width: "{size.border.value}", + style: "solid" + } + }, + } + }, + transform: { + hsl: { + type: 'value', + transitive: true, + matcher: (token) => token.original.value.h, + transformer: (token) => { + return `hsl(${token.value.h}, ${token.value.s}, ${token.value.l})` + } + }, + hslToHex: { + type: 'value', + transitive: true, + matcher: (token) => token.original.value.h, + transformer: (token) => { + return Color(`hsl(${token.value.h}, ${token.value.s}, ${token.value.l})`).toHexString(); + } + }, + cssBorder: { + type: 'value', + transitive: true, + matcher: (token) => token.path[0] === `border`, + transformer: (token) => { + return `${token.value.width} ${token.value.style} ${token.value.color}` + } + } + }, + platforms: { + // This will test to see if a value object for an hsl color works + // with and without `outputReferences` + cssHsl: { + buildPath, + transforms: StyleDictionary.transformGroup.css.concat([`hsl`]), + files: [{ + destination: `hsl.css`, + format: `css/variables`, + filter: (token) => token.attributes.category === `color` + },{ + destination: `hslWithReferences.css`, + format: `css/variables`, + filter: (token) => token.attributes.category === `color`, + options + }] + }, + + // This will test to see if a value object for an hsl that has been + // transformed to a hex color works with and without `outputReferences` + cssHex: { + buildPath, + transforms: StyleDictionary.transformGroup.css.concat([`cssBorder`,`hslToHex`]), + files: [{ + destination: 'hex.css', + format: 'css/variables', + filter: (token) => token.attributes.category === `color`, + },{ + destination: 'hexWithReferences.css', + format: 'css/variables', + filter: (token) => token.attributes.category === `color`, + options + }] + }, + + // This will test to see if a value object for a border + // works with and without `outputReferences` + cssBorder: { + buildPath, + transforms: StyleDictionary.transformGroup.css.concat([`cssBorder`]), + files: [{ + destination: 'border.css', + format: 'css/variables', + filter: (token) => token.attributes.category === `border`, + },{ + destination: 'borderWithReferences.css', + format: 'css/variables', + filter: (token) => token.attributes.category === `border`, + options + }] + }, + + scss: { + buildPath, + transforms: StyleDictionary.transformGroup.css.concat([`cssBorder`,`hslToHex`]), + files: [{ + destination: 'border.scss', + format: 'scss/variables', + filter: (token) => token.attributes.category === `border`, + },{ + destination: 'borderWithReferences.scss', + format: 'scss/variables', + filter: (token) => token.attributes.category === `border`, + options + }] + }, + } + }).buildAllPlatforms(); + + describe('css/variables', () => { + describe(`hsl syntax`, () => { + const output = fs.readFileSync(`${buildPath}hsl.css`, {encoding:'UTF-8'}); + it(`should match snapshot`, () => { + expect(output).toMatchSnapshot(); + }); + + describe(`with references`, () => { + const output = fs.readFileSync(`${buildPath}hslWithReferences.css`, {encoding:'UTF-8'}); + it(`should match snapshot`, () => { + expect(output).toMatchSnapshot(); + }); + }); + }); + + describe(`hex syntax`, () => { + const output = fs.readFileSync(`${buildPath}hex.css`, {encoding:'UTF-8'}); + it(`should match snapshot`, () => { + expect(output).toMatchSnapshot(); + }); + + describe(`with references`, () => { + const output = fs.readFileSync(`${buildPath}hexWithReferences.css`, {encoding:'UTF-8'}); + it(`should match snapshot`, () => { + expect(output).toMatchSnapshot(); + }); + }); + }); + + describe(`border`, () => { + const output = fs.readFileSync(`${buildPath}border.css`, {encoding:'UTF-8'}); + it(`should match snapshot`, () => { + expect(output).toMatchSnapshot(); + }); + + describe(`with references`, () => { + const output = fs.readFileSync(`${buildPath}borderWithReferences.css`, {encoding:'UTF-8'}); + it(`should match snapshot`, () => { + expect(output).toMatchSnapshot(); + }); + }); + }); + }); + + describe('scss/variables', () => { + const output = fs.readFileSync(`${buildPath}border.scss`, {encoding:'UTF-8'}); + it(`should match snapshot`, () => { + expect(output).toMatchSnapshot(); + }); + + describe(`with references`, () => { + const output = fs.readFileSync(`${buildPath}borderWithReferences.scss`, {encoding:'UTF-8'}); + it(`should match snapshot`, () => { + expect(output).toMatchSnapshot(); + }); + }); + }); + }); +}); + +afterAll(() => { + fs.emptyDirSync(buildPath); +}); \ No newline at end of file diff --git a/__tests__/utils/reference/getReferences.test.js b/__tests__/utils/reference/getReferences.test.js new file mode 100644 index 000000000..900f11063 --- /dev/null +++ b/__tests__/utils/reference/getReferences.test.js @@ -0,0 +1,78 @@ +/* + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +// `.getReferences` is bound to a dictionary object, so to test it we will +// create a dictionary object and then call `.getReferences` on it. +const createDictionary = require('../../../lib/utils/createDictionary'); + +const properties = { + color: { + red: { value: "#f00" }, + danger: { value: "{color.red.value}" } + }, + size: { + border: { value: "2px" } + }, + border: { + primary: { + // getReferences should work on objects like this: + value: { + color: "{color.red.value}", + width: "{size.border.value}", + style: "solid" + } + }, + secondary: { + // getReferences should work on interpolated values like this: + value: "{size.border.value} solid {color.red.value}" + } + } +} + +const dictionary = createDictionary({ properties }); + +describe('utils', () => { + describe('reference', () => { + describe('getReferences()', () => { + it(`should return an empty array if the value has no references`, () => { + expect(dictionary.getReferences(properties.color.red.value)).toEqual([]); + }); + + it(`should work with a single reference`, () => { + expect(dictionary.getReferences(properties.color.danger.value)).toEqual( + expect.arrayContaining([ + {value: "#f00"} + ]) + ); + }); + + it(`should work with object values`, () => { + expect(dictionary.getReferences(properties.border.primary.value)).toEqual( + expect.arrayContaining([ + {value: "2px"}, + {value: "#f00"} + ]) + ); + }); + + it(`should work with interpolated values`, () => { + expect(dictionary.getReferences(properties.border.secondary.value)).toEqual( + expect.arrayContaining([ + {value: "2px"}, + {value: "#f00"} + ]) + ); + }); + }); + }); +}); diff --git a/lib/common/formatHelpers/createPropertyFormatter.js b/lib/common/formatHelpers/createPropertyFormatter.js index 75393252f..fa2e00cba 100644 --- a/lib/common/formatHelpers/createPropertyFormatter.js +++ b/lib/common/formatHelpers/createPropertyFormatter.js @@ -11,8 +11,6 @@ * and limitations under the License. */ -const createReferenceRegex = require('../../utils/references/createReferenceRegex'); - const defaultFormatting = { prefix: '', commentStyle: 'long', @@ -78,26 +76,43 @@ function createPropertyFormatter({outputReferences, dictionary, format, formatti break; } - const regex = createReferenceRegex({}); - return function(prop) { let to_ret_prop = `${indentation}${prefix}${prop.name}${separator} `; let value = prop.value; + /** + * A single value can have multiple references either by interpolation: + * "value": "{size.border.width.value} solid {color.border.primary.value}" + * or if the value is an object: + * "value": { + * "size": "{size.border.width.value}", + * "style": "solid", + * "color": "{color.border.primary.value"} + * } + * This will see if there are references and if there are, replace + * the resolved value with the reference's name. + */ if (outputReferences && dictionary.usesReference(prop.original.value)) { - const refs = dictionary.getReferences(prop.original.value); - - value = prop.original.value.replace(regex, function(match, variable) { - // get the specific reference in the list of references - const ref = refs - .find(token => token.path.join('.') === variable.replace('.value','')) - .name; - if (format === 'css') { - return `var(${prefix}${ref})` - } else { - return `${prefix}${ref}`; - } - }); + // Formats that use this function expect `value` to be a string + // or else you will get '[object Object]' in the output + if (typeof value === 'string') { + const refs = dictionary.getReferences(prop.original.value); + refs.forEach(ref => { + // value should be a string that contains the resolved reference + // because Style Dictionary resolved this in the resolution step. + // Here we are undoing that by replacing the value with + // the reference's name + if (ref.value && ref.name) { + value = value.replace(ref.value, function() { + if (format === 'css') { + return `var(${prefix}${ref.name})` + } else { + return `${prefix}${ref.name}`; + } + }); + } + }); + } } to_ret_prop += prop.attributes.category === 'asset' ? `"${value}"` : value; diff --git a/lib/common/formatHelpers/sortByReference.js b/lib/common/formatHelpers/sortByReference.js index 653232b70..b15e60128 100644 --- a/lib/common/formatHelpers/sortByReference.js +++ b/lib/common/formatHelpers/sortByReference.js @@ -32,9 +32,9 @@ // If token a uses a reference and token b doesn't, b might come before a // read on.. - if (dictionary.usesReference(a.original.value)) { + if (a.original && dictionary.usesReference(a.original.value)) { // Both a and b have references, we need to see if the reference each other - if (dictionary.usesReference(b.original.value)) { + if (b.original && dictionary.usesReference(b.original.value)) { const aRefs = dictionary.getReferences(a.original.value); const bRefs = dictionary.getReferences(b.original.value); diff --git a/lib/utils/references/getReferences.js b/lib/utils/references/getReferences.js index d3354c097..1002f8459 100644 --- a/lib/utils/references/getReferences.js +++ b/lib/utils/references/getReferences.js @@ -39,8 +39,8 @@ function getReferences(value) { // references is an array of 0 or more references const references = []; - // function inside .replace runs multiple times if there are multiple matches - value.replace(regex, function(match, variable) { + // this will update the references array with the referenced tokens it finds. + function findReference(match, variable) { // remove 'value' to access the whole token object variable = variable.trim().replace('.value',''); @@ -55,7 +55,23 @@ function getReferences(value) { GroupMessages.add(GroupMessages.GROUP.FilteredOutputReferences, variable); } references.push(ref); - }); + } + + if (typeof value === 'string') { + // function inside .replace runs multiple times if there are multiple matches + value.replace(regex, findReference); + } + + // If the token's value is an object, run the replace reference + // on each value within that object. This mirrors the `usesReference` + // function which iterates over the object to see if there is a reference + if (typeof value === 'object') { + for (const key in value) { + if (value.hasOwnProperty(key)) { + value[key].replace(regex, findReference); + } + } + } return references; }