├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── README.md ├── __tests__ ├── __snapshots__ │ └── parser.test.js.snap ├── parser.test.js ├── selector.test.js ├── stylesheet.test.js └── supports.test.js ├── docs ├── README.md └── api.md ├── examples ├── animation.js ├── fontFace.js ├── media.js ├── mixins.js ├── selectors.js ├── supports.js └── variables.js ├── images ├── One.png ├── Two.png └── stylusreact.png ├── package.json ├── public ├── App.js ├── FiraCode-Retina.ttf ├── Laugh.ttf └── index.html ├── src ├── index.js ├── main │ └── createStylusComponent.js ├── parse │ ├── fontFace.js │ ├── getRootSelector.js │ ├── keyframes.js │ ├── parse.js │ └── selectors.js ├── rules │ ├── basicRules.js │ ├── mediaRules.js │ └── supportsRules.js └── utils │ ├── checkCode.js │ ├── processStylusCode.js │ ├── ruleTypes.js │ └── stylesheet.js ├── todos.md ├── utils └── testParser.js ├── webpack ├── webpack.config.dev.js └── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react-app" 4 | ] 5 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb', 3 | parser: 'babel-eslint', 4 | rules: { 5 | semi: 0, 6 | 'arrow-parens': 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | public -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | notifications: 5 | email: false 6 | script: 7 | - npm run test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stylus-in-react 2 | [![Build Status](https://travis-ci.org/nitin42/stylus-in-react.svg?branch=master)](https://travis-ci.org/nitin42/stylus-in-react) 3 | ![status](https://camo.githubusercontent.com/ea5cfca68ba7fb5b41078a9c4ccd38aae38ead4a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7374617475732d737461626c652d627269676874677265656e2e737667) 4 | ![size](https://img.shields.io/badge/size-16.2%20KB-brightgreen.svg) 5 | ![yarn version](https://camo.githubusercontent.com/957c1b2ba7212e71149d922a8d10d067f2d66758/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7961726e2d302e32312e332d626c75652e737667) 6 | ![license](https://camo.githubusercontent.com/743d6ca437fec2ad80985c1208501b7c7b4b97ae/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f646f637472696e652f6f726d2e737667) 7 | 8 |

9 | 10 |

11 | 12 | ## Documentation 13 | 14 | You can find the detailed documentation [here](./docs). 15 | 16 | ## License 17 | 18 | MIT 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/parser.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`stylus custom parser basic selector 1`] = ` 4 | Object { 5 | "element": "button", 6 | "hash": Object { 7 | "data-css-1tpbuej": "", 8 | }, 9 | "stylesheet": Array [ 10 | Object { 11 | "borderRadius": "4px", 12 | }, 13 | Object { 14 | "border": "2px solid #808080", 15 | }, 16 | Object { 17 | "color": "#ffc0cb", 18 | }, 19 | Object { 20 | ":hover": Object { 21 | "color": "#800080", 22 | }, 23 | }, 24 | ], 25 | } 26 | `; 27 | 28 | exports[`stylus custom parser works with @block 1`] = ` 29 | Object { 30 | "element": "div", 31 | "hash": Object { 32 | "data-css-16uf9cy": "", 33 | }, 34 | "stylesheet": Array [ 35 | Object { 36 | "width": "20px", 37 | }, 38 | Object { 39 | "height": "20px", 40 | }, 41 | ], 42 | } 43 | `; 44 | 45 | exports[`stylus custom parser works with built-in functions 1`] = ` 46 | Object { 47 | "element": "button", 48 | "hash": Object { 49 | "data-css-1xlcwgb": "", 50 | }, 51 | "stylesheet": Array [ 52 | Object { 53 | "color": "#0f0", 54 | }, 55 | Object { 56 | "backgroundColor": "#fff", 57 | }, 58 | ], 59 | } 60 | `; 61 | 62 | exports[`stylus custom parser works with char escaping 1`] = ` 63 | Object { 64 | "element": "div", 65 | "hash": Object { 66 | "data-css-fjjr1s": "", 67 | }, 68 | "stylesheet": Array [ 69 | Object { 70 | "padding": "3px", 71 | }, 72 | ], 73 | } 74 | `; 75 | 76 | exports[`stylus custom parser works with conditionals 1`] = ` 77 | Object { 78 | "element": "div", 79 | "hash": Object { 80 | "data-css-1xgqhfw": "", 81 | }, 82 | "stylesheet": Array [ 83 | Object { 84 | "margin": "5px 10px", 85 | }, 86 | ], 87 | } 88 | `; 89 | 90 | exports[`stylus custom parser works with functions 1`] = ` 91 | Object { 92 | "element": "button", 93 | "hash": Object { 94 | "data-css-1r0s7ik": "", 95 | }, 96 | "stylesheet": Array [ 97 | Object { 98 | "padding": "30px", 99 | }, 100 | ], 101 | } 102 | `; 103 | 104 | exports[`stylus custom parser works with interpolations 1`] = ` 105 | Object { 106 | "element": "button", 107 | "hash": Object { 108 | "data-css-llmc5k": "", 109 | }, 110 | "stylesheet": Array [ 111 | Object { 112 | "borderRadius": "1px 2px/3px 4px", 113 | }, 114 | ], 115 | } 116 | `; 117 | 118 | exports[`stylus custom parser works with mixins 1`] = ` 119 | Object { 120 | "element": "button", 121 | "hash": Object { 122 | "data-css-wy342g": "", 123 | }, 124 | "stylesheet": Array [ 125 | Object { 126 | "color": "#f00", 127 | }, 128 | Object { 129 | "borderRadius": "3px", 130 | }, 131 | ], 132 | } 133 | `; 134 | 135 | exports[`stylus custom parser works with operators (usage with mixins) 1`] = ` 136 | Object { 137 | "element": "body", 138 | "hash": Object { 139 | "data-css-tn55bb": "", 140 | }, 141 | "stylesheet": Array [ 142 | Object { 143 | "padding": "5px", 144 | }, 145 | ], 146 | } 147 | `; 148 | 149 | exports[`stylus custom parser works with pseudo selectors 1`] = ` 150 | Object { 151 | "element": "button", 152 | "hash": Object { 153 | "data-css-drp6v7": "", 154 | }, 155 | "stylesheet": Array [ 156 | Object { 157 | ":focus": Object { 158 | "outline": "none", 159 | }, 160 | }, 161 | Object { 162 | ":hover": Object { 163 | "color": "#ffc0cb", 164 | }, 165 | }, 166 | Object { 167 | ":active": Object { 168 | "outline": "none", 169 | }, 170 | }, 171 | ], 172 | } 173 | `; 174 | 175 | exports[`stylus custom parser works with rest params 1`] = ` 176 | Object { 177 | "element": "div", 178 | "hash": Object { 179 | "data-css-10ag0uy": "", 180 | }, 181 | "stylesheet": Array [ 182 | Object { 183 | "boxShadow": "1px 2px 5px #eee", 184 | }, 185 | ], 186 | } 187 | `; 188 | -------------------------------------------------------------------------------- /__tests__/parser.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | 4 | import parser from '../utils/testParser' 5 | 6 | describe('stylus custom parser', () => { 7 | test('sanity check', () => { 8 | const DIV = ` 9 | button 10 | color mistyrose 11 | border-radius 3px 12 | ` 13 | 14 | const data = parser(DIV) 15 | 16 | expect(data).toMatchObject({ 17 | element: 'button', 18 | hash: { 'data-css-1j9g9kt': '' }, 19 | stylesheet: [{ color: '#ffe4e1' }, { borderRadius: '3px' }] 20 | }) 21 | }) 22 | 23 | test('basic selector', () => { 24 | const BUTTON = ` 25 | button 26 | border-radius 4px 27 | border 2px solid grey 28 | color pink 29 | &:hover 30 | color purple 31 | ` 32 | 33 | const data = parser(BUTTON) 34 | 35 | expect(data).toMatchSnapshot() 36 | }) 37 | 38 | test('pythonic nature i.e gives errors on wrong indentation', () => { 39 | const BUTTON = ` 40 | button 41 | border-radius 4px 42 | ` 43 | 44 | try { 45 | const data = parser(BUTTON) 46 | } catch (e) { 47 | expect(e.message).toBe('Indentation error') 48 | } 49 | }) 50 | 51 | test('works with functions', () => { 52 | const BUTTON = ` 53 | plus(a, b) 54 | a + b 55 | 56 | button 57 | padding plus(10px, 20px) 58 | ` 59 | 60 | const data = parser(BUTTON) 61 | 62 | expect(data).toMatchSnapshot() 63 | expect(data.stylesheet[0].padding).toBe('30px') 64 | }) 65 | 66 | test('works with pseudo selectors', () => { 67 | const BUTTON = ` 68 | button 69 | &:focus 70 | outline none 71 | &:hover 72 | color pink 73 | &:active 74 | outline none 75 | ` 76 | 77 | const data = parser(BUTTON) 78 | 79 | expect(data).toMatchSnapshot() 80 | }) 81 | 82 | test('works with mixins', () => { 83 | const BUTTON = ` 84 | border-radius(n) 85 | border-radius n 86 | 87 | button 88 | color red 89 | border-radius(3px) 90 | ` 91 | 92 | const data = parser(BUTTON) 93 | 94 | expect(data).toMatchSnapshot() 95 | }) 96 | 97 | test('works with built-in functions', () => { 98 | const BUTTON = ` 99 | button 100 | color green(#000, 255) 101 | background-color lighten(mistyrose, 30) 102 | ` 103 | 104 | const data = parser(BUTTON) 105 | 106 | expect(data).toMatchSnapshot() 107 | }) 108 | 109 | test('works with rest params', () => { 110 | const DIV = ` 111 | box-shadow(args ...) 112 | box-shadow args 113 | 114 | div 115 | box-shadow 1px 2px 5px #eee 116 | ` 117 | 118 | const data = parser(DIV) 119 | 120 | expect(data).toMatchSnapshot() 121 | }) 122 | 123 | test('works with @block', () => { 124 | const DIV = ` 125 | foo = @block { 126 | width: 20px 127 | height: 20px 128 | } 129 | 130 | div 131 | {foo} 132 | ` 133 | 134 | const data = parser(DIV) 135 | 136 | expect(data).toMatchSnapshot() 137 | }) 138 | 139 | test('throws error on invalid element', () => { 140 | const DIV = ` 141 | d 142 | padding 10px 143 | ` 144 | 145 | try { 146 | const data = parser(DIV) 147 | } catch (e) { 148 | expect(e.message).toBe("'d' is not a valid HTML element.") 149 | } 150 | }) 151 | 152 | test('works with conditionals', () => { 153 | const DIV = ` 154 | overload-padding = true 155 | 156 | if overload-padding 157 | padding(y, x) 158 | margin y x 159 | 160 | div 161 | padding 5px 10px 162 | ` 163 | 164 | const data = parser(DIV) 165 | 166 | expect(data).toMatchSnapshot() 167 | }) 168 | 169 | test('works with char escaping', () => { 170 | const DIV = ` 171 | div 172 | padding (1 \+ 2)px 173 | ` 174 | 175 | const data = parser(DIV) 176 | 177 | expect(data).toMatchSnapshot() 178 | }) 179 | 180 | test('works with operators (usage with mixins)', () => { 181 | const DIV = ` 182 | pad(types = padding, n = 5px) 183 | if padding in types 184 | padding n 185 | if margin in types 186 | margin n 187 | 188 | body 189 | pad() 190 | ` 191 | 192 | const data = parser(DIV) 193 | 194 | expect(data).toMatchSnapshot() 195 | }) 196 | 197 | test('works with interpolations', () => { 198 | const BUTTON = ` 199 | apply(prop, args) 200 | {prop} args 201 | 202 | border-radius() 203 | apply('border-radius', arguments) 204 | 205 | box-shadow() 206 | apply('box-shadow', arguments) 207 | 208 | button 209 | border-radius 1px 2px / 3px 4px 210 | ` 211 | 212 | const data = parser(BUTTON) 213 | 214 | expect(data).toMatchSnapshot() 215 | }) 216 | }) 217 | -------------------------------------------------------------------------------- /__tests__/selector.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | 4 | import getParentNode from '../src/parse/selectors' 5 | 6 | describe('Selector', () => { 7 | test('returns a root selector', () => { 8 | const selectors = ['button:hover'] 9 | 10 | expect(getParentNode(selectors)).toBe('button') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /__tests__/stylesheet.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | import cssParser from 'css' 4 | import stylus from 'stylus' 5 | 6 | import createStylesheet from '../src/utils/stylesheet' 7 | import getParentNode from '../src/parse/selectors' 8 | 9 | const Example = ` 10 | button 11 | color pink 12 | border 3px solid mistyrose 13 | border-radius 4px 14 | &:hover 15 | color lighten(purple, 20) 16 | background-color mistyrose 17 | ` 18 | 19 | describe('Create stylesheet', () => { 20 | test('generates an array of styles', () => { 21 | let rules, selectors, element, AST 22 | 23 | stylus.render(Example, (err, css) => { 24 | AST = cssParser.parse(css, { source: 'css' }) 25 | rules = AST.stylesheet.rules 26 | selectors = 27 | AST.stylesheet.rules[0] !== undefined 28 | ? AST.stylesheet.rules[0].selectors 29 | : null 30 | element = getParentNode(selectors) 31 | }) 32 | 33 | const styles = createStylesheet(rules, element) 34 | 35 | const output = [ 36 | { color: '#ffc0cb' }, 37 | { border: '3px solid #ffe4e1' }, 38 | { borderRadius: '4px' }, 39 | { ':hover': { color: '#e600e6' } }, 40 | { ':hover': { backgroundColor: '#ffe4e1' } } 41 | ] 42 | 43 | expect(styles).toEqual(output) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /__tests__/supports.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | 4 | import parser from '../utils/testParser' 5 | 6 | describe('@support rules', () => { 7 | test('Example one', () => { 8 | const DIV = ` 9 | @supports (display: flex) 10 | div 11 | display flex 12 | ` 13 | 14 | const data = parser(DIV) 15 | 16 | expect(data).toEqual({ 17 | element: 'div', 18 | hash: { 'data-css-18ahxhe': '' }, 19 | stylesheet: [ 20 | [ 21 | { 22 | display: [ 23 | '-webkit-box', 24 | '-moz-box', 25 | '-ms-flexbox', 26 | '-webkit-flex', 27 | 'flex' 28 | ] 29 | } 30 | ] 31 | ] 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Docs 2 | 3 | ## Table of content 4 | 5 | * [Introduction](#introduction) 6 | * [Install](#install) 7 | * [Demo](#demo) 8 | * [Examples](#examples) 9 | * [Supported Features](#supported-features) 10 | * [API Reference](./api.md) 11 | 12 | ### Introduction 13 | 14 | I assume you know what [Stylus]() is and how it can be used in an expressive way to generate CSS. With stylus-in-react, you can directly style your React components with Stylus. 15 | 16 | I created this because Stylus has a lot of great features like transparent mixins, built-in functions, in-language functions and [many other features](http://stylus-lang.com/#features). 17 | 18 | ### Install 19 | 20 | ``` 21 | npm install stylus-in-react 22 | ``` 23 | 24 | This also depends on React so make sure you've already installed it! 25 | 26 | After you're done with the installation, place this in your index.html 27 | 28 | > IMPORTANT - This is required for client side usage of Stylus 29 | 30 | ``` 31 | 32 | ``` 33 | 34 | ### Demo 35 | 36 | ![demo](http://g.recordit.co/H0PWjG7kMT.gif) 37 | 38 | ### Examples 39 | 40 | You can find all the examples [here](../examples). 41 | 42 | ### Supported features 43 | 44 | * [selectors](http://stylus-lang.com/docs/selectors.html) 45 | * [variables](http://stylus-lang.com/docs/variables.html) 46 | * [interpolation](http://stylus-lang.com/docs/interpolations.html) 47 | * [operators](http://stylus-lang.com/docs/operators.html) 48 | * [mixins](http://stylus-lang.com/docs/mixins.html) 49 | * [functions](http://stylus-lang.com/docs/functions.html) 50 | * [keyword arguments](http://stylus-lang.com/docs/kwargs.html) 51 | * [built-in functions](http://stylus-lang.com/docs/bifs.html) 52 | * [rest params](http://stylus-lang.com/docs/vargs.html) 53 | * [conditionals](http://stylus-lang.com/docs/conditionals.html) 54 | * [@media queries](http://stylus-lang.com/docs/media.html) 55 | * [@font-face](http://stylus-lang.com/docs/font-face.html) 56 | * [@keyframes](http://stylus-lang.com/docs/keyframes.html) 57 | * [@Block](http://stylus-lang.com/docs/block.html) 58 | * [@supports](http://stylus-lang.com/docs/supports.html) 59 | * [char escaping](http://stylus-lang.com/docs/escape.html) 60 | * [iterations](http://stylus-lang.com/docs/iterations.html) 61 | * [hashes](http://stylus-lang.com/docs/hashes.html) 62 | 63 | ### API Reference 64 | 65 | You can the complete api reference [here](./api.md). 66 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ## stylus(code, { displayName }) 4 | 5 | Creates a stylus component with the stylus `code` and accepts an optional argument `displayName`. 6 | 7 | **Example** 8 | 9 | ```js 10 | const Button = stylus( 11 | ` 12 | button-styles(radius, size, color, type) 13 | border-radius radius 14 | border size type color 15 | color mistyrose 16 | background-color white 17 | 18 | button 19 | button-styles(3px, 2px, mistyrose, solid) 20 | `, 21 | { displayName: 'ButtonComponent' } 22 | ); 23 | ``` 24 | 25 |

26 | 27 |

28 | 29 | ## fontFace(code) 30 | 31 | Loads the given font-face and returns the font family name 32 | 33 | **Example** 34 | 35 | ```js 36 | const Fira = fontFace(` 37 | @font-face 38 | font-family FiraCode 39 | font-style normalize 40 | src url(./FiraCode-Retina.ttf) 41 | `); 42 | 43 | const Name = stylus(` 44 | h2 45 | font-family ${Fira} 46 | `, { displayName: 'Name' }); 47 | ``` 48 | 49 | ## keyframes(code) 50 | 51 | loads the animation keyframes and returns an animation name 52 | 53 | **Example** 54 | 55 | ```js 56 | const fadeIn = keyframes(` 57 | animation-name = fadeIn 58 | 59 | @keyframes animation-name 60 | for i in 0..10 61 | {10% * i} 62 | opacity (i/10) 63 | `); 64 | 65 | const FadeInButton = stylus( 66 | ` 67 | button 68 | animation ${fadeIn} 4s ease-out 69 | `, 70 | { displayName: "FadeInButton" } 71 | ); 72 | ``` 73 | 74 |

75 | 76 |

-------------------------------------------------------------------------------- /examples/animation.js: -------------------------------------------------------------------------------- 1 | const { keyframes, stylus } = require('../src/') 2 | 3 | const fadeIn = keyframes(` 4 | animation-name = fadeIn 5 | 6 | @keyframes animation-name 7 | for i in 0..10 8 | {10% * i} 9 | opacity (i/10) 10 | `) 11 | 12 | const FadeInButton = stylus( 13 | ` 14 | button 15 | animation ${fadeIn} 4s ease-out 16 | `, 17 | { displayName: 'FadeInButton' } 18 | ) 19 | 20 | module.exports = FadeInButton 21 | -------------------------------------------------------------------------------- /examples/fontFace.js: -------------------------------------------------------------------------------- 1 | const { fontFace, stylus } = require('../src/') 2 | 3 | const Fira = fontFace(` 4 | @font-face 5 | font-family FiraCode 6 | font-style normalize 7 | src url(./FiraCode-Retina.ttf) 8 | `) 9 | 10 | const Name = stylus( 11 | ` 12 | h2 13 | font-family ${Fira} 14 | `, 15 | { displayName: 'Name' } 16 | ) 17 | 18 | module.exports = Name 19 | -------------------------------------------------------------------------------- /examples/media.js: -------------------------------------------------------------------------------- 1 | const { stylus } = require('../src/') 2 | 3 | const ButtonMedia = stylus( 4 | ` 5 | button 6 | color red 7 | padding 10px 8 | @media screen and (min-width: 400px) 9 | padding 15px 10 | margin 10px 11 | `, 12 | { displayName: 'ButtonMedia' } 13 | ) 14 | 15 | module.exports = ButtonMedia 16 | -------------------------------------------------------------------------------- /examples/mixins.js: -------------------------------------------------------------------------------- 1 | const { stylus } = require('../src/') 2 | 3 | const ButtonMixin = stylus( 4 | ` 5 | button-styles(radius, size, color, type) 6 | border-radius radius 7 | border size type color 8 | color mistyrose 9 | background-color white 10 | 11 | button 12 | button-styles(3px, 2px, mistyrose, solid) 13 | `, 14 | { displayName: 'MixinButton' } 15 | ) 16 | 17 | module.exports = ButtonMixin 18 | -------------------------------------------------------------------------------- /examples/selectors.js: -------------------------------------------------------------------------------- 1 | const { stylus } = require('../src/') 2 | 3 | const Button = stylus( 4 | ` 5 | button 6 | color mistyrose 7 | background-color white 8 | border-radius 3px 9 | border 2px solid mistyrose 10 | &:hover 11 | color black 12 | background-color mistyrose 13 | &:focus 14 | outline none 15 | > * 16 | padding 10px 17 | `, 18 | { displayName: 'Button' } 19 | ) 20 | 21 | module.exports = Button 22 | -------------------------------------------------------------------------------- /examples/supports.js: -------------------------------------------------------------------------------- 1 | const { stylus } = require('../src/') 2 | 3 | const ButtonFloat = stylus( 4 | ` 5 | @supports not (display: flex) 6 | button 7 | float: right 8 | padding: 5px 9 | `, 10 | { displayName: 'FloatButton' } 11 | ) 12 | 13 | module.exports = ButtonFloat 14 | -------------------------------------------------------------------------------- /examples/variables.js: -------------------------------------------------------------------------------- 1 | const { stylus } = require('../src/') 2 | 3 | const ButtonVar = stylus( 4 | ` 5 | size = 12px 6 | 7 | button 8 | font-size size 9 | `, 10 | { displayName: 'ButtonVar' } 11 | ) 12 | 13 | module.exports = ButtonVar 14 | -------------------------------------------------------------------------------- /images/One.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/stylus-in-react/56ac8ec4e0cd026f6278588b4ba059abd815688c/images/One.png -------------------------------------------------------------------------------- /images/Two.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/stylus-in-react/56ac8ec4e0cd026f6278588b4ba059abd815688c/images/Two.png -------------------------------------------------------------------------------- /images/stylusreact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/stylus-in-react/56ac8ec4e0cd026f6278588b4ba059abd815688c/images/stylusreact.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stylus-in-react", 3 | "version": "2.0.0", 4 | "description": "Style React components with Stylus", 5 | "main": "build/main.js", 6 | "author": "Nitin Tulswani", 7 | "license": "MIT", 8 | "dependencies": { 9 | "camel-case": "^3.0.0", 10 | "common-tags": "^1.4.0", 11 | "css": "^2.2.1", 12 | "glamor": "^2.20.40", 13 | "html-tags": "^2.0.0", 14 | "inline-style-prefixer": "^3.0.7" 15 | }, 16 | "peerDependencies": { 17 | "react": "^16.0.0" 18 | }, 19 | "repository": { 20 | "url": "https://github.com/nitin42/stylus-in-react", 21 | "type": "git" 22 | }, 23 | "files": [ 24 | "build" 25 | ], 26 | "keywords": [ 27 | "Stylus", 28 | "React", 29 | "css-in-js", 30 | "css", 31 | "parse css" 32 | ], 33 | "devDependencies": { 34 | "babel-eslint": "^8.0.0", 35 | "babel-loader": "^7.1.2", 36 | "babel-preset-react-app": "^3.0.2", 37 | "compression-webpack-plugin": "^1.0.0", 38 | "eslint": "^4.6.1", 39 | "eslint-config-airbnb": "^15.1.0", 40 | "eslint-plugin-import": "^2.7.0", 41 | "eslint-plugin-jsx-a11y": "^5.1.1", 42 | "eslint-plugin-react": "^7.3.0", 43 | "husky": "^0.14.3", 44 | "jest": "^21.0.2", 45 | "jest-cli": "^21.0.2", 46 | "lint-staged": "^4.2.1", 47 | "prop-types": "^15.5.10", 48 | "react": "^15.6.1", 49 | "react-dom": "^15.6.1", 50 | "react-test-renderer": "^15.6.1", 51 | "stylus": "^0.54.5", 52 | "webpack": "^3.5.6", 53 | "webpack-dev-server": "^2.7.1" 54 | }, 55 | "scripts": { 56 | "validate": "yarn test && yarn lint", 57 | "precommit": "lint-staged", 58 | "test": "./node_modules/.bin/jest", 59 | "lint": "./node_modules/.bin/eslint ./src/", 60 | "start": "NODE_ENV=development ./node_modules/.bin/webpack-dev-server --content-base ./public --config ./webpack/webpack.config.dev.js --hot", 61 | "prebuild": "rm -rf ./build", 62 | "build": "NODE_ENV=production ./node_modules/.bin/webpack --config ./webpack/webpack.config.js --progress" 63 | }, 64 | "lint-staged": { 65 | "*.js": [ 66 | "prettier --single-quote --no-semi --write", 67 | "git add" 68 | ] 69 | }, 70 | "jest": { 71 | "testEnvironment": "node" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /public/App.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const { render } = require('react-dom') 3 | 4 | // Stylus component examples 5 | // These are few examples but you can do pretty much everything you used to do with Stylus 6 | const Button = require('../examples/selectors') 7 | const ButtonMixin = require('../examples/mixins') 8 | const ButtonVar = require('../examples/variables') 9 | const ButtonMedia = require('../examples/media') 10 | const FadeInButton = require('../examples/animation') 11 | const ButtonFloat = require('../examples/supports') 12 | const Name = require('../examples/fontFace') 13 | 14 | class App extends React.Component { 15 | render() { 16 | return Submit 17 | } 18 | } 19 | 20 | render(, document.getElementById('app')) 21 | -------------------------------------------------------------------------------- /public/FiraCode-Retina.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/stylus-in-react/56ac8ec4e0cd026f6278588b4ba059abd815688c/public/FiraCode-Retina.ttf -------------------------------------------------------------------------------- /public/Laugh.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/stylus-in-react/56ac8ec4e0cd026f6278588b4ba059abd815688c/public/Laugh.ttf -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React App 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const stylus = require('./main/createStylusComponent') 2 | const keyframes = require('./parse/keyframes') 3 | const fontFace = require('./parse/fontFace') 4 | 5 | module.exports = { 6 | stylus, 7 | keyframes, 8 | fontFace 9 | } 10 | -------------------------------------------------------------------------------- /src/main/createStylusComponent.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const processStylusCode = require('../utils/processStylusCode') 3 | const parseStylus = require('../parse/parse') 4 | const checkStylusCode = require('../utils/checkCode') 5 | 6 | /** 7 | * This function takes the stylus code, parses it and creates an object of css rules. 8 | * These rules are then passed to glamor's css method and are inserted into the stylesheet 9 | * @param {string} stylusCode 10 | */ 11 | function createStylusComponent(stylusCode, { displayName } = {}) { 12 | checkStylusCode(stylusCode, 'stylus()') 13 | 14 | const { hash, element } = parseStylus(processStylusCode(stylusCode)) 15 | 16 | /* eslint-disable react/prefer-stateless-function */ 17 | class StylusComponent extends React.Component { 18 | render() { 19 | return React.createElement( 20 | element, 21 | { 22 | className: `${hash}`, 23 | ...this.props 24 | }, 25 | /* eslint-disable react/prop-types */ 26 | this.props.children 27 | ) 28 | } 29 | } 30 | 31 | StylusComponent.displayName = 32 | displayName && typeof displayName === 'string' ? displayName : element 33 | 34 | return StylusComponent 35 | } 36 | 37 | module.exports = createStylusComponent 38 | -------------------------------------------------------------------------------- /src/parse/fontFace.js: -------------------------------------------------------------------------------- 1 | const glamor = require('glamor') 2 | const parser = require('css') 3 | const camelCase = require('camel-case') 4 | const processStylusCode = require('../utils/processStylusCode') 5 | const checkStylusCode = require('../utils/checkCode') 6 | 7 | /** 8 | * This function creates an object of @font-face rule declaration (property: value) 9 | * @param { array } rule 10 | */ 11 | function getDeclaration(rule) { 12 | const store = {} 13 | 14 | rule.declarations.forEach(decl => { 15 | Object.assign(store, { [camelCase(decl.property)]: decl.value }) 16 | }) 17 | 18 | return store 19 | } 20 | 21 | /** 22 | * This function returns an object of @font-face rules 23 | * @param { array } rules @font-face rules 24 | */ 25 | function getFontFaceProps(rules) { 26 | const store = {} 27 | 28 | rules.forEach(rule => { 29 | Object.assign(store, getDeclaration(rule)) 30 | }) 31 | 32 | return store 33 | } 34 | 35 | /** 36 | * This function returns the font-family name 37 | * @param { string } stylusCode Stylus code 38 | */ 39 | function fontFace(stylusCode) { 40 | checkStylusCode(stylusCode, 'fontFace()') 41 | 42 | let AST 43 | let rules 44 | let atFontFaceRules 45 | 46 | /* eslint-disable no-undef */ 47 | window.stylus.render( 48 | processStylusCode(stylusCode), 49 | { filename: 'source.css' }, 50 | (err, css) => { 51 | if (err) { 52 | throw new Error(err) 53 | } 54 | 55 | // CSS ast 56 | AST = parser.parse(css, { source: 'css' }) 57 | 58 | if (AST.stylesheet.rules[0].type !== 'font-face') { 59 | throw new Error('Not a @font-face rule.') 60 | } 61 | 62 | // @font-face rules 63 | rules = AST.stylesheet.rules 64 | 65 | // get the font-family name 66 | atFontFaceRules = getFontFaceProps(rules) 67 | } 68 | ) 69 | 70 | return glamor.css.fontFace(atFontFaceRules) 71 | } 72 | 73 | module.exports = fontFace 74 | -------------------------------------------------------------------------------- /src/parse/getRootSelector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * These are required for some checks against the types in AST 3 | */ 4 | const typesWhichRequireSelector = ['media', 'supports', 'import'] 5 | 6 | /** 7 | * This function returns the root selector required to create a React component 8 | * @param { Object } AST CSS 9 | */ 10 | function getRootSelector(AST) { 11 | const type = 12 | AST.stylesheet.rules[0] && AST.stylesheet.rules[0].type 13 | ? AST.stylesheet.rules[0].type 14 | : '' 15 | 16 | if (!typesWhichRequireSelector.includes(type)) { 17 | // Root selector rule 18 | return AST.stylesheet.rules[0].selectors 19 | } 20 | 21 | // alternate rules (@rules, :selector, contextual selectors and nested selectors) 22 | return AST.stylesheet.rules[0].rules[0].selectors 23 | } 24 | 25 | module.exports = getRootSelector 26 | -------------------------------------------------------------------------------- /src/parse/keyframes.js: -------------------------------------------------------------------------------- 1 | const glamor = require('glamor') 2 | const parser = require('css') 3 | const camelCase = require('camel-case') 4 | const processStylusCode = require('../utils/processStylusCode') 5 | const checkStylusCode = require('../utils/checkCode') 6 | 7 | /** 8 | * This function returns an object of a @keyframes rule 9 | * @param { object } keyframe rule 10 | */ 11 | function getProps(keyframe) { 12 | const props = {} 13 | 14 | keyframe.declarations.forEach(declaration => { 15 | Object.assign(props, { 16 | [camelCase(declaration.property)]: declaration.value 17 | }) 18 | }) 19 | 20 | return props 21 | } 22 | 23 | /** 24 | * This function stores each keyframe rule 25 | * @param { array } rule 26 | */ 27 | function assignRule(rule) { 28 | const store = {} 29 | 30 | rule.keyframes.forEach(keyframe => { 31 | Object.assign(store, { [keyframe.values[0]]: getProps(keyframe) }) 32 | }) 33 | 34 | return store 35 | } 36 | 37 | /** 38 | * This function returns an object of @keyframes rules 39 | * @param { array } rules keyframe rules 40 | */ 41 | function getKeyframeProps(rules) { 42 | const store = {} 43 | 44 | rules.forEach(rule => { 45 | Object.assign(store, assignRule(rule)) 46 | }) 47 | 48 | return store 49 | } 50 | 51 | /** 52 | * This function return an animation name (name_someHash) 53 | * @param { string } stylusCode Stylus code 54 | */ 55 | function keyframes(stylusCode) { 56 | checkStylusCode(stylusCode, 'keyframes()') 57 | 58 | let AST 59 | let name 60 | let rules 61 | let AtKeyframesRules 62 | 63 | /* eslint-disable no-undef */ 64 | window.stylus.render( 65 | processStylusCode(stylusCode), 66 | { filename: 'source.css' }, 67 | (err, css) => { 68 | if (err) { 69 | throw new Error(err) 70 | } 71 | 72 | AST = parser.parse(css, { source: 'css' }) 73 | 74 | if (AST.stylesheet.rules[0].type !== 'keyframes') { 75 | throw new Error('Not a keyframe rule.') 76 | } 77 | 78 | // Animation name 79 | name = AST.stylesheet.rules[0].name 80 | 81 | // @keyframes rules 82 | rules = AST.stylesheet.rules 83 | 84 | // @keyframes rules object 85 | AtKeyframesRules = getKeyframeProps(rules) 86 | } 87 | ) 88 | 89 | return glamor.css.keyframes(name, AtKeyframesRules) 90 | } 91 | 92 | module.exports = keyframes 93 | -------------------------------------------------------------------------------- /src/parse/parse.js: -------------------------------------------------------------------------------- 1 | const parser = require('css') 2 | const glamor = require('glamor') 3 | 4 | const getStylesheet = require('../utils/stylesheet') 5 | const getParentNode = require('../parse/selectors') 6 | const getRootSelector = require('../parse/getRootSelector') 7 | 8 | /** 9 | * This function takes the stylus code and parses it. It returns an object containing information 10 | * about the root element, stylesheet and css hash name generated by glamor. 11 | * @param { string } stylusCode 12 | */ 13 | function parseStylus(stylusCode) { 14 | let AST 15 | let rules 16 | let stylesheet 17 | let hash 18 | let selector 19 | let element 20 | 21 | /* eslint-disable no-undef */ 22 | window.stylus.render(stylusCode, { filename: 'source.css' }, (err, css) => { 23 | // throws parse errors 24 | if (err) { 25 | throw new Error(err) 26 | } 27 | 28 | // Generate css AST 29 | AST = parser.parse(css, { source: 'css' }) 30 | 31 | // Get the root selector 32 | selector = getRootSelector(AST) 33 | 34 | // Set the root element 35 | element = getParentNode(selector) 36 | 37 | // Style rules 38 | rules = AST.stylesheet.rules 39 | 40 | // Create array of styles 41 | stylesheet = getStylesheet(rules, element) 42 | 43 | // Pass styles as css rules to glamor's css constructor 44 | hash = glamor.css(stylesheet) 45 | }) 46 | 47 | return { 48 | hash, 49 | element, 50 | stylesheet 51 | } 52 | } 53 | 54 | module.exports = parseStylus 55 | -------------------------------------------------------------------------------- /src/parse/selectors.js: -------------------------------------------------------------------------------- 1 | const domElements = require('html-tags') 2 | 3 | /** 4 | * This function takes a selector array and returns the root selector. 5 | * For example - If there is only one css rule, button:hover{}. So it returns 'button' as 6 | * the root element 7 | * @param { array } selector 8 | */ 9 | function getParentNode(selector) { 10 | if (selector !== null) { 11 | const selectorName = selector[0].split(':')[0] 12 | 13 | if (!domElements.includes(selectorName)) { 14 | throw new Error(`'${selectorName}' is not a valid HTML element.`) 15 | } 16 | return selectorName 17 | } 18 | 19 | return null 20 | } 21 | 22 | module.exports = getParentNode 23 | -------------------------------------------------------------------------------- /src/rules/basicRules.js: -------------------------------------------------------------------------------- 1 | const prefixAll = require('inline-style-prefixer/static') 2 | const camelCase = require('camel-case') 3 | 4 | const types = require('../utils/ruleTypes') 5 | 6 | /** 7 | * This function returns an array of all the style rules 8 | * @param { array } rules 9 | * @param { string } root 10 | */ 11 | function getRules(rules, root) { 12 | const arr = [] 13 | 14 | rules.forEach(rule => { 15 | if (!types.includes(rule.type)) { 16 | if (rule.selectors[0] === root) { 17 | rule.declarations.forEach(decl => { 18 | arr.push(prefixAll({ [camelCase(decl.property)]: decl.value })) 19 | }) 20 | } else { 21 | rule.declarations.forEach(decl => { 22 | arr.push( 23 | prefixAll({ 24 | [rule.selectors[0].replace(root, '')]: { 25 | [camelCase(decl.property)]: decl.value 26 | } 27 | }) 28 | ) 29 | }) 30 | } 31 | } 32 | }) 33 | 34 | return arr 35 | } 36 | 37 | module.exports = getRules 38 | -------------------------------------------------------------------------------- /src/rules/mediaRules.js: -------------------------------------------------------------------------------- 1 | const camelCase = require('camel-case') 2 | 3 | /** 4 | * This function creates an object of a @media rule 5 | * @param { array } rule @media rule 6 | * @param { string } root root selector 7 | */ 8 | function assignProperty(rule, root) { 9 | const styles = {} 10 | 11 | rule.forEach(i => { 12 | if (i.selectors[0] === root) { 13 | i.declarations.forEach(declaration => { 14 | Object.assign(styles, { 15 | [camelCase(declaration.property)]: declaration.value 16 | }) 17 | }) 18 | } 19 | }) 20 | 21 | return styles 22 | } 23 | 24 | /** 25 | * This function returns an array of all the @media rule 26 | * @param { array } rules @media rules 27 | * @param { string } root root selector 28 | */ 29 | function getMediaRules(rules, root) { 30 | const arr = [] 31 | 32 | rules.forEach(rule => { 33 | if (rule.type === 'media') { 34 | arr.push({ [`@media ${rule.media}`]: assignProperty(rule.rules, root) }) 35 | } 36 | }) 37 | 38 | return arr 39 | } 40 | 41 | module.exports = getMediaRules 42 | -------------------------------------------------------------------------------- /src/rules/supportsRules.js: -------------------------------------------------------------------------------- 1 | const getRules = require('./basicRules') 2 | 3 | /** 4 | * This function returns an array of objects of @supports rules 5 | * @param { array } rules @supports rules 6 | * @param { string } root root selector 7 | */ 8 | function supportRules(rules, root) { 9 | const arr = [] 10 | 11 | rules.forEach(rule => { 12 | if (rule.type !== 'supports') return 13 | arr.push(getRules(rule.rules, root)) 14 | }) 15 | 16 | return arr 17 | } 18 | 19 | module.exports = supportRules 20 | -------------------------------------------------------------------------------- /src/utils/checkCode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This function validates the string of stylus code 3 | * @param { any } code 4 | * @param { name } constructor name 5 | */ 6 | function checkStylusCode(code, name) { 7 | if (typeof code !== 'string') { 8 | throw new Error( 9 | `${name} constructor expected a stylus string. You passed ${code}` 10 | ) 11 | } 12 | } 13 | 14 | module.exports = checkStylusCode 15 | -------------------------------------------------------------------------------- /src/utils/processStylusCode.js: -------------------------------------------------------------------------------- 1 | const { stripIndent } = require('common-tags') 2 | 3 | /** 4 | * This function strips the indentation and preprocess the stylus input 5 | * @param { string } stylusCode 6 | */ 7 | function processStylusCode(stylusCode) { 8 | if (!stylusCode) return null 9 | return stripIndent`${stylusCode}` 10 | } 11 | 12 | module.exports = processStylusCode 13 | -------------------------------------------------------------------------------- /src/utils/ruleTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Keep a track of type of style rule (currently, only some of them are implemented) 3 | */ 4 | module.exports = [ 5 | 'media', 6 | 'keyframes', 7 | 'font-face', 8 | 'supports', 9 | 'page', 10 | 'namespace', 11 | 'import', 12 | 'host', 13 | 'document', 14 | 'custom-media', 15 | 'charset' 16 | ] 17 | -------------------------------------------------------------------------------- /src/utils/stylesheet.js: -------------------------------------------------------------------------------- 1 | const domElements = require('html-tags') 2 | const getRules = require('../rules/basicRules') 3 | const mediaRules = require('../rules/mediaRules') 4 | const supportRules = require('../rules/supportsRules') 5 | 6 | /** 7 | * This function creates css rules (array of style objects) 8 | * @param { array } rules css rules array 9 | * @param { string } root root element 10 | */ 11 | function getStylesheet(rules, root) { 12 | if (!domElements.includes(root)) { 13 | return {} 14 | } 15 | 16 | const arr = [] 17 | 18 | arr.push( 19 | ...getRules(rules, root), 20 | ...supportRules(rules, root), 21 | ...mediaRules(rules, root) 22 | ) 23 | return arr 24 | } 25 | 26 | module.exports = getStylesheet 27 | -------------------------------------------------------------------------------- /todos.md: -------------------------------------------------------------------------------- 1 | # Todos 2 | 3 | * Experiment with returning multiple components 4 | * Experiment with @extend 5 | * Theme component 6 | * Performance -------------------------------------------------------------------------------- /utils/testParser.js: -------------------------------------------------------------------------------- 1 | const parser = require('css') 2 | const camelCase = require('camel-case') 3 | const glamor = require('glamor') 4 | const stylus = require('stylus') 5 | 6 | const getStylesheet = require('../src/utils/stylesheet') 7 | const getParentNode = require('../src/parse/selectors') 8 | const getRootSelector = require('../src/parse/getRootSelector') 9 | 10 | /** 11 | * This function takes a stylus code and parses it. It returns an object containing information 12 | * about the root element, stylesheet and css hash name generated by glamor. 13 | * @param { string } stylusCode 14 | */ 15 | function testParser(stylusCode) { 16 | let AST, rules, stylesheet, hash, selector, element 17 | 18 | stylus.render(stylusCode, { filename: 'source.css' }, (err, css) => { 19 | // throws parse errors 20 | if (err) { 21 | throw new Error('Indentation error') 22 | } 23 | 24 | // Generate css AST 25 | AST = parser.parse(css, { source: 'css' }) 26 | // Get the root selector 27 | selector = getRootSelector(AST) 28 | // Set the root element 29 | element = getParentNode(selector) 30 | // Style rules 31 | rules = AST.stylesheet.rules 32 | // Create array of styles 33 | stylesheet = getStylesheet(rules, element) 34 | // Pass styles as css rules to glamor's css constructor 35 | hash = glamor.css(stylesheet) 36 | }) 37 | 38 | return { 39 | hash, 40 | element, 41 | stylesheet 42 | } 43 | } 44 | 45 | module.exports = testParser 46 | -------------------------------------------------------------------------------- /webpack/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | module.exports = { 5 | entry: path.resolve(__dirname, '../public/App.js'), 6 | output: { 7 | filename: '../bundle.js', 8 | path: path.resolve(__dirname), 9 | publicPath: '/' 10 | }, 11 | node: { 12 | fs: 'empty' 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.js$/, 18 | exclude: ['node_modules'], 19 | use: 'babel-loader' 20 | } 21 | ] 22 | }, 23 | plugins: [ 24 | new webpack.optimize.UglifyJsPlugin({ 25 | compress: { 26 | warnings: false, 27 | comparisons: false 28 | }, 29 | output: { 30 | comments: false, 31 | ascii_only: true 32 | } 33 | }) 34 | ], 35 | devtool: 'cheap-eval-source-maps' 36 | } 37 | -------------------------------------------------------------------------------- /webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const CompressionPlugin = require('compression-webpack-plugin') 4 | 5 | const output = () => ({ 6 | filename: '[name].js', 7 | path: path.resolve(__dirname, '../build'), 8 | publicPath: '/', 9 | library: 'stylus-in-react', 10 | libraryTarget: 'umd' 11 | }) 12 | 13 | const externals = () => ({ 14 | 'inline-style-prefixer': 'inline-style-prefixer', 15 | 'camel-case': 'camel-case', 16 | css: 'css', 17 | glamor: 'glamor', 18 | 'html-tags': 'html-tags', 19 | react: 'react', 20 | 'common-tags': 'common-tags' 21 | }) 22 | 23 | const jsLoader = () => ({ 24 | test: /\.js$/, 25 | include: path.resolve(__dirname, '../src'), 26 | exclude: ['node_modules', 'public', 'images', 'utils', 'build', '__tests__'], 27 | use: 'babel-loader' 28 | }) 29 | 30 | const plugins = () => [ 31 | new webpack.LoaderOptionsPlugin({ 32 | minimize: true, 33 | debug: false 34 | }), 35 | new webpack.DefinePlugin({ 36 | 'process.env.NODE_ENV': 'production' 37 | }), 38 | new webpack.optimize.ModuleConcatenationPlugin(), 39 | new webpack.optimize.UglifyJsPlugin({ 40 | compress: { 41 | warnings: false, 42 | comparisons: false 43 | }, 44 | output: { 45 | comments: false, 46 | ascii_only: true 47 | } 48 | }), 49 | new CompressionPlugin({ 50 | asset: '[path].gz[query]', 51 | algorithm: 'gzip', 52 | test: /\.(js)$/, 53 | threshold: 10240, 54 | minRatio: 0.8 55 | }) 56 | ] 57 | 58 | module.exports = { 59 | entry: path.resolve(__dirname, '../src/index.js'), 60 | output: output(), 61 | target: 'web', 62 | node: { 63 | fs: 'empty' 64 | }, 65 | performance: { 66 | hints: 'error' 67 | }, 68 | context: path.resolve(__dirname), 69 | devtool: 'source-map', 70 | externals: externals(), 71 | module: { 72 | rules: [jsLoader()] 73 | }, 74 | plugins: plugins() 75 | } 76 | --------------------------------------------------------------------------------