diff --git a/.changeset/angry-balloons-yell.md b/.changeset/angry-balloons-yell.md new file mode 100644 index 000000000..21a8171fa --- /dev/null +++ b/.changeset/angry-balloons-yell.md @@ -0,0 +1,20 @@ +--- +'style-dictionary': major +--- + +BREAKING: +- `usesReference` util function is now `usesReferences` to be consistent plural form like the other reference util functions. +- `getReferences` first and second parameters have been swapped to be consistent with `resolveReferences`, so value first, then the full token object (instead of the entire dictionary instance). +- `getReferences` accepts a third options parameter which can be used to set reference Regex options as well as an unfilteredTokens object which can be used as a fallback when references are made to tokens that have been filtered out. There will be warnings logged for this. +- `format.formatter` removed old function signature of `(dictionary, platform, file)` in favor of `({ dictionary, platform, options, file })`. +- Types changes: + + - Style Dictionary is entirely strictly typed now, and there will be `.d.ts` files published next to every file, this means that if you import from one of Style Dictionary's entrypoints, you automatically get the types implicitly with it. This is a big win for people using TypeScript, as the majority of the codebase now has much better types, with much fewer `any`s. + - There is no more hand-written Style Dictionary module `index.d.ts` anymore that exposes all type interfaces on itself. This means that you can no longer grab types that aren't members of the Style Dictionary class directly from the default export of the main entrypoint. External types such as `Parser`, `Transform`, `DesignTokens`, etc. can be imported from the newly added types entrypoint: + + ```ts + import type { DesignTokens, Transform, Parser } from 'style-dictionary/types'; + ``` + + Please raise an issue if you find anything missing or suddenly broken. + - `Matcher`, `Transformer`, `Formatter`, etc. are still available, although no longer directly but rather as properties on their parents, so `Filter['matcher']`, `Transform['transformer']`, `Format['formatter']` diff --git a/__integration__/__snapshots__/android.test.snap.js b/__integration__/__snapshots__/android.test.snap.js index cbad02561..58814e3d5 100644 --- a/__integration__/__snapshots__/android.test.snap.js +++ b/__integration__/__snapshots__/android.test.snap.js @@ -171,7 +171,6 @@ snapshots["android/resources should match snapshot"] = 16.00dp 16.00dp 16.00dp - `; /* end snapshot android/resources should match snapshot */ @@ -345,7 +344,6 @@ snapshots["android/resources with references should match snapshot"] = 16.00dp 16.00dp 16.00dp - `; /* end snapshot android/resources with references should match snapshot */ @@ -510,7 +508,6 @@ snapshots["android/resources with filter should match snapshot"] = #ff6d1313 #ff601700 #ff08422f - `; /* end snapshot android/resources with filter should match snapshot */ diff --git a/__integration__/__snapshots__/customFormats.test.snap.js b/__integration__/__snapshots__/customFormats.test.snap.js index b02f9f46b..b2b1eec02 100644 --- a/__integration__/__snapshots__/customFormats.test.snap.js +++ b/__integration__/__snapshots__/customFormats.test.snap.js @@ -165,7 +165,7 @@ snapshots["inline custom with old args should match snapshot"] = ] } ], - "_tokens": { + "unfilteredTokens": { "size": { "padding": { "small": { @@ -430,6 +430,7 @@ snapshots["inline custom with old args should match snapshot"] = } } ], + "log": "warn", "transforms": [ { "type": "attribute" @@ -480,6 +481,7 @@ snapshots["inline custom with old args should match snapshot"] = } } ], + "log": "warn", "transforms": [ { "type": "attribute" @@ -669,7 +671,7 @@ snapshots["inline custom with new args should match snapshot"] = ] } ], - "_tokens": { + "unfilteredTokens": { "size": { "padding": { "small": { @@ -934,6 +936,7 @@ snapshots["inline custom with new args should match snapshot"] = } } ], + "log": "warn", "transforms": [ { "type": "attribute" @@ -1128,7 +1131,7 @@ snapshots["register custom format with old args should match snapshot"] = ] } ], - "_tokens": { + "unfilteredTokens": { "size": { "padding": { "small": { @@ -1378,13 +1381,6 @@ snapshots["register custom format with old args should match snapshot"] = "otherOption": "platform option" }, "files": [ - { - "destination": "registerCustomFormatWithOldArgs.json", - "options": { - "showFileHeader": true, - "otherOption": "Test" - } - }, { "destination": "registerCustomFormatWithNewArgs.json", "options": { @@ -1393,6 +1389,7 @@ snapshots["register custom format with old args should match snapshot"] = } } ], + "log": "warn", "transforms": [ { "type": "attribute" @@ -1409,13 +1406,6 @@ snapshots["register custom format with old args should match snapshot"] = ], "actions": [] }, - "file": { - "options": { - "otherOption": "Test", - "showFileHeader": true - }, - "destination": "registerCustomFormatWithOldArgs.json" - }, "options": { "otherOption": "Test", "showFileHeader": true @@ -1428,13 +1418,6 @@ snapshots["register custom format with old args should match snapshot"] = "otherOption": "platform option" }, "files": [ - { - "destination": "registerCustomFormatWithOldArgs.json", - "options": { - "showFileHeader": true, - "otherOption": "Test" - } - }, { "destination": "registerCustomFormatWithNewArgs.json", "options": { @@ -1443,6 +1426,7 @@ snapshots["register custom format with old args should match snapshot"] = } } ], + "log": "warn", "transforms": [ { "type": "attribute" @@ -1458,13 +1442,6 @@ snapshots["register custom format with old args should match snapshot"] = } ], "actions": [] - }, - "file": { - "destination": "registerCustomFormatWithOldArgs.json", - "options": { - "showFileHeader": true, - "otherOption": "Test" - } } }`; /* end snapshot register custom format with old args should match snapshot */ @@ -1632,7 +1609,7 @@ snapshots["register custom format with new args should match snapshot"] = ] } ], - "_tokens": { + "unfilteredTokens": { "size": { "padding": { "small": { @@ -1882,13 +1859,6 @@ snapshots["register custom format with new args should match snapshot"] = "otherOption": "platform option" }, "files": [ - { - "destination": "registerCustomFormatWithOldArgs.json", - "options": { - "showFileHeader": true, - "otherOption": "Test" - } - }, { "destination": "registerCustomFormatWithNewArgs.json", "options": { @@ -1897,6 +1867,7 @@ snapshots["register custom format with new args should match snapshot"] = } } ], + "log": "warn", "transforms": [ { "type": "attribute" diff --git a/__integration__/__snapshots__/objectValues.test.snap.js b/__integration__/__snapshots__/objectValues.test.snap.js index 627af2e87..61c01eb73 100644 --- a/__integration__/__snapshots__/objectValues.test.snap.js +++ b/__integration__/__snapshots__/objectValues.test.snap.js @@ -121,3 +121,123 @@ $border-primary: $size-border solid $color-red; `; /* end snapshot scss/variables with references should match snapshot */ +snapshots["integration object values css/variables shadow should match snapshot with references"] = +`/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --shadow-light: var(--color-red), var(--color-green); + --shadow-dark: var(--color-green), var(--color-red); +} +`; +/* end snapshot integration object values css/variables shadow should match snapshot with references */ + +snapshots["integration object values css/variables hsl syntax should match snapshot"] = +`/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --color-red: #ff0000; + --color-green: hsl(120, 50%, 50%); +} +`; +/* end snapshot integration object values css/variables hsl syntax should match snapshot */ + +snapshots["integration object values css/variables hsl syntax with references should match snapshot"] = +`/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --color-red: #ff0000; + --color-green: hsl(120, 50%, 50%); +} +`; +/* end snapshot integration object values css/variables hsl syntax with references should match snapshot */ + +snapshots["integration object values css/variables hex syntax should match snapshot"] = +`/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --color-red: #ff0000; + --color-green: #40bf40; +} +`; +/* end snapshot integration object values css/variables hex syntax should match snapshot */ + +snapshots["integration object values css/variables hex syntax with references should match snapshot"] = +`/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --color-red: #ff0000; + --color-green: #40bf40; +} +`; +/* end snapshot integration object values css/variables hex syntax with references should match snapshot */ + +snapshots["integration object values css/variables border should match snapshot"] = +`/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --border-primary: 0.125rem solid #ff0000; +} +`; +/* end snapshot integration object values css/variables border should match snapshot */ + +snapshots["integration object values css/variables border with references should match snapshot"] = +`/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --border-primary: var(--size-border) solid var(--color-red); +} +`; +/* end snapshot integration object values css/variables border with references should match snapshot */ + +snapshots["integration object values css/variables shadow should match snapshot"] = +`/** + * Do not edit directly + * Generated on Sat, 01 Jan 2000 00:00:00 GMT + */ + +:root { + --shadow-light: #ff0000, #40bf40; + --shadow-dark: #40bf40, #ff0000; +} +`; +/* end snapshot integration object values css/variables shadow should match snapshot */ + +snapshots["integration object values scss/variables should match snapshot"] = +` +// Do not edit directly +// Generated on Sat, 01 Jan 2000 00:00:00 GMT + +$border-primary: 0.125rem solid #ff0000; +`; +/* end snapshot integration object values scss/variables should match snapshot */ + +snapshots["integration object values scss/variables with references should match snapshot"] = +` +// Do not edit directly +// Generated on Sat, 01 Jan 2000 00:00:00 GMT + +$border-primary: $size-border solid $color-red; +`; +/* end snapshot integration object values scss/variables with references should match snapshot */ + diff --git a/__integration__/android.test.js b/__integration__/android.test.js index 4856ebd5e..97e6cc4f3 100644 --- a/__integration__/android.test.js +++ b/__integration__/android.test.js @@ -23,7 +23,7 @@ describe('integration', () => { describe('android', async () => { const sd = new StyleDictionary({ - source: [`__integration__/tokens/**/*.json?(c)`], + source: [`__integration__/tokens/**/[!_]*.json?(c)`], platforms: { android: { transformGroup: `android`, diff --git a/__integration__/compose.test.js b/__integration__/compose.test.js index 7c87fc3de..0aa6fcf2c 100644 --- a/__integration__/compose.test.js +++ b/__integration__/compose.test.js @@ -23,7 +23,7 @@ describe('integration', () => { describe('compose', async () => { const sd = new StyleDictionary({ - source: [`__integration__/tokens/**/*.json?(c)`], + source: [`__integration__/tokens/**/[!_]*.json?(c)`], platforms: { compose: { transformGroup: `compose`, diff --git a/__integration__/css.test.js b/__integration__/css.test.js index 777720ee5..b340d50d0 100644 --- a/__integration__/css.test.js +++ b/__integration__/css.test.js @@ -23,7 +23,7 @@ describe('integration', () => { describe('css', async () => { const sd = new StyleDictionary({ - source: [`__integration__/tokens/**/*.json?(c)`], + source: [`__integration__/tokens/**/[!_]*.json?(c)`], // Testing proper string interpolation with multiple references here. // This is a CSS/web-specific thing so only including them in this // integration test. diff --git a/__integration__/customFormats.test.js b/__integration__/customFormats.test.js index 68c4ee368..7c293b904 100644 --- a/__integration__/customFormats.test.js +++ b/__integration__/customFormats.test.js @@ -66,14 +66,6 @@ describe('integration', () => { otherOption: `platform option`, }, files: [ - { - destination: 'registerCustomFormatWithOldArgs.json', - format: 'registerCustomFormatWithOldArgs', - options: { - showFileHeader: true, - otherOption: 'Test', - }, - }, { destination: 'registerCustomFormatWithNewArgs.json', format: 'registerCustomFormatWithNewArgs', @@ -87,13 +79,6 @@ describe('integration', () => { }, }); - sd.registerFormat({ - name: 'registerCustomFormatWithOldArgs', - formatter: (dictionary, platform, file) => { - return JSON.stringify({ dictionary, platform, file }, null, 2); - }, - }); - sd.registerFormat({ name: 'registerCustomFormatWithNewArgs', formatter: (opts) => { @@ -103,24 +88,6 @@ describe('integration', () => { await sd.buildAllPlatforms(); - describe(`inline custom with old args`, () => { - const output = fs.readFileSync(`${buildPath}inlineCustomFormatWithOldArgs.json`, { - encoding: 'UTF-8', - }); - - it(`should match snapshot`, async () => { - await expect(output).to.matchSnapshot(); - }); - - it(`should receive proper arguments`, () => { - const { dictionary, platform, file } = JSON.parse(output); - expect(dictionary).to.have.property(`tokens`); - expect(dictionary).to.have.property(`allTokens`); - expect(platform).to.have.nested.property(`options.otherOption`, `platform option`); - expect(file).to.have.nested.property(`options.otherOption`, `Test`); - }); - }); - describe(`inline custom with new args`, async () => { const output = fs.readFileSync(`${buildPath}inlineCustomFormatWithNewArgs.json`, { encoding: 'UTF-8', @@ -139,24 +106,6 @@ describe('integration', () => { }); }); - describe(`register custom format with old args`, () => { - const output = fs.readFileSync(`${buildPath}registerCustomFormatWithOldArgs.json`, { - encoding: 'UTF-8', - }); - - it(`should match snapshot`, async () => { - await expect(output).to.matchSnapshot(); - }); - - it(`should receive proper arguments`, () => { - const { dictionary, platform, file } = JSON.parse(output); - expect(dictionary).to.have.property(`tokens`); - expect(dictionary).to.have.property(`allTokens`); - expect(platform).to.have.nested.property(`options.otherOption`, `platform option`); - expect(file).to.have.nested.property(`options.otherOption`, `Test`); - }); - }); - describe(`register custom format with new args`, () => { const output = fs.readFileSync(`${buildPath}registerCustomFormatWithNewArgs.json`, { encoding: 'UTF-8', diff --git a/__integration__/flutter.test.js b/__integration__/flutter.test.js index 5308cc465..b1188c455 100644 --- a/__integration__/flutter.test.js +++ b/__integration__/flutter.test.js @@ -23,7 +23,7 @@ describe('integration', () => { describe('flutter', async () => { const sd = new StyleDictionary({ - source: [`__integration__/tokens/**/*.json?(c)`], + source: [`__integration__/tokens/**/[!_]*.json?(c)`], platforms: { flutter: { transformGroup: `flutter`, diff --git a/__integration__/iOSObjectiveC.test.js b/__integration__/iOSObjectiveC.test.js index b488cb4ec..eed72f775 100644 --- a/__integration__/iOSObjectiveC.test.js +++ b/__integration__/iOSObjectiveC.test.js @@ -23,7 +23,7 @@ describe('integration', () => { describe('ios objective-c', async () => { const sd = new StyleDictionary({ - source: [`__integration__/tokens/**/*.json?(c)`], + source: [`__integration__/tokens/**/[!_]*.json?(c)`], platforms: { flutter: { transformGroup: `ios`, diff --git a/__integration__/logging/__snapshots__/config.test.snap.js b/__integration__/logging/__snapshots__/config.test.snap.js index ac42f9ec7..283a22e19 100644 --- a/__integration__/logging/__snapshots__/config.test.snap.js +++ b/__integration__/logging/__snapshots__/config.test.snap.js @@ -4,16 +4,16 @@ snapshots["integration > logging > config > property value collisions should not ` Property Value Collisions: Collision detected at: size.padding.small! Original value: 0.5, New value: 0.5 -Collision detected at: size.padding.small! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/padding.json +Collision detected at: size.padding.small! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/_padding.json Collision detected at: size.padding.small! Original value: true, New value: true Collision detected at: size.padding.medium! Original value: 1, New value: 1 -Collision detected at: size.padding.medium! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/padding.json +Collision detected at: size.padding.medium! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/_padding.json Collision detected at: size.padding.medium! Original value: true, New value: true Collision detected at: size.padding.large! Original value: 1, New value: 1 -Collision detected at: size.padding.large! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/padding.json +Collision detected at: size.padding.large! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/_padding.json Collision detected at: size.padding.large! Original value: true, New value: true Collision detected at: size.padding.xl! Original value: 1, New value: 1 -Collision detected at: size.padding.xl! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/padding.json +Collision detected at: size.padding.xl! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/_padding.json Collision detected at: size.padding.xl! Original value: true, New value: true `; @@ -23,16 +23,16 @@ snapshots["integration > logging > config > property value collisions should not ` Property Value Collisions: Collision detected at: size.padding.small! Original value: 0.5, New value: 0.5 -Collision detected at: size.padding.small! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/padding.json +Collision detected at: size.padding.small! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/_padding.json Collision detected at: size.padding.small! Original value: true, New value: true Collision detected at: size.padding.medium! Original value: 1, New value: 1 -Collision detected at: size.padding.medium! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/padding.json +Collision detected at: size.padding.medium! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/_padding.json Collision detected at: size.padding.medium! Original value: true, New value: true Collision detected at: size.padding.large! Original value: 1, New value: 1 -Collision detected at: size.padding.large! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/padding.json +Collision detected at: size.padding.large! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/_padding.json Collision detected at: size.padding.large! Original value: true, New value: true Collision detected at: size.padding.xl! Original value: 1, New value: 1 -Collision detected at: size.padding.xl! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/padding.json +Collision detected at: size.padding.xl! Original value: __integration__/tokens/size/padding.json, New value: __integration__/tokens/size/_padding.json Collision detected at: size.padding.xl! Original value: true, New value: true `; diff --git a/__integration__/logging/config.test.js b/__integration__/logging/config.test.js index 57479d902..3f4f3d9a7 100644 --- a/__integration__/logging/config.test.js +++ b/__integration__/logging/config.test.js @@ -42,7 +42,7 @@ describe(`integration >`, () => { source: [ // including a specific file twice will throw value collision warnings `__integration__/tokens/size/padding.json`, - `__integration__/tokens/size/padding.json`, + `__integration__/tokens/size/_padding.json`, ], platforms: {}, }); @@ -58,7 +58,7 @@ describe(`integration >`, () => { source: [ // including a specific file twice will throw value collision warnings `__integration__/tokens/size/padding.json`, - `__integration__/tokens/size/padding.json`, + `__integration__/tokens/size/_padding.json`, ], platforms: {}, }, diff --git a/__integration__/logging/file.test.js b/__integration__/logging/file.test.js index 15ba0abcb..8f1e1dfa4 100644 --- a/__integration__/logging/file.test.js +++ b/__integration__/logging/file.test.js @@ -36,7 +36,7 @@ describe(`integration`, () => { describe(`file`, () => { it(`should warn user empty tokens`, async () => { const sd = new StyleDictionary({ - source: [`__integration__/tokens/**/*.json?(c)`], + source: [`__integration__/tokens/**/[!_]*.json?(c)`], platforms: { css: { transformGroup: `css`, @@ -59,7 +59,7 @@ describe(`integration`, () => { it(`should warn user of name collisions`, async () => { const sd = new StyleDictionary({ - source: [`__integration__/tokens/**/*.json?(c)`], + source: [`__integration__/tokens/**/[!_]*.json?(c)`], platforms: { css: { // no name transform means there will be name collisions @@ -84,7 +84,7 @@ describe(`integration`, () => { 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)`], + source: [`__integration__/tokens/**/[!_]*.json?(c)`], platforms: { css: { // no name transform means there will be name collisions @@ -114,7 +114,7 @@ describe(`integration`, () => { it(`should warn user of filtered references`, async () => { const sd = new StyleDictionary({ - source: [`__integration__/tokens/**/*.json?(c)`], + source: [`__integration__/tokens/**/[!_]*.json?(c)`], platforms: { css: { transformGroup: `css`, @@ -143,7 +143,7 @@ describe(`integration`, () => { 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)`], + source: [`__integration__/tokens/**/[!_]*.json?(c)`], platforms: { css: { transformGroup: `css`, diff --git a/__integration__/objectValues.test.js b/__integration__/objectValues.test.js index c0ec12e24..9553b961f 100644 --- a/__integration__/objectValues.test.js +++ b/__integration__/objectValues.test.js @@ -20,198 +20,197 @@ import { clearOutput } from '../__tests__/__helpers.js'; const options = { outputReferences: true, }; - -describe('integration', () => { - afterEach(() => { - clearOutput(buildPath); - }); - - describe('object values', async () => { - const sd = new StyleDictionary({ - tokens: { - hue: `120`, - saturation: `50%`, - lightness: `50%`, - color: { - red: { value: '#f00' }, - green: { - value: { - h: '{hue}', - s: '{saturation}', - l: '{lightness}', - }, - }, - }, - size: { - border: { value: 0.125 }, +const sd = new StyleDictionary({ + tokens: { + hue: `120`, + saturation: `50%`, + lightness: `50%`, + color: { + red: { value: '#f00' }, + green: { + value: { + h: '{hue}', + s: '{saturation}', + l: '{lightness}', }, - border: { - primary: { - // getReferences should work on objects like this: - value: { - color: '{color.red.value}', - width: '{size.border.value}', - style: 'solid', - }, - }, + }, + }, + size: { + border: { value: 0.125 }, + }, + border: { + primary: { + // getReferences should work on objects like this: + value: { + color: '{color.red.value}', + width: '{size.border.value}', + style: 'solid', }, - shadow: { - light: { - value: [ - { - color: '{color.red.value}', - }, - { - color: '{color.green.value}', - }, - ], + }, + }, + shadow: { + light: { + value: [ + { + color: '{color.red.value}', }, - dark: { - value: [ - { - color: '{color.green.value}', - }, - { - color: '{color.red.value}', - }, - ], + { + color: '{color.green.value}', }, - }, + ], }, - transform: { - hsl: { - type: 'value', - transitive: true, - matcher: (token) => token.original.value.h, - transformer: (token) => { - return `hsl(${token.value.h}, ${token.value.s}, ${token.value.l})`; + dark: { + value: [ + { + color: '{color.green.value}', }, - }, - hslToHex: { - type: 'value', - transitive: true, - matcher: (token) => token.original.value.h, - transformer: (token) => { - return Color(`hsl(${token.value.h}, ${token.value.s}, ${token.value.l})`).toHexString(); + { + color: '{color.red.value}', }, + ], + }, + }, + }, + transform: { + hsl: { + type: 'value', + transitive: true, + matcher: (token) => token.original.value.h, + transformer: (token) => { + return `hsl(${token.value.h}, ${token.value.s}, ${token.value.l})`; + }, + }, + hslToHex: { + type: 'value', + transitive: true, + matcher: (token) => token.original.value.h, + transformer: (token) => { + return Color(`hsl(${token.value.h}, ${token.value.s}, ${token.value.l})`).toHexString(); + }, + }, + cssBorder: { + type: 'value', + transitive: true, + matcher: (token) => token.path[0] === `border`, + transformer: (token) => { + return `${token.value.width} ${token.value.style} ${token.value.color}`; + }, + }, + shadow: { + type: 'value', + transitive: true, + matcher: (token) => token.attributes.category === 'shadow', + transformer: (token) => { + return token.value.map((obj) => obj.color).join(', '); + }, + }, + }, + platforms: { + // This will test to see if a value object for an hsl color works + // with and without `outputReferences` + cssHsl: { + buildPath, + transforms: StyleDictionary.transformGroup.css.concat([`hsl`]), + files: [ + { + destination: `hsl.css`, + format: `css/variables`, + filter: (token) => token.attributes.category === `color`, }, - cssBorder: { - type: 'value', - transitive: true, - matcher: (token) => token.path[0] === `border`, - transformer: (token) => { - return `${token.value.width} ${token.value.style} ${token.value.color}`; - }, + { + destination: `hslWithReferences.css`, + format: `css/variables`, + filter: (token) => token.attributes.category === `color`, + options, }, - shadow: { - type: 'value', - transitive: true, - matcher: (token) => token.attributes.category === 'shadow', - transformer: (token) => { - return token.value.map((obj) => obj.color).join(', '); - }, + ], + }, + + // This will test to see if a value object for an hsl that has been + // transformed to a hex color works with and without `outputReferences` + cssHex: { + buildPath, + transforms: StyleDictionary.transformGroup.css.concat([`cssBorder`, `hslToHex`]), + files: [ + { + destination: 'hex.css', + format: 'css/variables', + filter: (token) => token.attributes.category === `color`, }, - }, - platforms: { - // This will test to see if a value object for an hsl color works - // with and without `outputReferences` - cssHsl: { - buildPath, - transforms: StyleDictionary.transformGroup.css.concat([`hsl`]), - files: [ - { - destination: `hsl.css`, - format: `css/variables`, - filter: (token) => token.attributes.category === `color`, - }, - { - destination: `hslWithReferences.css`, - format: `css/variables`, - filter: (token) => token.attributes.category === `color`, - options, - }, - ], + { + destination: 'hexWithReferences.css', + format: 'css/variables', + filter: (token) => token.attributes.category === `color`, + options, }, + ], + }, - // This will test to see if a value object for an hsl that has been - // transformed to a hex color works with and without `outputReferences` - cssHex: { - buildPath, - transforms: StyleDictionary.transformGroup.css.concat([`cssBorder`, `hslToHex`]), - files: [ - { - destination: 'hex.css', - format: 'css/variables', - filter: (token) => token.attributes.category === `color`, - }, - { - destination: 'hexWithReferences.css', - format: 'css/variables', - filter: (token) => token.attributes.category === `color`, - options, - }, - ], + // This will test to see if a value object for a border + // works with and without `outputReferences` + cssBorder: { + buildPath, + transforms: StyleDictionary.transformGroup.css.concat([`cssBorder`]), + files: [ + { + destination: 'border.css', + format: 'css/variables', + filter: (token) => token.attributes.category === `border`, }, - - // This will test to see if a value object for a border - // works with and without `outputReferences` - cssBorder: { - buildPath, - transforms: StyleDictionary.transformGroup.css.concat([`cssBorder`]), - files: [ - { - destination: 'border.css', - format: 'css/variables', - filter: (token) => token.attributes.category === `border`, - }, - { - destination: 'borderWithReferences.css', - format: 'css/variables', - filter: (token) => token.attributes.category === `border`, - options, - }, - ], + { + destination: 'borderWithReferences.css', + format: 'css/variables', + filter: (token) => token.attributes.category === `border`, + options, }, + ], + }, - cssShadow: { - buildPath, - transforms: StyleDictionary.transformGroup.css.concat([`shadow`, `hslToHex`]), - files: [ - { - destination: 'shadow.css', - format: 'css/variables', - filter: (token) => token.attributes.category === `shadow`, - }, - { - destination: 'shadowWithReferences.css', - format: 'css/variables', - filter: (token) => token.attributes.category === `shadow`, - options, - }, - ], + cssShadow: { + buildPath, + transforms: StyleDictionary.transformGroup.css.concat([`shadow`, `hslToHex`]), + files: [ + { + destination: 'shadow.css', + format: 'css/variables', + filter: (token) => token.attributes.category === `shadow`, + }, + { + destination: 'shadowWithReferences.css', + format: 'css/variables', + filter: (token) => token.attributes.category === `shadow`, + options, }, + ], + }, - scss: { - buildPath, - transforms: StyleDictionary.transformGroup.css.concat([`cssBorder`, `hslToHex`]), - files: [ - { - destination: 'border.scss', - format: 'scss/variables', - filter: (token) => token.attributes.category === `border`, - }, - { - destination: 'borderWithReferences.scss', - format: 'scss/variables', - filter: (token) => token.attributes.category === `border`, - options, - }, - ], + scss: { + buildPath, + transforms: StyleDictionary.transformGroup.css.concat([`cssBorder`, `hslToHex`]), + files: [ + { + destination: 'border.scss', + format: 'scss/variables', + filter: (token) => token.attributes.category === `border`, }, - }, - }); - await sd.buildAllPlatforms(); + { + destination: 'borderWithReferences.scss', + format: 'scss/variables', + filter: (token) => token.attributes.category === `border`, + options, + }, + ], + }, + }, +}); +await sd.buildAllPlatforms(); + +describe('integration', () => { + afterEach(() => { + clearOutput(buildPath); + }); + describe('object values', async () => { describe('css/variables', () => { describe(`hsl syntax`, () => { const output = fs.readFileSync(`${buildPath}hsl.css`, { diff --git a/__integration__/outputReferences.test.js b/__integration__/outputReferences.test.js index d4ad63ac3..15a9583e1 100644 --- a/__integration__/outputReferences.test.js +++ b/__integration__/outputReferences.test.js @@ -32,7 +32,7 @@ describe('integration', () => { const sd = new StyleDictionary({ // we are only testing showFileHeader options so we don't need // the full source. - source: [`__integration__/tokens/**/*.json?(c)`], + source: [`__integration__/tokens/**/[!_]*.json?(c)`], platforms: { css: { transformGroup: 'css', diff --git a/__integration__/scss.test.js b/__integration__/scss.test.js index 7efea8775..200e35216 100644 --- a/__integration__/scss.test.js +++ b/__integration__/scss.test.js @@ -24,7 +24,7 @@ describe(`integration`, () => { describe(`scss`, async () => { const sd = new StyleDictionary({ - source: [`__integration__/tokens/**/*.json?(c)`], + source: [`__integration__/tokens/**/[!_]*.json?(c)`], platforms: { css: { transformGroup: `scss`, diff --git a/__integration__/swift.test.js b/__integration__/swift.test.js index 35c198b25..68c6138bf 100644 --- a/__integration__/swift.test.js +++ b/__integration__/swift.test.js @@ -23,7 +23,7 @@ describe('integration', () => { describe('swift', async () => { const sd = new StyleDictionary({ - source: [`__integration__/tokens/**/*.json?(c)`], + source: [`__integration__/tokens/**/[!_]*.json?(c)`], platforms: { flutter: { transformGroup: `ios-swift`, diff --git a/__integration__/tokens/size/_padding.json b/__integration__/tokens/size/_padding.json new file mode 100644 index 000000000..6d2358d71 --- /dev/null +++ b/__integration__/tokens/size/_padding.json @@ -0,0 +1,10 @@ +{ + "size": { + "padding": { + "small": { "value": 0.5 }, + "medium": { "value": 1 }, + "large": { "value": 1 }, + "xl": { "value": 1 } + } + } +} \ No newline at end of file diff --git a/__tests__/StyleDictionary.test.js b/__tests__/StyleDictionary.test.js index c9dc0a9f1..78cc43dbc 100644 --- a/__tests__/StyleDictionary.test.js +++ b/__tests__/StyleDictionary.test.js @@ -212,7 +212,7 @@ describe('StyleDictionary class + extend method', () => { it('should throw an error if the collision is in source files and log is set to error', async () => { const sd = new StyleDictionary( { - source: ['__tests__/__tokens/paddings.json', '__tests__/__tokens/paddings.json'], + source: ['__tests__/__tokens/paddings.json', '__tests__/__tokens/_paddings.json'], log: 'error', }, { init: false }, diff --git a/__tests__/__snapshots__/StyleDictionary.test.snap.js b/__tests__/__snapshots__/StyleDictionary.test.snap.js index 6df9b7bb1..5d60dd17b 100644 --- a/__tests__/__snapshots__/StyleDictionary.test.snap.js +++ b/__tests__/__snapshots__/StyleDictionary.test.snap.js @@ -4,25 +4,25 @@ snapshots["StyleDictionary class + extend method should throw an error if the co ` Property Value Collisions: Collision detected at: size.padding.zero! Original value: 0, New value: 0 -Collision detected at: size.padding.zero! Original value: __tests__/__tokens/paddings.json, New value: __tests__/__tokens/paddings.json +Collision detected at: size.padding.zero! Original value: __tests__/__tokens/paddings.json, New value: __tests__/__tokens/_paddings.json Collision detected at: size.padding.zero! Original value: true, New value: true Collision detected at: size.padding.tiny! Original value: 3, New value: 3 -Collision detected at: size.padding.tiny! Original value: __tests__/__tokens/paddings.json, New value: __tests__/__tokens/paddings.json +Collision detected at: size.padding.tiny! Original value: __tests__/__tokens/paddings.json, New value: __tests__/__tokens/_paddings.json Collision detected at: size.padding.tiny! Original value: true, New value: true Collision detected at: size.padding.small! Original value: 5, New value: 5 -Collision detected at: size.padding.small! Original value: __tests__/__tokens/paddings.json, New value: __tests__/__tokens/paddings.json +Collision detected at: size.padding.small! Original value: __tests__/__tokens/paddings.json, New value: __tests__/__tokens/_paddings.json Collision detected at: size.padding.small! Original value: true, New value: true Collision detected at: size.padding.base! Original value: 10, New value: 10 -Collision detected at: size.padding.base! Original value: __tests__/__tokens/paddings.json, New value: __tests__/__tokens/paddings.json +Collision detected at: size.padding.base! Original value: __tests__/__tokens/paddings.json, New value: __tests__/__tokens/_paddings.json Collision detected at: size.padding.base! Original value: true, New value: true Collision detected at: size.padding.large! Original value: 15, New value: 15 -Collision detected at: size.padding.large! Original value: __tests__/__tokens/paddings.json, New value: __tests__/__tokens/paddings.json +Collision detected at: size.padding.large! Original value: __tests__/__tokens/paddings.json, New value: __tests__/__tokens/_paddings.json Collision detected at: size.padding.large! Original value: true, New value: true Collision detected at: size.padding.xl! Original value: 20, New value: 20 -Collision detected at: size.padding.xl! Original value: __tests__/__tokens/paddings.json, New value: __tests__/__tokens/paddings.json +Collision detected at: size.padding.xl! Original value: __tests__/__tokens/paddings.json, New value: __tests__/__tokens/_paddings.json Collision detected at: size.padding.xl! Original value: true, New value: true Collision detected at: size.padding.xxl! Original value: 30, New value: 30 -Collision detected at: size.padding.xxl! Original value: __tests__/__tokens/paddings.json, New value: __tests__/__tokens/paddings.json +Collision detected at: size.padding.xxl! Original value: __tests__/__tokens/paddings.json, New value: __tests__/__tokens/_paddings.json Collision detected at: size.padding.xxl! Original value: true, New value: true `; diff --git a/__tests__/__tokens/_paddings.json b/__tests__/__tokens/_paddings.json new file mode 100644 index 000000000..0ba5cc545 --- /dev/null +++ b/__tests__/__tokens/_paddings.json @@ -0,0 +1,27 @@ +{ + "size": { + "padding": { + "zero": { + "value": 0 + }, + "tiny": { + "value": "3" + }, + "small": { + "value": "5" + }, + "base": { + "value": "10" + }, + "large": { + "value": "15" + }, + "xl": { + "value": "20" + }, + "xxl": { + "value": "30" + } + } + } +} \ No newline at end of file diff --git a/__tests__/cleanDir.test.js b/__tests__/cleanDir.test.js index 6f363f97d..2d78dcde5 100644 --- a/__tests__/cleanDir.test.js +++ b/__tests__/cleanDir.test.js @@ -43,7 +43,6 @@ describe('cleanDir', () => { cleanDir( { destination: 'test.txt', format }, { buildPath: '__tests__/__output/extradir1/extradir2/' }, - {}, ); expect(dirExists('__tests__/__output/extradir1/extradir2')).to.be.false; expect(dirExists('__tests__/__output/extradir1')).to.be.false; diff --git a/__tests__/cleanDirs.test.js b/__tests__/cleanDirs.test.js index 33104240b..2fedec92e 100644 --- a/__tests__/cleanDirs.test.js +++ b/__tests__/cleanDirs.test.js @@ -56,28 +56,25 @@ describe('cleanDirs', () => { it('should delete without buildPath', () => { buildFiles(dictionary, platform); - cleanFiles(dictionary, platform); - cleanDirs(dictionary, platform); + cleanFiles(platform); + cleanDirs(platform); expect(dirExists('__tests__/__output/extradir1/extradir2')).to.be.false; expect(dirExists('__tests__/__output/extradir1')).to.be.false; }); it('should delete with buildPath', () => { buildFiles(dictionary, platformWithBuildPath); - cleanFiles(dictionary, platformWithBuildPath); - cleanDirs(dictionary, platformWithBuildPath); + cleanFiles(platformWithBuildPath); + cleanDirs(platformWithBuildPath); expect(dirExists('__tests__/__output/extradir1/extradir2')).to.be.false; expect(dirExists('__tests__/t__/__output/extradir1')).to.be.false; }); it('should throw if buildPath does not end in a trailing slash', () => { expect(function () { - cleanDirs( - {}, - { - buildPath: 'foo', - }, - ); + cleanDirs({ + buildPath: 'foo', + }); }).to.throw('Build path must end in a trailing slash or you will get weird file names.'); }); }); diff --git a/__tests__/cleanFiles.test.js b/__tests__/cleanFiles.test.js index 888027c22..187cc983a 100644 --- a/__tests__/cleanFiles.test.js +++ b/__tests__/cleanFiles.test.js @@ -55,13 +55,13 @@ describe('cleanFiles', () => { it('should delete without buildPath', () => { buildFiles(dictionary, platform); - cleanFiles(dictionary, platform); + cleanFiles(platform); expect(fileExists('__tests__/__output/test.json')).to.be.false; }); it('should delete with buildPath', () => { buildFiles(dictionary, platformWithBuildPath); - cleanFiles(dictionary, platformWithBuildPath); + cleanFiles(platformWithBuildPath); expect(fileExists('__tests__/t__/__output/test.json')).to.be.false; }); }); diff --git a/__tests__/common/formatHelpers/createPropertyFormatter.test.js b/__tests__/common/formatHelpers/createPropertyFormatter.test.js index 2282c21f5..7f23a7ee3 100644 --- a/__tests__/common/formatHelpers/createPropertyFormatter.test.js +++ b/__tests__/common/formatHelpers/createPropertyFormatter.test.js @@ -15,200 +15,190 @@ import createPropertyFormatter from '../../../lib/common/formatHelpers/createPro import createDictionary from '../../../lib/utils/createDictionary.js'; const dictionary = createDictionary({ - tokens: { - foo: { - original: { - value: '5px', - type: 'spacing', - }, - attributes: { - category: 'foo', - }, - name: 'foo', - path: ['foo'], + foo: { + original: { value: '5px', type: 'spacing', }, - ref: { - original: { - value: '{foo}', - type: 'spacing', - }, - attributes: { - category: 'ref', - }, - name: 'ref', - path: ['ref'], - value: '5px', + attributes: { + category: 'foo', + }, + name: 'foo', + path: ['foo'], + value: '5px', + type: 'spacing', + }, + ref: { + original: { + value: '{foo}', type: 'spacing', }, + attributes: { + category: 'ref', + }, + name: 'ref', + path: ['ref'], + value: '5px', + type: 'spacing', }, }); const transformedDictionary = createDictionary({ - tokens: { - foo: { - original: { - value: '5px', - type: 'spacing', - }, - attributes: { - category: 'foo', - }, - name: 'foo', - path: ['foo'], + foo: { + original: { value: '5px', type: 'spacing', }, - ref: { - original: { - value: '{foo}', - type: 'spacing', - }, - attributes: { - category: 'ref', - }, - name: 'ref', - path: ['ref'], - value: 'changed by transitive transform', + attributes: { + category: 'foo', + }, + name: 'foo', + path: ['foo'], + value: '5px', + type: 'spacing', + }, + ref: { + original: { + value: '{foo}', type: 'spacing', }, + attributes: { + category: 'ref', + }, + name: 'ref', + path: ['ref'], + value: 'changed by transitive transform', + type: 'spacing', }, }); const numberDictionary = createDictionary({ - tokens: { - foo: { - original: { - value: 10, - type: 'dimension', - }, - attributes: { - category: 'foo', - }, - name: 'foo', - path: ['foo'], + foo: { + original: { value: 10, type: 'dimension', }, - ref: { - original: { - value: '{foo}', - type: 'dimension', - }, - attributes: { - category: 'ref', - }, - name: 'ref', - path: ['ref'], - value: 10, + attributes: { + category: 'foo', + }, + name: 'foo', + path: ['foo'], + value: 10, + type: 'dimension', + }, + ref: { + original: { + value: '{foo}', type: 'dimension', }, - zero: { - original: { - value: 0, - type: 'dimension', - }, - attributes: { - category: 'zero', - }, - name: 'zero', - path: ['zero'], + attributes: { + category: 'ref', + }, + name: 'ref', + path: ['ref'], + value: 10, + type: 'dimension', + }, + zero: { + original: { value: 0, type: 'dimension', }, - 'ref-zero': { - original: { - value: '{zero}', - type: 'dimension', - }, - attributes: { - category: 'ref-zero', - }, - name: 'ref-zero', - path: ['ref-zero'], - value: 0, + attributes: { + category: 'zero', + }, + name: 'zero', + path: ['zero'], + value: 0, + type: 'dimension', + }, + 'ref-zero': { + original: { + value: '{zero}', type: 'dimension', }, + attributes: { + category: 'ref-zero', + }, + name: 'ref-zero', + path: ['ref-zero'], + value: 0, + type: 'dimension', }, }); const multiDictionary = createDictionary({ - tokens: { - foo: { - original: { - value: '10px', - type: 'spacing', - }, - attributes: { - category: 'foo', - }, - name: 'foo', - path: ['foo'], + foo: { + original: { value: '10px', type: 'spacing', }, - bar: { - original: { - value: '15px', - type: 'spacing', - }, - attributes: { - category: 'bar', - }, - name: 'bar', - path: ['bar'], + attributes: { + category: 'foo', + }, + name: 'foo', + path: ['foo'], + value: '10px', + type: 'spacing', + }, + bar: { + original: { value: '15px', type: 'spacing', }, - ref: { - original: { - value: '{foo} 5px {bar}', - type: 'spacing', - }, - attributes: { - category: 'ref', - }, - name: 'ref', - path: ['ref'], - value: '10px 5px 15px', + attributes: { + category: 'bar', + }, + name: 'bar', + path: ['bar'], + value: '15px', + type: 'spacing', + }, + ref: { + original: { + value: '{foo} 5px {bar}', type: 'spacing', }, + attributes: { + category: 'ref', + }, + name: 'ref', + path: ['ref'], + value: '10px 5px 15px', + type: 'spacing', }, }); const objectDictionary = createDictionary({ - tokens: { - foo: { - original: { - value: '5px', - type: 'spacing', - }, - attributes: { - category: 'foo', - }, - name: 'foo', - path: ['foo'], + foo: { + original: { value: '5px', type: 'spacing', }, - ref: { - original: { - value: { - width: '{foo}', - style: 'dashed', - color: '#FF00FF', - }, - type: 'border', - }, - attributes: { - category: 'ref', + attributes: { + category: 'foo', + }, + name: 'foo', + path: ['foo'], + value: '5px', + type: 'spacing', + }, + ref: { + original: { + value: { + width: '{foo}', + style: 'dashed', + color: '#FF00FF', }, - name: 'ref', - path: ['ref'], - value: '5px dashed #FF00FF', type: 'border', }, + attributes: { + category: 'ref', + }, + name: 'ref', + path: ['ref'], + value: '5px dashed #FF00FF', + type: 'border', }, }); @@ -324,20 +314,18 @@ describe('common', () => { }, }; - const commentDictionary = createDictionary({ - tokens: commentTokens, - }); + const commentDictionary = createDictionary(commentTokens); it('should default to putting comment next to the output value', async () => { // long commentStyle const cssFormatter = createPropertyFormatter({ format: 'css', - commentDictionary, + dictionary: commentDictionary, }); // short commentStyle const sassFormatter = createPropertyFormatter({ format: 'sass', - commentDictionary, + dictionary: commentDictionary, }); // red = single-line comment, blue = multi-line comment @@ -357,13 +345,13 @@ describe('common', () => { // long commentStyle const cssFormatter = createPropertyFormatter({ format: 'css', - commentDictionary, + dictionary: commentDictionary, formatting: { commentStyle: 'long', commentPosition: 'above' }, }); // short commentStyle const sassFormatter = createPropertyFormatter({ format: 'sass', - commentDictionary, + dictionary: commentDictionary, formatting: { commentStyle: 'short', commentPosition: 'above' }, }); diff --git a/__tests__/formats/__constants.js b/__tests__/formats/__constants.js index 1e0fa8060..475a90756 100644 --- a/__tests__/formats/__constants.js +++ b/__tests__/formats/__constants.js @@ -50,5 +50,5 @@ const iconTokens = { }, }; -export const colorDictionary = createDictionary({ tokens: colorTokens }); -export const iconDictionary = createDictionary({ tokens: iconTokens }); +export const colorDictionary = createDictionary(colorTokens); +export const iconDictionary = createDictionary(iconTokens); diff --git a/__tests__/formats/__snapshots__/all.test.snap.js b/__tests__/formats/__snapshots__/all.test.snap.js index 64ec2aff8..4e0d426a0 100644 --- a/__tests__/formats/__snapshots__/all.test.snap.js +++ b/__tests__/formats/__snapshots__/all.test.snap.js @@ -259,7 +259,6 @@ snapshots["formats all should match android/resources snapshot"] = --> #FF0000 - `; /* end snapshot formats all should match android/resources snapshot */ @@ -750,112 +749,6 @@ snapshots["formats all should match css/fonts.css snapshot"] = ``; /* end snapshot formats all should match css/fonts.css snapshot */ -snapshots["formats all should match registerCustomFormatWithOldArgs snapshot"] = -`{ - "dictionary": { - "dictionary": { - "tokens": { - "color": { - "red": { - "value": "#FF0000", - "original": { - "value": "#FF0000" - }, - "name": "color_red", - "comment": "comment", - "attributes": { - "category": "color", - "type": "red" - }, - "path": [ - "color", - "red" - ] - } - } - }, - "allTokens": [ - { - "value": "#FF0000", - "original": { - "value": "#FF0000" - }, - "name": "color_red", - "comment": "comment", - "attributes": { - "category": "color", - "type": "red" - }, - "path": [ - "color", - "red" - ] - } - ] - }, - "allTokens": [ - { - "value": "#FF0000", - "original": { - "value": "#FF0000" - }, - "name": "color_red", - "comment": "comment", - "attributes": { - "category": "color", - "type": "red" - }, - "path": [ - "color", - "red" - ] - } - ], - "tokens": { - "color": { - "red": { - "value": "#FF0000", - "original": { - "value": "#FF0000" - }, - "name": "color_red", - "comment": "comment", - "attributes": { - "category": "color", - "type": "red" - }, - "path": [ - "color", - "red" - ] - } - } - }, - "platform": {}, - "file": { - "destination": "__output/", - "format": "javascript/es6", - "filter": { - "attributes": { - "category": "color" - } - } - }, - "options": {} - }, - "platform": {}, - "file": { - "destination": "__output/", - "format": "javascript/es6", - "filter": { - "attributes": { - "category": "color" - } - } - } -}`; -/* end snapshot formats all should match registerCustomFormatWithOldArgs snapshot */ - snapshots["formats all should match registerCustomFormatWithNewArgs snapshot"] = `{ "dictionary": { diff --git a/__tests__/formats/__snapshots__/androidResources.test.snap.js b/__tests__/formats/__snapshots__/androidResources.test.snap.js index d00b81e3c..0da19810e 100644 --- a/__tests__/formats/__snapshots__/androidResources.test.snap.js +++ b/__tests__/formats/__snapshots__/androidResources.test.snap.js @@ -13,7 +13,6 @@ snapshots["formats android/resources should match default snapshot"] = 18rem #ff0000 #ffffff - `; /* end snapshot formats android/resources should match default snapshot */ @@ -29,7 +28,6 @@ snapshots["formats android/resources with resourceType override should match sna 18rem #ff0000 #ffffff - `; /* end snapshot formats android/resources with resourceType override should match snapshot */ @@ -43,7 +41,6 @@ snapshots["formats android/resources with resourceMap override should match snap #F2F3F4 #000000 - `; /* end snapshot formats android/resources with resourceMap override should match snapshot */ diff --git a/__tests__/formats/all.test.js b/__tests__/formats/all.test.js index 429ecb64e..228b365df 100644 --- a/__tests__/formats/all.test.js +++ b/__tests__/formats/all.test.js @@ -47,7 +47,7 @@ const tokens = { describe('formats', () => { Object.keys(formats).forEach((key) => { const formatter = formats[key].bind(file); - const dictionary = createDictionary({ tokens }); + const dictionary = createDictionary(tokens); const output = formatter( createFormatArgs({ dictionary, diff --git a/__tests__/formats/androidResources.test.js b/__tests__/formats/androidResources.test.js index defcd10f0..6319dea5d 100644 --- a/__tests__/formats/androidResources.test.js +++ b/__tests__/formats/androidResources.test.js @@ -106,8 +106,8 @@ const file = { format: 'android/resources', }; -const dictionary = createDictionary({ tokens }); -const customDictionary = createDictionary({ tokens: customTokens }); +const dictionary = createDictionary(tokens); +const customDictionary = createDictionary(customTokens); describe('formats', () => { describe(`android/resources`, () => { diff --git a/__tests__/formats/es6Constants.test.js b/__tests__/formats/es6Constants.test.js index a75630673..bdeaffc3c 100644 --- a/__tests__/formats/es6Constants.test.js +++ b/__tests__/formats/es6Constants.test.js @@ -45,7 +45,7 @@ const tokens = { }; const format = formats['javascript/es6']; -const dictionary = createDictionary({ tokens }); +const dictionary = createDictionary(tokens); describe('formats', () => { describe('javascript/es6', () => { diff --git a/__tests__/formats/javascriptModule.test.js b/__tests__/formats/javascriptModule.test.js index 1b578c904..981f2b6c4 100644 --- a/__tests__/formats/javascriptModule.test.js +++ b/__tests__/formats/javascriptModule.test.js @@ -32,7 +32,7 @@ const tokens = { }; const format = formats['javascript/module']; -const dictionary = createDictionary({ tokens }); +const dictionary = createDictionary(tokens); describe('formats', () => { describe('javascript/module', () => { diff --git a/__tests__/formats/javascriptModuleFlat.test.js b/__tests__/formats/javascriptModuleFlat.test.js index 42c1f42f0..64d360d25 100644 --- a/__tests__/formats/javascriptModuleFlat.test.js +++ b/__tests__/formats/javascriptModuleFlat.test.js @@ -37,7 +37,7 @@ const tokens = { }; const format = formats['javascript/module-flat']; -const dictionary = createDictionary({ tokens }); +const dictionary = createDictionary(tokens); describe('formats', () => { describe('javascript/module-flat', () => { diff --git a/__tests__/formats/javascriptObject.test.js b/__tests__/formats/javascriptObject.test.js index 2d65aa937..48ae7c8ac 100644 --- a/__tests__/formats/javascriptObject.test.js +++ b/__tests__/formats/javascriptObject.test.js @@ -28,7 +28,7 @@ const tokens = { }; const format = formats['javascript/object']; -const dictionary = createDictionary({ tokens }); +const dictionary = createDictionary(tokens); describe('formats', () => { describe('javascript/object', () => { diff --git a/__tests__/formats/javascriptUmd.test.js b/__tests__/formats/javascriptUmd.test.js index a3c262a34..f11a8ca91 100644 --- a/__tests__/formats/javascriptUmd.test.js +++ b/__tests__/formats/javascriptUmd.test.js @@ -32,7 +32,7 @@ const tokens = { }; const format = formats['javascript/umd']; -const dictionary = createDictionary({ tokens }); +const dictionary = createDictionary(tokens); describe('formats', () => { describe('javascript/umd', () => { diff --git a/__tests__/formats/json.test.js b/__tests__/formats/json.test.js index 83e12f918..8fbf8bccf 100644 --- a/__tests__/formats/json.test.js +++ b/__tests__/formats/json.test.js @@ -27,7 +27,7 @@ const tokens = { }; const format = formats['json']; -const dictionary = createDictionary({ tokens }); +const dictionary = createDictionary(tokens); describe('formats', () => { describe('json', () => { diff --git a/__tests__/formats/jsonNested.test.js b/__tests__/formats/jsonNested.test.js index 105828529..4cd30b2a1 100644 --- a/__tests__/formats/jsonNested.test.js +++ b/__tests__/formats/jsonNested.test.js @@ -36,7 +36,7 @@ const tokens = { }; const format = formats['json/nested']; -const dictionary = createDictionary({ tokens }); +const dictionary = createDictionary(tokens); describe('formats', function () { describe('json/nested', function () { diff --git a/__tests__/formats/lessIcons.test.js b/__tests__/formats/lessIcons.test.js index 38e54241b..3cfcdc899 100644 --- a/__tests__/formats/lessIcons.test.js +++ b/__tests__/formats/lessIcons.test.js @@ -55,7 +55,7 @@ const platform = { }; const format = formats['less/icons']; -const dictionary = createDictionary({ tokens }); +const dictionary = createDictionary(tokens); describe('formats', () => { describe('less/icons', () => { diff --git a/__tests__/formats/lessVariables.test.js b/__tests__/formats/lessVariables.test.js index 492241e78..862b39f4b 100644 --- a/__tests__/formats/lessVariables.test.js +++ b/__tests__/formats/lessVariables.test.js @@ -48,7 +48,7 @@ const tokens = { }; const format = formats['less/variables']; -const dictionary = createDictionary({ tokens }); +const dictionary = createDictionary(tokens); describe('formats', () => { describe('less/variables', () => { diff --git a/__tests__/formats/scssIcons.test.js b/__tests__/formats/scssIcons.test.js index ecb8272d1..ee8c53def 100644 --- a/__tests__/formats/scssIcons.test.js +++ b/__tests__/formats/scssIcons.test.js @@ -56,7 +56,7 @@ const platform = { }; const format = formats['scss/icons']; -const dictionary = createDictionary({ tokens }); +const dictionary = createDictionary(tokens); describe('formats', () => { describe('scss/icons', () => { diff --git a/__tests__/formats/scssMaps.test.js b/__tests__/formats/scssMaps.test.js index 4ccc16688..cfa609ba7 100644 --- a/__tests__/formats/scssMaps.test.js +++ b/__tests__/formats/scssMaps.test.js @@ -107,7 +107,7 @@ describe('formats', () => { }; const formatter = formats[key].bind(file); - const dictionary = createDictionary({ tokens }); + const dictionary = createDictionary(tokens); const output = formatter( createFormatArgs({ dictionary, diff --git a/__tests__/formats/scssVariables.test.js b/__tests__/formats/scssVariables.test.js index ba2ae57ef..aac5d1d0f 100644 --- a/__tests__/formats/scssVariables.test.js +++ b/__tests__/formats/scssVariables.test.js @@ -49,7 +49,7 @@ const tokens = { }; const format = formats['scss/variables']; -const dictionary = createDictionary({ tokens }); +const dictionary = createDictionary(tokens); describe('formats', () => { describe('scss/variables', () => { diff --git a/__tests__/formats/stylusVariable.test.js b/__tests__/formats/stylusVariable.test.js index 0cdf5448e..a5fc9e226 100644 --- a/__tests__/formats/stylusVariable.test.js +++ b/__tests__/formats/stylusVariable.test.js @@ -49,7 +49,7 @@ const tokens = { }; const format = formats['stylus/variables']; -const dictionary = createDictionary({ tokens }); +const dictionary = createDictionary(tokens); describe('formats', () => { describe('stylus/variables', () => { diff --git a/__tests__/formats/swiftFile.test.js b/__tests__/formats/swiftFile.test.js index a07f0d63b..ef793f675 100644 --- a/__tests__/formats/swiftFile.test.js +++ b/__tests__/formats/swiftFile.test.js @@ -45,7 +45,7 @@ const tokens = { }; const format = formats['ios-swift/any.swift']; -const dictionary = createDictionary({ tokens }); +const dictionary = createDictionary(tokens); describe('formats', () => { describe('ios-swift/any.swift', () => { diff --git a/__tests__/formats/typeScriptEs6Declarations.test.js b/__tests__/formats/typeScriptEs6Declarations.test.js index cd9bd4a60..264295f30 100644 --- a/__tests__/formats/typeScriptEs6Declarations.test.js +++ b/__tests__/formats/typeScriptEs6Declarations.test.js @@ -35,7 +35,7 @@ const format = formats['typescript/es6-declarations']; describe('formats', () => { describe('typescript/es6-declarations', () => { it('should be a valid TS file', () => { - const dictionary = createDictionary({ tokens }); + const dictionary = createDictionary(tokens); const output = format( createFormatArgs({ dictionary, @@ -61,7 +61,7 @@ describe('formats', () => { }, }; - const dictionary = createDictionary({ tokens }); + const dictionary = createDictionary(tokens); const output = format( createFormatArgs({ dictionary, diff --git a/__tests__/formats/typeScriptModuleDeclarations.test.js b/__tests__/formats/typeScriptModuleDeclarations.test.js index e065a2caf..5e424c34f 100644 --- a/__tests__/formats/typeScriptModuleDeclarations.test.js +++ b/__tests__/formats/typeScriptModuleDeclarations.test.js @@ -36,7 +36,7 @@ const format = formats['typescript/module-declarations'].bind(file); describe('formats', () => { describe('typescript/module-declarations', () => { it('should be a valid TS file', () => { - const dictionary = createDictionary({ tokens }); + const dictionary = createDictionary(tokens); const output = format( createFormatArgs({ dictionary, diff --git a/__tests__/register/action.test.js b/__tests__/register/action.test.js index b7acb47a0..43aafc9aa 100644 --- a/__tests__/register/action.test.js +++ b/__tests__/register/action.test.js @@ -12,6 +12,16 @@ */ import { expect } from 'chai'; import StyleDictionary from 'style-dictionary'; +import { registerSuite } from './register.suite.js'; + +registerSuite({ + config: { + do: () => {}, + undo: () => {}, + }, + registerMethod: 'registerAction', + prop: 'action', +}); describe('register', () => { describe('action', async () => { diff --git a/__tests__/register/fileHeader.test.js b/__tests__/register/fileHeader.test.js index 51eaa65b6..53cc83ee3 100644 --- a/__tests__/register/fileHeader.test.js +++ b/__tests__/register/fileHeader.test.js @@ -12,6 +12,15 @@ */ import { expect } from 'chai'; import StyleDictionary from 'style-dictionary'; +import { registerSuite } from './register.suite.js'; + +registerSuite({ + config: { + fileHeader: () => {}, + }, + registerMethod: 'registerFileHeader', + prop: 'fileHeader', +}); describe('register', () => { describe('fileHeader', async () => { @@ -39,7 +48,7 @@ describe('register', () => { }).to.throw('name must be a string'); expect(() => { - StyleDictionaryExtended.registerFilter({ + StyleDictionaryExtended.registerFileHeader({ name: {}, matcher: function () {}, }); diff --git a/__tests__/register/filter.test.js b/__tests__/register/filter.test.js index d6e3013b2..57dcd00a5 100644 --- a/__tests__/register/filter.test.js +++ b/__tests__/register/filter.test.js @@ -12,6 +12,15 @@ */ import { expect } from 'chai'; import StyleDictionary from 'style-dictionary'; +import { registerSuite } from './register.suite.js'; + +registerSuite({ + config: { + matcher: () => {}, + }, + registerMethod: 'registerFilter', + prop: 'filter', +}); describe('register', () => { describe('filter', async () => { diff --git a/__tests__/register/format.test.js b/__tests__/register/format.test.js index a65006477..4db414eeb 100644 --- a/__tests__/register/format.test.js +++ b/__tests__/register/format.test.js @@ -12,6 +12,15 @@ */ import { expect } from 'chai'; import StyleDictionary from 'style-dictionary'; +import { registerSuite } from './register.suite.js'; + +registerSuite({ + config: { + formatter: () => {}, + }, + registerMethod: 'registerFormat', + prop: 'format', +}); describe('register', () => { describe('format', async () => { diff --git a/__tests__/register/parser.test.js b/__tests__/register/parser.test.js index a284a4006..0c21bbf4c 100644 --- a/__tests__/register/parser.test.js +++ b/__tests__/register/parser.test.js @@ -12,6 +12,16 @@ */ import { expect } from 'chai'; import StyleDictionary from 'style-dictionary'; +// import { registerSuite } from './register.suite.js'; + +// Skipping for now because parsers signature doesn't really match the other things you can register +// registerSuite({ +// config: { +// parse: () => {}, +// }, +// registerMethod: 'registerParser', +// prop: 'parsers', +// }); describe('register', () => { describe('parser', async () => { diff --git a/__tests__/register/preprocessor.test.js b/__tests__/register/preprocessor.test.js index f4ae350d5..63020c1ca 100644 --- a/__tests__/register/preprocessor.test.js +++ b/__tests__/register/preprocessor.test.js @@ -12,6 +12,15 @@ */ import { expect } from 'chai'; import StyleDictionary from 'style-dictionary'; +import { registerSuite } from './register.suite.js'; + +registerSuite({ + config: { + preprocessor: () => {}, + }, + registerMethod: 'registerPreprocessor', + prop: 'preprocessors', +}); describe('register/transformGroup', async () => { let StyleDictionaryExtended; @@ -30,15 +39,6 @@ describe('register/transformGroup', async () => { expect(StyleDictionaryExtended.preprocessors['example-preprocessor']).to.not.be.undefined; }); - it('should support registering preprocessor on StyleDictionary instance, which registers it on the class', () => { - StyleDictionaryExtended.registerPreprocessor({ - name: 'example-preprocessor', - preprocessor: (dict) => dict, - }); - expect(StyleDictionary.preprocessors['example-preprocessor']).to.not.be.undefined; - expect(StyleDictionaryExtended.preprocessors['example-preprocessor']).to.not.be.undefined; - }); - it('should throw if the preprocessor name is not a string', () => { expect(() => { StyleDictionaryExtended.registerPreprocessor({ diff --git a/__tests__/register/register.suite.js b/__tests__/register/register.suite.js new file mode 100644 index 000000000..841f88477 --- /dev/null +++ b/__tests__/register/register.suite.js @@ -0,0 +1,73 @@ +import StyleDictionary from 'style-dictionary'; +import { expect } from 'chai'; + +export function registerSuite(opts) { + /** + * opts example: { + * config: { transformer: () => {} }, + * registerMethod: 'registerTransform', + * prop: 'transform', + * } + * This suite verifies a couple of rules with regards to registering something on class vs instance + */ + const { config, registerMethod, prop, defaultPropVal = {} } = opts; + const configFoo = { ...config, name: 'foo' }; + // same config but under a different name, needed for the third test + const configBar = { ...config, name: 'bar' }; + + describe('Register Test Suite', () => { + const reset = () => { + StyleDictionary[prop] = defaultPropVal; + }; + beforeEach(() => { + reset(); + }); + afterEach(() => { + reset(); + }); + + describe(`instance vs class registration: ${prop}`, () => { + it(`should allow registering ${prop} on class, affecting all instances`, async () => { + StyleDictionary[registerMethod](configFoo); + + const sd1 = new StyleDictionary(); + const sd2 = new StyleDictionary(); + const sd3 = await sd2.extend(); + + expect(sd1[prop][configFoo.name]).to.not.be.undefined; + expect(sd2[prop][configFoo.name]).to.not.be.undefined; + expect(sd3[prop][configFoo.name]).to.not.be.undefined; + }); + + it(`should allow registering ${prop} on instance, affecting only that instance`, async () => { + const sd1 = new StyleDictionary(); + const sd2 = new StyleDictionary(); + const sd3 = await sd2.extend(); + + sd2[registerMethod](configFoo); + + expect(sd1[prop][configFoo.name]).to.be.undefined; + expect(sd2[prop][configFoo.name]).to.not.be.undefined; + expect(sd3[prop][configFoo.name]).to.be.undefined; + }); + + it(`should combine class and instance registrations for ${prop} on the instance`, async () => { + StyleDictionary[registerMethod](configFoo); + + const sd1 = new StyleDictionary(); + const sd2 = new StyleDictionary(); + sd2[registerMethod](configBar); + const sd3 = await sd2.extend(); + + expect(sd1[prop][configFoo.name]).to.not.be.undefined; + expect(sd2[prop][configFoo.name]).to.not.be.undefined; + expect(sd3[prop][configFoo.name]).to.not.be.undefined; + // should not be registered on sd1, because we registered only on sd2 + expect(sd1[prop][configBar.name]).to.be.undefined; + expect(sd2[prop][configBar.name]).to.not.be.undefined; + // should be registered because sd3 extends sd2 + expect(sd3[prop][configBar.name]).to.not.be.undefined; + }); + }); + }); +} diff --git a/__tests__/register/transform.test.js b/__tests__/register/transform.test.js index a203af92a..48b1af38d 100644 --- a/__tests__/register/transform.test.js +++ b/__tests__/register/transform.test.js @@ -12,8 +12,166 @@ */ import { expect } from 'chai'; import StyleDictionary from 'style-dictionary'; +import { registerSuite } from './register.suite.js'; +import transform from '../../lib/common/transforms.js'; + +const transformerPxAppender = { + name: 'px-appender', + type: 'value', + transformer: (token) => `${token.value}px`, +}; + +const transformerValueIncrementer = { + name: 'value-incrementer', + type: 'value', + matcher: (token) => typeof token.value === 'number', + transformer: (token) => token.value + 1, +}; + +registerSuite({ + config: { + type: 'value', + transformer: () => {}, + }, + registerMethod: 'registerTransform', + prop: 'transform', +}); describe('register', () => { + beforeEach(() => { + StyleDictionary.transform = transform; + }); + afterEach(() => { + StyleDictionary.transform = transform; + }); + + describe('instance vs class registration', () => { + it('should allow registering on class, affecting all instances', async () => { + StyleDictionary.registerTransform(transformerPxAppender); + + const baseCfg = { + platforms: { + test: { + transforms: ['px-appender'], + }, + }, + }; + + const sd1 = new StyleDictionary({ + ...baseCfg, + tokens: { + size1: { + value: 1, + type: 'dimension', + }, + }, + }); + const sd2 = new StyleDictionary({ + ...baseCfg, + tokens: { + size2: { + value: 2, + type: 'dimension', + }, + }, + }); + const sd3 = await sd2.extend({ + ...baseCfg, + tokens: { + size3: { + value: 3, + type: 'dimension', + }, + }, + }); + + const [sd1After, sd2After, sd3After] = await Promise.all( + [sd1, sd2, sd3].map((sd) => sd.exportPlatform('test')), + ); + + expect(sd1.transform['px-appender']).to.not.be.undefined; + expect(sd2.transform['px-appender']).to.not.be.undefined; + expect(sd3.transform['px-appender']).to.not.be.undefined; + + expect(sd1After.size1.value).to.equal('1px'); + expect(sd2After.size2.value).to.equal('2px'); + expect(sd3After.size2.value).to.equal('2px'); + expect(sd3After.size3.value).to.equal('3px'); + }); + + it('should allow registering on instance, affecting only that instance', async () => { + const sd1 = new StyleDictionary(); + const sd2 = new StyleDictionary(); + const sd3 = await sd2.extend(); + + sd2.registerTransform(transformerPxAppender); + + expect(sd1.transform['px-appender']).to.be.undefined; + expect(sd2.transform['px-appender']).to.not.be.undefined; + expect(sd3.transform['px-appender']).to.be.undefined; + }); + + it('should combine class and instance registrations on the instance', async () => { + StyleDictionary.registerTransform(transformerPxAppender); + + const sd1 = new StyleDictionary({ + platforms: { + test: { + transforms: ['px-appender'], + }, + }, + tokens: { + size1: { + value: 1, + type: 'dimension', + }, + }, + }); + + const sd2 = new StyleDictionary({ + platforms: { + test: { + transforms: ['value-incrementer', 'px-appender'], + }, + }, + tokens: { + size2: { + value: 2, + type: 'dimension', + }, + }, + }); + sd2.registerTransform(transformerValueIncrementer); + + const sd3 = await sd2.extend({ + tokens: { + size3: { + value: 3, + type: 'dimension', + }, + }, + }); + + const [sd1After, sd2After, sd3After] = await Promise.all( + [sd1, sd2, sd3].map((sd) => sd.exportPlatform('test')), + ); + + expect(sd1.transform['px-appender']).to.not.be.undefined; + expect(sd2.transform['px-appender']).to.not.be.undefined; + expect(sd3.transform['px-appender']).to.not.be.undefined; + // should not be registered on sd1, because we registered only on sd2 + expect(sd1.transform['value-incrementer']).to.be.undefined; + expect(sd2.transform['value-incrementer']).to.not.be.undefined; + // should be registered because sd3 extends sd2 + expect(sd3.transform['value-incrementer']).to.not.be.undefined; + + expect(sd1After.size1.value).to.equal('1px'); + expect(sd2After.size2.value).to.equal('3px'); + expect(sd3After.size2.value).to.equal('3px'); + expect(sd3After.size3.value).to.equal('4px'); + }); + }); + describe('transform', async () => { const StyleDictionaryExtended = new StyleDictionary({}); diff --git a/__tests__/register/transformGroup.test.js b/__tests__/register/transformGroup.test.js index 8c17fa2c3..4f651e596 100644 --- a/__tests__/register/transformGroup.test.js +++ b/__tests__/register/transformGroup.test.js @@ -12,9 +12,18 @@ */ import { expect } from 'chai'; import StyleDictionary from 'style-dictionary'; +import { registerSuite } from './register.suite.js'; const dummyTransformName = 'transformGroup.test.js'; +registerSuite({ + config: { + transforms: ['size/px'], + }, + registerMethod: 'registerTransformGroup', + prop: 'transformGroup', +}); + describe('register/transformGroup', async () => { const StyleDictionaryExtended = new StyleDictionary({}); @@ -102,10 +111,15 @@ describe('register/transformGroup', async () => { expect(StyleDictionaryExtended.transformGroup.foo[0]).to.equal('size/px'); }); - it('should properly pass the registered format to instances', async () => { - const SDE2 = await StyleDictionaryExtended.extend({}); - expect(Array.isArray(SDE2.transformGroup.foo)).to.be.true; - expect(typeof SDE2.transformGroup.foo[0]).to.equal('string'); - expect(SDE2.transformGroup.foo[0]).to.equal('size/px'); + it('should properly pass the registered transformGroup to instances when extending', async () => { + const StyleDictionaryBase = new StyleDictionary({}); + StyleDictionaryBase.registerTransformGroup({ + name: 'bar', + transforms: ['size/px'], + }); + const SDE2 = await StyleDictionaryBase.extend({}); + expect(Array.isArray(SDE2.transformGroup.bar)).to.be.true; + expect(typeof SDE2.transformGroup.bar[0]).to.equal('string'); + expect(SDE2.transformGroup.bar[0]).to.equal('size/px'); }); }); diff --git a/__tests__/utils/reference/getReferences.test.js b/__tests__/utils/reference/getReferences.test.js index 78adcb3da..96c63cd88 100644 --- a/__tests__/utils/reference/getReferences.test.js +++ b/__tests__/utils/reference/getReferences.test.js @@ -13,7 +13,6 @@ import { expect } from 'chai'; import getReferences from '../../../lib/utils/references/getReferences.js'; -import createDictionary from '../../../lib/utils/createDictionary.js'; const tokens = { color: { @@ -47,34 +46,30 @@ const tokens = { }, }; -const dictionary = createDictionary({ tokens }); - describe('utils', () => { describe('reference', () => { describe('getReferences()', () => { it(`should return an empty array if the value has no references`, () => { - expect(getReferences(dictionary, tokens.color.red.value)).to.eql([]); + expect(getReferences(tokens.color.red.value, tokens)).to.eql([]); }); it(`should work with a single reference`, () => { - expect(getReferences(dictionary, tokens.color.danger.value)).to.eql([{ value: '#f00' }]); + expect(getReferences(tokens.color.danger.value, tokens)).to.eql([{ value: '#f00' }]); }); it(`should work with object values`, () => { - expect(getReferences(dictionary, tokens.border.primary.value)).to.eql([ + expect(getReferences(tokens.border.primary.value, tokens)).to.eql([ { value: '#f00' }, { value: '2px' }, ]); }); it(`should work with objects that have numbers`, () => { - expect(getReferences(dictionary, tokens.border.secondary.value)).to.eql([ - { value: '#f00' }, - ]); + expect(getReferences(tokens.border.secondary.value, tokens)).to.eql([{ value: '#f00' }]); }); it(`should work with interpolated values`, () => { - expect(getReferences(dictionary, tokens.border.tertiary.value)).to.eql([ + expect(getReferences(tokens.border.tertiary.value, tokens)).to.eql([ { value: '2px' }, { value: '#f00' }, ]); diff --git a/__tests__/utils/reference/usesReference.test.js b/__tests__/utils/reference/usesReferences.test.js similarity index 68% rename from __tests__/utils/reference/usesReference.test.js rename to __tests__/utils/reference/usesReferences.test.js index 3a132adc4..da1ebbae7 100644 --- a/__tests__/utils/reference/usesReference.test.js +++ b/__tests__/utils/reference/usesReferences.test.js @@ -11,43 +11,43 @@ * and limitations under the License. */ import { expect } from 'chai'; -import usesReference from '../../../lib/utils/references/usesReference.js'; +import usesReferences from '../../../lib/utils/references/usesReferences.js'; -describe('usesReference()', () => { +describe('usesReferences()', () => { it(`returns false for non-strings`, () => { - expect(usesReference(42)).to.be.false; + expect(usesReferences(42)).to.be.false; }); it(`returns false if value uses no reference`, () => { - expect(usesReference('foo.bar')).to.be.false; + expect(usesReferences('foo.bar')).to.be.false; }); it(`returns true if value is a reference`, () => { - expect(usesReference('{foo.bar}')).to.be.true; + expect(usesReferences('{foo.bar}')).to.be.true; }); it(`should return true if value uses a reference`, () => { - expect(usesReference('baz {foo.bar}')).to.be.true; + expect(usesReferences('baz {foo.bar}')).to.be.true; }); it(`returns true if an object uses a reference`, () => { - expect(usesReference({ foo: '{bar}' })).to.be.true; + expect(usesReferences({ foo: '{bar}' })).to.be.true; }); it(`returns false if an object doesn't have a reference`, () => { - expect(usesReference({ foo: 'bar' })).to.be.false; + expect(usesReferences({ foo: 'bar' })).to.be.false; }); it(`returns true if a nested object has a reference`, () => { - expect(usesReference({ foo: { bar: '{bar}' } })).to.be.true; + expect(usesReferences({ foo: { bar: '{bar}' } })).to.be.true; }); it(`returns true if an array uses a reference`, () => { - expect(usesReference(['foo', '{bar}'])).to.be.true; + expect(usesReferences(['foo', '{bar}'])).to.be.true; }); it(`returns false if an array doesn't use a reference`, () => { - expect(usesReference(['foo', 'bar'])).to.be.false; + expect(usesReferences(['foo', 'bar'])).to.be.false; }); describe(`with custom options`, () => { @@ -58,7 +58,7 @@ describe('usesReference()', () => { separator: '|', }; - expect(usesReference('(foo|bar)', customOpts)).to.be.true; + expect(usesReferences('(foo|bar)', customOpts)).to.be.true; }); }); }); diff --git a/examples/advanced/variables-in-outputs/README.md b/examples/advanced/variables-in-outputs/README.md index 8c359bf07..a31f248b9 100644 --- a/examples/advanced/variables-in-outputs/README.md +++ b/examples/advanced/variables-in-outputs/README.md @@ -14,7 +14,7 @@ At this point, you can run `npm run build`. This command will generate the outpu #### How does it work -The "build" command uses the `sd.config.js` file as the Style Dictionary configuration. It is configured to use JSON files in the `tokens/` directory as the source files. It adds a custom format directly in the configuration (as opposed to using the `.registerFormat()` method) that uses 2 new methods added as exposed utilities: `usesReference()` and `getReferences()`. Also, it uses a new configuration on some formats: `outputReferences: true` to include variable references in the output. +The "build" command uses the `sd.config.js` file as the Style Dictionary configuration. It is configured to use JSON files in the `tokens/` directory as the source files. It adds a custom format directly in the configuration (as opposed to using the `.registerFormat()` method) that uses 2 new methods added as exposed utilities: `usesReferences()` and `getReferences()`. Also, it uses a new configuration on some formats: `outputReferences: true` to include variable references in the output. #### What to look at @@ -23,19 +23,19 @@ The `sd.config.js` file has everything you need to see. The tokens included in t Here is an example that shows how to get an alias's name within a custom format: ```javascript -import { usesReference, getReferences } from 'style-dictionary/utils'; +import { usesReferences, getReferences } from 'style-dictionary/utils'; //... function({ dictionary }) { return dictionary.allTokens.map(token => { let value = JSON.stringify(token.value); - // the `dictionary` object now has `usesReference()` and - // `getReferences()` methods. `usesReference()` will return true if + // the `dictionary` object now has `usesReferences()` and + // `getReferences()` methods. `usesReferences()` will return true if // the value has a reference in it. `getReferences()` will return // an array of references to the whole tokens so that you can access their // names or any other attributes. - if (usesReference(token.original.value)) { - const refs = getReferences(dictionary, token.original.value); + if (usesReferences(token.original.value)) { + const refs = getReferences(token.original.value, dictionary); refs.forEach(ref => { value = value.replace(ref.value, function() { return `${ref.name}`; diff --git a/fs.js b/fs.js deleted file mode 100644 index c9b1693ea..000000000 --- a/fs.js +++ /dev/null @@ -1,7 +0,0 @@ -// Allow to be overridden by setter, set default to memfs for browser env, node:fs for node env -export let fs = (await import('@bundled-es-modules/memfs')).default; - -// since ES modules exports are read-only, use a setter -export const setFs = (_fs) => { - fs = _fs; -}; diff --git a/lib/Register.js b/lib/Register.js new file mode 100644 index 000000000..3fa3204e9 --- /dev/null +++ b/lib/Register.js @@ -0,0 +1,417 @@ +import transform from './common/transforms.js'; +import transformGroup from './common/transformGroups.js'; +import format from './common/formats.js'; +import action from './common/actions.js'; +import filter from './common/filters.js'; + +/** + * @typedef {import('../types/File.d.ts').FileHeader} FileHeader + * @typedef {import('../types/Parser.d.ts').Parser} Parser + * @typedef {import('../types/Preprocessor.d.ts').Preprocessor} Preprocessor + * @typedef {import('../types/Preprocessor.d.ts').preprocessor} preprocessor + * @typedef {import('../types/Transform.d.ts').Transform} Transform + * @typedef {import('../types/Filter.d.ts').Filter} Filter + * @typedef {import('../types/Filter.d.ts').Matcher} Matcher + * @typedef {import('../types/Format.d.ts').Format} Format + * @typedef {import('../types/Format.d.ts').Formatter} Formatter + * @typedef {import('../types/Action.d.ts').Action} Action + */ + +export class Register { + /** + * Below is a ton of boilerplate. Explanation: + * + * You can register things on the StyleDictionary class level e.g. StyleDictionary.registerFormat() + * You can also register these things on StyleDictionary instance (through config) or on StyleDictionary instance's platform property. + * + * Therefore, we have to make use of static props vs instance props and use getters and setters to merge these together. + */ + static transform = transform; + static transformGroup = transformGroup; + static format = format; + static action = action; + static filter = filter; + /** @type {Record} */ + static fileHeader = {}; + /** @type {Parser[]} */ + static parsers = []; // we need to initialise the array, since we don't have built-in parsers + /** @type {Record} */ + static preprocessors = {}; + + /** + * @param {Transform} cfg + */ + static registerTransform(cfg) { + // this = class + this.__registerTransform(cfg, this); + } + + /** + * @param {Transform} cfg + */ + registerTransform(cfg) { + // this = instance + /** @type {typeof Register} */ (this.constructor).__registerTransform(cfg, this); + } + + /** + * @param {Transform} transform + * @param {typeof Register | Register} target + */ + static __registerTransform(transform, target) { + const transformTypes = ['name', 'value', 'attribute']; + const { type, name, matcher, transitive, transformer } = transform; + if (typeof type !== 'string') throw new Error('type must be a string'); + if (transformTypes.indexOf(type) < 0) + throw new Error(type + ' type is not one of: ' + transformTypes.join(', ')); + if (typeof name !== 'string') throw new Error('name must be a string'); + if (matcher && typeof matcher !== 'function') throw new Error('matcher must be a function'); + if (typeof transformer !== 'function') throw new Error('transformer must be a function'); + + // make sure to trigger the setter + target.transform = { + ...target.transform, + [name]: { + type, + matcher, + transitive: !!transitive, + transformer, + }, + }; + return this; + } + + get transform() { + const ctor = /** @type {typeof Register} */ (this.constructor); + return { ...ctor.transform, ...this._transform }; + } + + /** @param {Record>} v */ + set transform(v) { + this._transform = v; + } + + /** + * @param {{ name: string; transforms: string[]; }} cfg + */ + static registerTransformGroup(cfg) { + // this = class + this.__registerTransformGroup(cfg, this); + } + + /** + * @param {{ name: string; transforms: string[]; }} cfg + */ + registerTransformGroup(cfg) { + // this = instance + /** @type {typeof Register} */ (this.constructor).__registerTransformGroup(cfg, this); + } + + /** + * @param {{ name: string; transforms: string[]; }} transformGroup + * @param {typeof Register | Register} target + */ + static __registerTransformGroup(transformGroup, target) { + const { name, transforms } = transformGroup; + if (typeof name !== 'string') throw new Error('transform name must be a string'); + if (!Array.isArray(transforms)) + throw new Error('transforms must be an array of registered value transforms'); + + transforms.forEach((t) => { + if (!(t in target.transform)) + throw new Error('transforms must be an array of registered value transforms'); + }); + // make sure to trigger the setter + target.transformGroup = { + ...target.transformGroup, + [name]: transforms, + }; + return target; + } + + get transformGroup() { + const ctor = /** @type {typeof Register} */ (this.constructor); + return { ...ctor.transformGroup, ...this._transformGroup }; + } + + /** @param {Record} v */ + set transformGroup(v) { + this._transformGroup = v; + } + + /** + * @param {Format} cfg + */ + static registerFormat(cfg) { + // this = class + this.__registerFormat(cfg, this); + } + + /** + * @param {Format} cfg + */ + registerFormat(cfg) { + // this = instance + /** @type {typeof Register} */ (this.constructor).__registerFormat(cfg, this); + } + + /** + * @param {Format} format + * @param {typeof Register | Register} target + */ + static __registerFormat(format, target) { + const { name, formatter } = format; + if (typeof name !== 'string') + throw new Error("Can't register format; format.name must be a string"); + if (typeof formatter !== 'function') + throw new Error("Can't register format; format.formatter must be a function"); + // make sure to trigger the setter + target.format = { + ...target.format, + [name]: formatter, + }; + return target; + } + + get format() { + const ctor = /** @type {typeof Register} */ (this.constructor); + return { ...ctor.format, ...this._format }; + } + + /** @param {Record} v */ + set format(v) { + this._format = v; + } + + /** + * @param {Action} cfg + */ + static registerAction(cfg) { + // this = class + this.__registerAction(cfg, this); + } + + /** + * @param {Action} cfg + */ + registerAction(cfg) { + // this = instance + /** @type {typeof Register} */ (this.constructor).__registerAction(cfg, this); + } + + /** + * @param {Action} action + * @param {typeof Register | Register} target + */ + static __registerAction(action, target) { + const { name, do: _do, undo } = action; + if (typeof name !== 'string') throw new Error('name must be a string'); + if (typeof _do !== 'function') throw new Error('do must be a function'); + // make sure to trigger the setter + target.action = { + ...target.action, + [name]: { + do: _do, + undo, + }, + }; + return target; + } + + get action() { + const ctor = /** @type {typeof Register} */ (this.constructor); + return { ...ctor.action, ...this._action }; + } + + /** @param {Record>} v */ + set action(v) { + this._action = v; + } + + /** + * @param {Filter} cfg + */ + static registerFilter(cfg) { + // this = class + this.__registerFilter(cfg, this); + } + + /** + * @param {Filter} cfg + */ + registerFilter(cfg) { + // this = instance + /** @type {typeof Register} */ (this.constructor).__registerFilter(cfg, this); + } + + /** + * @param {Filter} filter + * @param {typeof Register | Register} target + */ + static __registerFilter(filter, target) { + const { name, matcher } = filter; + if (typeof name !== 'string') + throw new Error("Can't register filter; filter.name must be a string"); + if (typeof matcher !== 'function') + throw new Error("Can't register filter; filter.matcher must be a function"); + // make sure to trigger the setter + target.filter = { + ...target.filter, + [name]: matcher, + }; + return target; + } + + get filter() { + const ctor = /** @type {typeof Register} */ (this.constructor); + return { ...ctor.filter, ...this._filter }; + } + + /** @param {Record} v */ + set filter(v) { + this._filter = v; + } + + /** + * @param {Parser} cfg + */ + static registerParser(cfg) { + // this = class + this.__registerParser(cfg, this); + } + + /** + * @param {Parser} cfg + */ + registerParser(cfg) { + // this = instance + /** @type {typeof Register} */ (this.constructor).__registerParser(cfg, this); + } + + /** + * @param {import('../types/Parser.d.ts').Parser} parser + * @param {typeof Register | Register} target + */ + static __registerParser(parser, target) { + const { pattern, parse } = parser; + if (!(pattern instanceof RegExp)) + throw new Error(`Can't register parser; parser.pattern must be a regular expression`); + if (typeof parse !== 'function') + throw new Error("Can't register parser; parser.parse must be a function"); + // make sure to trigger the setter + target.parsers = [...target.parsers, parser]; + return target; + } + + get parsers() { + const ctor = /** @type {typeof Register} */ (this.constructor); + return [...ctor.parsers, ...(this._parsers ?? [])]; + } + + /** @param {Parser[]} v */ + set parsers(v) { + this._parsers = v; + } + + /** + * @param {Preprocessor} cfg + */ + static registerPreprocessor(cfg) { + // this = class + this.__registerPreprocessor(cfg, this); + } + + /** + * @param {Preprocessor} cfg + */ + registerPreprocessor(cfg) { + // this = instance + /** @type {typeof Register} */ (this.constructor).__registerPreprocessor(cfg, this); + } + + /** + * @param {Preprocessor} cfg + * @param {typeof Register | Register} target + */ + static __registerPreprocessor(cfg, target) { + const { name, preprocessor } = cfg; + const errorPrefix = 'Cannot register preprocessor;'; + if (typeof name !== 'string') + throw new Error(`${errorPrefix} Preprocessor.name must be a string`); + if (!(preprocessor instanceof Function)) { + throw new Error(`${errorPrefix} Preprocessor.preprocessor must be a function`); + } + // make sure to trigger the setter + target.preprocessors = { + ...target.preprocessors, + [name]: preprocessor, + }; + return target; + } + + get preprocessors() { + const ctor = /** @type {typeof Register} */ (this.constructor); + return { ...ctor.preprocessors, ...this._preprocessors }; + } + + /** @param {Record} v */ + set preprocessors(v) { + this._preprocessors = v; + } + + /** + * @param {{name: string; fileHeader: FileHeader;}} cfg + */ + static registerFileHeader(cfg) { + // this = class + this.__registerFileHeader(cfg, this); + } + + /** + * @param {{name: string; fileHeader: FileHeader;}} cfg + */ + registerFileHeader(cfg) { + // this = instance + /** @type {typeof Register} */ (this.constructor).__registerFileHeader(cfg, this); + } + + /** + * @param {{name: string; fileHeader: FileHeader;}} cfg + * @param {typeof Register | Register} target + */ + static __registerFileHeader(cfg, target) { + const { name, fileHeader } = cfg; + if (typeof name !== 'string') + throw new Error("Can't register file header; options.name must be a string"); + if (typeof fileHeader !== 'function') + throw new Error("Can't register file header; options.fileHeader must be a function"); + + // make sure to trigger the setter + target.fileHeader = { + ...target.fileHeader, + [name]: fileHeader, + }; + return target; + } + + get fileHeader() { + const ctor = /** @type {typeof Register} */ (this.constructor); + return { ...ctor.fileHeader, ...this._fileHeader }; + } + + /** @param {Record} v */ + set fileHeader(v) { + this._fileHeader = v; + } + + constructor() { + this.transform = {}; + this.transformGroup = {}; + this.format = {}; + this.action = {}; + this.filter = {}; + this.fileHeader = {}; + this.parsers = []; // we need to initialise the array, since we don't have built-in parsers + this.preprocessors = {}; + } +} diff --git a/lib/StyleDictionary.js b/lib/StyleDictionary.js index ed0bb2303..e04cf0bbb 100644 --- a/lib/StyleDictionary.js +++ b/lib/StyleDictionary.js @@ -15,22 +15,9 @@ import JSON5 from 'json5'; import path from '@bundled-es-modules/path-browserify'; import { fs } from 'style-dictionary/fs'; import { deepmerge } from './utils/deepmerge.js'; +import { Register } from './Register.js'; -import transform from './common/transforms.js'; -import transformGroup from './common/transformGroups.js'; -import format from './common/formats.js'; -import action from './common/actions.js'; import * as formatHelpers from './common/formatHelpers/index.js'; -import filter from './common/filters.js'; - -import registerTransform from './register/transform.js'; -import registerTransformGroup from './register/transformGroup.js'; -import registerFormat from './register/format.js'; -import registerAction from './register/action.js'; -import registerFilter from './register/filter.js'; -import registerParser from './register/parser.js'; -import registerPreprocessor from './register/preprocessor.js'; -import registerFileHeader from './register/fileHeader.js'; import combineJSON from './utils/combineJSON.js'; import deepExtend from './utils/deepExtend.js'; @@ -48,11 +35,19 @@ import cleanFiles from './cleanFiles.js'; import cleanDirs from './cleanDirs.js'; import cleanActions from './cleanActions.js'; +/** + * @typedef {import('../types/Config.d.ts').Config} Config + * @typedef {import('../types/Config.d.ts').PlatformConfig} PlatformConfig + * @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 + */ + const PROPERTY_VALUE_COLLISIONS = GroupMessages.GROUP.PropertyValueCollisions; const PROPERTY_REFERENCE_WARNINGS = GroupMessages.GROUP.PropertyReferenceWarnings; -// TODO: add Type interface for this class - /** * Style Dictionary module * @@ -65,98 +60,54 @@ const PROPERTY_REFERENCE_WARNINGS = GroupMessages.GROUP.PropertyReferenceWarning * ``` */ -export default class StyleDictionary { +export default class StyleDictionary extends Register { // Placeholder is transformed on prepublish -> see scripts/inject-version.js // Another option might be import pkg from './package.json' with { "type": "json" } which would work in both browser and node, but support is not there yet. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#browser_compatibility static VERSION = ''; static formatHelpers = formatHelpers; - static transform = transform; - static transformGroup = transformGroup; - static format = format; - static action = action; - static filter = filter; - static fileHeader = {}; - static parsers = []; // we need to initialise the array, since we don't have built-in parsers - static preprocessors = {}; - - static registerTransform(...args) { - return registerTransform.call(this, ...args); - } - - static registerTransformGroup(...args) { - return registerTransformGroup.call(this, ...args); - } - static registerFormat(...args) { - return registerFormat.call(this, ...args); + /** @returns {Config} */ + get options() { + // merge locally registered things with options + // so that when we extend, we include registered things + const opts = deepmerge( + { + transform: this.transform, + transformGroup: this.transformGroup, + format: this.format, + action: this.action, + filter: this.filter, + fileHeader: this.fileHeader, + parsers: this.parsers, + preprocessors: this.preprocessors, + }, + this._options ?? {}, + ); + return opts; } - - static registerAction(...args) { - return registerAction.call(this, ...args); - } - - static registerFilter(...args) { - return registerFilter.call(this, ...args); - } - - static registerParser(...args) { - return registerParser.call(this, ...args); - } - - static registerPreprocessor(...args) { - return registerPreprocessor.call(this, ...args); - } - - static registerFileHeader(...args) { - return registerFileHeader.call(this, ...args); + /** @param {Config} v */ + set options(v) { + this._options = v; } - /** - * - * @param {Object|string} [config] - * @param {{ init: boolean }} [options] - */ constructor(config = {}, { init = true } = {}) { - // dynamically add these instance methods to delegate to class methods for register - [ - 'transform', - 'transformGroup', - 'format', - 'action', - 'filter', - 'fileHeader', - 'parser', - 'preprocessor', - ].forEach((prop) => { - Object.defineProperty(this, `register${prop.charAt(0).toUpperCase() + prop.slice(1)}`, { - value: (...args) => { - this.constructor[`register${prop.charAt(0).toUpperCase() + prop.slice(1)}`](...args); - }, - }); - - // Dynamically add getter/setter pairs so we can act as a gateway, merging - // the SD class options with SD instance options - const _prop = ['parser', 'preprocessor'].includes(prop) ? `${prop}s` : prop; - Object.defineProperty(this, _prop, { - get: function () { - if (prop === 'parser') { - return [...this.constructor[`${_prop}`], ...this[`_${_prop}`]]; - } - return { ...this.constructor[`${_prop}`], ...this[`_${_prop}`] }; - }, - set: function (v) { - this[`_${_prop}`] = v; - }, - }); - }); - + super(); + /** @type {'warn'|'error'} */ + this.log = 'warn'; this.config = config; this.options = {}; + /** @type {Tokens|TransformedTokens} */ this.tokens = {}; - this.allTokens = {}; - this.parsers = []; - this.preprocessors = {}; + /** @type {Token[]|TransformedToken[]} */ + this.allTokens = []; + /** + * Gets set after transform because filter happens on format level, + * so we know they are transformed by then. + * @type {TransformedTokens} + */ + this.unfilteredTokens = {}; + this.hasInitialized = new Promise((resolve) => { this.hasInitializedResolve = resolve; }); @@ -166,7 +117,7 @@ export default class StyleDictionary { // you can call constructor with { init: false } // and call SDInstance.extend() manually (and catch the error). if (init) { - this.init(config); + this.init(); } } @@ -174,6 +125,11 @@ export default class StyleDictionary { return this.extend(undefined, true); } + /** + * @param {Config} [config] + * @param {boolean} [mutateOriginal] + * @returns {Promise} + */ async extend(config = this.config, mutateOriginal = false) { // by default, if extend is called it means extending the current instance // with a new instance without mutating the original @@ -182,9 +138,13 @@ export default class StyleDictionary { return newSD.init(); } + /** @type {Config} */ let options; + /** @type {Tokens} */ let inlineTokens = {}; + /** @type {Tokens} */ let includeTokens = {}; + /** @type {Tokens} */ let sourceTokens = {}; // Overloaded method, can accept a string as a path that points to a JS or // JSON file or a plain object. Potentially refactor. @@ -192,7 +152,7 @@ export default class StyleDictionary { // get ext name without leading . const ext = path.extname(config).replace(/^\./, ''); if (['json', 'json5', 'jsonc'].includes(ext)) { - options = JSON5.parse(fs.readFileSync(config, 'utf-8')); + options = JSON5.parse(/** @type {string} */ (fs.readFileSync(config, 'utf-8'))); } else { // import path in Node has to be relative to cwd, in browser to root const fileToImport = path.resolve(typeof window === 'object' ? '' : process.cwd(), config); @@ -204,7 +164,9 @@ export default class StyleDictionary { // SD Config options should be passed to class instance as well Object.entries(options).forEach(([key, val]) => { - this[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; @@ -220,8 +182,7 @@ export default class StyleDictionary { if (this.options.include) { if (!Array.isArray(this.options.include)) throw new Error('include must be an array'); - includeTokens = await combineJSON(this.options.include, true, null, false, this.parsers); - this.include = null; // We don't want to carry over include references + includeTokens = await combineJSON(this.options.include, true, undefined, false, this.parsers); } // Update tokens with current package's source @@ -231,6 +192,7 @@ export default class StyleDictionary { sourceTokens = await combineJSON( this.options.source, true, + /** @param {Token} prop */ function Collision(prop) { GroupMessages.add( PROPERTY_VALUE_COLLISIONS, @@ -252,23 +214,25 @@ export default class StyleDictionary { console.log(warn); } } - - this.source = null; // We don't want to carry over the source references } // Merge inline, include, and source tokens const unprocessedTokens = deepExtend([{}, inlineTokens, includeTokens, sourceTokens]); this.tokens = await preprocess(unprocessedTokens, this.preprocessors); - this.hasInitializedResolve(); + this.hasInitializedResolve(null); // For chaining return this; } + /** + * @param {string} platform + * @returns {Promise} + */ async exportPlatform(platform) { await this.hasInitialized; - if (!platform || !this.options.platforms[platform]) { + if (!platform || !this.options?.platforms?.[platform]) { throw new Error('Please supply a valid platform'); } @@ -277,10 +241,16 @@ export default class StyleDictionary { let exportableResult = this.tokens; - // list keeping paths of props with applied value transformations + /** + * @type {string[]} + * list keeping paths of props with applied value transformations + */ const transformedPropRefs = []; - // list keeping paths of props that had references in it, and therefore - // could not (yet) have transformed + /** + * @type {string[]} + * list keeping paths of props that had references in it, and therefore + * could not (yet) have transformed + */ const deferredPropValueTransforms = []; const transformationContext = { @@ -289,9 +259,9 @@ export default class StyleDictionary { }; let deferredPropCount = 0; - let finished; + let finished = false; - while (typeof finished === 'undefined') { + while (!finished) { // We keep up transforming and resolving until all props are resolved // and every defined transformation was executed. Remember: transformations // can only be executed, if the value to be transformed, has no references @@ -366,16 +336,18 @@ export default class StyleDictionary { return exportableResult; } - async buildPlatform(platform) { + /** + * @param {string} platform + * @returns + */ + async getPlatform(platform) { await this.hasInitialized; - console.log('\n' + platform); - if (!this.options || !this.options?.platforms[platform]) { + if (!this.options?.platforms?.[platform]) { throw new Error(`Platform "${platform}" does not exist`); } - let tokens; // We don't want to mutate the original object const platformConfig = transformConfig(this.options.platforms[platform], this, platform); @@ -384,63 +356,55 @@ export default class StyleDictionary { // values like "1px solid {color.border.base}" we want to // transform the original value (color.border.base) before // replacing that value in the string. - tokens = await this.exportPlatform(platform); + const tokens = await this.exportPlatform(platform); + const dict = createDictionary(tokens); + this.allTokens = dict.allTokens; // This is the dictionary object we pass to the file // building and action methods. - const dictionary = createDictionary({ tokens }); + return { dictionary: dict, platformConfig }; + } + /** + * @param {string} platform + * @returns + */ + async buildPlatform(platform) { + const { dictionary, platformConfig } = await this.getPlatform(platform); buildFiles(dictionary, platformConfig); performActions(dictionary, platformConfig); - // For chaining return this; } async buildAllPlatforms() { await this.hasInitialized; - await Promise.all(Object.keys(this.options.platforms).map((key) => this.buildPlatform(key))); - + if (this.options?.platforms) { + await Promise.all(Object.keys(this.options.platforms).map((key) => this.buildPlatform(key))); + } // For chaining return this; } + /** + * @param {string} platform + * @returns + */ async cleanPlatform(platform) { - await this.hasInitialized; - - console.log('\n' + platform); - - if (!this.options || !this.options?.platforms[platform]) { - throw new Error('Platform ' + platform + " doesn't exist"); - } - - let tokens; - // We don't want to mutate the original object - const platformConfig = transformConfig(this.options.platforms[platform], this, platform); - - // We need to transform the object before we resolve the - // variable names because if a value contains concatenated - // values like "1px solid {color.border.base}" we want to - // transform the original value (color.border.base) before - // replacing that value in the string. - tokens = await this.exportPlatform(platform); - - // This is the dictionary object we pass to the file - // cleaning and action methods. - const dictionary = createDictionary({ tokens }); - + const { dictionary, platformConfig } = await this.getPlatform(platform); // We clean files first, then actions, ...and then directories? - cleanFiles(dictionary, platformConfig); + cleanFiles(platformConfig); cleanActions(dictionary, platformConfig); - cleanDirs(dictionary, platformConfig); - + cleanDirs(platformConfig); // For chaining return this; } async cleanAllPlatforms() { await this.hasInitialized; - await Promise.all(Object.keys(this.options.platforms).map((key) => this.cleanPlatform(key))); + if (this.options?.platforms) { + await Promise.all(Object.keys(this.options.platforms).map((key) => this.cleanPlatform(key))); + } // For chaining return this; } diff --git a/lib/buildFile.js b/lib/buildFile.js index f14ce6136..bad38e2a1 100644 --- a/lib/buildFile.js +++ b/lib/buildFile.js @@ -19,16 +19,22 @@ import GroupMessages from './utils/groupMessages.js'; import createFormatArgs from './utils/createFormatArgs.js'; +/** + * @typedef {import('../types/DesignToken.d.ts').Dictionary} Dictionary + * @typedef {import('../types/DesignToken.d.ts').TransformedToken} TransformedToken + * @typedef {import('../types/Config.d.ts').PlatformConfig} PlatformConfig + * @typedef {import('../types/File.d.ts').File} File + */ + /** * Takes the style token object and a format and returns a * string that can be written to a file. * @memberOf StyleDictionary - * @param {Object} file - * @param {Object} platform - * @param {Object} dictionary - * @returns {null} + * @param {File} file + * @param {PlatformConfig} platform + * @param {Dictionary} dictionary */ -export default function buildFile(file = {}, platform = {}, dictionary = {}) { +export default function buildFile(file, platform = {}, dictionary) { const { destination, filter } = file || {}; let { format } = file || {}; @@ -55,7 +61,7 @@ export default function buildFile(file = {}, platform = {}, dictionary = {}) { tokens: filteredTokens.tokens, allTokens: filteredTokens.allTokens, // keep the unfiltered tokens object for reference resolution - _tokens: dictionary.tokens, + unfilteredTokens: dictionary.tokens, }); // if tokens object is empty, return without creating a file @@ -69,7 +75,10 @@ export default function buildFile(file = {}, platform = {}, dictionary = {}) { return null; } - // Check for token name Collisions + /** + * Check for token name Collisions + * @type {Record} + */ const nameCollisionObj = {}; filteredTokens.allTokens && filteredTokens.allTokens.forEach((tokenData) => { @@ -110,8 +119,6 @@ export default function buildFile(file = {}, platform = {}, dictionary = {}) { platform, file, }), - platform, - file, ), ); diff --git a/lib/buildFiles.js b/lib/buildFiles.js index 2bf5029b0..99366ea96 100644 --- a/lib/buildFiles.js +++ b/lib/buildFiles.js @@ -13,15 +13,19 @@ import buildFile from './buildFile.js'; +/** + * @typedef {import('../types/DesignToken.d.ts').Dictionary} Dictionary + * @typedef {import('../types/Config.d.ts').PlatformConfig} Config + */ + /** * Takes a platform config object and a dictionary * object and builds all the files. Dictionary object * should have been transformed and resolved before this * point. * @memberOf StyleDictionary - * @param {Object} dictionary - * @param {Object} platform - * @returns {null} + * @param {Dictionary} dictionary + * @param {Config} platform */ export default function buildFiles(dictionary, platform) { if ( @@ -32,7 +36,7 @@ export default function buildFiles(dictionary, platform) { throw new Error('Build path must end in a trailing slash or you will get weird file names.'); } - platform.files.forEach(function (file) { + platform.files?.forEach(function (file) { if (file.format) { buildFile(file, platform, dictionary); } else { diff --git a/lib/cleanActions.js b/lib/cleanActions.js index 81ece9d1f..8865ec43a 100644 --- a/lib/cleanActions.js +++ b/lib/cleanActions.js @@ -11,6 +11,11 @@ * and limitations under the License. */ +/** + * @typedef {import('../types/DesignToken.d.ts').Dictionary} Dictionary + * @typedef {import('../types/Config.d.ts').PlatformConfig} Config + */ + /** * Performs the undo of any actions defined in a platform. Pretty * simple really. Actions should be an array of functions, @@ -18,14 +23,14 @@ * @static * @private * @memberof module:style-dictionary - * @param {Object} dictionary - * @param {Object} platform - * @returns {null} + * @param {Dictionary} dictionary + * @param {Config} platform */ export default function cleanActions(dictionary, platform) { if (platform.actions) { platform.actions.forEach(function (action) { - if (typeof action.undo === 'function') { + // ensure we've augmented action with the do/undo functions and that undo exists + if (typeof action !== 'string' && typeof action.undo === 'function') { action.undo(dictionary, platform); } }); diff --git a/lib/cleanDir.js b/lib/cleanDir.js index 0b609f81a..2b31a692d 100644 --- a/lib/cleanDir.js +++ b/lib/cleanDir.js @@ -15,16 +15,19 @@ import chalk from 'chalk'; import path from '@bundled-es-modules/path-browserify'; import { fs } from 'style-dictionary/fs'; +/** + * @typedef {import('../types/Config.d.ts').PlatformConfig} Config + * @typedef {import('../types/File.d.ts').File} File + */ + /** * Takes the style property object and a format and returns a * string that can be written to a file. * @memberOf StyleDictionary - * @param {Object} file - * @param {Object} platform - * @param {Object} dictionary (unused) - * @returns {null} + * @param {File} file + * @param {Config} [platform] */ -export default function cleanDir(file = {}, platform = {}) { +export default function cleanDir(file, platform = {}) { let { destination } = file; if (typeof destination !== 'string') throw new Error('Please enter a valid destination'); @@ -38,7 +41,7 @@ export default function cleanDir(file = {}, platform = {}) { while (dirname) { if (fs.existsSync(dirname)) { - if (fs.readdirSync(dirname).length === 0) { + if (fs.readdirSync(dirname, 'buffer').length === 0) { console.log(chalk.bold.red('-') + ' ' + dirname); fs.rmdirSync(dirname); } else { diff --git a/lib/cleanDirs.js b/lib/cleanDirs.js index c18b563e3..623a76994 100644 --- a/lib/cleanDirs.js +++ b/lib/cleanDirs.js @@ -13,25 +13,27 @@ import cleanDir from './cleanDir.js'; +/** + * @typedef {import('../types/Config.d.ts').PlatformConfig} Config + */ + /** * Takes a platform config object and a tokens * object and cleans all the files. Tokens object * should have been transformed and resolved before this * point. * @memberOf StyleDictionary - * @param {Object} dictionary - * @param {Object} platform - * @returns {null} + * @param {Config} platform */ -export default function cleanDirs(dictionary, platform) { +export default function cleanDirs(platform) { if (platform.buildPath && platform.buildPath.slice(-1) !== '/') { throw new Error('Build path must end in a trailing slash or you will get weird file names.'); } // while neither the format or dictionary are used by clean file I'm passing them in for symmetry to buildFile - platform.files.forEach(function (file) { + platform.files?.forEach((file) => { if (file.format) { - cleanDir(file, platform, dictionary); + cleanDir(file, platform); } else { throw new Error('Please supply a format'); } diff --git a/lib/cleanFile.js b/lib/cleanFile.js index 81c85ce9d..a1d0c1e43 100644 --- a/lib/cleanFile.js +++ b/lib/cleanFile.js @@ -14,19 +14,20 @@ import chalk from 'chalk'; import { fs } from 'style-dictionary/fs'; +/** + * @typedef {import('../types/File.d.ts').File} File + * @typedef {import('../types/Config.d.ts').PlatformConfig} PlatformConfig + */ + /** * Takes the style property object and a format and returns a * string that can be written to a file. * @memberOf StyleDictionary - * @param {String} destination - * @param {Function} format (unused) - * @param {Object} platform - * @param {Object} dictionary (unused) - * @param {Function} filter (unused) - * @returns {null} + * @param {File} file + * @param {PlatformConfig} [platform] */ -export default function cleanFile(file = {}, platform = {}) { - var { destination } = file; +export default function cleanFile(file, platform = {}) { + let { destination } = file; if (typeof destination !== 'string') throw new Error('Please enter a valid destination'); diff --git a/lib/cleanFiles.js b/lib/cleanFiles.js index 4d4a8464e..d726242a6 100644 --- a/lib/cleanFiles.js +++ b/lib/cleanFiles.js @@ -13,25 +13,27 @@ import cleanFile from './cleanFile.js'; +/** + * @typedef {import('../types/Config.d.ts').PlatformConfig} PlatformConfig + */ + /** * Takes a platform config object and a dictionary * object and cleans all the files. Dictionary object * should have been transformed and resolved before this * point. * @memberOf StyleDictionary - * @param {Object} dictionary - * @param {Object} platform - * @returns {null} + * @param {PlatformConfig} platform */ -export default function cleanFiles(dictionary, platform) { +export default function cleanFiles(platform) { if (platform.buildPath && platform.buildPath.slice(-1) !== '/') { throw new Error('Build path must end in a trailing slash or you will get weird file names.'); } // while neither the format or dictionary are used by clean file I'm passing them in for symmetry to buildFile - platform.files.forEach(function (file) { + platform.files?.forEach((file) => { if (file.format) { - cleanFile(file, platform, dictionary); + cleanFile(file, platform); } else { throw new Error('Please supply a template or formatter'); } diff --git a/lib/common/actions.js b/lib/common/actions.js index 6dfdf82b3..5b59f3e5c 100644 --- a/lib/common/actions.js +++ b/lib/common/actions.js @@ -13,21 +13,31 @@ import { fs } from 'style-dictionary/fs'; +/** + * @typedef {import('../../types/DesignToken.d.ts').Dictionary} Dictionary + * @typedef {import('../../types/Action.d.ts').Action} Action + * @typedef {import('../../types/Config.js').PlatformConfig} Config + * @typedef {import('../../types/DesignToken.d.ts').TransformedToken} Token + */ + /** * @namespace Actions + * @type {Record>} */ export default { /** * Action to copy images into appropriate android directories. * - * @type {Action} * @memberof Actions */ 'android/copyImages': { do: function (dictionary, config) { const imagesDir = `${config.buildPath}android/main/res/drawable-`; - dictionary.allTokens.forEach(function (token) { - if (token.attributes.category === 'asset' && token.attributes.type === 'image') { + /** + * @param {Token} token + */ + dictionary.allTokens.forEach((token) => { + if (token.attributes?.category === 'asset' && token.attributes.type === 'image') { const name = token.path.slice(2, 4).join('_'); const dir = `${imagesDir}${token.attributes.state}`; const path = `${dir}/${name}.png`; @@ -38,8 +48,11 @@ export default { }, undo: function (dictionary, config) { const imagesDir = `${config.buildPath}android/main/res/drawable-`; - dictionary.allTokens.forEach(function (token) { - if (token.attributes.category === 'asset' && token.attributes.type === 'image') { + /** + * @param {Token} token + */ + dictionary.allTokens.forEach((token) => { + if (token.attributes?.category === 'asset' && token.attributes.type === 'image') { const name = token.path.slice(2, 4).join('_'); const dir = `${imagesDir}${token.attributes.state}`; const path = `${dir}/${name}.png`; @@ -52,7 +65,6 @@ export default { /** * Action that copies everything in the assets directory to a new assets directory in the build path of the platform. * - * @type {Action} * @memberof Actions */ copy_assets: { diff --git a/lib/common/filters.js b/lib/common/filters.js index 7ecba6182..fff8154c7 100644 --- a/lib/common/filters.js +++ b/lib/common/filters.js @@ -11,18 +11,19 @@ * and limitations under the License. */ +/** + * @typedef {import('../../types/Filter.d.ts').Matcher} FilterMatcher + */ + /** * @namespace Filters */ +/** @type {Record} */ export default { /** * Remove a token from the ditribution output if it contains a key `private` set to true - * * @memberof Filters - * - * @param {Object} token - * @returns {Boolean} */ removePrivate: function (token) { return token && token.private ? false : true; diff --git a/lib/common/formatHelpers/createPropertyFormatter.js b/lib/common/formatHelpers/createPropertyFormatter.js index 9bf5f9e29..00e348c1c 100644 --- a/lib/common/formatHelpers/createPropertyFormatter.js +++ b/lib/common/formatHelpers/createPropertyFormatter.js @@ -11,8 +11,16 @@ * and limitations under the License. */ -import { usesReference, getReferences } from 'style-dictionary/utils'; +import { usesReferences, getReferences } from '../../utils/index.js'; +/** + * @typedef {import('../../../types/DesignToken.d.ts').Dictionary} Dictionary + * @typedef {import('../../../types/File.d.ts').FormattingOptions} Formatting + */ + +/** + * @type {Formatting} + */ const defaultFormatting = { prefix: '', commentStyle: 'long', @@ -26,22 +34,23 @@ const defaultFormatting = { * Split a string comment by newlines and * convert to multi-line comment if necessary * @param {string} to_ret_prop - * @param {{comment: string, style: 'short' | 'long', position: 'above' | 'inline', indentation: string}} options + * @param {string} comment + * @param {Formatting} options * @returns {string} */ -function addComment(to_ret_prop, options) { - const { comment, style, indentation } = options; - let { position } = options; +function addComment(to_ret_prop, comment, options) { + const { commentStyle, indentation } = options; + let { commentPosition } = options; const commentsByNewLine = comment.split('\n'); if (commentsByNewLine.length > 1) { - position = 'above'; + commentPosition = 'above'; } let processedComment; - switch (style) { + switch (commentStyle) { case 'short': - if (position === 'inline') { + if (commentPosition === 'inline') { processedComment = `// ${comment}`; } else { processedComment = commentsByNewLine.reduce( @@ -60,12 +69,12 @@ function addComment(to_ret_prop, options) { ); processedComment += `${indentation} */`; } else { - processedComment = `${position === 'above' ? indentation : ''}/* ${comment} */`; + processedComment = `${commentPosition === 'above' ? indentation : ''}/* ${comment} */`; } break; } - if (position === 'above') { + if (commentPosition === 'above') { // put the comment above the prop if it's multi-line or if commentStyle ended with -above to_ret_prop = `${processedComment}\n${to_ret_prop}`; } else { @@ -82,14 +91,6 @@ function addComment(to_ret_prop, options) { * which uses: prefix, indentation, separator, suffix, and commentStyle. * @memberof module:formatHelpers * @name createPropertyFormatter - * @param {Object} options - * @param {Boolean} options.outputReferences - Whether or not to output references. You will want to pass this from the `options` object sent to the formatter function. - * @param {Boolean} options.outputReferenceFallbacks - Whether or not to output css variable fallback values when using output references. You will want to pass this from the `options` object sent to the formatter function. - * @param {Dictionary} options.dictionary - The dictionary object sent to the formatter function - * @param {String} options.format - Available formats are: 'css', 'sass', 'less', and 'stylus'. If you want to customize the format and can't use one of those predefined formats, use the `formatting` option - * @param {Object} options.formatting - Custom formatting properties that define parts of a declaration line in code. The configurable strings are: prefix, indentation, separator, suffix, and commentStyle. Those are used to generate a line like this: `${indentation}${prefix}${prop.name}${separator} ${prop.value}${suffix}` - * @param {Boolean} options.themeable [false] - Whether tokens should default to being themeable. - * @returns {Function} * @example * ```javascript * StyleDictionary.registerFormat({ @@ -105,6 +106,14 @@ function addComment(to_ret_prop, options) { * } * }); * ``` + * @param {Object} options + * @param {boolean} [options.outputReferences] - Whether or not to output references. You will want to pass this from the `options` object sent to the formatter function. + * @param {boolean} [options.outputReferenceFallbacks] - Whether or not to output css variable fallback values when using output references. You will want to pass this from the `options` object sent to the formatter function. + * @param {Dictionary} options.dictionary - The dictionary object sent to the formatter function + * @param {string} [options.format] - Available formats are: 'css', 'sass', 'less', and 'stylus'. If you want to customize the format and can't use one of those predefined formats, use the `formatting` option + * @param {Formatting} [options.formatting] - Custom formatting properties that define parts of a declaration line in code. The configurable strings are: prefix, indentation, separator, suffix, and commentStyle. Those are used to generate a line like this: `${indentation}${prefix}${prop.name}${separator} ${prop.value}${suffix}` + * @param {boolean} [options.themeable] [false] - Whether tokens should default to being themeable. + * @returns {(prop: import('../../../types/DesignToken.d.ts').TransformedToken) => string} */ export default function createPropertyFormatter({ outputReferences = false, @@ -114,6 +123,7 @@ export default function createPropertyFormatter({ formatting = {}, themeable = false, }) { + /** @type {Formatting} */ const formatDefaults = {}; switch (format) { case 'css': @@ -140,12 +150,13 @@ export default function createPropertyFormatter({ formatDefaults.separator = '='; break; } - let { prefix, commentStyle, commentPosition, indentation, separator, suffix } = { + const mergedOptions = { ...defaultFormatting, ...formatDefaults, ...formatting, }; - + let { prefix, commentStyle, indentation, separator, suffix } = mergedOptions; + const { tokens, unfilteredTokens } = dictionary; return function (prop) { let to_ret_prop = `${indentation}${prefix}${prop.name}${separator} `; let value = prop.value; @@ -162,10 +173,10 @@ export default function createPropertyFormatter({ * This will see if there are references and if there are, replace * the resolved value with the reference's name. */ - if (outputReferences && usesReference(prop.original.value)) { + if (outputReferences && usesReferences(prop.original.value)) { // Formats that use this function expect `value` to be a string // or else you will get '[object Object]' in the output - const refs = getReferences(dictionary, prop.original.value); + const refs = getReferences(prop.original.value, tokens, { unfilteredTokens }, []); // original can either be an object value, which requires transitive value transformation in web CSS formats // or a different (primitive) type, meaning it can be stringified. @@ -207,7 +218,7 @@ export default function createPropertyFormatter({ }); } - to_ret_prop += prop.attributes.category === 'asset' ? `"${value}"` : value; + to_ret_prop += prop?.attributes?.category === 'asset' ? `"${value}"` : value; const themeable_prop = typeof prop.themeable === 'boolean' ? prop.themeable : themeable; if (format === 'sass' && themeable_prop) { @@ -217,12 +228,7 @@ export default function createPropertyFormatter({ to_ret_prop += suffix; if (prop.comment && commentStyle !== 'none') { - to_ret_prop = addComment(to_ret_prop, { - comment: prop.comment, - style: commentStyle, - position: commentPosition, - indentation, - }); + to_ret_prop = addComment(to_ret_prop, prop.comment, mergedOptions); } return to_ret_prop; diff --git a/lib/common/formatHelpers/fileHeader.js b/lib/common/formatHelpers/fileHeader.js index 4b426ceac..27d860efb 100644 --- a/lib/common/formatHelpers/fileHeader.js +++ b/lib/common/formatHelpers/fileHeader.js @@ -11,10 +11,14 @@ * and limitations under the License. */ -// no-op default -const defaultFileHeader = (arr) => arr; +/** + * @typedef {import('../../../types/File.d.ts').File} File + * @typedef {import('../../../types/File.d.ts').FileHeader} FileHeader + * @typedef {import('../../../types/File.d.ts').FormattingOptions} Formatting + */ const lineSeparator = `\n`; +/** @type {Formatting} */ const defaultFormatting = { lineSeparator, prefix: ` * `, @@ -30,9 +34,9 @@ const defaultFormatting = { * @memberof module:formatHelpers * @name fileHeader * @param {Object} options - * @param {File} options.file - The file object that is passed to the formatter. - * @param {String} options.commentStyle - The only options are 'short' and 'xml', which will use the // or \ style comments respectively. Anything else will use \/\* style comments. - * @param {Object} options.formatting - Custom formatting properties that define parts of a comment in code. The configurable strings are: prefix, lineSeparator, header, and footer. + * @param {File} [options.file] - The file object that is passed to the formatter. + * @param {'short' | 'xml' | 'long'} [options.commentStyle] - The only options are 'short', 'xml' and 'long', which will use the // or \ or \/\* style comments respectively. Default fallback is 'long'. + * @param {Formatting} [options.formatting] - Custom formatting properties that define parts of a comment in code. The configurable strings are: prefix, lineSeparator, header, and footer. * @returns {String} * @example * ```js @@ -46,18 +50,21 @@ const defaultFormatting = { * }); * ``` */ -export default function fileHeader({ file = {}, commentStyle, formatting = {} }) { +export default function fileHeader({ file, commentStyle, formatting = {} }) { // showFileHeader is true by default let showFileHeader = true; - if (file.options && typeof file.options.showFileHeader !== 'undefined') { + if (typeof file?.options?.showFileHeader !== 'undefined') { showFileHeader = file.options.showFileHeader; } // Return empty string if the showFileHeader is false if (!showFileHeader) return ''; - let fn = defaultFileHeader; - if (file.options && typeof file.options.fileHeader === 'function') { + /** + * @type {FileHeader} + */ + let fn = (arr) => arr; + if (typeof file?.options?.fileHeader === 'function') { fn = file.options.fileHeader; } @@ -77,6 +84,6 @@ export default function fileHeader({ file = {}, commentStyle, formatting = {} }) } return `${header}${fn(defaultHeader) - .map((line) => `${prefix}${line}`) + .map(/** @param {string} line */ (line) => `${prefix}${line}`) .join(lineSeparator)}${footer}`; } diff --git a/lib/common/formatHelpers/formattedVariables.js b/lib/common/formatHelpers/formattedVariables.js index 659b53454..ea17c8936 100644 --- a/lib/common/formatHelpers/formattedVariables.js +++ b/lib/common/formatHelpers/formattedVariables.js @@ -14,6 +14,13 @@ import createPropertyFormatter from './createPropertyFormatter.js'; import sortByReference from './sortByReference.js'; +/** + * @typedef {import('../../../types/DesignToken.d.ts').TransformedToken} Token + * @typedef {import('../../../types/DesignToken.d.ts').TransformedTokens} Tokens + * @typedef {import('../../../types/File.d.ts').FormattingOptions} Formatting + * @typedef {import('../../../types/DesignToken.d.ts').Dictionary} Dictionary + */ + const defaultFormatting = { lineSeparator: '\n', }; @@ -25,10 +32,10 @@ const defaultFormatting = { * @name formattedVariables * @param {Object} options * @param {String} options.format - What type of variables to output. Options are: css, sass, less, and stylus - * @param {Object} options.dictionary - The dictionary object that gets passed to the formatter method. - * @param {Boolean} options.outputReferences - Whether or not to output references - * @param {Object} options.formatting - Custom formatting properties that define parts of a declaration line in code. This will get passed to `formatHelpers.createPropertyFormatter` and used for the `lineSeparator` between lines of code. - * @param {Boolean} options.themeable [false] - Whether tokens should default to being themeable. + * @param {Dictionary} options.dictionary - The dictionary object that gets passed to the formatter method. + * @param {Boolean} [options.outputReferences] - Whether or not to output references + * @param {Formatting} [options.formatting] - Custom formatting properties that define parts of a declaration line in code. This will get passed to `formatHelpers.createPropertyFormatter` and used for the `lineSeparator` between lines of code. + * @param {Boolean} [options.themeable] [false] - Whether tokens should default to being themeable. * @returns {String} * @example * ```js @@ -51,7 +58,10 @@ export default function formattedVariables({ formatting = {}, themeable = false, }) { - let { allTokens } = dictionary; + // typecast, we know that by know the tokens have been transformed + let allTokens = /** @type {Token[]} */ (dictionary.allTokens); + /** @type {Tokens} */ + const tokens = dictionary.tokens; let { lineSeparator } = Object.assign({}, defaultFormatting, formatting); @@ -64,7 +74,9 @@ export default function formattedVariables({ if (outputReferences) { // note: using the spread operator here so we get a new array rather than // mutating the original - allTokens = [...allTokens].sort(sortByReference(dictionary)); + allTokens = [...allTokens].sort( + sortByReference(tokens, { unfilteredTokens: dictionary.unfilteredTokens }), + ); } return allTokens diff --git a/lib/common/formatHelpers/getTypeScriptType.js b/lib/common/formatHelpers/getTypeScriptType.js index 42eb0d869..036fa1bf4 100644 --- a/lib/common/formatHelpers/getTypeScriptType.js +++ b/lib/common/formatHelpers/getTypeScriptType.js @@ -11,6 +11,10 @@ * and limitations under the License. */ +/** + * @typedef {import('../../../types/Config.d.ts').LocalOptions} Options + */ + /** * Given some value, returns a basic valid TypeScript type for that value. * Supports numbers, strings, booleans, arrays and objects of any of those types. @@ -31,9 +35,8 @@ * } * }); *``` - * @param {*} value A value to check the type of. - * @param {Object} options - * @param {Boolean} options.outputStringLiterals - Whether or not to output literal types for string values + * @param {any} value A value to check the type of. + * @param {Options & {outputStringLiterals?: boolean}} [options] * @return {String} A valid name for a TypeScript type. * */ @@ -63,19 +66,19 @@ function getObjectType(value) { } /** - * @param {Array} value An array to check each property of + * @param {any[]} arr An array to check each property of * @returns {String} A valid type for the passed array and it's items */ -function getArrayType(passedArray) { - if (passedArray.length > 0) { - const firstValueType = getTypeScriptType(passedArray[0]); - if (passedArray.every((v) => getTypeScriptType(v) === firstValueType)) { +function getArrayType(arr) { + if (arr.length > 0) { + const firstValueType = getTypeScriptType(arr[0]); + if (arr.every((v) => getTypeScriptType(v) === firstValueType)) { return firstValueType + '[]'; } else { return `(${Array.from( new Set( - passedArray.map((item, index) => { - const isLast = passedArray.length === index + 1; + arr.map((item, index) => { + const isLast = arr.length === index + 1; return `${getTypeScriptType(item)}${!isLast ? ' | ' : ''}`; }), ), diff --git a/lib/common/formatHelpers/iconsWithPrefix.js b/lib/common/formatHelpers/iconsWithPrefix.js index 730f91eb9..94f096f42 100644 --- a/lib/common/formatHelpers/iconsWithPrefix.js +++ b/lib/common/formatHelpers/iconsWithPrefix.js @@ -11,6 +11,11 @@ * and limitations under the License. */ +/** + * @typedef {import('../../../types/DesignToken.d.ts').TransformedToken} Token + * @typedef {import('../../../types/Config.d.ts').LocalOptions} Options + */ + /** * * This is used to create CSS (and CSS pre-processor) lists of icons. It assumes you are @@ -21,7 +26,7 @@ * @name iconsWithPrefix * @param {String} prefix - Character to prefix variable names, like '$' for Sass * @param {Token[]} allTokens - allTokens array on the dictionary object passed to the formatter function. - * @param {Object} options - options object passed to the formatter function. + * @param {Options} options - options object passed to the formatter function. * @returns {String} * @example * ```js @@ -36,11 +41,11 @@ export default function iconsWithPrefix(prefix, allTokens, options) { return allTokens .filter(function (token) { - return token.attributes.category === 'content' && token.attributes.type === 'icon'; + return token.attributes?.category === 'content' && token.attributes.type === 'icon'; }) .map(function (token) { var varName = prefix + token.name + ': ' + token.value + ';'; - var className = '.' + options.prefix + '-icon.' + token.attributes.item + ':before '; + var className = '.' + options.prefix + '-icon.' + token.attributes?.item + ':before '; var declaration = '{ content: ' + prefix + token.name + '; }'; return varName + '\n' + className + declaration; }) diff --git a/lib/common/formatHelpers/minifyDictionary.js b/lib/common/formatHelpers/minifyDictionary.js index 3316defee..4746f7e34 100644 --- a/lib/common/formatHelpers/minifyDictionary.js +++ b/lib/common/formatHelpers/minifyDictionary.js @@ -11,12 +11,16 @@ * and limitations under the License. */ +/** + * @typedef {import('../../../types/DesignToken.d.ts').TransformedTokens} Tokens + */ + /** * Outputs an object stripping out everything except values * @memberof module:formatHelpers * @name minifyDictionary - * @param {Object} obj - The object to minify. You will most likely pass `dictionary.tokens` to it. - * @returns {Object} + * @param {Tokens} obj - The object to minify. You will most likely pass `dictionary.tokens` to it. + * @returns {Tokens} * @example * ```js * StyleDictionary.registerFormat({ @@ -32,7 +36,8 @@ export default function minifyDictionary(obj) { return obj; } - var toRet = {}; + /** @type {Tokens} */ + const toRet = {}; if (obj.hasOwnProperty('value')) { return obj.value; diff --git a/lib/common/formatHelpers/setComposeObjectProperties.js b/lib/common/formatHelpers/setComposeObjectProperties.js index 96bd9cacb..3a4486387 100644 --- a/lib/common/formatHelpers/setComposeObjectProperties.js +++ b/lib/common/formatHelpers/setComposeObjectProperties.js @@ -11,14 +11,18 @@ * and limitations under the License. */ +/** + * @typedef {import('../../../types/Config.d.ts').LocalOptions} Options + */ + /** * Outputs an object for compose format configurations. Sets import. * @memberof module:formatHelpers * @name setComposeObjectProperties - * @param {Object} options - The options object declared at configuration + * @param {Options & {import?:string[]}} [options] - The options object declared at configuration * @returns {Object} */ -export default function setComposeObjectProperties(options) { +export default function setComposeObjectProperties(options = {}) { if (typeof options.import === 'undefined') { options.import = ['androidx.compose.ui.graphics.Color', 'androidx.compose.ui.unit.*']; } else if (typeof options.import === 'string') { diff --git a/lib/common/formatHelpers/setSwiftFileProperties.js b/lib/common/formatHelpers/setSwiftFileProperties.js index a4d78fd70..c22e85edc 100644 --- a/lib/common/formatHelpers/setSwiftFileProperties.js +++ b/lib/common/formatHelpers/setSwiftFileProperties.js @@ -11,16 +11,20 @@ * and limitations under the License. */ +/** + * @typedef {import('../../../types/Config.d.ts').LocalOptions} Options + */ + /** * Outputs an object with swift format configurations. Sets import, object type and access control. * @memberof module:formatHelpers * @name setSwiftFileProperties - * @param {Object} options - The options object declared at configuration - * @param {String} objectType - The type of the object in the final file. Could be a class, enum, struct, etc. - * @param {String} transformGroup - The transformGroup of the file, so it can be applied proper import + * @param {Options & {objectType?:string; import?: string[]; accessControl?: string;}} [options] - The options object declared at configuration + * @param {String} [objectType] - The type of the object in the final file. Could be a class, enum, struct, etc. + * @param {String} [transformGroup] - The transformGroup of the file, so it can be applied proper import * @returns {Object} */ -export default function setSwiftFileProperties(options, objectType, transformGroup) { +export default function setSwiftFileProperties(options = {}, objectType, transformGroup) { if (typeof options.objectType === 'undefined') { if (typeof objectType === 'undefined') { options.objectType = 'class'; diff --git a/lib/common/formatHelpers/sortByName.js b/lib/common/formatHelpers/sortByName.js index c594721f4..db6d25329 100644 --- a/lib/common/formatHelpers/sortByName.js +++ b/lib/common/formatHelpers/sortByName.js @@ -11,6 +11,10 @@ * and limitations under the License. */ +/** + * @typedef {import("../../../types/DesignToken.d.ts").TransformedToken} Token + */ + /** * A sorting function to be used when iterating over `dictionary.allTokens` in * a format. @@ -27,9 +31,9 @@ * } * }); * ``` - * @param {*} a - first element for comparison - * @param {*} b - second element for comparison - * @returns {Integer} -1 or 1 depending on which element should come first based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort + * @param {Token} a - first element for comparison + * @param {Token} b - second element for comparison + * @returns {number} -1 or 1 depending on which element should come first based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort */ export default function sortByName(a, b) { if (b.name > a.name) { diff --git a/lib/common/formatHelpers/sortByReference.js b/lib/common/formatHelpers/sortByReference.js index d6abe1dfa..53f646897 100644 --- a/lib/common/formatHelpers/sortByReference.js +++ b/lib/common/formatHelpers/sortByReference.js @@ -11,7 +11,12 @@ * and limitations under the License. */ -import { usesReference, getReferences } from 'style-dictionary/utils'; +import { usesReferences, getReferences } from 'style-dictionary/utils'; + +/** + * @typedef {import('../../../types/DesignToken.d.ts').TransformedTokens} Tokens + * @typedef {import('../../../types/DesignToken.d.ts').TransformedToken} Token + */ /** * A function that returns a sorting function to be used with Array.sort that @@ -24,11 +29,17 @@ import { usesReference, getReferences } from 'style-dictionary/utils'; * ```javascript * dictionary.allTokens.sort(sortByReference(dictionary)) * ``` - * @param {Dictionary} dictionary - * @returns {Function} + * @param {Tokens} tokens + * @param {{unfilteredTokens?: Tokens}} [opts] + * @returns {(a: Token, b: Token) => number} */ -export default function sortByReference(dictionary) { - // The sorter function is recursive to account for multiple levels of nesting +export default function sortByReference(tokens, opts) { + /** + * The sorter function is recursive to account for multiple levels of nesting + * @param {Token} a + * @param {Token} b + * @returns + */ function sorter(a, b) { const aComesFirst = -1; const bComesFirst = 1; @@ -42,11 +53,15 @@ export default function sortByReference(dictionary) { // If token a uses a reference and token b doesn't, b might come before a // read on.. - if (a.original && usesReference(a.original.value)) { - // Both a and b have references, we need to see if the reference each other - if (b.original && usesReference(b.original.value)) { - const aRefs = getReferences(dictionary, a.original.value); - const bRefs = getReferences(dictionary, b.original.value); + if (a.original && usesReferences(a.original.value)) { + // Both a and b have references, we need to see if they reference each other + if (b.original && usesReferences(b.original.value)) { + const aRefs = getReferences(a.original.value, tokens, { + unfilteredTokens: opts?.unfilteredTokens, + }); + const bRefs = getReferences(b.original.value, tokens, { + unfilteredTokens: opts?.unfilteredTokens, + }); aRefs.forEach((aRef) => { // a references b, we want b to come first @@ -65,8 +80,8 @@ export default function sortByReference(dictionary) { // both a and b have references and don't reference each other // we go further down the rabbit hole (reference chain) return sorter(aRefs[0], bRefs[0]); - // a has a reference and b does not: } else { + // a has a reference and b does not: return bComesFirst; } // a does not have a reference it should come first regardless if b has one diff --git a/lib/common/formats.js b/lib/common/formats.js index 951f0893d..ee5c78b3c 100644 --- a/lib/common/formats.js +++ b/lib/common/formats.js @@ -47,20 +47,32 @@ import scssMapFlat from './templates/scss/map-flat.template.js'; import macrosTemplate from './templates/ios/macros.template.js'; import plistTemplate from './templates/ios/plist.template.js'; +/** + * @typedef {import('../../types/Format.d.ts').Format} Format + * @typedef {import('../../types/Format.d.ts').Formatter} Formatter + * @typedef {import('../../types/Format.d.ts').FormatterArguments} FormatOpts + * @typedef {import('../../types/DesignToken.d.ts').TransformedToken} Token + * @typedef {import('../../types/DesignToken.d.ts').TransformedTokens} Tokens + */ + /** * @namespace Formats */ +/** + * @type {Record} + */ const formats = { /** * Creates a CSS file with variable definitions based on the style dictionary * * @memberof Formats * @kind member - * @param {Object} options - * @param {Boolean} [options.showFileHeader=true] - Whether or not to include a comment that has the build date - * @param {Boolean} [options.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. - * @param {string} [options.selector] - Override the root css selector + * @typedef {Object} cssVariablesOpts + * @property {Boolean} [cssVariablesOpts.showFileHeader=true] - Whether or not to include a comment that has the build date + * @property {Boolean} [cssVariablesOpts.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @property {string} [cssVariablesOpts.selector] - Override the root css selector + * @param {FormatOpts & { options?: cssVariablesOpts}} options * @example * ```css * :root { @@ -87,6 +99,7 @@ const formats = { * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```scss * $tokens: ( @@ -108,9 +121,10 @@ const formats = { * * @memberof Formats * @kind member - * @param {Object} options - * @param {Boolean} [options.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. - * @param {Boolean} [options.themeable=true] - Whether or not tokens should default to being themeable, if not otherwise specified per token. + * @typedef {Object} scssMapDeepOpts + * @property {Boolean} [scssMapDeepOpts.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @property {Boolean} [scssMapDeepOpts.themeable=true] - Whether or not tokens should default to being themeable, if not otherwise specified per token. + * @param {FormatOpts & { options?: scssMapDeepOpts}} options * @example * ```scss * $color-background-base: #f0f0f0 !default; @@ -147,10 +161,11 @@ const formats = { * * @memberof Formats * @kind member - * @param {Object} options - * @param {Boolean} [options.showFileHeader=true] - Whether or not to include a comment that has the build date - * @param {Boolean} [options.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. - * @param {Boolean} [options.themeable=false] - Whether or not tokens should default to being themeable, if not otherwise specified per token. + * @typedef {Object} scssVariablesOpts + * @property {Boolean} [scssVariablesOpts.showFileHeader=true] - Whether or not to include a comment that has the build date + * @property {Boolean} [scssVariablesOpts.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @property {Boolean} [scssVariablesOpts.themeable=false] - Whether or not tokens should default to being themeable, if not otherwise specified per token. + * @param {FormatOpts & { options?: scssVariablesOpts}} options * @example * ```scss * $color-background-base: #f0f0f0; @@ -171,6 +186,7 @@ const formats = { * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```scss * $content-icon-email: '\E001'; @@ -189,9 +205,10 @@ const formats = { * * @memberof Formats * @kind member - * @param {Object} options - * @param {Boolean} [options.showFileHeader=true] - Whether or not to include a comment that has the build date - * @param {Boolean} [options.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @typedef {Object} lessVariablesOpts + * @property {Boolean} [lessVariablesOpts.showFileHeader=true] - Whether or not to include a comment that has the build date + * @property {Boolean} [lessVariablesOpts.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @param {FormatOpts & { options?: lessVariablesOpts}} options * @example * ```less * \@color-background-base: #f0f0f0; @@ -212,6 +229,7 @@ const formats = { * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```less * \@content-icon-email: '\E001'; @@ -230,6 +248,7 @@ const formats = { * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```stylus * $color-background-base= #f0f0f0; @@ -250,6 +269,7 @@ const formats = { * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```js * module.exports = { @@ -277,6 +297,7 @@ const formats = { * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```js * module.exports = { @@ -305,6 +326,7 @@ const formats = { * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```js * var StyleDictionary = { @@ -336,6 +358,7 @@ const formats = { * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```js * (function(root, factory) { @@ -360,7 +383,7 @@ const formats = { * ``` */ 'javascript/umd': function ({ dictionary, file }) { - var name = file.name || '_styleDictionary'; + const name = file.name || '_styleDictionary'; return ( fileHeader({ file }) + '(function(root, factory) {\n' + @@ -411,6 +434,7 @@ const formats = { * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```js * export const ColorBackgroundBase = '#ffffff'; @@ -422,7 +446,7 @@ const formats = { fileHeader({ file }) + dictionary.allTokens .map(function (token) { - var to_ret = 'export const ' + token.name + ' = ' + JSON.stringify(token.value) + ';'; + let to_ret = 'export const ' + token.name + ' = ' + JSON.stringify(token.value) + ';'; if (token.comment) to_ret = to_ret.concat(' // ' + token.comment); return to_ret; }) @@ -457,8 +481,9 @@ const formats = { * * @memberof Formats * @kind member - * @param {Object} options - * @param {Boolean} [options.outputStringLiterals=false] - Whether or not to output literal types for string values + * @typedef {Object} typescriptEs6DeclarationsOpts + * @property {Boolean} [options.outputStringLiterals=false] - Whether or not to output literal types for string values + * @param {FormatOpts & { options?: typescriptEs6DeclarationsOpts }} options * @example * ```typescript * export const ColorBackgroundBase : string; @@ -506,6 +531,7 @@ const formats = { * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```typescript * export default tokens; @@ -536,13 +562,17 @@ const formats = { */ 'typescript/module-declarations': function ({ dictionary, file, options }) { const { moduleName = `tokens` } = options; + /** + * @param {Tokens} obj + * @returns + */ function treeWalker(obj) { let type = Object.create(null); let has = Object.prototype.hasOwnProperty.bind(obj); if (has('value')) { type = 'DesignToken'; } else { - for (var k in obj) + for (let k in obj) if (has(k)) { switch (typeof obj[k]) { case 'object': @@ -598,9 +628,10 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member - * @param {Object} options - * @param {Boolean} [options.showFileHeader=true] - Whether or not to include a comment that has the build date - * @param {Boolean} [options.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @typedef {Object} androidResourcesOpts + * @property {Boolean} [androidResourcesOpts.showFileHeader=true] - Whether or not to include a comment that has the build date + * @property {Boolean} [androidResourcesOpts.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @param {FormatOpts & { options?: androidResourcesOpts }} options * @example * ```xml * @@ -610,8 +641,8 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * 14sp * ``` */ - 'android/resources': function ({ dictionary, options, file }) { - return androidResources({ dictionary, file, options, fileHeader }); + 'android/resources': function ({ dictionary, file }) { + return androidResources({ dictionary, file, fileHeader }); }, /** @@ -629,6 +660,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```xml * @@ -658,6 +690,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```xml * @@ -687,6 +720,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```xml * @@ -717,6 +751,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @todo Update the filter on this. * @example * ```xml @@ -748,6 +783,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```xml * @@ -768,12 +804,13 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member - * @param {String} className The name of the generated Kotlin object - * @param {String} packageName The package for the generated Kotlin object - * @param {Object} options - * @param {String[]} [options.import=['androidx.compose.ui.graphics.Color', 'androidx.compose.ui.unit.*']] - Modules to import. Can be a string or array of strings - * @param {Boolean} [options.showFileHeader=true] - Whether or not to include a comment that has the build date - * @param {Boolean} [options.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @typedef {Object} composeObjectOpts + * @property {String} [composeObjectOpts.className] The name of the generated Kotlin object + * @property {String} [composeObjectOpts.packageName] The package for the generated Kotlin object + * @property {String[]} [composeObjectOpts.import=['androidx.compose.ui.graphics.Color', 'androidx.compose.ui.unit.*']] - Modules to import. Can be a string or array of strings + * @property {Boolean} [composeObjectOpts.showFileHeader=true] - Whether or not to include a comment that has the build date + * @property {Boolean} [composeObjectOpts.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @param {FormatOpts & { options?: composeObjectOpts }} options * @example * ```kotlin * package com.example.tokens; @@ -786,8 +823,8 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * ``` */ 'compose/object': function ({ dictionary, options, file }) { + const { allTokens, tokens, unfilteredTokens } = dictionary; const template = _template(composeObject); - let allTokens; const { outputReferences } = options; const formatProperty = createPropertyFormatter({ outputReferences, @@ -798,15 +835,16 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul }, }); + let sortedTokens; if (outputReferences) { - allTokens = [...dictionary.allTokens].sort(sortByReference(dictionary)); + sortedTokens = [...allTokens].sort(sortByReference(tokens, { unfilteredTokens })); } else { - allTokens = [...dictionary.allTokens].sort(sortByName); + sortedTokens = [...allTokens].sort(sortByName); } options = setComposeObjectProperties(options); - return template({ allTokens, file, options, formatProperty, fileHeader }); + return template({ allTokens: sortedTokens, file, options, formatProperty, fileHeader }); }, // iOS templates @@ -816,6 +854,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```objectivec * #import @@ -836,6 +875,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @todo Fix this template and add example and usage */ 'ios/plist': function ({ dictionary, options, file }) { @@ -849,6 +889,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @todo Add example and usage */ 'ios/singleton.m': function ({ dictionary, options, file }) { @@ -862,6 +903,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @todo Add example and usage */ 'ios/singleton.h': function ({ dictionary, options, file }) { @@ -875,6 +917,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @todo Add example and usage */ 'ios/static.h': function ({ dictionary, options, file }) { @@ -888,6 +931,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @todo Add example and usage */ 'ios/static.m': function ({ dictionary, options, file }) { @@ -901,6 +945,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @todo Add example and usage */ 'ios/colors.h': function ({ dictionary, options, file }) { @@ -914,6 +959,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @todo Add example and usage */ 'ios/colors.m': function ({ dictionary, options, file }) { @@ -927,6 +973,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @todo Add example and usage */ 'ios/strings.h': function ({ dictionary, options, file }) { @@ -940,6 +987,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @todo Add example and usage */ 'ios/strings.m': function ({ dictionary, options, file }) { @@ -953,12 +1001,13 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member - * @param {Object} options - * @param {String} [options.accessControl=public] - Level of [access](https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html) of the generated swift object - * @param {String[]} [options.import=UIKit] - Modules to import. Can be a string or array of strings - * @param {String} [options.className] - The name of the generated Swift class - * @param {Boolean} [options.showFileHeader=true] - Whether or not to include a comment that has the build date - * @param {Boolean} [options.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @typedef {Object} iosSwiftClassOpts + * @property {String} [iosSwiftClassOpts.accessControl='public'] - Level of [access](https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html) of the generated swift object + * @property {String[]} [iosSwiftClassOpts.import='UIKit'] - Modules to import. Can be a string or array of strings + * @property {String} [iosSwiftClassOpts.className] - The name of the generated Swift class + * @property {Boolean} [iosSwiftClassOpts.showFileHeader=true] - Whether or not to include a comment that has the build date + * @property {Boolean} [iosSwiftClassOpts.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @param {FormatOpts & { options?: iosSwiftClassOpts }} options * @example * ```swift * public class StyleDictionary { @@ -967,8 +1016,8 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * ``` */ 'ios-swift/class.swift': function ({ dictionary, options, file, platform }) { + const { allTokens, tokens, unfilteredTokens } = dictionary; const template = _template(iosSwiftAny); - let allTokens; const { outputReferences } = options; options = setSwiftFileProperties(options, 'class', platform.transformGroup); const formatProperty = createPropertyFormatter({ @@ -979,13 +1028,14 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul }, }); + let sortedTokens; if (outputReferences) { - allTokens = [...dictionary.allTokens].sort(sortByReference(dictionary)); + sortedTokens = [...allTokens].sort(sortByReference(tokens, { unfilteredTokens })); } else { - allTokens = [...dictionary.allTokens].sort(sortByName); + sortedTokens = [...allTokens].sort(sortByName); } - return template({ allTokens, file, options, formatProperty, fileHeader }); + return template({ allTokens: sortedTokens, file, options, formatProperty, fileHeader }); }, /** @@ -993,11 +1043,12 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member - * @param {Object} options - * @param {String} [options.accessControl=public] - Level of [access](https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html) of the generated swift object - * @param {String[]} [options.import=UIKit] - Modules to import. Can be a string or array of strings - * @param {Boolean} [options.showFileHeader=true] - Whether or not to include a comment that has the build date - * @param {Boolean} [options.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @typedef {Object} iosSwiftEnumOpts + * @property {String} [iosSwiftEnumOpts.accessControl='public'] - Level of [access](https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html) of the generated swift object + * @property {String[]} [iosSwiftEnumOpts.import='UIKit'] - Modules to import. Can be a string or array of strings + * @property {Boolean} [iosSwiftEnumOpts.showFileHeader=true] - Whether or not to include a comment that has the build date + * @property {Boolean} [iosSwiftEnumOpts.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @param {FormatOpts & { options?: iosSwiftEnumOpts }} options * @example * ```swift * public enum StyleDictionary { @@ -1006,8 +1057,8 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * ``` */ 'ios-swift/enum.swift': function ({ dictionary, options, file, platform }) { + const { allTokens, tokens, unfilteredTokens } = dictionary; const template = _template(iosSwiftAny); - let allTokens; const { outputReferences } = options; options = setSwiftFileProperties(options, 'enum', platform.transformGroup); const formatProperty = createPropertyFormatter({ @@ -1018,12 +1069,13 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul }, }); + let sortedTokens; if (outputReferences) { - allTokens = [...dictionary.allTokens].sort(sortByReference(dictionary)); + sortedTokens = [...allTokens].sort(sortByReference(tokens, { unfilteredTokens })); } else { - allTokens = [...dictionary.allTokens].sort(sortByName); + sortedTokens = [...allTokens].sort(sortByName); } - return template({ allTokens, file, options, formatProperty, fileHeader }); + return template({ allTokens: sortedTokens, file, options, formatProperty, fileHeader }); }, /** @@ -1038,12 +1090,13 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member - * @param {Object} options - * @param {String} [options.accessControl=public] - Level of [access](https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html) of the generated swift object - * @param {String[]} [options.import=UIKit] - Modules to import. Can be a string or array of strings - * @param {String} [options.objectType=class] - The type of the generated Swift object - * @param {Boolean} [options.showFileHeader=true] - Whether or not to include a comment that has the build date - * @param {Boolean} [options.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @typedef {Object} iosSwiftAnyOpts + * @property {string} [iosSwiftAnyOpts.accessControl='public'] - Level of [access](https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html) of the generated swift object + * @property {string[]} [iosSwiftAnyOpts.import='UIKit'] - Modules to import. Can be a string or array of strings + * @property {string} [iosSwiftAnyOpts.objectType='class'] - The type of the generated Swift object + * @property {boolean} [iosSwiftAnyOpts.showFileHeader=true] - Whether or not to include a comment that has the build date + * @property {boolean} [iosSwiftAnyOpts.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @param {FormatOpts & { options?: iosSwiftAnyOpts }} options * @example * ```swift * import UIKit @@ -1055,8 +1108,8 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * ``` */ 'ios-swift/any.swift': function ({ dictionary, options, file, platform }) { + const { allTokens, tokens, unfilteredTokens } = dictionary; const template = _template(iosSwiftAny); - let allTokens; const { outputReferences } = options; options = setSwiftFileProperties(options, options.objectType, platform.transformGroup); const formatProperty = createPropertyFormatter({ @@ -1067,12 +1120,13 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul }, }); + let sortedTokens; if (outputReferences) { - allTokens = [...dictionary.allTokens].sort(sortByReference(dictionary)); + sortedTokens = [...allTokens].sort(sortByReference(tokens, { unfilteredTokens })); } else { - allTokens = [...dictionary.allTokens].sort(sortByName); + sortedTokens = [...allTokens].sort(sortByName); } - return template({ allTokens, file, options, formatProperty, fileHeader }); + return template({ allTokens: sortedTokens, file, options, formatProperty, fileHeader }); }, // Css templates @@ -1093,6 +1147,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```json * { @@ -1115,6 +1170,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```js * { @@ -1137,6 +1193,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```json * { @@ -1157,6 +1214,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```json * { @@ -1182,6 +1240,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```json * { @@ -1196,13 +1255,15 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * ``` */ 'sketch/palette': function ({ dictionary }) { - var to_ret = { + const to_ret = { compatibleVersion: '1.0', pluginVersion: '1.1', + /** @type {any[]} */ + colors: [], }; to_ret.colors = dictionary.allTokens .filter(function (token) { - return token.attributes.category === 'color' && token.attributes.type === 'base'; + return token.attributes?.category === 'color' && token.attributes.type === 'base'; }) .map(function (token) { return token.value; @@ -1217,6 +1278,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member + * @param {FormatOpts} options * @example * ```json * { @@ -1231,7 +1293,7 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * ``` */ 'sketch/palette/v2': function ({ dictionary }) { - var to_ret = { + const to_ret = { compatibleVersion: '2.0', pluginVersion: '2.2', colors: dictionary.allTokens.map(function (token) { @@ -1253,9 +1315,10 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * * @memberof Formats * @kind member - * @param {Object} options - * @param {Boolean} [options.showFileHeader=true] - Whether or not to include a comment that has the build date - * @param {Boolean} [options.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @typedef {Object} flutterClassOpts + * @property {Boolean} [flutterClassOpts.showFileHeader=true] - Whether or not to include a comment that has the build date + * @property {Boolean} [flutterClassOpts.outputReferences=false] - Whether or not to keep [references](/#/formats?id=references-in-output-files) (a -> b -> c) in the output. + * @param {FormatOpts & { options?: flutterClassOpts }} options * @example * ```dart * import 'package:flutter/material.dart'; @@ -1269,20 +1332,21 @@ declare const ${moduleName}: ${JSON.stringify(treeWalker(dictionary.tokens), nul * ``` */ 'flutter/class.dart': function ({ dictionary, options, file }) { + const { allTokens, tokens, unfilteredTokens } = dictionary; const template = _template(flutterClassDart); - let allTokens; const { outputReferences } = options; const formatProperty = createPropertyFormatter({ outputReferences, dictionary, }); + let sortedTokens; if (outputReferences) { - allTokens = [...dictionary.allTokens].sort(sortByReference(dictionary)); + sortedTokens = [...allTokens].sort(sortByReference(tokens, { unfilteredTokens })); } else { - allTokens = [...dictionary.allTokens].sort(sortByName); + sortedTokens = [...allTokens].sort(sortByName); } - return template({ allTokens, file, options, formatProperty, fileHeader }); + return template({ allTokens: sortedTokens, file, options, formatProperty, fileHeader }); }, }; diff --git a/lib/common/templates/android/resources.template.js b/lib/common/templates/android/resources.template.js index 2dff05ac3..9e779f135 100644 --- a/lib/common/templates/android/resources.template.js +++ b/lib/common/templates/android/resources.template.js @@ -11,14 +11,29 @@ * and limitations under the License. */ -import { usesReference, getReferences } from 'style-dictionary/utils'; +import { usesReferences, getReferences } from 'style-dictionary/utils'; +/** + * @typedef {import('../../../../types/DesignToken.d.ts').Dictionary} Dictionary + * @typedef {import('../../../../types/DesignToken.d.ts').TransformedToken} Token + * @typedef {import('../../../../types/DesignToken.d.ts').TransformedTokens} Tokens + * @typedef {import('../../../../types/File.d.ts').File} File + * @typedef {import('../../formatHelpers/fileHeader.js').default} FileHeader + */ + +/** + * @param {{ + * dictionary: Dictionary; + * file?: File; + * fileHeader?: FileHeader; + * }} opts + */ export default (opts) => { const { file, fileHeader, dictionary } = opts; - const resourceType = file.resourceType || null; + const resourceType = file?.resourceType || null; - const resourceMap = file.resourceMap || { + const resourceMap = file?.resourceMap || { size: 'dimen', color: 'color', string: 'string', @@ -27,19 +42,28 @@ export default (opts) => { number: 'integer', }; + /** + * @param {Token} prop + * @returns {string} + */ function propToType(prop) { if (resourceType) { return resourceType; } - if (resourceMap[prop.attributes.category]) { + if (prop.attributes?.category && resourceMap[prop.attributes.category]) { return resourceMap[prop.attributes.category]; } return 'string'; } - function propToValue(prop) { - if (file.options && file.options.outputReferences && usesReference(prop.original.value)) { - return `@${propToType(prop)}/${getReferences(dictionary, prop.original.value)[0].name}`; + /** + * @param {Token} prop + * @param {Tokens} tokens + * @returns {string} + */ + function propToValue(prop, tokens) { + if (file?.options && file.options.outputReferences && usesReferences(prop.original.value)) { + return `@${propToType(prop)}/${getReferences(prop.original.value, tokens)[0].name}`; } else { return prop.value; } @@ -47,15 +71,18 @@ export default (opts) => { return ` -${fileHeader({ file, commentStyle: 'xml' })} +${fileHeader ? fileHeader({ file, commentStyle: 'xml' }) : ''} - ${dictionary.allTokens - .map( - (prop) => - `<${propToType(prop)} name="${prop.name}">${propToValue(prop)}${ - prop.comment ? `` : '' - }`, - ) - .reduce((acc, curr) => acc + `${curr}\n `, '')} + ${ + /** @type {Token[]} */ (dictionary.allTokens) + .map( + (prop) => + `<${propToType(prop)} name="${prop.name}">${propToValue( + prop, + dictionary.tokens, + )}${prop.comment ? `` : ''}`, + ) + .join(`\n `) + } `; }; diff --git a/lib/common/transformGroups.js b/lib/common/transformGroups.js index 2b7699ffe..87a1b02ab 100644 --- a/lib/common/transformGroups.js +++ b/lib/common/transformGroups.js @@ -15,6 +15,9 @@ * @namespace TransformGroups */ +/** + * @type {Record} + */ export default { /** * Transforms: diff --git a/lib/common/transforms.js b/lib/common/transforms.js index c1eab08dd..f763186ff 100644 --- a/lib/common/transforms.js +++ b/lib/common/transforms.js @@ -16,60 +16,115 @@ import path from '@bundled-es-modules/path-browserify'; import { snakeCase, kebabCase, camelCase } from 'change-case'; import convertToBase64 from '../utils/convertToBase64.js'; +/** + * @typedef {import('../../types/Transform.d.ts').Transform} Transform + * @typedef {import('../../types/DesignToken.d.ts').TransformedToken} Token + * @typedef {import('../../types/Config.d.ts').PlatformConfig} Options + */ + +/** + * @param {string} str + * @returns {string} + */ + const UNICODE_PATTERN = /&#x([^;]+);/g; const camelOpts = { mergeAmbiguousCharacters: true, }; +/** + * @param {Token} token + * @returns {boolean} + */ function isColor(token) { - return token.attributes.category === 'color'; + return token.attributes?.category === 'color'; } +/** + * @param {Token} token + * @returns {boolean} + */ function isSize(token) { - return token.attributes.category === 'size'; + return token.attributes?.category === 'size'; } +/** + * @param {Token} token + * @returns {boolean} + */ function isFontSize(token) { return ( - token.attributes.category === 'size' && + token.attributes?.category === 'size' && (token.attributes.type === 'font' || token.attributes.type === 'icon') ); } +/** + * @param {Token} token + * @returns {boolean} + */ function isNotFontSize(token) { return ( - token.attributes.category === 'size' && + token.attributes?.category === 'size' && token.attributes.type !== 'font' && token.attributes.type !== 'icon' ); } +/** + * @param {Token} token + * @returns {boolean} + */ function isAsset(token) { - return token.attributes.category === 'asset'; + return token.attributes?.category === 'asset'; } +/** + * @param {Token} token + * @returns {boolean} + */ function isContent(token) { - return token.attributes.category === 'content'; + return token.attributes?.category === 'content'; } +/** + * @param {string} character + * @param {Token} token + * @returns {string} + */ function wrapValueWith(character, token) { return `${character}${token.value}${character}`; } +/** + * @param {Token} token + * @returns {string} + */ function wrapValueWithDoubleQuote(token) { return wrapValueWith('"', token); } +/** + * @param {string} name + * @param {string|number} value + * @param {string} unitType + * @returns {string} + */ function throwSizeError(name, value, unitType) { throw `Invalid Number: '${name}: ${value}' is not a valid number, cannot transform to '${unitType}' \n`; } +/** + * @param {Options} options + * @returns {number} + */ function getBasePxFontSize(options) { return (options && options.basePxFontSize) || 16; } /** * @namespace Transforms + * @type {Record>} */ export default { /** @@ -94,6 +149,7 @@ export default { transformer: function (token) { const attrNames = ['category', 'type', 'item', 'subitem', 'state']; const originalAttrs = token.attributes || {}; + /** @type {Record} */ const generatedAttrs = {}; for (let i = 0; i < token.path.length && i < attrNames.length; i++) { @@ -148,7 +204,7 @@ export default { 'name/human': { type: 'name', transformer: function (token) { - return [token.attributes.item, token.attributes.subitem].join(' '); + return [token.attributes?.item, token.attributes?.subitem].join(' '); }, }, @@ -287,8 +343,9 @@ export default { 'name/cti/pascal': { type: 'name', transformer: function (token, options) { + /** @param {string} str */ const upperFirst = function (str) { - return str ? str[0].toUpperCase() + str.substr(1) : ''; + return str ? str[0].toUpperCase() + str.slice(1) : ''; }; return upperFirst(camelCase([options.prefix].concat(token.path).join(' '), camelOpts)); }, @@ -562,7 +619,7 @@ export default { */ 'color/sketch': { type: 'value', - matcher: (token) => token.attributes.category === 'color', + matcher: /** @param {Token} token */ (token) => token.attributes?.category === 'color', transformer: function (token) { let color = Color(token.original.value).toRgb(); return { @@ -908,13 +965,19 @@ export default { */ 'content/icon': { type: 'value', - matcher: function (token) { - return token.attributes.category === 'content' && token.attributes.type === 'icon'; + matcher: /** @param {Token} token */ function (token) { + return token.attributes?.category === 'content' && token.attributes.type === 'icon'; }, transformer: function (token) { - return token.value.replace(UNICODE_PATTERN, function (match, variable) { - return "'\\" + variable + "'"; - }); + return token.value.replace( + UNICODE_PATTERN, + /** + * @param {string} match + * @param {string} variable */ + function (match, variable) { + return "'\\" + variable + "'"; + }, + ); }, }, @@ -943,7 +1006,7 @@ export default { * ```objectivec * // Matches: token.attributes.category === 'content' * // Returns: - * @"string" + * \@"string" * ``` * * @memberof Transforms @@ -978,15 +1041,15 @@ export default { * * ```objectivec * // Matches: token.attributes.category === 'font' - * // Returns: @"string" + * // Returns: \@"string" * ``` * * @memberof Transforms */ 'font/objC/literal': { type: 'value', - matcher: function (token) { - return token.attributes.category === 'font'; + matcher: /** @param {Token} token */ function (token) { + return token.attributes?.category === 'font'; }, transformer: function (token) { return '@' + wrapValueWithDoubleQuote(token); @@ -1005,8 +1068,8 @@ export default { */ 'font/swift/literal': { type: 'value', - matcher: function (token) { - return token.attributes.category === 'font'; + matcher: /** @param {Token} token */ function (token) { + return token.attributes?.category === 'font'; }, transformer: wrapValueWithDoubleQuote, }, @@ -1024,8 +1087,8 @@ export default { */ 'time/seconds': { type: 'value', - matcher: function (token) { - return token.attributes.category === 'time'; + matcher: /** @param {Token} token */ function (token) { + return token.attributes?.category === 'time'; }, transformer: function (token) { return (parseFloat(token.value) / 1000).toFixed(2) + 's'; @@ -1075,7 +1138,7 @@ export default { * * ```objectivec * // Matches: token.attributes.category === 'asset' - * // Returns: @"string" + * // Returns: \@"string" * ``` * * @memberof Transforms @@ -1167,8 +1230,8 @@ export default { */ 'font/flutter/literal': { type: 'value', - matcher: function (token) { - return token.attributes.category === 'font'; + matcher: /** @param {Token} token */ function (token) { + return token.attributes?.category === 'font'; }, transformer: wrapValueWithDoubleQuote, }, @@ -1188,7 +1251,7 @@ export default { matcher: isSize, transformer: function (token, options) { const baseFont = getBasePxFontSize(options); - return (parseFloat(token.value, 10) * baseFont).toFixed(2); + return (parseFloat(token.value) * baseFont).toFixed(2); }, }, }; diff --git a/lib/filterTokens.js b/lib/filterTokens.js index 2ba4305c9..2f716ee8c 100644 --- a/lib/filterTokens.js +++ b/lib/filterTokens.js @@ -10,17 +10,24 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions * and limitations under the License. */ -import { isPlainObject } from 'is-plain-object'; +import isPlainObject from 'is-plain-obj'; + +/** + * @typedef {import('../types/DesignToken.d.ts').Dictionary} Dictionary + * @typedef {import('../types/DesignToken.d.ts').TransformedTokens} Tokens + * @typedef {import('../types/DesignToken.d.ts').TransformedToken} Token + * @typedef {import('../types/Filter.d.ts').Matcher} Matcher + */ /** * Takes a nested object of tokens and filters them using the provided * function. * - * @param {Object|undefined|null} tokens - * @param {Function} filter - A function that receives a property object and + * @param {Tokens} tokens + * @param {Matcher} filter - A function that receives a property object and * returns `true` if the property should be included in the output or `false` * if the property should be excluded from the output. - * @returns {Object[]} tokens - A new object containing only the tokens + * @returns {Tokens} tokens - A new object containing only the tokens * that matched the filter. */ function filterTokenObject(tokens, filter) { @@ -34,7 +41,7 @@ function filterTokenObject(tokens, filter) { // the filter function and either include it in the final `acc` object or // exclude it (by returning the `acc` object without it added). } else if (typeof value.value !== 'undefined') { - return filter(value) ? { ...acc, [key]: value } : acc; + return filter(/** @type {Token} */ (value)) ? { ...acc, [key]: value } : acc; // If we got here we have an object that is not a property. We'll assume // it's an object containing multiple tokens and recursively filter it // using the `filterTokenObject` function. @@ -52,11 +59,11 @@ function filterTokenObject(tokens, filter) { * Takes a dictionary and filters the `allTokens` array and the `tokens` * object using a function provided by the user. * - * @param {Object} dictionary - * @param {Function} filter - A function that receives a token object + * @param {Dictionary} dictionary + * @param {Matcher} filter - A function that receives a token object * and returns `true` if the token should be included in the output * or `false` if the token should be excluded from the output - * @returns {Object} dictionary - A new dictionary containing only the + * @returns {Dictionary} dictionary - A new dictionary containing only the * tokens that matched the filter (or the original dictionary if no filter * function was provided). */ diff --git a/fs-node.js b/lib/fs-node.js similarity index 100% rename from fs-node.js rename to lib/fs-node.js diff --git a/lib/fs.js b/lib/fs.js new file mode 100644 index 000000000..b11b99246 --- /dev/null +++ b/lib/fs.js @@ -0,0 +1,17 @@ +/** + * @typedef {import('memfs').fs} memfs + * @typedef {import('node:fs')} nodefs + */ + +/** + * Allow to be overridden by setter, set default to memfs for browser env, node:fs for node env + */ +export let fs = /** @type {memfs|nodefs} */ ((await import('@bundled-es-modules/memfs')).default); + +/** + * since ES modules exports are read-only, use a setter + * @param {memfs|nodefs} _fs + */ +export const setFs = (_fs) => { + fs = _fs; +}; diff --git a/lib/performActions.js b/lib/performActions.js index 61e7a0048..81a0d4c37 100644 --- a/lib/performActions.js +++ b/lib/performActions.js @@ -11,20 +11,28 @@ * and limitations under the License. */ +/** + * @typedef {import('../types/DesignToken.d.ts').Dictionary} Dictionary + * @typedef {import('../types/Config.d.ts').PlatformConfig} Config + */ + /** * Performs any actions in a platform config. Pretty * simple really. Actions should be an array of functions, * the calling function should map the functions accordingly. * @private * @memberof module:style-dictionary - * @param {Object} dictionary - * @param {Object} platform - * @returns {null} + * @param {Dictionary} dictionary + * @param {Config} platform + * @returns */ export default function performActions(dictionary, platform) { if (platform.actions) { platform.actions.forEach(function (action) { - action.do(dictionary, platform); + // ensure we've augmented action with the do/undo functions + if (typeof action !== 'string') { + action.do(dictionary, platform); + } }); } } diff --git a/lib/register/action.js b/lib/register/action.js deleted file mode 100644 index 55c666fb4..000000000 --- a/lib/register/action.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -/** - * Adds a custom action to Style Dictionary. Custom - * actions can do whatever you need, such as: copying files, - * base64'ing files, running other build scripts, etc. - * After you register a custom action, you then use that - * action in a platform your config.json - * - * You can perform operations on files generated by the style dictionary - * as actions run after these files are generated. - * Actions are run sequentially, if you write synchronous code then - * it will block other actions, or if you use asynchronous code like Promises - * it will not block. - * - * @static - * @memberof module:style-dictionary - * @param {Object} action - * @param {String} action.name - The name of the action - * @param {Function} action.do - The action in the form of a function. - * @param {Function} [action.undo] - A function that undoes the action. - * @returns {module:style-dictionary} - * @example - * ```js - * StyleDictionary.registerAction({ - * name: 'copy_assets', - * do: function(dictionary, config) { - * console.log('Copying assets directory'); - * fs.copySync('assets', config.buildPath + 'assets'); - * }, - * undo: function(dictionary, config) { - * console.log('Cleaning assets directory'); - * fs.removeSync(config.buildPath + 'assets'); - * } - * }); - * ``` - */ -export default function registerAction(action) { - if (typeof action.name !== 'string') throw new Error('name must be a string'); - if (typeof action.do !== 'function') throw new Error('do must be a function'); - - this.action[action.name] = { - do: action.do, - undo: action.undo, - }; - - return this; -} diff --git a/lib/register/fileHeader.js b/lib/register/fileHeader.js deleted file mode 100644 index 82cbcd841..000000000 --- a/lib/register/fileHeader.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -/** - * Add a custom file header to the style dictionary. File headers are used in - * formats to display some information about how the file was built in a comment. - * @static - * @memberof module:style-dictionary - * @param {Object} options - * @param {String} options.name - Name of the format to be referenced in your config.json - * @param {function} options.fileHeader - Function that returns an array of strings, which will be mapped to comment lines. It takes a single argument which is the default message array. See [file headers](formats.md#file-headers) for more information. - * @returns {module:style-dictionary} - * @example - * ```js - * StyleDictionary.registerFileHeader({ - * name: 'myCustomHeader', - * fileHeader: function(defaultMessage) { - * return [ - * ...defaultMessage, - * `hello, world!` - * ]; - * } - * }) - * ``` - */ - -export default function registerFileHeader(options) { - if (typeof options.name !== 'string') - throw new Error("Can't register file header; options.name must be a string"); - if (typeof options.fileHeader !== 'function') - throw new Error("Can't register file header; options.fileHeader must be a function"); - - this.fileHeader[options.name] = options.fileHeader; - - return this; -} diff --git a/lib/register/filter.js b/lib/register/filter.js deleted file mode 100644 index 79bcac785..000000000 --- a/lib/register/filter.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -/** - * Add a custom filter to the style dictionary - * @static - * @memberof module:style-dictionary - * @param {Object} filter - * @param {String} filter.name - Name of the filter to be referenced in your config.json - * @param {Function} filter.matcher - Matcher function, return boolean if the token should be included. - * @returns {module:style-dictionary} - * @example - * ```js - * StyleDictionary.registerFilter({ - * name: 'isColor', - * matcher: function(token) { - * return token.attributes.category === 'color'; - * } - * }) - * ``` - */ -export default function registerFilter(filter) { - if (typeof filter.name !== 'string') - throw new Error("Can't register filter; filter.name must be a string"); - if (typeof filter.matcher !== 'function') - throw new Error("Can't register filter; filter.matcher must be a function"); - - this.filter[filter.name] = filter.matcher; - - return this; -} diff --git a/lib/register/format.js b/lib/register/format.js deleted file mode 100644 index 4aea45155..000000000 --- a/lib/register/format.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -/** - * @module format - */ - -/** - * The formatter function that is called when Style Dictionary builds files. - * - * @function formatter - * @memberof module:format - * @param {Object} args - A single argument to support named parameters and destructuring. - * @param {Object} args.dictionary - The transformed and resolved dictionary object - * @param {Object} args.dictionary.tokens - Object structure of the tokens that has been transformed and references resolved. - * @param {Array} args.dictionary.allTokens - Flattened array of all the tokens. This makes it easy to output a list, like a list of SCSS variables. - * @param {Object} args.platform - The platform configuration this format is being called in. - * @param {Object} args.file - The file configuration this format is being called in. - * @param {Object} args.options - Merged options object that combines platform level configuration and file level configuration. File options take precedence. - * @returns {String} - * @example - * ```js - * StyleDictionary.registerFormat({ - * name: 'myCustomFormat', - * formatter: function({dictionary, platform, options, file}) { - * return JSON.stringify(dictionary.tokens, null, 2); - * } - * }) - * ``` - */ - -/** - * Add a custom format to the style dictionary - * @static - * @memberof module:style-dictionary - * @param {Object} format - * @param {String} format.name - Name of the format to be referenced in your config.json - * @param {function} format.formatter - Function to perform the format. Takes a single argument. See [creating custom formats](formats.md#creating-formats) - * Must return a string, which is then written to a file. - * @returns {module:style-dictionary} - * @example - * ```js - * StyleDictionary.registerFormat({ - * name: 'json', - * formatter: function({dictionary, platform, options, file}) { - * return JSON.stringify(dictionary.tokens, null, 2); - * } - * }) - * ``` - */ -export default function registerFormat(format) { - if (typeof format.name !== 'string') - throw new Error("Can't register format; format.name must be a string"); - if (typeof format.formatter !== 'function') - throw new Error("Can't register format; format.formatter must be a function"); - - this.format[format.name] = format.formatter; - - return this; -} diff --git a/lib/register/parser.js b/lib/register/parser.js deleted file mode 100644 index b778082fd..000000000 --- a/lib/register/parser.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -/** - * Adds a custom parser to parse style dictionary files - * @static - * @memberof module:style-dictionary - * @param {Regex} pattern - A file path regular expression to match which files this parser should be be used on. This is similar to how webpack loaders work. `/\.json$/` will match any file ending in '.json', for example. - * @param {Function} parse - Function to parse the file contents. Takes 1 argument, which is an object with 2 attributes: contents wich is the string of the file contents and filePath. The function should return a plain Javascript object. - * @returns {module:style-dictionary} - * @example - * ```js - * StyleDictionary.registerParser({ - * pattern: /\.json$/, - * parse: ({contents, filePath}) => { - * return JSON.parse(contents); - * } - * }) - * ``` - */ -export default function registerParser(options) { - if (!(options.pattern instanceof RegExp)) - throw new Error(`Can't register parser; parser.pattern must be a regular expression`); - if (typeof options.parse !== 'function') - throw new Error("Can't register parser; parser.parse must be a function"); - - this.parsers.push(options); - - return this; -} diff --git a/lib/register/preprocessor.js b/lib/register/preprocessor.js deleted file mode 100644 index 828a15d14..000000000 --- a/lib/register/preprocessor.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -/** - * @typedef {import('../../types/Preprocessor').Preprocessor} Preprocessor - */ - -/** - * Adds a custom preprocessor to preprocess the parsed dictionary, before transforming individual tokens. - * @static - * @memberof module:style-dictionary - * @param {Preprocessor} cfg - Preprocessor object - * @returns {module:style-dictionary} - * @example - * ```js - * StyleDictionary.registerPreprocessor((dictionary) => { - * return dictionary; - * }); - * ``` - */ -export default function registerPreprocessor(cfg) { - const errorPrefix = 'Cannot register preprocessor;'; - if (typeof cfg.name !== 'string') - throw new Error(`${errorPrefix} Preprocessor.name must be a string`); - if (!(cfg.preprocessor instanceof Function)) { - throw new Error(`${errorPrefix} Preprocessor.preprocessor must be a function`); - } - this.preprocessors[cfg.name] = cfg.preprocessor; - return this; -} diff --git a/lib/register/transform.js b/lib/register/transform.js deleted file mode 100644 index 20a48babf..000000000 --- a/lib/register/transform.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -const transformTypes = ['name', 'value', 'attribute']; - -/** - * Add a custom transform to the Style Dictionary - * Transforms can manipulate a token's name, value, or attributes - * - * @static - * @name registerTransform - * @memberof module:style-dictionary - * @function - * @param {Object} transform - Transform object - * @param {String} transform.type - Type of transform, can be: name, attribute, or value - * @param {String} transform.name - Name of the transformer (used by transformGroup to call a list of transforms). - * @param {Boolean} transform.transitive - If the value transform should be applied transitively, i.e. should be applied to referenced values as well as absolute values. - * @param {Function} [transform.matcher] - Matcher function, return boolean if transform should be applied. If you omit the matcher function, it will match all tokens. - * @param {Function} transform.transformer Modifies a design token object. The transformer function will receive the token and the platform configuration as its arguments. The transformer function should return a string for name transforms, an object for attribute transforms, and same type of value for a value transform. - * @returns {module:style-dictionary} - * @example - * ```js - * StyleDictionary.registerTransform({ - * name: 'time/seconds', - * type: 'value', - * matcher: function(token) { - * return token.attributes.category === 'time'; - * }, - * transformer: function(token) { - * // Note the use of prop.original.value, - * // before any transforms are performed, the build system - * // clones the original token to the 'original' attribute. - * return (parseInt(token.original.value) / 1000).toString() + 's'; - * } - * }); - * ``` - */ -export default function registerTransform(options) { - if (typeof options.type !== 'string') throw new Error('type must be a string'); - if (transformTypes.indexOf(options.type) < 0) - throw new Error(options.type + ' type is not one of: ' + transformTypes.join(', ')); - if (typeof options.name !== 'string') throw new Error('name must be a string'); - if (options.matcher && typeof options.matcher !== 'function') - throw new Error('matcher must be a function'); - if (typeof options.transformer !== 'function') throw new Error('transformer must be a function'); - - const transform = { - type: options.type, - matcher: options.matcher, - transitive: !!options.transitive, - transformer: options.transformer, - }; - - this.transform[options.name] = transform; - return this; -} diff --git a/lib/register/transformGroup.js b/lib/register/transformGroup.js deleted file mode 100644 index beee04b56..000000000 --- a/lib/register/transformGroup.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -/** - * Add a custom transformGroup to the Style Dictionary, which is a - * group of transforms. - * @static - * @memberof module:style-dictionary - * @param {Object} transformGroup - * @param {String} transformGroup.name - Name of the transform group that will be referenced in config.json - * @param {String[]} transformGroup.transforms - Array of strings that reference the name of transforms to be applied in order. Transforms must be defined and match the name or there will be an error at build time. - * @returns {module:style-dictionary} - * @example - * ```js - * StyleDictionary.registerTransformGroup({ - * name: 'Swift', - * transforms: [ - * 'attribute/cti', - * 'size/pt', - * 'name/cti' - * ] - * }); - * ``` - */ -export default function registerTransformGroup(options) { - if (typeof options.name !== 'string') throw new Error('transform name must be a string'); - if (!Array.isArray(options.transforms)) - throw new Error('transforms must be an array of registered value transforms'); - - options.transforms.forEach( - function (t) { - if (!(t in this.transform)) - throw new Error('transforms must be an array of registered value transforms'); - }.bind(this), - ); - - this.transformGroup[options.name] = options.transforms; - return this; -} diff --git a/lib/transform/config.js b/lib/transform/config.js index ab53dd4a3..6a107882d 100644 --- a/lib/transform/config.js +++ b/lib/transform/config.js @@ -11,10 +11,18 @@ * and limitations under the License. */ -import { isPlainObject } from 'is-plain-object'; +import isPlainObject from 'is-plain-obj'; import deepExtend from '../utils/deepExtend.js'; import GroupMessages from '../utils/groupMessages.js'; +/** + * @typedef {import('../StyleDictionary.js').default} StyleDictionary + * @typedef {import('../../types/Transform.d.ts').Transform} Transform + * @typedef {import('../../types/File.d.ts').File} File + * @typedef {import('../../types/Filter.d.ts').Matcher} Matcher + * @typedef {import('../../types/Config.d.ts').PlatformConfig} PlatformConfig + */ + const MISSING_TRANSFORM_ERRORS = GroupMessages.GROUP.MissingRegisterTransformErrors; /** @@ -22,10 +30,10 @@ const MISSING_TRANSFORM_ERRORS = GroupMessages.GROUP.MissingRegisterTransformErr * that has filters, transforms, formats, and actions * mapped properly. * @private - * @param {Object} platformConfig - * @param {Object} dictionary - * @param {Object} platformName (only used for error messaging) - * @returns {Object} + * @param {PlatformConfig} platformConfig + * @param {StyleDictionary} dictionary + * @param {string} platformName (only used for error messaging) + * @returns {PlatformConfig} */ export default function transformConfig(platformConfig, dictionary, platformName) { const to_ret = { ...platformConfig }; // structuredClone not suitable due to config being able to contain Function() etc. @@ -36,9 +44,11 @@ export default function transformConfig(platformConfig, dictionary, platformName // it will throw an error to make the user aware that the transformGroup doesn't // exist. A valid case is if the user defines neither, no transforms will be // applied. + /** @type {string[]} */ let transforms = []; if (to_ret.transforms) { - transforms = to_ret.transforms; + // typecast because at this point, transforms are still strings without functions + transforms = /** @type {string[]} */ (to_ret.transforms); } else if (to_ret.transformGroup) { if (dictionary.transformGroup[to_ret.transformGroup]) { transforms = dictionary.transformGroup[to_ret.transformGroup]; @@ -82,8 +92,8 @@ None of ${transform_warnings} match the name of a registered transform. } // Apply registered fileHeaders onto the platform options - if (platformConfig.options && platformConfig.options.fileHeader) { - const fileHeader = platformConfig.options.fileHeader; + if (to_ret.options?.fileHeader) { + const fileHeader = to_ret.options.fileHeader; if (typeof fileHeader === 'string') { if (dictionary.fileHeader[fileHeader]) { to_ret.options.fileHeader = dictionary.fileHeader[fileHeader]; @@ -97,8 +107,8 @@ None of ${transform_warnings} match the name of a registered transform. } } - to_ret.files = (platformConfig.files || []).map(function (file) { - const ext = { options: {} }; + to_ret.files = (to_ret.files || []).map(function (file) { + const ext = /** @type {File} */ ({ options: {} }); if (file.options && file.options.fileHeader) { const fileHeader = file.options.fileHeader; if (typeof fileHeader === 'string') { @@ -122,9 +132,14 @@ None of ${transform_warnings} match the name of a registered transform. throw new Error("Can't find filter: " + file.filter); } } else if (typeof file.filter === 'object') { - // Recursively go over the object keys of filter object and - // return a filter Function that filters tokens - // by the specified object keys. + /** + * Recursively go over the object keys of filter object and + * return a filter Function that filters tokens + * by the specified object keys. + * @param {any} inputObj + * @param {any} testObj + * @returns {boolean} + */ const matchFn = function (inputObj, testObj) { if (isPlainObject(testObj)) { return Object.keys(testObj).every((key) => matchFn(inputObj[key], testObj[key])); @@ -132,9 +147,14 @@ None of ${transform_warnings} match the name of a registered transform. return inputObj == testObj; } }; + + /** + * @param {{[key: string]: unknown}} matchObj + */ const matches = function (matchObj) { let cloneObj = { ...matchObj }; // shallow clone, structuredClone not suitable because obj can contain "Function()" - let matchesFn = (inputObj) => matchFn(inputObj, cloneObj); + let matchesFn = /** @param {{[key: string]: unknown}} inputObj */ (inputObj) => + matchFn(inputObj, cloneObj); return matchesFn; }; ext.filter = matches(file.filter); @@ -146,23 +166,35 @@ None of ${transform_warnings} match the name of a registered transform. } if (file.format) { - if (dictionary.format[file.format]) { - ext.format = dictionary.format[file.format]; + /** + * We know at this point it should be a string + * Only later will it be transformed to contain the formatter function + */ + const format = /** @type {string} */ (file.format); + if (dictionary.format[format]) { + ext.format = dictionary.format[format]; } else { - throw new Error("Can't find format: " + file.format); + throw new Error("Can't find format: " + format); } } else { throw new Error('Please supply a format for file: ' + JSON.stringify(file)); } - return deepExtend([{}, file, ext]); - }); - to_ret.actions = (platformConfig.actions || []).map(function (action) { - if (typeof dictionary.action[action].undo !== 'function') { - console.warn(action + ' action does not have a clean function!'); - } - return dictionary.action[action]; + // destination is a required prop so we have to prefill it here, or it breaks return type + const extended = deepExtend([{ destination: '' }, file, ext]); + + return extended; }); + const actions = /** @type {string[]|undefined} */ (to_ret.actions) || []; + 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!'); + } + return dictionary.action[action]; + }, + ); + return to_ret; } diff --git a/lib/transform/object.js b/lib/transform/object.js index 641fe606f..3ebb041c5 100644 --- a/lib/transform/object.js +++ b/lib/transform/object.js @@ -11,12 +11,20 @@ * and limitations under the License. */ -import { isPlainObject } from 'is-plain-object'; -import usesValueReference from '../utils/references/usesReference.js'; +import isPlainObject from 'is-plain-obj'; +import usesValueReference from '../utils/references/usesReferences.js'; import getName from '../utils/references/getName.js'; import transformToken from './token.js'; import tokenSetup from './tokenSetup.js'; +/** + * @typedef {import('../../types/DesignToken.d.ts').DesignTokens} Tokens + * @typedef {import('../../types/DesignToken.d.ts').TransformedTokens} TransformedTokens + * @typedef {import('../../types/DesignToken.d.ts').DesignToken} Token + * @typedef {import('../../types/DesignToken.d.ts').TransformedToken} TransformedToken + * @typedef {import('../../types/Config.d.ts').PlatformConfig} PlatformConfig + */ + /** * Applies transforms on all tokens. This * does not happen inline, rather it is functional @@ -24,23 +32,20 @@ import tokenSetup from './tokenSetup.js'; * we can perform transforms for different platforms * on the same style dictionary. * @private - * @param {Object} obj - * @param {Object} options - * @param {Object} [ctx={}] - * @param {Array} [path=[]] - * @param {Object} [transformedObj={}] - * @returns {Object} + * @param {Tokens|TransformedTokens} obj + * @param {PlatformConfig} options + * @param {{ transformedPropRefs?: string[], deferredPropValueTransforms?: string[] }} [ctx] + * @param {string[]} [path] + * @param {Record} [transformedObj] + * @returns {Tokens|TransformedTokens} */ export default function transformObject( obj, options, { transformedPropRefs = [], deferredPropValueTransforms = [] } = {}, - path, - transformedObj, + path = [], + transformedObj = {}, ) { - transformedObj = transformedObj || {}; - path = path || []; - for (const name in obj) { if (!obj.hasOwnProperty(name)) { continue; @@ -62,14 +67,14 @@ export default function transformObject( // If the property is already transformed, just pass assign it to the // transformed object and move on. if (alreadyTransformed) { - transformedObj[name] = objProp; + transformedObj[name] = /** @type {Token|TransformedToken} */ (objProp); path.pop(); continue; } // Note: tokenSetup won't re-run if property has already been setup // it is safe to run this multiple times on the same property. - const setupProperty = tokenSetup(objProp, name, path); + const setupProperty = tokenSetup(/** @type {Token|TransformedToken} */ (objProp), name, path); // If property has a reference, defer its transformations until later if (usesValueReference(setupProperty.value, options)) { diff --git a/lib/transform/token.js b/lib/transform/token.js index 4a107469e..65fed1a71 100644 --- a/lib/transform/token.js +++ b/lib/transform/token.js @@ -11,31 +11,44 @@ * and limitations under the License. */ -import usesReference from '../utils/references/usesReference.js'; +import usesReferences from '../utils/references/usesReferences.js'; + +/** + * @typedef {import('../../types/DesignToken.d.ts').TransformedToken} Token + * @typedef {import('../../types/Config.d.ts').PlatformConfig} PlatformConfig + * @typedef {import('../../types/Transform.d.ts').Transform} Transform + * @typedef {import('../../types/Transform.d.ts').NameTransform} NameTransform + */ /** * Applies all transforms to a property. This is a pure function, * it returns a new property object rather than mutating it inline. * @private - * @param {Object} property - * @param {Object} options - * @returns {Object} - A new property object with transforms applied. + * @param {Token} property + * @param {PlatformConfig} options + * @returns {Token} - A new property object with transforms applied. */ export default function transformProperty(property, options) { const to_ret = structuredClone(property); - const transforms = options.transforms; + + const transforms = /** @type {Omit[]} */ (options.transforms) || []; for (let i = 0; i < transforms.length; i++) { const transform = transforms[i]; if (!transform.matcher || transform.matcher(to_ret)) { - if (transform.type === 'name') to_ret.name = transform.transformer(to_ret, options); + if (transform.type === 'name') { + to_ret.name = /** @type {Omit} */ (transform).transformer( + to_ret, + options, + ); + } // Don't try to transform the value if it is referencing another value // Only try to transform if the value is not a string or if it has '{}' - if (transform.type === 'value' && !usesReference(property.value, options)) { + if (transform.type === 'value' && !usesReferences(property.value, options)) { // Only transform non-referenced values (from original) // and transitive transforms if the value has been resolved - if (!usesReference(property.original.value, options) || transform.transitive) { + if (!usesReferences(property.original.value, options) || transform.transitive) { to_ret.value = transform.transformer(to_ret, options); } } diff --git a/lib/transform/tokenSetup.js b/lib/transform/tokenSetup.js index 178a43699..6f93c0dfb 100644 --- a/lib/transform/tokenSetup.js +++ b/lib/transform/tokenSetup.js @@ -11,8 +11,12 @@ * and limitations under the License. */ -import { isPlainObject } from 'is-plain-object'; -import deepExtend from '../utils/deepExtend.js'; +import isPlainObject from 'is-plain-obj'; + +/** + * @typedef {import('../../types/DesignToken.d.ts').DesignToken} Token + * @typedef {import('../../types/DesignToken.d.ts').TransformedToken} TransformedToken + */ /** * Takes a token object, a leaf node in a tokens object, and @@ -20,10 +24,10 @@ import deepExtend from '../utils/deepExtend.js'; * original object for safekeeping, adds a name, adds an attributes object, * and a path array. * @private - * @param {Object} token - the token object to setup - * @param {String} name - The name of the token, which will should be its key in the object. - * @param {Array} path - The path of keys to get to the token from the top level of the tokens object. - * @returns {Object} - A new object that is setup and ready to go. + * @param {Token|TransformedToken} token - the token object to setup + * @param {string} name - The name of the token, which will should be its key in the object. + * @param {string[]} path - The path of keys to get to the token from the top level of the tokens object. + * @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'); @@ -37,8 +41,7 @@ export default function tokenSetup(token, name, path) { // Initial token setup // Keep the original object tokens like it was in file (whitout additional data) // so we can key off them in the transforms - to_ret = deepExtend([{}, token]); - let to_ret_original = deepExtend([{}, token]); + let to_ret_original = structuredClone(token); delete to_ret_original.filePath; delete to_ret_original.isSource; @@ -51,6 +54,6 @@ export default function tokenSetup(token, name, path) { // like color_font_base to_ret.path = structuredClone(path); } - - return to_ret; + // now the token is for sure transformed and contains path, attributes, name and original props + return /** @type {TransformedToken} */ (to_ret); } diff --git a/lib/utils/combineJSON.js b/lib/utils/combineJSON.js index 00a883880..f47dccc9c 100644 --- a/lib/utils/combineJSON.js +++ b/lib/utils/combineJSON.js @@ -17,6 +17,16 @@ import path from '@bundled-es-modules/path-browserify'; import { fs } from 'style-dictionary/fs'; import deepExtend from './deepExtend.js'; +/** + * @typedef {import('../../types/DesignToken.d.ts').DesignTokens} Tokens + * @typedef {import('../../types/DesignToken.d.ts').DesignToken} Token + * @typedef {import('../../types/Parser.d.ts').Parser} Parser + */ + +/** + * @param {Tokens} obj + * @param {(obj: Tokens|Token, key: keyof Tokens|Token, slice: Tokens|Token) => void} fn + */ function traverseObj(obj, fn) { for (let key in obj) { fn.apply(null, [obj, key, obj[key]]); @@ -30,15 +40,23 @@ function traverseObj(obj, fn) { * Takes an array of json files and merges * them together. Optionally does a deep extend. * @private - * @param {String[]} arr - Array of paths to json (or node modules that export objects) files + * @param {string[]} arr - Array of paths to json (or node modules that export objects) files * @param {Boolean} [deep=false] - If it should perform a deep merge - * @param {Function} collision - A function to be called when a name collision happens that isn't a normal deep merge of objects - * @param {Boolean} [source=true] - If json files are "sources", tag tokens - * @param {Object[]} [parsers=[]] - Custom file parsers - * @returns {Object} + * @param {Function} [collision] - A function to be called when a name collision happens that isn't a normal deep merge of objects + * @param {boolean} [source] - If json files are "sources", tag tokens + * @param {Parser[]} [parsers] - Custom file parsers + * @returns {Promise} */ -export default async function combineJSON(arr, deep, collision, source, parsers = []) { +export default async function combineJSON( + arr, + deep = false, + collision, + source = true, + parsers = [], +) { + /** @type {Tokens} */ const to_ret = {}; + /** @type {string[]} */ let files = []; for (let i = 0; i < arr.length; i++) { @@ -58,7 +76,7 @@ export default async function combineJSON(arr, deep, collision, source, parsers parsers.forEach(({ pattern, parse }) => { if (filePath.match(pattern)) { file_content = parse({ - contents: fs.readFileSync(filePath, { encoding: 'UTF-8' }), + contents: /** @type {string} */ (fs.readFileSync(filePath, { encoding: 'utf-8' })), filePath, }); } @@ -66,37 +84,38 @@ export default async function combineJSON(arr, deep, collision, source, parsers // If there is no file_content then no custom parser ran on that file if (!file_content) { - let parsedFile; if (['.js', '.mjs'].includes(path.extname(filePath))) { const fileToImport = path.resolve( typeof window === 'object' ? '' : process.cwd(), filePath, ); - parsedFile = (await import(fileToImport)).default; + file_content = (await import(fileToImport)).default; } else { - parsedFile = JSON5.parse(fs.readFileSync(filePath)); + file_content = JSON5.parse(/** @type {string} */ (fs.readFileSync(filePath, 'utf-8'))); } - - file_content = deepExtend([file_content, parsedFile]); } } catch (e) { - e.message = 'Failed to load or parse JSON or JS Object: ' + e.message; - throw e; + if (e instanceof Error) { + e.message = 'Failed to load or parse JSON or JS Object: ' + e.message; + throw e; + } } - // Add some side data on each property to make filtering easier - traverseObj(file_content, (obj) => { - if (obj.hasOwnProperty('value') && !obj.filePath) { - obj.filePath = filePath; + if (file_content) { + // Add some side data on each property to make filtering easier + traverseObj(file_content, (obj) => { + if (obj.hasOwnProperty('value') && !obj.filePath) { + obj.filePath = filePath; - obj.isSource = source || source === undefined ? true : false; - } - }); + obj.isSource = source; + } + }); - if (deep) { - deepExtend([to_ret, file_content], collision); - } else { - Object.assign(to_ret, file_content); + if (deep) { + deepExtend([to_ret, file_content], collision); + } else { + Object.assign(to_ret, file_content); + } } } diff --git a/lib/utils/convertToBase64.js b/lib/utils/convertToBase64.js index cc2ec5014..67c09c81a 100644 --- a/lib/utils/convertToBase64.js +++ b/lib/utils/convertToBase64.js @@ -22,6 +22,6 @@ import { fs } from 'style-dictionary/fs'; export default function convertToBase64(filePath) { if (typeof filePath !== 'string') throw new Error('filePath name must be a string'); - const body = fs.readFileSync(filePath); + const body = /** @type {string} */ (fs.readFileSync(filePath, 'utf-8')); return btoa(body); } diff --git a/lib/utils/createDictionary.js b/lib/utils/createDictionary.js index 0bac703a8..a9b7e30a5 100644 --- a/lib/utils/createDictionary.js +++ b/lib/utils/createDictionary.js @@ -14,19 +14,14 @@ import flattenTokens from './flattenTokens.js'; /** - * - * @typedef Dictionary - * @property {Object} $tokens - * @property {Array} allTokens + * @typedef {import('../../types/DesignToken.d.ts').TransformedTokens} Tokens */ /** * Creates the dictionary object that is passed to formats and actions. - * @param {Object} args - * @param {Object} args.tokens - * @returns {Dictionary} + * @param {Tokens} tokens */ -export default function createDictionary({ tokens }) { +export default function createDictionary(tokens) { const allTokens = flattenTokens(tokens); return { tokens, diff --git a/lib/utils/createFormatArgs.js b/lib/utils/createFormatArgs.js index 8522afa1a..e4580200e 100644 --- a/lib/utils/createFormatArgs.js +++ b/lib/utils/createFormatArgs.js @@ -13,12 +13,31 @@ import deepExtend from './deepExtend.js'; -export default function createFormatArgs({ dictionary, platform, file = {} }) { +/** + * @typedef {import('../../types/DesignToken.js').Dictionary} Dictionary + * @typedef {import('../../types/Config.d.ts').PlatformConfig} PlatformConfig + * @typedef {import('../../types/File.d.ts').File} File + * + +/** + * + * @param {{ + * dictionary: Dictionary; + * platform: PlatformConfig; + * file: File; + * }} param0 + * @returns + */ +export default function createFormatArgs({ dictionary, platform, file }) { const { allTokens, tokens } = dictionary; // This will merge platform and file-level configuration // where the file configuration takes precedence const { options } = platform; - file = deepExtend([{}, { options }, file]); + const fileOptsTakenFromPlatform = /** @type {Partial} */ ({ options }); + + // we have to do some typecasting here. We assume that because deepExtends merges objects together, and "file" + // always has the destination prop, then result will be File rather than Partial, so we just typecast it. + file = /** @type {File} */ (deepExtend([{}, fileOptsTakenFromPlatform, file])); return { dictionary, diff --git a/lib/utils/deepExtend.js b/lib/utils/deepExtend.js index bd15ac809..b24deddfe 100644 --- a/lib/utils/deepExtend.js +++ b/lib/utils/deepExtend.js @@ -11,20 +11,34 @@ * and limitations under the License. */ -import { isPlainObject } from 'is-plain-object'; +import isPlainObject from 'is-plain-obj'; + +/** + * @typedef {import('../../types/DesignToken.d.ts').DesignTokens} Tokens + * @typedef {import('../../types/DesignToken.d.ts').TransformedTokens} TransformedTokens + */ /** * TODO: see if we can use deepmerge instead of maintaining our own utility + * Main reason for having our own is that we have a collision function that warns users + * when props from different objects collide, e.g. multiple token files colliding on the same token name + * https://github.com/TehShrike/deepmerge/issues/262 created a feature request * * Performs an deep extend on the objects, from right to left. * @private - * @param {Object[]} objects - An array of JS objects - * @param {Function} collision - A function to be called when a merge collision happens. - * @param {string[]} path - (for internal use) An array of strings which is the current path down the object when this is called recursively. - * @returns {Object} + * @template {Object} T - Generic type T extends from "Object", to be maximally permissive + * @param {Array} objects - An array of JS objects + * @param {Function} [collision] - A function to be called when a merge collision happens. + * @param {string[]} [path] - (for internal use) An array of strings which is the current path down the object when this is called recursively. + * @returns {T} */ export default function deepExtend(objects, collision, path) { - if (objects == null) return {}; + // typecast it to T, to circumvent error thrown because technically, + // T could be instantiated with a type that has more limited constraints than Obj + // but this isn't actually a problem for us since we are only merging objects together.. + const defaultVal = /** @type {T} */ ({}); + + if (objects == null) return defaultVal; let target = objects[0] || {}; @@ -32,7 +46,7 @@ export default function deepExtend(objects, collision, path) { // Handle case when target is a string or something (possible in deep copy) if (typeof target !== 'object') { - target = {}; + target = defaultVal; } for (let i = 1; i < objects.length; i++) { @@ -56,6 +70,7 @@ export default function deepExtend(objects, collision, path) { let copyIsArray = Array.isArray(copy); // Recurse if we're merging plain objects or arrays if (copy && (isPlainObject(copy) || copyIsArray)) { + /** @type {Tokens|TransformedTokens|any} */ let clone; if (copyIsArray) { copyIsArray = false; @@ -73,7 +88,7 @@ export default function deepExtend(objects, collision, path) { // Don't bring in undefined values } else if (copy !== undefined) { if (src != null && typeof collision == 'function') { - collision({ target: target, copy: options, path: path, key: name }); + collision({ target, copy: options, path, key: name }); } target[name] = copy; } diff --git a/lib/utils/deepmerge.js b/lib/utils/deepmerge.js index 5520b7937..276749db4 100644 --- a/lib/utils/deepmerge.js +++ b/lib/utils/deepmerge.js @@ -1,5 +1,5 @@ import _deepmerge from '@bundled-es-modules/deepmerge'; -import { isPlainObject } from 'is-plain-object'; +import isPlainObject from 'is-plain-obj'; /** * Wrapper around deepmerge that merges only plain objects and arrays @@ -8,7 +8,16 @@ import { isPlainObject } from 'is-plain-object'; */ export const deepmerge = (target, source) => { return _deepmerge(target, source, { - // Merge if object is array or a plain object (so not merging functions/class instances together) + /** + * Merge if object is array or a plain object (so not merging functions/class instances together) + * @param {Object} obj + */ isMergeableObject: (obj) => Array.isArray(obj) || isPlainObject(obj), + /** + * Combine arrays but remove duplicate primitives (e.g. no duplicate transforms) + * @param {Array} target + * @param {Array} source + */ + arrayMerge: (target, source) => Array.from(new Set([...target, ...source])), }); }; diff --git a/lib/utils/flattenTokens.js b/lib/utils/flattenTokens.js index d7b64d9b2..344a36997 100644 --- a/lib/utils/flattenTokens.js +++ b/lib/utils/flattenTokens.js @@ -11,30 +11,32 @@ * and limitations under the License. */ -import { isPlainObject } from 'is-plain-object'; +import isPlainObject from 'is-plain-obj'; + +/** + * @typedef {import('../../types/DesignToken.d.ts').TransformedTokens} Tokens + * @typedef {import('../../types/DesignToken.d.ts').TransformedToken} Token + */ /** * Takes an plain javascript object and will make a flat array of all the leaf nodes. * A leaf node in this context has a 'value' property. Potentially refactor this to * be more generic. * @private - * @param {Object} tokens - The plain object you want flattened into an array. - * @param {Array} [to_ret=[]] - Tokens array. This function is recursive therefore this is what gets passed along. - * @return {Array} + * @param {Tokens} tokens - The plain object you want flattened into an array. + * @param {Token[]} [to_ret] - Tokens array. This function is recursive therefore this is what gets passed along. + * @return {Token[]} */ -export default function flattenTokens(tokens, to_ret) { - to_ret = to_ret || []; - - for (var name in tokens) { +export default function flattenTokens(tokens, to_ret = []) { + for (let name in tokens) { if (tokens.hasOwnProperty(name)) { // TODO: this is a bit fragile and arbitrary to stop when we get to a 'value' property. if (isPlainObject(tokens[name]) && 'value' in tokens[name]) { - to_ret.push(tokens[name]); + to_ret.push(/** @type {Token} */ (tokens[name])); } else if (isPlainObject(tokens[name])) { flattenTokens(tokens[name], to_ret); } } } - return to_ret; } diff --git a/lib/utils/groupMessages.js b/lib/utils/groupMessages.js index 963d3b75c..818c6c85a 100644 --- a/lib/utils/groupMessages.js +++ b/lib/utils/groupMessages.js @@ -13,6 +13,7 @@ class GroupMessages { constructor() { + /** @type {{[key: string]: string[]}} */ this.groupedMessages = {}; this.GROUP = { PropertyReferenceWarnings: 'Property Reference Errors', @@ -26,12 +27,21 @@ class GroupMessages { }; } + /** + * + * @param {string} messageGroup + * @returns {string[]} + */ flush(messageGroup) { const messages = this.fetchMessages(messageGroup); this.clear(messageGroup); return messages; } + /** + * @param {string} messageGroup + * @param {string} message + */ add(messageGroup, message) { if (messageGroup) { if (!this.groupedMessages[messageGroup]) { @@ -43,14 +53,27 @@ class GroupMessages { } } + /** + * + * @param {string} messageGroup + * @returns {number} + */ count(messageGroup) { return this.groupedMessages[messageGroup] ? this.groupedMessages[messageGroup].length : 0; } + /** + * + * @param {string} messageGroup + * @returns {string[]} + */ fetchMessages(messageGroup) { return (messageGroup && this.groupedMessages[messageGroup]) || []; } + /** + * @param {string} messageGroup + */ clear(messageGroup) { messageGroup && this.groupedMessages[messageGroup] && delete this.groupedMessages[messageGroup]; } diff --git a/lib/utils/index.js b/lib/utils/index.js index 478779cd6..3be3ceafa 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -11,9 +11,9 @@ * and limitations under the License. */ -import usesReference from './references/usesReference.js'; +import usesReferences from './references/usesReferences.js'; import getReferences from './references/getReferences.js'; import { resolveReferences } from './references/resolveReferences.js'; // Public style-dictionary/utils API -export { usesReference, getReferences, resolveReferences }; +export { usesReferences, getReferences, resolveReferences }; diff --git a/lib/utils/preprocess.js b/lib/utils/preprocess.js index 7feb182d9..5b4d2809f 100644 --- a/lib/utils/preprocess.js +++ b/lib/utils/preprocess.js @@ -15,11 +15,12 @@ * Run all registered preprocessors on the dictionary, * returning the preprocessed dictionary in each step. * - * @typedef {import('../../types/DesignToken').DesignTokens} DesignTokens - * @typedef {import('../../types/Preprocessor').Preprocessor} Preprocessor + * @typedef {import('../../types/DesignToken.d.ts').DesignTokens} DesignTokens + * @typedef {import('../../types/Preprocessor.d.ts').Preprocessor} Preprocessor + * @typedef {import('../../types/Preprocessor.d.ts').preprocessor} preprocessor * * @param {DesignTokens} tokens - * @param {Preprocessor[]} preprocessorObj + * @param {Record} [preprocessorObj] * @returns {Promise} */ export async function preprocess(tokens, preprocessorObj = {}) { diff --git a/lib/utils/references/createReferenceRegex.js b/lib/utils/references/createReferenceRegex.js index 37b956898..fc0a7ac8e 100644 --- a/lib/utils/references/createReferenceRegex.js +++ b/lib/utils/references/createReferenceRegex.js @@ -13,8 +13,13 @@ import defaults from './defaults.js'; +/** + * @typedef {import('../../../types/Config.d.ts').RegexOptions} RegexOptions + * @param {RegexOptions} opts + * @returns {RegExp} + */ export default function createReferenceRegex(opts = {}) { - const options = Object.assign({}, defaults, opts); + const options = { ...defaults, ...opts }; return new RegExp( '\\' + diff --git a/lib/utils/references/defaults.js b/lib/utils/references/defaults.js index 64e55b534..4feefd24c 100644 --- a/lib/utils/references/defaults.js +++ b/lib/utils/references/defaults.js @@ -11,6 +11,9 @@ * and limitations under the License. */ +/** + * @type {Omit, "regex">} + */ export default { opening_character: '{', closing_character: '}', diff --git a/lib/utils/references/getName.js b/lib/utils/references/getName.js index a4b14b7ac..4278a35c3 100644 --- a/lib/utils/references/getName.js +++ b/lib/utils/references/getName.js @@ -16,9 +16,11 @@ import defaults from './defaults.js'; /** * Returns the paths name be joining its parts with a given separator. * + * @typedef {import('../../../types/Config.d.ts').RegexOptions} RegexOptions + * * @private - * @param {Array} path - * @param {Object} opts + * @param {string[]} path + * @param {RegexOptions} [opts] * @returns {string} - The paths name */ export default function getName(path, opts = {}) { diff --git a/lib/utils/references/getPathFromName.js b/lib/utils/references/getPathFromName.js index fe3d6b4fd..38afbdc28 100644 --- a/lib/utils/references/getPathFromName.js +++ b/lib/utils/references/getPathFromName.js @@ -15,16 +15,15 @@ import defaults from './defaults.js'; /** * Returns the path from a path name be splitting the name by a given separator. - * * @private * @param {string} pathName - * @param {Object} opts - * @returns {Array} - The path + * @param {string} separator + * @returns {string[]} - The path */ -export default function getPathFromName(pathName, opts = {}) { - const options = Object.assign({}, defaults, opts); +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'); } - return pathName.split(options.separator); + return pathName.split(sep); } diff --git a/lib/utils/references/getReferences.js b/lib/utils/references/getReferences.js index 218cb9fd8..a37b50a67 100644 --- a/lib/utils/references/getReferences.js +++ b/lib/utils/references/getReferences.js @@ -11,10 +11,11 @@ * and limitations under the License. */ -import getPath from './getPathFromName.js'; +import getPathFromName from './getPathFromName.js'; import createReferenceRegex from './createReferenceRegex.js'; import getValueByPath from './getValueByPath.js'; import GroupMessages from '../groupMessages.js'; +import defaults from './defaults.js'; /** * This is a helper function that is added to the dictionary object that @@ -25,42 +26,55 @@ import GroupMessages from '../groupMessages.js'; * ```css * --color-background-base: var(--color-core-white); * ``` + * @typedef {import('../../../types/Config.d.ts').RegexOptions} RegexOptions + * @typedef {import('../../StyleDictionary.js').default} Dictionary + * @typedef {import('../../../types/DesignToken.d.ts').TransformedTokens} Tokens + * @typedef {import('../../../types/DesignToken.d.ts').TransformedToken} Token * * @memberof Dictionary - * @param {Object} dictionary the dictionary to search in - * @param {string} value the value that contains a reference - * @param {object[]} references array of token's references because a token's value can contain multiple references due to string interpolation - * @returns {any} + * @param {string|Object} value the value that contains a reference + * @param {Tokens} tokens the dictionary to search in + * @param {RegexOptions & { unfilteredTokens?: Tokens }} [opts] + * @param {Token[]} [references] array of token's references because a token's value can contain multiple references due to string interpolation + * @returns {Token[]} */ -export default function getReferences(dictionary, value, references = []) { - const regex = createReferenceRegex({}); +export default function getReferences(value, tokens, opts = {}, references = []) { + const regex = createReferenceRegex(opts); - // this will update the references array with the referenced tokens it finds. + /** + * this will update the references array with the referenced tokens it finds. + * @param {string} match + * @param {string} variable + */ function findReference(match, variable) { // remove 'value' to access the whole token object variable = variable.trim().replace('.value', ''); // Find what the value is referencing - const pathName = getPath(variable); + const pathName = getPathFromName(variable, opts.separator ?? defaults.separator); - let ref = getValueByPath(pathName, dictionary.tokens); + let ref = getValueByPath(pathName, tokens); - if (!ref) { - // fall back on _tokens as it is unfiltered - ref = getValueByPath(pathName, dictionary._tokens); + if (!ref && opts.unfilteredTokens) { + // fall back on unfilteredTokens as it is unfiltered + ref = getValueByPath(pathName, opts.unfilteredTokens); // and warn the user about this GroupMessages.add(GroupMessages.GROUP.FilteredOutputReferences, variable); } - references.push(ref); + if (ref !== undefined) { + references.push(ref); + } + return ''; } if (typeof value === 'string') { // function inside .replace runs multiple times if there are multiple matches + // TODO: we don't need the replace's return value, considering using something else here value.replace(regex, findReference); } // If the token's value is an object, run the replace reference - // on each value within that object. This mirrors the `usesReference` + // on each value within that object. This mirrors the `usesReferences` // function which iterates over the object to see if there is a reference if (typeof value === 'object') { for (const key in value) { @@ -69,7 +83,7 @@ export default function getReferences(dictionary, value, references = []) { } // if it is an object, we go further down the rabbit hole if (value.hasOwnProperty(key) && typeof value[key] === 'object') { - getReferences(dictionary, value[key], references); + getReferences(value[key], tokens, opts, references); } } } diff --git a/lib/utils/references/getValueByPath.js b/lib/utils/references/getValueByPath.js index 4640dc4c2..f51c70ed1 100644 --- a/lib/utils/references/getValueByPath.js +++ b/lib/utils/references/getValueByPath.js @@ -11,8 +11,15 @@ * and limitations under the License. */ -export default function getValueByPath(path, obj) { - let ref = obj; +/** + * @typedef {import('../../../types/DesignToken.d.ts').TransformedTokens} Tokens + * @typedef {import('../../../types/DesignToken.d.ts').TransformedToken} Token + * @param {string[]} path + * @param {Tokens} tokensObj + * @returns {Token|undefined} + */ +export default function getValueByPath(path, tokensObj) { + let ref = tokensObj; if (!Array.isArray(path)) { return; @@ -24,10 +31,8 @@ export default function getValueByPath(path, obj) { ref = ref[path[i]]; } else { // set the reference as undefined if we don't find anything - ref = undefined; - break; + return undefined; } } - - return ref; + return /** @type {Token} */ (ref); } diff --git a/lib/utils/references/resolveReferences.js b/lib/utils/references/resolveReferences.js index 95c51dcd4..47e98c3eb 100644 --- a/lib/utils/references/resolveReferences.js +++ b/lib/utils/references/resolveReferences.js @@ -15,37 +15,56 @@ import GroupMessages from '../groupMessages.js'; import getPathFromName from './getPathFromName.js'; import getName from './getName.js'; import getValueByPath from './getValueByPath.js'; -import usesReference from './usesReference.js'; +import usesReferences from './usesReferences.js'; import createReferenceRegex from './createReferenceRegex.js'; import defaults from './defaults.js'; const PROPERTY_REFERENCE_WARNINGS = GroupMessages.GROUP.PropertyReferenceWarnings; +/** + * @typedef {import('../../../types/Config.d.ts').ResolveReferenceOptions} RefOpts + * @typedef {import('../../../types/Config.d.ts').ResolveReferenceOptionsInternal} RefOptsInternal + * @typedef {import('../../../types/DesignToken.d.ts').DesignTokens} DesignTokens + * @typedef {import('../../../types/DesignToken.d.ts').DesignToken} DesignToken + */ + +/** Utility to resolve references inside a string value + * @param {string} value + * @param {DesignTokens} tokens + * @param {RefOpts} [opts] + * @returns {string|number|undefined} + */ +export function resolveReferences(value, tokens, opts) { + return _resolveReferences(value, tokens, opts); +} + /** * Utility to resolve references inside a string value * @param {string} value - * @param {Object} dictionary - * @param {Object} opts - * @returns {string} + * @param {DesignTokens} tokens + * @param {RefOptsInternal} [opts] + * @returns {string|number|undefined} */ -export function resolveReferences( +export function _resolveReferences( value, - dictionary, + tokens, { regex, - ignorePaths = [], - current_context = [], separator = defaults.separator, opening_character = defaults.opening_character, closing_character = defaults.closing_character, + ignorePaths = [], // for internal usage + current_context = [], stack = [], foundCirc = {}, firstIteration = true, } = {}, ) { const reg = regex ?? createReferenceRegex({ opening_character, closing_character, separator }); + /** @type {DesignToken|string|number|undefined} */ let to_ret = value; + /** @type {DesignToken|string|number|undefined} */ let ref; // When we know the current context: @@ -56,25 +75,26 @@ export function resolveReferences( stack.push(getName(current_context)); } - // Replace the reference inline, but don't replace the whole string because - // references can be part of the value such as "1px solid {color.border.light}" - value.replace(reg, function (match, variable) { + /** + * Replace the reference inline, but don't replace the whole string because + * references can be part of the value such as "1px solid {color.border.light}" + */ + value.replace(reg, (match, /** @type {string} */ variable) => { variable = variable.trim(); // Find what the value is referencing - const pathName = getPathFromName(variable, { separator }); + const pathName = getPathFromName(variable, separator); const refHasValue = pathName[pathName.length - 1] === 'value'; if (refHasValue && ignorePaths.indexOf(variable) !== -1) { - return value; + return ''; } else if (!refHasValue && ignorePaths.indexOf(`${variable}.value`) !== -1) { - return value; + return ''; } stack.push(variable); - - ref = getValueByPath(pathName, dictionary); + ref = getValueByPath(pathName, tokens); // If the reference doesn't end in 'value' // and @@ -88,10 +108,10 @@ export function resolveReferences( if (typeof ref !== 'undefined') { if (typeof ref === 'string' || typeof ref === 'number') { - to_ret = value.replace(match, ref); + to_ret = value.replace(match, `${ref}`); // Recursive, therefore we can compute multi-layer variables like a = b, b = c, eventually a = c - if (usesReference(to_ret, reg)) { + if (usesReferences(to_ret, reg)) { const reference = to_ret.slice(1, -1); // Compare to found circular references @@ -121,7 +141,7 @@ export function resolveReferences( 'Circular definition cycle: ' + circStack.join(', '), ); } else { - to_ret = resolveReferences(to_ret, dictionary, { + to_ret = _resolveReferences(to_ret, tokens, { regex: reg, ignorePaths, current_context, @@ -152,9 +172,9 @@ export function resolveReferences( ); to_ret = ref; } - stack.pop(variable); + stack.pop(); - return to_ret; + return ''; }); return to_ret; diff --git a/lib/utils/references/usesReference.js b/lib/utils/references/usesReferences.js similarity index 82% rename from lib/utils/references/usesReference.js rename to lib/utils/references/usesReferences.js index b49b872f0..a16942254 100644 --- a/lib/utils/references/usesReference.js +++ b/lib/utils/references/usesReferences.js @@ -14,13 +14,14 @@ import createRegex from './createReferenceRegex.js'; /** + * @typedef {import('../../../types/Config.d.ts').RegexOptions} RegexOptions * Checks if the value uses a value reference. * @memberof Dictionary - * @param {string} value - * @param {Object|RegExp} regexOrOptions + * @param {string|any} value + * @param {RegExp|RegexOptions} [regexOrOptions] * @returns {boolean} - True, if the value uses a value reference */ -export default function usesReference(value, regexOrOptions = {}) { +export default function usesReferences(value, regexOrOptions = {}) { const regex = regexOrOptions instanceof RegExp ? regexOrOptions : createRegex(regexOrOptions); if (typeof value === 'string') { @@ -35,7 +36,7 @@ export default function usesReference(value, regexOrOptions = {}) { for (const key in value) { if (value.hasOwnProperty(key)) { const element = value[key]; - let reference = usesReference(element, regexOrOptions); + let reference = usesReferences(element, regexOrOptions); if (reference) { hasReference = true; break; diff --git a/lib/utils/resolveObject.js b/lib/utils/resolveObject.js index 5672e7771..c9c948df0 100644 --- a/lib/utils/resolveObject.js +++ b/lib/utils/resolveObject.js @@ -10,17 +10,31 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions * and limitations under the License. */ - import createReferenceRegex from './references/createReferenceRegex.js'; -import { resolveReferences } from './references/resolveReferences.js'; +import { _resolveReferences } from './references/resolveReferences.js'; + +/** + * @typedef {import('../../types/DesignToken.d.ts').TransformedTokens} TransformedTokens + * @typedef {import('../../types/DesignToken.d.ts').TransformedToken} TransformedToken + * @typedef {import('../../types/Config.d.ts').RegexOptions} RegexOptions + * @typedef {RegexOptions & {ignorePaths?: string[]; ignoreKeys?: string[]}} Options + */ const defaults = { ignoreKeys: ['original'], }; +/** + * + * @param {TransformedTokens} object + * @param {Options} _opts + * @returns + */ export default function resolveObject(object, _opts = {}) { + /** @type {Record} */ const foundCirc = {}; const opts = { ...defaults, ..._opts }; + /** @type {string[]} */ const current_context = []; const clone = structuredClone(object); // This object will be edited opts.regex = createReferenceRegex(opts); @@ -33,14 +47,15 @@ export default function resolveObject(object, _opts = {}) { } /** - * @param {Object} slice - slice within the full object - * @param {Object} fullObj - the full object - * @param {Object} opts - options such as regex, ignoreKeys, ignorePaths, etc. + * @param {TransformedTokens} slice - slice within the full object + * @param {TransformedTokens} fullObj - the full object + * @param {Options} opts - options such as regex, ignoreKeys, ignorePaths, etc. * @param {string[]} current_context - keeping track of the token group context that we're in + * @param {Record} foundCirc */ function traverseObj(slice, fullObj, opts, current_context, foundCirc) { for (const key in slice) { - if (!slice.hasOwnProperty(key)) { + if (!Object.hasOwn(slice, key)) { continue; } const value = slice[key]; @@ -55,13 +70,17 @@ function traverseObj(slice, fullObj, opts, current_context, foundCirc) { current_context.push(key); if (typeof value === 'object') { traverseObj(value, fullObj, opts, current_context, foundCirc); - } else { - if (typeof value === 'string' && value.indexOf('{') > -1) { - slice[key] = resolveReferences(value, fullObj, { + } else if (typeof value === 'string') { + let val = /** @type {string} */ (value); + if (val.indexOf('{') > -1) { + const ref = _resolveReferences(val, fullObj, { ...opts, current_context, foundCirc, }); + if (ref !== undefined) { + /** @type {any} */ (slice[key]) = ref; + } } } current_context.pop(); diff --git a/package-lock.json b/package-lock.json index 2f7b3b066..2802a2ebd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "chalk": "^5.3.0", "change-case": "^5.3.0", "commander": "^8.3.0", - "is-plain-object": "^5.0.0", + "is-plain-obj": "^4.1.0", "json5": "^2.2.2", "lodash-es": "^4.17.21", "tinycolor2": "^1.6.0" @@ -32,6 +32,8 @@ "@commitlint/config-conventional": "^18.4.3", "@esm-bundle/chai-as-promised": "^7.1.1", "@types/chai": "^4.3.9", + "@types/lodash-es": "^4.17.12", + "@types/tinycolor2": "^1.4.6", "@web/test-runner": "^0.18.0", "@web/test-runner-commands": "^0.9.0", "@web/test-runner-playwright": "^0.11.0", @@ -48,13 +50,14 @@ "jsdoc-tsimport-plugin": "^1.0.5", "less": "^4.1.2", "lint-staged": "^12.3.1", - "lodash": "^4.17.15", + "memfs": "^4.6.0", "mocha": "^10.2.0", "npm-run-all": "^4.1.5", "patch-package": "^8.0.0", "prettier": "^3.0.3", "sass": "^1.69.5", "stylus": "^0.56.0", + "typescript": "^5.3.3", "yaml": "^2.3.4" }, "engines": { @@ -4132,9 +4135,9 @@ } }, "node_modules/@jsdoc/salty": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.6.tgz", - "integrity": "sha512-aA+awb5yoml8TQ3CzI5Ue7sM3VMRC4l1zJJW4fgZ8OCL1wshJZhNzaf0PL85DSnOUw6QuFgeHGD/eq/xwwAF2g==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.7.tgz", + "integrity": "sha512-mh8LbS9d4Jq84KLw8pzho7XC2q2/IJGiJss3xwRoLD1A+EE16SjN4PfaG4jRCzKegTFLlN0Zd8SdUPE6XdoPFg==", "dev": true, "dependencies": { "lodash": "^4.17.21" @@ -4631,6 +4634,21 @@ "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", + "dev": true + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/markdown-it": { "version": "12.2.3", "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", @@ -4731,6 +4749,12 @@ "@types/node": "*" } }, + "node_modules/@types/tinycolor2": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", + "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", + "dev": true + }, "node_modules/@types/ws": { "version": "7.4.7", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", @@ -7745,16 +7769,17 @@ } }, "node_modules/docsify": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/docsify/-/docsify-4.13.1.tgz", - "integrity": "sha512-BsDypTBhw0mfslw9kZgAspCMZSM+sUIIDg5K/t1hNLkvbem9h64ZQc71e1IpY+iWsi/KdeqfazDfg52y2Lmm0A==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/docsify/-/docsify-4.12.2.tgz", + "integrity": "sha512-hpRez5upcvkYigT2zD8P5kH5t9HpSWL8yn/ZU/g04/WfAfxVNW6CPUVOOF1EsQUDxTRuyNTFOb6uUv+tPij3tg==", "dev": true, "hasInstallScript": true, "dependencies": { + "dompurify": "^2.3.1", "marked": "^1.2.9", "medium-zoom": "^1.0.6", "opencollective-postinstall": "^2.0.2", - "prismjs": "^1.27.0", + "prismjs": "^1.23.0", "strip-indent": "^3.0.0", "tinydate": "^1.3.0", "tweezer.js": "^1.4.0" @@ -9373,20 +9398,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -10546,20 +10557,14 @@ } }, "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-regex": { @@ -11457,9 +11462,9 @@ } }, "node_modules/less/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true, "optional": true, "bin": { @@ -12065,9 +12070,9 @@ "dev": true }, "node_modules/memfs": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.3.0.tgz", - "integrity": "sha512-bOMyYalKCLeEkd5l3sYv0XIsVPQW+2oGhYm8LwD1htXS637VmIdoXVrWPxZdbJlEogDIrTnm6wqqZBrYb7ZFPw==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.6.0.tgz", + "integrity": "sha512-I6mhA1//KEZfKRQT9LujyW6lRbX7RkC24xKododIDO3AGShcaFAMKElv1yFGWX8fD4UaSiwasr3NeQ5TdtHY1A==", "dependencies": { "json-joy": "^9.2.0", "thingies": "^1.11.1" @@ -12246,6 +12251,15 @@ "node": ">= 6" } }, + "node_modules/minimist-options/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mitt": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.0.tgz", @@ -12677,9 +12691,9 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -13813,20 +13827,6 @@ "node": ">=16" } }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -14352,9 +14352,9 @@ } }, "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true, "bin": { "semver": "bin/semver" @@ -16280,7 +16280,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 011b139e4..047fabae6 100644 --- a/package.json +++ b/package.json @@ -20,15 +20,13 @@ ], "type": "module", "exports": { - ".": { - "types": "./types/index.d.ts", - "default": "./lib/StyleDictionary.js" - }, + ".": "./lib/StyleDictionary.js", "./fs": { - "node": "./fs-node.js", - "default": "./fs.js" + "node": "./lib/fs-node.js", + "default": "./lib/fs.js" }, - "./utils": "./lib/utils/index.js" + "./utils": "./lib/utils/index.js", + "./types": "./types/index.d.ts" }, "bin": { "style-dictionary": "./bin/style-dictionary.js" @@ -40,8 +38,6 @@ "bin", "lib", "examples", - "fs.js", - "fs-node.js", "LICENSE", "NOTICE", "types" @@ -53,16 +49,17 @@ "lint": "run-p lint:*", "lint:eslint": "eslint \"**/*.js\"", "lint:prettier": "prettier \"**/*.{js,md}\" \"package.json\" --list-different || (echo '↑↑ these files are not prettier formatted ↑↑' && exit 1)", + "lint:types": "tsc --noEmit", "test": "npm run test:browser && npm run test:node", "test:browser": "web-test-runner --coverage", "test:browser:coverage": "cd coverage/lcov-report && npx http-server -o -c-1", "test:browser:watch": "web-test-runner --watch", "test:browser:update-snapshots": "web-test-runner --update-snapshots", "test:node": "mocha -r mocha-hooks.mjs './__integration__/**/*.test.js' './__tests__/**/*.test.js' './__node_tests__/**/*.test.js'", - "generate-docs": "node ./scripts/generateDocs.cjs", "serve-docs": "docsify serve docs -p 3000 -P 12345", "install-cli": "npm install -g $(npm pack)", - "release": "node ./scripts/version.cjs && npm run generate-docs && node scripts/inject-version.js && changeset publish", + "prerelease": "node scripts/inject-version.js && tsc --emitDeclarationOnly", + "release": "npm run prerelease && changeset publish", "prepare": "husky install", "postinstall": "patch-package" }, @@ -109,7 +106,7 @@ "chalk": "^5.3.0", "change-case": "^5.3.0", "commander": "^8.3.0", - "is-plain-object": "^5.0.0", + "is-plain-obj": "^4.1.0", "json5": "^2.2.2", "lodash-es": "^4.17.21", "tinycolor2": "^1.6.0" @@ -121,6 +118,8 @@ "@commitlint/config-conventional": "^18.4.3", "@esm-bundle/chai-as-promised": "^7.1.1", "@types/chai": "^4.3.9", + "@types/lodash-es": "^4.17.12", + "@types/tinycolor2": "^1.4.6", "@web/test-runner": "^0.18.0", "@web/test-runner-commands": "^0.9.0", "@web/test-runner-playwright": "^0.11.0", @@ -137,13 +136,14 @@ "jsdoc-tsimport-plugin": "^1.0.5", "less": "^4.1.2", "lint-staged": "^12.3.1", - "lodash": "^4.17.15", + "memfs": "^4.6.0", "mocha": "^10.2.0", "npm-run-all": "^4.1.5", "patch-package": "^8.0.0", "prettier": "^3.0.3", "sass": "^1.69.5", "stylus": "^0.56.0", + "typescript": "^5.3.3", "yaml": "^2.3.4" } } diff --git a/scripts/handlebars/templates/formats.hbs b/scripts/handlebars/templates/formats.hbs index 2a9b4a73f..95967fa09 100644 --- a/scripts/handlebars/templates/formats.hbs +++ b/scripts/handlebars/templates/formats.hbs @@ -244,23 +244,25 @@ Note: to support legacy ways of defining custom formats, this in th ## Custom format with output references -To take advantage of outputting references in your custom formats there are 2 helper methods in the `dictionary` argument passed to your formatter function: `usesReference(value)` and `getReferences(value)`. Here is an example using those: +To take advantage of outputting references in your custom formats there are 2 helper methods available: `usesReferences(value)` and `getReferences(value, tokens)`. Here is an example using those: ```javascript +import { getReferences, usesReferences } from 'style-dictionary/utils'; + StyleDictionary.registerFormat({ name: `es6WithReferences`, formatter: function({dictionary}) { return dictionary.allTokens.map(token => { let value = JSON.stringify(token.value); - // the `dictionary` object now has `usesReference()` and - // `getReferences()` methods. `usesReference()` will return true if + // the `dictionary` object now has `usesReferences()` and + // `getReferences()` methods. `usesReferences()` will return true if // the value has a reference in it. `getReferences()` will return // an array of references to the whole tokens so that you can access their // names or any other attributes. - if (dictionary.usesReference(token.original.value)) { + if (usesReferences(token.original.value)) { // Note: make sure to use `token.original.value` because // `token.value` is already resolved at this point. - const refs = dictionary.getReferences(token.original.value); + const refs = getReferences(token.original.value, dictionary.tokens); refs.forEach(ref => { value = value.replace(ref.value, function() { return `${ref.name}`; diff --git a/scripts/inject-version.js b/scripts/inject-version.js index fdf84fd6f..cc14a887b 100644 --- a/scripts/inject-version.js +++ b/scripts/inject-version.js @@ -1,7 +1,23 @@ import fs from 'node:fs'; +import glob from 'glob'; +import { execSync } from 'child_process'; -const filePath = 'lib/StyleDictionary.js'; -const indexContent = fs.readFileSync(filePath, 'utf-8'); -const { version } = JSON.parse(fs.readFileSync('package.json', 'utf-8')); +const { version, name } = JSON.parse(fs.readFileSync('package.json', 'utf-8')); + +// examples +const packageJSONs = glob.sync('./examples/*/*/package.json', {}); +packageJSONs.forEach(function (filePath) { + let pkg = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + if (pkg.devDependencies) { + pkg.devDependencies[name] = version; + fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2)); + } + execSync(`git add ${filePath}`); +}); + +// version in lib file +const sdPath = 'lib/StyleDictionary.js'; +const indexContent = fs.readFileSync(sdPath, 'utf-8'); const newIndexContent = indexContent.replace('', version); -fs.writeFileSync(filePath, newIndexContent, 'utf-8'); +fs.writeFileSync(sdPath, newIndexContent, 'utf-8'); +execSync(`git add ${sdPath}`); diff --git a/scripts/version.cjs b/scripts/version.cjs deleted file mode 100644 index b713a96e6..000000000 --- a/scripts/version.cjs +++ /dev/null @@ -1,15 +0,0 @@ -const fs = require('fs-extra'); -const glob = require('glob'); -const execSync = require('child_process').execSync; -const PACKAGE = require('../package.json'); -const packageJSONs = glob.sync('./examples/*/*/package.json', {}); - -packageJSONs.forEach(function (filePath) { - let pkg = fs.readJsonSync(filePath); - if (pkg.devDependencies) { - pkg.devDependencies[PACKAGE.name] = PACKAGE.version; - fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2)); - } - // Add the package.json file to staging and it'll get commited - execSync(`git add ${filePath}`); -}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..486f94ba9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "allowJs": true, + "checkJs": true, + "strict": true, + "noImplicitAny": true, + "skipLibCheck": true, + "declaration": true + }, + "include": ["types/**/*.d.ts", "lib/**/*.js"], + "exclude": ["node_modules", "**/coverage/*"] +} diff --git a/types/Action.d.ts b/types/Action.d.ts index 41b6c6cfb..a061cb2b7 100644 --- a/types/Action.d.ts +++ b/types/Action.d.ts @@ -11,13 +11,14 @@ * and limitations under the License. */ -import { Dictionary } from './Dictionary'; -import { Platform } from './Platform'; +import type { Dictionary } from './DesignToken.d.ts'; +import type { PlatformConfig } from './Config.d.ts'; export interface Action { + name: string; /** The action in the form of a function. */ do(dictionary: Dictionary, config: Platform): void; /** A function that undoes the action. */ undo?(dictionary: Dictionary, config: Platform): void; -} \ No newline at end of file +} diff --git a/types/Config.d.ts b/types/Config.d.ts index 8e6a9cf2b..c552b97d7 100644 --- a/types/Config.d.ts +++ b/types/Config.d.ts @@ -11,28 +11,64 @@ * and limitations under the License. */ -import { Parser } from './Parser'; -import { Preprocessor } from './Preprocessor'; -import { Transform } from './Transform'; -import { TransformGroup } from './TransformGroup'; -import { Filter } from './Filter'; -import { FileHeader } from './FileHeader'; -import { Formatter } from './Format'; -import { Action } from './Action'; -import { Platform } from './Platform'; -import { DesignTokens } from './DesignToken'; +import type { DesignTokens, TransformedToken } from './DesignToken.d.ts'; +import type { Filter, Matcher } from './Filter.d.ts'; +import type { FileHeader, File } from './File.d.ts'; +import type { Parser } from './Parser.d.ts'; +import type { Preprocessor } from './Preprocessor.d.ts'; +import type { Transform } from './Transform.d.ts'; +import type { Formatter } from './Format.d.ts'; +import type { Action } from './Action.d.ts'; + +export interface LocalOptions { + showFileHeader?: boolean; + fileHeader?: string | FileHeader; + outputReferences?: boolean; + [key: string]: any; +} + +export interface RegexOptions { + regex?: RegExp; + opening_character?: string; + closing_character?: string; + separator?: string; +} + +export interface ResolveReferenceOptions extends RegexOptions { + ignorePaths?: string[]; +} + +export interface ResolveReferenceOptionsInternal extends ResolveReferenceOptions { + current_context?: string[]; + stack?: string[]; + foundCirc?: Record; + firstIteration?: boolean; +} + +export interface PlatformConfig extends RegexOptions { + log?: 'warn' | 'error'; + transformGroup?: string; + transforms?: string[] | Omit[]; + basePxFontSize?: number; + prefix?: string; + buildPath?: string; + files?: File[]; + actions?: string[] | Omit[]; + options?: LocalOptions; +} export interface Config { + log?: 'warn' | 'error'; + source?: string[]; + include?: string[]; + tokens?: DesignTokens; + platforms?: Record; parsers?: Parser[]; preprocessors?: Record; transform?: Record; - transformGroup?: Record; + transformGroup?: Record; format?: Record; filter?: Record; fileHeader?: Record; action?: Record; - include?: string[]; - source?: string[]; - tokens?: DesignTokens; - platforms: Record; } diff --git a/types/DesignToken.d.ts b/types/DesignToken.d.ts index b6eee8b7f..25b70cda3 100644 --- a/types/DesignToken.d.ts +++ b/types/DesignToken.d.ts @@ -15,7 +15,7 @@ * This type is also used in the `typescript/module-declarations` format * Make sure to also change it there when this type changes! */ -interface DesignToken { +export interface DesignToken { value: any; name?: string; comment?: string; @@ -31,7 +31,41 @@ interface DesignToken { [key: string]: any; } -export { DesignToken }; export interface DesignTokens { [key: string]: DesignTokens | DesignToken; } + +export interface TransformedToken extends DesignToken { + name: string; + /** The object path of the property. + * + * `color: { background: { primary: { value: "#fff" } } }` will have a path of `['color', 'background', 'primary']`. + */ + path: string[]; + /** + * A pristine copy of the original property object. + * + * This is to make sure transforms and formats always have the unmodified version of the original property. + */ + original: DesignToken; + /** + * The file path of the file the token is defined in. + * + * This file path is derived from the source or include file path arrays defined in the configuration. + */ + filePath: string; + /** + * If the token is from a file defined in the source array as opposed to include in the [configuration](https://amzn.github.io/style-dictionary/#/config). + */ + isSource: boolean; +} + +export interface TransformedTokens { + [key: string]: TransformedTokens | TransformedToken; +} + +export interface Dictionary { + tokens: DesignTokens; + allTokens: TransformedToken[]; + unfilteredTokens?: DesignTokens; +} diff --git a/types/Dictionary.d.ts b/types/Dictionary.d.ts deleted file mode 100644 index 74ec1b936..000000000 --- a/types/Dictionary.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -import { TransformedToken, TransformedTokens } from './TransformedToken'; - -export interface Dictionary { - allTokens: TransformedToken[]; - tokens: TransformedTokens; -} diff --git a/types/File.d.ts b/types/File.d.ts index 87beb991c..605d97cdf 100644 --- a/types/File.d.ts +++ b/types/File.d.ts @@ -1,24 +1,28 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ +import type { TransformedToken } from './DesignToken.d.ts'; +import type { Formatter } from './Format.d.ts'; -import { Options } from './Options'; -import { TransformedToken } from './TransformedToken'; +export interface FormattingOptions { + prefix?: string; + suffix?: string; + lineSeparator?: string; + header?: string; + footer?: string; + commentStyle?: 'short' | 'long' | 'none'; + commentPosition?: 'above' | 'inline'; + indentation?: string; + separator?: string; +} + +export type FileHeader = (defaultMessage: string[]) => string[]; export interface File { className?: string; packageName?: string; destination: string; - format?: string; - filter?: string | Partial | ((token: TransformedToken) => boolean); - options?: Options; + format?: string | Formatter; + filter?: string | Partial | Matcher; + options?: LocalOptions; + resourceType?: string; + resourceMap?: Record; + name?: string; } diff --git a/types/FileHeader.d.ts b/types/FileHeader.d.ts deleted file mode 100644 index 8a48c2351..000000000 --- a/types/FileHeader.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -export type FileHeader = (defaultMessage: string[]) => string[]; diff --git a/types/Filter.d.ts b/types/Filter.d.ts index 5e7e29851..3d190d91b 100644 --- a/types/Filter.d.ts +++ b/types/Filter.d.ts @@ -10,10 +10,11 @@ * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions * and limitations under the License. */ - -import { Matcher } from './Matcher'; +import type { TransformedToken } from './DesignToken.d.ts'; export interface Filter { name: string; matcher: Matcher; -} \ No newline at end of file +} + +export type Matcher = (token: TransformedToken) => boolean; diff --git a/types/Format.d.ts b/types/Format.d.ts index 1c07fe32f..57f9200ec 100644 --- a/types/Format.d.ts +++ b/types/Format.d.ts @@ -11,10 +11,10 @@ * and limitations under the License. */ -import { Dictionary } from './Dictionary'; -import { File } from './File'; -import { Options } from './Options'; -import { Platform } from './Platform'; +import type { Dictionary } from './DesignToken.d.ts'; +import type { File } from './File.d.ts'; +import type { LocalOptions } from './Config.d.ts'; +import type { PlatformConfig } from './Platform.d.ts'; export interface FormatterArguments { /** @@ -28,18 +28,18 @@ export interface FormatterArguments { /** * The options object, */ - options: Options; + options: LocalOptions; /** * The platform configuration the format is called in */ - platform: Platform; + platform: PlatformConfig; } /** * The formatter function receives an overloaded object as its arguments and * it should return a string, which will be written to a file. */ -export type Formatter = (arguments: FormatterArguments) => string; +export type Formatter = ((arguments: FormatterArguments) => string) & { nested?: boolean }; export interface Format { name: string; diff --git a/types/FormatHelpers.d.ts b/types/FormatHelpers.d.ts index 5ef6bf1fa..e69de29bb 100644 --- a/types/FormatHelpers.d.ts +++ b/types/FormatHelpers.d.ts @@ -1,65 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -import { Dictionary } from './Dictionary'; -import { DesignToken } from './DesignToken'; -import { TransformedToken } from './TransformedToken'; -import { File } from './File'; - -export interface LineFormatting { - prefix?: string; - commentStyle?: 'short' | 'long' | 'none'; - commentPosition?: 'inline' | 'above'; - indentation?: string; - separator?: string; - suffix?: string; -} - -export type TokenFormatterArgs = { - outputReferences?: boolean; - dictionary: Dictionary; - format?: 'css' | 'sass' | 'less' | 'stylus'; - formatting?: LineFormatting; -}; - -export interface CommentFormatting { - prefix: string; - lineSeparator: string; - header: string; - footer: string; -} - -export interface FileHeaderArgs { - file: File; - commentStyle?: string; - formatting?: CommentFormatting; -} - -export interface FormattedVariablesArgs { - format: 'css' | 'sass'; - dictionary: Dictionary; - outputReferences?: boolean; - formatting?: LineFormatting; -} - -export interface FormatHelpers { - createPropertyFormatter: (args: TokenFormatterArgs) => (token: TransformedToken) => string; - fileHeader: (args: FileHeaderArgs) => string; - formattedVariables: (args: FormattedVariablesArgs) => string; - minifyDictionary: (dictionary: object) => object; - getTypeScriptType: (value: unknown) => string; - iconsWithPrefix: (prefix: string, allTokens: DesignToken[], options: object) => string; - sortByReference: (dictionary: Dictionary) => (a: TransformedToken, b: TransformedToken) => number; - sortByName: (a: DesignToken, b: DesignToken) => number; - setSwiftFileProperties: (options: object, objectType: string, transformGroup: string) => string; -} diff --git a/types/Matcher.d.ts b/types/Matcher.d.ts deleted file mode 100644 index 17171b6e9..000000000 --- a/types/Matcher.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -import { TransformedToken } from './TransformedToken'; - -export type Matcher = (token: TransformedToken) => boolean; \ No newline at end of file diff --git a/types/Options.d.ts b/types/Options.d.ts deleted file mode 100644 index b9c595203..000000000 --- a/types/Options.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -import { FileHeader } from './FileHeader'; - -export interface Options { - showFileHeader?: boolean; - fileHeader?: string | FileHeader; - outputReferences?: boolean; - [key: string]: any; -} \ No newline at end of file diff --git a/types/Parser.d.ts b/types/Parser.d.ts index 594318416..595c1b1e6 100644 --- a/types/Parser.d.ts +++ b/types/Parser.d.ts @@ -11,14 +11,14 @@ * and limitations under the License. */ -import { DesignTokens } from './DesignToken'; +import type { DesignTokens } from './DesignToken.d.ts'; export interface ParserOptions { contents: string; - filePath: string; + filePath?: string; } export interface Parser { pattern: RegExp; parse: (options: ParserOptions) => DesignTokens; -} \ No newline at end of file +} diff --git a/types/Platform.d.ts b/types/Platform.d.ts deleted file mode 100644 index f728420ae..000000000 --- a/types/Platform.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -import { Options } from './Options'; -import { File } from './File'; - -export type Platform> = { - transformGroup?: string; - transforms?: string[]; - basePxFontSize?: number; - prefix?: string; - buildPath?: string; - files?: File[]; - actions?: string[]; - options?: Options; -} & PlatformType; \ No newline at end of file diff --git a/types/Preprocessor.d.ts b/types/Preprocessor.d.ts index 2a8365cf0..f4d4ecbe3 100644 --- a/types/Preprocessor.d.ts +++ b/types/Preprocessor.d.ts @@ -11,7 +11,7 @@ * and limitations under the License. */ -import { DesignTokens } from './DesignToken'; +import type { DesignTokens } from './DesignToken.d.ts'; export type Preprocessor = { name: string; diff --git a/types/Transform.d.ts b/types/Transform.d.ts index 67bd11fa3..c76767334 100644 --- a/types/Transform.d.ts +++ b/types/Transform.d.ts @@ -11,39 +11,20 @@ * and limitations under the License. */ -import { Matcher } from './Matcher'; -import { TransformedToken } from './TransformedToken'; -import { Platform } from './Platform'; +import type { Matcher } from './Filter.d.ts'; +import type { TransformedToken } from './DesignToken.d.ts'; +import type { PlatformConfig } from './Config.d.ts'; -export interface NameTransform> { - type: "name"; +interface BaseTransform { + name: string; + type: Type; matcher?: Matcher; - transformer: ( - token: TransformedToken, - options: Platform - ) => string; -} - -export interface ValueTransform> { - type: "value"; transitive?: boolean; - matcher?: Matcher; - transformer: ( - token: TransformedToken, - options: Platform - ) => any; + transformer: (token: TransformedToken, options: PlatformConfig) => Value; } -export interface AttributeTransform> { - type: "attribute"; - matcher?: Matcher; - transformer: ( - token: TransformedToken, - options: Platform - ) => { [key: string]: any }; -} +export type NameTransform = BaseTransform<'name', string>; +export type AttributeTransform = BaseTransform<'attribute', Record>; +export type ValueTransform = BaseTransform<'value', unknown>; -export type Transform> = - | NameTransform - | ValueTransform - | AttributeTransform; \ No newline at end of file +export type Transform = NameTransform | AttributeTransform | ValueTransform; diff --git a/types/TransformGroup.d.ts b/types/TransformGroup.d.ts deleted file mode 100644 index 1b0ba9dc2..000000000 --- a/types/TransformGroup.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -export interface TransformGroup { - transforms: Array; -} \ No newline at end of file diff --git a/types/TransformedToken.d.ts b/types/TransformedToken.d.ts deleted file mode 100644 index 3540274f9..000000000 --- a/types/TransformedToken.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -import { DesignToken } from './DesignToken'; - -export type TransformedToken = DesignToken & { - name: string; - /** The object path of the property. - * - * `color: { background: { primary: { value: "#fff" } } }` will have a path of `['color', 'background', 'primary']`. - */ - path: string[]; - /** - * A pristine copy of the original property object. - * - * This is to make sure transforms and formats always have the unmodified version of the original property. - */ - original: DesignToken; - /** - * The file path of the file the token is defined in. - * - * This file path is derived from the source or include file path arrays defined in the configuration. - */ - filePath: string; - /** - * If the token is from a file defined in the source array as opposed to include in the [configuration](https://amzn.github.io/style-dictionary/#/config). - */ - isSource: boolean; -} - -export interface TransformedTokens { - [key: string]: TransformedTokens | TransformedToken; -} \ No newline at end of file diff --git a/types/_helpers.ts b/types/_helpers.ts deleted file mode 100644 index d5b9ad290..000000000 --- a/types/_helpers.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -export type Named = T & { name: string; }; diff --git a/types/index.d.ts b/types/index.d.ts index 7bdf1aac3..c01a739bb 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,358 +1,27 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ +export type { Action } from './Action.d.ts'; -// Minimum TypeScript Version: 3.0 +export type { PlatformConfig, Config } from './Config.d.ts'; -import { Action as _Action } from './Action'; -import { Config as _Config } from './Config'; -import { DesignToken as _DesignToken, DesignTokens as _DesignTokens } from './DesignToken'; -import { Dictionary as _Dictionary } from './Dictionary'; -import { File as _File } from './File'; -import { FileHeader as _FileHeader } from './FileHeader'; -import { Filter as _Filter } from './Filter'; -import { Format as _Format, Formatter as _Formatter } from './Format'; -import { FormatHelpers as _FormatHelpers } from './FormatHelpers'; -import { Matcher as _Matcher } from './Matcher'; -import { Options as _Options } from './Options'; -import { Parser as _Parser } from './Parser'; -import { Preprocessor as _Preprocessor, preprocessor as _preprocessor } from './Preprocessor'; -import { Platform as _Platform } from './Platform'; -import { Transform as _Transform } from './Transform'; -import { - TransformedToken as _TransformedToken, - TransformedTokens as _TransformedTokens, -} from './TransformedToken'; -import { TransformGroup as _TransformGroup } from './TransformGroup'; -import { Named as _Named } from './_helpers'; +export type { + DesignToken, + DesignTokens, + TransformedToken, + TransformedTokens, +} from './DesignToken.d.ts'; -// Because this library is used in Node and needs to be accessible -// as a CommonJS module, we are declaring it as a namespace so that -// autocomplete works for CommonJS files. -declare namespace StyleDictionary { - type Action = _Action; - type Config = _Config; - type DesignToken = _DesignToken; - type DesignTokens = _DesignTokens; - type Dictionary = _Dictionary; - type File = _File; - type FileHeader = _FileHeader; - type Filter = _Filter; - type Format = _Format; - type FormatHelpers = _FormatHelpers; - type Formatter = _Formatter; - type Matcher = _Matcher; - type Options = _Options; - type Parser = _Parser; - type Preprocessor = _Preprocessor; - type preprocessor = _preprocessor; - type Platform> = _Platform; - type Transform> = _Transform; - type TransformedToken = _TransformedToken; - type TransformedTokens = _TransformedTokens; - type TransformGroup = _TransformGroup; - type Named = _Named; +export type { FileHeader, File } from './File.d.ts'; - interface Core { - VERSION: string; - tokens: DesignTokens | TransformedTokens; - allTokens: TransformedTokens[]; - options: Config; +export type { Filter } from './Filter.d.ts'; - transform: Record; - transformGroup: Record; - format: Record; - action: Record; - filter: Record; - fileHeader: Record; - parsers: Parser[]; - preprocessors: Record; +export type { Format } from './Format.d.ts'; - formatHelpers: FormatHelpers; +export type { Parser } from './Parser.d.ts'; - /** - * Add a custom transform to the Style Dictionary - * Transforms can manipulate a token's name, value, or attributes - * - * @param {String} transform.type - Type of transform, can be: name, attribute, or value - * @param {String} transform.name - Name of the transformer (used by transformGroup to call a list of transforms). - * @param {Boolean} transform.transitive - If the value transform should be applied transitively, i.e. should be applied to referenced values as well as absolute values. - * @param {Function} [transform.matcher] - Matcher function, return boolean if transform should be applied. If you omit the matcher function, it will match all tokens. - * @param {Function} transform.transformer Modifies a design token object. The transformer function will receive the token and the platform configuration as its arguments. The transformer function should return a string for name transforms, an object for attribute transforms, and same type of value for a value transform. - * @example - * ```js - * StyleDictionary.registerTransform({ - * name: 'time/seconds', - * type: 'value', - * matcher: function(token) { - * return token.attributes.category === 'time'; - * }, - * transformer: function(token) { - * return (parseInt(token.original.value) / 1000).toString() + 's'; - * } - * }); - * ``` - */ - registerTransform(transform: Named>): this; +export type { Preprocessor } from './Preprocessor.d.ts'; - /** - * Add a custom transformGroup to the Style Dictionary, which is a - * group of transforms. - * @param {String} transformGroup.name - Name of the transform group that will be referenced in config.json - * @param {String[]} transformGroup.transforms - Array of strings that reference the name of transforms to be applied in order. Transforms must be defined and match the name or there will be an error at build time. - * @example - * ```js - * StyleDictionary.registerTransformGroup({ - * name: 'Swift', - * transforms: [ - * 'attribute/cti', - * 'size/pt', - * 'name/cti' - * ] - * }); - * ``` - */ - registerTransformGroup(transformGroup: Named): this; - - /** - * Add a custom format to Style Dictionary - * @param {String} format.name The name of the format to be added - * @param {Formatter} format.formatter The formatter function - * @example - * ```js - * StyleDictionary.registerFormat({ - * name: 'json', - * formatter: function({dictionary, platform, options, file}) { - * return JSON.stringify(dictionary.tokens, null, 2); - * } - * }) - * ``` - */ - registerFormat(format: Named): this; - - /** - * Add a custom template to Style Dictionary - * @deprecated registerTemplate will be removed in the future, please use registerFormat - * @param {String} template.name - The name of your template. You will refer to this in your config.json file. - * @param {String} template.template - Path to your lodash template - * @example - * ```js - * StyleDictionary.registerTemplate({ - * name: 'Swift/colors', - * template: __dirname + '/templates/swift/colors.template' - * }); - * ``` - */ - registerTemplate(template: Named<{ template: string }>): this; - - /** - * Add a custom filter to Style Dictionary. Filters are used to hide tokens from - * generated files. - * @param {String} filter.name - Name of the filter to be referenced in your config.json - * @param {Function} filter.matcher - Matcher function, return boolean if the token should be included. - * @example - * ```js - * StyleDictionary.registerFilter({ - * name: 'isColor', - * matcher: function(token) { - * return token.attributes.category === 'color'; - * } - * }) - * ``` - */ - registerFilter(filter: Named): this; - - /** - * Add a custom file header to Style Dictionary. File headers are used to write a - * custom messasge on top of the generated files. - * @param {String} fileHeader.name The name of the file header to be added - * @param {Function} fileHeader.fileHeader The file header function - * @example - * ```js - * StyleDictionary.registerFileHeader({ - * name: 'custmoHeader', - * fileHeader: function(defaultMessage) { - * return return [ - * `hello`, - * ...defaultMessage - * ] - * } - * }) - * ``` - */ - registerFileHeader(fileHeader: Named<{ fileHeader: FileHeader }>): this; - - /** - * Adds a custom parser to parse style dictionary files. This allows you to modify - * the design token data before it gets to Style Dictionary or write your - * token files in a language other than JSON, JSONC, JSON5, or CommonJS modules. - * - * @param {Regex} parser.pattern - A file path regular expression to match which files this parser should be be used on. This is similar to how webpack loaders work. `/\.json$/` will match any file ending in '.json', for example. - * @param {Function} parser.parse - Function to parse the file contents. Takes 1 argument, which is an object with 2 attributes: contents which is the string of the file contents and filePath. The function should return a plain Javascript object. - * @example - * ```js - * StyleDictionary.registerParser({ - * pattern: /\.json$/, - * parse: ({contents, filePath}) => { - * return JSON.parse(contents); - * } - * }) - * ``` - */ - registerParser(parser: Parser): this; - - /** - * Adds a custom preprocessor to preprocess dictionary object. - * - * @param {Preprocessor} Preprocessor - * @param {string} Preprocessor.name - name of the preprocessor - * @param {preprocessor} Preprocessor.preprocessor - Function to preprocess the dictionary. The function should return a plain Javascript object. - * @example - * ```js - * StyleDictionary.registerPreprocessor({ - * name: 'strip-third-party-meta', - * preprocessor: (dictionary) => { - * delete dictionary.thirdPartyMetadata; - * return dictionary; - * }, - * }); - * ``` - */ - registerPreprocessor(preprocessor: Preprocessor): this; - - /** - * Adds a custom action to Style Dictionary. Actions - * are functions that can do whatever you need, such as: copying files, - * base64'ing files, running other build scripts, etc. - * After you register a custom action, you then use that - * action in a platform your configuration - * - * You can perform operations on files generated by the style dictionary - * as actions run after these files are generated. - * Actions are run sequentially, if you write synchronous code then - * it will block other actions, or if you use asynchronous code like Promises - * it will not block. - * - * @param {String} action.name - The name of the action - * @param {Function} action.do - The action in the form of a function. - * @param {Function} [action.undo] - A function that undoes the action. - * @example - * ```js - * StyleDictionary.registerAction({ - * name: 'copy_assets', - * do: function(dictionary, config) { - * console.log('Copying assets directory'); - * fs.copySync('assets', config.buildPath + 'assets'); - * }, - * undo: function(dictionary, config) { - * console.log('Cleaning assets directory'); - * fs.removeSync(config.buildPath + 'assets'); - * } - * }); - * ``` - */ - registerAction(action: Named): this; - - /** - * Exports a tokens object with applied - * platform transforms. - * - * This is useful if you want to use a style - * dictionary in JS build tools like webpack. - * - * @param {String} platform - Name of the platform to be exported. This platform name must exist on the Style Dictionary configuration. - */ - exportPlatform(platform: string): TransformedTokens; - - /** - * Takes a platform and performs all transforms to - * the tokens object (non-mutative) then - * builds all the files and performs any actions. This is useful if you only want to - * build the artifacts of one platform to speed up the build process. - * - * This method is also used internally in `.buildAllPlatforms` to - * build each platform defined in the config. - * - * @param {String} platform - Name of the platform you want to build. This platform name must exist on the Style Dictionary configuration. - * @example - * ```js - * StyleDictionary.buildPlatform('web'); - * ``` - * ```bash - * $ style-dictionary build --platform web - * ``` - */ - buildPlatform(platform: string): this; - - /** - * Will build all the platforms defined in the configuration. - * - * @example - * ```js - * import StyleDictionary from 'style-dictionary'; - * const sd = await StyleDictionary.extend('config.json'); - * sd.buildAllPlatforms(); - * ``` - */ - buildAllPlatforms(): this; - - /** - * Takes a platform and performs all transforms to - * the tokens object (non-mutative) then - * cleans all the files and performs the undo method of any actions. - * - * @param {String} platform - */ - cleanPlatform(platform: string): this; - - /** - * Does the reverse of `.buildAllPlatforms` by - * performing a clean on each platform. This removes all the files - * defined in the platform and calls the undo method on any actions. - * - * @example - * ```js - * StyleDictionary.cleanAllPlatforms(); - * ``` - */ - cleanAllPlatforms(): this; - - /** - * Creates a Style Dictionary - * @param {String | Config} config - The configuration for Style Dictionary, can either be a path to a JSON or CommonJS file or a configuration object. - * @example - * ```js - * import StyleDictionary from 'style-dictionary'; - * const sd = await StyleDictionary.extend('config.json'); - * - * const sd = await StyleDictionary.extend({ - * source: ['tokens/*.json'], - * platforms: { - * scss: { - * transformGroup: 'scss', - * buildPath: 'build/', - * files: [{ - * destination: 'variables.scss', - * format: 'scss/variables' - * }] - * } - * // ... - * } - * }); - * ``` - */ - extend(config: string | Config): Promise; - } -} - -declare const StyleDictionary: StyleDictionary.Core; -export default StyleDictionary; +export type { + Transform, + NameTransform, + AttributeTransform, + ValueTransform, +} from './Transform.d.ts'; diff --git a/types/index.test-d.ts b/types/index.test-d.ts deleted file mode 100644 index 92b3c788e..000000000 --- a/types/index.test-d.ts +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with - * the License. A copy of the License is located at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -import { expectType, expectError, expectAssignable } from 'tsd'; -import StyleDictionary from './index'; - -expectType(StyleDictionary.buildAllPlatforms()); -expectType(StyleDictionary.buildPlatform('web')); - -expectType(StyleDictionary.cleanAllPlatforms()); - -expectType(StyleDictionary.cleanPlatform('web')); -expectType(StyleDictionary.exportPlatform('web')); - -expectType>(StyleDictionary.extend('config.json')); - -expectType>( - StyleDictionary.extend({ - source: ['tokens/**/*.json'], - platforms: { - scss: { - transformGroup: 'scss', - buildPath: 'build/', - files: [ - { - destination: 'variables.scss', - format: 'scss/variables', - }, - ], - }, - }, - }), -); - -expectType( - StyleDictionary.registerAction({ - name: 'copy_assets', - do: function (dictionary, config) { - console.log('Copying assets directory', 'assets', config.buildPath + 'assets'); - }, - undo: function (dictionary, config) { - console.log('Cleaning assets directory', config.buildPath + 'assets'); - }, - }), -); - -expectType( - StyleDictionary.registerFilter({ - name: 'isColor', - matcher: function (token) { - return token.attributes?.category === 'color'; - }, - }), -); - -expectType( - StyleDictionary.registerFormat({ - name: 'json', - formatter: function ({ dictionary, file, options, platform }) { - expectType(dictionary); - expectType(file); - expectType(options); - expectType(platform); - return JSON.stringify(dictionary.tokens, null, 2); - }, - }), -); - -expectType( - StyleDictionary.registerTemplate({ - name: 'Swift/colors', - template: __dirname + '/templates/swift/colors.template', - }), -); - -expectType( - StyleDictionary.registerTransform({ - name: 'time/seconds', - type: 'value', - matcher: function (token) { - return token.attributes?.category === 'time'; - }, - transformer: function (token) { - expectType(token); - return (parseInt(token.original.value) / 1000).toString() + 's'; - }, - }), -); - -type CustomPlatform = { - colorMode: 'light' | 'dark'; -}; - -expectType( - StyleDictionary.registerTransform({ - name: 'colormode', - type: 'value', - matcher: function (token) { - return token.attributes?.category === 'color'; - }, - transformer: function (token, platform) { - expectType(token); - expectType>(platform); - if (platform.colorMode === 'light') { - return 'light'; - } else { - return 'dark'; - } - }, - }), -); - -expectType( - StyleDictionary.registerTransformGroup({ - name: 'Swift', - transforms: ['attribute/cti', 'size/pt', 'name/cti'], - }), -); - -expectType( - StyleDictionary.registerParser({ - pattern: /\.json$/, - parse: ({ contents, filePath }) => { - return {}; - }, - }), -); - -expectType( - StyleDictionary.registerPreprocessor({ - name: 'foo', - preprocessor: (dictionary) => dictionary, - }), -); - -const file: StyleDictionary.File = { - destination: `somePath.json`, - format: `css/variables`, - filter: (token) => { - expectType(token); - return false; - }, - options: { - showFileHeader: true, - }, -}; - -expectType(file.options); - -// Files need a destination -expectError({ - format: `css/variables`, -}); - -expectAssignable({ - format: `css/variables`, - destination: `variables.css`, -}); - -expectAssignable({ - basePxFontSize: 16, -}); - -expectAssignable({ - transformGroup: `css`, -}); - -expectAssignable({ - transforms: [`attribute/cti`], -}); - -expectAssignable({ - colorMode: 'dark', - files: [], - transformGroup: 'scss', -}); - -expectAssignable({ - transforms: [`attribute/cti`], - files: [ - { - destination: `destination`, - }, - ], -}); - -expectError({ - transforms: `css`, -}); - -expectError({ - transformGroup: [`attribute/cti`], -}); - -/** - * Testing Options type. - * fileHeader needs to be a string or a function that returns a string[] - * showFileHeader must be a boolean - */ -expectError({ fileHeader: false }); -expectAssignable({ fileHeader: 'fileHeader' }); -expectError({ fileHeader: () => 42 }); -expectAssignable({ fileHeader: () => [`hello`] }); -expectError({ showFileHeader: 'false' }); diff --git a/types/typeless-modules/bundled-deepmerge.d.ts b/types/typeless-modules/bundled-deepmerge.d.ts new file mode 100644 index 000000000..699491771 --- /dev/null +++ b/types/typeless-modules/bundled-deepmerge.d.ts @@ -0,0 +1 @@ +declare module '@bundled-es-modules/deepmerge'; diff --git a/types/typeless-modules/bundled-glob.d.ts b/types/typeless-modules/bundled-glob.d.ts new file mode 100644 index 000000000..3dae150c8 --- /dev/null +++ b/types/typeless-modules/bundled-glob.d.ts @@ -0,0 +1 @@ +declare module '@bundled-es-modules/glob'; diff --git a/types/typeless-modules/bundled-memfs.d.ts b/types/typeless-modules/bundled-memfs.d.ts new file mode 100644 index 000000000..6867fbd86 --- /dev/null +++ b/types/typeless-modules/bundled-memfs.d.ts @@ -0,0 +1 @@ +declare module '@bundled-es-modules/memfs'; diff --git a/types/typeless-modules/bundled-path.d.ts b/types/typeless-modules/bundled-path.d.ts new file mode 100644 index 000000000..e13fa55d6 --- /dev/null +++ b/types/typeless-modules/bundled-path.d.ts @@ -0,0 +1 @@ +declare module '@bundled-es-modules/path-browserify';