vite-plugin-taro 0.5.4 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/dist/node/plugins/h5/create-stencil-client-adapter.d.ts +10 -13
- package/dist/node/plugins/h5/create-stencil-client-adapter.js +63 -51
- package/dist/node/plugins/h5/plugins.d.ts +10 -0
- package/dist/node/plugins/h5/plugins.js +39 -11
- package/dist/node/plugins/wx/dev/dev-host.js +103 -216
- package/dist/node/plugins/wx/dev/hmr-files.d.ts +4 -5
- package/dist/node/plugins/wx/dev/hmr-files.js +18 -8
- package/dist/node/plugins/wx/dev/patch-publisher.d.ts +25 -5
- package/dist/node/plugins/wx/dev/patch-publisher.js +30 -9
- package/dist/node/plugins/wx/dev/react-refresh.d.ts +12 -0
- package/dist/node/plugins/wx/dev/react-refresh.js +111 -108
- package/dist/node/plugins/wx/dev/wx-dev-options.d.ts +25 -0
- package/dist/node/plugins/wx/dev/wx-dev-options.js +147 -0
- package/dist/node/utils/oxc-transform.d.ts +21 -0
- package/dist/node/utils/oxc-transform.js +58 -0
- package/dist/node/utils/serialized-task-queue.d.ts +6 -2
- package/dist/node/utils/serialized-task-queue.js +10 -2
- package/dist/runtime/wx/capsule/page.js +14 -5
- package/dist/runtime/wx/dev/dev-runtime.js +70 -26
- package/package.json +4 -3
- package/src/node/plugins/h5/create-stencil-client-adapter.ts +74 -73
- package/src/node/plugins/h5/plugins.ts +42 -13
- package/src/node/plugins/wx/dev/dev-host.ts +121 -256
- package/src/node/plugins/wx/dev/hmr-files.ts +20 -8
- package/src/node/plugins/wx/dev/patch-publisher.ts +36 -10
- package/src/node/plugins/wx/dev/react-refresh.ts +125 -129
- package/src/node/plugins/wx/dev/wx-dev-options.ts +200 -0
- package/src/node/utils/oxc-transform.ts +77 -0
- package/src/node/utils/serialized-task-queue.ts +15 -3
- package/src/runtime/wx/capsule/page.ts +23 -9
- package/src/runtime/wx/dev/dev-runtime.ts +80 -28
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
import type { ServerResponse } from 'node:http'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import colors from 'picocolors'
|
|
4
|
-
import type
|
|
5
|
-
import { build } from 'rolldown'
|
|
6
|
-
import { type DevEngine, dev, viteReporterPlugin } from 'rolldown/experimental'
|
|
4
|
+
import { type DevEngine, type DevOptions, dev } from 'rolldown/experimental'
|
|
7
5
|
import type { Connect, ViteDevServer } from 'vite'
|
|
8
6
|
import type { VitePluginTaroOptions } from '../../../../options.ts'
|
|
9
|
-
import {
|
|
10
|
-
import { resolvePackageFile } from '../../../utils/packages.ts'
|
|
11
|
-
import { appShellFileName } from '../module.ts'
|
|
12
|
-
import { createWxDevMode } from './create-wx-dev-mode.ts'
|
|
13
|
-
import { emptyOutputDirectory } from './empty-output-directory.ts'
|
|
7
|
+
import { SerializedTaskQueue } from '../../../utils/serialized-task-queue.ts'
|
|
14
8
|
import {
|
|
15
9
|
type HmrInfo,
|
|
16
10
|
hmrControlPath,
|
|
@@ -22,13 +16,14 @@ import {
|
|
|
22
16
|
writeHmrFile
|
|
23
17
|
} from './hmr-files.ts'
|
|
24
18
|
import { PatchPublisher } from './patch-publisher.ts'
|
|
19
|
+
import { type BundledDev, installWxDevOptions, requireSingleOutput } from './wx-dev-options.ts'
|
|
25
20
|
|
|
26
21
|
export type WxDevHost = Readonly<{
|
|
27
22
|
close: () => Promise<void>
|
|
28
23
|
}>
|
|
29
24
|
|
|
30
|
-
type
|
|
31
|
-
kind: '
|
|
25
|
+
type AppliedReport = Readonly<{
|
|
26
|
+
kind: 'applied'
|
|
32
27
|
buildId: string
|
|
33
28
|
seq: number
|
|
34
29
|
}>
|
|
@@ -37,8 +32,14 @@ type DeliveryReport = Readonly<{
|
|
|
37
32
|
type RebuildReport = Readonly<{
|
|
38
33
|
kind: 'rebuild'
|
|
39
34
|
buildId: string
|
|
35
|
+
reason: string
|
|
40
36
|
}>
|
|
41
37
|
|
|
38
|
+
type HmrReport = AppliedReport | RebuildReport
|
|
39
|
+
type HmrUpdatesResult = Parameters<NonNullable<DevOptions['onHmrUpdates']>>[0]
|
|
40
|
+
type HmrUpdates = Exclude<HmrUpdatesResult, Error>
|
|
41
|
+
type DevOutputResult = Parameters<NonNullable<DevOptions['onOutput']>>[0]
|
|
42
|
+
|
|
42
43
|
/**
|
|
43
44
|
* Creates the wx dev host: the adapter that owns the physical Rolldown DevEngine (created
|
|
44
45
|
* with dev(...)) and the patch publisher, and replaces Vite's bundledDev.listen so the
|
|
@@ -53,23 +54,17 @@ export async function createWxDevHost({
|
|
|
53
54
|
options: VitePluginTaroOptions
|
|
54
55
|
}): Promise<WxDevHost> {
|
|
55
56
|
const bundledDev = getBundledDev(server)
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
const settleInitialBuild = once((error: Error | undefined): void => {
|
|
60
|
-
if (error) {
|
|
61
|
-
initialBuild.reject(error)
|
|
62
|
-
} else {
|
|
63
|
-
initialBuild.resolve()
|
|
64
|
-
}
|
|
65
|
-
})
|
|
57
|
+
// Rolldown invokes output callbacks without awaiting their promises. This queue is the single owner of mutable HMR host
|
|
58
|
+
// state and physical metadata writes, preventing a later patch or build identity from being overwritten by older work.
|
|
59
|
+
const hostTasks = new SerializedTaskQueue((operation, error) => logWxError(server.config.logger, operation, error))
|
|
66
60
|
|
|
67
61
|
const publisher = new PatchPublisher((content) =>
|
|
68
62
|
writeHmrFile(server.config.build.outDir, hmrPatchesFileName, content)
|
|
69
63
|
)
|
|
70
64
|
|
|
71
|
-
//
|
|
72
|
-
|
|
65
|
+
// DevEngine does not reject run() after an initial plugin failure. The options layer owns a first-build buildEnd barrier
|
|
66
|
+
// and exposes only its result; later build errors continue independently through onOutput and onHmrUpdates.
|
|
67
|
+
const initialBuild = installWxDevOptions({ bundledDev, server, options })
|
|
73
68
|
const engine: DevEngine = await createEngine()
|
|
74
69
|
|
|
75
70
|
// The wx dev host owns the only DevEngine. Vite's default listen() would create a second
|
|
@@ -79,37 +74,32 @@ export async function createWxDevHost({
|
|
|
79
74
|
bundledDev._devEngine = engine
|
|
80
75
|
bundledDev.triggerBundleRegenerationIfStale = async () => false
|
|
81
76
|
bundledDev.listen = async () => {
|
|
82
|
-
await Promise.all([engine.run(), initialBuild
|
|
77
|
+
await Promise.all([engine.run(), initialBuild])
|
|
83
78
|
await engine.ensureCurrentBuildFinish()
|
|
84
79
|
}
|
|
85
80
|
|
|
86
81
|
// Vite binds the port only after initServer (and therefore the initial build) completes, so
|
|
87
82
|
// the actual port is not observable while onOutput runs for the first build. The App metadata
|
|
88
83
|
// is written once the port is real; later full builds rewrite it from onOutput.
|
|
89
|
-
server.httpServer?.once('listening',
|
|
84
|
+
server.httpServer?.once('listening', () => {
|
|
85
|
+
hostTasks.enqueue('wx HMR initialization failed', rotateBuildSession)
|
|
86
|
+
})
|
|
90
87
|
|
|
91
88
|
// The runtime's metadata-only reports land on the control path; the buildId in each report
|
|
92
89
|
// IS the Rolldown client ID.
|
|
93
90
|
server.middlewares.use(hmrControlPath, (req, res) => void handleReport(req, res))
|
|
94
91
|
|
|
95
|
-
|
|
96
|
-
// opens the output directory directly, so the printed path is the one to select.
|
|
97
|
-
const originalPrintUrls = server.printUrls.bind(server)
|
|
98
|
-
server.printUrls = () => {
|
|
99
|
-
originalPrintUrls()
|
|
100
|
-
server.config.logger.info(
|
|
101
|
-
` ${colors.green('➜')} ${colors.bold('WeChat DevTools')}: ${colors.cyan(relativeToViteConfig(server.config.build.outDir, server.config.configFile, server.config.root))}`
|
|
102
|
-
)
|
|
103
|
-
}
|
|
92
|
+
installDevToolsPrinter(server)
|
|
104
93
|
|
|
105
94
|
return {
|
|
106
95
|
close: async () => {
|
|
96
|
+
await hostTasks.waitForIdle()
|
|
107
97
|
await engine.close()
|
|
108
98
|
}
|
|
109
99
|
}
|
|
110
100
|
|
|
111
101
|
/** Rotates the build identity and materializes the App metadata for it. */
|
|
112
|
-
async function
|
|
102
|
+
async function rotateBuildSession(): Promise<void> {
|
|
113
103
|
const port = boundPort(server)
|
|
114
104
|
if (port === undefined) {
|
|
115
105
|
// The initial output finishes before Vite binds its port. The listening listener
|
|
@@ -123,10 +113,7 @@ export async function createWxDevHost({
|
|
|
123
113
|
}
|
|
124
114
|
await engine.registerClient(buildId)
|
|
125
115
|
|
|
126
|
-
|
|
127
|
-
// matching reset patch file.
|
|
128
|
-
await writeHmrFile(server.config.build.outDir, hmrPatchesFileName, renderInitialHmrPatches())
|
|
129
|
-
await writeHmrInfo(server, buildId, port)
|
|
116
|
+
await publishBuildMetadata(server, buildId, port)
|
|
130
117
|
}
|
|
131
118
|
|
|
132
119
|
async function handleReport(req: Connect.IncomingMessage, res: ServerResponse): Promise<void> {
|
|
@@ -137,22 +124,8 @@ export async function createWxDevHost({
|
|
|
137
124
|
}
|
|
138
125
|
|
|
139
126
|
try {
|
|
140
|
-
const report = JSON.parse(await readBody(req)) as
|
|
141
|
-
|
|
142
|
-
// delayed reports from older builds are ignored so they can never influence the
|
|
143
|
-
// live build.
|
|
144
|
-
if (!publisher.isCurrentBuild(report.buildId)) {
|
|
145
|
-
res.end()
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
if (report.kind === 'rebuild') {
|
|
149
|
-
engine.triggerFullBuild()
|
|
150
|
-
res.end()
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
// Commit every newly acknowledged Rolldown payload to this client's ship map.
|
|
154
|
-
const deliveredFiles = publisher.acknowledge(report.seq)
|
|
155
|
-
await Promise.all(deliveredFiles.map((fileName) => engine.notifyPayloadDelivered(fileName)))
|
|
127
|
+
const report = JSON.parse(await readBody(req)) as HmrReport
|
|
128
|
+
await hostTasks.run(() => processReport(report))
|
|
156
129
|
res.end()
|
|
157
130
|
} catch (e) {
|
|
158
131
|
logWxError(server.config.logger, 'wx HMR report failed', e)
|
|
@@ -161,120 +134,102 @@ export async function createWxDevHost({
|
|
|
161
134
|
}
|
|
162
135
|
}
|
|
163
136
|
|
|
137
|
+
/** Applies one runtime receipt to the active physical patch history. */
|
|
138
|
+
function processReport(report: HmrReport): void {
|
|
139
|
+
// Delayed reports from older builds must never prune the live build's cumulative patch history.
|
|
140
|
+
if (!publisher.isCurrentBuild(report.buildId)) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
switch (report.kind) {
|
|
145
|
+
case 'rebuild': {
|
|
146
|
+
server.config.logger.info(`[vpt] wx runtime requested a full rebuild: ${report.reason}`)
|
|
147
|
+
engine.triggerFullBuild()
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
case 'applied': {
|
|
151
|
+
publisher.acknowledge(report.seq)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
164
157
|
/** Creates the physical DevEngine with the wx dev host hooks. */
|
|
165
158
|
async function createEngine(): Promise<DevEngine> {
|
|
166
159
|
const rolldownOptions = await bundledDev.getRolldownOptions()
|
|
167
|
-
|
|
168
|
-
throw new Error('wx development requires exactly one Rolldown output.')
|
|
169
|
-
}
|
|
160
|
+
const output = requireSingleOutput(rolldownOptions)
|
|
170
161
|
|
|
171
|
-
return dev(rolldownOptions,
|
|
172
|
-
onHmrUpdates:
|
|
173
|
-
|
|
174
|
-
logWxError(server.config.logger, 'wx HMR update failed', result)
|
|
175
|
-
return
|
|
176
|
-
}
|
|
177
|
-
// Collect the current client's batch first: one HMR event can carry several
|
|
178
|
-
// client envelopes, while only the active build may enter its patch history.
|
|
179
|
-
const batch: PatchUpdate[] = []
|
|
180
|
-
for (const { clientId, update } of result.updates) {
|
|
181
|
-
// Removed and delayed client sessions can still appear in a completed
|
|
182
|
-
// engine batch; they must never enter the current build's patch history.
|
|
183
|
-
if (!publisher.isCurrentBuild(clientId) || update.type === 'Noop') {
|
|
184
|
-
continue
|
|
185
|
-
}
|
|
186
|
-
if (update.type === 'Patch') {
|
|
187
|
-
batch.push(update)
|
|
188
|
-
continue
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
server.config.logger.info(
|
|
192
|
-
`[vpt] wx full rebuild required${update.reason ? `: ${update.reason}` : ''}`
|
|
193
|
-
)
|
|
194
|
-
engine.triggerFullBuild()
|
|
195
|
-
return
|
|
196
|
-
}
|
|
197
|
-
if (batch.length > 0) {
|
|
198
|
-
await publisher.produce(batch)
|
|
199
|
-
// server.config.logger.info('[vpt] wx patch produced')
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
onOutput: async (result) => {
|
|
203
|
-
if (result instanceof Error) {
|
|
204
|
-
logWxError(server.config.logger, 'wx dev build failed', result)
|
|
205
|
-
return
|
|
206
|
-
}
|
|
207
|
-
// A fresh build identity per complete physical build; the App runtime reads it
|
|
208
|
-
// from hmr/info.js before any module registers.
|
|
209
|
-
await startFreshBuild()
|
|
210
|
-
},
|
|
162
|
+
return dev(rolldownOptions, output, {
|
|
163
|
+
onHmrUpdates: handleHmrUpdates,
|
|
164
|
+
onOutput: handleDevOutput,
|
|
211
165
|
rebuildStrategy: 'never',
|
|
212
166
|
watch: { skipWrite: false }
|
|
213
167
|
})
|
|
214
168
|
}
|
|
215
169
|
|
|
216
|
-
/**
|
|
217
|
-
function
|
|
218
|
-
|
|
170
|
+
/** Converts Rolldown's non-awaited callback into one ordered host publication task. */
|
|
171
|
+
function handleHmrUpdates(result: HmrUpdatesResult): void {
|
|
172
|
+
if (result instanceof Error) {
|
|
173
|
+
logWxError(server.config.logger, 'wx HMR update failed', result)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
hostTasks.enqueue('wx HMR publication failed', () => publishUpdates(result))
|
|
177
|
+
}
|
|
219
178
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const output = rolldownOptions.output
|
|
227
|
-
const configuredOutput = server.config.build.rolldownOptions.output
|
|
228
|
-
if (Array.isArray(configuredOutput)) {
|
|
229
|
-
throw new Error('wx development supports one configured Rolldown output.')
|
|
179
|
+
/** Publishes only the active client's patches or requests the complete build required by Rolldown. */
|
|
180
|
+
async function publishUpdates(result: HmrUpdates): Promise<void> {
|
|
181
|
+
const batch: PatchUpdate[] = []
|
|
182
|
+
for (const { clientId, update } of result.updates) {
|
|
183
|
+
if (!publisher.isCurrentBuild(clientId) || update.type === 'Noop') {
|
|
184
|
+
continue
|
|
230
185
|
}
|
|
231
186
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const pageFiles = new Set(options.pages.map((page) => `${page.path}.js`))
|
|
236
|
-
|
|
237
|
-
const configured = (configuredOutput ?? {}) as Record<string, unknown>
|
|
238
|
-
Object.assign(output, configured, {
|
|
239
|
-
assetFileNames: createStableFileNames(configured.assetFileNames, 'assets/[name][extname]'),
|
|
240
|
-
banner: createEntryBanner(pageFiles),
|
|
241
|
-
chunkFileNames: createStableFileNames(configured.chunkFileNames, 'assets/[name].js'),
|
|
242
|
-
entryFileNames: createStableFileNames(configured.entryFileNames, '[name]'),
|
|
243
|
-
format: 'es',
|
|
244
|
-
minify: true,
|
|
245
|
-
sourcemap: false
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
rolldownOptions.experimental ??= {}
|
|
249
|
-
rolldownOptions.experimental.devMode = createWxDevMode(
|
|
250
|
-
rolldownOptions.experimental.devMode,
|
|
251
|
-
await bundleRuntimeSource()
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
const emptyOutputDirectoryPlugin: Plugin = {
|
|
255
|
-
name: 'vpt:wx-empty-output-directory',
|
|
256
|
-
renderStart: {
|
|
257
|
-
order: 'pre',
|
|
258
|
-
// DevEngine bypasses Vite's build-only output preparation. Clear stale files before every complete
|
|
259
|
-
// physical render while retaining the directory watched by WeChat DevTools.
|
|
260
|
-
handler: () => emptyOutputDirectory(server.config.build.outDir)
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
const reportInitialBuildPlugin: Plugin = {
|
|
264
|
-
name: 'vpt:wx-report-initial-build',
|
|
265
|
-
buildEnd: settleInitialBuild
|
|
187
|
+
if (update.type === 'Patch') {
|
|
188
|
+
batch.push(update)
|
|
189
|
+
continue
|
|
266
190
|
}
|
|
267
191
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
192
|
+
server.config.logger.info(`[vpt] wx full rebuild required${update.reason ? `: ${update.reason}` : ''}`)
|
|
193
|
+
engine.triggerFullBuild()
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (batch.length === 0) {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// The physical file must exist before Rolldown advances: once committed, later patches may be generated relative to
|
|
202
|
+
// this batch even if DevTools has not observed its file event yet. PatchPublisher keeps the unapplied range cumulative,
|
|
203
|
+
// so any later file generation still carries every factory needed to bridge the runtime's older application frontier.
|
|
204
|
+
await publisher.produce(batch)
|
|
205
|
+
|
|
206
|
+
await commitPublishedBatch(batch)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Advances Rolldown's published frontier in the same sequence order materialized in the cumulative physical file.
|
|
211
|
+
*
|
|
212
|
+
* Given a batch [5, 6], publisher.produce has already made factories [5, 6] visible in hmr/patches.js. This method then
|
|
213
|
+
* commits payload 5 followed by payload 6. If DevTools observes neither event before sequence 7 is published, the next
|
|
214
|
+
* physical file contains [5, 6, 7], while Rolldown is free to generate 7 relative to its already-published sequence 6.
|
|
215
|
+
*/
|
|
216
|
+
async function commitPublishedBatch(batch: readonly PatchUpdate[]): Promise<void> {
|
|
217
|
+
// Do not use Promise.all or deduplicate filenames. Multiple sequential payloads may target the same output filename,
|
|
218
|
+
// and each notification commits one distinct Rolldown payload. Awaiting in order preserves the exact frontier encoded
|
|
219
|
+
// by PatchUpdate.seq and prevents a later payload from becoming visible to the engine before its predecessor.
|
|
220
|
+
for (const patch of batch) {
|
|
221
|
+
await engine.notifyPayloadDelivered(patch.filename)
|
|
276
222
|
}
|
|
277
223
|
}
|
|
224
|
+
|
|
225
|
+
/** Rotates metadata after each successful complete output. */
|
|
226
|
+
function handleDevOutput(result: DevOutputResult): void {
|
|
227
|
+
if (result instanceof Error) {
|
|
228
|
+
logWxError(server.config.logger, 'wx dev build failed', result)
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
hostTasks.enqueue('wx dev build finalization failed', rotateBuildSession)
|
|
232
|
+
}
|
|
278
233
|
}
|
|
279
234
|
|
|
280
235
|
/** The bound HTTP port, or undefined before Vite's server is listening. */
|
|
@@ -290,16 +245,28 @@ function boundPort(server: ViteDevServer): number | undefined {
|
|
|
290
245
|
return address.port
|
|
291
246
|
}
|
|
292
247
|
|
|
293
|
-
/**
|
|
294
|
-
async function
|
|
248
|
+
/** Resets physical patches before exposing the matching immutable build identity to a new App heap. */
|
|
249
|
+
async function publishBuildMetadata(server: ViteDevServer, buildId: string, port: number): Promise<void> {
|
|
295
250
|
const info: HmrInfo = {
|
|
296
251
|
buildId,
|
|
297
252
|
endpoint: `${server.config.server.https ? 'https' : 'http'}://${resolveEndpointHost(server)}:${port}${hmrControlPath}`
|
|
298
253
|
}
|
|
299
254
|
|
|
255
|
+
await writeHmrFile(server.config.build.outDir, hmrPatchesFileName, renderInitialHmrPatches())
|
|
300
256
|
await writeHmrFile(server.config.build.outDir, hmrInfoFileName, renderHmrInfo(info))
|
|
301
257
|
}
|
|
302
258
|
|
|
259
|
+
/** Appends the physical project directory after Vite's normal server URLs. */
|
|
260
|
+
function installDevToolsPrinter(server: ViteDevServer): void {
|
|
261
|
+
const originalPrintUrls = server.printUrls.bind(server)
|
|
262
|
+
server.printUrls = () => {
|
|
263
|
+
originalPrintUrls()
|
|
264
|
+
server.config.logger.info(
|
|
265
|
+
` ${colors.green('➜')} ${colors.bold('WeChat DevTools')}: ${colors.cyan(relativeToViteConfig(server.config.build.outDir, server.config.configFile, server.config.root))}`
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
303
270
|
/**
|
|
304
271
|
* The project directory shown in the DevTools banner, relative to the Vite config:
|
|
305
272
|
* `dist/wx` instead of the absolute output path, so it can be pasted into DevTools.
|
|
@@ -320,22 +287,6 @@ function logWxError(logger: ViteDevServer['config']['logger'], prefix: string, e
|
|
|
320
287
|
}
|
|
321
288
|
}
|
|
322
289
|
|
|
323
|
-
// The runtime source is immutable for the host's lifetime (it changes only when the
|
|
324
|
-
// plugin is rebuilt), so the nested bundle runs once and every build reuses it.
|
|
325
|
-
const bundleRuntimeSource = once(
|
|
326
|
-
/** Bundles the runtime host and state machine into one plain script for injection. */
|
|
327
|
-
async function bundleRuntimeSource(): Promise<string> {
|
|
328
|
-
// write: false — only the code is consumed; without it rolldown drops the bundle into
|
|
329
|
-
// the default dist/ of the running project.
|
|
330
|
-
const result = await build({
|
|
331
|
-
input: resolvePackageFile('dist/runtime/wx/dev/dev-runtime.js'),
|
|
332
|
-
output: { format: 'iife', minify: true, sourcemap: false },
|
|
333
|
-
write: false
|
|
334
|
-
})
|
|
335
|
-
return result.output[0].code
|
|
336
|
-
}
|
|
337
|
-
)
|
|
338
|
-
|
|
339
290
|
const maximumBodyBytes = 64 * 1024
|
|
340
291
|
|
|
341
292
|
function readBody(req: Connect.IncomingMessage): Promise<string> {
|
|
@@ -366,92 +317,6 @@ function resolveEndpointHost(server: ViteDevServer): string {
|
|
|
366
317
|
return address.address.includes(':') ? `[${address.address}]` : address.address
|
|
367
318
|
}
|
|
368
319
|
|
|
369
|
-
/**
|
|
370
|
-
* Prepends entry banners. Banners are plain text appended after Rolldown's analysis, so the
|
|
371
|
-
* requires never become chunk dependencies (a bare require inside the injected runtime source
|
|
372
|
-
* would stall the build), and the wx render pipeline keeps this text after the hoisted chunk
|
|
373
|
-
* requires — so the runtime chunk exists before these run:
|
|
374
|
-
* - the app entry loads hmr/info.js and initializes the runtime before any module registers;
|
|
375
|
-
* - every page requires hmr/patches.js, the changed dependency that makes DevTools re-execute
|
|
376
|
-
* live Pages and thereby load physical updates.
|
|
377
|
-
*/
|
|
378
|
-
function createEntryBanner(pageFiles: ReadonlySet<string>): (chunk: { name: string; fileName: string }) => string {
|
|
379
|
-
return (chunk) => {
|
|
380
|
-
if (chunk.name === appShellFileName) {
|
|
381
|
-
return "__rolldown_runtime__.initialize(require('./hmr/info.js'));\n"
|
|
382
|
-
}
|
|
383
|
-
if (pageFiles.has(chunk.name)) {
|
|
384
|
-
// Page files live at `pages/<route>/index.js`, so the dependency path must be
|
|
385
|
-
// computed relative to each page's own directory.
|
|
386
|
-
const patchesPath = path.posix.relative(path.posix.dirname(chunk.fileName), 'hmr/patches.js')
|
|
387
|
-
return `require('${patchesPath}');\n`
|
|
388
|
-
}
|
|
389
|
-
return ''
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function createStableFileNames<Value>(addon: unknown, fallback: string): string | ((value: Value) => string) {
|
|
394
|
-
if (typeof addon === 'function') {
|
|
395
|
-
return (value) => toStableFileName(String(addon(value)))
|
|
396
|
-
}
|
|
397
|
-
return toStableFileName(typeof addon === 'string' ? addon : fallback)
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function toStableFileName(fileName: string): string {
|
|
401
|
-
return fileName
|
|
402
|
-
.replace(/(^|\/)\[hash(?::\d+)?\](?=\.|$)/g, '$1[name]')
|
|
403
|
-
.replace(/[-_.]\[hash(?::\d+)?\]/g, '')
|
|
404
|
-
.replace(/\[hash(?::\d+)?\]/g, '[name]')
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
type ViteTransformPlugin = {
|
|
408
|
-
_options?: { transformOptions?: { sourcemap?: boolean } }
|
|
409
|
-
name?: string
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function disableViteOxcSourcemap(pluginOption: unknown): void {
|
|
413
|
-
if (Array.isArray(pluginOption)) {
|
|
414
|
-
pluginOption.forEach(disableViteOxcSourcemap)
|
|
415
|
-
return
|
|
416
|
-
}
|
|
417
|
-
if (!pluginOption || typeof pluginOption !== 'object') {
|
|
418
|
-
return
|
|
419
|
-
}
|
|
420
|
-
const plugin = pluginOption as ViteTransformPlugin
|
|
421
|
-
if (plugin.name === 'builtin:vite-transform' && plugin._options?.transformOptions) {
|
|
422
|
-
plugin._options.transformOptions.sourcemap = false
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function createViteReporter(server: ViteDevServer) {
|
|
427
|
-
const { build, logger, root } = server.config
|
|
428
|
-
return viteReporterPlugin({
|
|
429
|
-
assetsDir: path.join(build.assetsDir, '/'),
|
|
430
|
-
chunkLimit: 2000,
|
|
431
|
-
isLib: Boolean(build.lib),
|
|
432
|
-
isTty: Boolean(process.stdout.isTTY && !process.env.CI),
|
|
433
|
-
logInfo: (message) => logger.info(message),
|
|
434
|
-
reportCompressedSize: false,
|
|
435
|
-
root,
|
|
436
|
-
warnLargeChunks: false
|
|
437
|
-
})
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
type BundledDevRolldownOptions = InputOptions & {
|
|
441
|
-
experimental?: {
|
|
442
|
-
[key: string]: unknown
|
|
443
|
-
devMode?: boolean | Record<string, unknown>
|
|
444
|
-
}
|
|
445
|
-
output?: OutputOptions | OutputOptions[]
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
type BundledDev = {
|
|
449
|
-
_devEngine?: DevEngine
|
|
450
|
-
getRolldownOptions(): Promise<BundledDevRolldownOptions>
|
|
451
|
-
listen(): Promise<void>
|
|
452
|
-
triggerBundleRegenerationIfStale(): Promise<boolean>
|
|
453
|
-
}
|
|
454
|
-
|
|
455
320
|
function getBundledDev(server: ViteDevServer): BundledDev {
|
|
456
321
|
const bundledDev = server.environments.client.bundledDev as unknown as BundledDev | undefined
|
|
457
322
|
if (!bundledDev) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
1
2
|
import fs from 'node:fs/promises'
|
|
2
3
|
import path from 'node:path'
|
|
3
4
|
import type { BindingClientHmrUpdate } from 'rolldown/experimental'
|
|
@@ -28,11 +29,10 @@ export function renderInitialHmrPatches(): string {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
* Renders the
|
|
32
|
+
* Renders the cumulative patch suffix as inert CommonJS data.
|
|
32
33
|
*
|
|
33
|
-
* DevTools re-executes the Page because this
|
|
34
|
-
*
|
|
35
|
-
* so the Page's imports below the require resolve against the freshly registered modules.
|
|
34
|
+
* DevTools re-executes the Page because this dependency changed. The Page entry passes the exported payload to the persistent
|
|
35
|
+
* App runtime synchronously before importing its capsule, keeping delivery explicit and leaving this file free of side effects.
|
|
36
36
|
*/
|
|
37
37
|
export function renderHmrPatches(buildId: string, patches: readonly PatchUpdate[]): string {
|
|
38
38
|
if (patches.length === 0) {
|
|
@@ -44,12 +44,24 @@ export function renderHmrPatches(buildId: string, patches: readonly PatchUpdate[
|
|
|
44
44
|
`{seq: ${patch.seq}, changedIds: ${JSON.stringify(patch.changedIds)}, factory: () => {\n${patch.code}\n}}`
|
|
45
45
|
)
|
|
46
46
|
|
|
47
|
-
return `
|
|
47
|
+
return `module.exports = {buildId: ${JSON.stringify(buildId)}, patches: [${rendered.join(',')}]};\n`
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/**
|
|
50
|
+
/** Atomically publishes one physical HMR module so DevTools can observe only complete JavaScript generations. */
|
|
51
51
|
export async function writeHmrFile(outDir: string, fileName: string, source: string): Promise<void> {
|
|
52
52
|
const filePath = path.join(outDir, fileName)
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const directory = path.dirname(filePath)
|
|
54
|
+
|
|
55
|
+
await fs.mkdir(directory, { recursive: true })
|
|
56
|
+
|
|
57
|
+
const temporaryPath = path.join(directory, `.${path.basename(filePath)}.${randomUUID()}.txt`)
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// DevTools ignores the temporary .txt file; keeping it beside the destination guarantees a same-filesystem rename.
|
|
61
|
+
await fs.writeFile(temporaryPath, source)
|
|
62
|
+
await fs.rename(temporaryPath, filePath)
|
|
63
|
+
} finally {
|
|
64
|
+
// rename removes the source path on success; force cleanup covers interrupted writes and failed replacements.
|
|
65
|
+
await fs.rm(temporaryPath, { force: true })
|
|
66
|
+
}
|
|
55
67
|
}
|
|
@@ -4,12 +4,24 @@ import { type PatchUpdate, renderHmrPatches } from './hmr-files.ts'
|
|
|
4
4
|
/** Abstracts the physical patches write; the engine owns the file destination. */
|
|
5
5
|
export type WritePatches = (content: string) => Promise<void>
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Owns the cumulative sequence range between the host's published frontier and the runtime's applied frontier.
|
|
9
|
+
*
|
|
10
|
+
* For example, if the runtime has applied sequence 4:
|
|
11
|
+
*
|
|
12
|
+
* 1. produce([5, 6]) writes [5, 6];
|
|
13
|
+
* 2. produce([7]) before an application report writes [5, 6, 7], so DevTools may miss the first file event safely;
|
|
14
|
+
* 3. acknowledge(6) retains [7];
|
|
15
|
+
* 4. produce([8]) then writes [7, 8], exactly the suffix the runtime still needs.
|
|
16
|
+
*
|
|
17
|
+
* The host may advance Rolldown's published frontier through 8 immediately after those writes, but this history is pruned only
|
|
18
|
+
* through 6 until the runtime reports successful application. Published sequence and applied sequence are deliberately distinct.
|
|
19
|
+
*/
|
|
8
20
|
export class PatchPublisher {
|
|
9
21
|
private readonly writePatches: WritePatches
|
|
10
22
|
private buildId: string | undefined
|
|
11
23
|
|
|
12
|
-
/** Executable updates retained
|
|
24
|
+
/** Executable updates retained until the runtime confirms successful application. */
|
|
13
25
|
private readonly pendingPatches: PatchUpdate[] = []
|
|
14
26
|
|
|
15
27
|
// Explicit field assignment: node --test strips types and does not support parameter properties.
|
|
@@ -31,19 +43,33 @@ export class PatchPublisher {
|
|
|
31
43
|
return { buildId, previousBuildId }
|
|
32
44
|
}
|
|
33
45
|
|
|
34
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Appends a Rolldown batch and publishes the complete range not yet applied by the runtime.
|
|
48
|
+
*
|
|
49
|
+
* Resolution means the cumulative JavaScript generation is physically visible. The host may then advance Rolldown's
|
|
50
|
+
* published frontier immediately; pendingPatches remains intact until an application report proves the runtime caught up.
|
|
51
|
+
*/
|
|
35
52
|
async produce(patches: readonly PatchUpdate[]): Promise<void> {
|
|
36
|
-
if (this.buildId === undefined || patches.length === 0)
|
|
53
|
+
if (this.buildId === undefined || patches.length === 0) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
37
57
|
this.pendingPatches.push(...patches)
|
|
58
|
+
|
|
38
59
|
await this.writePatches(renderHmrPatches(this.buildId, this.pendingPatches))
|
|
39
60
|
}
|
|
40
61
|
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Removes only the prefix covered by the runtime's successful application frontier. Publication alone never calls this:
|
|
64
|
+
* retaining the gap between published and applied frontiers is what lets DevTools safely miss intermediate file events.
|
|
65
|
+
*/
|
|
66
|
+
acknowledge(seq: number): void {
|
|
67
|
+
let appliedCount = 0
|
|
68
|
+
|
|
69
|
+
while (appliedCount < this.pendingPatches.length && this.pendingPatches[appliedCount].seq <= seq) {
|
|
70
|
+
appliedCount++
|
|
46
71
|
}
|
|
47
|
-
|
|
72
|
+
|
|
73
|
+
this.pendingPatches.splice(0, appliedCount)
|
|
48
74
|
}
|
|
49
75
|
}
|