diff --git a/.changeset/happy-glasses-teach.md b/.changeset/happy-glasses-teach.md new file mode 100644 index 000000000..84f9c21d7 --- /dev/null +++ b/.changeset/happy-glasses-teach.md @@ -0,0 +1,43 @@ +--- +'style-dictionary': major +--- + +BREAKING: Logging has been redesigned a fair bit and is more configurable now. + +Before: + +```json +{ + "log": "error" // 'error' | 'warn' -> 'warn' is the default value +} +``` + +After: + +```json +{ + "log": { + "warnings": "error", // 'error' | 'warn' -> 'warn' is the default value + "verbosity": "verbose" // 'default' | 'verbose' | 'silent' -> 'default' is the default value + } +} +``` + +Log is now and object and the old "log" option is now "warnings". + +This configures whether the following five warnings will be thrown as errors instead of being logged as warnings: + +- Token value collisions (in the source) +- Token name collisions (when exporting) +- Missing "undo" function for Actions +- File not created because no tokens found, or all of them filtered out +- Broken references in file when using outputReferences, but referring to a token that's been filtered out + +Verbosity configures whether the following warnings/errors should display in a verbose manner: + +- Token collisions of both types (value & name) +- Broken references due to outputReferences & filters +- Token reference errors + +And it also configures whether success/neutral logs should be logged at all. +Using "silent" (or --silent in the CLI) means no logs are shown apart from fatal errors. \ No newline at end of file diff --git a/__integration__/__snapshots__/customFormats.test.snap.js b/__integration__/__snapshots__/customFormats.test.snap.js index b29c1d5aa..bf91365b9 100644 --- a/__integration__/__snapshots__/customFormats.test.snap.js +++ b/__integration__/__snapshots__/customFormats.test.snap.js @@ -429,7 +429,10 @@ snapshots["integration custom formats inline custom with new args should match s } } ], - "log": "warn", + "log": { + "warnings": "warn", + "verbosity": "default" + }, "transforms": [ { "type": "attribute" @@ -882,7 +885,10 @@ snapshots["integration custom formats register custom format with new args shoul } } ], - "log": "warn", + "log": { + "warnings": "warn", + "verbosity": "default" + }, "transforms": [ { "type": "attribute" diff --git a/__integration__/__snapshots__/nameCollisions.test.snap.js b/__integration__/__snapshots__/nameCollisions.test.snap.js index 142208ef9..58c24f381 100644 --- a/__integration__/__snapshots__/nameCollisions.test.snap.js +++ b/__integration__/__snapshots__/nameCollisions.test.snap.js @@ -3,7 +3,7 @@ export const snapshots = {}; snapshots["integration name collisions should warn users of name collisions for flat files"] = `⚠️ __integration__/build/variables.css -While building variables.css, token collisions were found; output may be unexpected. +While building variables.css, token collisions were found; output may be unexpected. Ignore this warning if intentional. Output name red was generated by: color.red #f00 color.background.red #f00 @@ -13,3 +13,10 @@ This many-to-one issue is usually caused by some combination of: * overly inclusive file filters`; /* end snapshot integration name collisions should warn users of name collisions for flat files */ +snapshots["integration name collisions should warn users of name collisions for flat files, brief version"] = +`⚠️ __integration__/build/variables.css +While building variables.css, token collisions were found; output may be unexpected. Ignore this warning if intentional. + +Use --verbose or log.verbosity: 'verbose' option for more details`; +/* end snapshot integration name collisions should warn users of name collisions for flat files, brief version */ + diff --git a/__integration__/__snapshots__/outputReferences.test.snap.js b/__integration__/__snapshots__/outputReferences.test.snap.js index a70256704..33c6eafe5 100644 --- a/__integration__/__snapshots__/outputReferences.test.snap.js +++ b/__integration__/__snapshots__/outputReferences.test.snap.js @@ -1,9 +1,18 @@ /* @web/test-runner snapshot v1 */ export const snapshots = {}; -snapshots["integration output references should warn the user if filters out references"] = +snapshots["integration output references should warn the user if filters out references briefly"] = `⚠️ __integration__/build/filteredVariables.css -While building filteredVariables.css, filtered out token references were found; output may be unexpected. Here are the references that are used but not defined in the file +While building filteredVariables.css, filtered out token references were found; output may be unexpected. Ignore this warning if intentional. +Here are the references that are used but not defined in the file: + +Use --verbose or log.verbosity: 'verbose' option for more details`; +/* end snapshot integration output references should warn the user if filters out references briefly */ + +snapshots["integration output references should warn the user if filters out references with a detailed message when using verbose logging"] = +`⚠️ __integration__/build/filteredVariables.css +While building filteredVariables.css, filtered out token references were found; output may be unexpected. Ignore this warning if intentional. +Here are the references that are used but not defined in the file: color.core.neutral.100 color.core.neutral.0 color.core.neutral.200 @@ -12,5 +21,5 @@ color.core.orange.0 color.core.green.0 color.core.blue.0 This is caused when combining a filter and \`outputReferences\`.`; -/* end snapshot integration output references should warn the user if filters out references */ +/* end snapshot integration output references should warn the user if filters out references with a detailed message when using verbose logging */ diff --git a/__integration__/logging/__snapshots__/file.test.snap.js b/__integration__/logging/__snapshots__/file.test.snap.js index dfb1dc899..2f08fb137 100644 --- a/__integration__/logging/__snapshots__/file.test.snap.js +++ b/__integration__/logging/__snapshots__/file.test.snap.js @@ -1,16 +1,40 @@ /* @web/test-runner snapshot v1 */ export const snapshots = {}; -snapshots["integration logging file should warn user empty tokens"] = + +snapshots["integration logging file empty tokens should warn user about empty tokens"] = ` css No tokens for empty.css. File not created.`; -/* end snapshot integration logging file should warn user empty tokens */ +/* end snapshot integration logging file empty tokens should warn user about empty tokens */ + +snapshots["integration logging file name collisions should warn users briefly of name collisions by default"] = +` +css +⚠️ __integration__/build/nameCollisions.css +While building nameCollisions.css, token collisions were found; output may be unexpected. Ignore this warning if intentional. + +Use --verbose or log.verbosity: 'verbose' option for more details`; +/* end snapshot integration logging file name collisions should warn users briefly of name collisions by default */ + +snapshots["integration logging file name collisions should throw a brief error of name collisions with log level set to error"] = +`⚠️ __integration__/build/nameCollisions.css +While building nameCollisions.css, token collisions were found; output may be unexpected. Ignore this warning if intentional. + +Use --verbose or log.verbosity: 'verbose' option for more details`; +/* end snapshot integration logging file name collisions should throw a brief error of name collisions with log level set to error */ + +snapshots["integration logging file name collisions should throw a brief error of name collisions with log level set to error on platform level"] = +`⚠️ __integration__/build/nameCollisions.css +While building nameCollisions.css, token collisions were found; output may be unexpected. Ignore this warning if intentional. + +Use --verbose or log.verbosity: 'verbose' option for more details`; +/* end snapshot integration logging file name collisions should throw a brief error of name collisions with log level set to error on platform level */ -snapshots["integration logging file should warn user of name collisions"] = +snapshots["integration logging file name collisions should warn user of name collisions with a detailed message through \"verbose\" verbosity"] = ` css ⚠️ __integration__/build/nameCollisions.css -While building nameCollisions.css, token collisions were found; output may be unexpected. +While building nameCollisions.css, token collisions were found; output may be unexpected. Ignore this warning if intentional. Output name 0 was generated by: color.core.green.0 #ebf9eb color.core.teal.0 #e5f9f5 @@ -183,11 +207,11 @@ This many-to-one issue is usually caused by some combination of: * conflicting or similar paths/names in token definitions * platform transforms/transformGroups affecting names, especially when removing specificity * overly inclusive file filters`; -/* end snapshot integration logging file should warn user of name collisions */ +/* end snapshot integration logging file name collisions should warn user of name collisions with a detailed message through "verbose" verbosity */ -snapshots["integration logging file should not warn user of name collisions with log level set to error"] = +snapshots["integration logging file name collisions should throw detailed error of name collisions through \"verbose\" verbosity and log level set to error"] = `⚠️ __integration__/build/nameCollisions.css -While building nameCollisions.css, token collisions were found; output may be unexpected. +While building nameCollisions.css, token collisions were found; output may be unexpected. Ignore this warning if intentional. Output name 0 was generated by: color.core.green.0 #ebf9eb color.core.teal.0 #e5f9f5 @@ -360,12 +384,40 @@ This many-to-one issue is usually caused by some combination of: * conflicting or similar paths/names in token definitions * platform transforms/transformGroups affecting names, especially when removing specificity * overly inclusive file filters`; -/* end snapshot integration logging file should not warn user of name collisions with log level set to error */ -snapshots["integration logging file should warn user of filtered references"] = +/* end snapshot integration logging file name collisions should throw detailed error of name collisions through "verbose" verbosity and log level set to error */ + +snapshots["integration logging file filtered references should warn users briefly of filtered references by default"] = +` +css +⚠️ __integration__/build/filteredReferences.css +While building filteredReferences.css, filtered out token references were found; output may be unexpected. Ignore this warning if intentional. +Here are the references that are used but not defined in the file: + +Use --verbose or log.verbosity: 'verbose' option for more details`; +/* end snapshot integration logging file filtered references should warn users briefly of filtered references by default */ + +snapshots["integration logging file filtered references should throw a brief error of filtered references with log level set to error"] = +`⚠️ __integration__/build/filteredReferences.css +While building filteredReferences.css, filtered out token references were found; output may be unexpected. Ignore this warning if intentional. +Here are the references that are used but not defined in the file: + +Use --verbose or log.verbosity: 'verbose' option for more details`; +/* end snapshot integration logging file filtered references should throw a brief error of filtered references with log level set to error */ + +snapshots["integration logging file filtered references should throw a brief error of filtered references with log level set to error on platform level"] = +`⚠️ __integration__/build/filteredReferences.css +While building filteredReferences.css, filtered out token references were found; output may be unexpected. Ignore this warning if intentional. +Here are the references that are used but not defined in the file: + +Use --verbose or log.verbosity: 'verbose' option for more details`; +/* end snapshot integration logging file filtered references should throw a brief error of filtered references with log level set to error on platform level */ + +snapshots["integration logging file filtered references should warn user of filtered references with a detailed message through \"verbose\" verbosity"] = ` css ⚠️ __integration__/build/filteredReferences.css -While building filteredReferences.css, filtered out token references were found; output may be unexpected. Here are the references that are used but not defined in the file +While building filteredReferences.css, filtered out token references were found; output may be unexpected. Ignore this warning if intentional. +Here are the references that are used but not defined in the file: color.core.neutral.100 color.core.neutral.0 color.core.neutral.200 @@ -374,11 +426,12 @@ color.core.orange.0 color.core.green.0 color.core.blue.0 This is caused when combining a filter and \`outputReferences\`.`; -/* end snapshot integration logging file should warn user of filtered references */ +/* end snapshot integration logging file filtered references should warn user of filtered references with a detailed message through "verbose" verbosity */ -snapshots["integration logging file should not warn user of filtered references with log level set to error"] = +snapshots["integration logging file filtered references should throw detailed error of filtered references through \"verbose\" verbosity and log level set to error"] = `⚠️ __integration__/build/filteredReferences.css -While building filteredReferences.css, filtered out token references were found; output may be unexpected. Here are the references that are used but not defined in the file +While building filteredReferences.css, filtered out token references were found; output may be unexpected. Ignore this warning if intentional. +Here are the references that are used but not defined in the file: color.core.neutral.100 color.core.neutral.0 color.core.neutral.200 @@ -387,5 +440,5 @@ color.core.orange.0 color.core.green.0 color.core.blue.0 This is caused when combining a filter and \`outputReferences\`.`; -/* end snapshot integration logging file should not warn user of filtered references with log level set to error */ +/* end snapshot integration logging file filtered references should throw detailed error of filtered references through "verbose" verbosity and log level set to error */ diff --git a/__integration__/logging/__snapshots__/platform.test.snap.js b/__integration__/logging/__snapshots__/platform.test.snap.js index 7e23fdb0a..ada7aad0d 100644 --- a/__integration__/logging/__snapshots__/platform.test.snap.js +++ b/__integration__/logging/__snapshots__/platform.test.snap.js @@ -21,7 +21,7 @@ Unknown transformGroup "foo" found in platform "css": snapshots["integration logging platform property reference errors should throw and notify users of unknown references"] = ` Property Reference Errors: -Reference doesn't exist: color.danger.value tries to reference color.red.value, which is not defined. +color.danger.value tries to reference color.red.value, which is not defined. Problems were found when trying to resolve property references`; /* end snapshot integration logging platform property reference errors should throw and notify users of unknown references */ diff --git a/__integration__/logging/config.test.js b/__integration__/logging/config.test.js index 3f4f3d9a7..70feabd19 100644 --- a/__integration__/logging/config.test.js +++ b/__integration__/logging/config.test.js @@ -51,10 +51,26 @@ describe(`integration >`, () => { await expect(consoleOutput).to.matchSnapshot(); }); + it(`should not log anything if the log verbosity is set to silent`, async () => { + const sd = new StyleDictionary({ + log: { + verbosity: 'silent', + }, + source: [ + // including a specific file twice will throw value collision warnings + `__integration__/tokens/size/padding.json`, + `__integration__/tokens/size/_padding.json`, + ], + platforms: {}, + }); + await sd.hasInitialized; + await expect(stub.callCount).to.equal(0); + }); + it(`should not show warnings if given higher log level`, async () => { const sd = new StyleDictionary( { - log: `error`, + log: { warnings: `error` }, source: [ // including a specific file twice will throw value collision warnings `__integration__/tokens/size/padding.json`, diff --git a/__integration__/logging/file.test.js b/__integration__/logging/file.test.js index 8f1e1dfa4..ca9c06cc9 100644 --- a/__integration__/logging/file.test.js +++ b/__integration__/logging/file.test.js @@ -34,145 +34,455 @@ describe(`integration`, () => { describe(`logging`, () => { describe(`file`, () => { - it(`should warn user empty tokens`, async () => { - const sd = new StyleDictionary({ - source: [`__integration__/tokens/**/[!_]*.json?(c)`], - platforms: { - css: { - transformGroup: `css`, - files: [ - { - destination: `empty.css`, - format: `css/variables`, - filter: (token) => token.attributes.category === `foo`, - }, - ], - }, - }, - }); - - await sd.buildAllPlatforms(); - const logs = Array.from(stub.calls).flatMap((call) => call.args); - const consoleOutput = logs.map(cleanConsoleOutput).join('\n'); - await expect(consoleOutput).to.matchSnapshot(); - }); + describe('empty tokens', () => { + it(`should warn user about empty tokens`, async () => { + const sd = new StyleDictionary({ + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + transformGroup: `css`, + files: [ + { + destination: `empty.css`, + format: `css/variables`, + filter: (token) => token.attributes.category === `foo`, + }, + ], + }, + }, + }); - it(`should warn user of name collisions`, async () => { - const sd = new StyleDictionary({ - source: [`__integration__/tokens/**/[!_]*.json?(c)`], - platforms: { - css: { - // no name transform means there will be name collisions - transforms: [`attribute/cti`], - buildPath, - files: [ - { - destination: `nameCollisions.css`, - format: `css/variables`, - filter: (token) => token.attributes.category === `color`, - }, - ], - }, - }, - }); - await sd.buildAllPlatforms(); - const logs = Array.from(stub.calls).flatMap((call) => call.args); - const consoleOutput = logs.map(cleanConsoleOutput).join('\n'); - await expect(consoleOutput).to.matchSnapshot(); - }); + await sd.buildAllPlatforms(); + const logs = Array.from(stub.calls).flatMap((call) => call.args); + const consoleOutput = logs.map(cleanConsoleOutput).join('\n'); + await expect(consoleOutput).to.matchSnapshot(); + }); + + it(`should not warn user about empty tokens with silent log verbosity`, async () => { + const sd = new StyleDictionary({ + log: { verbosity: 'silent' }, + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + transformGroup: `css`, + files: [ + { + destination: `empty.css`, + format: `css/variables`, + filter: (token) => token.attributes.category === `foo`, + }, + ], + }, + }, + }); - it(`should not warn user of name collisions with log level set to error`, async () => { - const sd = new StyleDictionary({ - log: `error`, - source: [`__integration__/tokens/**/[!_]*.json?(c)`], - platforms: { - css: { - // no name transform means there will be name collisions - transforms: [`attribute/cti`], - buildPath, - files: [ - { - destination: `nameCollisions.css`, - format: `css/variables`, - filter: (token) => token.attributes.category === `color`, - }, - ], - }, - }, - }); - let error; - try { await sd.buildAllPlatforms(); - } catch (e) { - error = e; - } - await expect(cleanConsoleOutput(error.message)).to.matchSnapshot(); - // only log is the platform name at the start of the buildPlatform method - expect(stub.callCount).to.equal(1); - expect(stub.firstCall.args).to.eql(['\ncss']); + expect(stub.callCount).to.equal(0); + }); + + it(`should not warn user about empty tokens with log level set to error`, async () => { + const sd = new StyleDictionary({ + log: { warnings: 'error' }, + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + transformGroup: `css`, + files: [ + { + destination: `empty.css`, + format: `css/variables`, + filter: (token) => token.attributes.category === `foo`, + }, + ], + }, + }, + }); + + await expect(sd.buildAllPlatforms()).to.eventually.rejectedWith( + 'No tokens for empty.css. File not created.', + ); + }); + + it(`should not warn user about empty tokens with log level set to error on platform level`, async () => { + const sd = new StyleDictionary({ + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + log: { warnings: 'error' }, + transformGroup: `css`, + files: [ + { + destination: `empty.css`, + format: `css/variables`, + filter: (token) => token.attributes.category === `foo`, + }, + ], + }, + }, + }); + + await expect(sd.buildAllPlatforms()).to.eventually.rejectedWith( + 'No tokens for empty.css. File not created.', + ); + }); }); - it(`should warn user of filtered references`, async () => { - const sd = new StyleDictionary({ - source: [`__integration__/tokens/**/[!_]*.json?(c)`], - platforms: { - css: { - transformGroup: `css`, - buildPath, - files: [ - { - destination: `filteredReferences.css`, - format: `css/variables`, - options: { - outputReferences: true, - }, - // background colors have references, only including them - // should warn the user - filter: (token) => token.attributes.type === `background`, - }, - ], - }, - }, - }); - await sd.buildAllPlatforms(); - const logs = Array.from(stub.calls).flatMap((call) => call.args); - const consoleOutput = logs.map(cleanConsoleOutput).join('\n'); - await expect(consoleOutput).to.matchSnapshot(); + describe('name collisions', () => { + it(`should warn users briefly of name collisions by default`, async () => { + const sd = new StyleDictionary({ + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + // no name transform means there will be name collisions + transforms: [`attribute/cti`], + buildPath, + files: [ + { + destination: `nameCollisions.css`, + format: `css/variables`, + filter: (token) => token.attributes.category === `color`, + }, + ], + }, + }, + }); + await sd.buildAllPlatforms(); + const logs = Array.from(stub.calls).flatMap((call) => call.args); + const consoleOutput = logs.map(cleanConsoleOutput).join('\n'); + await expect(consoleOutput).to.matchSnapshot(); + }); + + it(`should not warn user of name collisions with log verbosity silent`, async () => { + const sd = new StyleDictionary({ + log: { verbosity: 'silent' }, + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + // no name transform means there will be name collisions + transforms: [`attribute/cti`], + buildPath, + files: [ + { + destination: `nameCollisions.css`, + format: `css/variables`, + filter: (token) => token.attributes.category === `color`, + }, + ], + }, + }, + }); + await sd.buildAllPlatforms(); + expect(stub.callCount).to.equal(0); + }); + + it(`should throw a brief error of name collisions with log level set to error`, async () => { + const sd = new StyleDictionary({ + log: { warnings: `error` }, + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + // no name transform means there will be name collisions + transforms: [`attribute/cti`], + buildPath, + files: [ + { + destination: `nameCollisions.css`, + format: `css/variables`, + filter: (token) => token.attributes.category === `color`, + }, + ], + }, + }, + }); + let error; + try { + await sd.buildAllPlatforms(); + } catch (e) { + error = e; + } + await expect(cleanConsoleOutput(error.message)).to.matchSnapshot(); + // only log is the platform name at the start of the buildPlatform method + expect(stub.callCount).to.equal(1); + expect(stub.firstCall.args).to.eql(['\ncss']); + }); + + it(`should throw a brief error of name collisions with log level set to error on platform level`, async () => { + const sd = new StyleDictionary({ + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + log: { warnings: `error` }, + // no name transform means there will be name collisions + transforms: [`attribute/cti`], + buildPath, + files: [ + { + destination: `nameCollisions.css`, + format: `css/variables`, + filter: (token) => token.attributes.category === `color`, + }, + ], + }, + }, + }); + let error; + try { + await sd.buildAllPlatforms(); + } catch (e) { + error = e; + } + await expect(cleanConsoleOutput(error.message)).to.matchSnapshot(); + // only log is the platform name at the start of the buildPlatform method + expect(stub.callCount).to.equal(1); + expect(stub.firstCall.args).to.eql(['\ncss']); + }); + + it(`should warn user of name collisions with a detailed message through "verbose" verbosity`, async () => { + const sd = new StyleDictionary({ + log: { verbosity: 'verbose' }, + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + // no name transform means there will be name collisions + transforms: [`attribute/cti`], + buildPath, + files: [ + { + destination: `nameCollisions.css`, + format: `css/variables`, + filter: (token) => token.attributes.category === `color`, + }, + ], + }, + }, + }); + await sd.buildAllPlatforms(); + const logs = Array.from(stub.calls).flatMap((call) => call.args); + const consoleOutput = logs.map(cleanConsoleOutput).join('\n'); + await expect(consoleOutput).to.matchSnapshot(); + }); + + it(`should throw detailed error of name collisions through "verbose" verbosity and log level set to error`, async () => { + const sd = new StyleDictionary({ + log: { warnings: `error`, verbosity: 'verbose' }, + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + // no name transform means there will be name collisions + transforms: [`attribute/cti`], + buildPath, + files: [ + { + destination: `nameCollisions.css`, + format: `css/variables`, + filter: (token) => token.attributes.category === `color`, + }, + ], + }, + }, + }); + let error; + try { + await sd.buildAllPlatforms(); + } catch (e) { + error = e; + } + await expect(cleanConsoleOutput(error.message)).to.matchSnapshot(); + // only log is the platform name at the start of the buildPlatform method + expect(stub.callCount).to.equal(1); + expect(stub.firstCall.args).to.eql(['\ncss']); + }); }); - it(`should not warn user of filtered references with log level set to error`, async () => { - const sd = new StyleDictionary({ - log: `error`, - source: [`__integration__/tokens/**/[!_]*.json?(c)`], - platforms: { - css: { - transformGroup: `css`, - buildPath, - files: [ - { - destination: `filteredReferences.css`, - format: `css/variables`, - options: { - outputReferences: true, - }, - // background colors have references, only including them - // should warn the user - filter: (token) => token.attributes.type === `background`, - }, - ], - }, - }, - }); - let error; - try { + describe('filtered references', () => { + it(`should warn users briefly of filtered references by default`, async () => { + const sd = new StyleDictionary({ + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + transformGroup: `css`, + buildPath, + files: [ + { + destination: `filteredReferences.css`, + format: `css/variables`, + options: { + outputReferences: true, + }, + // background colors have references, only including them + // should warn the user + filter: (token) => token.attributes.type === `background`, + }, + ], + }, + }, + }); await sd.buildAllPlatforms(); - } catch (e) { - error = e; - } - await expect(cleanConsoleOutput(error.message)).to.matchSnapshot(); - // only log is the platform name at the start of the buildPlatform method - expect(stub.callCount).to.equal(1); - expect(stub.firstCall.args).to.eql(['\ncss']); + const logs = Array.from(stub.calls).flatMap((call) => call.args); + const consoleOutput = logs.map(cleanConsoleOutput).join('\n'); + await expect(consoleOutput).to.matchSnapshot(); + }); + + it(`should not warn user of filtered references with log verbosity silent`, async () => { + const sd = new StyleDictionary({ + log: { verbosity: 'silent' }, + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + transformGroup: `css`, + buildPath, + files: [ + { + destination: `filteredReferences.css`, + format: `css/variables`, + options: { + outputReferences: true, + }, + // background colors have references, only including them + // should warn the user + filter: (token) => token.attributes.type === `background`, + }, + ], + }, + }, + }); + await sd.buildAllPlatforms(); + expect(stub.callCount).to.equal(0); + }); + + it(`should throw a brief error of filtered references with log level set to error`, async () => { + const sd = new StyleDictionary({ + log: { warnings: `error` }, + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + transformGroup: `css`, + buildPath, + files: [ + { + destination: `filteredReferences.css`, + format: `css/variables`, + options: { + outputReferences: true, + }, + // background colors have references, only including them + // should warn the user + filter: (token) => token.attributes.type === `background`, + }, + ], + }, + }, + }); + let error; + try { + await sd.buildAllPlatforms(); + } catch (e) { + error = e; + } + await expect(cleanConsoleOutput(error.message)).to.matchSnapshot(); + // only log is the platform name at the start of the buildPlatform method + expect(stub.callCount).to.equal(1); + expect(stub.firstCall.args).to.eql(['\ncss']); + }); + + it(`should throw a brief error of filtered references with log level set to error on platform level`, async () => { + const sd = new StyleDictionary({ + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + log: { warnings: `error` }, + transformGroup: `css`, + buildPath, + files: [ + { + destination: `filteredReferences.css`, + format: `css/variables`, + options: { + outputReferences: true, + }, + // background colors have references, only including them + // should warn the user + filter: (token) => token.attributes.type === `background`, + }, + ], + }, + }, + }); + let error; + try { + await sd.buildAllPlatforms(); + } catch (e) { + error = e; + } + await expect(cleanConsoleOutput(error.message)).to.matchSnapshot(); + // only log is the platform name at the start of the buildPlatform method + expect(stub.callCount).to.equal(1); + expect(stub.firstCall.args).to.eql(['\ncss']); + }); + + it(`should warn user of filtered references with a detailed message through "verbose" verbosity`, async () => { + const sd = new StyleDictionary({ + log: { verbosity: 'verbose' }, + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + transformGroup: `css`, + buildPath, + files: [ + { + destination: `filteredReferences.css`, + format: `css/variables`, + options: { + outputReferences: true, + }, + // background colors have references, only including them + // should warn the user + filter: (token) => token.attributes.type === `background`, + }, + ], + }, + }, + }); + await sd.buildAllPlatforms(); + const logs = Array.from(stub.calls).flatMap((call) => call.args); + const consoleOutput = logs.map(cleanConsoleOutput).join('\n'); + await expect(consoleOutput).to.matchSnapshot(); + }); + + it(`should throw detailed error of filtered references through "verbose" verbosity and log level set to error`, async () => { + const sd = new StyleDictionary({ + log: { warnings: `error`, verbosity: 'verbose' }, + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + transformGroup: `css`, + buildPath, + files: [ + { + destination: `filteredReferences.css`, + format: `css/variables`, + options: { + outputReferences: true, + }, + // background colors have references, only including them + // should warn the user + filter: (token) => token.attributes.type === `background`, + }, + ], + }, + }, + }); + let error; + try { + await sd.buildAllPlatforms(); + } catch (e) { + error = e; + } + await expect(cleanConsoleOutput(error.message)).to.matchSnapshot(); + // only log is the platform name at the start of the buildPlatform method + expect(stub.callCount).to.equal(1); + expect(stub.firstCall.args).to.eql(['\ncss']); + }); }); }); }); diff --git a/__integration__/nameCollisions.test.js b/__integration__/nameCollisions.test.js index 1e3080b94..428686d9f 100644 --- a/__integration__/nameCollisions.test.js +++ b/__integration__/nameCollisions.test.js @@ -37,11 +37,33 @@ describe('integration', async () => { }); describe('name collisions', async () => { + it(`should warn users of name collisions for flat files, brief version`, async () => { + const sd = new StyleDictionary({ + // we are only testing name collision warnings options so we don't need + // the full source. + tokens, + platforms: { + web: { + buildPath, + files: [ + { + destination: 'variables.css', + format: 'css/variables', + }, + ], + }, + }, + }); + await sd.buildAllPlatforms(); + await expect(stub.lastCall.args.map(cleanConsoleOutput).join('\n')).to.matchSnapshot(); + }); + it(`should warn users of name collisions for flat files`, async () => { const sd = new StyleDictionary({ // we are only testing name collision warnings options so we don't need // the full source. tokens, + log: { verbosity: 'verbose' }, platforms: { web: { buildPath, diff --git a/__integration__/outputReferences.test.js b/__integration__/outputReferences.test.js index 527c845a6..ba29fad83 100644 --- a/__integration__/outputReferences.test.js +++ b/__integration__/outputReferences.test.js @@ -28,7 +28,7 @@ describe('integration', async () => { }); describe('output references', async () => { - it('should warn the user if filters out references', async () => { + it('should warn the user if filters out references briefly', async () => { const sd = new StyleDictionary({ // we are only testing showFileHeader options so we don't need // the full source. @@ -56,5 +56,35 @@ describe('integration', async () => { await sd.buildAllPlatforms(); await expect(stub.lastCall.args.map(cleanConsoleOutput).join('\n')).to.matchSnapshot(); }); + + it('should warn the user if filters out references with a detailed message when using verbose logging', async () => { + const sd = new StyleDictionary({ + log: { verbosity: 'verbose' }, + // we are only testing showFileHeader options so we don't need + // the full source. + source: [`__integration__/tokens/**/[!_]*.json?(c)`], + platforms: { + css: { + transformGroup: 'css', + buildPath, + files: [ + { + destination: 'filteredVariables.css', + format: 'css/variables', + // filter tokens and use outputReferences + // Style Dictionary should build this file ok + // but warn the user + filter: (token) => token.attributes.type === 'background', + options: { + outputReferences: true, + }, + }, + ], + }, + }, + }); + await sd.buildAllPlatforms(); + await expect(stub.lastCall.args.map(cleanConsoleOutput).join('\n')).to.matchSnapshot(); + }); }); }); diff --git a/__integration__/w3c-forward-compat.test.js b/__integration__/w3c-forward-compat.test.js index 37c648180..bba3f6f21 100644 --- a/__integration__/w3c-forward-compat.test.js +++ b/__integration__/w3c-forward-compat.test.js @@ -48,14 +48,14 @@ describe('integration', async () => { type: 'value', matcher: (token) => token.$type === 'color', transformer: (token) => { - return Color(sd.options.usesDtcg ? token.$value : token.value).toRgbString(); + return Color(sd.usesDtcg ? token.$value : token.value).toRgbString(); }, }, 'custom/add/px': { type: 'value', matcher: (token) => token.$type === 'dimension', transformer: (token) => { - return `${sd.options.usesDtcg ? token.$value : token.value}px`; + return `${sd.usesDtcg ? token.$value : token.value}px`; }, }, }, diff --git a/__node_tests__/cliBuild.test.js b/__node_tests__/cliBuild.test.js index 300b5461e..c49c68c5b 100644 --- a/__node_tests__/cliBuild.test.js +++ b/__node_tests__/cliBuild.test.js @@ -18,7 +18,6 @@ import { clearOutput, fileExists } from '../__tests__/__helpers.js'; describe('cliBuildWithJsConfig', () => { beforeEach(() => { clearOutput(undefined, fs); - childProcess.execSync('node ./bin/style-dictionary build --config __tests__/__configs/test.js'); }); afterEach(() => { @@ -26,12 +25,54 @@ describe('cliBuildWithJsConfig', () => { }); it('should work with json config', () => { + childProcess.execSync('node ./bin/style-dictionary build --config __tests__/__configs/test.js'); expect(fileExists('__tests__/__output/web/_icons.css', fs)).to.be.true; expect(fileExists('__tests__/__output/android/colors.xml', fs)).to.be.true; }); it('should work with javascript config', () => { + childProcess.execSync('node ./bin/style-dictionary build --config __tests__/__configs/test.js'); expect(fileExists('__tests__/__output/web/_icons.css', fs)).to.be.true; expect(fileExists('__tests__/__output/android/colors.xml', fs)).to.be.true; }); + + describe('logging args', () => { + it('should not log anything if --silent is used', () => { + const result = childProcess.execSync( + 'node ./bin/style-dictionary build --config __tests__/__configs/test.js --silent', + ); + expect(result.toString()).to.equal(``); + }); + + it('should log briefly if neither --verbose nor --silent is used', () => { + //const logStub = stubMethod(console, 'log'); + const result = childProcess.execSync( + 'node ./bin/style-dictionary build --config __tests__/__configs/tokenCollisions.json', + ); + expect(result.toString()).to.equal(` +css +⚠️ __tests__/__output/css/vars.css +While building vars.css, token collisions were found; output may be unexpected. Ignore this warning if intentional. + +Use --verbose or log.verbosity: 'verbose' option for more details\n`); + }); + + it('should log verbosely if --verbose is used', () => { + //const logStub = stubMethod(console, 'log'); + const result = childProcess.execSync( + 'node ./bin/style-dictionary build --config __tests__/__configs/tokenCollisions.json --verbose', + ); + expect(result.toString()).to.equal(` +css +⚠️ __tests__/__output/css/vars.css +While building vars.css, token collisions were found; output may be unexpected. Ignore this warning if intentional. + Output name red was generated by: + color.red #f00 + color.background.red #f00 +This many-to-one issue is usually caused by some combination of: + * conflicting or similar paths/names in token definitions + * platform transforms/transformGroups affecting names, especially when removing specificity + * overly inclusive file filters\n`); + }); + }); }); diff --git a/__tests__/StyleDictionary.test.js b/__tests__/StyleDictionary.test.js index 6ace4d45a..155f16138 100644 --- a/__tests__/StyleDictionary.test.js +++ b/__tests__/StyleDictionary.test.js @@ -213,7 +213,7 @@ describe('StyleDictionary class + extend method', () => { const sd = new StyleDictionary( { source: ['__tests__/__tokens/paddings.json', '__tests__/__tokens/_paddings.json'], - log: 'error', + log: { warnings: 'error' }, }, { init: false }, ); @@ -337,6 +337,6 @@ describe('StyleDictionary class + extend method', () => { }, }); await sd.hasInitialized; - expect(sd.options.usesDtcg).to.be.true; + expect(sd.usesDtcg).to.be.true; }); }); diff --git a/__tests__/__configs/tokenCollisions.json b/__tests__/__configs/tokenCollisions.json new file mode 100644 index 000000000..d718eb949 --- /dev/null +++ b/__tests__/__configs/tokenCollisions.json @@ -0,0 +1,21 @@ +{ + "tokens": { + "color": { + "red": { "value": "#f00" }, + "background": { + "red": { "value": "{color.red.value}" } + } + } + }, + "platforms": { + "css": { + "buildPath": "__tests__/__output/css/", + "files": [ + { + "format": "css/variables", + "destination": "vars.css" + } + ] + } + } +} diff --git a/__tests__/exportPlatform.test.js b/__tests__/exportPlatform.test.js index 83786e750..b7485b672 100644 --- a/__tests__/exportPlatform.test.js +++ b/__tests__/exportPlatform.test.js @@ -473,7 +473,7 @@ describe('exportPlatform', () => { return token.$type === 'dimension'; }, transformer: (token) => { - return `${sd.options.usesDtcg ? token.$value : token.value}px`; + return `${sd.usesDtcg ? token.$value : token.value}px`; }, }, }, diff --git a/__tests__/transform/config.test.js b/__tests__/transform/config.test.js index 6de599d9b..6c522c165 100644 --- a/__tests__/transform/config.test.js +++ b/__tests__/transform/config.test.js @@ -11,7 +11,9 @@ * and limitations under the License. */ import { expect } from 'chai'; +import { restore, stubMethod } from 'hanbi'; import transformConfig from '../../lib/transform/config.js'; +import chalk from 'chalk'; const dictionary = { transformGroup: { @@ -96,5 +98,57 @@ None of "barTransform", "bazTransform" match the name of a registered transform. 'quxTransform', ]); }); + + it('warns the user if an action is used without a clean function', () => { + const cfg = { + action: { + foo: {}, + }, + }; + const platformCfg = { + actions: ['foo'], + }; + + const logStub = stubMethod(console, 'log'); + transformConfig(platformCfg, cfg, 'test'); + restore(); + expect(logStub.callCount).to.equal(1); + expect(Array.from(logStub.calls)[0].args[0]).to.equal( + chalk.rgb(255, 140, 0).bold('foo action does not have a clean function!'), + ); + }); + + it('throws if an action is used without a clean function with log.warnings set to error', () => { + const cfg = { + log: { warnings: 'error' }, + action: { + foo: {}, + }, + }; + const platformCfg = { + actions: ['foo'], + }; + + expect(() => transformConfig(platformCfg, cfg, 'test')).to.throw( + 'foo action does not have a clean function!', + ); + }); + + it('does not warn user at all when log.verbosity silent is used', () => { + const cfg = { + log: { verbosity: 'silent' }, + action: { + foo: {}, + }, + }; + const platformCfg = { + actions: ['foo'], + }; + + const logStub = stubMethod(console, 'log'); + transformConfig(platformCfg, cfg, 'test'); + restore(); + expect(logStub.callCount).to.equal(0); + }); }); }); diff --git a/__tests__/utils/reference/getReferences.test.js b/__tests__/utils/reference/getReferences.test.js index d2c655474..6a1f82374 100644 --- a/__tests__/utils/reference/getReferences.test.js +++ b/__tests__/utils/reference/getReferences.test.js @@ -52,7 +52,7 @@ describe('utils', () => { describe('public API', () => { it('should not collect errors but rather throw immediately when using public API', () => { expect(() => getReferences('{foo.bar}', tokens)).to.throw( - `Reference doesn't exist: tries to reference foo.bar, which is not defined.`, + `tries to reference foo.bar, which is not defined.`, ); }); }); diff --git a/__tests__/utils/reference/resolveReferences.test.js b/__tests__/utils/reference/resolveReferences.test.js index 1b0664697..66074092f 100644 --- a/__tests__/utils/reference/resolveReferences.test.js +++ b/__tests__/utils/reference/resolveReferences.test.js @@ -31,13 +31,13 @@ describe('utils', () => { it('should not collect errors but rather throw immediately when using public API', () => { const obj = fileToJSON('__tests__/__json_files/multiple_reference_errors.json'); expect(() => publicResolveReferences(obj.a.b, obj)).to.throw( - `Reference doesn't exist: tries to reference b.a, which is not defined.`, + `tries to reference b.a, which is not defined.`, ); expect(() => publicResolveReferences(obj.a.c, obj)).to.throw( - `Reference doesn't exist: tries to reference b.c, which is not defined.`, + `tries to reference b.c, which is not defined.`, ); expect(() => publicResolveReferences(obj.a.d, obj)).to.throw( - `Reference doesn't exist: tries to reference d, which is not defined.`, + `tries to reference d, which is not defined.`, ); }); }); @@ -123,8 +123,8 @@ describe('utils', () => { expect(resolveReferences(obj.foo, obj)).to.be.undefined; expect(resolveReferences(obj.error, obj)).to.be.undefined; expect(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS)).to.eql([ - "Reference doesn't exist: tries to reference bar, which is not defined.", - "Reference doesn't exist: tries to reference a.b.d, which is not defined.", + 'tries to reference bar, which is not defined.', + 'tries to reference a.b.d, which is not defined.', ]); }); @@ -220,9 +220,9 @@ describe('utils', () => { expect(GroupMessages.count(PROPERTY_REFERENCE_WARNINGS)).to.equal(3); expect(JSON.stringify(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS))).to.equal( JSON.stringify([ - "Reference doesn't exist: tries to reference b.a, which is not defined.", - "Reference doesn't exist: tries to reference b.c, which is not defined.", - "Reference doesn't exist: tries to reference d, which is not defined.", + 'tries to reference b.a, which is not defined.', + 'tries to reference b.c, which is not defined.', + 'tries to reference d, which is not defined.', ]), ); }); diff --git a/__tests__/utils/resolveObject.test.js b/__tests__/utils/resolveObject.test.js index c753b841d..f4fd59b5e 100644 --- a/__tests__/utils/resolveObject.test.js +++ b/__tests__/utils/resolveObject.test.js @@ -321,9 +321,9 @@ describe('utils', () => { expect(GroupMessages.count(PROPERTY_REFERENCE_WARNINGS)).to.equal(3); expect(JSON.stringify(GroupMessages.fetchMessages(PROPERTY_REFERENCE_WARNINGS))).to.equal( JSON.stringify([ - "Reference doesn't exist: a.b tries to reference b.a, which is not defined.", - "Reference doesn't exist: a.c tries to reference b.c, which is not defined.", - "Reference doesn't exist: a.d tries to reference d, which is not defined.", + 'a.b tries to reference b.a, which is not defined.', + 'a.c tries to reference b.c, which is not defined.', + 'a.d tries to reference d, which is not defined.', ]), ); }); diff --git a/bin/style-dictionary.js b/bin/style-dictionary.js index d07b04c36..dd6feebe8 100755 --- a/bin/style-dictionary.js +++ b/bin/style-dictionary.js @@ -43,6 +43,11 @@ program .command('build') .description('Builds a style dictionary package from the current directory.') .option('-c, --config ', 'set config path. defaults to ./config.json') + .option( + '-v, --verbose', + 'enable verbose logging for reference errors, token collisions and filtered tokens with outputReferences', + ) + .option('-s, --silent', 'silence all logging, except for fatal errors') .option( '-p, --platform [platform]', 'only build specific platforms. Must be defined in the config', @@ -57,6 +62,11 @@ program 'Removes files specified in the config of the style dictionary package of the current directory.', ) .option('-c, --config ', 'set config path. defaults to ./config.json') + .option( + '-v, --verbose', + 'enable verbose logging for reference errors, token collisions and filtered tokens with outputReferences', + ) + .option('-s, --silent', 'silence all logging, except for fatal errors') .option( '-p, --platform [platform]', 'only clean specific platform(s). Must be defined in the config', @@ -94,35 +104,35 @@ program.on('command:*', function () { process.exit(1); }); +function getSD(configPath, options) { + let verbosity; + if (options.verbose || options.silent) { + verbosity = options.verbose ? 'verbose' : 'silent'; + } + return new StyleDictionary(configPath, { verbosity }); +} + async function styleDictionaryBuild(options) { options = options || {}; const configPath = getConfigPath(options); - - // Create a style dictionary object with the config - const styleDictionary = new StyleDictionary(configPath); + const sd = getSD(configPath, options); if (options.platform && options.platform.length > 0) { - return Promise.all( - options.platforms.map((platform) => styleDictionary.buildPlatform(platform)), - ); + return Promise.all(options.platforms.map((platform) => sd.buildPlatform(platform))); } else { - return styleDictionary.buildAllPlatforms(); + return sd.buildAllPlatforms(); } } async function styleDictionaryClean(options) { options = options || {}; const configPath = getConfigPath(options); - - // Create a style dictionary object with the config - const styleDictionary = new StyleDictionary(configPath); + const sd = getSD(configPath, options); if (options.platform && options.platform.length > 0) { - return Promise.all( - options.platforms.map((platform) => styleDictionary.cleanPlatform(platform)), - ); + return Promise.all(options.platforms.map((platform) => sd.cleanPlatform(platform))); } else { - return styleDictionary.cleanAllPlatforms(); + return sd.cleanAllPlatforms(); } } diff --git a/docs/_sidebar.md b/docs/_sidebar.md index a7e700bea..7be1348f4 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -16,6 +16,7 @@ - [Using the NPM Module](using_the_npm_module.md) - [Using reference utilities](using_reference_utils.md) - [API](api.md) + - [Logging](logging.md) - [Parsers](parsers.md) - [Preprocessors](preprocessors.md) - [Transforms](transforms.md) diff --git a/docs/config.md b/docs/config.md index e4cfeffe1..050a47c09 100644 --- a/docs/config.md +++ b/docs/config.md @@ -147,6 +147,8 @@ You would then change your npm script or CLI command to run that file with Node: | Attribute | Type | Description | | :------------ | :----------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| log | Object (optional) | [Configure logging behavior](logging.md) to either reduce/silence logs or to make them more verbose for debugging purposes. | +| usesDtcg | Boolean (optional) | Whether the tokens are using [DTCG Format](https://tr.designtokens.org/format/) or not. Usually you won't need to configure this, as style-dictionary will auto-detect this format. | | transform | Object (optional) | Custom [transforms](transforms.md) you can include inline rather than using `.registerTransform`. The keys in this object will be the transform's name, the value should be an object with `type` | | format | Object (optional) | Custom [formats](formats.md) you can include inline in the configuration rather than using `.registerFormat`. The keys in this object will be for format's name and value should be the formatter function. | | action | Object (optional) | Custom inline [actions](actions.md). The keys in this object will be the action's name and the value should be an object containing `do` and `undo` methods. | @@ -155,7 +157,6 @@ You would then change your npm script or CLI command to run that file with Node: | include | Array[String] (optional) | An array of file path [globs](https://github.com/isaacs/node-glob) to design token files that contain default styles. Style Dictionary uses this as a base collection of design tokens. The tokens found using the "source" attribute will overwrite tokens found using include. | | source | Array[String] | An array of file path [globs](https://github.com/isaacs/node-glob) to design token files. Style Dictionary will do a deep merge of all of the token files, allowing you to organize your files however you want. | | tokens | Object | The tokens object is a way to include inline design tokens as opposed to using the `source` and `include` arrays. | -| properties | Object | **DEPRECATED** The properties object has been renamed to `tokens`. Using the `properties` object will still work for backwards compatibility. | | platforms | Object[Platform] | An object containing [platform](#platform) config objects that describe how the Style Dictionary should build for that platform. You can add any arbitrary attributes on this object that will get passed to formats and actions (more on these in a bit). This is useful for things like build paths, name prefixes, variable names, etc. | ### Platform diff --git a/docs/logging.md b/docs/logging.md new file mode 100644 index 000000000..df40b110c --- /dev/null +++ b/docs/logging.md @@ -0,0 +1,44 @@ +# Logging + +You can customize the logging behavior of style-dictionary to some extent. + +```js +const sd = new StyleDictionary({ + // these are the defaults + log: { + warnings: 'warn', // 'warn' | 'error' + verbosity: 'default', // 'default' | 'silent' | 'verbose' + }, +}); +``` + +> `log` can also be set on platform specific configuration + +| Param | Type | Description | +| ------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| log | `Object` | | +| log.warnings | `'warn'\|'error'` | Whether warnings should be logged as warnings or thrown as errors. 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 | + +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`: + +- Token value collisions (in the source) +- Token name collisions (when exporting) +- Missing "undo" function for Actions +- File not created because no tokens found, or all of them filtered out +- Broken references in file when using outputReferences, but referring to a token that's been filtered out + +Verbosity configures whether the following warnings/errors should display in a verbose manner: + +- Token collisions of both types (value & name) +- Broken references due to outputReferences & filters +- Token reference errors + +And through `'silent'` it also configures whether success/neutral logs should be logged at all. + +By default the verbosity ('default') will keep logs relatively brief to prevent noise. + +## CLI + +Log verbosity can be passed as an option in the CLI by passing either `-v` or `--verbose` to get verbose logging, +and `-s` or `--silent` to get silent logging. diff --git a/docs/using_reference_utils.md b/docs/using_reference_utils.md index cec42d8e3..adf6481bb 100644 --- a/docs/using_reference_utils.md +++ b/docs/using_reference_utils.md @@ -278,7 +278,7 @@ export const Border = `solid ${Spacing2} ${SemanticBgPrimary}`; ``` > Note that the above example does not support DTCG syntax, but this could be quite easily added, -> since you can query `sd.options.usesDtcg` or inside a formatter functions `dictionary.options.usesDtcg` +> since you can query `sd.usesDtcg` or inside a formatter functions `dictionary.options.usesDtcg` ### typeDtcgDelegate diff --git a/docs/using_the_cli.md b/docs/using_the_cli.md index af3794553..fed9ffc19 100644 --- a/docs/using_the_cli.md +++ b/docs/using_the_cli.md @@ -63,10 +63,12 @@ $ style-dictionary build [options] Options: -| Name | Usage | Description | -| :----------------- | :----------------------------------- | :---------------------------------------------------------------------------------------------------- | -| Configuration Path | -c , --config | Set the path to the configuration file. Defaults to './config.json'. | -| Platform | -p , --platform | Only build a specific platform. If not supplied, builds all platform found in the configuration file. | +| Name | Usage | Description | +| :----------------- | :----------------------------------- | :------------------------------------------------------------------------------------------------------- | +| Configuration Path | -c , --config | Set the path to the configuration file. Defaults to './config.json'. | +| Platform | -p , --platform | Only build a specific platform. If not supplied, builds all platform found in the configuration file. | +| Silent | -s, --silent | Silence all logging, except for fatal errors. | +| Verbose | -v, --verbose | Enable verbose logging for reference errors, token collisions and filtered tokens with outputReferences. | ## clean @@ -78,10 +80,12 @@ $ style-dictionary clean [options] Options: -| Name | Usage | Description | -| :----------------- | :----------------------------------- | :---------------------------------------------------------------------------------------------------- | -| Configuration Path | -c , --config | Set the path to the configuration file. Defaults to './config.json'. | -| Platform | -p , --platform | Only clean a specific platform. If not supplied, cleans all platform found in the configuration file. | +| Name | Usage | Description | +| :----------------- | :----------------------------------- | :------------------------------------------------------------------------------------------------------- | +| Configuration Path | -c , --config | Set the path to the configuration file. Defaults to './config.json'. | +| Platform | -p , --platform | Only clean a specific platform. If not supplied, cleans all platform found in the configuration file. | +| Silent | -s, --silent | Silence all logging, except for fatal errors. | +| Verbose | -v, --verbose | Enable verbose logging for reference errors, token collisions and filtered tokens with outputReferences. | ## init diff --git a/examples/advanced/font-face-rules/README.md b/examples/advanced/font-face-rules/README.md index a27253532..54e51e280 100644 --- a/examples/advanced/font-face-rules/README.md +++ b/examples/advanced/font-face-rules/README.md @@ -34,7 +34,7 @@ Set up the required dependencies by running the command `npm install` in your lo At this point, if you want to build the tokens you can run `npm run build`. This command will generate the files in the `build` folder. -Note, running this example will generate a "While building fonts.css, token collisions were found; output may be unexpected." warning. The warning is expected and can be ignored. +Note, running this example will generate a "While building fonts.css, token collisions were found; output may be unexpected. Ignore this warning if intentional." warning. The warning is expected and can be ignored. #### How does it work diff --git a/lib/StyleDictionary.js b/lib/StyleDictionary.js index a4267c3d5..3cbffbc2a 100644 --- a/lib/StyleDictionary.js +++ b/lib/StyleDictionary.js @@ -40,11 +40,13 @@ import cleanActions from './cleanActions.js'; /** * @typedef {import('../types/Config.d.ts').Config} Config * @typedef {import('../types/Config.d.ts').PlatformConfig} PlatformConfig + * @typedef {import('../types/Config.d.ts').LogConfig} LogConfig * @typedef {import('../types/DesignToken.d.ts').DesignToken} Token * @typedef {import('../types/DesignToken.d.ts').TransformedToken} TransformedToken * @typedef {import('../types/DesignToken.d.ts').DesignTokens} Tokens * @typedef {import('../types/DesignToken.d.ts').TransformedTokens} TransformedTokens * @typedef {import('../types/DesignToken.d.ts').Dictionary} Dictionary + * @typedef {'default' | 'verbose' | 'silent'} Verbosity */ const PROPERTY_VALUE_COLLISIONS = GroupMessages.GROUP.PropertyValueCollisions; @@ -93,16 +95,32 @@ export default class StyleDictionary extends Register { this._options = v; } - constructor(config = {}, { init = true } = {}) { + /** + * @param {Config | string} config + * @param {{ init?: boolean, verbosity?: Verbosity }} ctorOpts + */ + constructor(config = {}, { init = true, verbosity = undefined } = {}) { super(); - /** @type {'warn'|'error'} */ - this.log = 'warn'; this.config = config; this.options = {}; /** @type {Tokens|TransformedTokens} */ this.tokens = {}; /** @type {TransformedToken[]} */ this.allTokens = []; + /** @type {boolean | undefined} */ + this.usesDtcg = undefined; + /** @type {LogConfig} */ + this.log = { + warnings: 'warn', + verbosity: 'default', + }; + /** @type {string[]} */ + this.source = []; + /** @type {string[]} */ + this.include = []; + /** @type {Record} */ + this.platforms = {}; + /** * Gets set after transform because filter happens on format level, * so we know they are transformed by then. @@ -119,25 +137,33 @@ export default class StyleDictionary extends Register { // you can call constructor with { init: false } // and call SDInstance.extend() manually (and catch the error). if (init) { - this.init(); + this.init(verbosity); } } - async init() { - return this.extend(undefined, true); + /** + * @param {Verbosity} [verbosity] + * @returns + */ + async init(verbosity) { + return this.extend(undefined, true, verbosity); } /** - * @param {Config} [config] + * @param {Config | string} [config] * @param {boolean} [mutateOriginal] + * @param {Verbosity} [verbosity] * @returns {Promise} */ - async extend(config = this.config, mutateOriginal = false) { + async extend(config = this.config, mutateOriginal = false, verbosity) { // by default, if extend is called it means extending the current instance // with a new instance without mutating the original if (!mutateOriginal) { - const newSD = new StyleDictionary(deepmerge(this.options, config), { init: false }); - return newSD.init(); + const newSD = new StyleDictionary(deepmerge(this.options, config), { + init: false, + verbosity, + }); + return newSD.init(verbosity); } /** @type {Config} */ @@ -170,19 +196,31 @@ export default class StyleDictionary extends Register { options = config; } + this.log = { + // our defaults + ...this.log, + // user log options override the defaults + ...options.log, + // verbosity is a bit more complex + // can be passed imperatively by constructor (e.g. when using CLI --verbose / --silent) + // otherwise verbosity in user config or fallback to default + verbosity: verbosity ?? options.log?.verbosity ?? this.log.verbosity, + }; + this.options = { + ...options, + log: this.log, + }; + // SD Config options should be passed to class instance as well - Object.entries(options).forEach(([key, val]) => { + Object.entries(this.options).forEach(([key, val]) => { // Bit of a type hack, making the assumption that any property in options can be set as a prop on StyleDictonary instance const _key = /** @type {keyof StyleDictionary} */ (key); this[_key] = val; }); - this.options = options; - - let { usesDtcg } = this.options; - // Try to detect DTCG if not specified by user in options - if (Object.entries(this.tokens).length > 0 && usesDtcg === undefined) { - usesDtcg = detectDtcgSyntax(this.tokens); + // Try to detect DTCG if not specified by user in options and tokens is passed imperatively + if (Object.entries(this.tokens).length > 0 && this.usesDtcg === undefined) { + this.usesDtcg = detectDtcgSyntax(this.tokens); } // grab the inline tokens, ones either defined in the configuration object // or that already exist from extending another style dictionary instance @@ -190,32 +228,32 @@ export default class StyleDictionary extends Register { inlineTokens = deepExtend([{}, this.tokens || {}]); // Update tokens with includes from dependencies - if (this.options.include) { - if (!Array.isArray(this.options.include)) throw new Error('include must be an array'); + if (this.include) { + if (!Array.isArray(this.include)) throw new Error('include must be an array'); const result = await combineJSON( - this.options.include, + this.include, true, undefined, false, this.parsers, - this.options.usesDtcg, + this.usesDtcg, ); includeTokens = result.tokens; - // If it wasn't known yet whether DTCG is used, combineJSON util will have auto-detected it - if (usesDtcg === undefined) { - usesDtcg = result.usesDtcg; + // If it wasn't known yet whether DTCG is used, combineJSON util will have auto-detected it by now + if (this.usesDtcg === undefined) { + this.usesDtcg = result.usesDtcg; } } // Update tokens with current package's source // These highest precedence - if (this.options.source) { - if (!Array.isArray(this.options.source)) throw new Error('source must be an array'); + if (this.source) { + if (!Array.isArray(this.source)) throw new Error('source must be an array'); const result = await combineJSON( - this.options.source, + this.source, true, /** @param {Token} prop */ function Collision(prop) { @@ -228,21 +266,21 @@ export default class StyleDictionary extends Register { }, true, this.parsers, - this.options.usesDtcg, + this.usesDtcg, ); sourceTokens = result.tokens; // If it wasn't known yet whether DTCG is used, combineJSON util will have auto-detected it - if (usesDtcg === undefined) { - usesDtcg = result.usesDtcg; + if (this.usesDtcg === undefined) { + this.usesDtcg = result.usesDtcg; } if (GroupMessages.count(PROPERTY_VALUE_COLLISIONS) > 0) { const collisions = GroupMessages.flush(PROPERTY_VALUE_COLLISIONS).join('\n'); const warn = `\n${PROPERTY_VALUE_COLLISIONS}:\n${collisions}\n\n`; - if (options.log === 'error') { + if (this.log?.warnings === 'error') { throw new Error(warn); - } else { + } else if (this.log?.verbosity !== 'silent') { // eslint-disable-next-line no-console console.log(warn); } @@ -252,7 +290,7 @@ export default class StyleDictionary extends Register { // Merge inline, include, and source tokens const unprocessedTokens = deepExtend([{}, inlineTokens, includeTokens, sourceTokens]); this.tokens = await preprocess(unprocessedTokens, this.preprocessors); - this.options = { ...this.options, usesDtcg }; + this.options = { ...this.options, usesDtcg: this.usesDtcg }; this.hasInitializedResolve(null); // For chaining @@ -266,12 +304,12 @@ export default class StyleDictionary extends Register { async exportPlatform(platform) { await this.hasInitialized; - if (!platform || !this.options?.platforms?.[platform]) { + if (!platform || !this.platforms?.[platform]) { throw new Error('Please supply a valid platform'); } // We don't want to mutate the original object - const platformConfig = transformConfig(this.options.platforms[platform], this, platform); + const platformConfig = transformConfig(this.platforms[platform], this, platform); let exportableResult = this.tokens; @@ -342,11 +380,11 @@ export default class StyleDictionary extends Register { // referenced values, that have not (yet) been transformed should be excluded from resolving const ignorePathsToResolve = deferredPropValueTransforms.map((p) => - getName([p, this.options.usesDtcg ? '$value' : 'value']), + getName([p, this.usesDtcg ? '$value' : 'value']), ); exportableResult = resolveObject(transformed, { ignorePaths: ignorePathsToResolve, - usesDtcg: this.options.usesDtcg, + usesDtcg: this.usesDtcg, }); const newDeferredPropCount = deferredPropValueTransforms.length; @@ -361,7 +399,7 @@ export default class StyleDictionary extends Register { // the resolveObject method will find the circular references // we do this in case there are multiple circular references resolveObject(transformed, { - usesDtcg: this.options.usesDtcg, + usesDtcg: this.usesDtcg, }); finished = true; } else { @@ -386,15 +424,17 @@ export default class StyleDictionary extends Register { */ async getPlatform(platform) { await this.hasInitialized; - // eslint-disable-next-line no-console - console.log('\n' + platform); + if (this.log?.verbosity !== 'silent') { + // eslint-disable-next-line no-console + console.log('\n' + platform); + } - if (!this.options?.platforms?.[platform]) { + if (!this.platforms?.[platform]) { throw new Error(`Platform "${platform}" does not exist`); } // We don't want to mutate the original object - const platformConfig = transformConfig(this.options.platforms[platform], this, platform); + const platformConfig = transformConfig(this.platforms[platform], this, platform); // We need to transform the object before we resolve the // variable names because if a value contains concatenated @@ -402,9 +442,7 @@ export default class StyleDictionary extends Register { // transform the original value (color.border.base) before // replacing that value in the string. const tokens = await this.exportPlatform(platform); - this.allTokens = /** @type {TransformedToken[]} */ ( - flattenTokens(tokens, this.options.usesDtcg) - ); + this.allTokens = /** @type {TransformedToken[]} */ (flattenTokens(tokens, this.usesDtcg)); // This is the dictionary object we pass to the file // building and action methods. return { dictionary: { tokens, allTokens: this.allTokens }, platformConfig }; @@ -424,8 +462,8 @@ export default class StyleDictionary extends Register { async buildAllPlatforms() { await this.hasInitialized; - if (this.options?.platforms) { - await Promise.all(Object.keys(this.options.platforms).map((key) => this.buildPlatform(key))); + if (this.platforms) { + await Promise.all(Object.keys(this.platforms).map((key) => this.buildPlatform(key))); } // For chaining return this; @@ -447,8 +485,8 @@ export default class StyleDictionary extends Register { async cleanAllPlatforms() { await this.hasInitialized; - if (this.options?.platforms) { - await Promise.all(Object.keys(this.options.platforms).map((key) => this.cleanPlatform(key))); + if (this.platforms) { + await Promise.all(Object.keys(this.platforms).map((key) => this.cleanPlatform(key))); } // For chaining return this; diff --git a/lib/buildFile.js b/lib/buildFile.js index 4c73139e9..03c159a35 100644 --- a/lib/buildFile.js +++ b/lib/buildFile.js @@ -39,6 +39,10 @@ import createFormatArgs from './utils/createFormatArgs.js'; * @param {Config} options */ export default async function buildFile(file, platform = {}, dictionary, options) { + // eslint-disable-next-line no-console + const consoleLog = platform?.log?.verbosity === 'silent' ? () => {} : console.log; + const verbosityLog = `Use --verbose or log.verbosity: 'verbose' option for more details`; + const { destination } = file || {}; const filter = /** @type {Matcher|undefined} */ (file.filter); let { format } = file || {}; @@ -76,8 +80,11 @@ export default async function buildFile(file, platform = {}, dictionary, options filteredTokens.tokens.constructor === Object ) { let warnNoFile = `No tokens for ${destination}. File not created.`; - // eslint-disable-next-line no-console - console.log(chalk.rgb(255, 140, 0)(warnNoFile)); + if (platform.log?.warnings === 'error') { + throw new Error(warnNoFile); + } else { + consoleLog(chalk.rgb(255, 140, 0)(warnNoFile)); + } return null; } @@ -135,7 +142,7 @@ export default async function buildFile(file, platform = {}, dictionary, options // because they are not relevant. if ((nested || tokenNamesCollisionCount === 0) && filteredReferencesCount === 0) { // eslint-disable-next-line no-console - console.log(chalk.bold.green(`✔︎ ${fullDestination}`)); + consoleLog(chalk.bold.green(`✔︎ ${fullDestination}`)); } else { const warnHeader = `⚠️ ${fullDestination}`; if (tokenNamesCollisionCount > 0) { @@ -144,7 +151,9 @@ export default async function buildFile(file, platform = {}, dictionary, options ).join('\n '); const title = `While building ${chalk .rgb(255, 69, 0) - .bold(destination)}, token collisions were found; output may be unexpected.`; + .bold( + destination, + )}, token collisions were found; output may be unexpected. Ignore this warning if intentional.`; const help = chalk.rgb( 255, 165, @@ -157,12 +166,15 @@ export default async function buildFile(file, platform = {}, dictionary, options '* overly inclusive file filters', ].join('\n '), ); - const warn = `${warnHeader}\n${title}\n ${tokenNamesCollisionWarnings}\n${help}`; - if (platform?.log === 'error') { + const warn = + platform.log?.verbosity === 'verbose' + ? `${warnHeader}\n${title}\n ${tokenNamesCollisionWarnings}\n${help}` + : `${warnHeader}\n${title}\n\n${verbosityLog}`; + if (platform?.log?.warnings === 'error') { throw new Error(warn); - } else { + } else if (platform?.log?.verbosity !== 'silent') { // eslint-disable-next-line no-console - console.log(chalk.rgb(255, 140, 0).bold(warn)); + consoleLog(chalk.rgb(255, 140, 0).bold(warn)); } } @@ -174,18 +186,22 @@ export default async function buildFile(file, platform = {}, dictionary, options .rgb(255, 69, 0) .bold( destination, - )}, filtered out token references were found; output may be unexpected. Here are the references that are used but not defined in the file`; + )}, filtered out token references were found; output may be unexpected. Ignore this warning if intentional. +Here are the references that are used but not defined in the file:`; const help = chalk.rgb( 255, 165, 0, )(['This is caused when combining a filter and `outputReferences`.'].join('\n ')); - const warn = `${warnHeader}\n${title}\n ${filteredReferencesWarnings}\n${help}`; - if (platform?.log === 'error') { + const warn = + platform.log?.verbosity === 'verbose' + ? `${warnHeader}\n${title}\n ${filteredReferencesWarnings}\n${help}` + : `${warnHeader}\n${title}\n\n${verbosityLog}`; + if (platform?.log?.warnings === 'error') { throw new Error(warn); - } else { + } else if (platform?.log?.verbosity !== 'silent') { // eslint-disable-next-line no-console - console.log(chalk.rgb(255, 140, 0).bold(warn)); + consoleLog(chalk.rgb(255, 140, 0).bold(warn)); } } } diff --git a/lib/cleanDir.js b/lib/cleanDir.js index 9dd226207..48d4bca55 100644 --- a/lib/cleanDir.js +++ b/lib/cleanDir.js @@ -43,8 +43,10 @@ export default async function cleanDir(file, platform = {}) { if (fs.existsSync(dir)) { const dirContents = fs.readdirSync(dir, 'buffer'); if (dirContents.length === 0) { - // eslint-disable-next-line no-console - console.log(chalk.bold.red('-') + ' ' + dir); + if (platform.log?.verbosity !== 'silent') { + // eslint-disable-next-line no-console + console.log(chalk.bold.red('-') + ' ' + dir); + } fs.rmSync(dir, { recursive: true }); } else { break; diff --git a/lib/cleanFile.js b/lib/cleanFile.js index 73be495fd..6be33897b 100644 --- a/lib/cleanFile.js +++ b/lib/cleanFile.js @@ -27,6 +27,8 @@ import { fs } from 'style-dictionary/fs'; * @param {PlatformConfig} [platform] */ export default async function cleanFile(file, platform = {}) { + // eslint-disable-next-line no-console + const consoleLog = platform?.log?.verbosity === 'silent' ? () => {} : console.log; let { destination } = file; if (typeof destination !== 'string') throw new Error('Please enter a valid destination'); @@ -37,12 +39,10 @@ export default async function cleanFile(file, platform = {}) { } if (!fs.existsSync(destination)) { - // eslint-disable-next-line no-console - console.log(chalk.bold.red('!') + ' ' + destination + ', does not exist'); + consoleLog(chalk.bold.red('!') + ' ' + destination + ', does not exist'); return; } fs.unlinkSync(destination); - // eslint-disable-next-line no-console - console.log(chalk.bold.red('-') + ' ' + destination); + consoleLog(chalk.bold.red('-') + ' ' + destination); } diff --git a/lib/common/actions.js b/lib/common/actions.js index 6744b7e2f..a06c456c0 100644 --- a/lib/common/actions.js +++ b/lib/common/actions.js @@ -74,13 +74,17 @@ export default { */ copy_assets: { do: async function (_, config) { - // eslint-disable-next-line no-console - console.log('Copying assets directory to ' + config.buildPath + 'assets'); + if (config.log?.verbosity !== 'silent') { + // eslint-disable-next-line no-console + console.log('Copying assets directory to ' + config.buildPath + 'assets'); + } return fs.promises.copyFile('assets', config.buildPath + 'assets'); }, undo: async function (_, config) { - // eslint-disable-next-line no-console - console.log('Removing assets directory from ' + config.buildPath + 'assets'); + if (config.log?.verbosity !== 'silent') { + // eslint-disable-next-line no-console + console.log('Removing assets directory from ' + config.buildPath + 'assets'); + } return fs.promises.unlink(config.buildPath + 'assets'); }, }, diff --git a/lib/transform/config.js b/lib/transform/config.js index 3e4cc9387..d0e9512cd 100644 --- a/lib/transform/config.js +++ b/lib/transform/config.js @@ -14,6 +14,8 @@ import isPlainObject from 'is-plain-obj'; import deepExtend from '../utils/deepExtend.js'; import GroupMessages from '../utils/groupMessages.js'; +import { deepmerge } from '../utils/deepmerge.js'; +import chalk from 'chalk'; /** * @typedef {import('../StyleDictionary.js').default} StyleDictionary @@ -37,7 +39,7 @@ const MISSING_TRANSFORM_ERRORS = GroupMessages.GROUP.MissingRegisterTransformErr */ export default function transformConfig(platformConfig, dictionary, platformName) { const to_ret = { ...platformConfig }; // structuredClone not suitable due to config being able to contain Function() etc. - to_ret.log = platformConfig.log ?? dictionary.log; + to_ret.log = deepmerge(dictionary.log ?? {}, platformConfig.log ?? {}); // The platform can both a transformGroup or an array // of transforms. If given a transformGroup that doesn't exist, @@ -192,7 +194,13 @@ None of ${transform_warnings} match the name of a registered transform. to_ret.actions = actions.map( /** @param {string} action */ function (action) { if (typeof dictionary.action[action].undo !== 'function') { - console.warn(action + ' action does not have a clean function!'); + const message = `${action} action does not have a clean function!`; + if (to_ret.log?.warnings === 'error') { + throw new Error(message); + } else if (to_ret.log?.verbosity !== 'silent') { + // eslint-disable-next-line no-console + console.log(chalk.rgb(255, 140, 0).bold(message)); + } } return dictionary.action[action]; }, diff --git a/lib/utils/references/getReferences.js b/lib/utils/references/getReferences.js index 80b632a9c..c12929f0b 100644 --- a/lib/utils/references/getReferences.js +++ b/lib/utils/references/getReferences.js @@ -90,9 +90,7 @@ export function _getReferences( if (ref !== undefined) { references.push(ref); } else if (throwImmediately) { - throw new Error( - `Reference doesn't exist: tries to reference ${variable}, which is not defined.`, - ); + throw new Error(`tries to reference ${variable}, which is not defined.`); } return ''; } diff --git a/lib/utils/references/resolveReferences.js b/lib/utils/references/resolveReferences.js index 9b67d98a0..53704b6c4 100644 --- a/lib/utils/references/resolveReferences.js +++ b/lib/utils/references/resolveReferences.js @@ -175,9 +175,9 @@ export function _resolveReferences( // User might have passed current_context option which is path (arr) pointing to key // that this value is associated with, helpful for debugging const context = getName(current_context, { separator }); - const warning = `Reference doesn't exist:${ - context ? ` ${context}` : '' - } tries to reference ${variable}, which is not defined.`; + const warning = `${ + context ? `${context} ` : '' + }tries to reference ${variable}, which is not defined.`; if (throwImmediately) { throw new Error(warning); } else { diff --git a/types/Config.d.ts b/types/Config.d.ts index 8e56fdfaa..0324475b8 100644 --- a/types/Config.d.ts +++ b/types/Config.d.ts @@ -52,8 +52,13 @@ export interface ResolveReferencesOptionsInternal extends ResolveReferencesOptio throwImmediately?: boolean; } +export interface LogConfig { + warnings?: 'warn' | 'error'; + verbosity?: 'default' | 'silent' | 'verbose'; +} + export interface PlatformConfig extends RegexOptions { - log?: 'warn' | 'error'; + log?: LogConfig; transformGroup?: string; transforms?: string[] | Omit[]; basePxFontSize?: number; @@ -65,7 +70,7 @@ export interface PlatformConfig extends RegexOptions { } export interface Config { - log?: 'warn' | 'error'; + log?: LogConfig; source?: string[]; include?: string[]; tokens?: DesignTokens;