├── .babelrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── css ├── test.css └── themes │ └── default │ ├── base-styles.css │ └── colors.css └── plugin.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["es2015"] } 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | - "4" 5 | - "0.12" 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | ## 1.0.0 5 | 6 | - Initial release 7 | - Theming using theme() method 8 | - Adds '.css' suffix if not already provided (e.g. `theme(colors)` => `theme(colors.css)`) 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Andy Wermke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-theme - Proper theming for PostCSS 2 | [![Build Status](https://travis-ci.org/andywer/postcss-theme.svg?branch=master)](https://travis-ci.org/andywer/postcss-theme) 3 | [![NPM Version](https://img.shields.io/npm/v/postcss-theme.svg)](https://www.npmjs.com/package/postcss-theme) 4 | 5 | Super lightweight, straight-forward and written with performance in mind. 6 | Can be used with Webpack, JSPM/System.js or anywhere else where you use 7 | PostCSS! 8 | 9 | ## What is it able to do? 10 | 11 | You have a user interface and a bunch of CSS files / fancy CSS modules 12 | to style it. You want to be able to customize this styling. Let's say you have 13 | this CSS file: 14 | 15 | ```css 16 | /* header.css */ 17 | 18 | @value black, white from '../theme/colors.css'; 19 | 20 | .header { 21 | composes: menu from '../theme/menu.css'; 22 | background: black; 23 | color: white; 24 | } 25 | 26 | 27 | /* ../theme/colors.css */ 28 | 29 | @value black: #303030; 30 | @value white: #F8F8F8; 31 | 32 | 33 | /* ../theme/components/menu.css */ 34 | 35 | .menu { 36 | box-shadow: 2px 2px 5px; 37 | } 38 | ``` 39 | 40 | We are using the [postcss-modules-values](https://github.com/css-modules/postcss-modules-values) 41 | plugin here, so we can declare variables and import variables from other files 42 | using `@value`. 43 | And we use [postcss-modules-extract-imports](https://github.com/css-modules/postcss-modules-extract-imports) 44 | so we can merge classes from different files into the current class using 45 | `composes: some-other-class from './other-file.css'`. 46 | 47 | *But you want to be able to change the styling!* You could just overwrite all these 48 | style rules with your own ones, but that is a lot of work and you must adapt it 49 | everytime these rules change. 50 | 51 | So we use **postcss-theme** and do this: 52 | 53 | ```css 54 | /* header.css */ 55 | 56 | /* `theme(colors)` will be re-written to `"./path/to/theme/colors.css"` */ 57 | @value black, white from theme(colors); 58 | 59 | .header { 60 | /* will be resolved to the file path, too */ 61 | composes: menu from theme(components/menu); 62 | background: black; 63 | color: white; 64 | } 65 | ``` 66 | 67 | When configuring the PostCSS plugins in your webpack config or JSPM CSS loader: 68 | 69 | ```javascript 70 | import themePlugin from 'postcss-theme' 71 | 72 | /* postcss plugins: */ 73 | [ 74 | themePlugin({ themePath: './path/to/theme-folder' }), 75 | /* all other plugins go here */ 76 | ] 77 | ``` 78 | 79 | Ta-da! You are now able to specify the path to the directory containing your 80 | theme's CSS files during your build process. Just change it to a directory 81 | containing another theme if you want to change the styling. 82 | 83 | 84 | ## Installation 85 | 86 | ```bash 87 | npm install postcss-theme --save 88 | ``` 89 | 90 | ## Usage 91 | 92 | Just add this plugin to your array of PostCSS plugins and pass it an options 93 | object like `{ themePath: './path/to/theme-folder' }`. 94 | 95 | ### Webpack 96 | 97 | In your webpack config: 98 | 99 | ```javascript 100 | import theme from 'postcss-theme' 101 | 102 | module.exports = { 103 | module: { 104 | loaders: [ 105 | { 106 | test: /\.css$/, 107 | loader: 'style-loader!css-loader!postcss-loader' 108 | } 109 | ] 110 | }, 111 | postcss: function () { 112 | return [ 113 | theme({ themePath: './path/to/theme' }), 114 | // all other postcss plugins go here 115 | ] 116 | } 117 | } 118 | ``` 119 | 120 | ### JSPM (jspm-loader-css) 121 | 122 | In your css loader file (`css.js`): 123 | 124 | ```javascript 125 | import { CSSLoader, Plugins } from 'jspm-loader-css' 126 | import theme from 'postcss-theme' 127 | 128 | const { fetch, bundle } = new CSSLoader([ 129 | theme({ themePath: './path/to/theme' }), 130 | Plugins.localByDefault, 131 | Plugins.extractImports, 132 | Plugins.scope, 133 | Plugins.values, 134 | // or any other postcss plugins 135 | ], __moduleName) 136 | 137 | export { fetch, bundle } 138 | ``` 139 | 140 | ### Vanilla postcss call 141 | 142 | ```javascript 143 | import postcss from 'postcss' 144 | import autoprefixer from 'autoprefixer' 145 | import extractImports from 'postcss-modules-extract-imports' 146 | // ... 147 | import theme from 'postcss-theme' 148 | 149 | postcss([ 150 | theme({ 151 | themePath: './path/to/theme' 152 | }), 153 | extractImports, 154 | autoprefixer, 155 | // or whatever plugins you would like to use 156 | ]).process(/* ... */) 157 | ``` 158 | 159 | 160 | ## Changelog 161 | 162 | ### Version 1.1.1 163 | 164 | Pass `css.source` to custom file path resolvers. 165 | 166 | ### Version 1.1.0 167 | 168 | Allow passing a custom file path resolver function (`options.themeFileResolver`). 169 | 170 | ### Version 1.0.1 171 | 172 | Windows fix. See [this issue](https://github.com/andywer/postcss-theme/issues/1). 173 | 174 | ### Version 1.0.0 175 | 176 | Initial release. 177 | 178 | 179 | ## License 180 | 181 | This plugin is released under the terms of the MIT license. See [LICENSE](https://github.com/andywer/postcss-theme/blob/master/LICENSE) for details. 182 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var postcss = require('postcss') 4 | var valueParser = require('postcss-value-parser') 5 | 6 | var THEME_FUNCTION_NAME = 'theme' 7 | 8 | /** 9 | * Transforms the CSS: Replaces theme() statements by actual file paths. 10 | * @param {string} value 11 | * @param {function} themeFileResolver 12 | * @return {string} The transformed `value` param. 13 | */ 14 | function transform (value, themeFileResolver) { 15 | // Exit condition to improve performance 16 | if (value.indexOf(THEME_FUNCTION_NAME) === -1) { return value } 17 | 18 | return valueParser(value).walk(function (node) { 19 | if (node.type === 'function' && node.value === THEME_FUNCTION_NAME) { 20 | node.type = 'string' 21 | node.quote = '"' 22 | node.value = themeFileResolver(valueParser.stringify(node.nodes)) 23 | } 24 | }, true).toString() 25 | } 26 | 27 | // Like path.join(), but will always use '/' as separator, even on Windows. 28 | // See https://github.com/andywer/postcss-theme/issues/1 29 | function joinPaths (path1, path2) { 30 | return path1 + (path1.match(/\/$/) ? '' : '/') + path2 31 | } 32 | 33 | /** 34 | * Default theme file path resolver. Takes a path and the plugin options, 35 | * returns the transformed path. 36 | * 37 | * @param {string} themeFilePath As found in the CSS statement `theme()`. 38 | * @param {object} options 39 | * @return {string} Transformed themeFilePath. 40 | */ 41 | function defaultThemeFileResolver (themeFilePath, options) { 42 | if (!options || !options.themePath) { 43 | throw new Error('No theme path set.') 44 | } 45 | 46 | if (!themeFilePath.match(/\.css$/i)) { 47 | themeFilePath += '.css' 48 | } 49 | 50 | return joinPaths(options.themePath, themeFilePath) 51 | } 52 | 53 | /** 54 | * Plugin definition. 55 | * 56 | * @param {object} options { themePath: String, themeFileResolver: Function } 57 | */ 58 | module.exports = postcss.plugin('postcss-theme', function (options) { 59 | options = options || {} 60 | 61 | return function (css) { 62 | // proxy the resolver call, bind the 2nd and 3rd parameter 63 | var themeFileResolver = function (themeFilePath) { 64 | var resolver = options.themeFileResolver || defaultThemeFileResolver 65 | return resolver(themeFilePath, options, defaultThemeFileResolver, css.source) 66 | } 67 | 68 | css.walk(function (node) { 69 | if (node.type === 'decl') { 70 | node.value = transform(node.value, themeFileResolver) 71 | } else if (node.type === 'atrule') { 72 | node.params = transform(node.params, themeFileResolver) 73 | } 74 | }) 75 | } 76 | }) 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-theme", 3 | "version": "1.1.1", 4 | "description": "PostCSS plugin to enable versatile theming.", 5 | "keywords": [ "postcss", "theming", "theme", "plugin" ], 6 | "registry": "npm", 7 | "main": "index.js", 8 | "scripts": { 9 | "prepush": "npm test", 10 | "test": "standard index.js && mocha --compilers js:babel-register ./test/**/*.spec.js && node index.js" 11 | }, 12 | "author": "Andy Wermke ", 13 | "license": "MIT", 14 | "dependencies": { 15 | "postcss": "^5.0.19", 16 | "postcss-value-parser": "^3.3.0" 17 | }, 18 | "devDependencies": { 19 | "babel-preset-es2015": "^6.6.0", 20 | "babel-register": "^6.7.2", 21 | "chai": "^3.5.0", 22 | "husky": "^0.11.4", 23 | "mocha": "^2.4.5", 24 | "standard": "^6.0.8" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/andywer/postcss-theme.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/andywer/postcss-theme/issues" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/css/test.css: -------------------------------------------------------------------------------- 1 | @value black, white from theme(colors); 2 | 3 | .test-1 { 4 | composes: no-borders from theme(base-styles); 5 | color: black; 6 | } 7 | 8 | .test-2 { 9 | color: white; 10 | } 11 | -------------------------------------------------------------------------------- /test/css/themes/default/base-styles.css: -------------------------------------------------------------------------------- 1 | .no-borders { 2 | border: none; 3 | } 4 | -------------------------------------------------------------------------------- /test/css/themes/default/colors.css: -------------------------------------------------------------------------------- 1 | @value white: #F8F8F8; 2 | @value black: #303030; 3 | -------------------------------------------------------------------------------- /test/plugin.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import fs from 'fs' 4 | import path from 'path' 5 | 6 | import { expect } from 'chai' 7 | import postcss from 'postcss' 8 | 9 | import themePlugin from '../index' 10 | 11 | const CSS_INPUT_FILE = path.join(__dirname, 'css/test.css') 12 | 13 | function normalizeString (string) { 14 | return string.split('\n').map(line => line.trim()).join('') 15 | } 16 | 17 | function realpath (filePath) { 18 | return fs.realpathSync(path.join(__dirname, filePath)) 19 | } 20 | 21 | describe('postcss-theme', () => { 22 | let cssOutput 23 | 24 | it('runs successfully', () => { 25 | return postcss([ 26 | themePlugin({ 27 | themePath: __dirname + '/css/themes/default' 28 | }) 29 | ]) 30 | .process(fs.readFileSync(CSS_INPUT_FILE), { from: CSS_INPUT_FILE, to: path.basename(CSS_INPUT_FILE) }) 31 | .then(function (result) { 32 | cssOutput = result.css 33 | }) 34 | }) 35 | 36 | it('produces the expected output', () => { 37 | expect(normalizeString(cssOutput)).to.equal(normalizeString(` 38 | @value black, white from "${realpath('./css/themes/default/colors.css')}"; 39 | 40 | .test-1 { 41 | composes: no-borders from "${realpath('./css/themes/default/base-styles.css')}"; 42 | color: black; 43 | } 44 | 45 | .test-2 { 46 | color: white; 47 | } 48 | `)) 49 | }) 50 | 51 | it('uses a given themeFileResolver method', () => { 52 | function themeFileResolver (themeFilePath, options, defaultResolver, source) { 53 | expect(source.input.file).to.equal(CSS_INPUT_FILE) 54 | 55 | const themePath = __dirname + '/css/themes/default' 56 | return defaultResolver(themeFilePath + options.suffix, { themePath }) 57 | } 58 | 59 | return postcss([ 60 | themePlugin({ themeFileResolver, suffix: '-test' }) 61 | ]) 62 | .process(fs.readFileSync(CSS_INPUT_FILE), { from: CSS_INPUT_FILE, to: path.basename(CSS_INPUT_FILE) }) 63 | .then(function (result) { 64 | const firstOutputLine = result.css.split('\n')[ 0 ] 65 | const expectedPath = realpath('./css/themes/default') + '/colors-test.css' 66 | 67 | expect(firstOutputLine).to.equal(`@value black, white from "${expectedPath}";`) 68 | }) 69 | }) 70 | }) 71 | --------------------------------------------------------------------------------