vite-plugin-taro 0.6.3 → 0.6.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.
Files changed (29) hide show
  1. package/README.en.md +3 -3
  2. package/README.md +3 -3
  3. package/dist/node/plugins/wx/dev/dev-host.d.ts +5 -4
  4. package/dist/node/plugins/wx/dev/dev-host.js +19 -43
  5. package/dist/node/plugins/wx/dev/plugins.d.ts +4 -3
  6. package/dist/node/plugins/wx/dev/plugins.js +4 -4
  7. package/dist/node/plugins/wx/dev/wx-dev-options.d.ts +2 -3
  8. package/dist/node/plugins/wx/dev/wx-dev-options.js +4 -4
  9. package/dist/node/plugins/wx/plugins.js +5 -11
  10. package/dist/node/plugins/wx/styles/plugins.d.ts +141 -3
  11. package/dist/node/plugins/wx/styles/plugins.js +401 -89
  12. package/dist/node/utils/vite.d.ts +4 -14
  13. package/dist/node/utils/vite.js +7 -40
  14. package/package.json +4 -3
  15. package/src/node/plugins/wx/dev/dev-host.ts +22 -44
  16. package/src/node/plugins/wx/dev/plugins.ts +5 -4
  17. package/src/node/plugins/wx/dev/wx-dev-options.ts +5 -7
  18. package/src/node/plugins/wx/plugins.ts +5 -11
  19. package/src/node/plugins/wx/styles/plugins.ts +518 -93
  20. package/src/node/utils/vite.ts +13 -58
  21. package/dist/node/plugins/wx/dev/create-style-capture.d.ts +0 -54
  22. package/dist/node/plugins/wx/dev/create-style-capture.js +0 -173
  23. package/dist/node/plugins/wx/styles/transform-wx-style.d.ts +0 -8
  24. package/dist/node/plugins/wx/styles/transform-wx-style.js +0 -9
  25. package/dist/node/plugins/wx/styles/utils.d.ts +0 -39
  26. package/dist/node/plugins/wx/styles/utils.js +0 -95
  27. package/src/node/plugins/wx/dev/create-style-capture.ts +0 -248
  28. package/src/node/plugins/wx/styles/transform-wx-style.ts +0 -11
  29. package/src/node/plugins/wx/styles/utils.ts +0 -119
@@ -1,125 +1,550 @@
1
- import type { Plugin, PluginOption, Rolldown } from 'vite'
2
- import { WeappTailwindcss } from 'weapp-tailwindcss/vite'
3
- import { transformVitePlugin } from '../../../utils/vite.ts'
1
+ import path from 'node:path'
2
+ import { Scanner } from '@tailwindcss/oxide'
3
+ import type { PluginContext } from 'rolldown'
4
+ import { isCSSRequest, type Plugin, type Rolldown } from 'vite'
5
+ import { createContext } from 'weapp-tailwindcss/core'
6
+ import {
7
+ createWeappTailwindcssGenerator,
8
+ resolveTailwindV4Source,
9
+ type WeappTailwindcssGenerator
10
+ } from 'weapp-tailwindcss/generator'
11
+ import { normalizeModuleId } from '../../../utils/modules.ts'
12
+ import { wrapPluginTransform } from '../../../utils/vite.ts'
4
13
  import { tailwindcssBasedir } from '../../tailwind/tailwind-css.ts'
5
- import { transformWxStyle, wxStyleOptions } from './transform-wx-style.ts'
14
+ import { globalWxssFileName } from '../dev/hmr-files.ts'
6
15
 
7
- /*
8
- * WX style output order:
9
- *
10
- * weapp-tailwindcss output hooks
11
- * → vpt:wx-style-finalizer
12
- * → vpt:wx native companion emission
13
- *
14
- * All three generateBundle hooks retain hook-level `order: 'post'` and therefore execute in registration order. The
15
- * upstream plugin normally also uses plugin-level `enforce: 'post'`, which would move it behind both VPT plugins and
16
- * break this sequence. `alignGenerateBundleOrder` removes only that broader phase from upstream output hooks.
17
- */
16
+ /** Persistent Tailwind state owned by one physical CSS root across incremental Rolldown transforms. */
17
+ type TailwindRoot = Readonly<{
18
+ /** Exact root source used to decide whether the existing compiler can accept another candidate-only update. */
19
+ source: string
20
+ /** Authoritative raw candidates generated with the current source files; JavaScript and WXSS consume this same set. */
21
+ classSet: Set<string>
22
+ /** CSS imports and compiler inputs whose changes invalidate the generator rather than only its candidate cache. */
23
+ dependencies: ReadonlySet<string>
24
+ /** Stateful Tailwind compiler retaining its incremental candidate cache between source-file updates. */
25
+ generator: WeappTailwindcssGenerator
26
+ /** Oxide source matcher reused to enumerate the current files covered by Tailwind source patterns. */
27
+ scanner: Scanner
28
+ /** Marks a compiler dependency change that requires replacing the generator on the root's next transform. */
29
+ invalidated: boolean
30
+ }>
18
31
 
19
- /** Creates the complete WX Tailwind and global-style pipeline. */
20
- export function createWxStylePlugins(): PluginOption[] {
21
- const tailwindPlugins =
22
- WeappTailwindcss({
23
- // VPT is a custom Vite compiler.
24
- // Using Taro's adapter would import Taro-specific CSS ownership rules which we don't need.
25
- appType: 'weapp-vite',
26
- // WX generation rewrites Tailwind's split package imports before Vite tries to resolve them in the app.
27
- // Without this, strict workspaces fail on imports such as `tailwindcss/theme.css`.
28
- rewriteCssImports: true,
29
- platform: 'weapp',
30
- tailwindcssBasedir,
31
- generator: {
32
- target: 'weapp'
33
- },
34
- cssOptions: wxStyleOptions,
35
- logLevel: 'warn'
36
- }) ?? []
37
-
38
- return [transformVitePlugin(tailwindPlugins, alignGenerateBundleOrder), createWxStyleFinalizer(transformWxStyle)]
39
- }
32
+ /** Latest successful Vite CSS and optional Tailwind state joined by their normalized physical module ID. */
33
+ type StyleModule = Readonly<{
34
+ /** Vite-final CSS captured after preprocessors, PostCSS, and CSS Modules; absent until `vite:css-post` succeeds. */
35
+ css: string | undefined
36
+ /** Incremental Tailwind state; absent for ordinary CSS and removed when a root stops importing Tailwind. */
37
+ tailwind: TailwindRoot | undefined
38
+ }>
39
+
40
+ /** JavaScript code plus the physical filename required by the Weapp JavaScript transformer. */
41
+ type JavaScriptArtifact = Readonly<{
42
+ code: string
43
+ filename: string
44
+ }>
45
+
46
+ /** Vite plugin with the development-host operation that finalizes one coherent WX style/JavaScript transaction. */
47
+ export type WxStylePlugin = Plugin &
48
+ Readonly<{
49
+ /** Converts patch factories and publishes their matching global WXSS through the host's atomic writer. */
50
+ finalizeUpdate: <Artifact extends JavaScriptArtifact>(
51
+ artifacts: readonly Artifact[],
52
+ writeWxss: (wxss: string) => Promise<void>
53
+ ) => Promise<readonly Artifact[]>
54
+ }>
55
+
56
+ /** Vite CSS request modes that do not represent graph-owned application stylesheets. */
57
+ const ignoredStyleQueries = ['direct', 'inline', 'inline-css', 'raw', 'style-attr', 'transform-only', 'url'] as const
58
+
59
+ /** Whole-file conversion policy applied equally to complete builds and development updates. */
60
+ const wxStyleOptions = {
61
+ cssCalc: false,
62
+ autoprefixer: false,
63
+ rem2rpx: true,
64
+ px2rpx: true
65
+ } as const
40
66
 
41
67
  /**
42
- * Finalizes the one global stylesheet after upstream Tailwind generation.
68
+ * Creates the single owner of global WX style compilation, graph projection, JavaScript class rewriting, and publication.
69
+ *
70
+ * ## Architectural invariant
71
+ *
72
+ * A WX transaction must expose JavaScript and WXSS produced from one class-identity snapshot. Tailwind utility names can be
73
+ * rewritten for WeChat—for example, `py-5.5` becomes `py-5_d5`—so publishing either side independently can leave running code
74
+ * referring to selectors that do not yet exist. This plugin therefore treats reachable CSS, Tailwind candidates, converted
75
+ * WXSS, and converted JavaScript as one output. Complete builds and HMR updates both call `finalizeOutput()`; they differ only
76
+ * in how the returned bytes are materialized.
77
+ *
78
+ * ## Ownership boundaries
79
+ *
80
+ * The pipeline deliberately gives each subsystem one responsibility:
81
+ *
82
+ * 1. Rolldown owns module reachability and invalidation. VPT reads `getModuleInfo()` and registers watch files, but does not
83
+ * maintain a second import graph or decide independently which root should rerun.
84
+ * 2. The persistent Tailwind generator owns candidate discovery and incremental candidate removal. VPT invokes it only from
85
+ * the owning CSS root's Rolldown transform and never rescans the project during output publication.
86
+ * 3. Vite owns preprocessors, PostCSS, CSS Modules, and final module CSS semantics. VPT observes the input to the resolved
87
+ * `vite:css-post` hook only after the original hook succeeds; it never rereads source files or repeats CSS preprocessing.
88
+ * 4. The Weapp transformation context owns WX selector conversion and JavaScript class-string conversion. One retained context
89
+ * and one projected candidate set drive both operations.
90
+ * 5. VPT owns physical global WXSS and patch publication. Vite's browser CSS asset is only an intermediate carrier and is
91
+ * removed before VPT emits `assets/global.wxss`.
92
+ *
93
+ * Native Page and component WXSS are outside this global pipeline. The WX output plugin is registered after this style plugin
94
+ * and emits those opaque companions later. The WX configuration also enforces `cssCodeSplit: false`, so Vite contributes at
95
+ * most one browser compiler stylesheet for this plugin to replace.
96
+ *
97
+ * ## Compilation phases
98
+ *
99
+ * ### 1. Tailwind pre-transform
100
+ *
101
+ * The pre-transform checks physical application CSS for Tailwind imports or directives. Ordinary styles pass through. A
102
+ * Tailwind root compiles to browser CSS before Vite's normal CSS pipeline runs. Successful generation records the generator,
103
+ * scanner, current class set, compiler dependencies, and exact root source under the normalized physical module ID.
104
+ *
105
+ * Candidate files and compiler dependencies intentionally have different invalidation behavior:
106
+ *
107
+ * - Candidate-file changes rerun the root with the existing generator and scanner. `incrementalCache: true` updates additions
108
+ * and removals without discarding the generator's authoritative cache.
109
+ * - Compiler-dependency changes mark the root invalid. Its next Rolldown transform resolves a new Tailwind source and creates a
110
+ * new generator and scanner. Replacement is delayed until that transform has current source and plugin context.
111
+ * - If a stylesheet stops being a Tailwind root, its generator is disposed and its Tailwind state is removed. The later Vite
112
+ * CSS hook replaces the retained CSS after normal processing succeeds.
113
+ *
114
+ * ### 2. Vite-final CSS capture
115
+ *
116
+ * `configResolved` wraps the concrete `vite:css-post` transform while preserving its hook metadata, filter, ordering, and
117
+ * plugin context. The original Vite hook executes first, which preserves CSS Module exports and Vite's internal extraction
118
+ * state. Only a successful transform updates `styleByModuleId`; syntax errors therefore leave the last successful CSS available
119
+ * to the currently running application. Query modes such as `?raw`, `?url`, and `?inline` are excluded because they represent
120
+ * values rather than graph-owned stylesheets.
121
+ *
122
+ * ### 3. Live-graph projection
123
+ *
124
+ * Output finalization starts from resolved App/Page entry IDs and traverses Rolldown's current static and dynamic import edges
125
+ * in dependency-first post-order. Transaction-local visited sets terminate cycles and deduplicate shared modules and physical
126
+ * stylesheets. A retained stylesheet contributes only when its module is still reachable, so removing an import prunes its CSS
127
+ * and Tailwind candidates without a separate prune protocol or persistent topology cache. Candidate sets are unioned only from
128
+ * the Tailwind roots whose captured CSS survives that exact traversal, preserving the CSS/class identity invariant.
129
+ *
130
+ * ### 4. Shared WX finalization
131
+ *
132
+ * `finalizeOutput()` first converts the concatenated reachable CSS to WXSS, then transforms every supplied JavaScript artifact
133
+ * with the same projected class set. It returns data and performs no bundle mutation or filesystem publication. If either
134
+ * transformation fails, the promise rejects before callers expose partial output. JavaScript conversion is skipped when the
135
+ * projection contains no Tailwind candidates, preserving ordinary bundle bytes.
136
+ *
137
+ * ### 5a. Complete-build commit
43
138
  *
44
- * `cssCodeSplit: false` makes the compiler style global, but upstream can name it `.css` or `.wxss` depending on build
45
- * mode. This hook converts its complete final contents once and renames that compiler asset to `assets/global.wxss`. An
46
- * application without styles receives an empty global asset at the same stable path. Running earlier loses CSS from
47
- * dynamic chunks; running after native companion emission would also see Page and native-component WXSS files that must
48
- * remain opaque.
139
+ * The post-order `generateBundle` hook gathers all JavaScript chunks, finalizes them as one operation, and only then mutates the
140
+ * bundle. It assigns converted code, clears invalid source maps, removes Vite's intermediate browser stylesheet, and always
141
+ * emits `assets/global.wxss`. Emitting an empty global file is required because `app.wxss` imports it even when the application
142
+ * currently has no styles. Native output hooks run afterward and emit Page/component companion files independently.
143
+ *
144
+ * ### 5b. Development commit
145
+ *
146
+ * The development host calls `finalizeUpdate()` after Rolldown produces patch factories or a complete-output notification.
147
+ * Finalization uses the `PluginContext` captured by `buildStart`, so it observes the same current graph as the compiler. After
148
+ * all conversion succeeds, the host's atomic writer publishes changed WXSS before `finalizeUpdate()` returns converted patch
149
+ * factories. The patch publisher therefore cannot expose newer JavaScript class identities before matching selectors exist.
150
+ * `publishedWxss` advances only after a successful write and suppresses byte-identical writes that would otherwise trigger
151
+ * unnecessary WeChat DevTools reload events.
152
+ *
153
+ * ## Retained state and lifecycle
154
+ *
155
+ * The factory retains four explicit mutable state owners plus one library-owned transformation context:
156
+ *
157
+ * - `entryIds`: graph-exact App/Page entry identities resolved at the start of each build;
158
+ * - `graphContext`: the active Rolldown graph reader needed by host calls made outside plugin hooks;
159
+ * - `styleByModuleId`: the latest successful Vite CSS plus optional Tailwind state at one normalized module identity;
160
+ * - `publishedWxss`: the last durably published development stylesheet used for unchanged-write suppression;
161
+ * - `weappContext`: Weapp's internal conversion state, retained so selector and JavaScript rewriting share one context.
162
+ *
163
+ * The state owners remain scoped to one plugin instance; `entryIds` is atomically replaced after each complete resolution.
164
+ * Build-command bundles dispose Tailwind generators after bundle generation. A development watcher otherwise keeps them alive
165
+ * across updates and disposes them when it closes. Captured CSS survives compiler cleanup because output notifications can
166
+ * arrive after that cleanup, and is cleared only when the watcher terminates.
167
+ *
168
+ * ## Cost model
169
+ *
170
+ * Projection is `O(V + E + B + C)` for reachable modules, import edges, concatenated CSS bytes, and candidate insertions.
171
+ * JavaScript conversion is linear in the total supplied chunk or patch-factory bytes, subject to the Weapp parser's own cost.
172
+ * Retained memory is `O(B + C + D + F)` for latest CSS, candidate sets, compiler dependencies, and scanner file identities; no
173
+ * second application graph is retained. Tailwind's generator and Oxide scanner caches are intentionally persistent because
174
+ * recreating them on every candidate edit would repeat source normalization and scanning work.
49
175
  */
50
- function createWxStyleFinalizer(transformStyle: typeof transformWxStyle): Plugin {
51
- /*
52
- * DevEngine can omit an unchanged stylesheet from later complete output generations. Absence therefore has two meanings:
53
- * the first clean output genuinely has no styles, or a later output is reusing the physical stylesheet already on disk.
54
- * The Vite server creates a fresh plugin instance on a clean start, so this one lifecycle bit distinguishes those cases and
55
- * resets naturally on restart. Deliberately retain no CSS bytes or graph projection here: memory remains O(1), changed CSS
56
- * still arrives as a normal asset, and the dev host remains the only owner of incremental style preparation.
57
- */
58
- let hasFinalizedOutput = false
176
+ export function createWxStylePlugin(applicationEntryIds: readonly string[]): WxStylePlugin {
177
+ // This mutable root list is replaced in buildStart with Vite/Rolldown's exact cross-platform graph identities.
178
+ let entryIds = applicationEntryIds
179
+ // One retained Weapp context guarantees that CSS selectors and JavaScript class strings use the same conversion rules.
180
+ const weappContext = createContext({ appType: 'weapp-vite', logLevel: 'silent' })
181
+
182
+ // buildStart installs this mutable context because the development host finalizes output outside a Rolldown plugin hook.
183
+ let graphContext: PluginContext
184
+ // This mutable map is the only retained style store: Vite and Tailwind update separate fields at one module identity.
185
+ const styleByModuleId = new Map<string, StyleModule>()
186
+ // This mutable frontier advances only after the host durably writes WXSS, suppressing byte-identical filesystem events.
187
+ let publishedWxss: string | undefined
188
+
189
+ /** Binds retained plugin state to the context of the complete build or development transaction being finalized. */
190
+ const finalizeCurrentOutput = (context: PluginContext, javaScript: readonly JavaScriptArtifact[]) => {
191
+ return finalizeOutput(entryIds, styleByModuleId, context.getModuleInfo.bind(context), weappContext, javaScript)
192
+ }
193
+
194
+ /** Releases native compiler resources while retaining captured CSS needed by subsequent output callbacks. */
195
+ const disposeTailwindRoots = (): void => {
196
+ styleByModuleId.forEach((style, styleId) => {
197
+ if (style.tailwind) {
198
+ // Dispose each generator exactly once, then remove the root reference while preserving Vite-final CSS.
199
+ style.tailwind.generator.dispose?.()
200
+ styleByModuleId.set(styleId, { css: style.css, tailwind: undefined })
201
+ }
202
+ })
203
+ }
59
204
 
60
205
  return {
61
- name: 'vpt:wx-style-finalizer',
206
+ name: 'vpt:wx-styles',
207
+ /** Installs the single private Vite integration used to observe fully processed module CSS. */
208
+ configResolved(config) {
209
+ // `vite:css-post` is the boundary after all public CSS processing and before browser-module serialization.
210
+ const cssPostPlugin = config.plugins.find((plugin) => plugin.name === 'vite:css-post')!
211
+
212
+ wrapPluginTransform(cssPostPlugin, (transform) => {
213
+ return async function (css, id, options) {
214
+ // Run Vite first so a failed CSS transform never replaces the last successful retained artifact.
215
+ const result = await transform.call(this, css, id, options)
216
+
217
+ // Only physical application styles participate in WX graph projection; virtual request modes keep Vite semantics.
218
+ if (isApplicationStyle(id)) {
219
+ const styleId = normalizeModuleId(id)
220
+ styleByModuleId.set(styleId, {
221
+ css: css,
222
+ // Tailwind compilation runs earlier, so CSS capture must preserve the root state at this identity.
223
+ tailwind: styleByModuleId.get(styleId)?.tailwind
224
+ })
225
+ }
226
+ return result
227
+ }
228
+ })
229
+ },
230
+ /** Resolves exact graph roots and captures the graph reader used by host calls outside plugin hooks. */
231
+ async buildStart() {
232
+ // Resolve through Rolldown instead of reconstructing real paths, whose drive casing and separators vary on Windows.
233
+ const resolvedEntryIds = await Promise.all(
234
+ applicationEntryIds.map(async (entryId) => (await this.resolve(entryId))!.id)
235
+ )
236
+
237
+ // Commit the complete root set together so finalization never observes a partially resolved application graph.
238
+ entryIds = resolvedEntryIds
239
+ graphContext = this
240
+ },
241
+ transform: {
242
+ // Tailwind must expand before Vite's normal CSS pipeline produces the final module CSS captured above.
243
+ order: 'pre',
244
+ /** Compiles only Tailwind roots and registers every input needed for Rolldown-driven invalidation. */
245
+ async handler(code, id) {
246
+ // Query variants such as `?raw` are values, not application stylesheets, and must remain untouched.
247
+ if (!isApplicationStyle(id)) {
248
+ return
249
+ }
250
+
251
+ // Join this early Tailwind phase to the later Vite CSS capture through one normalized module identity.
252
+ const rootId = normalizeModuleId(id)
253
+ const style = styleByModuleId.get(rootId)
254
+ const previous = style?.tailwind
255
+
256
+ // A file can stop being a Tailwind root during HMR; dispose its compiler without discarding last-good Vite CSS.
257
+ if (!isTailwindRoot(code)) {
258
+ previous?.generator.dispose?.()
259
+ styleByModuleId.set(rootId, { css: style?.css, tailwind: undefined })
260
+ return
261
+ }
262
+
263
+ // Candidate-only updates reuse incremental caches; compiler-input updates replace the entire generator.
264
+ const reusable = previous?.invalidated ? undefined : previous
265
+ const compiled = await compileTailwindRoot(this.environment.config.root, rootId, code, reusable)
266
+ if (compiled.root.generator !== previous?.generator) {
267
+ previous?.generator.dispose?.()
268
+ }
269
+
270
+ // Replace the retained root record only after generation has produced a complete result.
271
+ styleByModuleId.set(rootId, { css: style?.css, tailwind: compiled.root })
272
+
273
+ // Compiler dependencies trigger generator replacement, while candidate files trigger incremental regeneration.
274
+ compiled.root.dependencies.forEach((file) => {
275
+ this.addWatchFile(file)
276
+ })
277
+ compiled.root.scanner.files.forEach((file) => {
278
+ this.addWatchFile(file)
279
+ })
280
+
281
+ // Vite receives browser CSS and remains the sole owner of PostCSS, preprocessors, and CSS Modules.
282
+ return { code: compiled.css, map: null }
283
+ }
284
+ },
285
+ /** Marks roots whose compiler inputs changed; Rolldown still decides when those roots are transformed. */
286
+ watchChange(id) {
287
+ const dependencyId = normalizeModuleId(id)
288
+
289
+ // A dependency may feed multiple roots, so every retained root must be checked before the next transform wave.
290
+ styleByModuleId.forEach((style, styleId) => {
291
+ if (style.tailwind?.dependencies.has(dependencyId)) {
292
+ styleByModuleId.set(styleId, {
293
+ css: style.css,
294
+ // Delay generator replacement until the owning root transform has current root source and graph context.
295
+ tailwind: { ...style.tailwind, invalidated: true }
296
+ })
297
+ }
298
+ })
299
+ },
62
300
  generateBundle: {
301
+ // Vite must finish chunking and CSS extraction before VPT can finalize the complete WX output transaction.
63
302
  order: 'post',
303
+ /** Converts every JavaScript chunk and the reachable CSS projection with one authoritative class set. */
64
304
  async handler(_, bundle) {
65
- const styles = Object.values(bundle).filter(isStyleAsset)
305
+ const outputs = Object.values(bundle)
66
306
 
67
- // Multiple compiler styles mean cssCodeSplit was re-enabled. Choosing one would silently lose CSS.
68
- if (styles.length > 1) {
69
- throw new Error('WX builds support at most one compiler-emitted stylesheet')
70
- }
307
+ // Step 1: preserve bundle order so finalized code can be assigned back by index without a second lookup map.
308
+ const chunks = outputs.filter((output): output is Rolldown.OutputChunk => output.type === 'chunk')
309
+
310
+ // Step 2: finish all fallible CSS and JavaScript conversion before mutating any bundle output.
311
+ const finalized = await finalizeCurrentOutput(
312
+ this,
313
+ chunks.map((chunk) => ({ code: chunk.code, filename: chunk.fileName }))
314
+ )
71
315
 
72
- if (styles.length === 0) {
73
- /*
74
- * A clean first output must materialize the stable import target imported by app.wxss. On later complete
75
- * outputs, DevEngine's physical writer preserves files omitted from the bundle; emitting the same empty
76
- * placeholder would instead overwrite valid unchanged WXSS. Emit nothing in that later case. A source or
77
- * configuration change that really removes all styles is already published as empty by ordinary style HMR,
78
- * while a clean server restart reaches this first-output branch and also clears any stale prior file.
79
- */
80
- if (!hasFinalizedOutput) {
81
- this.emitFile({ type: 'asset', fileName: 'assets/global.wxss', source: '' })
316
+ // Step 3: commit the converted JavaScript as one completed result and discard now-invalid source maps.
317
+ chunks.forEach((chunk, index) => {
318
+ chunk.code = finalized.javaScript[index]!
319
+ chunk.map = null
320
+ })
321
+
322
+ // Step 4: remove Vite's browser CSS carrier; VPT owns the sole physical global WX stylesheet.
323
+ Object.entries(bundle).forEach(([fileName, output]) => {
324
+ if (isStyleAsset(output)) {
325
+ delete bundle[fileName]
82
326
  }
83
- hasFinalizedOutput = true
84
- return
85
- }
327
+ })
86
328
 
87
- const [style] = styles
88
- const source = typeof style.source === 'string' ? style.source : new TextDecoder().decode(style.source)
89
- const transformedResult = await transformStyle(source)
329
+ // Step 5: always emit the imported global file, including an empty file for applications without styles.
330
+ this.emitFile({ type: 'asset', fileName: globalWxssFileName, source: finalized.wxss })
331
+ }
332
+ },
333
+ /** Releases build-only compiler resources after the final bundle has consumed their candidate sets. */
334
+ closeBundle() {
335
+ if (this.environment.config.command === 'build') {
336
+ disposeTailwindRoots()
337
+ }
338
+ },
339
+ /** Releases long-lived development resources and clears captured CSS when the owning watcher terminates. */
340
+ closeWatcher() {
341
+ disposeTailwindRoots()
342
+ styleByModuleId.clear()
343
+ },
344
+ /** Finalizes one development result and publishes matching WXSS before exposing converted patch factories. */
345
+ finalizeUpdate: async <Artifact extends JavaScriptArtifact>(
346
+ artifacts: readonly Artifact[],
347
+ writeWxss: (wxss: string) => Promise<void>
348
+ ): Promise<readonly Artifact[]> => {
349
+ // Step 1: complete every fallible conversion against one snapshot of the current module graph.
350
+ const output = await finalizeCurrentOutput(graphContext, artifacts)
90
351
 
91
- // Preserve the compiler stylesheet as the real global asset so its bundle metadata and ownership remain
92
- // intact. Only its finalized contents and stable WXSS identity change.
93
- style.source = transformedResult.css
94
- style.fileName = 'assets/global.wxss'
95
- hasFinalizedOutput = true
352
+ // Step 2: publish changed WXSS first so DevTools cannot observe JavaScript containing newer class identities.
353
+ if (output.wxss !== publishedWxss) {
354
+ await writeWxss(output.wxss)
355
+ // Advance the frontier only after the atomic writer succeeds; failed writes remain retryable.
356
+ publishedWxss = output.wxss
96
357
  }
358
+
359
+ // Step 3: preserve patch metadata and replace only code after the matching stylesheet is durable.
360
+ return artifacts.map((artifact, index) => ({ ...artifact, code: output.javaScript[index]! }))
97
361
  }
98
362
  }
99
363
  }
100
364
 
101
365
  /**
102
- * Adapts upstream plugin descriptors without mutating `weapp-tailwindcss` or patching node_modules.
103
- *
104
- * Vite first groups whole plugins by `enforce`, then orders individual hooks. Upstream's output plugins specify both
105
- * `enforce: 'post'` and `generateBundle.order: 'post'`. The plugin-level phase overrides their earlier registration and
106
- * places them after VPT's normal plugins, so VPT observes incomplete CSS. Making all of VPT post-enforced would fix that
107
- * one hook while unnecessarily reordering resolution and transforms.
366
+ * Produces WXSS and JavaScript from one live-graph projection.
108
367
  *
109
- * For upstream plugins that actually own generateBundle, clone the descriptor without plugin-level enforcement. Keep
110
- * hook-level `order: 'post'`: it still waits for ordinary bundle generation, while registration order becomes the sole
111
- * tie-breaker between upstream generation, VPT finalization and native output.
368
+ * The function receives every stateful dependency explicitly so tests and both output modes execute the same algorithm. It
369
+ * completes WXSS conversion before JavaScript conversion and returns bytes without publishing or mutating caller artifacts.
112
370
  */
113
- function alignGenerateBundleOrder(plugin: Plugin): Plugin {
114
- if (plugin.enforce !== 'post' || plugin.generateBundle === undefined) {
115
- return plugin
371
+ export async function finalizeOutput(
372
+ entryIds: readonly string[],
373
+ styleByModuleId: ReadonlyMap<
374
+ string,
375
+ Readonly<{
376
+ css: string | undefined
377
+ tailwind: Readonly<{ classSet: ReadonlySet<string> }> | undefined
378
+ }>
379
+ >,
380
+ getModuleInfo: (
381
+ moduleId: string
382
+ ) => Readonly<{ importedIds: readonly string[]; dynamicallyImportedIds: readonly string[] }> | null | undefined,
383
+ weappContext: Pick<ReturnType<typeof createContext>, 'transformJs' | 'transformWxss'>,
384
+ javaScript: readonly JavaScriptArtifact[]
385
+ ) {
386
+ // Step 1: derive cascade order, reachable CSS, and raw Tailwind candidates from the same current graph snapshot.
387
+ const projection = projectStyles(entryIds, styleByModuleId, getModuleInfo)
388
+
389
+ // Step 2: convert the complete stylesheet once; this fixes the selector identities JavaScript must subsequently use.
390
+ const wxss = (await weappContext.transformWxss(projection.css, wxStyleOptions)).css
391
+
392
+ // Step 3: transform artifacts independently but with the exact candidate set used by the stylesheet conversion.
393
+ const transformedJavaScript = await Promise.all(
394
+ javaScript.map(async (artifact) => {
395
+ // Ordinary CSS needs no class-string rewrite, so preserve JavaScript bytes when Tailwind contributed no candidates.
396
+ if (projection.classSet.size === 0) {
397
+ return artifact.code
398
+ }
399
+
400
+ const result = await weappContext.transformJs(artifact.code, {
401
+ filename: artifact.filename,
402
+ generateMap: false,
403
+ runtimeSet: projection.classSet
404
+ })
405
+ if (result.error) {
406
+ // Reject the whole transaction; callers have not mutated chunks or published WXSS at this point.
407
+ throw result.error
408
+ }
409
+ return result.code
410
+ })
411
+ )
412
+
413
+ // Returning data keeps physical bundle mutation and development filesystem publication at their respective owners.
414
+ return { javaScript: transformedJavaScript, wxss: wxss }
415
+ }
416
+
417
+ /** Selects styles reachable from the configured entries in deterministic dependency-first cascade order. */
418
+ function projectStyles(
419
+ entryIds: Parameters<typeof finalizeOutput>[0],
420
+ styleByModuleId: Parameters<typeof finalizeOutput>[1],
421
+ getModuleInfo: Parameters<typeof finalizeOutput>[2]
422
+ ) {
423
+ // This mutable transaction-local set terminates cycles and prevents repeated traversal through shared JavaScript modules.
424
+ const visitedModuleIds = new Set<string>()
425
+ // This mutable transaction-local set emits a physical stylesheet once even when multiple graph paths import it.
426
+ const visitedStyleIds = new Set<string>()
427
+ // This mutable transaction-local list records dependency-first CSS order for the final concatenated stylesheet.
428
+ const css: string[] = []
429
+ // This mutable transaction-local set unions candidates from exactly the Tailwind roots contributing reachable CSS.
430
+ const classSet = new Set<string>()
431
+
432
+ /** Performs a post-order graph visit so dependencies precede the modules that import them in the CSS cascade. */
433
+ const visit = (moduleId: string): void => {
434
+ // Step 1: claim the module before recursion to terminate cycles and shared dependency paths.
435
+ if (visitedModuleIds.has(moduleId)) {
436
+ return
437
+ }
438
+ visitedModuleIds.add(moduleId)
439
+
440
+ // Step 2: ignore IDs absent from the current graph; retained CSS alone never makes a removed module reachable.
441
+ const moduleInfo = getModuleInfo(moduleId)
442
+ if (!moduleInfo) {
443
+ return
444
+ }
445
+
446
+ // Step 3: visit static and dynamic dependencies before considering this module's own stylesheet contribution.
447
+ moduleInfo.importedIds.forEach(visit)
448
+ moduleInfo.dynamicallyImportedIds.forEach(visit)
449
+
450
+ // Step 4: join graph identity to captured style identity and append each reachable physical stylesheet once.
451
+ const styleId = normalizeModuleId(moduleId)
452
+ const style = styleByModuleId.get(styleId)
453
+ if (style?.css === undefined || visitedStyleIds.has(styleId)) {
454
+ return
455
+ }
456
+ visitedStyleIds.add(styleId)
457
+ css.push(style.css)
458
+
459
+ // Step 5: union candidates only from roots whose CSS survived this same reachability projection.
460
+ style.tailwind?.classSet.forEach((className) => {
461
+ classSet.add(className)
462
+ })
116
463
  }
117
464
 
118
- // Clone rather than mutate: upstream may retain or reuse the descriptor returned by its factory.
119
- return { ...plugin, enforce: undefined }
465
+ // Each App/Page entry is a root; shared visited sets deduplicate styles across the complete application projection.
466
+ entryIds.forEach(visit)
467
+
468
+ return { classSet: classSet, css: css.join('\n') }
469
+ }
470
+
471
+ /** Compiles one Tailwind root and returns replacement state without mutating the retained module store. */
472
+ async function compileTailwindRoot(
473
+ projectRoot: string,
474
+ rootId: string,
475
+ css: string,
476
+ previous: TailwindRoot | undefined
477
+ ): Promise<Readonly<{ css: string; root: TailwindRoot }>> {
478
+ // Step 1: exact root-source equality proves that candidate changes can reuse the existing compiler and scanner caches.
479
+ const reuse = previous?.source === css
480
+
481
+ // Step 2: compiler-input changes resolve a fresh Tailwind source and create a new generator before retained state changes.
482
+ const generator = reuse
483
+ ? previous.generator
484
+ : createWeappTailwindcssGenerator(
485
+ await resolveTailwindV4Source({
486
+ projectRoot: projectRoot,
487
+ cwd: tailwindcssBasedir,
488
+ cssSources: [{ css: css, base: path.dirname(rootId), file: rootId }]
489
+ })
490
+ )
491
+
492
+ try {
493
+ // Step 3: authoritative source scanning updates additions and removals in the persistent incremental candidate cache.
494
+ const generated = await generator.generate({ target: 'web', scanSources: true, incrementalCache: true })
495
+
496
+ // Step 4: return a complete immutable replacement record; the caller commits it only after this function succeeds.
497
+ return {
498
+ css: generated.css,
499
+ root: {
500
+ source: css,
501
+ classSet: generated.classSet,
502
+ dependencies: new Set(generated.dependencies.map(normalizeModuleId)),
503
+ generator: generator,
504
+ // Source patterns change with compiler inputs, while candidate-only updates can reuse their native matcher.
505
+ scanner: reuse ? previous.scanner : new Scanner({ sources: generated.sources }),
506
+ invalidated: false
507
+ }
508
+ }
509
+ } catch (error) {
510
+ // A new generator has no retained owner on failure; reused generators remain owned by the previous root record.
511
+ if (!reuse) {
512
+ generator.dispose?.()
513
+ }
514
+ throw error
515
+ }
516
+ }
517
+
518
+ /** Returns whether a Vite request represents a physical CSS module that contributes to application WXSS. */
519
+ function isApplicationStyle(id: string): boolean {
520
+ // Step 1: use Vite's predicate so every supported preprocessor extension follows the same path.
521
+ if (!isCSSRequest(id)) {
522
+ return false
523
+ }
524
+
525
+ // Step 2: a query-free CSS request is always a physical application stylesheet.
526
+ const queryStart = id.indexOf('?')
527
+ if (queryStart < 0) {
528
+ return true
529
+ }
530
+
531
+ // Step 3: reject Vite request modes whose values must not enter the global CSS projection.
532
+ const fragmentStart = id.indexOf('#', queryStart)
533
+ const query = id.slice(queryStart + 1, fragmentStart < 0 ? undefined : fragmentStart)
534
+ const parameters = new URLSearchParams(query)
535
+ return ignoredStyleQueries.every((parameter) => !parameters.has(parameter))
536
+ }
537
+
538
+ /** Detects source forms that require Tailwind compilation before Vite processes the resulting CSS. */
539
+ function isTailwindRoot(code: string): boolean {
540
+ // Tailwind v4 uses package imports; legacy directives remain accepted because the generator supports both forms.
541
+ return (
542
+ /@import\s+(?:url\(\s*)?['"]tailwindcss(?:\/[^'"]*)?['"]/.test(code) ||
543
+ /@tailwind\s+(?:base|components|utilities)\b/.test(code)
544
+ )
120
545
  }
121
546
 
122
- /** Selects only the compiler stylesheet; native WXSS assets are emitted by the later WX hook. */
547
+ /** Identifies Vite's browser stylesheet carrier, which VPT replaces after all final CSS has been captured. */
123
548
  function isStyleAsset(output: Rolldown.OutputBundle[string]): output is Rolldown.OutputAsset {
124
549
  return output.type === 'asset' && /\.(?:css|wxss)$/.test(output.fileName)
125
550
  }