├── .babelrc ├── .eslintrc ├── .gitignore ├── .npmrc ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── UNLICENSE ├── codecov.yml ├── develop └── index.js ├── jest.config.js ├── package.json ├── rollup.config.js ├── src ├── index.js ├── utils │ └── index.js └── visitor.js ├── test ├── .eslintrc ├── __snapshots__ │ ├── comments.spec.js.snap │ ├── prepend.spec.js.snap │ └── utils.spec.js.snap ├── apply.spec.js ├── comments.spec.js ├── control.spec.js ├── fixture │ ├── apply │ │ ├── expected.css │ │ └── input.css │ ├── control │ │ ├── expected.css │ │ └── input.css │ ├── overrides │ │ ├── expected.css │ │ └── input.css │ └── preserve │ │ ├── expected.css │ │ └── input.css ├── integration.spec.js ├── prepend.spec.js ├── preserve.spec.js └── utils.spec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": 12 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | "@babel/plugin-proposal-class-properties", 14 | "@babel/plugin-proposal-do-expressions" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | parser: "@babel/eslint-parser" 4 | extends: 5 | - "airbnb-base" 6 | - "plugin:jest/recommended" 7 | - "prettier" 8 | 9 | plugins: 10 | - "jest" 11 | 12 | env: 13 | jest: true 14 | 15 | rules: 16 | one-var: off 17 | prefer-const: off 18 | lines-between-class-members: off 19 | no-use-before-define: 20 | - error 21 | - functions: false 22 | comma-dangle: 23 | - error 24 | - arrays: "always-multiline" 25 | objects: "always-multiline" 26 | imports: "always-multiline" 27 | exports: "always-multiline" 28 | functions: ignore 29 | 30 | import/no-extraneous-dependencies: 31 | - error 32 | - devDependencies: true 33 | optionalDependencies: false 34 | peerDependencies: false 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | .jest-cache 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "arrowParens": "avoid" 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - lts/* 5 | - stable 6 | 7 | sudo: false 8 | 9 | git: 10 | depth: 10 11 | 12 | script: yarn run test:ci 13 | 14 | after_success: yarn codecov 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # postcss-apply change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [unreleased] 9 | 10 | ## [0.12.0] - 2019-02-27 11 | ### Changed 12 | * Upgrade Babel. 13 | * Remove `babel-runtime` dependency. 14 | 15 | ## [0.11.0] - 2018-08-11 16 | ### Changed 17 | * Upgrade PostCSS to version 7. 18 | **Breaking** Removes support for Node.js versions lower than 6. 19 | 20 | ## [0.10.0] - 2018-04-16 21 | ### Added 22 | * Remove immediate preceding comments in declarations. 23 | Prevent empty `:root` rules with only comments. 24 | 25 | ## [0.9.0] - 2018-03-10 26 | ### Added 27 | * Allow both object and string types for the `sets` option. 28 | 29 | ## [0.8.0] - 2017-05-28 30 | ### Added 31 | * Support for ancestor rules for `@apply` declarations. 32 | Allows for deep nested declarations like atRules. 33 | 34 | ## [0.7.0] - 2017-05-08 35 | ### Changed 36 | * PostCSS 6 upgrade. 37 | 38 | ### Fixed 39 | * Polyfill `Object.entries` for node versions lower than 7. 40 | 41 | ## [0.6.1] - 2017-03-10 42 | ### Fixed 43 | * A forgotten `console.log` in sources. 44 | 45 | ## [0.6.0] - 2017-03-08 46 | ### Added 47 | * A new `sets` option. 48 | Allows for in JS declared property sets. 49 | 50 | ## [0.5.0] - 2017-02-05 51 | ### Added 52 | * A new `preserve` option. 53 | Allows for keeping resolved declarations and `@apply` rules alongside. 54 | 55 | ## [0.4.0] - 2016-09-13 56 | ### Changed 57 | * Correctly handles property set overrides. 58 | [#10](https://github.com/pascalduez/postcss-apply/issues/10) 59 | 60 | ## [0.3.0] - 2016-06-23 61 | ### Changed 62 | * Several dependencies updates. 63 | * Renames in folder structure, files and main class. 64 | * Switch to `ava` for unit testing. 65 | * Switch to `nyc` for code coverage. 66 | * Clarify The Readme. 67 | ### Added 68 | * Integration unit tests. 69 | 70 | ## [0.2.0] - 2016-03-13 71 | ### Added 72 | * Support for parenthesis in mixin calls. 73 | Allows integration with Polymer. 74 | 75 | ## [0.1.0] - 2015-08-26 76 | * Initial release. 77 | 78 | [Unreleased]: https://github.com/pascalduez/postcss-apply/compare/0.11.0...HEAD 79 | [0.11.0]: https://github.com/pascalduez/postcss-apply/compare/0.9.0...0.11.0 80 | [0.10.0]: https://github.com/pascalduez/postcss-apply/compare/0.9.0...0.10.0 81 | [0.9.0]: https://github.com/pascalduez/postcss-apply/compare/0.8.0...0.9.0 82 | [0.8.0]: https://github.com/pascalduez/postcss-apply/compare/0.7.0...0.8.0 83 | [0.7.0]: https://github.com/pascalduez/postcss-apply/compare/0.6.1...0.7.0 84 | [0.6.1]: https://github.com/pascalduez/postcss-apply/compare/0.6.0...0.6.1 85 | [0.6.0]: https://github.com/pascalduez/postcss-apply/compare/0.5.0...0.6.0 86 | [0.5.0]: https://github.com/pascalduez/postcss-apply/compare/0.4.0...0.5.0 87 | [0.4.0]: https://github.com/pascalduez/postcss-apply/compare/0.3.0...0.4.0 88 | [0.3.0]: https://github.com/pascalduez/postcss-apply/compare/0.2.0...0.3.0 89 | [0.2.0]: https://github.com/pascalduez/postcss-apply/compare/0.1.0...0.2.0 90 | [0.1.0]: https://github.com/pascalduez/postcss-apply/tags/0.1.0 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-apply 2 | 3 | [![CSS Standard Status][css-image]][css-url] 4 | [![npm version][npm-image]][npm-url] 5 | [![Build Status][travis-image]][travis-url] 6 | [![Coverage Status][codecov-image]][codecov-url] 7 | 8 | 9 | > [PostCSS] plugin enabling custom property sets references 10 | 11 | Refer to [`postcss-custom-properties`](https://github.com/postcss/postcss-custom-properties#postcss-custom-properties-) for DOMless limitations. 12 | 13 | 14 | ## Web Platform status 15 | 16 | Spec (editor's draft): https://tabatkins.github.io/specs/css-apply-rule 17 | Browser support: https://www.chromestatus.com/feature/5753701012602880 18 | 19 | :warning: The `@apply` rule and custom property sets most likely won't get any more support from browser vendors as the spec is yet considered deprecated and [alternative solutions](https://tabatkins.github.io/specs/css-shadow-parts) are being discussed. 20 | Refer to following links for more infos: 21 | * https://discourse.wicg.io/t/needed-new-champion-for-css-apply-rule/2012 22 | * https://github.com/w3c/webcomponents/issues/300#issuecomment-276210974 23 | * http://www.xanthir.com/b4o00 24 | * https://github.com/w3c/csswg-drafts/issues/1047 25 | * https://chromium.googlesource.com/chromium/src/+/5874fca7324e4523a4bdecc8999bdadfdb6c4eff 26 | 27 | 28 | ## Installation 29 | 30 | ``` 31 | npm install postcss-apply --save-dev 32 | ``` 33 | 34 | 35 | ## Usage 36 | 37 | ```js 38 | const fs = require('fs'); 39 | const postcss = require('postcss'); 40 | const apply = require('postcss-apply'); 41 | 42 | let input = fs.readFileSync('input.css', 'utf8'); 43 | 44 | postcss() 45 | .use(apply) 46 | .process(input) 47 | .then(result => { 48 | fs.writeFileSync('output.css', result.css); 49 | }); 50 | ``` 51 | 52 | ## Examples 53 | 54 | ### In CSS declared sets 55 | 56 | ```css 57 | /* input */ 58 | 59 | :root { 60 | --toolbar-theme: { 61 | background-color: rebeccapurple; 62 | color: white; 63 | border: 1px solid green; 64 | }; 65 | } 66 | 67 | .Toolbar { 68 | @apply --toolbar-theme; 69 | } 70 | ``` 71 | 72 | ```css 73 | /* output */ 74 | 75 | .Toolbar { 76 | background-color: rebeccapurple; 77 | color: white; 78 | border: 1px solid green; 79 | } 80 | ``` 81 | 82 | ### In JS declared sets 83 | 84 | ```js 85 | const themes = { 86 | /* Set names won't be transformed, just `--` will be prepended. */ 87 | 'toolbar-theme': { 88 | /* Declaration properties can either be camel or kebab case. */ 89 | backgroundColor: 'rebeccapurple', 90 | color: 'white', 91 | border: '1px solid green', 92 | }, 93 | }; 94 | 95 | [...] 96 | postcss().use(apply({ sets: themes })) 97 | [...] 98 | ``` 99 | 100 | ```css 101 | /* input */ 102 | 103 | .Toolbar { 104 | @apply --toolbar-theme; 105 | } 106 | ``` 107 | 108 | ```css 109 | /* output */ 110 | 111 | .Toolbar { 112 | background-color: rebeccapurple; 113 | color: white; 114 | border: 1px solid green; 115 | } 116 | ``` 117 | 118 | ## options 119 | 120 | ### `preserve` 121 | type: `Boolean` 122 | default: `false` 123 | Allows for keeping resolved declarations and `@apply` rules alongside. 124 | 125 | ### `sets` 126 | type: `{ [customPropertyName: string]: Object | string }` 127 | default: `{}` 128 | Allows you to pass an object or string of custom property sets for `:root`. 129 | These definitions will be prepended, in such overridden by the one declared in CSS if they share the same name. 130 | The keys are automatically prefixed with the CSS `--` to make it easier to share sets in your codebase. 131 | 132 | 133 | ## Credits 134 | 135 | * [Pascal Duez](https://github.com/pascalduez) 136 | 137 | 138 | ## Licence 139 | 140 | postcss-apply is [unlicensed](http://unlicense.org/). 141 | 142 | 143 | 144 | [PostCSS]: https://github.com/postcss/postcss 145 | 146 | [css-url]: https://cssdb.org#rejected 147 | [css-image]: https://img.shields.io/badge/cssdb-rejected-red.svg?style=flat-square 148 | [npm-url]: https://www.npmjs.org/package/postcss-apply 149 | [npm-image]: http://img.shields.io/npm/v/postcss-apply.svg?style=flat-square 150 | [travis-url]: https://travis-ci.org/pascalduez/postcss-apply?branch=master 151 | [travis-image]: http://img.shields.io/travis/pascalduez/postcss-apply.svg?style=flat-square 152 | [codecov-url]: https://codecov.io/gh/pascalduez/postcss-apply 153 | [codecov-image]: https://img.shields.io/codecov/c/github/pascalduez/postcss-apply.svg?style=flat-square 154 | [spec]: https://tabatkins.github.io/specs/css-apply-rule 155 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | parsers: 2 | javascript: 3 | enable_partials: yes 4 | -------------------------------------------------------------------------------- /develop/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import postcss from 'postcss'; 5 | import reporter from 'postcss-reporter'; 6 | import plugin from '../src'; 7 | 8 | let from, to; 9 | let read = name => 10 | fs.readFileSync(path.join(process.cwd(), 'test', 'fixture', name), 'utf8'); 11 | 12 | let input = read('apply/input.css'); 13 | 14 | postcss() 15 | .use(plugin) 16 | .use(reporter) 17 | .process(input, { from, to }) 18 | .then(result => { 19 | console.log(result.css); 20 | }) 21 | .catch(console.error); 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const isCI = require('is-ci'); 2 | 3 | const config = { 4 | testEnvironment: 'node', 5 | cacheDirectory: '.jest-cache', 6 | collectCoverageFrom: ['src/*.js'], 7 | }; 8 | 9 | if (isCI) { 10 | Object.assign(config, { 11 | collectCoverage: true, 12 | }); 13 | } 14 | 15 | module.exports = config; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-apply", 3 | "version": "0.12.0", 4 | "description": "PostCSS plugin enabling custom properties sets references", 5 | "keywords": [ 6 | "css", 7 | "apply", 8 | "postcss", 9 | "postcss-plugin" 10 | ], 11 | "author": { 12 | "name": "Pascal Duez", 13 | "url": "https://github.com/pascalduez" 14 | }, 15 | "homepage": "https://github.com/pascalduez/postcss-apply", 16 | "bugs": "https://github.com/pascalduez/postcss-apply/issues", 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/pascalduez/postcss-apply.git" 20 | }, 21 | "license": "Unlicense", 22 | "files": [ 23 | "dist", 24 | "CHANGELOG.md", 25 | "index.js", 26 | "README.md", 27 | "UNLICENSE" 28 | ], 29 | "main": "dist/index.js", 30 | "module": "dist/index.m.js", 31 | "scripts": { 32 | "lint": "eslint src/ test/", 33 | "validate": "run-s lint", 34 | "test": "jest", 35 | "test:ci": "run-s validate test", 36 | "develop": "babel-node develop/", 37 | "prebuild": "rm -rf dist/", 38 | "build": "rollup -c", 39 | "prepare": "run-s build", 40 | "prepublishOnly": "run-s validate test" 41 | }, 42 | "dependencies": { 43 | "balanced-match": "^2.0.0", 44 | "postcss": "^7.0.14" 45 | }, 46 | "devDependencies": { 47 | "@babel/cli": "^7.17.6", 48 | "@babel/core": "^7.17.5", 49 | "@babel/eslint-parser": "^7.17.0", 50 | "@babel/plugin-proposal-class-properties": "^7.16.7", 51 | "@babel/plugin-proposal-do-expressions": "^7.16.7", 52 | "@babel/preset-env": "^7.16.11", 53 | "@rollup/plugin-babel": "^5.3.1", 54 | "@rollup/plugin-commonjs": "^21.0.2", 55 | "@rollup/plugin-json": "^4.1.0", 56 | "@rollup/plugin-node-resolve": "^13.1.3", 57 | "babel-eslint": "^10.1.0", 58 | "common-tags": "^1.8.2", 59 | "eslint": "^8.10.0", 60 | "eslint-config-airbnb-base": "^15.0.0", 61 | "eslint-config-prettier": "^8.5.0", 62 | "eslint-plugin-import": "^2.25.4", 63 | "eslint-plugin-jest": "^26.1.1", 64 | "is-ci": "^3.0.1", 65 | "jest": "^27.5.1", 66 | "npm-run-all": "^4.1.5", 67 | "opn-cli": "^5.0.0", 68 | "postcss-custom-properties": "10", 69 | "postcss-reporter": "^7.0.5", 70 | "prettier": "^2.5.1", 71 | "rollup": "^2.69.1" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import babel from '@rollup/plugin-babel'; 4 | import json from '@rollup/plugin-json'; 5 | import pkg from './package.json'; 6 | 7 | export default { 8 | plugins: [ 9 | babel({ 10 | exclude: ['node_modules/**'], 11 | babelHelpers: 'bundled' 12 | }), 13 | json(), 14 | resolve(), 15 | commonjs(), 16 | ], 17 | external: ['postcss', 'balanced-match'], 18 | input: 'src/index.js', 19 | output: [ 20 | { 21 | file: pkg.main, 22 | format: 'cjs', 23 | exports: 'default', 24 | }, 25 | { 26 | file: pkg.module, 27 | format: 'esm', 28 | }, 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { plugin } from 'postcss'; 2 | import Visitor from './visitor'; 3 | import { name } from '../package.json'; 4 | 5 | export default plugin(name, options => (css, result) => { 6 | const visitor = new Visitor(options); 7 | visitor.result = result; 8 | 9 | visitor.prepend(); 10 | 11 | css.walkRules(visitor.collect); 12 | 13 | visitor.resolveNested(); 14 | 15 | css.walkAtRules('apply', visitor.resolve); 16 | }); 17 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export function kebabify(prop) { 2 | const upperToHyphen = (match, offset, string) => { 3 | const addDash = offset && string.charAt(offset - 1) !== '-'; 4 | 5 | return (addDash ? '-' : '') + match.toLowerCase(); 6 | }; 7 | 8 | return prop.replace(/[A-Z]/g, upperToHyphen); 9 | } 10 | 11 | export const isPlainObject = arg => 12 | Object.prototype.toString.call(arg) === '[object Object]'; 13 | -------------------------------------------------------------------------------- /src/visitor.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | import balanced from 'balanced-match'; 4 | import postcss from 'postcss'; 5 | import { kebabify, isPlainObject } from './utils'; 6 | 7 | const RE_PROP_SET = /^(--)([\w-]+)(\s*)([:]?)$/; 8 | 9 | export default class Visitor { 10 | cache = {}; 11 | result = {}; 12 | options = {}; 13 | 14 | defaults = { 15 | preserve: false, 16 | sets: {}, 17 | }; 18 | 19 | constructor(options) { 20 | this.options = { 21 | ...this.defaults, 22 | ...options, 23 | }; 24 | } 25 | 26 | /** 27 | * Prepend JS defined sets into the cache before parsing. 28 | * This means CSS defined sets will overrides them if they share the same name. 29 | */ 30 | prepend = () => { 31 | const { sets } = this.options; 32 | 33 | Object.keys(sets).forEach(setName => { 34 | const newRule = postcss.rule({ selector: `--${setName}` }); 35 | 36 | const set = sets[setName]; 37 | 38 | if (typeof set === 'string') { 39 | newRule.prepend(set); 40 | } else if (isPlainObject(set)) { 41 | Object.entries(set).forEach(([prop, value]) => { 42 | newRule.prepend(postcss.decl({ prop: kebabify(prop), value })); 43 | }); 44 | } else { 45 | throw new Error( 46 | `Unrecognized set type \`${typeof set}\`, must be an object or string.` 47 | ); 48 | } 49 | 50 | this.cache[setName] = newRule; 51 | }); 52 | }; 53 | 54 | /** 55 | * Collect all `:root` declared property sets and save them. 56 | */ 57 | collect = rule => { 58 | const matches = RE_PROP_SET.exec(rule.selector); 59 | 60 | if (!matches) { 61 | return; 62 | } 63 | 64 | const setName = matches[2]; 65 | const { parent } = rule; 66 | 67 | if (parent.selector !== ':root') { 68 | rule.warn( 69 | this.result, 70 | 'Custom property set ignored: not scoped to top-level `:root` ' + 71 | `(--${setName}` + 72 | `${parent.type === 'rule' ? ` declared in ${parent.selector}` : ''})` 73 | ); 74 | 75 | if (parent.type === 'root') { 76 | rule.remove(); 77 | } 78 | 79 | return; 80 | } 81 | 82 | // Custom property sets override each other wholly, 83 | // rather than cascading together like colliding style rules do. 84 | // @see: https://tabatkins.github.io/specs/css-apply-rule/#defining 85 | const newRule = rule.clone(); 86 | this.cache[setName] = newRule; 87 | 88 | if (!this.options.preserve) { 89 | removeCommentBefore(rule); 90 | safeRemoveRule(rule); 91 | } 92 | 93 | if (!parent.nodes.length) { 94 | parent.remove(); 95 | } 96 | }; 97 | 98 | /** 99 | * Replace nested `@apply` at-rules declarations. 100 | */ 101 | resolveNested = () => { 102 | Object.keys(this.cache).forEach(rule => { 103 | this.cache[rule].walkAtRules('apply', atRule => { 104 | this.resolve(atRule); 105 | 106 | // @TODO honor `preserve` option. 107 | atRule.remove(); 108 | }); 109 | }); 110 | }; 111 | 112 | /** 113 | * Replace `@apply` at-rules declarations with provided custom property set. 114 | */ 115 | resolve = atRule => { 116 | let ancestor = atRule.parent; 117 | 118 | while (ancestor && ancestor.type !== 'rule') { 119 | ancestor = ancestor.parent; 120 | } 121 | 122 | if (!ancestor) { 123 | atRule.warn( 124 | this.result, 125 | 'The @apply rule can only be declared inside Rule type nodes.' 126 | ); 127 | 128 | atRule.remove(); 129 | return; 130 | } 131 | 132 | if (isDefinition(atRule.parent)) { 133 | return; 134 | } 135 | 136 | const param = getParamValue(atRule.params); 137 | const matches = RE_PROP_SET.exec(param); 138 | 139 | if (!matches) { 140 | return; 141 | } 142 | 143 | const setName = matches[2]; 144 | const { parent } = atRule; 145 | 146 | if (!(setName in this.cache)) { 147 | atRule.warn( 148 | this.result, 149 | `No custom property set declared for \`${setName}\`.` 150 | ); 151 | 152 | return; 153 | } 154 | 155 | const newRule = this.cache[setName].clone(); 156 | cleanIndent(newRule); 157 | 158 | if (this.options.preserve) { 159 | parent.insertBefore(atRule, newRule.nodes); 160 | 161 | return; 162 | } 163 | 164 | atRule.replaceWith(newRule.nodes); 165 | }; 166 | } 167 | 168 | /** 169 | * Helper: return whether the rule is a custom property set definition. 170 | */ 171 | function isDefinition(rule) { 172 | return ( 173 | !!rule.selector && 174 | !!RE_PROP_SET.exec(rule.selector) && 175 | rule.parent && 176 | !!rule.parent.selector && 177 | rule.parent.selector === ':root' 178 | ); 179 | } 180 | 181 | /** 182 | * Helper: allow parens usage in `@apply` AtRule declaration. 183 | * This is for Polymer integration. 184 | */ 185 | function getParamValue(param) { 186 | return /^\(/.test(param) ? balanced('(', ')', param).body : param; 187 | } 188 | 189 | /** 190 | * Helper: remove excessive declarations indentation. 191 | */ 192 | function cleanIndent(rule) { 193 | rule.walkDecls(decl => { 194 | if (typeof decl.raws.before === 'string') { 195 | decl.raws.before = decl.raws.before.replace(/[^\S\n\r]{2,}/, ' '); 196 | } 197 | }); 198 | } 199 | 200 | /** 201 | * Helper: correctly handle property sets removal and semi-colons. 202 | * @See: postcss/postcss#1014 203 | */ 204 | function safeRemoveRule(rule) { 205 | if (rule === rule.parent.last && rule.raws.ownSemicolon) { 206 | rule.parent.raws.semicolon = true; 207 | } 208 | 209 | rule.remove(); 210 | } 211 | 212 | /** 213 | * Helper: remove immediate preceding comments. 214 | */ 215 | function removeCommentBefore(node) { 216 | const previousNode = node.prev(); 217 | 218 | if (previousNode && previousNode.type === 'comment') { 219 | previousNode.remove(); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | rules: 3 | import/no-extraneous-dependencies: off 4 | -------------------------------------------------------------------------------- /test/__snapshots__/comments.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`comments should not remove immediate preceding comments in declarations with the preserve option 1`] = ` 4 | " 5 | :root { 6 | /* This should be pruned */ 7 | --toolbar-theme: { 8 | color: orangeRed; 9 | } 10 | } 11 | 12 | .toolbar { 13 | color: orangeRed; 14 | @apply --toolbar-theme; 15 | } 16 | " 17 | `; 18 | 19 | exports[`comments should remove immediate preceding comments in declarations 1`] = ` 20 | " 21 | .toolbar { 22 | color: orangeRed; 23 | } 24 | " 25 | `; 26 | -------------------------------------------------------------------------------- /test/__snapshots__/prepend.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`prepend should be able to be nested inside a CSS declared set 1`] = ` 4 | ".dummy { 5 | color: tomato; 6 | }" 7 | `; 8 | 9 | exports[`prepend should override sets from options with CSS declared ones 1`] = ` 10 | ".dummy { 11 | padding: none; 12 | color: orangeRed; 13 | }" 14 | `; 15 | 16 | exports[`prepend should prepend sets from options from a string set 1`] = ` 17 | ".dummy { 18 | fontSize: 1.4rem; 19 | @media (width >= 500px) { 20 | fontSize: 2.4rem; 21 | } 22 | }" 23 | `; 24 | 25 | exports[`prepend should prepend sets from options from an object set 1`] = ` 26 | ".dummy { 27 | color: tomato; 28 | font-size: 1.4rem; 29 | padding: 0 1rem; 30 | }" 31 | `; 32 | -------------------------------------------------------------------------------- /test/__snapshots__/utils.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`kekabify should convert camelCase properties to kebab-case 1`] = ` 4 | Array [ 5 | "border-radius", 6 | "background-color", 7 | "background-color", 8 | "font-size", 9 | "font-size", 10 | "border-top-left-radius", 11 | "-moz-whatever", 12 | "-webkit-who-cares", 13 | "--custom-prop", 14 | "--custom-prop", 15 | "--custom-prop", 16 | ] 17 | `; 18 | -------------------------------------------------------------------------------- /test/apply.spec.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import postcss from 'postcss'; 4 | import plugin from '../src'; 5 | 6 | const read = name => 7 | fs.readFileSync(path.join(__dirname, 'fixture', name), 'utf8'); 8 | 9 | describe('apply', () => { 10 | it('should properly apply and remove custom property sets', async () => { 11 | const input = read('apply/input.css'); 12 | const expected = read('apply/expected.css'); 13 | 14 | const result = await postcss() 15 | .use(plugin) 16 | .process(input, { from: undefined }); 17 | 18 | expect(result.css).toBe(expected); 19 | }); 20 | 21 | it('should properly apply custom property sets overrides', async () => { 22 | const input = read('overrides/input.css'); 23 | const expected = read('overrides/expected.css'); 24 | 25 | const result = await postcss() 26 | .use(plugin) 27 | .process(input, { from: undefined }); 28 | 29 | expect(result.css).toBe(expected); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/comments.spec.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss'; 2 | import plugin from '../src'; 3 | 4 | const input = ` 5 | :root { 6 | /* This should be pruned */ 7 | --toolbar-theme: { 8 | color: orangeRed; 9 | } 10 | } 11 | 12 | .toolbar { 13 | @apply --toolbar-theme; 14 | } 15 | `; 16 | 17 | let from; 18 | 19 | describe('comments', () => { 20 | it('should remove immediate preceding comments in declarations', async () => { 21 | const result = await postcss().use(plugin).process(input, { from }); 22 | 23 | expect(result.css).toMatchSnapshot(); 24 | }); 25 | 26 | it('should not remove immediate preceding comments in declarations with the preserve option', async () => { 27 | const result = await postcss() 28 | .use(plugin({ preserve: true })) 29 | .process(input, { from }); 30 | 31 | expect(result.css).toMatchSnapshot(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/control.spec.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import postcss from 'postcss'; 4 | import plugin from '../src'; 5 | 6 | const pluginName = require('../package.json').name; 7 | 8 | const read = name => 9 | fs.readFileSync(path.join(__dirname, 'fixture', name), 'utf8'); 10 | 11 | const expected = read('control/expected.css'); 12 | const input = read('control/input.css'); 13 | 14 | describe('control', () => { 15 | it('with no options', () => 16 | postcss([plugin]) 17 | .process(input, { from: undefined }) 18 | .then(result => { 19 | expect(result.css).toBe(expected); 20 | })); 21 | 22 | it('with options', () => 23 | postcss([plugin({})]) 24 | .process(input, { from: undefined }) 25 | .then(result => { 26 | expect(result.css).toBe(expected); 27 | })); 28 | 29 | it('PostCSS legacy API', () => { 30 | const result = postcss([plugin.postcss]).process(input, { from: undefined }) 31 | .css; 32 | 33 | expect(result).toBe(expected); 34 | }); 35 | 36 | it('PostCSS API', async () => { 37 | const processor = postcss(); 38 | processor.use(plugin); 39 | 40 | const result = await processor.process(input, { from: undefined }); 41 | 42 | expect(result.css).toBe(expected); 43 | 44 | expect(result.messages.length).toBeGreaterThan(0); 45 | 46 | expect(result.messages[0].type).toBe('warning'); 47 | expect(result.messages[1].type).toBe('warning'); 48 | expect(result.messages[2].type).toBe('warning'); 49 | expect(result.messages[3].type).toBe('warning'); 50 | 51 | expect(result.messages[0].text).toMatch( 52 | /Custom property set ignored: not scoped to top-level `:root`/ 53 | ); 54 | 55 | expect(result.messages[1].text).toMatch( 56 | /Custom property set ignored: not scoped to top-level `:root`/ 57 | ); 58 | 59 | expect(result.messages[2].text).toBe( 60 | 'No custom property set declared for `this-should-warn`.' 61 | ); 62 | 63 | expect(result.messages[3].text).toBe( 64 | 'The @apply rule can only be declared inside Rule type nodes.' 65 | ); 66 | 67 | expect(processor.plugins[0].postcssPlugin).toBe(pluginName); 68 | expect(processor.plugins[0].postcssVersion).toBeDefined(); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/fixture/apply/expected.css: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | background-color: hsl(120, 70%, 95%); 3 | border-radius: 4px; 4 | border: 1px solid var(--theme-color late); 5 | } 6 | 7 | .toolbar > .title { 8 | color: green; 9 | } 10 | 11 | .with-parens { 12 | color: tomato; 13 | } 14 | 15 | .nested-set-one { 16 | background-color: hsl(120, 70%, 95%); 17 | border-radius: 4px; 18 | border: 1px solid var(--theme-color late); 19 | } 20 | 21 | .nested-set-two { 22 | color: tomato; 23 | color: green; 24 | 25 | color: orange; 26 | } 27 | 28 | .nested-deeper { 29 | @media screen { 30 | background-color: hsl(120, 70%, 95%); 31 | border-radius: 4px; 32 | border: 1px solid var(--theme-color late); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/fixture/apply/input.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --toolbar-theme: { 3 | background-color: hsl(120, 70%, 95%); 4 | border-radius: 4px; 5 | border: 1px solid var(--theme-color late); 6 | } 7 | --toolbar-title-theme: { 8 | color: green; 9 | } 10 | --with-parens: { 11 | color: tomato; 12 | } 13 | --nested-set-one: { 14 | @apply --toolbar-theme; 15 | } 16 | --nested-set-two: { 17 | @apply --with-parens; 18 | @apply --toolbar-title-theme; 19 | 20 | color: orange; 21 | } 22 | } 23 | 24 | .toolbar { 25 | @apply --toolbar-theme; 26 | } 27 | 28 | .toolbar > .title { 29 | @apply --toolbar-title-theme; 30 | } 31 | 32 | .with-parens { 33 | @apply (--with-parens); 34 | } 35 | 36 | .nested-set-one { 37 | @apply --nested-set-one; 38 | } 39 | 40 | .nested-set-two { 41 | @apply --nested-set-two; 42 | } 43 | 44 | .nested-deeper { 45 | @media screen { 46 | @apply --toolbar-theme; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/fixture/control/expected.css: -------------------------------------------------------------------------------- 1 | .control { 2 | content: 'control'; 3 | } 4 | 5 | .should-pass { 6 | @apply not-a-prop-set; 7 | } 8 | 9 | .should-warn--not-root { 10 | --toolbar-title-theme: { 11 | color: green; 12 | } 13 | } 14 | 15 | .should-warn--not-declared { 16 | @apply --this-should-warn; 17 | } 18 | -------------------------------------------------------------------------------- /test/fixture/control/input.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --should-be-removed: { 3 | content: 'gone'; 4 | } 5 | } 6 | 7 | .control { 8 | content: 'control'; 9 | } 10 | 11 | .should-pass { 12 | @apply not-a-prop-set; 13 | } 14 | 15 | .should-warn--not-root { 16 | --toolbar-title-theme: { 17 | color: green; 18 | } 19 | } 20 | 21 | .should-warn--not-declared { 22 | @apply --this-should-warn; 23 | } 24 | 25 | --should-warn-about-root-scope-and-be-removed: { 26 | color: green; 27 | } 28 | 29 | @apply --should-warn-about-root-scope-and-be-removed; 30 | -------------------------------------------------------------------------------- /test/fixture/overrides/expected.css: -------------------------------------------------------------------------------- 1 | .override-before { 2 | color: blue; 3 | } 4 | 5 | .override-after { 6 | color: blue; 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/overrides/input.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --override: { 3 | color: red; 4 | background-color: red; 5 | } 6 | } 7 | 8 | .override-before { 9 | @apply --override; 10 | } 11 | 12 | :root { 13 | --override: { 14 | color: blue; 15 | } 16 | } 17 | 18 | .override-after { 19 | @apply --override; 20 | } 21 | -------------------------------------------------------------------------------- /test/fixture/preserve/expected.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --toolbar-theme: { 3 | background-color: hsl(120, 70%, 95%); 4 | border-radius: 4px; 5 | border: 1px solid var(--theme-color late); 6 | } 7 | --toolbar-title-theme: { 8 | color: green; 9 | } 10 | --with-parens: { 11 | color: tomato; 12 | } 13 | --nested-set-one: { 14 | @apply --toolbar-theme; 15 | } 16 | --nested-set-two: { 17 | @apply --with-parens; 18 | @apply --toolbar-title-theme; 19 | 20 | color: orange; 21 | } 22 | --override: { 23 | color: red; 24 | background-color: red; 25 | } 26 | } 27 | 28 | .toolbar { 29 | background-color: hsl(120, 70%, 95%); 30 | border-radius: 4px; 31 | border: 1px solid var(--theme-color late); 32 | @apply --toolbar-theme; 33 | } 34 | 35 | .toolbar > .title { 36 | color: green; 37 | @apply --toolbar-title-theme; 38 | } 39 | 40 | .with-parens { 41 | color: tomato; 42 | @apply (--with-parens); 43 | } 44 | 45 | .nested-set-one { 46 | background-color: hsl(120, 70%, 95%); 47 | border-radius: 4px; 48 | border: 1px solid var(--theme-color late); 49 | @apply --nested-set-one; 50 | } 51 | 52 | .nested-set-two { 53 | color: tomato; 54 | color: green; 55 | 56 | color: orange; 57 | @apply --nested-set-two; 58 | } 59 | 60 | .override-before { 61 | color: blue; 62 | @apply --override; 63 | } 64 | 65 | :root { 66 | --override: { 67 | color: blue; 68 | } 69 | } 70 | 71 | .override-after { 72 | color: blue; 73 | @apply --override; 74 | } 75 | -------------------------------------------------------------------------------- /test/fixture/preserve/input.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --toolbar-theme: { 3 | background-color: hsl(120, 70%, 95%); 4 | border-radius: 4px; 5 | border: 1px solid var(--theme-color late); 6 | } 7 | --toolbar-title-theme: { 8 | color: green; 9 | } 10 | --with-parens: { 11 | color: tomato; 12 | } 13 | --nested-set-one: { 14 | @apply --toolbar-theme; 15 | } 16 | --nested-set-two: { 17 | @apply --with-parens; 18 | @apply --toolbar-title-theme; 19 | 20 | color: orange; 21 | } 22 | --override: { 23 | color: red; 24 | background-color: red; 25 | } 26 | } 27 | 28 | .toolbar { 29 | @apply --toolbar-theme; 30 | } 31 | 32 | .toolbar > .title { 33 | @apply --toolbar-title-theme; 34 | } 35 | 36 | .with-parens { 37 | @apply (--with-parens); 38 | } 39 | 40 | .nested-set-one { 41 | @apply --nested-set-one; 42 | } 43 | 44 | .nested-set-two { 45 | @apply --nested-set-two; 46 | } 47 | 48 | .override-before { 49 | @apply --override; 50 | } 51 | 52 | :root { 53 | --override: { 54 | color: blue; 55 | } 56 | } 57 | 58 | .override-after { 59 | @apply --override; 60 | } 61 | -------------------------------------------------------------------------------- /test/integration.spec.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss'; 2 | import { stripIndent } from 'common-tags'; 3 | import customProperties from 'postcss-custom-properties'; 4 | import plugin from '../src'; 5 | 6 | describe('integration', () => { 7 | test('custom properties declaration without plugin', async () => { 8 | const input = stripIndent` 9 | :root { 10 | --should-stay: 'test'; 11 | --should-be-removed: { 12 | content: 'gone'; 13 | }; 14 | }`; 15 | 16 | const expected = stripIndent` 17 | :root { 18 | --should-stay: 'test'; 19 | }`; 20 | 21 | const result = await postcss() 22 | .use(plugin) 23 | .process(input, { from: undefined }); 24 | 25 | expect(result.css).toBe(expected); 26 | }); 27 | 28 | test('custom properties declaration with plugin first', async () => { 29 | const input = stripIndent` 30 | :root { 31 | --should-be-pruned: 'pruned'; 32 | --should-be-removed: { 33 | content: 'gone'; 34 | }; 35 | }`; 36 | 37 | const expected = input; 38 | 39 | const result = await postcss() 40 | .use(customProperties) 41 | .process(input, { from: undefined }); 42 | 43 | expect(result.css).toBe(expected); 44 | }); 45 | 46 | test('custom properties declaration with plugin last', async () => { 47 | const input = stripIndent` 48 | :root { 49 | --should-be-pruned: 'pruned'; 50 | --should-be-removed: { 51 | content: 'gone'; 52 | }; 53 | }`; 54 | 55 | const expected = stripIndent` 56 | :root { 57 | --should-be-pruned: 'pruned'; 58 | }`; 59 | 60 | const result = await postcss() 61 | .use(plugin) 62 | .use(customProperties) 63 | .process(input, { from: undefined }); 64 | 65 | expect(result.css).toBe(expected); 66 | }); 67 | 68 | test('custom properties without plugin', async () => { 69 | const input = stripIndent` 70 | :root { 71 | --should-stay: 'test'; 72 | --should-be-removed: { 73 | content: 'gone'; 74 | }; 75 | } 76 | 77 | .test { 78 | @apply --should-be-removed; 79 | content: var(--should-stay); 80 | }`; 81 | 82 | const expected = stripIndent` 83 | :root { 84 | --should-stay: 'test'; 85 | } 86 | 87 | .test { 88 | content: 'gone'; 89 | content: var(--should-stay); 90 | }`; 91 | 92 | const result = await postcss() 93 | .use(plugin) 94 | .process(input, { from: undefined }); 95 | 96 | expect(result.css).toBe(expected); 97 | }); 98 | 99 | test('custom properties with plugin', async () => { 100 | const input = stripIndent` 101 | :root { 102 | --custom-prop: 'prop'; 103 | --custom-prop-set: { 104 | content: 'set'; 105 | }; 106 | } 107 | 108 | .test { 109 | @apply --custom-prop-set; 110 | content: var(--custom-prop); 111 | }`; 112 | 113 | const expected = stripIndent` 114 | :root { 115 | --custom-prop: 'prop'; 116 | } 117 | 118 | .test { 119 | content: 'set'; 120 | content: 'prop'; 121 | content: var(--custom-prop); 122 | }`; 123 | 124 | const result = await postcss() 125 | .use(customProperties) 126 | .use(plugin) 127 | .process(input, { from: undefined }); 128 | 129 | expect(result.css).toBe(expected); 130 | }); 131 | 132 | test('custom properties nested without plugin', async () => { 133 | const input = stripIndent` 134 | :root { 135 | --custom-prop: 'prop'; 136 | --custom-prop-set: { 137 | content: var(--custom-prop); 138 | }; 139 | } 140 | 141 | .test { 142 | @apply --custom-prop-set; 143 | content: var(--custom-prop); 144 | }`; 145 | 146 | const expected = stripIndent` 147 | :root { 148 | --custom-prop: 'prop'; 149 | } 150 | 151 | .test { 152 | content: var(--custom-prop); 153 | content: var(--custom-prop); 154 | }`; 155 | 156 | const result = await postcss() 157 | .use(plugin) 158 | .process(input, { from: undefined }); 159 | 160 | expect(result.css).toBe(expected); 161 | }); 162 | 163 | test('custom properties nested with plugin first', async () => { 164 | const input = stripIndent` 165 | :root { 166 | --custom-prop: 'prop'; 167 | --custom-prop-set: { 168 | content: var(--custom-prop); 169 | }; 170 | } 171 | 172 | .test { 173 | @apply --custom-prop-set; 174 | content: var(--custom-prop); 175 | }`; 176 | 177 | const expected = stripIndent` 178 | :root { 179 | --custom-prop: 'prop'; 180 | } 181 | 182 | .test { 183 | content: 'prop'; 184 | content: var(--custom-prop); 185 | content: 'prop'; 186 | content: var(--custom-prop); 187 | }`; 188 | 189 | const result = await postcss() 190 | .use(customProperties) 191 | .use(plugin) 192 | .process(input, { from: undefined }); 193 | 194 | expect(result.css).toBe(expected); 195 | }); 196 | 197 | test('custom properties nested with plugin last', async () => { 198 | const input = stripIndent` 199 | :root { 200 | --custom-prop: 'prop'; 201 | --custom-prop-set: { 202 | content: var(--custom-prop); 203 | }; 204 | } 205 | 206 | .test { 207 | @apply --custom-prop-set; 208 | content: var(--custom-prop); 209 | }`; 210 | 211 | const expected = stripIndent` 212 | :root { 213 | --custom-prop: 'prop'; 214 | } 215 | 216 | .test { 217 | content: 'prop'; 218 | content: var(--custom-prop); 219 | content: 'prop'; 220 | content: var(--custom-prop); 221 | }`; 222 | 223 | const result = await postcss() 224 | .use(plugin) 225 | .use(customProperties) 226 | .process(input, { from: undefined }); 227 | 228 | expect(result.css).toBe(expected); 229 | }); 230 | 231 | test('custom properties nested with plugin first [preserve: false]', async () => { 232 | const input = stripIndent` 233 | :root { 234 | --custom-prop: 'prop'; 235 | --custom-prop-set: { 236 | content: var(--custom-prop); 237 | }; 238 | } 239 | 240 | .test { 241 | @apply --custom-prop-set; 242 | content: var(--custom-prop); 243 | }`; 244 | 245 | const expected = stripIndent` 246 | .test { 247 | content: 'prop'; 248 | content: 'prop'; 249 | }`; 250 | 251 | const result = await postcss() 252 | .use(customProperties({ preserve: false })) 253 | .use(plugin) 254 | .process(input, { from: undefined }); 255 | 256 | expect(result.css).toBe(expected); 257 | }); 258 | 259 | test('custom properties nested with plugin last [preserve: false]', async () => { 260 | const input = stripIndent` 261 | :root { 262 | --custom-prop: 'prop'; 263 | --custom-prop-set: { 264 | content: var(--custom-prop); 265 | }; 266 | } 267 | 268 | .test { 269 | @apply --custom-prop-set; 270 | content: var(--custom-prop); 271 | }`; 272 | 273 | const expected = stripIndent` 274 | .test { 275 | content: 'prop'; 276 | content: 'prop'; 277 | }`; 278 | 279 | const result = await postcss() 280 | .use(plugin) 281 | .use(customProperties({ preserve: false })) 282 | .process(input, { from: undefined }); 283 | 284 | expect(result.css).toBe(expected); 285 | }); 286 | }); 287 | -------------------------------------------------------------------------------- /test/prepend.spec.js: -------------------------------------------------------------------------------- 1 | import postcss from 'postcss'; 2 | import { stripIndent } from 'common-tags'; 3 | import plugin from '../src'; 4 | 5 | describe('prepend', () => { 6 | describe('should prepend sets from options', () => { 7 | it('from an object set', async () => { 8 | const input = stripIndent` 9 | .dummy { 10 | @apply --justatest; 11 | } 12 | `; 13 | 14 | const sets = { 15 | justatest: { 16 | padding: '0 1rem', 17 | fontSize: '1.4rem', 18 | color: 'tomato', 19 | }, 20 | }; 21 | 22 | const result = await postcss() 23 | .use(plugin({ sets })) 24 | .process(input, { from: undefined }); 25 | 26 | expect(result.css).toMatchSnapshot(); 27 | }); 28 | 29 | it('from a string set', async () => { 30 | const input = stripIndent` 31 | .dummy { 32 | @apply --justatest; 33 | } 34 | `; 35 | 36 | const sets = { 37 | justatest: ` 38 | fontSize: 1.4rem; 39 | 40 | @media (width >= 500px) { 41 | fontSize: 2.4rem; 42 | } 43 | `, 44 | }; 45 | 46 | const result = await postcss() 47 | .use(plugin({ sets })) 48 | .process(input, { from: undefined }); 49 | 50 | expect(result.css).toMatchSnapshot(); 51 | }); 52 | 53 | it('throws if the set is not an object or a string', async () => { 54 | const input = stripIndent` 55 | .dummy { 56 | @apply --justatest; 57 | } 58 | `; 59 | 60 | const sets = { 61 | justatest: () => {}, 62 | }; 63 | 64 | expect(() => { 65 | postcss() // eslint-disable-line no-unused-expressions 66 | .use(plugin({ sets })) 67 | .process(input, { from: undefined }).css; 68 | }).toThrow( 69 | 'Unrecognized set type `function`, must be an object or string.' 70 | ); 71 | }); 72 | }); 73 | 74 | it('should override sets from options with CSS declared ones', async () => { 75 | const input = stripIndent` 76 | :root { 77 | --justatest: { 78 | padding: none; 79 | color: orangeRed; 80 | } 81 | } 82 | 83 | .dummy { 84 | @apply --justatest; 85 | } 86 | `; 87 | 88 | const sets = { 89 | justatest: { 90 | padding: '0 1rem', 91 | fontSize: '1.4rem', 92 | color: 'tomato', 93 | }, 94 | }; 95 | 96 | const result = await postcss() 97 | .use(plugin({ sets })) 98 | .process(input, { from: undefined }); 99 | 100 | expect(result.css).toMatchSnapshot(); 101 | }); 102 | 103 | it('should be able to be nested inside a CSS declared set', async () => { 104 | const input = stripIndent` 105 | :root { 106 | --from-css: { 107 | @apply --from-js; 108 | } 109 | } 110 | 111 | .dummy { 112 | @apply --from-css; 113 | } 114 | `; 115 | 116 | const sets = { 117 | 'from-js': { 118 | color: 'tomato', 119 | }, 120 | }; 121 | 122 | const result = await postcss() 123 | .use(plugin({ sets })) 124 | .process(input, { from: undefined }); 125 | 126 | expect(result.css).toMatchSnapshot(); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /test/preserve.spec.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import postcss from 'postcss'; 4 | import plugin from '../src'; 5 | 6 | const read = name => 7 | fs.readFileSync(path.join(__dirname, 'fixture', name), 'utf8'); 8 | 9 | describe('the `preserve` option', () => { 10 | it('should properly apply and preserve custom property sets', async () => { 11 | const input = read('preserve/input.css'); 12 | const expected = read('preserve/expected.css'); 13 | 14 | const result = await postcss() 15 | .use(plugin({ preserve: true })) 16 | .process(input, { from: undefined }); 17 | 18 | expect(result.css).toBe(expected); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/utils.spec.js: -------------------------------------------------------------------------------- 1 | import { kebabify, isPlainObject } from '../src/utils'; 2 | 3 | describe('kekabify', () => { 4 | it('should convert camelCase properties to kebab-case', () => { 5 | const input = [ 6 | 'borderRadius', 7 | 'backgroundColor', 8 | 'background-color', 9 | 'fontSize', 10 | 'font-size', 11 | 'borderTopLeftRadius', 12 | '-moz-Whatever', 13 | '-webkit-whoCares', 14 | '--customProp', 15 | '--CustomProp', 16 | '--custom-prop', 17 | ]; 18 | 19 | const result = input.map(kebabify); 20 | 21 | expect(result).toMatchSnapshot(); 22 | }); 23 | }); 24 | 25 | describe('isPlainObject', () => { 26 | it('should assert for plain object types', () => { 27 | expect(isPlainObject({})).toBe(true); 28 | expect(isPlainObject([])).toBe(false); 29 | expect(isPlainObject(null)).toBe(false); 30 | expect(isPlainObject('')).toBe(false); 31 | }); 32 | }); 33 | --------------------------------------------------------------------------------