@ecopages/postcss-processor 0.2.0-alpha.1 → 0.2.0-alpha.11
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 +20 -0
- package/README.md +31 -67
- package/package.json +38 -32
- package/src/index.d.ts +2 -0
- package/src/index.js +2 -0
- package/src/plugin.d.ts +154 -0
- package/src/plugin.js +469 -0
- package/src/postcss-processor.d.ts +48 -0
- package/src/postcss-processor.js +69 -0
- package/src/presets/{index.ts → index.d.ts} +2 -3
- 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-v4.d.ts +47 -0
- package/src/presets/tailwind-v4.js +57 -0
- 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.ts → css-runtime-contract.d.ts} +2 -3
- package/src/runtime/css-runtime-contract.js +0 -0
- package/src/index.ts +0 -2
- package/src/plugin.ts +0 -354
- package/src/postcss-processor.ts +0 -157
- package/src/presets/tailwind-v3.ts +0 -61
- package/src/presets/tailwind-v4.ts +0 -113
- package/src/runtime/css-loader-plugin.ts +0 -37
- package/src/runtime/css-loader.bun.ts +0 -30
- 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.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileSystem } from "@ecopages/file-system";
|
|
3
|
+
import { Processor } from "@ecopages/core/plugins/processor";
|
|
4
|
+
import { Logger } from "@ecopages/logger";
|
|
5
|
+
import { PostCssProcessor } from "./postcss-processor.js";
|
|
6
|
+
import { createCssLoaderPlugin } from "./runtime/css-loader-plugin.js";
|
|
7
|
+
const logger = new Logger("[@ecopages/postcss-processor]", {
|
|
8
|
+
debug: process.env.ECOPAGES_LOGGER_DEBUG === "true"
|
|
9
|
+
});
|
|
10
|
+
class PostCssProcessorPlugin extends Processor {
|
|
11
|
+
static DEFAULT_OPTIONS = {
|
|
12
|
+
filter: /\.css$/
|
|
13
|
+
};
|
|
14
|
+
buildContributionsPrepared = false;
|
|
15
|
+
postcssPlugins = [];
|
|
16
|
+
pluginFactories;
|
|
17
|
+
runtimeCssCache = /* @__PURE__ */ new Map();
|
|
18
|
+
trackedCssFiles = /* @__PURE__ */ new Set();
|
|
19
|
+
watchQueue = Promise.resolve();
|
|
20
|
+
/**
|
|
21
|
+
* Maps an imported CSS file path → set of tracked CSS entry files that import it.
|
|
22
|
+
* Used to resolve which parent entry files need re-processing when a dependency changes.
|
|
23
|
+
*/
|
|
24
|
+
cssDependencyMap = /* @__PURE__ */ new Map();
|
|
25
|
+
getCssFilter() {
|
|
26
|
+
return this.options?.filter ?? PostCssProcessorPlugin.DEFAULT_OPTIONS.filter;
|
|
27
|
+
}
|
|
28
|
+
resolveProcessedCssPath(filePath) {
|
|
29
|
+
if (!this.context) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const relativePath = path.relative(this.context.srcDir, filePath);
|
|
33
|
+
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return path.join(this.context.distDir, "assets", relativePath);
|
|
37
|
+
}
|
|
38
|
+
readProcessedCssFromDist(filePath) {
|
|
39
|
+
const outputPath = this.resolveProcessedCssPath(filePath);
|
|
40
|
+
if (!outputPath || !fileSystem.exists(outputPath)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return fileSystem.readFileAsBuffer(outputPath).toString("utf-8");
|
|
44
|
+
}
|
|
45
|
+
async persistProcessedCss(filePath, css) {
|
|
46
|
+
const outputPath = this.resolveProcessedCssPath(filePath);
|
|
47
|
+
if (!outputPath) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
fileSystem.ensureDir(path.dirname(outputPath));
|
|
51
|
+
fileSystem.write(outputPath, css);
|
|
52
|
+
}
|
|
53
|
+
async prewarmRuntimeCssCache() {
|
|
54
|
+
if (!this.context) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const sourceFiles = await fileSystem.glob(["**/*.{css,scss,sass,less}"], {
|
|
58
|
+
cwd: this.context.srcDir
|
|
59
|
+
});
|
|
60
|
+
for (const relativePath of sourceFiles) {
|
|
61
|
+
const filePath = path.join(this.context.srcDir, relativePath);
|
|
62
|
+
if (!this.matchesFileFilter(filePath)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
this.trackedCssFiles.add(filePath);
|
|
66
|
+
const rawContents = await fileSystem.readFile(filePath);
|
|
67
|
+
let transformedInput = rawContents;
|
|
68
|
+
if (this.options?.transformInput) {
|
|
69
|
+
transformedInput = await this.options.transformInput(rawContents, filePath);
|
|
70
|
+
}
|
|
71
|
+
const processed = await this.process(transformedInput, filePath);
|
|
72
|
+
this.runtimeCssCache.set(filePath, processed);
|
|
73
|
+
await this.persistProcessedCss(filePath, processed);
|
|
74
|
+
}
|
|
75
|
+
this.buildCssDependencyMap();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Regex to match CSS @import statements and extract the path.
|
|
79
|
+
* Handles: @import './foo.css'; @import "./foo.css"; @import url('./foo.css');
|
|
80
|
+
*/
|
|
81
|
+
static CSS_IMPORT_REGEX = /@import\s+(?:url\(\s*)?['"]([^'"]+\.css)['"](?:\s*\))?\s*;/gm;
|
|
82
|
+
/**
|
|
83
|
+
* Builds the CSS dependency map by scanning tracked CSS files for @import directives.
|
|
84
|
+
* Maps each imported file to the set of tracked entry files that import it (directly or transitively).
|
|
85
|
+
*/
|
|
86
|
+
buildCssDependencyMap() {
|
|
87
|
+
this.cssDependencyMap.clear();
|
|
88
|
+
for (const entryFile of this.trackedCssFiles) {
|
|
89
|
+
if (!fileSystem.exists(entryFile)) continue;
|
|
90
|
+
const rawContents = fileSystem.readFileAsBuffer(entryFile).toString("utf-8");
|
|
91
|
+
const imports = this.extractCssImports(rawContents, entryFile);
|
|
92
|
+
for (const importedFile of imports) {
|
|
93
|
+
if (!this.cssDependencyMap.has(importedFile)) {
|
|
94
|
+
this.cssDependencyMap.set(importedFile, /* @__PURE__ */ new Set());
|
|
95
|
+
}
|
|
96
|
+
this.cssDependencyMap.get(importedFile).add(entryFile);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Extracts resolved absolute paths of CSS files imported via @import in the given CSS content.
|
|
102
|
+
* Recursively follows imports to capture transitive dependencies.
|
|
103
|
+
* It skips bare module imports like @import 'tailwindcss'.
|
|
104
|
+
* It recursively follows imports to capture transitive dependencies.
|
|
105
|
+
*/
|
|
106
|
+
extractCssImports(cssContent, fromFile, visited = /* @__PURE__ */ new Set()) {
|
|
107
|
+
const dir = path.dirname(fromFile);
|
|
108
|
+
const imports = [];
|
|
109
|
+
let match;
|
|
110
|
+
const regex = new RegExp(PostCssProcessorPlugin.CSS_IMPORT_REGEX.source, "gm");
|
|
111
|
+
while ((match = regex.exec(cssContent)) !== null) {
|
|
112
|
+
const importPath = match[1];
|
|
113
|
+
if (!importPath.startsWith(".") && !importPath.startsWith("/")) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const resolvedPath = path.resolve(dir, importPath);
|
|
117
|
+
if (visited.has(resolvedPath)) continue;
|
|
118
|
+
visited.add(resolvedPath);
|
|
119
|
+
imports.push(resolvedPath);
|
|
120
|
+
if (fileSystem.exists(resolvedPath)) {
|
|
121
|
+
const nestedContent = fileSystem.readFileAsBuffer(resolvedPath).toString("utf-8");
|
|
122
|
+
const nestedImports = this.extractCssImports(nestedContent, resolvedPath, visited);
|
|
123
|
+
imports.push(...nestedImports);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return imports;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Resolves a changed CSS file to its parent entry file(s) if it is an @import dependency.
|
|
130
|
+
* Returns an empty array if the file is not an import dependency (i.e., it's an entry file itself).
|
|
131
|
+
*/
|
|
132
|
+
resolveEntryFiles(filePath) {
|
|
133
|
+
const entries = this.cssDependencyMap.get(filePath);
|
|
134
|
+
if (!entries || entries.size === 0) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
return Array.from(entries);
|
|
138
|
+
}
|
|
139
|
+
transformCssSync(input) {
|
|
140
|
+
const cached = this.runtimeCssCache.get(input.filePath);
|
|
141
|
+
if (cached) {
|
|
142
|
+
return cached;
|
|
143
|
+
}
|
|
144
|
+
const persisted = this.readProcessedCssFromDist(input.filePath);
|
|
145
|
+
if (persisted) {
|
|
146
|
+
this.runtimeCssCache.set(input.filePath, persisted);
|
|
147
|
+
return persisted;
|
|
148
|
+
}
|
|
149
|
+
const { contents } = input;
|
|
150
|
+
return typeof contents === "string" ? contents : contents.toString("utf-8");
|
|
151
|
+
}
|
|
152
|
+
async transformCssAsync(input) {
|
|
153
|
+
const { contents, filePath } = input;
|
|
154
|
+
let transformed = typeof contents === "string" ? contents : contents.toString("utf-8");
|
|
155
|
+
if (this.options?.transformInput) {
|
|
156
|
+
const result = this.options.transformInput(contents, filePath);
|
|
157
|
+
transformed = typeof result.then === "function" ? await result : result;
|
|
158
|
+
}
|
|
159
|
+
const processed = await this.process(transformed, filePath);
|
|
160
|
+
this.runtimeCssCache.set(filePath, processed);
|
|
161
|
+
await this.persistProcessedCss(filePath, processed);
|
|
162
|
+
return processed;
|
|
163
|
+
}
|
|
164
|
+
matchesFileFilter(filepath) {
|
|
165
|
+
const filter = this.options?.filter ?? PostCssProcessorPlugin.DEFAULT_OPTIONS.filter;
|
|
166
|
+
return filter.test(filepath);
|
|
167
|
+
}
|
|
168
|
+
materializePluginFactories(pluginFactories) {
|
|
169
|
+
return Object.values(pluginFactories).map((factory) => factory());
|
|
170
|
+
}
|
|
171
|
+
refreshConfiguredPlugins() {
|
|
172
|
+
if (!this.pluginFactories) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.postcssPlugins = this.materializePluginFactories(this.pluginFactories);
|
|
176
|
+
}
|
|
177
|
+
enqueueWatchTask(task) {
|
|
178
|
+
const queuedTask = this.watchQueue.then(task, task);
|
|
179
|
+
this.watchQueue = queuedTask.catch(() => void 0);
|
|
180
|
+
return queuedTask;
|
|
181
|
+
}
|
|
182
|
+
getTrackedCssFiles() {
|
|
183
|
+
return Array.from(this.trackedCssFiles).filter(
|
|
184
|
+
(filePath) => this.matchesFileFilter(filePath) && fileSystem.exists(filePath)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
async handleDependencyChange(bridge) {
|
|
188
|
+
if (!this.context) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const cssFiles = this.getTrackedCssFiles();
|
|
192
|
+
if (cssFiles.length === 0) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
this.refreshConfiguredPlugins();
|
|
196
|
+
for (const cssFilePath of cssFiles) {
|
|
197
|
+
await this.handleCssChange(cssFilePath, bridge, false);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
constructor(config = {
|
|
201
|
+
options: PostCssProcessorPlugin.DEFAULT_OPTIONS
|
|
202
|
+
}) {
|
|
203
|
+
super({
|
|
204
|
+
name: "ecopages-postcss-processor",
|
|
205
|
+
description: "A Processor for transforming CSS files using PostCSS.",
|
|
206
|
+
capabilities: [
|
|
207
|
+
{
|
|
208
|
+
kind: "stylesheet",
|
|
209
|
+
extensions: ["*.{css,scss,sass,less}"]
|
|
210
|
+
}
|
|
211
|
+
],
|
|
212
|
+
watch: {
|
|
213
|
+
paths: [],
|
|
214
|
+
extensions: [
|
|
215
|
+
".css",
|
|
216
|
+
".scss",
|
|
217
|
+
".sass",
|
|
218
|
+
".less",
|
|
219
|
+
".tsx",
|
|
220
|
+
".ts",
|
|
221
|
+
".jsx",
|
|
222
|
+
".js",
|
|
223
|
+
".mdx",
|
|
224
|
+
".html",
|
|
225
|
+
".svelte",
|
|
226
|
+
".vue"
|
|
227
|
+
],
|
|
228
|
+
onChange: async ({ path: path2, bridge }) => {
|
|
229
|
+
await this.enqueueWatchTask(async () => {
|
|
230
|
+
if (this.matchesFileFilter(path2)) {
|
|
231
|
+
await this.handleCssChange(path2, bridge);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
await this.handleDependencyChange(bridge);
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
onCreate: async ({ path: path2, bridge }) => {
|
|
238
|
+
await this.enqueueWatchTask(async () => {
|
|
239
|
+
if (this.matchesFileFilter(path2)) {
|
|
240
|
+
this.trackedCssFiles.add(path2);
|
|
241
|
+
this.buildCssDependencyMap();
|
|
242
|
+
await this.handleCssChange(path2, bridge);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
await this.handleDependencyChange(bridge);
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
onDelete: async ({ path: path2, bridge }) => {
|
|
249
|
+
await this.enqueueWatchTask(async () => {
|
|
250
|
+
if (this.matchesFileFilter(path2)) {
|
|
251
|
+
this.runtimeCssCache.delete(path2);
|
|
252
|
+
this.trackedCssFiles.delete(path2);
|
|
253
|
+
this.cssDependencyMap.delete(path2);
|
|
254
|
+
this.buildCssDependencyMap();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
await this.handleDependencyChange(bridge);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
...config
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Handles CSS file changes during development.
|
|
266
|
+
* If the file is an @import dependency, re-processes the parent entry file(s) instead.
|
|
267
|
+
* Broadcasts a css-update event for hot reloading.
|
|
268
|
+
*/
|
|
269
|
+
async handleCssChange(filePath, bridge, refreshPlugins = true) {
|
|
270
|
+
if (!this.context) return;
|
|
271
|
+
if (!fileSystem.exists(filePath)) return;
|
|
272
|
+
const entryFiles = this.resolveEntryFiles(filePath);
|
|
273
|
+
if (entryFiles.length > 0) {
|
|
274
|
+
logger.debug(`CSS dependency changed: ${filePath}, re-processing ${entryFiles.length} parent(s)`);
|
|
275
|
+
for (const entryFile of entryFiles) {
|
|
276
|
+
this.runtimeCssCache.delete(entryFile);
|
|
277
|
+
await this.processAndBroadcast(entryFile, bridge, refreshPlugins);
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
await this.processAndBroadcast(filePath, bridge, refreshPlugins);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Processes a CSS file and broadcasts a css-update event.
|
|
285
|
+
* Skips broadcast if the processed output hasn't changed.
|
|
286
|
+
*/
|
|
287
|
+
async processAndBroadcast(filePath, bridge, refreshPlugins = true) {
|
|
288
|
+
if (!this.context) return;
|
|
289
|
+
if (!fileSystem.exists(filePath)) return;
|
|
290
|
+
try {
|
|
291
|
+
this.trackedCssFiles.add(filePath);
|
|
292
|
+
if (refreshPlugins) {
|
|
293
|
+
this.refreshConfiguredPlugins();
|
|
294
|
+
}
|
|
295
|
+
let content = await fileSystem.readFile(filePath);
|
|
296
|
+
if (this.options?.transformInput) {
|
|
297
|
+
content = await this.options.transformInput(content, filePath);
|
|
298
|
+
}
|
|
299
|
+
const processed = await this.process(content, filePath);
|
|
300
|
+
const cached = this.runtimeCssCache.get(filePath);
|
|
301
|
+
if (cached === processed) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this.runtimeCssCache.set(filePath, processed);
|
|
305
|
+
await this.persistProcessedCss(filePath, processed);
|
|
306
|
+
bridge.cssUpdate(filePath);
|
|
307
|
+
logger.debug(`Processed CSS: ${filePath}`);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
310
|
+
logger.error(`Failed to process CSS: ${filePath}`, errorMessage);
|
|
311
|
+
bridge.error(errorMessage);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
get buildPlugins() {
|
|
315
|
+
return [
|
|
316
|
+
createCssLoaderPlugin({
|
|
317
|
+
name: "postcss-processor-build-loader",
|
|
318
|
+
filter: this.getCssFilter(),
|
|
319
|
+
transform: this.transformCssAsync.bind(this)
|
|
320
|
+
})
|
|
321
|
+
];
|
|
322
|
+
}
|
|
323
|
+
get plugins() {
|
|
324
|
+
return [
|
|
325
|
+
createCssLoaderPlugin({
|
|
326
|
+
name: "postcss-processor-runtime-loader",
|
|
327
|
+
filter: this.getCssFilter(),
|
|
328
|
+
transform: this.transformCssSync.bind(this)
|
|
329
|
+
})
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Resolves the configured PostCSS plugin list before config build seals the
|
|
334
|
+
* app manifest.
|
|
335
|
+
*
|
|
336
|
+
* @remarks
|
|
337
|
+
* Runtime setup reuses this prepared list and only performs cache prewarming.
|
|
338
|
+
*/
|
|
339
|
+
async prepareBuildContributions() {
|
|
340
|
+
if (this.buildContributionsPrepared) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
await this.collectPostcssPlugins();
|
|
344
|
+
this.buildContributionsPrepared = true;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Prepares build contributions if not already done and prewarms the runtime CSS cache.
|
|
348
|
+
*/
|
|
349
|
+
async setup() {
|
|
350
|
+
await this.prepareBuildContributions();
|
|
351
|
+
await this.prewarmRuntimeCssCache();
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Get the PostCSS plugins from the options or a config file.
|
|
355
|
+
* Searches for postcss.config.{js,cjs,mjs,ts} in the root directory.
|
|
356
|
+
*/
|
|
357
|
+
async collectPostcssPlugins() {
|
|
358
|
+
if (!this.context) {
|
|
359
|
+
throw new Error("Context must be set");
|
|
360
|
+
}
|
|
361
|
+
const configExtensions = ["js", "cjs", "mjs", "ts"];
|
|
362
|
+
let foundConfigPath;
|
|
363
|
+
let loadedPlugins;
|
|
364
|
+
let loadedPluginFactories;
|
|
365
|
+
for (const ext of configExtensions) {
|
|
366
|
+
const configPath = path.join(this.context.rootDir, `postcss.config.${ext}`);
|
|
367
|
+
if (fileSystem.exists(configPath)) {
|
|
368
|
+
foundConfigPath = configPath;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (foundConfigPath) {
|
|
373
|
+
try {
|
|
374
|
+
logger.debug(`Loading PostCSS config from: ${foundConfigPath}`);
|
|
375
|
+
const postcssConfigModule = await import(
|
|
376
|
+
/* @vite-ignore */
|
|
377
|
+
foundConfigPath
|
|
378
|
+
);
|
|
379
|
+
const postcssConfig = postcssConfigModule.default || postcssConfigModule;
|
|
380
|
+
if (postcssConfig && typeof postcssConfig.pluginFactories === "object" && postcssConfig.pluginFactories !== null) {
|
|
381
|
+
loadedPluginFactories = postcssConfig.pluginFactories;
|
|
382
|
+
}
|
|
383
|
+
if (postcssConfig && typeof postcssConfig.plugins === "object" && postcssConfig.plugins !== null) {
|
|
384
|
+
if (Array.isArray(postcssConfig.plugins)) {
|
|
385
|
+
loadedPlugins = postcssConfig.plugins;
|
|
386
|
+
} else {
|
|
387
|
+
loadedPlugins = Object.values(postcssConfig.plugins);
|
|
388
|
+
}
|
|
389
|
+
logger.debug(`Successfully loaded ${loadedPlugins?.length ?? 0} plugins from config file.`);
|
|
390
|
+
} else {
|
|
391
|
+
logger.warn(
|
|
392
|
+
`PostCSS config file found (${foundConfigPath}), but no valid 'plugins' export detected.`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
} catch (error) {
|
|
396
|
+
logger.error(`Error loading PostCSS config from ${foundConfigPath}: ${error.message}`, error);
|
|
397
|
+
loadedPlugins = void 0;
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
logger.debug("No PostCSS config file found in root directory.");
|
|
401
|
+
}
|
|
402
|
+
if (loadedPluginFactories) {
|
|
403
|
+
this.pluginFactories = loadedPluginFactories;
|
|
404
|
+
this.postcssPlugins = this.materializePluginFactories(loadedPluginFactories);
|
|
405
|
+
} else if (loadedPlugins) {
|
|
406
|
+
this.pluginFactories = void 0;
|
|
407
|
+
this.postcssPlugins = loadedPlugins;
|
|
408
|
+
} else if (this.options?.pluginFactories || this.options?.plugins) {
|
|
409
|
+
this.pluginFactories = this.options?.pluginFactories;
|
|
410
|
+
if (this.options?.plugins) {
|
|
411
|
+
logger.debug("Using PostCSS plugins provided in processor options.");
|
|
412
|
+
this.postcssPlugins = Object.values(this.options.plugins);
|
|
413
|
+
} else if (this.options?.pluginFactories) {
|
|
414
|
+
logger.debug("Using PostCSS plugin factories provided in processor options.");
|
|
415
|
+
this.postcssPlugins = this.materializePluginFactories(this.options.pluginFactories);
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
logger.warn(
|
|
419
|
+
"No PostCSS plugins configured. Use a preset like tailwindV3Preset() or tailwindV4Preset(), provide plugins via options, or create a postcss.config file."
|
|
420
|
+
);
|
|
421
|
+
this.pluginFactories = void 0;
|
|
422
|
+
this.postcssPlugins = [];
|
|
423
|
+
}
|
|
424
|
+
if (!this.postcssPlugins || this.postcssPlugins.length === 0) {
|
|
425
|
+
logger.warn("No PostCSS plugins configured or loaded. CSS processing might be minimal.");
|
|
426
|
+
this.postcssPlugins = [];
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Process CSS content
|
|
431
|
+
* @param fileAsString CSS content as string
|
|
432
|
+
* @param filePath Optional file path for resolving relative imports
|
|
433
|
+
* @returns Processed CSS
|
|
434
|
+
*/
|
|
435
|
+
async process(fileAsString, filePath) {
|
|
436
|
+
const input = this.options?.transformInput && filePath ? await this.options.transformInput(fileAsString, filePath) : fileAsString;
|
|
437
|
+
return await PostCssProcessor.processStringOrBuffer(input, {
|
|
438
|
+
filePath,
|
|
439
|
+
plugins: this.postcssPlugins,
|
|
440
|
+
transformOutput: this.options?.transformOutput
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
processSync(fileAsString, filePath) {
|
|
444
|
+
const input = this.options?.transformInput && filePath ? this.options.transformInput(fileAsString, filePath) : fileAsString;
|
|
445
|
+
if (input instanceof Promise) {
|
|
446
|
+
throw new Error("transformInput must be synchronous when used with processSync");
|
|
447
|
+
}
|
|
448
|
+
return PostCssProcessor.processStringOrBufferSync(input, {
|
|
449
|
+
filePath,
|
|
450
|
+
plugins: this.postcssPlugins,
|
|
451
|
+
transformOutput: this.options?.transformOutput
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Teardown the PostCSS processor.
|
|
456
|
+
*/
|
|
457
|
+
async teardown() {
|
|
458
|
+
logger.debug("Tearing down PostCSS processor");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const postcssProcessorPlugin = (config) => {
|
|
462
|
+
return new PostCssProcessorPlugin({
|
|
463
|
+
options: config
|
|
464
|
+
});
|
|
465
|
+
};
|
|
466
|
+
export {
|
|
467
|
+
PostCssProcessorPlugin,
|
|
468
|
+
postcssProcessorPlugin
|
|
469
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -2,6 +2,5 @@
|
|
|
2
2
|
* PostCSS Processor Presets
|
|
3
3
|
* @module @ecopages/postcss-processor/presets
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
export {
|
|
7
|
-
export { tailwindV4Preset, type TailwindV4PresetOptions } from './tailwind-v4';
|
|
5
|
+
export { tailwindV3Preset } from './tailwind-v3.js';
|
|
6
|
+
export { tailwindV4Preset, type TailwindV4PresetOptions } from './tailwind-v4.js';
|
|
@@ -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
|
+
};
|