mya-parser-vue-loader
Version:
A parser plugin for mya to parser vue component.
286 lines (237 loc) • 8.38 kB
JavaScript
var path = require('path');
var objectAssign = require('object-assign');
var hash = require('hash-sum');
var compiler = require('vue-template-compiler');
var genId = require('./lib/gen-id');
var rewriteStyle = require('./lib/style-rewriter');
var compileTemplate = require('./lib/template-compiler');
var insertCSS = require('./lib/insert-css');
var replaceScopedFlag = require('./lib/replace-scoped-flag');
// 缓存模块内容的 md5
var contentCache = {};
// 缓存 style 标签的个数
var styleNumCache = {};
function generateSourceMap(file, sourceMap, content) {
sourceMap.sourcesContent = [content];
const mapping = fis.file.wrap(file.dirname + '/' + file.filename + file.rExt + '.map');
mapping.setContent(JSON.stringify(sourceMap, null, 4));
// file.extras = file.extras || {};
// file.extras.derived = file.extras.derived || [];
// file.extras.derived.push(mapping);
file.derived.push(mapping);
}
function generateHMRCode(id, output, file) {
// 是否第一次初始化
const inited = !contentCache[id];
if (inited) {
createContentCache(id);
createStyleNumCache(id, output.styles.length);
}
const isTemplateUpdated = checkContentUpdate(id, output.template && output.template.content, 'template');
const isScriptUpdated = checkContentUpdate(id, clearComment(output.script && output.script.content).trim(), 'script');
const isStyleUpdated = checkContentUpdate(id, output.styles.map(style => style.content || '').join(''), 'style');
const isStyleNumDeleted = checkStyleNumDeleted(id, output.styles.length);
file.extras.hmr = file.extras.hmr = {};
if (!inited && isStyleNumDeleted) {
// 强制刷新页面
file.extras.hmr.forceReload = true;
}
if (inited || isScriptUpdated) {
file.extras.hmr.noReload = false;
return `if (module.hot) {
(function () {
var hotAPI = window.vueHotReloadApi;
hotAPI.install(require('vue'), false);
if (!hotAPI.compatible) return;
module.hot.accept('${id}');
if (module.hot.data['${id}']) {
hotAPI.reload('${id}', __vue__options__);
} else {
module.hot.data['${id}'] = true;
hotAPI.createRecord('${id}', __vue__options__);
}
})();
}`;
}
// script 内容未变化,只 hotAPI.rerender,不 hotAPI.reload
file.extras.hmr.noReload = true;
return `if (module.hot) {
(function () {
var hotAPI = window.vueHotReloadApi;
hotAPI.install(require('vue'), false);
if (!hotAPI.compatible) return;
module.hot.accept('${id}');
if (module.hot.data['${id}']) {
var options = {
render: __vue__options__.render,
_withStripped: true,
staticRenderFns: __vue__options__.staticRenderFns
};
hotAPI.rerender('${id}', options);
} else {
module.hot.data['${id}'] = true;
hotAPI.createRecord('${id}', __vue__options__);
}
})();
}`;
}
function createContentCache(id) {
contentCache[id] = {
template: '',
script: '',
style: ''
}
}
function checkContentUpdate(id, content='', type) {
const contentHash = hash(id + content);
if (contentHash === contentCache[id][type]) {
return false;
}
contentCache[id][type] = contentHash;
return true;
}
function clearComment(content='') {
// content 可能为 null
content = content || '';
return content.replace(/^\s*\/\/$/gm, '');
}
function createStyleNumCache(id, num) {
styleNumCache[id] = num;
}
function checkStyleNumDeleted(id, num) {
if (styleNumCache[id] <= num) {
styleNumCache[id] = num;
return false;
}
styleNumCache[id] = num;
return true;
}
// exports
module.exports = function(content, file, conf) {
var scriptStr = '';
var output, configs, jsLang;
// configs
configs = objectAssign({
cssScopedFlag: '__vuec__',
extractCSS: true,
cssScopedIdPrefix: '_v-',
cssScopedHashType: 'sum',
cssScopedHashLength: 8,
styleNameJoin: '',
runtimeOnly: false,
hot: false
}, conf);
// 兼容content为buffer的情况
content = content.toString();
// generate css scope id
var id = configs.cssScopedIdPrefix + genId(file, configs);
// 兼容旧的cssScopedFlag
if (configs.cssScopedFlag) {
content = replaceScopedFlag(content, configs.cssScopedFlag, id);
}
// parse
var output = compiler.parseComponent(content.toString(), { pad: true });
// check for scoped style nodes
var hasScopedStyle = output.styles.some(function (style) {
return style.scoped
});
// script
if (output.script) {
scriptStr = output.script.content;
jsLang = output.script.lang || 'js';
} else {
scriptStr += 'module.exports = {}';
jsLang = 'js';
}
fis.once('parser-babel:getSourceMap', function(sourceMap, subpath) {
if (file.subpath === subpath) {
generateSourceMap(file, sourceMap, content)
}
});
// 部分内容以 js 的方式编译一次。如果要支持 cooffe 需要这么配置。
// 只针对预编译语言
// fis.match('*.vue:cooffe', {
// parser: fis.plugin('cooffe')
// })
scriptStr = fis.compile.partial(scriptStr, file, {
ext: jsLang,
isJsLike: true
});
// 开启 sourceMap 时,在 mya-parser-babel 中会触发 parser-babel:getSourceMap
// 未开启 sourceMap 时,需要手动触发事件,防止 EventEmitter memory leak
fis.emit('parser-babel:getSourceMap', {}, '');
scriptStr += '\nvar __vue__options__;\n';
scriptStr += 'if(exports && exports.__esModule && exports.default){\n';
scriptStr += ' __vue__options__ = exports.default;\n';
scriptStr += '}else{\n';
scriptStr += ' __vue__options__ = module.exports;\n';
scriptStr += '}\n';
if(output.template){
var templateContent = fis.compile.partial(output.template.content, file, {
ext: output.template.lang || 'html',
isHtmlLike: true
});
// runtimeOnly
if(configs.runtimeOnly){
var result = compileTemplate(templateContent);
if(result){
scriptStr += '__vue__options__.render =' + result.render + '\n';
scriptStr += '__vue__options__.staticRenderFns =' + result.staticRenderFns + '\n';
}
}else{
// template
scriptStr += '__vue__options__.template = ' + JSON.stringify(templateContent) + '\n';
}
}
if(hasScopedStyle){
// template
scriptStr += '__vue__options__._scopeId = ' + JSON.stringify(id) + '\n';
}
// hot module replacement
if (configs.hot) {
let id = file.subpath.slice(1);
scriptStr += generateHMRCode(id, output, file);
}
// style
output.styles.forEach(function(item, index) {
if (!item.content && !configs.hot) {
return;
}
// empty string, or all space line
if (/^\s*$/.test(item.content) && !configs.hot) {
return;
}
// css也采用片段编译,更好的支持less、sass等其他语言
var styleContent = fis.compile.partial(item.content, file, {
ext: item.lang || 'css',
isCssLike: true
});
styleContent = rewriteStyle(id, styleContent, item.scoped, {})
// 开启 hmr 时,style以内联方式插入页面
if (!configs.extractCSS || configs.hot) {
var styleId = output.styles.length == 1 ? file.id : `${file.id}-${index}`;
// configs.hot ?
// scriptStr += `\n;(${insertCSS})(${JSON.stringify(styleContent.trim())}, '${styleId}');\n` :
// scriptStr += '\n;(' + insertCSS + ')(' + JSON.stringify(styleContent) + ');\n';
scriptStr += `\n;(${insertCSS})(${JSON.stringify(styleContent.trim())}, '${styleId}');\n`;
return scriptStr;
}
var styleFileName, styleFile;
if (output['styles'].length == 1) {
styleFileName = file.realpathNoExt + configs.styleNameJoin + '.css';
} else {
styleFileName = file.realpathNoExt + configs.styleNameJoin + '-' + index + '.css';
}
styleFile = fis.file.wrap(styleFileName);
styleFile.cache = file.cache;
styleFile.isCssLike = true;
styleFile.setContent(styleContent);
fis.compile.process(styleFile);
styleFile.links.forEach(function(derived) {
file.addLink(derived);
});
file.derived.push(styleFile);
file.addRequire(styleFile.getId());
});
return scriptStr;
};