@mpxjs/webpack-plugin
Version:
mpx compile core
753 lines (703 loc) • 25.4 kB
JavaScript
const async = require('async')
const JSON5 = require('json5')
const path = require('path')
const parseComponent = require('../parser')
const config = require('../config')
const parseRequest = require('../utils/parse-request')
const evalJSONJS = require('../utils/eval-json-js')
const getRulesRunner = require('../platform/index')
const addQuery = require('../utils/add-query')
const getJSONContent = require('../utils/get-json-content')
const createHelpers = require('../helpers')
const createJSONHelper = require('./helper')
const RecordIndependentDependency = require('../dependencies/RecordIndependentDependency')
const RecordRuntimeInfoDependency = require('../dependencies/RecordRuntimeInfoDependency')
const { MPX_DISABLE_EXTRACTOR_CACHE, RESOLVE_IGNORED_ERR, JSON_JS_EXT } = require('../utils/const')
const resolve = require('../utils/resolve')
const resolveTabBarPath = require('../utils/resolve-tab-bar-path')
const normalize = require('../utils/normalize')
const mpxViewPath = normalize.lib('runtime/components/ali/mpx-view.mpx')
const mpxTextPath = normalize.lib('runtime/components/ali/mpx-text.mpx')
const resolveMpxCustomElementPath = require('../utils/resolve-mpx-custom-element-path')
module.exports = function (content) {
const nativeCallback = this.async()
const mpx = this.getMpx()
if (!mpx) {
return nativeCallback(null, content)
}
// json模块必须每次都创建(但并不是每次都需要build),用于动态添加编译入口,传递信息以禁用父级extractor的缓存
this.emitFile(MPX_DISABLE_EXTRACTOR_CACHE, '', undefined, { skipEmit: true })
// 微信插件下要求组件使用相对路径
const useRelativePath = mpx.isPluginMode || mpx.useRelativePath
const { resourcePath, queryObj } = parseRequest(this.resource)
const useJSONJS = queryObj.useJSONJS || this.resourcePath.endsWith(JSON_JS_EXT)
const packageName = queryObj.packageRoot || mpx.currentPackageRoot || 'main'
const pagesMap = mpx.pagesMap
const componentsMap = mpx.componentsMap[packageName]
const appInfo = mpx.appInfo
const mode = mpx.mode
const env = mpx.env
const globalSrcMode = mpx.srcMode
const localSrcMode = queryObj.mode
const srcMode = localSrcMode || globalSrcMode
const projectRoot = mpx.projectRoot
const isApp = !(pagesMap[resourcePath] || componentsMap[resourcePath])
const publicPath = this._compilation.outputOptions.publicPath || ''
const fs = this._compiler.inputFileSystem
const runtimeCompile = queryObj.isDynamic
const emitWarning = (msg) => {
this.emitWarning(
new Error('[json compiler][' + this.resource + ']: ' + msg)
)
}
const emitError = (msg) => {
this.emitError(
new Error('[json compiler][' + this.resource + ']: ' + msg)
)
}
const fillInComponentPlaceholder = (name, placeholder, placeholderEntry) => {
const componentPlaceholder = json.componentPlaceholder || {}
if (componentPlaceholder[name]) return
componentPlaceholder[name] = placeholder
json.componentPlaceholder = componentPlaceholder
if (placeholderEntry && !json.usingComponents[placeholder]) json.usingComponents[placeholder] = placeholderEntry
}
const normalizePlaceholder = (placeholder) => {
if (typeof placeholder === 'string') {
const placeholderMap = mode === 'ali'
? {
view: { name: 'mpx-view', resource: mpxViewPath },
text: { name: 'mpx-text', resource: mpxTextPath }
}
: {}
placeholder = placeholderMap[placeholder] || { name: placeholder }
}
if (!placeholder.name) {
emitError('The asyncSubpackageRules configuration format of @mpxjs/webpack-plugin a is incorrect')
}
return placeholder
}
const {
isUrlRequest,
urlToRequest,
processPage,
processDynamicEntry,
processComponent,
processJsExport
} = createJSONHelper({
loaderContext: this,
emitWarning,
emitError
})
const { getRequestString } = createHelpers(this)
let currentName
if (isApp) {
currentName = appInfo.name
} else {
currentName = componentsMap[resourcePath] || pagesMap[resourcePath]
}
const relativePath = useRelativePath ? publicPath + path.dirname(currentName) : ''
const copydir = (dir, context, callback) => {
fs.readdir(dir, (err, files) => {
if (err) return callback(err)
async.each(files, (file, callback) => {
file = path.join(dir, file)
async.waterfall([
(callback) => {
fs.stat(file, callback)
},
(stats, callback) => {
if (stats.isDirectory()) {
copydir(file, context, callback)
} else {
fs.readFile(file, (err, content) => {
if (err) return callback(err)
if (!this._compilation) return callback()
const targetPath = path.relative(context, file)
this.emitFile(targetPath, content)
callback()
})
}
}
], callback)
}, callback)
})
}
const callback = (err, processOutput) => {
if (err) return nativeCallback(err)
let output = `var json = ${JSON.stringify(json, null, 2)};\n`
if (processOutput) output = processOutput(output)
const jsonSpace = this.minimize ? 0 : 2
output += `module.exports = JSON.stringify(json, null, ${jsonSpace});\n`
nativeCallback(null, output)
}
let json
try {
if (useJSONJS) {
json = evalJSONJS(content, this.resourcePath, this)
} else {
json = JSON5.parse(content || '{}')
}
} catch (err) {
return callback(err)
}
// json补全
if (pagesMap[resourcePath]) {
// page
if (!mpx.forceUsePageCtor) {
if (!json.usingComponents) {
json.usingComponents = {}
}
if (!json.component && mode === 'swan') {
json.component = true
}
}
} else if (componentsMap[resourcePath]) {
// component
if (json.component !== true) {
json.component = true
}
}
const dependencyComponentsMap = {}
if (queryObj.mpxCustomElement) {
this.cacheable(false)
mpx.collectDynamicSlotDependencies(packageName)
}
if (runtimeCompile) {
json.usingComponents = json.usingComponents || {}
}
// 快应用补全json配置,必填项
if (mode === 'qa' && isApp) {
const defaultConf = {
package: '',
name: '',
icon: 'assets/images/logo.png',
versionName: '',
versionCode: 1,
minPlatformVersion: 1080
}
json = Object.assign({}, defaultConf, json)
}
const rulesRunnerOptions = {
mode,
srcMode,
type: 'json',
waterfall: true,
warn: emitWarning,
error: emitError,
data: {
// polyfill global usingComponents
globalComponents: mpx.globalComponents
}
}
if (!isApp) {
rulesRunnerOptions.mainKey = pagesMap[resourcePath] ? 'page' : 'component'
}
const rulesRunner = getRulesRunner(rulesRunnerOptions)
if (rulesRunner) {
rulesRunner(json)
}
const processComponents = (components, context, callback) => {
if (components) {
async.eachOf(components, (component, name, callback) => {
processComponent(component, context, { relativePath }, (err, entry, { tarRoot, placeholder, resourcePath, queryObj = {} } = {}) => {
if (err === RESOLVE_IGNORED_ERR) {
delete components[name]
return callback()
}
if (err) return callback(err)
components[name] = entry
if (runtimeCompile) {
// 替换组件的 hashName,并删除原有的组件配置
const hashName = 'm' + mpx.pathHash(resourcePath)
components[hashName] = entry
delete components[name]
dependencyComponentsMap[name] = {
hashName,
resourcePath,
isDynamic: queryObj.isDynamic
}
}
if (tarRoot) {
if (placeholder) {
placeholder = normalizePlaceholder(placeholder)
if (placeholder.resource) {
processComponent(placeholder.resource, projectRoot, { relativePath }, (err, entry) => {
if (err) return callback(err)
fillInComponentPlaceholder(name, placeholder.name, entry)
callback()
})
} else {
fillInComponentPlaceholder(name, placeholder.name)
callback()
}
} else {
if (!json.componentPlaceholder || !json.componentPlaceholder[name]) {
const errMsg = `componentPlaceholder of "${name}" doesn't exist! \n\r`
emitError(errMsg)
}
callback()
}
} else {
callback()
}
})
}, (err) => {
if (err) return callback(err)
const mpxCustomElementPath = resolveMpxCustomElementPath(packageName)
if (runtimeCompile) {
components.element = mpxCustomElementPath
components.mpx_dynamic_slot = '' // 运行时组件打标记,在 processAssets 统一替换
this._module.addPresentationalDependency(new RecordRuntimeInfoDependency(packageName, resourcePath, { type: 'json', info: dependencyComponentsMap }))
}
if (queryObj.mpxCustomElement) {
components.element = mpxCustomElementPath
Object.assign(components, mpx.getPackageInjectedComponentsMap(packageName))
}
callback()
})
} else {
callback()
}
}
if (isApp) {
// app.json
const localPages = []
const subPackagesCfg = {}
const pageKeySet = new Set()
const defaultPagePath = require.resolve('../runtime/components/wx/default-page.mpx')
const processPages = (pages, context, tarRoot = '', callback) => {
if (pages) {
const pagesCache = []
async.each(pages, (page, callback) => {
processPage(page, context, tarRoot, (err, entry, { isFirst, key, resource } = {}) => {
if (err) return callback(err === RESOLVE_IGNORED_ERR ? null : err)
if (pageKeySet.has(key)) return callback()
if (resource.startsWith(defaultPagePath)) {
pagesCache.push(entry)
return callback()
}
pageKeySet.add(key)
if (tarRoot && subPackagesCfg) {
subPackagesCfg[tarRoot].pages.push(entry)
} else {
// 确保首页
if (isFirst) {
localPages.unshift(entry)
} else {
localPages.push(entry)
}
}
callback()
})
}, (err) => {
if (err) return callback(err)
if (tarRoot && subPackagesCfg) {
if (!subPackagesCfg[tarRoot].pages.length && pagesCache[0]) {
subPackagesCfg[tarRoot].pages.push(pagesCache[0])
}
} else {
if (!localPages.length && pagesCache[0]) {
localPages.push(pagesCache[0])
}
}
callback()
})
} else {
callback()
}
}
const processPackages = (packages, context, callback) => {
if (packages) {
async.each(packages, (packagePath, callback) => {
const { queryObj } = parseRequest(packagePath)
async.waterfall([
(callback) => {
resolve(context, packagePath, this, (err, result) => {
if (err) return callback(err)
const { rawResourcePath } = parseRequest(result)
callback(err, rawResourcePath)
})
},
(result, callback) => {
fs.readFile(result, (err, content) => {
if (err) return callback(err)
callback(err, result, content.toString('utf-8'))
})
},
(result, content, callback) => {
const extName = path.extname(result)
if (extName === '.mpx') {
const parts = parseComponent(content, {
filePath: result,
needMap: this.sourceMap,
mode,
env
})
// 对于通过.mpx文件声明的独立分包,默认将其自身的script block视为init module
if (queryObj.independent === true) queryObj.independent = result
getJSONContent(parts.json || {}, result, this, (err, content) => {
callback(err, result, content)
})
} else {
callback(null, result, content)
}
},
(result, content, callback) => {
try {
content = JSON5.parse(content)
} catch (err) {
return callback(err)
}
const processSelfQueue = []
const context = path.dirname(result)
if (content.pages) {
const tarRoot = queryObj.root
if (tarRoot) {
delete queryObj.root
const subPackage = {
tarRoot,
pages: content.pages,
...queryObj
}
if (content.plugins) {
subPackage.plugins = content.plugins
}
processSelfQueue.push((callback) => {
processSubPackage(subPackage, context, callback)
})
} else {
processSelfQueue.push((callback) => {
processPages(content.pages, context, '', callback)
})
}
}
if (content.packages) {
processSelfQueue.push((callback) => {
processPackages(content.packages, context, callback)
})
}
if (processSelfQueue.length) {
async.parallel(processSelfQueue, callback)
} else {
callback()
}
}
], (err) => {
callback(err === RESOLVE_IGNORED_ERR ? null : err)
})
}, callback)
} else {
callback()
}
}
const getOtherConfig = (config) => {
const result = {}
const blackListMap = {
tarRoot: true,
srcRoot: true,
root: true,
pages: true
}
for (const key in config) {
if (!blackListMap[key]) {
result[key] = config[key]
}
}
return result
}
const recordIndependent = (root, request) => {
this._module && this._module.addPresentationalDependency(new RecordIndependentDependency(root, request))
}
const processIndependent = (otherConfig, context, tarRoot, callback) => {
// 支付宝不支持独立分包,无需处理
const independent = otherConfig.independent
if (!independent || mode === 'ali') {
delete otherConfig.independent
return callback()
}
// independent配置为字符串时视为init module
if (typeof independent === 'string') {
otherConfig.independent = true
resolve(context, independent, this, (err, result) => {
if (err) return callback(err)
recordIndependent(tarRoot, result)
callback()
})
} else {
recordIndependent(tarRoot, true)
callback()
}
}
// 为了获取资源的所属子包,该函数需串行执行
const processSubPackage = (subPackage, context, callback) => {
if (subPackage) {
if (typeof subPackage.root === 'string' && subPackage.root.startsWith('.')) {
emitError(`Current subpackage root [${subPackage.root}] is not allow starts with '.'`)
return callback()
}
const tarRoot = subPackage.tarRoot || subPackage.root || ''
const srcRoot = subPackage.srcRoot || subPackage.root || ''
if (!tarRoot) return callback()
context = path.join(context, srcRoot)
const otherConfig = getOtherConfig(subPackage)
subPackagesCfg[tarRoot] = subPackagesCfg[tarRoot] || {
root: tarRoot,
pages: []
}
async.parallel([
(callback) => {
processIndependent(otherConfig, context, tarRoot, callback)
},
(callback) => {
processPages(subPackage.pages, context, tarRoot, callback)
},
(callback) => {
processPlugins(subPackage.plugins, context, tarRoot, callback)
}
], (err) => {
if (err) return callback(err)
Object.assign(subPackagesCfg[tarRoot], otherConfig)
callback()
})
} else {
callback()
}
}
const processSubPackages = (subPackages, context, callback) => {
if (subPackages) {
async.each(subPackages, (subPackage, callback) => {
processSubPackage(subPackage, context, callback)
}, callback)
} else {
callback()
}
}
const processTabBar = (output) => {
const tabBarCfg = config[mode].tabBar
const itemKey = tabBarCfg.itemKey
const iconKey = tabBarCfg.iconKey
const activeIconKey = tabBarCfg.activeIconKey
if (json.tabBar && json.tabBar[itemKey]) {
json.tabBar[itemKey].forEach((item, index) => {
if (item[iconKey] && isUrlRequest(item[iconKey])) {
output += `json.tabBar.${itemKey}[${index}].${iconKey} = require("${addQuery(urlToRequest(item[iconKey]), { useLocal: true })}");\n`
}
if (item[activeIconKey] && isUrlRequest(item[activeIconKey])) {
output += `json.tabBar.${itemKey}[${index}].${activeIconKey} = require("${addQuery(urlToRequest(item[activeIconKey]), { useLocal: true })}");\n`
}
})
}
return output
}
const processOptionMenu = (output) => {
const optionMenuCfg = config[mode].optionMenu
if (optionMenuCfg && json.optionMenu) {
const iconKey = optionMenuCfg.iconKey
if (json.optionMenu[iconKey] && isUrlRequest(json.optionMenu[iconKey])) {
output += `json.optionMenu.${iconKey} = require("${addQuery(urlToRequest(json.optionMenu[iconKey]), { useLocal: true })}");\n`
}
}
return output
}
const processThemeLocation = (output) => {
if (json.themeLocation && isUrlRequest(json.themeLocation)) {
const requestString = getRequestString('json', { src: urlToRequest(json.themeLocation) }, {
isTheme: true,
isStatic: true
})
output += `json.themeLocation = require(${requestString});\n`
}
return output
}
const processWorkers = (workers, context, callback) => {
if (workers) {
const workersPath = path.join(context, workers)
this.addContextDependency(workersPath)
copydir(workersPath, context, callback)
} else {
callback()
}
}
const processCustomTabBar = (tabBar, context, callback) => {
const outputCustomKey = config[mode].tabBar.customKey
if (tabBar && tabBar[outputCustomKey]) {
const srcCustomKey = config[srcMode].tabBar.customKey
const srcPath = resolveTabBarPath(srcCustomKey)
const outputPath = resolveTabBarPath(outputCustomKey)
processComponent(`./${srcPath}`, context, {
outputPath,
extraOptions: {
replaceContent: 'true'
}
}, (err, entry) => {
if (err === RESOLVE_IGNORED_ERR) {
delete tabBar[srcCustomKey]
return callback()
}
if (err) return callback(err)
tabBar[outputCustomKey] = entry // hack for javascript parser call hook.
callback()
})
} else {
callback()
}
}
const processAppBar = (appBar, context, callback) => {
if (appBar) {
processComponent('./app-bar/index', context, {
outputPath: 'app-bar/index',
extraOptions: {
replaceContent: 'true'
}
}, (err, entry) => {
if (err === RESOLVE_IGNORED_ERR) {
return callback()
}
if (err) return callback(err)
appBar.custom = entry // hack for javascript parser call hook.
callback()
})
} else {
callback()
}
}
const processPluginGenericsImplementation = (plugin, context, tarRoot, callback) => {
if (!plugin.genericsImplementation) return callback()
const relativePath = useRelativePath ? publicPath + tarRoot : ''
async.eachOf(plugin.genericsImplementation, (genericComponents, name, callback) => {
async.eachOf(genericComponents, (genericComponentPath, name, callback) => {
processComponent(genericComponentPath, context, {
tarRoot,
relativePath
}, (err, entry) => {
if (err === RESOLVE_IGNORED_ERR) {
delete genericComponents[name]
return callback()
}
if (err) return callback(err)
genericComponents[name] = entry
callback()
})
}, callback)
}, callback)
}
const processPluginExport = (plugin, context, tarRoot, callback) => {
if (!plugin.export) return callback()
processJsExport(plugin.export, context, tarRoot, (err, entry) => {
if (err === RESOLVE_IGNORED_ERR) {
delete plugin.export
return callback()
}
if (err) return callback(err)
plugin.export = entry
callback()
})
}
const processPlugins = (plugins, context, tarRoot = '', callback) => {
if (mode !== 'wx' || !plugins) return callback() // 目前只有微信支持导出到插件
async.eachOf(plugins, (plugin, name, callback) => {
async.parallel([
(callback) => {
processPluginGenericsImplementation(plugin, context, tarRoot, callback)
},
(callback) => {
processPluginExport(plugin, context, tarRoot, callback)
}
], callback)
}, callback)
}
async.parallel([
(callback) => {
// 添加首页标识
if (json.pages && json.pages[0]) {
if (typeof json.pages[0] !== 'string') {
json.pages[0].src = addQuery(json.pages[0].src, { isFirst: true })
} else {
json.pages[0] = addQuery(json.pages[0], { isFirst: true })
}
}
processPages(json.pages, this.context, '', callback)
},
(callback) => {
processComponents(json.usingComponents, this.context, callback)
},
(callback) => {
processPlugins(json.plugins, this.context, '', callback)
},
(callback) => {
processWorkers(json.workers, this.context, callback)
},
(callback) => {
processPackages(json.packages, this.context, callback)
},
(callback) => {
processCustomTabBar(json.tabBar, this.context, callback)
},
(callback) => {
processSubPackages(json.subPackages || json.subpackages, this.context, callback)
},
(callback) => {
processAppBar(json.appBar, this.context, callback)
}
], (err) => {
if (err) return callback(err)
delete json.packages
delete json.subpackages
delete json.subPackages
json.pages = localPages
for (const root in subPackagesCfg) {
const subPackageCfg = subPackagesCfg[root]
// 分包不存在 pages,输出 subPackages 字段会报错
// tt模式下分包异步允许一个分包不存在 pages
if (subPackageCfg.pages.length || mode === 'tt') {
if (!json.subPackages) {
json.subPackages = []
}
json.subPackages.push(subPackageCfg)
}
}
const processOutput = (output) => {
output = processDynamicEntry(output)
output = processTabBar(output)
output = processOptionMenu(output)
output = processThemeLocation(output)
return output
}
callback(null, processOutput)
})
} else {
// page.json或component.json
const processGenerics = (generics, context, callback) => {
if (generics) {
async.eachOf(generics, (generic, name, callback) => {
if (generic.default) {
processComponent(generic.default, context, { relativePath }, (err, entry) => {
if (err === RESOLVE_IGNORED_ERR) {
delete generic.default
return callback()
}
if (err) return callback(err)
generic.default = entry
callback()
})
} else {
callback()
}
}, callback)
} else {
callback()
}
}
async.parallel([
(callback) => {
processComponents(json.usingComponents, this.context, callback)
},
(callback) => {
processGenerics(json.componentGenerics, this.context, callback)
}
], (err) => {
callback(err, processDynamicEntry)
})
}
}