vite-plugin-taro 0.5.13 → 0.5.14

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 (71) hide show
  1. package/dist/node/plugins/client/client-taro.d.ts +1 -1
  2. package/dist/node/plugins/client/client-taro.js +2 -1
  3. package/dist/node/plugins/h5/create-stencil-client-adapter.d.ts +1 -1
  4. package/dist/node/plugins/h5/create-stencil-client-adapter.js +2 -1
  5. package/dist/node/plugins/h5/resolver/module-resolver.js +2 -1
  6. package/dist/node/plugins/wx/dev/create-hmr-results-stream.d.ts +23 -0
  7. package/dist/node/plugins/wx/dev/create-hmr-results-stream.js +57 -0
  8. package/dist/node/plugins/wx/dev/create-style-capture.d.ts +42 -0
  9. package/dist/node/plugins/wx/dev/create-style-capture.js +125 -0
  10. package/dist/node/plugins/wx/dev/dev-host.d.ts +1 -1
  11. package/dist/node/plugins/wx/dev/dev-host.js +151 -93
  12. package/dist/node/plugins/wx/dev/hmr-files.d.ts +1 -0
  13. package/dist/node/plugins/wx/dev/hmr-files.js +1 -0
  14. package/dist/node/plugins/wx/dev/host-actions.d.ts +16 -0
  15. package/dist/node/plugins/wx/dev/host-actions.js +58 -0
  16. package/dist/node/plugins/wx/dev/patch-publisher.d.ts +2 -2
  17. package/dist/node/plugins/wx/dev/patch-publisher.js +17 -1
  18. package/dist/node/plugins/wx/dev/plugins.d.ts +4 -0
  19. package/dist/node/plugins/wx/dev/plugins.js +24 -8
  20. package/dist/node/plugins/wx/dev/react-refresh.js +6 -2
  21. package/dist/node/plugins/wx/dev/runtime-reports.d.ts +27 -0
  22. package/dist/node/plugins/wx/dev/runtime-reports.js +45 -0
  23. package/dist/node/plugins/wx/dev/wx-dev-options.d.ts +7 -3
  24. package/dist/node/plugins/wx/dev/wx-dev-options.js +46 -28
  25. package/dist/node/plugins/wx/module.d.ts +1 -1
  26. package/dist/node/plugins/wx/module.js +2 -1
  27. package/dist/node/plugins/wx/native/compile-native-component-interface.js +2 -1
  28. package/dist/node/plugins/wx/native/create-native-component-output.js +2 -0
  29. package/dist/node/plugins/wx/native/native-component-assets.d.ts +1 -1
  30. package/dist/node/plugins/wx/native/native-component-assets.js +3 -3
  31. package/dist/node/plugins/wx/native/native-component-interface.js +2 -1
  32. package/dist/node/plugins/wx/resolve/resolver.js +3 -2
  33. package/dist/node/plugins/wx/styles/plugins.js +20 -1
  34. package/dist/node/utils/modules.d.ts +1 -1
  35. package/dist/node/utils/modules.js +4 -3
  36. package/dist/node/utils/packages.js +2 -2
  37. package/package.json +4 -3
  38. package/src/node/plugins/client/client-taro.ts +2 -2
  39. package/src/node/plugins/h5/create-stencil-client-adapter.ts +2 -2
  40. package/src/node/plugins/h5/resolver/module-resolver.ts +2 -1
  41. package/src/node/plugins/wx/dev/create-hmr-results-stream.ts +71 -0
  42. package/src/node/plugins/wx/dev/create-style-capture.ts +179 -0
  43. package/src/node/plugins/wx/dev/dev-host.ts +177 -124
  44. package/src/node/plugins/wx/dev/hmr-files.ts +1 -0
  45. package/src/node/plugins/wx/dev/host-actions.ts +88 -0
  46. package/src/node/plugins/wx/dev/patch-publisher.ts +19 -2
  47. package/src/node/plugins/wx/dev/plugins.ts +25 -8
  48. package/src/node/plugins/wx/dev/react-refresh.ts +6 -2
  49. package/src/node/plugins/wx/dev/runtime-reports.ts +70 -0
  50. package/src/node/plugins/wx/dev/wx-dev-options.ts +50 -36
  51. package/src/node/plugins/wx/module.ts +2 -4
  52. package/src/node/plugins/wx/native/compile-native-component-interface.ts +2 -1
  53. package/src/node/plugins/wx/native/create-native-component-output.ts +2 -0
  54. package/src/node/plugins/wx/native/native-component-assets.ts +3 -4
  55. package/src/node/plugins/wx/native/native-component-interface.ts +2 -1
  56. package/src/node/plugins/wx/resolve/resolver.ts +3 -2
  57. package/src/node/plugins/wx/styles/plugins.ts +21 -1
  58. package/src/node/utils/modules.ts +4 -3
  59. package/src/node/utils/packages.ts +2 -2
  60. package/dist/node/plugins/wx/dev/create-wx-dev-mode.d.ts +0 -6
  61. package/dist/node/plugins/wx/dev/create-wx-dev-mode.js +0 -9
  62. package/dist/node/plugins/wx/dev/styles/create-style-capture-plugin.d.ts +0 -12
  63. package/dist/node/plugins/wx/dev/styles/create-style-capture-plugin.js +0 -21
  64. package/dist/node/plugins/wx/dev/styles/publish-style-hmr.d.ts +0 -27
  65. package/dist/node/plugins/wx/dev/styles/publish-style-hmr.js +0 -48
  66. package/dist/node/utils/serialized-task-queue.d.ts +0 -13
  67. package/dist/node/utils/serialized-task-queue.js +0 -25
  68. package/src/node/plugins/wx/dev/create-wx-dev-mode.ts +0 -14
  69. package/src/node/plugins/wx/dev/styles/create-style-capture-plugin.ts +0 -34
  70. package/src/node/plugins/wx/dev/styles/publish-style-hmr.ts +0 -71
  71. package/src/node/utils/serialized-task-queue.ts +0 -32
@@ -0,0 +1,179 @@
1
+ import type { GetModuleInfo, Plugin } from 'rolldown'
2
+ import { isCSSRequest } from 'vite'
3
+ import { normalizeModuleId } from '../../../utils/modules.ts'
4
+ import { transformWxStyle } from '../styles/transform-wx-style.ts'
5
+ import {
6
+ composeGraphStyleCss,
7
+ createGraphStylePlan,
8
+ createTailwindSidecarId,
9
+ extractViteCss,
10
+ isGlobalStyleRequest
11
+ } from '../styles/utils.ts'
12
+ import { globalWxssFileName, writeHmrFile } from './hmr-files.ts'
13
+
14
+ type ProcessedStyle = Readonly<{
15
+ css: string
16
+ isTailwindRoot: boolean
17
+ }>
18
+
19
+ type CompleteOutputFile =
20
+ | Readonly<{ type: 'asset'; fileName: string; source: string | Uint8Array }>
21
+ | Readonly<{ type: 'chunk'; fileName: string }>
22
+
23
+ export type StyleCaptureAction =
24
+ | Readonly<{ kind: 'capture-graph'; getModuleInfo: GetModuleInfo }>
25
+ | Readonly<{ kind: 'capture-style'; id: string; style: ProcessedStyle }>
26
+
27
+ /**
28
+ * Owns capture hooks, the host's style projection, and its physical WXSS publication frontier.
29
+ *
30
+ * Rolldown remains authoritative for topology through its live graph reader. Plugin hooks emit typed actions so the host can
31
+ * serialize captures with every other effect; only the matching capture methods mutate this projection.
32
+ */
33
+ export function createStyleCapture({
34
+ applicationEntryIds,
35
+ outDir,
36
+ emit,
37
+ transformTailwindRoot
38
+ }: {
39
+ applicationEntryIds: readonly string[]
40
+ outDir: string
41
+ emit: (action: StyleCaptureAction) => void
42
+ transformTailwindRoot: (rootId: string, requestId: string) => Promise<Readonly<{ code: string }> | null>
43
+ }): Readonly<{
44
+ bindOutput: (output: readonly CompleteOutputFile[]) => void
45
+ captureGraph: (reader: GetModuleInfo) => void
46
+ captureStyle: (id: string, style: ProcessedStyle) => void
47
+ plugin: Plugin
48
+ publishChanged: (changedIds: readonly string[]) => Promise<void>
49
+ }> {
50
+ /*
51
+ * This mutable capability is rebound by buildStart for each complete generation. Rolldown keeps the captured function live
52
+ * across incremental graph edits, so HMR traverses authoritative current topology without maintaining a shadow graph. It is
53
+ * undefined only before the first buildStart; retaining an older reader across a later complete build would bind transactions
54
+ * to the wrong engine generation, while snapshotting module data would require O(V + E) mutation on every edit.
55
+ */
56
+ let getModuleInfo: GetModuleInfo | undefined
57
+ /*
58
+ * This mutable Map is the CSS projection Rolldown does not retain: successful final transform hooks replace one module's
59
+ * processed bytes, failed transforms intentionally leave its last valid generation, and a successful multi-root Tailwind
60
+ * refresh commits all replacements only after every sibling resolves. Entries are not deleted when topology changes because
61
+ * reachability comes from the live graph plan; eager deletion would need a duplicate reverse graph and could remove CSS still
62
+ * shared by another App/Page root. Its size is bounded by style module identities seen during this server lifecycle.
63
+ */
64
+ const processedStyles = new Map<string, ProcessedStyle>()
65
+ /*
66
+ * This mutable value mirrors the WXSS bytes currently durable on disk. It has one owner but two real transition sources:
67
+ *
68
+ * 1. DevEngine physically writes complete output before onOutput, so bindOutput only observes and adopts that external
69
+ * commit. An omitted asset means DevEngine preserved the existing file and this value must remain unchanged.
70
+ * 2. Incremental HMR is physically written by this host, so publishChanged advances the value only after its atomic write
71
+ * succeeds (or after byte equality proves no write was needed).
72
+ *
73
+ * Combining these operations behind one generic update event would still require branching between “observe an existing
74
+ * write” and “perform a new write”, while hiding their different durability boundaries. A genuine single path would require
75
+ * preventing DevEngine from writing complete WXSS and transferring that entire output responsibility to the host, adding
76
+ * asset interception and ordering machinery. The two explicit methods are therefore the smallest accurate state model.
77
+ */
78
+ let publishedWxss: string | undefined
79
+
80
+ const plugin: Plugin = {
81
+ name: 'vpt:wx-dev-style-capture',
82
+ buildStart() {
83
+ emit({ kind: 'capture-graph', getModuleInfo: (moduleId) => this.getModuleInfo(moduleId) })
84
+ },
85
+ transform(code, id) {
86
+ if (!isGlobalStyleRequest(id)) {
87
+ return
88
+ }
89
+
90
+ const css = extractViteCss(code, id)
91
+ emit({
92
+ kind: 'capture-style',
93
+ id: normalizeModuleId(id),
94
+ style: {
95
+ css: css,
96
+ isTailwindRoot: css.includes('weapp-tailwindcss vite-generated-css:')
97
+ }
98
+ })
99
+ }
100
+ }
101
+
102
+ return {
103
+ bindOutput(output) {
104
+ /*
105
+ * Complete output is one of the two real physical writers. Its emitted asset is therefore the authoritative frontier
106
+ * when present; omission means DevEngine preserved the existing file and this state must remain unchanged.
107
+ */
108
+ const style = output.find(
109
+ (file): file is Extract<CompleteOutputFile, { type: 'asset' }> =>
110
+ file.type === 'asset' && file.fileName === globalWxssFileName
111
+ )
112
+ if (style) {
113
+ publishedWxss = typeof style.source === 'string' ? style.source : new TextDecoder().decode(style.source)
114
+ }
115
+ },
116
+ captureGraph(reader) {
117
+ getModuleInfo = reader
118
+ },
119
+ captureStyle(id, style) {
120
+ processedStyles.set(id, style)
121
+ },
122
+ plugin: plugin,
123
+ async publishChanged(changedIds) {
124
+ const styleChanged = changedIds.some(isGlobalStyleRequest)
125
+ const candidatesChanged = changedIds.some((id) => !isCSSRequest(id))
126
+ if (!styleChanged && !candidatesChanged) {
127
+ return
128
+ }
129
+ if (!getModuleInfo) {
130
+ throw new Error('WX style graph is unavailable before HMR publication')
131
+ }
132
+
133
+ // Traverse topology exactly once; root refresh and final rendering consume this immutable transaction plan.
134
+ const styleIds = createGraphStylePlan(applicationEntryIds, getModuleInfo, (styleId) =>
135
+ processedStyles.has(styleId)
136
+ )
137
+ if (candidatesChanged) {
138
+ await refreshTailwindStyles(styleIds, processedStyles, transformTailwindRoot)
139
+ }
140
+ const css = composeGraphStyleCss(styleIds, (styleId) => requireProcessedStyle(processedStyles, styleId).css)
141
+ const wxss = (await transformWxStyle(css)).css
142
+ if (wxss !== publishedWxss) {
143
+ await writeHmrFile(outDir, globalWxssFileName, wxss)
144
+ }
145
+ // HMR is the second physical writer; advance the frontier only after its atomic write succeeds or was unnecessary.
146
+ publishedWxss = wxss
147
+ }
148
+ }
149
+ }
150
+
151
+ /** Regenerates all reachable Tailwind roots and commits the cache only when every transform succeeds. */
152
+ async function refreshTailwindStyles(
153
+ styleIds: readonly string[],
154
+ processedStyles: Map<string, ProcessedStyle>,
155
+ transformRoot: (rootId: string, requestId: string) => Promise<Readonly<{ code: string }> | null>
156
+ ): Promise<void> {
157
+ const roots = styleIds.filter((styleId) => requireProcessedStyle(processedStyles, styleId).isTailwindRoot)
158
+ const refreshedStyles = await Promise.all(
159
+ roots.map(async (rootId) => {
160
+ const requestId = createTailwindSidecarId(rootId)
161
+ const result = await transformRoot(rootId, requestId)
162
+ if (!result) {
163
+ throw new Error(`Tailwind sidecar transform produced no result: ${requestId}`)
164
+ }
165
+ return [rootId, { css: extractViteCss(result.code, requestId), isTailwindRoot: true }] as const
166
+ })
167
+ )
168
+ refreshedStyles.forEach(([rootId, style]) => {
169
+ processedStyles.set(rootId, style)
170
+ })
171
+ }
172
+
173
+ function requireProcessedStyle(processedStyles: ReadonlyMap<string, ProcessedStyle>, styleId: string): ProcessedStyle {
174
+ const style = processedStyles.get(styleId)
175
+ if (!style) {
176
+ throw new Error(`WX style plan references uncaptured CSS: ${styleId}`)
177
+ }
178
+ return style
179
+ }
@@ -2,12 +2,12 @@ import { readFile } from 'node:fs/promises'
2
2
  import type { ServerResponse } from 'node:http'
3
3
  import path from 'node:path'
4
4
  import colors from 'picocolors'
5
- import type { GetModuleInfo } from 'rolldown'
6
5
  import { type DevEngine, type DevOptions, dev } from 'rolldown/experimental'
7
- import { type Connect, isCSSRequest, type ViteDevServer } from 'vite'
6
+ import { asyncScheduler } from 'rxjs'
7
+ import type { Connect, ViteDevServer } from 'vite'
8
8
  import type { VitePluginTaroOptions } from '../../../../options.ts'
9
- import { SerializedTaskQueue } from '../../../utils/serialized-task-queue.ts'
10
- import { createGraphStylePlan, isGlobalStyleRequest } from '../styles/utils.ts'
9
+ import { createHmrResultsStream } from './create-hmr-results-stream.ts'
10
+ import { createStyleCapture, type StyleCaptureAction } from './create-style-capture.ts'
11
11
  import {
12
12
  developmentAppWxssFileName,
13
13
  type HmrInfo,
@@ -20,32 +20,31 @@ import {
20
20
  renderInitialHmrPatches,
21
21
  writeHmrFile
22
22
  } from './hmr-files.ts'
23
+ import { createHostActions } from './host-actions.ts'
23
24
  import { PatchPublisher } from './patch-publisher.ts'
24
- import { createStyleCapturePlugin, type ProcessedStyle } from './styles/create-style-capture-plugin.ts'
25
- import { globalWxssFileName, publishStyleHmr, refreshTailwindStyles } from './styles/publish-style-hmr.ts'
25
+ import { createRuntimeReportsStream, type RuntimeReport } from './runtime-reports.ts'
26
26
  import { type BundledDev, installWxDevOptions, requireSingleOutput } from './wx-dev-options.ts'
27
27
 
28
28
  export type WxDevHost = Readonly<{
29
29
  close: () => Promise<void>
30
30
  }>
31
31
 
32
- type AppliedReport = Readonly<{
33
- kind: 'applied'
34
- buildId: string
35
- seq: number
36
- }>
37
-
38
- /** The runtime hit an unrecoverable state (e.g. a corrupted patch range) and needs a full rebuild. */
39
- type RebuildReport = Readonly<{
40
- kind: 'rebuild'
41
- buildId: string
42
- reason: string
43
- }>
44
-
45
- type HmrReport = AppliedReport | RebuildReport
46
32
  type HmrUpdatesResult = Parameters<NonNullable<DevOptions['onHmrUpdates']>>[0]
47
33
  type HmrUpdates = Exclude<HmrUpdatesResult, Error>
48
34
  type DevOutputResult = Parameters<NonNullable<DevOptions['onOutput']>>[0]
35
+ /** A complete build callback is a host action whether it carries durable output or an initial/later build failure. */
36
+ type OutputAction = Readonly<{ kind: 'output'; result: DevOutputResult }>
37
+
38
+ type HostAction =
39
+ | StyleCaptureAction
40
+ | Readonly<{ kind: 'publish'; result: HmrUpdates }>
41
+ | Readonly<{ kind: 'error'; error: Error }>
42
+ | Readonly<{ kind: 'reports'; reports: readonly RuntimeReport[] }>
43
+ | OutputAction
44
+ | Readonly<{ kind: 'listening' }>
45
+
46
+ /** One short trailing-edge window absorbs editor bursts before style preparation and physical Page notification. */
47
+ const hmrSettleMilliseconds = 32
49
48
 
50
49
  /**
51
50
  * Creates the wx dev host: the adapter that owns the physical Rolldown DevEngine (created
@@ -66,48 +65,76 @@ export async function createWxDevHost({
66
65
  applicationEntryIds: readonly string[]
67
66
  }): Promise<WxDevHost> {
68
67
  const bundledDev = getBundledDev(server)
69
- // This is the host's mutable style projection: CSS absent from Rolldown, the live graph capability rebound by each
70
- // complete build, and the last durable WXSS bytes used only to suppress identical filesystem publications. Style capture
71
- // remains O(1); derived order and reachability stay local to each HMR transaction.
72
- const styleState: {
73
- getModuleInfo: GetModuleInfo | undefined
74
- processedStyles: Map<string, ProcessedStyle>
75
- publishedWxss: string | undefined
76
- } = {
77
- getModuleInfo: undefined,
78
- processedStyles: new Map(),
79
- publishedWxss: undefined
80
- }
81
- const styleCapturePlugin = createStyleCapturePlugin({
82
- captureGraph(reader) {
83
- styleState.getModuleInfo = reader
84
- },
85
- captureStyle(id, style) {
86
- styleState.processedStyles.set(id, style)
87
- }
88
- })
89
68
 
90
- // Rolldown invokes output callbacks without awaiting their promises. This queue is the single owner of mutable HMR host
91
- // state and physical metadata writes, preventing a later patch or build identity from being overwritten by older work.
92
- const hostTasks = new SerializedTaskQueue((operation, error) => logWxError(server.config.logger, operation, error))
69
+ // All callbacks admit typed actions through this edge; concatMap is the sole owner of effect ordering and mutable host state.
70
+ const hostActions = createHostActions<HostAction>(applyHostAction, (action, error) =>
71
+ logWxError(server.config.logger, `wx HMR ${action.kind} failed`, error)
72
+ )
93
73
 
74
+ const styleCapture = createStyleCapture({
75
+ applicationEntryIds: applicationEntryIds,
76
+ outDir: server.config.build.outDir,
77
+ // Capture hooks run only after engine.run, when the host action subscription and all reducer dependencies are ready.
78
+ emit: (action) => hostActions.next(action),
79
+ transformTailwindRoot: async (rootId, requestId) => {
80
+ // The capture contains generated CSS, while the sidecar must re-run from raw Tailwind directives. Vite exposes no
81
+ // raw source in its live module graph, so this read is the authoritative source generation rather than a read-back.
82
+ return server.environments.client.pluginContainer.transform(await readFile(rootId, 'utf8'), requestId)
83
+ }
84
+ })
94
85
  const publisher = new PatchPublisher((content) =>
95
86
  writeHmrFile(server.config.build.outDir, hmrPatchesFileName, content)
96
87
  )
97
88
 
98
- // DevEngine does not reject run() after an initial plugin failure. The options layer owns a first-build buildEnd barrier
99
- // and exposes only its result; later build errors continue independently through onOutput and onHmrUpdates.
100
- const initialBuild = installWxDevOptions({ bundledDev, server, options, hostPlugins: [styleCapturePlugin] })
89
+ // Option installation now configures only Rolldown. Build lifecycle results enter through the engine's output action below.
90
+ installWxDevOptions({ bundledDev, server, options, hostPlugins: [styleCapture.plugin] })
101
91
  const engine: DevEngine = await createEngine()
102
92
 
103
- // The wx dev host owns the only DevEngine. Vite's default listen() would create a second
104
- // skip-write engine that renders into memory for browser HMR; instead the physical
105
- // engine's initial build must finish before the HTTP server becomes ready, because
106
- // DevTools opens the output directory directly and the app requires its files on disk.
93
+ const hmrResults = createHmrResultsStream(
94
+ hmrSettleMilliseconds,
95
+ asyncScheduler,
96
+ (result) => {
97
+ // One reduced window becomes one existing host transaction: style preparation, one cumulative patch write, then
98
+ // ordered delivery notifications for every original Rolldown payload retained by the stream.
99
+ hostActions.next({ kind: 'publish', result: result })
100
+ },
101
+ (error) => {
102
+ /*
103
+ * Invalid syntax is a normal intermediate editor generation. Rolldown produced no patch, so neither the published
104
+ * nor applied runtime frontier needs repair. Triggering a complete build here would immediately compile the same
105
+ * invalid source and empirically wedges that DevEngine generation. Admit only the diagnostic; when the user saves
106
+ * valid source, the watch engine emits an ordinary successful callback and resumes the normal publication path.
107
+ */
108
+ hostActions.next({ kind: 'error', error: error })
109
+ }
110
+ )
111
+
112
+ const runtimeReports = createRuntimeReportsStream(hmrSettleMilliseconds, asyncScheduler, (reports) => {
113
+ // ACK conflation happens before admission, but publisher validation and mutation remain ordered with patch writes and
114
+ // build rotation. Reports arriving during publication therefore execute only after that physical transaction commits.
115
+ hostActions.next({ kind: 'reports', reports: reports })
116
+ })
117
+ /*
118
+ * BundledDev is Vite's mutable environment adapter. Transfer its engine slot to the one physical writer created above so
119
+ * Vite middleware and close() address the same engine; leaving the slot untouched would make listen() create a second,
120
+ * skip-write browser engine with a divergent graph. Disable Vite's access-triggered regeneration because runtime reports and
121
+ * Rolldown HMR results now own every rebuild decision. Finally replace listen so readiness waits for physical initial output
122
+ * instead of Vite's in-memory bundle. These assignments are installation-time ownership transfer, never per-update state.
123
+ */
107
124
  bundledDev._devEngine = engine
108
125
  bundledDev.triggerBundleRegenerationIfStale = async () => false
109
126
  bundledDev.listen = async () => {
110
- await Promise.all([engine.run(), initialBuild])
127
+ /*
128
+ * Subscribe before run because DevEngine intentionally fulfills run() after an initial plugin failure and emits that
129
+ * Error through onOutput. Observing the existing action edge preserves the exact failure without a second buildEnd
130
+ * Promise or forwarding Subject; waitForAction does not consume the action from concatMap's reducer subscription.
131
+ */
132
+ const initialOutput = hostActions.waitForAction((action): action is OutputAction => action.kind === 'output')
133
+ await engine.run()
134
+ const { result } = await initialOutput
135
+ if (result instanceof Error) {
136
+ throw result
137
+ }
111
138
  await engine.ensureCurrentBuildFinish()
112
139
  }
113
140
 
@@ -115,7 +142,8 @@ export async function createWxDevHost({
115
142
  // the actual port is not observable while onOutput runs for the first build. The App metadata
116
143
  // is written once the port is real; later full builds rewrite it from onOutput.
117
144
  server.httpServer?.once('listening', () => {
118
- hostTasks.enqueue('wx HMR initialization failed', rotateBuildSession)
145
+ // Port availability is lifecycle data, so initial identity rotation follows prior output/capture actions in the reducer.
146
+ hostActions.next({ kind: 'listening' })
119
147
  })
120
148
 
121
149
  // The runtime's metadata-only reports land on the control path; the buildId in each report
@@ -126,8 +154,13 @@ export async function createWxDevHost({
126
154
 
127
155
  return {
128
156
  close: async () => {
129
- await hostTasks.waitForIdle()
130
- await engine.close()
157
+ // Quiet-window completion first admits final HMR publications and rebuild reports into the serialized action edge.
158
+ hmrResults.complete()
159
+ runtimeReports.complete()
160
+ await hostActions.waitForIdle()
161
+ // Keep the action edge open until the final generation has admitted all capture and output callbacks.
162
+ await engine.ensureCurrentBuildFinish()
163
+ await hostActions.complete()
131
164
  }
132
165
  }
133
166
 
@@ -149,6 +182,10 @@ export async function createWxDevHost({
149
182
  await publishBuildMetadata(server, buildId, port)
150
183
  }
151
184
 
185
+ /**
186
+ * Mutates only the request-local HTTP response: unsupported methods become 404, malformed bounded bodies become 400, and a
187
+ * valid report is admitted before the default 200 response ends. No response object or parser state crosses requests.
188
+ */
152
189
  async function handleReport(req: Connect.IncomingMessage, res: ServerResponse): Promise<void> {
153
190
  if (req.method !== 'POST') {
154
191
  res.statusCode = 404
@@ -157,8 +194,8 @@ export async function createWxDevHost({
157
194
  }
158
195
 
159
196
  try {
160
- const report = JSON.parse(await readBody(req)) as HmrReport
161
- await hostTasks.run(() => processReport(report))
197
+ const report = JSON.parse(await readBody(req)) as RuntimeReport
198
+ runtimeReports.next(report)
162
199
  res.end()
163
200
  } catch (e) {
164
201
  logWxError(server.config.logger, 'wx HMR report failed', e)
@@ -167,8 +204,39 @@ export async function createWxDevHost({
167
204
  }
168
205
  }
169
206
 
207
+ /** Reduces one merged source action through the existing authoritative host state. */
208
+ function applyHostAction(action: HostAction): void | Promise<void> {
209
+ switch (action.kind) {
210
+ case 'capture-graph':
211
+ // The buildStart reader is a live capability; replacing it only here makes the reducer own build rebinding.
212
+ styleCapture.captureGraph(action.getModuleInfo)
213
+ return
214
+ case 'capture-style':
215
+ // A failed upstream transform emits no action, intentionally retaining the last valid processed CSS generation.
216
+ styleCapture.captureStyle(action.id, action.style)
217
+ return
218
+ case 'publish':
219
+ return publishUpdates(action.result)
220
+ case 'error':
221
+ logWxError(server.config.logger, 'wx HMR update failed', action.error)
222
+ return
223
+ case 'reports':
224
+ action.reports.forEach(processReport)
225
+ return
226
+ case 'output':
227
+ if (action.result instanceof Error) {
228
+ logWxError(server.config.logger, 'wx dev build failed', action.result)
229
+ return
230
+ }
231
+ styleCapture.bindOutput(action.result.output)
232
+ return rotateBuildSession()
233
+ case 'listening':
234
+ return rotateBuildSession()
235
+ }
236
+ }
237
+
170
238
  /** Applies one runtime receipt to the active physical patch history. */
171
- function processReport(report: HmrReport): void {
239
+ function processReport(report: RuntimeReport): void {
172
240
  // Delayed reports from older builds must never prune the live build's cumulative patch history.
173
241
  if (!publisher.isCurrentBuild(report.buildId)) {
174
242
  return
@@ -176,8 +244,7 @@ export async function createWxDevHost({
176
244
 
177
245
  switch (report.kind) {
178
246
  case 'rebuild': {
179
- server.config.logger.info(`[vpt] wx runtime requested a full rebuild: ${report.reason}`)
180
- engine.triggerFullBuild()
247
+ requestFullBuild(report.reason)
181
248
  return
182
249
  }
183
250
  case 'applied': {
@@ -193,87 +260,76 @@ export async function createWxDevHost({
193
260
  const output = requireSingleOutput(rolldownOptions)
194
261
 
195
262
  return dev(rolldownOptions, output, {
196
- onHmrUpdates: handleHmrUpdates,
197
- onOutput: handleDevOutput,
263
+ /**
264
+ * Admits Rolldown's non-awaited callback without starting asynchronous host work on the binding callback stack.
265
+ * The Subject retains Error values as control events and successful values as lossless patch data until its quiet edge.
266
+ */
267
+ onHmrUpdates: (result: HmrUpdatesResult) => {
268
+ hmrResults.next(result)
269
+ },
270
+ // Initial and later complete builds share one admission path; startup merely observes the first OutputAction.
271
+ onOutput: (result: DevOutputResult) => {
272
+ hostActions.next({ kind: 'output', result: result })
273
+ },
198
274
  rebuildStrategy: 'never',
199
- watch: { skipWrite: false }
275
+ watch: {
276
+ // Rolldown must observe every source generation and emit every incremental factory: later patches do not
277
+ // reconstruct modules changed only by an earlier callback. RxJS conflates publication after compilation,
278
+ // preserving the complete patch sequence while avoiding repeated physical DevTools notifications.
279
+ skipWrite: false,
280
+ useDebounce: false
281
+ }
200
282
  })
201
283
  }
202
284
 
203
- /** Converts Rolldown's non-awaited callback into one ordered host publication task. */
204
- function handleHmrUpdates(result: HmrUpdatesResult): void {
205
- if (result instanceof Error) {
206
- logWxError(server.config.logger, 'wx HMR update failed', result)
207
- return
208
- }
209
- hostTasks.enqueue('wx HMR publication failed', () => publishUpdates(result))
285
+ /** Centralizes the one diagnostic and DevEngine command used by every rebuild authority. */
286
+ function requestFullBuild(reason: string | undefined): void {
287
+ server.config.logger.info(`[vpt] wx full rebuild required${reason ? `: ${reason}` : ''}`)
288
+ engine.triggerFullBuild()
210
289
  }
211
290
 
212
- /** Publishes only the active client's patches or requests the complete build required by Rolldown. */
291
+ /** Selects active updates and publishes their resulting host transaction. */
213
292
  async function publishUpdates(result: HmrUpdates): Promise<void> {
214
- const batch: PatchUpdate[] = []
293
+ /*
294
+ * This transaction-local array preserves every patch in one pass and permits immediate rebuild dominance. It never escapes
295
+ * the serialized action. A filter/find/flatMap pipeline would scan a large burst repeatedly and allocate an intermediate.
296
+ */
297
+ const patches: PatchUpdate[] = []
215
298
  for (const { clientId, update } of result.updates) {
216
299
  if (!publisher.isCurrentBuild(clientId) || update.type === 'Noop') {
217
300
  continue
218
301
  }
219
302
 
220
303
  if (update.type === 'Patch') {
221
- batch.push(update)
304
+ patches.push(update)
222
305
  continue
223
306
  }
224
307
 
225
- server.config.logger.info(`[vpt] wx full rebuild required${update.reason ? `: ${update.reason}` : ''}`)
226
- engine.triggerFullBuild()
308
+ requestFullBuild(update.reason)
227
309
  return
228
310
  }
229
311
 
230
- if (batch.length === 0) {
312
+ await publishPatchBatch(patches)
313
+ }
314
+
315
+ /** Publishes one coherent WXSS, patch-file, and Rolldown-frontier transaction. */
316
+ async function publishPatchBatch(patches: readonly PatchUpdate[]): Promise<void> {
317
+ if (patches.length === 0) {
231
318
  return
232
319
  }
233
320
 
234
321
  // `onHmrUpdates` is the transaction boundary after every affected transform has updated graph and candidate state.
235
322
  // Every non-CSS edit may alter imports or Tailwind classes; rendering broadly and comparing finalized bytes avoids
236
323
  // source scanning while preventing unrelated JavaScript edits from notifying DevTools through an identical rename.
237
- await publishChangedStyles(batch)
324
+ await styleCapture.publishChanged(patches.flatMap((patch) => patch.changedIds))
238
325
 
239
326
  // Publish global.wxss before the matching JavaScript patch so DevTools observes a coherent HMR transaction.
240
327
  // The physical file must exist before Rolldown advances: once committed, later patches may be generated relative to
241
328
  // this batch even if DevTools has not observed its file event yet. PatchPublisher keeps the unapplied range cumulative,
242
329
  // so any later file generation still carries every factory needed to bridge the runtime's older application frontier.
243
- await publisher.produce(batch)
244
-
245
- await commitPublishedBatch(batch)
246
- }
247
-
248
- /** Publishes the style projection for one completed patch transaction when its source or topology may have changed. */
249
- async function publishChangedStyles(batch: readonly PatchUpdate[]): Promise<void> {
250
- const changedIds = batch.flatMap((patch) => patch.changedIds)
251
- const styleChanged = changedIds.some(isGlobalStyleRequest)
252
- const candidatesChanged = changedIds.some((id) => !isCSSRequest(id))
253
- if (!styleChanged && !candidatesChanged) {
254
- return
255
- }
330
+ await publisher.produce(patches)
256
331
 
257
- // buildStart installs this reader before Rolldown can produce either a complete output or an incremental batch.
258
- const getModuleInfo = styleState.getModuleInfo
259
- if (!getModuleInfo) {
260
- throw new Error('WX style graph is unavailable before HMR publication')
261
- }
262
- // Traverse topology exactly once; root refresh and final rendering consume this immutable transaction plan.
263
- const styleIds = createGraphStylePlan(applicationEntryIds, getModuleInfo, (styleId) =>
264
- styleState.processedStyles.has(styleId)
265
- )
266
- if (candidatesChanged) {
267
- await refreshTailwindStyles(styleIds, styleState.processedStyles, async (rootId, requestId) =>
268
- server.environments.client.pluginContainer.transform(await readFile(rootId, 'utf8'), requestId)
269
- )
270
- }
271
- styleState.publishedWxss = await publishStyleHmr({
272
- styleIds: styleIds,
273
- outDir: server.config.build.outDir,
274
- processedStyles: styleState.processedStyles,
275
- publishedWxss: styleState.publishedWxss
276
- })
332
+ await commitPublishedBatch(patches)
277
333
  }
278
334
 
279
335
  /**
@@ -291,21 +347,6 @@ export async function createWxDevHost({
291
347
  await engine.notifyPayloadDelivered(patch.filename)
292
348
  }
293
349
  }
294
-
295
- /** Rotates metadata after each successful complete output. */
296
- function handleDevOutput(result: DevOutputResult): void {
297
- if (result instanceof Error) {
298
- logWxError(server.config.logger, 'wx dev build failed', result)
299
- return
300
- }
301
- hostTasks.enqueue('wx dev build finalization failed', finalizeDevOutput)
302
- }
303
-
304
- /** Rebinds the byte frontier after a complete build replaces the physical stylesheet outside HMR publication. */
305
- async function finalizeDevOutput(): Promise<void> {
306
- styleState.publishedWxss = await readFile(path.join(server.config.build.outDir, globalWxssFileName), 'utf8')
307
- await rotateBuildSession()
308
- }
309
350
  }
310
351
 
311
352
  /** The bound HTTP port, or undefined before Vite's server is listening. */
@@ -340,6 +381,12 @@ async function publishBuildMetadata(server: ViteDevServer, buildId: string, port
340
381
 
341
382
  /** Replaces browser server URLs with the physical project directory consumed by WeChat DevTools. */
342
383
  function installDevToolsPrinter(server: ViteDevServer): void {
384
+ /*
385
+ * Vite exposes printing as a mutable server callback because URLs are unknown until startup. WX serves control reports over
386
+ * HTTP but users open the physical output directory, so retaining the browser printer advertises unusable navigation URLs.
387
+ * Replacing only this presentation callback leaves resolved URLs and server routing untouched and naturally dies with the
388
+ * server instance; logging elsewhere would duplicate Vite's one readiness notification.
389
+ */
343
390
  server.printUrls = () => {
344
391
  server.config.logger.info(
345
392
  ` ${colors.green('➜')} ${colors.bold('WeChat DevTools')}: ${colors.cyan(relativeToViteConfig(server.config.build.outDir, server.config.configFile, server.config.root))}`
@@ -371,6 +418,12 @@ const maximumBodyBytes = 64 * 1024
371
418
 
372
419
  function readBody(req: Connect.IncomingMessage): Promise<string> {
373
420
  return new Promise((resolve, reject) => {
421
+ /*
422
+ * Node delivers one HTTP body through multiple callback invocations, so this request-local mutable string is the minimal
423
+ * state that joins chunks in wire order. The 64 KiB bound caps both retained memory and repeated-concatenation work; the
424
+ * value is parsed once on end and then discarded. Parsing per chunk is invalid because JSON tokens may cross boundaries,
425
+ * while retaining request bodies in host state would couple unrelated Page reports and leak across the server lifecycle.
426
+ */
374
427
  let body = ''
375
428
  req.on('data', (chunk: Buffer) => {
376
429
  body += chunk.toString('utf8')
@@ -4,6 +4,7 @@ import path from 'node:path'
4
4
  import type { BindingClientHmrUpdate } from 'rolldown/experimental'
5
5
 
6
6
  export const developmentAppWxssFileName = 'app.wxss'
7
+ export const globalWxssFileName = 'assets/global.wxss'
7
8
  export const hmrInfoFileName = 'hmr/info.js'
8
9
  export const hmrPatchesFileName = 'hmr/patches.js'
9
10