├── .eslintignore ├── .eslintrc.json ├── .flowconfig ├── .gitignore ├── .release-it.json ├── README.md ├── __fixtures__ ├── computed-properties │ ├── code.js │ └── output.js ├── nested-styles │ ├── code.js │ └── output.js ├── plain-object │ ├── code.js │ └── output.js ├── styled-call-expression │ ├── code.js │ └── output.js ├── styled-member-expression │ ├── code.js │ └── output.js └── vendor-prefixes │ ├── code.js │ └── output.js ├── __tests__ └── index.js ├── index.js ├── package.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /coverage/ 3 | /__fixtures__/ 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "satya164", 3 | 4 | "env": { 5 | "node": true 6 | }, 7 | 8 | "rules": { 9 | "import/no-commonjs": 0 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | 11 | [strict] 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # Typescript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "increment": "conventional:angular", 3 | "changelogCommand": "conventional-changelog -p angular | tail -n +3", 4 | "safeBump": false, 5 | "src": { 6 | "commitMessage": "chore: release %s", 7 | "tagName": "v%s" 8 | }, 9 | "npm": { 10 | "publish": true 11 | }, 12 | "github": { 13 | "release": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-object-styles-to-template 2 | 3 | Babel plugin to transpile object styles to template literal. 4 | 5 | The plugin will convert styles written in object syntax to tagged template literal which libraries like [`linaria`](https://github.com/callstack/linaria) and [`styled-components`](https://www.styled-components.com/) can consume. 6 | 7 | ## Usage 8 | 9 | Install the plugin: 10 | 11 | ```sh 12 | yarn add --dev babel-plugin-object-styles-to-template 13 | ``` 14 | 15 | Then include it in your `.babelrc`: 16 | 17 | ```json 18 | { 19 | "plugins": ["object-styles-to-template"] 20 | } 21 | ``` 22 | 23 | ## Example 24 | 25 | When you write the following: 26 | 27 | ```js 28 | const container = css({ 29 | flex: 1, 30 | padding: 10, 31 | backgroundColor: 'orange', 32 | color: colors.white, 33 | 34 | '&:hover': { 35 | backgroundColor: 'tomato', 36 | }, 37 | }); 38 | ``` 39 | 40 | It's transpiled to: 41 | 42 | ```js 43 | const container = css` 44 | flex: 1; 45 | padding: 10px; 46 | background-color: orange; 47 | color: ${colors.white}; 48 | 49 | &:hover { 50 | background-color: tomato; 51 | } 52 | `; 53 | ``` 54 | 55 | The styled components syntax is also supported. So when you write the following: 56 | 57 | ```js 58 | const FancyButton = styled(Button)({ 59 | backgroundColor: 'papayawhip', 60 | }); 61 | ``` 62 | 63 | It's transpiled to: 64 | 65 | ```js 66 | const FancyButton = styled(Button)` 67 | background-color: papayawhip; 68 | `; 69 | ``` 70 | 71 | ## Options 72 | 73 | You can selectively enable/disable the tags transpiled by the plugin: 74 | 75 | - `css: boolean`: Whether to transpile `css` tags. Default: `true`. 76 | - `styled: boolean`: Whether to transpile styled components like `styled` tags. Default `true`. 77 | 78 | To pass options to the plugin, you can use the following syntax: 79 | 80 | ```json 81 | { 82 | "plugins": [ 83 | ["object-styles-to-template", { "css": true, "styled": false }] 84 | ] 85 | } 86 | ``` 87 | -------------------------------------------------------------------------------- /__fixtures__/computed-properties/code.js: -------------------------------------------------------------------------------- 1 | const title = css({ 2 | [prop]: 'test', 3 | 'content': '""', 4 | }); 5 | -------------------------------------------------------------------------------- /__fixtures__/computed-properties/output.js: -------------------------------------------------------------------------------- 1 | const title = css` 2 | ${prop}: test; 3 | content: ""; 4 | `; 5 | -------------------------------------------------------------------------------- /__fixtures__/nested-styles/code.js: -------------------------------------------------------------------------------- 1 | const title = css({ 2 | '&:hover': { 3 | color: `blue`, 4 | }, 5 | 6 | section: { 7 | fontSize: 20, 8 | }, 9 | 10 | [test]: { 11 | fontSize: '3em', 12 | }, 13 | 14 | '@keyframes bounce': { 15 | '0%': { transform: 'scale(1.01)' }, 16 | '100%': { transform: 'scale(0.99)' } 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /__fixtures__/nested-styles/output.js: -------------------------------------------------------------------------------- 1 | const title = css` 2 | &:hover { 3 | color: ${`blue`}; 4 | } 5 | 6 | section { 7 | font-size: 20px; 8 | } 9 | 10 | ${test} { 11 | font-size: 3em; 12 | } 13 | 14 | @keyframes bounce { 15 | 0% { 16 | transform: scale(1.01); 17 | } 18 | 19 | 100% { 20 | transform: scale(0.99); 21 | } 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /__fixtures__/plain-object/code.js: -------------------------------------------------------------------------------- 1 | const title = css({ 2 | flex: 1, 3 | padding: 10, 4 | backgroundColor: 'orange', 5 | color: colors.blue, 6 | margin: spacing, 7 | }); 8 | -------------------------------------------------------------------------------- /__fixtures__/plain-object/output.js: -------------------------------------------------------------------------------- 1 | const title = css` 2 | flex: 1; 3 | padding: 10px; 4 | background-color: orange; 5 | color: ${colors.blue}; 6 | margin: ${spacing}; 7 | `; 8 | -------------------------------------------------------------------------------- /__fixtures__/styled-call-expression/code.js: -------------------------------------------------------------------------------- 1 | const FancyButton = styled(Button)({ 2 | backgroundColor: 'orange', 3 | color: colors.blue, 4 | margin: spacing, 5 | }); 6 | -------------------------------------------------------------------------------- /__fixtures__/styled-call-expression/output.js: -------------------------------------------------------------------------------- 1 | const FancyButton = styled(Button)` 2 | background-color: orange; 3 | color: ${colors.blue}; 4 | margin: ${spacing}; 5 | `; 6 | -------------------------------------------------------------------------------- /__fixtures__/styled-member-expression/code.js: -------------------------------------------------------------------------------- 1 | const FancyButton = styled.button({ 2 | backgroundColor: 'orange', 3 | color: props => props.color, 4 | margin: spacing, 5 | }); 6 | -------------------------------------------------------------------------------- /__fixtures__/styled-member-expression/output.js: -------------------------------------------------------------------------------- 1 | const FancyButton = styled.button` 2 | background-color: orange; 3 | color: ${props => props.color}; 4 | margin: ${spacing}; 5 | `; 6 | -------------------------------------------------------------------------------- /__fixtures__/vendor-prefixes/code.js: -------------------------------------------------------------------------------- 1 | const box = css({ 2 | WebkitOpacity: .8, 3 | MozOpacity: .8, 4 | msOpacity: .8, 5 | OOpacity: .8, 6 | WebkitBorderRadius: 10, 7 | MozBorderRadius: 10, 8 | msBorderRadius: 10, 9 | OBorderRadius: 10, 10 | }); 11 | -------------------------------------------------------------------------------- /__fixtures__/vendor-prefixes/output.js: -------------------------------------------------------------------------------- 1 | const box = css` 2 | -webkit-opacity: 0.8; 3 | -moz-opacity: 0.8; 4 | -ms-opacity: 0.8; 5 | -o-opacity: 0.8; 6 | -webkit-border-radius: 10px; 7 | -moz-border-radius: 10px; 8 | -ms-border-radius: 10px; 9 | -o-border-radius: 10px; 10 | `; 11 | -------------------------------------------------------------------------------- /__tests__/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | const path = require('path'); 4 | const tester = require('babel-plugin-tester'); 5 | 6 | tester({ 7 | plugin: require('../index'), 8 | pluginName: 'object-styles-to-template', 9 | fixtures: path.join(__dirname, '..', '__fixtures__'), 10 | }); 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | const hyphenate = s => 4 | // Hyphenate CSS property names from camelCase version from JS string 5 | // Special case for `-ms` because in JS it starts with `ms` unlike `Webkit` 6 | s.replace(/([A-Z])/g, g => `-${g[0].toLowerCase()}`).replace(/^ms-/, '-ms-'); 7 | 8 | const unitless = { 9 | animationIterationCount: true, 10 | borderImageOutset: true, 11 | borderImageSlice: true, 12 | borderImageWidth: true, 13 | boxFlex: true, 14 | boxFlexGroup: true, 15 | boxOrdinalGroup: true, 16 | columnCount: true, 17 | columns: true, 18 | flex: true, 19 | flexGrow: true, 20 | flexPositive: true, 21 | flexShrink: true, 22 | flexNegative: true, 23 | flexOrder: true, 24 | gridRow: true, 25 | gridRowEnd: true, 26 | gridRowSpan: true, 27 | gridRowStart: true, 28 | gridColumn: true, 29 | gridColumnEnd: true, 30 | gridColumnSpan: true, 31 | gridColumnStart: true, 32 | fontWeight: true, 33 | lineClamp: true, 34 | lineHeight: true, 35 | opacity: true, 36 | order: true, 37 | orphans: true, 38 | tabSize: true, 39 | widows: true, 40 | zIndex: true, 41 | zoom: true, 42 | 43 | // SVG-related properties 44 | fillOpacity: true, 45 | floodOpacity: true, 46 | stopOpacity: true, 47 | strokeDasharray: true, 48 | strokeDashoffset: true, 49 | strokeMiterlimit: true, 50 | strokeOpacity: true, 51 | strokeWidth: true, 52 | }; 53 | 54 | /* :: 55 | type Options = { 56 | css?: boolean, 57 | styled?: boolean 58 | } 59 | */ 60 | 61 | module.exports = function(babel /*: any */, options /* : Options */ = {}) { 62 | const { types: t } = babel; 63 | 64 | return { 65 | visitor: { 66 | CallExpression(path /*: any */) { 67 | const { callee, arguments: args } = path.node; 68 | 69 | if ( 70 | ((options.css !== false && 71 | t.isIdentifier(callee) && 72 | callee.name === 'css') || // Matches `css` 73 | (options.styled !== false && 74 | t.isCallExpression(callee) && 75 | t.isIdentifier(callee.callee) && 76 | callee.arguments.length === 1 && 77 | callee.callee.name === 'styled') || // Matches `styled(Button)` 78 | (options.styled !== false && 79 | t.isMemberExpression(callee) && 80 | t.isIdentifier(callee.object) && 81 | t.isIdentifier(callee.property) && 82 | callee.object.name === 'styled')) && // Matches `styled.button` 83 | args.length === 1 && 84 | t.isObjectExpression(args[0]) 85 | ) { 86 | const quasis = []; 87 | const expressions = []; 88 | 89 | let text = ''; 90 | 91 | const indentation = ' '; 92 | const finalize = (expr, str) => { 93 | quasis.push(t.templateElement({ raw: text })); 94 | expressions.push(expr); 95 | text = str; 96 | }; 97 | const serialize = (styles, level = 1) => { 98 | const indent = indentation.repeat(level); 99 | 100 | styles.forEach((prop, i) => { 101 | if (t.isObjectExpression(prop.value)) { 102 | if (i !== 0) { 103 | text += '\n'; 104 | } 105 | 106 | if (prop.computed) { 107 | text += `\n${indent}`; 108 | finalize(prop.key, ' {'); 109 | } else { 110 | let key; 111 | 112 | if (t.isIdentifier(prop.key)) { 113 | key = prop.key.name; 114 | } else { 115 | key = prop.key.value; 116 | } 117 | 118 | text += `\n${indent}${key} {`; 119 | } 120 | 121 | serialize(prop.value.properties, level + 1); 122 | text += `\n${indent}}`; 123 | return; 124 | } 125 | 126 | let key; 127 | 128 | if (prop.computed) { 129 | text += `\n${indent}`; 130 | finalize(prop.key, ': '); 131 | } else { 132 | if (t.isIdentifier(prop.key)) { 133 | key = prop.key.name; 134 | } else { 135 | key = prop.key.value; 136 | } 137 | 138 | text += `\n${indent}${hyphenate(key)}: `; 139 | } 140 | 141 | if ( 142 | t.isStringLiteral(prop.value) || 143 | t.isNumericLiteral(prop.value) 144 | ) { 145 | let value = prop.value.value; 146 | 147 | if ( 148 | t.isNumericLiteral(prop.value) && 149 | key && 150 | !unitless[ 151 | // Strip vendor prefixes when checking if the value is unitless 152 | key.replace( 153 | /^(Webkit|Moz|O|ms)([A-Z])(.+)$/, 154 | (match, p1, p2, p3) => `${p2.toLowerCase()}${p3}` 155 | ) 156 | ] 157 | ) { 158 | value += 'px'; 159 | } 160 | 161 | text += `${value};`; 162 | } else { 163 | finalize(prop.value, ';'); 164 | } 165 | }); 166 | }; 167 | 168 | serialize(args[0].properties); 169 | quasis.push(t.templateElement({ raw: `${text}\n` })); 170 | 171 | const start = path.node.loc.start; 172 | 173 | path.replaceWith( 174 | t.taggedTemplateExpression( 175 | callee, 176 | t.templateLiteral(quasis, expressions) 177 | ) 178 | ); 179 | path.node.loc = { start }; 180 | path.requeue(); 181 | } 182 | }, 183 | }, 184 | }; 185 | }; 186 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-object-styles-to-template", 3 | "version": "0.2.2", 4 | "description": "Babel plugin to transpile object styles to template literal", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "author": "Satyajit Sahoo ", 8 | "repository": "git@github.com:satya164/babel-plugin-object-styles-to-template.git", 9 | "bugs": { 10 | "url": "https://github.com/satya164/babel-plugin-object-styles-to-template/issues" 11 | }, 12 | "homepage": "https://github.com/satya164/babel-plugin-object-styles-to-template#readme", 13 | "keywords": [ 14 | "react", 15 | "css", 16 | "css-in-js", 17 | "styled-components", 18 | "babel-plugin", 19 | "babel" 20 | ], 21 | "scripts": { 22 | "lint": "eslint .", 23 | "flow": "flow", 24 | "test": "jest", 25 | "release": "release-it" 26 | }, 27 | "publishConfig": { 28 | "registry": "https://registry.npmjs.org/" 29 | }, 30 | "devDependencies": { 31 | "@babel/core": "^7.1.0", 32 | "babel-plugin-tester": "^5.5.1", 33 | "conventional-changelog-cli": "^2.0.5", 34 | "dedent": "^0.7.0", 35 | "eslint": "^5.6.0", 36 | "eslint-config-satya164": "^2.0.1", 37 | "flow-bin": "^0.81.0", 38 | "jest": "^23.6.0", 39 | "prettier": "^1.14.3", 40 | "release-it": "^7.6.1" 41 | } 42 | } 43 | --------------------------------------------------------------------------------