├── .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 |
--------------------------------------------------------------------------------