UNPKG

@mpxjs/webpack-plugin

Version:

mpx compile core

1,157 lines (1,064 loc) 89.5 kB
'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) {