@ecopages/postcss-processor 0.2.0-alpha.2 → 0.2.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,10 @@ All notable changes to `@ecopages/postcss-processor` are documented here.
6
6
 
7
7
  ## [UNRELEASED] — TBD
8
8
 
9
+ ### Bug Fixes
10
+
11
+ - Rebuilt tracked stylesheets from fresh PostCSS plugin instances on non-CSS source changes so Tailwind utility generation updates without stale caches or forced reloads.
12
+
9
13
  ### Features
10
14
 
11
15
  - **Runtime CSS loaders** — Added `css-loader-plugin.ts`, `css-loader.bun.ts`, and `css-runtime-contract.ts` to support runtime CSS loading. CSS is now cached at runtime to avoid redundant processing during HMR (`cbaafea4`, `e7653c9b`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/postcss-processor",
3
- "version": "0.2.0-alpha.2",
3
+ "version": "0.2.0-alpha.3",
4
4
  "description": "Postcss processor, transform string or postcss file to css",
5
5
  "keywords": [
6
6
  "postcss",
@@ -17,8 +17,8 @@
17
17
  "directory": "packages/processors/postcss-processor"
18
18
  },
19
19
  "dependencies": {
20
- "@ecopages/core": "0.2.0-alpha.2",
21
- "@ecopages/file-system": "0.2.0-alpha.2",
20
+ "@ecopages/core": "0.2.0-alpha.3",
21
+ "@ecopages/file-system": "0.2.0-alpha.3",
22
22
  "@ecopages/logger": "latest",
23
23
  "autoprefixer": "^10.4.0",
24
24
  "browserslist": "^4.28.1",
package/src/plugin.d.ts CHANGED
@@ -9,6 +9,15 @@ import type postcss from 'postcss';
9
9
  * Record of PostCSS plugins keyed by name
10
10
  */
11
11
  export type PluginsRecord = Record<string, postcss.AcceptedPlugin>;
12
+ /**
13
+ * Lazily creates PostCSS plugins.
14
+ *
15
+ * This is primarily used in development when a non-CSS file change forces the
16
+ * processor to rebuild tracked stylesheets. Some plugins, including Tailwind,
17
+ * keep internal caches in long-lived plugin instances, so recreating them is
18
+ * required to pick up newly discovered classes.
19
+ */
20
+ export type PluginFactoryRecord = Record<string, () => postcss.AcceptedPlugin>;
12
21
  /**
13
22
  * Configuration for the PostCSS processor
14
23
  */
@@ -38,6 +47,13 @@ export interface PostCssProcessorPluginConfig {
38
47
  * @default undefined (uses default plugins)
39
48
  */
40
49
  plugins?: PluginsRecord;
50
+ /**
51
+ * Factory functions for recreating stateful PostCSS plugins.
52
+ *
53
+ * When provided, Ecopages uses these factories to build a fresh plugin list
54
+ * for dependency-driven stylesheet rebuilds during development.
55
+ */
56
+ pluginFactories?: PluginFactoryRecord;
41
57
  }
42
58
  /**
43
59
  * PostCssProcessorPlugin
@@ -46,7 +62,10 @@ export interface PostCssProcessorPluginConfig {
46
62
  export declare class PostCssProcessorPlugin extends Processor<PostCssProcessorPluginConfig> {
47
63
  static DEFAULT_OPTIONS: Required<Pick<PostCssProcessorPluginConfig, 'filter'>>;
48
64
  private postcssPlugins;
65
+ private pluginFactories?;
49
66
  private readonly runtimeCssCache;
67
+ private readonly trackedCssFiles;
68
+ private watchQueue;
50
69
  private getCssFilter;
51
70
  private resolveProcessedCssPath;
52
71
  private readProcessedCssFromDist;
@@ -55,6 +74,11 @@ export declare class PostCssProcessorPlugin extends Processor<PostCssProcessorPl
55
74
  private transformCssSync;
56
75
  private transformCssAsync;
57
76
  matchesFileFilter(filepath: string): boolean;
77
+ private materializePluginFactories;
78
+ private refreshConfiguredPlugins;
79
+ private enqueueWatchTask;
80
+ private getTrackedCssFiles;
81
+ private handleDependencyChange;
58
82
  constructor(config?: Omit<ProcessorConfig<PostCssProcessorPluginConfig>, 'name' | 'description'>);
59
83
  /**
60
84
  * Handles CSS file changes during development.
package/src/plugin.js CHANGED
@@ -12,7 +12,10 @@ class PostCssProcessorPlugin extends Processor {
12
12
  filter: /\.css$/
13
13
  };
14
14
  postcssPlugins = [];
15
+ pluginFactories;
15
16
  runtimeCssCache = /* @__PURE__ */ new Map();
17
+ trackedCssFiles = /* @__PURE__ */ new Set();
18
+ watchQueue = Promise.resolve();
16
19
  getCssFilter() {
17
20
  return this.options?.filter ?? PostCssProcessorPlugin.DEFAULT_OPTIONS.filter;
18
21
  }
@@ -53,6 +56,7 @@ class PostCssProcessorPlugin extends Processor {
53
56
  if (!this.matchesFileFilter(filePath)) {
54
57
  continue;
55
58
  }
59
+ this.trackedCssFiles.add(filePath);
56
60
  const rawContents = await fileSystem.readFile(filePath);
57
61
  let transformedInput = rawContents;
58
62
  if (this.options?.transformInput) {
@@ -92,6 +96,38 @@ class PostCssProcessorPlugin extends Processor {
92
96
  const filter = this.options?.filter ?? PostCssProcessorPlugin.DEFAULT_OPTIONS.filter;
93
97
  return filter.test(filepath);
94
98
  }
99
+ materializePluginFactories(pluginFactories) {
100
+ return Object.values(pluginFactories).map((factory) => factory());
101
+ }
102
+ refreshConfiguredPlugins() {
103
+ if (!this.pluginFactories) {
104
+ return;
105
+ }
106
+ this.postcssPlugins = this.materializePluginFactories(this.pluginFactories);
107
+ }
108
+ enqueueWatchTask(task) {
109
+ const queuedTask = this.watchQueue.then(task, task);
110
+ this.watchQueue = queuedTask.catch(() => void 0);
111
+ return queuedTask;
112
+ }
113
+ getTrackedCssFiles() {
114
+ return Array.from(this.trackedCssFiles).filter(
115
+ (filePath) => this.matchesFileFilter(filePath) && fileSystem.exists(filePath)
116
+ );
117
+ }
118
+ async handleDependencyChange(bridge) {
119
+ if (!this.context) {
120
+ return;
121
+ }
122
+ const cssFiles = this.getTrackedCssFiles();
123
+ if (cssFiles.length === 0) {
124
+ return;
125
+ }
126
+ this.refreshConfiguredPlugins();
127
+ for (const cssFilePath of cssFiles) {
128
+ await this.handleCssChange(cssFilePath, bridge, false);
129
+ }
130
+ }
95
131
  constructor(config = {
96
132
  options: PostCssProcessorPlugin.DEFAULT_OPTIONS
97
133
  }) {
@@ -101,14 +137,52 @@ class PostCssProcessorPlugin extends Processor {
101
137
  capabilities: [
102
138
  {
103
139
  kind: "stylesheet",
104
- extensions: ["*.css"]
140
+ extensions: ["*.{css,scss,sass,less}"]
105
141
  }
106
142
  ],
107
143
  watch: {
108
144
  paths: [],
109
- extensions: [".css", ".scss", ".sass", ".less"],
145
+ extensions: [
146
+ ".css",
147
+ ".scss",
148
+ ".sass",
149
+ ".less",
150
+ ".tsx",
151
+ ".ts",
152
+ ".jsx",
153
+ ".js",
154
+ ".mdx",
155
+ ".html",
156
+ ".svelte",
157
+ ".vue"
158
+ ],
110
159
  onChange: async ({ path: path2, bridge }) => {
111
- await this.handleCssChange(path2, bridge);
160
+ await this.enqueueWatchTask(async () => {
161
+ if (this.matchesFileFilter(path2)) {
162
+ await this.handleCssChange(path2, bridge);
163
+ return;
164
+ }
165
+ await this.handleDependencyChange(bridge);
166
+ });
167
+ },
168
+ onCreate: async ({ path: path2, bridge }) => {
169
+ await this.enqueueWatchTask(async () => {
170
+ if (this.matchesFileFilter(path2)) {
171
+ await this.handleCssChange(path2, bridge);
172
+ return;
173
+ }
174
+ await this.handleDependencyChange(bridge);
175
+ });
176
+ },
177
+ onDelete: async ({ path: path2, bridge }) => {
178
+ await this.enqueueWatchTask(async () => {
179
+ if (this.matchesFileFilter(path2)) {
180
+ this.runtimeCssCache.delete(path2);
181
+ this.trackedCssFiles.delete(path2);
182
+ return;
183
+ }
184
+ await this.handleDependencyChange(bridge);
185
+ });
112
186
  }
113
187
  },
114
188
  ...config
@@ -118,14 +192,23 @@ class PostCssProcessorPlugin extends Processor {
118
192
  * Handles CSS file changes during development.
119
193
  * Processes the file and broadcasts a css-update event for hot reloading.
120
194
  */
121
- async handleCssChange(filePath, bridge) {
195
+ async handleCssChange(filePath, bridge, refreshPlugins = true) {
122
196
  if (!this.context) return;
197
+ if (!fileSystem.exists(filePath)) return;
123
198
  try {
199
+ this.trackedCssFiles.add(filePath);
200
+ if (refreshPlugins) {
201
+ this.refreshConfiguredPlugins();
202
+ }
124
203
  let content = await fileSystem.readFile(filePath);
125
204
  if (this.options?.transformInput) {
126
205
  content = await this.options.transformInput(content, filePath);
127
206
  }
128
207
  const processed = await this.process(content, filePath);
208
+ const cached = this.runtimeCssCache.get(filePath);
209
+ if (cached === processed) {
210
+ return;
211
+ }
129
212
  this.runtimeCssCache.set(filePath, processed);
130
213
  await this.persistProcessedCss(filePath, processed);
131
214
  bridge.cssUpdate(filePath);
@@ -172,6 +255,7 @@ class PostCssProcessorPlugin extends Processor {
172
255
  const configExtensions = ["js", "cjs", "mjs", "ts"];
173
256
  let foundConfigPath;
174
257
  let loadedPlugins;
258
+ let loadedPluginFactories;
175
259
  for (const ext of configExtensions) {
176
260
  const configPath = path.join(this.context.rootDir, `postcss.config.${ext}`);
177
261
  if (fileSystem.exists(configPath)) {
@@ -184,6 +268,9 @@ class PostCssProcessorPlugin extends Processor {
184
268
  logger.debug(`Loading PostCSS config from: ${foundConfigPath}`);
185
269
  const postcssConfigModule = await import(foundConfigPath);
186
270
  const postcssConfig = postcssConfigModule.default || postcssConfigModule;
271
+ if (postcssConfig && typeof postcssConfig.pluginFactories === "object" && postcssConfig.pluginFactories !== null) {
272
+ loadedPluginFactories = postcssConfig.pluginFactories;
273
+ }
187
274
  if (postcssConfig && typeof postcssConfig.plugins === "object" && postcssConfig.plugins !== null) {
188
275
  if (Array.isArray(postcssConfig.plugins)) {
189
276
  loadedPlugins = postcssConfig.plugins;
@@ -203,15 +290,25 @@ class PostCssProcessorPlugin extends Processor {
203
290
  } else {
204
291
  logger.debug("No PostCSS config file found in root directory.");
205
292
  }
206
- if (loadedPlugins) {
293
+ if (loadedPluginFactories) {
294
+ this.pluginFactories = loadedPluginFactories;
295
+ this.postcssPlugins = this.materializePluginFactories(loadedPluginFactories);
296
+ } else if (loadedPlugins) {
297
+ this.pluginFactories = void 0;
207
298
  this.postcssPlugins = loadedPlugins;
299
+ } else if (this.options?.pluginFactories) {
300
+ logger.debug("Using PostCSS plugin factories provided in processor options.");
301
+ this.pluginFactories = this.options.pluginFactories;
302
+ this.postcssPlugins = this.materializePluginFactories(this.options.pluginFactories);
208
303
  } else if (this.options?.plugins) {
209
304
  logger.debug("Using PostCSS plugins provided in processor options.");
305
+ this.pluginFactories = void 0;
210
306
  this.postcssPlugins = Object.values(this.options.plugins);
211
307
  } else {
212
308
  logger.warn(
213
309
  "No PostCSS plugins configured. Use a preset like tailwindV3Preset() or tailwindV4Preset(), provide plugins via options, or create a postcss.config file."
214
310
  );
311
+ this.pluginFactories = void 0;
215
312
  this.postcssPlugins = [];
216
313
  }
217
314
  if (!this.postcssPlugins || this.postcssPlugins.length === 0) {
package/src/plugin.ts CHANGED
@@ -23,6 +23,16 @@ const logger = new Logger('[@ecopages/postcss-processor]', {
23
23
  */
24
24
  export type PluginsRecord = Record<string, postcss.AcceptedPlugin>;
25
25
 
26
+ /**
27
+ * Lazily creates PostCSS plugins.
28
+ *
29
+ * This is primarily used in development when a non-CSS file change forces the
30
+ * processor to rebuild tracked stylesheets. Some plugins, including Tailwind,
31
+ * keep internal caches in long-lived plugin instances, so recreating them is
32
+ * required to pick up newly discovered classes.
33
+ */
34
+ export type PluginFactoryRecord = Record<string, () => postcss.AcceptedPlugin>;
35
+
26
36
  /**
27
37
  * Configuration for the PostCSS processor
28
38
  */
@@ -52,6 +62,13 @@ export interface PostCssProcessorPluginConfig {
52
62
  * @default undefined (uses default plugins)
53
63
  */
54
64
  plugins?: PluginsRecord;
65
+ /**
66
+ * Factory functions for recreating stateful PostCSS plugins.
67
+ *
68
+ * When provided, Ecopages uses these factories to build a fresh plugin list
69
+ * for dependency-driven stylesheet rebuilds during development.
70
+ */
71
+ pluginFactories?: PluginFactoryRecord;
55
72
  }
56
73
 
57
74
  /**
@@ -64,7 +81,10 @@ export class PostCssProcessorPlugin extends Processor<PostCssProcessorPluginConf
64
81
  };
65
82
 
66
83
  private postcssPlugins: postcss.AcceptedPlugin[] = [];
84
+ private pluginFactories?: PluginFactoryRecord;
67
85
  private readonly runtimeCssCache = new Map<string, string>();
86
+ private readonly trackedCssFiles = new Set<string>();
87
+ private watchQueue: Promise<void> = Promise.resolve();
68
88
 
69
89
  private getCssFilter(): RegExp {
70
90
  return this.options?.filter ?? PostCssProcessorPlugin.DEFAULT_OPTIONS.filter;
@@ -117,6 +137,8 @@ export class PostCssProcessorPlugin extends Processor<PostCssProcessorPluginConf
117
137
  continue;
118
138
  }
119
139
 
140
+ this.trackedCssFiles.add(filePath);
141
+
120
142
  const rawContents = await fileSystem.readFile(filePath);
121
143
  let transformedInput = rawContents;
122
144
 
@@ -169,6 +191,47 @@ export class PostCssProcessorPlugin extends Processor<PostCssProcessorPluginConf
169
191
  return filter.test(filepath);
170
192
  }
171
193
 
194
+ private materializePluginFactories(pluginFactories: PluginFactoryRecord): postcss.AcceptedPlugin[] {
195
+ return Object.values(pluginFactories).map((factory) => factory());
196
+ }
197
+
198
+ private refreshConfiguredPlugins(): void {
199
+ if (!this.pluginFactories) {
200
+ return;
201
+ }
202
+
203
+ this.postcssPlugins = this.materializePluginFactories(this.pluginFactories);
204
+ }
205
+
206
+ private enqueueWatchTask(task: () => Promise<void>): Promise<void> {
207
+ const queuedTask = this.watchQueue.then(task, task);
208
+ this.watchQueue = queuedTask.catch(() => undefined);
209
+ return queuedTask;
210
+ }
211
+
212
+ private getTrackedCssFiles(): string[] {
213
+ return Array.from(this.trackedCssFiles).filter(
214
+ (filePath) => this.matchesFileFilter(filePath) && fileSystem.exists(filePath),
215
+ );
216
+ }
217
+
218
+ private async handleDependencyChange(bridge: IClientBridge): Promise<void> {
219
+ if (!this.context) {
220
+ return;
221
+ }
222
+
223
+ const cssFiles = this.getTrackedCssFiles();
224
+ if (cssFiles.length === 0) {
225
+ return;
226
+ }
227
+
228
+ this.refreshConfiguredPlugins();
229
+
230
+ for (const cssFilePath of cssFiles) {
231
+ await this.handleCssChange(cssFilePath, bridge, false);
232
+ }
233
+ }
234
+
172
235
  constructor(
173
236
  config: Omit<ProcessorConfig<PostCssProcessorPluginConfig>, 'name' | 'description'> = {
174
237
  options: PostCssProcessorPlugin.DEFAULT_OPTIONS,
@@ -180,14 +243,55 @@ export class PostCssProcessorPlugin extends Processor<PostCssProcessorPluginConf
180
243
  capabilities: [
181
244
  {
182
245
  kind: 'stylesheet',
183
- extensions: ['*.css'],
246
+ extensions: ['*.{css,scss,sass,less}'],
184
247
  },
185
248
  ],
186
249
  watch: {
187
250
  paths: [],
188
- extensions: ['.css', '.scss', '.sass', '.less'],
251
+ extensions: [
252
+ '.css',
253
+ '.scss',
254
+ '.sass',
255
+ '.less',
256
+ '.tsx',
257
+ '.ts',
258
+ '.jsx',
259
+ '.js',
260
+ '.mdx',
261
+ '.html',
262
+ '.svelte',
263
+ '.vue',
264
+ ],
189
265
  onChange: async ({ path, bridge }) => {
190
- await this.handleCssChange(path, bridge);
266
+ await this.enqueueWatchTask(async () => {
267
+ if (this.matchesFileFilter(path)) {
268
+ await this.handleCssChange(path, bridge);
269
+ return;
270
+ }
271
+
272
+ await this.handleDependencyChange(bridge);
273
+ });
274
+ },
275
+ onCreate: async ({ path, bridge }) => {
276
+ await this.enqueueWatchTask(async () => {
277
+ if (this.matchesFileFilter(path)) {
278
+ await this.handleCssChange(path, bridge);
279
+ return;
280
+ }
281
+
282
+ await this.handleDependencyChange(bridge);
283
+ });
284
+ },
285
+ onDelete: async ({ path, bridge }) => {
286
+ await this.enqueueWatchTask(async () => {
287
+ if (this.matchesFileFilter(path)) {
288
+ this.runtimeCssCache.delete(path);
289
+ this.trackedCssFiles.delete(path);
290
+ return;
291
+ }
292
+
293
+ await this.handleDependencyChange(bridge);
294
+ });
191
295
  },
192
296
  },
193
297
  ...config,
@@ -198,10 +302,17 @@ export class PostCssProcessorPlugin extends Processor<PostCssProcessorPluginConf
198
302
  * Handles CSS file changes during development.
199
303
  * Processes the file and broadcasts a css-update event for hot reloading.
200
304
  */
201
- private async handleCssChange(filePath: string, bridge: IClientBridge): Promise<void> {
305
+ private async handleCssChange(filePath: string, bridge: IClientBridge, refreshPlugins = true): Promise<void> {
202
306
  if (!this.context) return;
307
+ if (!fileSystem.exists(filePath)) return;
203
308
 
204
309
  try {
310
+ this.trackedCssFiles.add(filePath);
311
+
312
+ if (refreshPlugins) {
313
+ this.refreshConfiguredPlugins();
314
+ }
315
+
205
316
  let content = await fileSystem.readFile(filePath);
206
317
 
207
318
  if (this.options?.transformInput) {
@@ -209,6 +320,12 @@ export class PostCssProcessorPlugin extends Processor<PostCssProcessorPluginConf
209
320
  }
210
321
 
211
322
  const processed = await this.process(content, filePath);
323
+
324
+ const cached = this.runtimeCssCache.get(filePath);
325
+ if (cached === processed) {
326
+ return;
327
+ }
328
+
212
329
  this.runtimeCssCache.set(filePath, processed);
213
330
  await this.persistProcessedCss(filePath, processed);
214
331
 
@@ -262,6 +379,7 @@ export class PostCssProcessorPlugin extends Processor<PostCssProcessorPluginConf
262
379
  const configExtensions = ['js', 'cjs', 'mjs', 'ts'];
263
380
  let foundConfigPath: string | undefined;
264
381
  let loadedPlugins: postcss.AcceptedPlugin[] | undefined;
382
+ let loadedPluginFactories: PluginFactoryRecord | undefined;
265
383
 
266
384
  for (const ext of configExtensions) {
267
385
  const configPath = path.join(this.context.rootDir, `postcss.config.${ext}`);
@@ -277,6 +395,13 @@ export class PostCssProcessorPlugin extends Processor<PostCssProcessorPluginConf
277
395
 
278
396
  const postcssConfigModule = await import(foundConfigPath);
279
397
  const postcssConfig = postcssConfigModule.default || postcssConfigModule;
398
+ if (
399
+ postcssConfig &&
400
+ typeof postcssConfig.pluginFactories === 'object' &&
401
+ postcssConfig.pluginFactories !== null
402
+ ) {
403
+ loadedPluginFactories = postcssConfig.pluginFactories as PluginFactoryRecord;
404
+ }
280
405
 
281
406
  if (postcssConfig && typeof postcssConfig.plugins === 'object' && postcssConfig.plugins !== null) {
282
407
  if (Array.isArray(postcssConfig.plugins)) {
@@ -298,16 +423,26 @@ export class PostCssProcessorPlugin extends Processor<PostCssProcessorPluginConf
298
423
  logger.debug('No PostCSS config file found in root directory.');
299
424
  }
300
425
 
301
- if (loadedPlugins) {
426
+ if (loadedPluginFactories) {
427
+ this.pluginFactories = loadedPluginFactories;
428
+ this.postcssPlugins = this.materializePluginFactories(loadedPluginFactories);
429
+ } else if (loadedPlugins) {
430
+ this.pluginFactories = undefined;
302
431
  this.postcssPlugins = loadedPlugins;
432
+ } else if (this.options?.pluginFactories) {
433
+ logger.debug('Using PostCSS plugin factories provided in processor options.');
434
+ this.pluginFactories = this.options.pluginFactories;
435
+ this.postcssPlugins = this.materializePluginFactories(this.options.pluginFactories);
303
436
  } else if (this.options?.plugins) {
304
437
  logger.debug('Using PostCSS plugins provided in processor options.');
438
+ this.pluginFactories = undefined;
305
439
  this.postcssPlugins = Object.values(this.options.plugins);
306
440
  } else {
307
441
  logger.warn(
308
442
  'No PostCSS plugins configured. Use a preset like tailwindV3Preset() or tailwindV4Preset(), ' +
309
443
  'provide plugins via options, or create a postcss.config file.',
310
444
  );
445
+ this.pluginFactories = undefined;
311
446
  this.postcssPlugins = [];
312
447
  }
313
448
 
@@ -12,6 +12,8 @@ import type { PostCssProcessorPluginConfig } from '../plugin.js';
12
12
  * Features:
13
13
  * - Uses classic Tailwind v3 plugin stack
14
14
  * - Includes postcss-import, tailwindcss/nesting, tailwindcss, autoprefixer, cssnano
15
+ * - Returns both `plugins` for immediate use and `pluginFactories` so Ecopages
16
+ * can recreate fresh Tailwind/PostCSS plugin instances on dependency-driven rebuilds
15
17
  *
16
18
  * @example
17
19
  * ```typescript
@@ -9,14 +9,17 @@ function tailwindV3Preset() {
9
9
  const autoprefixerOptions = browserslistConfig ? {} : {
10
10
  overrideBrowserslist: [">0.3%", "not ie 11", "not dead", "not op_mini all"]
11
11
  };
12
- const plugins = {
13
- "postcss-import": postcssImport(),
14
- "tailwindcss/nesting": tailwindcssNesting(),
15
- tailwindcss: tailwindcss(),
16
- autoprefixer: autoprefixer(autoprefixerOptions),
17
- cssnano: cssnano()
12
+ const pluginFactories = {
13
+ "postcss-import": () => postcssImport(),
14
+ "tailwindcss/nesting": () => tailwindcssNesting(),
15
+ tailwindcss: () => tailwindcss(),
16
+ autoprefixer: () => autoprefixer(autoprefixerOptions),
17
+ cssnano: () => cssnano()
18
18
  };
19
- return { plugins };
19
+ const plugins = Object.fromEntries(
20
+ Object.entries(pluginFactories).map(([name, factory]) => [name, factory()])
21
+ );
22
+ return { plugins, pluginFactories };
20
23
  }
21
24
  export {
22
25
  tailwindV3Preset
@@ -13,7 +13,7 @@ import type postcss from 'postcss';
13
13
  import postcssImport from 'postcss-import';
14
14
  import tailwindcss from 'tailwindcss';
15
15
  import tailwindcssNesting from 'tailwindcss/nesting/index.js';
16
- import type { PostCssProcessorPluginConfig } from '../plugin.ts';
16
+ import type { PluginFactoryRecord, PostCssProcessorPluginConfig } from '../plugin.ts';
17
17
 
18
18
  type PluginsRecord = Record<string, postcss.AcceptedPlugin>;
19
19
 
@@ -23,6 +23,8 @@ type PluginsRecord = Record<string, postcss.AcceptedPlugin>;
23
23
  * Features:
24
24
  * - Uses classic Tailwind v3 plugin stack
25
25
  * - Includes postcss-import, tailwindcss/nesting, tailwindcss, autoprefixer, cssnano
26
+ * - Returns both `plugins` for immediate use and `pluginFactories` so Ecopages
27
+ * can recreate fresh Tailwind/PostCSS plugin instances on dependency-driven rebuilds
26
28
  *
27
29
  * @example
28
30
  * ```typescript
@@ -49,13 +51,22 @@ export function tailwindV3Preset(): PostCssProcessorPluginConfig {
49
51
  overrideBrowserslist: ['>0.3%', 'not ie 11', 'not dead', 'not op_mini all'],
50
52
  };
51
53
 
52
- const plugins: PluginsRecord = {
53
- 'postcss-import': postcssImport(),
54
- 'tailwindcss/nesting': tailwindcssNesting(),
55
- tailwindcss: tailwindcss(),
56
- autoprefixer: autoprefixer(autoprefixerOptions),
57
- cssnano: cssnano(),
54
+ const pluginFactories: PluginFactoryRecord = {
55
+ 'postcss-import': () => postcssImport(),
56
+ 'tailwindcss/nesting': () => tailwindcssNesting(),
57
+ tailwindcss: () => tailwindcss(),
58
+ autoprefixer: () => autoprefixer(autoprefixerOptions),
59
+ cssnano: () => cssnano(),
58
60
  };
59
61
 
60
- return { plugins };
62
+ const plugins: PluginsRecord = Object.fromEntries(
63
+ Object.entries(pluginFactories).map(([name, factory]) => [name, factory()]),
64
+ ) as PluginsRecord;
65
+
66
+ /**
67
+ * Keep both forms:
68
+ * - `plugins` are used immediately by the processor
69
+ * - `pluginFactories` let the processor recreate fresh plugin instances later
70
+ */
71
+ return { plugins, pluginFactories };
61
72
  }
@@ -23,6 +23,8 @@ export interface TailwindV4PresetOptions {
23
23
  * - Uses `@tailwindcss/postcss` plugin (v4)
24
24
  * - Automatically injects `@reference` headers for `@apply` support
25
25
  * - Includes cssnano for CSS minification
26
+ * - Returns both `plugins` for immediate use and `pluginFactories` so Ecopages
27
+ * can recreate fresh Tailwind/PostCSS plugin instances on dependency-driven rebuilds
26
28
  *
27
29
  * @example
28
30
  * ```typescript
@@ -11,14 +11,20 @@ function tailwindV4Preset(options) {
11
11
  const autoprefixerOptions = browserslistConfig ? {} : {
12
12
  overrideBrowserslist: [">0.3%", "not ie 11", "not dead", "not op_mini all"]
13
13
  };
14
+ const pluginFactories = {
15
+ "postcss-import": () => postcssImport(),
16
+ "postcss-nested": () => postcssNested(),
17
+ "@tailwindcss/postcss": () => tailwindcss(),
18
+ autoprefixer: () => autoprefixer(autoprefixerOptions),
19
+ cssnano: () => cssnano()
20
+ };
14
21
  return {
15
- plugins: {
16
- "postcss-import": postcssImport(),
17
- "postcss-nested": postcssNested(),
18
- "@tailwindcss/postcss": tailwindcss(),
19
- autoprefixer: autoprefixer(autoprefixerOptions),
20
- cssnano: cssnano()
21
- },
22
+ /**
23
+ * Instantiate the initial plugin list for the active processor instance.
24
+ * Fresh instances can later be recreated from `pluginFactories`.
25
+ */
26
+ plugins: Object.fromEntries(Object.entries(pluginFactories).map(([name, factory]) => [name, factory()])),
27
+ pluginFactories,
22
28
  transformInput: async (contents, filePath) => {
23
29
  const css = contents instanceof Buffer ? contents.toString("utf-8") : contents;
24
30
  const normalizedFilePath = path.resolve(filePath);
@@ -13,7 +13,7 @@ import cssnano from 'cssnano';
13
13
  import path from 'node:path';
14
14
  import postcssImport from 'postcss-import';
15
15
  import postcssNested from 'postcss-nested';
16
- import type { PostCssProcessorPluginConfig } from '../plugin.ts';
16
+ import type { PluginFactoryRecord, PostCssProcessorPluginConfig } from '../plugin.ts';
17
17
 
18
18
  /**
19
19
  * Options for Tailwind v4 preset
@@ -33,6 +33,8 @@ export interface TailwindV4PresetOptions {
33
33
  * - Uses `@tailwindcss/postcss` plugin (v4)
34
34
  * - Automatically injects `@reference` headers for `@apply` support
35
35
  * - Includes cssnano for CSS minification
36
+ * - Returns both `plugins` for immediate use and `pluginFactories` so Ecopages
37
+ * can recreate fresh Tailwind/PostCSS plugin instances on dependency-driven rebuilds
36
38
  *
37
39
  * @example
38
40
  * ```typescript
@@ -63,14 +65,21 @@ export function tailwindV4Preset(options: TailwindV4PresetOptions): PostCssProce
63
65
  overrideBrowserslist: ['>0.3%', 'not ie 11', 'not dead', 'not op_mini all'],
64
66
  };
65
67
 
68
+ const pluginFactories: PluginFactoryRecord = {
69
+ 'postcss-import': () => postcssImport(),
70
+ 'postcss-nested': () => postcssNested(),
71
+ '@tailwindcss/postcss': () => tailwindcss(),
72
+ autoprefixer: () => autoprefixer(autoprefixerOptions),
73
+ cssnano: () => cssnano(),
74
+ };
75
+
66
76
  return {
67
- plugins: {
68
- 'postcss-import': postcssImport(),
69
- 'postcss-nested': postcssNested(),
70
- '@tailwindcss/postcss': tailwindcss(),
71
- autoprefixer: autoprefixer(autoprefixerOptions),
72
- cssnano: cssnano(),
73
- },
77
+ /**
78
+ * Instantiate the initial plugin list for the active processor instance.
79
+ * Fresh instances can later be recreated from `pluginFactories`.
80
+ */
81
+ plugins: Object.fromEntries(Object.entries(pluginFactories).map(([name, factory]) => [name, factory()])),
82
+ pluginFactories,
74
83
  transformInput: async (contents: string | Buffer, filePath: string): Promise<string> => {
75
84
  const css = contents instanceof Buffer ? contents.toString('utf-8') : (contents as string);
76
85
  const normalizedFilePath = path.resolve(filePath);