diff --git a/.changeset/chilly-numbers-work.md b/.changeset/chilly-numbers-work.md new file mode 100644 index 000000000..b04733bba --- /dev/null +++ b/.changeset/chilly-numbers-work.md @@ -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. diff --git a/__integration__/__snapshots__/customFormats.test.snap.js b/__integration__/__snapshots__/customFormats.test.snap.js index 060c21614..4e4a4bad9 100644 --- a/__integration__/__snapshots__/customFormats.test.snap.js +++ b/__integration__/__snapshots__/customFormats.test.snap.js @@ -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": [ { @@ -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", @@ -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": [ { @@ -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", diff --git a/__tests__/StyleDictionary.test.js b/__tests__/StyleDictionary.test.js index b20c8dadf..0e5db372d 100644 --- a/__tests__/StyleDictionary.test.js +++ b/__tests__/StyleDictionary.test.js @@ -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) { @@ -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(); }); @@ -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 = { diff --git a/__tests__/transform/tokenSetup.test.js b/__tests__/transform/tokenSetup.test.js index c4a7426d4..6ebf79dd6 100644 --- a/__tests__/transform/tokenSetup.test.js +++ b/__tests__/transform/tokenSetup.test.js @@ -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', () => { diff --git a/__tests__/utils/reference/getReferences.test.js b/__tests__/utils/reference/getReferences.test.js index aebc01224..f2ebc217c 100644 --- a/__tests__/utils/reference/getReferences.test.js +++ b/__tests__/utils/reference/getReferences.test.js @@ -12,6 +12,7 @@ */ import { expect } from 'chai'; +import { restore, stubMethod } from 'hanbi'; import { _getReferences, getReferences } from '../../../lib/utils/references/getReferences.js'; const tokens = { @@ -50,9 +51,31 @@ 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 warn 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 }); + expect(stub.firstCall.args[0]).to.equal( + `Filtered out token references were found: color.red`, ); }); }); diff --git a/__tests__/utils/reference/resolveReferences.test.js b/__tests__/utils/reference/resolveReferences.test.js index 66074092f..635f2e577 100644 --- a/__tests__/utils/reference/resolveReferences.test.js +++ b/__tests__/utils/reference/resolveReferences.test.js @@ -11,6 +11,7 @@ * and limitations under the License. */ import { expect } from 'chai'; +import { restore, stubMethod } from 'hanbi'; import { fileToJSON } from '../../__helpers.js'; import { _resolveReferences as resolveReferences, @@ -25,6 +26,7 @@ describe('utils', () => { describe('resolveReferences', () => { beforeEach(() => { GroupMessages.clear(PROPERTY_REFERENCE_WARNINGS); + restore(); }); describe('public API', () => { @@ -40,6 +42,23 @@ describe('utils', () => { `tries to reference d, which is not defined.`, ); }); + + it('should only console.error if throwOnBrokenReferences is disabled', async () => { + const stub = stubMethod(console, 'error'); + publicResolveReferences('{foo.bar}', {}, { throwOnBrokenReferences: false }); + expect(stub.firstCall.args[0]).to.equal( + `tries to reference foo.bar, which is not defined.`, + ); + }); + + it('should gracefully handle basic circular references if throwOnBrokenReferences is disabled', () => { + const stub = stubMethod(console, 'error'); + const obj = fileToJSON('__tests__/__json_files/circular.json'); + expect(publicResolveReferences(obj.a, obj, { throwOnBrokenReferences: false })).to.equal( + '{b}', + ); + expect(stub.firstCall.args[0]).to.equal('Circular definition cycle: b, c, d, a, b'); + }); }); it('should do simple references', () => { @@ -137,6 +156,15 @@ describe('utils', () => { ); }); + it('should gracefully handle basic circular references', () => { + const obj = fileToJSON('__tests__/__json_files/circular.json'); + expect(resolveReferences(obj.a, obj)).to.equal('{b}'); + expect(GroupMessages.count(PROPERTY_REFERENCE_WARNINGS)).to.equal(1); + expect(JSON.stringify(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS))).to.equal( + JSON.stringify(['Circular definition cycle: b, c, d, a, b']), + ); + }); + it('should gracefully handle basic and nested circular references', () => { const obj = fileToJSON('__tests__/__json_files/circular_2.json'); expect(resolveReferences(obj.j, obj)).to.equal('{a.b.c}'); diff --git a/docs/src/content/docs/reference/Utils/references.md b/docs/src/content/docs/reference/Utils/references.md index 44bcc6fc5..5c4964986 100644 --- a/docs/src/content/docs/reference/Utils/references.md +++ b/docs/src/content/docs/reference/Utils/references.md @@ -53,10 +53,10 @@ resolveReferences('solid {spacing.2} {colors.black}', sd.tokens); // alternative resolveReferences('solid {spacing.2} {colors.black}', sd.tokens, { usesDtcg: true }); // Assumes DTCG spec format, with $ prefix ($value, $type) ``` -:::note -You can pass a third `options` argument where you can pass some configuration options for how references are resolved -Most notable option for public usage is `usesDtcg`, if set to true, the `resolveReferences` utility will assume DTCG syntax (`$value` props). -::: +You can pass a third `options` argument where you can pass some configuration options for how references are resolved: + +- `usesDtcg` boolean, if set to true, the `resolveReferences` utility will assume DTCG syntax (`$value` props) +- `throwOnBrokenReferences` boolean, if set to true, it will `console.error` for reference errors instead of fatally throw ## getReferences @@ -97,10 +97,10 @@ getReferences('solid {spacing.2} {colors.black}', sd.tokens, { usesDtcg: true }) */ ``` -:::note -You can pass a third `options` argument where you can pass some configuration options for how references are resolved -Most notable option for public usage is `usesDtcg`, if set to true, the `resolveReferences` utility will assume DTCG syntax (`$value` props) -::: +You can pass a third `options` argument where you can pass some configuration options for how references are resolved: + +- `usesDtcg` boolean, if set to true, the `resolveReferences` utility will assume DTCG syntax (`$value` props) +- `throwOnBrokenReferences` boolean, if set to true, it will `console.error` for reference errors instead of fatally throw ### Complicated example diff --git a/docs/src/content/docs/reference/logging.md b/docs/src/content/docs/reference/logging.md index ac58d0e21..cbf607ab0 100644 --- a/docs/src/content/docs/reference/logging.md +++ b/docs/src/content/docs/reference/logging.md @@ -12,17 +12,22 @@ const sd = new StyleDictionary({ log: { warnings: 'warn', // 'warn' | 'error' | 'disabled' verbosity: 'default', // 'default' | 'silent' | 'verbose' + errors: { + brokenReferences: 'throw', // 'throw' | 'console' + }, }, }); ``` > `log` can also be set on platform specific configuration -| Param | Type | Description | -| --------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `log` | `Object` | | -| `log.warnings` | `'warn' \| 'error' \| 'disabled'` | Whether warnings should be logged as warnings, thrown as errors or disabled entirely. Defaults to 'warn' | -| `log.verbosity` | `'default' \|'silent' \| 'verbose'` | How verbose logs should be, default value is 'default'. 'silent' means no logs at all apart from fatal errors. 'verbose' means detailed error messages for debugging | +| Param | Type | Description | +| ----------------------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `log` | `Object` | | +| `log.warnings` | `'warn' \| 'error' \| 'disabled'` | Whether warnings should be logged as warnings, thrown as errors or disabled entirely. Defaults to 'warn' | +| `log.verbosity` | `'default' \|'silent' \| 'verbose'` | How verbose logs should be, default value is 'default'. 'silent' means no logs at all apart from fatal errors. 'verbose' means detailed error messages for debugging | +| `log.errors` | `Object` | How verbose logs should be, default value is 'default'. 'silent' means no logs at all apart from fatal errors. 'verbose' means detailed error messages for debugging | +| `log.errors.brokenReferences` | `'throw' \| 'console'` | Whether broken references in tokens should throw a fatal error or only a `console.error` without exiting the process. | There are five types of warnings that will be thrown as errors instead of being logged as warnings when `log.warnings` is set to `error`: diff --git a/docs/starlight-config.ts b/docs/starlight-config.ts index 7e89b5a54..eb86774c5 100644 --- a/docs/starlight-config.ts +++ b/docs/starlight-config.ts @@ -18,11 +18,11 @@ export default { 'Export your Design Tokens to any platform. iOS, Android, CSS, JS, HTML, sketch files, style documentation, or anything you can think of. Forward-compatible with Design Token Community Group spec.', logo: { src: './src/assets/logo.png', alt: 'Style-Dictionary logo, Pascal the chameleon.' }, editLink: { - baseUrl: 'https://github.com/amzn/style-dictionary/edit/v4/src/content/docs/', + baseUrl: 'https://github.com/amzn/style-dictionary/edit/v4/docs/src/content/docs/', }, favicon: '/favicon.png', social: { - github: 'https://github.com/amzn/style-dictionary', + github: 'https://github.com/amzn/style-dictionary/tree/v4', slack: 'https://join.slack.com/t/tokens-studio/shared_invite/zt-1p8ea3m6t-C163oJcN9g3~YZTKRgo2hg', }, diff --git a/lib/StyleDictionary.js b/lib/StyleDictionary.js index 8da4f691c..367c56914 100644 --- a/lib/StyleDictionary.js +++ b/lib/StyleDictionary.js @@ -59,6 +59,7 @@ import cleanActions from './cleanActions.js'; const PROPERTY_VALUE_COLLISIONS = GroupMessages.GROUP.PropertyValueCollisions; const PROPERTY_REFERENCE_WARNINGS = GroupMessages.GROUP.PropertyReferenceWarnings; const UNKNOWN_CSS_FONT_PROPS_WARNINGS = GroupMessages.GROUP.UnknownCSSFontProperties; +const FILTER_WARNINGS = GroupMessages.GROUP.FilteredOutputReferences; /** * Style Dictionary module @@ -118,6 +119,9 @@ export default class StyleDictionary extends Register { this.log = { warnings: 'warn', verbosity: 'default', + errors: { + brokenReferences: 'throw', + }, }; /** @type {string[]} */ this.source = []; @@ -503,7 +507,11 @@ export default class StyleDictionary extends Register { err += `${verbosityInfo}\n`; } - throw new Error(err); + if (this.log.errors?.brokenReferences === 'throw') { + throw new Error(err); + } else { + console.error(err); + } } const unknownPropsWarningCount = GroupMessages.count(UNKNOWN_CSS_FONT_PROPS_WARNINGS); @@ -669,9 +677,7 @@ export default class StyleDictionary extends Register { }), ); - const filteredReferencesCount = GroupMessages.count( - GroupMessages.GROUP.FilteredOutputReferences, - ); + const filteredReferencesCount = GroupMessages.count(FILTER_WARNINGS); // don't show name collision warnings for nested type formats // because they are not relevant. @@ -716,9 +722,7 @@ export default class StyleDictionary extends Register { } if (filteredReferencesCount > 0) { - const filteredReferencesWarnings = GroupMessages.flush( - GroupMessages.GROUP.FilteredOutputReferences, - ).join('\n '); + const filteredReferencesWarnings = GroupMessages.flush(FILTER_WARNINGS).join('\n '); const title = `While building ${chalk .rgb(255, 69, 0) .bold( diff --git a/lib/cleanFiles.js b/lib/cleanFiles.js index 230de0c64..3fc9f29dc 100644 --- a/lib/cleanFiles.js +++ b/lib/cleanFiles.js @@ -38,7 +38,7 @@ export default async function cleanFiles(platform, vol) { if (file.format) { return cleanFile(file, platform, vol); } else { - throw new Error('Please supply a template or format'); + throw new Error('Please supply a format'); } }), ); diff --git a/lib/transform/tokenSetup.js b/lib/transform/tokenSetup.js index 6f93c0dfb..fe61c5569 100644 --- a/lib/transform/tokenSetup.js +++ b/lib/transform/tokenSetup.js @@ -30,9 +30,9 @@ import isPlainObject from 'is-plain-obj'; * @returns {TransformedToken} - A new object that is setup and ready to go. */ export default function tokenSetup(token, name, path) { - if (!token && !isPlainObject(token)) throw new Error('Property object must be an object'); - if (!name || !(typeof name === 'string')) throw new Error('Name must be a string'); - if (!path || !Array.isArray(path)) throw new Error('Path must be an array'); + if (!token && !isPlainObject(token)) throw new Error('Token object must be of type "object"'); + if (!name || !(typeof name === 'string')) throw new Error('Token name must be a string'); + if (!path || !Array.isArray(path)) throw new Error('Token path must be an array'); let to_ret = token; diff --git a/lib/utils/convertToBase64.js b/lib/utils/convertToBase64.js index 0628716d2..69c786d2b 100644 --- a/lib/utils/convertToBase64.js +++ b/lib/utils/convertToBase64.js @@ -23,7 +23,7 @@ import { resolve } from '../resolve.js'; * @returns {String} */ export default function convertToBase64(filePath, vol = fs) { - if (typeof filePath !== 'string') throw new Error('filePath name must be a string'); + if (typeof filePath !== 'string') throw new Error('Token filePath name must be a string'); const body = /** @type {string} */ ( vol.readFileSync(resolve(filePath, vol.__custom_fs__), 'utf-8') diff --git a/lib/utils/references/getName.js b/lib/utils/references/getName.js index 4278a35c3..1cd344135 100644 --- a/lib/utils/references/getName.js +++ b/lib/utils/references/getName.js @@ -26,7 +26,7 @@ import defaults from './defaults.js'; export default function getName(path, opts = {}) { const options = { ...defaults, ...opts }; if (!Array.isArray(path)) { - throw new Error('Getting name for path failed. Path must be an array'); + throw new Error('Getting name for path failed. Token path must be an array of strings'); } return path.join(options.separator); } diff --git a/lib/utils/references/getPathFromName.js b/lib/utils/references/getPathFromName.js index 38afbdc28..29f0fd878 100644 --- a/lib/utils/references/getPathFromName.js +++ b/lib/utils/references/getPathFromName.js @@ -23,7 +23,7 @@ import defaults from './defaults.js'; export default function getPathFromName(pathName, separator) { const sep = separator ?? defaults.separator; if (typeof pathName !== 'string') { - throw new Error('Getting path from name failed. Name must be a string'); + throw new Error('Getting path from name failed. Token name must be a string'); } return pathName.split(sep); } diff --git a/lib/utils/references/getReferences.js b/lib/utils/references/getReferences.js index 48f20066f..963072dd6 100644 --- a/lib/utils/references/getReferences.js +++ b/lib/utils/references/getReferences.js @@ -20,6 +20,7 @@ const FILTER_WARNINGS = GroupMessages.GROUP.FilteredOutputReferences; /** * @typedef {import('../../../types/Config.d.ts').GetReferencesOptions} GetReferencesOptions + * @typedef {import('../../../types/Config.d.ts').GetReferencesOptionsInternal} GetReferencesOptionsInternal * @typedef {import('../../StyleDictionary.js').default} Dictionary * @typedef {import('../../../types/DesignToken.d.ts').TransformedTokens} Tokens * @typedef {import('../../../types/DesignToken.d.ts').TransformedToken} Token @@ -34,7 +35,7 @@ const FILTER_WARNINGS = GroupMessages.GROUP.FilteredOutputReferences; * @returns {Token[]} */ export function getReferences(value, tokens, opts = {}, references = []) { - return _getReferences(value, tokens, opts, references, true); + return _getReferences(value, tokens, { ...opts, warnImmediately: true }, references); } /** @@ -49,48 +50,61 @@ export function getReferences(value, tokens, opts = {}, references = []) { * * @param {string|Object} value the value that contains a reference * @param {Tokens} tokens the dictionary to search in - * @param {GetReferencesOptions & { unfilteredTokens?: Tokens }} [opts] + * @param {GetReferencesOptionsInternal} [opts] * @param {Token[]} [references] array of token's references because a token's value can contain multiple references due to string interpolation - * @param {boolean} [throwImmediately] * @returns {Token[]} */ -export function _getReferences( - value, - tokens, - opts = {}, - references = [], - throwImmediately = false, -) { - const { usesDtcg } = opts; +export function _getReferences(value, tokens, opts = {}, references = []) { + const { + usesDtcg, + separator, + throwOnBrokenReferences = true, + warnImmediately, + unfilteredTokens, + } = opts; const regex = createReferenceRegex(opts); /** * this will update the references array with the referenced tokens it finds. - * @param {string} match + * @param {string} _ * @param {string} variable */ - function findReference(match, variable) { + function findReference(_, variable) { // remove 'value' to access the whole token object variable = variable.trim().replace(`.${usesDtcg ? '$' : ''}value`, ''); // Find what the value is referencing - const pathName = getPathFromName(variable, opts.separator ?? defaults.separator); + const pathName = getPathFromName(variable, separator ?? defaults.separator); let ref = getValueByPath(pathName, tokens); - if (ref === undefined && opts.unfilteredTokens) { - if (!throwImmediately) { - // warn the user about this + let unfilteredWarning; + if (ref === undefined && unfilteredTokens) { + // warn the user about this + if (warnImmediately) { + unfilteredWarning = `Filtered out token references were found: ${variable}`; + } else { + // we collect the warning and warn later in the process GroupMessages.add(FILTER_WARNINGS, variable); } // fall back on unfilteredTokens as it is unfiltered - ref = getValueByPath(pathName, opts.unfilteredTokens); + ref = getValueByPath(pathName, unfilteredTokens); } if (ref !== undefined) { references.push({ ...ref, ref: pathName }); - } else if (throwImmediately) { - throw new Error(`tries to reference ${variable}, which is not defined.`); + // not undefined anymore which means that if unfilteredWarning was set earlier, + // the missing ref is due to it being filtered out + if (unfilteredWarning) { + console.warn(unfilteredWarning); + } + } else { + const errMessage = `Tries to reference ${variable}, which is not defined.`; + if (throwOnBrokenReferences) { + throw new Error(errMessage); + } else { + console.error(errMessage); + } } return ''; } diff --git a/lib/utils/references/resolveReferences.js b/lib/utils/references/resolveReferences.js index 53704b6c4..8f985f27d 100644 --- a/lib/utils/references/resolveReferences.js +++ b/lib/utils/references/resolveReferences.js @@ -36,7 +36,9 @@ const PROPERTY_REFERENCE_WARNINGS = GroupMessages.GROUP.PropertyReferenceWarning * @returns {string|number|undefined} */ export function resolveReferences(value, tokens, opts) { - return _resolveReferences(value, tokens, { ...opts, throwImmediately: true }); + // when using this public API / util, we always throw warnings immediately rather than + // putting them in the GroupMessages PROPERTY_REFERENCE_WARNINGS to collect and throw later on. + return _resolveReferences(value, tokens, { ...opts, warnImmediately: true }); } /** @@ -54,14 +56,15 @@ export function _resolveReferences( separator = defaults.separator, opening_character = defaults.opening_character, closing_character = defaults.closing_character, - ignorePaths = [], usesDtcg = false, + throwOnBrokenReferences = true, + warnImmediately = false, // for internal usage + ignorePaths = [], current_context = [], stack = [], foundCirc = {}, firstIteration = true, - throwImmediately = false, } = {}, ) { const reg = regex ?? createReferenceRegex({ opening_character, closing_character, separator }); @@ -90,8 +93,6 @@ export function _resolveReferences( const pathName = getPathFromName(variable, separator); const refHasValue = valProp === pathName[pathName.length - 1]; - - // FIXME: shouldn't these two "refHasValue" conditions be reversed?? if (refHasValue && ignorePaths.indexOf(variable) !== -1) { return ''; } else if (!refHasValue && ignorePaths.indexOf(`${variable}.${valProp}`) !== -1) { @@ -142,8 +143,12 @@ export function _resolveReferences( // Add circ reference info to our list of warning messages const warning = `Circular definition cycle: ${circStack.join(', ')}`; - if (throwImmediately) { - throw new Error(warning); + if (warnImmediately) { + if (throwOnBrokenReferences) { + throw new Error(warning); + } else { + console.error(warning); + } } else { GroupMessages.add( PROPERTY_REFERENCE_WARNINGS, @@ -155,6 +160,8 @@ export function _resolveReferences( regex: reg, ignorePaths, usesDtcg, + throwOnBrokenReferences, + warnImmediately, current_context, separator, stack, @@ -178,8 +185,12 @@ export function _resolveReferences( const warning = `${ context ? `${context} ` : '' }tries to reference ${variable}, which is not defined.`; - if (throwImmediately) { - throw new Error(warning); + if (warnImmediately) { + if (throwOnBrokenReferences) { + throw new Error(warning); + } else { + console.error(warning); + } } else { GroupMessages.add(PROPERTY_REFERENCE_WARNINGS, warning); } diff --git a/package-lock.json b/package-lock.json index 700724c90..c63f12074 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "style-dictionary", - "version": "4.0.0-prerelease.32", + "version": "4.0.0-prerelease.35", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "style-dictionary", - "version": "4.0.0-prerelease.32", + "version": "4.0.0-prerelease.35", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/types/Config.d.ts b/types/Config.d.ts index 367190e1e..83b913b70 100644 --- a/types/Config.d.ts +++ b/types/Config.d.ts @@ -49,25 +49,34 @@ export interface RegexOptions { export interface GetReferencesOptions extends RegexOptions { usesDtcg?: boolean; + throwOnBrokenReferences?: boolean; +} + +export interface GetReferencesOptionsInternal extends GetReferencesOptions { + warnImmediately?: boolean; unfilteredTokens?: DesignTokens; } export interface ResolveReferencesOptions extends RegexOptions { - ignorePaths?: string[]; usesDtcg?: boolean; + throwOnBrokenReferences?: boolean; } export interface ResolveReferencesOptionsInternal extends ResolveReferencesOptions { + ignorePaths?: string[]; current_context?: string[]; stack?: string[]; foundCirc?: Record; firstIteration?: boolean; - throwImmediately?: boolean; + warnImmediately?: boolean; } export interface LogConfig { warnings?: 'warn' | 'error' | 'disabled'; verbosity?: 'default' | 'silent' | 'verbose'; + errors?: { + brokenReferences?: 'throw' | 'console'; + }; } export type ExpandFilter = (