Skip to content

Commit 0410295

Browse files
authoredJan 8, 2024
feat: improve and test ref utils error handling (#1077)

File tree

13 files changed

+123
-50
lines changed

13 files changed

+123
-50
lines changed
 

‎.changeset/kind-rats-invite.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'style-dictionary': minor
3+
---
4+
5+
Improve and test the error handling of standalone usage of reference utilities.

‎__integration__/logging/__snapshots__/platform.test.snap.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Unknown transformGroup "foo" found in platform "css":
2121
snapshots["integration logging platform property reference errors should throw and notify users of unknown references"] =
2222
`
2323
Property Reference Errors:
24-
Reference doesn't exist: color.danger.value tries to reference color.red.value, which is not defined
24+
Reference doesn't exist: color.danger.value tries to reference color.red.value, which is not defined.
2525
2626
Problems were found when trying to resolve property references`;
2727
/* end snapshot integration logging platform property reference errors should throw and notify users of unknown references */

‎__tests__/utils/reference/getReferences.test.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
*/
1313

1414
import { expect } from 'chai';
15-
import getReferences from '../../../lib/utils/references/getReferences.js';
15+
import getReferences, {
16+
getReferences as publicGetReferences,
17+
} from '../../../lib/utils/references/getReferences.js';
1618

1719
const tokens = {
1820
color: {
@@ -49,6 +51,14 @@ const tokens = {
4951
describe('utils', () => {
5052
describe('reference', () => {
5153
describe('getReferences()', () => {
54+
describe('public API', () => {
55+
it('should not collect errors but rather throw immediately when using public API', () => {
56+
expect(() => publicGetReferences('{foo.bar}', tokens)).to.throw(
57+
`Reference doesn't exist: tries to reference foo.bar, which is not defined.`,
58+
);
59+
});
60+
});
61+
5262
it(`should return an empty array if the value has no references`, () => {
5363
expect(getReferences(tokens.color.red.value, tokens)).to.eql([]);
5464
});

‎__tests__/utils/reference/resolveReferences.test.js

+24-6
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
*/
1313
import { expect } from 'chai';
1414
import { fileToJSON } from '../../__helpers.js';
15-
import { resolveReferences } from '../../../lib/utils/references/resolveReferences.js';
15+
import {
16+
_resolveReferences as resolveReferences,
17+
resolveReferences as publicResolveReferences,
18+
} from '../../../lib/utils/references/resolveReferences.js';
1619
import GroupMessages from '../../../lib/utils/groupMessages.js';
1720

1821
const PROPERTY_REFERENCE_WARNINGS = GroupMessages.GROUP.PropertyReferenceWarnings;
@@ -24,6 +27,21 @@ describe('utils', () => {
2427
GroupMessages.clear(PROPERTY_REFERENCE_WARNINGS);
2528
});
2629

30+
describe('public API', () => {
31+
it('should not collect errors but rather throw immediately when using public API', () => {
32+
const obj = fileToJSON('__tests__/__json_files/multiple_reference_errors.json');
33+
expect(() => publicResolveReferences(obj.a.b, obj)).to.throw(
34+
`Reference doesn't exist: tries to reference b.a, which is not defined.`,
35+
);
36+
expect(() => publicResolveReferences(obj.a.c, obj)).to.throw(
37+
`Reference doesn't exist: tries to reference b.c, which is not defined.`,
38+
);
39+
expect(() => publicResolveReferences(obj.a.d, obj)).to.throw(
40+
`Reference doesn't exist: tries to reference d, which is not defined.`,
41+
);
42+
});
43+
});
44+
2745
it('should do simple references', () => {
2846
const test = resolveReferences('{foo}', fileToJSON('__tests__/__json_files/simple.json'));
2947
expect(test).to.equal('bar');
@@ -105,8 +123,8 @@ describe('utils', () => {
105123
expect(resolveReferences(obj.foo, obj)).to.be.undefined;
106124
expect(resolveReferences(obj.error, obj)).to.be.undefined;
107125
expect(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS)).to.eql([
108-
"Reference doesn't exist: tries to reference bar, which is not defined",
109-
"Reference doesn't exist: tries to reference a.b.d, which is not defined",
126+
"Reference doesn't exist: tries to reference bar, which is not defined.",
127+
"Reference doesn't exist: tries to reference a.b.d, which is not defined.",
110128
]);
111129
});
112130

@@ -202,9 +220,9 @@ describe('utils', () => {
202220
expect(GroupMessages.count(PROPERTY_REFERENCE_WARNINGS)).to.equal(3);
203221
expect(JSON.stringify(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS))).to.equal(
204222
JSON.stringify([
205-
"Reference doesn't exist: tries to reference b.a, which is not defined",
206-
"Reference doesn't exist: tries to reference b.c, which is not defined",
207-
"Reference doesn't exist: tries to reference d, which is not defined",
223+
"Reference doesn't exist: tries to reference b.a, which is not defined.",
224+
"Reference doesn't exist: tries to reference b.c, which is not defined.",
225+
"Reference doesn't exist: tries to reference d, which is not defined.",
208226
]),
209227
);
210228
});

‎__tests__/utils/resolveObject.test.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,9 @@ describe('utils', () => {
321321
expect(GroupMessages.count(PROPERTY_REFERENCE_WARNINGS)).to.equal(3);
322322
expect(JSON.stringify(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS))).to.equal(
323323
JSON.stringify([
324-
"Reference doesn't exist: a.b tries to reference b.a, which is not defined",
325-
"Reference doesn't exist: a.c tries to reference b.c, which is not defined",
326-
"Reference doesn't exist: a.d tries to reference d, which is not defined",
324+
"Reference doesn't exist: a.b tries to reference b.a, which is not defined.",
325+
"Reference doesn't exist: a.c tries to reference b.c, which is not defined.",
326+
"Reference doesn't exist: a.d tries to reference d, which is not defined.",
327327
]),
328328
);
329329
});

‎lib/buildFile.js

+10-10
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export default function buildFile(file, platform = {}, dictionary) {
110110
}
111111
});
112112

113-
let tokenNamesCollisionCount = GroupMessages.count(PROPERTY_NAME_COLLISION_WARNINGS);
113+
const tokenNamesCollisionCount = GroupMessages.count(PROPERTY_NAME_COLLISION_WARNINGS);
114114
fs.writeFileSync(
115115
fullDestination,
116116
format(
@@ -122,7 +122,7 @@ export default function buildFile(file, platform = {}, dictionary) {
122122
),
123123
);
124124

125-
let filteredReferencesCount = GroupMessages.count(GroupMessages.GROUP.FilteredOutputReferences);
125+
const filteredReferencesCount = GroupMessages.count(GroupMessages.GROUP.FilteredOutputReferences);
126126

127127
// don't show name collision warnings for nested type formats
128128
// because they are not relevant.
@@ -131,13 +131,13 @@ export default function buildFile(file, platform = {}, dictionary) {
131131
} else {
132132
const warnHeader = `⚠️ ${fullDestination}`;
133133
if (tokenNamesCollisionCount > 0) {
134-
let tokenNamesCollisionWarnings = GroupMessages.fetchMessages(
134+
const tokenNamesCollisionWarnings = GroupMessages.fetchMessages(
135135
PROPERTY_NAME_COLLISION_WARNINGS,
136136
).join('\n ');
137-
let title = `While building ${chalk
137+
const title = `While building ${chalk
138138
.rgb(255, 69, 0)
139139
.bold(destination)}, token collisions were found; output may be unexpected.`;
140-
let help = chalk.rgb(
140+
const help = chalk.rgb(
141141
255,
142142
165,
143143
0,
@@ -149,7 +149,7 @@ export default function buildFile(file, platform = {}, dictionary) {
149149
'* overly inclusive file filters',
150150
].join('\n '),
151151
);
152-
let warn = `${warnHeader}\n${title}\n ${tokenNamesCollisionWarnings}\n${help}`;
152+
const warn = `${warnHeader}\n${title}\n ${tokenNamesCollisionWarnings}\n${help}`;
153153
if (platform?.log === 'error') {
154154
throw new Error(warn);
155155
} else {
@@ -158,20 +158,20 @@ export default function buildFile(file, platform = {}, dictionary) {
158158
}
159159

160160
if (filteredReferencesCount > 0) {
161-
let filteredReferencesWarnings = GroupMessages.flush(
161+
const filteredReferencesWarnings = GroupMessages.flush(
162162
GroupMessages.GROUP.FilteredOutputReferences,
163163
).join('\n ');
164-
let title = `While building ${chalk
164+
const title = `While building ${chalk
165165
.rgb(255, 69, 0)
166166
.bold(
167167
destination,
168168
)}, filtered out token references were found; output may be unexpected. Here are the references that are used but not defined in the file`;
169-
let help = chalk.rgb(
169+
const help = chalk.rgb(
170170
255,
171171
165,
172172
0,
173173
)(['This is caused when combining a filter and `outputReferences`.'].join('\n '));
174-
let warn = `${warnHeader}\n${title}\n ${filteredReferencesWarnings}\n${help}`;
174+
const warn = `${warnHeader}\n${title}\n ${filteredReferencesWarnings}\n${help}`;
175175
if (platform?.log === 'error') {
176176
throw new Error(warn);
177177
} else {

‎lib/common/formatHelpers/createPropertyFormatter.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
* CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
1111
* and limitations under the License.
1212
*/
13-
14-
import { usesReferences, getReferences } from '../../utils/index.js';
13+
import getReferences from '../../utils/references/getReferences.js';
14+
import usesReferences from '../../utils/references/usesReferences.js';
1515

1616
/**
1717
* @typedef {import('../../../types/DesignToken.d.ts').Dictionary} Dictionary

‎lib/common/formatHelpers/sortByReference.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
* and limitations under the License.
1212
*/
1313

14-
import { usesReferences, getReferences } from 'style-dictionary/utils';
14+
import usesReferences from '../../utils/references/usesReferences.js';
15+
import getReferences from '../../utils/references/getReferences.js';
1516

1617
/**
1718
* @typedef {import('../../../types/DesignToken.d.ts').TransformedTokens} Tokens

‎lib/utils/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*/
1313

1414
import usesReferences from './references/usesReferences.js';
15-
import getReferences from './references/getReferences.js';
15+
import { getReferences } from './references/getReferences.js';
1616
import { resolveReferences } from './references/resolveReferences.js';
1717

1818
// Public style-dictionary/utils API

‎lib/utils/references/getReferences.js

+40-11
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,33 @@
1010
* CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
1111
* and limitations under the License.
1212
*/
13-
1413
import getPathFromName from './getPathFromName.js';
1514
import createReferenceRegex from './createReferenceRegex.js';
1615
import getValueByPath from './getValueByPath.js';
1716
import GroupMessages from '../groupMessages.js';
1817
import defaults from './defaults.js';
1918

19+
const FILTER_WARNINGS = GroupMessages.GROUP.FilteredOutputReferences;
20+
21+
/**
22+
* @typedef {import('../../../types/Config.d.ts').RegexOptions} RegexOptions
23+
* @typedef {import('../../StyleDictionary.js').default} Dictionary
24+
* @typedef {import('../../../types/DesignToken.d.ts').TransformedTokens} Tokens
25+
* @typedef {import('../../../types/DesignToken.d.ts').TransformedToken} Token
26+
*
27+
* Public API wrapper around the function below this one
28+
*
29+
* @memberof Dictionary
30+
* @param {string|Object<string, string|any>} value the value that contains a reference
31+
* @param {Tokens} tokens the dictionary to search in
32+
* @param {RegexOptions & { unfilteredTokens?: Tokens }} [opts]
33+
* @param {Token[]} [references] array of token's references because a token's value can contain multiple references due to string interpolation
34+
* @returns {Token[]}
35+
*/
36+
export function getReferences(value, tokens, opts = {}, references = []) {
37+
return _getReferences(value, tokens, opts, references, true);
38+
}
39+
2040
/**
2141
* This is a helper function that is added to the dictionary object that
2242
* is passed to formats and actions. It will resolve a reference giving
@@ -26,19 +46,21 @@ import defaults from './defaults.js';
2646
* ```css
2747
* --color-background-base: var(--color-core-white);
2848
* ```
29-
* @typedef {import('../../../types/Config.d.ts').RegexOptions} RegexOptions
30-
* @typedef {import('../../StyleDictionary.js').default} Dictionary
31-
* @typedef {import('../../../types/DesignToken.d.ts').TransformedTokens} Tokens
32-
* @typedef {import('../../../types/DesignToken.d.ts').TransformedToken} Token
3349
*
34-
* @memberof Dictionary
3550
* @param {string|Object<string, string|any>} value the value that contains a reference
3651
* @param {Tokens} tokens the dictionary to search in
3752
* @param {RegexOptions & { unfilteredTokens?: Tokens }} [opts]
3853
* @param {Token[]} [references] array of token's references because a token's value can contain multiple references due to string interpolation
54+
* @param {boolean} [throwImmediately]
3955
* @returns {Token[]}
4056
*/
41-
export default function getReferences(value, tokens, opts = {}, references = []) {
57+
export default function _getReferences(
58+
value,
59+
tokens,
60+
opts = {},
61+
references = [],
62+
throwImmediately = false,
63+
) {
4264
const regex = createReferenceRegex(opts);
4365

4466
/**
@@ -55,14 +77,21 @@ export default function getReferences(value, tokens, opts = {}, references = [])
5577

5678
let ref = getValueByPath(pathName, tokens);
5779

58-
if (!ref && opts.unfilteredTokens) {
80+
if (ref === undefined && opts.unfilteredTokens) {
81+
if (!throwImmediately) {
82+
// warn the user about this
83+
GroupMessages.add(FILTER_WARNINGS, variable);
84+
}
5985
// fall back on unfilteredTokens as it is unfiltered
6086
ref = getValueByPath(pathName, opts.unfilteredTokens);
61-
// and warn the user about this
62-
GroupMessages.add(GroupMessages.GROUP.FilteredOutputReferences, variable);
6387
}
88+
6489
if (ref !== undefined) {
6590
references.push(ref);
91+
} else if (throwImmediately) {
92+
throw new Error(
93+
`Reference doesn't exist: tries to reference ${variable}, which is not defined.`,
94+
);
6695
}
6796
return '';
6897
}
@@ -83,7 +112,7 @@ export default function getReferences(value, tokens, opts = {}, references = [])
83112
}
84113
// if it is an object, we go further down the rabbit hole
85114
if (value.hasOwnProperty(key) && typeof value[key] === 'object') {
86-
getReferences(value[key], tokens, opts, references);
115+
_getReferences(value[key], tokens, opts, references);
87116
}
88117
}
89118
}

‎lib/utils/references/resolveReferences.js

+21-12
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@ const PROPERTY_REFERENCE_WARNINGS = GroupMessages.GROUP.PropertyReferenceWarning
2828
* @typedef {import('../../../types/DesignToken.d.ts').DesignToken} DesignToken
2929
*/
3030

31-
/** Utility to resolve references inside a string value
31+
/**
32+
* Public API wrapper around the functon below this one
3233
* @param {string} value
3334
* @param {DesignTokens} tokens
3435
* @param {RefOpts} [opts]
3536
* @returns {string|number|undefined}
3637
*/
3738
export function resolveReferences(value, tokens, opts) {
38-
return _resolveReferences(value, tokens, opts);
39+
return _resolveReferences(value, tokens, { ...opts, throwImmediately: true });
3940
}
4041

4142
/**
@@ -59,6 +60,7 @@ export function _resolveReferences(
5960
stack = [],
6061
foundCirc = {},
6162
firstIteration = true,
63+
throwImmediately = false,
6264
} = {},
6365
) {
6466
const reg = regex ?? createReferenceRegex({ opening_character, closing_character, separator });
@@ -136,10 +138,15 @@ export function _resolveReferences(
136138
circStack.push(reference);
137139

138140
// Add circ reference info to our list of warning messages
139-
GroupMessages.add(
140-
PROPERTY_REFERENCE_WARNINGS,
141-
'Circular definition cycle: ' + circStack.join(', '),
142-
);
141+
const warning = `Circular definition cycle: ${circStack.join(', ')}`;
142+
if (throwImmediately) {
143+
throw new Error(warning);
144+
} else {
145+
GroupMessages.add(
146+
PROPERTY_REFERENCE_WARNINGS,
147+
'Circular definition cycle: ' + circStack.join(', '),
148+
);
149+
}
143150
} else {
144151
to_ret = _resolveReferences(to_ret, tokens, {
145152
regex: reg,
@@ -164,12 +171,14 @@ export function _resolveReferences(
164171
// User might have passed current_context option which is path (arr) pointing to key
165172
// that this value is associated with, helpful for debugging
166173
const context = getName(current_context, { separator });
167-
GroupMessages.add(
168-
PROPERTY_REFERENCE_WARNINGS,
169-
`Reference doesn't exist:${
170-
context ? ` ${context}` : ''
171-
} tries to reference ${variable}, which is not defined`,
172-
);
174+
const warning = `Reference doesn't exist:${
175+
context ? ` ${context}` : ''
176+
} tries to reference ${variable}, which is not defined.`;
177+
if (throwImmediately) {
178+
throw new Error(warning);
179+
} else {
180+
GroupMessages.add(PROPERTY_REFERENCE_WARNINGS, warning);
181+
}
173182
to_ret = ref;
174183
}
175184
stack.pop();

‎package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎types/Config.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface ResolveReferenceOptionsInternal extends ResolveReferenceOptions
4343
stack?: string[];
4444
foundCirc?: Record<string, boolean>;
4545
firstIteration?: boolean;
46+
throwImmediately?: boolean;
4647
}
4748

4849
export interface PlatformConfig extends RegexOptions {

0 commit comments

Comments
 (0)
Please sign in to comment.