@taro-hmr-compile/plugin-webpack 0.0.1-alpha.8 → 0.0.1
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/README.md +101 -0
- package/dist/compile/hmr-compiler-loader.js +44 -7
- package/dist/compile/hmr-compiler-loader.js.map +1 -1
- package/dist/compile/hmr-compiler-plugin.d.ts +7 -7
- package/dist/compile/hmr-compiler-plugin.js +2 -29
- package/dist/compile/hmr-compiler-plugin.js.map +1 -1
- package/dist/compile/hmr-output-plugin.d.ts +28 -0
- package/dist/compile/hmr-output-plugin.js +145 -0
- package/dist/compile/hmr-output-plugin.js.map +1 -0
- package/dist/compile/hmr-style-plugin.d.ts +25 -0
- package/dist/compile/hmr-style-plugin.js +53 -0
- package/dist/compile/hmr-style-plugin.js.map +1 -0
- package/dist/compile/lazy-compiler-loader.d.ts +2 -0
- package/dist/compile/lazy-compiler-loader.js +17 -5
- package/dist/compile/lazy-compiler-loader.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -6
- package/dist/index.js.map +1 -1
- package/dist/patch/hmr-patch-plugin.d.ts +6 -0
- package/dist/patch/hmr-patch-plugin.js +53 -4
- package/dist/patch/hmr-patch-plugin.js.map +1 -1
- package/dist/patch/hmr-patch-processor.d.ts +2 -1
- package/dist/patch/hmr-patch-processor.js +39 -22
- package/dist/patch/hmr-patch-processor.js.map +1 -1
- package/dist/patch/hmr-patch-runtime-plugin.js +1 -1
- package/dist/patch/types.d.ts +1 -0
- package/dist/patch/utils.d.ts +2 -0
- package/dist/patch/utils.js +11 -5
- package/dist/patch/utils.js.map +1 -1
- package/package.json +4 -2
- package/src/compile/hmr-compiler-loader.ts +43 -7
- package/src/compile/hmr-compiler-plugin.ts +9 -34
- package/src/compile/hmr-output-plugin.ts +158 -0
- package/src/compile/hmr-style-plugin.ts +70 -0
- package/src/compile/lazy-compiler-loader.ts +20 -5
- package/src/constants.ts +2 -0
- package/src/index.ts +23 -9
- package/src/patch/hmr-patch-plugin.ts +53 -5
- package/src/patch/hmr-patch-processor.ts +48 -28
- package/src/patch/hmr-patch-runtime-plugin.ts +1 -1
- package/src/patch/types.ts +1 -0
- package/src/patch/utils.ts +10 -5
- package/src/types/taro-service.d.ts +15 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taro-hmr-compile/plugin-webpack",
|
|
3
|
-
"version": "0.0.1
|
|
3
|
+
"version": "0.0.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"@types/babel__template": "^7.4.4",
|
|
35
35
|
"@types/babel__traverse": "^7.28.0",
|
|
36
36
|
"@types/debug": "^4.1.13",
|
|
37
|
+
"@types/lodash": "^4.17.26",
|
|
37
38
|
"@types/node": "^22.20.1",
|
|
38
39
|
"typescript": "^5.8.2",
|
|
39
40
|
"webpack": "^5.110.3"
|
|
@@ -44,8 +45,9 @@
|
|
|
44
45
|
"@babel/template": "^7.29.7",
|
|
45
46
|
"@babel/traverse": "^7.29.8",
|
|
46
47
|
"@babel/types": "^7.29.8",
|
|
47
|
-
"@taro-hmr-compile/core": "0.0.1
|
|
48
|
+
"@taro-hmr-compile/core": "0.0.1",
|
|
48
49
|
"debug": "^4.4.3",
|
|
50
|
+
"lodash": "^4.18.1",
|
|
49
51
|
"tapable": "^2.3.3",
|
|
50
52
|
"webpack-sources": "^3.5.1"
|
|
51
53
|
},
|
|
@@ -22,22 +22,29 @@ export const injectAppHmrRuntime = (source: string, debugNamespaces = '') => {
|
|
|
22
22
|
const ast = parser.parse(source, {sourceType: 'module'})
|
|
23
23
|
|
|
24
24
|
const buildHmrCode = template.statements(`
|
|
25
|
-
import {
|
|
25
|
+
import { updatePatchContext, mergePatches, enableLog as enableHmrCoreLog } from ${JSON.stringify(CORE_RUNTIME_REQUEST)}
|
|
26
26
|
import { log, enableLog as enableHmrWebpackLog } from ${JSON.stringify(LOG_REQUEST)}
|
|
27
27
|
enableHmrCoreLog(${JSON.stringify(debugNamespaces)})
|
|
28
28
|
enableHmrWebpackLog(${JSON.stringify(debugNamespaces)})
|
|
29
29
|
globalThis.log = log.extend('runtime')
|
|
30
|
+
updatePatchContext.initialize(${webpack.RuntimeGlobals.getFullHash}())
|
|
30
31
|
globalThis.miniHmrApplyPatch = async (patches)=>{
|
|
32
|
+
let transaction
|
|
31
33
|
try {
|
|
32
34
|
const { miniHmr } = __webpack_require__
|
|
33
35
|
const bootstrapPatch = patches.find(patch => patch.type === 'bootstrap')
|
|
36
|
+
const shouldApplyBootstrapPatch = miniHmr.shouldApplyBootstrapPatch(bootstrapPatch)
|
|
37
|
+
const currentHash = ${webpack.RuntimeGlobals.getFullHash}()
|
|
38
|
+
const updatePatch = mergePatches(currentHash, patches)
|
|
34
39
|
|
|
35
|
-
if (
|
|
40
|
+
if (shouldApplyBootstrapPatch) miniHmr.applyBootstrapPatch(bootstrapPatch)
|
|
41
|
+
if (!updatePatch) return
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
hmrState.setLastFullHash(updatePatch?.toHash ?? ${webpack.RuntimeGlobals.getFullHash}())
|
|
43
|
+
transaction = updatePatchContext.beginTransaction({fromHash: currentHash,toHash: updatePatch.toHash})
|
|
39
44
|
await miniHmr.applyUpdatePatch(updatePatch)
|
|
45
|
+
updatePatchContext.completeTransaction(transaction.id)
|
|
40
46
|
} catch (error) {
|
|
47
|
+
if (transaction) updatePatchContext.abortTransaction(transaction.id)
|
|
41
48
|
console.error('[taro-hmr-compile] patch apply failed', error)
|
|
42
49
|
return Promise.resolve()
|
|
43
50
|
}
|
|
@@ -75,6 +82,26 @@ export const transformPageConfig = (source: string, sourceFileName?: string) =>
|
|
|
75
82
|
})
|
|
76
83
|
}
|
|
77
84
|
|
|
85
|
+
const isHotAcceptCall = (node: t.Node | null | undefined) => {
|
|
86
|
+
if (!t.isCallExpression(node)) return false
|
|
87
|
+
const {callee} = node
|
|
88
|
+
if (!t.isMemberExpression(callee) || !t.isIdentifier(callee.property, {name: 'accept'})) return false
|
|
89
|
+
const hot = callee.object
|
|
90
|
+
return t.isMemberExpression(hot) && t.isIdentifier(hot.object, {name: 'module'}) && t.isIdentifier(hot.property, {name: 'hot'})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const hasHotAccept = (programPath: NodePath<t.Program>) => {
|
|
94
|
+
return programPath.node.body.some(statement => {
|
|
95
|
+
if (t.isExpressionStatement(statement)) return isHotAcceptCall(statement.expression)
|
|
96
|
+
if (!t.isIfStatement(statement)) return false
|
|
97
|
+
|
|
98
|
+
const {consequent} = statement
|
|
99
|
+
if (t.isExpressionStatement(consequent)) return isHotAcceptCall(consequent.expression)
|
|
100
|
+
if (!t.isBlockStatement(consequent)) return false
|
|
101
|
+
return consequent.body.some(node => t.isExpressionStatement(node) && isHotAcceptCall(node.expression))
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
traverse(ast, {
|
|
79
106
|
Program(programPath) {
|
|
80
107
|
|
|
@@ -92,6 +119,7 @@ export const transformPageConfig = (source: string, sourceFileName?: string) =>
|
|
|
92
119
|
|
|
93
120
|
// 判断是否存在 withHmrPageConfig 引用
|
|
94
121
|
const hasWithHmrPageConfigBinding = programPath.scope.hasBinding(WITH_HMR_PAGE_CONFIG)
|
|
122
|
+
const shouldInjectHotAccept = !hasHotAccept(programPath)
|
|
95
123
|
|
|
96
124
|
// 生成 withHmrPageConfig 的别名
|
|
97
125
|
const wrapperIdentifier = hasWithHmrPageConfigBinding ? programPath.scope.generateUidIdentifier(WITH_HMR_PAGE_CONFIG) : t.identifier(WITH_HMR_PAGE_CONFIG)
|
|
@@ -102,24 +130,32 @@ export const transformPageConfig = (source: string, sourceFileName?: string) =>
|
|
|
102
130
|
return template.statement(`import { ${WITH_HMR_PAGE_CONFIG} as %%WRAPPER%% } from ${JSON.stringify(CORE_RUNTIME_REQUEST)};`)({WRAPPER: wrapperIdentifier})
|
|
103
131
|
})()
|
|
104
132
|
|
|
133
|
+
let transformedPageConfig = false
|
|
134
|
+
|
|
105
135
|
programPath.traverse({
|
|
106
136
|
CallExpression(CallExpressionPath) {
|
|
107
137
|
const {node: CallExpressionNode} = CallExpressionPath
|
|
108
138
|
if (!t.isIdentifier(CallExpressionNode.callee, {name: CREATE_PAGE_CONFIG})) return
|
|
109
139
|
const [_, pageName] = CallExpressionNode.arguments
|
|
110
|
-
const wrappedPageConfig = template.expression(`%%WRAPPER%%(%%PAGE_CONFIG%%, %%PAGE_NAME
|
|
140
|
+
const wrappedPageConfig = template.expression(`%%WRAPPER%%(%%PAGE_CONFIG%%, %%PAGE_NAME%%, typeof module !== 'undefined' ? module.hot : undefined)`)({
|
|
111
141
|
WRAPPER: wrapperIdentifier,
|
|
112
142
|
PAGE_CONFIG: CallExpressionNode,
|
|
113
143
|
PAGE_NAME: pageName,
|
|
114
144
|
})
|
|
115
145
|
|
|
116
|
-
if (!hasImportWithHmrPageConfig(programPath)) programPath.unshiftContainer('body', runtimeImport)
|
|
117
|
-
|
|
118
146
|
CallExpressionPath.replaceWith(wrappedPageConfig)
|
|
119
147
|
CallExpressionPath.skip()
|
|
148
|
+
transformedPageConfig = true
|
|
120
149
|
}
|
|
121
150
|
})
|
|
122
151
|
|
|
152
|
+
if (!transformedPageConfig) return
|
|
153
|
+
|
|
154
|
+
programPath.unshiftContainer('body', runtimeImport)
|
|
155
|
+
if (shouldInjectHotAccept) {
|
|
156
|
+
programPath.pushContainer('body', template.statement(`if (typeof module !== 'undefined' && module.hot) module.hot.accept();`)())
|
|
157
|
+
}
|
|
158
|
+
|
|
123
159
|
},
|
|
124
160
|
})
|
|
125
161
|
|
|
@@ -20,18 +20,21 @@ interface TaroNormalModule {
|
|
|
20
20
|
loaders: Array<{ loader: string; options?: unknown }>
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
type StyleFilename = NonNullable<Compiler['options']['output']['cssFilename']>
|
|
24
|
-
|
|
25
23
|
export interface HmrCompilerPluginOptions {
|
|
26
24
|
paths: IPaths
|
|
27
25
|
appConfig: AppConfig
|
|
28
|
-
styleFilename?: StyleFilename
|
|
29
26
|
debugNamespaces?: string
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
type CompileRequested = SyncHook<[page: string]>
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
interface ChunkModulesRenderedOptions {
|
|
32
|
+
source: string
|
|
33
|
+
isBootstrapChunk: boolean
|
|
34
|
+
chunk: Chunk
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type ChunkModulesRendered = SyncHook<[options: ChunkModulesRenderedOptions]>
|
|
35
38
|
|
|
36
39
|
export interface HmrCompilerHooks {
|
|
37
40
|
compileRequested: CompileRequested
|
|
@@ -56,8 +59,6 @@ export class HmrCompilerPlugin {
|
|
|
56
59
|
|
|
57
60
|
private compiler?: Compiler
|
|
58
61
|
|
|
59
|
-
private hasCompletedInitialBuild = false
|
|
60
|
-
|
|
61
62
|
// 页面编译请求可能在任意时刻到达。先排队到下一轮 watchRun,避免与正在执行的编译相互覆盖。
|
|
62
63
|
private readonly pendingFiles = new Set<string>()
|
|
63
64
|
|
|
@@ -80,7 +81,7 @@ export class HmrCompilerPlugin {
|
|
|
80
81
|
|
|
81
82
|
static getCompilationHooks(compilation: Compilation): HmrCompilationHooks {
|
|
82
83
|
if (!HmrCompilerPlugin.compilationHooksMap.get(compilation)) {
|
|
83
|
-
const chunkModulesRendered: ChunkModulesRendered = new SyncHook(['
|
|
84
|
+
const chunkModulesRendered: ChunkModulesRendered = new SyncHook(['options'])
|
|
84
85
|
HmrCompilerPlugin.compilationHooksMap.set(compilation, {chunkModulesRendered})
|
|
85
86
|
}
|
|
86
87
|
return HmrCompilerPlugin.compilationHooksMap.get(compilation)!
|
|
@@ -134,7 +135,7 @@ export class HmrCompilerPlugin {
|
|
|
134
135
|
// 因而 hash、HMR diff 和 hot-update 仍以完整模块图为准。
|
|
135
136
|
javascriptHooks.renderChunk.tap({name: PLUGIN_NAME, stage: -10_000}, (source, {chunk}) => {
|
|
136
137
|
const isBootstrapChunk = isPageChunk(chunk)
|
|
137
|
-
hmrHooks.chunkModulesRendered.call(source.source().toString(), isBootstrapChunk)
|
|
138
|
+
hmrHooks.chunkModulesRendered.call({source: source.source().toString(), isBootstrapChunk, chunk})
|
|
138
139
|
return isBootstrapChunk ? new RawSource('{}') : source
|
|
139
140
|
})
|
|
140
141
|
|
|
@@ -167,17 +168,6 @@ export class HmrCompilerPlugin {
|
|
|
167
168
|
},
|
|
168
169
|
)
|
|
169
170
|
|
|
170
|
-
const stage = compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT
|
|
171
|
-
compilation.hooks.processAssets.tap({name: PLUGIN_NAME, stage}, () => {
|
|
172
|
-
if (!this.hasCompletedInitialBuild && compilation.hash && compilation.errors.length === 0) {
|
|
173
|
-
this.emitMissingPageStyleAssets(compilation)
|
|
174
|
-
}
|
|
175
|
-
})
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
compiler.hooks.afterEmit.tap(PLUGIN_NAME, compilation => {
|
|
179
|
-
const isSuccessful = compilation.errors.length === 0 && Boolean(compilation.hash)
|
|
180
|
-
this.hasCompletedInitialBuild = this.hasCompletedInitialBuild || isSuccessful
|
|
181
171
|
})
|
|
182
172
|
|
|
183
173
|
// 当前批次完成后释放快照;编译期间新进入 pendingFiles 的请求不会被清理。
|
|
@@ -204,23 +194,8 @@ export class HmrCompilerPlugin {
|
|
|
204
194
|
return Array.from(pages).some(page => compilation.entrypoints.get(page)?.getEntrypointChunk() === chunk)
|
|
205
195
|
}
|
|
206
196
|
|
|
207
|
-
/** PageShell 不包含真实页面的样式依赖,为缺失的页面样式补充空资产。 */
|
|
208
|
-
private emitMissingPageStyleAssets(compilation: Compilation) {
|
|
209
|
-
const {appConfig, styleFilename} = this.options
|
|
210
|
-
if (!styleFilename) return
|
|
211
|
-
|
|
212
|
-
getAllPages(appConfig).forEach(page => {
|
|
213
|
-
const chunk = compilation.entrypoints.get(page)?.getEntrypointChunk()
|
|
214
|
-
if (!chunk) return
|
|
215
|
-
|
|
216
|
-
const assetName = compilation.getPath(styleFilename, {chunk, contentHashType: 'css/mini-extract'})
|
|
217
|
-
if (!compilation.getAsset(assetName)) compilation.emitAsset(assetName, new RawSource(''))
|
|
218
|
-
})
|
|
219
|
-
}
|
|
220
|
-
|
|
221
197
|
private reset() {
|
|
222
198
|
clearCompiledPages(this.options.paths.appPath)
|
|
223
|
-
this.hasCompletedInitialBuild = false
|
|
224
199
|
this.rebuildingFiles.clear()
|
|
225
200
|
this.pendingFiles.clear()
|
|
226
201
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {randomUUID} from 'node:crypto'
|
|
2
|
+
import type {NoParamCallback, PathOrFileDescriptor, WriteFileOptions} from 'node:fs'
|
|
3
|
+
import {mkdir, rename, rm, writeFile} from 'node:fs/promises'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import type {Compilation, Compiler, OutputFileSystem} from 'webpack'
|
|
6
|
+
|
|
7
|
+
export const PLUGIN_NAME = 'HmrOutputPlugin'
|
|
8
|
+
|
|
9
|
+
type WriteFile = OutputFileSystem['writeFile']
|
|
10
|
+
|
|
11
|
+
type WriteFileData = string | NodeJS.ArrayBufferView
|
|
12
|
+
|
|
13
|
+
export interface AtomicOutputPluginOptions {
|
|
14
|
+
/** 这些资产仍由 Webpack 生成,但等其他资产输出完成后才按配置顺序发布。 */
|
|
15
|
+
deferredAssets?: readonly string[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface DeferredOutput {
|
|
19
|
+
data: WriteFileData
|
|
20
|
+
file: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class HmrOutputPlugin {
|
|
24
|
+
private readonly deferredAssets: readonly string[]
|
|
25
|
+
|
|
26
|
+
private readonly deferredOutputs = new WeakMap<Compilation, Map<string, DeferredOutput>>()
|
|
27
|
+
|
|
28
|
+
constructor(options: AtomicOutputPluginOptions = {}) {
|
|
29
|
+
this.deferredAssets = options.deferredAssets ?? []
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
apply(compiler: Compiler): void {
|
|
33
|
+
const outputFileSystem = compiler.outputFileSystem
|
|
34
|
+
|
|
35
|
+
if (!outputFileSystem) throw new Error(`[${PLUGIN_NAME}] Webpack outputFileSystem 尚未初始化`)
|
|
36
|
+
|
|
37
|
+
compiler.outputFileSystem = this.createOutputFileSystem(compiler, outputFileSystem)
|
|
38
|
+
|
|
39
|
+
// 尽可能在其他 emit tap 之后读取资产,随后 Webpack 才开始物理输出。
|
|
40
|
+
compiler.hooks.emit.tap({name: PLUGIN_NAME, stage: Number.MAX_SAFE_INTEGER}, compilation => {
|
|
41
|
+
if (compilation.errors.length > 0) return
|
|
42
|
+
this.collectDeferredOutputs(compiler, compilation)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// 等待普通资产写入完成后,再按配置顺序发布延迟资产。
|
|
46
|
+
compiler.hooks.afterEmit.tapPromise(PLUGIN_NAME, async compilation => {
|
|
47
|
+
try {
|
|
48
|
+
if (compilation.errors.length > 0) return
|
|
49
|
+
await this.flushDeferredOutputs(compilation)
|
|
50
|
+
} finally {
|
|
51
|
+
this.deferredOutputs.delete(compilation)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 代理原始 OutputFileSystem。
|
|
58
|
+
* 仅重写 writeFile,其余方法调用原始文件系统实现。
|
|
59
|
+
*/
|
|
60
|
+
private createOutputFileSystem(compiler: Compiler, original: OutputFileSystem): OutputFileSystem {
|
|
61
|
+
return new Proxy(original, {
|
|
62
|
+
get: (target, property) => {
|
|
63
|
+
if (property === 'writeFile') return this.createWriteFile(compiler, original)
|
|
64
|
+
const value = Reflect.get(target, property, target)
|
|
65
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 创建自定义 writeFile 实现。
|
|
72
|
+
*/
|
|
73
|
+
private createWriteFile(compiler: Compiler, original: OutputFileSystem): WriteFile {
|
|
74
|
+
const originalWriteFile = original.writeFile.bind(original)
|
|
75
|
+
|
|
76
|
+
return (file: PathOrFileDescriptor, data: WriteFileData, optionsOrCallback: WriteFileOptions | NoParamCallback, optionalCallback?: NoParamCallback): void => {
|
|
77
|
+
|
|
78
|
+
const callback = (() => {
|
|
79
|
+
if (typeof optionsOrCallback === 'function') return optionsOrCallback
|
|
80
|
+
return optionalCallback
|
|
81
|
+
})()
|
|
82
|
+
|
|
83
|
+
const triggerOriginalWriteFile = () => {
|
|
84
|
+
if (typeof optionsOrCallback === 'function') return originalWriteFile(file, data, optionsOrCallback)
|
|
85
|
+
if (optionalCallback) originalWriteFile(file, data, optionsOrCallback, optionalCallback)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeof file !== 'string' || !callback) return triggerOriginalWriteFile()
|
|
89
|
+
|
|
90
|
+
const assetName = this.getAssetName(compiler, file)
|
|
91
|
+
|
|
92
|
+
if (assetName && this.deferredAssets.includes(assetName)) return callback(null)
|
|
93
|
+
|
|
94
|
+
this.writeFileAtomically(file, data).then(() => callback(null)).catch(error => callback(error))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private collectDeferredOutputs(compiler: Compiler, compilation: Compilation): void {
|
|
99
|
+
if (this.deferredAssets.length === 0) return
|
|
100
|
+
|
|
101
|
+
const {path: outputPath} = compiler.options.output
|
|
102
|
+
|
|
103
|
+
if (!outputPath) throw new Error(`[${PLUGIN_NAME}] 无法收集延迟资产:output.path 未配置`)
|
|
104
|
+
|
|
105
|
+
const outputs = new Map<string, DeferredOutput>()
|
|
106
|
+
|
|
107
|
+
this.deferredAssets.forEach(assetName => {
|
|
108
|
+
const asset = compilation.getAsset(assetName)
|
|
109
|
+
if (!asset) return
|
|
110
|
+
const data = asset.source.source()
|
|
111
|
+
outputs.set(assetName, {data, file: path.join(outputPath, assetName)})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
this.deferredOutputs.set(compilation, outputs)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private getAssetName(compiler: Compiler, file: string) {
|
|
118
|
+
const {path: outputPath} = compiler.options.output
|
|
119
|
+
|
|
120
|
+
if (!outputPath) return
|
|
121
|
+
|
|
122
|
+
const assetName = path.relative(outputPath, file).replaceAll('\\', '/')
|
|
123
|
+
|
|
124
|
+
if (assetName.startsWith('../') || path.isAbsolute(assetName)) return
|
|
125
|
+
|
|
126
|
+
return assetName
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async flushDeferredOutputs(compilation: Compilation) {
|
|
130
|
+
const outputs = this.deferredOutputs.get(compilation)
|
|
131
|
+
|
|
132
|
+
if (!outputs) return
|
|
133
|
+
|
|
134
|
+
return Promise.all(this.deferredAssets.map(assetName => {
|
|
135
|
+
const output = outputs.get(assetName)
|
|
136
|
+
if (!output) return
|
|
137
|
+
return this.writeFileAtomically(output.file, output.data)
|
|
138
|
+
}))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 通过同目录临时文件和 rename 原子替换目标文件。
|
|
143
|
+
*/
|
|
144
|
+
private async writeFileAtomically(targetPath: string, data: WriteFileData): Promise<void> {
|
|
145
|
+
const directory = path.dirname(targetPath)
|
|
146
|
+
|
|
147
|
+
const temporaryPath = path.join(directory, `.${path.basename(targetPath)}.${randomUUID()}.tmp`)
|
|
148
|
+
|
|
149
|
+
await mkdir(directory, {recursive: true})
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await writeFile(temporaryPath, data)
|
|
153
|
+
await rename(temporaryPath, targetPath)
|
|
154
|
+
} finally {
|
|
155
|
+
await rm(temporaryPath, {force: true})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type {Compilation, Compiler} from 'webpack'
|
|
2
|
+
import {RawSource} from 'webpack-sources'
|
|
3
|
+
|
|
4
|
+
export interface HmrStylePluginOptions {
|
|
5
|
+
/** Webpack/Taro 生成的全局样式资产。 */
|
|
6
|
+
sourceAsset: string
|
|
7
|
+
/** 固定入口资产;后续编译持续引用 patchAsset。 */
|
|
8
|
+
entryAsset: string
|
|
9
|
+
/** 每轮热编译输出的独立样式资产。 */
|
|
10
|
+
patchAsset: string
|
|
11
|
+
/** 是否将入口固定为对 patchAsset 的导入。 */
|
|
12
|
+
preserveEntry?: boolean
|
|
13
|
+
/** 自定义源样式到 patch 样式的内容转换。 */
|
|
14
|
+
transform?: (source: string, compilation: Compilation) => string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PLUGIN_NAME = 'HmrStylePlugin'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 将构建器生成的全局样式复制到独立 HMR 资产,并可固定入口文件。
|
|
21
|
+
* 应在构建器完成最终资产变换后运行。
|
|
22
|
+
*/
|
|
23
|
+
export class HmrStylePlugin {
|
|
24
|
+
private readonly options: HmrStylePluginOptions
|
|
25
|
+
|
|
26
|
+
private initialBuildDone = false
|
|
27
|
+
|
|
28
|
+
constructor(options: HmrStylePluginOptions) {
|
|
29
|
+
this.options = options
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
apply(compiler: Compiler) {
|
|
33
|
+
compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
|
|
34
|
+
const stage = compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT
|
|
35
|
+
compilation.hooks.processAssets.tap({name: PLUGIN_NAME, stage}, () => this.processAssets(compilation))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
compiler.hooks.afterEmit.tap(PLUGIN_NAME, compilation => {
|
|
39
|
+
if (compilation.errors.length === 0 && compilation.hash) this.initialBuildDone = true
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
compiler.hooks.watchClose.tap(PLUGIN_NAME, () => {
|
|
43
|
+
this.initialBuildDone = false
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private processAssets(compilation: Compilation) {
|
|
48
|
+
if (!compilation.hash || compilation.errors.length > 0) return
|
|
49
|
+
|
|
50
|
+
const {sourceAsset, entryAsset, patchAsset, preserveEntry = true, transform} = this.options
|
|
51
|
+
const source = compilation.getAsset(sourceAsset)?.source.source().toString()
|
|
52
|
+
|
|
53
|
+
if (source !== undefined) {
|
|
54
|
+
const patchSource = transform ? transform(source, compilation) : source
|
|
55
|
+
this.updateAsset(compilation, patchAsset, new RawSource(patchSource), {
|
|
56
|
+
hotModuleReplacement: this.initialBuildDone
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (preserveEntry) {
|
|
61
|
+
const entry = new RawSource(`@import ${JSON.stringify(`./${patchAsset}`)};\n`)
|
|
62
|
+
this.updateAsset(compilation, entryAsset, entry)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private updateAsset(compilation: Compilation, name: string, source: RawSource, info = {}) {
|
|
67
|
+
if (compilation.getAsset(name)) compilation.updateAsset(name, source, info)
|
|
68
|
+
else compilation.emitAsset(name, source, info)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -9,14 +9,16 @@ interface CreateShellOpt {
|
|
|
9
9
|
page: string
|
|
10
10
|
framework: 'react'
|
|
11
11
|
compileUrl: string
|
|
12
|
+
memo: boolean
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export const createReactShell = (opt: CreateShellOpt) => {
|
|
15
|
-
const {page, compileUrl} = opt
|
|
16
|
+
const {page, compileUrl, memo} = opt
|
|
16
17
|
return `
|
|
17
18
|
import { PageShell } from ${JSON.stringify(require.resolve('@taro-hmr-compile/core/runtime'))}
|
|
19
|
+
${memo ? "import React from 'react'" : ''}
|
|
18
20
|
const Page = () => <PageShell page={${JSON.stringify(page)}} compileUrl={${JSON.stringify(compileUrl)}}/>
|
|
19
|
-
export default Page
|
|
21
|
+
export default ${memo ? 'React.memo(Page)' : 'Page'}
|
|
20
22
|
`
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -25,6 +27,10 @@ const createShell = (opt: CreateShellOpt) => {
|
|
|
25
27
|
throw new Error(`unsupported framework: ${opt.framework}`)
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
const isMemoPage = (source: string) => {
|
|
31
|
+
return /\bexport\s+default\s+(?:(?:React\.)?memo|observer)\s*\(/.test(source)
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
|
|
29
35
|
interface LoaderOptions {
|
|
30
36
|
appPath: string
|
|
@@ -32,20 +38,29 @@ interface LoaderOptions {
|
|
|
32
38
|
appConfig: AppConfig
|
|
33
39
|
framework: 'react'
|
|
34
40
|
compileUrl: string
|
|
41
|
+
lazyCompileExclude?: string[]
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
const lazyCompilerLoader: LoaderDefinitionFunction<LoaderOptions> = function (source) {
|
|
38
|
-
const {appPath, framework, appConfig, sourcePath, compileUrl} = this.getOptions()
|
|
45
|
+
const {appPath, framework, appConfig, sourcePath, compileUrl, lazyCompileExclude = []} = this.getOptions()
|
|
39
46
|
|
|
40
47
|
const {dir: resourceDir, name: resourceName} = path.parse(path.relative(sourcePath, this.resourcePath))
|
|
41
48
|
|
|
42
|
-
const resourcePath = path.join(resourceDir, resourceName)
|
|
49
|
+
const resourcePath = path.join(resourceDir, resourceName).replace(/\\/g, '/')
|
|
43
50
|
|
|
44
51
|
const isPageEntry = getAllPages(appConfig).includes(resourcePath)
|
|
45
52
|
|
|
46
53
|
if (!isPageEntry || isPageCompiled(appPath, resourcePath)) return source
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
const isExcluded = lazyCompileExclude.some(excludePath => {
|
|
56
|
+
if (!excludePath) return false
|
|
57
|
+
const normalizedPath = path.posix.normalize(excludePath.replace(/\\/g, '/')).replace(/^\/+/, '')
|
|
58
|
+
return normalizedPath.endsWith('/') ? resourcePath.startsWith(normalizedPath) : resourcePath === normalizedPath
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
if (isExcluded) return source
|
|
62
|
+
|
|
63
|
+
return createShell({page: resourcePath, framework, compileUrl, memo: isMemoPage(source)})
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
export default lazyCompilerLoader
|
package/src/constants.ts
CHANGED
|
@@ -2,6 +2,8 @@ export const HMR_OUTPUT_PATH = 'hmr'
|
|
|
2
2
|
|
|
3
3
|
export const HMR_PATCH_ASSET = `${HMR_OUTPUT_PATH}/patch.js`
|
|
4
4
|
|
|
5
|
+
export const HMR_PATCH_STYLE_ASSET = `${HMR_OUTPUT_PATH}/patch.wxss`
|
|
6
|
+
|
|
5
7
|
export const DEFAULT_HMR_PATCH_HISTORY_LIMIT = 20
|
|
6
8
|
|
|
7
9
|
export const DEFAULT_DEBUG_NAMESPACES = 'taro-hmr-compile:*'
|
package/src/index.ts
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import type {IPluginContext} from '@tarojs/service'
|
|
2
2
|
import type {AppConfig} from '@tarojs/taro'
|
|
3
|
-
import {HMR_OUTPUT_PATH, DEFAULT_DEBUG_NAMESPACES} from './constants'
|
|
3
|
+
import {HMR_OUTPUT_PATH, HMR_PATCH_ASSET, HMR_PATCH_STYLE_ASSET, DEFAULT_DEBUG_NAMESPACES} from './constants'
|
|
4
4
|
import {getAppConfig} from './utils'
|
|
5
5
|
import {HmrCompilerPlugin} from './compile/hmr-compiler-plugin'
|
|
6
6
|
import {HmrPatchPlugin} from './patch/hmr-patch-plugin'
|
|
7
7
|
import {HmrServerPlugin} from './server/hmr-server-plugin'
|
|
8
|
-
import {
|
|
8
|
+
import {findAvailablePort} from '@taro-hmr-compile/core/compiler'
|
|
9
|
+
import {HmrOutputPlugin} from './compile/hmr-output-plugin'
|
|
9
10
|
import {enableLog} from './log'
|
|
10
11
|
|
|
12
|
+
const CUSTOM_WRAPPER = 'custom-wrapper'
|
|
11
13
|
|
|
12
14
|
export interface WebpackHmrCompileOpt {
|
|
13
15
|
host: string
|
|
14
16
|
port: number
|
|
15
17
|
patchHistoryLimit: number
|
|
16
18
|
debug: boolean | string
|
|
19
|
+
/** 相对于 sourceRoot 的页面路径(不含扩展名),支持开头 /,末尾 / 表示目录。 */
|
|
20
|
+
lazyCompileExclude: string[]
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export default (ctx: IPluginContext, pluginOpts: Partial<WebpackHmrCompileOpt>) => {
|
|
20
24
|
if (!ctx.runOpts.options?.isWatch) return
|
|
21
25
|
|
|
22
|
-
const {appPath, sourcePath
|
|
26
|
+
const {appPath, sourcePath} = ctx.paths
|
|
23
27
|
|
|
24
28
|
const {framework} = ctx.initialConfig
|
|
25
29
|
|
|
@@ -36,7 +40,10 @@ export default (ctx: IPluginContext, pluginOpts: Partial<WebpackHmrCompileOpt>)
|
|
|
36
40
|
|
|
37
41
|
if (debugNamespaces) enableLog(debugNamespaces)
|
|
38
42
|
|
|
39
|
-
ctx.
|
|
43
|
+
ctx.modifyComponentConfig(({componentConfig}) => {
|
|
44
|
+
if (componentConfig.thirdPartyComponents.has(CUSTOM_WRAPPER)) return
|
|
45
|
+
componentConfig.thirdPartyComponents.set(CUSTOM_WRAPPER, new Set())
|
|
46
|
+
})
|
|
40
47
|
|
|
41
48
|
ctx.modifyRunnerOpts(({opts}) => {
|
|
42
49
|
opts.output ??= {}
|
|
@@ -50,6 +57,8 @@ export default (ctx: IPluginContext, pluginOpts: Partial<WebpackHmrCompileOpt>)
|
|
|
50
57
|
|
|
51
58
|
if (port !== preferredPort) console.warn(`[taro-hmr-compile] 端口 ${preferredPort} 已被占用,自动使用端口 ${port}`)
|
|
52
59
|
|
|
60
|
+
chain.optimization.usedExports(false)
|
|
61
|
+
|
|
53
62
|
chain.module
|
|
54
63
|
.rule('lazy-compiler')
|
|
55
64
|
.test(/\.[jt]sx?$/)
|
|
@@ -57,7 +66,7 @@ export default (ctx: IPluginContext, pluginOpts: Partial<WebpackHmrCompileOpt>)
|
|
|
57
66
|
.end()
|
|
58
67
|
.use('lazy-compiler-loader')
|
|
59
68
|
.loader(require.resolve('./compile/lazy-compiler-loader'))
|
|
60
|
-
.options({appPath, sourcePath, appConfig, framework, compileUrl})
|
|
69
|
+
.options({appPath, sourcePath, appConfig, framework, compileUrl, lazyCompileExclude: pluginOpts.lazyCompileExclude})
|
|
61
70
|
.end()
|
|
62
71
|
|
|
63
72
|
chain.output
|
|
@@ -66,12 +75,17 @@ export default (ctx: IPluginContext, pluginOpts: Partial<WebpackHmrCompileOpt>)
|
|
|
66
75
|
|
|
67
76
|
const {patchHistoryLimit} = pluginOpts
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
chain.plugin('hmr-compiler-plugin').use(HmrCompilerPlugin, [{
|
|
79
|
+
paths: ctx.paths,
|
|
80
|
+
appConfig,
|
|
81
|
+
debugNamespaces
|
|
82
|
+
}])
|
|
73
83
|
chain.plugin('hmr-plugin').use(webpack.HotModuleReplacementPlugin)
|
|
74
84
|
|
|
85
|
+
chain.plugin('hmr-output-plugin').use(HmrOutputPlugin, [{
|
|
86
|
+
deferredAssets: [HMR_PATCH_STYLE_ASSET, HMR_PATCH_ASSET, 'app.wxss'],
|
|
87
|
+
}])
|
|
88
|
+
|
|
75
89
|
chain.plugin('hmr-patch-plugin').use(HmrPatchPlugin, [{patchHistoryLimit}])
|
|
76
90
|
|
|
77
91
|
chain.plugin('hmr-server-plugin').use(HmrServerPlugin, [{host, port}])
|