@taro-hmr-compile/plugin-webpack 0.0.1-alpha.9 → 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.
Files changed (38) hide show
  1. package/README.md +101 -0
  2. package/dist/compile/hmr-compiler-plugin.d.ts +7 -7
  3. package/dist/compile/hmr-compiler-plugin.js +2 -29
  4. package/dist/compile/hmr-compiler-plugin.js.map +1 -1
  5. package/dist/compile/hmr-output-plugin.d.ts +28 -0
  6. package/dist/compile/hmr-output-plugin.js +145 -0
  7. package/dist/compile/hmr-output-plugin.js.map +1 -0
  8. package/dist/compile/hmr-style-plugin.d.ts +25 -0
  9. package/dist/compile/hmr-style-plugin.js +53 -0
  10. package/dist/compile/hmr-style-plugin.js.map +1 -0
  11. package/dist/compile/lazy-compiler-loader.d.ts +1 -0
  12. package/dist/compile/lazy-compiler-loader.js +10 -2
  13. package/dist/compile/lazy-compiler-loader.js.map +1 -1
  14. package/dist/constants.d.ts +1 -0
  15. package/dist/constants.js +2 -1
  16. package/dist/constants.js.map +1 -1
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +17 -6
  19. package/dist/index.js.map +1 -1
  20. package/dist/patch/hmr-patch-plugin.d.ts +6 -0
  21. package/dist/patch/hmr-patch-plugin.js +53 -4
  22. package/dist/patch/hmr-patch-plugin.js.map +1 -1
  23. package/dist/patch/hmr-patch-processor.d.ts +2 -1
  24. package/dist/patch/hmr-patch-processor.js +37 -21
  25. package/dist/patch/hmr-patch-processor.js.map +1 -1
  26. package/dist/patch/utils.js +3 -3
  27. package/dist/patch/utils.js.map +1 -1
  28. package/package.json +4 -2
  29. package/src/compile/hmr-compiler-plugin.ts +9 -34
  30. package/src/compile/hmr-output-plugin.ts +158 -0
  31. package/src/compile/hmr-style-plugin.ts +70 -0
  32. package/src/compile/lazy-compiler-loader.ts +11 -2
  33. package/src/constants.ts +2 -0
  34. package/src/index.ts +21 -9
  35. package/src/patch/hmr-patch-plugin.ts +53 -5
  36. package/src/patch/hmr-patch-processor.ts +45 -27
  37. package/src/patch/utils.ts +3 -3
  38. package/src/types/taro-service.d.ts +15 -0
@@ -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
+ }
@@ -38,19 +38,28 @@ interface LoaderOptions {
38
38
  appConfig: AppConfig
39
39
  framework: 'react'
40
40
  compileUrl: string
41
+ lazyCompileExclude?: string[]
41
42
  }
42
43
 
43
44
  const lazyCompilerLoader: LoaderDefinitionFunction<LoaderOptions> = function (source) {
44
- const {appPath, framework, appConfig, sourcePath, compileUrl} = this.getOptions()
45
+ const {appPath, framework, appConfig, sourcePath, compileUrl, lazyCompileExclude = []} = this.getOptions()
45
46
 
46
47
  const {dir: resourceDir, name: resourceName} = path.parse(path.relative(sourcePath, this.resourcePath))
47
48
 
48
- const resourcePath = path.join(resourceDir, resourceName)
49
+ const resourcePath = path.join(resourceDir, resourceName).replace(/\\/g, '/')
49
50
 
50
51
  const isPageEntry = getAllPages(appConfig).includes(resourcePath)
51
52
 
52
53
  if (!isPageEntry || isPageCompiled(appPath, resourcePath)) return source
53
54
 
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
+
54
63
  return createShell({page: resourcePath, framework, compileUrl, memo: isMemoPage(source)})
55
64
  }
56
65
 
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 {emptyDirFile, findAvailablePort} from '@taro-hmr-compile/core/compiler'
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, outputPath} = ctx.paths
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.onBuildStart(() => emptyDirFile(outputPath))
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 ??= {}
@@ -59,7 +66,7 @@ export default (ctx: IPluginContext, pluginOpts: Partial<WebpackHmrCompileOpt>)
59
66
  .end()
60
67
  .use('lazy-compiler-loader')
61
68
  .loader(require.resolve('./compile/lazy-compiler-loader'))
62
- .options({appPath, sourcePath, appConfig, framework, compileUrl})
69
+ .options({appPath, sourcePath, appConfig, framework, compileUrl, lazyCompileExclude: pluginOpts.lazyCompileExclude})
63
70
  .end()
64
71
 
65
72
  chain.output
@@ -68,12 +75,17 @@ export default (ctx: IPluginContext, pluginOpts: Partial<WebpackHmrCompileOpt>)
68
75
 
69
76
  const {patchHistoryLimit} = pluginOpts
70
77
 
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
-
78
+ chain.plugin('hmr-compiler-plugin').use(HmrCompilerPlugin, [{
79
+ paths: ctx.paths,
80
+ appConfig,
81
+ debugNamespaces
82
+ }])
75
83
  chain.plugin('hmr-plugin').use(webpack.HotModuleReplacementPlugin)
76
84
 
85
+ chain.plugin('hmr-output-plugin').use(HmrOutputPlugin, [{
86
+ deferredAssets: [HMR_PATCH_STYLE_ASSET, HMR_PATCH_ASSET, 'app.wxss'],
87
+ }])
88
+
77
89
  chain.plugin('hmr-patch-plugin').use(HmrPatchPlugin, [{patchHistoryLimit}])
78
90
 
79
91
  chain.plugin('hmr-server-plugin').use(HmrServerPlugin, [{host, port}])
@@ -1,6 +1,7 @@
1
1
  import type {Compilation, Compiler} from 'webpack'
2
2
  import {RawSource} from 'webpack-sources'
3
- import {HMR_PATCH_ASSET} from '../constants'
3
+ import path from 'node:path'
4
+ import {HMR_PATCH_ASSET, HMR_PATCH_STYLE_ASSET} from '../constants'
4
5
  import {HmrCompilerPlugin} from '../compile/hmr-compiler-plugin'
5
6
  import {HmrPatchRuntimePlugin} from './hmr-patch-runtime-plugin'
6
7
  import {HmrPatchProcessor, HmrPatchProcessorOptions} from "./hmr-patch-processor";
@@ -10,6 +11,7 @@ const HOT_UPDATE_MANIFEST_SUFFIX = '.hot-update.json'
10
11
  const HOT_UPDATE_SCRIPT_SUFFIX = '.hot-update.js'
11
12
  const STYLE_ASSET_PATTERN = /\.(?:css|wxss|acss|ttss|qss|jxss)(?:\.map)?$/i
12
13
  const TEMPLATE_RUNTIME_ASSET_PATTERN = /(?:^|\/)(?:base\.wxml|utils\.wxs)$/i
14
+ const STYLE_ENTRY_ASSET = 'app.wxss'
13
15
 
14
16
  type HmrPatchPluginOptions = Partial<HmrPatchProcessorOptions>
15
17
 
@@ -25,6 +27,8 @@ export class HmrPatchPlugin {
25
27
 
26
28
  private readonly patchProcessor: HmrPatchProcessor
27
29
 
30
+ private readonly preservedAssets = new Set([HMR_PATCH_ASSET, STYLE_ENTRY_ASSET, HMR_PATCH_STYLE_ASSET])
31
+
28
32
  private readonly emittedAssetSources = new Map<string, string>()
29
33
 
30
34
  constructor(options: HmrPatchPluginOptions) {
@@ -35,13 +39,18 @@ export class HmrPatchPlugin {
35
39
  new HmrPatchRuntimePlugin().apply(compiler)
36
40
 
37
41
  compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
38
- HmrCompilerPlugin.getCompilationHooks(compilation).chunkModulesRendered.tap(PLUGIN_NAME, (source, isBootstrapChunk) => {
39
- this.patchProcessor.processChunkModules({compilation, source, isBootstrapChunk})
42
+ HmrCompilerPlugin.getCompilationHooks(compilation).chunkModulesRendered.tap(PLUGIN_NAME, options => {
43
+ this.patchProcessor.processChunkModules({compilation, ...options})
40
44
  })
41
45
 
42
46
  // 等 Webpack 生成完整的 hot-update assets 后,再统一转换成固定的 patch.js。
43
47
  const stage = compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT
44
- compilation.hooks.processAssets.tap({name: PLUGIN_NAME, stage}, () => this.processAssets(compilation))
48
+ compilation.hooks.processAssets.tap({name: PLUGIN_NAME, stage}, () => {
49
+ if (compilation.errors.length > 0 || !compilation.hash) return
50
+ this.processStyleAssets(compilation)
51
+ this.processAssets(compilation)
52
+ })
53
+ this.removeHotUpdateAssets(compilation)
45
54
  })
46
55
 
47
56
  compiler.hooks.afterEmit.tap(PLUGIN_NAME, compilation => {
@@ -82,10 +91,40 @@ export class HmrPatchPlugin {
82
91
  this.previousHash = toHash
83
92
  }
84
93
 
94
+ /** 将 Webpack 汇总后的 App 样式放入固定 patch 文件,App 入口只保留稳定引用。 */
95
+ private processStyleAssets(compilation: Compilation) {
96
+ const entryAsset = compilation.getAsset(STYLE_ENTRY_ASSET)
97
+ const source = entryAsset?.source.source().toString()
98
+ if (!source) return
99
+
100
+ const importPath = path.posix.relative(path.posix.dirname(STYLE_ENTRY_ASSET), HMR_PATCH_STYLE_ASSET)
101
+ const patchImport = importPath.startsWith('.') ? importPath : `./${importPath}`
102
+ const alreadyPatched = source.trim() === `@import "${patchImport}";` || source.trim() === `@import '${patchImport}';`
103
+
104
+ if (!alreadyPatched) {
105
+ const patchDirectory = path.posix.dirname(HMR_PATCH_STYLE_ASSET)
106
+ const sourceDirectory = path.posix.dirname(STYLE_ENTRY_ASSET)
107
+ const patchSource = source.replace(/(@import\s+)(["'])([^"']+)\2/g, (match, prefix: string, quote: string, request: string) => {
108
+ if (/^(?:[a-z]+:|\/|#)/i.test(request)) return match
109
+ const resolved = path.posix.normalize(path.posix.join(sourceDirectory, request))
110
+ const relative = path.posix.relative(patchDirectory, resolved)
111
+ return `${prefix}${quote}${relative.startsWith('.') ? relative : `./${relative}`}${quote}`
112
+ })
113
+ this.emitAsset(compilation, HMR_PATCH_STYLE_ASSET, new RawSource(patchSource))
114
+ }
115
+
116
+ this.emitAsset(compilation, STYLE_ENTRY_ASSET, new RawSource(`@import ${JSON.stringify(patchImport)};\n`))
117
+ }
118
+
119
+ private emitAsset(compilation: Compilation, assetName: string, source: RawSource) {
120
+ if (compilation.getAsset(assetName)) compilation.updateAsset(assetName, source)
121
+ else compilation.emitAsset(assetName, source)
122
+ }
123
+
85
124
  /** 热更新转换完成后,保留固定 patch 与发生变化的模板、平台样式资产。 */
86
125
  private removeSupersededAssets(compilation: Compilation) {
87
126
  compilation.getAssets().forEach(asset => {
88
- if (asset.name === HMR_PATCH_ASSET) return
127
+ if (this.preservedAssets.has(asset.name)) return
89
128
  if (!isPassthroughAsset(asset.name)) return compilation.deleteAsset(asset.name)
90
129
 
91
130
  const source = asset.source.source().toString()
@@ -108,4 +147,13 @@ export class HmrPatchPlugin {
108
147
  this.emittedAssetSources.clear()
109
148
  this.patchProcessor.reset()
110
149
  }
150
+ /** 补丁已从 hot-update assets 生成后,在 Webpack 写盘前从本次 compilation 移除它们。 */
151
+ private removeHotUpdateAssets(compilation: Compilation) {
152
+ compilation.getAssets().forEach(asset => {
153
+ if (asset.name.endsWith(HOT_UPDATE_MANIFEST_SUFFIX) || asset.name.endsWith(HOT_UPDATE_SCRIPT_SUFFIX)) {
154
+ compilation.deleteAsset(asset.name)
155
+ }
156
+ })
157
+ }
158
+
111
159
  }
@@ -1,23 +1,18 @@
1
- import {type Compilation,} from "webpack";
2
- import generator from '@babel/generator'
1
+ import {type Chunk, type Compilation,} from "webpack";
3
2
  import {
4
3
  parseHotUpdateAssets,
5
4
  ParseHotUpdateAssetsOptions,
6
5
  parseChunkAsset, mergeUpdatePatches, generatePatchAssetSource,
7
6
  } from "./utils";
8
- import {createHash} from "crypto";
7
+ import isNil from 'lodash/isNil'
9
8
  import {ModuleFactory, UpdatePatchSource} from "./types";
10
9
  import {DEFAULT_HMR_PATCH_HISTORY_LIMIT} from "../constants";
11
10
 
12
- interface ModuleHash {
13
- moduleId: string;
14
- factoryHash: string;
15
- }
16
-
17
11
  interface ProcessChunkModulesOptions {
18
12
  source: string
19
13
  compilation: Compilation,
20
- isBootstrapChunk: boolean
14
+ isBootstrapChunk: boolean,
15
+ chunk: Chunk
21
16
  }
22
17
 
23
18
  export interface HmrPatchProcessorOptions {
@@ -31,7 +26,14 @@ interface ProcessUpdateOptions extends ParseHotUpdateAssetsOptions {
31
26
 
32
27
  type BootstrapFactories = Map<string, ModuleFactory>
33
28
 
34
- type ModuleHashesByFullHash = Map<string, Map<string, ModuleHash>>
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
+ }
35
37
 
36
38
  export class HmrPatchProcessor {
37
39
  private readonly bootstrapFactories: BootstrapFactories = new Map()
@@ -62,9 +64,9 @@ export class HmrPatchProcessor {
62
64
 
63
65
  const changedModuleIds = new Set<string>()
64
66
  moduleIds.forEach(moduleId => {
65
- const fromFactoryHash = fromModuleHashes.get(moduleId)?.factoryHash
66
- const toFactoryHash = toModuleHashes.get(moduleId)?.factoryHash
67
- if (fromFactoryHash !== toFactoryHash) changedModuleIds.add(moduleId)
67
+ const fromModuleHash = fromModuleHashes.get(moduleId)
68
+ const toModuleHash = toModuleHashes.get(moduleId)
69
+ if (!areModuleHashesEqual(fromModuleHash, toModuleHash)) changedModuleIds.add(moduleId)
68
70
  })
69
71
 
70
72
  return changedModuleIds
@@ -105,28 +107,44 @@ export class HmrPatchProcessor {
105
107
  }
106
108
 
107
109
  processChunkModules(opt: ProcessChunkModulesOptions) {
108
- const {hash: fullHash} = opt.compilation
109
- const modules = parseChunkAsset(opt.source)
110
+ const {chunk, compilation, isBootstrapChunk, source} = opt
111
+ const {hash: fullHash} = compilation
112
+ const modules = parseChunkAsset(source)
110
113
  modules.forEach(moduleFactory => {
111
114
  const {moduleId, factory} = moduleFactory
112
115
 
113
- if (opt.isBootstrapChunk) {
114
- const compilationFactories = this.bootstrapFactoriesByCompilation.get(opt.compilation) ?? new Map()
116
+ if (isBootstrapChunk) {
117
+ const compilationFactories = this.bootstrapFactoriesByCompilation.get(compilation) ?? new Map()
115
118
  compilationFactories.set(moduleId, {moduleId, factory})
116
- this.bootstrapFactoriesByCompilation.set(opt.compilation, compilationFactories)
119
+ this.bootstrapFactoriesByCompilation.set(compilation, compilationFactories)
117
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
118
137
 
119
- if (!fullHash) return
138
+ const normalizedModuleId = String(moduleId)
120
139
 
121
- const moduleHashes = (() => {
122
- const hasCurFullHashModuleHashMap = this.moduleHashesByFullHash.has(fullHash)
123
- if (!hasCurFullHashModuleHashMap) this.moduleHashesByFullHash.set(fullHash, new Map())
124
- return this.moduleHashesByFullHash.get(fullHash)!
125
- })()
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)
126
144
 
127
- const {code: source} = generator(factory, {compact: true, comments: false, minified: true,})
128
- const factoryHash = createHash('sha256').update(source).digest('hex')
129
- moduleHashes.set(moduleId, {moduleId, factoryHash})
145
+ const hashes = moduleHashes.get(normalizedModuleId) ?? new Set<string>()
146
+ hashes.add(moduleHash)
147
+ moduleHashes.set(normalizedModuleId, hashes)
130
148
  })
131
149
  }
132
150
 
@@ -17,7 +17,7 @@ const parseModuleFactories = (expression: ObjectExpression) => {
17
17
 
18
18
  const moduleFactories = new Map<string, ModuleFactory>()
19
19
 
20
- if (!t.isObjectExpression(expression)) return moduleFactories.values()
20
+ if (!t.isObjectExpression(expression)) return Array.from(moduleFactories.values())
21
21
 
22
22
  expression.properties.forEach(property => {
23
23
  if (!t.isObjectProperty(property) || !t.isExpression(property.value)) throw new Error('[taro-hmr-compile] HMR 模块对象包含无法识别的属性')
@@ -33,7 +33,7 @@ const parseModuleFactories = (expression: ObjectExpression) => {
33
33
  moduleFactories.set(moduleId, {factory, moduleId})
34
34
  })
35
35
 
36
- return moduleFactories.values()
36
+ return Array.from(moduleFactories.values())
37
37
  }
38
38
 
39
39
 
@@ -128,7 +128,7 @@ export const mergeUpdatePatches = (basePatch: UpdatePatchSource, nextPatch: Upda
128
128
  type: 'update',
129
129
  fromHash: basePatch.fromHash,
130
130
  toHash: nextPatch.toHash,
131
- moduleFactories: [...moduleFactories.values()],
131
+ moduleFactories: Array.from(moduleFactories.values()),
132
132
  runtimeModules: [...basePatch.runtimeModules, ...nextPatch.runtimeModules],
133
133
  removedModules: [...removedModules],
134
134
  }
@@ -0,0 +1,15 @@
1
+ import type { IProjectConfig } from '@tarojs/taro/types/compile'
2
+ import type { IComponentConfig } from '@tarojs/taro/types/compile/hooks'
3
+
4
+ declare module '@tarojs/service' {
5
+ interface IPluginContext {
6
+ modifyComponentConfig: (
7
+ callback: (args: {
8
+ componentConfig: IComponentConfig
9
+ config: IProjectConfig
10
+ }) => void,
11
+ ) => void
12
+ }
13
+ }
14
+
15
+ export {}