Skip to content

Commit

Permalink
fix: filterTokens util to support random metadata props (#1354)
Browse files Browse the repository at this point in the history
Fixes #1347
  • Loading branch information
jorenbroekema authored Oct 1, 2024
1 parent b0c1ba7 commit 26728b9
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .changeset/wicked-pears-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'style-dictionary': patch
---

Fix `filterTokens` utility to deal with random metadata properties throughout token groups, without throwing errors.
65 changes: 58 additions & 7 deletions __tests__/filterTokens.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -115,29 +155,40 @@ 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');
expect(filteredDictionary.tokens).to.not.have.property('color');
});

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));
Expand Down
32 changes: 17 additions & 15 deletions lib/filterTokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 26728b9

Please sign in to comment.