@taro-hmr-compile/plugin-webpack 0.0.1-alpha.6 → 0.0.1-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compile/hmr-compiler-loader.d.ts +1 -1
- package/dist/compile/hmr-compiler-loader.js +48 -40
- package/dist/compile/hmr-compiler-loader.js.map +1 -1
- package/dist/compile/hmr-compiler-plugin.d.ts +4 -2
- package/dist/compile/hmr-compiler-plugin.js +11 -14
- package/dist/compile/hmr-compiler-plugin.js.map +1 -1
- package/dist/patch/hmr-patch-plugin.d.ts +6 -5
- package/dist/patch/hmr-patch-plugin.js +33 -179
- package/dist/patch/hmr-patch-plugin.js.map +1 -1
- package/dist/patch/hmr-patch-processor.d.ts +29 -0
- package/dist/patch/hmr-patch-processor.js +105 -0
- package/dist/patch/hmr-patch-processor.js.map +1 -0
- package/dist/patch/types.d.ts +15 -0
- package/dist/patch/types.js +3 -0
- package/dist/patch/types.js.map +1 -0
- package/dist/patch/utils.d.ts +26 -0
- package/dist/patch/utils.js +173 -0
- package/dist/patch/utils.js.map +1 -0
- package/package.json +2 -2
- package/src/compile/hmr-compiler-loader.ts +58 -40
- package/src/compile/hmr-compiler-plugin.ts +18 -16
- package/src/patch/hmr-patch-plugin.ts +42 -168
- package/src/patch/hmr-patch-processor.ts +149 -0
- package/src/patch/types.ts +19 -0
- package/src/patch/utils.ts +174 -0
- package/dist/patch/hmr-patch-manager.d.ts +0 -30
- package/dist/patch/hmr-patch-manager.js +0 -75
- package/dist/patch/hmr-patch-manager.js.map +0 -1
- package/src/patch/hmr-patch-manager.ts +0 -102
|
@@ -1,217 +1,90 @@
|
|
|
1
|
-
import generator from '@babel/generator'
|
|
2
|
-
import {parse} from '@babel/parser'
|
|
3
|
-
import template from '@babel/template'
|
|
4
|
-
import traverse from '@babel/traverse'
|
|
5
|
-
import * as t from '@babel/types'
|
|
6
1
|
import type {Compilation, Compiler} from 'webpack'
|
|
7
2
|
import {RawSource} from 'webpack-sources'
|
|
8
|
-
import {
|
|
3
|
+
import {HMR_PATCH_ASSET} from '../constants'
|
|
9
4
|
import {HmrCompilerPlugin} from '../compile/hmr-compiler-plugin'
|
|
10
|
-
import {HmrPatchManager} from './hmr-patch-manager'
|
|
11
|
-
import type {PatchSource, UpdatePatchSource} from './hmr-patch-manager'
|
|
12
5
|
import {HmrPatchRuntimePlugin} from './hmr-patch-runtime-plugin'
|
|
6
|
+
import {HmrPatchProcessor, HmrPatchProcessorOptions} from "./hmr-patch-processor";
|
|
13
7
|
|
|
14
8
|
const PLUGIN_NAME = 'HmrPatchPlugin'
|
|
15
9
|
const HOT_UPDATE_MANIFEST_SUFFIX = '.hot-update.json'
|
|
16
10
|
const HOT_UPDATE_SCRIPT_SUFFIX = '.hot-update.js'
|
|
17
|
-
const HOT_UPDATE_HASH_PATTERN = /\.([0-9a-f]+)\.hot-update\.json$/i
|
|
18
11
|
const STYLE_ASSET_PATTERN = /\.(?:css|wxss|acss|ttss|qss|jxss)(?:\.map)?$/i
|
|
19
12
|
const TEMPLATE_RUNTIME_ASSET_PATTERN = /(?:^|\/)(?:base\.wxml|utils\.wxs)$/i
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
m?: Array<string | number>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type CompilationAsset = ReturnType<Compilation['getAssets']>[number]
|
|
26
|
-
|
|
27
|
-
interface HotUpdateAssets {
|
|
28
|
-
manifests: CompilationAsset[]
|
|
29
|
-
scripts: CompilationAsset[]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface HmrPatchPluginOptions {
|
|
33
|
-
patchHistoryLimit?: number
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const collectModuleFactories = (source: string, moduleFactories: Map<string, t.Expression>) => {
|
|
37
|
-
const ast = parse(`(${source})`, {sourceType: 'script'})
|
|
38
|
-
const statement = ast.program.body[0]
|
|
39
|
-
if (!t.isExpressionStatement(statement) || !t.isObjectExpression(statement.expression)) {
|
|
40
|
-
throw new Error('[taro-hmr-compile] Page Chunk 模块段不是对象表达式')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
statement.expression.properties.forEach(property => {
|
|
44
|
-
if (!t.isObjectProperty(property) || !t.isExpression(property.value)) {
|
|
45
|
-
throw new Error('[taro-hmr-compile] Page Chunk 模块对象包含无法识别的属性')
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const {key} = property
|
|
49
|
-
let moduleId: string
|
|
50
|
-
if (t.isStringLiteral(key) || t.isNumericLiteral(key)) moduleId = String(key.value)
|
|
51
|
-
else if (t.isIdentifier(key) && !property.computed) moduleId = key.name
|
|
52
|
-
else throw new Error('[taro-hmr-compile] Page Chunk 模块对象包含无法识别的模块 ID')
|
|
53
|
-
|
|
54
|
-
moduleFactories.set(moduleId, property.value)
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** 将本轮 Webpack hot-update 产物转换为内部 update 表示。 */
|
|
59
|
-
const createUpdatePatch = (toHash: string, assets: HotUpdateAssets): UpdatePatchSource => {
|
|
60
|
-
const manifestAsset = assets.manifests[0]
|
|
61
|
-
const hashMatch = manifestAsset.name.match(HOT_UPDATE_HASH_PATTERN)
|
|
62
|
-
if (!hashMatch) throw new Error(`[taro-hmr-compile] 无法从 ${manifestAsset.name} 提取 HMR 基线 hash`)
|
|
63
|
-
|
|
64
|
-
const moduleFactories = new Map<string, t.Expression>()
|
|
65
|
-
assets.scripts.forEach(asset => {
|
|
66
|
-
const ast = parse(asset.source.source().toString(), {sourceType: 'script'})
|
|
67
|
-
|
|
68
|
-
traverse(ast, {
|
|
69
|
-
CallExpression(nodePath) {
|
|
70
|
-
const {callee} = nodePath.node
|
|
71
|
-
if (!t.isMemberExpression(callee) || !t.isIdentifier(callee.object, {name: 'wx'})) return
|
|
72
|
-
if (!t.isStringLiteral(callee.property) || !callee.property.value.startsWith('webpackHotUpdate')) return
|
|
73
|
-
|
|
74
|
-
const modules = nodePath.node.arguments[1]
|
|
75
|
-
if (!t.isObjectExpression(modules)) return
|
|
76
|
-
|
|
77
|
-
modules.properties.forEach(property => {
|
|
78
|
-
if (!t.isObjectProperty(property) || !t.isExpression(property.value)) {
|
|
79
|
-
throw new Error('[taro-hmr-compile] HMR 模块对象包含无法识别的属性')
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const {key} = property
|
|
83
|
-
let moduleId: string
|
|
84
|
-
if (t.isStringLiteral(key) || t.isNumericLiteral(key)) moduleId = String(key.value)
|
|
85
|
-
else if (t.isIdentifier(key) && !property.computed) moduleId = key.name
|
|
86
|
-
else throw new Error('[taro-hmr-compile] HMR 模块对象包含无法识别的模块 ID')
|
|
87
|
-
|
|
88
|
-
moduleFactories.set(moduleId, property.value)
|
|
89
|
-
})
|
|
90
|
-
},
|
|
91
|
-
})
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
const removedModules = new Set<string>()
|
|
95
|
-
assets.manifests.forEach(asset => {
|
|
96
|
-
const manifest = JSON.parse(asset.source.source().toString()) as HotUpdateManifest
|
|
97
|
-
manifest.m?.forEach(moduleId => removedModules.add(String(moduleId)))
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
type: 'update',
|
|
102
|
-
fromHash: hashMatch[1],
|
|
103
|
-
toHash,
|
|
104
|
-
moduleFactories,
|
|
105
|
-
removedModules: Array.from(removedModules),
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** 将内部 Patch 表示序列化为固定 patch.js。 */
|
|
110
|
-
const createPatchAssetSource = (patches: PatchSource[]): string => {
|
|
111
|
-
const buildBootstrapPatchNode = template.expression(`({
|
|
112
|
-
type: 'bootstrap',
|
|
113
|
-
modules: %%MODULES%%
|
|
114
|
-
})`)
|
|
115
|
-
const buildUpdatePatchNode = template.expression(`({
|
|
116
|
-
type: 'update',
|
|
117
|
-
fromHash: %%FROM_HASH%%,
|
|
118
|
-
toHash: %%TO_HASH%%,
|
|
119
|
-
modules: %%MODULES%%,
|
|
120
|
-
removedModules: %%REMOVED_MODULES%%
|
|
121
|
-
})`)
|
|
122
|
-
const patchNodes = patches.map(patch => {
|
|
123
|
-
const modules = t.objectExpression(Array.from(
|
|
124
|
-
patch.moduleFactories,
|
|
125
|
-
([moduleId, factory]) => t.objectProperty(t.stringLiteral(moduleId), factory),
|
|
126
|
-
))
|
|
127
|
-
if (patch.type === 'bootstrap') return buildBootstrapPatchNode({MODULES: modules})
|
|
128
|
-
|
|
129
|
-
return buildUpdatePatchNode({
|
|
130
|
-
FROM_HASH: t.stringLiteral(patch.fromHash),
|
|
131
|
-
TO_HASH: t.stringLiteral(patch.toHash),
|
|
132
|
-
MODULES: modules,
|
|
133
|
-
REMOVED_MODULES: t.arrayExpression(patch.removedModules.map(moduleId => t.stringLiteral(moduleId))),
|
|
134
|
-
})
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
const statements = template.statements('module.exports = %%PATCHES%%;')({PATCHES: t.arrayExpression(patchNodes)})
|
|
138
|
-
return generator(t.program(statements)).code
|
|
139
|
-
}
|
|
14
|
+
type HmrPatchPluginOptions = Partial<HmrPatchProcessorOptions>
|
|
140
15
|
|
|
141
16
|
const isPassthroughAsset = (assetName: string) => {
|
|
142
17
|
return STYLE_ASSET_PATTERN.test(assetName) || TEMPLATE_RUNTIME_ASSET_PATTERN.test(assetName)
|
|
143
18
|
}
|
|
144
19
|
|
|
145
20
|
export class HmrPatchPlugin {
|
|
146
|
-
private hasCompletedInitialBuild = false
|
|
147
21
|
|
|
148
|
-
private
|
|
22
|
+
private previousHash: string = ''
|
|
23
|
+
|
|
24
|
+
private initialBuildDone = false
|
|
25
|
+
|
|
26
|
+
private readonly patchProcessor: HmrPatchProcessor
|
|
149
27
|
|
|
150
28
|
private readonly emittedAssetSources = new Map<string, string>()
|
|
151
29
|
|
|
152
30
|
constructor(options: HmrPatchPluginOptions) {
|
|
153
|
-
this.
|
|
31
|
+
this.patchProcessor = new HmrPatchProcessor(options)
|
|
154
32
|
}
|
|
155
33
|
|
|
156
34
|
apply(compiler: Compiler) {
|
|
157
35
|
new HmrPatchRuntimePlugin().apply(compiler)
|
|
158
36
|
|
|
159
37
|
compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
HmrCompilerPlugin.getCompilationHooks(compilation).pageChunkModules.tap(PLUGIN_NAME, source => {
|
|
163
|
-
collectModuleFactories(source, initialPageFactories)
|
|
38
|
+
HmrCompilerPlugin.getCompilationHooks(compilation).chunkModulesRendered.tap(PLUGIN_NAME, (source, isBootstrapChunk) => {
|
|
39
|
+
this.patchProcessor.processChunkModules({compilation, source, isBootstrapChunk})
|
|
164
40
|
})
|
|
165
41
|
|
|
166
42
|
// 等 Webpack 生成完整的 hot-update assets 后,再统一转换成固定的 patch.js。
|
|
167
43
|
const stage = compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT
|
|
168
|
-
compilation.hooks.processAssets.tap({name: PLUGIN_NAME, stage}, () =>
|
|
169
|
-
this.processAssets(compilation, initialPageFactories)
|
|
170
|
-
})
|
|
44
|
+
compilation.hooks.processAssets.tap({name: PLUGIN_NAME, stage}, () => this.processAssets(compilation))
|
|
171
45
|
})
|
|
172
46
|
|
|
173
47
|
compiler.hooks.afterEmit.tap(PLUGIN_NAME, compilation => {
|
|
174
48
|
const isSuccessful = compilation.errors.length === 0 && Boolean(compilation.hash)
|
|
175
|
-
this.
|
|
49
|
+
this.initialBuildDone = this.initialBuildDone || isSuccessful
|
|
176
50
|
})
|
|
177
51
|
|
|
178
52
|
compiler.hooks.watchClose.tap(PLUGIN_NAME, () => this.reset())
|
|
179
53
|
}
|
|
180
54
|
|
|
181
|
-
private processAssets(compilation: Compilation
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
55
|
+
private processAssets(compilation: Compilation) {
|
|
56
|
+
const {previousHash: fromHash, initialBuildDone} = this
|
|
57
|
+
|
|
58
|
+
const {hash: toHash, errors: compilationErrors} = compilation
|
|
59
|
+
|
|
60
|
+
if (!toHash || compilationErrors.length > 0) return
|
|
61
|
+
// 首次编译完成过滤文件输出
|
|
62
|
+
if (!initialBuildDone) {
|
|
63
|
+
this.patchProcessor.commitBootstrapFactories(compilation)
|
|
64
|
+
compilation.getAssets().forEach(asset => {
|
|
65
|
+
if (!isPassthroughAsset(asset.name)) return
|
|
66
|
+
this.emittedAssetSources.set(asset.name, asset.source.source().toString())
|
|
192
67
|
})
|
|
193
|
-
this.
|
|
194
|
-
this.emitPatchAsset(compilation, false)
|
|
195
|
-
return
|
|
68
|
+
this.previousHash = toHash
|
|
69
|
+
return this.emitPatchAsset(compilation, false)
|
|
196
70
|
}
|
|
197
71
|
|
|
198
|
-
const
|
|
199
|
-
manifests: originalAssets.filter(asset => asset.name.endsWith(HOT_UPDATE_MANIFEST_SUFFIX)),
|
|
200
|
-
scripts: originalAssets.filter(asset => asset.name.endsWith(HOT_UPDATE_SCRIPT_SUFFIX)),
|
|
201
|
-
}
|
|
72
|
+
const manifests = compilation.getAssets().filter(asset => asset.name.endsWith(HOT_UPDATE_MANIFEST_SUFFIX))
|
|
202
73
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
74
|
+
const scripts = compilation.getAssets().filter(asset => asset.name.endsWith(HOT_UPDATE_SCRIPT_SUFFIX))
|
|
75
|
+
|
|
76
|
+
this.patchProcessor.processUpdate({toHash, fromHash, manifests, scripts})
|
|
77
|
+
|
|
78
|
+
this.emitPatchAsset(compilation, true)
|
|
79
|
+
|
|
80
|
+
this.removeSupersededAssets(compilation)
|
|
207
81
|
|
|
208
|
-
|
|
209
|
-
this.removeSupersededAssets(compilation, originalAssets)
|
|
82
|
+
this.previousHash = toHash
|
|
210
83
|
}
|
|
211
84
|
|
|
212
85
|
/** 热更新转换完成后,保留固定 patch 与发生变化的模板、平台样式资产。 */
|
|
213
|
-
private removeSupersededAssets(compilation: Compilation
|
|
214
|
-
|
|
86
|
+
private removeSupersededAssets(compilation: Compilation) {
|
|
87
|
+
compilation.getAssets().forEach(asset => {
|
|
215
88
|
if (asset.name === HMR_PATCH_ASSET) return
|
|
216
89
|
if (!isPassthroughAsset(asset.name)) return compilation.deleteAsset(asset.name)
|
|
217
90
|
|
|
@@ -223,15 +96,16 @@ export class HmrPatchPlugin {
|
|
|
223
96
|
}
|
|
224
97
|
|
|
225
98
|
private emitPatchAsset(compilation: Compilation, isHotUpdate: boolean) {
|
|
226
|
-
const source = new RawSource(
|
|
99
|
+
const source = new RawSource(this.patchProcessor.generate())
|
|
227
100
|
const assetInfo = {hotModuleReplacement: isHotUpdate}
|
|
228
101
|
if (compilation.getAsset(HMR_PATCH_ASSET)) compilation.updateAsset(HMR_PATCH_ASSET, source, assetInfo)
|
|
229
102
|
else compilation.emitAsset(HMR_PATCH_ASSET, source, assetInfo)
|
|
230
103
|
}
|
|
231
104
|
|
|
232
105
|
private reset() {
|
|
233
|
-
this.
|
|
106
|
+
this.previousHash = ''
|
|
107
|
+
this.initialBuildDone = false
|
|
234
108
|
this.emittedAssetSources.clear()
|
|
235
|
-
this.
|
|
109
|
+
this.patchProcessor.reset()
|
|
236
110
|
}
|
|
237
111
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {type Compilation,} from "webpack";
|
|
2
|
+
import generator from '@babel/generator'
|
|
3
|
+
import {
|
|
4
|
+
parseHotUpdateAssets,
|
|
5
|
+
ParseHotUpdateAssetsOptions,
|
|
6
|
+
parseChunkAsset, mergeUpdatePatches, generatePatchAssetSource,
|
|
7
|
+
} from "./utils";
|
|
8
|
+
import {createHash} from "crypto";
|
|
9
|
+
import {ModuleFactory, UpdatePatchSource} from "./types";
|
|
10
|
+
import {DEFAULT_HMR_PATCH_HISTORY_LIMIT} from "../constants";
|
|
11
|
+
|
|
12
|
+
interface ModuleHash {
|
|
13
|
+
moduleId: string;
|
|
14
|
+
factoryHash: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ProcessChunkModulesOptions {
|
|
18
|
+
source: string
|
|
19
|
+
compilation: Compilation,
|
|
20
|
+
isBootstrapChunk: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HmrPatchProcessorOptions {
|
|
24
|
+
patchHistoryLimit?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ProcessUpdateOptions extends ParseHotUpdateAssetsOptions {
|
|
28
|
+
fromHash: string
|
|
29
|
+
toHash: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type BootstrapFactories = Map<string, ModuleFactory>
|
|
33
|
+
|
|
34
|
+
type ModuleHashesByFullHash = Map<string, Map<string, ModuleHash>>
|
|
35
|
+
|
|
36
|
+
export class HmrPatchProcessor {
|
|
37
|
+
private readonly bootstrapFactories: BootstrapFactories = new Map()
|
|
38
|
+
|
|
39
|
+
private readonly bootstrapFactoriesByCompilation = new WeakMap<Compilation, BootstrapFactories>()
|
|
40
|
+
|
|
41
|
+
private readonly moduleHashesByFullHash: ModuleHashesByFullHash = new Map()
|
|
42
|
+
|
|
43
|
+
private recentUpdatePatches: UpdatePatchSource[] = []
|
|
44
|
+
|
|
45
|
+
private readonly opts: Partial<HmrPatchProcessorOptions>
|
|
46
|
+
|
|
47
|
+
constructor(opts: Partial<HmrPatchProcessorOptions>) {
|
|
48
|
+
this.opts = opts
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private getChangedModuleIdsByFullHash(fromHash: string, toHash: string): Set<string> | undefined {
|
|
52
|
+
const fromModuleHashes = this.moduleHashesByFullHash.get(fromHash)
|
|
53
|
+
|
|
54
|
+
const toModuleHashes = this.moduleHashesByFullHash.get(toHash)
|
|
55
|
+
|
|
56
|
+
// 任一编译缺少 Hash 基线时不做过滤,避免误丢有效的 hot-update 模块。
|
|
57
|
+
if (!fromModuleHashes || !toModuleHashes) return
|
|
58
|
+
|
|
59
|
+
// 同时检查前后两次编译的模块,确保新增、修改和删除都能被识别。
|
|
60
|
+
const moduleIds = new Set(fromModuleHashes.keys())
|
|
61
|
+
toModuleHashes.forEach((_, moduleId) => moduleIds.add(moduleId))
|
|
62
|
+
|
|
63
|
+
const changedModuleIds = new Set<string>()
|
|
64
|
+
moduleIds.forEach(moduleId => {
|
|
65
|
+
const fromFactoryHash = fromModuleHashes.get(moduleId)?.factoryHash
|
|
66
|
+
const toFactoryHash = toModuleHashes.get(moduleId)?.factoryHash
|
|
67
|
+
if (fromFactoryHash !== toFactoryHash) changedModuleIds.add(moduleId)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return changedModuleIds
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
processUpdate(opt: ProcessUpdateOptions) {
|
|
74
|
+
const {fromHash, toHash} = opt
|
|
75
|
+
|
|
76
|
+
const tempPatchInfo = parseHotUpdateAssets(opt)
|
|
77
|
+
|
|
78
|
+
const changedModuleIds = this.getChangedModuleIdsByFullHash(fromHash, toHash)
|
|
79
|
+
|
|
80
|
+
const moduleFactories = (() => {
|
|
81
|
+
if (!changedModuleIds) return Array.from(tempPatchInfo.moduleFactories.values())
|
|
82
|
+
return Array.from(tempPatchInfo.moduleFactories.values()).filter(item => changedModuleIds.has(item.moduleId))
|
|
83
|
+
})()
|
|
84
|
+
|
|
85
|
+
const removedModules = (() => {
|
|
86
|
+
if (!changedModuleIds) return Array.from(tempPatchInfo.removedModules.values())
|
|
87
|
+
return Array.from(tempPatchInfo.removedModules.values()).filter(item => changedModuleIds.has(item))
|
|
88
|
+
})()
|
|
89
|
+
|
|
90
|
+
this.recentUpdatePatches.push({type: 'update', moduleFactories, fromHash, toHash, removedModules})
|
|
91
|
+
|
|
92
|
+
const patchHistoryLimit = Math.max(1, this.opts.patchHistoryLimit ?? DEFAULT_HMR_PATCH_HISTORY_LIMIT)
|
|
93
|
+
|
|
94
|
+
if (this.recentUpdatePatches.length <= patchHistoryLimit) return
|
|
95
|
+
|
|
96
|
+
const [oldestUpdatePatch, secondOldestUpdatePatch, ...rest] = this.recentUpdatePatches
|
|
97
|
+
|
|
98
|
+
const mergedUpdatePatch = mergeUpdatePatches(oldestUpdatePatch, secondOldestUpdatePatch)
|
|
99
|
+
|
|
100
|
+
this.recentUpdatePatches = [mergedUpdatePatch, ...rest]
|
|
101
|
+
|
|
102
|
+
this.moduleHashesByFullHash.delete(oldestUpdatePatch.toHash)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
processChunkModules(opt: ProcessChunkModulesOptions) {
|
|
106
|
+
const {hash: fullHash} = opt.compilation
|
|
107
|
+
const modules = parseChunkAsset(opt.source)
|
|
108
|
+
modules.forEach(moduleFactory => {
|
|
109
|
+
const {moduleId, factory} = moduleFactory
|
|
110
|
+
|
|
111
|
+
if (opt.isBootstrapChunk) {
|
|
112
|
+
const compilationFactories = this.bootstrapFactoriesByCompilation.get(opt.compilation) ?? new Map()
|
|
113
|
+
compilationFactories.set(moduleId, {moduleId, factory})
|
|
114
|
+
this.bootstrapFactoriesByCompilation.set(opt.compilation, compilationFactories)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!fullHash) return
|
|
118
|
+
|
|
119
|
+
const moduleHashes = (() => {
|
|
120
|
+
const hasCurFullHashModuleHashMap = this.moduleHashesByFullHash.has(fullHash)
|
|
121
|
+
if (!hasCurFullHashModuleHashMap) this.moduleHashesByFullHash.set(fullHash, new Map())
|
|
122
|
+
return this.moduleHashesByFullHash.get(fullHash)!
|
|
123
|
+
})()
|
|
124
|
+
|
|
125
|
+
const {code: source} = generator(factory, {compact: true, comments: false, minified: true,})
|
|
126
|
+
const factoryHash = createHash('sha256').update(source).digest('hex')
|
|
127
|
+
moduleHashes.set(moduleId, {moduleId, factoryHash})
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
commitBootstrapFactories(compilation: Compilation) {
|
|
132
|
+
this.bootstrapFactories.clear()
|
|
133
|
+
this.bootstrapFactoriesByCompilation.get(compilation)?.forEach(moduleFactory => {
|
|
134
|
+
this.bootstrapFactories.set(moduleFactory.moduleId, moduleFactory)
|
|
135
|
+
})
|
|
136
|
+
this.bootstrapFactoriesByCompilation.delete(compilation)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
generate() {
|
|
140
|
+
const {bootstrapFactories, recentUpdatePatches} = this
|
|
141
|
+
return generatePatchAssetSource({bootstrapFactories, recentUpdatePatches})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
reset() {
|
|
145
|
+
this.bootstrapFactories.clear()
|
|
146
|
+
this.moduleHashesByFullHash.clear()
|
|
147
|
+
this.recentUpdatePatches = []
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type {Expression} from "@babel/types";
|
|
2
|
+
|
|
3
|
+
export interface ModuleFactory {
|
|
4
|
+
factory: Expression,
|
|
5
|
+
moduleId: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface HotUpdateManifest {
|
|
9
|
+
m?: Array<string | number>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UpdatePatchSource {
|
|
13
|
+
type: 'update'
|
|
14
|
+
fromHash: string
|
|
15
|
+
toHash: string
|
|
16
|
+
moduleFactories: ModuleFactory[]
|
|
17
|
+
removedModules: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
import {parse} from "@babel/parser";
|
|
3
|
+
import traverse from "@babel/traverse";
|
|
4
|
+
import template from "@babel/template";
|
|
5
|
+
import generator from "@babel/generator";
|
|
6
|
+
import {Asset} from "webpack";
|
|
7
|
+
import {HotUpdateManifest, ModuleFactory} from "./types";
|
|
8
|
+
import type {ObjectExpression} from "@babel/types";
|
|
9
|
+
import {UpdatePatchSource} from "./types";
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 解析模块 Factory
|
|
14
|
+
* @param expression
|
|
15
|
+
*/
|
|
16
|
+
const parseModuleFactories = (expression: ObjectExpression) => {
|
|
17
|
+
|
|
18
|
+
const moduleFactories = new Map<string, ModuleFactory>()
|
|
19
|
+
|
|
20
|
+
if (!t.isObjectExpression(expression)) return moduleFactories.values()
|
|
21
|
+
|
|
22
|
+
expression.properties.forEach(property => {
|
|
23
|
+
if (!t.isObjectProperty(property) || !t.isExpression(property.value)) throw new Error('[taro-hmr-compile] HMR 模块对象包含无法识别的属性')
|
|
24
|
+
|
|
25
|
+
const {key, value: factory} = property
|
|
26
|
+
|
|
27
|
+
const moduleId = (() => {
|
|
28
|
+
if (t.isStringLiteral(key) || t.isNumericLiteral(key)) return String(key.value)
|
|
29
|
+
if (t.isIdentifier(key) && !property.computed) return key.name
|
|
30
|
+
throw new Error('[taro-hmr-compile] Chunk 模块对象包含无法识别的模块 ID')
|
|
31
|
+
})()
|
|
32
|
+
|
|
33
|
+
moduleFactories.set(moduleId, {factory, moduleId})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return moduleFactories.values()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
export interface ParseHotUpdateAssetsOptions {
|
|
41
|
+
manifests: Readonly<Asset>[]
|
|
42
|
+
scripts: Readonly<Asset>[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 解析热更新产物
|
|
47
|
+
* @param opt
|
|
48
|
+
*/
|
|
49
|
+
export const parseHotUpdateAssets = (opt: ParseHotUpdateAssetsOptions) => {
|
|
50
|
+
const {manifests, scripts} = opt
|
|
51
|
+
|
|
52
|
+
const moduleFactories = new Map<string, ModuleFactory>()
|
|
53
|
+
|
|
54
|
+
scripts.forEach(asset => {
|
|
55
|
+
|
|
56
|
+
const ast = parse(asset.source.source().toString(), {sourceType: 'script'})
|
|
57
|
+
|
|
58
|
+
traverse(ast, {
|
|
59
|
+
CallExpression(nodePath) {
|
|
60
|
+
const {callee} = nodePath.node
|
|
61
|
+
|
|
62
|
+
if (!t.isMemberExpression(callee) || !t.isIdentifier(callee.object, {name: 'wx'})) return
|
|
63
|
+
|
|
64
|
+
if (!t.isStringLiteral(callee.property) || !callee.property.value.startsWith('webpackHotUpdate')) return
|
|
65
|
+
|
|
66
|
+
const [_, modules] = nodePath.node.arguments
|
|
67
|
+
|
|
68
|
+
if (!t.isObjectExpression(modules)) return
|
|
69
|
+
|
|
70
|
+
parseModuleFactories(modules).forEach(module => moduleFactories.set(module.moduleId, module))
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const removedModules = new Set<string>()
|
|
76
|
+
|
|
77
|
+
manifests.forEach((module) => {
|
|
78
|
+
|
|
79
|
+
const manifest = JSON.parse(module.source.source().toString()) as HotUpdateManifest
|
|
80
|
+
|
|
81
|
+
const {m: tempRemoveModules = []} = manifest
|
|
82
|
+
|
|
83
|
+
tempRemoveModules.forEach(removedModule => removedModules.add(String(removedModule)))
|
|
84
|
+
}, [])
|
|
85
|
+
|
|
86
|
+
return {moduleFactories, removedModules}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 解析 chunk 产物
|
|
91
|
+
* @param source
|
|
92
|
+
*/
|
|
93
|
+
export const parseChunkAsset = (source: string): Map<string, ModuleFactory> => {
|
|
94
|
+
const ast = parse(`(${source})`, {sourceType: 'script'})
|
|
95
|
+
|
|
96
|
+
const [statement] = ast.program.body
|
|
97
|
+
|
|
98
|
+
if (!t.isExpressionStatement(statement) || !t.isObjectExpression(statement.expression)) throw new Error('[taro-hmr-compile] Chunk 模块段不是对象表达式')
|
|
99
|
+
|
|
100
|
+
const moduleFactories = new Map<string, ModuleFactory>()
|
|
101
|
+
|
|
102
|
+
parseModuleFactories(statement.expression).forEach(module => moduleFactories.set(module.moduleId, module))
|
|
103
|
+
|
|
104
|
+
return moduleFactories
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const mergeUpdatePatches = (basePatch: UpdatePatchSource, nextPatch: UpdatePatchSource,): UpdatePatchSource => {
|
|
108
|
+
if (basePatch.toHash !== nextPatch.fromHash) throw new Error(`[taro-hmr-compile] 无法合并不连续的 HMR patch:${basePatch.toHash} !== ${nextPatch.fromHash}`)
|
|
109
|
+
|
|
110
|
+
const moduleFactories = new Map(basePatch.moduleFactories.map(moduleFactory => [moduleFactory.moduleId, moduleFactory]))
|
|
111
|
+
|
|
112
|
+
const removedModules = new Set(basePatch.removedModules)
|
|
113
|
+
|
|
114
|
+
nextPatch.moduleFactories.forEach(moduleFactory => {
|
|
115
|
+
const {moduleId} = moduleFactory
|
|
116
|
+
moduleFactories.set(moduleId, moduleFactory)
|
|
117
|
+
removedModules.delete(moduleId)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
nextPatch.removedModules.forEach(moduleId => {
|
|
121
|
+
moduleFactories.delete(moduleId)
|
|
122
|
+
removedModules.add(moduleId)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
type: 'update',
|
|
127
|
+
fromHash: basePatch.fromHash,
|
|
128
|
+
toHash: nextPatch.toHash,
|
|
129
|
+
moduleFactories: [...moduleFactories.values()],
|
|
130
|
+
removedModules: [...removedModules],
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface generatePatchAssetSourceOpts {
|
|
135
|
+
bootstrapFactories: Map<string, ModuleFactory>
|
|
136
|
+
recentUpdatePatches: UpdatePatchSource[]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const generatePatchAssetSource = (opt: generatePatchAssetSourceOpts): string => {
|
|
140
|
+
const {bootstrapFactories, recentUpdatePatches} = opt
|
|
141
|
+
const buildBootstrapPatch = template.expression(`({
|
|
142
|
+
type: 'bootstrap',
|
|
143
|
+
modules: MODULES
|
|
144
|
+
})`)
|
|
145
|
+
const buildUpdatePatch = template.expression(`({
|
|
146
|
+
type: 'update',
|
|
147
|
+
fromHash: "FROM_HASH",
|
|
148
|
+
toHash: "TO_HASH",
|
|
149
|
+
modules: MODULES,
|
|
150
|
+
removedModules: REMOVED_MODULES
|
|
151
|
+
})`)
|
|
152
|
+
const buildPatchAsset = template.program('module.exports = PATCHES;')
|
|
153
|
+
const bootstrapModules = Array.from(bootstrapFactories.values()).map((moduleFactory) => {
|
|
154
|
+
const {moduleId, factory} = moduleFactory
|
|
155
|
+
return t.objectProperty(t.stringLiteral(moduleId), factory)
|
|
156
|
+
})
|
|
157
|
+
const patchNodes = [
|
|
158
|
+
buildBootstrapPatch({MODULES: t.objectExpression(bootstrapModules)}),
|
|
159
|
+
...recentUpdatePatches.map(patch => {
|
|
160
|
+
const updateModules = patch.moduleFactories.map((moduleFactory) => {
|
|
161
|
+
const {moduleId, factory} = moduleFactory
|
|
162
|
+
return t.objectProperty(t.stringLiteral(moduleId), factory)
|
|
163
|
+
})
|
|
164
|
+
return buildUpdatePatch({
|
|
165
|
+
FROM_HASH: patch.fromHash,
|
|
166
|
+
TO_HASH: patch.toHash,
|
|
167
|
+
MODULES: t.objectExpression(updateModules),
|
|
168
|
+
REMOVED_MODULES: t.arrayExpression(patch.removedModules.map(moduleId => t.stringLiteral(moduleId))),
|
|
169
|
+
})
|
|
170
|
+
}),
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
return generator(buildPatchAsset({PATCHES: t.arrayExpression(patchNodes)})).code
|
|
174
|
+
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { Expression } from '@babel/types';
|
|
2
|
-
export interface BootstrapPatchSource {
|
|
3
|
-
type: 'bootstrap';
|
|
4
|
-
moduleFactories: Map<string, Expression>;
|
|
5
|
-
}
|
|
6
|
-
export interface UpdatePatchSource {
|
|
7
|
-
type: 'update';
|
|
8
|
-
fromHash: string;
|
|
9
|
-
toHash: string;
|
|
10
|
-
moduleFactories: Map<string, Expression>;
|
|
11
|
-
removedModules: string[];
|
|
12
|
-
}
|
|
13
|
-
export type PatchSource = BootstrapPatchSource | UpdatePatchSource;
|
|
14
|
-
/** 仅维护 bootstrap/update Patch 状态、连续性和压缩,不依赖 Webpack 编译对象。 */
|
|
15
|
-
export declare class HmrPatchManager {
|
|
16
|
-
private bootstrapPatch?;
|
|
17
|
-
private compactedUpdatePatch?;
|
|
18
|
-
private readonly recentUpdatePatches;
|
|
19
|
-
private readonly limit;
|
|
20
|
-
constructor(limit: number);
|
|
21
|
-
clear(): void;
|
|
22
|
-
setBootstrap(moduleFactories: Map<string, Expression>): void;
|
|
23
|
-
/** 追加 update;发现 hash 不连续时丢弃旧 update 链,并在追加后执行容量压缩。 */
|
|
24
|
-
appendUpdate(patch: UpdatePatchSource): void;
|
|
25
|
-
/** 按客户端消费顺序返回 bootstrap、压缩 update 与近期 update。 */
|
|
26
|
-
getPatches(): PatchSource[];
|
|
27
|
-
private clearUpdateHistory;
|
|
28
|
-
/** 将超出上限的旧 update 逐步合并到首个 update;bootstrap 永不参与 merge。 */
|
|
29
|
-
private compactUpdateHistory;
|
|
30
|
-
}
|