UNPKG

32.8 kBJavaScriptView Raw
1'use strict'
2
3const path = require('path')
4const ConcatSource = require('webpack-sources').ConcatSource
5const ResolveDependency = require('./dependency/ResolveDependency')
6const InjectDependency = require('./dependency/InjectDependency')
7const ReplaceDependency = require('./dependency/ReplaceDependency')
8const NullFactory = require('webpack/lib/NullFactory')
9const normalize = require('./utils/normalize')
10const toPosix = require('./utils/to-posix')
11const addQuery = require('./utils/add-query')
12const DefinePlugin = require('webpack/lib/DefinePlugin')
13const ExternalsPlugin = require('webpack/lib/ExternalsPlugin')
14const AddModePlugin = require('./resolver/AddModePlugin')
15const CommonJsRequireDependency = require('webpack/lib/dependencies/CommonJsRequireDependency')
16const HarmonyImportSideEffectDependency = require('webpack/lib/dependencies/HarmonyImportSideEffectDependency')
17const RequireHeaderDependency = require('webpack/lib/dependencies/RequireHeaderDependency')
18const RemovedModuleDependency = require('./dependency/RemovedModuleDependency')
19const SplitChunksPlugin = require('webpack/lib/optimize/SplitChunksPlugin')
20const fixRelative = require('./utils/fix-relative')
21const parseRequest = require('./utils/parse-request')
22const matchCondition = require('./utils/match-condition')
23
24const isProductionLikeMode = options => {
25 return options.mode === 'production' || !options.mode
26}
27
28const outputFilename = '[name].js'
29const publicPath = '/'
30
31function isChunkInPackage (chunkName, packageName) {
32 return (new RegExp(`^${packageName}\\/`)).test(chunkName)
33}
34
35function getPackageCacheGroup (packageName) {
36 if (packageName === 'main') {
37 return {
38 name: 'bundle',
39 minChunks: 2,
40 chunks: 'all'
41 }
42 } else {
43 return {
44 test: (module, chunks) => {
45 return chunks.every((chunk) => {
46 return isChunkInPackage(chunk.name, packageName)
47 })
48 },
49 name: `${packageName}/bundle`,
50 minChunks: 2,
51 minSize: 1000,
52 priority: 100,
53 chunks: 'all'
54 }
55 }
56}
57
58let loaderOptions
59
60const externalsMap = {
61 weui: /^weui-miniprogram/
62}
63
64class MpxWebpackPlugin {
65 constructor (options = {}) {
66 options.mode = options.mode || 'wx'
67
68 options.srcMode = options.srcMode || options.mode
69 if (options.mode !== options.srcMode && options.srcMode !== 'wx') {
70 throw new Error('MpxWebpackPlugin supports srcMode to be "wx" only temporarily!')
71 }
72 if (options.mode === 'web' && options.srcMode !== 'wx') {
73 throw new Error('MpxWebpackPlugin supports mode to be "web" only when srcMode is set to "wx"!')
74 }
75 if (!Array.isArray(options.externalClasses)) {
76 options.externalClasses = ['custom-class', 'i-class']
77 }
78
79 options.externalClasses = options.externalClasses.map((className) => {
80 return {
81 className,
82 replacement: className.replace(/-(.)/g, (matched, $1) => {
83 return $1.toUpperCase()
84 })
85 }
86 })
87 options.resolveMode = options.resolveMode || 'webpack'
88 options.writeMode = options.writeMode || 'changed'
89 options.autoScopeRules = options.autoScopeRules || {}
90 options.forceDisableInject = options.forceDisableInject || false
91 options.forceDisableProxyCtor = options.forceDisableProxyCtor || false
92 options.transMpxRules = options.transMpxRules || {
93 include: () => true
94 }
95 if (options.autoSplit === undefined) {
96 // web模式下默认不开启autoSplit
97 options.autoSplit = options.mode !== 'web'
98 }
99 // 通过默认defs配置实现mode及srcMode的注入,简化内部处理逻辑
100 options.defs = Object.assign({}, options.defs, {
101 '__mpx_mode__': options.mode,
102 '__mpx_src_mode__': options.srcMode
103 })
104 // 批量指定源码mode
105 options.modeRules = options.modeRules || {}
106 options.generateBuildMap = options.generateBuildMap || false
107 options.attributes = options.attributes || []
108 options.externals = (options.externals || []).map((external) => {
109 return externalsMap[external] || external
110 })
111 options.forceUsePageCtor = options.forceUsePageCtor || false
112 this.options = options
113 }
114
115 static loader (options) {
116 loaderOptions = options
117 return { loader: normalize.lib('loader'), options }
118 }
119
120 static pluginLoader (options) {
121 return { loader: normalize.lib('plugin-loader'), options }
122 }
123
124 static wxsPreLoader (options) {
125 return { loader: normalize.lib('wxs/wxs-pre-loader'), options }
126 }
127
128 static urlLoader (options) {
129 return { loader: normalize.lib('url-loader'), options }
130 }
131
132 static fileLoader (options) {
133 return { loader: normalize.lib('file-loader'), options }
134 }
135
136 runModeRules (request) {
137 const { resourcePath, queryObj } = parseRequest(request)
138 if (queryObj.mode) {
139 return request
140 }
141 const mode = this.options.mode
142 const modeRule = this.options.modeRules[mode]
143 if (!modeRule) {
144 return request
145 }
146 if (matchCondition(resourcePath, modeRule)) {
147 return addQuery(request, { mode })
148 }
149 return request
150 }
151
152 apply (compiler) {
153 if (!compiler.__mpx__) {
154 compiler.__mpx__ = true
155 } else {
156 throw new Error('Multiple MpxWebpackPlugin instances exist in webpack compiler, please check webpack plugins config!')
157 }
158 const warnings = []
159 const errors = []
160 if (this.options.mode !== 'web') {
161 // 强制设置publicPath为'/'
162 if (compiler.options.output.publicPath && compiler.options.output.publicPath !== publicPath) {
163 warnings.push(`webpack options: MpxWebpackPlugin accept options.output.publicPath to be ${publicPath} only, custom options.output.publicPath will be ignored!`)
164 }
165 compiler.options.output.publicPath = publicPath
166 if (compiler.options.output.filename && compiler.options.output.filename !== outputFilename) {
167 warnings.push(`webpack options: MpxWebpackPlugin accept options.output.filename to be ${outputFilename} only, custom options.output.filename will be ignored!`)
168 }
169 compiler.options.output.filename = compiler.options.output.chunkFilename = outputFilename
170 }
171
172 if (!compiler.options.node || !compiler.options.node.global) {
173 compiler.options.node = compiler.options.node || {}
174 compiler.options.node.global = true
175 warnings.push(`webpack options: MpxWebpackPlugin strongly depends options.node.globel to be true, custom options.node will be ignored!`)
176 }
177
178 const resolvePlugin = new AddModePlugin('before-resolve', this.options.mode, 'resolve')
179
180 if (Array.isArray(compiler.options.resolve.plugins)) {
181 compiler.options.resolve.plugins.push(resolvePlugin)
182 } else {
183 compiler.options.resolve.plugins = [resolvePlugin]
184 }
185
186 let splitChunksPlugin
187 let splitChunksOptions
188
189 if (this.options.autoSplit) {
190 compiler.options.optimization.runtimeChunk = {
191 name: 'bundle'
192 }
193 splitChunksOptions = compiler.options.optimization.splitChunks
194 delete compiler.options.optimization.splitChunks
195 splitChunksPlugin = new SplitChunksPlugin(splitChunksOptions)
196 splitChunksPlugin.apply(compiler)
197 }
198
199 // 代理writeFile
200 if (this.options.writeMode === 'changed') {
201 const writedFileContentMap = new Map()
202 const originalWriteFile = compiler.outputFileSystem.writeFile
203 compiler.outputFileSystem.writeFile = (filePath, content, callback) => {
204 if (writedFileContentMap.has(filePath) && writedFileContentMap.get(filePath).equals(content)) {
205 return callback()
206 }
207 writedFileContentMap.set(filePath, content)
208 originalWriteFile(filePath, content, callback)
209 }
210 }
211 const defs = this.options.defs
212
213 const defsOpt = {
214 '__mpx_wxs__': DefinePlugin.runtimeValue(({ module }) => {
215 return JSON.stringify(!!module.wxs)
216 })
217 }
218
219 Object.keys(defs).forEach((key) => {
220 defsOpt[key] = JSON.stringify(defs[key])
221 })
222
223 // define mode & defs
224 new DefinePlugin(defsOpt).apply(compiler)
225
226 new ExternalsPlugin('commonjs2', this.options.externals).apply(compiler)
227
228 compiler.hooks.compilation.tap('MpxWebpackPlugin ', (compilation) => {
229 compilation.hooks.normalModuleLoader.tap('MpxWebpackPlugin', (loaderContext, module) => {
230 // 设置loaderContext的minimize
231 if (isProductionLikeMode(compiler.options)) {
232 loaderContext.minimize = true
233 }
234 })
235 })
236
237 let mpx
238
239 compiler.hooks.thisCompilation.tap('MpxWebpackPlugin', (compilation, { normalModuleFactory }) => {
240 compilation.warnings = compilation.warnings.concat(warnings)
241 compilation.errors = compilation.errors.concat(errors)
242 // additionalAssets和mpx由于包含缓存机制,必须在每次compilation时重新初始化
243 const additionalAssets = {}
244 if (!compilation.__mpx__) {
245 // init mpx
246 mpx = compilation.__mpx__ = {
247 // pages全局记录,无需区分主包分包
248 pagesMap: {},
249 // 记录pages对应的entry,处理多appEntry输出web多页项目时可能出现的pagePath冲突的问题,多appEntry输出目前仅web模式支持
250 pagesEntryMap: {},
251 // 组件资源记录,依照所属包进行记录,冗余存储,只要某个包有引用会添加对应记录,不管其会不会在当前包输出,这样设计主要是为了在resolve时能够以较低成本找到特定资源的输出路径
252 componentsMap: {
253 main: {}
254 },
255 // 静态资源(图片,字体,独立样式)等,依照所属包进行记录,冗余存储,同上
256 staticResourceMap: {
257 main: {}
258 },
259 hasApp: false,
260 // 记录静态资源首次命中的分包,当有其他分包再次引用了同样的静态资源时,对其request添加packageName query以避免模块缓存导致loader不再执行
261 staticResourceHit: {},
262 loaderOptions,
263 extractedMap: {},
264 extractSeenFile: {},
265 usingComponents: [],
266 // todo es6 map读写性能高于object,之后会逐步替换
267 vueContentCache: new Map(),
268 currentPackageRoot: '',
269 wxsMap: {},
270 wxsConentMap: {},
271 forceDisableInject: this.options.forceDisableInject,
272 forceUsePageCtor: this.options.forceUsePageCtor,
273 resolveMode: this.options.resolveMode,
274 mode: this.options.mode,
275 srcMode: this.options.srcMode,
276 globalMpxAttrsFilter: this.options.globalMpxAttrsFilter,
277 externalClasses: this.options.externalClasses,
278 projectRoot: this.options.projectRoot,
279 autoScopeRules: this.options.autoScopeRules,
280 // native文件专用相关配置
281 nativeOptions: Object.assign({
282 cssLangs: ['css', 'less', 'stylus', 'scss', 'sass']
283 }, this.options.nativeOptions),
284 defs: this.options.defs,
285 i18n: this.options.i18n,
286 appTitle: 'Mpx homepage',
287 attributes: this.options.attributes,
288 externals: this.options.externals,
289 extract: (content, file, index, sideEffects) => {
290 additionalAssets[file] = additionalAssets[file] || []
291 if (!additionalAssets[file][index]) {
292 additionalAssets[file][index] = content
293 }
294 sideEffects && sideEffects(additionalAssets)
295 },
296 // 组件和静态资源的输出规则如下:
297 // 1. 主包引用的资源输出至主包
298 // 2. 分包引用且主包引用过的资源输出至主包,不在当前分包重复输出
299 // 3. 分包引用且无其他包引用的资源输出至当前分包
300 // 4. 分包引用且其他分包也引用过的资源,重复输出至当前分包
301 // 5. 当用户通过packageName query显式指定了资源的所属包时,输出至指定的包
302 getPackageInfo (resource, { outputPath, isStatic, error }) {
303 let packageRoot = ''
304 let packageName = 'main'
305 const currentPackageRoot = mpx.currentPackageRoot
306 const currentPackageName = currentPackageRoot || 'main'
307 const { resourcePath, queryObj } = parseRequest(resource)
308 const resourceMap = isStatic ? mpx.staticResourceMap : mpx.componentsMap
309 // 主包中有引用一律使用主包中资源,不再额外输出
310 if (!resourceMap.main[resourcePath]) {
311 if (queryObj.packageName) {
312 packageName = queryObj.packageName
313 packageRoot = packageName === 'main' ? '' : packageName
314 if (packageName !== currentPackageName && packageName !== 'main') {
315 error && error(new Error(`根据小程序分包资源引用规则,资源只支持声明为当前分包或者主包,否则可能会导致资源无法引用的问题,当前资源的当前分包为${currentPackageName},资源查询字符串声明的分包为${packageName},请检查!`))
316 }
317 } else if (currentPackageRoot) {
318 packageName = packageRoot = currentPackageRoot
319 }
320 }
321
322 outputPath = toPosix(path.join(packageRoot, outputPath))
323
324 const currentResourceMap = resourceMap[currentPackageName]
325 const actualResourceMap = resourceMap[packageName]
326
327 let alreadyOutputed = false
328 // 如果之前已经进行过输出,则不需要重复进行
329 if (actualResourceMap[resourcePath]) {
330 outputPath = actualResourceMap[resourcePath]
331 alreadyOutputed = true
332 }
333 // 将当前的currentResourceMap和实际进行输出的actualResourceMap都填充上,便于resolve时使用
334 currentResourceMap[resourcePath] = actualResourceMap[resourcePath] = outputPath
335
336 if (isStatic && packageName !== 'main' && !mpx.staticResourceHit[resourcePath]) {
337 mpx.staticResourceHit[resourcePath] = packageName
338 }
339
340 return {
341 packageName,
342 packageRoot,
343 resourcePath,
344 queryObj,
345 outputPath,
346 alreadyOutputed
347 }
348 }
349 }
350 }
351
352 if (splitChunksPlugin) {
353 // 自动跟进分包配置修改splitChunksPlugin配置
354 compilation.hooks.finishModules.tap('MpxWebpackPlugin', (modules) => {
355 let needInit = false
356 Object.keys(mpx.componentsMap).forEach((packageName) => {
357 if (!splitChunksOptions.cacheGroups.hasOwnProperty(packageName)) {
358 needInit = true
359 splitChunksOptions.cacheGroups[packageName] = getPackageCacheGroup(packageName)
360 }
361 })
362 if (needInit) {
363 splitChunksPlugin.options = SplitChunksPlugin.normalizeOptions(splitChunksOptions)
364 }
365 })
366 }
367
368 compilation.hooks.optimizeModules.tap('MpxWebpackPlugin', (modules) => {
369 modules.forEach((module) => {
370 if (module.needRemove) {
371 let removed = false
372 module.reasons.forEach((reason) => {
373 if (reason.module) {
374 if (reason.dependency instanceof HarmonyImportSideEffectDependency) {
375 reason.module.removeDependency(reason.dependency)
376 reason.module.addDependency(new RemovedModuleDependency(reason.dependency.request))
377 removed = true
378 } else if (reason.dependency instanceof CommonJsRequireDependency && reason.dependency.loc.range) {
379 let index = reason.module.dependencies.indexOf(reason.dependency)
380 if (index > -1 && reason.module.dependencies[index + 1] instanceof RequireHeaderDependency) {
381 reason.module.dependencies.splice(index, 2)
382 reason.module.addDependency(new RemovedModuleDependency(reason.dependency.request, reason.dependency.loc.range))
383 removed = true
384 }
385 }
386 }
387 })
388 if (removed) {
389 module.chunksIterable.forEach((chunk) => {
390 module.removeChunk(chunk)
391 })
392 module.disconnect()
393 }
394 }
395 })
396 })
397
398 compilation.hooks.additionalAssets.tapAsync('MpxWebpackPlugin', (callback) => {
399 for (let file in additionalAssets) {
400 let content = new ConcatSource()
401 if (additionalAssets[file].prefix) {
402 additionalAssets[file].prefix.forEach((item) => {
403 content.add(item)
404 })
405 }
406 additionalAssets[file].forEach((item) => {
407 content.add(item)
408 })
409 compilation.assets[file] = content
410 }
411 callback()
412 })
413
414 compilation.dependencyFactories.set(ResolveDependency, new NullFactory())
415 compilation.dependencyTemplates.set(ResolveDependency, new ResolveDependency.Template())
416
417 compilation.dependencyFactories.set(InjectDependency, new NullFactory())
418 compilation.dependencyTemplates.set(InjectDependency, new InjectDependency.Template())
419
420 compilation.dependencyFactories.set(ReplaceDependency, new NullFactory())
421 compilation.dependencyTemplates.set(ReplaceDependency, new ReplaceDependency.Template())
422
423 compilation.dependencyFactories.set(RemovedModuleDependency, normalModuleFactory)
424 compilation.dependencyTemplates.set(RemovedModuleDependency, new RemovedModuleDependency.Template())
425
426 normalModuleFactory.hooks.parser.for('javascript/auto').tap('MpxWebpackPlugin', (parser) => {
427 // hack预处理,将expr.range写入loc中便于在CommonJsRequireDependency中获取,移除无效require
428 parser.hooks.call.for('require').tap({ name: 'MpxWebpackPlugin', stage: -100 }, (expr) => {
429 expr.loc.range = expr.range
430 })
431
432 parser.hooks.call.for('__mpx_resolve_path__').tap('MpxWebpackPlugin', (expr) => {
433 if (expr.arguments[0]) {
434 const resource = expr.arguments[0].value
435 const { queryObj } = parseRequest(resource)
436 const packageName = queryObj.packageName
437 const pagesMap = mpx.pagesMap
438 const componentsMap = mpx.componentsMap
439 const staticResourceMap = mpx.staticResourceMap
440 const publicPath = mpx.mode === 'web' ? '' : compilation.outputOptions.publicPath
441 const range = expr.range
442 const issuerResource = parser.state.module.issuer.resource
443 const dep = new ResolveDependency(resource, packageName, pagesMap, componentsMap, staticResourceMap, publicPath, range, issuerResource)
444 parser.state.current.addDependency(dep)
445 return true
446 }
447 })
448
449 const transHandler = (expr) => {
450 const module = parser.state.module
451 const current = parser.state.current
452 const { queryObj, resourcePath } = parseRequest(module.resource)
453 const localSrcMode = queryObj.mode
454 const globalSrcMode = this.options.srcMode
455 const srcMode = localSrcMode || globalSrcMode
456 const mode = this.options.mode
457
458 let target
459
460 if (expr.type === 'Identifier') {
461 target = expr
462 } else if (expr.type === 'MemberExpression') {
463 target = expr.object
464 }
465 if (!matchCondition(resourcePath, this.options.transMpxRules) || resourcePath.indexOf('@mpxjs') !== -1 || !target || mode === srcMode) {
466 return
467 }
468
469 const type = target.name
470
471 const name = type === 'wx' ? 'mpx' : 'createFactory'
472 const replaceContent = type === 'wx' ? 'mpx' : `${name}(${JSON.stringify(type)})`
473
474 const dep = new ReplaceDependency(replaceContent, target.range)
475 current.addDependency(dep)
476
477 let needInject = true
478 for (let v of module.variables) {
479 if (v.name === name) {
480 needInject = false
481 break
482 }
483 }
484 if (needInject) {
485 const expression = `require(${JSON.stringify(`@mpxjs/core/src/runtime/${name}`)})`
486 const deps = []
487 parser.parse(expression, {
488 current: {
489 addDependency: dep => {
490 dep.userRequest = name
491 deps.push(dep)
492 }
493 },
494 module
495 })
496 module.addVariable(name, expression, deps)
497 }
498 }
499
500 // hack babel polyfill global
501 parser.hooks.evaluate.for('CallExpression').tap('MpxWebpackPlugin', (expr) => {
502 if (/core-js/.test(parser.state.module.resource)) {
503 const current = parser.state.current
504 const arg0 = expr.arguments[0]
505 const callee = expr.callee
506 if (arg0 && arg0.value === 'return this' && callee.name === 'Function') {
507 current.addDependency(new InjectDependency({
508 content: '(function() { return this })() || ',
509 index: expr.range[0]
510 }))
511 }
512 }
513 })
514
515 if (this.options.srcMode !== this.options.mode) {
516 // 全量替换未声明的wx identifier
517 parser.hooks.expression.for('wx').tap('MpxWebpackPlugin', transHandler)
518
519 // parser.hooks.evaluate.for('MemberExpression').tap('MpxWebpackPlugin', (expr) => {
520 // // Undeclared varible for wx[identifier]()
521 // // TODO Unable to handle wx[identifier]
522 // if (expr.object.name === 'wx' && !parser.scope.definitions.has('wx')) {
523 // transHandler(expr)
524 // }
525 // })
526 // // Trans for wx.xx, wx['xx'], wx.xx(), wx['xx']()
527 // parser.hooks.expressionAnyMember.for('wx').tap('MpxWebpackPlugin', transHandler)
528 // Proxy ctor for transMode
529 if (!this.options.forceDisableProxyCtor) {
530 parser.hooks.call.for('Page').tap('MpxWebpackPlugin', (expr) => {
531 transHandler(expr.callee)
532 })
533 parser.hooks.call.for('Component').tap('MpxWebpackPlugin', (expr) => {
534 transHandler(expr.callee)
535 })
536 parser.hooks.call.for('App').tap('MpxWebpackPlugin', (expr) => {
537 transHandler(expr.callee)
538 })
539 if (this.options.mode === 'ali') {
540 // 支付宝不支持Behaviors
541 parser.hooks.call.for('Behavior').tap('MpxWebpackPlugin', (expr) => {
542 transHandler(expr.callee)
543 })
544 }
545 }
546 }
547
548 const apiBlackListMap = [
549 'createApp',
550 'createPage',
551 'createComponent',
552 'createStore',
553 'toPureObject',
554 'mixin: injectMixins',
555 'injectMixins',
556 'observable',
557 'extendObservable',
558 'watch',
559 'use',
560 'set',
561 'get',
562 'remove',
563 'setConvertRule',
564 'createAction'
565 ].reduce((map, api) => {
566 map[api] = true
567 return map
568 }, {})
569
570 const handler = (expr) => {
571 const callee = expr.callee
572 const args = expr.arguments
573 const name = callee.object.name
574 const { queryObj, resourcePath } = parseRequest(parser.state.module.resource)
575
576 if (apiBlackListMap[callee.property.name || callee.property.value] || (name !== 'mpx' && name !== 'wx') || (name === 'wx' && !matchCondition(resourcePath, this.options.transMpxRules))) {
577 return
578 }
579
580 const localSrcMode = queryObj.mode
581 const globalSrcMode = this.options.srcMode
582 const srcMode = localSrcMode || globalSrcMode
583 const srcModeString = `__mpx_src_mode_${srcMode}__`
584 const dep = new InjectDependency({
585 content: args.length
586 ? `, ${JSON.stringify(srcModeString)}`
587 : JSON.stringify(srcModeString),
588 index: expr.end - 1
589 })
590 parser.state.current.addDependency(dep)
591 }
592
593 if (this.options.srcMode !== this.options.mode) {
594 parser.hooks.callAnyMember.for('imported var').tap('MpxWebpackPlugin', handler)
595 parser.hooks.callAnyMember.for('mpx').tap('MpxWebpackPlugin', handler)
596 parser.hooks.callAnyMember.for('wx').tap('MpxWebpackPlugin', handler)
597 }
598 })
599 })
600
601 compiler.hooks.normalModuleFactory.tap('MpxWebpackPlugin', (normalModuleFactory) => {
602 // resolve前修改原始request
603 normalModuleFactory.hooks.beforeResolve.tapAsync('MpxWebpackPlugin', (data, callback) => {
604 let request = data.request
605 let { queryObj, resource } = parseRequest(request)
606 if (queryObj.resolve) {
607 // 此处的query用于将资源引用的当前包信息传递给resolveDependency
608 const pathLoader = normalize.lib('path-loader')
609 const packageName = mpx.currentPackageRoot || 'main'
610 resource = addQuery(resource, {
611 packageName
612 })
613 data.request = `!!${pathLoader}!${resource}`
614 } else if (queryObj.wxsModule) {
615 const wxsPreLoader = normalize.lib('wxs/wxs-pre-loader')
616 if (!/wxs-loader/.test(request)) {
617 data.request = `!!${wxsPreLoader}!${resource}`
618 }
619 }
620 callback(null, data)
621 })
622
623 // resolve完成后修改loaders或者resource/request
624 normalModuleFactory.hooks.afterResolve.tapAsync('MpxWebpackPlugin', (data, callback) => {
625 const isFromMpx = /\.(mpx|vue)/.test(data.resource)
626 if (data.loaders && isFromMpx) {
627 data.loaders.forEach((loader) => {
628 if (/ts-loader/.test(loader.loader)) {
629 loader.options = Object.assign({}, { appendTsSuffixTo: [/\.(mpx|vue)$/] })
630 }
631 })
632 }
633 // 根据用户传入的modeRules对特定资源添加mode query
634 data.resource = this.runModeRules(data.resource)
635
636 if (mpx.currentPackageRoot) {
637 const resourcePath = parseRequest(data.resource).resourcePath
638
639 const staticResourceHit = mpx.staticResourceHit
640 const packageName = mpx.currentPackageRoot || 'main'
641
642 let needAddQuery = false
643
644 if (staticResourceHit[resourcePath] && staticResourceHit[resourcePath] !== packageName) {
645 needAddQuery = true
646 }
647
648 if (needAddQuery) {
649 // 此处的query用于避免静态资源模块缓存,确保不同分包中引用的静态资源为不同模块
650 data.request = addQuery(data.request, {
651 packageName
652 })
653 }
654 }
655 callback(null, data)
656 })
657 })
658
659 compiler.hooks.emit.tapAsync('MpxWebpackPlugin', (compilation, callback) => {
660 if (this.options.mode === 'web') return callback()
661 const jsonpFunction = compilation.outputOptions.jsonpFunction
662
663 function getTargetFile (file) {
664 let targetFile = file
665 const queryStringIdx = targetFile.indexOf('?')
666 if (queryStringIdx >= 0) {
667 targetFile = targetFile.substr(0, queryStringIdx)
668 }
669 return targetFile
670 }
671
672 const processedChunk = new Set()
673 const rootName = compilation._preparedEntrypoints[0].name
674
675 function processChunk (chunk, isRuntime, relativeChunks) {
676 if (!chunk.files[0] || processedChunk.has(chunk)) {
677 return
678 }
679
680 let originalSource = compilation.assets[chunk.files[0]]
681 const source = new ConcatSource()
682 source.add('\nvar window = window || {};\n\n')
683
684 relativeChunks.forEach((relativeChunk, index) => {
685 if (!relativeChunk.files[0]) return
686 let chunkPath = getTargetFile(chunk.files[0])
687 let relativePath = getTargetFile(relativeChunk.files[0])
688 relativePath = path.relative(path.dirname(chunkPath), relativePath)
689 relativePath = fixRelative(relativePath, mpx.mode)
690 relativePath = toPosix(relativePath)
691 if (index === 0) {
692 // 引用runtime
693 // 支付宝分包独立打包,通过全局context获取webpackJSONP
694 if (mpx.mode === 'ali') {
695 if (chunk.name === rootName) {
696 // 在rootChunk中挂载jsonpFunction
697 source.add('// process ali subpackages runtime in root chunk\n' +
698 'var context = (function() { return this })() || Function("return this")();\n\n')
699 source.add(`context[${JSON.stringify(jsonpFunction)}] = window[${JSON.stringify(jsonpFunction)}] = require("${relativePath}");\n`)
700 } else {
701 // 其余chunk中通过context全局传递runtime
702 source.add('// process ali subpackages runtime in other chunk\n' +
703 'var context = (function() { return this })() || Function("return this")();\n\n')
704 source.add(`window[${JSON.stringify(jsonpFunction)}] = context[${JSON.stringify(jsonpFunction)}];\n`)
705 }
706 } else {
707 source.add(`window[${JSON.stringify(jsonpFunction)}] = require("${relativePath}");\n`)
708 }
709 } else {
710 source.add(`require("${relativePath}");\n`)
711 }
712 })
713
714 if (isRuntime) {
715 source.add('var context = (function() { return this })() || Function("return this")();\n')
716 source.add(`
717// Fix babel runtime in some quirky environment like ali & qq dev.
718if(!context.console) {
719 try {
720 context.console = console;
721 context.setInterval = setInterval;
722 context.setTimeout = setTimeout;
723 context.JSON = JSON;
724 context.Math = Math;
725 context.RegExp = RegExp;
726 context.Infinity = Infinity;
727 context.isFinite = isFinite;
728 context.parseFloat = parseFloat;
729 context.parseInt = parseInt;
730 context.Promise = Promise;
731 context.WeakMap = WeakMap;
732 context.Reflect = Reflect;
733 context.RangeError = RangeError;
734 context.TypeError = TypeError;
735 context.Uint8Array = Uint8Array;
736 context.DataView = DataView;
737 context.ArrayBuffer = ArrayBuffer;
738 context.Symbol = Symbol;
739 } catch(e){
740 }
741}
742\n`)
743 if (mpx.mode === 'swan') {
744 source.add('// swan runtime fix\n' +
745 'if (!context.navigator) {\n' +
746 ' context.navigator = {};\n' +
747 '}\n' +
748 'Object.defineProperty(context.navigator, "standalone",{\n' +
749 ' configurable: true,' +
750 ' enumerable: true,' +
751 ' get () {\n' +
752 ' return true;\n' +
753 ' }\n' +
754 '});\n\n')
755 }
756 source.add(originalSource)
757 source.add(`\nmodule.exports = window[${JSON.stringify(jsonpFunction)}];\n`)
758 } else {
759 if (mpx.pluginMain === chunk.name) {
760 source.add('module.exports =\n')
761 }
762 source.add(originalSource)
763 }
764
765 compilation.assets[chunk.files[0]] = source
766 processedChunk.add(chunk)
767 }
768
769 compilation.chunkGroups.forEach((chunkGroup) => {
770 if (!chunkGroup.isInitial()) {
771 return
772 }
773
774 let runtimeChunk, entryChunk
775 let middleChunks = []
776
777 let chunksLength = chunkGroup.chunks.length
778
779 chunkGroup.chunks.forEach((chunk, index) => {
780 if (index === 0) {
781 runtimeChunk = chunk
782 } else if (index === chunksLength - 1) {
783 entryChunk = chunk
784 } else {
785 middleChunks.push(chunk)
786 }
787 })
788
789 if (runtimeChunk) {
790 processChunk(runtimeChunk, true, [])
791 if (middleChunks.length) {
792 middleChunks.forEach((middleChunk) => {
793 processChunk(middleChunk, false, [runtimeChunk])
794 })
795 }
796 if (entryChunk) {
797 middleChunks.unshift(runtimeChunk)
798 processChunk(entryChunk, false, middleChunks)
799 }
800 }
801 })
802
803 if (this.options.generateBuildMap) {
804 const pagesMap = compilation.__mpx__.pagesMap
805 const componentsPackageMap = compilation.__mpx__.componentsMap
806 const componentsMap = Object.keys(componentsPackageMap).map(item => componentsPackageMap[item]).reduce((pre, cur) => { return { ...pre, ...cur } }, {})
807 const outputMap = JSON.stringify({ ...pagesMap, ...componentsMap })
808 compilation.assets['../outputMap.json'] = {
809 source: () => {
810 return outputMap
811 },
812 size: () => {
813 return Buffer.byteLength(outputMap, 'utf8')
814 }
815 }
816 }
817
818 callback()
819 })
820 }
821}
822
823module.exports = MpxWebpackPlugin