Skip to content

Commit

Permalink
feat: allow not throwing on broken refs
Browse files Browse the repository at this point in the history
  • Loading branch information
jorenbroekema committed Jun 14, 2024
1 parent 1b8bdff commit 5e10b70
Show file tree
Hide file tree
Showing 26 changed files with 302 additions and 118 deletions.
21 changes: 21 additions & 0 deletions .changeset/chilly-numbers-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'style-dictionary': minor
---

Allow not throwing fatal errors on broken token references/aliases, but `console.error` instead.
`resolveReferences` and `getReferences` both allow passing `throwOnBrokenReferences` option, setting this to false will prevent a fatal error.
You can also configure this on global/platform `log` property:

```json
{
"log": {
"errors": {
"brokenReferences": "console"
}
}
}
```

This setting defaults to `"error"` when not configured.

Some minor grammatical improvements to some of the error logs were also done.
20 changes: 16 additions & 4 deletions __integration__/__snapshots__/customFormats.test.snap.js
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,10 @@ snapshots["integration custom formats inline custom with new args should match s
],
"log": {
"warnings": "warn",
"verbosity": "default"
"verbosity": "default",
"errors": {
"brokenReferences": "throw"
}
},
"transforms": [
{
Expand Down Expand Up @@ -945,7 +948,10 @@ snapshots["integration custom formats inline custom with new args should match s
},
"log": {
"warnings": "warn",
"verbosity": "default"
"verbosity": "default",
"errors": {
"brokenReferences": "throw"
}
},
"usesDtcg": false,
"otherOption": "Test",
Expand Down Expand Up @@ -1503,7 +1509,10 @@ snapshots["integration custom formats register custom format with new args shoul
],
"log": {
"warnings": "warn",
"verbosity": "default"
"verbosity": "default",
"errors": {
"brokenReferences": "throw"
}
},
"transforms": [
{
Expand Down Expand Up @@ -1891,7 +1900,10 @@ snapshots["integration custom formats register custom format with new args shoul
},
"log": {
"warnings": "warn",
"verbosity": "default"
"verbosity": "default",
"errors": {
"brokenReferences": "throw"
}
},
"usesDtcg": false,
"otherOption": "Test",
Expand Down
6 changes: 6 additions & 0 deletions __integration__/logging/__snapshots__/file.test.snap.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,3 +439,9 @@ color.core.blue.0
This is caused when combining a filter and \`outputReferences\`.`;
/* end snapshot integration logging file filtered references should throw detailed error of filtered references through "verbose" verbosity and log level set to error */

snapshots["integration logging file empty tokens should not warn user about empty tokens with silent log verbosity"] =
`
css
No tokens for empty.css. File not created.`;
/* end snapshot integration logging file empty tokens should not warn user about empty tokens with silent log verbosity */

50 changes: 50 additions & 0 deletions __tests__/StyleDictionary.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { resolve } from '../lib/resolve.js';
import GroupMessages from '../lib/utils/groupMessages.js';
import flattenTokens from '../lib/utils/flattenTokens.js';
import formats from '../lib/common/formats.js';
import { restore, stubMethod } from 'hanbi';

function traverseObj(obj, fn) {
for (let key in obj) {
Expand Down Expand Up @@ -52,6 +53,7 @@ const test_props = {
// extend method is called by StyleDictionary constructor, therefore we're basically testing both things here
describe('StyleDictionary class', () => {
beforeEach(() => {
restore();
clearOutput();
});

Expand Down Expand Up @@ -344,6 +346,54 @@ describe('StyleDictionary class', () => {
});
});

describe('reference errors', () => {
it('should throw an error by default if broken references are encountered', async () => {
const sd = new StyleDictionary({
tokens: {
foo: {
value: '{bar}',
type: 'other',
},
},
platforms: {
css: {},
},
});

await expect(sd.exportPlatform('css')).to.eventually.be.rejectedWith(`
Reference Errors:
Some token references (1) could not be found.
Use log.verbosity "verbose" or use CLI option --verbose for more details.
`);
});

it('should only log an error if broken references are encountered and log.errors.brokenReferences is set to console-', async () => {
const stub = stubMethod(console, 'error');
const sd = new StyleDictionary({
log: {
errors: {
brokenReferences: 'console',
},
},
tokens: {
foo: {
value: '{bar}',
type: 'other',
},
},
platforms: {
css: {},
},
});
await sd.exportPlatform('css');
expect(stub.firstCall.args[0]).to.equal(`
Reference Errors:
Some token references (1) could not be found.
Use log.verbosity "verbose" or use CLI option --verbose for more details.
`);
});
});

describe('expand object value tokens', () => {
it('should not expand object value tokens by default', async () => {
const input = {
Expand Down
8 changes: 5 additions & 3 deletions __tests__/transform/tokenSetup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,17 @@ import tokenSetup from '../../lib/transform/tokenSetup.js';
describe('transform', () => {
describe('tokenSetup', () => {
it('should error if property is not an object', () => {
expect(tokenSetup.bind(null, null, 'foo', [])).to.throw('Property object must be an object');
expect(tokenSetup.bind(null, null, 'foo', [])).to.throw(
'Token object must be of type "object"',
);
});

it('should error if name in not a string', () => {
expect(tokenSetup.bind(null, {}, null, [])).to.throw('Name must be a string');
expect(tokenSetup.bind(null, {}, null, [])).to.throw('Token name must be a string');
});

it('should error path is not an array', () => {
expect(tokenSetup.bind(null, {}, 'name', null)).to.throw('Path must be an array');
expect(tokenSetup.bind(null, {}, 'name', null)).to.throw('Token path must be an array');
});

it('should work if all the args are proper', () => {
Expand Down
40 changes: 33 additions & 7 deletions __tests__/utils/reference/getReferences.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
*/

import { expect } from 'chai';
import { _getReferences, getReferences } from '../../../lib/utils/references/getReferences.js';
import { restore, stubMethod } from 'hanbi';
import { getReferences } from '../../../lib/utils/references/getReferences.js';

const tokens = {
color: {
Expand Down Expand Up @@ -50,38 +51,63 @@ describe('utils', () => {
describe('reference', () => {
describe('getReferences()', () => {
describe('public API', () => {
beforeEach(() => {
restore();
});

it('should not collect errors but rather throw immediately when using public API', () => {
expect(() => getReferences('{foo.bar}', tokens)).to.throw(
`tries to reference foo.bar, which is not defined.`,
`Tries to reference foo.bar, which is not defined.`,
);
});

it('should not collect errors but rather throw immediately when using public API', () => {
const stub = stubMethod(console, 'error');
getReferences('{foo.bar}', tokens, { throwOnBrokenReferences: false });
expect(stub.firstCall.args[0]).to.equal(
`Tries to reference foo.bar, which is not defined.`,
);
});

it('should allow warning immediately when references are filtered out', async () => {
const stub = stubMethod(console, 'warn');
const clonedTokens = structuredClone(tokens);
delete clonedTokens.color.red;
getReferences('{color.red}', clonedTokens, {
unfilteredTokens: tokens,
warnImmediately: true,
});
expect(stub.firstCall.args[0]).to.equal(
`Filtered out token references were found: color.red`,
);
});
});

it(`should return an empty array if the value has no references`, () => {
expect(_getReferences(tokens.color.red.value, tokens)).to.eql([]);
expect(getReferences(tokens.color.red.value, tokens)).to.eql([]);
});

it(`should work with a single reference`, () => {
expect(_getReferences(tokens.color.danger.value, tokens)).to.eql([
expect(getReferences(tokens.color.danger.value, tokens)).to.eql([
{ ref: ['color', 'red'], value: '#f00' },
]);
});

it(`should work with object values`, () => {
expect(_getReferences(tokens.border.primary.value, tokens)).to.eql([
expect(getReferences(tokens.border.primary.value, tokens)).to.eql([
{ ref: ['color', 'red'], value: '#f00' },
{ ref: ['size', 'border'], value: '2px' },
]);
});

it(`should work with objects that have numbers`, () => {
expect(_getReferences(tokens.border.secondary.value, tokens)).to.eql([
expect(getReferences(tokens.border.secondary.value, tokens)).to.eql([
{ ref: ['color', 'red'], value: '#f00' },
]);
});

it(`should work with interpolated values`, () => {
expect(_getReferences(tokens.border.tertiary.value, tokens)).to.eql([
expect(getReferences(tokens.border.tertiary.value, tokens)).to.eql([
{ ref: ['size', 'border'], value: '2px' },
{ ref: ['color', 'red'], value: '#f00' },
]);
Expand Down
Loading

0 comments on commit 5e10b70

Please sign in to comment.