diff --git a/.changeset/wicked-pears-eat.md b/.changeset/wicked-pears-eat.md new file mode 100644 index 00000000..5d69e175 --- /dev/null +++ b/.changeset/wicked-pears-eat.md @@ -0,0 +1,5 @@ +--- +'style-dictionary': patch +--- + +Fix `filterTokens` utility to deal with random metadata properties throughout token groups, without throwing errors. diff --git a/__tests__/filterTokens.test.js b/__tests__/filterTokens.test.js index 1d3ef482..c643b7f8 100644 --- a/__tests__/filterTokens.test.js +++ b/__tests__/filterTokens.test.js @@ -86,6 +86,46 @@ const tokens = { }, }; +const random_meta_tokens = { + description: null, + meta: undefined, + more_meta: [], + foo: { + description: null, + meta: undefined, + more_meta: [], + bar: { + description: null, + meta: undefined, + more_meta: [], + value: 0, + type: 'number', + original: { + value: 0, + }, + name: 'foo-bar', + path: ['foo', 'bar'], + }, + }, + qux: { + description: null, + meta: undefined, + more_meta: [], + value: 0, + type: 'number', + original: { + value: 0, + }, + name: 'qux', + path: ['qux'], + }, +}; + +const random_meta_dictionary = { + tokens: random_meta_tokens, + allTokens: flattenTokens(random_meta_tokens), +}; + const falsy_values = { kept: kept, not_kept: not_kept, @@ -115,11 +155,11 @@ describe('filterTokens', () => { }); it('should work with a filter function', async () => { - const filter = (property) => property.path.includes('size'); + const filter = (token) => token.path.includes('size'); const filteredDictionary = await filterTokens(dictionary, filter); - filteredDictionary.allTokens.forEach((property) => { - expect(property).to.not.equal(colorRed); - expect(property).not.to.not.equal(colorBlue); + filteredDictionary.allTokens.forEach((token) => { + expect(token).to.not.equal(colorRed); + expect(token).not.to.not.equal(colorBlue); }); expect(filteredDictionary.allTokens).to.eql([sizeSmall, sizeLarge]); expect(filteredDictionary.tokens).to.have.property('size'); @@ -127,17 +167,28 @@ describe('filterTokens', () => { }); it('should work with falsy values and a filter function', async () => { - const filter = (property) => property.path.includes('kept'); + const filter = (token) => token.path.includes('kept'); const filteredDictionary = await filterTokens(falsy_dictionary, filter); - filteredDictionary.allTokens.forEach((property) => { - expect(property).to.not.equal(not_kept); + filteredDictionary.allTokens.forEach((token) => { + expect(token).to.not.equal(not_kept); }); expect(filteredDictionary.allTokens).to.eql([kept]); expect(filteredDictionary.tokens).to.have.property('kept'); expect(filteredDictionary.tokens).to.not.have.property('not_kept'); }); + it('should work with random metadata props inside tokens / token groups', async () => { + const filter = (token) => { + return token.path.includes('bar'); + }; + + const filteredDictionary = await filterTokens(random_meta_dictionary, filter); + expect(filteredDictionary.allTokens).to.eql([random_meta_tokens.foo.bar]); + expect(filteredDictionary.tokens).to.have.nested.property('foo.bar'); + expect(filteredDictionary.tokens).to.not.have.property('qux'); + }); + it('should work with async filters', async () => { const filtered = await filterTokens(dictionary, async (token) => { await new Promise((resolve) => setTimeout(resolve, 100)); diff --git a/lib/filterTokens.js b/lib/filterTokens.js index 93de1dc0..966594c6 100644 --- a/lib/filterTokens.js +++ b/lib/filterTokens.js @@ -48,25 +48,27 @@ async function filterTokenObject(tokens, filter, options) { // out const result = await Object.entries(tokens ?? []).reduce(async (_acc, [key, token]) => { const acc = await _acc; - const tokenValue = options.usesDtcg ? token.$value : token.value; // If the token is not an object, we don't know what it is. We return it as-is. if (!isPlainObject(token)) { return acc; - // If the token has a `value` member we know it's a property, pass it to - // the filter function and either include it in the final `acc` object or - // exclude it (by returning the `acc` object without it added). - } else if (typeof tokenValue !== 'undefined') { - const filtered = await asyncFilter(/** @type {Token[]} */ ([token]), filter, options); - return filtered.length === 0 ? acc : { ...acc, [key]: token }; - // If we got here we have an object that is not a property. We'll assume - // it's an object containing multiple tokens and recursively filter it - // using the `filterTokenObject` function. } else { - const filtered = await filterTokenObject(token, filter, options); - // If the filtered object is not empty then add it to the final `acc` - // object. If it is empty then every property inside of it was filtered - // out, then exclude it entirely from the final `acc` object. - return Object.entries(filtered || {}).length < 1 ? acc : { ...acc, [key]: filtered }; + const tokenValue = options.usesDtcg ? token.$value : token.value; + if (typeof tokenValue === 'undefined') { + // If we got here we have an object that is not a token. We'll assume + // it's token group containing multiple tokens and recursively filter it + // using the `filterTokenObject` function. + const filtered = await filterTokenObject(token, filter, options); + // If the filtered object is not empty then add it to the final `acc` + // object. If it is empty then every token inside of it was filtered + // out, then exclude it entirely from the final `acc` object. + return Object.entries(filtered || {}).length < 1 ? acc : { ...acc, [key]: filtered }; + } else { + // If the token has a `value` member we know it's a token, pass it to + // the filter function and either include it in the final `acc` object or + // exclude it (by returning the `acc` object without it added). + const filtered = await asyncFilter(/** @type {Token[]} */ ([token]), filter, options); + return filtered.length === 0 ? acc : { ...acc, [key]: token }; + } } }, {}); return result;