UNPKG

rollup-plugin-vue

Version:
364 lines (318 loc) 10.4 kB
import { createFilter } from 'rollup-pluginutils'; import { writeFile } from 'fs'; import MagicString from 'magic-string'; import deIndent from 'de-indent'; import htmlMinifier from 'html-minifier'; import parse5 from 'parse5'; import validateTemplate from 'vue-template-validator'; import { relative } from 'path'; /** * Check the lang attribute of a parse5 node. * * @param {Node|*} node * @return {String|undefined} */ function checkLang(node) { if (node.attrs) { for (var i = 0, list = node.attrs; i < list.length; i += 1) { var attr = list[i]; if (attr.name === 'lang') { return attr.value; } } } return undefined; } /** * Pad content with empty lines to get correct line number in errors. * * @param content * @returns {string} */ function padContent(content) { return content .split(/\r?\n/g) .map(function () { return ''; }) .join('\n'); } /** * Wrap code inside a with statement inside a function * This is necessary for Vue 2 template compilation * * @param {string} code * @returns {string} */ function wrapRenderFunction(code) { // Replace with(this) by something that works on strict mode // https://github.com/vuejs/vue-template-es2015-compiler/blob/master/index.js code = code.replace(/with\(this\)/g, 'if(window.__VUE_WITH_STATEMENT__)'); return ("function(){" + code + "}"); } /** * Only support for es5 modules * * @param script * @param render * @param lang * @returns {string} */ function injectRender(script, render, lang) { if (['js', 'babel'].indexOf(lang.toLowerCase()) > -1) { var matches = /(export default[^{]*\{)/g.exec(script); if (matches) { return script.split(matches[1]) .join("" + (matches[1]) + "render: " + (wrapRenderFunction(render.render)) + "," + 'staticRenderFns: [' + (render.staticRenderFns.map(wrapRenderFunction).join(',')) + "]," ); } } throw new Error('[rollup-plugin-vue] could not find place to inject template in script.'); } /** * @param script * @param template * @param lang * @returns {string} */ function injectTemplate(script, template, lang) { if (['js', 'babel'].indexOf(lang.toLowerCase()) > -1) { var matches = /(export default[^{]*\{)/g.exec(script); if (matches) { return script.split(matches[1]) .join(((matches[1]) + " template: " + (JSON.stringify(template)) + ",")); } } throw new Error('[rollup-plugin-vue] could not find place to inject template in script.'); } /** * Compile template: DeIndent and minify html. * @param {Node} node * @param {string} filePath * @param {string} content * @param {*} options */ function processTemplate(node, filePath, content, options) { node = node.content; var warnings = validateTemplate(node, content); if (warnings) { var relativePath = relative(process.cwd(), filePath); warnings.forEach(function (msg) { console.warn(("\n Warning in " + relativePath + ":\n " + msg)); }); } /* eslint-disable no-underscore-dangle */ var start = node.childNodes[0].__location.startOffset; var end = node.childNodes[node.childNodes.length - 1].__location.endOffset; var template = deIndent(content.slice(start, end)); /* eslint-enable no-underscore-dangle */ return htmlMinifier.minify(template, options.htmlMinifier); } /** * @param {Node|ASTNode} node * @param {string} filePath * @param {string} content * @param templateOrRender */ function processScript(node, filePath, content, templateOrRender) { var lang = checkLang(node) || 'js'; var template = templateOrRender.template; var render = templateOrRender.render; var script = parse5.serialize(node); // pad the script to ensure correct line number for syntax errors var location = content.indexOf(script); var before = padContent(content.slice(0, location)); script = before + script; var map = new MagicString(script); if (template) { script = injectTemplate(script, template, lang); } else if (render) { script = injectRender(script, render, lang); } script = deIndent(script); return { code: script, map: map, }; } function vueTransform(code, filePath, options) { // 1. Parse the file into an HTML tree var fragment = parse5.parseFragment(code, { locationInfo: true }); // 2. Walk through the top level nodes and check for their types var nodes = {}; for (var i = fragment.childNodes.length - 1; i >= 0; i -= 1) { nodes[fragment.childNodes[i].nodeName] = fragment.childNodes[i]; } // 3. Don't touch files that don't look like Vue components if (!nodes.script) { throw new Error('There must be at least one script tag or one' + ' template tag per *.vue file.'); } // 4. Process template var template = nodes.template ? processTemplate(nodes.template, filePath, code, options) : undefined; var js; if (options.compileTemplate) { /* eslint-disable */ var render = template ? require('vue-template-compiler').compile(template) : undefined; /* eslint-enable */ js = processScript(nodes.script, filePath, code, { render: render }); } else { js = processScript(nodes.script, filePath, code, { template: template }); } // 5. Process script & style return { js: js.code, map: js.map, css: nodes.style && { content: parse5.serialize(nodes.style), lang: checkLang(nodes.style), }, }; } var DEFAULT_OPTIONS = { htmlMinifier: { customAttrSurround: [[/@/, new RegExp('')], [/:/, new RegExp('')]], collapseWhitespace: true, removeComments: true, collapseBooleanAttributes: true, removeAttributeQuotes: true, // this is disabled by default to avoid removing // "type" on <input type="text"> removeRedundantAttributes: false, useShortDoctype: true, removeEmptyAttributes: true, removeOptionalTags: true, }, }; function mergeOptions(options, defaults) { Object.keys(defaults).forEach(function (key) { var val = defaults[key]; if (key in options) { if (typeof options[key] === 'object') { mergeOptions(options[key], val); } } else { options[key] = val; } }); return options; } function vue(options) { if ( options === void 0 ) options = {}; var filter = createFilter(options.include, options.exclude); delete options.include; delete options.exclude; /* eslint-disable */ try { var vueVersion = require('vue').version; if (parseInt(vueVersion.split('.')[0], 10) >= 2) { if (!('compileTemplate' in options)) { options.compileTemplate = true; } } else { if (options.compileTemplate === true) { console.warn('Vue version < 2.0.0 does not support compiled template.'); } options.compileTemplate = false; } } catch (e) { } /* eslint-enable */ options = mergeOptions(options, DEFAULT_OPTIONS); var styles = {}; var rollupOptions = {}; var generated = false; var generateStyleBundle = function () { if (options.css === false) { return; } if (generated) { return; } generated = true; // Combine all stylesheets. var css = ''; Object.keys(styles).forEach(function (key) { css += styles[key].content || ''; }); // Emit styles through callback if (typeof options.css === 'function') { options.css(css, styles); return; } // Don't generate empty style file. if (!css.trim().length) { return; } var dest = options.css; if (typeof dest !== 'string') { // Guess destination filename dest = rollupOptions.dest || 'bundle.js'; if (dest.endsWith('.js')) { dest = dest.slice(0, -3); } dest = dest + ".css"; } // Emit styles to file writeFile(dest, css, function (err) { if (err) { throw err; } emitted(dest, css.length); }); }; return { name: 'vue', options: function options$1(o) { rollupOptions = o; return o; }, transform: function transform(source, id) { if (!filter(id) || !id.endsWith('.vue')) { return null; } var ref = vueTransform(source, id, options); var js = ref.js; var css = ref.css; var map = ref.map; // Map of every stylesheet styles[id] = css || {}; // Component javascript with inlined html template return { code: js, map: map.generateMap({ hires: true }), }; }, transformBundle: function transformBundle(source) { generateStyleBundle(); var map = new MagicString(source); return { code: source.replace(/if[\s]*\(window\.__VUE_WITH_STATEMENT__\)/g, 'with(this)'), map: map.generateMap({ hires: true }), }; }, ongenerate: function ongenerate(opts, rendered) { generateStyleBundle(); rendered.code = rendered.code.replace( /if[\s]*\(window\.__VUE_WITH_STATEMENT__\)/g, 'with(this)'); }, }; } function emitted(text, bytes) { console.log(green(text), getSize(bytes)); } function green(text) { return ("\u001b[1m\u001b[32m" + text + "\u001b[39m\u001b[22m"); } function getSize(bytes) { if (bytes < 10000) { return ((bytes.toFixed(0)) + " B"); } return bytes < 1024000 ? (((bytes / 1024).toPrecision(3)) + " kB'") : (((bytes / 1024 / 1024).toPrecision(4)) + " MB"); } export default vue;