mpvue-loader
Version:
mpvue single-file component loader for Webpack
237 lines (211 loc) • 8.27 kB
JavaScript
const babel = require('babel-core')
const path = require('path')
const fs = require('fs')
const deepEqual = require('deep-equal')
const compiler = require('mpvue-template-compiler')
const { parseConfig, parseComponentsDeps, parseGlobalComponents, clearGlobalComponents } = require('./parse')
const { parseComponentsDeps: parseComponentsDepsTs } = require('./parse-ts')
const { genPageML } = require('./templates')
const {
cacheFileInfo,
getFileInfo,
getCompInfo,
resolveTarget,
covertCCVar,
cacheSlots,
getSlots,
htmlBeautify,
getBabelrc
} = require('./util')
function genComponentMPML (compiled, options, emitFile, emitError, emitWarning, fileExt) {
options.components['slots'] = { src: '/components/slots', name: 'slots' }
const { code: mpmlContent, compiled: compiledResult, slots, importCode } = compiler.compileToMPML(compiled, options, fileExt)
const { mpErrors, mpTips } = compiledResult
// 缓存 slots,延迟编译
cacheSlots(slots, importCode)
if (mpErrors && mpErrors.length) {
emitError('\n Error compiling template:\n' + mpErrors.map(e => ` - ${e}`).join('\n') + '\n')
}
if (mpTips && mpTips.length) {
emitWarning(mpTips.map(e => ` - ${e}`).join('\n') + '\n')
}
return htmlBeautify(mpmlContent)
}
function createPageMPML (emitFile, resourcePath, rootComponent, context, fileExt) {
const { src } = getFileInfo(resourcePath) || {}
const { name, filePath } = getCompInfo(context, rootComponent, fileExt)
const MPMLContent = genPageML(name, filePath, fileExt)
emitFile(`${src}.${fileExt.template}`, MPMLContent)
}
// 更新全局组件时,需要重新生成 mpml,用这个字段保存所有需要更新的页面及其参数
const cacheCreateMPMLFns = {}
function createComponentMPML ({ emitWarning, emitError, emitFile, resourcePath, context, compiled, fileExt }) {
cacheCreateMPMLFns[resourcePath] = arguments
const { pageType, moduleId, components } = getFileInfo(resourcePath) || {}
const { name, filePath } = getCompInfo(context, resourcePath, fileExt)
const options = { components, pageType, name, moduleId }
const MPMLContent = genComponentMPML(compiled, options, emitFile, emitError, emitWarning, fileExt)
emitFile(filePath, MPMLContent)
}
let slotsHookAdded = false
function compileMPML (compiled, html, options) {
const fileExt = options.fileExt
if (!slotsHookAdded) {
// avoid add hook several times during compilation
slotsHookAdded = true
// TODO: support webpack4
this._compilation.plugin('seal', () => {
const content = getSlots()
if (content.trim()) {
this.emitFile(`components/slots.${fileExt.template}`, htmlBeautify(content))
}
slotsHookAdded = false
})
}
return new Promise(resolve => {
const pollComponentsStatus = () => {
const { pageType, components } = getFileInfo(this.resourcePath) || {}
if (!pageType || (components && !components.isCompleted)) {
setTimeout(pollComponentsStatus, 20)
} else {
resolve()
}
}
pollComponentsStatus()
}).then(() => {
createComponentMPML({
emitWarning: this.emitWarning,
emitError: this.emitError,
emitFile: this.emitFile,
resourcePath: this.resourcePath,
context: this.options.context,
rootComponent: null,
compiled, html,
fileExt
})
})
}
// 针对 .vue 单文件的脚本逻辑的处理
// 处理出当前单文件组件的子组件依赖
function compileMPScript (script, mpOptioins, moduleId) {
const { resourcePath, options, resolve, context } = this
const babelrc = getBabelrc(mpOptioins.globalBabelrc)
let scriptContent = script.content
const babelOptions = { extends: babelrc, plugins: [parseComponentsDeps] }
if (script.src) {
const scriptpath = path.join(path.dirname(resourcePath), script.src)
scriptContent = fs.readFileSync(scriptpath).toString()
}
let metadata
if (script.lang === 'ts') {
metadata = parseComponentsDepsTs(scriptContent)
} else {
const result = babel.transform(scriptContent, babelOptions)
metadata = result.metadata
}
// metadata: importsMap, components
const { importsMap, components: originComponents } = metadata
// 处理子组件的信息
const components = {}
const fileInfo = resolveTarget(resourcePath, options.entry)
const callback = () => resolveComponent(resourcePath, fileInfo, importsMap, components, moduleId)
if (originComponents) {
resolveSrc(originComponents, components, resolve, context, options.context, mpOptioins.fileExt)
.then(() => callback())
.catch(err => {
console.error(err)
callback()
})
} else {
callback()
}
return script
}
// checkMPEntry 针对 entry main.js 的入口处理: 编译出 app, page 的入口js、mpml、json
let globalComponents
function compileMP (content, mpOptioins) {
const { resourcePath, emitFile, resolve, context, options } = this
const fileInfo = resolveTarget(resourcePath, options.entry)
cacheFileInfo(resourcePath, fileInfo)
const { isApp, isPage } = fileInfo
if (isApp) {
// 解析前将可能存在的全局组件清空
clearGlobalComponents()
}
const babelrc = getBabelrc(mpOptioins.globalBabelrc)
// app入口进行全局component解析
const { metadata } = babel.transform(content, { extends: babelrc, plugins: isApp ? [parseConfig, parseGlobalComponents] : [parseConfig] })
// metadata: config
const { rootComponent, globalComponents: globalComps } = metadata
if (isApp) {
// 保存旧数据,用于对比
const oldGlobalComponents = globalComponents
// 开始解析组件路径时把全局组件清空,解析完成后再进行赋值,标志全局组件解析完成
globalComponents = null
// 解析全局组件的路径
const components = {}
resolveSrc(globalComps, components, resolve, context, options.context, mpOptioins.fileExt).then(() => {
handleResult(components)
}).catch(err => {
console.error(err)
handleResult(components)
})
const handleResult = components => {
globalComponents = components
// 热更时,如果全局组件更新,需要重新生成所有的 mpml
if (oldGlobalComponents && !deepEqual(oldGlobalComponents, globalComponents)) {
// 更新所有页面的组件
Object.keys(cacheResolveComponents).forEach(k => {
resolveComponent(...cacheResolveComponents[k])
})
// 重新生成所有 mpml
Object.keys(cacheCreateMPMLFns).forEach(k => {
createComponentMPML(...cacheCreateMPMLFns[k])
})
}
}
}
if (isApp || isPage) {
// 这儿应该异步在所有的模块都清晰后再生成
// 生成入口 mpml
if (isPage && rootComponent) {
resolve(context, rootComponent, (err, rootComponentSrc) => {
if (err) return
// 这儿需要搞定 根组件的 路径
createPageMPML(emitFile, resourcePath, rootComponentSrc, this.options.context, mpOptioins.fileExt)
})
}
}
return content
}
function resolveSrc (originComponents, components, resolveFn, context, projectRoot, fileExt) {
return Promise.all(Object.keys(originComponents).map(k => {
return new Promise((resolve, reject) => {
resolveFn(context, originComponents[k], (err, realSrc) => {
if (err) return reject(err)
const com = covertCCVar(k)
const { filePath, name } = getCompInfo(projectRoot, realSrc, fileExt)
components[com] = { src: filePath, name }
resolve()
})
})
}))
}
const cacheResolveComponents = {}
function resolveComponent (resourcePath, fileInfo, importsMap, localComponents, moduleId) {
// 需要等待全局组件解析完成
if (!globalComponents) {
setTimeout(resolveComponent, 20, ...arguments)
} else {
// 保存当前所有参数,在热更时如果全局组件发生变化,需要进行组件更新
cacheResolveComponents[resourcePath] = arguments
const components = Object.assign({}, globalComponents, localComponents)
components.isCompleted = true
cacheFileInfo(resourcePath, fileInfo, { importsMap, components, moduleId })
}
}
module.exports = {
compileMP,
compileMPML,
compileMPScript
}