├── .gitignore ├── Makefile ├── README.md ├── Styling.js ├── browser.js ├── examples ├── extract │ ├── .gitignore │ ├── Button.js │ ├── Button.style.js │ ├── README.md │ ├── Theme.js │ ├── package.json │ └── webpack.config.js └── simple │ ├── .gitignore │ ├── Button.js │ ├── Button.style.js │ ├── README.md │ ├── Theme.js │ ├── package.json │ └── webpack.config.js ├── index.js ├── loader.js ├── omit.js ├── package.json ├── renderStyling.js └── renderStylingSheet.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DELETE_ON_ERROR: 2 | 3 | version-major version-minor version-patch: 4 | @npm version $(@:version-%=%) 5 | 6 | publish: 7 | @git push --tags origin HEAD:master 8 | @npm publish 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Styling 2 | ======= 3 | 4 | Styling is the [Webpack][] based tool to write component styles with the full 5 | power of JavaScript: 6 | ```js 7 | import styling from 'styling' 8 | import {baseColor} from './theme' 9 | 10 | export let button = styling({ 11 | backgroundColor: baseColor 12 | }) 13 | ``` 14 | Why 15 | --- 16 | 17 | * Modules, variables, functions, all of these works out of the box because you 18 | use JavaScript. 19 | 20 | * Rich ecosystem of ready to use [npm][] packages: for example you can use 21 | [color][] for color manipulation. 22 | 23 | * Compatability with the existent CSS tools such as [autoprefixer][] and a ton 24 | of other [PostCSS][] transforms. 25 | 26 | * Compatability with the existent JS tools such as compile-to-js languages 27 | (CoffeeScript, TypeScript), type checkers (FlowType), linters (ESLint) and 28 | others. 29 | 30 | How 31 | --- 32 | 33 | Styling is implemented as a [Webpack][] loader which executes JavaScript code to 34 | produce *styling* objects. 35 | 36 | Each styling object is then converted to a [CSS module][] and passed further to 37 | Webpack CSS processing pipeline (usually css-loader and style-loader). 38 | 39 | Consuming styling styles is no different than consuming a CSS module: you get a 40 | mapping of CSS class names which can be used to style your components. 41 | 42 | Limitations 43 | ----------- 44 | 45 | You should still keep your UI code and your stylesheet code separate as 46 | stylesheet code executes during bundling and doesn't have any runtime 47 | representation. 48 | 49 | Installation 50 | ------------ 51 | 52 | Install from [npm][]: 53 | ```bash 54 | % npm install styling 55 | ``` 56 | Usage 57 | ----- 58 | 59 | Add the following configuration to `webpack.config.js`: 60 | ```js 61 | var styling = require('styling') 62 | 63 | module.exports = { 64 | module: { 65 | loaders: [ 66 | { 67 | test: /\.style\.js/, 68 | loader: styling( 69 | ['style', 'css'], // loaders to execute after styling 70 | ['babel'] // loaders to execute before styling 71 | ) 72 | } 73 | ] 74 | } 75 | } 76 | ``` 77 | Function `styling` configures loader and accepts two arguments, one for 78 | *postloaders* and one for *preloaders*. 79 | 80 | Now you can write styles with the full power of JavaScript, `Button.style.js`: 81 | ```js 82 | import styling from 'styling' 83 | 84 | export let self = styling({ 85 | backgroundColor: 'red', 86 | borderWidth: 1 + 10, 87 | 88 | hover: { 89 | borderWidth: 100 90 | } 91 | }) 92 | ``` 93 | And consume them, `Button.js`: 94 | ```js 95 | import ButtonStyle from './Button.style' 96 | 97 | export function render() { 98 | return `` 99 | } 100 | ``` 101 | Usage with Extract Text Webpack plugin 102 | -------------------------------------- 103 | 104 | Styling is compatible with [extract-text-webpack-plugin][] so you can have your 105 | styles extracted into a separate CSS bundle by Webpack. This is how you 106 | configure it to do so: 107 | ```js 108 | var styling = require('styling') 109 | var ExtractTextWebpackPlugin = require('extract-text-webpack-plugin') 110 | 111 | module.exports = { 112 | module: { 113 | loaders: [ 114 | { 115 | test: /\.style\.js/, 116 | loader: styling(ExtractTextWebpackPlugin.extract('style', 'css'), 'babel') 117 | } 118 | ] 119 | }, 120 | 121 | plugins: [ 122 | new ExtractTextWebpackPlugin('bundle.css') 123 | ] 124 | } 125 | ``` 126 | [npm]: http://npmjs.org 127 | [Webpack]: http://webpack.github.io/ 128 | [extract-text-webpack-plugin]: https://github.com/webpack/extract-text-webpack-plugin 129 | [color]: https://www.npmjs.com/package/color 130 | [CSS module]: https://github.com/css-modules/css-modules 131 | [autoprefixer]: https://github.com/postcss/autoprefixer 132 | [PostCSS]: http://postcss.parts/ 133 | -------------------------------------------------------------------------------- /Styling.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2015, Andrey Popp 3 | */ 4 | 5 | var KEY = '@@styling'; 6 | 7 | function Styling(spec) { 8 | this[KEY] = spec; 9 | this.rules = spec; 10 | } 11 | 12 | Styling.prototype.getSpec = function Styling_getStyle() { 13 | return this[KEY]; 14 | } 15 | 16 | Styling.is = function Styling_is(obj) { 17 | return obj && obj[KEY] !== undefined; 18 | } 19 | 20 | module.exports = Styling; 21 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2015, Andrey Popp 3 | */ 4 | 5 | var Styling = require('./Styling'); 6 | 7 | function styling(spec) { 8 | return new Styling(spec); 9 | } 10 | 11 | module.exports = styling; 12 | -------------------------------------------------------------------------------- /examples/extract/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /examples/extract/Button.js: -------------------------------------------------------------------------------- 1 | import Style from './Button.style' 2 | 3 | export default class Button extends React.Component { 4 | 5 | render() { 6 | return ( 7 | 13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/extract/Button.style.js: -------------------------------------------------------------------------------- 1 | import styling from 'styling' 2 | import {bgColor} from './Theme' 3 | 4 | export let self = styling({ 5 | background: bgColor 6 | }) 7 | 8 | export let icon = styling({ 9 | padding: 10 + 5 10 | }) 11 | 12 | export let caption = styling({ 13 | fontWeight: 'bold' 14 | }) 15 | -------------------------------------------------------------------------------- /examples/extract/README.md: -------------------------------------------------------------------------------- 1 | styling-example-extract 2 | ======================= 3 | 4 | This is an example of using styling to write component styles which are then 5 | extracted into a separate CSS chunk. 6 | 7 | Build: 8 | 9 | % npm install . 10 | % npm run webpack 11 | 12 | Project description: 13 | 14 | ├── webpack.config.js Webpack config 15 | ├── Button.js Component 16 | ├── Button.style.js Component styles 17 | └── Theme.js Theme constants (used by styles) 18 | 19 | Build output desciption: 20 | 21 | build/ 22 | ├── bundle.css Built styles 23 | └── bundle.js Built code 24 | -------------------------------------------------------------------------------- /examples/extract/Theme.js: -------------------------------------------------------------------------------- 1 | export let bgColor = 'red' 2 | -------------------------------------------------------------------------------- /examples/extract/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "styling-example-simple", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "webpack": "webpack" 6 | }, 7 | "dependencies": { 8 | "styling": "../../", 9 | "babel-loader": "^5.3.2", 10 | "css-loader": "^0.16.0", 11 | "extract-text-webpack-plugin": "^0.8.2", 12 | "style-loader": "^0.12.3", 13 | "webpack": "^1.11.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/extract/webpack.config.js: -------------------------------------------------------------------------------- 1 | var styling = require('styling'); 2 | var ExtractTextWebpackPlugin = require('extract-text-webpack-plugin'); 3 | 4 | module.exports = { 5 | 6 | entry: './Button', 7 | 8 | output: { 9 | path: './build', 10 | filename: 'bundle.js' 11 | }, 12 | 13 | module: { 14 | loaders: [ 15 | { 16 | include: /\.style\.js$/, 17 | loader: styling(ExtractTextWebpackPlugin.extract('style', 'css?modules'), 'babel') 18 | }, 19 | { 20 | include: /\.js$/, 21 | loader: 'babel' 22 | } 23 | ] 24 | }, 25 | 26 | plugins: [ 27 | new ExtractTextWebpackPlugin('bundle.css') 28 | ] 29 | }; 30 | -------------------------------------------------------------------------------- /examples/simple/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /examples/simple/Button.js: -------------------------------------------------------------------------------- 1 | import Style from './Button.style' 2 | 3 | export default class Button { 4 | 5 | render() { 6 | return ( 7 | 13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/simple/Button.style.js: -------------------------------------------------------------------------------- 1 | import styling from 'styling' 2 | import {bgColor} from './Theme' 3 | 4 | export let self = styling({ 5 | background: bgColor 6 | }) 7 | 8 | export let icon = styling({ 9 | padding: 10 + 5 10 | }) 11 | 12 | export let caption = styling({ 13 | fontWeight: 'bold' 14 | }) 15 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | styling-example-simple 2 | ====================== 3 | 4 | This is an example of using styling to write component styles. 5 | 6 | Build: 7 | 8 | % npm install . 9 | % npm run webpack 10 | 11 | Project description: 12 | 13 | ├── webpack.config.js Webpack config 14 | ├── Button.js Component 15 | ├── Button.style.js Component styles 16 | └── Theme.js Theme constants (used by styles) 17 | 18 | Build output desciption: 19 | 20 | build/ 21 | └── bundle.js Built bundle (including code and styles) 22 | -------------------------------------------------------------------------------- /examples/simple/Theme.js: -------------------------------------------------------------------------------- 1 | export let bgColor = 'red' 2 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "styling-example-simple", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "webpack": "webpack" 6 | }, 7 | "dependencies": { 8 | "styling": "../../", 9 | "babel-loader": "^5.3.2", 10 | "css-loader": "^0.16.0", 11 | "style-loader": "^0.12.3", 12 | "webpack": "^1.11.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/simple/webpack.config.js: -------------------------------------------------------------------------------- 1 | var styling = require('styling'); 2 | 3 | module.exports = { 4 | 5 | entry: './Button', 6 | 7 | output: { 8 | path: './build', 9 | filename: 'bundle.js' 10 | }, 11 | 12 | module: { 13 | loaders: [ 14 | { 15 | include: /\.style\.js$/, 16 | loader: styling(['style', 'css'], ['babel']) 17 | }, 18 | { 19 | include: /\.js$/, 20 | loader: 'babel' 21 | } 22 | ] 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2015, Andrey Popp 3 | */ 4 | 5 | var omitRequest = require.resolve('./omit'); 6 | var loaderRequest = require.resolve('./loader'); 7 | 8 | module.exports = function styling(post, pre) { 9 | if (Array.isArray(post)) { 10 | post = post.join('!'); 11 | } 12 | if (Array.isArray(pre)) { 13 | pre = pre.join('!'); 14 | } 15 | return [omitRequest, post, loaderRequest, pre || ''].join('!'); 16 | } 17 | -------------------------------------------------------------------------------- /loader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2015, Andrey Popp 3 | */ 4 | 5 | var path = require('path'); 6 | var Module = require('module'); 7 | var Styling = require('./Styling'); 8 | var renderStylingSheet = require('./renderStylingSheet'); 9 | 10 | var NodeTemplatePlugin = require('webpack/lib/node/NodeTemplatePlugin'); 11 | var NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin'); 12 | var LibraryTemplatePlugin = require('webpack/lib/LibraryTemplatePlugin'); 13 | var SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin'); 14 | var LimitChunkCountPlugin = require('webpack/lib/optimize/LimitChunkCountPlugin'); 15 | 16 | var renderStylingSheetMod = require.resolve('./renderStylingSheet'); 17 | 18 | var extractTextWebpackPluginKey; 19 | try { 20 | extractTextWebpackPluginKey = path.dirname(require.resolve('extract-text-webpack-plugin')); 21 | } catch (error) {} 22 | 23 | module.exports = function styling(content) { 24 | this.cacheable(); 25 | if (this[__dirname] === false) { 26 | return ''; 27 | } else if (typeof this[extractTextWebpackPluginKey] === 'function') { 28 | return ''; 29 | } else if (this[extractTextWebpackPluginKey] === false) { 30 | var request = this.request.split('!').slice(this.loaderIndex + 1).join('!'); 31 | produce(this, request, this.async()); 32 | } else { 33 | return ''; 34 | } 35 | }; 36 | 37 | module.exports.pitch = function stylingPitch(request, precedingRequest, data) { 38 | this.cacheable(); 39 | if (this[__dirname] === false) { 40 | // if we already inside the loader 41 | return; 42 | } else if (extractTextWebpackPluginKey in this) { 43 | // if extract-text-webpack-plugin is active we do all work in a loader phase 44 | return; 45 | } else { 46 | produce(this, request, this.async()); 47 | } 48 | }; 49 | 50 | function produce(loader, request, callback) { 51 | var outputFilename = "styling-output-filename"; 52 | var outputOptions = {filename: outputFilename}; 53 | var childCompiler = getRootCompilation(loader).createChildCompiler("styling-compiler", outputOptions); 54 | childCompiler.apply(new NodeTemplatePlugin(outputOptions)); 55 | childCompiler.apply(new LibraryTemplatePlugin(null, "commonjs2")); 56 | childCompiler.apply(new NodeTargetPlugin()); 57 | childCompiler.apply(new SingleEntryPlugin(loader.context, "!!" + request)); 58 | childCompiler.apply(new LimitChunkCountPlugin({ maxChunks: 1 })); 59 | 60 | var subCache = "subcache " + __dirname + " " + request; 61 | 62 | childCompiler.plugin("compilation", function(compilation) { 63 | if (compilation.cache) { 64 | if(!compilation.cache[subCache]) { 65 | compilation.cache[subCache] = {}; 66 | } 67 | compilation.cache = compilation.cache[subCache]; 68 | } 69 | }); 70 | 71 | // We set loaderContext[__dirname] = false to indicate we already in 72 | // a child compiler so we don't spawn another child compilers from there. 73 | childCompiler.plugin("this-compilation", function(compilation) { 74 | compilation.plugin("normal-module-loader", function(loaderContext) { 75 | loaderContext[__dirname] = false; 76 | if (extractTextWebpackPluginKey in loader) { 77 | loaderContext[extractTextWebpackPluginKey] = loader[extractTextWebpackPluginKey]; 78 | } 79 | }); 80 | }); 81 | 82 | var source; 83 | childCompiler.plugin("after-compile", function(compilation, callback) { 84 | source = compilation.assets[outputFilename] && compilation.assets[outputFilename].source(); 85 | 86 | // Remove all chunk assets 87 | compilation.chunks.forEach(function(chunk) { 88 | chunk.files.forEach(function(file) { 89 | delete compilation.assets[file]; 90 | }); 91 | }); 92 | 93 | callback(); 94 | }); 95 | 96 | childCompiler.runAsChild(function(error, entries, compilation) { 97 | if (error) { 98 | return callback(error); 99 | } 100 | if (compilation.errors.length > 0) { 101 | return callback(compilation.errors[0]); 102 | } 103 | if (!source) { 104 | return callback(new Error("Didn't get a result from child compiler")); 105 | } 106 | compilation.fileDependencies.forEach(function(dep) { 107 | loader.addDependency(dep); 108 | }); 109 | compilation.contextDependencies.forEach(function(dep) { 110 | loader.addContextDependency(dep); 111 | }); 112 | try { 113 | var exports = loader.exec(source, request); 114 | var text = renderStylingSheet(exports); 115 | } catch (e) { 116 | return callback(e); 117 | } 118 | if (text) { 119 | callback(null, text); 120 | } else { 121 | callback(); 122 | } 123 | }); 124 | } 125 | 126 | function getRootCompilation(loader) { 127 | var compiler = loader._compiler; 128 | var compilation = loader._compilation; 129 | while (compiler.parentCompilation) { 130 | compilation = compiler.parentCompilation; 131 | compiler = compilation.compiler; 132 | } 133 | return compilation; 134 | } 135 | -------------------------------------------------------------------------------- /omit.js: -------------------------------------------------------------------------------- 1 | var loaderPath = require.resolve('./loader'); 2 | 3 | module.exports = function(content) { 4 | this.cacheable(); 5 | return content; 6 | } 7 | 8 | module.exports.pitch = function(request) { 9 | if (this[__dirname] === false) { 10 | request = request.split('!'); 11 | while (request.length > 1) { 12 | var req = request.shift(); 13 | if (req === loaderPath) { 14 | break; 15 | } 16 | } 17 | request = request.join('!'); 18 | return 'module.exports = require(' + JSON.stringify('!!' + request) + ');'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "styling", 3 | "version": "0.4.1", 4 | "description": "Style components with JavaScript", 5 | "main": "index.js", 6 | "browser": "browser.js", 7 | "webpackLoader": "loader.js", 8 | "author": "Andrey Popp <8mayday@gmail.com>", 9 | "license": "MIT", 10 | "repository": "andreypopp/styling", 11 | "keywords": [ 12 | "webpack", 13 | "webpack-loader", 14 | "css-modules", 15 | "css", 16 | "style" 17 | ], 18 | "dependencies": {} 19 | } 20 | -------------------------------------------------------------------------------- /renderStyling.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2013-2015, Facebook, Inc. 3 | * @copyright 2015, Andrey Popp 4 | */ 5 | 6 | function renderStyling(key, styling) { 7 | var stylesheet = []; 8 | var spec = styling.getSpec(); 9 | if (typeof spec === 'string') { 10 | stylesheet.push(spec); 11 | } else { 12 | var self = {}; 13 | for (var prop in spec) { 14 | if (spec.hasOwnProperty(prop)) { 15 | var value = spec[prop]; 16 | value = valueOf(value); 17 | if (value && typeof value === 'object' && !Array.isArray(value)) { 18 | stylesheet.push(renderStyle(key, prop, value)); 19 | } else { 20 | self[prop] = value; 21 | } 22 | } 23 | } 24 | stylesheet.unshift(renderStyle(key, null, self)); 25 | } 26 | return stylesheet; 27 | } 28 | 29 | function renderStyle(name, state, style) { 30 | var css = ''; 31 | css += renderSelector(name, state) + ' {\n'; 32 | for (var prop in style) { 33 | if (style.hasOwnProperty(prop)) { 34 | var value = valueOf(style[prop]); 35 | if (Array.isArray(value)) { 36 | for (var i = 0; i < value.length; i++) { 37 | css += ' ' + renderProp(prop, value[i]) + '\n'; 38 | } 39 | } else { 40 | css += ' ' + renderProp(prop, value) + '\n'; 41 | } 42 | } 43 | } 44 | css += '}'; 45 | return css; 46 | } 47 | 48 | function renderProp(key, value) { 49 | value = valueOf(value); 50 | 51 | var isNonNumeric = isNaN(value); 52 | if (isNonNumeric || value === 0 || 53 | IS_UNITLESS_NUMBER.hasOwnProperty(key) && IS_UNITLESS_NUMBER[key]) { 54 | value = '' + value; 55 | } else { 56 | value = value + 'px'; 57 | } 58 | key = key.replace(CAMEL_CASE_TO_DASH_CASE, '$1-$2').toLowerCase(); 59 | return key + ': ' + value + ';'; 60 | } 61 | 62 | function renderSelector(name, state) { 63 | return ':local(.' + name + (state ? ':' + state : '') + ')'; 64 | } 65 | 66 | function valueOf(value) { 67 | if (value != null) { 68 | return value.valueOf(); 69 | } else { 70 | return value; 71 | } 72 | } 73 | 74 | var CAMEL_CASE_TO_DASH_CASE = /([a-z]|^)([A-Z])/g; 75 | 76 | var IS_UNITLESS_NUMBER = { 77 | boxFlex: true, 78 | boxFlexGroup: true, 79 | columnCount: true, 80 | flex: true, 81 | flexGrow: true, 82 | flexPositive: true, 83 | flexShrink: true, 84 | flexNegative: true, 85 | fontWeight: true, 86 | lineClamp: true, 87 | lineHeight: true, 88 | opacity: true, 89 | order: true, 90 | orphans: true, 91 | tabSize: true, 92 | widows: true, 93 | zIndex: true, 94 | zoom: true, 95 | 96 | // SVG-related properties 97 | fillOpacity: true, 98 | strokeDashoffset: true, 99 | strokeOpacity: true, 100 | strokeWidth: true, 101 | }; 102 | 103 | module.exports = renderStyling; 104 | -------------------------------------------------------------------------------- /renderStylingSheet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2015, Andrey Popp 3 | */ 4 | 5 | var Styling = require('./Styling'); 6 | var renderStyling = require('./renderStyling'); 7 | 8 | function renderStylingSheet(sheet) { 9 | var stylesheet = []; 10 | for (var key in sheet) { 11 | if (sheet.hasOwnProperty(key)) { 12 | var styling = sheet[key]; 13 | if (Styling.is(styling)) { 14 | stylesheet = stylesheet.concat(renderStyling(key, styling)); 15 | } 16 | } 17 | } 18 | return stylesheet.join('\n\n'); 19 | } 20 | 21 | module.exports = renderStylingSheet; 22 | --------------------------------------------------------------------------------