@mpxjs/webpack-plugin
Version:
mpx compile core
1,157 lines (1,064 loc) • 89.5 kB
JavaScript
'use strict'
const path = require('path')
const { ConcatSource, RawSource } = require('webpack').sources
const ResolveDependency = require('./dependencies/ResolveDependency')
const InjectDependency = require('./dependencies/InjectDependency')
const ReplaceDependency = require('./dependencies/ReplaceDependency')
const NullFactory = require('webpack/lib/NullFactory')
const CommonJsVariableDependency = require('./dependencies/CommonJsVariableDependency')
const CommonJsAsyncDependency = require('./dependencies/CommonJsAsyncDependency')
const CommonJsExtractDependency = require('./dependencies/CommonJsExtractDependency')
const NormalModule = require('webpack/lib/NormalModule')
const EntryPlugin = require('webpack/lib/EntryPlugin')
const JavascriptModulesPlugin = require('webpack/lib/javascript/JavascriptModulesPlugin')
const FlagEntryExportAsUsedPlugin = require('webpack/lib/FlagEntryExportAsUsedPlugin')
const FileSystemInfo = require('webpack/lib/FileSystemInfo')
const ImportDependency = require('webpack/lib/dependencies/ImportDependency')
const ImportDependencyTemplate = require('./dependencies/ImportDependencyTemplate')
const AsyncDependenciesBlock = require('webpack/lib/AsyncDependenciesBlock')
const ProvidePlugin = require('webpack/lib/ProvidePlugin')
const normalize = require('./utils/normalize')
const toPosix = require('./utils/to-posix')
const addQuery = require('./utils/add-query')
const hasOwn = require('./utils/has-own')
const { every } = require('./utils/set')
const DefinePlugin = require('webpack/lib/DefinePlugin')
const ExternalsPlugin = require('webpack/lib/ExternalsPlugin')
const AddModePlugin = require('./resolver/AddModePlugin')
const AddEnvPlugin = require('./resolver/AddEnvPlugin')
const PackageEntryPlugin = require('./resolver/PackageEntryPlugin')
const DynamicRuntimePlugin = require('./resolver/DynamicRuntimePlugin')
const FixDescriptionInfoPlugin = require('./resolver/FixDescriptionInfoPlugin')
// const CommonJsRequireDependency = require('webpack/lib/dependencies/CommonJsRequireDependency')
// const HarmonyImportSideEffectDependency = require('webpack/lib/dependencies/HarmonyImportSideEffectDependency')
// const RequireHeaderDependency = require('webpack/lib/dependencies/RequireHeaderDependency')
// const RemovedModuleDependency = require('./dependencies/RemovedModuleDependency')
const AppEntryDependency = require('./dependencies/AppEntryDependency')
const RecordPageConfigMapDependency = require('./dependencies/RecordPageConfigsMapDependency')
const RecordResourceMapDependency = require('./dependencies/RecordResourceMapDependency')
const RecordGlobalComponentsDependency = require('./dependencies/RecordGlobalComponentsDependency')
const RecordIndependentDependency = require('./dependencies/RecordIndependentDependency')
const DynamicEntryDependency = require('./dependencies/DynamicEntryDependency')
const FlagPluginDependency = require('./dependencies/FlagPluginDependency')
const RemoveEntryDependency = require('./dependencies/RemoveEntryDependency')
const RecordLoaderContentDependency = require('./dependencies/RecordLoaderContentDependency')
const RecordRuntimeInfoDependency = require('./dependencies/RecordRuntimeInfoDependency')
const RequireExternalDependency = require('./dependencies/RequireExternalDependency')
const SplitChunksPlugin = require('webpack/lib/optimize/SplitChunksPlugin')
const fixRelative = require('./utils/fix-relative')
const parseRequest = require('./utils/parse-request')
const { transSubpackage } = require('./utils/trans-async-sub-rules')
const { matchCondition } = require('./utils/match-condition')
const processDefs = require('./utils/process-defs')
const config = require('./config')
const hash = require('hash-sum')
const nativeLoaderPath = normalize.lib('native-loader')
const wxssLoaderPath = normalize.lib('wxss/index')
const wxmlLoaderPath = normalize.lib('wxml/loader')
const wxsLoaderPath = normalize.lib('wxs/loader')
const styleCompilerPath = normalize.lib('style-compiler/index')
const styleStripConditionalPath = normalize.lib('style-compiler/strip-conditional-loader')
const templateCompilerPath = normalize.lib('template-compiler/index')
const jsonCompilerPath = normalize.lib('json-compiler/index')
const jsonThemeCompilerPath = normalize.lib('json-compiler/theme')
const jsonPluginCompilerPath = normalize.lib('json-compiler/plugin')
const mpxGlobalRuntimePath = normalize.lib('runtime/mpxGlobal')
const extractorPath = normalize.lib('extractor')
const async = require('async')
const { parseQuery } = require('loader-utils')
const stringifyLoadersAndResource = require('./utils/stringify-loaders-resource')
const emitFile = require('./utils/emit-file')
const { MPX_PROCESSED_FLAG, MPX_DISABLE_EXTRACTOR_CACHE, MPX_APP_MODULE_ID } = require('./utils/const')
const isEmptyObject = require('./utils/is-empty-object')
const DynamicPlugin = require('./resolver/DynamicPlugin')
const { isReact, isWeb } = require('./utils/env')
const VirtualModulesPlugin = require('webpack-virtual-modules')
const RuntimeGlobals = require('webpack/lib/RuntimeGlobals')
const LoadAsyncChunkModule = require('./react/LoadAsyncChunkModule')
const ExternalModule = require('webpack/lib/ExternalModule')
const { RetryRuntimeModule, RetryRuntimeGlobal } = require('./retry-runtime-module')
require('./utils/check-core-version-match')
const isProductionLikeMode = options => {
return options.mode === 'production' || !options.mode
}
const isStaticModule = module => {
if (!module.resource) return false
const { queryObj } = parseRequest(module.resource)
let isStatic = queryObj.isStatic || false
if (module.loaders) {
for (const loader of module.loaders) {
if (/(url-loader|file-loader)/.test(loader.loader)) {
isStatic = true
break
}
}
}
return isStatic
}
const outputFilename = '[name].js'
const publicPath = '/'
const isChunkInPackage = (chunkName, packageName) => {
return (new RegExp(`^${packageName}\\/`)).test(chunkName)
}
const externalsMap = {
weui: /^weui-miniprogram/
}
const warnings = []
const errors = []
class EntryNode {
constructor (module, type) {
this.module = module
this.type = type
this.parents = new Set()
this.children = new Set()
}
addChild (node) {
this.children.add(node)
node.parents.add(this)
}
}
class MpxWebpackPlugin {
constructor (options = {}) {
options.mode = options.mode || 'wx'
options.env = options.env || ''
options.srcMode = options.srcMode || options.mode
if (options.mode !== options.srcMode && options.srcMode !== 'wx') {
errors.push('MpxWebpackPlugin supports srcMode to be "wx" only temporarily!')
}
if (isWeb(options.mode) && options.srcMode !== 'wx') {
errors.push('MpxWebpackPlugin supports mode to be "web" only when srcMode is set to "wx"!')
}
if (isReact(options.mode) && options.srcMode !== 'wx') {
errors.push('MpxWebpackPlugin supports mode to be "ios" | "android" | "harmony" only when srcMode is set to "wx"!')
}
if (options.dynamicComponentRules && !options.dynamicRuntime) {
errors.push('Please make sure you have set dynamicRuntime true in mpx webpack plugin config because you have use the dynamic runtime feature.')
}
if (options.transSubpackageRules && !isReact(options.mode)) {
warnings.push('MpxWebpackPlugin transSubpackageRules option only supports "ios", "android", or "harmony" mode')
}
options.externalClasses = options.externalClasses || ['custom-class', 'i-class']
options.resolveMode = options.resolveMode || 'webpack'
options.writeMode = options.writeMode || 'changed'
options.autoScopeRules = options.autoScopeRules || {}
options.autoVirtualHostRules = options.autoVirtualHostRules || {}
options.customTextRules = options.customTextRules || {}
options.forceDisableProxyCtor = options.forceDisableProxyCtor || false
options.transMpxRules = options.transMpxRules || {
include: () => true
}
// 通过默认defs配置实现mode及srcMode的注入,简化内部处理逻辑
options.defs = Object.assign({}, options.defs, {
__mpx_mode__: options.mode,
__mpx_src_mode__: options.srcMode,
__mpx_env__: options.env,
__mpx_dynamic_runtime__: options.dynamicRuntime
})
// 批量指定源码mode
options.modeRules = options.modeRules || {}
options.generateBuildMap = options.generateBuildMap || false
options.attributes = options.attributes || []
options.externals = (options.externals || []).map((external) => {
return externalsMap[external] || external
})
options.projectRoot = options.projectRoot || process.cwd()
options.forceUsePageCtor = options.forceUsePageCtor || false
options.postcssInlineConfig = options.postcssInlineConfig || {}
options.transRpxRules = options.transRpxRules || null
options.auditResource = options.auditResource || false
options.decodeHTMLText = options.decodeHTMLText || false
options.i18n = options.i18n || null
options.checkUsingComponentsRules = options.checkUsingComponentsRules || (options.checkUsingComponents ? { include: () => true } : { exclude: () => true })
options.reportSize = options.reportSize || null
options.pathHashMode = options.pathHashMode || 'absolute'
options.forceDisableBuiltInLoader = options.forceDisableBuiltInLoader || false
options.useRelativePath = options.useRelativePath || false
options.subpackageModulesRules = options.subpackageModulesRules || {}
options.forceMainPackageRules = options.forceMainPackageRules || {}
options.forceProxyEventRules = options.forceProxyEventRules || {}
options.disableRequireAsync = options.disableRequireAsync || false
options.miniNpmPackages = options.miniNpmPackages || []
options.normalNpmPackages = options.normalNpmPackages || []
options.fileConditionRules = options.fileConditionRules || {
include: () => true
}
options.customOutputPath = options.customOutputPath || null
options.customComponentModuleId = options.customComponentModuleId || null
options.nativeConfig = Object.assign({
cssLangs: ['css', 'less', 'stylus', 'scss', 'sass']
}, options.nativeConfig)
options.webConfig = options.webConfig || {}
options.rnConfig = options.rnConfig || {}
options.partialCompileRules = options.partialCompileRules || null
options.asyncSubpackageRules = options.asyncSubpackageRules || []
options.optimizeRenderRules = options.optimizeRenderRules ? (Array.isArray(options.optimizeRenderRules) ? options.optimizeRenderRules : [options.optimizeRenderRules]) : []
options.retryRequireAsync = options.retryRequireAsync || false
if (options.retryRequireAsync === true) {
options.retryRequireAsync = {
times: 1,
interval: 0
}
}
options.optimizeSize = options.optimizeSize || false
options.dynamicComponentRules = options.dynamicComponentRules || {}// 运行时组件配置
this.options = options
// Hack for buildDependencies
const rawResolveBuildDependencies = FileSystemInfo.prototype.resolveBuildDependencies
FileSystemInfo.prototype.resolveBuildDependencies = function (context, deps, rawCallback) {
return rawResolveBuildDependencies.call(this, context, deps, (err, result) => {
if (result && typeof options.hackResolveBuildDependencies === 'function') options.hackResolveBuildDependencies(result)
return rawCallback(err, result)
})
}
}
static loader (options = {}) {
if (options.transRpx) {
warnings.push('Mpx loader option [transRpx] is deprecated now, please use mpx webpack plugin config [transRpxRules] instead!')
}
return {
loader: normalize.lib('loader'),
options
}
}
static nativeLoader (options = {}) {
return {
loader: nativeLoaderPath,
options
}
}
static wxssLoader (options) {
return {
loader: normalize.lib('wxss/index'),
options
}
}
static wxmlLoader (options) {
return {
loader: normalize.lib('wxml/loader'),
options
}
}
static pluginLoader (options = {}) {
return {
loader: normalize.lib('json-compiler/plugin'),
options
}
}
static wxsPreLoader (options = {}) {
return {
loader: normalize.lib('wxs/pre-loader'),
options
}
}
static urlLoader (options = {}) {
return {
loader: normalize.lib('url-loader'),
options
}
}
static fileLoader (options = {}) {
return {
loader: normalize.lib('file-loader'),
options
}
}
static getPageEntry (request) {
return addQuery(request, { isPage: true })
}
static getComponentEntry (request) {
return addQuery(request, { isComponent: true })
}
static getNativeEntry (request) {
return `!!${nativeLoaderPath}!${request}`
}
static getPluginEntry (request) {
return addQuery(request, {
mpx: true,
extract: true,
isPlugin: true,
asScript: true,
type: 'json'
})
}
runModeRules (data) {
const { resourcePath, queryObj } = parseRequest(data.resource)
if (queryObj.mode) {
return
}
const mode = this.options.mode
const modeRule = this.options.modeRules[mode]
if (!modeRule) {
return
}
if (matchCondition(resourcePath, modeRule)) {
data.resource = addQuery(data.resource, { mode })
data.request = addQuery(data.request, { mode })
}
}
apply (compiler) {
if (!compiler.__mpx__) {
compiler.__mpx__ = true
} else {
errors.push('Multiple MpxWebpackPlugin instances exist in webpack compiler, please check webpack plugins config!')
}
// 将entry export标记为used且不可mangle,避免require.async生成的js chunk在生产环境下报错
new FlagEntryExportAsUsedPlugin(true, 'entry').apply(compiler)
let __vfs = null
if (isWeb(this.options.mode)) {
for (const plugin of compiler.options.plugins) {
if (plugin instanceof VirtualModulesPlugin) {
__vfs = plugin
break
}
}
if (!__vfs) {
__vfs = new VirtualModulesPlugin()
compiler.options.plugins.push(__vfs)
}
}
compiler.options.plugins.push(new ProvidePlugin(
{
mpxGlobal: mpxGlobalRuntimePath
}
))
if (!isWeb(this.options.mode) && !isReact(this.options.mode)) {
// 强制设置publicPath为'/'
if (compiler.options.output.publicPath && compiler.options.output.publicPath !== publicPath) {
warnings.push(`webpack options: MpxWebpackPlugin accept options.output.publicPath to be ${publicPath} only, custom options.output.publicPath will be ignored!`)
}
compiler.options.output.publicPath = publicPath
if (compiler.options.output.filename && compiler.options.output.filename !== outputFilename) {
warnings.push(`webpack options: MpxWebpackPlugin accept options.output.filename to be ${outputFilename} only, custom options.output.filename will be ignored!`)
}
compiler.options.output.filename = compiler.options.output.chunkFilename = outputFilename
if (this.options.optimizeSize && isProductionLikeMode(compiler.options)) {
compiler.options.optimization.chunkIds = 'total-size'
compiler.options.optimization.moduleIds = 'natural'
compiler.options.optimization.mangleExports = 'size'
compiler.options.output.globalObject = 'g'
// todo chunkLoadingGlobal不具备项目唯一性,在多构建产物混编时可能存在问题,尤其在支付宝使用全局对象传递的情况下
compiler.options.output.chunkLoadingGlobal = 'c'
}
}
if (!compiler.options.node || !compiler.options.node.global) {
compiler.options.node = compiler.options.node || {}
compiler.options.node.global = true
}
const addModeOptions = {
fileConditionRules: this.options.fileConditionRules
}
const mode = this.options.mode
if (mode === 'web' || mode === 'ios' || mode === 'android' || mode === 'harmony') {
// 'web' | 'ios' | 'android' | 'harmony' 下,使用implicitMode强制进行平台转换
addModeOptions.implicitMode = true
}
if (mode === 'android' || mode === 'harmony') {
// 'android' | 'harmony' 下,使用 mode = 'ios' 进行兼容兜底
addModeOptions.defaultMode = 'ios'
}
const addModePlugin = new AddModePlugin('before-file', this.options.mode, addModeOptions, 'file')
const addEnvPlugin = new AddEnvPlugin('before-file', this.options.env, this.options.fileConditionRules, 'file')
const packageEntryPlugin = new PackageEntryPlugin('before-file', this.options.miniNpmPackages, this.options.normalNpmPackages, 'file')
const dynamicPlugin = new DynamicPlugin('result', this.options.dynamicComponentRules)
if (Array.isArray(compiler.options.resolve.plugins)) {
compiler.options.resolve.plugins.push(addModePlugin)
} else {
compiler.options.resolve.plugins = [addModePlugin]
}
if (this.options.env) {
compiler.options.resolve.plugins.push(addEnvPlugin)
}
if (this.options.dynamicRuntime) {
compiler.options.resolve.plugins.push(new DynamicRuntimePlugin('before-file', 'file'))
}
compiler.options.resolve.plugins.push(packageEntryPlugin)
compiler.options.resolve.plugins.push(new FixDescriptionInfoPlugin())
compiler.options.resolve.plugins.push(dynamicPlugin)
const optimization = compiler.options.optimization
if (!isWeb(this.options.mode) && !isReact(this.options.mode)) {
optimization.runtimeChunk = {
name: (entrypoint) => {
for (const packageName in mpx.independentSubpackagesMap) {
if (hasOwn(mpx.independentSubpackagesMap, packageName) && isChunkInPackage(entrypoint.name, packageName)) {
return `${packageName}/bundle`
}
}
return 'bundle'
}
}
}
let splitChunksOptions = null
let splitChunksPlugin = null
// 输出web ssr需要将optimization.splitChunks设置为false以关闭splitChunks
if (optimization.splitChunks !== false) {
splitChunksOptions = Object.assign({
chunks: 'all',
usedExports: optimization.usedExports === true,
minChunks: 1,
minSize: 1000,
enforceSizeThreshold: Infinity,
maxAsyncRequests: 30,
maxInitialRequests: 30,
automaticNameDelimiter: '-',
cacheGroups: {}
}, optimization.splitChunks)
splitChunksOptions.defaultSizeTypes = ['javascript', 'unknown']
delete optimization.splitChunks
splitChunksPlugin = new SplitChunksPlugin(splitChunksOptions)
splitChunksPlugin.apply(compiler)
}
// 代理writeFile
if (this.options.writeMode === 'changed') {
const writedFileContentMap = new Map()
const originalWriteFile = compiler.outputFileSystem.writeFile
// fs.writeFile(file, data[, options], callback)
compiler.outputFileSystem.writeFile = (filePath, content, ...args) => {
const lastContent = writedFileContentMap.get(filePath)
if (Buffer.isBuffer(lastContent) ? lastContent.equals(content) : lastContent === content) {
const callback = args[args.length - 1]
if (typeof callback === 'function') {
callback()
}
return
}
writedFileContentMap.set(filePath, content)
originalWriteFile(filePath, content, ...args)
}
}
const defs = this.options.defs
const typeExtMap = config[this.options.mode].typeExtMap
const defsOpt = {
__mpx_wxs__: DefinePlugin.runtimeValue(({ module }) => {
return JSON.stringify(!!module.wxs)
})
}
Object.keys(defs).forEach((key) => {
defsOpt[key] = JSON.stringify(defs[key])
})
// define mode & defs
new DefinePlugin(defsOpt).apply(compiler)
new ExternalsPlugin('commonjs2', this.options.externals).apply(compiler)
let mpx
if (this.options.partialCompileRules) {
function isResolvingPage (obj) {
// valid query should start with '?'
const query = parseQuery(obj.query || '?')
return query.isPage && !query.type
}
// new PartialCompilePlugin(this.options.partialCompile).apply(compiler)
compiler.resolverFactory.hooks.resolver.intercept({
factory: (type, hook) => {
hook.tap('MpxPartialCompilePlugin', (resolver) => {
resolver.hooks.result.tapAsync({
name: 'MpxPartialCompilePlugin',
stage: -100
}, (obj, resolverContext, callback) => {
if (obj.path.startsWith(require.resolve('./runtime/components/wx/default-page.mpx'))) {
return callback(null, obj)
}
if (isResolvingPage(obj) && !matchCondition(obj.path, this.options.partialCompileRules)) {
const infix = obj.query ? '&' : '?'
obj.query += `${infix}resourcePath=${obj.path}`
obj.path = require.resolve('./runtime/components/wx/default-page.mpx')
}
callback(null, obj)
})
})
return hook
}
})
}
const getPackageCacheGroup = packageName => {
if (packageName === 'main') {
return {
// 对于独立分包模块不应用该cacheGroup
test: (module) => {
let isIndependent = false
if (module.resource) {
const { queryObj } = parseRequest(module.resource)
isIndependent = !!queryObj.independent
} else {
const identifier = module.identifier()
isIndependent = /\|independent=/.test(identifier)
}
return !isIndependent
},
name: 'bundle',
minChunks: 2,
chunks: 'all'
}
} else {
return {
test: (module, { chunkGraph }) => {
const chunks = chunkGraph.getModuleChunksIterable(module)
return chunks.size && every(chunks, (chunk) => {
return isChunkInPackage(chunk.name, packageName)
})
},
name: `${packageName}/bundle`,
minChunks: 2,
priority: 100,
chunks: 'all'
}
}
}
const processSubpackagesEntriesMap = (subPackageEntriesType, compilation, callback) => {
const mpx = compilation.__mpx__
if (mpx && !isEmptyObject(mpx[subPackageEntriesType])) {
const subpackagesEntriesMap = mpx[subPackageEntriesType]
// 执行分包队列前清空 mpx[subPackageEntriesType]
mpx[subPackageEntriesType] = {}
async.eachOfSeries(subpackagesEntriesMap, (deps, packageRoot, callback) => {
mpx.currentPackageRoot = packageRoot
mpx.componentsMap[packageRoot] = mpx.componentsMap[packageRoot] || {}
mpx.staticResourcesMap[packageRoot] = mpx.staticResourcesMap[packageRoot] || {}
mpx.subpackageModulesMap[packageRoot] = mpx.subpackageModulesMap[packageRoot] || {}
async.each(deps, (dep, callback) => {
dep.addEntry(compilation, (err, result) => {
if (err) return callback(err)
dep.resultPath = mpx.replacePathMap[dep.key] = result.resultPath
callback()
})
}, callback)
}, (err) => {
if (err) return callback(err)
// 如果执行完当前队列后产生了新的分包执行队列(一般由异步分包组件造成),则继续执行
processSubpackagesEntriesMap(subPackageEntriesType, compilation, callback)
})
} else {
callback()
}
}
const checkDynamicEntryInfo = (compilation) => {
for (const packageName in mpx.dynamicEntryInfo) {
const entryMap = mpx.dynamicEntryInfo[packageName]
if (packageName !== 'main' && !entryMap.hasPage) {
// 引用未注册分包的所有资源
const resources = entryMap.entries.map(info => info.resource).join(',')
compilation.errors.push(new Error(`资源${resources}通过分包异步声明为${packageName}分包, 但${packageName}分包未注册或不存在相关页面!`))
}
}
}
// 构建分包队列,在finishMake钩子当中最先执行,stage传递-1000
compiler.hooks.finishMake.tapAsync({
name: 'MpxWebpackPlugin',
stage: -1000
}, (compilation, callback) => {
async.series([
(callback) => {
processSubpackagesEntriesMap('subpackagesEntriesMap', compilation, callback)
},
(callback) => {
processSubpackagesEntriesMap('postSubpackageEntriesMap', compilation, callback)
}
], (err) => {
if (err) return callback(err)
if (mpx.supportRequireAsync && mpx.mode !== 'tt') {
// 字节小程序异步分包中不能包含page,忽略该检查
checkDynamicEntryInfo(compilation)
}
callback()
})
})
compiler.hooks.compilation.tap({
name: 'MpxWebpackPlugin',
stage: 100
}, (compilation, { normalModuleFactory }) => {
NormalModule.getCompilationHooks(compilation).loader.tap('MpxWebpackPlugin', (loaderContext) => {
// 设置loaderContext的minimize
if (isProductionLikeMode(compiler.options)) {
loaderContext.minimize = true
}
loaderContext.getMpx = () => {
return mpx
}
})
compilation.hooks.runtimeRequirementInTree
.for(RetryRuntimeGlobal)
.tap('MpxWebpackPlugin', (chunk) => {
compilation.addRuntimeModule(chunk, new RetryRuntimeModule())
return true
})
if (isReact(this.options.mode)) {
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.loadScript)
.tap({
stage: -1000,
name: 'LoadAsyncChunk'
}, (chunk, set) => {
compilation.addRuntimeModule(
chunk,
new LoadAsyncChunkModule(this.options.rnConfig && this.options.rnConfig.asyncChunk && this.options.rnConfig.asyncChunk.timeout)
)
return true
})
}
compilation.dependencyFactories.set(ResolveDependency, new NullFactory())
compilation.dependencyTemplates.set(ResolveDependency, new ResolveDependency.Template())
compilation.dependencyFactories.set(InjectDependency, new NullFactory())
compilation.dependencyTemplates.set(InjectDependency, new InjectDependency.Template())
compilation.dependencyFactories.set(ReplaceDependency, new NullFactory())
compilation.dependencyTemplates.set(ReplaceDependency, new ReplaceDependency.Template())
compilation.dependencyFactories.set(AppEntryDependency, new NullFactory())
compilation.dependencyTemplates.set(AppEntryDependency, new AppEntryDependency.Template())
compilation.dependencyFactories.set(DynamicEntryDependency, new NullFactory())
compilation.dependencyTemplates.set(DynamicEntryDependency, new DynamicEntryDependency.Template())
compilation.dependencyFactories.set(FlagPluginDependency, new NullFactory())
compilation.dependencyTemplates.set(FlagPluginDependency, new FlagPluginDependency.Template())
compilation.dependencyFactories.set(RemoveEntryDependency, new NullFactory())
compilation.dependencyTemplates.set(RemoveEntryDependency, new RemoveEntryDependency.Template())
compilation.dependencyFactories.set(RecordPageConfigMapDependency, new NullFactory())
compilation.dependencyTemplates.set(RecordPageConfigMapDependency, new RecordPageConfigMapDependency.Template())
compilation.dependencyFactories.set(RecordResourceMapDependency, new NullFactory())
compilation.dependencyTemplates.set(RecordResourceMapDependency, new RecordResourceMapDependency.Template())
compilation.dependencyFactories.set(RecordGlobalComponentsDependency, new NullFactory())
compilation.dependencyTemplates.set(RecordGlobalComponentsDependency, new RecordGlobalComponentsDependency.Template())
compilation.dependencyFactories.set(RecordIndependentDependency, new NullFactory())
compilation.dependencyTemplates.set(RecordIndependentDependency, new RecordIndependentDependency.Template())
compilation.dependencyFactories.set(CommonJsVariableDependency, normalModuleFactory)
compilation.dependencyTemplates.set(CommonJsVariableDependency, new CommonJsVariableDependency.Template())
compilation.dependencyFactories.set(CommonJsAsyncDependency, normalModuleFactory)
compilation.dependencyTemplates.set(CommonJsAsyncDependency, new CommonJsAsyncDependency.Template())
compilation.dependencyFactories.set(CommonJsExtractDependency, normalModuleFactory)
compilation.dependencyTemplates.set(CommonJsExtractDependency, new CommonJsExtractDependency.Template())
compilation.dependencyFactories.set(RecordLoaderContentDependency, new NullFactory())
compilation.dependencyTemplates.set(RecordLoaderContentDependency, new RecordLoaderContentDependency.Template())
compilation.dependencyFactories.set(RecordRuntimeInfoDependency, new NullFactory())
compilation.dependencyTemplates.set(RecordRuntimeInfoDependency, new RecordRuntimeInfoDependency.Template())
compilation.dependencyFactories.set(RequireExternalDependency, new NullFactory())
compilation.dependencyTemplates.set(RequireExternalDependency, new RequireExternalDependency.Template())
compilation.dependencyTemplates.set(ImportDependency, new ImportDependencyTemplate())
})
compiler.hooks.thisCompilation.tap('MpxWebpackPlugin', (compilation, { normalModuleFactory }) => {
compilation.warnings.push(...warnings)
compilation.errors.push(...errors)
const moduleGraph = compilation.moduleGraph
if (!compilation.__mpx__) {
// init mpx
mpx = compilation.__mpx__ = {
// 用于使用webpack-virtual-modules功能,目前仅输出web时下支持使用
__vfs,
// app信息,便于获取appName
appInfo: {},
// pageConfig信息
pageConfigsMap: {},
// pages全局记录,无需区分主包分包
pagesMap: {},
// 组件资源记录,依照所属包进行记录
componentsMap: {
main: {}
},
// 静态资源(图片,字体,独立样式)等,依照所属包进行记录
staticResourcesMap: {
main: {}
},
// 用于记录命中subpackageModulesRules的js模块分包归属,用于js多分包冗余输出
subpackageModulesMap: {
main: {}
},
// 记录其他资源,如pluginMain、pluginExport,无需区分主包分包
otherResourcesMap: {},
// 记录独立分包
independentSubpackagesMap: {},
subpackagesEntriesMap: {},
postSubpackageEntriesMap: {},
replacePathMap: {},
exportModules: new Set(),
// 记录动态添加入口的分包信息
dynamicEntryInfo: {},
// 记录entryModule与entryNode的对应关系,用于体积分析
entryNodeModulesMap: new Map(),
// 记录与asset相关联的modules,用于体积分析
assetsModulesMap: new Map(),
// 记录与asset相关联的ast,用于体积分析和esCheck,避免重复parse
assetsASTsMap: new Map(),
// 记录RequireExternalDependency相关资源路径
externalRequestsMap: new Map(),
globalComponents: {},
globalComponentsInfo: {},
// todo es6 map读写性能高于object,之后会逐步替换
wxsAssetsCache: new Map(),
addEntryPromiseMap: new Map(),
currentPackageRoot: '',
wxsContentMap: {},
forceUsePageCtor: this.options.forceUsePageCtor,
resolveMode: this.options.resolveMode,
mode: this.options.mode,
srcMode: this.options.srcMode,
env: this.options.env,
externalClasses: this.options.externalClasses,
projectRoot: this.options.projectRoot,
autoScopeRules: this.options.autoScopeRules,
autoVirtualHostRules: this.options.autoVirtualHostRules,
customTextRules: this.options.customTextRules,
transRpxRules: this.options.transRpxRules,
postcssInlineConfig: this.options.postcssInlineConfig,
decodeHTMLText: this.options.decodeHTMLText,
// native文件专用配置
nativeConfig: this.options.nativeConfig,
// 输出web专用配置
webConfig: this.options.webConfig,
// 输出rn专用配置
rnConfig: this.options.rnConfig,
loaderContentCache: new Map(),
tabBarMap: {},
defs: processDefs(this.options.defs),
i18n: this.options.i18n,
checkUsingComponentsRules: this.options.checkUsingComponentsRules,
forceDisableBuiltInLoader: this.options.forceDisableBuiltInLoader,
appTitle: 'Mpx homepage',
attributes: this.options.attributes,
externals: this.options.externals,
useRelativePath: this.options.useRelativePath,
removedChunks: [],
forceProxyEventRules: this.options.forceProxyEventRules,
// 若配置disableRequireAsync=true, 则全平台构建不支持异步分包
supportRequireAsync: !this.options.disableRequireAsync && (this.options.mode === 'wx' || this.options.mode === 'ali' || this.options.mode === 'tt' || isWeb(this.options.mode) || isReact(this.options.mode)),
partialCompileRules: this.options.partialCompileRules,
collectDynamicEntryInfo: ({ resource, packageName, filename, entryType, hasAsync }) => {
const curInfo = mpx.dynamicEntryInfo[packageName] = mpx.dynamicEntryInfo[packageName] || {
hasPage: false,
entries: []
}
if (entryType === 'page') curInfo.hasPage = true
curInfo.entries.push({
entryType,
resource,
filename,
hasAsync
})
},
asyncSubpackageRules: this.options.asyncSubpackageRules,
transSubpackageRules: this.options.transSubpackageRules,
optimizeRenderRules: this.options.optimizeRenderRules,
pathHash: (resourcePath) => {
if (this.options.pathHashMode === 'relative' && this.options.projectRoot) {
return hash(path.relative(this.options.projectRoot, resourcePath))
}
return hash(resourcePath)
},
addEntry: (request, name, callback) => {
const dep = EntryPlugin.createDependency(request, { name })
compilation.addEntry(compiler.context, dep, { name }, callback)
return dep
},
getModuleId: (filePath, isApp = false) => {
if (isApp) return MPX_APP_MODULE_ID
const customComponentModuleId = this.options.customComponentModuleId
if (typeof customComponentModuleId === 'function') {
const customModuleId = customComponentModuleId(filePath)
if (customModuleId) return customModuleId
}
return '_' + mpx.pathHash(filePath)
},
getEntryNode: (module, type) => {
const entryNodeModulesMap = mpx.entryNodeModulesMap
let entryNode = entryNodeModulesMap.get(module)
if (!entryNode) {
entryNode = new EntryNode(module, type)
entryNodeModulesMap.set(module, entryNode)
} else if (type) {
if (entryNode.type && entryNode.type !== type) {
compilation.errors.push(`获取request为${module.request}的entryNode时类型与已有节点冲突, 当前注册的type为${type}, 已有节点的type为${entryNode.type}!`)
}
entryNode.type = type
}
return entryNode
},
getOutputPath: (resourcePath, type, { ext = '', conflictPath = '' } = {}) => {
const name = path.parse(resourcePath).name
const hash = mpx.pathHash(resourcePath)
const customOutputPath = this.options.customOutputPath
if (conflictPath) return conflictPath.replace(/(\.[^\\/]+)?$/, match => hash + match)
if (typeof customOutputPath === 'function') return customOutputPath(type, name, hash, ext, resourcePath).replace(/^\//, '')
if (type === 'component' || type === 'page') return path.join(type + 's', name + hash, 'index' + ext)
return path.join(type, name + hash + ext)
},
extractedFilesCache: new Map(),
getExtractedFile: (resource, { error } = {}) => {
const cache = mpx.extractedFilesCache.get(resource)
if (cache) return cache
const { resourcePath, queryObj } = parseRequest(resource)
const { type, isStatic, isPlugin } = queryObj
let file
if (isPlugin) {
file = 'plugin.json'
} else if (isStatic) {
const packageRoot = queryObj.packageRoot || ''
file = toPosix(path.join(packageRoot, mpx.getOutputPath(resourcePath, type, { ext: typeExtMap[type] })))
} else {
const appInfo = mpx.appInfo
const pagesMap = mpx.pagesMap
const packageName = queryObj.packageRoot || mpx.currentPackageRoot || 'main'
const componentsMap = mpx.componentsMap[packageName]
let filename = resourcePath === appInfo.resourcePath ? appInfo.name : (pagesMap[resourcePath] || componentsMap[resourcePath])
if (!filename) {
error && error(new Error('Get extracted file error: missing filename!'))
filename = 'missing-filename'
}
file = filename + typeExtMap[type]
}
mpx.extractedFilesCache.set(resource, file)
return file
},
recordResourceMap: ({
resourcePath,
resourceType,
outputPath,
packageRoot = '',
recordOnly,
warn,
error
}) => {
const packageName = packageRoot || 'main'
const resourceMap = mpx[`${resourceType}sMap`] || mpx.otherResourcesMap
const currentResourceMap = resourceMap.main ? resourceMap[packageName] = resourceMap[packageName] || {} : resourceMap
let alreadyOutputted = false
if (outputPath) {
if (!currentResourceMap[resourcePath] || currentResourceMap[resourcePath] === true) {
if (!recordOnly) {
// 在非recordOnly的模式下,进行输出路径冲突检测,如果存在输出路径冲突,则对输出路径进行重命名
for (const key in currentResourceMap) {
// todo 用outputPathMap来检测输出路径冲突
if (currentResourceMap[key] === outputPath && key !== resourcePath) {
outputPath = mpx.getOutputPath(resourcePath, resourceType, { conflictPath: outputPath })
warn && warn(new Error(`Current ${resourceType} [${resourcePath}] is registered with conflicted outputPath [${currentResourceMap[key]}] which is already existed in system, will be renamed with [${outputPath}], use ?resolve to get the real outputPath!`))
break
}
}
}
currentResourceMap[resourcePath] = outputPath
} else {
if (currentResourceMap[resourcePath] === outputPath) {
alreadyOutputted = true
} else {
error && error(new Error(`Current ${resourceType} [${resourcePath}] is already registered with outputPath [${currentResourceMap[resourcePath]}], you can not register it with another outputPath [${outputPath}]!`))
}
}
} else if (!currentResourceMap[resourcePath]) {
currentResourceMap[resourcePath] = true
}
return {
outputPath,
alreadyOutputted
}
},
// 组件和静态资源的输出规则如下:
// 1. 主包引用的资源输出至主包
// 2. 分包引用且主包引用过的资源输出至主包,不在当前分包重复输出
// 3. 分包引用且无其他包引用的资源输出至当前分包
// 4. 分包引用且其他分包也引用过的资源,重复输出至当前分包
getPackageInfo: ({ resource, resourceType, outputPath, issuerResource, warn, error }) => {
let packageRoot = ''
let packageName = 'main'
const { resourcePath } = parseRequest(resource)
const currentPackageRoot = mpx.currentPackageRoot
const currentPackageName = currentPackageRoot || 'main'
const isIndependent = !!mpx.independentSubpackagesMap[currentPackageRoot]
const resourceMap = mpx[`${resourceType}sMap`] || mpx.otherResourcesMap
if (!resourceMap.main) {
packageRoot = currentPackageRoot
packageName = currentPackageName
} else {
// 主包中有引用一律使用主包中资源,不再额外输出
// 资源路径匹配到forceMainPackageRules规则时强制输出到主包,降低分包资源冗余
// 如果存在issuer且issuerPackageRoot与当前packageRoot不一致,也输出到主包
// todo forceMainPackageRules规则目前只能处理当前资源,不能处理资源子树,配置不当有可能会导致资源引用错误
let isMain = resourceMap.main[resourcePath] || matchCondition(resourcePath, this.options.forceMainPackageRules)
if (issuerResource) {
const { queryObj } = parseRequest(issuerResource)
const issuerPackageRoot = queryObj.packageRoot || ''
if (issuerPackageRoot !== currentPackageRoot) {
warn && warn(new Error(`当前模块[${resource}]的引用者[${issuerResource}]不带有分包标记或分包标记与当前分包不符,模块资源将被输出到主包,可以尝试将引用者加入到subpackageModulesRules来解决这个问题!`))
isMain = true
}
}
if (!isMain || isIndependent) {
packageRoot = currentPackageRoot
packageName = currentPackageName
if (this.options.auditResource && resourceType !== 'subpackageModule' && !isIndependent) {
if (this.options.auditResource !== 'component' || resourceType === 'component') {
Object.keys(resourceMap).filter(key => key !== 'main').forEach((key) => {
if (resourceMap[key][resourcePath] && key !== packageName) {
warn && warn(new Error(`当前${resourceType === 'component' ? '组件' : '静态'}资源${resourcePath}在分包${key}和分包${packageName}中都有引用,会分别输出到两个分包中,为了总体积最优,可以在主包中建立引用声明以消除资源输出冗余!`))
}
})
}
}
}
resourceMap[packageName] = resourceMap[packageName] || {}
}
if (outputPath) outputPath = toPosix(path.join(packageRoot, outputPath))
return {
packageName,
packageRoot,
// 返回outputPath及alreadyOutputted
...mpx.recordResourceMap({
resourcePath,
resourceType,
outputPath,
packageRoot,
warn,
error
})
}
},
// 以包为维度记录不同 package 需要的组件属性等信息,用以最终 mpx-custom-element 相关文件的输出
runtimeInfo: {},
// 记录运行时组件依赖的运行时组件当中使用的基础组件 slot,最终依据依赖关系注入到运行时组件的 json 配置当中
dynamicSlotDependencies: {},
// 模板引擎参数,用来检测模板引擎支持渲染的模板
dynamicTemplateRuleRunner: this.options.dynamicTemplateRuleRunner,
// 依据 package 注入到 mpx-custom-element-*.json 里面的组件路径
getPackageInjectedComponentsMap: (packageName = 'main') => {
const res = {}
const runtimeInfo = mpx.runtimeInfo[packageName] || {}
const componentsMap = mpx.componentsMap[packageName] || {}
const publicPath = compilation.outputOptions.publicPath || ''
for (const componentPath in runtimeInfo) {
Object.values(runtimeInfo[componentPath].json).forEach(({ hashName, resourcePath }) => {
const outputPath = componentsMap[resourcePath]
if (outputPath) {
res[hashName] = publicPath + outputPath
}
})
}
return res
},
// 获取生成基础递归渲染模版的节点配置信息
getPackageInjectedTemplateConfig: (packageName = 'main') => {
const res = {
baseComponents: {
block: {}
},
runtimeComponents: {},
normalComponents: {}
}
const runtimeInfo = mpx.runtimeInfo[packageName] || {}
// 包含了某个分包当中所有的运行时组件
for (const resourcePath in runtimeInfo) {
const { json, template } = runtimeInfo[resourcePath]
const customComponents = template.customComponents || {}
const baseComponents = template.baseComponents || {}
// 合并自定义组件的属性
for (const componentName in customComponents) {
const extraAttrs = {}
const attrsMap = customComponents[componentName]
const { hashName, isDynamic } = json[componentName] || {}
let componentType = 'normalComponents'
if (isDynamic) {
componentType = 'runtimeComponents'
extraAttrs.slots = ''
}
if (!res[componentType][hashName]) {
res[componentType][hashName] = {}
}
Object.assign(res[componentType][hashName], {
...attrsMap,
...extraAttrs
})
}
// 合并基础节点的属性
for (const componentName in baseComponents) {
const attrsMap = baseComponents[componentName]
if (!res.baseComponents[componentName]) {
res.baseComponents[componentName] = {}
}
Object.assign(res.baseComponents[componentName], attrsMap)
}
}
return res
},
injectDynamicSlotDependencies: (usingComponents, resourcePath) => {
const dynamicSlotReg = /"mpx_dynamic_slot":\s*""*/
const content = mpx.dynamicSlotDependencies[resourcePath] ? JSON.stringify(mpx.dynamicSlotDependencies[resourcePath]).slice(1, -1) : ''
const result = usingComponents.replace(dynamicSlotReg, content).replace(/,\s*(\}|\])/g, '$1')
return result
},
changeHashNameForAstNode: (templateAst, componentsMap) => {
const nameReg = /"tag":\s*"([^"]*)",?/g
// 替换 astNode hashName
const result = templateAst.replace(nameReg, function (match, tag) {
if (componentsMap[tag]) {
const { hashName, isDynamic } = componentsMap[tag]
let content = `"tag": "${hashName}",`
if (isDynamic) {
content += '"dynamic": true,'
}
return content
}
return match
}).replace(/,\s*(\}|\])/g, '$1')
return result
},
collectDynamicSlotDependencies: (packageName = 'main') => {
const componentsMap = mpx.componentsMap[packageName] || {}
const publicPath = compilation.outputOptions.publicPath || ''
const runtimeInfo = mpx.runtimeInfo[packageName]
for (const resourcePath in runtimeInfo) {
const { template, json } = runtimeInfo[resourcePath]
const dynamicSlotDependencies = template.dynamicSlotDependencies || []
dynamicSlotDependencies.forEach((slotDependencies) => {
let lastNeedInjectNode = slotDependencies[0]
for (let i = 1; i <= slotDependencies.length - 1; i++) {
const componentName = slotDependencies[i]
const { resourcePath, isDynamic } = json[componentName] || {}
if (isDynamic) {
const { resourcePath: path, hashName } = json[lastNeedInjectNode]
mpx.dynamicSlotDependencies[resourcePath] = mpx.dynamicSlotDependencies[resourcePath] || {}
Object.assign(mpx.dynamicSlotDependencies[resourcePath], {
[hashName]: publicPath + componentsMap[path]
})
lastNeedInjectNode = slotDependencies[i]
}
}
})
}
}
}
}
const rawProcessModuleDependencies = compilation.processModuleDependencies
compilation.processModuleDependencies = (module, callback) => {
const presentationalDependencies = module.presentationalDependencies || []
const errors = []
async.forEach(presentationalDependencies.filter((dep) => dep.mpxAction), (dep, callback) => {
dep.mpxAction(module, compilation, (err) => {
if (err) errors.push(err)
callback()
})
}, () => {
compilation.errors.push(...errors)
rawProcessModuleDependencies.call(compilation, module, callback)
})
}
const rawFactorizeModule = compilation.factorizeModule
compilation.factorizeModule = (options, callback) => {
const originModule = options.originModule
let proxyedCallback = callback
if (originModule) {
proxyedCallback = (err, module) => {
// 避免selfModuleFactory的情况
if (module && module !== originModule) {
module.issuerResource = originModule.resource
}
return callback(err, module)
}
}
return rawFactorizeModule.call(compilation, options, proxyedCallback)
}
// 处理watch时缓存模块中的buildInfo
// 在调用addModule前对module添加分包信息,以控制分包输出及消除缓存,该操作由afterResolve钩子迁移至此是由于dependencyCache的存在,watch状态下afterResolve钩子并不会对所有模块执行,而模块的packageName在watch过程中是可能发生变更的,如新增删除一个分包资源的主包引用
const rawAddModule = compilation.addModule
compilation.addModule = (module, callback) => {
const issuerResource = module.issuerResource
const currentPackageRoot = mpx.currentPackageRoot
const independent = mpx.independentSubpackagesMap[currentPackageRoot]
if (module.resource) {
// NormalModule
const isStatic = isStaticModule(module)
let needPackageQuery = isStatic || independent
if (!needPackageQuery) {
const { resourcePath } = parseRequest(module.resource)
needPackageQuery = matchCondition(resourcePath, this.options.subpackageModulesRules)
}
if (needPackageQuery) {