gulp-vue-single-file-component
Version:
This plugin compiles Vue single file components (SFC) to plain JavaScript.
170 lines (145 loc) • 7.03 kB
JavaScript
var fs = require('fs');
var less = require('less');
var parse5 = require('parse5');
var sass = require('sass');
var through = require('through2');
var PLUGIN_NAME = 'gulp-vue-single-file-component';
var LOG_PREFIX = '[' + PLUGIN_NAME + ']';
function getAttribute(node, name) {
var attr = node.attrs.filter(function(attr) {
return attr.name == name;
});
return attr.length ? attr[0].value : null;
}
function minify(input) {
return input.split('\n').map(function(line) {
return line.trim();
}).filter(function(line) {
return line != '';
}).join(' ') // Make sure it keeps a space (fixes https://github.com/nfplee/gulp-vue-single-file-component/issues/6).
.replace(/"/g, '\\"')
.trim();
}
/**
* This plugin compiles Vue single file components to plain Javascript.
*
* ## Usage:
* ```js
* gulp.task('vue', function() {
* return gulp.src('./js/components/*.vue')
* .pipe(vueComponent({ ...options })
* .pipe(gulp.dest('./dist/'));
* ```
* ## Options
* @param debug Display verbose output when building components.
* @param loadCssMethod Method used to load the component's CSS within the application.
* @param lessOptions Options passed to `less`. See the [`less` docs](https://lesscss.org/usage/#less-options).
* @param sassOptions Options passed to `sass`. See the [`sass` docs](https://sass-lang.com/documentation/js-api/interfaces/options/).
*/
module.exports = function(options) {
var defaults = {
debug: false,
loadCssMethod: 'require.loadCss',
lessOptions: {},
sassOptions: {},
};
var settings = Object.assign({}, defaults, options);
return through.obj(function(file, encoding, callback) {
if (file.isNull()) {
return callback(null, file);
}
if (file.isStream()) {
console.log(LOG_PREFIX, 'Cannot use streamed files');
return callback();
}
if (settings.debug) {
console.log(LOG_PREFIX, 'File =>', file.path);
}
// Parse the the file content and get the tag content.
var contents = file.contents.toString(encoding),
fragment = parse5.parseFragment(contents, {
locationInfo: true
}),
tagContent = [];
// Work out whether the component is a petite-vue component (since petite-vue exports a function instead of an object).
var isPetiteVue = contents.includes('export default function');
fragment.childNodes.forEach(function(node) {
if (node.tagName == 'style') {
var style = parse5.serialize(node),
lang = getAttribute(node, 'lang'),
href = getAttribute(node, 'href');
if (lang == 'less') {
var options = Object.assign({
compress: true
}, settings.lessOptions);
less.render(style, options, function(e, result) {
tagContent['style'] = '{ content: "' + minify(result.css) + '" }';
});
} else if (lang == 'sass' || lang == 'scss') {
var options = Object.assign({
style: 'compressed',
syntax: lang == 'sass' ? 'indented' : 'scss',
}, settings.sassOptions);
var result;
if (href) {
result = sass.compile(href, options);
} else {
result = sass.compileString(style, options);
}
tagContent['style'] = '{ content: "' + minify(result.css) + '" }';
} else {
if (href) {
tagContent['style'] = '{ url: \'' + href + '\' }';
} else {
tagContent['style'] = '{ content: "' + minify(style) + '" }';
}
}
} else if (node.tagName == 'template') {
var template,
include = getAttribute(node, 'include');
if (include) {
template = fs.readFileSync(include, 'utf-8');
} else {
//var treeAdapter = parse5.treeAdapters.default,
// docFragment = treeAdapter.createDocumentFragment();
//treeAdapter.appendChild(docFragment, node);
//template = parse5.serialize(docFragment);
//template = template.replace('<template>', '');
//template = template.substring(0, template.lastIndexOf('</template>'));
// parse5 lowercases any attributes in the template which causes problems when using scoped slots within the template.
template = contents.substring(contents.indexOf('<template>') + 10, contents.lastIndexOf('</template>'));
}
tagContent['template'] = minify(template);
} else if (node.tagName === 'script') {
tagContent['script'] = parse5.serialize(node);
}
});
// Build up the file content.
var content = tagContent['script'];
// Add a beforeCreate event to load the CSS (if applicable).
if (tagContent['style']) {
if (!isPetiteVue) {
if (!content.includes('beforeCreate()')) {
content = content.replace(/(export default [^{]*{)/, '$1\n beforeCreate() {\n ' + settings.loadCssMethod + '(' + tagContent['style'] + ');\n },');
}
} else {
content = content.replace(/export default(.*?)return {/s, 'export default$1' + settings.loadCssMethod + '(' + tagContent['style'] + ');\n return {');
}
}
// Add the template (if applicable).
if (tagContent['template']) {
// Note: Using " intead of ` allows us to use template literals e.g. <button :id="`my-dynamic-id-${id}`">Text</button>. However we must make sure the template has removed all line breaks.
if (!isPetiteVue) {
if (!content.includes('template:')) {
content = content.replace(/(export default [^{]*{)/, '$1\n template: "' + tagContent['template'] + '",');
}
} else {
if (!content.includes('$template:')) {
content = content.replace(/(export default.*?return {)/s, '$1\n $template: "' + tagContent['template'] + '",');
}
}
}
file.contents = new Buffer(content);
callback(null, file);
});
}