diff --git a/CHANGELOG.md b/CHANGELOG.md index 318f333..7658e67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [8.1.1] + +- Added `resolveMetaAliases` option to resolve aliases in metadata. - see [#172](https://github.com/salesforce-ux/theo/issues/172) + ## [8.0.1] - Upgraded vulnerable dependency diff --git a/CLI.md b/CLI.md index 7e45eec..bea72e0 100644 --- a/CLI.md +++ b/CLI.md @@ -12,20 +12,23 @@ $ theo <[file]> [options] ### Options -|Name|Description|Default| -|----|-----------|-------| -|`--transform`|valid theo transform|`raw`| -|`--format`|Comma separated list of valid theo formats|`raw.json`| -|`--dest`|The path where the result should be written|stdout| -|`--setup`|The path to an optional JS module that can set up Theo before transformation.|| +| Name | Description | Default | +| ---------------------- | ----------------------------------------------------------------------------- | ---------- | +| `--transform` | valid theo transform | `raw` | +| `--format` | Comma separated list of valid theo formats | `raw.json` | +| `--dest` | The path where the result should be written | stdout | +| `--setup` | The path to an optional JS module that can set up Theo before transformation. | | +| `--resolveMetaAliases` | Resolve aliases in metadata | `false` | ### transforms / formats Formats are valid theo supported formats, check the [documentation](https://github.com/salesforce-ux/theo#available-formats) for a full list of supported transforms and formats. Usage example with formats: + ``` $ theo tokens.yml --transform web --format scss,cssmodules.css +$ theo tokens.yml --transform web --format scss,cssmodules.css --resolveMetaAliases ``` ### setup module @@ -33,11 +36,12 @@ $ theo tokens.yml --transform web --format scss,cssmodules.css A valid setup module exports a function that takes theo as the first argument. Example module (example.js): + ``` module.exports = theo => { theo.registerValueTransform( - 'addpx', - prop => prop.get('type') === 'size', + 'addpx', + prop => prop.get('type') === 'size', prop => prop.get('value') + 'px' ); theo.registerTransform("web", ['addpx']); @@ -45,6 +49,7 @@ module.exports = theo => { ``` Usage example with setup + ``` $ theo tokens.yml --setup example.js --transform web --format scss ``` diff --git a/README.md b/README.md index 994d682..53bbb7f 100644 --- a/README.md +++ b/README.md @@ -31,16 +31,16 @@ aliases: ``` ```js -const theo = require('theo'); +const theo = require("theo"); theo .convert({ transform: { - type: 'web', - file: 'buttons.yml' + type: "web", + file: "buttons.yml" }, format: { - type: 'scss' + type: "scss" } }) .then(scss => { @@ -55,26 +55,26 @@ Theo is divided into two primary features: transforms and formats. Transforms are a named group of value transforms. Theo ships with several predefined transforms. -| Name | Value Transforms -|-- | --- -| `raw` | `[]` -| `web` | `['color/rgb']` -| `ios` | `['color/rgb', 'relative/pixelValue', 'percentage/float']` -| `android` | `['color/hex8argb', 'relative/pixelValue', 'percentage/float']` +| Name | Value Transforms | +| --------- | --------------------------------------------------------------- | +| `raw` | `[]` | +| `web` | `['color/rgb']` | +| `ios` | `['color/rgb', 'relative/pixelValue', 'percentage/float']` | +| `android` | `['color/hex8argb', 'relative/pixelValue', 'percentage/float']` | ### Value Transforms Value transforms are used to conditionaly transform the value of a property. Below are the value transforms that ship with Theo along with the predicate that triggers them. -| Name | Predicate | Description -|--- | --- | --- -| `color/rgb` | `prop.type === 'color'` | Convert to rgb -| `color/hex` | `prop.type === 'color'` | Convert to hex -| `color/hex8rgba` | `prop.type === 'color'` | Convert to hex8rgba -| `color/hex8argb` | `prop.type === 'color'` | Convert to hex8argb -| `percentage/float`| `/%/.test(prop.value)` | Convert a percentage to a decimal percentage -| `relative/pixel` | `isRelativeSpacing` | Convert a r/em value to a pixel value -| `relative/pixelValue` | `isRelativeSpacing` | Convert a r/em value to a pixel value (excluding the `px` suffix) +| Name | Predicate | Description | +| --------------------- | ----------------------- | ----------------------------------------------------------------- | +| `color/rgb` | `prop.type === 'color'` | Convert to rgb | +| `color/hex` | `prop.type === 'color'` | Convert to hex | +| `color/hex8rgba` | `prop.type === 'color'` | Convert to hex8rgba | +| `color/hex8argb` | `prop.type === 'color'` | Convert to hex8argb | +| `percentage/float` | `/%/.test(prop.value)` | Convert a percentage to a decimal percentage | +| `relative/pixel` | `isRelativeSpacing` | Convert a r/em value to a pixel value | +| `relative/pixelValue` | `isRelativeSpacing` | Convert a r/em value to a pixel value (excluding the `px` suffix) | ### Custom Transforms / Value Transforms @@ -184,7 +184,7 @@ $file-name-list: ( ```js // If prop has 'comment' key, that value will go here. -export const propName = 'PROP_VALUE'; +export const propName = "PROP_VALUE"; ``` ### common.js @@ -192,7 +192,7 @@ export const propName = 'PROP_VALUE'; ```js module.exports = { // If prop has 'comment' key, that value will go here. - propName: 'PROP_VALUE' + propName: "PROP_VALUE" }; ``` @@ -204,7 +204,7 @@ module.exports = { // When passing "format" options to theo.convert(), this format can be // passed with an additional options object. let formatOptions = { - type: 'html', + type: "html", options: { transformPropName: name => name.toUpperCase() } @@ -212,38 +212,40 @@ let formatOptions = { ``` #### Configurable options -| Option | Type | Default | Description -|-- | -- | -- | --- -| `transformPropName` | `function` | [`lodash/camelCase`](https://lodash.com/docs/#camelCase) | Converts `name` to camel case. + +| Option | Type | Default | Description | +| ------------------- | ---------- | -------------------------------------------------------- | ------------------------------ | +| `transformPropName` | `function` | [`lodash/camelCase`](https://lodash.com/docs/#camelCase) | Converts `name` to camel case. | #### Supported categories + Tokens are grouped by category then categories are conditionally rendered under a human-friendly display name. Tokens with `category` values not in this list will still be converted and included in the generated output for all other formats. -| Category | Friendly Name -|-- | --- -| `spacing` | Spacing -| `sizing` | Sizing -| `font` | Fonts -| `font-style` | Font Styles -| `font-weight` | Font Weights -| `font-size` | Font Sizes -| `line-height` | Line Heights -| `font-family` | Font Families -| `border-style` | Border Styles -| `border-color` | Border Colors -| `radius` | Radius -| `border-radius` | Border Radii -| `hr-color` | Horizontal Rule Colors -| `background-color` | Background Colors -| `gradient` | Gradients -| `background-gradient` | Background Gradients -| `drop-shadow` | Drop Shadows -| `box-shadow` | Box Shadows -| `inner-shadow` | Inner Drop Shadows -| `text-color` | Text Colors -| `text-shadow` | Text Shadows -| `time` | Time -| `media-query` | Media Queries +| Category | Friendly Name | +| --------------------- | ---------------------- | +| `spacing` | Spacing | +| `sizing` | Sizing | +| `font` | Fonts | +| `font-style` | Font Styles | +| `font-weight` | Font Weights | +| `font-size` | Font Sizes | +| `line-height` | Line Heights | +| `font-family` | Font Families | +| `border-style` | Border Styles | +| `border-color` | Border Colors | +| `radius` | Radius | +| `border-radius` | Border Radii | +| `hr-color` | Horizontal Rule Colors | +| `background-color` | Background Colors | +| `gradient` | Gradients | +| `background-gradient` | Background Gradients | +| `drop-shadow` | Drop Shadows | +| `box-shadow` | Box Shadows | +| `inner-shadow` | Inner Drop Shadows | +| `text-color` | Text Colors | +| `text-shadow` | Text Shadows | +| `time` | Time | +| `media-query` | Media Queries | ### json @@ -257,11 +259,11 @@ Tokens are grouped by category then categories are conditionally rendered under ```json5 { - "props": { - "PROP_NAME": { - "value": "PROP_VALUE", - "type": "PROP_TYPE", - "category": "PROP_CATEGORY" + props: { + PROP_NAME: { + value: "PROP_VALUE", + type: "PROP_TYPE", + category: "PROP_CATEGORY" } } } @@ -271,12 +273,12 @@ Tokens are grouped by category then categories are conditionally rendered under ```json5 { - "properties": [ + properties: [ { - "name": "propName", - "value": "PROP_VALUE", - "type": "PROP_TYPE", - "category": "PROP_CATEGORY" + name: "propName", + value: "PROP_VALUE", + type: "PROP_TYPE", + category: "PROP_CATEGORY" } ] } @@ -306,10 +308,10 @@ Tokens are grouped by category then categories are conditionally rendered under ### Custom Format (Handlebars) ```js -const theo = require('theo'); +const theo = require("theo"); theo.registerFormat( - 'array.js', + "array.js", ` // Source: {{stem meta.file}} module.exports = [ @@ -330,21 +332,21 @@ such as `camelcase` and `stem`, are available and will assist in formatting stri You may also register a format using a function: ```js -const camelCase = require('lodash/camelCase'); -const path = require('path'); -const theo = require('theo'); +const camelCase = require("lodash/camelCase"); +const path = require("path"); +const theo = require("theo"); -theo.registerFormat('array.js', result => { +theo.registerFormat("array.js", result => { // "result" is an Immutable.Map // https://facebook.github.io/immutable-js/ return ` module.exports = [ - // Source: ${path.basename(result.getIn(['meta', 'file']))} + // Source: ${path.basename(result.getIn(["meta", "file"]))} ${result - .get('props') + .get("props") .map( prop => ` - ['${camelCase(prop.get('name'))}', '${prop.get('value')}'], + ['${camelCase(prop.get("name"))}', '${prop.get("value")}'], ` ) .toJS()} @@ -358,7 +360,15 @@ theo.registerFormat('array.js', result => { ```js type ConvertOptions = { transform: TransformOptions, - format: FormatOptions + format: FormatOptions, + /* + This option configures theo to resolve aliases. It is set (true) by default and + currently CANNOT be disabled. + */ + resolveAliases?: boolean, + + // This option configures theo to resolve aliases in metadata. This is off (false) by default. + resolveMetaAliases?: boolean } type TransformOptions = { @@ -441,40 +451,40 @@ or [YAML](http://yaml.org/) and should conform to the following spec: { // Required // A map of property names and value objects - "props": { - "color_brand": { + props: { + color_brand: { // Required // Can be any valid JSON value - "value": "#ff0000", + value: "#ff0000", // Required // Describe the type of value // [color|number|...] - "type": "color", + type: "color", // Required // Describe the category of this property // Often used for style guide generation - "category": "background", + category: "background", // Optional // This value will be included during transform // but excluded during formatting - "meta": { + meta: { // This value might be needed for some special transform - "foo": "bar" + foo: "bar" } } }, - + // Optional // Alternatively, you can define props as an array // Useful for maintaining source order in output tokens - "props": [ + props: [ { // Required - "name": "color_brand", - + name: "color_brand" + // All other properties same as above } ], @@ -482,21 +492,21 @@ or [YAML](http://yaml.org/) and should conform to the following spec: // Optional // This object will be merged into each property // Values defined on a property level will take precedence - "global": { - "category": "some-category", - "meta": { - "foo": "baz" + global: { + category: "some-category", + meta: { + foo: "baz" } }, // Optional // Share values across multiple props // Aliases are resolved like: {!sky} - "aliases": { - "sky": "blue", - "grass": { - "value": "green", - "yourMetadata": "How grass looks" + aliases: { + sky: "blue", + grass: { + value: "green", + yourMetadata: "How grass looks" } }, @@ -505,9 +515,9 @@ or [YAML](http://yaml.org/) and should conform to the following spec: // "aliases" will be imported as well // "aliases" will already be resolved // "global" will already be merged into each prop - // Imports resolve according to the Node.js module resolution algorithm: + // Imports resolve according to the Node.js module resolution algorithm: // https://nodejs.org/api/modules.html#modules_all_together - "imports": [ + imports: [ // Absolute file path "/home/me/file.json", // Relative file path: resolves from the directory of the file where the import occurs diff --git a/bin/scripts/build.js b/bin/scripts/build.js index ab6c63a..7e68773 100644 --- a/bin/scripts/build.js +++ b/bin/scripts/build.js @@ -5,7 +5,14 @@ const theo = require("../../lib"); const fs = require("fs-extra"); const path = require("path"); -module.exports = ({ src = "", dest, setup, formats, transform }) => { +module.exports = ({ + src = "", + dest, + setup, + formats, + transform, + resolveMetaAliases +}) => { if (setup) { const setupModuleFile = path.resolve(process.cwd(), setup); require(setupModuleFile)(theo); @@ -17,7 +24,8 @@ module.exports = ({ src = "", dest, setup, formats, transform }) => { .convert({ transform: { type: transform, - file: path.resolve(process.cwd(), src) + file: path.resolve(process.cwd(), src), + resolveMetaAliases }, format: { type: format diff --git a/bin/theo.js b/bin/theo.js index cd57d12..78b0a2f 100755 --- a/bin/theo.js +++ b/bin/theo.js @@ -11,7 +11,8 @@ const options = { dest: argv.dest, setup: argv.setup, formats: (argv.format || "raw.json").split(","), - transform: argv.transform || "raw" + transform: argv.transform || "raw", + resolveMetaAliases: argv.resolveMetaAliases || false }; build(options).catch(error => { diff --git a/lib/__tests__/definition.js b/lib/__tests__/definition.js index ca70199..3578562 100644 --- a/lib/__tests__/definition.js +++ b/lib/__tests__/definition.js @@ -261,6 +261,66 @@ it("resolves nested aliases", done => { ); }); +it("resolves aliases in metadata when resolveMetaAliases flag is set (true)", done => { + transform( + getFixture(` + aliases: + ALIAS_A: + value: "#FF0000" + ALIAS_B: + value: "#00AAAA" + props: + TOKEN_A: + value: "{!ALIAS_A}" + meta: + meta_test: "{!ALIAS_B}" + `), + Immutable.Map({ includeMeta: true, resolveMetaAliases: true }) + ).fold( + e => { + throw new Error(e); + }, + r => { + const value = r.getIn(["props", "TOKEN_A", "value"]); + const metaValue = r.getIn(["props", "TOKEN_A", "meta", "meta_test"]); + + expect(value).toEqual("#FF0000"); + expect(metaValue).toEqual("#00AAAA"); + done(); + } + ); +}); + +it("DOES NOT resolve aliases in metadata when resolveMetaAliases flag is NOT set (false)", done => { + transform( + getFixture(` + aliases: + ALIAS_A: + value: "#FF0000" + ALIAS_B: + value: "#00AAAA" + props: + TOKEN_A: + value: "{!ALIAS_A}" + meta: + meta_test: "{!ALIAS_B}" + `), + Immutable.Map({ includeMeta: true, resolveMetaAliases: false }) + ).fold( + e => { + throw new Error(e); + }, + r => { + const value = r.getIn(["props", "TOKEN_A", "value"]); + const metaValue = r.getIn(["props", "TOKEN_A", "meta", "meta_test"]); + + expect(value).toEqual("#FF0000"); + expect(metaValue).toEqual("{!ALIAS_B}"); + done(); + } + ); +}); + it("returns an error for missing aliases values", done => { transform( getFixture(` diff --git a/lib/definition.js b/lib/definition.js index 135bcab..21684f2 100644 --- a/lib/definition.js +++ b/lib/definition.js @@ -43,6 +43,12 @@ const transform = (def, options) => { ? Either.of(def) : Either.try(resolveAliases)(def) ) + .chain( + def => + options.get("resolveMetaAliases") === false + ? Either.of(def) + : Either.try(resolveMetaAliases)(def) + ) .map(addPropName); /** * Merge the "global" object into each prop @@ -157,6 +163,7 @@ const transform = (def, options) => { .merge(props) ) .delete("imports"); + /** * Resolve aliases that refer to other aliases */ @@ -174,25 +181,57 @@ const transform = (def, options) => { ); return aliases.map(resolve); }); + /** * Resolve aliases inside prop values */ const resolveAliases = def => def.update("props", props => { const aliases = def.get("aliases", Immutable.Map()); - return props.map((value, key) => - value.update("value", v => - recursiveMap(v, v => { + + return props.map((value, key) => { + return value.update("value", v => { + return recursiveMap(v, v => { if (typeof v !== "string") return v; return allMatches(v, ALIAS_PATTERN).reduce((v, [alias, key]) => { if (!aliases.has(key)) throw new Error(`Alias "${key}" not found`); return v.replace(alias, aliases.getIn([key, "value"])); }, v); - }) - ) - ); + }); + }); + }); + }); + + /** + * Resolve aliases inside prop meta data + */ + const resolveMetaAliases = def => { + return def.update("props", data => { + const aliases = def.get("aliases", Immutable.Map()); + + return data.map((value, key) => { + if (Immutable.Map.isMap(value.get("meta"))) { + return value.update("meta", md => { + return recursiveMap(md, md => { + if (typeof md !== "string") return md; + return allMatches(md, ALIAS_PATTERN).reduce( + (md, [alias, key]) => { + if (!aliases.has(key)) + throw new Error(`Alias "${key}" not found`); + return md.replace(alias, aliases.getIn([key, "value"])); + }, + md + ); + }); + }); + } else { + return value; + } + }); }); + }; + /** * */ diff --git a/lib/index.js b/lib/index.js index dbac836..a198c7b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -117,10 +117,13 @@ const format = options => const convertSync = options => transform(options.transform) .chain(data => format(Object.assign({}, options.format, { data }))) - .fold(e => { - if (isString(e)) throw new Error(e); - throw e; - }, r => r); + .fold( + e => { + if (isString(e)) throw new Error(e); + throw e; + }, + r => r + ); const convert = options => Either.try(convertSync)(options).fold( diff --git a/package.json b/package.json index 6383348..eed7fc0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "theo", - "version": "8.0.1", + "version": "8.1.1", "license": "BSD-3-Clause", "description": "Design Tokens formatter", "keywords": [