From 18477c15333f54db2a81d745d95749329e9f3541 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Mon, 14 Mar 2022 20:14:57 +0100 Subject: [PATCH 01/32] design tokens --- package-lock.json | 18 ++ plugins/postcss-design-tokens/.gitignore | 6 + plugins/postcss-design-tokens/.nvmrc | 1 + plugins/postcss-design-tokens/.tape.mjs | 54 ++++++ plugins/postcss-design-tokens/CHANGELOG.md | 5 + plugins/postcss-design-tokens/INSTALL.md | 176 ++++++++++++++++++ plugins/postcss-design-tokens/LICENSE.md | 108 +++++++++++ plugins/postcss-design-tokens/README.md | 97 ++++++++++ plugins/postcss-design-tokens/docs/README.md | 51 +++++ plugins/postcss-design-tokens/package.json | 61 ++++++ plugins/postcss-design-tokens/src/index.ts | 52 ++++++ .../postcss-design-tokens/test/_import.mjs | 6 + .../postcss-design-tokens/test/_require.cjs | 6 + plugins/postcss-design-tokens/test/basic.css | 20 ++ .../test/basic.expect.css | 20 ++ .../test/examples/example.css | 20 ++ .../test/examples/example.expect.css | 7 + plugins/postcss-design-tokens/tsconfig.json | 9 + 18 files changed, 717 insertions(+) create mode 100644 plugins/postcss-design-tokens/.gitignore create mode 100644 plugins/postcss-design-tokens/.nvmrc create mode 100644 plugins/postcss-design-tokens/.tape.mjs create mode 100644 plugins/postcss-design-tokens/CHANGELOG.md create mode 100644 plugins/postcss-design-tokens/INSTALL.md create mode 100644 plugins/postcss-design-tokens/LICENSE.md create mode 100644 plugins/postcss-design-tokens/README.md create mode 100644 plugins/postcss-design-tokens/docs/README.md create mode 100644 plugins/postcss-design-tokens/package.json create mode 100644 plugins/postcss-design-tokens/src/index.ts create mode 100644 plugins/postcss-design-tokens/test/_import.mjs create mode 100644 plugins/postcss-design-tokens/test/_require.cjs create mode 100644 plugins/postcss-design-tokens/test/basic.css create mode 100644 plugins/postcss-design-tokens/test/basic.expect.css create mode 100644 plugins/postcss-design-tokens/test/examples/example.css create mode 100644 plugins/postcss-design-tokens/test/examples/example.expect.css create mode 100644 plugins/postcss-design-tokens/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 99c0679ce..6aa802dd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1766,6 +1766,10 @@ "resolved": "plugins/postcss-color-function", "link": true }, + "node_modules/@csstools/postcss-design-tokens": { + "resolved": "plugins/postcss-design-tokens", + "link": true + }, "node_modules/@csstools/postcss-font-format-keywords": { "resolved": "plugins/postcss-font-format-keywords", "link": true @@ -6725,6 +6729,16 @@ "postcss": "^8.4" } }, + "plugins/postcss-design-tokens": { + "version": "1.0.0", + "license": "CC0-1.0", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, "plugins/postcss-dir-pseudo-class": { "version": "6.0.4", "license": "CC0-1.0", @@ -8222,6 +8236,10 @@ "postcss-value-parser": "^4.2.0" } }, + "@csstools/postcss-design-tokens": { + "version": "file:plugins/postcss-design-tokens", + "requires": {} + }, "@csstools/postcss-font-format-keywords": { "version": "file:plugins/postcss-font-format-keywords", "requires": { diff --git a/plugins/postcss-design-tokens/.gitignore b/plugins/postcss-design-tokens/.gitignore new file mode 100644 index 000000000..7172b04f1 --- /dev/null +++ b/plugins/postcss-design-tokens/.gitignore @@ -0,0 +1,6 @@ +node_modules +package-lock.json +yarn.lock +*.result.css +*.result.css.map +dist/* diff --git a/plugins/postcss-design-tokens/.nvmrc b/plugins/postcss-design-tokens/.nvmrc new file mode 100644 index 000000000..f0b10f153 --- /dev/null +++ b/plugins/postcss-design-tokens/.nvmrc @@ -0,0 +1 @@ +v16.13.1 diff --git a/plugins/postcss-design-tokens/.tape.mjs b/plugins/postcss-design-tokens/.tape.mjs new file mode 100644 index 000000000..4e04f1f9b --- /dev/null +++ b/plugins/postcss-design-tokens/.tape.mjs @@ -0,0 +1,54 @@ +import postcssTape from '../../packages/postcss-tape/dist/index.mjs'; +import plugin from '@csstools/postcss-design-tokens'; + +postcssTape(plugin)({ + basic: { + message: "supports basic usage", + options: { + requireDesignTokens: { + properties: [ + 'color' + ] + }, + designTokens: { + selectors: [ + { + name: 'button', + value: ':is(.button, button[type="submit"])', + required: false, + deprecated: false, + } + ], + atSupports: [ + { + name: 'cover', + value: '(object-fit: cover)', + required: false, + deprecated: false, + } + ], + atMedia: [ + { + name: 'medium', + value: '(min-width: 768px)', + required: false, + deprecated: false + } + ], + values: [ + { + name: 'my-color', + value: '#f00', + required: false, + deprecated: false, + allowedProperties: [], + blockedProperties: [], + } + ] + } + } + }, + 'examples/example': { + message: 'minimal example', + }, +}); diff --git a/plugins/postcss-design-tokens/CHANGELOG.md b/plugins/postcss-design-tokens/CHANGELOG.md new file mode 100644 index 000000000..ea750358a --- /dev/null +++ b/plugins/postcss-design-tokens/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changes to PostCSS Design Tokens + +### 1.0.0 (Unreleased) + +- Initial version diff --git a/plugins/postcss-design-tokens/INSTALL.md b/plugins/postcss-design-tokens/INSTALL.md new file mode 100644 index 000000000..5a59c270a --- /dev/null +++ b/plugins/postcss-design-tokens/INSTALL.md @@ -0,0 +1,176 @@ +# Installing PostCSS Design Tokens + +[PostCSS Design Tokens] runs in all Node environments, with special instructions for: + +| [Node](#node) | [PostCSS CLI](#postcss-cli) | [Webpack](#webpack) | [Create React App](#create-react-app) | [Gulp](#gulp) | [Grunt](#grunt) | +| --- | --- | --- | --- | --- | --- | + +## Node + +Add [PostCSS Design Tokens] to your project: + +```bash +npm install postcss @csstools/postcss-design-tokens --save-dev +``` + +Use it as a [PostCSS] plugin: + +```js +const postcss = require('postcss'); +const postcssBasePlugin = require('@csstools/postcss-design-tokens'); + +postcss([ + postcssBasePlugin(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +## PostCSS CLI + +Add [PostCSS CLI] to your project: + +```bash +npm install postcss-cli @csstools/postcss-design-tokens --save-dev +``` + +Use [PostCSS Design Tokens] in your `postcss.config.js` configuration file: + +```js +const postcssBasePlugin = require('@csstools/postcss-design-tokens'); + +module.exports = { + plugins: [ + postcssBasePlugin(/* pluginOptions */) + ] +} +``` + +## Webpack + +_Webpack version 5_ + +Add [PostCSS Loader] to your project: + +```bash +npm install postcss-loader @csstools/postcss-design-tokens --save-dev +``` + +Use [PostCSS Design Tokens] in your Webpack configuration: + +```js +module.exports = { + module: { + rules: [ + { + test: /\.css$/i, + use: [ + "style-loader", + { + loader: "css-loader", + options: { importLoaders: 1 }, + }, + { + loader: "postcss-loader", + options: { + postcssOptions: { + plugins: [ + [ + "@csstools/postcss-design-tokens", + { + // Options + }, + ], + ], + }, + }, + }, + ], + }, + ], + }, +}; +``` + +## Create React App + +Add [React App Rewired] and [React App Rewire PostCSS] to your project: + +```bash +npm install react-app-rewired react-app-rewire-postcss @csstools/postcss-design-tokens --save-dev +``` + +Use [React App Rewire PostCSS] and [PostCSS Design Tokens] in your +`config-overrides.js` file: + +```js +const reactAppRewirePostcss = require('react-app-rewire-postcss'); +const postcssBasePlugin = require('@csstools/postcss-design-tokens'); + +module.exports = config => reactAppRewirePostcss(config, { + plugins: () => [ + postcssBasePlugin(/* pluginOptions */) + ] +}); +``` + +## Gulp + +Add [Gulp PostCSS] to your project: + +```bash +npm install gulp-postcss @csstools/postcss-design-tokens --save-dev +``` + +Use [PostCSS Design Tokens] in your Gulpfile: + +```js +const postcss = require('gulp-postcss'); +const postcssBasePlugin = require('@csstools/postcss-design-tokens'); + +gulp.task('css', function () { + var plugins = [ + postcssBasePlugin(/* pluginOptions */) + ]; + + return gulp.src('./src/*.css') + .pipe(postcss(plugins)) + .pipe(gulp.dest('.')); +}); +``` + +## Grunt + +Add [Grunt PostCSS] to your project: + +```bash +npm install grunt-postcss @csstools/postcss-design-tokens --save-dev +``` + +Use [PostCSS Design Tokens] in your Gruntfile: + +```js +const postcssBasePlugin = require('@csstools/postcss-design-tokens'); + +grunt.loadNpmTasks('grunt-postcss'); + +grunt.initConfig({ + postcss: { + options: { + processors: [ + postcssBasePlugin(/* pluginOptions */) + ] + }, + dist: { + src: '*.css' + } + } +}); +``` + +[Gulp PostCSS]: https://github.com/postcss/gulp-postcss +[Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss +[PostCSS]: https://github.com/postcss/postcss +[PostCSS CLI]: https://github.com/postcss/postcss-cli +[PostCSS Loader]: https://github.com/postcss/postcss-loader +[PostCSS Design Tokens]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-design-tokens +[React App Rewire PostCSS]: https://github.com/csstools/react-app-rewire-postcss +[React App Rewired]: https://github.com/timarney/react-app-rewired diff --git a/plugins/postcss-design-tokens/LICENSE.md b/plugins/postcss-design-tokens/LICENSE.md new file mode 100644 index 000000000..0bc1fa706 --- /dev/null +++ b/plugins/postcss-design-tokens/LICENSE.md @@ -0,0 +1,108 @@ +# CC0 1.0 Universal + +## Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an “owner”) of an original work of +authorship and/or a database (each, a “Work”). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific works +(“Commons”) that the public can reliably and without fear of later claims of +infringement build upon, modify, incorporate in other works, reuse and +redistribute as freely as possible in any form whatsoever and for any purposes, +including without limitation commercial purposes. These owners may contribute +to the Commons to promote the ideal of a free culture and the further +production of creative, cultural and scientific works, or to gain reputation or +greater distribution for their Work in part through the use and efforts of +others. + +For these and/or other purposes and motivations, and without any expectation of +additional consideration or compensation, the person associating CC0 with a +Work (the “Affirmer”), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and +publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be + protected by copyright and related or neighboring rights (“Copyright and + Related Rights”). Copyright and Related Rights include, but are not limited + to, the following: + 1. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + 2. moral rights retained by the original author(s) and/or performer(s); + 3. publicity and privacy rights pertaining to a person’s image or likeness + depicted in a Work; + 4. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(i), below; + 5. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + 6. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + 7. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations + thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, + applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and + unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright + and Related Rights and associated claims and causes of action, whether now + known or unknown (including existing as well as future claims and causes of + action), in the Work (i) in all territories worldwide, (ii) for the maximum + duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the “Waiver”). Affirmer + makes the Waiver for the benefit of each member of the public at large and + to the detriment of Affirmer’s heirs and successors, fully intending that + such Waiver shall not be subject to revocation, rescission, cancellation, + termination, or any other legal or equitable action to disrupt the quiet + enjoyment of the Work by the public as contemplated by Affirmer’s express + Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be + judged legally invalid or ineffective under applicable law, then the Waiver + shall be preserved to the maximum extent permitted taking into account + Affirmer’s express Statement of Purpose. In addition, to the extent the + Waiver is so judged Affirmer hereby grants to each affected person a + royalty-free, non transferable, non sublicensable, non exclusive, + irrevocable and unconditional license to exercise Affirmer’s Copyright and + Related Rights in the Work (i) in all territories worldwide, (ii) for the + maximum duration provided by applicable law or treaty (including future time + extensions), (iii) in any current or future medium and for any number of + copies, and (iv) for any purpose whatsoever, including without limitation + commercial, advertising or promotional purposes (the “License”). The License + shall be deemed effective as of the date CC0 was applied by Affirmer to the + Work. Should any part of the License for any reason be judged legally + invalid or ineffective under applicable law, such partial invalidity or + ineffectiveness shall not invalidate the remainder of the License, and in + such case Affirmer hereby affirms that he or she will not (i) exercise any + of his or her remaining Copyright and Related Rights in the Work or (ii) + assert any associated claims and causes of action with respect to the Work, + in either case contrary to Affirmer’s express Statement of Purpose. + +4. Limitations and Disclaimers. + 1. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + 2. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or + otherwise, including without limitation warranties of title, + merchantability, fitness for a particular purpose, non infringement, or + the absence of latent or other defects, accuracy, or the present or + absence of errors, whether or not discoverable, all to the greatest + extent permissible under applicable law. + 3. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person’s Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the Work. + 4. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see +http://creativecommons.org/publicdomain/zero/1.0/. diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md new file mode 100644 index 000000000..92fa6dd7b --- /dev/null +++ b/plugins/postcss-design-tokens/README.md @@ -0,0 +1,97 @@ +# PostCSS Design Tokens [PostCSS Logo][postcss] + +[npm version][npm-url] +[CSS Standard Status][css-url] +[Build Status][cli-url] +[Discord][discord] + +[PostCSS Design Tokens] lets easily create new plugins following some [CSS Specification]. + +```pcss +.foo { + color: red; +} + +.baz { + color: green; +} + +/* becomes */ + +.foo { + color: blue; +} + +.baz { + color: green; +} +``` + +## Usage + +Add [PostCSS Design Tokens] to your project: + +```bash +npm install postcss @csstools/postcss-design-tokens --save-dev +``` + +Use it as a [PostCSS] plugin: + +```js +const postcss = require('postcss'); +const postcssBasePlugin = require('@csstools/postcss-design-tokens'); + +postcss([ + postcssBasePlugin(/* pluginOptions */) +]).process(YOUR_CSS /*, processOptions */); +``` + +[PostCSS Design Tokens] runs in all Node environments, with special +instructions for: + +| [Node](INSTALL.md#node) | [PostCSS CLI](INSTALL.md#postcss-cli) | [Webpack](INSTALL.md#webpack) | [Create React App](INSTALL.md#create-react-app) | [Gulp](INSTALL.md#gulp) | [Grunt](INSTALL.md#grunt) | +| --- | --- | --- | --- | --- | --- | + +## Options + +### preserve + +The `preserve` option determines whether the original notation +is preserved. By default, it is not preserved. + +```js +postcssBasePlugin({ preserve: true }) +``` + +```pcss +.foo { + color: red; +} + +.baz { + color: green; +} + +/* becomes */ + +.foo { + color: blue; + color: red; +} + +.baz { + color: green; +} +``` + +[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test +[css-url]: https://cssdb.org/#TODO +[discord]: https://discord.gg/bUadyRwkJS +[npm-url]: https://www.npmjs.com/package/@csstools/postcss-design-tokens + +[Gulp PostCSS]: https://github.com/postcss/gulp-postcss +[Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss +[PostCSS]: https://github.com/postcss/postcss +[PostCSS Loader]: https://github.com/postcss/postcss-loader +[PostCSS Design Tokens]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-design-tokens +[CSS Specification]: #TODO diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md new file mode 100644 index 000000000..3bc455892 --- /dev/null +++ b/plugins/postcss-design-tokens/docs/README.md @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + +
+ +[] lets easily create new plugins following some [CSS Specification]. + +```pcss + + +/* becomes */ + + +``` + + + + + +## Options + +### preserve + +The `preserve` option determines whether the original notation +is preserved. By default, it is not preserved. + +```js +({ preserve: true }) +``` + +```pcss + + +/* becomes */ + + +``` + + +[CSS Specification]: diff --git a/plugins/postcss-design-tokens/package.json b/plugins/postcss-design-tokens/package.json new file mode 100644 index 000000000..f146c70f0 --- /dev/null +++ b/plugins/postcss-design-tokens/package.json @@ -0,0 +1,61 @@ +{ + "name": "@csstools/postcss-design-tokens", + "description": "TODO: Add description for Design Tokens", + "version": "1.0.0", + "author": "Jonathan Neal ", + "license": "CC0-1.0", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "main": "dist/index.cjs", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.cjs", + "default": "./dist/index.mjs" + } + }, + "files": [ + "CHANGELOG.md", + "LICENSE.md", + "README.md", + "dist" + ], + "peerDependencies": { + "postcss": "^8.3" + }, + "scripts": { + "build": "rollup -c ../../rollup/default.js", + "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true });\"", + "docs": "node ../../.github/bin/generate-docs/install.mjs && node ../../.github/bin/generate-docs/readme.mjs", + "lint": "npm run lint:eslint && npm run lint:package-json", + "lint:eslint": "eslint ./src --ext .js --ext .ts --ext .mjs --no-error-on-unmatched-pattern", + "lint:package-json": "node ../../.github/bin/format-package-json.mjs", + "prepublishOnly": "npm run clean && npm run build && npm run test", + "stryker": "stryker run --logLevel error", + "test": "node .tape.mjs && npm run test:exports", + "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs", + "test:rewrite-expects": "REWRITE_EXPECTS=true node .tape.mjs" + }, + "repository": { + "type": "git", + "url": "https://github.com/csstools/postcss-plugins.git", + "directory": "plugins/postcss-design-tokens" + }, + "keywords": [ + "postcss-plugin" + ], + "csstools": { + "cssdbId": "TODO", + "exportName": "postcssDesignTokens", + "humanReadableName": "PostCSS Design Tokens", + "specUrl": "#TODO" + }, + "volta": { + "extends": "../../package.json" + }, + "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-design-tokens#readme", + "bugs": "https://github.com/csstools/postcss-plugins/issues" +} \ No newline at end of file diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts new file mode 100644 index 000000000..d4d6e3bbd --- /dev/null +++ b/plugins/postcss-design-tokens/src/index.ts @@ -0,0 +1,52 @@ +import type { PluginCreator } from 'postcss'; + +type pluginOptions = { color?: string, preserve?: boolean }; + +const creator: PluginCreator = (opts?: pluginOptions) => { + const options = Object.assign( + // Default options + { + color: null, + preserve: false, + }, + // Provided options + opts, + ); + + return { + postcssPlugin: 'postcss-design-tokens', + Declaration(decl) { + if (decl.value === 'red') { + // Determine the new value. + let newValue = 'blue'; + if (options.color) { + newValue = options.color; + } + + // Check if it is different from the current value. + if (newValue === decl.value) { + return; + } + + // Insert the new value before the current value. + decl.cloneBefore({ + prop: 'color', + value: newValue, + }); + + // If the current value is preserved we are done and return here. + if (options.preserve) { + return; + } + + // If the current value is not preserved we remove it. + decl.remove(); + } + }, + }; +}; + +creator.postcss = true; + +export default creator; + diff --git a/plugins/postcss-design-tokens/test/_import.mjs b/plugins/postcss-design-tokens/test/_import.mjs new file mode 100644 index 000000000..97e23a010 --- /dev/null +++ b/plugins/postcss-design-tokens/test/_import.mjs @@ -0,0 +1,6 @@ +import assert from 'assert'; +import plugin from '@csstools/postcss-design-tokens'; +plugin(); + +assert.ok(plugin.postcss, 'should have "postcss flag"'); +assert.equal(typeof plugin, 'function', 'should return a function'); diff --git a/plugins/postcss-design-tokens/test/_require.cjs b/plugins/postcss-design-tokens/test/_require.cjs new file mode 100644 index 000000000..e48299ce9 --- /dev/null +++ b/plugins/postcss-design-tokens/test/_require.cjs @@ -0,0 +1,6 @@ +const assert = require('assert'); +const plugin = require('@csstools/postcss-design-tokens'); +plugin(); + +assert.ok(plugin.postcss, 'should have "postcss flag"'); +assert.equal(typeof plugin, 'function', 'should return a function'); diff --git a/plugins/postcss-design-tokens/test/basic.css b/plugins/postcss-design-tokens/test/basic.css new file mode 100644 index 000000000..e77d47cc2 --- /dev/null +++ b/plugins/postcss-design-tokens/test/basic.css @@ -0,0 +1,20 @@ +.foo { + color: design-token(color); +} + +@media (design-token: medium) { + .baz { + color: green; + } +} + +@supports design-token(cover) { + .baz { + color: green; + object-fit: cover; + } +} + +:design-token(button) { + color: green; +} diff --git a/plugins/postcss-design-tokens/test/basic.expect.css b/plugins/postcss-design-tokens/test/basic.expect.css new file mode 100644 index 000000000..f2e42303e --- /dev/null +++ b/plugins/postcss-design-tokens/test/basic.expect.css @@ -0,0 +1,20 @@ +.foo { + color: #f00; +} + +@media (min-width: 768px) { + .baz { + color: green; + } +} + +@supports (object-fit: cover) { + .baz { + color: green; + object-fit: cover; + } +} + +:is(.button, button[type="submit"]) { + color: green; +} diff --git a/plugins/postcss-design-tokens/test/examples/example.css b/plugins/postcss-design-tokens/test/examples/example.css new file mode 100644 index 000000000..e77d47cc2 --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/example.css @@ -0,0 +1,20 @@ +.foo { + color: design-token(color); +} + +@media (design-token: medium) { + .baz { + color: green; + } +} + +@supports design-token(cover) { + .baz { + color: green; + object-fit: cover; + } +} + +:design-token(button) { + color: green; +} diff --git a/plugins/postcss-design-tokens/test/examples/example.expect.css b/plugins/postcss-design-tokens/test/examples/example.expect.css new file mode 100644 index 000000000..9d738d5ac --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/example.expect.css @@ -0,0 +1,7 @@ +.foo { + color: blue; +} + +.baz { + color: green; +} diff --git a/plugins/postcss-design-tokens/tsconfig.json b/plugins/postcss-design-tokens/tsconfig.json new file mode 100644 index 000000000..e0d06239c --- /dev/null +++ b/plugins/postcss-design-tokens/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declarationDir": "." + }, + "include": ["./src/**/*"], + "exclude": ["dist"], +} From 83bf59b05048276953159a884ee950fe71a210a0 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Tue, 15 Mar 2022 14:28:27 +0100 Subject: [PATCH 02/32] wip --- plugins/postcss-design-tokens/.tape.mjs | 61 +++++++- plugins/postcss-design-tokens/package.json | 5 +- plugins/postcss-design-tokens/src/index.ts | 99 +++++++----- plugins/postcss-design-tokens/src/options.ts | 145 ++++++++++++++++++ plugins/postcss-design-tokens/src/values.ts | 123 +++++++++++++++ plugins/postcss-design-tokens/test/basic.css | 9 +- .../test/basic.expect.css | 13 +- .../test/examples/example.css | 9 +- .../test/examples/example.expect.css | 20 ++- 9 files changed, 423 insertions(+), 61 deletions(-) create mode 100644 plugins/postcss-design-tokens/src/options.ts create mode 100644 plugins/postcss-design-tokens/src/values.ts diff --git a/plugins/postcss-design-tokens/.tape.mjs b/plugins/postcss-design-tokens/.tape.mjs index 4e04f1f9b..6fd977c66 100644 --- a/plugins/postcss-design-tokens/.tape.mjs +++ b/plugins/postcss-design-tokens/.tape.mjs @@ -5,7 +5,7 @@ postcssTape(plugin)({ basic: { message: "supports basic usage", options: { - requireDesignTokens: { + requiresDesignTokens: { properties: [ 'color' ] @@ -15,7 +15,6 @@ postcssTape(plugin)({ { name: 'button', value: ':is(.button, button[type="submit"])', - required: false, deprecated: false, } ], @@ -23,7 +22,6 @@ postcssTape(plugin)({ { name: 'cover', value: '(object-fit: cover)', - required: false, deprecated: false, } ], @@ -31,15 +29,20 @@ postcssTape(plugin)({ { name: 'medium', value: '(min-width: 768px)', - required: false, deprecated: false } ], values: [ { - name: 'my-color', + name: 'color', value: '#f00', - required: false, + deprecated: false, + allowedProperties: [], + blockedProperties: [], + }, + { + name: 'red-components', + value: '255, 0, 0', deprecated: false, allowedProperties: [], blockedProperties: [], @@ -50,5 +53,51 @@ postcssTape(plugin)({ }, 'examples/example': { message: 'minimal example', + options: { + requiresDesignTokens: { + properties: [ + 'color' + ] + }, + designTokens: { + selectors: [ + { + name: 'button', + value: ':is(.button, button[type="submit"])', + deprecated: false, + } + ], + atSupports: [ + { + name: 'cover', + value: '(object-fit: cover)', + deprecated: false, + } + ], + atMedia: [ + { + name: 'medium', + value: '(min-width: 768px)', + deprecated: false + } + ], + values: [ + { + name: 'color', + value: '#f00', + deprecated: false, + allowedProperties: [], + blockedProperties: [], + }, + { + name: 'red-components', + value: '255, 0, 0', + deprecated: false, + allowedProperties: [], + blockedProperties: [], + } + ] + } + } }, }); diff --git a/plugins/postcss-design-tokens/package.json b/plugins/postcss-design-tokens/package.json index f146c70f0..0341324ed 100644 --- a/plugins/postcss-design-tokens/package.json +++ b/plugins/postcss-design-tokens/package.json @@ -23,6 +23,9 @@ "README.md", "dist" ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, "peerDependencies": { "postcss": "^8.3" }, @@ -58,4 +61,4 @@ }, "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-design-tokens#readme", "bugs": "https://github.com/csstools/postcss-plugins/issues" -} \ No newline at end of file +} diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts index d4d6e3bbd..d2f6e787b 100644 --- a/plugins/postcss-design-tokens/src/index.ts +++ b/plugins/postcss-design-tokens/src/index.ts @@ -1,47 +1,72 @@ import type { PluginCreator } from 'postcss'; +import { AtMediaOptions, AtSupportsOptions, SelectorOptions, validateOptions, ValueOptions } from './options'; +import { onCSSValue, onCSSValueRequiredDesignToken, onCSSValueUnknownDesignToken } from './values'; -type pluginOptions = { color?: string, preserve?: boolean }; +type pluginOptions = { + requiresDesignTokens?: { + properties: Array, + }, + designTokens?: { + atMedia?: Array, + atSupports?: Array, + selectors?: Array, + values?: Array + } +}; const creator: PluginCreator = (opts?: pluginOptions) => { - const options = Object.assign( - // Default options - { - color: null, - preserve: false, - }, - // Provided options - opts, - ); + const config = validateOptions(opts); return { postcssPlugin: 'postcss-design-tokens', - Declaration(decl) { - if (decl.value === 'red') { - // Determine the new value. - let newValue = 'blue'; - if (options.color) { - newValue = options.color; - } - - // Check if it is different from the current value. - if (newValue === decl.value) { - return; - } - - // Insert the new value before the current value. - decl.cloneBefore({ - prop: 'color', - value: newValue, - }); - - // If the current value is preserved we are done and return here. - if (options.preserve) { - return; - } - - // If the current value is not preserved we remove it. - decl.remove(); - } + prepare() { + return { + Once(root, { result }) { + + if (config.requiresDesignTokens.properties.size > 0) { + // Emit warnings for properties that use custom values when not allowed. + + root.walkDecls(decl => { + if (config.requiresDesignTokens.properties.has(decl.prop)) { + if (decl.value.indexOf('design-token') === -1) { + decl.warn(result, `"${decl.prop}" must always use design tokens.`); + return; + } + + const modifiedValue = onCSSValueRequiredDesignToken(config, result, decl); + if (modifiedValue === false || modifiedValue === decl.value) { + decl.warn(result, `"${decl.prop}" must always use design tokens.`); + return; + } + + decl.value = modifiedValue; + } + }); + } + }, + OnceExit(root, { result }) { + // Emit warnings if design tokens remain after processing. + root.walkDecls(decl => { + if (decl.value.indexOf('design-token') === -1) { + return; + } + + onCSSValueUnknownDesignToken(config, result, decl); + }); + }, + Declaration(decl, { result }) { + if (decl.value.indexOf('design-token') === -1) { + return; + } + + const modifiedValue = onCSSValue(config, result, decl); + if (modifiedValue === decl.value) { + return; + } + + decl.value = modifiedValue; + }, + }; }, }; }; diff --git a/plugins/postcss-design-tokens/src/options.ts b/plugins/postcss-design-tokens/src/options.ts new file mode 100644 index 000000000..d4a0d6388 --- /dev/null +++ b/plugins/postcss-design-tokens/src/options.ts @@ -0,0 +1,145 @@ +// Public API vs. Internal usage. +// The public API has a lot of optional parameters and simpler typing like "Array". +// This is compatible with JSON. +// +// The internal usage typings do not have any optional parameters and use more powerful types like "Map" and "Set". + +// Public API +type TokenOptions = { + name: string, + value: string, + deprecated?: boolean, +} + +export type AtMediaOptions = TokenOptions; +export type AtSupportsOptions = TokenOptions; +export type SelectorOptions = TokenOptions; + +export type ValueOptions = { + name: string, + value: string, + deprecated?: boolean, + allowedProperties?: Array, + blockedProperties?: Array, +} + +// Internal usage +type TokenConfig = { + name: string, + value: string, + deprecated: boolean, +} + +export type AtMediaConfig = TokenConfig; +export type AtSupportsConfig = TokenConfig; +export type SelectorConfig = TokenConfig; + +export type ValueConfig = { + name: string, + value: string, + deprecated: boolean, + allowedProperties: Set, + blockedProperties: Set, +} + +function validateSubOptions(rawSubOptions: unknown): { name: string, value: string, deprecated: boolean } { + const subOptions = Object(rawSubOptions); + return { + name: (typeof subOptions.name === 'string') ? subOptions.name : '', + value: (typeof subOptions.value === 'string') ? subOptions.value : '', + deprecated: (typeof subOptions.deprecated === 'boolean') ? subOptions.deprecated : false, + }; +} + +export type Config = { + requiresDesignTokens: { + properties: Set, + }, + designTokens: { + atMedia: Map, + atSupports: Map, + selectors: Map, + values: Map + } +}; + +export function validateOptions(rawOptions: unknown) { + const config: Config = { + requiresDesignTokens: { + properties: new Set(), + }, + designTokens: { + atMedia: new Map(), + atSupports: new Map(), + selectors: new Map(), + values: new Map(), + }, + }; + + const requiresDesignTokens = Object(rawOptions).requiresDesignTokens; + if (requiresDesignTokens && Array.isArray(Object(requiresDesignTokens).properties)) { + requiresDesignTokens.properties.forEach((x) => { + if (typeof x === 'string') { + config.requiresDesignTokens.properties.add(x); + } + }); + } + + const designTokens = Object(rawOptions).designTokens; + if (designTokens) { + if (Array.isArray(Object(designTokens).atMedia)) { + designTokens.atMedia.map((x) => { + return validateSubOptions(x); + }).filter((x: TokenConfig) => { + return x.name !== '' && x.value !== ''; + }).forEach((x: TokenConfig) => { + config.designTokens.atMedia.set(x.name, x); + }); + } + + if (Array.isArray(Object(designTokens).atSupports)) { + designTokens.atSupports.map((x) => { + return validateSubOptions(x); + }).filter((x: TokenConfig) => { + return x.name !== '' && x.value !== ''; + }).forEach((x: TokenConfig) => { + config.designTokens.atSupports.set(x.name, x); + }); + } + + if (Array.isArray(Object(designTokens).selectors)) { + designTokens.selectors.map((x) => { + return validateSubOptions(x); + }).filter((x: TokenConfig) => { + return x.name !== '' && x.value !== ''; + }).forEach((x: TokenConfig) => { + config.designTokens.selectors.set(x.name, x); + }); + } + + if (Array.isArray(Object(designTokens).values)) { + designTokens.values.map((x) => { + const allowedProperties : Array = (Array.isArray(Object(x).allowedProperties)) ? x.allowedProperties.filter((y) => { + return typeof y === 'string'; + }) : []; + const blockedProperties: Array = (Array.isArray(Object(x).blockedProperties)) ? x.blockedProperties.filter((y) => { + return typeof y === 'string'; + }) : []; + + const xx: ValueConfig = { + ...validateSubOptions(x), + allowedProperties: new Set(allowedProperties), + blockedProperties: new Set(blockedProperties), + }; + + return xx; + }).filter((x: ValueConfig) => { + return x.name !== '' && x.value !== ''; + }).forEach((x: ValueConfig) => { + config.designTokens.values.set(x.name, x); + }); + } + } + + return config; +} diff --git a/plugins/postcss-design-tokens/src/values.ts b/plugins/postcss-design-tokens/src/values.ts new file mode 100644 index 000000000..829f5e1de --- /dev/null +++ b/plugins/postcss-design-tokens/src/values.ts @@ -0,0 +1,123 @@ +import type { Declaration, Result } from 'postcss'; +import valuesParser from 'postcss-value-parser'; +import { Config } from './options'; + +export function onCSSValue(config: Config, result: Result, decl: Declaration) { + const valueAST = valuesParser(decl.value); + + valueAST.walk(node => { + if (node.type !== 'function' || node.value !== 'design-token') { + return; + } + + if (!node.nodes || node.nodes.length !== 1) { + decl.warn(result, 'Expected a single keyword for the design-token function.'); + return; + } + + if (node.nodes[0].type !== 'word') { + decl.warn(result, 'Expected a single keyword for the design-token function.'); + return; + } + + const replacement = config.designTokens.values.get(node.nodes[0].value); + if (!replacement) { + return; + } + + if (replacement.deprecated) { + decl.warn(result, `design-token: "${replacement.name}" is deprecated.`); + } + + if (replacement.allowedProperties.size && !replacement.allowedProperties.has(decl.prop)) { + decl.warn(result, `design-token: "${replacement.name}" is not allowed for "${decl.prop}".`); + return; + } + + if (replacement.blockedProperties.size && replacement.blockedProperties.has(decl.prop)) { + decl.warn(result, `design-token: "${replacement.name}" is not allowed for "${decl.prop}".`); + return; + } + + node.value = replacement.value; + node.nodes = undefined; + }); + + return String(valueAST); +} + +export function onCSSValueRequiredDesignToken(config: Config, result: Result, decl: Declaration) { + const valueAST = valuesParser(decl.value); + + let hasNonDesignTokens = false; + + valueAST.nodes.forEach((node) => { + // Allowed, even when the property requires design tokens. + if (node.type === 'comment' || node.type === 'space' || node.type === 'div') { + return; + } + + if (node.type !== 'function' || node.value !== 'design-token') { + hasNonDesignTokens = true; + return; + } + + if (!node.nodes || node.nodes.length !== 1) { + decl.warn(result, 'Expected a single keyword for the design-token function.'); + return false; + } + + if (node.nodes[0].type !== 'word') { + decl.warn(result, 'Expected a single keyword for the design-token function.'); + return false; + } + + const replacement = config.designTokens.values.get(node.nodes[0].value); + if (!replacement) { + return; + } + + if (replacement.deprecated) { + decl.warn(result, `design-token: "${replacement.name}" is deprecated.`); + } + + if (replacement.allowedProperties.size && !replacement.allowedProperties.has(decl.prop)) { + decl.warn(result, `design-token: "${replacement.name}" is not allowed for "${decl.prop}".`); + return false; + } + + if (replacement.blockedProperties.size && replacement.blockedProperties.has(decl.prop)) { + decl.warn(result, `design-token: "${replacement.name}" is not allowed for "${decl.prop}".`); + return false; + } + + node.value = replacement.value; + node.nodes = undefined; + }); + + if (hasNonDesignTokens) { + return false; + } + + return String(valueAST); +} + +export function onCSSValueUnknownDesignToken(config: Config, result: Result, decl: Declaration) { + const valueAST = valuesParser(decl.value); + + valueAST.walk(node => { + if (node.type !== 'function' || node.value !== 'design-token') { + return; + } + + if (!node.nodes || node.nodes.length !== 1) { + return; + } + + if (node.nodes[0].type !== 'word') { + return; + } + + decl.warn(result, `design-token: "${node.nodes[0].value}" is not configured.`); + }); +} diff --git a/plugins/postcss-design-tokens/test/basic.css b/plugins/postcss-design-tokens/test/basic.css index e77d47cc2..5cd4316d0 100644 --- a/plugins/postcss-design-tokens/test/basic.css +++ b/plugins/postcss-design-tokens/test/basic.css @@ -4,17 +4,18 @@ @media (design-token: medium) { .baz { - color: green; + color: design-token(color); + background-color: rgb(design-token(red-components)); } } -@supports design-token(cover) { +@supports (design-token: cover) { .baz { - color: green; + color: design-token(color); object-fit: cover; } } :design-token(button) { - color: green; + color: design-token(color); } diff --git a/plugins/postcss-design-tokens/test/basic.expect.css b/plugins/postcss-design-tokens/test/basic.expect.css index f2e42303e..924fcf346 100644 --- a/plugins/postcss-design-tokens/test/basic.expect.css +++ b/plugins/postcss-design-tokens/test/basic.expect.css @@ -2,19 +2,20 @@ color: #f00; } -@media (min-width: 768px) { +@media (design-token: medium) { .baz { - color: green; + color: #f00; + background-color: rgb(255, 0, 0); } } -@supports (object-fit: cover) { +@supports (design-token: cover) { .baz { - color: green; + color: #f00; object-fit: cover; } } -:is(.button, button[type="submit"]) { - color: green; +:design-token(button) { + color: #f00; } diff --git a/plugins/postcss-design-tokens/test/examples/example.css b/plugins/postcss-design-tokens/test/examples/example.css index e77d47cc2..5cd4316d0 100644 --- a/plugins/postcss-design-tokens/test/examples/example.css +++ b/plugins/postcss-design-tokens/test/examples/example.css @@ -4,17 +4,18 @@ @media (design-token: medium) { .baz { - color: green; + color: design-token(color); + background-color: rgb(design-token(red-components)); } } -@supports design-token(cover) { +@supports (design-token: cover) { .baz { - color: green; + color: design-token(color); object-fit: cover; } } :design-token(button) { - color: green; + color: design-token(color); } diff --git a/plugins/postcss-design-tokens/test/examples/example.expect.css b/plugins/postcss-design-tokens/test/examples/example.expect.css index 9d738d5ac..924fcf346 100644 --- a/plugins/postcss-design-tokens/test/examples/example.expect.css +++ b/plugins/postcss-design-tokens/test/examples/example.expect.css @@ -1,7 +1,21 @@ .foo { - color: blue; + color: #f00; } -.baz { - color: green; +@media (design-token: medium) { + .baz { + color: #f00; + background-color: rgb(255, 0, 0); + } +} + +@supports (design-token: cover) { + .baz { + color: #f00; + object-fit: cover; + } +} + +:design-token(button) { + color: #f00; } From 522c2a639fc405afc02a00aba70d11f393dc2fff Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 27 Mar 2022 11:22:50 +0200 Subject: [PATCH 03/32] wip --- package-lock.json | 38 ++++- plugins/postcss-design-tokens/.tape.mjs | 105 ++----------- plugins/postcss-design-tokens/INSTALL.md | 20 +-- plugins/postcss-design-tokens/README.md | 67 ++++---- plugins/postcss-design-tokens/docs/README.md | 18 ++- plugins/postcss-design-tokens/package.json | 4 + .../src/data-formats/base/token.ts | 3 + .../design-tokens-specification/type.ts | 14 ++ .../design-tokens-specification/value.ts | 86 +++++++++++ .../src/data-formats/parse-import.ts | 79 ++++++++++ .../style-dictionary/style-dictionary.ts | 13 ++ .../data-formats/style-dictionary/v3/group.ts | 37 +++++ .../data-formats/style-dictionary/v3/value.ts | 48 ++++++ .../src/data-formats/token.ts | 9 ++ plugins/postcss-design-tokens/src/index.ts | 73 ++++----- plugins/postcss-design-tokens/src/options.ts | 145 ------------------ plugins/postcss-design-tokens/src/values.ts | 109 ++----------- plugins/postcss-design-tokens/test/basic.css | 25 +-- .../test/basic.expect.css | 22 +-- .../test/examples/example.css | 23 +-- .../test/examples/example.dark.expect.css | 5 + .../test/examples/example.expect.css | 22 +-- .../test/examples/tokens-dark.json | 7 + .../test/examples/tokens-light.json | 7 + .../postcss-design-tokens/test/imported.css | 1 + .../postcss-design-tokens/test/tokens.json | 7 + plugins/postcss-design-tokens/tsconfig.json | 3 +- rollup/presets/package-typescript.js | 2 +- 28 files changed, 487 insertions(+), 505 deletions(-) create mode 100644 plugins/postcss-design-tokens/src/data-formats/base/token.ts create mode 100644 plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/type.ts create mode 100644 plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/value.ts create mode 100644 plugins/postcss-design-tokens/src/data-formats/parse-import.ts create mode 100644 plugins/postcss-design-tokens/src/data-formats/style-dictionary/style-dictionary.ts create mode 100644 plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/group.ts create mode 100644 plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts create mode 100644 plugins/postcss-design-tokens/src/data-formats/token.ts delete mode 100644 plugins/postcss-design-tokens/src/options.ts create mode 100644 plugins/postcss-design-tokens/test/examples/example.dark.expect.css create mode 100644 plugins/postcss-design-tokens/test/examples/tokens-dark.json create mode 100644 plugins/postcss-design-tokens/test/examples/tokens-light.json create mode 100644 plugins/postcss-design-tokens/test/imported.css create mode 100644 plugins/postcss-design-tokens/test/tokens.json diff --git a/package-lock.json b/package-lock.json index ecaa4113f..e64d6cfcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5255,9 +5255,9 @@ "link": true }, "node_modules/postcss-import": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.0.2.tgz", - "integrity": "sha512-BJ2pVK4KhUyMcqjuKs9RijV5tatNzNa73e/32aBVE/ejYPe37iH+6vAu9WvqUkB5OAYgLHzbSvzHnorybJCm9g==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", "dev": true, "dependencies": { "postcss-value-parser": "^4.0.0", @@ -5998,6 +5998,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-dictionary-design-tokens-example": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/style-dictionary-design-tokens-example/-/style-dictionary-design-tokens-example-1.0.0.tgz", + "integrity": "sha512-dUjgpSbx4uOAj3qZ1Tja3Vur3w+/UbVlT7JW2aAH3U7w8Hqm7nKwE0dCVU8O94sSFCs0jsdfPlvcYsEIIzLtvA==", + "dev": true + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -6696,8 +6702,16 @@ } }, "plugins/postcss-design-tokens": { + "name": "@csstools/postcss-design-tokens", "version": "1.0.0", "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "devDependencies": { + "postcss-import": "^14.1.0", + "style-dictionary-design-tokens-example": "^1.0.0" + }, "engines": { "node": "^12 || ^14 || >=16" }, @@ -8292,7 +8306,11 @@ }, "@csstools/postcss-design-tokens": { "version": "file:plugins/postcss-design-tokens", - "requires": {} + "requires": { + "postcss-import": "^14.1.0", + "postcss-value-parser": "^4.2.0", + "style-dictionary-design-tokens-example": "^1.0.0" + } }, "@csstools/postcss-font-format-keywords": { "version": "file:plugins/postcss-font-format-keywords", @@ -10880,9 +10898,9 @@ } }, "postcss-import": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.0.2.tgz", - "integrity": "sha512-BJ2pVK4KhUyMcqjuKs9RijV5tatNzNa73e/32aBVE/ejYPe37iH+6vAu9WvqUkB5OAYgLHzbSvzHnorybJCm9g==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", "dev": true, "requires": { "postcss-value-parser": "^4.0.0", @@ -11471,6 +11489,12 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "style-dictionary-design-tokens-example": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/style-dictionary-design-tokens-example/-/style-dictionary-design-tokens-example-1.0.0.tgz", + "integrity": "sha512-dUjgpSbx4uOAj3qZ1Tja3Vur3w+/UbVlT7JW2aAH3U7w8Hqm7nKwE0dCVU8O94sSFCs0jsdfPlvcYsEIIzLtvA==", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/plugins/postcss-design-tokens/.tape.mjs b/plugins/postcss-design-tokens/.tape.mjs index 6fd977c66..e18f44cb5 100644 --- a/plugins/postcss-design-tokens/.tape.mjs +++ b/plugins/postcss-design-tokens/.tape.mjs @@ -1,103 +1,26 @@ import postcssTape from '../../packages/postcss-tape/dist/index.mjs'; import plugin from '@csstools/postcss-design-tokens'; +import postcssImport from 'postcss-import'; postcssTape(plugin)({ basic: { message: "supports basic usage", - options: { - requiresDesignTokens: { - properties: [ - 'color' - ] - }, - designTokens: { - selectors: [ - { - name: 'button', - value: ':is(.button, button[type="submit"])', - deprecated: false, - } - ], - atSupports: [ - { - name: 'cover', - value: '(object-fit: cover)', - deprecated: false, - } - ], - atMedia: [ - { - name: 'medium', - value: '(min-width: 768px)', - deprecated: false - } - ], - values: [ - { - name: 'color', - value: '#f00', - deprecated: false, - allowedProperties: [], - blockedProperties: [], - }, - { - name: 'red-components', - value: '255, 0, 0', - deprecated: false, - allowedProperties: [], - blockedProperties: [], - } - ] - } - } + options: {}, + plugins: [ + postcssImport(), + plugin() + ] }, 'examples/example': { message: 'minimal example', + options: {}, + }, + 'examples/example:dark': { + message: 'minimal example with dark theme', options: { - requiresDesignTokens: { - properties: [ - 'color' - ] - }, - designTokens: { - selectors: [ - { - name: 'button', - value: ':is(.button, button[type="submit"])', - deprecated: false, - } - ], - atSupports: [ - { - name: 'cover', - value: '(object-fit: cover)', - deprecated: false, - } - ], - atMedia: [ - { - name: 'medium', - value: '(min-width: 768px)', - deprecated: false - } - ], - values: [ - { - name: 'color', - value: '#f00', - deprecated: false, - allowedProperties: [], - blockedProperties: [], - }, - { - name: 'red-components', - value: '255, 0, 0', - deprecated: false, - allowedProperties: [], - blockedProperties: [], - } - ] - } - } + variants: [ + 'dark' + ] + }, }, }); diff --git a/plugins/postcss-design-tokens/INSTALL.md b/plugins/postcss-design-tokens/INSTALL.md index 5a59c270a..a6635e80b 100644 --- a/plugins/postcss-design-tokens/INSTALL.md +++ b/plugins/postcss-design-tokens/INSTALL.md @@ -17,10 +17,10 @@ Use it as a [PostCSS] plugin: ```js const postcss = require('postcss'); -const postcssBasePlugin = require('@csstools/postcss-design-tokens'); +const postcssDesignTokens = require('@csstools/postcss-design-tokens'); postcss([ - postcssBasePlugin(/* pluginOptions */) + postcssDesignTokens(/* pluginOptions */) ]).process(YOUR_CSS /*, processOptions */); ``` @@ -35,11 +35,11 @@ npm install postcss-cli @csstools/postcss-design-tokens --save-dev Use [PostCSS Design Tokens] in your `postcss.config.js` configuration file: ```js -const postcssBasePlugin = require('@csstools/postcss-design-tokens'); +const postcssDesignTokens = require('@csstools/postcss-design-tokens'); module.exports = { plugins: [ - postcssBasePlugin(/* pluginOptions */) + postcssDesignTokens(/* pluginOptions */) ] } ``` @@ -103,11 +103,11 @@ Use [React App Rewire PostCSS] and [PostCSS Design Tokens] in your ```js const reactAppRewirePostcss = require('react-app-rewire-postcss'); -const postcssBasePlugin = require('@csstools/postcss-design-tokens'); +const postcssDesignTokens = require('@csstools/postcss-design-tokens'); module.exports = config => reactAppRewirePostcss(config, { plugins: () => [ - postcssBasePlugin(/* pluginOptions */) + postcssDesignTokens(/* pluginOptions */) ] }); ``` @@ -124,11 +124,11 @@ Use [PostCSS Design Tokens] in your Gulpfile: ```js const postcss = require('gulp-postcss'); -const postcssBasePlugin = require('@csstools/postcss-design-tokens'); +const postcssDesignTokens = require('@csstools/postcss-design-tokens'); gulp.task('css', function () { var plugins = [ - postcssBasePlugin(/* pluginOptions */) + postcssDesignTokens(/* pluginOptions */) ]; return gulp.src('./src/*.css') @@ -148,7 +148,7 @@ npm install grunt-postcss @csstools/postcss-design-tokens --save-dev Use [PostCSS Design Tokens] in your Gruntfile: ```js -const postcssBasePlugin = require('@csstools/postcss-design-tokens'); +const postcssDesignTokens = require('@csstools/postcss-design-tokens'); grunt.loadNpmTasks('grunt-postcss'); @@ -156,7 +156,7 @@ grunt.initConfig({ postcss: { options: { processors: [ - postcssBasePlugin(/* pluginOptions */) + postcssDesignTokens(/* pluginOptions */) ] }, dist: { diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index 92fa6dd7b..ff96f55b8 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -7,23 +7,30 @@ [PostCSS Design Tokens] lets easily create new plugins following some [CSS Specification]. -```pcss -.foo { - color: red; +```json +{ + "color": { + "background": { + "primary": { "value": "#fff" } + } + } } +``` + +```pcss +@design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens-dark.json') variant('dark') vendor('style-dictionary') version('3'); -.baz { - color: green; +.foo { + color: design-token('color.background.primary'); } /* becomes */ -.foo { - color: blue; -} +@design-tokens url('./tokens-dark.json') variant('dark') vendor('style-dictionary') version('3'); -.baz { - color: green; +.foo { + color: #fff; } ``` @@ -39,10 +46,10 @@ Use it as a [PostCSS] plugin: ```js const postcss = require('postcss'); -const postcssBasePlugin = require('@csstools/postcss-design-tokens'); +const postcssDesignTokens = require('@csstools/postcss-design-tokens'); postcss([ - postcssBasePlugin(/* pluginOptions */) + postcssDesignTokens(/* pluginOptions */) ]).process(YOUR_CSS /*, processOptions */); ``` @@ -54,33 +61,39 @@ instructions for: ## Options -### preserve +### variants -The `preserve` option determines whether the original notation -is preserved. By default, it is not preserved. +The `variants` option determines which design tokens are used. +This allows you to generate multiple themed stylesheets. ```js -postcssBasePlugin({ preserve: true }) +postcssDesignTokens({ variants: ['dark'] }) ``` -```pcss -.foo { - color: red; +```json +{ + "color": { + "background": { + "primary": { "value": "#000" } + } + } } +``` + +```pcss +@design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens-dark.json') variant('dark') vendor('style-dictionary') version('3'); -.baz { - color: green; +.foo { + color: design-token('color.background.primary'); } /* becomes */ -.foo { - color: blue; - color: red; -} +@design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); -.baz { - color: green; +.foo { + color: #000; } ``` diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md index 3bc455892..7cbf40f26 100644 --- a/plugins/postcss-design-tokens/docs/README.md +++ b/plugins/postcss-design-tokens/docs/README.md @@ -16,6 +16,10 @@ [] lets easily create new plugins following some [CSS Specification]. +```json + +``` + ```pcss @@ -30,13 +34,17 @@ ## Options -### preserve +### variants -The `preserve` option determines whether the original notation -is preserved. By default, it is not preserved. +The `variants` option determines which design tokens are used. +This allows you to generate multiple themed stylesheets. ```js -({ preserve: true }) +({ variants: ['dark'] }) +``` + +```json + ``` ```pcss @@ -44,7 +52,7 @@ is preserved. By default, it is not preserved. /* becomes */ - + ``` diff --git a/plugins/postcss-design-tokens/package.json b/plugins/postcss-design-tokens/package.json index 0341324ed..33698da03 100644 --- a/plugins/postcss-design-tokens/package.json +++ b/plugins/postcss-design-tokens/package.json @@ -29,6 +29,10 @@ "peerDependencies": { "postcss": "^8.3" }, + "devDependencies": { + "postcss-import": "^14.1.0", + "style-dictionary-design-tokens-example": "^1.0.0" + }, "scripts": { "build": "rollup -c ../../rollup/default.js", "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true });\"", diff --git a/plugins/postcss-design-tokens/src/data-formats/base/token.ts b/plugins/postcss-design-tokens/src/data-formats/base/token.ts new file mode 100644 index 000000000..ac789984f --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/base/token.ts @@ -0,0 +1,3 @@ +export interface Token { + cssValue(): string +} diff --git a/plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/type.ts b/plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/type.ts new file mode 100644 index 000000000..7b9637ce0 --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/type.ts @@ -0,0 +1,14 @@ +export enum TokenType { + String = 'string', + Ident = 'ident', + Number = 'number', + Boolean = 'boolean', + Color = 'color', + Dimension = 'dimension', + FontFamily = 'fontFamily', + FontWeight = 'fontWeight', + FontStyle = 'fontStyle', + Duration = 'duration', + CubicBezier = 'cubicBezier', + URI = 'uri', +} diff --git a/plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/value.ts b/plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/value.ts new file mode 100644 index 000000000..76fa79902 --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/value.ts @@ -0,0 +1,86 @@ +import { TokenType } from './type'; + +export type TokenValue = TokenString | TokenIdent; + +export type TokenBaseValue = { + '$type': T; + '$name'?: string; + '$description'?: string; + '$deprecated'?: boolean; +} + +export interface TokenString extends TokenBaseValue { + '$value': string; +} + +export interface TokenIdent extends TokenBaseValue { + '$value': string; +} + +export interface TokenNumber extends TokenBaseValue { + '$value': number; +} + +export interface TokenBoolean extends TokenBaseValue { + '$value': boolean; +} + +export interface TokenColor extends TokenBaseValue { + '$value': { + colorSpace: 'srgb' | 'srgb-linear' | 'a98-rgb' | 'prophoto-rgb' | 'display-p3' | 'rec2020' | 'xyz-d50' | 'xyz-d65' | 'xyz' + channels: Array + alpha: number + }; +} + +export interface TokenDimension extends TokenBaseValue { + '$value': { + unit: string + value: number + }; +} + +export interface TokenFontFamily extends TokenBaseValue { + '$value': Array; +} + +export interface TokenFontWeight extends TokenBaseValue { + '$value': number | + 'thin' | + 'hairline' | + 'extra-light' | + 'ultra-light' | + 'light' | + 'normal' | + 'regular' | + 'book' | + 'medium' | + 'semi-bold' | + 'demi-bold' | + 'bold' | + 'extra-bold' | + 'ultra-bold' | + 'black' | + 'heavy' | + 'extra-black' | + 'ultra-black'; +} + +export interface TokenFontStyle extends TokenBaseValue { + '$value': [TokenIdent, TokenDimension?]; +} + +export interface TokenDuration extends TokenBaseValue { + '$value': { + unit: string + value: number + }; +} + +export interface TokenCubicBezier extends TokenBaseValue { + '$value': Array; +} + +export interface TokenURI extends TokenBaseValue { + '$value': string; +} diff --git a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts new file mode 100644 index 000000000..756453c8d --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts @@ -0,0 +1,79 @@ +import valueParser from 'postcss-value-parser'; +import { Token } from './base/token'; +import { extractStyleDictionaryTokens, latestStyleDictionaryVersion } from './style-dictionary/style-dictionary'; +import path from 'path'; +import { promises as fsp } from 'fs'; + +function parseImport(statement: string): { filePath: string, vendor: string, version: string, variant: string } { + const importAST = valueParser(statement); + + const result = { + filePath: '', + vendor: 'standard', + version: '', + variant: 'default', + }; + + importAST.walk((node) => { + if (node.type === 'function' && node.value === 'url') { + result.filePath = node.nodes[0].value; + } + + if (node.type === 'function' && node.value === 'vendor') { + result.vendor = node.nodes[0].value; + } + + if (node.type === 'function' && node.value === 'version') { + result.version = node.nodes[0].value; + } + + if (node.type === 'function' && node.value === 'variant') { + result.variant = node.nodes[0].value; + } + }); + + if (!result.variant) { + result.variant = 'default'; + } + + if (!result.version) { + switch (result.vendor) { + case 'standard': + result.version = '0.0.1'; + break; + + case 'style-dictionary': + result.version = latestStyleDictionaryVersion; + break; + + default: + break; + } + } + + return result; +} + +export async function tokensFromImport(currentVariants: Array, sourceFilePath: string, statement: string): Promise<{ filePath: string, tokens: Map }|false> { + const { filePath, vendor, version, variant } = parseImport(statement); + if (!currentVariants.includes(variant)) { + return false; + } + + const resolvedPath = path.resolve(path.dirname(sourceFilePath), filePath); + const fileContents = await fsp.readFile(resolvedPath, 'utf8'); + const tokenContents = JSON.parse(fileContents); + + switch (vendor) { + case 'style-dictionary': + return { + filePath: path.resolve(filePath), + tokens: extractStyleDictionaryTokens(version, tokenContents, resolvedPath), + }; + + default: + break; + } + + throw new Error('Unsupported vendor: ' + vendor); +} diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/style-dictionary.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/style-dictionary.ts new file mode 100644 index 000000000..a2b588294 --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/style-dictionary.ts @@ -0,0 +1,13 @@ + +import { Token } from '../base/token'; +import { extractStyleDictionaryV3Tokens, StyleDictionaryV3TokenGroup } from './v3/group'; + +export const latestStyleDictionaryVersion = '3'; + +export function extractStyleDictionaryTokens(version: string, node: unknown, filePath: string): Map { + if (version === '3') { + return extractStyleDictionaryV3Tokens(node as StyleDictionaryV3TokenGroup, filePath); + } + + throw new Error('Unsupported version: ' + version); +} diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/group.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/group.ts new file mode 100644 index 000000000..2115706ae --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/group.ts @@ -0,0 +1,37 @@ +import { Token } from '../../base/token'; +import { extractStyleDictionaryV3Token, StyleDictionaryV3TokenValue } from './value'; + +export type StyleDictionaryV3TokenGroup = { + [key: string]: StyleDictionaryV3TokenGroup | StyleDictionaryV3TokenValue; +} + +function extractTokens(node: StyleDictionaryV3TokenGroup, path: Array, filePath: string): Map { + const result: Map = new Map(); + for (const key in node) { + if (Object.hasOwnProperty.call(node, key)) { + const child = Object(node[key]); + if (!child) { + throw new Error('Parsing error'); + } + + if (typeof child['value'] !== 'undefined') { + const token = extractStyleDictionaryV3Token(child, key, path, filePath); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result.set(token.metadata!.path.join('.'), token ); + continue; + } + + for (const [tokenPath, token] of extractTokens(child, [...path, key], filePath).entries()) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result.set(tokenPath, token); + continue; + } + } + } + + return result; +} + +export function extractStyleDictionaryV3Tokens(node: StyleDictionaryV3TokenGroup, filePath: string): Map { + return extractTokens(node, [], filePath); +} diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts new file mode 100644 index 000000000..8b96fbefe --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts @@ -0,0 +1,48 @@ +export type StyleDictionaryV3TokenValue = { + cssValue(): string + // The value of the design token. This can be any type of data, a hex string, an integer, a file path to a file, even an object or array. + value: unknown + // Usually the name for a design token is generated with a name transform, but you can write your own if you choose. By default Style Dictionary will add a default name which is the key of the design token object. + name?: string + // The comment attribute will show up in a code comment in output files if the format supports it. + comment?: string + // This is used in formats that support override-able or themeable values like the !default flag in Sass. + themeable?: boolean + // Extra information about the design token you want to include. Attribute transforms will modify this object so be careful + attributes?: unknown + + // Set by the token parser + metadata?: { + // A default name of the design token that is set to the key of the design token. This is only added if you do not provide one. + name?: string + // The object path of the design token. + path: Array + // 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. + isSource: boolean + } +} + +export function extractStyleDictionaryV3Token(node: unknown, key: string, path: Array, filePath: string): StyleDictionaryV3TokenValue { + if (typeof node['value'] === 'undefined') { + throw new Error('Token value is undefined : ' + path.join('.')); + } + + const value = node['value'] ?? undefined; + + return { + value: value, + cssValue: () => { + return value ?? ''; + }, + name: node['name'] ?? key, + comment: node['comment'] ?? undefined, + metadata: { + name: node['name'] ? key : undefined, + path: [...path, key], + filePath: filePath, + isSource: true, + }, + }; +} diff --git a/plugins/postcss-design-tokens/src/data-formats/token.ts b/plugins/postcss-design-tokens/src/data-formats/token.ts new file mode 100644 index 000000000..704cb796c --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/token.ts @@ -0,0 +1,9 @@ +import { Token } from './base/token'; + +export function mergeTokens(a: Map, b: Map): Map { + const result = new Map(a); + for (const [key, value] of b) { + result.set(key, value); + } + return result; +} diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts index d2f6e787b..e2d7aabbf 100644 --- a/plugins/postcss-design-tokens/src/index.ts +++ b/plugins/postcss-design-tokens/src/index.ts @@ -1,65 +1,52 @@ import type { PluginCreator } from 'postcss'; -import { AtMediaOptions, AtSupportsOptions, SelectorOptions, validateOptions, ValueOptions } from './options'; -import { onCSSValue, onCSSValueRequiredDesignToken, onCSSValueUnknownDesignToken } from './values'; +import { Token } from './data-formats/base/token'; +import { tokensFromImport } from './data-formats/parse-import'; +import { mergeTokens } from './data-formats/token'; +import { onCSSValue } from './values'; type pluginOptions = { - requiresDesignTokens?: { - properties: Array, - }, - designTokens?: { - atMedia?: Array, - atSupports?: Array, - selectors?: Array, - values?: Array - } -}; + variants?: Array +} const creator: PluginCreator = (opts?: pluginOptions) => { - const config = validateOptions(opts); + const variants = opts?.variants ?? []; + if (variants.length === 0) { + variants.push('default'); + } return { postcssPlugin: 'postcss-design-tokens', prepare() { - return { - Once(root, { result }) { + let tokens = new Map(); - if (config.requiresDesignTokens.properties.size > 0) { - // Emit warnings for properties that use custom values when not allowed. - - root.walkDecls(decl => { - if (config.requiresDesignTokens.properties.has(decl.prop)) { - if (decl.value.indexOf('design-token') === -1) { - decl.warn(result, `"${decl.prop}" must always use design tokens.`); - return; - } - - const modifiedValue = onCSSValueRequiredDesignToken(config, result, decl); - if (modifiedValue === false || modifiedValue === decl.value) { - decl.warn(result, `"${decl.prop}" must always use design tokens.`); - return; - } - - decl.value = modifiedValue; - } - }); - } - }, - OnceExit(root, { result }) { - // Emit warnings if design tokens remain after processing. - root.walkDecls(decl => { - if (decl.value.indexOf('design-token') === -1) { + return { + AtRule: { + 'design-tokens': async (atRule, { result }) => { + if (!atRule?.source?.input?.file) { return; } + const importResult = await tokensFromImport(variants, atRule.source.input.file, atRule.params); + if (!importResult) { + atRule.remove(); + return; + } + + result.messages.push({ + type: 'dependency', + plugin: 'postcss-design-tokens', + file: importResult.filePath, + parent: atRule.source.input.file, + }); - onCSSValueUnknownDesignToken(config, result, decl); - }); + tokens = mergeTokens(tokens, importResult.tokens); + }, }, Declaration(decl, { result }) { if (decl.value.indexOf('design-token') === -1) { return; } - const modifiedValue = onCSSValue(config, result, decl); + const modifiedValue = onCSSValue(tokens, result, decl); if (modifiedValue === decl.value) { return; } diff --git a/plugins/postcss-design-tokens/src/options.ts b/plugins/postcss-design-tokens/src/options.ts deleted file mode 100644 index d4a0d6388..000000000 --- a/plugins/postcss-design-tokens/src/options.ts +++ /dev/null @@ -1,145 +0,0 @@ -// Public API vs. Internal usage. -// The public API has a lot of optional parameters and simpler typing like "Array". -// This is compatible with JSON. -// -// The internal usage typings do not have any optional parameters and use more powerful types like "Map" and "Set". - -// Public API -type TokenOptions = { - name: string, - value: string, - deprecated?: boolean, -} - -export type AtMediaOptions = TokenOptions; -export type AtSupportsOptions = TokenOptions; -export type SelectorOptions = TokenOptions; - -export type ValueOptions = { - name: string, - value: string, - deprecated?: boolean, - allowedProperties?: Array, - blockedProperties?: Array, -} - -// Internal usage -type TokenConfig = { - name: string, - value: string, - deprecated: boolean, -} - -export type AtMediaConfig = TokenConfig; -export type AtSupportsConfig = TokenConfig; -export type SelectorConfig = TokenConfig; - -export type ValueConfig = { - name: string, - value: string, - deprecated: boolean, - allowedProperties: Set, - blockedProperties: Set, -} - -function validateSubOptions(rawSubOptions: unknown): { name: string, value: string, deprecated: boolean } { - const subOptions = Object(rawSubOptions); - return { - name: (typeof subOptions.name === 'string') ? subOptions.name : '', - value: (typeof subOptions.value === 'string') ? subOptions.value : '', - deprecated: (typeof subOptions.deprecated === 'boolean') ? subOptions.deprecated : false, - }; -} - -export type Config = { - requiresDesignTokens: { - properties: Set, - }, - designTokens: { - atMedia: Map, - atSupports: Map, - selectors: Map, - values: Map - } -}; - -export function validateOptions(rawOptions: unknown) { - const config: Config = { - requiresDesignTokens: { - properties: new Set(), - }, - designTokens: { - atMedia: new Map(), - atSupports: new Map(), - selectors: new Map(), - values: new Map(), - }, - }; - - const requiresDesignTokens = Object(rawOptions).requiresDesignTokens; - if (requiresDesignTokens && Array.isArray(Object(requiresDesignTokens).properties)) { - requiresDesignTokens.properties.forEach((x) => { - if (typeof x === 'string') { - config.requiresDesignTokens.properties.add(x); - } - }); - } - - const designTokens = Object(rawOptions).designTokens; - if (designTokens) { - if (Array.isArray(Object(designTokens).atMedia)) { - designTokens.atMedia.map((x) => { - return validateSubOptions(x); - }).filter((x: TokenConfig) => { - return x.name !== '' && x.value !== ''; - }).forEach((x: TokenConfig) => { - config.designTokens.atMedia.set(x.name, x); - }); - } - - if (Array.isArray(Object(designTokens).atSupports)) { - designTokens.atSupports.map((x) => { - return validateSubOptions(x); - }).filter((x: TokenConfig) => { - return x.name !== '' && x.value !== ''; - }).forEach((x: TokenConfig) => { - config.designTokens.atSupports.set(x.name, x); - }); - } - - if (Array.isArray(Object(designTokens).selectors)) { - designTokens.selectors.map((x) => { - return validateSubOptions(x); - }).filter((x: TokenConfig) => { - return x.name !== '' && x.value !== ''; - }).forEach((x: TokenConfig) => { - config.designTokens.selectors.set(x.name, x); - }); - } - - if (Array.isArray(Object(designTokens).values)) { - designTokens.values.map((x) => { - const allowedProperties : Array = (Array.isArray(Object(x).allowedProperties)) ? x.allowedProperties.filter((y) => { - return typeof y === 'string'; - }) : []; - const blockedProperties: Array = (Array.isArray(Object(x).blockedProperties)) ? x.blockedProperties.filter((y) => { - return typeof y === 'string'; - }) : []; - - const xx: ValueConfig = { - ...validateSubOptions(x), - allowedProperties: new Set(allowedProperties), - blockedProperties: new Set(blockedProperties), - }; - - return xx; - }).filter((x: ValueConfig) => { - return x.name !== '' && x.value !== ''; - }).forEach((x: ValueConfig) => { - config.designTokens.values.set(x.name, x); - }); - } - } - - return config; -} diff --git a/plugins/postcss-design-tokens/src/values.ts b/plugins/postcss-design-tokens/src/values.ts index 829f5e1de..e1db3194d 100644 --- a/plugins/postcss-design-tokens/src/values.ts +++ b/plugins/postcss-design-tokens/src/values.ts @@ -1,9 +1,9 @@ import type { Declaration, Result } from 'postcss'; -import valuesParser from 'postcss-value-parser'; -import { Config } from './options'; +import valueParser from 'postcss-value-parser'; +import { Token } from './data-formats/base/token'; -export function onCSSValue(config: Config, result: Result, decl: Declaration) { - const valueAST = valuesParser(decl.value); +export function onCSSValue(tokens: Map, result: Result, decl: Declaration) { + const valueAST = valueParser(decl.value); valueAST.walk(node => { if (node.type !== 'function' || node.value !== 'design-token') { @@ -11,113 +11,24 @@ export function onCSSValue(config: Config, result: Result, decl: Declaration) { } if (!node.nodes || node.nodes.length !== 1) { - decl.warn(result, 'Expected a single keyword for the design-token function.'); + decl.warn(result, 'Expected a single string literal for the design-token function.'); return; } - if (node.nodes[0].type !== 'word') { - decl.warn(result, 'Expected a single keyword for the design-token function.'); + if (node.nodes[0].type !== 'string') { + decl.warn(result, 'Expected a single string literal for the design-token function.'); return; } - const replacement = config.designTokens.values.get(node.nodes[0].value); + const replacement = tokens.get(node.nodes[0].value); if (!replacement) { + decl.warn(result, `design-token: "${node.nodes[0].value}" is not configured.`); return; } - if (replacement.deprecated) { - decl.warn(result, `design-token: "${replacement.name}" is deprecated.`); - } - - if (replacement.allowedProperties.size && !replacement.allowedProperties.has(decl.prop)) { - decl.warn(result, `design-token: "${replacement.name}" is not allowed for "${decl.prop}".`); - return; - } - - if (replacement.blockedProperties.size && replacement.blockedProperties.has(decl.prop)) { - decl.warn(result, `design-token: "${replacement.name}" is not allowed for "${decl.prop}".`); - return; - } - - node.value = replacement.value; + node.value = replacement.cssValue(); node.nodes = undefined; }); return String(valueAST); } - -export function onCSSValueRequiredDesignToken(config: Config, result: Result, decl: Declaration) { - const valueAST = valuesParser(decl.value); - - let hasNonDesignTokens = false; - - valueAST.nodes.forEach((node) => { - // Allowed, even when the property requires design tokens. - if (node.type === 'comment' || node.type === 'space' || node.type === 'div') { - return; - } - - if (node.type !== 'function' || node.value !== 'design-token') { - hasNonDesignTokens = true; - return; - } - - if (!node.nodes || node.nodes.length !== 1) { - decl.warn(result, 'Expected a single keyword for the design-token function.'); - return false; - } - - if (node.nodes[0].type !== 'word') { - decl.warn(result, 'Expected a single keyword for the design-token function.'); - return false; - } - - const replacement = config.designTokens.values.get(node.nodes[0].value); - if (!replacement) { - return; - } - - if (replacement.deprecated) { - decl.warn(result, `design-token: "${replacement.name}" is deprecated.`); - } - - if (replacement.allowedProperties.size && !replacement.allowedProperties.has(decl.prop)) { - decl.warn(result, `design-token: "${replacement.name}" is not allowed for "${decl.prop}".`); - return false; - } - - if (replacement.blockedProperties.size && replacement.blockedProperties.has(decl.prop)) { - decl.warn(result, `design-token: "${replacement.name}" is not allowed for "${decl.prop}".`); - return false; - } - - node.value = replacement.value; - node.nodes = undefined; - }); - - if (hasNonDesignTokens) { - return false; - } - - return String(valueAST); -} - -export function onCSSValueUnknownDesignToken(config: Config, result: Result, decl: Declaration) { - const valueAST = valuesParser(decl.value); - - valueAST.walk(node => { - if (node.type !== 'function' || node.value !== 'design-token') { - return; - } - - if (!node.nodes || node.nodes.length !== 1) { - return; - } - - if (node.nodes[0].type !== 'word') { - return; - } - - decl.warn(result, `design-token: "${node.nodes[0].value}" is not configured.`); - }); -} diff --git a/plugins/postcss-design-tokens/test/basic.css b/plugins/postcss-design-tokens/test/basic.css index 5cd4316d0..4d7979248 100644 --- a/plugins/postcss-design-tokens/test/basic.css +++ b/plugins/postcss-design-tokens/test/basic.css @@ -1,21 +1,8 @@ -.foo { - color: design-token(color); -} - -@media (design-token: medium) { - .baz { - color: design-token(color); - background-color: rgb(design-token(red-components)); - } -} +@import url('./imported.css'); +@design-tokens url('./tokens.json') vendor('style-dictionary') version('3'); -@supports (design-token: cover) { - .baz { - color: design-token(color); - object-fit: cover; - } -} - -:design-token(button) { - color: design-token(color); +.foo { + font-family: design-token('font.family.serif'); + font-size: design-token('size.font.small'); + color: design-token('color.font.base'); } diff --git a/plugins/postcss-design-tokens/test/basic.expect.css b/plugins/postcss-design-tokens/test/basic.expect.css index 924fcf346..d0e8338ea 100644 --- a/plugins/postcss-design-tokens/test/basic.expect.css +++ b/plugins/postcss-design-tokens/test/basic.expect.css @@ -1,21 +1,5 @@ .foo { - color: #f00; -} - -@media (design-token: medium) { - .baz { - color: #f00; - background-color: rgb(255, 0, 0); - } -} - -@supports (design-token: cover) { - .baz { - color: #f00; - object-fit: cover; - } -} - -:design-token(button) { - color: #f00; + font-family: serif; + font-size: 10; + color: #111111; } diff --git a/plugins/postcss-design-tokens/test/examples/example.css b/plugins/postcss-design-tokens/test/examples/example.css index 5cd4316d0..3cc76267a 100644 --- a/plugins/postcss-design-tokens/test/examples/example.css +++ b/plugins/postcss-design-tokens/test/examples/example.css @@ -1,21 +1,6 @@ -.foo { - color: design-token(color); -} - -@media (design-token: medium) { - .baz { - color: design-token(color); - background-color: rgb(design-token(red-components)); - } -} +@design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens-dark.json') variant('dark') vendor('style-dictionary') version('3'); -@supports (design-token: cover) { - .baz { - color: design-token(color); - object-fit: cover; - } -} - -:design-token(button) { - color: design-token(color); +.foo { + color: design-token('color.background.primary'); } diff --git a/plugins/postcss-design-tokens/test/examples/example.dark.expect.css b/plugins/postcss-design-tokens/test/examples/example.dark.expect.css new file mode 100644 index 000000000..a88065a4d --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/example.dark.expect.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); + +.foo { + color: #000; +} diff --git a/plugins/postcss-design-tokens/test/examples/example.expect.css b/plugins/postcss-design-tokens/test/examples/example.expect.css index 924fcf346..9d816767f 100644 --- a/plugins/postcss-design-tokens/test/examples/example.expect.css +++ b/plugins/postcss-design-tokens/test/examples/example.expect.css @@ -1,21 +1,5 @@ -.foo { - color: #f00; -} - -@media (design-token: medium) { - .baz { - color: #f00; - background-color: rgb(255, 0, 0); - } -} +@design-tokens url('./tokens-dark.json') variant('dark') vendor('style-dictionary') version('3'); -@supports (design-token: cover) { - .baz { - color: #f00; - object-fit: cover; - } -} - -:design-token(button) { - color: #f00; +.foo { + color: #fff; } diff --git a/plugins/postcss-design-tokens/test/examples/tokens-dark.json b/plugins/postcss-design-tokens/test/examples/tokens-dark.json new file mode 100644 index 000000000..09de442b7 --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/tokens-dark.json @@ -0,0 +1,7 @@ +{ + "color": { + "background": { + "primary": { "value": "#000" } + } + } +} diff --git a/plugins/postcss-design-tokens/test/examples/tokens-light.json b/plugins/postcss-design-tokens/test/examples/tokens-light.json new file mode 100644 index 000000000..9776bbaed --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/tokens-light.json @@ -0,0 +1,7 @@ +{ + "color": { + "background": { + "primary": { "value": "#fff" } + } + } +} diff --git a/plugins/postcss-design-tokens/test/imported.css b/plugins/postcss-design-tokens/test/imported.css new file mode 100644 index 000000000..833f5e11c --- /dev/null +++ b/plugins/postcss-design-tokens/test/imported.css @@ -0,0 +1 @@ +@design-tokens url('../../../node_modules/style-dictionary-design-tokens-example/style-dictionary.tokens.json') vendor('style-dictionary') version('3'); diff --git a/plugins/postcss-design-tokens/test/tokens.json b/plugins/postcss-design-tokens/test/tokens.json new file mode 100644 index 000000000..9ee085a9c --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens.json @@ -0,0 +1,7 @@ +{ + "font": { + "family": { + "serif": { "value": "serif" } + } + } +} diff --git a/plugins/postcss-design-tokens/tsconfig.json b/plugins/postcss-design-tokens/tsconfig.json index e0d06239c..e2c1924fe 100644 --- a/plugins/postcss-design-tokens/tsconfig.json +++ b/plugins/postcss-design-tokens/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist", - "declarationDir": "." + "declarationDir": ".", + "module": "es2020", }, "include": ["./src/**/*"], "exclude": ["dist"], diff --git a/rollup/presets/package-typescript.js b/rollup/presets/package-typescript.js index d765bf703..6e739ef65 100644 --- a/rollup/presets/package-typescript.js +++ b/rollup/presets/package-typescript.js @@ -21,7 +21,7 @@ export function packageTypescript() { extensions: ['.js', '.ts'], presets: packageBabelPreset, }), - terser(), + // terser(), ], }, ]; From 8488665d370cfaf61e579c75d548d56611db2c1a Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 27 Mar 2022 11:25:11 +0200 Subject: [PATCH 04/32] wip --- plugins/postcss-design-tokens/README.md | 4 ++-- plugins/postcss-design-tokens/package.json | 7 +++---- plugins/postcss-design-tokens/test/basic.expect.css | 2 ++ .../test/examples/example.dark.expect.css | 2 +- .../postcss-design-tokens/test/examples/example.expect.css | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index ff96f55b8..8f6aae920 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -27,7 +27,7 @@ /* becomes */ -@design-tokens url('./tokens-dark.json') variant('dark') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); .foo { color: #fff; @@ -90,7 +90,7 @@ postcssDesignTokens({ variants: ['dark'] }) /* becomes */ -@design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens-dark.json') variant('dark') vendor('style-dictionary') version('3'); .foo { color: #000; diff --git a/plugins/postcss-design-tokens/package.json b/plugins/postcss-design-tokens/package.json index 33698da03..a6b43019a 100644 --- a/plugins/postcss-design-tokens/package.json +++ b/plugins/postcss-design-tokens/package.json @@ -41,16 +41,17 @@ "lint:eslint": "eslint ./src --ext .js --ext .ts --ext .mjs --no-error-on-unmatched-pattern", "lint:package-json": "node ../../.github/bin/format-package-json.mjs", "prepublishOnly": "npm run clean && npm run build && npm run test", - "stryker": "stryker run --logLevel error", "test": "node .tape.mjs && npm run test:exports", "test:exports": "node ./test/_import.mjs && node ./test/_require.cjs", "test:rewrite-expects": "REWRITE_EXPECTS=true node .tape.mjs" }, + "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-design-tokens#readme", "repository": { "type": "git", "url": "https://github.com/csstools/postcss-plugins.git", "directory": "plugins/postcss-design-tokens" }, + "bugs": "https://github.com/csstools/postcss-plugins/issues", "keywords": [ "postcss-plugin" ], @@ -62,7 +63,5 @@ }, "volta": { "extends": "../../package.json" - }, - "homepage": "https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-design-tokens#readme", - "bugs": "https://github.com/csstools/postcss-plugins/issues" + } } diff --git a/plugins/postcss-design-tokens/test/basic.expect.css b/plugins/postcss-design-tokens/test/basic.expect.css index d0e8338ea..438e4934d 100644 --- a/plugins/postcss-design-tokens/test/basic.expect.css +++ b/plugins/postcss-design-tokens/test/basic.expect.css @@ -1,3 +1,5 @@ +@design-tokens url('../../../node_modules/style-dictionary-design-tokens-example/style-dictionary.tokens.json') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens.json') vendor('style-dictionary') version('3'); .foo { font-family: serif; font-size: 10; diff --git a/plugins/postcss-design-tokens/test/examples/example.dark.expect.css b/plugins/postcss-design-tokens/test/examples/example.dark.expect.css index a88065a4d..8fa22a271 100644 --- a/plugins/postcss-design-tokens/test/examples/example.dark.expect.css +++ b/plugins/postcss-design-tokens/test/examples/example.dark.expect.css @@ -1,4 +1,4 @@ -@design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens-dark.json') variant('dark') vendor('style-dictionary') version('3'); .foo { color: #000; diff --git a/plugins/postcss-design-tokens/test/examples/example.expect.css b/plugins/postcss-design-tokens/test/examples/example.expect.css index 9d816767f..5dc769526 100644 --- a/plugins/postcss-design-tokens/test/examples/example.expect.css +++ b/plugins/postcss-design-tokens/test/examples/example.expect.css @@ -1,4 +1,4 @@ -@design-tokens url('./tokens-dark.json') variant('dark') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); .foo { color: #fff; From 7fdc4e56f625e32b7ab0353f400e0959485a81c1 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 27 Mar 2022 11:26:35 +0200 Subject: [PATCH 05/32] wip --- plugins/postcss-design-tokens/README.md | 4 ---- plugins/postcss-design-tokens/src/index.ts | 2 +- plugins/postcss-design-tokens/test/basic.expect.css | 2 -- .../test/examples/example.dark.expect.css | 2 -- .../postcss-design-tokens/test/examples/example.expect.css | 2 -- 5 files changed, 1 insertion(+), 11 deletions(-) diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index 8f6aae920..19650e18f 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -27,8 +27,6 @@ /* becomes */ -@design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); - .foo { color: #fff; } @@ -90,8 +88,6 @@ postcssDesignTokens({ variants: ['dark'] }) /* becomes */ -@design-tokens url('./tokens-dark.json') variant('dark') vendor('style-dictionary') version('3'); - .foo { color: #000; } diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts index e2d7aabbf..60eea2ce5 100644 --- a/plugins/postcss-design-tokens/src/index.ts +++ b/plugins/postcss-design-tokens/src/index.ts @@ -26,8 +26,8 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } const importResult = await tokensFromImport(variants, atRule.source.input.file, atRule.params); + atRule.remove(); if (!importResult) { - atRule.remove(); return; } diff --git a/plugins/postcss-design-tokens/test/basic.expect.css b/plugins/postcss-design-tokens/test/basic.expect.css index 438e4934d..d0e8338ea 100644 --- a/plugins/postcss-design-tokens/test/basic.expect.css +++ b/plugins/postcss-design-tokens/test/basic.expect.css @@ -1,5 +1,3 @@ -@design-tokens url('../../../node_modules/style-dictionary-design-tokens-example/style-dictionary.tokens.json') vendor('style-dictionary') version('3'); -@design-tokens url('./tokens.json') vendor('style-dictionary') version('3'); .foo { font-family: serif; font-size: 10; diff --git a/plugins/postcss-design-tokens/test/examples/example.dark.expect.css b/plugins/postcss-design-tokens/test/examples/example.dark.expect.css index 8fa22a271..cab059366 100644 --- a/plugins/postcss-design-tokens/test/examples/example.dark.expect.css +++ b/plugins/postcss-design-tokens/test/examples/example.dark.expect.css @@ -1,5 +1,3 @@ -@design-tokens url('./tokens-dark.json') variant('dark') vendor('style-dictionary') version('3'); - .foo { color: #000; } diff --git a/plugins/postcss-design-tokens/test/examples/example.expect.css b/plugins/postcss-design-tokens/test/examples/example.expect.css index 5dc769526..33a418a59 100644 --- a/plugins/postcss-design-tokens/test/examples/example.expect.css +++ b/plugins/postcss-design-tokens/test/examples/example.expect.css @@ -1,5 +1,3 @@ -@design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); - .foo { color: #fff; } From 51f193ba5162be2d7478d780d1c5322275bd220b Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 27 Mar 2022 12:27:57 +0200 Subject: [PATCH 06/32] wip --- plugins/postcss-design-tokens/README.md | 4 ++-- .../src/data-formats/parse-import.ts | 16 ++++++++-------- .../test/examples/example.css | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index 19650e18f..391f29bf0 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -19,7 +19,7 @@ ```pcss @design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); -@design-tokens url('./tokens-dark.json') variant('dark') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens-dark.json') variants('dark') vendor('style-dictionary') version('3'); .foo { color: design-token('color.background.primary'); @@ -80,7 +80,7 @@ postcssDesignTokens({ variants: ['dark'] }) ```pcss @design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); -@design-tokens url('./tokens-dark.json') variant('dark') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens-dark.json') variants('dark') vendor('style-dictionary') version('3'); .foo { color: design-token('color.background.primary'); diff --git a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts index 756453c8d..507c6ec7b 100644 --- a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts +++ b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts @@ -4,14 +4,14 @@ import { extractStyleDictionaryTokens, latestStyleDictionaryVersion } from './st import path from 'path'; import { promises as fsp } from 'fs'; -function parseImport(statement: string): { filePath: string, vendor: string, version: string, variant: string } { +function parseImport(statement: string): { filePath: string, vendor: string, version: string, variants: Array } { const importAST = valueParser(statement); const result = { filePath: '', vendor: 'standard', version: '', - variant: 'default', + variants: ['default'], }; importAST.walk((node) => { @@ -27,13 +27,13 @@ function parseImport(statement: string): { filePath: string, vendor: string, ver result.version = node.nodes[0].value; } - if (node.type === 'function' && node.value === 'variant') { - result.variant = node.nodes[0].value; + if (node.type === 'function' && node.value === 'variants') { + result.variants = node.nodes.filter((child) => child.type === 'string').map((child) => child.value); } }); - if (!result.variant) { - result.variant = 'default'; + if (!result.variants.length) { + result.variants = ['default']; } if (!result.version) { @@ -55,8 +55,8 @@ function parseImport(statement: string): { filePath: string, vendor: string, ver } export async function tokensFromImport(currentVariants: Array, sourceFilePath: string, statement: string): Promise<{ filePath: string, tokens: Map }|false> { - const { filePath, vendor, version, variant } = parseImport(statement); - if (!currentVariants.includes(variant)) { + const { filePath, vendor, version, variants } = parseImport(statement); + if (!currentVariants.every((variant) => variants.includes(variant))) { return false; } diff --git a/plugins/postcss-design-tokens/test/examples/example.css b/plugins/postcss-design-tokens/test/examples/example.css index 3cc76267a..28d4ecca4 100644 --- a/plugins/postcss-design-tokens/test/examples/example.css +++ b/plugins/postcss-design-tokens/test/examples/example.css @@ -1,5 +1,5 @@ @design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); -@design-tokens url('./tokens-dark.json') variant('dark') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens-dark.json') variants('dark') vendor('style-dictionary') version('3'); .foo { color: design-token('color.background.primary'); From f8bfa50b21f81f82ffabc37be4316df80aa017d6 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 27 Mar 2022 12:40:29 +0200 Subject: [PATCH 07/32] wip --- plugins/postcss-design-tokens/.tape.mjs | 6 ++++++ .../src/data-formats/parse-import.ts | 2 +- plugins/postcss-design-tokens/test/basic.css | 2 +- .../test/{tokens.json => tokens/basic.json} | 0 .../test/tokens/color_dark_branded-blue.tokens.json | 5 +++++ .../test/tokens/color_dark_branded-green.tokens.json | 5 +++++ .../test/tokens/color_light_branded-blue.tokens.json | 5 +++++ .../tokens/color_light_branded-green.tokens.json | 5 +++++ .../test/tokens/size_desktop.tokens.json | 5 +++++ .../test/tokens/size_mobile.tokens.json | 5 +++++ .../test/tokens/size_tablet.tokens.json | 5 +++++ plugins/postcss-design-tokens/test/variants.css | 12 ++++++++++++ .../postcss-design-tokens/test/variants.expect.css | 4 ++++ 13 files changed, 59 insertions(+), 2 deletions(-) rename plugins/postcss-design-tokens/test/{tokens.json => tokens/basic.json} (100%) create mode 100644 plugins/postcss-design-tokens/test/tokens/color_dark_branded-blue.tokens.json create mode 100644 plugins/postcss-design-tokens/test/tokens/color_dark_branded-green.tokens.json create mode 100644 plugins/postcss-design-tokens/test/tokens/color_light_branded-blue.tokens.json create mode 100644 plugins/postcss-design-tokens/test/tokens/color_light_branded-green.tokens.json create mode 100644 plugins/postcss-design-tokens/test/tokens/size_desktop.tokens.json create mode 100644 plugins/postcss-design-tokens/test/tokens/size_mobile.tokens.json create mode 100644 plugins/postcss-design-tokens/test/tokens/size_tablet.tokens.json create mode 100644 plugins/postcss-design-tokens/test/variants.css create mode 100644 plugins/postcss-design-tokens/test/variants.expect.css diff --git a/plugins/postcss-design-tokens/.tape.mjs b/plugins/postcss-design-tokens/.tape.mjs index e18f44cb5..0f15d5ffd 100644 --- a/plugins/postcss-design-tokens/.tape.mjs +++ b/plugins/postcss-design-tokens/.tape.mjs @@ -11,6 +11,12 @@ postcssTape(plugin)({ plugin() ] }, + 'variants': { + message: "supports basic usage", + options: { + variants: ['dark', 'tablet', 'branded-green'] + } + }, 'examples/example': { message: 'minimal example', options: {}, diff --git a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts index 507c6ec7b..3eb049e1b 100644 --- a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts +++ b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts @@ -56,7 +56,7 @@ function parseImport(statement: string): { filePath: string, vendor: string, ver export async function tokensFromImport(currentVariants: Array, sourceFilePath: string, statement: string): Promise<{ filePath: string, tokens: Map }|false> { const { filePath, vendor, version, variants } = parseImport(statement); - if (!currentVariants.every((variant) => variants.includes(variant))) { + if (!variants.every((variant) => currentVariants.includes(variant))) { return false; } diff --git a/plugins/postcss-design-tokens/test/basic.css b/plugins/postcss-design-tokens/test/basic.css index 4d7979248..0eb5dc7d2 100644 --- a/plugins/postcss-design-tokens/test/basic.css +++ b/plugins/postcss-design-tokens/test/basic.css @@ -1,5 +1,5 @@ @import url('./imported.css'); -@design-tokens url('./tokens.json') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens/basic.json') vendor('style-dictionary') version('3'); .foo { font-family: design-token('font.family.serif'); diff --git a/plugins/postcss-design-tokens/test/tokens.json b/plugins/postcss-design-tokens/test/tokens/basic.json similarity index 100% rename from plugins/postcss-design-tokens/test/tokens.json rename to plugins/postcss-design-tokens/test/tokens/basic.json diff --git a/plugins/postcss-design-tokens/test/tokens/color_dark_branded-blue.tokens.json b/plugins/postcss-design-tokens/test/tokens/color_dark_branded-blue.tokens.json new file mode 100644 index 000000000..8554a449d --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/color_dark_branded-blue.tokens.json @@ -0,0 +1,5 @@ +{ + "color": { + "value": "rgb(0, 0, 120)" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/color_dark_branded-green.tokens.json b/plugins/postcss-design-tokens/test/tokens/color_dark_branded-green.tokens.json new file mode 100644 index 000000000..aac06ff82 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/color_dark_branded-green.tokens.json @@ -0,0 +1,5 @@ +{ + "color": { + "value": "rgb(0, 120, 0)" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/color_light_branded-blue.tokens.json b/plugins/postcss-design-tokens/test/tokens/color_light_branded-blue.tokens.json new file mode 100644 index 000000000..8569a6d3a --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/color_light_branded-blue.tokens.json @@ -0,0 +1,5 @@ +{ + "color": { + "value": "rgb(50, 50, 250)" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/color_light_branded-green.tokens.json b/plugins/postcss-design-tokens/test/tokens/color_light_branded-green.tokens.json new file mode 100644 index 000000000..3e9ac3d69 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/color_light_branded-green.tokens.json @@ -0,0 +1,5 @@ +{ + "color": { + "value": "rgb(50, 250, 50)" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/size_desktop.tokens.json b/plugins/postcss-design-tokens/test/tokens/size_desktop.tokens.json new file mode 100644 index 000000000..0e36ff29e --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/size_desktop.tokens.json @@ -0,0 +1,5 @@ +{ + "size": { + "value": "20px" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/size_mobile.tokens.json b/plugins/postcss-design-tokens/test/tokens/size_mobile.tokens.json new file mode 100644 index 000000000..06069df89 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/size_mobile.tokens.json @@ -0,0 +1,5 @@ +{ + "size": { + "value": "16px" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/size_tablet.tokens.json b/plugins/postcss-design-tokens/test/tokens/size_tablet.tokens.json new file mode 100644 index 000000000..8102cac0c --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/size_tablet.tokens.json @@ -0,0 +1,5 @@ +{ + "size": { + "value": "18px" + } +} diff --git a/plugins/postcss-design-tokens/test/variants.css b/plugins/postcss-design-tokens/test/variants.css new file mode 100644 index 000000000..6db1c1d4f --- /dev/null +++ b/plugins/postcss-design-tokens/test/variants.css @@ -0,0 +1,12 @@ +@design-tokens url('./tokens/color_dark_branded-blue.tokens.json') variants('dark', 'branded-blue') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens/color_light_branded-blue.tokens.json') variants('light', 'branded-blue') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens/color_dark_branded-green.tokens.json') variants('dark', 'branded-green') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens/color_light_branded-green.tokens.json') variants('light', 'branded-green') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens/size_mobile.tokens.json') variants('mobile') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens/size_tablet.tokens.json') variants('tablet') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens/size_desktop.tokens.json') variants('desktop') vendor('style-dictionary') version('3'); + +.foo { + color: design-token('color'); + font-size: design-token('size'); +} diff --git a/plugins/postcss-design-tokens/test/variants.expect.css b/plugins/postcss-design-tokens/test/variants.expect.css new file mode 100644 index 000000000..6b837bc2b --- /dev/null +++ b/plugins/postcss-design-tokens/test/variants.expect.css @@ -0,0 +1,4 @@ +.foo { + color: rgb(0, 120, 0); + font-size: 18px; +} From 797b01d6e1b0382a959174ca59fff54b9bf84a83 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 27 Mar 2022 12:55:02 +0200 Subject: [PATCH 08/32] wip --- .../src/data-formats/parse-import.ts | 8 +++++++- plugins/postcss-design-tokens/src/index.ts | 13 +++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts index 3eb049e1b..5ae1ae21c 100644 --- a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts +++ b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts @@ -54,13 +54,19 @@ function parseImport(statement: string): { filePath: string, vendor: string, ver return result; } -export async function tokensFromImport(currentVariants: Array, sourceFilePath: string, statement: string): Promise<{ filePath: string, tokens: Map }|false> { +export async function tokensFromImport(currentVariants: Array, sourceFilePath: string, statement: string, alreadyImported: Set): Promise<{ filePath: string, tokens: Map }|false> { const { filePath, vendor, version, variants } = parseImport(statement); if (!variants.every((variant) => currentVariants.includes(variant))) { return false; } const resolvedPath = path.resolve(path.dirname(sourceFilePath), filePath); + if (alreadyImported.has(resolvedPath)) { + return false; + } + + alreadyImported.add(resolvedPath); + const fileContents = await fsp.readFile(resolvedPath, 'utf8'); const tokenContents = JSON.parse(fileContents); diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts index 60eea2ce5..f52cdce95 100644 --- a/plugins/postcss-design-tokens/src/index.ts +++ b/plugins/postcss-design-tokens/src/index.ts @@ -18,15 +18,24 @@ const creator: PluginCreator = (opts?: pluginOptions) => { postcssPlugin: 'postcss-design-tokens', prepare() { let tokens = new Map(); + let importedFiles = new Set(); return { + OnceExit() { + tokens = new Map(); + importedFiles = new Set(); + }, AtRule: { 'design-tokens': async (atRule, { result }) => { if (!atRule?.source?.input?.file) { return; } - const importResult = await tokensFromImport(variants, atRule.source.input.file, atRule.params); + + const filePath = atRule.source.input.file; + const params = atRule.params; atRule.remove(); + + const importResult = await tokensFromImport(variants, filePath, params, importedFiles); if (!importResult) { return; } @@ -35,7 +44,7 @@ const creator: PluginCreator = (opts?: pluginOptions) => { type: 'dependency', plugin: 'postcss-design-tokens', file: importResult.filePath, - parent: atRule.source.input.file, + parent: filePath, }); tokens = mergeTokens(tokens, importResult.tokens); From 81619416621efc5ea4007d2ea69049baceb2d10d Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 27 Mar 2022 13:58:15 +0200 Subject: [PATCH 09/32] feedback --- plugins/postcss-design-tokens/README.md | 8 ++-- .../src/data-formats/parse-import.ts | 38 +++++-------------- plugins/postcss-design-tokens/test/basic.css | 2 +- .../test/examples/example.css | 4 +- .../postcss-design-tokens/test/imported.css | 2 +- .../postcss-design-tokens/test/variants.css | 14 +++---- 6 files changed, 24 insertions(+), 44 deletions(-) diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index 391f29bf0..33a772a8d 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -18,8 +18,8 @@ ``` ```pcss -@design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); -@design-tokens url('./tokens-dark.json') variants('dark') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens-light.json') format('style-dictionary3'); +@design-tokens url('./tokens-dark.json') variants('dark') format('style-dictionary3'); .foo { color: design-token('color.background.primary'); @@ -79,8 +79,8 @@ postcssDesignTokens({ variants: ['dark'] }) ``` ```pcss -@design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); -@design-tokens url('./tokens-dark.json') variants('dark') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens-light.json') format('style-dictionary3'); +@design-tokens url('./tokens-dark.json') variants('dark') format('style-dictionary3'); .foo { color: design-token('color.background.primary'); diff --git a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts index 5ae1ae21c..7d86e3654 100644 --- a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts +++ b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts @@ -4,13 +4,12 @@ import { extractStyleDictionaryTokens, latestStyleDictionaryVersion } from './st import path from 'path'; import { promises as fsp } from 'fs'; -function parseImport(statement: string): { filePath: string, vendor: string, version: string, variants: Array } { +function parseImport(statement: string): { filePath: string, format: string, variants: Array } { const importAST = valueParser(statement); const result = { filePath: '', - vendor: 'standard', - version: '', + format: 'standard', variants: ['default'], }; @@ -19,12 +18,8 @@ function parseImport(statement: string): { filePath: string, vendor: string, ver result.filePath = node.nodes[0].value; } - if (node.type === 'function' && node.value === 'vendor') { - result.vendor = node.nodes[0].value; - } - - if (node.type === 'function' && node.value === 'version') { - result.version = node.nodes[0].value; + if (node.type === 'function' && node.value === 'format') { + result.format = node.nodes[0].value; } if (node.type === 'function' && node.value === 'variants') { @@ -36,26 +31,11 @@ function parseImport(statement: string): { filePath: string, vendor: string, ver result.variants = ['default']; } - if (!result.version) { - switch (result.vendor) { - case 'standard': - result.version = '0.0.1'; - break; - - case 'style-dictionary': - result.version = latestStyleDictionaryVersion; - break; - - default: - break; - } - } - return result; } export async function tokensFromImport(currentVariants: Array, sourceFilePath: string, statement: string, alreadyImported: Set): Promise<{ filePath: string, tokens: Map }|false> { - const { filePath, vendor, version, variants } = parseImport(statement); + const { filePath, format, variants } = parseImport(statement); if (!variants.every((variant) => currentVariants.includes(variant))) { return false; } @@ -70,16 +50,16 @@ export async function tokensFromImport(currentVariants: Array, sourceFil const fileContents = await fsp.readFile(resolvedPath, 'utf8'); const tokenContents = JSON.parse(fileContents); - switch (vendor) { - case 'style-dictionary': + switch (format) { + case 'style-dictionary3': return { filePath: path.resolve(filePath), - tokens: extractStyleDictionaryTokens(version, tokenContents, resolvedPath), + tokens: extractStyleDictionaryTokens('3', tokenContents, resolvedPath), }; default: break; } - throw new Error('Unsupported vendor: ' + vendor); + throw new Error('Unsupported format: ' + format); } diff --git a/plugins/postcss-design-tokens/test/basic.css b/plugins/postcss-design-tokens/test/basic.css index 0eb5dc7d2..5456feccc 100644 --- a/plugins/postcss-design-tokens/test/basic.css +++ b/plugins/postcss-design-tokens/test/basic.css @@ -1,5 +1,5 @@ @import url('./imported.css'); -@design-tokens url('./tokens/basic.json') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens/basic.json') format('style-dictionary3'); .foo { font-family: design-token('font.family.serif'); diff --git a/plugins/postcss-design-tokens/test/examples/example.css b/plugins/postcss-design-tokens/test/examples/example.css index 28d4ecca4..cca0fe960 100644 --- a/plugins/postcss-design-tokens/test/examples/example.css +++ b/plugins/postcss-design-tokens/test/examples/example.css @@ -1,5 +1,5 @@ -@design-tokens url('./tokens-light.json') vendor('style-dictionary') version('3'); -@design-tokens url('./tokens-dark.json') variants('dark') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens-light.json') format('style-dictionary3'); +@design-tokens url('./tokens-dark.json') variants('dark') format('style-dictionary3'); .foo { color: design-token('color.background.primary'); diff --git a/plugins/postcss-design-tokens/test/imported.css b/plugins/postcss-design-tokens/test/imported.css index 833f5e11c..6c68c5991 100644 --- a/plugins/postcss-design-tokens/test/imported.css +++ b/plugins/postcss-design-tokens/test/imported.css @@ -1 +1 @@ -@design-tokens url('../../../node_modules/style-dictionary-design-tokens-example/style-dictionary.tokens.json') vendor('style-dictionary') version('3'); +@design-tokens url('../../../node_modules/style-dictionary-design-tokens-example/style-dictionary.tokens.json') format('style-dictionary3'); diff --git a/plugins/postcss-design-tokens/test/variants.css b/plugins/postcss-design-tokens/test/variants.css index 6db1c1d4f..fce21ce06 100644 --- a/plugins/postcss-design-tokens/test/variants.css +++ b/plugins/postcss-design-tokens/test/variants.css @@ -1,10 +1,10 @@ -@design-tokens url('./tokens/color_dark_branded-blue.tokens.json') variants('dark', 'branded-blue') vendor('style-dictionary') version('3'); -@design-tokens url('./tokens/color_light_branded-blue.tokens.json') variants('light', 'branded-blue') vendor('style-dictionary') version('3'); -@design-tokens url('./tokens/color_dark_branded-green.tokens.json') variants('dark', 'branded-green') vendor('style-dictionary') version('3'); -@design-tokens url('./tokens/color_light_branded-green.tokens.json') variants('light', 'branded-green') vendor('style-dictionary') version('3'); -@design-tokens url('./tokens/size_mobile.tokens.json') variants('mobile') vendor('style-dictionary') version('3'); -@design-tokens url('./tokens/size_tablet.tokens.json') variants('tablet') vendor('style-dictionary') version('3'); -@design-tokens url('./tokens/size_desktop.tokens.json') variants('desktop') vendor('style-dictionary') version('3'); +@design-tokens url('./tokens/color_dark_branded-blue.tokens.json') variants('dark', 'branded-blue') format('style-dictionary3'); +@design-tokens url('./tokens/color_light_branded-blue.tokens.json') variants('light', 'branded-blue') format('style-dictionary3'); +@design-tokens url('./tokens/color_dark_branded-green.tokens.json') variants('dark', 'branded-green') format('style-dictionary3'); +@design-tokens url('./tokens/color_light_branded-green.tokens.json') variants('light', 'branded-green') format('style-dictionary3'); +@design-tokens url('./tokens/size_mobile.tokens.json') variants('mobile') format('style-dictionary3'); +@design-tokens url('./tokens/size_tablet.tokens.json') variants('tablet') format('style-dictionary3'); +@design-tokens url('./tokens/size_desktop.tokens.json') variants('desktop') format('style-dictionary3'); .foo { color: design-token('color'); From ffafaadf9677617bf02d4dec1baa40cddac2e894 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 27 Mar 2022 15:45:15 +0200 Subject: [PATCH 10/32] gather tokens first --- .../src/data-formats/parse-import.ts | 6 ++++-- plugins/postcss-design-tokens/src/index.ts | 21 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts index 7d86e3654..b685ead57 100644 --- a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts +++ b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts @@ -1,6 +1,6 @@ import valueParser from 'postcss-value-parser'; import { Token } from './base/token'; -import { extractStyleDictionaryTokens, latestStyleDictionaryVersion } from './style-dictionary/style-dictionary'; +import { extractStyleDictionaryTokens } from './style-dictionary/style-dictionary'; import path from 'path'; import { promises as fsp } from 'fs'; @@ -23,7 +23,9 @@ function parseImport(statement: string): { filePath: string, format: string, var } if (node.type === 'function' && node.value === 'variants') { - result.variants = node.nodes.filter((child) => child.type === 'string').map((child) => child.value); + result.variants = node.nodes.filter((child) => { + return child.type === 'string'; + }).map((child) => child.value); } }); diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts index f52cdce95..19a239f5e 100644 --- a/plugins/postcss-design-tokens/src/index.ts +++ b/plugins/postcss-design-tokens/src/index.ts @@ -25,9 +25,11 @@ const creator: PluginCreator = (opts?: pluginOptions) => { tokens = new Map(); importedFiles = new Set(); }, - AtRule: { - 'design-tokens': async (atRule, { result }) => { + Once: async (root, { result }) => { + const designTokenAtRules: Array<{filePath: string, params: string}> = []; + root.walkAtRules('design-tokens', (atRule) => { if (!atRule?.source?.input?.file) { + atRule.remove(); return; } @@ -35,20 +37,27 @@ const creator: PluginCreator = (opts?: pluginOptions) => { const params = atRule.params; atRule.remove(); - const importResult = await tokensFromImport(variants, filePath, params, importedFiles); + designTokenAtRules.push({ + filePath: filePath, + params: params, + }); + }); + + for (const atRule of designTokenAtRules.values()) { + const importResult = await tokensFromImport(variants, atRule.filePath, atRule.params, importedFiles); if (!importResult) { - return; + continue; } result.messages.push({ type: 'dependency', plugin: 'postcss-design-tokens', file: importResult.filePath, - parent: filePath, + parent: atRule.filePath, }); tokens = mergeTokens(tokens, importResult.tokens); - }, + } }, Declaration(decl, { result }) { if (decl.value.indexOf('design-token') === -1) { From b2abffc9e69a4d079e4776956d1d5fb996907c84 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 27 Mar 2022 18:13:59 +0200 Subject: [PATCH 11/32] feedback --- plugins/postcss-design-tokens/.tape.mjs | 8 +++----- plugins/postcss-design-tokens/README.md | 12 +++++++----- plugins/postcss-design-tokens/docs/README.md | 8 +++++--- .../src/data-formats/parse-import.ts | 18 +++++++++--------- plugins/postcss-design-tokens/src/index.ts | 10 +++++----- .../test/examples/example.css | 2 +- plugins/postcss-design-tokens/test/is.css | 12 ++++++++++++ .../{variants.expect.css => is.expect.css} | 0 .../postcss-design-tokens/test/variants.css | 12 ------------ 9 files changed, 42 insertions(+), 40 deletions(-) create mode 100644 plugins/postcss-design-tokens/test/is.css rename plugins/postcss-design-tokens/test/{variants.expect.css => is.expect.css} (100%) delete mode 100644 plugins/postcss-design-tokens/test/variants.css diff --git a/plugins/postcss-design-tokens/.tape.mjs b/plugins/postcss-design-tokens/.tape.mjs index 0f15d5ffd..60b696675 100644 --- a/plugins/postcss-design-tokens/.tape.mjs +++ b/plugins/postcss-design-tokens/.tape.mjs @@ -11,10 +11,10 @@ postcssTape(plugin)({ plugin() ] }, - 'variants': { + 'is': { message: "supports basic usage", options: { - variants: ['dark', 'tablet', 'branded-green'] + is: ['dark', 'tablet', 'branded-green'] } }, 'examples/example': { @@ -24,9 +24,7 @@ postcssTape(plugin)({ 'examples/example:dark': { message: 'minimal example with dark theme', options: { - variants: [ - 'dark' - ] + is: ['dark'] }, }, }); diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index 33a772a8d..fd4fb2860 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -19,7 +19,7 @@ ```pcss @design-tokens url('./tokens-light.json') format('style-dictionary3'); -@design-tokens url('./tokens-dark.json') variants('dark') format('style-dictionary3'); +@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3'); .foo { color: design-token('color.background.primary'); @@ -59,13 +59,15 @@ instructions for: ## Options -### variants +### is -The `variants` option determines which design tokens are used. +The `is` option determines which design tokens are used. This allows you to generate multiple themed stylesheets. +By default only `@design-tokens` without any `when('foo')` conditions are used. + ```js -postcssDesignTokens({ variants: ['dark'] }) +postcssDesignTokens({ is: ['dark'] }) ``` ```json @@ -80,7 +82,7 @@ postcssDesignTokens({ variants: ['dark'] }) ```pcss @design-tokens url('./tokens-light.json') format('style-dictionary3'); -@design-tokens url('./tokens-dark.json') variants('dark') format('style-dictionary3'); +@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3'); .foo { color: design-token('color.background.primary'); diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md index 7cbf40f26..0e308a9f3 100644 --- a/plugins/postcss-design-tokens/docs/README.md +++ b/plugins/postcss-design-tokens/docs/README.md @@ -34,13 +34,15 @@ ## Options -### variants +### is -The `variants` option determines which design tokens are used. +The `is` option determines which design tokens are used. This allows you to generate multiple themed stylesheets. +By default only `@design-tokens` without any `when('foo')` conditions are used. + ```js -({ variants: ['dark'] }) +({ is: ['dark'] }) ``` ```json diff --git a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts index b685ead57..39f609421 100644 --- a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts +++ b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts @@ -4,13 +4,13 @@ import { extractStyleDictionaryTokens } from './style-dictionary/style-dictionar import path from 'path'; import { promises as fsp } from 'fs'; -function parseImport(statement: string): { filePath: string, format: string, variants: Array } { +function parseImport(statement: string): { filePath: string, format: string, conditions: Array } { const importAST = valueParser(statement); const result = { filePath: '', format: 'standard', - variants: ['default'], + conditions: ['default'], }; importAST.walk((node) => { @@ -22,23 +22,23 @@ function parseImport(statement: string): { filePath: string, format: string, var result.format = node.nodes[0].value; } - if (node.type === 'function' && node.value === 'variants') { - result.variants = node.nodes.filter((child) => { + if (node.type === 'function' && node.value === 'when') { + result.conditions = node.nodes.filter((child) => { return child.type === 'string'; }).map((child) => child.value); } }); - if (!result.variants.length) { - result.variants = ['default']; + if (!result.conditions.length) { + result.conditions = ['default']; } return result; } -export async function tokensFromImport(currentVariants: Array, sourceFilePath: string, statement: string, alreadyImported: Set): Promise<{ filePath: string, tokens: Map }|false> { - const { filePath, format, variants } = parseImport(statement); - if (!variants.every((variant) => currentVariants.includes(variant))) { +export async function tokensFromImport(buildIs: Array, sourceFilePath: string, statement: string, alreadyImported: Set): Promise<{ filePath: string, tokens: Map }|false> { + const { filePath, format, conditions } = parseImport(statement); + if (!conditions.every((condition) => buildIs.includes(condition))) { return false; } diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts index 19a239f5e..e393f14ce 100644 --- a/plugins/postcss-design-tokens/src/index.ts +++ b/plugins/postcss-design-tokens/src/index.ts @@ -5,13 +5,13 @@ import { mergeTokens } from './data-formats/token'; import { onCSSValue } from './values'; type pluginOptions = { - variants?: Array + is?: Array } const creator: PluginCreator = (opts?: pluginOptions) => { - const variants = opts?.variants ?? []; - if (variants.length === 0) { - variants.push('default'); + const buildIs = opts?.is ?? []; + if (buildIs.length === 0) { + buildIs.push('default'); } return { @@ -44,7 +44,7 @@ const creator: PluginCreator = (opts?: pluginOptions) => { }); for (const atRule of designTokenAtRules.values()) { - const importResult = await tokensFromImport(variants, atRule.filePath, atRule.params, importedFiles); + const importResult = await tokensFromImport(buildIs, atRule.filePath, atRule.params, importedFiles); if (!importResult) { continue; } diff --git a/plugins/postcss-design-tokens/test/examples/example.css b/plugins/postcss-design-tokens/test/examples/example.css index cca0fe960..8592a21da 100644 --- a/plugins/postcss-design-tokens/test/examples/example.css +++ b/plugins/postcss-design-tokens/test/examples/example.css @@ -1,5 +1,5 @@ @design-tokens url('./tokens-light.json') format('style-dictionary3'); -@design-tokens url('./tokens-dark.json') variants('dark') format('style-dictionary3'); +@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3'); .foo { color: design-token('color.background.primary'); diff --git a/plugins/postcss-design-tokens/test/is.css b/plugins/postcss-design-tokens/test/is.css new file mode 100644 index 000000000..57ec7371b --- /dev/null +++ b/plugins/postcss-design-tokens/test/is.css @@ -0,0 +1,12 @@ +@design-tokens url('./tokens/color_dark_branded-blue.tokens.json') when('dark', 'branded-blue') format('style-dictionary3'); +@design-tokens url('./tokens/color_light_branded-blue.tokens.json') when('light', 'branded-blue') format('style-dictionary3'); +@design-tokens url('./tokens/color_dark_branded-green.tokens.json') when('dark', 'branded-green') format('style-dictionary3'); +@design-tokens url('./tokens/color_light_branded-green.tokens.json') when('light', 'branded-green') format('style-dictionary3'); +@design-tokens url('./tokens/size_mobile.tokens.json') when('mobile') format('style-dictionary3'); +@design-tokens url('./tokens/size_tablet.tokens.json') when('tablet') format('style-dictionary3'); +@design-tokens url('./tokens/size_desktop.tokens.json') when('desktop') format('style-dictionary3'); + +.foo { + color: design-token('color'); + font-size: design-token('size'); +} diff --git a/plugins/postcss-design-tokens/test/variants.expect.css b/plugins/postcss-design-tokens/test/is.expect.css similarity index 100% rename from plugins/postcss-design-tokens/test/variants.expect.css rename to plugins/postcss-design-tokens/test/is.expect.css diff --git a/plugins/postcss-design-tokens/test/variants.css b/plugins/postcss-design-tokens/test/variants.css deleted file mode 100644 index fce21ce06..000000000 --- a/plugins/postcss-design-tokens/test/variants.css +++ /dev/null @@ -1,12 +0,0 @@ -@design-tokens url('./tokens/color_dark_branded-blue.tokens.json') variants('dark', 'branded-blue') format('style-dictionary3'); -@design-tokens url('./tokens/color_light_branded-blue.tokens.json') variants('light', 'branded-blue') format('style-dictionary3'); -@design-tokens url('./tokens/color_dark_branded-green.tokens.json') variants('dark', 'branded-green') format('style-dictionary3'); -@design-tokens url('./tokens/color_light_branded-green.tokens.json') variants('light', 'branded-green') format('style-dictionary3'); -@design-tokens url('./tokens/size_mobile.tokens.json') variants('mobile') format('style-dictionary3'); -@design-tokens url('./tokens/size_tablet.tokens.json') variants('tablet') format('style-dictionary3'); -@design-tokens url('./tokens/size_desktop.tokens.json') variants('desktop') format('style-dictionary3'); - -.foo { - color: design-token('color'); - font-size: design-token('size'); -} From c5a3f77611445f0776f19ec4bf53866e32a1a77e Mon Sep 17 00:00:00 2001 From: romainmenke Date: Thu, 31 Mar 2022 17:33:45 +0200 Subject: [PATCH 12/32] wip --- .../json-transformer/index.cjs | 21 +++++++++++++++++++ .../json-transformer/package.json | 15 +++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 e2e/style-dictionary/json-transformer/index.cjs create mode 100644 e2e/style-dictionary/json-transformer/package.json diff --git a/e2e/style-dictionary/json-transformer/index.cjs b/e2e/style-dictionary/json-transformer/index.cjs new file mode 100644 index 000000000..2b485b55e --- /dev/null +++ b/e2e/style-dictionary/json-transformer/index.cjs @@ -0,0 +1,21 @@ +const StyleDictionary = require('style-dictionary').extend('config.json'); + +StyleDictionary.registerFormat({ + name: 'csstools/designTokens', + formatter: function ({ dictionary, platform, options, file }) { + return JSON.stringify(dictionary.tokens, null, 2); + }, +}); + +StyleDictionary.registerTransform({ + name: 'time/seconds', + type: 'value', + matcher: function (prop) { + return prop.attributes.category === 'time'; + }, + transformer: function (prop) { + return (parseInt(prop.original.value) / 1000).toString() + 's'; + }, +}); + +StyleDictionary.buildAllPlatforms(); diff --git a/e2e/style-dictionary/json-transformer/package.json b/e2e/style-dictionary/json-transformer/package.json new file mode 100644 index 000000000..b41157566 --- /dev/null +++ b/e2e/style-dictionary/json-transformer/package.json @@ -0,0 +1,15 @@ +{ + "name": "@csstools/style-dictionary--json-transformer", + "private": true, + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "devDependencies": { + "style-dictionary": "^3.7.0" + }, + "author": "", + "license": "MIT" +} From 2ff810fa4e7709815ac6262235a1019f92fc17b4 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 3 Apr 2022 12:34:48 +0200 Subject: [PATCH 13/32] implement dereferencing --- .../json-transformer/index.cjs | 21 -- .../json-transformer/package.json | 15 -- plugins/postcss-design-tokens/.tape.mjs | 8 +- plugins/postcss-design-tokens/README.md | 57 +++- plugins/postcss-design-tokens/docs/README.md | 42 ++- .../src/data-formats/parse-import.ts | 4 +- .../style-dictionary/v3/dereference.ts | 246 ++++++++++++++++++ .../data-formats/style-dictionary/v3/group.ts | 3 +- .../src/data-formats/toposort/toposort.ts | 104 ++++++++ plugins/postcss-design-tokens/src/index.ts | 2 +- plugins/postcss-design-tokens/test/basic.css | 7 +- .../test/basic.expect.css | 6 +- .../test/examples/example-conditional.css | 6 + ...ss => example-conditional.dark.expect.css} | 0 .../examples/example-conditional.expect.css | 3 + .../test/examples/example.css | 3 +- .../test/examples/tokens.json | 7 + .../test/tokens/basic.json | 17 +- 18 files changed, 495 insertions(+), 56 deletions(-) delete mode 100644 e2e/style-dictionary/json-transformer/index.cjs delete mode 100644 e2e/style-dictionary/json-transformer/package.json create mode 100644 plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts create mode 100644 plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts create mode 100644 plugins/postcss-design-tokens/test/examples/example-conditional.css rename plugins/postcss-design-tokens/test/examples/{example.dark.expect.css => example-conditional.dark.expect.css} (100%) create mode 100644 plugins/postcss-design-tokens/test/examples/example-conditional.expect.css create mode 100644 plugins/postcss-design-tokens/test/examples/tokens.json diff --git a/e2e/style-dictionary/json-transformer/index.cjs b/e2e/style-dictionary/json-transformer/index.cjs deleted file mode 100644 index 2b485b55e..000000000 --- a/e2e/style-dictionary/json-transformer/index.cjs +++ /dev/null @@ -1,21 +0,0 @@ -const StyleDictionary = require('style-dictionary').extend('config.json'); - -StyleDictionary.registerFormat({ - name: 'csstools/designTokens', - formatter: function ({ dictionary, platform, options, file }) { - return JSON.stringify(dictionary.tokens, null, 2); - }, -}); - -StyleDictionary.registerTransform({ - name: 'time/seconds', - type: 'value', - matcher: function (prop) { - return prop.attributes.category === 'time'; - }, - transformer: function (prop) { - return (parseInt(prop.original.value) / 1000).toString() + 's'; - }, -}); - -StyleDictionary.buildAllPlatforms(); diff --git a/e2e/style-dictionary/json-transformer/package.json b/e2e/style-dictionary/json-transformer/package.json deleted file mode 100644 index b41157566..000000000 --- a/e2e/style-dictionary/json-transformer/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@csstools/style-dictionary--json-transformer", - "private": true, - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "devDependencies": { - "style-dictionary": "^3.7.0" - }, - "author": "", - "license": "MIT" -} diff --git a/plugins/postcss-design-tokens/.tape.mjs b/plugins/postcss-design-tokens/.tape.mjs index 60b696675..9bc5019b7 100644 --- a/plugins/postcss-design-tokens/.tape.mjs +++ b/plugins/postcss-design-tokens/.tape.mjs @@ -21,8 +21,12 @@ postcssTape(plugin)({ message: 'minimal example', options: {}, }, - 'examples/example:dark': { - message: 'minimal example with dark theme', + 'examples/example-conditional': { + message: 'minimal example with conditional imports : default', + options: {}, + }, + 'examples/example-conditional:dark': { + message: 'minimal example with conditional imports : dark', options: { is: ['dark'] }, diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index fd4fb2860..f99cd463d 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -18,8 +18,7 @@ ``` ```pcss -@design-tokens url('./tokens-light.json') format('style-dictionary3'); -@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3'); +@design-tokens url('./tokens.json') format('style-dictionary3'); .foo { color: design-token('color.background.primary'); @@ -66,8 +65,18 @@ This allows you to generate multiple themed stylesheets. By default only `@design-tokens` without any `when('foo')` conditions are used. -```js -postcssDesignTokens({ is: ['dark'] }) +#### Example usage + +**For these two token files :** + +```json +{ + "color": { + "background": { + "primary": { "value": "#fff" } + } + } +} ``` ```json @@ -80,6 +89,46 @@ postcssDesignTokens({ is: ['dark'] }) } ``` +**And this CSS :** + +```pcss +@design-tokens url('./tokens-light.json') format('style-dictionary3'); +@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3'); + +.foo { + color: design-token('color.background.primary'); +} +``` + +**You can configure :** + +##### No `is` option. + +```js +postcssDesignTokens() +``` + +```pcss +@design-tokens url('./tokens-light.json') format('style-dictionary3'); +@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3'); + +.foo { + color: design-token('color.background.primary'); +} + +/* becomes */ + +.foo { + color: #fff; +} +``` + +##### `is` option set to 'dark'. + +```js +postcssDesignTokens({ is: ['dark'] }) +``` + ```pcss @design-tokens url('./tokens-light.json') format('style-dictionary3'); @design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3'); diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md index 0e308a9f3..ac3c9bced 100644 --- a/plugins/postcss-design-tokens/docs/README.md +++ b/plugins/postcss-design-tokens/docs/README.md @@ -17,7 +17,7 @@ [] lets easily create new plugins following some [CSS Specification]. ```json - + ``` ```pcss @@ -41,20 +41,52 @@ This allows you to generate multiple themed stylesheets. By default only `@design-tokens` without any `when('foo')` conditions are used. -```js -({ is: ['dark'] }) +#### Example usage + +**For these two token files :** + +```json + ``` ```json ``` +**And this CSS :** + ```pcss - + +``` + +**You can configure :** + +##### No `is` option. + +```js +() +``` + +```pcss + + +/* becomes */ + + +``` + +##### `is` option set to 'dark'. + +```js +({ is: ['dark'] }) +``` + +```pcss + /* becomes */ - + ``` diff --git a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts index 39f609421..d978506f2 100644 --- a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts +++ b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts @@ -10,7 +10,7 @@ function parseImport(statement: string): { filePath: string, format: string, con const result = { filePath: '', format: 'standard', - conditions: ['default'], + conditions: ['6b4e71e7-4787-42f7-a092-8684961895db'], // a random, but shared default condition }; importAST.walk((node) => { @@ -30,7 +30,7 @@ function parseImport(statement: string): { filePath: string, format: string, con }); if (!result.conditions.length) { - result.conditions = ['default']; + result.conditions = ['6b4e71e7-4787-42f7-a092-8684961895db']; // a random, but shared default condition } return result; diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts new file mode 100644 index 000000000..2c2e12577 --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts @@ -0,0 +1,246 @@ +import { toposort } from '../../toposort/toposort'; +import { StyleDictionaryV3TokenValue } from './value'; + +export function dereferenceTokenValues(tokens: Map): Map { + const tainted = new Set(); + const referenceASTs = new Map>(); + + // Gather all references. + { + for (const [id, token] of tokens.entries()) { + const referenceAST = parseReferences(token.value); + if (!referenceAST.length) { + continue; + } + + tainted.add(id); + referenceASTs.set(id, referenceAST); + } + } + + // Fast dereference. + // Only handles references for which a value can found immediately. + { + for (const [id, referenceAST] of referenceASTs.entries()) { + for (let i = 0; i < referenceAST.length; i++) { + const reference = referenceAST[i]; + if (reference.type !== 'value-reference') { + continue; + } + + if (tainted.has(reference.raw)) { + continue; + } + + if (!tokens.has(reference.raw)) { + throw new Error('Alias "' + reference.raw + '" not found'); + } + + const sourceToken = tokens.get(reference.raw); + referenceAST[i] = { + type: 'value-resolved', + raw: reference.raw, + value: sourceToken.cssValue(), + }; + } + + const hasFurtherReferences = referenceAST.some(part => part.type === 'value-reference'); + if (hasFurtherReferences) { + continue; + } + + const value = (referenceAST as ValuePartsResolved).map(part => part.value).join(''); + const currentToken = tokens.get(id); + + currentToken.value = value; + currentToken.cssValue = () => { + return value ?? ''; + }; + + tokens.set(id, currentToken); + tainted.delete(id); + referenceASTs.delete(id); + } + + if (tainted.size === 0) { + return tokens; + } + } + + // Topological dereferrence. + { + const nodes : Array = Array.from(tokens.keys()); + const edges: Array> = []; + + for (const [id, referenceAST] of referenceASTs.entries()) { + for (let i = 0; i < referenceAST.length; i++) { + const reference = referenceAST[i]; + if (reference.type !== 'value-reference') { + continue; + } + + edges.push([reference.raw, id]); + } + } + + const sorted = toposort(nodes, edges); + if (!sorted) { + throw new Error('Circular reference detected'); + } + + for (const id of sorted) { + if (!referenceASTs.has(id)) { + continue; + } + + const referenceAST = referenceASTs.get(id); + for (let i = 0; i < referenceAST.length; i++) { + const reference = referenceAST[i]; + if (reference.type !== 'value-reference') { + continue; + } + + if (tainted.has(reference.raw)) { + throw new Error('Alias "' + reference.raw + '" can not be resolved'); + } + + if (!tokens.has(reference.raw)) { + throw new Error('Alias "' + reference.raw + '" not found'); + } + + const sourceToken = tokens.get(reference.raw); + referenceAST[i] = { + type: 'value-resolved', + raw: reference.raw, + value: sourceToken.cssValue(), + }; + } + + const hasFurtherReferences = referenceAST.some(part => part.type === 'value-reference'); + if (hasFurtherReferences) { + throw new Error('Token "' + id + '" can not be fully resolved'); + } + + const value = (referenceAST as ValuePartsResolved).map(part => part.value).join(''); + const currentToken = tokens.get(id); + + currentToken.value = value; + currentToken.cssValue = () => { + return value ?? ''; + }; + + tokens.set(id, currentToken); + tainted.delete(id); + referenceASTs.delete(id); + } + + if (tainted.size === 0) { + return tokens; + } + } + + return tokens; +} + +type ValuePartsResolved = Array; +type ValuePart = ValueReference | ValueNonReference | ValueResolved; + +type ValueReference = { + type: 'value-reference', + raw: string, +} + +type ValueResolved = { + type: 'value-resolved', + raw: string, + value: string, +} + +type ValueNonReference = { + type: 'value-non-reference', + value: string, +} + +function parseReferences(valueWithReferences: unknown): Array { + if (typeof valueWithReferences !== 'string') { + return []; + } + + if (valueWithReferences.indexOf('{') === -1) { + return []; + } + + const result: Array = []; + let hasReferences = false; + + let inReference = false; + let buf = ''; + + for (let index = 0; index < valueWithReferences.length; index++) { + const char = valueWithReferences[index]; + + switch (char) { + case '{': + if (inReference) { + throw new Error('Unexpected "{" in "' + valueWithReferences + '" at ' + index); + } + + if (buf.length > 0) { + result.push({ + type: 'value-non-reference', + value: buf, + }); + buf = ''; + } + + inReference = true; + break; + case '}': + if (!inReference) { + throw new Error('Unexpected "}" in "' + valueWithReferences + '" at ' + index); + } + + if (buf.length === 0) { + throw new Error('Empty alias "{}" in "' + valueWithReferences + '" at ' + index); + } + + { + let reference = buf.trim(); + if (reference.slice(-6) === '.value') { + reference = reference.slice(0, -6); + } + + result.push({ + type: 'value-reference', + raw: reference, + }); + buf = ''; + } + + hasReferences = true; + inReference = false; + break; + + default: + buf += char; + break; + } + } + + if (inReference) { + throw new Error('Unexpected end of alias in "' + valueWithReferences + '"'); + } + + if (buf.length > 0) { + result.push({ + type: 'value-non-reference', + value: buf, + }); + } + + if (!hasReferences) { + return []; + } + + return result; +} diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/group.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/group.ts index 2115706ae..b21c3955b 100644 --- a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/group.ts +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/group.ts @@ -1,4 +1,5 @@ import { Token } from '../../base/token'; +import { dereferenceTokenValues } from './dereference'; import { extractStyleDictionaryV3Token, StyleDictionaryV3TokenValue } from './value'; export type StyleDictionaryV3TokenGroup = { @@ -33,5 +34,5 @@ function extractTokens(node: StyleDictionaryV3TokenGroup, path: Array, f } export function extractStyleDictionaryV3Tokens(node: StyleDictionaryV3TokenGroup, filePath: string): Map { - return extractTokens(node, [], filePath); + return dereferenceTokenValues(extractTokens(node, [], filePath)); } diff --git a/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts b/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts new file mode 100644 index 000000000..15f3c39be --- /dev/null +++ b/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts @@ -0,0 +1,104 @@ +// Toposort - Topological sorting for node.js +// Copyright (c) 2012 by Marcel Klehr +// MIT LICENSE +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +export function toposort(nodes: Array, edges: Array>): Array { + let cursor = nodes.length; + const sorted: Array = new Array(cursor); + const visited = {}; + let i = cursor; + // Better data structures make algorithm much faster. + const outgoingEdges = makeOutgoingEdges(edges); + const nodesHash = makeNodesHash(nodes); + + // check for unknown nodes + edges.forEach(function (edge) { + if (!nodesHash.has(edge[0]) || !nodesHash.has(edge[1])) { + throw new Error('Unknown node. There is an unknown node in the supplied edges.'); + } + }); + + while (i--) { + if (!visited[i]) { + visit(nodes[i], i, new Set()); + } + } + + return sorted; + + function visit(node, j, predecessors) { + if (predecessors.has(node)) { + let nodeRep; + try { + nodeRep = ', node was:' + JSON.stringify(node); + } catch (e) { + nodeRep = ''; + } + throw new Error('Cyclic dependency' + nodeRep); + } + + if (!nodesHash.has(node)) { + throw new Error('Found unknown node. Make sure to provided all involved nodes. Unknown node: ' + JSON.stringify(node)); + } + + if (visited[j]) { + return; + } + visited[j] = true; + + let outgoing = outgoingEdges.get(node) || new Set(); + outgoing = Array.from(outgoing); + + // eslint-disable-next-line no-cond-assign + if (j = outgoing.length) { + predecessors.add(node); + do { + const child = outgoing[--j]; + visit(child, nodesHash.get(child), predecessors); + } while (j); + predecessors.delete(node); + } + + sorted[--cursor] = node; + } +} + +function makeOutgoingEdges(arr) { + const edges = new Map(); + for (let i = 0, len = arr.length; i < len; i++) { + const edge = arr[i]; + if (!edges.has(edge[0])) { + edges.set(edge[0], new Set()); + } + if (!edges.has(edge[1])) { + edges.set(edge[1], new Set()); + } + edges.get(edge[0]).add(edge[1]); + } + return edges; +} + +function makeNodesHash(arr) { + const res = new Map(); + for (let i = 0, len = arr.length; i < len; i++) { + res.set(arr[i], i); + } + return res; +} diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts index e393f14ce..ccf415d0b 100644 --- a/plugins/postcss-design-tokens/src/index.ts +++ b/plugins/postcss-design-tokens/src/index.ts @@ -11,7 +11,7 @@ type pluginOptions = { const creator: PluginCreator = (opts?: pluginOptions) => { const buildIs = opts?.is ?? []; if (buildIs.length === 0) { - buildIs.push('default'); + buildIs.push('6b4e71e7-4787-42f7-a092-8684961895db'); // a random, but shared default condition } return { diff --git a/plugins/postcss-design-tokens/test/basic.css b/plugins/postcss-design-tokens/test/basic.css index 5456feccc..3f9ddf7f4 100644 --- a/plugins/postcss-design-tokens/test/basic.css +++ b/plugins/postcss-design-tokens/test/basic.css @@ -2,7 +2,12 @@ @design-tokens url('./tokens/basic.json') format('style-dictionary3'); .foo { - font-family: design-token('font.family.serif'); + font-family: design-token('font.family.base'); font-size: design-token('size.font.small'); color: design-token('color.font.base'); } + +.card { + background-color: design-token('card.background'); + color: design-token('card.foreground'); +} diff --git a/plugins/postcss-design-tokens/test/basic.expect.css b/plugins/postcss-design-tokens/test/basic.expect.css index d0e8338ea..459bcb6ad 100644 --- a/plugins/postcss-design-tokens/test/basic.expect.css +++ b/plugins/postcss-design-tokens/test/basic.expect.css @@ -1,5 +1,9 @@ .foo { - font-family: serif; + font-family: Helvetica sans; font-size: 10; color: #111111; } +.card { + background-color: blue; + color: red; +} diff --git a/plugins/postcss-design-tokens/test/examples/example-conditional.css b/plugins/postcss-design-tokens/test/examples/example-conditional.css new file mode 100644 index 000000000..8592a21da --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/example-conditional.css @@ -0,0 +1,6 @@ +@design-tokens url('./tokens-light.json') format('style-dictionary3'); +@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3'); + +.foo { + color: design-token('color.background.primary'); +} diff --git a/plugins/postcss-design-tokens/test/examples/example.dark.expect.css b/plugins/postcss-design-tokens/test/examples/example-conditional.dark.expect.css similarity index 100% rename from plugins/postcss-design-tokens/test/examples/example.dark.expect.css rename to plugins/postcss-design-tokens/test/examples/example-conditional.dark.expect.css diff --git a/plugins/postcss-design-tokens/test/examples/example-conditional.expect.css b/plugins/postcss-design-tokens/test/examples/example-conditional.expect.css new file mode 100644 index 000000000..33a418a59 --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/example-conditional.expect.css @@ -0,0 +1,3 @@ +.foo { + color: #fff; +} diff --git a/plugins/postcss-design-tokens/test/examples/example.css b/plugins/postcss-design-tokens/test/examples/example.css index 8592a21da..c5d97f83a 100644 --- a/plugins/postcss-design-tokens/test/examples/example.css +++ b/plugins/postcss-design-tokens/test/examples/example.css @@ -1,5 +1,4 @@ -@design-tokens url('./tokens-light.json') format('style-dictionary3'); -@design-tokens url('./tokens-dark.json') when('dark') format('style-dictionary3'); +@design-tokens url('./tokens.json') format('style-dictionary3'); .foo { color: design-token('color.background.primary'); diff --git a/plugins/postcss-design-tokens/test/examples/tokens.json b/plugins/postcss-design-tokens/test/examples/tokens.json new file mode 100644 index 000000000..9776bbaed --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/tokens.json @@ -0,0 +1,7 @@ +{ + "color": { + "background": { + "primary": { "value": "#fff" } + } + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/basic.json b/plugins/postcss-design-tokens/test/tokens/basic.json index 9ee085a9c..925b731ff 100644 --- a/plugins/postcss-design-tokens/test/tokens/basic.json +++ b/plugins/postcss-design-tokens/test/tokens/basic.json @@ -1,7 +1,22 @@ { "font": { "family": { - "serif": { "value": "serif" } + "sans": { "value": "sans" }, + "helvetica": { "value": "Helvetica" }, + "serif": { "value": "serif" }, + "base": { "value": "{ font.family.helvetica.value} {font.family.sans.value}" } } + }, + "card": { + "foreground": { "value": "{logical-color.foreground}" }, + "background": { "value": "{logical-color.background}" } + }, + "base-color": { + "red": { "value": "red" }, + "blue": { "value": "blue" } + }, + "logical-color": { + "foreground": { "value": "{base-color.red}" }, + "background": { "value": "{base-color.blue}" } } } From e1daac40edfaceb35055ed45e46f0af5bbaa75b7 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 3 Apr 2022 13:07:36 +0200 Subject: [PATCH 14/32] error handling --- plugins/postcss-design-tokens/.tape.mjs | 5 +++++ .../src/data-formats/toposort/toposort.ts | 6 +++--- plugins/postcss-design-tokens/src/index.ts | 16 +++++++++++----- plugins/postcss-design-tokens/test/errors.css | 4 ++++ .../postcss-design-tokens/test/errors.expect.css | 1 + .../test/tokens/circular.json | 6 ++++++ .../test/tokens/missing.json | 5 +++++ 7 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 plugins/postcss-design-tokens/test/errors.css create mode 100644 plugins/postcss-design-tokens/test/errors.expect.css create mode 100644 plugins/postcss-design-tokens/test/tokens/circular.json create mode 100644 plugins/postcss-design-tokens/test/tokens/missing.json diff --git a/plugins/postcss-design-tokens/.tape.mjs b/plugins/postcss-design-tokens/.tape.mjs index 9bc5019b7..ad0f3c60d 100644 --- a/plugins/postcss-design-tokens/.tape.mjs +++ b/plugins/postcss-design-tokens/.tape.mjs @@ -11,6 +11,11 @@ postcssTape(plugin)({ plugin() ] }, + 'errors': { + message: "handles issues correctly", + options: {}, + warnings: 4 + }, 'is': { message: "supports basic usage", options: { diff --git a/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts b/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts index 15f3c39be..b3890c1c8 100644 --- a/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts +++ b/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts @@ -31,7 +31,7 @@ export function toposort(nodes: Array, edges: Array>): Arr // check for unknown nodes edges.forEach(function (edge) { if (!nodesHash.has(edge[0]) || !nodesHash.has(edge[1])) { - throw new Error('Unknown node. There is an unknown node in the supplied edges.'); + throw new Error('Unknown token. Make sure to provide all tokens used in aliases.'); } }); @@ -47,7 +47,7 @@ export function toposort(nodes: Array, edges: Array>): Arr if (predecessors.has(node)) { let nodeRep; try { - nodeRep = ', node was:' + JSON.stringify(node); + nodeRep = ', token was: ' + JSON.stringify(node); } catch (e) { nodeRep = ''; } @@ -55,7 +55,7 @@ export function toposort(nodes: Array, edges: Array>): Arr } if (!nodesHash.has(node)) { - throw new Error('Found unknown node. Make sure to provided all involved nodes. Unknown node: ' + JSON.stringify(node)); + throw new Error('Found unknown token. Make sure to provided all involved tokens. Unknown token: ' + JSON.stringify(node)); } if (visited[j]) { diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts index ccf415d0b..a6e724c19 100644 --- a/plugins/postcss-design-tokens/src/index.ts +++ b/plugins/postcss-design-tokens/src/index.ts @@ -1,4 +1,4 @@ -import type { PluginCreator } from 'postcss'; +import type { Node, PluginCreator } from 'postcss'; import { Token } from './data-formats/base/token'; import { tokensFromImport } from './data-formats/parse-import'; import { mergeTokens } from './data-formats/token'; @@ -26,10 +26,9 @@ const creator: PluginCreator = (opts?: pluginOptions) => { importedFiles = new Set(); }, Once: async (root, { result }) => { - const designTokenAtRules: Array<{filePath: string, params: string}> = []; + const designTokenAtRules: Array<{filePath: string, params: string, node: Node}> = []; root.walkAtRules('design-tokens', (atRule) => { if (!atRule?.source?.input?.file) { - atRule.remove(); return; } @@ -40,12 +39,19 @@ const creator: PluginCreator = (opts?: pluginOptions) => { designTokenAtRules.push({ filePath: filePath, params: params, + node: atRule, }); }); for (const atRule of designTokenAtRules.values()) { - const importResult = await tokensFromImport(buildIs, atRule.filePath, atRule.params, importedFiles); - if (!importResult) { + let importResult: { filePath: string, tokens: Map }|false; + try { + importResult = await tokensFromImport(buildIs, atRule.filePath, atRule.params, importedFiles); + if (!importResult) { + continue; + } + } catch (e) { + atRule.node.warn(result, `Failed to import design tokens from "${atRule.params}" with error:\n\t` + e.message); continue; } diff --git a/plugins/postcss-design-tokens/test/errors.css b/plugins/postcss-design-tokens/test/errors.css new file mode 100644 index 000000000..92fb33bab --- /dev/null +++ b/plugins/postcss-design-tokens/test/errors.css @@ -0,0 +1,4 @@ +@design-tokens url('./tokens/does-not-exist.json') format('style-dictionary3'); +@design-tokens url('./tokens/basic.json') format('does not exist'); +@design-tokens url('./tokens/circular.json') format('style-dictionary3'); +@design-tokens url('./tokens/missing.json') format('style-dictionary3'); diff --git a/plugins/postcss-design-tokens/test/errors.expect.css b/plugins/postcss-design-tokens/test/errors.expect.css new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/plugins/postcss-design-tokens/test/errors.expect.css @@ -0,0 +1 @@ + diff --git a/plugins/postcss-design-tokens/test/tokens/circular.json b/plugins/postcss-design-tokens/test/tokens/circular.json new file mode 100644 index 000000000..805827672 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/circular.json @@ -0,0 +1,6 @@ +{ + "a": { + "1": { "value": "{a.2}" }, + "2": { "value": "{a.1}" } + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/missing.json b/plugins/postcss-design-tokens/test/tokens/missing.json new file mode 100644 index 000000000..e08a0b0bc --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/missing.json @@ -0,0 +1,5 @@ +{ + "a": { + "1": { "value": "{a.2}" } + } +} From cb2b08f6642bf153865b45311ddbbc3754ec5005 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Thu, 21 Apr 2022 09:32:11 +0200 Subject: [PATCH 15/32] typo --- .../src/data-formats/style-dictionary/v3/dereference.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts index 2c2e12577..e32b3aa1e 100644 --- a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts @@ -67,7 +67,7 @@ export function dereferenceTokenValues(tokens: Map = Array.from(tokens.keys()); const edges: Array> = []; From 93e6e6a7d0e6b19c66734a821ad0cfec685eea81 Mon Sep 17 00:00:00 2001 From: Romain Menke <11521496+romainmenke@users.noreply.github.com> Date: Sat, 21 May 2022 10:46:41 +0200 Subject: [PATCH 16/32] design tokens : unit conversion (#388) --- plugins/postcss-design-tokens/.tape.mjs | 12 +++- .../src/data-formats/base/token.ts | 9 ++- .../style-dictionary/v3/dereference.ts | 11 ++-- .../data-formats/style-dictionary/v3/value.ts | 64 ++++++++++++++++++- plugins/postcss-design-tokens/src/index.ts | 6 +- plugins/postcss-design-tokens/src/options.ts | 6 ++ plugins/postcss-design-tokens/src/values.ts | 41 +++++++++--- plugins/postcss-design-tokens/test/basic.css | 41 ++++++++++++ .../test/basic.expect.css | 30 +++++++++ .../test/basic.rootFontSize-20.expect.css | 39 +++++++++++ .../test/tokens/basic.json | 23 +++++++ 11 files changed, 260 insertions(+), 22 deletions(-) create mode 100644 plugins/postcss-design-tokens/src/options.ts create mode 100644 plugins/postcss-design-tokens/test/basic.rootFontSize-20.expect.css diff --git a/plugins/postcss-design-tokens/.tape.mjs b/plugins/postcss-design-tokens/.tape.mjs index ad0f3c60d..909449c6b 100644 --- a/plugins/postcss-design-tokens/.tape.mjs +++ b/plugins/postcss-design-tokens/.tape.mjs @@ -5,12 +5,22 @@ import postcssImport from 'postcss-import'; postcssTape(plugin)({ basic: { message: "supports basic usage", - options: {}, plugins: [ postcssImport(), plugin() ] }, + 'basic:rootFontSize-20': { + message: "supports basic usage with { unitsAndValues { rootFontSize: 20 } }", + plugins: [ + postcssImport(), + plugin({ + unitsAndValues: { + rootFontSize: 20 + } + }) + ] + }, 'errors': { message: "handles issues correctly", options: {}, diff --git a/plugins/postcss-design-tokens/src/data-formats/base/token.ts b/plugins/postcss-design-tokens/src/data-formats/base/token.ts index ac789984f..5b1a12f1e 100644 --- a/plugins/postcss-design-tokens/src/data-formats/base/token.ts +++ b/plugins/postcss-design-tokens/src/data-formats/base/token.ts @@ -1,3 +1,10 @@ +export interface TokenTransformOptions { + pluginOptions?: { + rootFontSize?: number; + }; + toUnit?: string; +} + export interface Token { - cssValue(): string + cssValue(opts?: TokenTransformOptions): string } diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts index e32b3aa1e..897bed1bf 100644 --- a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts @@ -1,5 +1,6 @@ +import { TokenTransformOptions } from '../../base/token'; import { toposort } from '../../toposort/toposort'; -import { StyleDictionaryV3TokenValue } from './value'; +import { applyTransformsToValue, StyleDictionaryV3TokenValue } from './value'; export function dereferenceTokenValues(tokens: Map): Map { const tainted = new Set(); @@ -53,8 +54,8 @@ export function dereferenceTokenValues(tokens: Map { - return value ?? ''; + currentToken.cssValue = (transformOptions: TokenTransformOptions) => { + return applyTransformsToValue(value, transformOptions); }; tokens.set(id, currentToken); @@ -125,8 +126,8 @@ export function dereferenceTokenValues(tokens: Map { - return value ?? ''; + currentToken.cssValue = (transformOptions: TokenTransformOptions) => { + return applyTransformsToValue(value, transformOptions); }; tokens.set(id, currentToken); diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts index 8b96fbefe..9f3a5a1fe 100644 --- a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts @@ -1,5 +1,8 @@ +import { TokenTransformOptions } from '../../base/token'; +import valueParser from 'postcss-value-parser'; + export type StyleDictionaryV3TokenValue = { - cssValue(): string + cssValue(transformOptions?: TokenTransformOptions): string // The value of the design token. This can be any type of data, a hex string, an integer, a file path to a file, even an object or array. value: unknown // Usually the name for a design token is generated with a name transform, but you can write your own if you choose. By default Style Dictionary will add a default name which is the key of the design token object. @@ -33,8 +36,8 @@ export function extractStyleDictionaryV3Token(node: unknown, key: string, path: return { value: value, - cssValue: () => { - return value ?? ''; + cssValue: (transformOptions?: TokenTransformOptions) => { + return applyTransformsToValue(value, transformOptions); }, name: node['name'] ?? key, comment: node['comment'] ?? undefined, @@ -46,3 +49,58 @@ export function extractStyleDictionaryV3Token(node: unknown, key: string, path: }, }; } + +export function applyTransformsToValue(value: string|undefined|null, transformOptions?: TokenTransformOptions): string { + if (!value) { + return ''; + } + + if (!transformOptions) { + return value; + } + + if (transformOptions.toUnit) { + const dimension = valueParser.unit(value ?? ''); + if (!dimension || dimension.unit === transformOptions.toUnit) { + return `${value}`; + } + + if (dimension.unit === 'rem' && transformOptions.toUnit === 'px') { + return remToPx(parseFloat(dimension.number), transformOptions.pluginOptions?.rootFontSize ?? 16); + } + + if (dimension.unit === 'px' && transformOptions.toUnit === 'rem') { + return pxToRem(parseFloat(dimension.number), transformOptions.pluginOptions?.rootFontSize ?? 16); + } + } + + return value; +} + +function remToPx(value: number, rootFontSize: number): string { + return `${formatFloat(value * rootFontSize)}px`; +} + +function pxToRem(value: number, rootFontSize: number): string { + return `${formatFloat(value / rootFontSize)}rem`; +} + +function formatFloat(value: number): string { + if (Number.isInteger(value)) { + return value.toString(); + } + + let fixedPrecision = value.toFixed(5); + for (let i = fixedPrecision.length; i > 0; i--) { + if (fixedPrecision[i] === '.') { + break; + } + + if (fixedPrecision[i] !== '0') { + fixedPrecision = fixedPrecision.slice(0, i + 1); + continue; + } + } + + return fixedPrecision; +} diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts index a6e724c19..580defa0a 100644 --- a/plugins/postcss-design-tokens/src/index.ts +++ b/plugins/postcss-design-tokens/src/index.ts @@ -2,11 +2,9 @@ import type { Node, PluginCreator } from 'postcss'; import { Token } from './data-formats/base/token'; import { tokensFromImport } from './data-formats/parse-import'; import { mergeTokens } from './data-formats/token'; +import { pluginOptions } from './options'; import { onCSSValue } from './values'; -type pluginOptions = { - is?: Array -} const creator: PluginCreator = (opts?: pluginOptions) => { const buildIs = opts?.is ?? []; @@ -70,7 +68,7 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } - const modifiedValue = onCSSValue(tokens, result, decl); + const modifiedValue = onCSSValue(tokens, result, decl, opts); if (modifiedValue === decl.value) { return; } diff --git a/plugins/postcss-design-tokens/src/options.ts b/plugins/postcss-design-tokens/src/options.ts new file mode 100644 index 000000000..1d4dcb734 --- /dev/null +++ b/plugins/postcss-design-tokens/src/options.ts @@ -0,0 +1,6 @@ +export type pluginOptions = { + is?: Array + unitsAndValues?: { + rootFontSize?: number + } +} diff --git a/plugins/postcss-design-tokens/src/values.ts b/plugins/postcss-design-tokens/src/values.ts index e1db3194d..22cc949dc 100644 --- a/plugins/postcss-design-tokens/src/values.ts +++ b/plugins/postcss-design-tokens/src/values.ts @@ -1,8 +1,9 @@ import type { Declaration, Result } from 'postcss'; import valueParser from 'postcss-value-parser'; -import { Token } from './data-formats/base/token'; +import { Token, TokenTransformOptions } from './data-formats/base/token'; +import { pluginOptions } from './options'; -export function onCSSValue(tokens: Map, result: Result, decl: Declaration) { +export function onCSSValue(tokens: Map, result: Result, decl: Declaration, opts?: pluginOptions) { const valueAST = valueParser(decl.value); valueAST.walk(node => { @@ -10,23 +11,47 @@ export function onCSSValue(tokens: Map, result: Result, decl: Dec return; } - if (!node.nodes || node.nodes.length !== 1) { - decl.warn(result, 'Expected a single string literal for the design-token function.'); + if (!node.nodes || node.nodes.length === 0) { + decl.warn(result, 'Expected at least a single string literal for the design-token function.'); return; } if (node.nodes[0].type !== 'string') { - decl.warn(result, 'Expected a single string literal for the design-token function.'); + decl.warn(result, 'Expected at least a single string literal for the design-token function.'); return; } - const replacement = tokens.get(node.nodes[0].value); + const tokenName = node.nodes[0].value; + const replacement = tokens.get(tokenName); if (!replacement) { - decl.warn(result, `design-token: "${node.nodes[0].value}" is not configured.`); + decl.warn(result, `design-token: "${tokenName}" is not configured.`); return; } - node.value = replacement.cssValue(); + const remainingNodes = node.nodes.slice(1).filter(x => x.type === 'word'); + if (!remainingNodes.length) { + node.value = replacement.cssValue(); + node.nodes = undefined; + return; + } + + const transformOptions: TokenTransformOptions = { + pluginOptions: opts?.unitsAndValues, + }; + for (let i = 0; i < remainingNodes.length; i++) { + if ( + remainingNodes[i].type === 'word' && + remainingNodes[i].value === 'to' && + remainingNodes[i + 1] && + remainingNodes[i + 1].type === 'word' && + ['px', 'rem'].includes(remainingNodes[i + 1].value) + ) { + transformOptions.toUnit = remainingNodes[i + 1].value; + i++; + } + } + + node.value = replacement.cssValue(transformOptions); node.nodes = undefined; }); diff --git a/plugins/postcss-design-tokens/test/basic.css b/plugins/postcss-design-tokens/test/basic.css index 3f9ddf7f4..9fc8643f7 100644 --- a/plugins/postcss-design-tokens/test/basic.css +++ b/plugins/postcss-design-tokens/test/basic.css @@ -10,4 +10,45 @@ .card { background-color: design-token('card.background'); color: design-token('card.foreground'); + color: design-token( 'card.foreground'); + color: design-token('card.foreground' ); + color: design-token( + /* a foreground color */ + 'card.foreground' + ); + color: design-token( + 'card.foreground' + /* a foreground color */ + ); +} + +.px-to-px { + padding-bottom: design-token('space.small' to px); + padding-bottom: design-token('space.default' to px); + padding-bottom: design-token('space.large' to px); +} + +.px-to-rem { + padding-bottom: design-token('space.small' to rem); + padding-bottom: design-token('space.default' to rem); + padding-bottom: design-token('space.large' to rem); +} + +.rem-to-rem { + padding-bottom: design-token('space.small-b' to rem); + padding-bottom: design-token('space.default-b' to rem); + padding-bottom: design-token('space.large-b' to rem); +} + +.rem-to-px { + padding-bottom: design-token('space.small-b' to px); + padding-bottom: design-token('space.default-b' to px); + padding-bottom: design-token('space.large-b' to px); +} + +.invalid-conversion { + color: design-token('card.foreground' to rem); + color: design-token('card.foreground' to px); + color: design-token('space.lh' to rem); + color: design-token('space.lh' to px); } diff --git a/plugins/postcss-design-tokens/test/basic.expect.css b/plugins/postcss-design-tokens/test/basic.expect.css index 459bcb6ad..d4b93d305 100644 --- a/plugins/postcss-design-tokens/test/basic.expect.css +++ b/plugins/postcss-design-tokens/test/basic.expect.css @@ -6,4 +6,34 @@ .card { background-color: blue; color: red; + color: red; + color: red; + color: red; + color: red; +} +.px-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.px-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.1rem; + padding-bottom: 2rem; +} +.rem-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.125rem; + padding-bottom: 2rem; +} +.rem-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.invalid-conversion { + color: red; + color: red; + color: 1lh; + color: 1lh; } diff --git a/plugins/postcss-design-tokens/test/basic.rootFontSize-20.expect.css b/plugins/postcss-design-tokens/test/basic.rootFontSize-20.expect.css new file mode 100644 index 000000000..d1915ac36 --- /dev/null +++ b/plugins/postcss-design-tokens/test/basic.rootFontSize-20.expect.css @@ -0,0 +1,39 @@ +.foo { + font-family: Helvetica sans; + font-size: 10; + color: #111111; +} +.card { + background-color: blue; + color: red; + color: red; + color: red; + color: red; + color: red; +} +.px-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.px-to-rem { + padding-bottom: 0.4rem; + padding-bottom: 0.9rem; + padding-bottom: 1.6rem; +} +.rem-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.125rem; + padding-bottom: 2rem; +} +.rem-to-px { + padding-bottom: 10px; + padding-bottom: 22.5px; + padding-bottom: 40px; +} +.invalid-conversion { + color: red; + color: red; + color: 1lh; + color: 1lh; +} diff --git a/plugins/postcss-design-tokens/test/tokens/basic.json b/plugins/postcss-design-tokens/test/tokens/basic.json index 925b731ff..1ec411322 100644 --- a/plugins/postcss-design-tokens/test/tokens/basic.json +++ b/plugins/postcss-design-tokens/test/tokens/basic.json @@ -18,5 +18,28 @@ "logical-color": { "foreground": { "value": "{base-color.red}" }, "background": { "value": "{base-color.blue}" } + }, + "space": { + "small": { + "value": "8px" + }, + "default": { + "value": "18px" + }, + "large": { + "value": "32px" + }, + "small-b": { + "value": "0.5rem" + }, + "default-b": { + "value": "1.125rem" + }, + "large-b": { + "value": "2rem" + }, + "lh": { + "value": "1lh" + } } } From 42e317f32fb7176c202b7f86d077f1c29537dbee Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sat, 21 May 2022 10:57:12 +0200 Subject: [PATCH 17/32] update docs --- plugins/postcss-design-tokens/.tape.mjs | 8 ++++ plugins/postcss-design-tokens/README.md | 44 +++++++++++++++++++ plugins/postcss-design-tokens/docs/README.md | 25 +++++++++++ .../test/examples/example.css | 2 + .../test/examples/example.expect.css | 2 + .../example.rootFontSize-20.expect.css | 5 +++ .../test/examples/tokens.json | 5 +++ 7 files changed, 91 insertions(+) create mode 100644 plugins/postcss-design-tokens/test/examples/example.rootFontSize-20.expect.css diff --git a/plugins/postcss-design-tokens/.tape.mjs b/plugins/postcss-design-tokens/.tape.mjs index 909449c6b..a54df4608 100644 --- a/plugins/postcss-design-tokens/.tape.mjs +++ b/plugins/postcss-design-tokens/.tape.mjs @@ -46,4 +46,12 @@ postcssTape(plugin)({ is: ['dark'] }, }, + 'examples/example:rootFontSize-20': { + message: "minimal example with { unitsAndValues { rootFontSize: 20 } }", + options: { + unitsAndValues: { + rootFontSize: 20 + } + } + }, }); diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index f99cd463d..a440e0aa2 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -13,6 +13,11 @@ "background": { "primary": { "value": "#fff" } } + }, + "size": { + "spacing": { + "small": { "value": "16px" } + } } } ``` @@ -22,12 +27,16 @@ .foo { color: design-token('color.background.primary'); + padding-left: design-token('size.spacing.small' to px); + padding-bottom: design-token('size.spacing.small' to rem); } /* becomes */ .foo { color: #fff; + padding-left: 16px; + padding-bottom: 1rem; } ``` @@ -144,6 +153,41 @@ postcssDesignTokens({ is: ['dark'] }) } ``` +### unitsAndValues + +The `unitsAndValues` option allows you to control some aspects of how design values are converted to CSS. +`rem` <-> `px` for example can only be calculated when we know the root font size. + +#### rootFontSize + +defaults to `16` + +```js +postcssDesignTokens({ + unitsAndValues: { + rootFontSize: 20, + }, +}) +``` + +```pcss +@design-tokens url('./tokens.json') format('style-dictionary3'); + +.foo { + color: design-token('color.background.primary'); + padding-left: design-token('size.spacing.small' to px); + padding-bottom: design-token('size.spacing.small' to rem); +} + +/* becomes */ + +.foo { + color: #fff; + padding-left: 16px; + padding-bottom: 0.8rem; +} +``` + [cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test [css-url]: https://cssdb.org/#TODO [discord]: https://discord.gg/bUadyRwkJS diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md index ac3c9bced..9a2a597e1 100644 --- a/plugins/postcss-design-tokens/docs/README.md +++ b/plugins/postcss-design-tokens/docs/README.md @@ -89,5 +89,30 @@ By default only `@design-tokens` without any `when('foo')` conditions are used. ``` +### unitsAndValues + +The `unitsAndValues` option allows you to control some aspects of how design values are converted to CSS. +`rem` <-> `px` for example can only be calculated when we know the root font size. + +#### rootFontSize + +defaults to `16` + +```js +({ + unitsAndValues: { + rootFontSize: 20, + }, +}) +``` + +```pcss + + +/* becomes */ + + +``` + [CSS Specification]: diff --git a/plugins/postcss-design-tokens/test/examples/example.css b/plugins/postcss-design-tokens/test/examples/example.css index c5d97f83a..54326026c 100644 --- a/plugins/postcss-design-tokens/test/examples/example.css +++ b/plugins/postcss-design-tokens/test/examples/example.css @@ -2,4 +2,6 @@ .foo { color: design-token('color.background.primary'); + padding-left: design-token('size.spacing.small' to px); + padding-bottom: design-token('size.spacing.small' to rem); } diff --git a/plugins/postcss-design-tokens/test/examples/example.expect.css b/plugins/postcss-design-tokens/test/examples/example.expect.css index 33a418a59..2581d8fa1 100644 --- a/plugins/postcss-design-tokens/test/examples/example.expect.css +++ b/plugins/postcss-design-tokens/test/examples/example.expect.css @@ -1,3 +1,5 @@ .foo { color: #fff; + padding-left: 16px; + padding-bottom: 1rem; } diff --git a/plugins/postcss-design-tokens/test/examples/example.rootFontSize-20.expect.css b/plugins/postcss-design-tokens/test/examples/example.rootFontSize-20.expect.css new file mode 100644 index 000000000..d6252800d --- /dev/null +++ b/plugins/postcss-design-tokens/test/examples/example.rootFontSize-20.expect.css @@ -0,0 +1,5 @@ +.foo { + color: #fff; + padding-left: 16px; + padding-bottom: 0.8rem; +} diff --git a/plugins/postcss-design-tokens/test/examples/tokens.json b/plugins/postcss-design-tokens/test/examples/tokens.json index 9776bbaed..1ff537046 100644 --- a/plugins/postcss-design-tokens/test/examples/tokens.json +++ b/plugins/postcss-design-tokens/test/examples/tokens.json @@ -3,5 +3,10 @@ "background": { "primary": { "value": "#fff" } } + }, + "size": { + "spacing": { + "small": { "value": "16px" } + } } } From 6e00a6bc9bbd16f6ca81f69c1f5ca91b4248c931 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sat, 21 May 2022 11:32:45 +0200 Subject: [PATCH 18/32] add syntax docs --- plugins/postcss-design-tokens/README.md | 44 ++++++++++++++++++++ plugins/postcss-design-tokens/docs/README.md | 44 ++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index a440e0aa2..9f14df1cc 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -188,6 +188,50 @@ postcssDesignTokens({ } ``` +## Syntax + +[PostCSS Design Tokens] is non-standard and is not part of any official CSS Specification. + +### `@design-tokens` rule + +The `@design-tokens` rule is used to import design tokens from a JSON file into your CSS. + +```pcss +@design-tokens url('./tokens.json') format('style-dictionary3'); +``` + +```pcss +@design-tokens url('./tokens.json') format('style-dictionary3'); +@design-tokens url('./tokens-dark-mode.json') format('style-dictionary3') when('dark'); +``` + +``` +@design-tokens [ | ] + [ when() ]? + format(); + + = + + = [ 'style-dictionary3' ] +``` + +### `design-token()` function + +The `design-token()` function takes a token path and returns the token value. + +```pcss +.foo { + color: design-token('color.background.primary'); +} +``` + +``` +design-token() = design-token( [ to ]? ) + + = + = [ px | rem ] +``` + [cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test [css-url]: https://cssdb.org/#TODO [discord]: https://discord.gg/bUadyRwkJS diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md index 9a2a597e1..f9a4c2b59 100644 --- a/plugins/postcss-design-tokens/docs/README.md +++ b/plugins/postcss-design-tokens/docs/README.md @@ -114,5 +114,49 @@ defaults to `16` ``` +## Syntax + +[] is non-standard and is not part of any official CSS Specification. + +### `@design-tokens` rule + +The `@design-tokens` rule is used to import design tokens from a JSON file into your CSS. + +```pcss +@design-tokens url('./tokens.json') format('style-dictionary3'); +``` + +```pcss +@design-tokens url('./tokens.json') format('style-dictionary3'); +@design-tokens url('./tokens-dark-mode.json') format('style-dictionary3') when('dark'); +``` + +``` +@design-tokens [ | ] + [ when() ]? + format(); + + = + + = [ 'style-dictionary3' ] +``` + +### `design-token()` function + +The `design-token()` function takes a token path and returns the token value. + +```pcss +.foo { + color: design-token('color.background.primary'); +} +``` + +``` +design-token() = design-token( [ to ]? ) + + = + = [ px | rem ] +``` + [CSS Specification]: From da16cd6ae94f3ea3065885969fe1eda9070cafb0 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sat, 21 May 2022 12:29:48 +0200 Subject: [PATCH 19/32] update docs --- .github/bin/generate-docs/readme.mjs | 12 ++++++++++-- plugins/postcss-base-plugin/README.md | 2 +- plugins/postcss-base-plugin/docs/README.md | 2 +- plugins/postcss-design-tokens/README.md | 7 +++---- plugins/postcss-design-tokens/docs/README.md | 3 +-- plugins/postcss-design-tokens/package.json | 4 +--- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/bin/generate-docs/readme.mjs b/.github/bin/generate-docs/readme.mjs index 957e7a6b0..69379812c 100644 --- a/.github/bin/generate-docs/readme.mjs +++ b/.github/bin/generate-docs/readme.mjs @@ -29,7 +29,11 @@ installDoc = installDoc.replace(` installDoc = installDoc.replace('
', `# [PostCSS Logo][postcss] [npm version][npm-url] -[CSS Standard Status][css-url] +${ + packageJSONInfo.csstools?.cssdbId ? + `[CSS Standard Status][css-url]` : + '' +} [Build Status][cli-url] [Discord][discord]`); @@ -62,7 +66,11 @@ instructions for: // Insert "Link List" section installDoc = installDoc.replace('', `[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test -[css-url]: https://cssdb.org/# +${ + packageJSONInfo.csstools?.cssdbId ? + `[css-url]: https://cssdb.org/#` : + '' +} [discord]: https://discord.gg/bUadyRwkJS [npm-url]: https://www.npmjs.com/package/ diff --git a/plugins/postcss-base-plugin/README.md b/plugins/postcss-base-plugin/README.md index 0d18299e7..69f952683 100644 --- a/plugins/postcss-base-plugin/README.md +++ b/plugins/postcss-base-plugin/README.md @@ -5,7 +5,7 @@ [Build Status][cli-url] [Discord][discord] -[PostCSS Base Plugin] lets easily create new plugins following some [CSS Specification]. +[PostCSS Base Plugin] lets you easily create new plugins following some [CSS Specification]. ```pcss .foo { diff --git a/plugins/postcss-base-plugin/docs/README.md b/plugins/postcss-base-plugin/docs/README.md index 3bc455892..8680a65c8 100644 --- a/plugins/postcss-base-plugin/docs/README.md +++ b/plugins/postcss-base-plugin/docs/README.md @@ -14,7 +14,7 @@
-[] lets easily create new plugins following some [CSS Specification]. +[] lets you easily create new plugins following some [CSS Specification]. ```pcss diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index 9f14df1cc..ec2d7854f 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -1,11 +1,11 @@ # PostCSS Design Tokens [PostCSS Logo][postcss] [npm version][npm-url] -[CSS Standard Status][css-url] + [Build Status][cli-url] [Discord][discord] -[PostCSS Design Tokens] lets easily create new plugins following some [CSS Specification]. +[PostCSS Design Tokens] lets you use design tokens in your CSS source files. ```json { @@ -233,7 +233,7 @@ design-token() = design-token( [ to ]? ) ``` [cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test -[css-url]: https://cssdb.org/#TODO + [discord]: https://discord.gg/bUadyRwkJS [npm-url]: https://www.npmjs.com/package/@csstools/postcss-design-tokens @@ -242,4 +242,3 @@ design-token() = design-token( [ to ]? ) [PostCSS]: https://github.com/postcss/postcss [PostCSS Loader]: https://github.com/postcss/postcss-loader [PostCSS Design Tokens]: https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-design-tokens -[CSS Specification]: #TODO diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md index f9a4c2b59..e077c9a38 100644 --- a/plugins/postcss-design-tokens/docs/README.md +++ b/plugins/postcss-design-tokens/docs/README.md @@ -14,7 +14,7 @@
-[] lets easily create new plugins following some [CSS Specification]. +[] lets you use design tokens in your CSS source files. ```json @@ -159,4 +159,3 @@ design-token() = design-token( [ to ]? ) ``` -[CSS Specification]: diff --git a/plugins/postcss-design-tokens/package.json b/plugins/postcss-design-tokens/package.json index a6b43019a..79512436e 100644 --- a/plugins/postcss-design-tokens/package.json +++ b/plugins/postcss-design-tokens/package.json @@ -56,10 +56,8 @@ "postcss-plugin" ], "csstools": { - "cssdbId": "TODO", "exportName": "postcssDesignTokens", - "humanReadableName": "PostCSS Design Tokens", - "specUrl": "#TODO" + "humanReadableName": "PostCSS Design Tokens" }, "volta": { "extends": "../../package.json" From 93af0a3061468a10438b132aa95289fd5660fd2e Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sat, 21 May 2022 12:34:24 +0200 Subject: [PATCH 20/32] better docs gen --- .github/bin/generate-docs/readme.mjs | 12 ++++++------ plugins/postcss-base-plugin/README.md | 5 +---- plugins/postcss-cascade-layers/README.md | 5 +---- plugins/postcss-color-function/README.md | 5 +---- plugins/postcss-design-tokens/README.md | 5 +---- .../postcss-gradients-interpolation-method/README.md | 5 +---- plugins/postcss-stepped-value-functions/README.md | 5 +---- plugins/postcss-trigonometric-functions/README.md | 5 +---- 8 files changed, 13 insertions(+), 34 deletions(-) diff --git a/.github/bin/generate-docs/readme.mjs b/.github/bin/generate-docs/readme.mjs index 69379812c..5c8cf232a 100644 --- a/.github/bin/generate-docs/readme.mjs +++ b/.github/bin/generate-docs/readme.mjs @@ -28,14 +28,14 @@ installDoc = installDoc.replace(` // Insert "Header" section installDoc = installDoc.replace('
', `# [PostCSS Logo][postcss] -[npm version][npm-url] -${ +` + `[npm version][npm-url] ` + +`${ packageJSONInfo.csstools?.cssdbId ? - `[CSS Standard Status][css-url]` : + `[CSS Standard Status][css-url] ` : '' -} -[Build Status][cli-url] -[Discord][discord]`); +}` + +`[Build Status][cli-url] ` + +`[Discord][discord]`); // Insert "Usage" section installDoc = installDoc.replace('', `## Usage diff --git a/plugins/postcss-base-plugin/README.md b/plugins/postcss-base-plugin/README.md index 69f952683..9fa7056e9 100644 --- a/plugins/postcss-base-plugin/README.md +++ b/plugins/postcss-base-plugin/README.md @@ -1,9 +1,6 @@ # PostCSS Base Plugin [PostCSS Logo][postcss] -[npm version][npm-url] -[CSS Standard Status][css-url] -[Build Status][cli-url] -[Discord][discord] +[npm version][npm-url] [CSS Standard Status][css-url] [Build Status][cli-url] [Discord][discord] [PostCSS Base Plugin] lets you easily create new plugins following some [CSS Specification]. diff --git a/plugins/postcss-cascade-layers/README.md b/plugins/postcss-cascade-layers/README.md index cfa0007c3..6f5b7954b 100644 --- a/plugins/postcss-cascade-layers/README.md +++ b/plugins/postcss-cascade-layers/README.md @@ -1,9 +1,6 @@ # PostCSS Cascade Layers [PostCSS Logo][postcss] -[npm version][npm-url] -[CSS Standard Status][css-url] -[Build Status][cli-url] -[Discord][discord] +[npm version][npm-url] [CSS Standard Status][css-url] [Build Status][cli-url] [Discord][discord] [PostCSS Cascade Layers] lets you use `@layer` following the [Cascade Layers Specification]. For more information on layers, checkout [A Complete Guide to CSS Cascade Layers] by Miriam Suzanne. diff --git a/plugins/postcss-color-function/README.md b/plugins/postcss-color-function/README.md index c19e804e1..62d6b294d 100644 --- a/plugins/postcss-color-function/README.md +++ b/plugins/postcss-color-function/README.md @@ -1,9 +1,6 @@ # PostCSS Color Function [PostCSS Logo][postcss] -[npm version][npm-url] -[CSS Standard Status][css-url] -[Build Status][cli-url] -[Discord][discord] +[npm version][npm-url] [CSS Standard Status][css-url] [Build Status][cli-url] [Discord][discord] [PostCSS Color Function] lets you use the `color` function in CSS, following the [CSS Color] specification. diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index ec2d7854f..7405a1740 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -1,9 +1,6 @@ # PostCSS Design Tokens [PostCSS Logo][postcss] -[npm version][npm-url] - -[Build Status][cli-url] -[Discord][discord] +[npm version][npm-url] [Build Status][cli-url] [Discord][discord] [PostCSS Design Tokens] lets you use design tokens in your CSS source files. diff --git a/plugins/postcss-gradients-interpolation-method/README.md b/plugins/postcss-gradients-interpolation-method/README.md index fbb284fd8..690e33012 100644 --- a/plugins/postcss-gradients-interpolation-method/README.md +++ b/plugins/postcss-gradients-interpolation-method/README.md @@ -1,9 +1,6 @@ # PostCSS Gradients Interpolation Method [PostCSS Logo][postcss] -[npm version][npm-url] -[CSS Standard Status][css-url] -[Build Status][cli-url] -[Discord][discord] +[npm version][npm-url] [CSS Standard Status][css-url] [Build Status][cli-url] [Discord][discord] [PostCSS Gradients Interpolation Method] lets you use different interpolation methods in CSS gradient functions following [CSS Specification]. diff --git a/plugins/postcss-stepped-value-functions/README.md b/plugins/postcss-stepped-value-functions/README.md index aaa9a4dbd..3cd235169 100644 --- a/plugins/postcss-stepped-value-functions/README.md +++ b/plugins/postcss-stepped-value-functions/README.md @@ -1,9 +1,6 @@ # PostCSS Stepped Value Functions [PostCSS Logo][postcss] -[npm version][npm-url] -[CSS Standard Status][css-url] -[Build Status][cli-url] -[Discord][discord] +[npm version][npm-url] [CSS Standard Status][css-url] [Build Status][cli-url] [Discord][discord] [PostCSS Stepped Value Functions] lets you use `round`, `rem` and `mod` stepped value functions, following the [CSS Values 4]. diff --git a/plugins/postcss-trigonometric-functions/README.md b/plugins/postcss-trigonometric-functions/README.md index 5b5973d2b..d52d95fc7 100644 --- a/plugins/postcss-trigonometric-functions/README.md +++ b/plugins/postcss-trigonometric-functions/README.md @@ -1,9 +1,6 @@ # PostCSS Trigonometric Functions [PostCSS Logo][postcss] -[npm version][npm-url] -[CSS Standard Status][css-url] -[Build Status][cli-url] -[Discord][discord] +[npm version][npm-url] [CSS Standard Status][css-url] [Build Status][cli-url] [Discord][discord] [PostCSS Trigonometric Functions] lets you use `sin`, `cos`, `tan`, `asin`, `acos`, `atan` and `atan2` to be able to compute trigonometric relationships following the [CSS Values 4] specification. From 4c16724033f08b661ded3d16909953c256fb351d Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sat, 21 May 2022 12:41:22 +0200 Subject: [PATCH 21/32] cleanup --- package-lock.json | 4 ++++ plugins/postcss-design-tokens/package.json | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 15ee3de93..72f75f66f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7003,6 +7003,10 @@ "engines": { "node": "^12 || ^14 || >=16" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, "peerDependencies": { "postcss": "^8.3" } diff --git a/plugins/postcss-design-tokens/package.json b/plugins/postcss-design-tokens/package.json index 79512436e..e0cc6333a 100644 --- a/plugins/postcss-design-tokens/package.json +++ b/plugins/postcss-design-tokens/package.json @@ -1,9 +1,23 @@ { "name": "@csstools/postcss-design-tokens", - "description": "TODO: Add description for Design Tokens", + "description": "Use design tokens in your CSS", "version": "1.0.0", - "author": "Jonathan Neal ", + "contributors": [ + { + "name": "Antonio Laguna", + "email": "antonio@laguna.es", + "url": "https://antonio.laguna.es" + }, + { + "name": "Romain Menke ", + "email": "romainmenke@gmail.com" + } + ], "license": "CC0-1.0", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, "engines": { "node": "^12 || ^14 || >=16" }, @@ -53,6 +67,7 @@ }, "bugs": "https://github.com/csstools/postcss-plugins/issues", "keywords": [ + "design-tokens", "postcss-plugin" ], "csstools": { From 962fa081bcc4ebf9abf67b6995fd3d0ea755b0e5 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sat, 21 May 2022 15:31:38 +0200 Subject: [PATCH 22/32] more docs --- plugins/postcss-design-tokens/README.md | 10 ++++++++++ plugins/postcss-design-tokens/docs/README.md | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index 7405a1740..929080290 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -212,6 +212,16 @@ The `@design-tokens` rule is used to import design tokens from a JSON file into = [ 'style-dictionary3' ] ``` +All `@design-tokens` rules in a document are evaluated in order of appearance. +If a token with the same path and name already exists it will be overridden. + +All `@design-tokens` rules are evaluated before any `design-token()` functions. + +`@design-tokens` rules can never be made conditional through `@supports`, `@media` or other conditional rules. + +Any form of nesting is meaningless, `@design-tokens` will always be evaluated as if they were declared at the top level. + + ### `design-token()` function The `design-token()` function takes a token path and returns the token value. diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md index e077c9a38..ccf14be80 100644 --- a/plugins/postcss-design-tokens/docs/README.md +++ b/plugins/postcss-design-tokens/docs/README.md @@ -141,6 +141,16 @@ The `@design-tokens` rule is used to import design tokens from a JSON file into = [ 'style-dictionary3' ] ``` +All `@design-tokens` rules in a document are evaluated in order of appearance. +If a token with the same path and name already exists it will be overridden. + +All `@design-tokens` rules are evaluated before any `design-token()` functions. + +`@design-tokens` rules can never be made conditional through `@supports`, `@media` or other conditional rules. + +Any form of nesting is meaningless, `@design-tokens` will always be evaluated as if they were declared at the top level. + + ### `design-token()` function The `design-token()` function takes a token path and returns the token value. From f89199e02b37d6457b3ed0340fd7fd55b59b9667 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sat, 21 May 2022 18:19:45 +0200 Subject: [PATCH 23/32] more docs --- plugins/postcss-design-tokens/README.md | 8 ++++++++ plugins/postcss-design-tokens/docs/README.md | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index 929080290..46f379aa8 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -62,6 +62,14 @@ instructions for: | [Node](INSTALL.md#node) | [PostCSS CLI](INSTALL.md#postcss-cli) | [Webpack](INSTALL.md#webpack) | [Create React App](INSTALL.md#create-react-app) | [Gulp](INSTALL.md#gulp) | [Grunt](INSTALL.md#grunt) | | --- | --- | --- | --- | --- | --- | +## Formats + +At this time there is no standardized format for design tokens. +Although there is an ongoing effort to create this, we feel it is still too early to adopt this. + +For the moment we only support [Style Dictionary](https://amzn.github.io/style-dictionary/#/). +Use `style-dictionary3` in `@design-tokens` rules to pick this format. + ## Options ### is diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md index ccf14be80..c28d66fed 100644 --- a/plugins/postcss-design-tokens/docs/README.md +++ b/plugins/postcss-design-tokens/docs/README.md @@ -32,6 +32,14 @@ +## Formats + +At this time there is no standardized format for design tokens. +Although there is an ongoing effort to create this, we feel it is still too early to adopt this. + +For the moment we only support [Style Dictionary](https://amzn.github.io/style-dictionary/#/). +Use `style-dictionary3` in `@design-tokens` rules to pick this format. + ## Options ### is From ead7d8c5988ec48d41a8cdc0336dd311fe1863fc Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sat, 21 May 2022 18:22:37 +0200 Subject: [PATCH 24/32] cleanup --- .../design-tokens-specification/type.ts | 14 --- .../design-tokens-specification/value.ts | 86 ------------------- 2 files changed, 100 deletions(-) delete mode 100644 plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/type.ts delete mode 100644 plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/value.ts diff --git a/plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/type.ts b/plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/type.ts deleted file mode 100644 index 7b9637ce0..000000000 --- a/plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/type.ts +++ /dev/null @@ -1,14 +0,0 @@ -export enum TokenType { - String = 'string', - Ident = 'ident', - Number = 'number', - Boolean = 'boolean', - Color = 'color', - Dimension = 'dimension', - FontFamily = 'fontFamily', - FontWeight = 'fontWeight', - FontStyle = 'fontStyle', - Duration = 'duration', - CubicBezier = 'cubicBezier', - URI = 'uri', -} diff --git a/plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/value.ts b/plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/value.ts deleted file mode 100644 index 76fa79902..000000000 --- a/plugins/postcss-design-tokens/src/data-formats/design-tokens-specification/value.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { TokenType } from './type'; - -export type TokenValue = TokenString | TokenIdent; - -export type TokenBaseValue = { - '$type': T; - '$name'?: string; - '$description'?: string; - '$deprecated'?: boolean; -} - -export interface TokenString extends TokenBaseValue { - '$value': string; -} - -export interface TokenIdent extends TokenBaseValue { - '$value': string; -} - -export interface TokenNumber extends TokenBaseValue { - '$value': number; -} - -export interface TokenBoolean extends TokenBaseValue { - '$value': boolean; -} - -export interface TokenColor extends TokenBaseValue { - '$value': { - colorSpace: 'srgb' | 'srgb-linear' | 'a98-rgb' | 'prophoto-rgb' | 'display-p3' | 'rec2020' | 'xyz-d50' | 'xyz-d65' | 'xyz' - channels: Array - alpha: number - }; -} - -export interface TokenDimension extends TokenBaseValue { - '$value': { - unit: string - value: number - }; -} - -export interface TokenFontFamily extends TokenBaseValue { - '$value': Array; -} - -export interface TokenFontWeight extends TokenBaseValue { - '$value': number | - 'thin' | - 'hairline' | - 'extra-light' | - 'ultra-light' | - 'light' | - 'normal' | - 'regular' | - 'book' | - 'medium' | - 'semi-bold' | - 'demi-bold' | - 'bold' | - 'extra-bold' | - 'ultra-bold' | - 'black' | - 'heavy' | - 'extra-black' | - 'ultra-black'; -} - -export interface TokenFontStyle extends TokenBaseValue { - '$value': [TokenIdent, TokenDimension?]; -} - -export interface TokenDuration extends TokenBaseValue { - '$value': { - unit: string - value: number - }; -} - -export interface TokenCubicBezier extends TokenBaseValue { - '$value': Array; -} - -export interface TokenURI extends TokenBaseValue { - '$value': string; -} From 368ce4ae2276cce3cc73e2b1dbb5bd1b53b3c62e Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 22 May 2022 09:58:15 +0200 Subject: [PATCH 25/32] improve the typing --- .../data-formats/style-dictionary/v3/dereference.ts | 11 ++++++----- .../src/data-formats/style-dictionary/v3/value.ts | 10 +++++----- .../src/data-formats/toposort/toposort.ts | 8 ++++---- plugins/postcss-design-tokens/src/index.ts | 2 +- plugins/postcss-design-tokens/src/values.ts | 6 ++++-- plugins/postcss-design-tokens/tsconfig.json | 1 + 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts index 897bed1bf..5ac37c07b 100644 --- a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/dereference.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { TokenTransformOptions } from '../../base/token'; import { toposort } from '../../toposort/toposort'; import { applyTransformsToValue, StyleDictionaryV3TokenValue } from './value'; @@ -37,7 +38,7 @@ export function dereferenceTokenValues(tokens: Map part.value).join(''); - const currentToken = tokens.get(id); + const currentToken = tokens.get(id)!; currentToken.value = value; currentToken.cssValue = (transformOptions: TokenTransformOptions) => { @@ -94,7 +95,7 @@ export function dereferenceTokenValues(tokens: Map< referenceAST.length; i++) { const reference = referenceAST[i]; if (reference.type !== 'value-reference') { @@ -109,7 +110,7 @@ export function dereferenceTokenValues(tokens: Map part.value).join(''); - const currentToken = tokens.get(id); + const currentToken = tokens.get(id)!; currentToken.value = value; currentToken.cssValue = (transformOptions: TokenTransformOptions) => { diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts index 9f3a5a1fe..fce191bac 100644 --- a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts @@ -27,22 +27,22 @@ export type StyleDictionaryV3TokenValue = { } } -export function extractStyleDictionaryV3Token(node: unknown, key: string, path: Array, filePath: string): StyleDictionaryV3TokenValue { +export function extractStyleDictionaryV3Token(node: Record, key: string, path: Array, filePath: string): StyleDictionaryV3TokenValue { if (typeof node['value'] === 'undefined') { throw new Error('Token value is undefined : ' + path.join('.')); } - const value = node['value'] ?? undefined; + const value = String(node['value']); return { value: value, cssValue: (transformOptions?: TokenTransformOptions) => { return applyTransformsToValue(value, transformOptions); }, - name: node['name'] ?? key, - comment: node['comment'] ?? undefined, + name: String(node['name'] ?? '') || key, + comment: String(node['comment'] ?? '') || undefined, metadata: { - name: node['name'] ? key : undefined, + name: String(node['name'] ?? '') ? key : undefined, path: [...path, key], filePath: filePath, isSource: true, diff --git a/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts b/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts index b3890c1c8..73b773d40 100644 --- a/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts +++ b/plugins/postcss-design-tokens/src/data-formats/toposort/toposort.ts @@ -22,7 +22,7 @@ export function toposort(nodes: Array, edges: Array>): Array { let cursor = nodes.length; const sorted: Array = new Array(cursor); - const visited = {}; + const visited: Record = {}; let i = cursor; // Better data structures make algorithm much faster. const outgoingEdges = makeOutgoingEdges(edges); @@ -43,7 +43,7 @@ export function toposort(nodes: Array, edges: Array>): Arr return sorted; - function visit(node, j, predecessors) { + function visit(node: string, j: number, predecessors: Set) { if (predecessors.has(node)) { let nodeRep; try { @@ -80,7 +80,7 @@ export function toposort(nodes: Array, edges: Array>): Arr } } -function makeOutgoingEdges(arr) { +function makeOutgoingEdges(arr: Array>) { const edges = new Map(); for (let i = 0, len = arr.length; i < len; i++) { const edge = arr[i]; @@ -95,7 +95,7 @@ function makeOutgoingEdges(arr) { return edges; } -function makeNodesHash(arr) { +function makeNodesHash(arr: Array) { const res = new Map(); for (let i = 0, len = arr.length; i < len; i++) { res.set(arr[i], i); diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts index 580defa0a..e834d22d3 100644 --- a/plugins/postcss-design-tokens/src/index.ts +++ b/plugins/postcss-design-tokens/src/index.ts @@ -49,7 +49,7 @@ const creator: PluginCreator = (opts?: pluginOptions) => { continue; } } catch (e) { - atRule.node.warn(result, `Failed to import design tokens from "${atRule.params}" with error:\n\t` + e.message); + atRule.node.warn(result, `Failed to import design tokens from "${atRule.params}" with error:\n\t` + (e as Error).message); continue; } diff --git a/plugins/postcss-design-tokens/src/values.ts b/plugins/postcss-design-tokens/src/values.ts index 22cc949dc..ea48aab53 100644 --- a/plugins/postcss-design-tokens/src/values.ts +++ b/plugins/postcss-design-tokens/src/values.ts @@ -31,7 +31,8 @@ export function onCSSValue(tokens: Map, result: Result, decl: Dec const remainingNodes = node.nodes.slice(1).filter(x => x.type === 'word'); if (!remainingNodes.length) { node.value = replacement.cssValue(); - node.nodes = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (node as any).nodes = undefined; return; } @@ -52,7 +53,8 @@ export function onCSSValue(tokens: Map, result: Result, decl: Dec } node.value = replacement.cssValue(transformOptions); - node.nodes = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (node as any).nodes = undefined; }); return String(valueAST); diff --git a/plugins/postcss-design-tokens/tsconfig.json b/plugins/postcss-design-tokens/tsconfig.json index e2c1924fe..64a39afa7 100644 --- a/plugins/postcss-design-tokens/tsconfig.json +++ b/plugins/postcss-design-tokens/tsconfig.json @@ -4,6 +4,7 @@ "outDir": "dist", "declarationDir": ".", "module": "es2020", + "strict": true, }, "include": ["./src/**/*"], "exclude": ["dist"], From d13fe3863ab67a51afc822c0cd5e7175dfe1dce1 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 22 May 2022 14:07:09 +0200 Subject: [PATCH 26/32] more examples --- plugins/postcss-design-tokens/README.md | 4 ++++ plugins/postcss-design-tokens/test/examples/example.css | 1 + .../postcss-design-tokens/test/examples/example.expect.css | 1 + .../test/examples/example.rootFontSize-20.expect.css | 1 + 4 files changed, 7 insertions(+) diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index 46f379aa8..a66327c3d 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -24,6 +24,7 @@ .foo { color: design-token('color.background.primary'); + padding-top: design-token('size.spacing.small'); padding-left: design-token('size.spacing.small' to px); padding-bottom: design-token('size.spacing.small' to rem); } @@ -32,6 +33,7 @@ .foo { color: #fff; + padding-top: 16px; padding-left: 16px; padding-bottom: 1rem; } @@ -180,6 +182,7 @@ postcssDesignTokens({ .foo { color: design-token('color.background.primary'); + padding-top: design-token('size.spacing.small'); padding-left: design-token('size.spacing.small' to px); padding-bottom: design-token('size.spacing.small' to rem); } @@ -188,6 +191,7 @@ postcssDesignTokens({ .foo { color: #fff; + padding-top: 16px; padding-left: 16px; padding-bottom: 0.8rem; } diff --git a/plugins/postcss-design-tokens/test/examples/example.css b/plugins/postcss-design-tokens/test/examples/example.css index 54326026c..e4e3e2c99 100644 --- a/plugins/postcss-design-tokens/test/examples/example.css +++ b/plugins/postcss-design-tokens/test/examples/example.css @@ -2,6 +2,7 @@ .foo { color: design-token('color.background.primary'); + padding-top: design-token('size.spacing.small'); padding-left: design-token('size.spacing.small' to px); padding-bottom: design-token('size.spacing.small' to rem); } diff --git a/plugins/postcss-design-tokens/test/examples/example.expect.css b/plugins/postcss-design-tokens/test/examples/example.expect.css index 2581d8fa1..5c5964adb 100644 --- a/plugins/postcss-design-tokens/test/examples/example.expect.css +++ b/plugins/postcss-design-tokens/test/examples/example.expect.css @@ -1,5 +1,6 @@ .foo { color: #fff; + padding-top: 16px; padding-left: 16px; padding-bottom: 1rem; } diff --git a/plugins/postcss-design-tokens/test/examples/example.rootFontSize-20.expect.css b/plugins/postcss-design-tokens/test/examples/example.rootFontSize-20.expect.css index d6252800d..6b28e7c0e 100644 --- a/plugins/postcss-design-tokens/test/examples/example.rootFontSize-20.expect.css +++ b/plugins/postcss-design-tokens/test/examples/example.rootFontSize-20.expect.css @@ -1,5 +1,6 @@ .foo { color: #fff; + padding-top: 16px; padding-left: 16px; padding-bottom: 0.8rem; } From 5581e0b3657d32e87354238aba10081c62b524a6 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 22 May 2022 14:58:01 +0200 Subject: [PATCH 27/32] stricter value and plugin option handling --- plugins/postcss-design-tokens/.tape.mjs | 49 ++++++++++++++++++- .../postcss-design-tokens/src/constants.ts | 2 + .../src/data-formats/base/token.ts | 4 +- .../src/data-formats/parse-import.ts | 5 +- .../data-formats/style-dictionary/v3/value.ts | 11 ++++- plugins/postcss-design-tokens/src/index.ts | 11 ++--- plugins/postcss-design-tokens/src/options.ts | 44 +++++++++++++++++ plugins/postcss-design-tokens/src/values.ts | 6 +-- .../test/basic.rootFontSize-NaN.expect.css | 39 +++++++++++++++ .../basic.rootFontSize-invalid.expect.css | 39 +++++++++++++++ .../test/tokens/value-parsing-a.json | 5 ++ .../test/tokens/value-parsing-b.json | 5 ++ .../test/tokens/value-parsing-c.json | 7 +++ .../test/tokens/value-parsing-d.json | 5 ++ .../test/tokens/value-parsing-e.json | 5 ++ .../test/tokens/value-parsing-f.json | 5 ++ .../test/tokens/value-parsing-g.json | 5 ++ .../test/value-parsing-a.css | 5 ++ .../test/value-parsing-a.expect.css | 3 ++ .../test/value-parsing-b.css | 5 ++ .../test/value-parsing-b.expect.css | 3 ++ .../test/value-parsing-c.css | 5 ++ .../test/value-parsing-c.expect.css | 3 ++ .../test/value-parsing-d.css | 5 ++ .../test/value-parsing-d.expect.css | 3 ++ .../test/value-parsing-e.css | 5 ++ .../test/value-parsing-e.expect.css | 3 ++ .../test/value-parsing-f.css | 5 ++ .../test/value-parsing-f.expect.css | 3 ++ .../test/value-parsing-g.css | 5 ++ .../test/value-parsing-g.expect.css | 3 ++ 31 files changed, 287 insertions(+), 16 deletions(-) create mode 100644 plugins/postcss-design-tokens/src/constants.ts create mode 100644 plugins/postcss-design-tokens/test/basic.rootFontSize-NaN.expect.css create mode 100644 plugins/postcss-design-tokens/test/basic.rootFontSize-invalid.expect.css create mode 100644 plugins/postcss-design-tokens/test/tokens/value-parsing-a.json create mode 100644 plugins/postcss-design-tokens/test/tokens/value-parsing-b.json create mode 100644 plugins/postcss-design-tokens/test/tokens/value-parsing-c.json create mode 100644 plugins/postcss-design-tokens/test/tokens/value-parsing-d.json create mode 100644 plugins/postcss-design-tokens/test/tokens/value-parsing-e.json create mode 100644 plugins/postcss-design-tokens/test/tokens/value-parsing-f.json create mode 100644 plugins/postcss-design-tokens/test/tokens/value-parsing-g.json create mode 100644 plugins/postcss-design-tokens/test/value-parsing-a.css create mode 100644 plugins/postcss-design-tokens/test/value-parsing-a.expect.css create mode 100644 plugins/postcss-design-tokens/test/value-parsing-b.css create mode 100644 plugins/postcss-design-tokens/test/value-parsing-b.expect.css create mode 100644 plugins/postcss-design-tokens/test/value-parsing-c.css create mode 100644 plugins/postcss-design-tokens/test/value-parsing-c.expect.css create mode 100644 plugins/postcss-design-tokens/test/value-parsing-d.css create mode 100644 plugins/postcss-design-tokens/test/value-parsing-d.expect.css create mode 100644 plugins/postcss-design-tokens/test/value-parsing-e.css create mode 100644 plugins/postcss-design-tokens/test/value-parsing-e.expect.css create mode 100644 plugins/postcss-design-tokens/test/value-parsing-f.css create mode 100644 plugins/postcss-design-tokens/test/value-parsing-f.expect.css create mode 100644 plugins/postcss-design-tokens/test/value-parsing-g.css create mode 100644 plugins/postcss-design-tokens/test/value-parsing-g.expect.css diff --git a/plugins/postcss-design-tokens/.tape.mjs b/plugins/postcss-design-tokens/.tape.mjs index a54df4608..075221bb4 100644 --- a/plugins/postcss-design-tokens/.tape.mjs +++ b/plugins/postcss-design-tokens/.tape.mjs @@ -21,17 +21,64 @@ postcssTape(plugin)({ }) ] }, + 'basic:rootFontSize-NaN': { + message: "supports basic usage with { unitsAndValues { rootFontSize: NaN } }", + plugins: [ + postcssImport(), + plugin({ + unitsAndValues: { + rootFontSize: NaN + } + }) + ] + }, + 'basic:rootFontSize-invalid': { + message: "supports basic usage with { unitsAndValues { rootFontSize: invalid } }", + plugins: [ + postcssImport(), + plugin({ + unitsAndValues: { + rootFontSize: 'invalid' + } + }) + ] + }, 'errors': { message: "handles issues correctly", options: {}, warnings: 4 }, 'is': { - message: "supports basic usage", + message: "supports basic usage with { is ['dark', 'tablet', 'branded-green'] }", options: { is: ['dark', 'tablet', 'branded-green'] } }, + 'value-parsing-a': { + message: "supports value parsing (A)", + }, + 'value-parsing-b': { + message: "supports value parsing (B)", + }, + 'value-parsing-c': { + message: "supports value parsing (C)", + warnings: 2 + }, + 'value-parsing-d': { + message: "supports value parsing (D)", + warnings: 2 + }, + 'value-parsing-e': { + message: "supports value parsing (E)", + warnings: 2 + }, + 'value-parsing-f': { + message: "supports value parsing (F)", + }, + 'value-parsing-g': { + message: "supports value parsing (G)", + warnings: 2 + }, 'examples/example': { message: 'minimal example', options: {}, diff --git a/plugins/postcss-design-tokens/src/constants.ts b/plugins/postcss-design-tokens/src/constants.ts new file mode 100644 index 000000000..c77d745b5 --- /dev/null +++ b/plugins/postcss-design-tokens/src/constants.ts @@ -0,0 +1,2 @@ +// a random, but shared default condition +export const DEFAULT_CONDITION = '6b4e71e7-4787-42f7-a092-8684961895db'; diff --git a/plugins/postcss-design-tokens/src/data-formats/base/token.ts b/plugins/postcss-design-tokens/src/data-formats/base/token.ts index 5b1a12f1e..5838f8695 100644 --- a/plugins/postcss-design-tokens/src/data-formats/base/token.ts +++ b/plugins/postcss-design-tokens/src/data-formats/base/token.ts @@ -1,6 +1,6 @@ export interface TokenTransformOptions { - pluginOptions?: { - rootFontSize?: number; + pluginOptions: { + rootFontSize: number; }; toUnit?: string; } diff --git a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts index d978506f2..f3089c72c 100644 --- a/plugins/postcss-design-tokens/src/data-formats/parse-import.ts +++ b/plugins/postcss-design-tokens/src/data-formats/parse-import.ts @@ -3,6 +3,7 @@ import { Token } from './base/token'; import { extractStyleDictionaryTokens } from './style-dictionary/style-dictionary'; import path from 'path'; import { promises as fsp } from 'fs'; +import { DEFAULT_CONDITION } from '../constants'; function parseImport(statement: string): { filePath: string, format: string, conditions: Array } { const importAST = valueParser(statement); @@ -10,7 +11,7 @@ function parseImport(statement: string): { filePath: string, format: string, con const result = { filePath: '', format: 'standard', - conditions: ['6b4e71e7-4787-42f7-a092-8684961895db'], // a random, but shared default condition + conditions: [DEFAULT_CONDITION], }; importAST.walk((node) => { @@ -30,7 +31,7 @@ function parseImport(statement: string): { filePath: string, format: string, con }); if (!result.conditions.length) { - result.conditions = ['6b4e71e7-4787-42f7-a092-8684961895db']; // a random, but shared default condition + result.conditions = [DEFAULT_CONDITION]; } return result; diff --git a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts index fce191bac..6a854e099 100644 --- a/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts +++ b/plugins/postcss-design-tokens/src/data-formats/style-dictionary/v3/value.ts @@ -29,7 +29,16 @@ export type StyleDictionaryV3TokenValue = { export function extractStyleDictionaryV3Token(node: Record, key: string, path: Array, filePath: string): StyleDictionaryV3TokenValue { if (typeof node['value'] === 'undefined') { - throw new Error('Token value is undefined : ' + path.join('.')); + throw new Error('Token value is undefined for "' + [...path, key].join('.') + '"'); + } + + switch (typeof node['value']) { + case 'string': + case 'number': + break; + + default: + throw new Error('Token value is not a string or a number for "' + [...path, key].join('.') + '"'); } const value = String(node['value']); diff --git a/plugins/postcss-design-tokens/src/index.ts b/plugins/postcss-design-tokens/src/index.ts index e834d22d3..0cd1ecada 100644 --- a/plugins/postcss-design-tokens/src/index.ts +++ b/plugins/postcss-design-tokens/src/index.ts @@ -2,15 +2,12 @@ import type { Node, PluginCreator } from 'postcss'; import { Token } from './data-formats/base/token'; import { tokensFromImport } from './data-formats/parse-import'; import { mergeTokens } from './data-formats/token'; -import { pluginOptions } from './options'; +import { parsePluginOptions, pluginOptions } from './options'; import { onCSSValue } from './values'; const creator: PluginCreator = (opts?: pluginOptions) => { - const buildIs = opts?.is ?? []; - if (buildIs.length === 0) { - buildIs.push('6b4e71e7-4787-42f7-a092-8684961895db'); // a random, but shared default condition - } + const options = parsePluginOptions(opts); return { postcssPlugin: 'postcss-design-tokens', @@ -44,7 +41,7 @@ const creator: PluginCreator = (opts?: pluginOptions) => { for (const atRule of designTokenAtRules.values()) { let importResult: { filePath: string, tokens: Map }|false; try { - importResult = await tokensFromImport(buildIs, atRule.filePath, atRule.params, importedFiles); + importResult = await tokensFromImport(options.is, atRule.filePath, atRule.params, importedFiles); if (!importResult) { continue; } @@ -68,7 +65,7 @@ const creator: PluginCreator = (opts?: pluginOptions) => { return; } - const modifiedValue = onCSSValue(tokens, result, decl, opts); + const modifiedValue = onCSSValue(tokens, result, decl, options); if (modifiedValue === decl.value) { return; } diff --git a/plugins/postcss-design-tokens/src/options.ts b/plugins/postcss-design-tokens/src/options.ts index 1d4dcb734..adebc06b5 100644 --- a/plugins/postcss-design-tokens/src/options.ts +++ b/plugins/postcss-design-tokens/src/options.ts @@ -1,6 +1,50 @@ +import { DEFAULT_CONDITION } from './constants'; + export type pluginOptions = { is?: Array unitsAndValues?: { rootFontSize?: number } } + +export type parsedPluginOptions = { + is: Array + unitsAndValues: { + rootFontSize: number + } +} + +export function parsePluginOptions(opts?: pluginOptions): parsedPluginOptions { + const options: parsedPluginOptions = { + is: [DEFAULT_CONDITION], + unitsAndValues: { + rootFontSize: 16, + }, + }; + + if (!opts) { + return options; + } + + if (typeof opts !== 'object') { + return options; + } + + if (Array.isArray(opts.is)) { + options.is = opts.is.filter((x) => typeof x === 'string'); + } + + if (options.is.length === 0) { + options.is = [DEFAULT_CONDITION]; + } + + if (typeof opts.unitsAndValues === 'object' && typeof opts.unitsAndValues.rootFontSize === 'number' && isPositiveAndNotZero(opts.unitsAndValues.rootFontSize)) { + options.unitsAndValues.rootFontSize = opts.unitsAndValues.rootFontSize; + } + + return options; +} + +function isPositiveAndNotZero(x: number) { + return x > 0 && x !== Infinity; +} diff --git a/plugins/postcss-design-tokens/src/values.ts b/plugins/postcss-design-tokens/src/values.ts index ea48aab53..f3447bcf6 100644 --- a/plugins/postcss-design-tokens/src/values.ts +++ b/plugins/postcss-design-tokens/src/values.ts @@ -1,9 +1,9 @@ import type { Declaration, Result } from 'postcss'; import valueParser from 'postcss-value-parser'; import { Token, TokenTransformOptions } from './data-formats/base/token'; -import { pluginOptions } from './options'; +import { parsedPluginOptions } from './options'; -export function onCSSValue(tokens: Map, result: Result, decl: Declaration, opts?: pluginOptions) { +export function onCSSValue(tokens: Map, result: Result, decl: Declaration, opts: parsedPluginOptions) { const valueAST = valueParser(decl.value); valueAST.walk(node => { @@ -37,7 +37,7 @@ export function onCSSValue(tokens: Map, result: Result, decl: Dec } const transformOptions: TokenTransformOptions = { - pluginOptions: opts?.unitsAndValues, + pluginOptions: opts.unitsAndValues, }; for (let i = 0; i < remainingNodes.length; i++) { if ( diff --git a/plugins/postcss-design-tokens/test/basic.rootFontSize-NaN.expect.css b/plugins/postcss-design-tokens/test/basic.rootFontSize-NaN.expect.css new file mode 100644 index 000000000..d4b93d305 --- /dev/null +++ b/plugins/postcss-design-tokens/test/basic.rootFontSize-NaN.expect.css @@ -0,0 +1,39 @@ +.foo { + font-family: Helvetica sans; + font-size: 10; + color: #111111; +} +.card { + background-color: blue; + color: red; + color: red; + color: red; + color: red; + color: red; +} +.px-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.px-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.1rem; + padding-bottom: 2rem; +} +.rem-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.125rem; + padding-bottom: 2rem; +} +.rem-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.invalid-conversion { + color: red; + color: red; + color: 1lh; + color: 1lh; +} diff --git a/plugins/postcss-design-tokens/test/basic.rootFontSize-invalid.expect.css b/plugins/postcss-design-tokens/test/basic.rootFontSize-invalid.expect.css new file mode 100644 index 000000000..d4b93d305 --- /dev/null +++ b/plugins/postcss-design-tokens/test/basic.rootFontSize-invalid.expect.css @@ -0,0 +1,39 @@ +.foo { + font-family: Helvetica sans; + font-size: 10; + color: #111111; +} +.card { + background-color: blue; + color: red; + color: red; + color: red; + color: red; + color: red; +} +.px-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.px-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.1rem; + padding-bottom: 2rem; +} +.rem-to-rem { + padding-bottom: 0.5rem; + padding-bottom: 1.125rem; + padding-bottom: 2rem; +} +.rem-to-px { + padding-bottom: 8px; + padding-bottom: 18px; + padding-bottom: 32px; +} +.invalid-conversion { + color: red; + color: red; + color: 1lh; + color: 1lh; +} diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-a.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-a.json new file mode 100644 index 000000000..f3970cd27 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-a.json @@ -0,0 +1,5 @@ +{ + "token": { + "value": 20 + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-b.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-b.json new file mode 100644 index 000000000..22b517934 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-b.json @@ -0,0 +1,5 @@ +{ + "token": { + "value": "foo" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-c.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-c.json new file mode 100644 index 000000000..474f5a633 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-c.json @@ -0,0 +1,7 @@ +{ + "token": { + "value": { + "an-object": true + } + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-d.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-d.json new file mode 100644 index 000000000..728e997aa --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-d.json @@ -0,0 +1,5 @@ +{ + "token": { + "value": false + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-e.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-e.json new file mode 100644 index 000000000..adac6202e --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-e.json @@ -0,0 +1,5 @@ +{ + "token": { + "value": null + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-f.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-f.json new file mode 100644 index 000000000..b16797c95 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-f.json @@ -0,0 +1,5 @@ +{ + "token": { + "value": "\"foo\"" + } +} diff --git a/plugins/postcss-design-tokens/test/tokens/value-parsing-g.json b/plugins/postcss-design-tokens/test/tokens/value-parsing-g.json new file mode 100644 index 000000000..f94530ca5 --- /dev/null +++ b/plugins/postcss-design-tokens/test/tokens/value-parsing-g.json @@ -0,0 +1,5 @@ +{ + "token": { + "value": [20] + } +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-a.css b/plugins/postcss-design-tokens/test/value-parsing-a.css new file mode 100644 index 000000000..7ee8b4fb8 --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-a.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens/value-parsing-a.json') format('style-dictionary3'); + +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-a.expect.css b/plugins/postcss-design-tokens/test/value-parsing-a.expect.css new file mode 100644 index 000000000..ed650bb67 --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-a.expect.css @@ -0,0 +1,3 @@ +.value-parsing { + order: 20; +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-b.css b/plugins/postcss-design-tokens/test/value-parsing-b.css new file mode 100644 index 000000000..d29163c4a --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-b.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens/value-parsing-b.json') format('style-dictionary3'); + +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-b.expect.css b/plugins/postcss-design-tokens/test/value-parsing-b.expect.css new file mode 100644 index 000000000..e98253051 --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-b.expect.css @@ -0,0 +1,3 @@ +.value-parsing { + order: foo; +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-c.css b/plugins/postcss-design-tokens/test/value-parsing-c.css new file mode 100644 index 000000000..57ad788c7 --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-c.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens/value-parsing-c.json') format('style-dictionary3'); + +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-c.expect.css b/plugins/postcss-design-tokens/test/value-parsing-c.expect.css new file mode 100644 index 000000000..4ffb47baf --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-c.expect.css @@ -0,0 +1,3 @@ +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-d.css b/plugins/postcss-design-tokens/test/value-parsing-d.css new file mode 100644 index 000000000..5bcc9dcba --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-d.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens/value-parsing-d.json') format('style-dictionary3'); + +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-d.expect.css b/plugins/postcss-design-tokens/test/value-parsing-d.expect.css new file mode 100644 index 000000000..4ffb47baf --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-d.expect.css @@ -0,0 +1,3 @@ +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-e.css b/plugins/postcss-design-tokens/test/value-parsing-e.css new file mode 100644 index 000000000..cabafcf23 --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-e.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens/value-parsing-e.json') format('style-dictionary3'); + +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-e.expect.css b/plugins/postcss-design-tokens/test/value-parsing-e.expect.css new file mode 100644 index 000000000..4ffb47baf --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-e.expect.css @@ -0,0 +1,3 @@ +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-f.css b/plugins/postcss-design-tokens/test/value-parsing-f.css new file mode 100644 index 000000000..34a46664b --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-f.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens/value-parsing-f.json') format('style-dictionary3'); + +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-f.expect.css b/plugins/postcss-design-tokens/test/value-parsing-f.expect.css new file mode 100644 index 000000000..4c99d32ef --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-f.expect.css @@ -0,0 +1,3 @@ +.value-parsing { + order: "foo"; +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-g.css b/plugins/postcss-design-tokens/test/value-parsing-g.css new file mode 100644 index 000000000..801cfb750 --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-g.css @@ -0,0 +1,5 @@ +@design-tokens url('./tokens/value-parsing-g.json') format('style-dictionary3'); + +.value-parsing { + order: design-token('token'); +} diff --git a/plugins/postcss-design-tokens/test/value-parsing-g.expect.css b/plugins/postcss-design-tokens/test/value-parsing-g.expect.css new file mode 100644 index 000000000..4ffb47baf --- /dev/null +++ b/plugins/postcss-design-tokens/test/value-parsing-g.expect.css @@ -0,0 +1,3 @@ +.value-parsing { + order: design-token('token'); +} From 786cd9b10791fd83aaacdd5bfc1f148155ad8ed7 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 22 May 2022 15:17:54 +0200 Subject: [PATCH 28/32] more docs --- plugins/postcss-design-tokens/README.md | 15 ++++++++++++++- plugins/postcss-design-tokens/docs/README.md | 15 ++++++++++++++- plugins/postcss-design-tokens/test/is.css | 8 ++++---- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index a66327c3d..095b45df0 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -216,7 +216,7 @@ The `@design-tokens` rule is used to import design tokens from a JSON file into ``` @design-tokens [ | ] - [ when() ]? + [ when(*) ]? format(); = @@ -229,8 +229,21 @@ If a token with the same path and name already exists it will be overridden. All `@design-tokens` rules are evaluated before any `design-token()` functions. +`@design-tokens` rules can be conditional through `when` conditions. Multiple values can be specified in `when`. + +> ```css +> /* always evaluated when tooling receives 'blue' and 'muted' as arguments */ +> @design-tokens url('./tokens.json') format('style-dictionary3') when('blue' 'muted'); +> ``` + `@design-tokens` rules can never be made conditional through `@supports`, `@media` or other conditional rules. +> ```css +> @media (min-width: 500px) { +> @design-tokens url('./tokens.json') format('style-dictionary3'); /* always evaluated */ +> } +> ``` + Any form of nesting is meaningless, `@design-tokens` will always be evaluated as if they were declared at the top level. diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md index c28d66fed..cc84941cc 100644 --- a/plugins/postcss-design-tokens/docs/README.md +++ b/plugins/postcss-design-tokens/docs/README.md @@ -141,7 +141,7 @@ The `@design-tokens` rule is used to import design tokens from a JSON file into ``` @design-tokens [ | ] - [ when() ]? + [ when(*) ]? format(); = @@ -154,8 +154,21 @@ If a token with the same path and name already exists it will be overridden. All `@design-tokens` rules are evaluated before any `design-token()` functions. +`@design-tokens` rules can be conditional through `when` conditions. Multiple values can be specified in `when`. + +> ```css +> /* always evaluated when tooling receives 'blue' and 'muted' as arguments */ +> @design-tokens url('./tokens.json') format('style-dictionary3') when('blue' 'muted'); +> ``` + `@design-tokens` rules can never be made conditional through `@supports`, `@media` or other conditional rules. +> ```css +> @media (min-width: 500px) { +> @design-tokens url('./tokens.json') format('style-dictionary3'); /* always evaluated */ +> } +> ``` + Any form of nesting is meaningless, `@design-tokens` will always be evaluated as if they were declared at the top level. diff --git a/plugins/postcss-design-tokens/test/is.css b/plugins/postcss-design-tokens/test/is.css index 57ec7371b..78b783e71 100644 --- a/plugins/postcss-design-tokens/test/is.css +++ b/plugins/postcss-design-tokens/test/is.css @@ -1,7 +1,7 @@ -@design-tokens url('./tokens/color_dark_branded-blue.tokens.json') when('dark', 'branded-blue') format('style-dictionary3'); -@design-tokens url('./tokens/color_light_branded-blue.tokens.json') when('light', 'branded-blue') format('style-dictionary3'); -@design-tokens url('./tokens/color_dark_branded-green.tokens.json') when('dark', 'branded-green') format('style-dictionary3'); -@design-tokens url('./tokens/color_light_branded-green.tokens.json') when('light', 'branded-green') format('style-dictionary3'); +@design-tokens url('./tokens/color_dark_branded-blue.tokens.json') when('dark' 'branded-blue') format('style-dictionary3'); +@design-tokens url('./tokens/color_light_branded-blue.tokens.json') when('light' 'branded-blue') format('style-dictionary3'); +@design-tokens url('./tokens/color_dark_branded-green.tokens.json') when('dark' 'branded-green') format('style-dictionary3'); +@design-tokens url('./tokens/color_light_branded-green.tokens.json') when('light' 'branded-green') format('style-dictionary3'); @design-tokens url('./tokens/size_mobile.tokens.json') when('mobile') format('style-dictionary3'); @design-tokens url('./tokens/size_tablet.tokens.json') when('tablet') format('style-dictionary3'); @design-tokens url('./tokens/size_desktop.tokens.json') when('desktop') format('style-dictionary3'); From ee6e138904d13b9d067c7f8136b19f042a668c80 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 22 May 2022 15:19:10 +0200 Subject: [PATCH 29/32] fix --- plugins/postcss-design-tokens/README.md | 2 +- plugins/postcss-design-tokens/docs/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index 095b45df0..8d1d055f2 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -232,7 +232,7 @@ All `@design-tokens` rules are evaluated before any `design-token()` functions. `@design-tokens` rules can be conditional through `when` conditions. Multiple values can be specified in `when`. > ```css -> /* always evaluated when tooling receives 'blue' and 'muted' as arguments */ +> /* only evaluated when tooling receives 'blue' and 'muted' as arguments */ > @design-tokens url('./tokens.json') format('style-dictionary3') when('blue' 'muted'); > ``` diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md index cc84941cc..2d054a009 100644 --- a/plugins/postcss-design-tokens/docs/README.md +++ b/plugins/postcss-design-tokens/docs/README.md @@ -157,7 +157,7 @@ All `@design-tokens` rules are evaluated before any `design-token()` functions. `@design-tokens` rules can be conditional through `when` conditions. Multiple values can be specified in `when`. > ```css -> /* always evaluated when tooling receives 'blue' and 'muted' as arguments */ +> /* only evaluated when tooling receives 'blue' and 'muted' as arguments */ > @design-tokens url('./tokens.json') format('style-dictionary3') when('blue' 'muted'); > ``` From bd714e5f12d150332403f86e7e951909c91f87a5 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 22 May 2022 18:55:46 +0200 Subject: [PATCH 30/32] document VSCode extension --- plugins/postcss-design-tokens/README.md | 10 ++++++++++ plugins/postcss-design-tokens/docs/README.md | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index 8d1d055f2..1c967729f 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -201,6 +201,16 @@ postcssDesignTokens({ [PostCSS Design Tokens] is non-standard and is not part of any official CSS Specification. +### Editor support + +This is all very new and we hope that one day design tokens will become first class citizens in editors and other tools. +Until then we will do our best to provide extensions. +These will have rough edges but should illustrate were we want to go. + +| editor | plugin | +| --- | --- | +| VSCode | [CSSTools Design Tokens](https://marketplace.visualstudio.com/items?itemName=RomainMenke.csstools-design-tokens) | + ### `@design-tokens` rule The `@design-tokens` rule is used to import design tokens from a JSON file into your CSS. diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md index 2d054a009..1a6103d41 100644 --- a/plugins/postcss-design-tokens/docs/README.md +++ b/plugins/postcss-design-tokens/docs/README.md @@ -126,6 +126,16 @@ defaults to `16` [] is non-standard and is not part of any official CSS Specification. +### Editor support + +This is all very new and we hope that one day design tokens will become first class citizens in editors and other tools. +Until then we will do our best to provide extensions. +These will have rough edges but should illustrate were we want to go. + +| editor | plugin | +| --- | --- | +| VSCode | [CSSTools Design Tokens](https://marketplace.visualstudio.com/items?itemName=RomainMenke.csstools-design-tokens) | + ### `@design-tokens` rule The `@design-tokens` rule is used to import design tokens from a JSON file into your CSS. From bfa3d9963a3691d5ee3cccad485d95d3a2310eec Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 22 May 2022 19:03:09 +0200 Subject: [PATCH 31/32] clarify multiple when --- plugins/postcss-design-tokens/README.md | 3 ++- plugins/postcss-design-tokens/docs/README.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/postcss-design-tokens/README.md b/plugins/postcss-design-tokens/README.md index 1c967729f..87614e8e1 100644 --- a/plugins/postcss-design-tokens/README.md +++ b/plugins/postcss-design-tokens/README.md @@ -239,7 +239,8 @@ If a token with the same path and name already exists it will be overridden. All `@design-tokens` rules are evaluated before any `design-token()` functions. -`@design-tokens` rules can be conditional through `when` conditions. Multiple values can be specified in `when`. +`@design-tokens` rules can be conditional through `when` conditions. Multiple values can be specified in `when`.
+Multiple conditions always have an `AND` relationship. > ```css > /* only evaluated when tooling receives 'blue' and 'muted' as arguments */ diff --git a/plugins/postcss-design-tokens/docs/README.md b/plugins/postcss-design-tokens/docs/README.md index 1a6103d41..e105b158c 100644 --- a/plugins/postcss-design-tokens/docs/README.md +++ b/plugins/postcss-design-tokens/docs/README.md @@ -164,7 +164,8 @@ If a token with the same path and name already exists it will be overridden. All `@design-tokens` rules are evaluated before any `design-token()` functions. -`@design-tokens` rules can be conditional through `when` conditions. Multiple values can be specified in `when`. +`@design-tokens` rules can be conditional through `when` conditions. Multiple values can be specified in `when`.
+Multiple conditions always have an `AND` relationship. > ```css > /* only evaluated when tooling receives 'blue' and 'muted' as arguments */ From 215b945adbdc52603a8cb100f20f7fac05685804 Mon Sep 17 00:00:00 2001 From: romainmenke Date: Sun, 22 May 2022 19:04:23 +0200 Subject: [PATCH 32/32] cleanup --- rollup/presets/package-typescript.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollup/presets/package-typescript.js b/rollup/presets/package-typescript.js index 6e739ef65..d765bf703 100644 --- a/rollup/presets/package-typescript.js +++ b/rollup/presets/package-typescript.js @@ -21,7 +21,7 @@ export function packageTypescript() { extensions: ['.js', '.ts'], presets: packageBabelPreset, }), - // terser(), + terser(), ], }, ];