├── .gitignore ├── README.md ├── index.js ├── lib ├── gen-id.js ├── langs │ ├── coffee.js │ ├── index.js │ ├── jade.js │ ├── less.js │ ├── pug.js │ ├── sass.js │ └── stylus.js ├── normalize-import.js └── style-rewriter.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # systemjs-plugin-vue 2 | 3 | > SystemJS plugin for Vue single file components 4 | 5 | ## !!! Maintenance Status !!! 6 | 7 | This project is currently on hold, and it may not work properly with the latest Vue 2.0 versions. This is because the interest in using SystemJS with Vue doesn't seem to justify the effort required to maintain this plugin. If you are interested in taking over the maintenance of this project, please open an issue! 8 | 9 | ## Usage 10 | 11 | This plugin is only tested with jspm 0.17+ and Vue.js 2.0+. 12 | 13 | ``` bash 14 | jspm install npm:vue@2.0.0-alpha.5 15 | jspm install --dev npm:systemjs-plugin-vue 16 | ``` 17 | 18 | ``` js 19 | System.config({ 20 | "meta": { 21 | "*.vue": { 22 | "loader": "systemjs-plugin-vue" 23 | } 24 | } 25 | }) 26 | ``` 27 | 28 | You can use [this vue-cli template](https://github.com/vuejs-templates/systemjs) to scaffold an example project. 29 | 30 | ## Features 31 | 32 | - ES2015 by default (requires `systemsjs-plugin-babel`) 33 | 34 | - `lang="xxx"` pre-processors 35 | 36 | - Scoped CSS 37 | 38 | - PostCSS support 39 | 40 | - CSS are automatically extracted across all components and injected as a single ` 55 | ``` 56 | 57 | These are the preprocessors supported out of the box: 58 | 59 | - stylus 60 | - less 61 | - scss (via `node-sass`, use `sass` in [config section](#configuring-options)) 62 | - jade 63 | - pug 64 | - coffee-script (use `coffee` in [config section](#configuring-options)) 65 | 66 | ## Configuring Options 67 | 68 | You can add a Vue section in your SystemJS config: 69 | 70 | ``` js 71 | System.config({ 72 | vue: { 73 | // options 74 | } 75 | }) 76 | ``` 77 | 78 | Or, alternatively, create a `vue.config.js` file at the root of your project, and export the configuration object. 79 | 80 | ### Passing Options to Pre-Processors 81 | 82 | Just add a nested object under `vue`: 83 | 84 | ``` js 85 | System.config({ 86 | vue: { 87 | less: { 88 | paths: [...] // custom less @import paths 89 | } 90 | } 91 | }) 92 | ``` 93 | 94 | ### Custom Lang Compiler 95 | 96 | You can provide custom `lang` compilers under the `compilers` option. It is recommended to use `vue.config.js` for custom compilers, and in most cases you will want to import your Node dependencies as raw Node modules: 97 | 98 | ``` js 99 | // vue.config.js 100 | export default { 101 | compilers: { 102 | 'my-lang' (raw, filename) { 103 | return System.import('@node/my-lang').then(myLang => { 104 | return myLang.compile(raw) // assumes returning a promise 105 | }) 106 | } 107 | } 108 | } 109 | ``` 110 | 111 | ### PostCSS Configuration 112 | 113 | Use `vue.postcss` in your SystemJS config file. The option can be: 114 | 115 | - An array of plugins. Each plugin can either be a string module name or an array with the first element being the module name and the second element being an options object to be passed to that plugin. 116 | 117 | Example: 118 | 119 | ``` js 120 | System.config({ 121 | vue: { 122 | postcss: [ 123 | 'postcss-nested', 124 | ['autoprefixer', { browsers: 'last 2 versions' }] 125 | ] 126 | } 127 | }) 128 | ``` 129 | 130 | - An object containing `options` (passed to `postcss.process()`) and `plugins`. The parser and stringifier options can also be string module names and will be resolved automatically. 131 | 132 | Example: 133 | 134 | ``` js 135 | System.config({ 136 | vue: { 137 | postcss: { 138 | options: { 139 | parser: 'sugarss' 140 | }, 141 | plugins: [...] 142 | } 143 | } 144 | }) 145 | ``` 146 | 147 | If using `vue.config.js`, you can also directly provide the imported plugins instead of string module names. 148 | 149 | ## Caveats 150 | 151 | - SystemJS' hot reload mechanism is quite limiting and it's currently not possible to support the same level of hot-reload available in `vue-loader` and `vueify`. 152 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var vueCompiler = require('vue-template-compiler') 3 | var falafel = require('falafel') 4 | var rewriteCSS = require('./lib/style-rewriter.js') 5 | var genId = require('./lib/gen-id.js') 6 | var defaultLangs = require('./lib/langs/index.js') 7 | var normalizeImport = require('./lib/normalize-import') 8 | 9 | exports.translate = function (load, opts) { 10 | opts = opts || {} 11 | if (fs.existsSync && fs.existsSync('vue.config.js')) { 12 | return normalizeImport('vue.config.js').then(vueOpts => { 13 | return compile(load, opts, vueOpts) 14 | }) 15 | } else { 16 | return compile(load, opts, this.vue || {}) 17 | } 18 | } 19 | 20 | function compile (load, opts, vueOpts) { 21 | var langs = Object.assign({}, defaultLangs, vueOpts.compilers) 22 | var sfc = load.metadata.sfc = vueCompiler.parseComponent(load.source, { pad: true }) 23 | // normalize potential custom compilers 24 | var normalizeLangs = Object.keys(langs).map(lang => normalizeImport(langs[lang])) 25 | return Promise.all(normalizeLangs).then(() => { 26 | // resolve lang="xxx" for all parts 27 | var langsPromises = [] 28 | var scriptCompiler = sfc.script && sfc.script.lang && langs[sfc.script.lang] 29 | if (scriptCompiler) { 30 | langsPromises.push(scriptCompiler(sfc.script.content, load.name, vueOpts).then(script => { 31 | sfc.script.content = script 32 | })) 33 | } 34 | var templateCompiler = sfc.template && sfc.template.lang && langs[sfc.template.lang] 35 | if (templateCompiler) { 36 | langsPromises.push(templateCompiler(sfc.template.content, load.name, vueOpts).then(template => { 37 | sfc.template.content = template 38 | })) 39 | } 40 | sfc.styles.forEach((s, i) => { 41 | var compiler = s.lang && langs[s.lang] 42 | if (compiler) { 43 | langsPromises.push(compiler(s.content, load.name, vueOpts).then(css => { 44 | sfc.styles[i].content = css 45 | })) 46 | } 47 | }) 48 | return Promise.all(langsPromises) 49 | }).then(() => { 50 | // style 51 | var hasScoped = sfc.styles.some(s => s.scoped) 52 | var scopeId = hasScoped ? 'data-v-' + genId(load.name) : null 53 | if (sfc.styles.length) { 54 | var style = Promise.all(sfc.styles.map(s => { 55 | return rewriteCSS(scopeId, s, load.name, opts.minify, vueOpts) 56 | })).then(compiledStyles => { 57 | return compiledStyles.join('\n') 58 | }) 59 | if (typeof window !== 'undefined') { 60 | style.then(injectStyle) 61 | } else { 62 | load.metadata.vueStyle = style 63 | } 64 | } 65 | var templateModuleName = getTemplateModuleName(load.name) 66 | // in-browser template handling 67 | if (typeof window !== 'undefined' && sfc.template) { 68 | System.set(templateModuleName, System.newModule( 69 | vueCompiler.compileToFunctions(sfc.template.content) 70 | )) 71 | } 72 | // script 73 | let script = sfc.script ? sfc.script.content : '' 74 | if (sfc.template) { 75 | script = script || 'export default {}' 76 | script = falafel(script, { 77 | ecmaVersion: 6, 78 | sourceType: 'module' 79 | }, node => { 80 | if (node.type === 'ObjectExpression') { 81 | if (node.parent && ( 82 | node.parent.type === 'ExportDefaultDeclaration' || ( 83 | node.parent.type === 'AssignmentExpression' && 84 | node.parent.left.source() === 'module.exports' 85 | ) 86 | )) { 87 | node.update(node.source().replace(/^\{/, 88 | `{render:__renderFns__.render,` + 89 | `staticRenderFns:__renderFns__.staticRenderFns,` + 90 | (scopeId ? `_scopeId:"${scopeId}",` : '') 91 | )) 92 | } 93 | } 94 | }) 95 | 96 | script = `var __renderFns__ = System.get(${JSON.stringify(templateModuleName)});` + script 97 | } 98 | return script 99 | }) 100 | } 101 | 102 | var cssInject = "(function(c){if (typeof document == 'undefined') return; var d=document,a='appendChild',i='styleSheet',s=d.createElement('style');s.type='text/css';d.getElementsByTagName('head')[0][a](s);s[i]?s[i].cssText=c:s[a](d.createTextNode(c));})" 103 | if (typeof window === 'undefined') { 104 | exports.bundle = function (loads) { 105 | var style = Promise.all( 106 | loads 107 | .map(load => load.metadata.vueStyle) 108 | .filter(s => s) 109 | ).then(styles => { 110 | return `${cssInject}(${JSON.stringify(styles.join('\n'))});\n` 111 | }) 112 | 113 | var templateModules = loads 114 | .filter(l => l.metadata.sfc.template) 115 | .map(l => compileTemplateAsModule(l.name, l.metadata.sfc.template.content)) 116 | .join('\n') 117 | 118 | return style.then(style => templateModules + '\n' + style) 119 | } 120 | } 121 | 122 | function injectStyle (style) { 123 | var styleTag = document.createElement('style') 124 | styleTag.textContent = style 125 | document.head.appendChild(styleTag) 126 | } 127 | 128 | function getTemplateModuleName (name) { 129 | if (System.getCanonicalName) { 130 | name = System.getCanonicalName(name) 131 | } 132 | return name + '.template' 133 | } 134 | 135 | function compileTemplateAsModule (name, template) { 136 | name = getTemplateModuleName(name) 137 | var fns = vueCompiler.compile(template) 138 | return `System.set(${JSON.stringify(name)},System.newModule({\n` + 139 | `render:${toFn(fns.render)},\n` + 140 | `staticRenderFns:[${fns.staticRenderFns.map(toFn).join(',')}]\n` + 141 | `}));` 142 | } 143 | 144 | function toFn (code) { 145 | return `function(){${code}}` 146 | } 147 | -------------------------------------------------------------------------------- /lib/gen-id.js: -------------------------------------------------------------------------------- 1 | // utility for generating a uid for each component file 2 | // used in scoped CSS rewriting 3 | var fileUid = 1 4 | var fileRegistry = Object.create(null) 5 | 6 | module.exports = function genId (file) { 7 | return fileRegistry[file] || (fileRegistry[file] = fileUid++) 8 | } 9 | -------------------------------------------------------------------------------- /lib/langs/coffee.js: -------------------------------------------------------------------------------- 1 | module.exports = function (raw, filename, options) { 2 | return System.import('@node/coffee-script').then(coffee => { 3 | return new Promise((resolve, reject) => { 4 | var compiled 5 | try { 6 | compiled = coffee.compile(raw, Object.assign({ 7 | bare: true, 8 | filename: filename, 9 | sourceMap: options.sourceMap 10 | }, options.coffee)) 11 | } catch (err) { 12 | return reject(err) 13 | } 14 | if (options.sourceMap) { 15 | compiled = { 16 | code: compiled.js, 17 | map: compiled.v3SourceMap 18 | } 19 | } 20 | resolve(compiled) 21 | }) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /lib/langs/index.js: -------------------------------------------------------------------------------- 1 | // built-in compilers 2 | module.exports = { 3 | coffee: require('./coffee'), 4 | less: require('./less'), 5 | sass: require('./sass'), 6 | scss: require('./sass'), 7 | stylus: require('./stylus'), 8 | jade: require('./jade'), 9 | pug: require('./pug') 10 | } 11 | -------------------------------------------------------------------------------- /lib/langs/jade.js: -------------------------------------------------------------------------------- 1 | module.exports = function (raw, filename, options) { 2 | return System.import('@node/jade').then(jade => { 3 | return new Promise((resolve, reject) => { 4 | var html 5 | try { 6 | html = jade.compile(raw, Object.assign({}, options.jade))() 7 | } catch (err) { 8 | return reject(err) 9 | } 10 | resolve(html) 11 | }) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /lib/langs/less.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | module.exports = function (raw, filename, options) { 4 | return System.import('@node/less').then(less => { 5 | return new Promise((resolve, reject) => { 6 | var opts = Object.assign({ 7 | filename: path.basename(filename) 8 | }, options.less) 9 | 10 | // provide import path 11 | var dir = path.dirname(filename) 12 | var paths = [dir, process.cwd()] 13 | opts.paths = opts.paths 14 | ? opts.paths.concat(paths) 15 | : paths 16 | 17 | less.render(raw, opts, (err, res) => { 18 | if (err) { 19 | return reject(err) 20 | } 21 | // Less 2.0 returns an object instead rendered string 22 | if (typeof res === 'object') { 23 | res = res.css 24 | } 25 | resolve(res) 26 | }) 27 | }) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /lib/langs/pug.js: -------------------------------------------------------------------------------- 1 | module.exports = function (raw, filename, options) { 2 | return System.import('@node/pug').then(pug => { 3 | return new Promise((resolve, reject) => { 4 | var html 5 | try { 6 | html = pug.compile(raw, Object.assign({}, options.pug))() 7 | } catch (err) { 8 | return reject(err) 9 | } 10 | resolve(html) 11 | }) 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /lib/langs/sass.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | module.exports = function (raw, filename, options) { 4 | return System.import('@node/node-sass').then(sass => { 5 | return new Promise((resolve, reject) => { 6 | var sassOptions = Object.assign({ 7 | data: raw, 8 | sourceComments: true, 9 | success: (res) => { 10 | if (typeof res === 'object') { 11 | resolve(res.css) 12 | } else { 13 | resolve(res) // compat for node-sass < 2.0.0 14 | } 15 | }, 16 | error: reject 17 | }, options.sass) 18 | 19 | var dir = path.dirname(filename) 20 | var paths = [dir, process.cwd()] 21 | sassOptions.includePaths = sassOptions.includePaths 22 | ? sassOptions.includePaths.concat(paths) 23 | : paths 24 | 25 | sass.render( 26 | sassOptions, 27 | // callback for node-sass > 3.0.0 28 | (err, res) => { 29 | if (err) { 30 | reject(err) 31 | } else { 32 | resolve(res.css.toString()) 33 | } 34 | } 35 | ) 36 | }) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /lib/langs/stylus.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | module.exports = function (raw, filename, options) { 4 | return System.import('@node/stylus').then(stylus => { 5 | return new Promise((resolve, reject) => { 6 | var opts = Object.assign({ 7 | filename: path.basename(filename) 8 | }, options.stylus) 9 | 10 | var dir = path.dirname(filename) 11 | var paths = [dir, process.cwd()] 12 | opts.paths = opts.paths 13 | ? opts.paths.concat(paths) 14 | : paths 15 | 16 | // using the renderer API so that we can 17 | // check deps after compilation 18 | var renderer = stylus(raw) 19 | Object.keys(opts).forEach((key) => { 20 | renderer.set(key, opts[key]) 21 | }) 22 | 23 | renderer.render((err, res) => { 24 | if (err) return reject(err) 25 | resolve(res) 26 | }) 27 | }) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /lib/normalize-import.js: -------------------------------------------------------------------------------- 1 | module.exports = function normalizeImport (p) { 2 | if (typeof p === 'string') { 3 | return System.import(p).then(normalize) 4 | } else if (Array.isArray(p)) { 5 | return System.import(p[0]).then(plugin => normalize(plugin)(p[1])) 6 | } else { 7 | return p 8 | } 9 | } 10 | 11 | function normalize (e) { 12 | return typeof e === 'object' && e.default 13 | ? e.default 14 | : e 15 | } 16 | -------------------------------------------------------------------------------- /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 normalizeImport = require('./normalize-import') 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 | module.exports = function (id, style, name, minify, opts) { 33 | var css = style.content.trim() 34 | var scoped = style.scoped 35 | if (!scoped && !minify) { 36 | return Promise.resolve(css) 37 | } 38 | var key = id + '!!' + css 39 | var val = cache.get(key) 40 | if (val) { 41 | return Promise.resolve(val) 42 | } else { 43 | var postcssOpts = opts.postcss || {} 44 | // resolve postcss plugins 45 | var plugins = Array.isArray(postcssOpts) 46 | ? postcssOpts.slice() 47 | : Array.isArray(postcssOpts.plugins) 48 | ? postcssOpts.plugins.slice() 49 | : [] 50 | // scoped css rewrite 51 | if (scoped) { 52 | plugins.push(addId) 53 | } 54 | // minification 55 | if (minify) { 56 | plugins.push(['cssnano', Object.assign({ safe: true }, opts.cssnano)]) 57 | } 58 | return Promise.all(plugins.map(normalizeImport)).then(plugins => { 59 | // resolve postcss options 60 | var optionsPromises = [] 61 | var pos = Object.assign({}, postcssOpts.options) 62 | if (pos) { 63 | var parser = pos.parser || (pos.syntax && pos.syntax.parser) 64 | if (parser) { 65 | optionsPromises.push(normalizeImport(parser).then(parser => { 66 | pos.parser = parser 67 | })) 68 | } 69 | var stringifier = pos.stringifier || (pos.syntax && pos.syntax.stringifier) 70 | if (stringifier) { 71 | optionsPromises.push(normalizeImport(stringifier)).then(stringifier => { 72 | pos.stringifier = stringifier 73 | }) 74 | } 75 | } 76 | return Promise.all(optionsPromises).then(() => { 77 | currentId = id 78 | return postcss(plugins) 79 | .process(css, pos) 80 | .then(res => res.css) 81 | }) 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "systemjs-plugin-vue", 3 | "version": "1.2.0", 4 | "description": "SystemJS plugin for single file Vue components", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/vuejs/systemjs-plugin-vue.git" 12 | }, 13 | "keywords": [ 14 | "vue", 15 | "systemjs", 16 | "jspm" 17 | ], 18 | "author": "Evan You", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/vuejs/systemjs-plugin-vue/issues" 22 | }, 23 | "homepage": "https://github.com/vuejs/systemjs-plugin-vue#readme", 24 | "dependencies": { 25 | "cssnano": "^3.7.1", 26 | "falafel": "^1.2.0", 27 | "lru-cache": "^4.0.1", 28 | "postcss": "^5.0.21", 29 | "postcss-selector-parser": "^2.1.0", 30 | "vue-template-compiler": "^2.0.0-alpha.5" 31 | } 32 | } 33 | --------------------------------------------------------------------------------