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,183 @@
1
+ /**
2
+ * Pure host-side stop-and-wait protocol state machine.
3
+ *
4
+ * It retains versioned Rolldown deltas and selects one missing range for the active App Service. HTTP and filesystem
5
+ * effects are represented as commands and handled by update-transport.ts.
6
+ */
7
+ type WxUpdateDelta = {
8
+ version: number
9
+ code: string
10
+ }
11
+
12
+ export type WxUpdateBatch = {
13
+ buildId: string
14
+ sessionId: string
15
+ fromVersion: number
16
+ targetVersion: number
17
+ deltas: WxUpdateDelta[]
18
+ }
19
+
20
+ type WxUpdateServerState = {
21
+ buildId: string
22
+ hostVersion: number
23
+ retainedDeltaBytes: number
24
+ deltas: WxUpdateDelta[]
25
+ activeSessionId?: string
26
+ retiredSessionIds: string[]
27
+ inFlight?: Pick<WxUpdateBatch, 'sessionId' | 'fromVersion' | 'targetVersion'>
28
+ }
29
+
30
+ type WxUpdateServerEvent =
31
+ | { type: 'delta-added'; code: string; bytes?: number }
32
+ | { type: 'client-registered'; buildId: string; sessionId: string; version: number }
33
+ | { type: 'client-reported'; buildId: string; sessionId: string; version: number }
34
+ | { type: 'batch-publish-failed'; sessionId: string; targetVersion: number }
35
+ | { type: 'full-build-committed'; buildId: string }
36
+
37
+ export type WxUpdateServerCommand =
38
+ | { type: 'publish-batch'; batch: WxUpdateBatch }
39
+ | { type: 'request-full-build'; reason: 'client-version-ahead' | 'invalid-client-version' }
40
+ | { type: 'ignore-client'; reason: 'stale-build' | 'retired-session' | 'unknown-session' }
41
+
42
+ type WxUpdateServerTransition = {
43
+ state: WxUpdateServerState
44
+ commands: WxUpdateServerCommand[]
45
+ }
46
+
47
+ export function createWxUpdateServerState(buildId: string): WxUpdateServerState {
48
+ return {
49
+ buildId,
50
+ hostVersion: 0,
51
+ retainedDeltaBytes: 0,
52
+ deltas: [],
53
+ retiredSessionIds: []
54
+ }
55
+ }
56
+
57
+ export function transitionWxUpdateServer(
58
+ state: WxUpdateServerState,
59
+ event: WxUpdateServerEvent
60
+ ): WxUpdateServerTransition {
61
+ switch (event.type) {
62
+ case 'delta-added': {
63
+ const version = state.hostVersion + 1
64
+ return transition({
65
+ ...state,
66
+ hostVersion: version,
67
+ retainedDeltaBytes: state.retainedDeltaBytes + (event.bytes ?? event.code.length),
68
+ deltas: [...state.deltas, { version, code: event.code }]
69
+ })
70
+ }
71
+ case 'client-registered':
72
+ return registerClient(state, event)
73
+ case 'client-reported':
74
+ return reportClient(state, event)
75
+ case 'batch-publish-failed':
76
+ if (state.inFlight?.sessionId !== event.sessionId || state.inFlight.targetVersion !== event.targetVersion) {
77
+ return transition(state)
78
+ }
79
+ return transition({ ...state, inFlight: undefined })
80
+ case 'full-build-committed':
81
+ return transition(createWxUpdateServerState(event.buildId))
82
+ }
83
+ }
84
+
85
+ function registerClient(
86
+ state: WxUpdateServerState,
87
+ client: { buildId: string; sessionId: string; version: number }
88
+ ): WxUpdateServerTransition {
89
+ const rejected = rejectClient(state, client)
90
+ if (rejected) return rejected
91
+ if (state.retiredSessionIds.includes(client.sessionId)) {
92
+ return command(state, { type: 'ignore-client', reason: 'retired-session' })
93
+ }
94
+ if (state.activeSessionId === client.sessionId) return transition(state)
95
+
96
+ const retiredSessionIds = state.activeSessionId
97
+ ? [...new Set([...state.retiredSessionIds, state.activeSessionId])]
98
+ : state.retiredSessionIds
99
+ const registered = {
100
+ ...state,
101
+ activeSessionId: client.sessionId,
102
+ retiredSessionIds,
103
+ inFlight: undefined
104
+ }
105
+ return transition(registered)
106
+ }
107
+
108
+ function reportClient(
109
+ state: WxUpdateServerState,
110
+ client: { buildId: string; sessionId: string; version: number }
111
+ ): WxUpdateServerTransition {
112
+ const rejected = rejectClient(state, client)
113
+ if (rejected) return rejected
114
+ if (state.retiredSessionIds.includes(client.sessionId)) {
115
+ return command(state, { type: 'ignore-client', reason: 'retired-session' })
116
+ }
117
+ if (client.sessionId !== state.activeSessionId) {
118
+ return command(state, { type: 'ignore-client', reason: 'unknown-session' })
119
+ }
120
+ return synchronizeClient(state, client.version)
121
+ }
122
+
123
+ function rejectClient(
124
+ state: WxUpdateServerState,
125
+ client: { buildId: string; version: number }
126
+ ): WxUpdateServerTransition | undefined {
127
+ if (client.buildId !== state.buildId) {
128
+ return command(state, { type: 'ignore-client', reason: 'stale-build' })
129
+ }
130
+ if (!Number.isInteger(client.version) || client.version < 0) {
131
+ return command(state, { type: 'request-full-build', reason: 'invalid-client-version' })
132
+ }
133
+ if (client.version > state.hostVersion) {
134
+ return command(state, { type: 'request-full-build', reason: 'client-version-ahead' })
135
+ }
136
+ }
137
+
138
+ function synchronizeClient(state: WxUpdateServerState, clientVersion: number): WxUpdateServerTransition {
139
+ let synchronized = state
140
+ const inFlight = state.inFlight
141
+ if (inFlight) {
142
+ if (clientVersion === inFlight.fromVersion) {
143
+ const batch = createBatch(state, clientVersion, inFlight.targetVersion)
144
+ return command(state, { type: 'publish-batch', batch })
145
+ }
146
+ if (clientVersion !== inFlight.targetVersion) {
147
+ return command(state, { type: 'request-full-build', reason: 'invalid-client-version' })
148
+ }
149
+ synchronized = { ...state, inFlight: undefined }
150
+ }
151
+ if (clientVersion >= synchronized.hostVersion) return transition(synchronized)
152
+
153
+ const batch = createBatch(synchronized, clientVersion, synchronized.hostVersion)
154
+ return command(
155
+ {
156
+ ...synchronized,
157
+ inFlight: {
158
+ sessionId: batch.sessionId,
159
+ fromVersion: batch.fromVersion,
160
+ targetVersion: batch.targetVersion
161
+ }
162
+ },
163
+ { type: 'publish-batch', batch }
164
+ )
165
+ }
166
+
167
+ function createBatch(state: WxUpdateServerState, fromVersion: number, targetVersion: number): WxUpdateBatch {
168
+ return {
169
+ buildId: state.buildId,
170
+ sessionId: state.activeSessionId!,
171
+ fromVersion,
172
+ targetVersion,
173
+ deltas: state.deltas.filter((delta) => delta.version > fromVersion && delta.version <= targetVersion)
174
+ }
175
+ }
176
+
177
+ function transition(state: WxUpdateServerState): WxUpdateServerTransition {
178
+ return { state, commands: [] }
179
+ }
180
+
181
+ function command(state: WxUpdateServerState, value: WxUpdateServerCommand): WxUpdateServerTransition {
182
+ return { state, commands: [value] }
183
+ }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Adapts the pure server protocol to Vite HTTP middleware and literal update-file publication.
3
+ * HTTP messages contain metadata only; executable JavaScript reaches DevTools through the injected file writer.
4
+ */
5
+ import { randomBytes } from 'node:crypto'
6
+ import type { IncomingMessage, ServerResponse } from 'node:http'
7
+ import type { ViteDevServer } from 'vite'
8
+ import {
9
+ createWxUpdateServerState,
10
+ transitionWxUpdateServer,
11
+ type WxUpdateBatch,
12
+ type WxUpdateServerCommand
13
+ } from './update-server-state.ts'
14
+
15
+ const endpointPath = '/__vite_plugin_taro_wx_update__'
16
+ // Keep the host response below update-client.ts's 30-second wx.request timeout.
17
+ const pollTimeout = 25_000
18
+
19
+ type PendingPoll = {
20
+ response: ServerResponse
21
+ timeout: NodeJS.Timeout
22
+ }
23
+
24
+ type UpdateRequest = {
25
+ token?: unknown
26
+ action?: unknown
27
+ buildId?: unknown
28
+ sessionId?: unknown
29
+ version?: unknown
30
+ }
31
+
32
+ export class WxUpdateTransport {
33
+ private state = createWxUpdateServerState(createId())
34
+ private readonly token = createId()
35
+ private readonly pendingPolls = new Map<string, PendingPoll>()
36
+ private closed = false
37
+
38
+ constructor(
39
+ private readonly server: ViteDevServer,
40
+ private readonly requestFullBuild: () => void,
41
+ private readonly writeUpdateFile: (buildId: string, source: string) => Promise<void>
42
+ ) {}
43
+
44
+ get retainedDeltaCount(): number {
45
+ return this.state.hostVersion
46
+ }
47
+
48
+ get retainedDeltaBytes(): number {
49
+ return this.state.retainedDeltaBytes
50
+ }
51
+
52
+ install(): void {
53
+ this.server.middlewares.use(endpointPath, this.handleRequest)
54
+ }
55
+
56
+ close(): void {
57
+ this.closed = true
58
+ this.respondToAll({ type: 'rebuilding' })
59
+ }
60
+
61
+ addDelta(code: string): void {
62
+ this.apply({ type: 'delta-added', code, bytes: Buffer.byteLength(code) })
63
+ this.respondToAll({ type: 'changed' })
64
+ }
65
+
66
+ createBuildId(): string {
67
+ return createId()
68
+ }
69
+
70
+ isCurrentBuild(buildId: string): boolean {
71
+ return this.state.buildId === buildId
72
+ }
73
+
74
+ commitFullBuild(buildId: string): void {
75
+ this.apply({ type: 'full-build-committed', buildId })
76
+ this.respondToAll({ type: 'rebuilding' })
77
+ }
78
+
79
+ /** Generates late-bound App metadata after Vite has selected its actual listening port. */
80
+ createControlSource(buildId = this.state.buildId): string {
81
+ const address = this.server.httpServer?.address()
82
+ const port = address && typeof address !== 'string' ? address.port : this.server.config.server.port
83
+ return `globalThis.__VITE_PLUGIN_TARO_WX_CONTROL__ = ${JSON.stringify({
84
+ url: `http://localhost:${port}${endpointPath}`,
85
+ token: this.token,
86
+ buildId
87
+ })};\n`
88
+ }
89
+
90
+ private readonly handleRequest = async (request: IncomingMessage, response: ServerResponse): Promise<void> => {
91
+ if (this.closed || request.method !== 'POST') {
92
+ respond(response, 404, { type: 'not-found' })
93
+ return
94
+ }
95
+
96
+ let body: UpdateRequest
97
+ try {
98
+ body = JSON.parse(await readBody(request)) as UpdateRequest
99
+ } catch {
100
+ respond(response, 400, { type: 'invalid-request' })
101
+ return
102
+ }
103
+ if (body.token !== this.token || !isClientReport(body)) {
104
+ respond(response, 403, { type: 'forbidden' })
105
+ return
106
+ }
107
+
108
+ if (body.action === 'rebuild') {
109
+ this.requestFullBuild()
110
+ respond(response, 202, { type: 'rebuilding' })
111
+ return
112
+ }
113
+ if (body.action === 'register') {
114
+ const transition = this.apply({
115
+ type: 'client-registered',
116
+ buildId: body.buildId,
117
+ sessionId: body.sessionId,
118
+ version: body.version
119
+ })
120
+ if (transition.some((command) => command.type === 'request-full-build')) this.requestFullBuild()
121
+ respond(response, 200, { type: 'registered' })
122
+ return
123
+ }
124
+ if (body.action !== 'poll') {
125
+ respond(response, 400, { type: 'invalid-request' })
126
+ return
127
+ }
128
+
129
+ const commands = this.apply({
130
+ type: 'client-reported',
131
+ buildId: body.buildId,
132
+ sessionId: body.sessionId,
133
+ version: body.version
134
+ })
135
+ await this.executePollCommands(body.sessionId, commands, response)
136
+ }
137
+
138
+ private async executePollCommands(
139
+ sessionId: string,
140
+ commands: WxUpdateServerCommand[],
141
+ response: ServerResponse
142
+ ): Promise<void> {
143
+ const publish = commands.find((command) => command.type === 'publish-batch')
144
+ if (publish?.type === 'publish-batch') {
145
+ try {
146
+ await this.writeBatch(publish.batch)
147
+ respond(response, 200, { type: 'batch-published', targetVersion: publish.batch.targetVersion })
148
+ } catch {
149
+ this.apply({
150
+ type: 'batch-publish-failed',
151
+ sessionId: publish.batch.sessionId,
152
+ targetVersion: publish.batch.targetVersion
153
+ })
154
+ respond(response, 500, { type: 'publish-failed' })
155
+ }
156
+ return
157
+ }
158
+ if (commands.some((command) => command.type === 'request-full-build')) {
159
+ this.requestFullBuild()
160
+ respond(response, 202, { type: 'rebuilding' })
161
+ return
162
+ }
163
+ if (commands.some((command) => command.type === 'ignore-client')) {
164
+ respond(response, 409, { type: 'rebuilding' })
165
+ return
166
+ }
167
+ this.holdPoll(sessionId, response)
168
+ }
169
+
170
+ private holdPoll(sessionId: string, response: ServerResponse): void {
171
+ const previous = this.pendingPolls.get(sessionId)
172
+ if (previous) this.finishPoll(sessionId, previous, { type: 'changed' })
173
+
174
+ let pending: PendingPoll
175
+ pending = {
176
+ response,
177
+ timeout: setTimeout(() => this.finishPoll(sessionId, pending, { type: 'idle' }), pollTimeout)
178
+ }
179
+ this.pendingPolls.set(sessionId, pending)
180
+ response.on('close', () => this.finishPoll(sessionId, pending))
181
+ }
182
+
183
+ private respondToAll(value: { type: string }): void {
184
+ for (const [sessionId, pending] of this.pendingPolls) this.finishPoll(sessionId, pending, value)
185
+ }
186
+
187
+ private finishPoll(sessionId: string, pending: PendingPoll, value?: { type: string }): void {
188
+ if (this.pendingPolls.get(sessionId) !== pending) return
189
+ clearTimeout(pending.timeout)
190
+ this.pendingPolls.delete(sessionId)
191
+ if (value) respond(pending.response, 200, value)
192
+ }
193
+
194
+ private apply(event: Parameters<typeof transitionWxUpdateServer>[1]): WxUpdateServerCommand[] {
195
+ const transition = transitionWxUpdateServer(this.state, event)
196
+ this.state = transition.state
197
+ return transition.commands
198
+ }
199
+
200
+ private async writeBatch(batch: WxUpdateBatch): Promise<void> {
201
+ await this.writeUpdateFile(batch.buildId, renderBatch(batch, createId()))
202
+ }
203
+ }
204
+
205
+ /** Renders executable code for update.js; this source is never included in an HTTP response. */
206
+ function renderBatch(batch: WxUpdateBatch, nonce: string): string {
207
+ return `// ${nonce}\nglobalThis.__VITE_PLUGIN_TARO_WX_UPDATE_CLIENT__.receiveBatch(${JSON.stringify({
208
+ buildId: batch.buildId,
209
+ fromVersion: batch.fromVersion,
210
+ targetVersion: batch.targetVersion
211
+ })}, () => {
212
+ ${indent(batch.deltas.map((delta) => delta.code).join('\n'), 4)}
213
+ });\n`
214
+ }
215
+
216
+ function indent(value: string, spaces: number): string {
217
+ const prefix = ' '.repeat(spaces)
218
+ return value
219
+ .split('\n')
220
+ .map((line) => `${prefix}${line}`)
221
+ .join('\n')
222
+ }
223
+
224
+ function isClientReport(value: UpdateRequest): value is {
225
+ token: string
226
+ action: 'register' | 'poll' | 'rebuild'
227
+ buildId: string
228
+ sessionId: string
229
+ version: number
230
+ } {
231
+ return (
232
+ typeof value.token === 'string' &&
233
+ typeof value.action === 'string' &&
234
+ typeof value.buildId === 'string' &&
235
+ typeof value.sessionId === 'string' &&
236
+ typeof value.version === 'number'
237
+ )
238
+ }
239
+
240
+ async function readBody(request: IncomingMessage): Promise<string> {
241
+ const chunks: Buffer[] = []
242
+ let size = 0
243
+ for await (const chunk of request) {
244
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
245
+ size += buffer.length
246
+ if (size > 64 * 1024) throw new Error('WX update request body is too large.')
247
+ chunks.push(buffer)
248
+ }
249
+ return Buffer.concat(chunks).toString('utf8')
250
+ }
251
+
252
+ function respond(response: ServerResponse, status: number, body: unknown): void {
253
+ if (response.writableEnded) return
254
+ response.statusCode = status
255
+ response.setHeader('content-type', 'application/json')
256
+ response.end(JSON.stringify(body))
257
+ }
258
+
259
+ function createId(): string {
260
+ return randomBytes(16).toString('hex')
261
+ }
@@ -0,0 +1,203 @@
1
+ import path from 'node:path'
2
+ import type { ResolvedConfig, ViteDevServer } from 'vite'
3
+ import { wxPagePreloadFile, wxUpdateControlFile, wxUpdateFile } from '../development-files.ts'
4
+ import type { WxOutputFile } from './output.ts'
5
+ import { wxRolldownRuntimeSource } from './rolldown-runtime-source.ts'
6
+
7
+ const clientId = 'vite-plugin-taro-wx'
8
+
9
+ /** HMR boundary shape returned by Vite's private WX DevEngine integration. */
10
+ type WxDevEngineBoundary = {
11
+ boundary: string
12
+ acceptedVia: string
13
+ }
14
+
15
+ /** Update result shape returned by Vite's private bundled-development engine. */
16
+ export type WxDevEngineUpdate =
17
+ | { type: 'Noop' }
18
+ | { type: 'FullReload'; reason?: string }
19
+ | {
20
+ type: 'Patch'
21
+ code: string
22
+ filename: string
23
+ hmrBoundaries: WxDevEngineBoundary[]
24
+ }
25
+
26
+ type WxDevEngineInternal = {
27
+ registerModules(clientId: string, modules: string[]): void
28
+ ensureCurrentBuildFinish(): Promise<void>
29
+ ensureLatestBuildOutput(): Promise<unknown>
30
+ getBundleState(): Promise<{ lastBuildErrored: boolean }>
31
+ triggerFullBuild(): void
32
+ }
33
+
34
+ type WxRolldownOptions = {
35
+ output?: Record<string, unknown> | Array<Record<string, unknown>>
36
+ experimental?: {
37
+ devMode?: boolean | Record<string, unknown>
38
+ [key: string]: unknown
39
+ }
40
+ [key: string]: unknown
41
+ }
42
+
43
+ type WxDevEngineClient = {
44
+ send(payload: unknown): void
45
+ }
46
+
47
+ type ViteBundledDevInternal = {
48
+ _devEngine?: WxDevEngineInternal
49
+ clients: {
50
+ setupIfNeeded(client: WxDevEngineClient, clientId: string): void
51
+ }
52
+ getRolldownOptions(): Promise<WxRolldownOptions>
53
+ storeOutputFiles(output: WxOutputFile[]): void
54
+ handleHmrOutput(client: WxDevEngineClient, files: string[], output: WxDevEngineUpdate, info?: unknown): void
55
+ listen(): Promise<void>
56
+ }
57
+
58
+ type ViteBundledDevCallbacks = {
59
+ onOutput(output: WxOutputFile[]): void
60
+ onPatch(files: string[], output: WxDevEngineUpdate): boolean
61
+ onError(message: string): void
62
+ waitForInitialBundle(): Promise<void>
63
+ }
64
+
65
+ /** Isolates the private Vite bundled-development API used by the WX session. */
66
+ export class ViteBundledDevAdapter {
67
+ private bundledDev: ViteBundledDevInternal | undefined
68
+
69
+ constructor(
70
+ private readonly config: ResolvedConfig,
71
+ private readonly server: ViteDevServer,
72
+ private readonly callbacks: ViteBundledDevCallbacks
73
+ ) {}
74
+
75
+ install(): void {
76
+ const bundledDev = this.server.environments.client.bundledDev as unknown as ViteBundledDevInternal | undefined
77
+ if (!bundledDev) throw new Error('vite-plugin-taro requires Vite bundled development for WX development.')
78
+ if (
79
+ typeof bundledDev.getRolldownOptions !== 'function' ||
80
+ typeof bundledDev.storeOutputFiles !== 'function' ||
81
+ typeof bundledDev.handleHmrOutput !== 'function'
82
+ ) {
83
+ throw new Error('vite-plugin-taro does not support this Vite bundled-development API shape.')
84
+ }
85
+ this.bundledDev = bundledDev
86
+ this.installOptions(bundledDev)
87
+ this.installOutput(bundledDev)
88
+ this.installPatches(bundledDev)
89
+ this.installListener(bundledDev)
90
+ }
91
+
92
+ registerBundleModules(output: WxOutputFile[]): number {
93
+ const moduleIds = new Set<string>()
94
+ for (const item of output) {
95
+ if (item.type !== 'chunk') continue
96
+ for (const id of Object.keys(item.modules ?? {})) moduleIds.add(toStableModuleId(id, this.config.root))
97
+ }
98
+ this.registerModules([...moduleIds])
99
+ return moduleIds.size
100
+ }
101
+
102
+ registerPatchModules(code: string): void {
103
+ const moduleIds = new Set<string>()
104
+ for (const match of code.matchAll(/create(?:Esm|Cjs)Initializer\("([^"]+)"/g)) moduleIds.add(match[1])
105
+ this.registerModules([...moduleIds])
106
+ }
107
+
108
+ private registerModules(moduleIds: string[]): void {
109
+ if (moduleIds.length) this.bundledDev?._devEngine?.registerModules(clientId, moduleIds)
110
+ }
111
+
112
+ async rebuild(): Promise<void> {
113
+ const engine = this.bundledDev?._devEngine
114
+ if (!engine) return
115
+ engine.triggerFullBuild()
116
+ await engine.ensureLatestBuildOutput()
117
+ }
118
+
119
+ private installOptions(bundledDev: ViteBundledDevInternal): void {
120
+ const getRolldownOptions = bundledDev.getRolldownOptions.bind(bundledDev)
121
+ bundledDev.getRolldownOptions = async () => {
122
+ const options = await getRolldownOptions()
123
+ if (!options.output) options.output = {}
124
+ const output = getFirstOutput(options)
125
+ const configuredOutput = this.config.build.rolldownOptions.output
126
+ const desiredOutput = Array.isArray(configuredOutput) ? configuredOutput[0] : configuredOutput
127
+ Object.assign(output, desiredOutput, {
128
+ format: 'cjs',
129
+ sourcemap: false,
130
+ minify: false,
131
+ banner: createWxDevelopmentBanner
132
+ })
133
+ options.experimental ??= {}
134
+ options.experimental.devMode = {
135
+ ...(typeof options.experimental.devMode === 'object' ? options.experimental.devMode : {}),
136
+ lazy: false,
137
+ // Rolldown compiles this source into runtime.js before any Taro/React module executes.
138
+ implement: wxRolldownRuntimeSource
139
+ }
140
+ return options
141
+ }
142
+ }
143
+
144
+ private installOutput(bundledDev: ViteBundledDevInternal): void {
145
+ const storeOutputFiles = bundledDev.storeOutputFiles.bind(bundledDev)
146
+ bundledDev.storeOutputFiles = (output) => {
147
+ storeOutputFiles(output)
148
+ this.callbacks.onOutput(output)
149
+ }
150
+ }
151
+
152
+ private installPatches(bundledDev: ViteBundledDevInternal): void {
153
+ const handleHmrOutput = bundledDev.handleHmrOutput.bind(bundledDev)
154
+ bundledDev.handleHmrOutput = (client, files, output, info) => {
155
+ if (output.type === 'Noop') return
156
+ if (this.callbacks.onPatch(files, output)) handleHmrOutput(client, files, output, info)
157
+ }
158
+ }
159
+
160
+ private installListener(bundledDev: ViteBundledDevInternal): void {
161
+ const listen = bundledDev.listen.bind(bundledDev)
162
+ bundledDev.listen = async () => {
163
+ await listen()
164
+ bundledDev.clients.setupIfNeeded({ send: (payload) => this.handlePayload(payload) }, clientId)
165
+ const engine = bundledDev._devEngine
166
+ if (!engine) throw new Error('vite-plugin-taro expected Vite to initialize the WX DevEngine.')
167
+ await engine.ensureCurrentBuildFinish()
168
+ if ((await engine.getBundleState()).lastBuildErrored) {
169
+ throw new Error('The initial WX bundled-development build failed.')
170
+ }
171
+ await this.callbacks.waitForInitialBundle()
172
+ }
173
+ }
174
+
175
+ private handlePayload(payload: unknown): void {
176
+ if (typeof payload !== 'object' || payload === null) return
177
+ const candidate = payload as { type?: unknown; err?: { message?: unknown } }
178
+ if (candidate.type === 'error' && typeof candidate.err?.message === 'string') {
179
+ this.callbacks.onError(candidate.err.message)
180
+ }
181
+ }
182
+ }
183
+
184
+ function toStableModuleId(id: string, root: string): string {
185
+ const normalizedId = id.replace(/\\/g, '/')
186
+ if (normalizedId.startsWith('\0') || !path.posix.isAbsolute(normalizedId)) return normalizedId
187
+ return path.posix.relative(root.replace(/\\/g, '/'), normalizedId)
188
+ }
189
+
190
+ function getFirstOutput(options: WxRolldownOptions): Record<string, unknown> {
191
+ if (!Array.isArray(options.output)) return options.output as Record<string, unknown>
192
+ const output = options.output[0] ?? {}
193
+ options.output[0] = output
194
+ return output
195
+ }
196
+
197
+ /** Adds native direct dependencies whose execution order cannot be represented through the Rolldown graph. */
198
+ function createWxDevelopmentBanner(chunk: { fileName: string }): string {
199
+ if (chunk.fileName === 'app.js') return `require(${JSON.stringify(`./${wxUpdateControlFile}`)});`
200
+ if (!chunk.fileName.startsWith('pages/') || !chunk.fileName.endsWith('.js')) return ''
201
+ const prefix = '../'.repeat(chunk.fileName.split('/').length - 1)
202
+ return `require(${JSON.stringify(`${prefix}runtime.js`)});require(${JSON.stringify(`${prefix}${wxPagePreloadFile}`)});require(${JSON.stringify(`${prefix}${wxUpdateFile}`)});`
203
+ }
@@ -0,0 +1,11 @@
1
+ /** Plugin-owned files added only to a WX development output. */
2
+ export const wxDevelopmentDirectory = 'vpt-hmr'
3
+
4
+ /** App-only metadata containing the local transport URL, token, and build ID. */
5
+ export const wxUpdateControlFile = `${wxDevelopmentDirectory}/control.js`
6
+
7
+ /** Stable page-component initialization required before replaying retained updates. */
8
+ export const wxPagePreloadFile = `${wxDevelopmentDirectory}/preload.js`
9
+
10
+ /** The one mutable, literal JavaScript update dependency required by every page. */
11
+ export const wxUpdateFile = `${wxDevelopmentDirectory}/update.js`