├── .gitattributes ├── .gitignore ├── compiler.js ├── examples └── nuxt │ ├── nuxt.config.js │ ├── package.json │ └── pages │ └── index.vue ├── .prettierrc ├── lib ├── compiler.js ├── index.js ├── units.js ├── parseComponent.js └── parseScript.js ├── nuxt ├── plugin.js └── index.js ├── example ├── index.js ├── poi.config.js └── App.vue ├── .editorconfig ├── poi └── index.js ├── circle.yml ├── .github └── FUNDING.yml ├── LICENSE ├── package.json ├── test ├── parseComponent.test.js └── __snapshots__ │ └── parseComponent.test.js.snap └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nuxt 3 | -------------------------------------------------------------------------------- /compiler.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/compiler') 2 | -------------------------------------------------------------------------------- /examples/nuxt/nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | modules: ['styled-vue/nuxt'] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "bracketSpacing": true 5 | } 6 | -------------------------------------------------------------------------------- /lib/compiler.js: -------------------------------------------------------------------------------- 1 | module.exports = require('vue-template-compiler') 2 | module.exports.parseComponent = require('./parseComponent') 3 | -------------------------------------------------------------------------------- /nuxt/plugin.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | // eslint-disable-next-line import/no-unresolved 3 | import { StyledVue } from 'styled-vue' 4 | 5 | Vue.use(StyledVue) 6 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { StyledVue } from 'styled-vue' 3 | import App from './App.vue' 4 | 5 | Vue.use(StyledVue) 6 | 7 | new Vue({ 8 | el: '#app', 9 | render: h => h(App) 10 | }) 11 | -------------------------------------------------------------------------------- /examples/nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "nuxt": "^2.11.0", 8 | "styled-vue": "^0.3.3" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /poi/index.js: -------------------------------------------------------------------------------- 1 | exports.name = 'styled-vue' 2 | 3 | exports.apply = api => { 4 | api.hook('createWebpackChain', config => { 5 | config.module 6 | .rule('vue') 7 | .use('vue-loader') 8 | .tap(options => { 9 | return Object.assign({}, options, { compiler: require('../compiler') }) 10 | }) 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /nuxt/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = function({ registerStyledVue = true } = {}) { 4 | // Lazy load compiler on build only 5 | this.nuxt.hook('build:before', () => { 6 | const { vue } = this.options.build.loaders 7 | vue.compiler = require('../lib/compiler') 8 | }) 9 | 10 | if (registerStyledVue) { 11 | this.addPlugin(path.join(__dirname, 'plugin.js')) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/poi.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: path.join(__dirname, 'index.js'), 5 | chainWebpack(config) { 6 | config.module 7 | .rule('vue') 8 | .use('vue-loader') 9 | .tap(options => { 10 | options.compiler = require('../compiler') 11 | return options 12 | }) 13 | 14 | config.resolve.alias.set('styled-vue', path.join(__dirname, '../lib')) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/nuxt/pages/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | Object.defineProperty(exports, '__esModule', { value: true }) 3 | 4 | function css(_) { 5 | throw new Error( 6 | `You need to replace vue-template-compiler with styled-vue/compiler` 7 | ) 8 | } 9 | 10 | function StyledVue(Vue) { 11 | Vue.component('styled-vue-global-css', { 12 | render: function(h) { 13 | var globalStyle = this.$parent.$options.globalStyle(this.$parent) 14 | var css = '' 15 | for (var key in globalStyle) { 16 | css += key + ':' + globalStyle[key] + ';' 17 | } 18 | return h('style', {}, [':root {' + css + '}']) 19 | } 20 | }) 21 | } 22 | 23 | exports.css = css 24 | 25 | exports.StyledVue = StyledVue 26 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | 2 | version: 2 3 | jobs: 4 | build: 5 | docker: 6 | - image: circleci/node:latest 7 | branches: 8 | ignore: 9 | - gh-pages # list of branches to ignore 10 | - /release\/.*/ # or ignore regexes 11 | steps: 12 | - checkout 13 | - restore_cache: 14 | key: dependency-cache-{{ checksum "yarn.lock" }} 15 | - run: 16 | name: install dependences 17 | command: yarn 18 | - save_cache: 19 | key: dependency-cache-{{ checksum "yarn.lock" }} 20 | paths: 21 | - ./node_modules 22 | - run: 23 | name: test 24 | command: yarn test 25 | - run: 26 | name: release 27 | command: yarn semantic-release 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: egoist # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /example/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 45 | -------------------------------------------------------------------------------- /lib/units.js: -------------------------------------------------------------------------------- 1 | // https://www.w3.org/TR/css-values-4/ 2 | const UNITS = [ 3 | // font relative lengths 4 | 'em', 5 | 'ex', 6 | 'cap', 7 | 'ch', 8 | 'ic', 9 | 'rem', 10 | 'lh', 11 | 'rlh', 12 | 13 | // viewport percentage lengths 14 | 'vw', 15 | 'vh', 16 | 'vi', 17 | 'vb', 18 | 'vmin', 19 | 'vmax', 20 | 21 | // absolute lengths 22 | 'cm', 23 | 'mm', 24 | 'Q', 25 | 'in', 26 | 'pc', 27 | 'pt', 28 | 'px', 29 | 30 | // angle units 31 | 'deg', 32 | 'grad', 33 | 'rad', 34 | 'turn', 35 | 36 | // duration units 37 | 's', 38 | 'ms', 39 | 40 | // frequency units 41 | 'Hz', 42 | 'kHz', 43 | 44 | // resolution units 45 | 'dpi', 46 | 'dpcm', 47 | 'dppx', 48 | 'x', 49 | 50 | // https://www.w3.org/TR/css-grid-1/#fr-unit 51 | 'fr', 52 | 53 | // percentages 54 | '%' 55 | ] 56 | 57 | const UNITS_RE = new RegExp(`(${UNITS.join('|')});?(\\s|\\n|$)`) 58 | 59 | module.exports = { 60 | UNITS, 61 | UNITS_RE 62 | } 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) EGOIST <0x142857@gmail.com> (https://egoist.sh) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "styled-vue", 3 | "version": "0.1.3", 4 | "description": "Zero-runtime CSS-in-JS for Vue.js.", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib", 8 | "nuxt", 9 | "poi", 10 | "compiler.js" 11 | ], 12 | "scripts": { 13 | "test": "npm run lint && jest", 14 | "lint": "xo", 15 | "toc": "markdown-toc README.md -i", 16 | "example": "poi --config example/poi.config.js -s" 17 | }, 18 | "repository": { 19 | "url": "egoist/styled-vue", 20 | "type": "git" 21 | }, 22 | "author": "egoist<0x142857@gmail.com>", 23 | "license": "MIT", 24 | "dependencies": { 25 | "@babel/generator": "^7.2.2", 26 | "@babel/parser": "^7.2.3", 27 | "@babel/traverse": "^7.2.3", 28 | "@babel/types": "^7.2.2", 29 | "hash-sum": "^2.0.0", 30 | "posthtml": "^0.11.3", 31 | "vue-template-compiler": "^2.6.11" 32 | }, 33 | "devDependencies": { 34 | "commitizen": "^3.0.5", 35 | "cz-conventional-changelog": "^2.1.0", 36 | "eslint-config-prettier": "^3.3.0", 37 | "eslint-config-rem": "^4.0.0", 38 | "eslint-plugin-prettier": "^3.0.0", 39 | "husky": "^1.0.0-rc.13", 40 | "jest": "^23.6.0", 41 | "lint-staged": "^7.2.0", 42 | "markdown-toc": "^1.2.0", 43 | "poi": "^12.7.5", 44 | "polished": "^2.3.1", 45 | "prettier": "^1.15.2", 46 | "semantic-release": "^15.13.3", 47 | "vue": "^2.6.11", 48 | "xo": "^0.23.0" 49 | }, 50 | "xo": { 51 | "extends": [ 52 | "rem", 53 | "plugin:prettier/recommended" 54 | ], 55 | "ignores": [ 56 | "**/example/**" 57 | ], 58 | "envs": [ 59 | "jest" 60 | ], 61 | "rules": { 62 | "unicorn/filename-case": "off", 63 | "unicorn/no-abusive-eslint-disable": "off" 64 | } 65 | }, 66 | "husky": { 67 | "hooks": { 68 | "pre-commit": "lint-staged" 69 | } 70 | }, 71 | "lint-staged": { 72 | "*.js": [ 73 | "xo --fix", 74 | "git add" 75 | ], 76 | "README.md": [ 77 | "markdown-toc -i", 78 | "prettier --write", 79 | "git add" 80 | ], 81 | "*.{json,md,vue}": [ 82 | "prettier --write", 83 | "git add" 84 | ] 85 | }, 86 | "release": { 87 | "branch": "master" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/parseComponent.js: -------------------------------------------------------------------------------- 1 | const { parseComponent } = require('vue-template-compiler') 2 | const posthtml = require('posthtml') 3 | const parseScript = require('./parseScript') 4 | 5 | module.exports = (content, opts) => { 6 | const sfc = parseComponent(content, opts) 7 | 8 | if (sfc.script) { 9 | const { 10 | style, 11 | styleLang, 12 | hasVars, 13 | scriptContent, 14 | hasGlobalVars, 15 | globalStyle, 16 | globalStyleLang 17 | } = parseScript(sfc.script) 18 | 19 | sfc.script.content = scriptContent 20 | 21 | if (sfc.template && (hasVars || hasGlobalVars)) { 22 | sfc.template.content = posthtml([ 23 | tree => { 24 | for (const node of tree) { 25 | if (node.tag) { 26 | if (hasVars) { 27 | node.attrs = node.attrs || {} 28 | const existing = 29 | node.attrs[':style'] || node.attrs['v-bind:style'] 30 | node.attrs[':style'] = `$options.style(this, ${existing})` 31 | } 32 | if (hasGlobalVars) { 33 | node.content = node.content || [] 34 | node.content.unshift({ 35 | tag: 'styled-vue-global-css' 36 | }) 37 | } 38 | break 39 | } 40 | } 41 | return tree 42 | } 43 | ]).process(sfc.template.content, { 44 | sync: true, 45 | recognizeSelfClosing: true 46 | }).html 47 | } 48 | 49 | let contentLength = content.length 50 | 51 | const addStyleTag = (styleContent, styleLang, isGlobal) => { 52 | const style = { 53 | type: 'style', 54 | content: styleContent, 55 | attrs: {}, 56 | // TODO: this might be wrong 57 | start: contentLength, 58 | end: contentLength + styleContent.length 59 | } 60 | if (styleLang) { 61 | style.lang = styleLang 62 | style.attrs.lang = styleLang 63 | } 64 | if (!isGlobal) { 65 | style.scoped = true 66 | style.attrs.scoped = true 67 | } 68 | sfc.styles.push(style) 69 | contentLength += styleContent.length 70 | } 71 | 72 | if (globalStyle) { 73 | addStyleTag(globalStyle, globalStyleLang, true) 74 | } 75 | 76 | if (style) { 77 | addStyleTag(style, styleLang, false) 78 | } 79 | } 80 | 81 | return sfc 82 | } 83 | -------------------------------------------------------------------------------- /test/parseComponent.test.js: -------------------------------------------------------------------------------- 1 | const parseComponent = require('../lib/parseComponent') 2 | 3 | function snapshot(title, source) { 4 | test(title, () => { 5 | const sfc = parseComponent(source) 6 | const output = ` 7 | 10 | 11 | 14 | 15 | ${sfc.styles 16 | .map( 17 | style => ` 18 | 21 | ` 22 | ) 23 | .join('\n')} 24 | ` 25 | expect(` 26 | === source === 27 | ${source} 28 | 29 | === output === 30 | ${output} 31 | `).toMatchSnapshot() 32 | }) 33 | } 34 | 35 | snapshot( 36 | 'simple', 37 | ` 38 | 41 | 52 | ` 53 | ) 54 | 55 | snapshot( 56 | 'no css', 57 | ` 58 | 61 | 64 | ` 65 | ) 66 | 67 | snapshot( 68 | 'no vars', 69 | ` 70 | 73 | 79 | ` 80 | ) 81 | 82 | snapshot( 83 | 'global style', 84 | ` 85 | 88 | 94 | ` 95 | ) 96 | 97 | snapshot( 98 | 'global style with scoped style', 99 | ` 100 | 103 | 110 | ` 111 | ) 112 | 113 | snapshot( 114 | 'global style with scoped style with css variables', 115 | ` 116 | 119 | 126 | ` 127 | ) 128 | -------------------------------------------------------------------------------- /test/__snapshots__/parseComponent.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`global style 1`] = ` 4 | " 5 | === source === 6 | 7 | 10 | 16 | 17 | 18 | === output === 19 | 20 | 25 | 26 | 32 | 33 | 34 | 37 | 38 | 39 | " 40 | `; 41 | 42 | exports[`global style with scoped style 1`] = ` 43 | " 44 | === source === 45 | 46 | 49 | 56 | 57 | 58 | === output === 59 | 60 | 65 | 66 | 74 | 75 | 76 | 79 | 80 | 81 | 84 | 85 | 86 | " 87 | `; 88 | 89 | exports[`global style with scoped style with css variables 1`] = ` 90 | " 91 | === source === 92 | 93 | 96 | 103 | 104 | 105 | === output === 106 | 107 | 112 | 113 | 126 | 127 | 128 | 131 | 132 | 133 | 136 | 137 | 138 | " 139 | `; 140 | 141 | exports[`no css 1`] = ` 142 | " 143 | === source === 144 | 145 | 148 | 151 | 152 | 153 | === output === 154 | 155 | 160 | 161 | 164 | 165 | 166 | 167 | " 168 | `; 169 | 170 | exports[`no vars 1`] = ` 171 | " 172 | === source === 173 | 174 | 177 | 183 | 184 | 185 | === output === 186 | 187 | 192 | 193 | 199 | 200 | 201 | 204 | 205 | 206 | " 207 | `; 208 | 209 | exports[`simple 1`] = ` 210 | " 211 | === source === 212 | 213 | 216 | 227 | 228 | 229 | === output === 230 | 231 | 236 | 237 | 250 | 251 | 252 | 260 | 261 | 262 | " 263 | `; 264 | -------------------------------------------------------------------------------- /lib/parseScript.js: -------------------------------------------------------------------------------- 1 | const parser = require('@babel/parser') 2 | const traverse = require('@babel/traverse') 3 | const generator = require('@babel/generator') 4 | const t = require('@babel/types') 5 | const hashSum = require('hash-sum') 6 | const { UNITS_RE } = require('./units') 7 | 8 | const LANGS = ['css', 'stylus', 'less', 'sass', 'scss'] 9 | 10 | module.exports = script => { 11 | let style 12 | let styleLang 13 | let globalStyle 14 | let globalStyleLang 15 | let hasVars = false 16 | let hasGlobalVars = false 17 | 18 | const ast = parser.parse(script.content, { 19 | sourceType: 'module', 20 | plugins: [ 21 | 'jsx', 22 | 'typescript', 23 | 'objectRestSpread', 24 | 'classProperties', 25 | 'decorators-legacy', 26 | 'dynamicImport' 27 | ] 28 | }) 29 | 30 | const parseTaggedTemplate = ref => { 31 | const { quasi } = ref.node 32 | const quasis = [] 33 | const vars = [] 34 | const varDeclarations = [] 35 | 36 | for (const [i, q] of quasi.quasis.entries()) { 37 | quasis.push(q) 38 | if (quasi.expressions[i]) { 39 | const variableId = `v${hashSum(quasi.expressions[i])}` 40 | const value = `var(--${variableId})` 41 | quasis.push({ 42 | type: 'TemplateElement', 43 | value: { raw: value, cooked: value } 44 | }) 45 | 46 | // Check unit 47 | let unit 48 | const nextQuasis = quasi.quasis[i + 1] 49 | if (nextQuasis) { 50 | // Test the next 6 chars 51 | const match = UNITS_RE.exec(nextQuasis.value.raw.slice(0, 6)) 52 | unit = match && match[1] 53 | if (unit) { 54 | nextQuasis.value.raw = nextQuasis.value.raw.slice(unit.length) 55 | nextQuasis.value.cooked = nextQuasis.value.cooked.slice(unit.length) 56 | } 57 | } 58 | 59 | // var v0 = vm => vm.color 60 | varDeclarations.push( 61 | t.variableDeclaration('var', [ 62 | t.variableDeclarator(t.identifier(variableId), quasi.expressions[i]) 63 | ]) 64 | ) 65 | 66 | // { '--v0': v0(vm) } 67 | // { '--v0': v0(vm) + unit } 68 | const id = t.identifier(variableId) 69 | const expType = quasi.expressions[i].type 70 | const mustBeFunction = [ 71 | 'FunctionExpression', 72 | 'ArrowFunctionExpression' 73 | ].includes(expType) 74 | // TODO: add more conditions 75 | const mustNotBeFunction = 76 | expType.endsWith('Literal') || ['BinaryExpression'].includes(expType) 77 | const getValue = isFunction => { 78 | let leftExp 79 | if (isFunction) { 80 | leftExp = t.callExpression(id, [t.identifier('vm')]) 81 | } else { 82 | leftExp = id 83 | } 84 | return unit 85 | ? t.binaryExpression('+', leftExp, t.stringLiteral(unit)) 86 | : leftExp 87 | } 88 | vars.push( 89 | t.objectProperty( 90 | t.stringLiteral(`--${variableId}`), 91 | mustBeFunction 92 | ? getValue(true) 93 | : mustNotBeFunction 94 | ? getValue(false) 95 | : t.conditionalExpression( 96 | t.binaryExpression( 97 | '===', 98 | t.unaryExpression('typeof', id), 99 | t.stringLiteral('function') 100 | ), 101 | getValue(true), 102 | getValue(false) 103 | ) 104 | ) 105 | ) 106 | } 107 | } 108 | 109 | const str = quasis.reduce((res, next) => { 110 | res += next.value.raw 111 | return res 112 | }, '') 113 | 114 | ref.node.quasi.quasis = [ 115 | { 116 | type: 'TemplateElement', 117 | value: { raw: str, cooked: str }, 118 | tail: true 119 | } 120 | ] 121 | ref.node.quasi.expressions = [] 122 | 123 | return { 124 | extractedStyle: ref.get('quasi').evaluate().value, 125 | vars, 126 | varDeclarations 127 | } 128 | } 129 | 130 | traverse.default(ast, { 131 | ImportDeclaration(path) { 132 | if (path.node.source.value !== 'styled-vue') { 133 | return 134 | } 135 | 136 | for (const specifier of path.node.specifiers) { 137 | if (specifier.type !== 'ImportSpecifier') { 138 | continue 139 | } 140 | 141 | const lang = specifier.imported.name 142 | 143 | if (!LANGS.includes(lang)) { 144 | throw new Error(`[styled-vue] "${lang}" is not supported`) 145 | } 146 | 147 | const binding = path.scope.getBinding(specifier.local.name) 148 | 149 | for (let i = 0; i < binding.referencePaths.length; i++) { 150 | // The tagged template path 151 | const ref = binding.referencePaths[i].parentPath 152 | // The object property path 153 | const propertyPath = ref.parentPath 154 | 155 | if ( 156 | !propertyPath || 157 | propertyPath.node.type !== 'ObjectProperty' || 158 | !['style', 'globalStyle'].includes(propertyPath.node.key.name) 159 | ) { 160 | throw new Error( 161 | `css tag must be assigned to component property "style" or "globalStyle"` 162 | ) 163 | } 164 | 165 | const isGlobal = propertyPath.node.key.name === 'globalStyle' 166 | const { vars, varDeclarations, extractedStyle } = parseTaggedTemplate( 167 | ref 168 | ) 169 | if (isGlobal) { 170 | globalStyle = extractedStyle 171 | globalStyleLang = lang 172 | if (vars.length > 0) { 173 | hasGlobalVars = true 174 | } 175 | } else { 176 | style = extractedStyle 177 | styleLang = lang 178 | if (vars.length > 0) { 179 | hasVars = true 180 | } 181 | } 182 | 183 | if (vars.length > 0) { 184 | ref.replaceWith( 185 | t.functionExpression( 186 | null, 187 | [ 188 | t.identifier('vm'), 189 | !isGlobal && t.identifier('existing') // Global vars are handled differently 190 | ].filter(Boolean), 191 | t.blockStatement([ 192 | ...varDeclarations, 193 | t.returnStatement( 194 | isGlobal 195 | ? t.objectExpression(vars) 196 | : t.arrayExpression([ 197 | t.identifier('existing'), 198 | t.objectExpression(vars) 199 | ]) 200 | ) 201 | ]) 202 | ) 203 | ) 204 | } else { 205 | const NoVarsFound = t.identifier('undefined') 206 | NoVarsFound.trailingComments = [ 207 | { type: 'CommentLine', value: ' No CSS variables' } 208 | ] 209 | ref.replaceWith(NoVarsFound) 210 | } 211 | } 212 | } 213 | 214 | // Remove the import 215 | path.remove() 216 | } 217 | }) 218 | 219 | return { 220 | style, 221 | styleLang, 222 | globalStyle, 223 | globalStyleLang, 224 | hasGlobalVars, 225 | hasVars, 226 | scriptContent: generator.default(ast).code 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ATTENTION: this project is not actively maintained for now (I still push some code once in a while), if you want to see improvements or get support, please consider [sponsoring me](https://github.com/sponsors/egoist). 2 | 3 | ![styled-vue](https://user-images.githubusercontent.com/8784712/50833110-30b41180-138b-11e9-9ddd-20af6afc1747.png) 4 | 5 | --- 6 | 7 | [![NPM version](https://flat.badgen.net/npm/v/styled-vue?scale=1.5)](https://npmjs.com/package/styled-vue) [![NPM downloads](https://flat.badgen.net/npm/dm/styled-vue?scale=1.5)](https://npmjs.com/package/styled-vue) [![CircleCI](https://flat.badgen.net/circleci/github/egoist/styled-vue/master?scale=1.5)](https://circleci.com/gh/egoist/styled-vue/tree/master) [![chat](https://flat.badgen.net/badge/chat%20on/discord/7289DA?scale=1.5)](https://chat.egoist.sh) 8 | 9 | **Please consider [donating](https://github.com/sponsors/egoist) to this project's author, [EGOIST](#author), to show your ❤️ and support.** 10 | 11 | ## Features 12 | 13 | - Zero-runtime CSS-in-JS for Vue SFC without compromise 14 | - Scoped CSS by default 15 | - CSS preprocessors support 16 | - Dynamic style just works (no IE support) 17 | - Working with SSR out-of-the-box 18 | - Hot module replacement still works 19 | - You get all the features without any config! 20 | 21 | ## Table of Contents 22 | 23 | 24 | 25 | - [Install](#install) 26 | - [Example](#example) 27 | - [How to use](#how-to-use) 28 | - [Using with webpack](#using-with-webpack) 29 | - [Using with Vue CLI](#using-with-vue-cli) 30 | - [Using with Poi](#using-with-poi) 31 | - [Using with Nuxt.js](#using-with-nuxtjs) 32 | - [How does it work](#how-does-it-work) 33 | - [CSS Preprocessors](#css-preprocessors) 34 | - [Global Styles](#global-styles) 35 | - [TypeScript](#typescript) 36 | - [Editor Plugins](#editor-plugins) 37 | - [VSCode](#vscode) 38 | - [Atom](#atom) 39 | - [Inspirations](#inspirations) 40 | - [Contributing](#contributing) 41 | - [Author](#author) 42 | 43 | 44 | 45 | ## Install 46 | 47 | ```bash 48 | yarn add styled-vue --dev 49 | ``` 50 | 51 | Then register the Vue plugin (**optional**): 52 | 53 | ```js 54 | import Vue from 'vue' 55 | import { StyledVue } from 'styled-vue' 56 | 57 | Vue.use(StyledVue) 58 | ``` 59 | 60 | So far the plugin is only required for [globalStyle](#global-styles), if you only need scoped style, you can safely skip this. 61 | 62 | ## Example 63 | 64 | ```vue 65 | 68 | 69 | 84 | ``` 85 | 86 | And that's it, it's like writing `.vue` file's scoped CSS in the ` 169 | ``` 170 | 171 | Output: 172 | 173 | ```vue 174 | 177 | 178 | 190 | 191 | 197 | ``` 198 | 199 | Under the hood, it uses [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables) for dynamic styles, that's why this feature is not supported in IE. 200 | 201 | ### CSS Preprocessors 202 | 203 | ```js 204 | import { less, sass, scss, stylus } from 'styled-vue' 205 | ``` 206 | 207 | Just use corresponding exports from `styled-vue`. 208 | 209 | The CSS will be passed to `vue-loader` and parsed by PostCSS if a `postcss.config.js` exists in your project, so it really just works like `