├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── css-to-mui-loader.js ├── css-to-mui-loader.test.js ├── package-lock.json └── package.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [`airbnb-base`, `prettier`], 8 | plugins: [`prettier`], 9 | rules: { 10 | 'arrow-body-style': [`error`, `always`], 11 | 'arrow-parens': [`error`, `always`], 12 | 'func-names': `off`, 13 | 'func-style': [`error`, `expression`, { allowArrowFunctions: true }], 14 | 'global-require': `off`, 15 | 'import/first': `error`, 16 | 'import/order': [ 17 | `error`, 18 | { 19 | 'newlines-between': `always`, 20 | groups: [ 21 | 'builtin', 22 | 'external', 23 | 'internal', 24 | 'parent', 25 | 'sibling', 26 | 'index', 27 | ], 28 | }, 29 | ], 30 | indent: [`error`, 2, { SwitchCase: 1 }], 31 | 'max-len': [ 32 | `error`, 33 | { 34 | code: 80, 35 | ignoreUrls: true, 36 | }, 37 | ], 38 | 'no-console': `error`, 39 | 'no-constant-condition': `off`, 40 | 'no-continue': `off`, 41 | 'no-param-reassign': `off`, 42 | 'no-use-before-define': `off`, 43 | 'no-restricted-syntax': `off`, 44 | 'no-throw-literal': `error`, 45 | 'object-curly-spacing': [`error`, `always`], 46 | 'object-shorthand': `off`, 47 | 'prefer-arrow-callback': `off`, 48 | 'prefer-const': `error`, 49 | 'prefer-destructuring': `off`, 50 | 'prettier/prettier': [ 51 | `error`, 52 | { 53 | arrowParens: `always`, 54 | bracketSpacing: true, 55 | jsxBracketSameLine: true, 56 | printWidth: 80, 57 | semi: true, 58 | singleQuote: true, 59 | tabWidth: 2, 60 | trailingComma: `es5`, 61 | useTabs: false, 62 | }, 63 | ], 64 | quotes: [`error`, `backtick`], 65 | semi: `error`, 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Testing 5 | coverage 6 | 7 | # Temporary editor files 8 | .DS_Store 9 | *~ 10 | *.temp 11 | 12 | # Logs 13 | npm-debug.log* 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "jsxBracketSameLine": true, 5 | "printWidth": 80, 6 | "semi": true, 7 | "singleQuote": true, 8 | "tabWidth": 2, 9 | "trailingComma": "es5", 10 | "useTabs": false 11 | } 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.0.2 2 | ###### Jul 10, 2019 3 | 4 | - Fixed bug where `-mui-mixins` would include a trailing comma after the last function argument, which breaks in IE11 5 | - Bump eslint versions 6 | 7 | ## 2.0.1 8 | ###### Jun 29, 2019 9 | 10 | **NOTE: This version of css-to-mui-loader fixes keyframe animations for jss@10.0.0** 11 | 12 | - Fix animation names to support jss@10.0.0-alpha.1. This version of jss switched `@keyframes` to be [scoped by default](https://github.com/cssinjs/jss/blob/master/changelog.md#breaking-changes-2). @material-ui/styles has been using jss@10 [for some time](https://github.com/mui-org/material-ui/blob/master/CHANGELOG.md#material-uistylesv300-alpha5). 13 | 14 | ## 2.0.0 15 | ###### Jun 29, 2019 16 | 17 | **BREAKING CHANGES: css-to-mui-loader now only supports @materia-ui/core@4.0.0 and up** 18 | 19 | - Switch to new Material UI `theme.spacing()` API, [introduced](https://github.com/mui-org/material-ui/blob/master/CHANGELOG.md#deprecation) in @material-ui/core@4.0.0 20 | 21 | ## 1.3.3 22 | ###### Jun 16, 2019 23 | 24 | - Upgraded devDependencies 25 | 26 | ## 1.3.2 27 | ###### Mar 5, 2019 28 | 29 | - Upgraded devDependencies 30 | 31 | ## 1.3.1 32 | ###### Dec 2, 2018 33 | 34 | - Added link to [css-to-mui-loader-example](https://github.com/mcdougal/css-to-mui-loader-example) demo repo in README 35 | - Added MIT license file 36 | 37 | ## 1.3.0 38 | ###### Dec 2, 2018 39 | 40 | - `babel-loader` is no longer required. Instead of this: 41 | ```js 42 | rules: [ 43 | { 44 | test: /\.css$/, 45 | use: [ 'babel-loader', 'css-to-mui-loader' ] 46 | } 47 | ] 48 | ``` 49 | you can now just do this: 50 | ```js 51 | rules: [ 52 | { 53 | test: /\.css$/, 54 | use: [ 'css-to-mui-loader' ] 55 | } 56 | ] 57 | ``` 58 | 59 | ## 1.2.1 60 | ###### Oct 26, 2018 61 | 62 | - Updated README to reflect that ES5 compatibility has not been reached and 63 | babel-loader is still required 64 | 65 | ## 1.2.0 66 | ###### Oct 26, 2018 67 | 68 | - Closer to full ES5 compatibility. Removed the following from loader output: 69 | - template literals 70 | - object rest spread 71 | - Fixed a bug where comments in keyframes would cause the loader to crash 72 | 73 | ## 1.1.0 74 | ###### Sept 22, 2018 75 | 76 | - Added support for `@keyframes` 77 | - Fixed a bug where defining a media query multiple times would cause it to render 78 | above other rules, resulting in specificity problems 79 | - Only run `prettier` if the `CSS_TO_MUI_TEST` environment variable is non-null. 80 | Code formatting was just used to make expected test output easier to read, but 81 | it is not needed otherwise. 82 | 83 | ## 1.0.2 84 | ###### Sept 7, 2018 85 | 86 | - Moved `prettier` to dependencies instead of devDependencies 87 | 88 | ## 1.0.1 89 | ###### Sept 7, 2018 90 | 91 | - README updates 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Cedric McDougal 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 all 13 | 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 THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # css-to-mui-loader 2 | [![NPM version][npm-image]][npm-url] 3 | 4 | Webpack loader for using external CSS files with [Material UI](https://github.com/mui-org/material-ui). 5 | 6 | [Install](#install) | [Usage](#usage) | [Description](#description) | [Features](#features) | [Demo](#demo) | [Linting](#linting) | [Help out](#help-out) 7 | 8 | ## Install 9 | 10 | ```bash 11 | npm install css-to-mui-loader 12 | ``` 13 | 14 | #### Dependency version support: 15 | 16 | ##### material-ui 17 | 18 | - css-to-mui-loader@2.0.0 and up supports material-ui v4 19 | - css-to-mui-loader@1.3.3 and down supports material-ui v3 20 | 21 | ##### jss 22 | 23 | - css-to-mui-loader@2.0.1 and up supports jss v10 24 | - css-to-mui-loader@1.3.3 and down supports jss v9 25 | 26 | ## Usage 27 | 28 | **styles.css** 29 | ```css 30 | .button { 31 | background: $(theme.palette.primary.main); 32 | padding: 2su; /* Material UI spacing units */ 33 | } 34 | 35 | .button:hover { 36 | background: $(theme.palette.primary.light); 37 | } 38 | ``` 39 | 40 | **MyComponent.js** 41 | ```js 42 | import Button from '@material-ui/core/Button'; 43 | import { withStyles } from '@material-ui/core/styles'; 44 | import styles from './styles.css'; 45 | 46 | const MyComponent = withStyles(styles)(({ classes }) => ( 47 | 50 | )); 51 | ``` 52 | 53 | **webpack.config.js** 54 | ```js 55 | module.exports = { 56 | module: { 57 | rules: [ 58 | { 59 | test: /\.css$/, 60 | use: [ 'css-to-mui-loader' ] 61 | } 62 | ] 63 | } 64 | } 65 | ``` 66 | 67 | ## Description 68 | 69 | The `css-to-mui-loader` allows you to write external CSS files then import them for use in your Material UI components. It provides shortcuts for accessing the Material UI theme within the CSS itself. 70 | 71 | Why? 72 | 73 | 1. CSS is more concise 74 | 2. Designers don't want to write JS 75 | 3. You can copy/paste CSS directly from Chrome Inspector 76 | 4. You still get component-scoped CSS and a centralized theme 77 | 78 | ## Features 79 | 80 | Provides custom unit for Material UI spacing 81 | ```css 82 | .spacing { 83 | padding: 10su; /* Equal to theme.spacing.unit * 10 */ 84 | } 85 | ``` 86 | 87 | Provides access to the Material UI theme 88 | ```css 89 | .theme { 90 | color: $(theme.palette.primary.main); 91 | z-index: $(theme.zIndex.appBar); 92 | } 93 | ``` 94 | 95 | Supports media queries using the Material UI theme breakpoints 96 | ```css 97 | @media $(theme.breakpoints.down('sm')) { 98 | .media { 99 | display: none; 100 | } 101 | } 102 | ``` 103 | 104 | Allows Material UI theme objects to be included as mixins 105 | ```css 106 | .mixins { 107 | -mui-mixins: theme.typography.display4, theme.shape; 108 | } 109 | ``` 110 | 111 | Supports classes, child selectors and pseudo-classes 112 | ```css 113 | .parent.qualifier .child:hover * { 114 | padding: 10px; 115 | } 116 | ``` 117 | 118 | Supports CSS variables 119 | ```css 120 | :root { 121 | --small-spacing: 2su; 122 | } 123 | 124 | .variables { 125 | margin: var(--small-spacing); 126 | } 127 | ``` 128 | 129 | Supports keyframes 130 | ```css 131 | @keyframes my-animation { 132 | 0% { opacity: 0; } 133 | 100% { opacity: 1; } 134 | } 135 | 136 | .keyframes { 137 | animation: my-animation 1s ease-in-out; 138 | } 139 | ``` 140 | 141 | If you want to know what the loader output looks like, 142 | [take a look at the tests](/css-to-mui-loader.test.js). 143 | 144 | ## Demo 145 | 146 | Check out the [css-to-mui-loader-example](https://github.com/mcdougal/css-to-mui-loader-example) repository for a bare-bones demo bootstrapped with [create-react-app](https://github.com/facebook/create-react-app). 147 | 148 | ## Linting 149 | 150 | Some linters might complain about the custom syntax, but there are usually rules you can enable to address this. For example, the following `.stylelintrc` for [stylelint](https://github.com/stylelint/stylelint) does not raise any errors with the custom `css-to-mui-loader` syntax: 151 | 152 | ```json 153 | { 154 | "extends": "stylelint-config-standard", 155 | "plugins": [ 156 | "stylelint-order" 157 | ], 158 | "rules": { 159 | "function-name-case": null, 160 | "property-no-unknown": [ 161 | true, 162 | { 163 | "ignoreProperties": ["-mui-mixins"] 164 | } 165 | ], 166 | "unit-no-unknown": [ 167 | true, 168 | { 169 | "ignoreUnits": ["/^su$/"] 170 | } 171 | ] 172 | } 173 | } 174 | ``` 175 | 176 | ## Help out 177 | 178 | Pull requests, issues, complaints and suggestions are all welcome. 179 | 180 | [npm-image]: https://img.shields.io/npm/v/css-to-mui-loader.svg?style=flat-square 181 | [npm-url]: https://npmjs.org/package/css-to-mui-loader 182 | -------------------------------------------------------------------------------- /css-to-mui-loader.js: -------------------------------------------------------------------------------- 1 | const css = require(`css`); 2 | 3 | /** 4 | * Object.assign polyfill, used to merge mixins with css rules. Taken from: 5 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill 6 | */ 7 | const OBJECT_ASSIGN_POLYFILL = ` 8 | function cssToMuiLoaderAssign(target, varArgs) { // .length of function is 2 9 | 'use strict'; 10 | if (target == null) { // TypeError if undefined or null 11 | throw new TypeError('Cannot convert undefined or null to object'); 12 | } 13 | 14 | var to = Object(target); 15 | 16 | for (var index = 1; index < arguments.length; index++) { 17 | var nextSource = arguments[index]; 18 | 19 | if (nextSource != null) { // Skip over if undefined or null 20 | for (var nextKey in nextSource) { 21 | // Avoid bugs when hasOwnProperty is shadowed 22 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { 23 | to[nextKey] = nextSource[nextKey]; 24 | } 25 | } 26 | } 27 | } 28 | 29 | return to; 30 | } 31 | `; 32 | 33 | /** 34 | * CSS properties need to be converted to camelCase to work with JSS. We also 35 | * may need to convert user-defined variables to camelCase so they can be used 36 | * as JavaScript variables. 37 | */ 38 | const hyphenToCamelCase = function(s) { 39 | return s.replace(/-([a-z])/g, function(g) { 40 | return g[1].toUpperCase(); 41 | }); 42 | }; 43 | 44 | /** 45 | * Convert a custom CSS variable name into a valid JavaScript variable name. 46 | */ 47 | const transpileCustomVariableName = function(variable) { 48 | return hyphenToCamelCase(variable.replace(/^--/, ``)); 49 | }; 50 | 51 | /** 52 | * Convert a CSS property (e.g. background-color) into its JSS equivalent 53 | * (e.g. backgroundColor). 54 | */ 55 | const transpileProperty = function(property) { 56 | return hyphenToCamelCase(property); 57 | }; 58 | 59 | /** 60 | * If the given string starts with a Material UI "spacing unit" value 61 | * (e.g. 10su), return a transpiled version of the unit and the number of 62 | * characters used by the value in the original string. 63 | * 64 | * Example: The string '10su 5su' will parse the '10su' and return: 65 | * { 66 | * newValue: `theme.spacing(10) + 'px'`, 67 | * offset: 4, // # of chars in '10su' 68 | * } 69 | */ 70 | const consumeCustomUnit = function(s) { 71 | const cuIndex = s.indexOf(`su`); 72 | 73 | if (cuIndex !== -1) { 74 | const value = s.slice(0, cuIndex); 75 | 76 | if (value.search(/^-?[\d.]+$/) !== -1) { 77 | if (value.indexOf(`.`) !== -1) { 78 | throw new Error( 79 | `Custom units cannot be fractions, received: ${value}su` 80 | ); 81 | } 82 | 83 | return { 84 | newValue: `theme.spacing(${value}) + 'px'`, 85 | offset: value.length + 2, // Also consume the 'su' 86 | }; 87 | } 88 | } 89 | 90 | return null; 91 | }; 92 | 93 | /** 94 | * If the given string starts with a CSS variable (e.g. var(--my-var)), return 95 | * a transpiled variable name and the number of characters used by the variable 96 | * in the original string. 97 | * 98 | * Example: The string 'var(--my-var) .2s ease-in-out' will parse the 99 | * 'var(--my-var)' and return: 100 | * { 101 | * newValue: myVar, 102 | * offset: 13, // # of chars in 'var(--my-var)' 103 | * } 104 | */ 105 | const consumeCssVariable = function(s) { 106 | if (s.startsWith(`var(`) && s.indexOf(`)`) !== -1) { 107 | const part = s.slice(0, s.indexOf(`)`) + 1); 108 | const varName = part.slice(4).slice(0, -1); 109 | 110 | return { 111 | newValue: transpileCustomVariableName(varName), 112 | offset: part.length, 113 | }; 114 | } 115 | 116 | return null; 117 | }; 118 | 119 | /** 120 | * If the given string starts with a JavaScript "escape hatch" 121 | * (e.g. $(theme.palette.colors.white)), return a transpiled version of the 122 | * JavaScript and the number of characters used by the JS code in the original 123 | * string. 124 | * 125 | * Example: The string '$(theme.palette.colors.white)' will return: 126 | * { 127 | * newValue: theme.palette.colors.white, 128 | * offset: 29, // # of chars in '$(theme.palette.colors.white)' 129 | * } 130 | */ 131 | const consumeJsEscapeHatch = function(s) { 132 | if (s.startsWith(`$(`)) { 133 | let remaining = s.slice(2); 134 | let parenCount = 1; 135 | 136 | while (parenCount > 0 && remaining.length > 0) { 137 | if (remaining[0] === `)`) { 138 | parenCount -= 1; 139 | } else if (remaining[0] === `(`) { 140 | parenCount += 1; 141 | } 142 | 143 | remaining = remaining.slice(1); 144 | } 145 | 146 | if (parenCount === 0 || remaining.length > 0) { 147 | const offset = s.length - remaining.length; 148 | const newValue = s.slice(2, offset - 1); 149 | 150 | return { 151 | newValue: newValue, 152 | offset: offset, 153 | }; 154 | } 155 | } 156 | 157 | return null; 158 | }; 159 | 160 | /** 161 | * Convert any CSS value into a string of equivalent JS code. 162 | */ 163 | const transpileValue = function(value, property) { 164 | const newValue = []; 165 | let remaining = value; 166 | let currStr = ``; 167 | 168 | while (remaining.length > 0) { 169 | let result = consumeCustomUnit(remaining); 170 | 171 | if (!result) { 172 | result = consumeCssVariable(remaining); 173 | } 174 | 175 | if (!result) { 176 | result = consumeJsEscapeHatch(remaining); 177 | } 178 | 179 | if (!result) { 180 | // Consume another character 181 | currStr += remaining[0]; 182 | remaining = remaining.slice(1); 183 | } else { 184 | // Add the current string to the new value array 185 | if (currStr) { 186 | let safeStr = currStr.replace(/'/g, `\\'`).replace(/\n/g, ` `); 187 | if (property === `animation` || property === `animationName`) { 188 | // JSS syntax for using locally scoped animation name 189 | safeStr = `$${safeStr}`; 190 | } 191 | newValue.push(`'${safeStr}'`); 192 | currStr = ``; 193 | } 194 | 195 | // Add the consumed value to the new value array 196 | newValue.push(result.newValue); 197 | remaining = remaining.slice(result.offset); 198 | } 199 | } 200 | 201 | if (currStr) { 202 | let safeStr = currStr.replace(/'/g, `\\'`).replace(/\n/g, ` `); 203 | if (property === `animation` || property === `animationName`) { 204 | // JSS syntax for using locally scoped animation name 205 | safeStr = `$${safeStr}`; 206 | } 207 | newValue.push(`'${safeStr}'`); 208 | } 209 | 210 | return newValue.join(`+`); 211 | }; 212 | 213 | const transpileDeclarations = function(declarations) { 214 | const mixins = declarations.find(function(declaration) { 215 | return declaration.property === `-mui-mixins`; 216 | }); 217 | 218 | let mixinsStr = null; 219 | 220 | if (mixins) { 221 | mixinsStr = mixins.value 222 | .split(`,`) 223 | .map(function(mixin) { 224 | return `${mixin},`; 225 | }) 226 | .join(``); 227 | } 228 | 229 | const declarationsStr = declarations 230 | .filter(function(declaration) { 231 | return declaration.property && declaration.property !== `-mui-mixins`; 232 | }) 233 | .map(function(declaration) { 234 | const property = transpileProperty(declaration.property); 235 | const value = transpileValue(declaration.value, property); 236 | 237 | return `'${property}': ${value},`; 238 | }) 239 | .join(``); 240 | 241 | let transpiledDeclarations; 242 | 243 | if (mixinsStr) { 244 | transpiledDeclarations = ` 245 | ${mixinsStr} 246 | { ${declarationsStr} }, 247 | `; 248 | } else { 249 | transpiledDeclarations = declarationsStr; 250 | } 251 | 252 | return { 253 | declarations: transpiledDeclarations, 254 | usesMixins: Boolean(mixinsStr), 255 | }; 256 | }; 257 | 258 | const transpileRoot = function(root) { 259 | if (!root) { 260 | return ``; 261 | } 262 | 263 | return root.declarations 264 | .map(function(declaration) { 265 | const property = transpileCustomVariableName(declaration.property); 266 | const value = transpileValue(declaration.value); 267 | 268 | return `const ${property} = ${value};`; 269 | }) 270 | .join(``); 271 | }; 272 | 273 | const transpileChildClasses = function(childRules) { 274 | return childRules 275 | .map(function(childRule) { 276 | const { declarations, usesMixins } = transpileDeclarations( 277 | childRule.declarations 278 | ); 279 | 280 | const selector = `'&${childRule.selector.replace(/\./g, `$`)}'`; 281 | 282 | if (usesMixins) { 283 | return ` 284 | ${selector}: cssToMuiLoaderAssign( 285 | {}, 286 | ${declarations.replace(/,[\n ]*$/, ``)} 287 | ), 288 | `; 289 | } 290 | 291 | return ` 292 | ${selector}: { 293 | ${declarations} 294 | }, 295 | `; 296 | }) 297 | .join(``); 298 | }; 299 | 300 | /** 301 | * Return a string of JavaScript code that presents the given keyframes 302 | * rule as an entry in a JavaScript object. 303 | */ 304 | const transpileKeyframes = function(keyframes) { 305 | const transpiledKeyframes = keyframes.keyframes.map(function(keyframe) { 306 | const { declarations, usesMixins } = transpileDeclarations( 307 | keyframe.declarations 308 | ); 309 | 310 | const selector = `'${keyframe.values.join(`,`)}'`; 311 | 312 | if (usesMixins) { 313 | const declarationsStr = declarations.replace(/,[\n ]*$/, ``); 314 | return `${selector}: cssToMuiLoaderAssign({}, ${declarationsStr}),`; 315 | } 316 | 317 | return `${selector}: { ${declarations} },`; 318 | }); 319 | 320 | return ` 321 | '@${keyframes.vendor || ``}keyframes ${keyframes.name}': { 322 | ${transpiledKeyframes.join(``)} 323 | }, 324 | `; 325 | }; 326 | 327 | /** 328 | * Return a string of JavaScript code that presents the given CSS rule 329 | * as an entry in a JavaScript object. 330 | */ 331 | const transpileRule = function(rule) { 332 | if (!rule.selector.startsWith(`.`)) { 333 | throw new Error( 334 | `Only CSS class selectors are supported, received: ${rule.selector}` 335 | ); 336 | } 337 | 338 | let declarations = ``; 339 | let childClasses = ``; 340 | let usesMixins = false; 341 | 342 | if (rule.declarations && rule.declarations.length > 0) { 343 | ({ declarations, usesMixins } = transpileDeclarations(rule.declarations)); 344 | } 345 | 346 | if (rule.childRules && rule.childRules.length > 0) { 347 | childClasses = transpileChildClasses(rule.childRules); 348 | } 349 | 350 | const selector = `'${rule.selector.replace(/^\./, ``)}'`; 351 | 352 | if (usesMixins) { 353 | return ` 354 | ${selector}: cssToMuiLoaderAssign( 355 | {}, 356 | ${declarations} 357 | { ${childClasses} } 358 | ), 359 | `; 360 | } 361 | 362 | return ` 363 | ${selector}: { 364 | ${declarations} 365 | ${childClasses} 366 | }, 367 | `; 368 | }; 369 | 370 | /** 371 | * Return a string of JavaScript code that presents the given media query 372 | * rule as an entry in a JavaScript object. 373 | */ 374 | const transpileMedia = function(media) { 375 | // Matches lines similar to: $(theme.palette.primary.main) 376 | const match = /^\$\((.+)\)$/.exec(media.selector); 377 | 378 | if (!match) { 379 | throw new Error( 380 | `Invalid @media format, use Material UI breakpoints, e.g.: ` + 381 | `@media $(theme.breakpoints.down('xs')). Received: ${media.selector}` 382 | ); 383 | } 384 | 385 | const transpiledRules = Object.values(media.rules).map(transpileRule); 386 | 387 | return ` 388 | output[${match[1]}] = { 389 | ${transpiledRules.join(``)} 390 | }; 391 | `; 392 | }; 393 | 394 | /** 395 | * Return a string of JavaScript code that presents the given CSS rules 396 | * as a Material UI JSS function. 397 | */ 398 | const transpile = function(rules, mediaQueries, keyframes) { 399 | const root = rules[`:root`]; 400 | const rulesWithoutRoot = Object.assign({}, rules); 401 | delete rulesWithoutRoot[`:root`]; 402 | 403 | const transpiledKeyframes = Object.values(keyframes).map(transpileKeyframes); 404 | const transpiledRules = Object.values(rulesWithoutRoot).map(transpileRule); 405 | const transpiledMedia = Object.values(mediaQueries).map(transpileMedia); 406 | 407 | let transpiledCss; 408 | 409 | if (transpiledMedia.length > 0) { 410 | transpiledCss = ` 411 | ${transpileRoot(root)} 412 | var output = { 413 | ${transpiledKeyframes.join(``)} 414 | ${transpiledRules.join(``)} 415 | }; 416 | 417 | ${transpiledMedia.join(``)} 418 | 419 | return output; 420 | `; 421 | } else { 422 | transpiledCss = ` 423 | ${transpileRoot(root)} 424 | return { 425 | ${transpiledKeyframes.join(``)} 426 | ${transpiledRules.join(``)} 427 | }; 428 | `; 429 | } 430 | 431 | let objectAssignPolyfill = ``; 432 | 433 | if (transpiledCss.includes(`cssToMuiLoaderAssign`)) { 434 | objectAssignPolyfill = OBJECT_ASSIGN_POLYFILL; 435 | } 436 | 437 | return ` 438 | module.exports = function cssToMuiLoader(theme) { 439 | ${objectAssignPolyfill} 440 | ${transpiledCss} 441 | }; 442 | `; 443 | }; 444 | 445 | /** 446 | * Extract the rules from the source CSS AST. Return a structure that 447 | * is closer to the final JSS output (for example, make psuedo-classes 448 | * child rules of their associated parent selectors). 449 | */ 450 | const parseRules = function(rules) { 451 | const parsedRules = {}; 452 | 453 | const getOrCreateRule = function(selector) { 454 | if (!parsedRules[selector]) { 455 | parsedRules[selector] = { 456 | selector: selector, 457 | declarations: [], 458 | childRules: [], 459 | }; 460 | } 461 | 462 | return parsedRules[selector]; 463 | }; 464 | 465 | rules.forEach(function(rule) { 466 | if (rule.type === `rule`) { 467 | const declarations = rule.declarations 468 | .filter(function(declaration) { 469 | return declaration.type === `declaration`; 470 | }) 471 | .map(function(declaration) { 472 | return { 473 | property: declaration.property, 474 | value: declaration.value, 475 | }; 476 | }); 477 | 478 | rule.selectors.forEach(function(selector) { 479 | // Matches CSS class followed by anything else, for example: 480 | // .test:hover 481 | // .test1.test2 482 | // .test1 .test2 483 | const childSelectorsMatch = /(\.-?[_a-zA-Z]+[_a-zA-Z0-9-]*)(.*)$/.exec( 484 | selector 485 | ); 486 | 487 | if (childSelectorsMatch && childSelectorsMatch[2]) { 488 | const classSelector = childSelectorsMatch[1]; 489 | const childSelector = childSelectorsMatch[2]; 490 | 491 | // Matches all valid CSS classes in the child selector string 492 | const childClassesList = childSelector.match( 493 | /\.-?[_a-zA-Z]+[_a-zA-Z0-9-]*/g 494 | ); 495 | 496 | if (childClassesList) { 497 | // Make sure a class exists for all possible child selectors 498 | childClassesList.forEach(function(childClass) { 499 | getOrCreateRule(childClass); 500 | }); 501 | } 502 | 503 | const currRule = getOrCreateRule(classSelector); 504 | 505 | currRule.childRules.push({ 506 | selector: childSelector, 507 | declarations: declarations, 508 | }); 509 | } else { 510 | const currRule = getOrCreateRule(selector); 511 | currRule.declarations = currRule.declarations.concat(declarations); 512 | } 513 | }); 514 | } 515 | }); 516 | 517 | return parsedRules; 518 | }; 519 | 520 | /** 521 | * Extract the media queries from the source CSS AST. Merge the child rules 522 | * of media queries with the same names. 523 | */ 524 | const parseMediaQueries = function(rules) { 525 | const parsedMedia = {}; 526 | 527 | rules.forEach(function(rule) { 528 | if (rule.type === `media`) { 529 | if (!parsedMedia[rule.media]) { 530 | parsedMedia[rule.media] = { 531 | selector: rule.media, 532 | rules: {}, 533 | }; 534 | } 535 | 536 | const currMedia = parsedMedia[rule.media]; 537 | 538 | // Parse the child rules and merge them with the existing rules 539 | Object.entries(parseRules(rule.rules)).forEach(function(entry) { 540 | const selector = entry[0]; 541 | const newRule = entry[1]; 542 | const currRule = currMedia.rules[selector]; 543 | 544 | if (currRule) { 545 | currRule.declarations = currRule.declarations.concat( 546 | newRule.declarations 547 | ); 548 | } else { 549 | currMedia.rules[selector] = newRule; 550 | } 551 | }); 552 | } 553 | }); 554 | 555 | return parsedMedia; 556 | }; 557 | 558 | /** 559 | * Extract the keyframes from the source CSS AST. If there are multiple 560 | * keyframes with the same name, the last one is used. 561 | */ 562 | const parseKeyframes = function(rules) { 563 | const parsedKeyframes = {}; 564 | 565 | rules.forEach(function(rule) { 566 | if (rule.type === `keyframes`) { 567 | parsedKeyframes[rule.name] = rule; 568 | } 569 | }); 570 | 571 | return parsedKeyframes; 572 | }; 573 | 574 | const handleSyntaxError = function(source, e) { 575 | const lines = source.split(`\n`).map(function(line, i) { 576 | return { 577 | text: line, 578 | lineNumber: i, 579 | isError: i === e.line - 1, 580 | }; 581 | }); 582 | 583 | const previewLines = lines.slice( 584 | Math.max(e.line - 4, 0), 585 | Math.min(e.line + 1, lines.length) 586 | ); 587 | 588 | let lineNumberWidth = 0; 589 | 590 | previewLines.forEach(function(line) { 591 | lineNumberWidth = Math.max(lineNumberWidth, `${line.lineNumber}`.length); 592 | }); 593 | 594 | const msg = [`SyntaxError: ${e.reason}`, ``]; 595 | 596 | previewLines.forEach(function(line) { 597 | let gutter = ``; 598 | 599 | if (line.isError) { 600 | gutter += `> `; 601 | } else { 602 | gutter += ` `; 603 | } 604 | 605 | gutter += `${line.lineNumber}`.padStart(lineNumberWidth); 606 | 607 | msg.push(`${gutter} | ${line.text}`); 608 | 609 | if (line.isError) { 610 | const emptyGutter = ` `.repeat(2 + lineNumberWidth); 611 | msg.push(`${emptyGutter} | ${` `.repeat(e.column - 1)}^`); 612 | } 613 | }); 614 | 615 | return new Error(msg.join(`\n`)); 616 | }; 617 | 618 | module.exports = function cssToMuiLoader(source) { 619 | let ast; 620 | 621 | try { 622 | ast = css.parse(source); 623 | } catch (e) { 624 | if (e.line) { 625 | throw handleSyntaxError(source, e); 626 | } 627 | 628 | throw e; 629 | } 630 | 631 | const rules = parseRules(ast.stylesheet.rules); 632 | const mediaQueries = parseMediaQueries(ast.stylesheet.rules); 633 | const keyframes = parseKeyframes(ast.stylesheet.rules); 634 | 635 | return transpile(rules, mediaQueries, keyframes); 636 | }; 637 | -------------------------------------------------------------------------------- /css-to-mui-loader.test.js: -------------------------------------------------------------------------------- 1 | const prettier = require(`prettier`); 2 | const cssToMuiLoader = require(`./css-to-mui-loader`); 3 | 4 | const OBJECT_ASSIGN_POLYFILL = ` 5 | function cssToMuiLoaderAssign(target, varArgs) { 6 | // .length of function is 2 7 | 'use strict'; 8 | if (target == null) { 9 | // TypeError if undefined or null 10 | throw new TypeError('Cannot convert undefined or null to object'); 11 | } 12 | var to = Object(target); 13 | for (var index = 1; index < arguments.length; index++) { 14 | var nextSource = arguments[index]; 15 | if (nextSource != null) { 16 | // Skip over if undefined or null 17 | for (var nextKey in nextSource) { 18 | // Avoid bugs when hasOwnProperty is shadowed 19 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { 20 | to[nextKey] = nextSource[nextKey]; 21 | } 22 | } 23 | } 24 | } 25 | return to; 26 | } 27 | `; 28 | 29 | const runLoader = (css) => { 30 | return prettier 31 | .format(cssToMuiLoader(css), { 32 | parser: `babel`, 33 | arrowParens: `always`, 34 | bracketSpacing: true, 35 | semi: true, 36 | singleQuote: true, 37 | tabWidth: 2, 38 | trailingComma: `all`, 39 | }) 40 | .replace(/\s+$/gm, ``) 41 | .trim(); 42 | }; 43 | 44 | it(`throws an error on invalid CSS syntax`, () => { 45 | const css = ` 46 | .test { 47 | padding 10px; 48 | } 49 | `; 50 | 51 | expect(() => { 52 | runLoader(css); 53 | }).toThrow(); 54 | }); 55 | 56 | it(`works on single rule with single property`, () => { 57 | const css = ` 58 | .test { 59 | padding: 10px; 60 | } 61 | `; 62 | 63 | const jss = ` 64 | module.exports = function cssToMuiLoader(theme) { 65 | return { 66 | test: { 67 | padding: '10px', 68 | }, 69 | }; 70 | }; 71 | `; 72 | 73 | expect(runLoader(css)).toBe(jss.trim()); 74 | }); 75 | 76 | it(`supports rule names with dashes`, () => { 77 | const css = ` 78 | .test-dash { 79 | padding: 10px; 80 | } 81 | `; 82 | 83 | const jss = ` 84 | module.exports = function cssToMuiLoader(theme) { 85 | return { 86 | 'test-dash': { 87 | padding: '10px', 88 | }, 89 | }; 90 | }; 91 | `; 92 | 93 | expect(runLoader(css)).toBe(jss.trim()); 94 | }); 95 | 96 | it(`works on values with quotes before the end`, () => { 97 | const css = ` 98 | .quote-test { 99 | font-family: 'Times New Roman', $(theme.typography.caption.fontFamily); 100 | } 101 | `; 102 | 103 | const jss = ` 104 | module.exports = function cssToMuiLoader(theme) { 105 | return { 106 | 'quote-test': { 107 | fontFamily: "'Times New Roman', " + theme.typography.caption.fontFamily, 108 | }, 109 | }; 110 | }; 111 | `; 112 | 113 | expect(runLoader(css)).toBe(jss.trim()); 114 | }); 115 | 116 | it(`works on values with quotes at end`, () => { 117 | const css = ` 118 | .quote-test { 119 | font-family: $(theme.typography.caption.fontFamily), 'Times New Roman'; 120 | } 121 | `; 122 | 123 | const jss = ` 124 | module.exports = function cssToMuiLoader(theme) { 125 | return { 126 | 'quote-test': { 127 | fontFamily: theme.typography.caption.fontFamily + ", 'Times New Roman'", 128 | }, 129 | }; 130 | }; 131 | `; 132 | 133 | expect(runLoader(css)).toBe(jss.trim()); 134 | }); 135 | 136 | it(`works on values with quotes at beginning and end`, () => { 137 | const css = ` 138 | .quote-test { 139 | font-family: 140 | 'Arial', $(theme.typography.caption.fontFamily), 'Times New Roman'; 141 | } 142 | `; 143 | 144 | const jss = ` 145 | module.exports = function cssToMuiLoader(theme) { 146 | return { 147 | 'quote-test': { 148 | fontFamily: 149 | "'Arial', " + 150 | theme.typography.caption.fontFamily + 151 | ", 'Times New Roman'", 152 | }, 153 | }; 154 | }; 155 | `; 156 | 157 | expect(runLoader(css)).toBe(jss.trim()); 158 | }); 159 | 160 | it(`works on empty string`, () => { 161 | const css = ` 162 | .quote-test::before { 163 | content: ''; 164 | } 165 | `; 166 | 167 | const jss = ` 168 | module.exports = function cssToMuiLoader(theme) { 169 | return { 170 | 'quote-test': { 171 | '&::before': { 172 | content: "''", 173 | }, 174 | }, 175 | }; 176 | }; 177 | `; 178 | 179 | expect(runLoader(css)).toBe(jss.trim()); 180 | }); 181 | 182 | it(`works on strings with single and double quotes`, () => { 183 | const css = ` 184 | .quote-test::before { 185 | content: "\\"Let's go to the store,\\" he said."; 186 | } 187 | `; 188 | 189 | const jss = ` 190 | module.exports = function cssToMuiLoader(theme) { 191 | return { 192 | 'quote-test': { 193 | '&::before': { 194 | content: '""Let\\'s go to the store," he said."', 195 | }, 196 | }, 197 | }; 198 | }; 199 | `; 200 | 201 | expect(runLoader(css)).toBe(jss.trim()); 202 | }); 203 | 204 | it(`works on content with multiple values`, () => { 205 | const css = ` 206 | .quote-test::before { 207 | content: "$0" counter(my-awesome-counter) '.00'; 208 | } 209 | `; 210 | 211 | const jss = ` 212 | module.exports = function cssToMuiLoader(theme) { 213 | return { 214 | 'quote-test': { 215 | '&::before': { 216 | content: '"$0" counter(my-awesome-counter) \\'.00\\'', 217 | }, 218 | }, 219 | }; 220 | }; 221 | `; 222 | 223 | expect(runLoader(css)).toBe(jss.trim()); 224 | }); 225 | 226 | it(`works on values with newlines`, () => { 227 | const css = ` 228 | .test { 229 | border-color: transparent transparent transparent 230 | $(theme.palette.common.white); 231 | } 232 | `; 233 | 234 | const jss = ` 235 | module.exports = function cssToMuiLoader(theme) { 236 | return { 237 | test: { 238 | borderColor: 239 | 'transparent transparent transparent ' + theme.palette.common.white, 240 | }, 241 | }; 242 | }; 243 | `; 244 | 245 | expect(runLoader(css)).toBe(jss.trim()); 246 | }); 247 | 248 | it(`throws an error on non-class selectors`, () => { 249 | const css = ` 250 | #test { 251 | padding: 10px; 252 | } 253 | `; 254 | 255 | expect(() => { 256 | runLoader(css); 257 | }).toThrow(); 258 | }); 259 | 260 | it(`works on single rule with multiple selectors`, () => { 261 | const css = ` 262 | .test1, .test2 { 263 | padding: 10px; 264 | } 265 | `; 266 | 267 | const jss = ` 268 | module.exports = function cssToMuiLoader(theme) { 269 | return { 270 | test1: { 271 | padding: '10px', 272 | }, 273 | test2: { 274 | padding: '10px', 275 | }, 276 | }; 277 | }; 278 | `; 279 | 280 | expect(runLoader(css)).toBe(jss.trim()); 281 | }); 282 | 283 | it(`works on single rule with multiple properties`, () => { 284 | const css = ` 285 | .test { 286 | margin: 20px; 287 | padding: 10px; 288 | } 289 | `; 290 | 291 | const jss = ` 292 | module.exports = function cssToMuiLoader(theme) { 293 | return { 294 | test: { 295 | margin: '20px', 296 | padding: '10px', 297 | }, 298 | }; 299 | }; 300 | `; 301 | 302 | expect(runLoader(css)).toBe(jss.trim()); 303 | }); 304 | 305 | it(`works on a single rule with multiple classes`, () => { 306 | const css = ` 307 | .test1.test2 { 308 | padding: 10px; 309 | } 310 | `; 311 | 312 | const jss = ` 313 | module.exports = function cssToMuiLoader(theme) { 314 | return { 315 | test2: {}, 316 | test1: { 317 | '&$test2': { 318 | padding: '10px', 319 | }, 320 | }, 321 | }; 322 | }; 323 | `; 324 | 325 | expect(runLoader(css)).toBe(jss.trim()); 326 | }); 327 | 328 | it(`works on a single rule with multiple class levels`, () => { 329 | const css = ` 330 | .test1 .test2 { 331 | padding: 10px; 332 | } 333 | `; 334 | 335 | const jss = ` 336 | module.exports = function cssToMuiLoader(theme) { 337 | return { 338 | test2: {}, 339 | test1: { 340 | '& $test2': { 341 | padding: '10px', 342 | }, 343 | }, 344 | }; 345 | }; 346 | `; 347 | 348 | expect(runLoader(css)).toBe(jss.trim()); 349 | }); 350 | 351 | it(`works on a child all selector`, () => { 352 | const css = ` 353 | .test1 * { 354 | padding: 10px; 355 | } 356 | `; 357 | 358 | const jss = ` 359 | module.exports = function cssToMuiLoader(theme) { 360 | return { 361 | test1: { 362 | '& *': { 363 | padding: '10px', 364 | }, 365 | }, 366 | }; 367 | }; 368 | `; 369 | 370 | expect(runLoader(css)).toBe(jss.trim()); 371 | }); 372 | 373 | it(`maintains ordering of properties`, () => { 374 | const css = ` 375 | .test { 376 | align-items: center; 377 | background-color: $(theme.palette.gray[200]); 378 | border-left: 2px solid transparent; 379 | display: flex; 380 | flex-direction: column; 381 | font-size: inherit; 382 | margin: 20px; 383 | max-width: 600px; 384 | padding: 10px; 385 | white-space: nowrap; 386 | } 387 | `; 388 | 389 | const jss = ` 390 | module.exports = function cssToMuiLoader(theme) { 391 | return { 392 | test: { 393 | alignItems: 'center', 394 | backgroundColor: theme.palette.gray[200], 395 | borderLeft: '2px solid transparent', 396 | display: 'flex', 397 | flexDirection: 'column', 398 | fontSize: 'inherit', 399 | margin: '20px', 400 | maxWidth: '600px', 401 | padding: '10px', 402 | whiteSpace: 'nowrap', 403 | }, 404 | }; 405 | }; 406 | `; 407 | 408 | expect(runLoader(css)).toBe(jss.trim()); 409 | }); 410 | 411 | it(`works on multiple rules with multiple properties`, () => { 412 | const css = ` 413 | .test1 { 414 | margin: 20px; 415 | padding: 10px; 416 | } 417 | 418 | .test2 { 419 | background: red; 420 | color: blue; 421 | } 422 | `; 423 | 424 | const jss = ` 425 | module.exports = function cssToMuiLoader(theme) { 426 | return { 427 | test1: { 428 | margin: '20px', 429 | padding: '10px', 430 | }, 431 | test2: { 432 | background: 'red', 433 | color: 'blue', 434 | }, 435 | }; 436 | }; 437 | `; 438 | 439 | expect(runLoader(css)).toBe(jss.trim()); 440 | }); 441 | 442 | it(`combines declarations from the same rule defined multiple times`, () => { 443 | const css = ` 444 | .test { 445 | margin: 20px; 446 | } 447 | 448 | .test { 449 | background: red; 450 | margin: 25px; 451 | } 452 | `; 453 | 454 | const jss = ` 455 | module.exports = function cssToMuiLoader(theme) { 456 | return { 457 | test: { 458 | margin: '20px', 459 | background: 'red', 460 | margin: '25px', 461 | }, 462 | }; 463 | }; 464 | `; 465 | 466 | expect(runLoader(css)).toBe(jss.trim()); 467 | }); 468 | 469 | it(`ignores top-level comments`, () => { 470 | const css = ` 471 | /* 472 | .todo { 473 | margin: 0; 474 | } 475 | */ 476 | .test { 477 | padding: 10px; 478 | } 479 | `; 480 | 481 | const jss = ` 482 | module.exports = function cssToMuiLoader(theme) { 483 | return { 484 | test: { 485 | padding: '10px', 486 | }, 487 | }; 488 | }; 489 | `; 490 | 491 | expect(runLoader(css)).toBe(jss.trim()); 492 | }); 493 | 494 | it(`ignores nested comments`, () => { 495 | const css = ` 496 | .test { 497 | padding: 10px; 498 | /* 499 | .todo { 500 | margin: 0; 501 | } 502 | */ 503 | } 504 | `; 505 | 506 | const jss = ` 507 | module.exports = function cssToMuiLoader(theme) { 508 | return { 509 | test: { 510 | padding: '10px', 511 | }, 512 | }; 513 | }; 514 | `; 515 | 516 | expect(runLoader(css)).toBe(jss.trim()); 517 | }); 518 | 519 | it(`ignores inline comments`, () => { 520 | const css = ` 521 | .test { 522 | padding: 10px; /* TODO: something */ 523 | } 524 | `; 525 | 526 | const jss = ` 527 | module.exports = function cssToMuiLoader(theme) { 528 | return { 529 | test: { 530 | padding: '10px', 531 | }, 532 | }; 533 | }; 534 | `; 535 | 536 | expect(runLoader(css)).toBe(jss.trim()); 537 | }); 538 | 539 | it(`converts hyphens to camelCase`, () => { 540 | const css = ` 541 | .test { 542 | text-align: center; 543 | } 544 | `; 545 | 546 | const jss = ` 547 | module.exports = function cssToMuiLoader(theme) { 548 | return { 549 | test: { 550 | textAlign: 'center', 551 | }, 552 | }; 553 | }; 554 | `; 555 | 556 | expect(runLoader(css)).toBe(jss.trim()); 557 | }); 558 | 559 | it(`converts custom units for single positive integer value`, () => { 560 | const css = ` 561 | .test { 562 | padding: 10su; 563 | } 564 | `; 565 | 566 | const jss = ` 567 | module.exports = function cssToMuiLoader(theme) { 568 | return { 569 | test: { 570 | padding: theme.spacing(10) + 'px', 571 | }, 572 | }; 573 | }; 574 | `; 575 | 576 | expect(runLoader(css)).toBe(jss.trim()); 577 | }); 578 | 579 | it(`converts custom units for single negative integer value`, () => { 580 | const css = ` 581 | .test { 582 | padding: -10su; 583 | } 584 | `; 585 | 586 | const jss = ` 587 | module.exports = function cssToMuiLoader(theme) { 588 | return { 589 | test: { 590 | padding: theme.spacing(-10) + 'px', 591 | }, 592 | }; 593 | }; 594 | `; 595 | 596 | expect(runLoader(css)).toBe(jss.trim()); 597 | }); 598 | 599 | it(`throws an error for a decimal custom unit`, () => { 600 | const css = ` 601 | .test { 602 | padding: 1.1su; 603 | } 604 | `; 605 | 606 | expect(() => { 607 | runLoader(css); 608 | }).toThrow(); 609 | }); 610 | 611 | it(`converts custom units for multiple values`, () => { 612 | const css = ` 613 | .test { 614 | padding: 11su 12su 13su 14su; 615 | } 616 | `; 617 | 618 | const jss = ` 619 | module.exports = function cssToMuiLoader(theme) { 620 | return { 621 | test: { 622 | padding: 623 | theme.spacing(11) + 624 | 'px' + 625 | ' ' + 626 | theme.spacing(12) + 627 | 'px' + 628 | ' ' + 629 | theme.spacing(13) + 630 | 'px' + 631 | ' ' + 632 | theme.spacing(14) + 633 | 'px', 634 | }, 635 | }; 636 | }; 637 | `; 638 | 639 | expect(runLoader(css)).toBe(jss.trim()); 640 | }); 641 | 642 | it(`converts custom units when one value is 0`, () => { 643 | const css = ` 644 | .test { 645 | padding: 0 2su; 646 | } 647 | `; 648 | 649 | const jss = ` 650 | module.exports = function cssToMuiLoader(theme) { 651 | return { 652 | test: { 653 | padding: '0 ' + theme.spacing(2) + 'px', 654 | }, 655 | }; 656 | }; 657 | `; 658 | 659 | expect(runLoader(css)).toBe(jss.trim()); 660 | }); 661 | 662 | it(`does not convert custom units when the only value is 0`, () => { 663 | const css = ` 664 | .test { 665 | padding: 0; 666 | } 667 | `; 668 | 669 | const jss = ` 670 | module.exports = function cssToMuiLoader(theme) { 671 | return { 672 | test: { 673 | padding: '0', 674 | }, 675 | }; 676 | }; 677 | `; 678 | 679 | expect(runLoader(css)).toBe(jss.trim()); 680 | }); 681 | 682 | it(`supports mixing custom units with builtin units`, () => { 683 | const css = ` 684 | .test { 685 | padding: 11px 1rem 0 10su; 686 | } 687 | `; 688 | 689 | const jss = ` 690 | module.exports = function cssToMuiLoader(theme) { 691 | return { 692 | test: { 693 | padding: '11px 1rem 0 ' + theme.spacing(10) + 'px', 694 | }, 695 | }; 696 | }; 697 | `; 698 | 699 | expect(runLoader(css)).toBe(jss.trim()); 700 | }); 701 | 702 | it(`works when custom units are defined inside a transform`, () => { 703 | const css = ` 704 | .test { 705 | transform: translate(-3su); 706 | } 707 | `; 708 | 709 | const jss = ` 710 | module.exports = function cssToMuiLoader(theme) { 711 | return { 712 | test: { 713 | transform: 'translate(' + theme.spacing(-3) + 'px' + ')', 714 | }, 715 | }; 716 | }; 717 | `; 718 | 719 | expect(runLoader(css)).toBe(jss.trim()); 720 | }); 721 | 722 | it(`provides an escape hatch for running JS code`, () => { 723 | const css = ` 724 | .test { 725 | color: $(theme.palette.primary.main); 726 | } 727 | `; 728 | 729 | const jss = ` 730 | module.exports = function cssToMuiLoader(theme) { 731 | return { 732 | test: { 733 | color: theme.palette.primary.main, 734 | }, 735 | }; 736 | }; 737 | `; 738 | 739 | expect(runLoader(css)).toBe(jss.trim()); 740 | }); 741 | 742 | it(`supports JS escape hatch if it's not the only thing on the line`, () => { 743 | const css = ` 744 | .test { 745 | transition: max-width $(theme.transitions.standard); 746 | } 747 | `; 748 | 749 | const jss = ` 750 | module.exports = function cssToMuiLoader(theme) { 751 | return { 752 | test: { 753 | transition: 'max-width ' + theme.transitions.standard, 754 | }, 755 | }; 756 | }; 757 | `; 758 | 759 | expect(runLoader(css)).toBe(jss.trim()); 760 | }); 761 | 762 | it(`supports JS escape hatch with nested parens`, () => { 763 | const css = ` 764 | .test { 765 | border: 1px solid $(theme.utils.rgba(theme.palette.primary, .2)); 766 | } 767 | `; 768 | 769 | const jss = ` 770 | module.exports = function cssToMuiLoader(theme) { 771 | return { 772 | test: { 773 | border: '1px solid ' + theme.utils.rgba(theme.palette.primary, 0.2), 774 | }, 775 | }; 776 | }; 777 | `; 778 | 779 | expect(runLoader(css)).toBe(jss.trim()); 780 | }); 781 | 782 | it(`supports CSS variables with basic values`, () => { 783 | const css = ` 784 | :root { 785 | --my-color: blue; 786 | } 787 | 788 | .test { 789 | background: var(--my-color); 790 | } 791 | `; 792 | 793 | const jss = ` 794 | module.exports = function cssToMuiLoader(theme) { 795 | const myColor = 'blue'; 796 | return { 797 | test: { 798 | background: myColor, 799 | }, 800 | }; 801 | }; 802 | `; 803 | 804 | expect(runLoader(css)).toBe(jss.trim()); 805 | }); 806 | 807 | it(`supports CSS variables with custom units`, () => { 808 | const css = ` 809 | :root { 810 | --common-padding: 10su; 811 | } 812 | 813 | .test { 814 | padding: var(--common-padding); 815 | } 816 | `; 817 | 818 | const jss = ` 819 | module.exports = function cssToMuiLoader(theme) { 820 | const commonPadding = theme.spacing(10) + 'px'; 821 | return { 822 | test: { 823 | padding: commonPadding, 824 | }, 825 | }; 826 | }; 827 | `; 828 | 829 | expect(runLoader(css)).toBe(jss.trim()); 830 | }); 831 | 832 | it(`supports CSS variables with JS`, () => { 833 | const css = ` 834 | :root { 835 | --my-color: $(theme.palette.gray[200]); 836 | } 837 | 838 | .test { 839 | color: var(--my-color); 840 | } 841 | `; 842 | 843 | const jss = ` 844 | module.exports = function cssToMuiLoader(theme) { 845 | const myColor = theme.palette.gray[200]; 846 | return { 847 | test: { 848 | color: myColor, 849 | }, 850 | }; 851 | }; 852 | `; 853 | 854 | expect(runLoader(css)).toBe(jss.trim()); 855 | }); 856 | 857 | it(`supports CSS variables mixed with JS escape hatch`, () => { 858 | const css = ` 859 | :root { 860 | --transition-prop: max-width; 861 | } 862 | 863 | .test { 864 | transition: var(--transition-prop) $(theme.transitions.standard); 865 | } 866 | `; 867 | 868 | const jss = ` 869 | module.exports = function cssToMuiLoader(theme) { 870 | const transitionProp = 'max-width'; 871 | return { 872 | test: { 873 | transition: transitionProp + ' ' + theme.transitions.standard, 874 | }, 875 | }; 876 | }; 877 | `; 878 | 879 | expect(runLoader(css)).toBe(jss.trim()); 880 | }); 881 | 882 | it(`supports multiple CSS variables`, () => { 883 | const css = ` 884 | :root { 885 | --my-color: $(theme.palette.gray[200]); 886 | --my-padding: 2su; 887 | } 888 | 889 | .test { 890 | color: var(--my-color); 891 | padding: var(--my-padding); 892 | } 893 | `; 894 | 895 | const jss = ` 896 | module.exports = function cssToMuiLoader(theme) { 897 | const myColor = theme.palette.gray[200]; 898 | const myPadding = theme.spacing(2) + 'px'; 899 | return { 900 | test: { 901 | color: myColor, 902 | padding: myPadding, 903 | }, 904 | }; 905 | }; 906 | `; 907 | 908 | expect(runLoader(css)).toBe(jss.trim()); 909 | }); 910 | 911 | it(`supports mix of CSS variables, JS escape hatch and custom units`, () => { 912 | const css = ` 913 | :root { 914 | --p: 1su; 915 | } 916 | 917 | .test { 918 | padding: 1px 2su var(--p) $(theme.p)px; 919 | } 920 | `; 921 | 922 | const jss = ` 923 | module.exports = function cssToMuiLoader(theme) { 924 | const p = theme.spacing(1) + 'px'; 925 | return { 926 | test: { 927 | padding: 928 | '1px ' + theme.spacing(2) + 'px' + ' ' + p + ' ' + theme.p + 'px', 929 | }, 930 | }; 931 | }; 932 | `; 933 | 934 | expect(runLoader(css)).toBe(jss.trim()); 935 | }); 936 | 937 | it(`supports locally scoped keyframe names with animation shorthand`, () => { 938 | const css = ` 939 | .test { 940 | animation: my-animation 1s ease-in-out; 941 | } 942 | `; 943 | 944 | const jss = ` 945 | module.exports = function cssToMuiLoader(theme) { 946 | return { 947 | test: { 948 | animation: '$my-animation 1s ease-in-out', 949 | }, 950 | }; 951 | }; 952 | `; 953 | 954 | expect(runLoader(css)).toBe(jss.trim()); 955 | }); 956 | 957 | it(`supports locally scoped keyframe names with animation name`, () => { 958 | const css = ` 959 | .test { 960 | animation-duration: 1s; 961 | animation-name: my-animation; 962 | } 963 | `; 964 | 965 | const jss = ` 966 | module.exports = function cssToMuiLoader(theme) { 967 | return { 968 | test: { 969 | animationDuration: '1s', 970 | animationName: '$my-animation', 971 | }, 972 | }; 973 | }; 974 | `; 975 | 976 | expect(runLoader(css)).toBe(jss.trim()); 977 | }); 978 | 979 | it(`supports basic keyframes`, () => { 980 | const css = ` 981 | @keyframes my-animation { 982 | 0% { background: $(theme.palette.common.white); } 983 | 100% { background: $(theme.palette.common.black); } 984 | } 985 | `; 986 | 987 | const jss = ` 988 | module.exports = function cssToMuiLoader(theme) { 989 | return { 990 | '@keyframes my-animation': { 991 | '0%': { background: theme.palette.common.white }, 992 | '100%': { background: theme.palette.common.black }, 993 | }, 994 | }; 995 | }; 996 | `; 997 | 998 | expect(runLoader(css)).toBe(jss.trim()); 999 | }); 1000 | 1001 | it(`supports keyframes with multiple percentages`, () => { 1002 | const css = ` 1003 | @keyframes my-animation { 1004 | 0%, 75% { background: $(theme.palette.common.white); } 1005 | 25%, 90%, 100% { background: $(theme.palette.common.black); } 1006 | } 1007 | `; 1008 | 1009 | const jss = ` 1010 | module.exports = function cssToMuiLoader(theme) { 1011 | return { 1012 | '@keyframes my-animation': { 1013 | '0%,75%': { background: theme.palette.common.white }, 1014 | '25%,90%,100%': { background: theme.palette.common.black }, 1015 | }, 1016 | }; 1017 | }; 1018 | `; 1019 | 1020 | expect(runLoader(css)).toBe(jss.trim()); 1021 | }); 1022 | 1023 | it(`supports keyframes with multiple declarations`, () => { 1024 | const css = ` 1025 | @keyframes my-animation { 1026 | 0% { 1027 | background: $(theme.palette.common.white); 1028 | padding: 1su; 1029 | } 1030 | 1031 | 100% { 1032 | background: $(theme.palette.common.black); 1033 | padding: 2su; 1034 | } 1035 | } 1036 | `; 1037 | 1038 | const jss = ` 1039 | module.exports = function cssToMuiLoader(theme) { 1040 | return { 1041 | '@keyframes my-animation': { 1042 | '0%': { 1043 | background: theme.palette.common.white, 1044 | padding: theme.spacing(1) + 'px', 1045 | }, 1046 | '100%': { 1047 | background: theme.palette.common.black, 1048 | padding: theme.spacing(2) + 'px', 1049 | }, 1050 | }, 1051 | }; 1052 | }; 1053 | `; 1054 | 1055 | expect(runLoader(css)).toBe(jss.trim()); 1056 | }); 1057 | 1058 | it(`overrides keyframes with the same name`, () => { 1059 | const css = ` 1060 | @keyframes my-animation { 1061 | 0% { background: $(theme.palette.common.white); } 1062 | } 1063 | @keyframes my-animation { 1064 | 100% { background: $(theme.palette.common.black); } 1065 | } 1066 | `; 1067 | 1068 | const jss = ` 1069 | module.exports = function cssToMuiLoader(theme) { 1070 | return { 1071 | '@keyframes my-animation': { 1072 | '100%': { background: theme.palette.common.black }, 1073 | }, 1074 | }; 1075 | }; 1076 | `; 1077 | 1078 | expect(runLoader(css)).toBe(jss.trim()); 1079 | }); 1080 | 1081 | it(`supports keyframes with vendor prefixes`, () => { 1082 | const css = ` 1083 | @-webkit-keyframes my-animation { 1084 | 0% { opacity: 0; } 1085 | 100% { opacity: 1; } 1086 | } 1087 | `; 1088 | 1089 | const jss = ` 1090 | module.exports = function cssToMuiLoader(theme) { 1091 | return { 1092 | '@-webkit-keyframes my-animation': { 1093 | '0%': { opacity: '0' }, 1094 | '100%': { opacity: '1' }, 1095 | }, 1096 | }; 1097 | }; 1098 | `; 1099 | 1100 | expect(runLoader(css)).toBe(jss.trim()); 1101 | }); 1102 | 1103 | it(`supports keyframes with comments`, () => { 1104 | const css = ` 1105 | @keyframes my-animation { 1106 | 0% { 1107 | transform: translateY(-100%); 1108 | } 1109 | 1110 | 100% { 1111 | transform: translateY(-100%); 1112 | /* transform: translateY(-100%) rotateX(180deg); */ 1113 | } 1114 | } 1115 | `; 1116 | 1117 | const jss = ` 1118 | module.exports = function cssToMuiLoader(theme) { 1119 | return { 1120 | '@keyframes my-animation': { 1121 | '0%': { transform: 'translateY(-100%)' }, 1122 | '100%': { transform: 'translateY(-100%)' }, 1123 | }, 1124 | }; 1125 | }; 1126 | `; 1127 | 1128 | expect(runLoader(css)).toBe(jss.trim()); 1129 | }); 1130 | 1131 | it(`supports media queries that are defined once`, () => { 1132 | const css = ` 1133 | .test { 1134 | padding: 20px; 1135 | } 1136 | 1137 | @media $(theme.breakpoints.down('xs')) { 1138 | .test { 1139 | padding: 5px; 1140 | } 1141 | } 1142 | `; 1143 | 1144 | const jss = ` 1145 | module.exports = function cssToMuiLoader(theme) { 1146 | var output = { 1147 | test: { 1148 | padding: '20px', 1149 | }, 1150 | }; 1151 | output[theme.breakpoints.down('xs')] = { 1152 | test: { 1153 | padding: '5px', 1154 | }, 1155 | }; 1156 | return output; 1157 | }; 1158 | `; 1159 | 1160 | expect(runLoader(css)).toBe(jss.trim()); 1161 | }); 1162 | 1163 | it(`supports media queries that are defined more than once`, () => { 1164 | const css = ` 1165 | .test1 { 1166 | padding: 20px; 1167 | } 1168 | 1169 | @media $(theme.breakpoints.down('xs')) { 1170 | .test1 { 1171 | padding: 5px; 1172 | } 1173 | } 1174 | 1175 | @media $(theme.breakpoints.down('xs')) { 1176 | .test1 { 1177 | margin: 0; 1178 | } 1179 | } 1180 | 1181 | .test2 { 1182 | padding: 50px; 1183 | } 1184 | 1185 | @media $(theme.breakpoints.down('xs')) { 1186 | .test2 { 1187 | padding: 10px; 1188 | } 1189 | } 1190 | `; 1191 | 1192 | const jss = ` 1193 | module.exports = function cssToMuiLoader(theme) { 1194 | var output = { 1195 | test1: { 1196 | padding: '20px', 1197 | }, 1198 | test2: { 1199 | padding: '50px', 1200 | }, 1201 | }; 1202 | output[theme.breakpoints.down('xs')] = { 1203 | test1: { 1204 | padding: '5px', 1205 | margin: '0', 1206 | }, 1207 | test2: { 1208 | padding: '10px', 1209 | }, 1210 | }; 1211 | return output; 1212 | }; 1213 | `; 1214 | 1215 | expect(runLoader(css)).toBe(jss.trim()); 1216 | }); 1217 | 1218 | it(`supports multiple media queries`, () => { 1219 | const css = ` 1220 | .test { 1221 | padding: 20px; 1222 | } 1223 | 1224 | @media $(theme.breakpoints.down('sm')) { 1225 | .test { 1226 | padding: 10px; 1227 | } 1228 | } 1229 | 1230 | @media $(theme.breakpoints.down('xs')) { 1231 | .test { 1232 | padding: 5px; 1233 | } 1234 | } 1235 | `; 1236 | 1237 | const jss = ` 1238 | module.exports = function cssToMuiLoader(theme) { 1239 | var output = { 1240 | test: { 1241 | padding: '20px', 1242 | }, 1243 | }; 1244 | output[theme.breakpoints.down('sm')] = { 1245 | test: { 1246 | padding: '10px', 1247 | }, 1248 | }; 1249 | output[theme.breakpoints.down('xs')] = { 1250 | test: { 1251 | padding: '5px', 1252 | }, 1253 | }; 1254 | return output; 1255 | }; 1256 | `; 1257 | 1258 | expect(runLoader(css)).toBe(jss.trim()); 1259 | }); 1260 | 1261 | it(`throws an error on media queries that don't use Material UI`, () => { 1262 | const css = ` 1263 | .test { 1264 | padding: 20px; 1265 | } 1266 | 1267 | @media (min-width: 30em) { 1268 | .test { 1269 | padding: 5px; 1270 | } 1271 | } 1272 | `; 1273 | 1274 | expect(() => { 1275 | runLoader(css); 1276 | }).toThrow(); 1277 | }); 1278 | 1279 | it(`supports pseudo-classes on base class`, () => { 1280 | const css = ` 1281 | .test { 1282 | background: green; 1283 | color: red; 1284 | } 1285 | 1286 | .test:hover { 1287 | background: pink; 1288 | color: blue; 1289 | } 1290 | `; 1291 | 1292 | const jss = ` 1293 | module.exports = function cssToMuiLoader(theme) { 1294 | return { 1295 | test: { 1296 | background: 'green', 1297 | color: 'red', 1298 | '&:hover': { 1299 | background: 'pink', 1300 | color: 'blue', 1301 | }, 1302 | }, 1303 | }; 1304 | }; 1305 | `; 1306 | 1307 | expect(runLoader(css)).toBe(jss.trim()); 1308 | }); 1309 | 1310 | it(`supports pseudo-classes before base class`, () => { 1311 | const css = ` 1312 | .test:hover { 1313 | background: pink; 1314 | color: blue; 1315 | } 1316 | 1317 | .test { 1318 | background: green; 1319 | color: red; 1320 | } 1321 | `; 1322 | 1323 | const jss = ` 1324 | module.exports = function cssToMuiLoader(theme) { 1325 | return { 1326 | test: { 1327 | background: 'green', 1328 | color: 'red', 1329 | '&:hover': { 1330 | background: 'pink', 1331 | color: 'blue', 1332 | }, 1333 | }, 1334 | }; 1335 | }; 1336 | `; 1337 | 1338 | expect(runLoader(css)).toBe(jss.trim()); 1339 | }); 1340 | 1341 | it(`supports pseudo-classes without base classes`, () => { 1342 | const css = ` 1343 | .test:hover { 1344 | background: pink; 1345 | } 1346 | `; 1347 | 1348 | const jss = ` 1349 | module.exports = function cssToMuiLoader(theme) { 1350 | return { 1351 | test: { 1352 | '&:hover': { 1353 | background: 'pink', 1354 | }, 1355 | }, 1356 | }; 1357 | }; 1358 | `; 1359 | 1360 | expect(runLoader(css)).toBe(jss.trim()); 1361 | }); 1362 | 1363 | it(`supports multiple pseudo-classeses`, () => { 1364 | const css = ` 1365 | .test:nth-child(2):hover { 1366 | background: pink; 1367 | } 1368 | `; 1369 | 1370 | const jss = ` 1371 | module.exports = function cssToMuiLoader(theme) { 1372 | return { 1373 | test: { 1374 | '&:nth-child(2):hover': { 1375 | background: 'pink', 1376 | }, 1377 | }, 1378 | }; 1379 | }; 1380 | `; 1381 | 1382 | expect(runLoader(css)).toBe(jss.trim()); 1383 | }); 1384 | 1385 | it(`supports multiple pseudo-classes selectors`, () => { 1386 | const css = ` 1387 | .test:hover, .test:focus { 1388 | background: pink; 1389 | } 1390 | `; 1391 | 1392 | const jss = ` 1393 | module.exports = function cssToMuiLoader(theme) { 1394 | return { 1395 | test: { 1396 | '&:hover': { 1397 | background: 'pink', 1398 | }, 1399 | '&:focus': { 1400 | background: 'pink', 1401 | }, 1402 | }, 1403 | }; 1404 | }; 1405 | `; 1406 | 1407 | expect(runLoader(css)).toBe(jss.trim()); 1408 | }); 1409 | 1410 | it(`supports pseudo-classes inside media queries`, () => { 1411 | const css = ` 1412 | .test:hover { 1413 | background: pink; 1414 | } 1415 | 1416 | @media $(theme.breakpoints.down('xs')) { 1417 | .test:hover { 1418 | background: pink; 1419 | } 1420 | } 1421 | `; 1422 | 1423 | const jss = ` 1424 | module.exports = function cssToMuiLoader(theme) { 1425 | var output = { 1426 | test: { 1427 | '&:hover': { 1428 | background: 'pink', 1429 | }, 1430 | }, 1431 | }; 1432 | output[theme.breakpoints.down('xs')] = { 1433 | test: { 1434 | '&:hover': { 1435 | background: 'pink', 1436 | }, 1437 | }, 1438 | }; 1439 | return output; 1440 | }; 1441 | `; 1442 | 1443 | expect(runLoader(css)).toBe(jss.trim()); 1444 | }); 1445 | 1446 | it(`supports pseudo-classes on multiple class levels`, () => { 1447 | const css = ` 1448 | .test1.test2 .test3:hover { 1449 | padding: 10px; 1450 | } 1451 | `; 1452 | 1453 | const jss = ` 1454 | module.exports = function cssToMuiLoader(theme) { 1455 | return { 1456 | test2: {}, 1457 | test3: {}, 1458 | test1: { 1459 | '&$test2 $test3:hover': { 1460 | padding: '10px', 1461 | }, 1462 | }, 1463 | }; 1464 | }; 1465 | `; 1466 | 1467 | expect(runLoader(css)).toBe(jss.trim()); 1468 | }); 1469 | 1470 | it(`supports a single mixin with no other properties`, () => { 1471 | const css = ` 1472 | .test { 1473 | -mui-mixins: theme.mixins.customMixin; 1474 | } 1475 | `; 1476 | 1477 | const jss = ` 1478 | module.exports = function cssToMuiLoader(theme) { 1479 | ${OBJECT_ASSIGN_POLYFILL.trim()} 1480 | return { 1481 | test: cssToMuiLoaderAssign( 1482 | {}, 1483 | theme.mixins.customMixin, 1484 | {}, 1485 | {}, 1486 | ), 1487 | }; 1488 | }; 1489 | `; 1490 | 1491 | expect(runLoader(css)).toBe(jss.trim()); 1492 | }); 1493 | 1494 | it(`supports a single mixin with other properties`, () => { 1495 | const css = ` 1496 | .test { 1497 | border: 1px solid red; 1498 | -mui-mixins: theme.mixins.customMixin; 1499 | padding: 10px; 1500 | } 1501 | `; 1502 | 1503 | const jss = ` 1504 | module.exports = function cssToMuiLoader(theme) { 1505 | ${OBJECT_ASSIGN_POLYFILL.trim()} 1506 | return { 1507 | test: cssToMuiLoaderAssign( 1508 | {}, 1509 | theme.mixins.customMixin, 1510 | { border: '1px solid red', padding: '10px' }, 1511 | {}, 1512 | ), 1513 | }; 1514 | }; 1515 | `; 1516 | 1517 | expect(runLoader(css)).toBe(jss.trim()); 1518 | }); 1519 | 1520 | it(`supports multiple mixins`, () => { 1521 | const css = ` 1522 | .test { 1523 | border: 1px solid red; 1524 | -mui-mixins: theme.mixins.mixin1, theme.mixins.mixin2, theme.mixins.mixin3; 1525 | padding: 10px; 1526 | } 1527 | `; 1528 | 1529 | const jss = ` 1530 | module.exports = function cssToMuiLoader(theme) { 1531 | ${OBJECT_ASSIGN_POLYFILL.trim()} 1532 | return { 1533 | test: cssToMuiLoaderAssign( 1534 | {}, 1535 | theme.mixins.mixin1, 1536 | theme.mixins.mixin2, 1537 | theme.mixins.mixin3, 1538 | { border: '1px solid red', padding: '10px' }, 1539 | {}, 1540 | ), 1541 | }; 1542 | }; 1543 | `; 1544 | 1545 | expect(runLoader(css)).toBe(jss.trim()); 1546 | }); 1547 | 1548 | it(`supports mixins on pseudo-classes with multiple class levels`, () => { 1549 | const css = ` 1550 | .test1 { 1551 | border: 1px solid red; 1552 | -mui-mixins: theme.mixins.customMixin1; 1553 | } 1554 | 1555 | .test1 .test2:hover { 1556 | -mui-mixins: theme.mixins.customMixin2; 1557 | padding: 1su; 1558 | } 1559 | 1560 | .test1 .test3:hover { 1561 | -mui-mixins: theme.mixins.customMixin3; 1562 | } 1563 | `; 1564 | 1565 | const jss = ` 1566 | module.exports = function cssToMuiLoader(theme) { 1567 | ${OBJECT_ASSIGN_POLYFILL.trim()} 1568 | return { 1569 | test1: cssToMuiLoaderAssign( 1570 | {}, 1571 | theme.mixins.customMixin1, 1572 | { border: '1px solid red' }, 1573 | { 1574 | '& $test2:hover': cssToMuiLoaderAssign( 1575 | {}, 1576 | theme.mixins.customMixin2, 1577 | { padding: theme.spacing(1) + 'px' }, 1578 | ), 1579 | '& $test3:hover': cssToMuiLoaderAssign( 1580 | {}, 1581 | theme.mixins.customMixin3, 1582 | {}, 1583 | ), 1584 | }, 1585 | ), 1586 | test2: {}, 1587 | test3: {}, 1588 | }; 1589 | }; 1590 | `; 1591 | 1592 | expect(runLoader(css)).toBe(jss.trim()); 1593 | }); 1594 | 1595 | it(`supports mixins inside media queries`, () => { 1596 | const css = ` 1597 | @media $(theme.breakpoints.down('xs')) { 1598 | .test { 1599 | -mui-mixins: theme.mixins.customMixin; 1600 | } 1601 | } 1602 | `; 1603 | 1604 | const jss = ` 1605 | module.exports = function cssToMuiLoader(theme) { 1606 | ${OBJECT_ASSIGN_POLYFILL.trim()} 1607 | var output = {}; 1608 | output[theme.breakpoints.down('xs')] = { 1609 | test: cssToMuiLoaderAssign( 1610 | {}, 1611 | theme.mixins.customMixin, 1612 | {}, 1613 | {}, 1614 | ), 1615 | }; 1616 | return output; 1617 | }; 1618 | `; 1619 | 1620 | expect(runLoader(css)).toBe(jss.trim()); 1621 | }); 1622 | 1623 | it(`supports mixins, properties and children inside media queries`, () => { 1624 | const css = ` 1625 | @media $(theme.breakpoints.down('xs')) { 1626 | .test { 1627 | -mui-mixins: theme.mixins.customMixin; 1628 | width: 10su; 1629 | } 1630 | 1631 | .test::after { 1632 | background: $(theme.palette.primary.main); 1633 | content: "we're testing"; 1634 | -mui-mixins: theme.mixins.customMixin; 1635 | } 1636 | } 1637 | `; 1638 | 1639 | const jss = ` 1640 | module.exports = function cssToMuiLoader(theme) { 1641 | ${OBJECT_ASSIGN_POLYFILL.trim()} 1642 | var output = {}; 1643 | output[theme.breakpoints.down('xs')] = { 1644 | test: cssToMuiLoaderAssign( 1645 | {}, 1646 | theme.mixins.customMixin, 1647 | { width: theme.spacing(10) + 'px' }, 1648 | { 1649 | '&::after': cssToMuiLoaderAssign( 1650 | {}, 1651 | theme.mixins.customMixin, 1652 | { 1653 | background: theme.palette.primary.main, 1654 | content: '"we\\'re testing"', 1655 | }, 1656 | ), 1657 | }, 1658 | ), 1659 | }; 1660 | return output; 1661 | }; 1662 | `; 1663 | 1664 | expect(runLoader(css)).toBe(jss.trim()); 1665 | }); 1666 | 1667 | it(`supports mixins inside keyframes`, () => { 1668 | const css = ` 1669 | @keyframes my-animation { 1670 | 0% { 1671 | -mui-mixins: theme.mixins.customMixin1; 1672 | padding: 20px; 1673 | } 1674 | 1675 | 100% { 1676 | -mui-mixins: theme.mixins.customMixin2; 1677 | padding: 10px; 1678 | } 1679 | } 1680 | `; 1681 | 1682 | const jss = ` 1683 | module.exports = function cssToMuiLoader(theme) { 1684 | ${OBJECT_ASSIGN_POLYFILL.trim()} 1685 | return { 1686 | '@keyframes my-animation': { 1687 | '0%': cssToMuiLoaderAssign({}, theme.mixins.customMixin1, { 1688 | padding: '20px', 1689 | }), 1690 | '100%': cssToMuiLoaderAssign({}, theme.mixins.customMixin2, { 1691 | padding: '10px', 1692 | }), 1693 | }, 1694 | }; 1695 | }; 1696 | `; 1697 | 1698 | expect(runLoader(css)).toBe(jss.trim()); 1699 | }); 1700 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-to-mui-loader", 3 | "version": "2.0.1", 4 | "description": "Webpack loader for using external CSS files with Material UI", 5 | "keywords": [ 6 | "webpack", 7 | "loader", 8 | "css", 9 | "jss", 10 | "material", 11 | "ui" 12 | ], 13 | "license": "MIT", 14 | "author": "Cedric McDougal", 15 | "main": "css-to-mui-loader.js", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/mcdougal/css-to-mui-loader.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/mcdougal/css-to-mui-loader/issues" 22 | }, 23 | "homepage": "https://github.com/mcdougal/css-to-mui-loader#readme", 24 | "scripts": { 25 | "lint": "eslint *.js", 26 | "test": "jest" 27 | }, 28 | "devDependencies": { 29 | "eslint": "^6.0.1", 30 | "eslint-config-airbnb-base": "^13.2.0", 31 | "eslint-config-prettier": "^6.0.0", 32 | "eslint-plugin-import": "^2.18.0", 33 | "eslint-plugin-prettier": "^3.1.0", 34 | "jest": "^24.8.0", 35 | "prettier": "^1.18.2" 36 | }, 37 | "dependencies": { 38 | "css": "^2.2.4" 39 | } 40 | } 41 | --------------------------------------------------------------------------------