@taro-hmr-compile/plugin-webpack 0.0.1-alpha.10
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-compiler-loader.d.ts +9 -0
- package/dist/compile/hmr-compiler-loader.js +199 -0
- package/dist/compile/hmr-compiler-loader.js.map +1 -0
- package/dist/compile/hmr-compiler-plugin.d.ts +46 -0
- package/dist/compile/hmr-compiler-plugin.js +173 -0
- package/dist/compile/hmr-compiler-plugin.js.map +1 -0
- package/dist/compile/lazy-compiler-loader.d.ts +18 -0
- package/dist/compile/lazy-compiler-loader.js +38 -0
- package/dist/compile/lazy-compiler-loader.js.map +1 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +8 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/log.d.ts +3 -0
- package/dist/log.js +11 -0
- package/dist/log.js.map +1 -0
- package/dist/patch/hmr-patch-plugin.d.ts +17 -0
- package/dist/patch/hmr-patch-plugin.js +93 -0
- package/dist/patch/hmr-patch-plugin.js.map +1 -0
- package/dist/patch/hmr-patch-processor.d.ts +30 -0
- package/dist/patch/hmr-patch-processor.js +122 -0
- package/dist/patch/hmr-patch-processor.js.map +1 -0
- package/dist/patch/hmr-patch-runtime-plugin.d.ts +4 -0
- package/dist/patch/hmr-patch-runtime-plugin.js +113 -0
- package/dist/patch/hmr-patch-runtime-plugin.js.map +1 -0
- package/dist/patch/types.d.ts +16 -0
- package/dist/patch/types.js +3 -0
- package/dist/patch/types.js.map +1 -0
- package/dist/patch/utils.d.ts +28 -0
- package/dist/patch/utils.js +179 -0
- package/dist/patch/utils.js.map +1 -0
- package/dist/server/hmr-server-plugin.d.ts +13 -0
- package/dist/server/hmr-server-plugin.js +42 -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 +58 -0
- package/src/compile/compile-state.ts +15 -0
- package/src/compile/hmr-compiler-loader.ts +177 -0
- package/src/compile/hmr-compiler-plugin.ts +233 -0
- package/src/compile/lazy-compiler-loader.ts +57 -0
- package/src/constants.ts +7 -0
- package/src/index.ts +81 -0
- package/src/log.ts +5 -0
- package/src/patch/hmr-patch-plugin.ts +111 -0
- package/src/patch/hmr-patch-processor.ts +169 -0
- package/src/patch/hmr-patch-runtime-plugin.ts +119 -0
- package/src/patch/types.ts +20 -0
- package/src/patch/utils.ts +179 -0
- package/src/server/hmr-server-plugin.ts +40 -0
- package/src/utils.ts +25 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,233 @@
|
|
|
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
|
+
import {log} from "../log";
|
|
13
|
+
|
|
14
|
+
const compilerLog = log.extend('HmrCompilerPlugin')
|
|
15
|
+
|
|
16
|
+
const PLUGIN_NAME = 'HmrCompilerPlugin'
|
|
17
|
+
|
|
18
|
+
interface TaroNormalModule {
|
|
19
|
+
miniType?: string
|
|
20
|
+
loaders: Array<{ loader: string; options?: unknown }>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type StyleFilename = NonNullable<Compiler['options']['output']['cssFilename']>
|
|
24
|
+
|
|
25
|
+
export interface HmrCompilerPluginOptions {
|
|
26
|
+
paths: IPaths
|
|
27
|
+
appConfig: AppConfig
|
|
28
|
+
styleFilename?: StyleFilename
|
|
29
|
+
debugNamespaces?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type CompileRequested = SyncHook<[page: string]>
|
|
33
|
+
|
|
34
|
+
interface ChunkModulesRenderedOptions {
|
|
35
|
+
source: string
|
|
36
|
+
isBootstrapChunk: boolean
|
|
37
|
+
chunk: Chunk
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type ChunkModulesRendered = SyncHook<[options: ChunkModulesRenderedOptions]>
|
|
41
|
+
|
|
42
|
+
export interface HmrCompilerHooks {
|
|
43
|
+
compileRequested: CompileRequested
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface HmrCompilationHooks {
|
|
47
|
+
chunkModulesRendered: ChunkModulesRendered
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const applyPatchBeforeStartup = (source: Source, request: string) => {
|
|
51
|
+
const patchRequest = JSON.stringify(`${request}.js`)
|
|
52
|
+
return new ConcatSource(
|
|
53
|
+
`globalThis.miniHmrApplyPatch(require(${patchRequest}));\n`,
|
|
54
|
+
source,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class HmrCompilerPlugin {
|
|
59
|
+
private static readonly compilerHooksMap = new WeakMap<Compiler, HmrCompilerHooks>()
|
|
60
|
+
|
|
61
|
+
private static readonly compilationHooksMap = new WeakMap<Compilation, HmrCompilationHooks>()
|
|
62
|
+
|
|
63
|
+
private compiler?: Compiler
|
|
64
|
+
|
|
65
|
+
private hasCompletedInitialBuild = false
|
|
66
|
+
|
|
67
|
+
// 页面编译请求可能在任意时刻到达。先排队到下一轮 watchRun,避免与正在执行的编译相互覆盖。
|
|
68
|
+
private readonly pendingFiles = new Set<string>()
|
|
69
|
+
|
|
70
|
+
// 当前编译批次的文件快照。与 pendingFiles 分离,确保编译期间新到达的请求留给下一批处理。
|
|
71
|
+
private readonly rebuildingFiles = new Set<string>()
|
|
72
|
+
|
|
73
|
+
private readonly options: HmrCompilerPluginOptions
|
|
74
|
+
|
|
75
|
+
constructor(options: HmrCompilerPluginOptions) {
|
|
76
|
+
this.options = options
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static getCompilerHooks(compiler: Compiler): HmrCompilerHooks {
|
|
80
|
+
if (!HmrCompilerPlugin.compilerHooksMap.get(compiler)) {
|
|
81
|
+
const compileRequested: CompileRequested = new SyncHook(['page'])
|
|
82
|
+
HmrCompilerPlugin.compilerHooksMap.set(compiler, {compileRequested})
|
|
83
|
+
}
|
|
84
|
+
return HmrCompilerPlugin.compilerHooksMap.get(compiler)!
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static getCompilationHooks(compilation: Compilation): HmrCompilationHooks {
|
|
88
|
+
if (!HmrCompilerPlugin.compilationHooksMap.get(compilation)) {
|
|
89
|
+
const chunkModulesRendered: ChunkModulesRendered = new SyncHook(['options'])
|
|
90
|
+
HmrCompilerPlugin.compilationHooksMap.set(compilation, {chunkModulesRendered})
|
|
91
|
+
}
|
|
92
|
+
return HmrCompilerPlugin.compilationHooksMap.get(compilation)!
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
apply(compiler: Compiler) {
|
|
96
|
+
this.compiler = compiler
|
|
97
|
+
|
|
98
|
+
HmrCompilerPlugin.getCompilerHooks(compiler).compileRequested.tap(PLUGIN_NAME, page => {
|
|
99
|
+
this.requestCompile(page)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
compiler.hooks.watchRun.tap(PLUGIN_NAME, currentCompiler => {
|
|
103
|
+
if (this.pendingFiles.size === 0) return
|
|
104
|
+
|
|
105
|
+
// 页面是否使用 Shell 由内存中的 compile-state 决定,源文件本身没有发生变化。
|
|
106
|
+
// 因此需要把请求显式加入 modifiedFiles,并保存当前批次供 needBuild 强制重建。
|
|
107
|
+
this.rebuildingFiles.clear()
|
|
108
|
+
this.pendingFiles.forEach(file => this.rebuildingFiles.add(file))
|
|
109
|
+
currentCompiler.modifiedFiles = new Set([...(currentCompiler.modifiedFiles ?? []), ...this.pendingFiles])
|
|
110
|
+
this.pendingFiles.clear()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
|
|
114
|
+
const pages = new Set(getAllPages(this.options.appConfig))
|
|
115
|
+
const isPageChunk = (chunk: Chunk) => this.isPageChunk(compilation, pages, chunk)
|
|
116
|
+
const hmrHooks = HmrCompilerPlugin.getCompilationHooks(compilation)
|
|
117
|
+
const javascriptHooks = compiler.webpack.javascript.JavascriptModulesPlugin.getCompilationHooks(compilation)
|
|
118
|
+
|
|
119
|
+
const HMR_COMPILER_LOADER = require.resolve('./hmr-compiler-loader')
|
|
120
|
+
|
|
121
|
+
const TARO_LOADER = '@tarojs/taro-loader'
|
|
122
|
+
|
|
123
|
+
// 尽量晚执行
|
|
124
|
+
compiler.webpack.NormalModule.getCompilationHooks(compilation).loader.tap({name: PLUGIN_NAME, stage: 10_000}, (_loaderContext, normalModule: TaroNormalModule) => {
|
|
125
|
+
if (!normalModule.miniType) return
|
|
126
|
+
|
|
127
|
+
if (normalModule.loaders.some(item => item.loader === HMR_COMPILER_LOADER)) return
|
|
128
|
+
|
|
129
|
+
const taroLoaderIndex = normalModule.loaders.findIndex(item => item.loader.replace(/\\/g, '/').includes(TARO_LOADER))
|
|
130
|
+
|
|
131
|
+
if (taroLoaderIndex < 0) return
|
|
132
|
+
|
|
133
|
+
const loaderOptions = {miniType: normalModule.miniType, debugNamespaces: this.options.debugNamespaces}
|
|
134
|
+
// Loader 从右向左执行;插到 taro-loader 左侧才能处理它生成的入口注册代码。
|
|
135
|
+
normalModule.loaders.splice(taroLoaderIndex, 0, {loader: HMR_COMPILER_LOADER, options: loaderOptions})
|
|
136
|
+
},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// 只修改 Page Chunk 的 JS transport。Webpack 的 ModuleGraph / ChunkGraph 保持不变,
|
|
140
|
+
// 因而 hash、HMR diff 和 hot-update 仍以完整模块图为准。
|
|
141
|
+
javascriptHooks.renderChunk.tap({name: PLUGIN_NAME, stage: -10_000}, (source, {chunk}) => {
|
|
142
|
+
const isBootstrapChunk = isPageChunk(chunk)
|
|
143
|
+
hmrHooks.chunkModulesRendered.call({source: source.source().toString(), isBootstrapChunk, chunk})
|
|
144
|
+
return isBootstrapChunk ? new RawSource('{}') : source
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
javascriptHooks.renderStartup.tap(PLUGIN_NAME, (source, _module, {chunk}) => {
|
|
148
|
+
if (!isPageChunk(chunk)) return source
|
|
149
|
+
|
|
150
|
+
const filenameTemplate = compiler.webpack.javascript.JavascriptModulesPlugin
|
|
151
|
+
.getChunkFilenameTemplate(chunk, compilation.outputOptions)
|
|
152
|
+
const assetName = compilation.getPath(filenameTemplate, {
|
|
153
|
+
chunk,
|
|
154
|
+
contentHashType: 'javascript',
|
|
155
|
+
hash: compilation.hash,
|
|
156
|
+
})
|
|
157
|
+
const patchModule = HMR_PATCH_ASSET.replace(/\.js$/, '')
|
|
158
|
+
const relativePath = path.posix.relative(path.posix.dirname(assetName), patchModule)
|
|
159
|
+
const request = relativePath.startsWith('.') ? relativePath : `./${relativePath}`
|
|
160
|
+
|
|
161
|
+
// 微信要求 Page() 在当前页面脚本的同步执行阶段完成。bootstrap 会同步补齐工厂,
|
|
162
|
+
// update 继续异步排队,因此这里不能用 Promise.then 包裹 Webpack startup。
|
|
163
|
+
return applyPatchBeforeStartup(source, request)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
compiler.webpack.NormalModule.getCompilationHooks(compilation).needBuild.tapAsync(
|
|
167
|
+
PLUGIN_NAME,
|
|
168
|
+
(module, _context, callback) => {
|
|
169
|
+
const [resourcePath] = module.resource?.split('?') ?? []
|
|
170
|
+
// 绕过 Webpack 的模块缓存,让 loader 根据最新 compile-state 将页面 Shell 替换为真实页面。
|
|
171
|
+
if (resourcePath && this.rebuildingFiles.has(resourcePath)) return callback(null, true)
|
|
172
|
+
callback()
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
const stage = compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT
|
|
177
|
+
compilation.hooks.processAssets.tap({name: PLUGIN_NAME, stage}, () => {
|
|
178
|
+
if (!this.hasCompletedInitialBuild && compilation.hash && compilation.errors.length === 0) {
|
|
179
|
+
this.emitMissingPageStyleAssets(compilation)
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
compiler.hooks.afterEmit.tap(PLUGIN_NAME, compilation => {
|
|
185
|
+
const isSuccessful = compilation.errors.length === 0 && Boolean(compilation.hash)
|
|
186
|
+
this.hasCompletedInitialBuild = this.hasCompletedInitialBuild || isSuccessful
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// 当前批次完成后释放快照;编译期间新进入 pendingFiles 的请求不会被清理。
|
|
190
|
+
compiler.hooks.done.tap(PLUGIN_NAME, () => this.rebuildingFiles.clear())
|
|
191
|
+
|
|
192
|
+
compiler.hooks.watchClose.tap(PLUGIN_NAME, () => this.reset())
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private requestCompile(page: string) {
|
|
196
|
+
try {
|
|
197
|
+
if (isPageCompiled(this.options.paths.appPath, page)) return
|
|
198
|
+
// loader 在下一轮构建中读取该状态;必须先标记,再触发 watch invalidation。
|
|
199
|
+
compilerLog('start compile %s', page)
|
|
200
|
+
markPageCompiled(this.options.paths.appPath, page)
|
|
201
|
+
this.pendingFiles.add(resolveMainFilePath(path.resolve(this.options.paths.sourcePath, page)))
|
|
202
|
+
this.compiler?.watching?.invalidate()
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error('[taro-hmr-compile] 页面编译触发失败', error)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private isPageChunk(compilation: Compilation, pages: Set<string>, chunk: Chunk) {
|
|
209
|
+
if (chunk.name && pages.has(chunk.name)) return true
|
|
210
|
+
return Array.from(pages).some(page => compilation.entrypoints.get(page)?.getEntrypointChunk() === chunk)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** PageShell 不包含真实页面的样式依赖,为缺失的页面样式补充空资产。 */
|
|
214
|
+
private emitMissingPageStyleAssets(compilation: Compilation) {
|
|
215
|
+
const {appConfig, styleFilename} = this.options
|
|
216
|
+
if (!styleFilename) return
|
|
217
|
+
|
|
218
|
+
getAllPages(appConfig).forEach(page => {
|
|
219
|
+
const chunk = compilation.entrypoints.get(page)?.getEntrypointChunk()
|
|
220
|
+
if (!chunk) return
|
|
221
|
+
|
|
222
|
+
const assetName = compilation.getPath(styleFilename, {chunk, contentHashType: 'css/mini-extract'})
|
|
223
|
+
if (!compilation.getAsset(assetName)) compilation.emitAsset(assetName, new RawSource(''))
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private reset() {
|
|
228
|
+
clearCompiledPages(this.options.paths.appPath)
|
|
229
|
+
this.hasCompletedInitialBuild = false
|
|
230
|
+
this.rebuildingFiles.clear()
|
|
231
|
+
this.pendingFiles.clear()
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type {LoaderDefinitionFunction} from 'webpack'
|
|
2
|
+
import path from "path";
|
|
3
|
+
import {getAllPages} from '../utils'
|
|
4
|
+
import {isPageCompiled} from './compile-state'
|
|
5
|
+
import type {AppConfig} from "@tarojs/taro";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
interface CreateShellOpt {
|
|
9
|
+
page: string
|
|
10
|
+
framework: 'react'
|
|
11
|
+
compileUrl: string
|
|
12
|
+
memo: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const createReactShell = (opt: CreateShellOpt) => {
|
|
16
|
+
const {page, compileUrl, memo} = opt
|
|
17
|
+
return `
|
|
18
|
+
import { PageShell } from ${JSON.stringify(require.resolve('@taro-hmr-compile/core/runtime'))}
|
|
19
|
+
${memo ? "import React from 'react'" : ''}
|
|
20
|
+
const Page = () => <PageShell page={${JSON.stringify(page)}} compileUrl={${JSON.stringify(compileUrl)}}/>
|
|
21
|
+
export default ${memo ? 'React.memo(Page)' : 'Page'}
|
|
22
|
+
`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const createShell = (opt: CreateShellOpt) => {
|
|
26
|
+
if (opt.framework === 'react') return createReactShell(opt)
|
|
27
|
+
throw new Error(`unsupported framework: ${opt.framework}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isMemoPage = (source: string) => {
|
|
31
|
+
return /\bexport\s+default\s+(?:(?:React\.)?memo|observer)\s*\(/.test(source)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
interface LoaderOptions {
|
|
36
|
+
appPath: string
|
|
37
|
+
sourcePath: string
|
|
38
|
+
appConfig: AppConfig
|
|
39
|
+
framework: 'react'
|
|
40
|
+
compileUrl: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const lazyCompilerLoader: LoaderDefinitionFunction<LoaderOptions> = function (source) {
|
|
44
|
+
const {appPath, framework, appConfig, sourcePath, compileUrl} = this.getOptions()
|
|
45
|
+
|
|
46
|
+
const {dir: resourceDir, name: resourceName} = path.parse(path.relative(sourcePath, this.resourcePath))
|
|
47
|
+
|
|
48
|
+
const resourcePath = path.join(resourceDir, resourceName)
|
|
49
|
+
|
|
50
|
+
const isPageEntry = getAllPages(appConfig).includes(resourcePath)
|
|
51
|
+
|
|
52
|
+
if (!isPageEntry || isPageCompiled(appPath, resourcePath)) return source
|
|
53
|
+
|
|
54
|
+
return createShell({page: resourcePath, framework, compileUrl, memo: isMemoPage(source)})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default lazyCompilerLoader
|
package/src/constants.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type {IPluginContext} from '@tarojs/service'
|
|
2
|
+
import type {AppConfig} from '@tarojs/taro'
|
|
3
|
+
import {HMR_OUTPUT_PATH, DEFAULT_DEBUG_NAMESPACES} 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 {emptyDirFile, findAvailablePort} from '@taro-hmr-compile/core/compiler'
|
|
9
|
+
import {enableLog} from './log'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export interface WebpackHmrCompileOpt {
|
|
13
|
+
host: string
|
|
14
|
+
port: number
|
|
15
|
+
patchHistoryLimit: number
|
|
16
|
+
debug: boolean | string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default (ctx: IPluginContext, pluginOpts: Partial<WebpackHmrCompileOpt>) => {
|
|
20
|
+
if (!ctx.runOpts.options?.isWatch) return
|
|
21
|
+
|
|
22
|
+
const {appPath, sourcePath, outputPath} = ctx.paths
|
|
23
|
+
|
|
24
|
+
const {framework} = ctx.initialConfig
|
|
25
|
+
|
|
26
|
+
const appConfig: AppConfig = getAppConfig(ctx)
|
|
27
|
+
|
|
28
|
+
const host = pluginOpts.host ?? '127.0.0.1'
|
|
29
|
+
|
|
30
|
+
const preferredPort = pluginOpts.port ?? 8080
|
|
31
|
+
|
|
32
|
+
const debugNamespaces = (() => {
|
|
33
|
+
if (typeof pluginOpts.debug === 'string') return pluginOpts.debug
|
|
34
|
+
return pluginOpts.debug ? DEFAULT_DEBUG_NAMESPACES : undefined
|
|
35
|
+
})()
|
|
36
|
+
|
|
37
|
+
if (debugNamespaces) enableLog(debugNamespaces)
|
|
38
|
+
|
|
39
|
+
ctx.onBuildStart(() => emptyDirFile(outputPath))
|
|
40
|
+
|
|
41
|
+
ctx.modifyRunnerOpts(({opts}) => {
|
|
42
|
+
opts.output ??= {}
|
|
43
|
+
opts.output.clean = false
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
ctx.modifyWebpackChain(async ({chain, webpack}) => {
|
|
47
|
+
const port = await findAvailablePort(preferredPort, host)
|
|
48
|
+
|
|
49
|
+
const compileUrl = `http://${host}:${port}/compile`
|
|
50
|
+
|
|
51
|
+
if (port !== preferredPort) console.warn(`[taro-hmr-compile] 端口 ${preferredPort} 已被占用,自动使用端口 ${port}`)
|
|
52
|
+
|
|
53
|
+
chain.optimization.usedExports(false)
|
|
54
|
+
|
|
55
|
+
chain.module
|
|
56
|
+
.rule('lazy-compiler')
|
|
57
|
+
.test(/\.[jt]sx?$/)
|
|
58
|
+
.include.add(ctx.paths.sourcePath)
|
|
59
|
+
.end()
|
|
60
|
+
.use('lazy-compiler-loader')
|
|
61
|
+
.loader(require.resolve('./compile/lazy-compiler-loader'))
|
|
62
|
+
.options({appPath, sourcePath, appConfig, framework, compileUrl})
|
|
63
|
+
.end()
|
|
64
|
+
|
|
65
|
+
chain.output
|
|
66
|
+
.set('hotUpdateChunkFilename', `${HMR_OUTPUT_PATH}/[id].[fullhash].hot-update.js`)
|
|
67
|
+
.set('hotUpdateMainFilename', `${HMR_OUTPUT_PATH}/[runtime].[fullhash].hot-update.json`)
|
|
68
|
+
|
|
69
|
+
const {patchHistoryLimit} = pluginOpts
|
|
70
|
+
|
|
71
|
+
const [{filename: styleFilename}] = chain.plugin('miniCssExtractPlugin')?.get('args') || []
|
|
72
|
+
|
|
73
|
+
chain.plugin('hmr-compiler-plugin').use(HmrCompilerPlugin, [{paths: ctx.paths, appConfig, styleFilename, debugNamespaces}])
|
|
74
|
+
|
|
75
|
+
chain.plugin('hmr-plugin').use(webpack.HotModuleReplacementPlugin)
|
|
76
|
+
|
|
77
|
+
chain.plugin('hmr-patch-plugin').use(HmrPatchPlugin, [{patchHistoryLimit}])
|
|
78
|
+
|
|
79
|
+
chain.plugin('hmr-server-plugin').use(HmrServerPlugin, [{host, port}])
|
|
80
|
+
})
|
|
81
|
+
}
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type {Compilation, Compiler} from 'webpack'
|
|
2
|
+
import {RawSource} from 'webpack-sources'
|
|
3
|
+
import {HMR_PATCH_ASSET} from '../constants'
|
|
4
|
+
import {HmrCompilerPlugin} from '../compile/hmr-compiler-plugin'
|
|
5
|
+
import {HmrPatchRuntimePlugin} from './hmr-patch-runtime-plugin'
|
|
6
|
+
import {HmrPatchProcessor, HmrPatchProcessorOptions} from "./hmr-patch-processor";
|
|
7
|
+
|
|
8
|
+
const PLUGIN_NAME = 'HmrPatchPlugin'
|
|
9
|
+
const HOT_UPDATE_MANIFEST_SUFFIX = '.hot-update.json'
|
|
10
|
+
const HOT_UPDATE_SCRIPT_SUFFIX = '.hot-update.js'
|
|
11
|
+
const STYLE_ASSET_PATTERN = /\.(?:css|wxss|acss|ttss|qss|jxss)(?:\.map)?$/i
|
|
12
|
+
const TEMPLATE_RUNTIME_ASSET_PATTERN = /(?:^|\/)(?:base\.wxml|utils\.wxs)$/i
|
|
13
|
+
|
|
14
|
+
type HmrPatchPluginOptions = Partial<HmrPatchProcessorOptions>
|
|
15
|
+
|
|
16
|
+
const isPassthroughAsset = (assetName: string) => {
|
|
17
|
+
return STYLE_ASSET_PATTERN.test(assetName) || TEMPLATE_RUNTIME_ASSET_PATTERN.test(assetName)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class HmrPatchPlugin {
|
|
21
|
+
|
|
22
|
+
private previousHash: string = ''
|
|
23
|
+
|
|
24
|
+
private initialBuildDone = false
|
|
25
|
+
|
|
26
|
+
private readonly patchProcessor: HmrPatchProcessor
|
|
27
|
+
|
|
28
|
+
private readonly emittedAssetSources = new Map<string, string>()
|
|
29
|
+
|
|
30
|
+
constructor(options: HmrPatchPluginOptions) {
|
|
31
|
+
this.patchProcessor = new HmrPatchProcessor(options)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
apply(compiler: Compiler) {
|
|
35
|
+
new HmrPatchRuntimePlugin().apply(compiler)
|
|
36
|
+
|
|
37
|
+
compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
|
|
38
|
+
HmrCompilerPlugin.getCompilationHooks(compilation).chunkModulesRendered.tap(PLUGIN_NAME, options => {
|
|
39
|
+
this.patchProcessor.processChunkModules({compilation, ...options})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// 等 Webpack 生成完整的 hot-update assets 后,再统一转换成固定的 patch.js。
|
|
43
|
+
const stage = compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT
|
|
44
|
+
compilation.hooks.processAssets.tap({name: PLUGIN_NAME, stage}, () => this.processAssets(compilation))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
compiler.hooks.afterEmit.tap(PLUGIN_NAME, compilation => {
|
|
48
|
+
const isSuccessful = compilation.errors.length === 0 && Boolean(compilation.hash)
|
|
49
|
+
this.initialBuildDone = this.initialBuildDone || isSuccessful
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
compiler.hooks.watchClose.tap(PLUGIN_NAME, () => this.reset())
|
|
53
|
+
}
|
|
54
|
+
|
|
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())
|
|
67
|
+
})
|
|
68
|
+
this.previousHash = toHash
|
|
69
|
+
return this.emitPatchAsset(compilation, false)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const manifests = compilation.getAssets().filter(asset => asset.name.endsWith(HOT_UPDATE_MANIFEST_SUFFIX))
|
|
73
|
+
|
|
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)
|
|
81
|
+
|
|
82
|
+
this.previousHash = toHash
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 热更新转换完成后,保留固定 patch 与发生变化的模板、平台样式资产。 */
|
|
86
|
+
private removeSupersededAssets(compilation: Compilation) {
|
|
87
|
+
compilation.getAssets().forEach(asset => {
|
|
88
|
+
if (asset.name === HMR_PATCH_ASSET) return
|
|
89
|
+
if (!isPassthroughAsset(asset.name)) return compilation.deleteAsset(asset.name)
|
|
90
|
+
|
|
91
|
+
const source = asset.source.source().toString()
|
|
92
|
+
const previousSource = this.emittedAssetSources.get(asset.name)
|
|
93
|
+
this.emittedAssetSources.set(asset.name, source)
|
|
94
|
+
if (previousSource === source) compilation.deleteAsset(asset.name)
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private emitPatchAsset(compilation: Compilation, isHotUpdate: boolean) {
|
|
99
|
+
const source = new RawSource(this.patchProcessor.generate())
|
|
100
|
+
const assetInfo = {hotModuleReplacement: isHotUpdate}
|
|
101
|
+
if (compilation.getAsset(HMR_PATCH_ASSET)) compilation.updateAsset(HMR_PATCH_ASSET, source, assetInfo)
|
|
102
|
+
else compilation.emitAsset(HMR_PATCH_ASSET, source, assetInfo)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private reset() {
|
|
106
|
+
this.previousHash = ''
|
|
107
|
+
this.initialBuildDone = false
|
|
108
|
+
this.emittedAssetSources.clear()
|
|
109
|
+
this.patchProcessor.reset()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {type Chunk, type Compilation,} from "webpack";
|
|
2
|
+
import {
|
|
3
|
+
parseHotUpdateAssets,
|
|
4
|
+
ParseHotUpdateAssetsOptions,
|
|
5
|
+
parseChunkAsset, mergeUpdatePatches, generatePatchAssetSource,
|
|
6
|
+
} from "./utils";
|
|
7
|
+
import isNil from 'lodash/isNil'
|
|
8
|
+
import {ModuleFactory, UpdatePatchSource} from "./types";
|
|
9
|
+
import {DEFAULT_HMR_PATCH_HISTORY_LIMIT} from "../constants";
|
|
10
|
+
|
|
11
|
+
interface ProcessChunkModulesOptions {
|
|
12
|
+
source: string
|
|
13
|
+
compilation: Compilation,
|
|
14
|
+
isBootstrapChunk: boolean,
|
|
15
|
+
chunk: Chunk
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface HmrPatchProcessorOptions {
|
|
19
|
+
patchHistoryLimit?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ProcessUpdateOptions extends ParseHotUpdateAssetsOptions {
|
|
23
|
+
fromHash: string
|
|
24
|
+
toHash: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type BootstrapFactories = Map<string, ModuleFactory>
|
|
28
|
+
|
|
29
|
+
type ModuleHashes = Map<string, Set<string>>
|
|
30
|
+
|
|
31
|
+
type ModuleHashesByFullHash = Map<string, ModuleHashes>
|
|
32
|
+
|
|
33
|
+
const areModuleHashesEqual = (left: Set<string> | undefined, right: Set<string> | undefined): boolean => {
|
|
34
|
+
if (!left || !right || left.size !== right.size) return false
|
|
35
|
+
return Array.from(left).every(hash => right.has(hash))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class HmrPatchProcessor {
|
|
39
|
+
private readonly bootstrapFactories: BootstrapFactories = new Map()
|
|
40
|
+
|
|
41
|
+
private readonly bootstrapFactoriesByCompilation = new WeakMap<Compilation, BootstrapFactories>()
|
|
42
|
+
|
|
43
|
+
private readonly moduleHashesByFullHash: ModuleHashesByFullHash = new Map()
|
|
44
|
+
|
|
45
|
+
private recentUpdatePatches: UpdatePatchSource[] = []
|
|
46
|
+
|
|
47
|
+
private readonly opts: Partial<HmrPatchProcessorOptions>
|
|
48
|
+
|
|
49
|
+
constructor(opts: Partial<HmrPatchProcessorOptions>) {
|
|
50
|
+
this.opts = opts
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private getChangedModuleIdsByFullHash(fromHash: string, toHash: string): Set<string> | undefined {
|
|
54
|
+
const fromModuleHashes = this.moduleHashesByFullHash.get(fromHash)
|
|
55
|
+
|
|
56
|
+
const toModuleHashes = this.moduleHashesByFullHash.get(toHash)
|
|
57
|
+
|
|
58
|
+
// 任一编译缺少 Hash 基线时不做过滤,避免误丢有效的 hot-update 模块。
|
|
59
|
+
if (!fromModuleHashes || !toModuleHashes) return
|
|
60
|
+
|
|
61
|
+
// 同时检查前后两次编译的模块,确保新增、修改和删除都能被识别。
|
|
62
|
+
const moduleIds = new Set(fromModuleHashes.keys())
|
|
63
|
+
toModuleHashes.forEach((_, moduleId) => moduleIds.add(moduleId))
|
|
64
|
+
|
|
65
|
+
const changedModuleIds = new Set<string>()
|
|
66
|
+
moduleIds.forEach(moduleId => {
|
|
67
|
+
const fromModuleHash = fromModuleHashes.get(moduleId)
|
|
68
|
+
const toModuleHash = toModuleHashes.get(moduleId)
|
|
69
|
+
if (!areModuleHashesEqual(fromModuleHash, toModuleHash)) changedModuleIds.add(moduleId)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return changedModuleIds
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
processUpdate(opt: ProcessUpdateOptions) {
|
|
76
|
+
const {fromHash, toHash} = opt
|
|
77
|
+
|
|
78
|
+
const tempPatchInfo = parseHotUpdateAssets(opt)
|
|
79
|
+
|
|
80
|
+
const changedModuleIds = this.getChangedModuleIdsByFullHash(fromHash, toHash)
|
|
81
|
+
|
|
82
|
+
const moduleFactories = (() => {
|
|
83
|
+
if (!changedModuleIds) return Array.from(tempPatchInfo.moduleFactories.values())
|
|
84
|
+
return Array.from(tempPatchInfo.moduleFactories.values()).filter(item => changedModuleIds.has(item.moduleId))
|
|
85
|
+
})()
|
|
86
|
+
|
|
87
|
+
const removedModules = (() => {
|
|
88
|
+
if (!changedModuleIds) return Array.from(tempPatchInfo.removedModules.values())
|
|
89
|
+
return Array.from(tempPatchInfo.removedModules.values()).filter(item => changedModuleIds.has(item))
|
|
90
|
+
})()
|
|
91
|
+
|
|
92
|
+
const {runtimeModules} = tempPatchInfo
|
|
93
|
+
|
|
94
|
+
this.recentUpdatePatches.push({type: 'update', moduleFactories, runtimeModules, fromHash, toHash, removedModules})
|
|
95
|
+
|
|
96
|
+
const patchHistoryLimit = Math.max(1, this.opts.patchHistoryLimit ?? DEFAULT_HMR_PATCH_HISTORY_LIMIT)
|
|
97
|
+
|
|
98
|
+
if (this.recentUpdatePatches.length <= patchHistoryLimit) return
|
|
99
|
+
|
|
100
|
+
const [oldestUpdatePatch, secondOldestUpdatePatch, ...rest] = this.recentUpdatePatches
|
|
101
|
+
|
|
102
|
+
const mergedUpdatePatch = mergeUpdatePatches(oldestUpdatePatch, secondOldestUpdatePatch)
|
|
103
|
+
|
|
104
|
+
this.recentUpdatePatches = [mergedUpdatePatch, ...rest]
|
|
105
|
+
|
|
106
|
+
this.moduleHashesByFullHash.delete(oldestUpdatePatch.toHash)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
processChunkModules(opt: ProcessChunkModulesOptions) {
|
|
110
|
+
const {chunk, compilation, isBootstrapChunk, source} = opt
|
|
111
|
+
const {hash: fullHash} = compilation
|
|
112
|
+
const modules = parseChunkAsset(source)
|
|
113
|
+
modules.forEach(moduleFactory => {
|
|
114
|
+
const {moduleId, factory} = moduleFactory
|
|
115
|
+
|
|
116
|
+
if (isBootstrapChunk) {
|
|
117
|
+
const compilationFactories = this.bootstrapFactoriesByCompilation.get(compilation) ?? new Map()
|
|
118
|
+
compilationFactories.set(moduleId, {moduleId, factory})
|
|
119
|
+
this.bootstrapFactoriesByCompilation.set(compilation, compilationFactories)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (!fullHash) return
|
|
124
|
+
|
|
125
|
+
const moduleHashes = (() => {
|
|
126
|
+
const hasCurFullHashModuleHashMap = this.moduleHashesByFullHash.has(fullHash)
|
|
127
|
+
if (!hasCurFullHashModuleHashMap) this.moduleHashesByFullHash.set(fullHash, new Map())
|
|
128
|
+
return this.moduleHashesByFullHash.get(fullHash)!
|
|
129
|
+
})()
|
|
130
|
+
|
|
131
|
+
const { chunkGraph, codeGenerationResults } = compilation
|
|
132
|
+
|
|
133
|
+
Array.from(chunkGraph.getChunkModulesIterable(chunk)).forEach(module => {
|
|
134
|
+
const moduleId = chunkGraph.getModuleId(module)
|
|
135
|
+
|
|
136
|
+
if (isNil(moduleId)) return
|
|
137
|
+
|
|
138
|
+
const normalizedModuleId = String(moduleId)
|
|
139
|
+
|
|
140
|
+
// 获取模块在当前 chunk.runtime 下的 Hash,优先从 codeGenerationResults 获取,如果没有对应的代码生成结果,则回退到 chunkGraph 获取
|
|
141
|
+
const moduleHash = codeGenerationResults?.has(module, chunk.runtime)
|
|
142
|
+
? codeGenerationResults.getHash(module, chunk.runtime)
|
|
143
|
+
: chunkGraph.getModuleHash(module, chunk.runtime)
|
|
144
|
+
|
|
145
|
+
const hashes = moduleHashes.get(normalizedModuleId) ?? new Set<string>()
|
|
146
|
+
hashes.add(moduleHash)
|
|
147
|
+
moduleHashes.set(normalizedModuleId, hashes)
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
commitBootstrapFactories(compilation: Compilation) {
|
|
152
|
+
this.bootstrapFactories.clear()
|
|
153
|
+
this.bootstrapFactoriesByCompilation.get(compilation)?.forEach(moduleFactory => {
|
|
154
|
+
this.bootstrapFactories.set(moduleFactory.moduleId, moduleFactory)
|
|
155
|
+
})
|
|
156
|
+
this.bootstrapFactoriesByCompilation.delete(compilation)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
generate() {
|
|
160
|
+
const {bootstrapFactories, recentUpdatePatches} = this
|
|
161
|
+
return generatePatchAssetSource({bootstrapFactories, recentUpdatePatches})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
reset() {
|
|
165
|
+
this.bootstrapFactories.clear()
|
|
166
|
+
this.moduleHashesByFullHash.clear()
|
|
167
|
+
this.recentUpdatePatches = []
|
|
168
|
+
}
|
|
169
|
+
}
|