@taro-hmr-compile/plugin-webpack 0.0.1-alpha.3
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/compile-state.d.ts +3 -0
- package/dist/compile/compile-state.js +21 -0
- package/dist/compile/compile-state.js.map +1 -0
- package/dist/compile/hmr-compile-loader.d.ts +11 -0
- package/dist/compile/hmr-compile-loader.js +52 -0
- package/dist/compile/hmr-compile-loader.js.map +1 -0
- package/dist/compile/hmr-compiler-plugin.d.ts +38 -0
- package/dist/compile/hmr-compiler-plugin.js +158 -0
- package/dist/compile/hmr-compiler-plugin.js.map +1 -0
- package/dist/compile/inject-app-hmr-runtime.d.ts +1 -0
- package/dist/compile/inject-app-hmr-runtime.js +46 -0
- package/dist/compile/inject-app-hmr-runtime.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/patch/hmr-patch-manager.d.ts +30 -0
- package/dist/patch/hmr-patch-manager.js +75 -0
- package/dist/patch/hmr-patch-manager.js.map +1 -0
- package/dist/patch/hmr-patch-plugin.d.ts +16 -0
- package/dist/patch/hmr-patch-plugin.js +239 -0
- package/dist/patch/hmr-patch-plugin.js.map +1 -0
- package/dist/patch/hmr-patch-runtime-plugin.d.ts +4 -0
- package/dist/patch/hmr-patch-runtime-plugin.js +142 -0
- package/dist/patch/hmr-patch-runtime-plugin.js.map +1 -0
- package/dist/server/hmr-server-plugin.d.ts +13 -0
- package/dist/server/hmr-server-plugin.js +29 -0
- package/dist/server/hmr-server-plugin.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +25 -0
- package/dist/utils.js.map +1 -0
- package/package.json +51 -0
- package/src/compile/compile-state.ts +15 -0
- package/src/compile/hmr-compile-loader.ts +72 -0
- package/src/compile/hmr-compiler-plugin.ts +191 -0
- package/src/compile/inject-app-hmr-runtime.ts +44 -0
- package/src/constants.ts +5 -0
- package/src/index.ts +67 -0
- package/src/patch/hmr-patch-manager.ts +102 -0
- package/src/patch/hmr-patch-plugin.ts +237 -0
- package/src/patch/hmr-patch-runtime-plugin.ts +150 -0
- package/src/server/hmr-server-plugin.ts +34 -0
- package/src/utils.ts +25 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import {resolveMainFilePath} from '@tarojs/helper'
|
|
3
|
+
import type {IPaths} from '@tarojs/service'
|
|
4
|
+
import type {AppConfig} from '@tarojs/taro'
|
|
5
|
+
import {SyncHook} from 'tapable'
|
|
6
|
+
import type {Chunk, Compilation, Compiler} from 'webpack'
|
|
7
|
+
import {ConcatSource, RawSource} from 'webpack-sources'
|
|
8
|
+
import type {Source} from 'webpack-sources'
|
|
9
|
+
import {HMR_PATCH_ASSET} from '../constants'
|
|
10
|
+
import {clearCompiledPages, isPageCompiled, markPageCompiled} from './compile-state'
|
|
11
|
+
import {getAllPages} from '../utils'
|
|
12
|
+
|
|
13
|
+
const PLUGIN_NAME = 'HmrCompilerPlugin'
|
|
14
|
+
|
|
15
|
+
type StyleFilename = NonNullable<Compiler['options']['output']['cssFilename']>
|
|
16
|
+
|
|
17
|
+
export interface HmrCompilerPluginOptions {
|
|
18
|
+
paths: IPaths
|
|
19
|
+
appConfig: AppConfig
|
|
20
|
+
styleFilename?: StyleFilename
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface HmrCompilerHooks {
|
|
24
|
+
compileRequested: SyncHook<[page: string]>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface HmrCompilationHooks {
|
|
28
|
+
pageChunkModules: SyncHook<[source: string]>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const applyPatchBeforeStartup = (source: Source, request: string) => {
|
|
32
|
+
const patchRequest = JSON.stringify(`${request}.js`)
|
|
33
|
+
return new ConcatSource(
|
|
34
|
+
`globalThis.miniHmrApplyPatch(require(${patchRequest}));\n`,
|
|
35
|
+
source,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class HmrCompilerPlugin {
|
|
40
|
+
private static readonly compilerHooksMap = new WeakMap<Compiler, HmrCompilerHooks>()
|
|
41
|
+
|
|
42
|
+
private static readonly compilationHooksMap = new WeakMap<Compilation, HmrCompilationHooks>()
|
|
43
|
+
|
|
44
|
+
private compiler?: Compiler
|
|
45
|
+
|
|
46
|
+
private hasCompletedInitialBuild = false
|
|
47
|
+
|
|
48
|
+
// 页面编译请求可能在任意时刻到达。先排队到下一轮 watchRun,避免与正在执行的编译相互覆盖。
|
|
49
|
+
private readonly pendingFiles = new Set<string>()
|
|
50
|
+
|
|
51
|
+
// 当前编译批次的文件快照。与 pendingFiles 分离,确保编译期间新到达的请求留给下一批处理。
|
|
52
|
+
private readonly rebuildingFiles = new Set<string>()
|
|
53
|
+
|
|
54
|
+
constructor(private readonly options: HmrCompilerPluginOptions) {}
|
|
55
|
+
|
|
56
|
+
static getCompilerHooks(compiler: Compiler): HmrCompilerHooks {
|
|
57
|
+
let hooks = HmrCompilerPlugin.compilerHooksMap.get(compiler)
|
|
58
|
+
if (!hooks) {
|
|
59
|
+
hooks = {compileRequested: new SyncHook(['page'])}
|
|
60
|
+
HmrCompilerPlugin.compilerHooksMap.set(compiler, hooks)
|
|
61
|
+
}
|
|
62
|
+
return hooks
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static getCompilationHooks(compilation: Compilation): HmrCompilationHooks {
|
|
66
|
+
let hooks = HmrCompilerPlugin.compilationHooksMap.get(compilation)
|
|
67
|
+
if (!hooks) {
|
|
68
|
+
hooks = {pageChunkModules: new SyncHook(['source'])}
|
|
69
|
+
HmrCompilerPlugin.compilationHooksMap.set(compilation, hooks)
|
|
70
|
+
}
|
|
71
|
+
return hooks
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
apply(compiler: Compiler) {
|
|
75
|
+
this.compiler = compiler
|
|
76
|
+
|
|
77
|
+
HmrCompilerPlugin.getCompilerHooks(compiler).compileRequested.tap(PLUGIN_NAME, page => {
|
|
78
|
+
this.requestCompile(page)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
compiler.hooks.watchRun.tap(PLUGIN_NAME, currentCompiler => {
|
|
82
|
+
if (this.pendingFiles.size === 0) return
|
|
83
|
+
|
|
84
|
+
// 页面是否使用 Shell 由内存中的 compile-state 决定,源文件本身没有发生变化。
|
|
85
|
+
// 因此需要把请求显式加入 modifiedFiles,并保存当前批次供 needBuild 强制重建。
|
|
86
|
+
this.rebuildingFiles.clear()
|
|
87
|
+
this.pendingFiles.forEach(file => this.rebuildingFiles.add(file))
|
|
88
|
+
currentCompiler.modifiedFiles = new Set([...(currentCompiler.modifiedFiles ?? []), ...this.pendingFiles])
|
|
89
|
+
this.pendingFiles.clear()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
|
|
93
|
+
const pages = new Set(getAllPages(this.options.appConfig))
|
|
94
|
+
const isPageChunk = (chunk: Chunk) => this.isPageChunk(compilation, pages, chunk)
|
|
95
|
+
const hmrHooks = HmrCompilerPlugin.getCompilationHooks(compilation)
|
|
96
|
+
const javascriptHooks = compiler.webpack.javascript.JavascriptModulesPlugin.getCompilationHooks(compilation)
|
|
97
|
+
|
|
98
|
+
// 只修改 Page Chunk 的 JS transport。Webpack 的 ModuleGraph / ChunkGraph 保持不变,
|
|
99
|
+
// 因而 hash、HMR diff 和 hot-update 仍以完整模块图为准。
|
|
100
|
+
javascriptHooks.renderChunk.tap({name: PLUGIN_NAME, stage: -10_000}, (source, {chunk}) => {
|
|
101
|
+
if (!isPageChunk(chunk)) return source
|
|
102
|
+
hmrHooks.pageChunkModules.call(source.source().toString())
|
|
103
|
+
return new RawSource('{}')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
javascriptHooks.renderStartup.tap(PLUGIN_NAME, (source, _module, {chunk}) => {
|
|
107
|
+
if (!isPageChunk(chunk)) return source
|
|
108
|
+
|
|
109
|
+
const filenameTemplate = compiler.webpack.javascript.JavascriptModulesPlugin
|
|
110
|
+
.getChunkFilenameTemplate(chunk, compilation.outputOptions)
|
|
111
|
+
const assetName = compilation.getPath(filenameTemplate, {
|
|
112
|
+
chunk,
|
|
113
|
+
contentHashType: 'javascript',
|
|
114
|
+
hash: compilation.hash,
|
|
115
|
+
})
|
|
116
|
+
const patchModule = HMR_PATCH_ASSET.replace(/\.js$/, '')
|
|
117
|
+
const relativePath = path.posix.relative(path.posix.dirname(assetName), patchModule)
|
|
118
|
+
const request = relativePath.startsWith('.') ? relativePath : `./${relativePath}`
|
|
119
|
+
|
|
120
|
+
// 微信要求 Page() 在当前页面脚本的同步执行阶段完成。bootstrap 会同步补齐工厂,
|
|
121
|
+
// update 继续异步排队,因此这里不能用 Promise.then 包裹 Webpack startup。
|
|
122
|
+
return applyPatchBeforeStartup(source, request)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
compiler.webpack.NormalModule.getCompilationHooks(compilation).needBuild.tapAsync(
|
|
126
|
+
PLUGIN_NAME,
|
|
127
|
+
(module, _context, callback) => {
|
|
128
|
+
const [resourcePath] = module.resource?.split('?') ?? []
|
|
129
|
+
// 绕过 Webpack 的模块缓存,让 loader 根据最新 compile-state 将页面 Shell 替换为真实页面。
|
|
130
|
+
if (resourcePath && this.rebuildingFiles.has(resourcePath)) return callback(null, true)
|
|
131
|
+
callback()
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
const stage = compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT
|
|
136
|
+
compilation.hooks.processAssets.tap({name: PLUGIN_NAME, stage}, () => {
|
|
137
|
+
if (!this.hasCompletedInitialBuild && compilation.hash && compilation.errors.length === 0) {
|
|
138
|
+
this.emitMissingPageStyleAssets(compilation)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
compiler.hooks.afterEmit.tap(PLUGIN_NAME, compilation => {
|
|
144
|
+
const isSuccessful = compilation.errors.length === 0 && Boolean(compilation.hash)
|
|
145
|
+
this.hasCompletedInitialBuild = this.hasCompletedInitialBuild || isSuccessful
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// 当前批次完成后释放快照;编译期间新进入 pendingFiles 的请求不会被清理。
|
|
149
|
+
compiler.hooks.done.tap(PLUGIN_NAME, () => this.rebuildingFiles.clear())
|
|
150
|
+
|
|
151
|
+
compiler.hooks.watchClose.tap(PLUGIN_NAME, () => this.reset())
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private requestCompile(page: string) {
|
|
155
|
+
try {
|
|
156
|
+
if (isPageCompiled(this.options.paths.appPath, page)) return
|
|
157
|
+
// loader 在下一轮构建中读取该状态;必须先标记,再触发 watch invalidation。
|
|
158
|
+
markPageCompiled(this.options.paths.appPath, page)
|
|
159
|
+
this.pendingFiles.add(resolveMainFilePath(path.resolve(this.options.paths.sourcePath, page)))
|
|
160
|
+
this.compiler?.watching?.invalidate()
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('[taro-hmr-compile] 页面编译触发失败', error)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private isPageChunk(compilation: Compilation, pages: Set<string>, chunk: Chunk) {
|
|
167
|
+
if (chunk.name && pages.has(chunk.name)) return true
|
|
168
|
+
return Array.from(pages).some(page => compilation.entrypoints.get(page)?.getEntrypointChunk() === chunk)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** PageShell 不包含真实页面的样式依赖,为缺失的页面样式补充空资产。 */
|
|
172
|
+
private emitMissingPageStyleAssets(compilation: Compilation) {
|
|
173
|
+
const {appConfig, styleFilename} = this.options
|
|
174
|
+
if (!styleFilename) return
|
|
175
|
+
|
|
176
|
+
getAllPages(appConfig).forEach(page => {
|
|
177
|
+
const chunk = compilation.entrypoints.get(page)?.getEntrypointChunk()
|
|
178
|
+
if (!chunk) return
|
|
179
|
+
|
|
180
|
+
const assetName = compilation.getPath(styleFilename, {chunk, contentHashType: 'css/mini-extract'})
|
|
181
|
+
if (!compilation.getAsset(assetName)) compilation.emitAsset(assetName, new RawSource(''))
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private reset() {
|
|
186
|
+
clearCompiledPages(this.options.paths.appPath)
|
|
187
|
+
this.hasCompletedInitialBuild = false
|
|
188
|
+
this.rebuildingFiles.clear()
|
|
189
|
+
this.pendingFiles.clear()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import template from '@babel/template'
|
|
2
|
+
import traverse from '@babel/traverse'
|
|
3
|
+
import generator from '@babel/generator'
|
|
4
|
+
import parser from '@babel/parser'
|
|
5
|
+
import webpack from "webpack";
|
|
6
|
+
|
|
7
|
+
export const injectAppHmrRuntime = (source: string) => {
|
|
8
|
+
const ast = parser.parse(source, {sourceType: 'module', plugins: ['typescript', 'jsx']})
|
|
9
|
+
|
|
10
|
+
const buildHmrCode = template.statements(`
|
|
11
|
+
import { mergePatches, injectHmrLifecycle } from '@taro-hmr-compile/core/runtime'
|
|
12
|
+
injectHmrLifecycle()
|
|
13
|
+
globalThis.miniHmrApplyPatch = (patches)=>{
|
|
14
|
+
const bootstrapPatch = patches.find(patch => patch.type === 'bootstrap')
|
|
15
|
+
return __webpack_require__.miniHmr.applyPatch(bootstrapPatch).then(() => {
|
|
16
|
+
const updatePatch = mergePatches(${webpack.RuntimeGlobals.getFullHash}(), patches)
|
|
17
|
+
return __webpack_require__.miniHmr.applyPatch(updatePatch)
|
|
18
|
+
}).catch(error => {
|
|
19
|
+
console.error('[taro-hmr-compile] patch apply failed', error)
|
|
20
|
+
})
|
|
21
|
+
};
|
|
22
|
+
`, {placeholderPattern: false})
|
|
23
|
+
|
|
24
|
+
traverse(ast, {
|
|
25
|
+
Program(path) {
|
|
26
|
+
const body = path.get('body')
|
|
27
|
+
|
|
28
|
+
const hasInjected = body.some(item => {
|
|
29
|
+
return item.isImportDeclaration() && item.node.source.value === '@taro-hmr-compile/core/runtime'
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
if (hasInjected) return
|
|
33
|
+
|
|
34
|
+
const nodes = buildHmrCode()
|
|
35
|
+
|
|
36
|
+
const lastImport = [...body].reverse().find(item => item.isImportDeclaration())
|
|
37
|
+
|
|
38
|
+
if (lastImport) lastImport.insertAfter(nodes)
|
|
39
|
+
|
|
40
|
+
if (!lastImport) path.unshiftContainer('body', nodes)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
return generator(ast).code
|
|
44
|
+
}
|
package/src/constants.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type {IPluginContext} from '@tarojs/service'
|
|
2
|
+
import type {AppConfig} from '@tarojs/taro'
|
|
3
|
+
import {HMR_OUTPUT_PATH} from './constants'
|
|
4
|
+
import {getAppConfig} from './utils'
|
|
5
|
+
import {HmrCompilerPlugin} from './compile/hmr-compiler-plugin'
|
|
6
|
+
import {HmrPatchPlugin} from './patch/hmr-patch-plugin'
|
|
7
|
+
import {HmrServerPlugin} from './server/hmr-server-plugin'
|
|
8
|
+
import {setupReactRefresh} from '@taro-hmr-compile/react-refresh/compiler'
|
|
9
|
+
import {emptyDirFile} from '@taro-hmr-compile/core/compiler'
|
|
10
|
+
|
|
11
|
+
export interface WebpackHmrCompileOpt {
|
|
12
|
+
host: string
|
|
13
|
+
port: number
|
|
14
|
+
patchHistoryLimit: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default (ctx: IPluginContext, pluginOpts: Partial<WebpackHmrCompileOpt>) => {
|
|
18
|
+
if (!ctx.runOpts.options?.isWatch) return
|
|
19
|
+
|
|
20
|
+
const {appPath, sourcePath, outputPath} = ctx.paths
|
|
21
|
+
|
|
22
|
+
const {framework} = ctx.initialConfig
|
|
23
|
+
|
|
24
|
+
const appConfig: AppConfig = getAppConfig(ctx)
|
|
25
|
+
|
|
26
|
+
const host = pluginOpts.host ?? '127.0.0.1'
|
|
27
|
+
const port = pluginOpts.port ?? 8080
|
|
28
|
+
const compileUrl = `http://${host}:${port}/compile`
|
|
29
|
+
|
|
30
|
+
setupReactRefresh(ctx)
|
|
31
|
+
|
|
32
|
+
ctx.onBuildStart(() => emptyDirFile(outputPath))
|
|
33
|
+
|
|
34
|
+
ctx.modifyRunnerOpts(({ opts }) => {
|
|
35
|
+
opts.output ??= {}
|
|
36
|
+
opts.output.clean = false
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
ctx.modifyWebpackChain(({chain, webpack}) => {
|
|
40
|
+
|
|
41
|
+
chain.module
|
|
42
|
+
.rule('hmr-compile')
|
|
43
|
+
.test(/\.[jt]sx?$/)
|
|
44
|
+
.include.add(ctx.paths.sourcePath)
|
|
45
|
+
.end()
|
|
46
|
+
.use('hmr-compile-loader')
|
|
47
|
+
.loader(require.resolve('./compile/hmr-compile-loader'))
|
|
48
|
+
.options({appPath, sourcePath, appConfig, framework, compileUrl})
|
|
49
|
+
.end()
|
|
50
|
+
|
|
51
|
+
chain.output
|
|
52
|
+
.set('hotUpdateChunkFilename', `${HMR_OUTPUT_PATH}/[id].[fullhash].hot-update.js`)
|
|
53
|
+
.set('hotUpdateMainFilename', `${HMR_OUTPUT_PATH}/[runtime].[fullhash].hot-update.json`)
|
|
54
|
+
|
|
55
|
+
const {patchHistoryLimit} = pluginOpts
|
|
56
|
+
|
|
57
|
+
const [{filename: styleFilename}] = chain.plugin('miniCssExtractPlugin')?.get('args') || []
|
|
58
|
+
|
|
59
|
+
chain.plugin('hmr-compiler-plugin').use(HmrCompilerPlugin, [{paths: ctx.paths, appConfig, styleFilename}])
|
|
60
|
+
|
|
61
|
+
chain.plugin('hmr-plugin').use(webpack.HotModuleReplacementPlugin)
|
|
62
|
+
|
|
63
|
+
chain.plugin('hmr-patch-plugin').use(HmrPatchPlugin, [{patchHistoryLimit}])
|
|
64
|
+
|
|
65
|
+
chain.plugin('hmr-server-plugin').use(HmrServerPlugin, [{host, port}])
|
|
66
|
+
})
|
|
67
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type {Expression} from '@babel/types'
|
|
2
|
+
|
|
3
|
+
export interface BootstrapPatchSource {
|
|
4
|
+
type: 'bootstrap'
|
|
5
|
+
moduleFactories: Map<string, Expression>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface UpdatePatchSource {
|
|
9
|
+
type: 'update'
|
|
10
|
+
fromHash: string
|
|
11
|
+
toHash: string
|
|
12
|
+
moduleFactories: Map<string, Expression>
|
|
13
|
+
removedModules: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type PatchSource = BootstrapPatchSource | UpdatePatchSource
|
|
17
|
+
|
|
18
|
+
/** 合并两个连续 update,并按后一份 update 的最终模块状态解决修改和删除冲突。 */
|
|
19
|
+
const mergeUpdatePatches = (basePatch: UpdatePatchSource, nextPatch: UpdatePatchSource): UpdatePatchSource => {
|
|
20
|
+
if (basePatch.toHash !== nextPatch.fromHash) {
|
|
21
|
+
throw new Error(`[taro-hmr-compile] 无法合并不连续的 HMR patch:${basePatch.toHash} !== ${nextPatch.fromHash}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const moduleFactories = new Map(basePatch.moduleFactories)
|
|
25
|
+
const removedModules = new Set(basePatch.removedModules)
|
|
26
|
+
|
|
27
|
+
nextPatch.moduleFactories.forEach((factory, moduleId) => {
|
|
28
|
+
moduleFactories.set(moduleId, factory)
|
|
29
|
+
removedModules.delete(moduleId)
|
|
30
|
+
})
|
|
31
|
+
nextPatch.removedModules.forEach(moduleId => {
|
|
32
|
+
moduleFactories.delete(moduleId)
|
|
33
|
+
removedModules.add(moduleId)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
type: 'update',
|
|
38
|
+
fromHash: basePatch.fromHash,
|
|
39
|
+
toHash: nextPatch.toHash,
|
|
40
|
+
moduleFactories,
|
|
41
|
+
removedModules: Array.from(removedModules),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 仅维护 bootstrap/update Patch 状态、连续性和压缩,不依赖 Webpack 编译对象。 */
|
|
46
|
+
export class HmrPatchManager {
|
|
47
|
+
private bootstrapPatch?: BootstrapPatchSource
|
|
48
|
+
|
|
49
|
+
private compactedUpdatePatch?: UpdatePatchSource
|
|
50
|
+
|
|
51
|
+
private readonly recentUpdatePatches: UpdatePatchSource[] = []
|
|
52
|
+
|
|
53
|
+
private readonly limit: number
|
|
54
|
+
|
|
55
|
+
constructor(limit: number) {
|
|
56
|
+
this.limit = Math.max(1, limit)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
clear() {
|
|
60
|
+
this.bootstrapPatch = undefined
|
|
61
|
+
this.clearUpdateHistory()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setBootstrap(moduleFactories: Map<string, Expression>) {
|
|
65
|
+
this.bootstrapPatch = moduleFactories.size > 0
|
|
66
|
+
? {type: 'bootstrap', moduleFactories: new Map(moduleFactories)}
|
|
67
|
+
: undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 追加 update;发现 hash 不连续时丢弃旧 update 链,并在追加后执行容量压缩。 */
|
|
71
|
+
appendUpdate(patch: UpdatePatchSource) {
|
|
72
|
+
const latestPatch = this.recentUpdatePatches[this.recentUpdatePatches.length - 1] ?? this.compactedUpdatePatch
|
|
73
|
+
if (latestPatch && latestPatch.toHash !== patch.fromHash) this.clearUpdateHistory()
|
|
74
|
+
this.recentUpdatePatches.push(patch)
|
|
75
|
+
this.compactUpdateHistory()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 按客户端消费顺序返回 bootstrap、压缩 update 与近期 update。 */
|
|
79
|
+
getPatches(): PatchSource[] {
|
|
80
|
+
const updatePatches = this.compactedUpdatePatch
|
|
81
|
+
? [this.compactedUpdatePatch, ...this.recentUpdatePatches]
|
|
82
|
+
: this.recentUpdatePatches
|
|
83
|
+
return this.bootstrapPatch
|
|
84
|
+
? [this.bootstrapPatch, ...updatePatches]
|
|
85
|
+
: updatePatches
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private clearUpdateHistory() {
|
|
89
|
+
this.compactedUpdatePatch = undefined
|
|
90
|
+
this.recentUpdatePatches.length = 0
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 将超出上限的旧 update 逐步合并到首个 update;bootstrap 永不参与 merge。 */
|
|
94
|
+
private compactUpdateHistory() {
|
|
95
|
+
while (this.recentUpdatePatches.length > this.limit) {
|
|
96
|
+
const oldestUpdatePatch = this.recentUpdatePatches.shift()!
|
|
97
|
+
this.compactedUpdatePatch = this.compactedUpdatePatch
|
|
98
|
+
? mergeUpdatePatches(this.compactedUpdatePatch, oldestUpdatePatch)
|
|
99
|
+
: oldestUpdatePatch
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
import type {Compilation, Compiler} from 'webpack'
|
|
7
|
+
import {RawSource} from 'webpack-sources'
|
|
8
|
+
import {DEFAULT_HMR_PATCH_HISTORY_LIMIT, HMR_PATCH_ASSET} from '../constants'
|
|
9
|
+
import {HmrCompilerPlugin} from '../compile/hmr-compiler-plugin'
|
|
10
|
+
import {HmrPatchManager} from '../patch/hmr-patch-manager'
|
|
11
|
+
import type {PatchSource, UpdatePatchSource} from '../patch/hmr-patch-manager'
|
|
12
|
+
import {HmrPatchRuntimePlugin} from './hmr-patch-runtime-plugin'
|
|
13
|
+
|
|
14
|
+
const PLUGIN_NAME = 'HmrPatchPlugin'
|
|
15
|
+
const HOT_UPDATE_MANIFEST_SUFFIX = '.hot-update.json'
|
|
16
|
+
const HOT_UPDATE_SCRIPT_SUFFIX = '.hot-update.js'
|
|
17
|
+
const HOT_UPDATE_HASH_PATTERN = /\.([0-9a-f]+)\.hot-update\.json$/i
|
|
18
|
+
const STYLE_ASSET_PATTERN = /\.(?:css|wxss|acss|ttss|qss|jxss)(?:\.map)?$/i
|
|
19
|
+
const TEMPLATE_RUNTIME_ASSET_PATTERN = /(?:^|\/)(?:base\.wxml|utils\.wxs)$/i
|
|
20
|
+
|
|
21
|
+
interface HotUpdateManifest {
|
|
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
|
+
}
|
|
140
|
+
|
|
141
|
+
const isPassthroughAsset = (assetName: string) => {
|
|
142
|
+
return STYLE_ASSET_PATTERN.test(assetName) || TEMPLATE_RUNTIME_ASSET_PATTERN.test(assetName)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export class HmrPatchPlugin {
|
|
146
|
+
private hasCompletedInitialBuild = false
|
|
147
|
+
|
|
148
|
+
private readonly patchManager: HmrPatchManager
|
|
149
|
+
|
|
150
|
+
private readonly emittedAssetSources = new Map<string, string>()
|
|
151
|
+
|
|
152
|
+
constructor(options: HmrPatchPluginOptions) {
|
|
153
|
+
this.patchManager = new HmrPatchManager(options.patchHistoryLimit ?? DEFAULT_HMR_PATCH_HISTORY_LIMIT)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
apply(compiler: Compiler) {
|
|
157
|
+
new HmrPatchRuntimePlugin().apply(compiler)
|
|
158
|
+
|
|
159
|
+
compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
|
|
160
|
+
const initialPageFactories = new Map<string, t.Expression>()
|
|
161
|
+
|
|
162
|
+
HmrCompilerPlugin.getCompilationHooks(compilation).pageChunkModules.tap(PLUGIN_NAME, source => {
|
|
163
|
+
collectModuleFactories(source, initialPageFactories)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// 等 Webpack 生成完整的 hot-update assets 后,再统一转换成固定的 patch.js。
|
|
167
|
+
const stage = compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT
|
|
168
|
+
compilation.hooks.processAssets.tap({name: PLUGIN_NAME, stage}, () => {
|
|
169
|
+
this.processAssets(compilation, initialPageFactories)
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
compiler.hooks.afterEmit.tap(PLUGIN_NAME, compilation => {
|
|
174
|
+
const isSuccessful = compilation.errors.length === 0 && Boolean(compilation.hash)
|
|
175
|
+
this.hasCompletedInitialBuild = this.hasCompletedInitialBuild || isSuccessful
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
compiler.hooks.watchClose.tap(PLUGIN_NAME, () => this.reset())
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private processAssets(compilation: Compilation, initialPageFactories: Map<string, t.Expression>) {
|
|
182
|
+
if (!compilation.hash || compilation.errors.length > 0) return
|
|
183
|
+
|
|
184
|
+
const originalAssets = compilation.getAssets()
|
|
185
|
+
if (!this.hasCompletedInitialBuild) {
|
|
186
|
+
this.patchManager.clear()
|
|
187
|
+
this.emittedAssetSources.clear()
|
|
188
|
+
originalAssets.forEach(asset => {
|
|
189
|
+
if (isPassthroughAsset(asset.name)) {
|
|
190
|
+
this.emittedAssetSources.set(asset.name, asset.source.source().toString())
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
this.patchManager.setBootstrap(initialPageFactories)
|
|
194
|
+
this.emitPatchAsset(compilation, false)
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const hotUpdateAssets: HotUpdateAssets = {
|
|
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
|
+
}
|
|
202
|
+
|
|
203
|
+
if (hotUpdateAssets.manifests.length) {
|
|
204
|
+
this.patchManager.appendUpdate(createUpdatePatch(compilation.hash, hotUpdateAssets))
|
|
205
|
+
this.emitPatchAsset(compilation, true)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 增量 compilation 仍可能包含普通 chunk 资产,不能让它们覆盖初始 PageShell。
|
|
209
|
+
this.removeSupersededAssets(compilation, originalAssets)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 热更新转换完成后,保留固定 patch 与发生变化的模板、平台样式资产。 */
|
|
213
|
+
private removeSupersededAssets(compilation: Compilation, assets: CompilationAsset[]) {
|
|
214
|
+
assets.forEach(asset => {
|
|
215
|
+
if (asset.name === HMR_PATCH_ASSET) return
|
|
216
|
+
if (!isPassthroughAsset(asset.name)) return compilation.deleteAsset(asset.name)
|
|
217
|
+
|
|
218
|
+
const source = asset.source.source().toString()
|
|
219
|
+
const previousSource = this.emittedAssetSources.get(asset.name)
|
|
220
|
+
this.emittedAssetSources.set(asset.name, source)
|
|
221
|
+
if (previousSource === source) compilation.deleteAsset(asset.name)
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private emitPatchAsset(compilation: Compilation, isHotUpdate: boolean) {
|
|
226
|
+
const source = new RawSource(createPatchAssetSource(this.patchManager.getPatches()))
|
|
227
|
+
const assetInfo = {hotModuleReplacement: isHotUpdate}
|
|
228
|
+
if (compilation.getAsset(HMR_PATCH_ASSET)) compilation.updateAsset(HMR_PATCH_ASSET, source, assetInfo)
|
|
229
|
+
else compilation.emitAsset(HMR_PATCH_ASSET, source, assetInfo)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private reset() {
|
|
233
|
+
this.hasCompletedInitialBuild = false
|
|
234
|
+
this.emittedAssetSources.clear()
|
|
235
|
+
this.patchManager.clear()
|
|
236
|
+
}
|
|
237
|
+
}
|