rollup-plugin-vue
Version:
Roll .vue files
364 lines (318 loc) • 10.4 kB
JavaScript
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;