vite-plugin-taro 0.2.2 → 0.3.0

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 (133) hide show
  1. package/README.md +83 -106
  2. package/README.zh.md +83 -104
  3. package/dist/node/build-context.d.ts +25 -0
  4. package/dist/node/build-context.js +48 -0
  5. package/dist/node/css/css-pipeline.d.ts +21 -0
  6. package/dist/node/css/css-pipeline.js +129 -0
  7. package/dist/node/plugins/conditional-directives.d.ts +3 -0
  8. package/dist/node/plugins/conditional-directives.js +62 -0
  9. package/dist/node/plugins/taro-runtime.d.ts +5 -0
  10. package/dist/node/plugins/taro-runtime.js +18 -0
  11. package/dist/node/targets/h5/plugin.d.ts +6 -0
  12. package/dist/node/targets/h5/plugin.js +117 -0
  13. package/dist/node/targets/h5/virtual-modules.d.ts +5 -0
  14. package/dist/node/targets/h5/virtual-modules.js +60 -0
  15. package/dist/node/targets/wx/companion-assets.d.ts +18 -0
  16. package/dist/node/targets/wx/companion-assets.js +121 -0
  17. package/dist/node/targets/wx/dev-server/development-session.d.ts +31 -0
  18. package/dist/node/targets/wx/dev-server/development-session.js +202 -0
  19. package/dist/node/targets/wx/dev-server/full-build-scheduler.d.ts +14 -0
  20. package/dist/node/targets/wx/dev-server/full-build-scheduler.js +60 -0
  21. package/dist/node/targets/wx/dev-server/js-utils.d.ts +5 -0
  22. package/dist/node/targets/wx/dev-server/js-utils.js +28 -0
  23. package/dist/node/targets/wx/dev-server/output.d.ts +19 -0
  24. package/dist/node/targets/wx/dev-server/output.js +44 -0
  25. package/dist/node/targets/wx/dev-server/rolldown-runtime-source.d.ts +8 -0
  26. package/dist/node/targets/wx/dev-server/rolldown-runtime-source.js +95 -0
  27. package/dist/node/targets/wx/dev-server/update-server-state.d.ts +65 -0
  28. package/dist/node/targets/wx/dev-server/update-server-state.js +116 -0
  29. package/dist/node/targets/wx/dev-server/update-transport.d.ts +28 -0
  30. package/dist/node/targets/wx/dev-server/update-transport.js +212 -0
  31. package/dist/node/targets/wx/dev-server/vite-bundled-dev-adapter.d.ts +44 -0
  32. package/dist/node/targets/wx/dev-server/vite-bundled-dev-adapter.js +145 -0
  33. package/dist/node/targets/wx/development-files.d.ts +8 -0
  34. package/dist/node/targets/wx/development-files.js +8 -0
  35. package/dist/node/targets/wx/plugin.d.ts +6 -0
  36. package/dist/node/targets/wx/plugin.js +143 -0
  37. package/dist/node/targets/wx/react-refresh.d.ts +4 -0
  38. package/dist/node/targets/wx/react-refresh.js +38 -0
  39. package/dist/node/targets/wx/virtual-modules.d.ts +16 -0
  40. package/dist/node/targets/wx/virtual-modules.js +120 -0
  41. package/dist/node/utils/async.d.ts +8 -0
  42. package/dist/node/utils/async.js +16 -0
  43. package/dist/node/utils/filesystem.d.ts +13 -0
  44. package/dist/node/utils/filesystem.js +43 -0
  45. package/dist/{vite/utils.d.ts → node/utils/modules.d.ts} +2 -2
  46. package/dist/{vite/utils.js → node/utils/modules.js} +10 -3
  47. package/dist/node/utils/packages.d.ts +2 -0
  48. package/dist/node/utils/packages.js +8 -0
  49. package/dist/node/vite-plugin.d.ts +4 -0
  50. package/dist/node/vite-plugin.js +33 -0
  51. package/dist/options.d.ts +21 -0
  52. package/dist/{shim/h5.d.ts → runtime/h5/taro-runtime.d.ts} +3 -1
  53. package/dist/runtime/h5/taro-runtime.js +9 -0
  54. package/dist/runtime/taro/api.d.ts +5 -0
  55. package/dist/{virtual → runtime/taro}/api.js +3 -0
  56. package/dist/runtime/taro/components.d.ts +2 -0
  57. package/dist/runtime/taro/components.js +2 -0
  58. package/dist/runtime/wx/page-update.d.ts +6 -0
  59. package/dist/runtime/wx/page-update.js +157 -0
  60. package/dist/{shim/wx.d.ts → runtime/wx/taro-runtime.d.ts} +1 -0
  61. package/dist/{shim/wx.js → runtime/wx/taro-runtime.js} +1 -0
  62. package/dist/runtime/wx/update-client-state.d.ts +68 -0
  63. package/dist/runtime/wx/update-client-state.js +84 -0
  64. package/dist/runtime/wx/update-client.d.ts +1 -0
  65. package/dist/runtime/wx/update-client.js +170 -0
  66. package/dist/vite.d.ts +2 -2
  67. package/dist/vite.js +1 -1
  68. package/package.json +15 -11
  69. package/src/node/build-context.ts +63 -0
  70. package/src/node/css/css-pipeline.ts +147 -0
  71. package/src/node/plugins/conditional-directives.ts +76 -0
  72. package/src/node/plugins/taro-runtime.ts +22 -0
  73. package/src/node/targets/h5/plugin.ts +135 -0
  74. package/src/node/targets/h5/virtual-modules.ts +71 -0
  75. package/src/node/targets/wx/companion-assets.ts +160 -0
  76. package/src/node/targets/wx/dev-server/development-session.ts +253 -0
  77. package/src/node/targets/wx/dev-server/full-build-scheduler.ts +62 -0
  78. package/src/node/targets/wx/dev-server/js-utils.ts +33 -0
  79. package/src/node/targets/wx/dev-server/output.ts +56 -0
  80. package/src/node/targets/wx/dev-server/rolldown-runtime-source.ts +95 -0
  81. package/src/node/targets/wx/dev-server/update-server-state.ts +183 -0
  82. package/src/node/targets/wx/dev-server/update-transport.ts +261 -0
  83. package/src/node/targets/wx/dev-server/vite-bundled-dev-adapter.ts +203 -0
  84. package/src/node/targets/wx/development-files.ts +11 -0
  85. package/src/node/targets/wx/plugin.ts +163 -0
  86. package/src/node/targets/wx/react-refresh.ts +51 -0
  87. package/src/node/targets/wx/virtual-modules.ts +152 -0
  88. package/src/node/utils/async.ts +19 -0
  89. package/src/node/utils/filesystem.ts +45 -0
  90. package/src/{vite/utils.ts → node/utils/modules.ts} +11 -3
  91. package/src/node/utils/packages.ts +10 -0
  92. package/src/node/vite-plugin.ts +41 -0
  93. package/src/options.ts +25 -0
  94. package/src/runtime/h5/taro-css.d.ts +3 -0
  95. package/src/runtime/h5/taro-runtime.ts +10 -0
  96. package/src/{virtual → runtime/taro}/api.ts +3 -0
  97. package/src/runtime/taro/components.ts +2 -0
  98. package/src/runtime/wx/page-update.ts +230 -0
  99. package/src/runtime/wx/react-refresh.d.ts +20 -0
  100. package/src/{shim/wx.ts → runtime/wx/taro-runtime.ts} +1 -0
  101. package/src/runtime/wx/update-client-state.ts +138 -0
  102. package/src/runtime/wx/update-client.ts +222 -0
  103. package/src/vite.ts +2 -2
  104. package/dist/shim/h5.js +0 -5
  105. package/dist/virtual/api.d.ts +0 -3
  106. package/dist/virtual/components.d.ts +0 -1
  107. package/dist/virtual/components.js +0 -1
  108. package/dist/vite/constants.d.ts +0 -4
  109. package/dist/vite/constants.js +0 -8
  110. package/dist/vite/plugins.d.ts +0 -8
  111. package/dist/vite/plugins.js +0 -186
  112. package/dist/vite/tailwindcss.d.ts +0 -3
  113. package/dist/vite/tailwindcss.js +0 -35
  114. package/dist/vite/targets/h5.d.ts +0 -31
  115. package/dist/vite/targets/h5.js +0 -201
  116. package/dist/vite/targets/wx.d.ts +0 -74
  117. package/dist/vite/targets/wx.js +0 -362
  118. package/dist/vite/types.d.ts +0 -38
  119. package/dist/vite/virtual-modules.d.ts +0 -3
  120. package/dist/vite/virtual-modules.js +0 -13
  121. package/dist/vite/vite-plugin-taro.d.ts +0 -7
  122. package/dist/vite/vite-plugin-taro.js +0 -80
  123. package/src/shim/h5.ts +0 -6
  124. package/src/virtual/components.ts +0 -1
  125. package/src/vite/constants.ts +0 -12
  126. package/src/vite/plugins.ts +0 -218
  127. package/src/vite/tailwindcss.ts +0 -41
  128. package/src/vite/targets/h5.ts +0 -229
  129. package/src/vite/targets/wx.ts +0 -436
  130. package/src/vite/types.ts +0 -48
  131. package/src/vite/virtual-modules.ts +0 -14
  132. package/src/vite/vite-plugin-taro.ts +0 -106
  133. /package/dist/{vite/types.js → options.js} +0 -0
@@ -0,0 +1,160 @@
1
+ import path from 'node:path'
2
+ import { recursiveMerge } from '@tarojs/helper'
3
+ import { Weapp as WxPlatform } from '@tarojs/plugin-platform-weapp'
4
+ import type { JsonObject } from '../../../options.ts'
5
+ import type { BuildContext } from '../../build-context.ts'
6
+ import { normalizeModuleId } from '../../utils/modules.ts'
7
+ import { packageRequire } from '../../utils/packages.ts'
8
+
9
+ type WxAssetSource = string | Uint8Array
10
+
11
+ export type WxBundle = Record<
12
+ string,
13
+ {
14
+ type: 'asset' | 'chunk'
15
+ source?: WxAssetSource
16
+ modules?: Record<string, { renderedExports?: string[] }>
17
+ }
18
+ >
19
+
20
+ type WxAssetEmitter = {
21
+ emitFile(asset: { type: 'asset'; fileName: string; source: WxAssetSource }): string
22
+ }
23
+
24
+ type WxTemplateComponentConfig = {
25
+ includes: Set<string>
26
+ exclude: Set<string>
27
+ thirdPartyComponents: Map<string, Set<string>>
28
+ includeAll: boolean
29
+ }
30
+
31
+ const taroWxComponentsPath = packageRequire.resolve('@tarojs/plugin-platform-weapp/dist/components-react')
32
+ const templateBuilder = createWxTemplateBuilder()
33
+
34
+ export function emitWxCompanionAssets(emitter: WxAssetEmitter, bundle: WxBundle, context: BuildContext): void {
35
+ for (const asset of createWxCompanionAssets(bundle, context)) {
36
+ emitter.emitFile({ type: 'asset', fileName: asset.fileName, source: asset.source })
37
+ }
38
+ }
39
+
40
+ function createWxCompanionAssets(
41
+ bundle: WxBundle,
42
+ context: BuildContext
43
+ ): { fileName: string; source: WxAssetSource }[] {
44
+ const json = (value: JsonObject) => (context.development ? JSON.stringify(value, null, 2) : JSON.stringify(value))
45
+
46
+ return [
47
+ { fileName: 'app.json', source: json(context.project.appConfig) },
48
+ { fileName: 'app.wxss', source: collectWxBundleStyles(bundle) },
49
+ {
50
+ fileName: 'base.wxml',
51
+ source: templateBuilder.buildTemplate(collectWxTemplateComponentConfig(bundle))
52
+ },
53
+ { fileName: 'utils.wxs', source: templateBuilder.buildXScript() },
54
+ { fileName: 'comp.wxml', source: templateBuilder.buildBaseComponentTemplate('.wxml') },
55
+ { fileName: 'comp.json', source: json(createWxComponentConfig()) },
56
+ { fileName: 'project.config.json', source: json(createWxProjectConfig(context)) },
57
+ { fileName: 'project.private.config.json', source: json(context.project.projectPrivateConfigJson) },
58
+ { fileName: 'sitemap.json', source: json(context.project.sitemapJson) },
59
+ ...context.project.pages.flatMap((page) => [
60
+ {
61
+ fileName: `${page.path}.wxml`,
62
+ source: templateBuilder.buildPageTemplate(relativeWxRootAsset(page.path, 'base.wxml'), {
63
+ content: page.config,
64
+ path: page.path
65
+ })
66
+ },
67
+ {
68
+ fileName: `${page.path}.json`,
69
+ source: json({
70
+ ...page.config,
71
+ usingComponents: { comp: relativeWxRootAsset(page.path, 'comp') }
72
+ })
73
+ },
74
+ { fileName: `${page.path}.wxss`, source: '' }
75
+ ])
76
+ ]
77
+ }
78
+
79
+ function createWxProjectConfig(context: BuildContext): JsonObject {
80
+ const projectConfig = context.project.projectConfigJson
81
+ const setting = isJsonObject(projectConfig.setting) ? projectConfig.setting : {}
82
+ return {
83
+ ...projectConfig,
84
+ setting: { ...setting, compileHotReLoad: true }
85
+ }
86
+ }
87
+
88
+ function isJsonObject(value: unknown): value is JsonObject {
89
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
90
+ }
91
+
92
+ function createWxTemplateBuilder() {
93
+ const platform = new WxPlatform(
94
+ { helper: { recursiveMerge }, modifyWebpackChain() {}, registerPlatform() {} },
95
+ {},
96
+ {}
97
+ )
98
+ platform.modifyTemplate({})
99
+ return platform.template
100
+ }
101
+
102
+ function relativeWxRootAsset(pagePath: string, rootAsset: string): string {
103
+ const relativePath = path.posix.relative(path.posix.dirname(pagePath), rootAsset)
104
+ return relativePath.startsWith('.') ? relativePath : `./${relativePath}`
105
+ }
106
+
107
+ function collectWxTemplateComponentConfig(bundle: WxBundle): WxTemplateComponentConfig {
108
+ const config: WxTemplateComponentConfig = {
109
+ includes: new Set([
110
+ 'view',
111
+ 'catch-view',
112
+ 'static-view',
113
+ 'pure-view',
114
+ 'click-view',
115
+ 'scroll-view',
116
+ 'image',
117
+ 'static-image',
118
+ 'text',
119
+ 'static-text'
120
+ ]),
121
+ exclude: new Set(),
122
+ thirdPartyComponents: new Map(),
123
+ includeAll: false
124
+ }
125
+ const components = findBundleModule(bundle, taroWxComponentsPath)
126
+ for (const name of components?.renderedExports ?? []) config.includes.add(toDashed(name))
127
+ return config
128
+ }
129
+
130
+ function findBundleModule(bundle: WxBundle, resolvedId: string): { renderedExports?: string[] } | undefined {
131
+ const normalizedResolvedId = normalizeModuleId(resolvedId)
132
+ for (const item of Object.values(bundle)) {
133
+ if (item.type !== 'chunk') continue
134
+ const found = Object.entries(item.modules ?? {}).find(([id]) => normalizeModuleId(id) === normalizedResolvedId)
135
+ if (found) return found[1]
136
+ }
137
+ }
138
+
139
+ function toDashed(value: string): string {
140
+ return value.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
141
+ }
142
+
143
+ function createWxComponentConfig(): JsonObject {
144
+ return {
145
+ component: true,
146
+ styleIsolation: 'apply-shared',
147
+ usingComponents: { comp: './comp' }
148
+ }
149
+ }
150
+
151
+ function collectWxBundleStyles(bundle: WxBundle): string {
152
+ const styles: string[] = []
153
+ for (const [fileName, item] of Object.entries(bundle)) {
154
+ if (item.type !== 'asset' || !fileName.endsWith('.css')) continue
155
+ const source = typeof item.source === 'string' ? item.source : new TextDecoder().decode(item.source)
156
+ if (source) styles.push(source)
157
+ delete bundle[fileName]
158
+ }
159
+ return styles.join('\n')
160
+ }
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Top-level owner of one WX development lifecycle.
3
+ *
4
+ * It serializes every output write, connects DevEngine patches to the update transport, and coalesces conservative full
5
+ * rebuilds. No other module writes plugin-generated WX development files directly.
6
+ */
7
+ import fs from 'node:fs/promises'
8
+ import path from 'node:path'
9
+ import colors from 'picocolors'
10
+ import type { ViteDevServer } from 'vite'
11
+ import type { BuildContext } from '../../../build-context.ts'
12
+ import { SerializedTaskQueue } from '../../../utils/async.ts'
13
+ import {
14
+ copyDirectoryIfExists,
15
+ copyFileOrRemove,
16
+ writeFileAtomically,
17
+ writeFilesAtomically
18
+ } from '../../../utils/filesystem.ts'
19
+ import { wxDevelopmentDirectory, wxUpdateControlFile, wxUpdateFile } from '../development-files.ts'
20
+ import { FullBuildScheduler } from './full-build-scheduler.ts'
21
+ import { transformWxCompatibleJavaScript, transformWxOutputChunks } from './js-utils.ts'
22
+ import { isWxFullBuildOutput, normalizeWxBundleStyles, setWxAppStyles, type WxOutputFile } from './output.ts'
23
+ import { WxUpdateTransport } from './update-transport.ts'
24
+ import { ViteBundledDevAdapter, type WxDevEngineUpdate } from './vite-bundled-dev-adapter.ts'
25
+
26
+ const maxRetainedDeltaCount = 1_000
27
+ const maxRetainedDeltaBytes = 16 * 1024 * 1024
28
+ const fullBuildDebounceDelay = 100
29
+
30
+ type WxDevServerContext = Pick<BuildContext, 'vite' | 'css'>
31
+ type SessionSnapshot = Readonly<{
32
+ lifecycle: 'open' | 'closed'
33
+ outputPhase: 'starting' | 'ready'
34
+ latestAppStyles: string
35
+ }>
36
+
37
+ /** Owns one WX bundled-development graph and all writes to its fixed output directory. */
38
+ export class WxDevelopmentSession {
39
+ private readonly adapter: ViteBundledDevAdapter
40
+ private readonly updateTransport: WxUpdateTransport
41
+ private readonly outDir: string
42
+ private readonly initialBundle = Promise.withResolvers<void>()
43
+ private readonly outputQueue: SerializedTaskQueue
44
+ private readonly fullBuildScheduler: FullBuildScheduler
45
+ private originalPrintUrls: (() => void) | undefined
46
+ private snapshot: SessionSnapshot = { lifecycle: 'open', outputPhase: 'starting', latestAppStyles: '' }
47
+
48
+ constructor(
49
+ private readonly context: WxDevServerContext,
50
+ private readonly server: ViteDevServer
51
+ ) {
52
+ const config = context.vite
53
+ this.outDir = path.resolve(config.root, config.build.outDir)
54
+ this.outputQueue = new SerializedTaskQueue((error) => this.reportOutputError(error))
55
+ this.updateTransport = new WxUpdateTransport(
56
+ server,
57
+ () => this.requestFullBuild(),
58
+ (buildId, source) =>
59
+ this.outputQueue.enqueue(async () => {
60
+ // A full build queued before this write invalidates the old batch instead of letting it overwrite
61
+ // the new build's empty update.js.
62
+ if (!this.updateTransport.isCurrentBuild(buildId)) return
63
+ await writeFileAtomically(path.join(this.outDir, wxUpdateFile), source)
64
+ })
65
+ )
66
+ this.adapter = new ViteBundledDevAdapter(config, server, {
67
+ onOutput: (output) => this.handleBundleOutput(output),
68
+ onPatch: (files, output) => this.handlePatch(files, output),
69
+ onError: (message) => this.handleError(message),
70
+ waitForInitialBundle: () => this.waitForInitialBundle()
71
+ })
72
+ this.fullBuildScheduler = new FullBuildScheduler(
73
+ fullBuildDebounceDelay,
74
+ () => this.adapter.rebuild(),
75
+ (error) => this.reportRebuildError(error)
76
+ )
77
+ }
78
+
79
+ private get config() {
80
+ return this.context.vite
81
+ }
82
+
83
+ install(): void {
84
+ this.updateTransport.install()
85
+ this.server.httpServer?.once('listening', this.handleHttpListening)
86
+ this.adapter.install()
87
+ this.originalPrintUrls = this.server.printUrls.bind(this.server)
88
+ this.server.printUrls = this.printUrls
89
+ if (this.config.publicDir) this.server.watcher.add(this.config.publicDir)
90
+ this.server.watcher.on('change', this.handleWatchedFile)
91
+ this.server.watcher.on('add', this.handleWatchedFile)
92
+ this.server.watcher.on('unlink', this.handleWatchedFile)
93
+ }
94
+
95
+ async close(): Promise<void> {
96
+ if (this.snapshot.lifecycle === 'closed') return
97
+ this.snapshot = { ...this.snapshot, lifecycle: 'closed' }
98
+ this.server.watcher.off('change', this.handleWatchedFile)
99
+ this.server.watcher.off('add', this.handleWatchedFile)
100
+ this.server.watcher.off('unlink', this.handleWatchedFile)
101
+ this.server.httpServer?.off('listening', this.handleHttpListening)
102
+ if (this.server.printUrls === this.printUrls && this.originalPrintUrls) {
103
+ this.server.printUrls = this.originalPrintUrls
104
+ }
105
+ this.updateTransport.close()
106
+ await this.fullBuildScheduler.close()
107
+ await this.outputQueue.waitForIdle()
108
+ }
109
+
110
+ private readonly printUrls = (): void => {
111
+ this.originalPrintUrls?.()
112
+ const projectDirectory = relativeToViteConfig(this.outDir, this.config.configFile, this.config.root)
113
+ this.server.config.logger.info(
114
+ ` ${colors.green('➜')} ${colors.bold('WeChat DevTools')}: ${colors.cyan(projectDirectory)}`
115
+ )
116
+ }
117
+
118
+ private readonly handleHttpListening = (): void => {
119
+ this.outputQueue.enqueue(() =>
120
+ writeFileAtomically(path.join(this.outDir, wxUpdateControlFile), this.updateTransport.createControlSource())
121
+ )
122
+ }
123
+
124
+ private readonly handleWatchedFile = (file: string): void => {
125
+ if (isFileInside(file, this.config.publicDir)) {
126
+ const destination = path.join(this.outDir, path.relative(this.config.publicDir, file))
127
+ this.outputQueue.enqueue(() => copyFileOrRemove(file, destination))
128
+ this.requestFullBuild()
129
+ }
130
+ }
131
+
132
+ private handleBundleOutput(output: WxOutputFile[]): void {
133
+ const appStyles = normalizeWxBundleStyles(output)
134
+ if (appStyles !== undefined) this.snapshot = { ...this.snapshot, latestAppStyles: appStyles }
135
+ if (isWxFullBuildOutput(output) && this.snapshot.latestAppStyles) {
136
+ setWxAppStyles(output, this.snapshot.latestAppStyles)
137
+ }
138
+
139
+ if (!isWxFullBuildOutput(output)) {
140
+ this.outputQueue.enqueue(async () => {
141
+ await transformWxOutputChunks(output)
142
+ await writeDevelopmentOutput(this.outDir, output)
143
+ })
144
+ return
145
+ }
146
+
147
+ const buildId = this.updateTransport.createBuildId()
148
+ setDevelopmentAsset(output, wxUpdateControlFile, this.updateTransport.createControlSource(buildId))
149
+ setDevelopmentAsset(output, wxUpdateFile, 'void 0;\n')
150
+ this.outputQueue.enqueue(async () => {
151
+ await transformWxOutputChunks(output)
152
+ if (this.snapshot.outputPhase === 'starting') {
153
+ // The directory is plugin-owned; clearing it once removes stale files from previous protocol designs.
154
+ await fs.rm(path.join(this.outDir, wxDevelopmentDirectory), { recursive: true, force: true })
155
+ }
156
+ // Invalidate old HTTP reports before writing the new build epoch into the fixed DevTools directory.
157
+ this.updateTransport.commitFullBuild(buildId)
158
+ await writeDevelopmentOutput(this.outDir, output)
159
+ await copyDirectoryIfExists(this.config.publicDir, this.outDir)
160
+ const moduleCount = this.adapter.registerBundleModules(output)
161
+ this.snapshot = { ...this.snapshot, outputPhase: 'ready' }
162
+ this.initialBundle.resolve()
163
+ this.server.config.logger.info(
164
+ `[vite-plugin-taro] WX bundle ready (${moduleCount} modules, ${output.length} files)`
165
+ )
166
+ })
167
+ }
168
+
169
+ private handlePatch(files: string[], output: WxDevEngineUpdate): boolean {
170
+ if (!isSafeJavaScriptPatch(files, output)) {
171
+ this.requestFullBuild()
172
+ return false
173
+ }
174
+
175
+ this.adapter.registerPatchModules(output.code)
176
+ this.outputQueue.enqueue(async () => {
177
+ const transformed = await this.context.css.transformWxClassNames(output.code, output.filename)
178
+ const compatibleCode = await transformWxCompatibleJavaScript(transformed.code, output.filename)
179
+ if (
180
+ this.updateTransport.retainedDeltaCount >= maxRetainedDeltaCount ||
181
+ this.updateTransport.retainedDeltaBytes + Buffer.byteLength(compatibleCode) >= maxRetainedDeltaBytes
182
+ ) {
183
+ this.requestFullBuild()
184
+ return
185
+ }
186
+ this.updateTransport.addDelta(compatibleCode)
187
+ })
188
+ return true
189
+ }
190
+
191
+ private handleError(message: string): void {
192
+ this.server.config.logger.error('[vite-plugin-taro] WX update failed', { error: new Error(message) })
193
+ }
194
+
195
+ private requestFullBuild(): void {
196
+ this.fullBuildScheduler.request()
197
+ }
198
+
199
+ private async waitForInitialBundle(): Promise<void> {
200
+ await this.initialBundle.promise
201
+ await this.outputQueue.waitForIdle()
202
+ }
203
+
204
+ private reportOutputError(error: unknown): void {
205
+ const normalizedError = error instanceof Error ? error : new Error(String(error))
206
+ this.server.config.logger.error(
207
+ `[vite-plugin-taro] WX development output failed: ${normalizedError.stack ?? normalizedError.message}`
208
+ )
209
+ }
210
+
211
+ private reportRebuildError(error: unknown): void {
212
+ const normalizedError = error instanceof Error ? error : new Error(String(error))
213
+ this.server.config.logger.error('[vite-plugin-taro] WX rebuild failed', { error: normalizedError })
214
+ }
215
+ }
216
+
217
+ function writeDevelopmentOutput(outDir: string, output: WxOutputFile[]): Promise<void> {
218
+ return writeFilesAtomically(
219
+ output.map((item) => ({
220
+ file: path.join(outDir, item.fileName),
221
+ source: item.type === 'chunk' ? item.code : item.source
222
+ }))
223
+ )
224
+ }
225
+
226
+ function setDevelopmentAsset(output: WxOutputFile[], fileName: string, source: string): void {
227
+ const index = output.findIndex((item) => item.type === 'asset' && item.fileName === fileName)
228
+ const asset: WxOutputFile = { type: 'asset', fileName, source }
229
+ if (index >= 0) output[index] = asset
230
+ else output.push(asset)
231
+ }
232
+
233
+ function relativeToViteConfig(outDir: string, configFile: string | undefined, root: string): string {
234
+ const configDirectory = configFile ? path.dirname(configFile) : root
235
+ const relativePath = path.relative(configDirectory, outDir).replaceAll('\\', '/')
236
+ if (!relativePath) return '.'
237
+ return relativePath.startsWith('.') ? relativePath : `./${relativePath}`
238
+ }
239
+
240
+ function isFileInside(file: string, directory: string): boolean {
241
+ if (!directory) return false
242
+ const relative = path.relative(directory, file)
243
+ return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative)
244
+ }
245
+
246
+ function isSafeJavaScriptPatch(
247
+ files: string[],
248
+ output: WxDevEngineUpdate
249
+ ): output is Extract<WxDevEngineUpdate, { type: 'Patch' }> {
250
+ if (output.type !== 'Patch' || output.hmrBoundaries.length === 0) return false
251
+ if (output.code.includes('__vite__updateStyle') || output.code.includes('.updateStyle(')) return false
252
+ return files.every((file) => /\.[cm]?[jt]sx?$/.test(file))
253
+ }
@@ -0,0 +1,62 @@
1
+ /** Full-build scheduling owned by one WX development session. */
2
+
3
+ type FullBuildPhase = 'idle' | 'scheduled' | 'building' | 'building-with-pending-request' | 'closed'
4
+
5
+ /** Coalesces trailing-edge requests while guaranteeing one full build at a time. */
6
+ export class FullBuildScheduler {
7
+ private readonly delay: number
8
+ private readonly runBuild: () => Promise<void>
9
+ private readonly reportError: (error: unknown) => void
10
+ private phase: FullBuildPhase = 'idle'
11
+ private timer: NodeJS.Timeout | undefined
12
+ private work: Promise<void> | undefined
13
+
14
+ constructor(delay: number, runBuild: () => Promise<void>, reportError: (error: unknown) => void) {
15
+ this.delay = delay
16
+ this.runBuild = runBuild
17
+ this.reportError = reportError
18
+ }
19
+
20
+ request(): void {
21
+ switch (this.phase) {
22
+ case 'closed':
23
+ return
24
+ case 'building':
25
+ this.phase = 'building-with-pending-request'
26
+ return
27
+ case 'building-with-pending-request':
28
+ return
29
+ case 'idle':
30
+ case 'scheduled':
31
+ if (this.timer) clearTimeout(this.timer)
32
+ this.phase = 'scheduled'
33
+ this.timer = setTimeout(() => this.start(), this.delay)
34
+ }
35
+ }
36
+
37
+ async close(): Promise<void> {
38
+ if (this.phase === 'closed') {
39
+ await this.work
40
+ return
41
+ }
42
+ this.phase = 'closed'
43
+ if (this.timer) clearTimeout(this.timer)
44
+ this.timer = undefined
45
+ await this.work
46
+ }
47
+
48
+ private start(): void {
49
+ if (this.phase !== 'scheduled') return
50
+ this.timer = undefined
51
+ this.phase = 'building'
52
+ this.work = this.runBuild()
53
+ .catch(this.reportError)
54
+ .finally(() => {
55
+ this.work = undefined
56
+ if (this.phase === 'closed') return
57
+ const repeat = this.phase === 'building-with-pending-request'
58
+ this.phase = 'idle'
59
+ if (repeat) this.request()
60
+ })
61
+ }
62
+ }
@@ -0,0 +1,33 @@
1
+ import { transformWithOxc } from 'vite'
2
+ import type { WxOutputFile } from './output.ts'
3
+
4
+ const wxJavaScriptTarget = 'es2018'
5
+
6
+ /** Lowers generated JavaScript syntax that WeChat's upload parser does not accept. */
7
+ export async function transformWxCompatibleJavaScript(code: string, filename: string): Promise<string> {
8
+ const result = await transformWithOxc(code, filename, {
9
+ lang: 'js',
10
+ target: wxJavaScriptTarget,
11
+ sourcemap: false,
12
+ assumptions: {
13
+ // Avoid external Oxc helpers when lowering public class fields in already-bundled chunks.
14
+ setPublicClassFields: true
15
+ }
16
+ })
17
+ return result.code
18
+ }
19
+
20
+ /** Applies the compatibility transform to every JavaScript chunk in a DevEngine output batch. */
21
+ export async function transformWxOutputChunks(output: WxOutputFile[]): Promise<void> {
22
+ await Promise.all(
23
+ output.map(async (item, index) => {
24
+ if (item.type !== 'chunk') return
25
+ output[index] = {
26
+ type: 'chunk',
27
+ fileName: item.fileName,
28
+ modules: item.modules,
29
+ code: await transformWxCompatibleJavaScript(item.code, item.fileName)
30
+ }
31
+ })
32
+ )
33
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Normalizes private DevEngine output before the development session writes it to the fixed WX project directory.
3
+ * DevEngine rebuilds may be partial, so these helpers mutate only files present in the current output.
4
+ */
5
+
6
+ /** File shape emitted by Vite's private bundled-development engine. */
7
+ export type WxOutputFile =
8
+ | {
9
+ type: 'chunk'
10
+ fileName: string
11
+ code: string
12
+ modules?: Record<string, unknown>
13
+ }
14
+ | {
15
+ type: 'asset'
16
+ fileName: string
17
+ source: string | Uint8Array
18
+ }
19
+
20
+ /** Extracts Vite's development CSS payloads and materializes them as app.wxss. */
21
+ export function normalizeWxBundleStyles(output: WxOutputFile[]): string | undefined {
22
+ const styles: string[] = []
23
+ for (let index = output.length - 1; index >= 0; index--) {
24
+ const item = output[index]
25
+ if (!item) continue
26
+ if (item.type === 'asset' && item.fileName.endsWith('.css')) {
27
+ styles.unshift(typeof item.source === 'string' ? item.source : new TextDecoder().decode(item.source))
28
+ output.splice(index, 1)
29
+ } else if (item.type === 'chunk') {
30
+ styles.push(...collectEmbeddedStyles(item.code))
31
+ }
32
+ }
33
+ if (styles.length === 0) return
34
+ const source = styles.join('\n')
35
+ setWxAppStyles(output, source)
36
+ return source
37
+ }
38
+
39
+ export function setWxAppStyles(output: WxOutputFile[], source: string): void {
40
+ const index = output.findIndex((item) => item.type === 'asset' && item.fileName === 'app.wxss')
41
+ const appStyle: WxOutputFile = { type: 'asset', fileName: 'app.wxss', source }
42
+ if (index >= 0) output[index] = appStyle
43
+ else output.push(appStyle)
44
+ }
45
+
46
+ export function isWxFullBuildOutput(output: WxOutputFile[]): boolean {
47
+ return output.some((item) => item.fileName === 'app.js')
48
+ }
49
+
50
+ function collectEmbeddedStyles(code: string): string[] {
51
+ const styles: string[] = []
52
+ for (const match of code.matchAll(/__vite__css(?:\$\d+)?\s*=\s*("(?:\\.|[^"\\])*");/g)) {
53
+ if (match[1]) styles.push(JSON.parse(match[1]) as string)
54
+ }
55
+ return styles
56
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Self-contained source injected into Rolldown's DevEngine as the WX module bootstrap runtime.
3
+ *
4
+ * This TypeScript module runs in Node and only exports source text. The exported JavaScript becomes
5
+ * `runtime.js`, executes before every application module, and therefore cannot import Taro, React,
6
+ * or any module that depends on the runtime it is creating.
7
+ */
8
+ export const wxRolldownRuntimeSource = `
9
+ var BaseDevRuntime = DevRuntime;
10
+ class WxHotContext {
11
+ callbacks = [];
12
+ data = {};
13
+ _internal = { updateStyle() {}, removeStyle() {} };
14
+ constructor(moduleId) { this.moduleId = moduleId; }
15
+ accept(...args) {
16
+ if (args.length === 0) {
17
+ this.callbacks.push({ deps: this.moduleId, callback: undefined });
18
+ } else if (args.length === 1 && typeof args[0] === 'function') {
19
+ this.callbacks.push({ deps: this.moduleId, callback: args[0] });
20
+ } else if (args.length === 1) {
21
+ this.callbacks.push({ deps: args[0], callback: undefined });
22
+ } else {
23
+ this.callbacks.push({ deps: args[0], callback: args[1] });
24
+ }
25
+ }
26
+ acceptExports(_exports, callback) { this.accept(callback); }
27
+ dispose() {}
28
+ prune() {}
29
+ invalidate() {}
30
+ on() {}
31
+ off() {}
32
+ send() {}
33
+ }
34
+ class WxRolldownRuntime extends BaseDevRuntime {
35
+ contexts = new Map();
36
+ patchedModules = new Set();
37
+ applyingPatch = false;
38
+
39
+ createEsmInitializer = (id, initialize, _deduplicate, result) => () => {
40
+ if (!initialize) return result;
41
+ if (!this.applyingPatch && this.patchedModules.has(id)) {
42
+ initialize = undefined;
43
+ return result;
44
+ }
45
+ const callback = initialize;
46
+ initialize = undefined;
47
+ result = callback(id);
48
+ if (this.applyingPatch) this.patchedModules.add(id);
49
+ return result;
50
+ };
51
+
52
+ createCjsInitializer = (id, initialize, _deduplicate, module) => () => {
53
+ if (module) return module.exports;
54
+ if (!this.applyingPatch && this.patchedModules.has(id)) return this.loadExports(id);
55
+ module = { exports: {} };
56
+ initialize(module.exports, module, id);
57
+ if (this.applyingPatch) this.patchedModules.add(id);
58
+ return module.exports;
59
+ };
60
+
61
+ createModuleHotContext(moduleId) {
62
+ const previous = this.contexts.get(moduleId);
63
+ const context = new WxHotContext(moduleId);
64
+ if (previous) {
65
+ context.callbacks = previous.callbacks;
66
+ context.data = previous.data;
67
+ }
68
+ this.contexts.set(moduleId, context);
69
+ return context;
70
+ }
71
+
72
+ beginPatch() { this.applyingPatch = true; }
73
+ endPatch() { this.applyingPatch = false; }
74
+
75
+ applyUpdates(boundaries) {
76
+ for (const [boundary, acceptedVia] of boundaries) {
77
+ const context = this.contexts.get(boundary);
78
+ if (!context) continue;
79
+ const callbacks = [...context.callbacks];
80
+ if (boundary === acceptedVia) context.callbacks = [];
81
+ for (const { deps, callback } of callbacks) {
82
+ if (!callback) continue;
83
+ if (Array.isArray(deps)) {
84
+ if (deps.includes(acceptedVia)) callback(deps.map((id) => this.loadExports(id)));
85
+ } else if (deps === acceptedVia) {
86
+ callback(this.loadExports(acceptedVia));
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ // A full DevEngine runtime starts a new page-update generation before any application chunk executes.
93
+ globalThis.__VITE_PLUGIN_TARO_WX_PAGE_UPDATE__ = { ready: false };
94
+ globalThis.__rolldown_runtime__ = new WxRolldownRuntime(undefined, 'vite-plugin-taro-wx');
95
+ `