vite-plugin-taro 0.6.0 → 0.6.2

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 (68) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/node/plugins/h5/transform-app.js +1 -1
  4. package/dist/node/plugins/wx/dev/plugins.js +1 -1
  5. package/dist/node/plugins/wx/dev/wx-dev-options.js +22 -1
  6. package/dist/node/plugins/wx/module/chunk-path.d.ts +8 -0
  7. package/dist/node/plugins/wx/module/chunk-path.js +18 -0
  8. package/dist/node/plugins/wx/{module.js → module/module.js} +2 -2
  9. package/dist/node/plugins/wx/native/create-native-component-output.d.ts +3 -1
  10. package/dist/node/plugins/wx/native/create-native-component-output.js +3 -5
  11. package/dist/node/plugins/wx/output/files.d.ts +3 -2
  12. package/dist/node/plugins/wx/output/files.js +2 -2
  13. package/dist/node/plugins/wx/output/json.d.ts +1 -1
  14. package/dist/node/plugins/wx/output/json.js +1 -1
  15. package/dist/node/plugins/wx/placer/placement.d.ts +51 -0
  16. package/dist/node/plugins/wx/placer/placement.js +245 -0
  17. package/dist/node/plugins/wx/placer/placer.d.ts +76 -0
  18. package/dist/node/plugins/wx/placer/placer.js +147 -0
  19. package/dist/node/plugins/wx/plugins.js +17 -17
  20. package/dist/node/plugins/wx/render/capsule.d.ts +35 -3
  21. package/dist/node/plugins/wx/render/capsule.js +53 -12
  22. package/dist/node/plugins/wx/render/native.d.ts +20 -2
  23. package/dist/node/plugins/wx/render/native.js +539 -35
  24. package/dist/node/plugins/wx/render/system-js/string-editor.d.ts +24 -0
  25. package/dist/node/plugins/wx/render/system-js/string-editor.js +93 -0
  26. package/dist/node/plugins/wx/render/system-js/system-js.d.ts +23 -0
  27. package/dist/node/plugins/wx/render/system-js/system-js.js +602 -0
  28. package/dist/node/plugins/wx/render/transport.d.ts +7 -3
  29. package/dist/node/plugins/wx/render/transport.js +19 -10
  30. package/dist/node/plugins/wx/resolve/resolver.js +1 -1
  31. package/dist/node/plugins/wx/resolve/specialize-bootstrap.js +1 -1
  32. package/dist/node/plugins/wx/resolve/specialize-page-capsule.js +1 -1
  33. package/dist/node/utils/modules.d.ts +0 -2
  34. package/dist/node/utils/modules.js +0 -7
  35. package/dist/node/utils/transform.d.ts +0 -3
  36. package/dist/node/utils/transform.js +0 -23
  37. package/package.json +8 -6
  38. package/src/node/plugins/h5/transform-app.ts +1 -1
  39. package/src/node/plugins/wx/dev/plugins.ts +1 -1
  40. package/src/node/plugins/wx/dev/wx-dev-options.ts +22 -1
  41. package/src/node/plugins/wx/module/chunk-path.ts +22 -0
  42. package/src/node/plugins/wx/{module.ts → module/module.ts} +2 -2
  43. package/src/node/plugins/wx/native/create-native-component-output.ts +6 -5
  44. package/src/node/plugins/wx/output/files.ts +5 -3
  45. package/src/node/plugins/wx/output/json.ts +1 -2
  46. package/src/node/plugins/wx/placer/placement.ts +364 -0
  47. package/src/node/plugins/wx/placer/placer.ts +179 -0
  48. package/src/node/plugins/wx/plugins.ts +19 -19
  49. package/src/node/plugins/wx/render/capsule.ts +53 -17
  50. package/src/node/plugins/wx/render/native.ts +670 -55
  51. package/src/node/plugins/wx/render/system-js/string-editor.ts +115 -0
  52. package/src/node/plugins/wx/render/system-js/system-js.ts +831 -0
  53. package/src/node/plugins/wx/render/transport.ts +25 -9
  54. package/src/node/plugins/wx/resolve/resolver.ts +1 -1
  55. package/src/node/plugins/wx/resolve/specialize-bootstrap.ts +1 -1
  56. package/src/node/plugins/wx/resolve/specialize-page-capsule.ts +1 -1
  57. package/src/node/utils/modules.ts +0 -8
  58. package/src/node/utils/transform.ts +0 -30
  59. package/dist/node/plugins/wx/placement/placer.d.ts +0 -78
  60. package/dist/node/plugins/wx/placement/placer.js +0 -158
  61. package/dist/node/plugins/wx/placement/plan.d.ts +0 -46
  62. package/dist/node/plugins/wx/placement/plan.js +0 -210
  63. package/dist/node/plugins/wx/render/capsule-wrapper.d.ts +0 -3
  64. package/dist/node/plugins/wx/render/capsule-wrapper.js +0 -64
  65. package/src/node/plugins/wx/placement/placer.ts +0 -187
  66. package/src/node/plugins/wx/placement/plan.ts +0 -306
  67. package/src/node/plugins/wx/render/capsule-wrapper.ts +0 -86
  68. /package/dist/node/plugins/wx/{module.d.ts → module/module.d.ts} +0 -0
@@ -0,0 +1,364 @@
1
+ import { createHash } from 'node:crypto'
2
+ import type { Rolldown } from 'vite'
3
+
4
+ // Leave headroom below WeChat's 2M subpackage limit for rendered wrappers and native assets.
5
+ export const subpackagePlanningBudget = 1_900_000
6
+ const generatedSubpackageRootPrefix = 'sub/p_'
7
+
8
+ /** Identifies one generated code-only subpackage by its physical output root. */
9
+ export type SubpackageLocation = {
10
+ /** Discriminates generated subpackages from main. */
11
+ kind: 'subpackage'
12
+ /** Native subpackage root relative to the Mini Program output directory. */
13
+ root: string
14
+ }
15
+
16
+ /** Physical package ownership for one final Rolldown chunk. */
17
+ export type PackageLocation = { kind: 'main' } | SubpackageLocation
18
+
19
+ /** Native app.json declaration for one generated code-only subpackage. */
20
+ export type GeneratedSubpackage = {
21
+ /** Stable native alias derived from the generated root hash. */
22
+ name: string
23
+ /** Physical directory containing this subpackage's emitted capsules. */
24
+ root: string
25
+ /** Marks this as a code-only subpackage with no native Page routes. */
26
+ pages: readonly []
27
+ }
28
+
29
+ /** Immutable ownership and materialization operations for one complete final-chunk graph. */
30
+ export type Placement = Readonly<{
31
+ getPackageLocation(chunk: Rolldown.RenderedChunk | Rolldown.OutputChunk): PackageLocation
32
+ getPhysicalChunkId(chunk: Rolldown.RenderedChunk): string
33
+ getLoadMode(chunk: Rolldown.RenderedChunk): 'sync' | 'async'
34
+ finalize(bundle: Rolldown.OutputBundle): readonly GeneratedSubpackage[]
35
+ }>
36
+
37
+ /** Shared main-package value used for every synchronously reachable chunk. */
38
+ const mainPackage = { kind: 'main' } as const
39
+
40
+ type PlaceableChunk = {
41
+ chunkId: string
42
+ estimatedBytes: number
43
+ transitions: ReadonlySet<number>
44
+ }
45
+
46
+ type ChunkBin = {
47
+ chunkIds: string[]
48
+ estimatedBytes: number
49
+ transitions: Set<number>
50
+ }
51
+
52
+ type BinCandidate = {
53
+ bin: ChunkBin
54
+ overlap: number
55
+ remainingBytes: number
56
+ }
57
+
58
+ type PackedSubpackage = SubpackageLocation & {
59
+ chunkIds: readonly string[]
60
+ }
61
+
62
+ /**
63
+ * Applies Load-Transition Hypergraph Partitioning to Rolldown's final preliminary chunk graph:
64
+ *
65
+ * 1. Sort preliminary filenames to remove callback and object-enumeration order from every later decision.
66
+ * 2. Reserve every explicit entry and its complete static closure in main because native startup must load it synchronously.
67
+ * 3. Treat each dynamic-import edge as one load transition; the target's static closure is that transition's hyperedge.
68
+ * Nested dynamic edges remain separate transitions rather than being folded into the parent closure.
69
+ * 4. Index every lazy chunk by all transitions requiring it. Shared chunks therefore carry global demand rather than being
70
+ * assigned according to the first source module or dynamic root that happens to visit them.
71
+ * 5. Order chunks by transition demand, estimated emitted bytes, then preliminary filename. Place each chunk once into the
72
+ * fitting bin with maximum transition overlap, using best-fit remaining capacity only as a tie-breaker.
73
+ * 6. Hash each bin's sorted preliminary filenames into a deterministic physical package root and return unique ownership.
74
+ *
75
+ * Analysis costs the sum of transition static-closure traversals. Packing scans fitting bins and intersects sparse
76
+ * transition sets; its worst case is O(CBT), while practical graphs have few bins and sparse transition membership.
77
+ */
78
+ export function createPlacement({
79
+ chunks,
80
+ getAdditionalModuleBytes
81
+ }: {
82
+ chunks: Readonly<Record<string, Rolldown.RenderedChunk>>
83
+ getAdditionalModuleBytes(moduleId: string): number
84
+ }): Placement {
85
+ const chunkById = new Map(Object.entries(chunks).sort(([left], [right]) => left.localeCompare(right)))
86
+ const mainChunkIds = findMainChunkIds(chunkById)
87
+ const transitionsByChunk = collectTransitionsByChunk({ chunks: chunkById, mainChunkIds: mainChunkIds })
88
+ const placeableChunks = [...chunkById]
89
+ .filter(([chunkId]) => !mainChunkIds.has(chunkId))
90
+ .map(([chunkId, chunk]) => ({
91
+ chunkId: chunkId,
92
+ estimatedBytes:
93
+ estimateChunkBytes(chunk) +
94
+ chunk.moduleIds.reduce((bytes, moduleId) => bytes + getAdditionalModuleBytes(moduleId), 0),
95
+ transitions: transitionsByChunk.get(chunkId) ?? new Set<number>()
96
+ }))
97
+ const subpackages = packChunks({
98
+ chunks: placeableChunks,
99
+ planningBudgetBytes: subpackagePlanningBudget
100
+ }).map(createPackedSubpackage)
101
+
102
+ // This local map accumulates the immutable plan returned to output materialization.
103
+ const locationByChunk = new Map<string, PackageLocation>()
104
+ for (const chunkId of mainChunkIds) {
105
+ locationByChunk.set(chunkId, mainPackage)
106
+ }
107
+ for (const subpackage of subpackages) {
108
+ for (const chunkId of subpackage.chunkIds) {
109
+ locationByChunk.set(chunkId, subpackage)
110
+ }
111
+ }
112
+ function getLocation(chunkId: string): PackageLocation {
113
+ const location = locationByChunk.get(chunkId)
114
+ if (!location) {
115
+ throw new Error(`wx placement is missing final chunk: ${chunkId}`)
116
+ }
117
+ return location
118
+ }
119
+
120
+ /** Resolves typed ownership from Rolldown's preliminary physical filename before or after finalization. */
121
+ function getPackageLocation(chunk: Rolldown.RenderedChunk | Rolldown.OutputChunk): PackageLocation {
122
+ return getLocation('preliminaryFileName' in chunk ? chunk.preliminaryFileName : chunk.fileName)
123
+ }
124
+
125
+ return {
126
+ getPackageLocation: getPackageLocation,
127
+
128
+ /** Adds the planned package root to a physical preliminary path without changing the chunk's SystemJS identity. */
129
+ getPhysicalChunkId(chunk: Rolldown.RenderedChunk): string {
130
+ const location = getPackageLocation(chunk)
131
+ return location.kind === 'main' ? chunk.fileName : `${location.root}/${chunk.fileName}`
132
+ },
133
+
134
+ /** Selects the native loading API directly from typed package ownership. */
135
+ getLoadMode(chunk: Rolldown.RenderedChunk): 'sync' | 'async' {
136
+ return getPackageLocation(chunk).kind === 'subpackage' ? 'async' : 'sync'
137
+ },
138
+
139
+ /** Assigns each final chunk's Rolldown-owned physical filename and declares typed owners that survived output. */
140
+ finalize(bundle: Rolldown.OutputBundle): readonly GeneratedSubpackage[] {
141
+ const outputChunks = Object.values(bundle).filter(
142
+ (output): output is Rolldown.OutputChunk => output.type === 'chunk'
143
+ )
144
+ // This local mutable set deduplicates typed package owners that retain at least one final output chunk.
145
+ const roots = new Set<string>()
146
+ for (const chunk of outputChunks) {
147
+ // OutputChunk.fileName contains the resolved content hash. preliminaryFileName preserves the exact physical
148
+ // candidate with placeholders that identified this chunk when placement was created during renderChunk.
149
+ const location = getLocation(chunk.preliminaryFileName)
150
+ if (location.kind !== 'subpackage') {
151
+ continue
152
+ }
153
+ // OutputChunk is mutable in generateBundle. Assigning fileName makes Rolldown retain all chunk metadata and
154
+ // write that same chunk at its physical package path; deleting bundle keys or re-emitting would lose identity.
155
+ chunk.fileName = `${location.root}/${chunk.fileName}`
156
+ roots.add(location.root)
157
+ }
158
+
159
+ return [...roots].sort().map((root) => ({
160
+ name: root.slice(root.lastIndexOf('/') + 1),
161
+ root: root,
162
+ pages: []
163
+ }))
164
+ }
165
+ }
166
+ }
167
+
168
+ /** Keeps every explicit output entry and its complete static chunk closure in main. */
169
+ function findMainChunkIds(chunks: ReadonlyMap<string, Rolldown.RenderedChunk>): Set<string> {
170
+ const mainChunkIds = new Set<string>()
171
+ // The worklist avoids recursion and visits every eager static edge once.
172
+ const pending = [...chunks].filter(([, chunk]) => chunk.isEntry).map(([chunkId]) => chunkId)
173
+ while (pending.length > 0) {
174
+ const chunkId = pending.pop()
175
+ if (!chunkId || mainChunkIds.has(chunkId)) {
176
+ continue
177
+ }
178
+ const chunk = chunks.get(chunkId)
179
+ if (!chunk) {
180
+ continue
181
+ }
182
+ mainChunkIds.add(chunkId)
183
+ pending.push(...chunk.imports)
184
+ }
185
+ return mainChunkIds
186
+ }
187
+
188
+ /** Creates every load-transition hyperedge and indexes its static closure by chunk. */
189
+ function collectTransitionsByChunk({
190
+ chunks,
191
+ mainChunkIds
192
+ }: {
193
+ chunks: ReadonlyMap<string, Rolldown.RenderedChunk>
194
+ mainChunkIds: ReadonlySet<string>
195
+ }): Map<string, ReadonlySet<number>> {
196
+ const transitionsByChunk = new Map<string, Set<number>>()
197
+ let transitionId = 0
198
+ for (const chunk of chunks.values()) {
199
+ for (const targetId of [...chunk.dynamicImports].sort()) {
200
+ if (!chunks.has(targetId) || mainChunkIds.has(targetId)) {
201
+ continue
202
+ }
203
+ for (const chunkId of collectStaticClosure({
204
+ rootId: targetId,
205
+ chunks: chunks,
206
+ mainChunkIds: mainChunkIds
207
+ })) {
208
+ const transitions = transitionsByChunk.get(chunkId) ?? new Set<number>()
209
+ transitions.add(transitionId)
210
+ transitionsByChunk.set(chunkId, transitions)
211
+ }
212
+ transitionId++
213
+ }
214
+ }
215
+ return transitionsByChunk
216
+ }
217
+
218
+ /** Collects one transition's static closure; nested dynamic imports remain independent transitions. */
219
+ function collectStaticClosure({
220
+ rootId,
221
+ chunks,
222
+ mainChunkIds
223
+ }: {
224
+ rootId: string
225
+ chunks: ReadonlyMap<string, Rolldown.RenderedChunk>
226
+ mainChunkIds: ReadonlySet<string>
227
+ }): string[] {
228
+ const closure = new Set<string>()
229
+ // The worklist follows static edges only; the visited set terminates cycles.
230
+ const pending = [rootId]
231
+ while (pending.length > 0) {
232
+ const chunkId = pending.pop()
233
+ if (!chunkId || closure.has(chunkId) || mainChunkIds.has(chunkId)) {
234
+ continue
235
+ }
236
+ const chunk = chunks.get(chunkId)
237
+ if (!chunk) {
238
+ continue
239
+ }
240
+ closure.add(chunkId)
241
+ pending.push(...chunk.imports)
242
+ }
243
+ return [...closure].sort()
244
+ }
245
+
246
+ /** Partitions final chunks by transition overlap before best-fit capacity. */
247
+ function packChunks({
248
+ chunks,
249
+ planningBudgetBytes
250
+ }: {
251
+ chunks: readonly PlaceableChunk[]
252
+ planningBudgetBytes: number
253
+ }): ChunkBin[] {
254
+ const bins: ChunkBin[] = []
255
+ const orderedChunks = [...chunks].sort(compareChunks)
256
+ for (const chunk of orderedChunks) {
257
+ const bin =
258
+ chunk.estimatedBytes <= planningBudgetBytes
259
+ ? (findBestBin({ bins: bins, chunk: chunk, planningBudgetBytes: planningBudgetBytes }) ??
260
+ createBin(bins))
261
+ : createBin(bins)
262
+ placeChunk(bin, chunk)
263
+ }
264
+ return bins
265
+ }
266
+
267
+ /** Gives globally shared transition demand priority, then size and stable preliminary filename. */
268
+ function compareChunks(left: PlaceableChunk, right: PlaceableChunk): number {
269
+ return (
270
+ right.transitions.size - left.transitions.size ||
271
+ right.estimatedBytes - left.estimatedBytes ||
272
+ left.chunkId.localeCompare(right.chunkId)
273
+ )
274
+ }
275
+
276
+ /** Chooses maximum transition overlap, then the fullest fitting package and stable creation order. */
277
+ function findBestBin({
278
+ bins,
279
+ chunk,
280
+ planningBudgetBytes
281
+ }: {
282
+ bins: readonly ChunkBin[]
283
+ chunk: PlaceableChunk
284
+ planningBudgetBytes: number
285
+ }): ChunkBin | undefined {
286
+ let best: BinCandidate | undefined
287
+ for (const bin of bins) {
288
+ if (bin.estimatedBytes + chunk.estimatedBytes > planningBudgetBytes) {
289
+ continue
290
+ }
291
+ const candidate = {
292
+ bin: bin,
293
+ overlap: countOverlap(bin.transitions, chunk.transitions),
294
+ remainingBytes: planningBudgetBytes - bin.estimatedBytes - chunk.estimatedBytes
295
+ }
296
+ if (
297
+ !best ||
298
+ candidate.overlap > best.overlap ||
299
+ (candidate.overlap === best.overlap && candidate.remainingBytes < best.remainingBytes)
300
+ ) {
301
+ best = candidate
302
+ }
303
+ }
304
+ return best?.bin
305
+ }
306
+
307
+ /** Counts transition-package incidences removed by one candidate placement. */
308
+ function countOverlap(left: ReadonlySet<number>, right: ReadonlySet<number>): number {
309
+ const [smaller, larger] = left.size <= right.size ? [left, right] : [right, left]
310
+ let overlap = 0
311
+ for (const transition of smaller) {
312
+ if (larger.has(transition)) {
313
+ overlap++
314
+ }
315
+ }
316
+ return overlap
317
+ }
318
+
319
+ /** Creates one planner-local mutable package. */
320
+ function createBin(bins: ChunkBin[]): ChunkBin {
321
+ const bin: ChunkBin = {
322
+ chunkIds: [],
323
+ estimatedBytes: 0,
324
+ transitions: new Set<number>()
325
+ }
326
+ bins.push(bin)
327
+ return bin
328
+ }
329
+
330
+ /** Applies one irreversible, non-duplicating final chunk assignment. */
331
+ function placeChunk(bin: ChunkBin, chunk: PlaceableChunk): void {
332
+ bin.chunkIds.push(chunk.chunkId)
333
+ bin.estimatedBytes += chunk.estimatedBytes
334
+ for (const transition of chunk.transitions) {
335
+ bin.transitions.add(transition)
336
+ }
337
+ }
338
+
339
+ /** Estimates final output bytes from tree-shaken modules plus a bounded generated-chunk allowance. */
340
+ function estimateChunkBytes(chunk: Rolldown.RenderedChunk): number {
341
+ const renderedModuleBytes = Object.values(chunk.modules).reduce(
342
+ (bytes, module) => bytes + (module.code ? Buffer.byteLength(module.code, 'utf8') : 0),
343
+ 0
344
+ )
345
+ const moduleWrapperBytes = chunk.moduleIds.length * 64
346
+ const referenceBytes = (chunk.imports.length + chunk.dynamicImports.length) * 32
347
+ return renderedModuleBytes + moduleWrapperBytes + referenceBytes + 64
348
+ }
349
+
350
+ /** Freezes membership and derives a stable root from sorted preliminary filenames. */
351
+ function createPackedSubpackage(bin: ChunkBin): PackedSubpackage {
352
+ const chunkIds = [...bin.chunkIds].sort()
353
+ const hash = createHash('sha256').update(chunkIds.join('\0')).digest('hex').slice(0, 8)
354
+ return {
355
+ kind: 'subpackage',
356
+ root: `${generatedSubpackageRootPrefix}${hash}`,
357
+ chunkIds: chunkIds
358
+ }
359
+ }
360
+
361
+ /** Tests the plugin-owned output prefix that physically identifies every generated subpackage. */
362
+ export function isGeneratedSubpackageFile(fileName: string): boolean {
363
+ return fileName.startsWith(generatedSubpackageRootPrefix)
364
+ }
@@ -0,0 +1,179 @@
1
+ import { normalizePath, type Plugin, type Rolldown } from 'vite'
2
+ import { getWxExecutionKind, isTransportModule } from '../module/module.ts'
3
+ import { getNativeComponentAssetBytes } from '../native/native-component-assets.ts'
4
+ import { createPlacement, type GeneratedSubpackage, type PackageLocation, type Placement } from './placement.ts'
5
+
6
+ export type { GeneratedSubpackage, Placement } from './placement.ts'
7
+
8
+ const pnpmFrameworkPackagePattern = /\/node_modules\/\.pnpm\/(?:@tarojs\+|react(?:-dom|-reconciler)?@|scheduler@)/
9
+ const workspaceFrameworkPackagePattern = /\/packages\/(?:taro-react|taro-plugin-framework-react)\//
10
+
11
+ /** Selects the explicit React/Taro roots whose complete dependency closure forms the framework vendor chunk. */
12
+ export function isWxFrameworkVendorModule(moduleId: string): boolean {
13
+ const normalizedId = normalizePath(moduleId)
14
+ return pnpmFrameworkPackagePattern.test(normalizedId) || workspaceFrameworkPackagePattern.test(normalizedId)
15
+ }
16
+
17
+ type PlacementState =
18
+ | { phase: 'idle' }
19
+ | { phase: 'awaiting-chunks' }
20
+ | { phase: 'planned'; placement: Placement }
21
+ | { phase: 'finalized'; placement: Placement; subpackages: readonly GeneratedSubpackage[] }
22
+
23
+ /** Placement services consumed by the later `vpt:wx` rendering and output hooks. */
24
+ export type WxPlacementPlugin = Plugin &
25
+ Readonly<{
26
+ getPackageLocation(chunk: Rolldown.RenderedChunk | Rolldown.OutputChunk): PackageLocation
27
+ getPhysicalChunkId(chunk: Rolldown.RenderedChunk): string
28
+ getLoadMode(chunk: Rolldown.RenderedChunk): 'sync' | 'async'
29
+ getSubpackages(): readonly GeneratedSubpackage[]
30
+ }>
31
+
32
+ /**
33
+ * Rolldown options owned by WX placement. Every field enforces a distinct output invariant. The plugin returns this object
34
+ * from its config hook, while direct Rolldown integration tests reuse the same value to exercise the identical lifecycle.
35
+ */
36
+ export const placementRolldownOptions = {
37
+ /**
38
+ * Output-stage naming remains under Rolldown's ownership. These options establish physical candidates and hash
39
+ * participation only; LTHP mutates the resulting OutputChunk filenames later without replacing the chunks.
40
+ */
41
+ output: {
42
+ /**
43
+ * React and Taro form one stable framework boundary shared by the App and every Page capsule. Keeping their complete
44
+ * dependency closure together prevents application edits from invalidating framework chunk identity and makes later
45
+ * development generations eligible to reuse the unchanged vendor. All remaining modules use Rolldown's automatic
46
+ * chunking; physical WX package placement operates on those final chunks without imposing another split strategy.
47
+ */
48
+ codeSplitting: {
49
+ groups: [
50
+ {
51
+ name: 'vendor',
52
+ test: isWxFrameworkVendorModule,
53
+ priority: 100,
54
+ includeDependenciesRecursively: true
55
+ }
56
+ ]
57
+ },
58
+ /**
59
+ * Native App/Page/Component shells are files addressed directly by WeChat and must retain the exact names configured
60
+ * in `input`, such as `app.js` and `pages/home/index.js`. Transport is excluded even though it is CommonJS:
61
+ * application chunks import its content-hashed path, so it belongs with hashed runtime/capsule entries. `[hash]`
62
+ * remains a Rolldown placeholder here and is resolved only after renderChunk transforms finish.
63
+ */
64
+ entryFileNames(chunk: Rolldown.PreRenderedChunk): string {
65
+ return getWxExecutionKind(chunk) === 'native' && !isTransportModule(chunk)
66
+ ? '[name]'
67
+ : 'assets/[name]-[hash].js'
68
+ },
69
+ /**
70
+ * Leaves chunk identity and collision handling entirely to Rolldown. This package-neutral physical pattern deliberately
71
+ * contains no LTHP owner; generateBundle adds only the selected package root to the existing Rolldown filename.
72
+ */
73
+ chunkFileNames: 'assets/[name]-[hash].js',
74
+ /**
75
+ * Emits generic Rolldown assets under one collision-resistant hashed namespace. Native-component folders are not
76
+ * governed by this option: createNativeComponentOutput preserves their required relative filenames and relocates the
77
+ * complete folder beside its owning JavaScript chunk after LTHP finalization.
78
+ */
79
+ assetFileNames: 'assets/[name]-[hash][extname]'
80
+ },
81
+ /**
82
+ * Keeps every native entry's required exports while allowing Rolldown to add cross-chunk bindings created by natural code
83
+ * splitting. `strict` can reject those extensions; `exports-only` can merge away native boundaries; `allow-extension`
84
+ * preserves the shell/capsule contract without forcing source-module placement groups.
85
+ */
86
+ preserveEntrySignatures: 'allow-extension' as const
87
+ }
88
+
89
+ /**
90
+ * Creates the `vpt:wx-placer` lifecycle owner:
91
+ *
92
+ * 1. Its config hook installs package-neutral Rolldown names and entry-signature semantics.
93
+ * 2. `renderStart` atomically starts a generation in `awaiting-chunks`; no stale placement remains reachable.
94
+ * 3. Its first pre-order `renderChunk` creates one immutable LTHP placement from the complete tree-shaken graph.
95
+ * 4. `vpt:wx` asks this plugin only for package ownership, physical relocation, and native loading mode.
96
+ * 5. Its pre-order `generateBundle` assigns each OutputChunk its package-qualified filename and publishes app.json declarations.
97
+ *
98
+ * The discriminated state is the only generation-local mutation: `idle → awaiting-chunks → planned → finalized`. Each hook
99
+ * performs one whole-state transition, so stale graph state, duplicate planning, and partially reset generations are
100
+ * unrepresentable.
101
+ */
102
+ export function createWxPlacementPlugin(): WxPlacementPlugin {
103
+ // This one mutable cell is the output-generation state machine described above; hooks replace it atomically by phase.
104
+ let state: PlacementState = { phase: 'idle' }
105
+
106
+ function requirePlacement(): Placement {
107
+ if (state.phase === 'idle' || state.phase === 'awaiting-chunks') {
108
+ throw new Error('wx placement is unavailable before Rolldown exposes the final chunk graph')
109
+ }
110
+ return state.placement
111
+ }
112
+
113
+ return {
114
+ name: 'vpt:wx-placer',
115
+
116
+ config() {
117
+ return {
118
+ build: {
119
+ rolldownOptions: placementRolldownOptions
120
+ }
121
+ }
122
+ },
123
+
124
+ renderStart() {
125
+ state = { phase: 'awaiting-chunks' }
126
+ },
127
+
128
+ renderChunk: {
129
+ order: 'pre',
130
+ handler(_code, _chunk, _outputOptions, meta) {
131
+ if (state.phase === 'planned') {
132
+ return
133
+ }
134
+ if (state.phase !== 'awaiting-chunks') {
135
+ throw new Error(`wx placement received final chunks during the ${state.phase} phase`)
136
+ }
137
+ state = {
138
+ phase: 'planned',
139
+ placement: createPlacement({
140
+ chunks: meta.chunks,
141
+ getAdditionalModuleBytes: (moduleId) =>
142
+ getNativeComponentAssetBytes(this.getModuleInfo(moduleId)?.meta)
143
+ })
144
+ }
145
+ }
146
+ },
147
+
148
+ generateBundle: {
149
+ order: 'pre',
150
+ handler(_outputOptions, bundle) {
151
+ const placement = requirePlacement()
152
+ state = {
153
+ phase: 'finalized',
154
+ placement: placement,
155
+ subpackages: placement.finalize(bundle)
156
+ }
157
+ }
158
+ },
159
+
160
+ getPackageLocation(chunk: Rolldown.RenderedChunk | Rolldown.OutputChunk): PackageLocation {
161
+ return requirePlacement().getPackageLocation(chunk)
162
+ },
163
+
164
+ getPhysicalChunkId(chunk: Rolldown.RenderedChunk): string {
165
+ return requirePlacement().getPhysicalChunkId(chunk)
166
+ },
167
+
168
+ getLoadMode(chunk: Rolldown.RenderedChunk): 'sync' | 'async' {
169
+ return requirePlacement().getLoadMode(chunk)
170
+ },
171
+
172
+ getSubpackages(): readonly GeneratedSubpackage[] {
173
+ if (state.phase !== 'finalized') {
174
+ throw new Error('wx subpackages are unavailable before output finalization')
175
+ }
176
+ return state.subpackages
177
+ }
178
+ }
179
+ }
@@ -4,11 +4,10 @@ import { esTarget } from '../../utils/constant.ts'
4
4
  import { packageRequire } from '../../utils/packages.ts'
5
5
  import { clientTaroNativeId } from '../client/constant.ts'
6
6
  import { createWxDevelopmentPlugin } from './dev/plugins.ts'
7
- import { getWxExecutionKind, isTransportModule } from './module.ts'
7
+ import { getWxExecutionKind, isTransportModule } from './module/module.ts'
8
8
  import { compileNativeComponentInterface } from './native/compile-native-component-interface.ts'
9
- import { getNativeComponentAssetBytes } from './native/native-component-assets.ts'
10
9
  import { createOutputFiles } from './output/files.ts'
11
- import { createPlacer } from './placement/placer.ts'
10
+ import { createWxPlacementPlugin, type WxPlacementPlugin } from './placer/placer.ts'
12
11
  import { renderCapsule } from './render/capsule.ts'
13
12
  import { renderNative } from './render/native.ts'
14
13
  import { materializeTransport } from './render/transport.ts'
@@ -23,17 +22,18 @@ export function createWxTargetPlugins(options: VptOptions): PluginOption[] {
23
22
 
24
23
  // Reuse the resolver instance's ordered application subset. Rolldown's complete input also contains bootstrap, transport,
25
24
  // shell, and component entries; entry membership alone cannot recover which roots define the App/Page CSS cascade.
25
+ const placement = createWxPlacementPlugin()
26
+
26
27
  return [
28
+ placement,
27
29
  createWxStylePlugins(),
28
- createWxPlugin(options, resolver),
30
+ createWxPlugin(options, resolver, placement),
29
31
  createWxDevelopmentPlugin(options, resolver.applicationEntryIds)
30
32
  ]
31
33
  }
32
34
 
33
35
  /** Configures the complete wx target build pipeline. */
34
- function createWxPlugin(options: VptOptions, resolver: WxResolver): Plugin {
35
- const placer = createPlacer()
36
-
36
+ function createWxPlugin(options: VptOptions, resolver: WxResolver, placement: WxPlacementPlugin): Plugin {
37
37
  return {
38
38
  name: 'vpt:wx',
39
39
 
@@ -70,7 +70,8 @@ function createWxPlugin(options: VptOptions, resolver: WxResolver): Plugin {
70
70
  target: esTarget,
71
71
 
72
72
  rolldownOptions: {
73
- ...placer.rolldownOptions,
73
+ // The dedicated vpt:wx-placer plugin owns output naming and entry-signature semantics. This plugin owns
74
+ // only the closed named input set of native shells, lifecycle capsules, bootstrap, and transport entries.
74
75
  input: resolver.input
75
76
  }
76
77
  }
@@ -99,17 +100,11 @@ function createWxPlugin(options: VptOptions, resolver: WxResolver): Plugin {
99
100
  }
100
101
  },
101
102
 
102
- renderStart() {
103
- placer.analyze({
104
- moduleIds: this.getModuleIds(),
105
- getModuleInfo: (moduleId) => this.getModuleInfo(moduleId),
106
- getAdditionalModuleBytes: (info) => getNativeComponentAssetBytes(info.meta)
107
- })
108
- },
109
-
110
103
  renderChunk: {
111
104
  order: 'post',
112
105
  async handler(code, chunk, outputOptions, meta) {
106
+ // vpt:wx-placer runs first and has already created immutable placement from this complete chunk graph.
107
+
113
108
  const executionKind = getWxExecutionKind(chunk)
114
109
  const sourcemap = Boolean(outputOptions.sourcemap)
115
110
 
@@ -126,7 +121,8 @@ function createWxPlugin(options: VptOptions, resolver: WxResolver): Plugin {
126
121
  code: native.code,
127
122
  transportChunk: chunk,
128
123
  chunks: meta.chunks,
129
- getLoadMode: placer.getLoadMode,
124
+ getLoadMode: placement.getLoadMode,
125
+ getPhysicalChunkId: placement.getPhysicalChunkId,
130
126
  sourcemap
131
127
  })
132
128
  }
@@ -144,13 +140,17 @@ function createWxPlugin(options: VptOptions, resolver: WxResolver): Plugin {
144
140
  */
145
141
  order: 'post',
146
142
  async handler(_, bundle) {
147
- const subpackages = placer.getSubpackages(bundle)
143
+ // LTHP joins OutputChunks to their preliminary logical IDs and assigns Rolldown-owned physical filenames.
144
+ // createOutputFiles then observes those paths to relocate native component folders, emit placeholders, and
145
+ // declare only surviving package roots in app.json. No JavaScript chunk is manually emitted or copied.
146
+ const subpackages = placement.getSubpackages()
148
147
 
149
148
  const outputFiles = await createOutputFiles({
150
149
  bundle,
151
150
  options,
152
151
  subpackages,
153
- getModuleInfo: (moduleId) => this.getModuleInfo(moduleId)
152
+ getModuleInfo: (moduleId) => this.getModuleInfo(moduleId),
153
+ getPackageLocation: placement.getPackageLocation
154
154
  })
155
155
 
156
156
  outputFiles.forEach((file) => {