@ecopages/postcss-processor 0.2.0-alpha.1 → 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 +26 -0
- package/package.json +38 -32
- package/src/index.d.ts +2 -0
- package/src/index.js +2 -0
- package/src/plugin.d.ts +112 -0
- package/src/plugin.js +354 -0
- package/src/plugin.ts +140 -5
- package/src/postcss-processor.d.ts +48 -0
- package/src/postcss-processor.js +69 -0
- package/src/presets/index.d.ts +6 -0
- package/src/presets/index.js +6 -0
- package/src/presets/tailwind-v3.d.ts +34 -0
- package/src/presets/tailwind-v3.js +26 -0
- package/src/presets/tailwind-v3.ts +19 -8
- package/src/presets/tailwind-v4.d.ts +47 -0
- package/src/presets/tailwind-v4.js +57 -0
- package/src/presets/tailwind-v4.ts +17 -8
- package/src/runtime/css-loader-plugin.d.ts +9 -0
- package/src/runtime/css-loader-plugin.js +28 -0
- package/src/runtime/css-loader.bun.d.ts +9 -0
- package/src/runtime/css-loader.bun.js +22 -0
- package/src/runtime/css-runtime-contract.d.ts +5 -0
- package/src/runtime/css-runtime-contract.js +0 -0
- package/src/test/css/base.css +0 -3
- package/src/test/css/correct.css +0 -3
- package/src/test/css/error.css +0 -3
- package/src/test/css/external-plugins.css +0 -13
- package/src/test/css/import.css +0 -4
- package/src/test/plugin.test.ts +0 -74
- package/src/test/postcss-processor.test.ts +0 -106
- package/src/test/presets.test.ts +0 -140
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: [
|
|
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.
|
|
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 (
|
|
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
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This module contains the PostCSS Processor
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
import postcss from 'postcss';
|
|
6
|
+
/**
|
|
7
|
+
* PostCSS Processor Options
|
|
8
|
+
*/
|
|
9
|
+
export type PostCssProcessorOptions = {
|
|
10
|
+
plugins?: postcss.AcceptedPlugin[];
|
|
11
|
+
/**
|
|
12
|
+
* Optional file path for resolving relative imports
|
|
13
|
+
*/
|
|
14
|
+
filePath?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Optional callback to transform the output CSS
|
|
17
|
+
* @param css The processed CSS
|
|
18
|
+
* @returns The transformed CSS
|
|
19
|
+
*/
|
|
20
|
+
transformOutput?: (css: string) => string | Promise<string>;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* ProcessPath
|
|
24
|
+
* @param path string
|
|
25
|
+
* @param options {@link PostCssProcessorOptions}
|
|
26
|
+
* @returns string
|
|
27
|
+
*/
|
|
28
|
+
export type ProcessPath = (path: string, options?: PostCssProcessorOptions) => Promise<string>;
|
|
29
|
+
/**
|
|
30
|
+
* ProcessStringOrBuffer
|
|
31
|
+
* @param contents string | Buffer
|
|
32
|
+
* @param options {@link PostCssProcessorOptions}
|
|
33
|
+
* @returns string
|
|
34
|
+
*/
|
|
35
|
+
export type ProcessStringOrBuffer = (contents: string | Buffer, options?: PostCssProcessorOptions) => Promise<string>;
|
|
36
|
+
export declare function getFileAsBuffer(path: string): Buffer;
|
|
37
|
+
export type ProcessStringOrBufferSync = (contents: string | Buffer, options?: PostCssProcessorOptions) => string;
|
|
38
|
+
/**
|
|
39
|
+
* PostCSS Processor
|
|
40
|
+
* - {@link processPath} : It processes the given path using PostCSS
|
|
41
|
+
* - {@link processStringOrBuffer}: It processes the given string or buffer using PostCSS
|
|
42
|
+
* - {@link processStringOrBufferSync}: It processes the given string or buffer synchronously using PostCSS (requires all plugins to be sync)
|
|
43
|
+
*/
|
|
44
|
+
export declare const PostCssProcessor: {
|
|
45
|
+
processPath: ProcessPath;
|
|
46
|
+
processStringOrBuffer: ProcessStringOrBuffer;
|
|
47
|
+
processStringOrBufferSync: ProcessStringOrBufferSync;
|
|
48
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { Logger } from "@ecopages/logger";
|
|
3
|
+
import postcss from "postcss";
|
|
4
|
+
const appLogger = new Logger("[@ecopages/postcss-processor]");
|
|
5
|
+
function getFileAsBuffer(path) {
|
|
6
|
+
try {
|
|
7
|
+
if (!existsSync(path)) {
|
|
8
|
+
throw new Error(`File: ${path} not found`);
|
|
9
|
+
}
|
|
10
|
+
return readFileSync(path);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
13
|
+
throw new Error(`[ecopages] Error reading file: ${path}, ${errorMessage}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const getPlugins = (options) => {
|
|
17
|
+
if (!options || !options.plugins) return [];
|
|
18
|
+
return Array.isArray(options.plugins) ? options.plugins : Object.values(options.plugins);
|
|
19
|
+
};
|
|
20
|
+
const processPath = async (path, options) => {
|
|
21
|
+
const contents = getFileAsBuffer(path);
|
|
22
|
+
return postcss(getPlugins(options)).process(contents, { from: path }).then((result) => result.css).catch((error) => {
|
|
23
|
+
appLogger.error("Error processing file with PostCssProcessor", error.message);
|
|
24
|
+
return "";
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
const processStringOrBuffer = async (contents, options) => {
|
|
28
|
+
if (!contents) return "";
|
|
29
|
+
return postcss(getPlugins(options)).process(contents, { from: options?.filePath }).then(async (result) => {
|
|
30
|
+
let css = result.css;
|
|
31
|
+
if (options?.transformOutput) {
|
|
32
|
+
css = await options.transformOutput(css);
|
|
33
|
+
}
|
|
34
|
+
return css;
|
|
35
|
+
}).catch((error) => {
|
|
36
|
+
appLogger.error("Error processing string or buffer with PostCssProcessor", error.message);
|
|
37
|
+
return "";
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
const processStringOrBufferSync = (contents, options) => {
|
|
41
|
+
if (!contents) return "";
|
|
42
|
+
try {
|
|
43
|
+
const result = postcss(getPlugins(options)).process(contents, { from: options?.filePath });
|
|
44
|
+
let css = result.css;
|
|
45
|
+
if (options?.transformOutput) {
|
|
46
|
+
const output = options.transformOutput(css);
|
|
47
|
+
if (output instanceof Promise) {
|
|
48
|
+
throw new Error("transformOutput must be synchronous when used with processStringOrBufferSync");
|
|
49
|
+
}
|
|
50
|
+
css = output;
|
|
51
|
+
}
|
|
52
|
+
return css;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error instanceof Error && error.message.includes("transformOutput must be synchronous")) {
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
appLogger.error("Error processing string or buffer with PostCssProcessor", error.message);
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const PostCssProcessor = {
|
|
62
|
+
processPath,
|
|
63
|
+
processStringOrBuffer,
|
|
64
|
+
processStringOrBufferSync
|
|
65
|
+
};
|
|
66
|
+
export {
|
|
67
|
+
PostCssProcessor,
|
|
68
|
+
getFileAsBuffer
|
|
69
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind CSS v3 Preset for PostCSS Processor
|
|
3
|
+
* @module @ecopages/postcss-processor/presets/tailwind-v3
|
|
4
|
+
*
|
|
5
|
+
* Requires: tailwindcss, postcss-import, autoprefixer, cssnano
|
|
6
|
+
* Install: bun add tailwindcss postcss-import autoprefixer cssnano
|
|
7
|
+
*/
|
|
8
|
+
import type { PostCssProcessorPluginConfig } from '../plugin.js';
|
|
9
|
+
/**
|
|
10
|
+
* Creates a PostCSS processor config preset for Tailwind CSS v3.
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Uses classic Tailwind v3 plugin stack
|
|
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
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { postcssProcessorPlugin } from '@ecopages/postcss-processor';
|
|
21
|
+
* import { tailwindV3Preset } from '@ecopages/postcss-processor/presets';
|
|
22
|
+
*
|
|
23
|
+
* // Basic usage
|
|
24
|
+
* postcssProcessorPlugin(tailwindV3Preset())
|
|
25
|
+
*
|
|
26
|
+
* // Extend with additional plugins
|
|
27
|
+
* const preset = tailwindV3Preset();
|
|
28
|
+
* postcssProcessorPlugin({
|
|
29
|
+
* ...preset,
|
|
30
|
+
* plugins: { ...preset.plugins, myPlugin: myPlugin() },
|
|
31
|
+
* })
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function tailwindV3Preset(): PostCssProcessorPluginConfig;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import autoprefixer from "autoprefixer";
|
|
2
|
+
import browserslist from "browserslist";
|
|
3
|
+
import cssnano from "cssnano";
|
|
4
|
+
import postcssImport from "postcss-import";
|
|
5
|
+
import tailwindcss from "tailwindcss";
|
|
6
|
+
import tailwindcssNesting from "tailwindcss/nesting/index.js";
|
|
7
|
+
function tailwindV3Preset() {
|
|
8
|
+
const browserslistConfig = browserslist.loadConfig({ path: process.cwd() });
|
|
9
|
+
const autoprefixerOptions = browserslistConfig ? {} : {
|
|
10
|
+
overrideBrowserslist: [">0.3%", "not ie 11", "not dead", "not op_mini all"]
|
|
11
|
+
};
|
|
12
|
+
const pluginFactories = {
|
|
13
|
+
"postcss-import": () => postcssImport(),
|
|
14
|
+
"tailwindcss/nesting": () => tailwindcssNesting(),
|
|
15
|
+
tailwindcss: () => tailwindcss(),
|
|
16
|
+
autoprefixer: () => autoprefixer(autoprefixerOptions),
|
|
17
|
+
cssnano: () => cssnano()
|
|
18
|
+
};
|
|
19
|
+
const plugins = Object.fromEntries(
|
|
20
|
+
Object.entries(pluginFactories).map(([name, factory]) => [name, factory()])
|
|
21
|
+
);
|
|
22
|
+
return { plugins, pluginFactories };
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
tailwindV3Preset
|
|
26
|
+
};
|
|
@@ -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
|
|
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
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind CSS v4 Preset for PostCSS Processor
|
|
3
|
+
* @module @ecopages/postcss-processor/presets/tailwind-v4
|
|
4
|
+
*
|
|
5
|
+
* Requires: @tailwindcss/postcss, cssnano
|
|
6
|
+
* Install: bun add @tailwindcss/postcss cssnano
|
|
7
|
+
*/
|
|
8
|
+
import type { PostCssProcessorPluginConfig } from '../plugin.js';
|
|
9
|
+
/**
|
|
10
|
+
* Options for Tailwind v4 preset
|
|
11
|
+
*/
|
|
12
|
+
export interface TailwindV4PresetOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Absolute path to the main Tailwind CSS file containing `@import "tailwindcss"`.
|
|
15
|
+
* Used to calculate relative @reference paths for CSS files using @apply.
|
|
16
|
+
*/
|
|
17
|
+
referencePath: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a PostCSS processor config preset for Tailwind CSS v4.
|
|
21
|
+
*
|
|
22
|
+
* Features:
|
|
23
|
+
* - Uses `@tailwindcss/postcss` plugin (v4)
|
|
24
|
+
* - Automatically injects `@reference` headers for `@apply` support
|
|
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
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* import { postcssProcessorPlugin } from '@ecopages/postcss-processor';
|
|
32
|
+
* import { tailwindV4Preset } from '@ecopages/postcss-processor/presets';
|
|
33
|
+
*
|
|
34
|
+
* // Basic usage
|
|
35
|
+
* postcssProcessorPlugin(tailwindV4Preset({
|
|
36
|
+
* referencePath: path.resolve(import.meta.dir, 'src/styles/tailwind.css'),
|
|
37
|
+
* }))
|
|
38
|
+
*
|
|
39
|
+
* // Extend with additional plugins
|
|
40
|
+
* const preset = tailwindV4Preset({ referencePath });
|
|
41
|
+
* postcssProcessorPlugin({
|
|
42
|
+
* ...preset,
|
|
43
|
+
* plugins: { ...preset.plugins, myPlugin: myPlugin() },
|
|
44
|
+
* })
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function tailwindV4Preset(options: TailwindV4PresetOptions): PostCssProcessorPluginConfig;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import tailwindcss from "@tailwindcss/postcss";
|
|
2
|
+
import autoprefixer from "autoprefixer";
|
|
3
|
+
import browserslist from "browserslist";
|
|
4
|
+
import cssnano from "cssnano";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import postcssImport from "postcss-import";
|
|
7
|
+
import postcssNested from "postcss-nested";
|
|
8
|
+
function tailwindV4Preset(options) {
|
|
9
|
+
const { referencePath } = options;
|
|
10
|
+
const browserslistConfig = browserslist.loadConfig({ path: process.cwd() });
|
|
11
|
+
const autoprefixerOptions = browserslistConfig ? {} : {
|
|
12
|
+
overrideBrowserslist: [">0.3%", "not ie 11", "not dead", "not op_mini all"]
|
|
13
|
+
};
|
|
14
|
+
const pluginFactories = {
|
|
15
|
+
"postcss-import": () => postcssImport(),
|
|
16
|
+
"postcss-nested": () => postcssNested(),
|
|
17
|
+
"@tailwindcss/postcss": () => tailwindcss(),
|
|
18
|
+
autoprefixer: () => autoprefixer(autoprefixerOptions),
|
|
19
|
+
cssnano: () => cssnano()
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
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,
|
|
28
|
+
transformInput: async (contents, filePath) => {
|
|
29
|
+
const css = contents instanceof Buffer ? contents.toString("utf-8") : contents;
|
|
30
|
+
const normalizedFilePath = path.resolve(filePath);
|
|
31
|
+
const normalizedReferencePath = path.resolve(referencePath);
|
|
32
|
+
if (normalizedFilePath === normalizedReferencePath) {
|
|
33
|
+
return css;
|
|
34
|
+
}
|
|
35
|
+
if (/^\s*@reference\s+/m.test(css)) {
|
|
36
|
+
return css;
|
|
37
|
+
}
|
|
38
|
+
const relativePath = path.relative(path.dirname(filePath), referencePath);
|
|
39
|
+
if (css.includes(`@import '${relativePath}'`) || css.includes(`@import "${relativePath}"`)) {
|
|
40
|
+
return css;
|
|
41
|
+
}
|
|
42
|
+
const tailwindImportPattern = /^@import\s+['"]tailwindcss(?:\/[^'"]*)?['"];?\s*$/m;
|
|
43
|
+
if (tailwindImportPattern.test(css)) {
|
|
44
|
+
return css.replace(tailwindImportPattern, `@import '${relativePath}';`);
|
|
45
|
+
}
|
|
46
|
+
if (css.includes("@apply")) {
|
|
47
|
+
return `@reference "${relativePath}";
|
|
48
|
+
|
|
49
|
+
${css}`;
|
|
50
|
+
}
|
|
51
|
+
return css;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
tailwindV4Preset
|
|
57
|
+
};
|
|
@@ -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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
|
|
2
|
+
import type { CssTransform } from './css-runtime-contract';
|
|
3
|
+
type CssLoaderOptions = {
|
|
4
|
+
name: string;
|
|
5
|
+
filter: RegExp;
|
|
6
|
+
transform: CssTransform;
|
|
7
|
+
};
|
|
8
|
+
export declare const createCssLoaderPlugin: ({ name, filter, transform }: CssLoaderOptions) => EcoBuildPlugin;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getFileAsBuffer } from "../postcss-processor";
|
|
2
|
+
const createCssLoaderPlugin = ({ name, filter, transform }) => ({
|
|
3
|
+
name,
|
|
4
|
+
setup(build) {
|
|
5
|
+
build.onLoad({ filter }, (args) => {
|
|
6
|
+
const rawFile = getFileAsBuffer(args.path);
|
|
7
|
+
const css = transform({
|
|
8
|
+
contents: rawFile,
|
|
9
|
+
filePath: args.path
|
|
10
|
+
});
|
|
11
|
+
if (css instanceof Promise) {
|
|
12
|
+
return css.then((resolved) => ({
|
|
13
|
+
exports: { default: resolved },
|
|
14
|
+
loader: "object"
|
|
15
|
+
}));
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
exports: {
|
|
19
|
+
default: css
|
|
20
|
+
},
|
|
21
|
+
loader: "object"
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
export {
|
|
27
|
+
createCssLoaderPlugin
|
|
28
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
|
|
2
|
+
import type { CssTransform } from './css-runtime-contract';
|
|
3
|
+
type BunCssLoaderOptions = {
|
|
4
|
+
name: string;
|
|
5
|
+
filter: RegExp;
|
|
6
|
+
transform: CssTransform;
|
|
7
|
+
};
|
|
8
|
+
export declare const createBunCssLoaderPlugin: ({ name, filter, transform }: BunCssLoaderOptions) => EcoBuildPlugin;
|
|
9
|
+
export {};
|