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