├── test ├── fixtures │ ├── style-import.css │ ├── template-import.jade │ ├── style-import-scoped.css │ ├── script-import.vue │ ├── style-export.vue │ ├── postcss.vue │ ├── template-import.vue │ ├── pug.vue │ ├── media-query.vue │ ├── style-import.vue │ ├── script-import.js │ ├── basic.vue │ ├── scoped-css.vue │ └── pre-processors.vue └── test.js ├── .gitignore ├── circle.yml ├── .eslintrc ├── lib ├── gen-id.js ├── compilers │ ├── index.js │ ├── pug.js │ ├── jade.js │ ├── coffee.js │ ├── less.js │ ├── stylus.js │ ├── sass.js │ └── babel.js ├── normalize.js ├── insert-css.js ├── template-compiler.js ├── ensure-require.js ├── style-rewriter.js └── compiler.js ├── plugins └── extract-css.js ├── LICENSE ├── index.js ├── package.json └── README.md /test/fixtures/style-import.css: -------------------------------------------------------------------------------- 1 | h1 { color: red; } 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | test/temp 4 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6 4 | -------------------------------------------------------------------------------- /test/fixtures/template-import.jade: -------------------------------------------------------------------------------- 1 | div 2 | h1 hello 3 | -------------------------------------------------------------------------------- /test/fixtures/style-import-scoped.css: -------------------------------------------------------------------------------- 1 | h1 { color: green; } 2 | -------------------------------------------------------------------------------- /test/fixtures/script-import.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/style-export.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/fixtures/postcss.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /test/fixtures/template-import.vue: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "vue", 4 | "env": { 5 | "mocha": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/pug.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /test/fixtures/media-query.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /test/fixtures/style-import.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /test/fixtures/script-import.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data () { 3 | return { 4 | msg: 'Hello from Component A!' 5 | } 6 | } 7 | }; -------------------------------------------------------------------------------- /lib/gen-id.js: -------------------------------------------------------------------------------- 1 | // utility for generating a uid for each component file 2 | // used in scoped CSS rewriting 3 | var hash = require('hash-sum') 4 | var cache = Object.create(null) 5 | 6 | module.exports = function genId (file) { 7 | return cache[file] || (cache[file] = hash(file)) 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/basic.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /lib/compilers/index.js: -------------------------------------------------------------------------------- 1 | // built-in compilers 2 | module.exports = { 3 | coffee: require('./coffee'), 4 | babel: require('./babel'), 5 | less: require('./less'), 6 | sass: require('./sass'), 7 | scss: require('./sass'), 8 | stylus: require('./stylus'), 9 | jade: require('./jade'), 10 | pug: require('./pug') 11 | } 12 | -------------------------------------------------------------------------------- /lib/compilers/pug.js: -------------------------------------------------------------------------------- 1 | var ensureRequire = require('../ensure-require.js') 2 | 3 | module.exports = function (raw, cb, compiler) { 4 | ensureRequire('pug', 'pug') 5 | var pug = require('pug') 6 | try { 7 | var html = pug.compile(raw, compiler.options.pug || {})() 8 | } catch (err) { 9 | return cb(err) 10 | } 11 | cb(null, html) 12 | } 13 | -------------------------------------------------------------------------------- /lib/compilers/jade.js: -------------------------------------------------------------------------------- 1 | var ensureRequire = require('../ensure-require.js') 2 | 3 | module.exports = function (raw, cb, compiler) { 4 | ensureRequire('jade', 'jade') 5 | var jade = require('jade') 6 | try { 7 | var html = jade.compile(raw, compiler.options.jade || {})() 8 | } catch (err) { 9 | return cb(err) 10 | } 11 | cb(null, html) 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/scoped-css.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /lib/normalize.js: -------------------------------------------------------------------------------- 1 | var IS_TEST = !!process.env.VUEIFY_TEST 2 | var fs = require('fs') 3 | var path = require('path') 4 | 5 | exports.lib = function (file) { 6 | if (IS_TEST) { 7 | return path.resolve(__dirname, file) 8 | } else { 9 | return 'vueify/lib/' + file 10 | } 11 | } 12 | 13 | exports.dep = function (dep) { 14 | if (IS_TEST) { 15 | return dep 16 | } else if (fs.existsSync(path.resolve(__dirname, '../node_modules', dep))) { 17 | // npm 2 or npm linked 18 | return 'vueify/node_modules/' + dep 19 | } else { 20 | // npm 3 21 | return dep 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/fixtures/pre-processors.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | 22 | 23 | 29 | 30 | 35 | -------------------------------------------------------------------------------- /lib/insert-css.js: -------------------------------------------------------------------------------- 1 | var inserted = exports.cache = {} 2 | 3 | function noop () {} 4 | 5 | exports.insert = function (css) { 6 | if (inserted[css]) return noop 7 | inserted[css] = true 8 | 9 | var elem = document.createElement('style') 10 | elem.setAttribute('type', 'text/css') 11 | 12 | if ('textContent' in elem) { 13 | elem.textContent = css 14 | } else { 15 | elem.styleSheet.cssText = css 16 | } 17 | 18 | document.getElementsByTagName('head')[0].appendChild(elem) 19 | return function () { 20 | document.getElementsByTagName('head')[0].removeChild(elem) 21 | inserted[css] = false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/compilers/coffee.js: -------------------------------------------------------------------------------- 1 | var ensureRequire = require('../ensure-require.js') 2 | 3 | module.exports = function (raw, cb, compiler) { 4 | ensureRequire('coffee', ['coffee-script']) 5 | var coffee = require('coffee-script') 6 | var compiled 7 | try { 8 | compiled = coffee.compile(raw, compiler.options.coffee || { 9 | bare: true, 10 | sourceMap: compiler.options.sourceMap 11 | }) 12 | } catch (err) { 13 | return cb(err) 14 | } 15 | if (compiler.options.sourceMap) { 16 | compiled = { 17 | code: compiled.js, 18 | map: compiled.v3SourceMap 19 | } 20 | } 21 | cb(null, compiled) 22 | } 23 | -------------------------------------------------------------------------------- /lib/template-compiler.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var vueCompiler = require('vue-template-compiler') 3 | var transpile = require('vue-template-es2015-compiler') 4 | 5 | module.exports = function compileTemplate (template, compiler) { 6 | var compiled = vueCompiler.compile(template) 7 | if (compiled.errors.length) { 8 | compiled.errors.forEach(function (msg) { 9 | console.error('\n' + chalk.red(msg) + '\n') 10 | }) 11 | throw new Error('Vue template compilation failed') 12 | } else { 13 | return { 14 | render: toFunction(compiled.render), 15 | staticRenderFns: '[' + compiled.staticRenderFns.map(toFunction).join(',') + ']' 16 | } 17 | } 18 | } 19 | 20 | function toFunction (code) { 21 | return transpile('function render () {' + code + '}') 22 | } 23 | -------------------------------------------------------------------------------- /plugins/extract-css.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var compiler = require('../lib/compiler') 3 | 4 | module.exports = function (b, opts) { 5 | compiler.applyConfig({ 6 | extractCSS: true 7 | }) 8 | 9 | var styles = Object.create(null) 10 | var outPath = opts.out || opts.o || 'bundle.css' 11 | 12 | b.on('bundle', function (bs) { 13 | bs.on('end', function () { 14 | var css = Object.keys(styles) 15 | .map(function (file) { return styles[file] }) 16 | .join('\n') 17 | if (typeof outPath === 'object' && outPath.write) { 18 | outPath.write(css) 19 | outPath.end() 20 | } else if (typeof outPath === 'string') { 21 | fs.writeFile(outPath, css, function () {}) 22 | } 23 | }) 24 | }) 25 | 26 | b.on('transform', function (tr, file) { 27 | if (tr.vueify) { 28 | tr.on('vueify-style', function (e) { 29 | styles[e.file] = e.style 30 | }) 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /lib/compilers/less.js: -------------------------------------------------------------------------------- 1 | var assign = require('object-assign') 2 | var path = require('path') 3 | var ensureRequire = require('../ensure-require.js') 4 | 5 | module.exports = function (raw, cb, compiler, filePath) { 6 | ensureRequire('less', 'less') 7 | var less = require('less') 8 | 9 | var opts = assign({ 10 | filename: path.basename(filePath) 11 | }, compiler.options.less) 12 | 13 | // provide import path 14 | var dir = path.dirname(filePath) 15 | var paths = [dir, process.cwd()] 16 | opts.paths = opts.paths 17 | ? opts.paths.concat(paths) 18 | : paths 19 | 20 | less.render(raw, opts, function (err, res) { 21 | if (err) { 22 | return cb(err) 23 | } 24 | // Less 2.0 returns an object instead rendered string 25 | if (typeof res === 'object') { 26 | res.imports.forEach(function (file) { 27 | compiler.emit('dependency', file) 28 | }) 29 | res = res.css 30 | } 31 | cb(null, res) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /lib/compilers/stylus.js: -------------------------------------------------------------------------------- 1 | var assign = require('object-assign') 2 | var path = require('path') 3 | var ensureRequire = require('../ensure-require.js') 4 | 5 | module.exports = function (raw, cb, compiler, filePath) { 6 | ensureRequire('stylus', 'stylus') 7 | var stylus = require('stylus') 8 | 9 | var opts = assign({ 10 | filename: path.basename(filePath) 11 | }, compiler.options.stylus || {}) 12 | 13 | var dir = path.dirname(filePath) 14 | var paths = [dir, process.cwd()] 15 | opts.paths = opts.paths 16 | ? opts.paths.concat(paths) 17 | : paths 18 | 19 | // using the renderer API so that we can 20 | // check deps after compilation 21 | var renderer = stylus(raw) 22 | Object.keys(opts).forEach(function (key) { 23 | renderer.set(key, opts[key]) 24 | }) 25 | 26 | renderer.render(function (err, css) { 27 | if (err) return cb(err) 28 | renderer.deps().forEach(function (file) { 29 | compiler.emit('dependency', file) 30 | }) 31 | cb(null, css) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Evan You 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 | -------------------------------------------------------------------------------- /lib/ensure-require.js: -------------------------------------------------------------------------------- 1 | module.exports = function (name, deps) { 2 | var i, len 3 | var missing = [] 4 | if (typeof deps === 'string') { 5 | deps = [deps] 6 | } 7 | for (i = 0, len = deps.length; i < len; i++) { 8 | var mis 9 | var req = deps[i] 10 | if (typeof req === 'string') { 11 | mis = req 12 | } else { 13 | mis = req[1] 14 | req = req[0] 15 | } 16 | try { 17 | // hack for babel-runtime because it does not expose "main" field 18 | if (req === 'babel-runtime') { 19 | req = 'babel-runtime/core-js' 20 | } 21 | require.resolve(req) 22 | } catch (e) { 23 | missing.push(mis) 24 | } 25 | } 26 | if (missing.length > 0) { 27 | var message = 'You are trying to use "' + name + '". ' 28 | var npmInstall = 'npm install --save-dev ' + missing.join(' ') 29 | if (missing.length > 1) { 30 | var last = missing.pop() 31 | message += missing.join(', ') + ' and ' + last + ' are ' 32 | } else { 33 | message += missing[0] + ' is ' 34 | } 35 | message += 'missing.\n\nTo install run:\n' + npmInstall 36 | throw new Error(message) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/compilers/sass.js: -------------------------------------------------------------------------------- 1 | var assign = require('object-assign') 2 | var path = require('path') 3 | var ensureRequire = require('../ensure-require.js') 4 | 5 | module.exports = function (raw, cb, compiler, filePath) { 6 | ensureRequire('sass', 'node-sass') 7 | var sass = require('node-sass') 8 | 9 | var sassOptions = assign({ 10 | data: raw, 11 | success: function (res) { 12 | if (typeof res === 'object') { 13 | cb(null, res.css) 14 | } else { 15 | cb(null, res) // compat for node-sass < 2.0.0 16 | } 17 | }, 18 | error: function (err) { 19 | cb(err) 20 | } 21 | }, compiler.options.sass || { 22 | sourceComments: true 23 | }) 24 | 25 | var dir = path.dirname(filePath) 26 | var paths = [dir, process.cwd()] 27 | sassOptions.includePaths = sassOptions.includePaths 28 | ? sassOptions.includePaths.concat(paths) 29 | : paths 30 | 31 | sass.render( 32 | sassOptions, 33 | // callback for node-sass > 3.0.0 34 | function (err, res) { 35 | if (err) { 36 | cb(err) 37 | } else { 38 | res.stats.includedFiles.forEach(function (file) { 39 | compiler.emit('dependency', file) 40 | }) 41 | cb(null, res.css.toString()) 42 | } 43 | } 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var through = require('through') 2 | var compiler = require('./lib/compiler') 3 | 4 | module.exports = function vueify (file, options) { 5 | if (!/.vue$/.test(file)) { 6 | return through() 7 | } 8 | 9 | compiler.loadConfig() 10 | compiler.applyConfig(options) 11 | compiler.applyConfig({ 12 | sourceMap: options._flags.debug 13 | }) 14 | 15 | var data = '' 16 | var stream = through(write, end) 17 | stream.vueify = true 18 | 19 | function dependency (file) { 20 | stream.emit('file', file) 21 | } 22 | 23 | function emitStyle (e) { 24 | stream.emit('vueify-style', e) 25 | } 26 | 27 | function write (buf) { 28 | data += buf 29 | } 30 | 31 | function end () { 32 | stream.emit('file', file) 33 | compiler.on('dependency', dependency) 34 | compiler.on('style', emitStyle) 35 | 36 | compiler.compile(data, file, function (error, result) { 37 | compiler.removeListener('dependency', dependency) 38 | compiler.removeListener('style', emitStyle) 39 | if (error) { 40 | stream.emit('error', error) 41 | // browserify doesn't log the stack by default... 42 | console.error(error.stack.replace(/^.*?\n/, '')) 43 | } 44 | stream.queue(result) 45 | stream.queue(null) 46 | }) 47 | } 48 | 49 | return stream 50 | } 51 | 52 | // expose compiler 53 | module.exports.compiler = compiler 54 | -------------------------------------------------------------------------------- /lib/compilers/babel.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var json = require('json5') 4 | var assign = require('object-assign') 5 | var ensureRequire = require('../ensure-require') 6 | 7 | var defaultBabelOptions = { 8 | presets: ['es2015'], 9 | plugins: ['transform-runtime'] 10 | } 11 | 12 | var babelRcPath = path.resolve(process.cwd(), '.babelrc') 13 | var babelOptions = fs.existsSync(babelRcPath) 14 | ? getBabelRc() || defaultBabelOptions 15 | : defaultBabelOptions 16 | 17 | function getBabelRc () { 18 | var rc 19 | try { 20 | rc = json.parse(fs.readFileSync(babelRcPath, 'utf-8')) 21 | } catch (e) { 22 | throw new Error('[vueify] Your .babelrc seems to be incorrectly formatted.') 23 | } 24 | return rc 25 | } 26 | 27 | module.exports = function (raw, cb, compiler, filePath) { 28 | if ((compiler.options.babel || babelOptions) === defaultBabelOptions) { 29 | try { 30 | ensureRequire('babel', ['babel-preset-es2015', 'babel-runtime', 'babel-plugin-transform-runtime']) 31 | } catch (e) { 32 | console.error(e.message) 33 | console.error( 34 | '\n^^^ You are seeing this because you are using Vueify\'s default babel ' + 35 | 'configuration. You can override this with .babelrc or the babel option ' + 36 | 'in vue.config.js.' 37 | ) 38 | } 39 | } 40 | 41 | try { 42 | var babel = require('babel-core') 43 | var options = assign({ 44 | comments: false, 45 | filename: filePath, 46 | sourceMaps: compiler.options.sourceMap 47 | }, compiler.options.babel || babelOptions) 48 | var res = babel.transform(raw, options) 49 | } catch (err) { 50 | return cb(err) 51 | } 52 | cb(null, res) 53 | } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vueify", 3 | "version": "9.4.1", 4 | "description": "Vue component transform for Browserify", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vuejs/vueify" 9 | }, 10 | "keywords": [ 11 | "vue", 12 | "browserify" 13 | ], 14 | "author": "Evan You", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/vuejs/vueify/issues" 18 | }, 19 | "scripts": { 20 | "test": "eslint index.js lib && mocha test/test.js --slow=5000 --timeout=10000" 21 | }, 22 | "homepage": "https://github.com/vuejs/vueify", 23 | "dependencies": { 24 | "chalk": "^1.1.1", 25 | "convert-source-map": "^1.2.0", 26 | "cssnano": "^3.3.2", 27 | "hash-sum": "^1.0.2", 28 | "lru-cache": "^4.0.0", 29 | "object-assign": "^4.0.1", 30 | "postcss": "^5.0.10", 31 | "postcss-selector-parser": "^2.0.0", 32 | "source-map": "^0.5.6", 33 | "through": "^2.3.6", 34 | "json5": "^0.5.1", 35 | "vue-hot-reload-api": "^2.0.1", 36 | "vue-template-compiler": "^2.0.0-alpha.8", 37 | "vue-template-es2015-compiler": "^1.2.2" 38 | }, 39 | "devDependencies": { 40 | "babel-core": "^6.0.0", 41 | "babel-plugin-transform-runtime": "^6.0.0", 42 | "babel-preset-es2015": "^6.0.0", 43 | "babel-runtime": "^6.0.0", 44 | "browserify": "^13.0.1", 45 | "chai": "^3.5.0", 46 | "coffee-script": "^1.10.0", 47 | "eslint": "^2.13.0", 48 | "eslint-config-vue": "^1.0.3", 49 | "eslint-plugin-html": "^1.5.3", 50 | "jade": "^1.11.0", 51 | "jsdom": "^9.2.1", 52 | "less": "^2.5.1", 53 | "mkdirp": "^0.5.1", 54 | "mocha": "^2.3.3", 55 | "node-sass": "^3.3.3", 56 | "pug": "^2.0.0-alpha6", 57 | "rimraf": "^2.5.2", 58 | "stylus": "^0.52.4" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/style-rewriter.js: -------------------------------------------------------------------------------- 1 | var postcss = require('postcss') 2 | var selectorParser = require('postcss-selector-parser') 3 | var cache = require('lru-cache')(100) 4 | var assign = require('object-assign') 5 | 6 | var currentId 7 | var addId = postcss.plugin('add-id', function () { 8 | return function (root) { 9 | root.each(function rewriteSelector (node) { 10 | if (!node.selector) { 11 | // handle media queries 12 | if (node.type === 'atrule' && node.name === 'media') { 13 | node.each(rewriteSelector) 14 | } 15 | return 16 | } 17 | node.selector = selectorParser(function (selectors) { 18 | selectors.each(function (selector) { 19 | var node = null 20 | selector.each(function (n) { 21 | if (n.type !== 'pseudo') node = n 22 | }) 23 | selector.insertAfter(node, selectorParser.attribute({ 24 | attribute: currentId 25 | })) 26 | }) 27 | }).process(node.selector).result 28 | }) 29 | } 30 | }) 31 | 32 | /** 33 | * Add attribute selector to css 34 | * 35 | * @param {String} id 36 | * @param {String} css 37 | * @param {Boolean} scoped 38 | * @param {Object} options 39 | * @return {Promise} 40 | */ 41 | 42 | module.exports = function (id, css, scoped, options) { 43 | var key = id + '!!' + scoped + '!!' + css 44 | var val = cache.get(key) 45 | if (val) { 46 | return Promise.resolve(val) 47 | } else { 48 | var plugins = [] 49 | var opts = {} 50 | 51 | if (options.postcss instanceof Array) { 52 | plugins = options.postcss.slice() 53 | } else if (options.postcss instanceof Object) { 54 | plugins = options.postcss.plugins || [] 55 | opts = options.postcss.options 56 | } 57 | 58 | // scoped css rewrite 59 | // make sure the addId plugin is only pushed once 60 | if (scoped && plugins.indexOf(addId) === -1) { 61 | plugins.push(addId) 62 | } 63 | 64 | // remove the addId plugin if the style block is not scoped 65 | if (!scoped && plugins.indexOf(addId) !== -1) { 66 | plugins.splice(plugins.indexOf(addId), 1) 67 | } 68 | 69 | // minification 70 | if (process.env.NODE_ENV === 'production') { 71 | plugins.push(require('cssnano')(assign({ 72 | safe: true 73 | }, options.cssnano))) 74 | } 75 | currentId = id 76 | return postcss(plugins) 77 | .process(css, opts) 78 | .then(function (res) { 79 | cache.set(key, res.css) 80 | return res.css 81 | }) 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | process.env.VUEIFY_TEST = true 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const expect = require('chai').expect 6 | const rimraf = require('rimraf') 7 | const mkdirp = require('mkdirp') 8 | const browserify = require('browserify') 9 | const vueify = require('../index') 10 | const jsdom = require('jsdom') 11 | const vueCompiler = require('vue-template-compiler') 12 | const transpile = require('vue-template-es2015-compiler') 13 | const genId = require('../lib/gen-id') 14 | 15 | const tempDir = path.resolve(__dirname, './temp') 16 | const mockEntry = path.resolve(tempDir, 'entry.js') 17 | rimraf.sync(tempDir) 18 | mkdirp.sync(tempDir) 19 | 20 | function test (file, assert) { 21 | it(file, done => { 22 | fs.writeFileSync(mockEntry, 'window.vueModule = require("../fixtures/' + file + '.vue")') 23 | browserify(mockEntry) 24 | .transform(vueify) 25 | .bundle((err, buf) => { 26 | if (err) return done(err) 27 | jsdom.env({ 28 | html: '', 29 | src: [buf.toString()], 30 | done: (err, window) => { 31 | if (err) return done(err) 32 | assert(window) 33 | done() 34 | } 35 | }) 36 | }) 37 | }) 38 | } 39 | 40 | function testCssExtract (file, assert) { 41 | it(file, done => { 42 | fs.writeFileSync(mockEntry, 'window.vueModule = require("../fixtures/' + file + '.vue")') 43 | browserify(mockEntry) 44 | .transform(vueify) 45 | .plugin('./plugins/extract-css', { out: { write: assert, end: done }}) 46 | .bundle((err, buf) => { 47 | if (err) return done(err) 48 | }) 49 | }) 50 | } 51 | 52 | function assertRenderFn (options, template) { 53 | const compiled = vueCompiler.compile(template) 54 | expect(options.render.toString()).to.equal(transpile('function render() {' + compiled.render + '}')) 55 | } 56 | 57 | describe('vueify', () => { 58 | test('basic', window => { 59 | const module = window.vueModule 60 | assertRenderFn(module, '

{{msg}}

') 61 | expect(module.data().msg).to.contain('Hello from Component A!') 62 | const style = window.document.querySelector('style').textContent 63 | expect(style).to.contain('comp-a h2 {\n color: #f00;\n}') 64 | }) 65 | 66 | test('pre-processors', window => { 67 | var module = window.vueModule 68 | assertRenderFn(module, 69 | '
' + 70 | '

This is the app

' + 71 | '' + 72 | '' + 73 | '
' 74 | ) 75 | expect(module.data().msg).to.contain('Hello from coffee!') 76 | var style = window.document.querySelector('style').textContent 77 | // stylus 78 | expect(style).to.contain('body {\n font: 100% Helvetica, sans-serif;\n color: #999;\n}') 79 | // sass 80 | expect(style).to.contain('h1 {\n color: red;') 81 | // less 82 | expect(style).to.contain('h1 {\n color: green;') 83 | }) 84 | 85 | test('pug', window => { 86 | var module = window.vueModule 87 | assertRenderFn(module, 88 | '
' + 89 | '

This is the app

' + 90 | '' + 91 | '' + 92 | '
' 93 | ) 94 | }) 95 | 96 | test('scoped-css', window => { 97 | var module = window.vueModule 98 | var id = 'data-v-' + genId(require.resolve('./fixtures/scoped-css.vue')) 99 | expect(module._scopeId).to.equal(id) 100 | assertRenderFn(module, 101 | '
' + 102 | '

hi

\n' + 103 | '

hi

\n' + 104 | '\n' + 105 | '

' + 106 | '
' 107 | ) 108 | var style = window.document.querySelector('style').textContent 109 | expect(style).to.contain('.test[' + id + '] {\n color: yellow;\n}') 110 | expect(style).to.contain('.test[' + id + ']:after {\n content: \'bye!\';\n}') 111 | expect(style).to.contain('h1[' + id + '] {\n color: green;\n}') 112 | }) 113 | 114 | test('style-import', window => { 115 | var styles = window.document.querySelectorAll('style') 116 | expect(styles[0].textContent).to.contain('h1 { color: red; }') 117 | // import with scoped 118 | var id = 'data-v-' + genId(require.resolve('./fixtures/style-import.vue')) 119 | expect(styles[0].textContent).to.contain('h1[' + id + '] { color: green; }') 120 | }) 121 | 122 | test('template-import', window => { 123 | var module = window.vueModule 124 | assertRenderFn(module, '

hello

') 125 | }) 126 | 127 | test('script-import', window => { 128 | var module = window.vueModule 129 | expect(module.data().msg).to.contain('Hello from Component A!') 130 | }) 131 | 132 | test('media-query', window => { 133 | var style = window.document.querySelector('style').textContent 134 | var id = 'data-v-' + genId(require.resolve('./fixtures/media-query.vue')) 135 | expect(style).to.contain('@media print {\n .foo[' + id + '] {\n color: #000;\n }\n}') 136 | }) 137 | 138 | testCssExtract('style-export', css => { 139 | expect(css).to.equal('h2 {color: red;}') 140 | }) 141 | }) 142 | -------------------------------------------------------------------------------- /lib/compiler.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | var chalk = require('chalk') 4 | var hash = require('hash-sum') 5 | var Emitter = require('events').EventEmitter 6 | var vueCompiler = require('vue-template-compiler') 7 | var sourceMap = require('source-map') 8 | var convert = require('convert-source-map') 9 | 10 | var genId = require('./gen-id') 11 | var normalize = require('./normalize') 12 | var compilers = require('./compilers') 13 | var rewriteStyle = require('./style-rewriter') 14 | var compileTemplate = require('./template-compiler') 15 | 16 | // determine dynamic script paths 17 | var hotReloadAPIPath = normalize.dep('vue-hot-reload-api') 18 | var insertCSSPath = normalize.lib('insert-css') 19 | 20 | var hasBabel = true 21 | try { 22 | require('babel-core') 23 | } catch (e) { 24 | hasBabel = false 25 | } 26 | 27 | var splitRE = /\r?\n/g 28 | var resolvedPartsCache = Object.create(null) 29 | 30 | // expose compiler 31 | var compiler = module.exports = new Emitter() 32 | compiler.setMaxListeners(Infinity) 33 | 34 | // options 35 | var options = compiler.options = {} 36 | 37 | // load user config 38 | compiler.loadConfig = function () { 39 | var fs = require('fs') 40 | var path = require('path') 41 | var configPath = path.resolve(process.cwd(), 'vue.config.js') 42 | if (fs.existsSync(configPath)) { 43 | compiler.applyConfig(require(configPath)) 44 | } 45 | } 46 | 47 | // apply config 48 | compiler.applyConfig = function (config) { 49 | // copy user options to default options 50 | Object.keys(config).forEach(function (key) { 51 | if (key !== 'customCompilers') { 52 | options[key] = config[key] 53 | } else { 54 | // register compilers 55 | Object.keys(config[key]).forEach(function (name) { 56 | compilers[name] = config[key][name] 57 | }) 58 | } 59 | }) 60 | } 61 | 62 | compiler.compile = function (content, filePath, cb) { 63 | var isProduction = process.env.NODE_ENV === 'production' 64 | var isServer = process.env.VUE_ENV === 'server' 65 | var isTest = !!process.env.VUEIFY_TEST 66 | 67 | // generate css scope id 68 | var id = 'data-v-' + genId(filePath) 69 | // parse the component into parts 70 | var parts = vueCompiler.parseComponent(content, { pad: true }) 71 | 72 | // check for scoped style nodes 73 | var hasScopedStyle = parts.styles.some(function (style) { 74 | return style.scoped 75 | }) 76 | 77 | var resolvedParts = { 78 | template: null, 79 | script: null, 80 | styles: [] 81 | } 82 | 83 | Promise.all([ 84 | processTemplate(parts.template, filePath, resolvedParts), 85 | processScript(parts.script, filePath, resolvedParts) 86 | ].concat(parts.styles.map(function (style) { 87 | return processStyle(style, filePath, id, resolvedParts) 88 | }))) 89 | .then(mergeParts) 90 | .catch(cb) 91 | 92 | function mergeParts () { 93 | // check whether script/template has changed 94 | var prevParts = resolvedPartsCache[id] || {} 95 | resolvedPartsCache[id] = resolvedParts 96 | var scriptChanged = resolvedParts.script !== prevParts.script 97 | var templateChanged = resolvedParts.template !== prevParts.template 98 | 99 | var output = '' 100 | var map = null 101 | // styles 102 | var style = resolvedParts.styles.join('\n') 103 | if (style && !isServer) { 104 | // emit style 105 | compiler.emit('style', { 106 | file: filePath, 107 | style: style 108 | }) 109 | if (!options.extractCSS) { 110 | style = JSON.stringify(style) 111 | output += 112 | 'var __vueify_style_dispose__ = require("' + insertCSSPath + '").insert(' + style + ')\n' 113 | } 114 | } 115 | // script 116 | var script = resolvedParts.script 117 | if (script) { 118 | if (options.sourceMap) { 119 | map = generateSourceMap(script, output) 120 | } 121 | output += 122 | ';(function(){\n' + script + '\n})()\n' + 123 | // babel 6 compat 124 | 'if (module.exports.__esModule) module.exports = module.exports.default\n' 125 | } 126 | // in case the user exports with Vue.extend 127 | output += 'var __vue__options__ = (typeof module.exports === "function"' + 128 | '? module.exports.options' + 129 | ': module.exports)\n' 130 | // template 131 | var template = resolvedParts.template 132 | if (template) { 133 | if (!isProduction && !isServer) { 134 | output += 135 | 'if (__vue__options__.functional) {console.error("' + 136 | '[vueify] functional components are not supported and ' + 137 | 'should be defined in plain js files using render functions.' + 138 | '")}\n' 139 | } 140 | var beforeLines 141 | if (map) { 142 | beforeLines = output.split(splitRE).length 143 | } 144 | output += 145 | '__vue__options__.render = ' + template.render + '\n' + 146 | '__vue__options__.staticRenderFns = ' + template.staticRenderFns + '\n' 147 | if (map) { 148 | addTemplateMapping(content, parts, output, map, beforeLines) 149 | } 150 | } 151 | // scoped CSS id 152 | if (hasScopedStyle) { 153 | output += '__vue__options__._scopeId = "' + id + '"\n' 154 | } 155 | // hot reload 156 | if (!isProduction && !isTest && !isServer) { 157 | output += 158 | 'if (module.hot) {(function () {' + 159 | ' var hotAPI = require("' + hotReloadAPIPath + '")\n' + 160 | ' hotAPI.install(require("vue"), true)\n' + 161 | ' if (!hotAPI.compatible) return\n' + 162 | ' module.hot.accept()\n' + 163 | // remove style tag on dispose 164 | (style && !options.extractCSS 165 | ? ' module.hot.dispose(__vueify_style_dispose__)\n' 166 | : '') + 167 | ' if (!module.hot.data) {\n' + 168 | // initial insert 169 | ' hotAPI.createRecord("' + id + '", __vue__options__)\n' + 170 | ' } else {\n' + 171 | // update 172 | (scriptChanged 173 | ? ' hotAPI.reload("' + id + '", __vue__options__)\n' 174 | : templateChanged 175 | ? ' hotAPI.rerender("' + id + '", __vue__options__)\n' 176 | : '' 177 | ) + 178 | ' }\n' + 179 | '})()}' 180 | } 181 | if (map) { 182 | output += '\n' + convert.fromJSON(map.toString()).toComment() 183 | } 184 | cb(null, output) 185 | } 186 | 187 | function generateSourceMap (script, output) { 188 | // hot-reload source map busting 189 | var hashedFilename = path.basename(filePath) + '?' + hash(filePath + content) 190 | var map = new sourceMap.SourceMapGenerator() 191 | map.setSourceContent(hashedFilename, content) 192 | // check input source map from babel/coffee etc 193 | var inMap = resolvedParts.map 194 | var inMapConsumer = inMap && new sourceMap.SourceMapConsumer(inMap) 195 | var generatedOffset = (output ? output.split(splitRE).length : 0) + 1 196 | script.split(splitRE).forEach(function (line, index) { 197 | var ln = index + 1 198 | var originalLine = inMapConsumer 199 | ? inMapConsumer.originalPositionFor({ line: ln, column: 0 }).line 200 | : ln 201 | if (originalLine) { 202 | map.addMapping({ 203 | source: hashedFilename, 204 | generated: { 205 | line: ln + generatedOffset, 206 | column: 0 207 | }, 208 | original: { 209 | line: originalLine, 210 | column: 0 211 | } 212 | }) 213 | } 214 | }) 215 | map._hashedFilename = hashedFilename 216 | return map 217 | } 218 | } 219 | 220 | function addTemplateMapping (content, parts, output, map, beforeLines) { 221 | var afterLines = output.split(splitRE).length 222 | var templateLine = content.slice(0, parts.template.start).split(splitRE).length 223 | for (; beforeLines < afterLines; beforeLines++) { 224 | map.addMapping({ 225 | source: map._hashedFilename, 226 | generated: { 227 | line: beforeLines, 228 | column: 0 229 | }, 230 | original: { 231 | line: templateLine, 232 | column: 0 233 | } 234 | }) 235 | } 236 | } 237 | 238 | function processTemplate (part, filePath, parts) { 239 | if (!part) return Promise.resolve() 240 | var template = getContent(part, filePath) 241 | return compileAsPromise('template', template, part.lang, filePath) 242 | .then(function (res) { 243 | parts.template = compileTemplate(res, compiler) 244 | }) 245 | } 246 | 247 | function processScript (part, filePath, parts) { 248 | if (!part) return Promise.resolve() 249 | var lang = part.lang || (hasBabel ? 'babel' : null) 250 | var script = getContent(part, filePath) 251 | return compileAsPromise('script', script, lang, filePath) 252 | .then(function (res) { 253 | if (typeof res === 'string') { 254 | parts.script = res 255 | } else { 256 | parts.script = res.code 257 | parts.map = res.map 258 | } 259 | }) 260 | } 261 | 262 | function processStyle (part, filePath, id, parts) { 263 | var style = getContent(part, filePath) 264 | return compileAsPromise('style', style, part.lang, filePath) 265 | .then(function (res) { 266 | res = res.trim() 267 | return rewriteStyle(id, res, part.scoped, options).then(function (res) { 268 | parts.styles.push(res) 269 | }) 270 | }) 271 | } 272 | 273 | function getContent (part, filePath) { 274 | return part.src 275 | ? loadSrc(part.src, filePath) 276 | : part.content 277 | } 278 | 279 | function loadSrc (src, filePath) { 280 | var dir = path.dirname(filePath) 281 | var srcPath = path.resolve(dir, src) 282 | compiler.emit('dependency', srcPath) 283 | try { 284 | return fs.readFileSync(srcPath, 'utf-8') 285 | } catch (e) { 286 | console.error(chalk.red( 287 | 'Failed to load src: "' + src + 288 | '" from file: "' + filePath + '"' 289 | )) 290 | } 291 | } 292 | 293 | function compileAsPromise (type, source, lang, filePath) { 294 | var compile = compilers[lang] 295 | if (compile) { 296 | return new Promise(function (resolve, reject) { 297 | compile(source, function (err, res) { 298 | if (err) { 299 | // report babel error codeframe 300 | if (err.codeFrame) { 301 | process.nextTick(function () { 302 | console.error(err.codeFrame) 303 | }) 304 | } 305 | return reject(err) 306 | } 307 | resolve(res) 308 | }, compiler, filePath) 309 | }) 310 | } else { 311 | return Promise.resolve(source) 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THIS REPOSITORY IS DEPRECATED 2 | 3 | > Note: We are concentrating our efforts on supporting webpack and rollup. 4 | 5 | ## vueify [![Build Status](https://circleci.com/gh/vuejs/vueify.svg?style=shield)](https://circleci.com/gh/vuejs/vueify) [![npm version](https://badge.fury.io/js/vueify.svg)](http://badge.fury.io/js/vueify) 6 | 7 | > [Browserify](http://browserify.org/) transform for [Vue.js](http://vuejs.org/) components, with scoped CSS and component hot-reloading. 8 | 9 | **NOTE: master branch now hosts version ^9.0, which only works with Vue ^2.0. Vueify 8.x which works with Vue 1.x is in the [8.x branch](https://github.com/vuejs/vueify/tree/8.x).** 10 | 11 | This transform allows you to write your components in this format: 12 | 13 | ``` html 14 | // app.vue 15 | 20 | 21 | 24 | 25 | 34 | ``` 35 | 36 | You can also mix preprocessor languages in the component file: 37 | 38 | ``` vue 39 | // app.vue 40 | 44 | 45 | 48 | 49 | 54 | ``` 55 | 56 | And you can import using the `src` attribute: 57 | 58 | ``` html 59 | 60 | ``` 61 | 62 | Under the hood, the transform will: 63 | 64 | - extract the styles, compile them and insert them with the `insert-css` module. 65 | - extract the template, compile it and add it to your exported options. 66 | 67 | You can `require()` other stuff in the ` 97 | 98 | ``` 99 | 100 | If you are using `vueify` in Node: 101 | 102 | ``` js 103 | var fs = require("fs") 104 | var browserify = require('browserify') 105 | var vueify = require('vueify') 106 | 107 | browserify('./main.js') 108 | .transform(vueify) 109 | .bundle() 110 | .pipe(fs.createWriteStream("bundle.js")) 111 | ``` 112 | 113 | ## Building for Production 114 | 115 | Make sure to have the `NODE_ENV` environment variable set to `"production"` when building for production! This strips away unnecessary code (e.g. hot-reload) for smaller bundle size. 116 | 117 | If you are using Gulp, note that `gulp --production` **does not** affect vueify; you still need to explicitly set `NODE_ENV=production`. 118 | 119 | ## ES2015 with Babel 120 | 121 | Vueify is pre-configured to work with Babel. Simply install Babel-related dependencies: 122 | 123 | ``` bash 124 | npm install\ 125 | babel-core\ 126 | babel-preset-es2015\ 127 | --save-dev 128 | ``` 129 | 130 | Then create a `.babelrc`: 131 | 132 | ``` json 133 | { 134 | "presets": ["es2015"] 135 | } 136 | ``` 137 | 138 | And voila! You can now write ES2015 in your `*.vue` files. Note if you want to use ES2015 on normal `*.js` files, you will also need [babelify](https://github.com/babel/babelify). 139 | 140 | You can also configure babel with the `babel` field in `vue.config.js`, which will take the highest priority. 141 | 142 | ## Enabling Other Pre-Processors 143 | 144 | For other pre-processors, you also need to install the corresponding node modules to enable the compilation. e.g. to get stylus compiled in your Vue components, do `npm install stylus --save-dev`. 145 | 146 | These are the preprocessors supported by vueify out of the box: 147 | 148 | - stylus 149 | - less 150 | - scss (via `node-sass`, use `sass` in [config section](#configuring-options)) 151 | - jade 152 | - pug 153 | - coffee-script (use `coffee` in [config section](#configuring-options)) 154 | 155 | ## PostCSS 156 | 157 | Vueify uses PostCSS for scoped CSS rewrite. You can also provide your own PostCSS plugins! See [config section](#configuring-options) below for an example. 158 | 159 | ## Configuring Options 160 | 161 | Create a `vue.config.js` file at where your build command is run (usually the root level of your project): 162 | 163 | ``` js 164 | module.exports = { 165 | // configure a built-in compiler 166 | sass: { 167 | includePaths: [...] 168 | }, 169 | // provide your own postcss plugins 170 | postcss: [...], 171 | // register custom compilers 172 | customCompilers: { 173 | // for tags with lang="ts" 174 | ts: function (content, cb, compiler, filePath) { 175 | // content: content extracted from lang="ts" blocks 176 | // cb: the callback to call when you're done compiling 177 | // compiler: the vueify compiler instance 178 | // filePath: the path for the file being compiled 179 | // 180 | // compile some TypeScript... and when you're done: 181 | cb(null, result) 182 | } 183 | } 184 | } 185 | ``` 186 | 187 | Example using custom PostCSS plugin: 188 | 189 | ``` js 190 | var cssnext = require('cssnext') 191 | 192 | module.exports = { 193 | postcss: [cssnext()] 194 | } 195 | ``` 196 | 197 | Alternatively, if you are using `vueify` in Node and don't want to create a `vue.config.js` file: 198 | 199 | ``` js 200 | var fs = require("fs") 201 | var browserify = require('browserify') 202 | var vueify = require('vueify') 203 | 204 | // apply custom config 205 | vueify.compiler.applyConfig({ 206 | // ...same as in vue.config.js 207 | }) 208 | 209 | browserify('./main.js') 210 | .transform(vueify) 211 | .bundle() 212 | .pipe(fs.createWriteStream("bundle.js")) 213 | ``` 214 | 215 | Or simply pass configuration object to `vueify` (in Node) (for instance to set sass search paths as in the following example): 216 | 217 | ``` js 218 | var fs = require("fs") 219 | var browserify = require('browserify') 220 | var vueify = require('vueify') 221 | 222 | browserify('./main.js') 223 | .transform(vueify, { 224 | sass: { 225 | includePaths: [...] 226 | }, 227 | // ...same as in vue.config.js 228 | }) 229 | .bundle() 230 | .pipe(fs.createWriteStream("bundle.js")) 231 | ``` 232 | 233 | ## Scoped CSS 234 | 235 | When a ` 243 | 246 | ``` 247 | 248 | Into the following: 249 | 250 | ``` html 251 | 256 | 259 | ``` 260 | 261 | ### Scoped CSS Notes 262 | 263 | 1. You can include both scoped and non-scoped styles in the same component. 264 | 265 | 2. The following will be affected by both the parent's scoped CSS and the child's scoped CSS: 266 | - A child component's root node 267 | - Content inserted to a child component via `` 268 | 269 | ## Hot Reload 270 | 271 | To enable hot component reloading, you need to install the [browserify-hmr](https://github.com/AgentME/browserify-hmr) plugin: 272 | 273 | ``` bash 274 | npm install browserify-hmr --save-dev 275 | watchify -p browserify-hmr index.js -o bundle.js 276 | ``` 277 | 278 | You can scaffold a hot-reload enabled project easily using `vue-cli` and the [this template](https://github.com/vuejs-templates/browserify-simple). 279 | 280 | ## CSS Extraction 281 | 282 | By default, the CSS in each component is injected into the page using a `