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

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 ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@ecopages/postcss-processor` are documented here.
4
+
5
+ > **Note:** Changelog tracking begins at version `0.2.0`. Changes prior to this release are not recorded here but are available in the git history.
6
+
7
+ ## [UNRELEASED] — TBD
8
+
9
+ ### Features
10
+
11
+ - **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`).
12
+ - **`PostcssProcessor` class** — New `postcss-processor.ts` exposes a programmatic API for the processor separate from the plugin DSL.
13
+
14
+ ### Refactoring
15
+
16
+ - `plugin.ts` significantly overhauled to integrate with the new build adapter and support build dependency graph registration (`e7653c9b`).
17
+ - Test suite updated for esbuild adapter and Node runtime compatibility (`31a44458`).
18
+ - Removed unused `@types/postcss` dev dependency.
19
+
20
+ ### Tests
21
+
22
+ - Updated `plugin.test.ts`, `postcss-processor.test.ts`, and `presets.test.ts` for new plugin contract.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/postcss-processor",
3
- "version": "0.2.0-alpha.1",
3
+ "version": "0.2.0-alpha.2",
4
4
  "description": "Postcss processor, transform string or postcss file to css",
5
5
  "keywords": [
6
6
  "postcss",
@@ -8,24 +8,17 @@
8
8
  "css"
9
9
  ],
10
10
  "license": "MIT",
11
- "main": "./src/postcss-processor.ts",
11
+ "main": "./src/postcss-processor.js",
12
12
  "type": "module",
13
- "types": "./src/postcss-processor.ts",
14
- "files": [
15
- "src"
16
- ],
13
+ "types": "./src/postcss-processor.d.ts",
17
14
  "repository": {
18
15
  "type": "git",
19
16
  "url": "https://github.com/ecopages/ecopages.git",
20
17
  "directory": "packages/processors/postcss-processor"
21
18
  },
22
- "scripts": {
23
- "typecheck": "tsc --noEmit",
24
- "release:jsr": "bunx jsr publish"
25
- },
26
19
  "dependencies": {
27
- "@ecopages/core": "workspace:*",
28
- "@ecopages/file-system": "workspace:*",
20
+ "@ecopages/core": "0.2.0-alpha.2",
21
+ "@ecopages/file-system": "0.2.0-alpha.2",
29
22
  "@ecopages/logger": "latest",
30
23
  "autoprefixer": "^10.4.0",
31
24
  "browserslist": "^4.28.1",
@@ -46,37 +39,50 @@
46
39
  "optional": true
47
40
  }
48
41
  },
49
- "devDependencies": {
50
- "@tailwindcss/postcss": "^4.1.18",
51
- "@types/bun": "latest",
52
- "@types/postcss-import": "^14",
53
- "postcss-simple-vars": "^7.0.1",
54
- "tailwindcss": "^3.4.19"
55
- },
56
42
  "exports": {
57
43
  ".": {
58
- "default": "./src/index.ts",
59
- "types": "./src/index.ts"
44
+ "default": "./src/index.js",
45
+ "types": "./src/index.d.ts"
60
46
  },
61
47
  "./postcss-processor": {
62
- "default": "./src/postcss-processor.ts",
63
- "types": "./src/postcss-processor.ts"
48
+ "default": "./src/postcss-processor.js",
49
+ "types": "./src/postcss-processor.d.ts"
64
50
  },
65
51
  "./plugin": {
66
- "default": "./src/plugin.ts",
67
- "types": "./src/plugin.ts"
52
+ "default": "./src/plugin.js",
53
+ "types": "./src/plugin.d.ts"
68
54
  },
69
55
  "./presets": {
70
- "default": "./src/presets/index.ts",
71
- "types": "./src/presets/index.ts"
56
+ "default": "./src/presets/index.js",
57
+ "types": "./src/presets/index.d.ts"
72
58
  },
73
59
  "./presets/tailwind-v3": {
74
- "default": "./src/presets/tailwind-v3.ts",
75
- "types": "./src/presets/tailwind-v3.ts"
60
+ "default": "./src/presets/tailwind-v3.js",
61
+ "types": "./src/presets/tailwind-v3.d.ts"
76
62
  },
77
63
  "./presets/tailwind-v4": {
78
- "default": "./src/presets/tailwind-v4.ts",
79
- "types": "./src/presets/tailwind-v4.ts"
64
+ "default": "./src/presets/tailwind-v4.js",
65
+ "types": "./src/presets/tailwind-v4.d.ts"
66
+ },
67
+ "./postcss-processor.ts": {
68
+ "default": "./src/postcss-processor.js",
69
+ "types": "./src/postcss-processor.d.ts"
70
+ },
71
+ "./plugin.ts": {
72
+ "default": "./src/plugin.js",
73
+ "types": "./src/plugin.d.ts"
74
+ },
75
+ "./presets.ts": {
76
+ "default": "./src/presets/index.js",
77
+ "types": "./src/presets/index.d.ts"
78
+ },
79
+ "./presets/tailwind-v3.ts": {
80
+ "default": "./src/presets/tailwind-v3.js",
81
+ "types": "./src/presets/tailwind-v3.d.ts"
82
+ },
83
+ "./presets/tailwind-v4.ts": {
84
+ "default": "./src/presets/tailwind-v4.js",
85
+ "types": "./src/presets/tailwind-v4.d.ts"
80
86
  }
81
87
  }
82
- }
88
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './plugin';
2
+ export * from './postcss-processor';
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./plugin";
2
+ export * from "./postcss-processor";
@@ -0,0 +1,88 @@
1
+ /**
2
+ * PostCssProcessorPlugin
3
+ * @module @ecopages/postcss-processor
4
+ */
5
+ import { Processor, type ProcessorConfig } from '@ecopages/core/plugins/processor';
6
+ import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
7
+ import type postcss from 'postcss';
8
+ /**
9
+ * Record of PostCSS plugins keyed by name
10
+ */
11
+ export type PluginsRecord = Record<string, postcss.AcceptedPlugin>;
12
+ /**
13
+ * Configuration for the PostCSS processor
14
+ */
15
+ export interface PostCssProcessorPluginConfig {
16
+ /**
17
+ * Regex filter to match files to process
18
+ */
19
+ filter?: RegExp;
20
+ /**
21
+ * Function to transform the contents of the file.
22
+ * It can be handy to add a custom header or footer to the file.
23
+ * Useful for injecting Tailwind v4 `@reference` directives.
24
+ * @param contents The contents of the file
25
+ * @param filePath The absolute path to the CSS file being processed
26
+ * @returns The transformed contents
27
+ */
28
+ transformInput?: (contents: string | Buffer, filePath: string) => string | Promise<string>;
29
+ /**
30
+ * Function to transform the output CSS after PostCSS processing.
31
+ * It can be handy to add a custom header or footer to the processed CSS.
32
+ * @param css The processed CSS
33
+ * @returns The transformed CSS
34
+ */
35
+ transformOutput?: (css: string) => Promise<string> | string;
36
+ /**
37
+ * Custom PostCSS plugins to use instead of the default ones
38
+ * @default undefined (uses default plugins)
39
+ */
40
+ plugins?: PluginsRecord;
41
+ }
42
+ /**
43
+ * PostCssProcessorPlugin
44
+ * A Processor for transforming CSS files.
45
+ */
46
+ export declare class PostCssProcessorPlugin extends Processor<PostCssProcessorPluginConfig> {
47
+ static DEFAULT_OPTIONS: Required<Pick<PostCssProcessorPluginConfig, 'filter'>>;
48
+ private postcssPlugins;
49
+ private readonly runtimeCssCache;
50
+ private getCssFilter;
51
+ private resolveProcessedCssPath;
52
+ private readProcessedCssFromDist;
53
+ private persistProcessedCss;
54
+ private prewarmRuntimeCssCache;
55
+ private transformCssSync;
56
+ private transformCssAsync;
57
+ matchesFileFilter(filepath: string): boolean;
58
+ constructor(config?: Omit<ProcessorConfig<PostCssProcessorPluginConfig>, 'name' | 'description'>);
59
+ /**
60
+ * Handles CSS file changes during development.
61
+ * Processes the file and broadcasts a css-update event for hot reloading.
62
+ */
63
+ private handleCssChange;
64
+ get buildPlugins(): EcoBuildPlugin[];
65
+ get plugins(): EcoBuildPlugin[];
66
+ /**
67
+ * Setup the PostCSS processor.
68
+ */
69
+ setup(): Promise<void>;
70
+ /**
71
+ * Get the PostCSS plugins from the options or a config file.
72
+ * Searches for postcss.config.{js,cjs,mjs,ts} in the root directory.
73
+ */
74
+ private collectPostcssPlugins;
75
+ /**
76
+ * Process CSS content
77
+ * @param fileAsString CSS content as string
78
+ * @param filePath Optional file path for resolving relative imports
79
+ * @returns Processed CSS
80
+ */
81
+ process(fileAsString: string, filePath?: string): Promise<string>;
82
+ processSync(fileAsString: string, filePath?: string): string;
83
+ /**
84
+ * Teardown the PostCSS processor.
85
+ */
86
+ teardown(): Promise<void>;
87
+ }
88
+ export declare const postcssProcessorPlugin: (config?: PostCssProcessorPluginConfig) => PostCssProcessorPlugin;
package/src/plugin.js ADDED
@@ -0,0 +1,257 @@
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";
6
+ import { createCssLoaderPlugin } from "./runtime/css-loader-plugin";
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
+ postcssPlugins = [];
15
+ runtimeCssCache = /* @__PURE__ */ new Map();
16
+ getCssFilter() {
17
+ return this.options?.filter ?? PostCssProcessorPlugin.DEFAULT_OPTIONS.filter;
18
+ }
19
+ resolveProcessedCssPath(filePath) {
20
+ if (!this.context) {
21
+ return null;
22
+ }
23
+ const relativePath = path.relative(this.context.srcDir, filePath);
24
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
25
+ return null;
26
+ }
27
+ return path.join(this.context.distDir, "assets", relativePath);
28
+ }
29
+ readProcessedCssFromDist(filePath) {
30
+ const outputPath = this.resolveProcessedCssPath(filePath);
31
+ if (!outputPath || !fileSystem.exists(outputPath)) {
32
+ return null;
33
+ }
34
+ return fileSystem.readFileAsBuffer(outputPath).toString("utf-8");
35
+ }
36
+ async persistProcessedCss(filePath, css) {
37
+ const outputPath = this.resolveProcessedCssPath(filePath);
38
+ if (!outputPath) {
39
+ return;
40
+ }
41
+ fileSystem.ensureDir(path.dirname(outputPath));
42
+ fileSystem.write(outputPath, css);
43
+ }
44
+ async prewarmRuntimeCssCache() {
45
+ if (!this.context) {
46
+ return;
47
+ }
48
+ const sourceFiles = await fileSystem.glob(["**/*.{css,scss,sass,less}"], {
49
+ cwd: this.context.srcDir
50
+ });
51
+ for (const relativePath of sourceFiles) {
52
+ const filePath = path.join(this.context.srcDir, relativePath);
53
+ if (!this.matchesFileFilter(filePath)) {
54
+ continue;
55
+ }
56
+ const rawContents = await fileSystem.readFile(filePath);
57
+ let transformedInput = rawContents;
58
+ if (this.options?.transformInput) {
59
+ transformedInput = await this.options.transformInput(rawContents, filePath);
60
+ }
61
+ const processed = await this.process(transformedInput, filePath);
62
+ this.runtimeCssCache.set(filePath, processed);
63
+ await this.persistProcessedCss(filePath, processed);
64
+ }
65
+ }
66
+ transformCssSync(input) {
67
+ const cached = this.runtimeCssCache.get(input.filePath);
68
+ if (cached) {
69
+ return cached;
70
+ }
71
+ const persisted = this.readProcessedCssFromDist(input.filePath);
72
+ if (persisted) {
73
+ this.runtimeCssCache.set(input.filePath, persisted);
74
+ return persisted;
75
+ }
76
+ const { contents } = input;
77
+ return typeof contents === "string" ? contents : contents.toString("utf-8");
78
+ }
79
+ async transformCssAsync(input) {
80
+ const { contents, filePath } = input;
81
+ let transformed = typeof contents === "string" ? contents : contents.toString("utf-8");
82
+ if (this.options?.transformInput) {
83
+ const result = this.options.transformInput(contents, filePath);
84
+ transformed = typeof result.then === "function" ? await result : result;
85
+ }
86
+ const processed = await this.process(transformed, filePath);
87
+ this.runtimeCssCache.set(filePath, processed);
88
+ await this.persistProcessedCss(filePath, processed);
89
+ return processed;
90
+ }
91
+ matchesFileFilter(filepath) {
92
+ const filter = this.options?.filter ?? PostCssProcessorPlugin.DEFAULT_OPTIONS.filter;
93
+ return filter.test(filepath);
94
+ }
95
+ constructor(config = {
96
+ options: PostCssProcessorPlugin.DEFAULT_OPTIONS
97
+ }) {
98
+ super({
99
+ name: "ecopages-postcss-processor",
100
+ description: "A Processor for transforming CSS files using PostCSS.",
101
+ capabilities: [
102
+ {
103
+ kind: "stylesheet",
104
+ extensions: ["*.css"]
105
+ }
106
+ ],
107
+ watch: {
108
+ paths: [],
109
+ extensions: [".css", ".scss", ".sass", ".less"],
110
+ onChange: async ({ path: path2, bridge }) => {
111
+ await this.handleCssChange(path2, bridge);
112
+ }
113
+ },
114
+ ...config
115
+ });
116
+ }
117
+ /**
118
+ * Handles CSS file changes during development.
119
+ * Processes the file and broadcasts a css-update event for hot reloading.
120
+ */
121
+ async handleCssChange(filePath, bridge) {
122
+ if (!this.context) return;
123
+ try {
124
+ let content = await fileSystem.readFile(filePath);
125
+ if (this.options?.transformInput) {
126
+ content = await this.options.transformInput(content, filePath);
127
+ }
128
+ const processed = await this.process(content, filePath);
129
+ this.runtimeCssCache.set(filePath, processed);
130
+ await this.persistProcessedCss(filePath, processed);
131
+ bridge.cssUpdate(filePath);
132
+ logger.debug(`Processed CSS: ${filePath}`);
133
+ } catch (error) {
134
+ const errorMessage = error instanceof Error ? error.message : String(error);
135
+ logger.error(`Failed to process CSS: ${filePath}`, errorMessage);
136
+ bridge.error(errorMessage);
137
+ }
138
+ }
139
+ get buildPlugins() {
140
+ return [
141
+ createCssLoaderPlugin({
142
+ name: "postcss-processor-build-loader",
143
+ filter: this.getCssFilter(),
144
+ transform: this.transformCssAsync.bind(this)
145
+ })
146
+ ];
147
+ }
148
+ get plugins() {
149
+ return [
150
+ createCssLoaderPlugin({
151
+ name: "postcss-processor-runtime-loader",
152
+ filter: this.getCssFilter(),
153
+ transform: this.transformCssSync.bind(this)
154
+ })
155
+ ];
156
+ }
157
+ /**
158
+ * Setup the PostCSS processor.
159
+ */
160
+ async setup() {
161
+ await this.collectPostcssPlugins();
162
+ await this.prewarmRuntimeCssCache();
163
+ }
164
+ /**
165
+ * Get the PostCSS plugins from the options or a config file.
166
+ * Searches for postcss.config.{js,cjs,mjs,ts} in the root directory.
167
+ */
168
+ async collectPostcssPlugins() {
169
+ if (!this.context) {
170
+ throw new Error("Context must be set");
171
+ }
172
+ const configExtensions = ["js", "cjs", "mjs", "ts"];
173
+ let foundConfigPath;
174
+ let loadedPlugins;
175
+ for (const ext of configExtensions) {
176
+ const configPath = path.join(this.context.rootDir, `postcss.config.${ext}`);
177
+ if (fileSystem.exists(configPath)) {
178
+ foundConfigPath = configPath;
179
+ break;
180
+ }
181
+ }
182
+ if (foundConfigPath) {
183
+ try {
184
+ logger.debug(`Loading PostCSS config from: ${foundConfigPath}`);
185
+ const postcssConfigModule = await import(foundConfigPath);
186
+ const postcssConfig = postcssConfigModule.default || postcssConfigModule;
187
+ if (postcssConfig && typeof postcssConfig.plugins === "object" && postcssConfig.plugins !== null) {
188
+ if (Array.isArray(postcssConfig.plugins)) {
189
+ loadedPlugins = postcssConfig.plugins;
190
+ } else {
191
+ loadedPlugins = Object.values(postcssConfig.plugins);
192
+ }
193
+ logger.debug(`Successfully loaded ${loadedPlugins?.length ?? 0} plugins from config file.`);
194
+ } else {
195
+ logger.warn(
196
+ `PostCSS config file found (${foundConfigPath}), but no valid 'plugins' export detected.`
197
+ );
198
+ }
199
+ } catch (error) {
200
+ logger.error(`Error loading PostCSS config from ${foundConfigPath}: ${error.message}`, error);
201
+ loadedPlugins = void 0;
202
+ }
203
+ } else {
204
+ logger.debug("No PostCSS config file found in root directory.");
205
+ }
206
+ if (loadedPlugins) {
207
+ this.postcssPlugins = loadedPlugins;
208
+ } else if (this.options?.plugins) {
209
+ logger.debug("Using PostCSS plugins provided in processor options.");
210
+ this.postcssPlugins = Object.values(this.options.plugins);
211
+ } else {
212
+ logger.warn(
213
+ "No PostCSS plugins configured. Use a preset like tailwindV3Preset() or tailwindV4Preset(), provide plugins via options, or create a postcss.config file."
214
+ );
215
+ this.postcssPlugins = [];
216
+ }
217
+ if (!this.postcssPlugins || this.postcssPlugins.length === 0) {
218
+ logger.warn("No PostCSS plugins configured or loaded. CSS processing might be minimal.");
219
+ this.postcssPlugins = [];
220
+ }
221
+ }
222
+ /**
223
+ * Process CSS content
224
+ * @param fileAsString CSS content as string
225
+ * @param filePath Optional file path for resolving relative imports
226
+ * @returns Processed CSS
227
+ */
228
+ async process(fileAsString, filePath) {
229
+ return await PostCssProcessor.processStringOrBuffer(fileAsString, {
230
+ filePath,
231
+ plugins: this.postcssPlugins,
232
+ transformOutput: this.options?.transformOutput
233
+ });
234
+ }
235
+ processSync(fileAsString, filePath) {
236
+ return PostCssProcessor.processStringOrBufferSync(fileAsString, {
237
+ filePath,
238
+ plugins: this.postcssPlugins,
239
+ transformOutput: this.options?.transformOutput
240
+ });
241
+ }
242
+ /**
243
+ * Teardown the PostCSS processor.
244
+ */
245
+ async teardown() {
246
+ logger.debug("Tearing down PostCSS processor");
247
+ }
248
+ }
249
+ const postcssProcessorPlugin = (config) => {
250
+ return new PostCssProcessorPlugin({
251
+ options: config
252
+ });
253
+ };
254
+ export {
255
+ PostCssProcessorPlugin,
256
+ postcssProcessorPlugin
257
+ };
@@ -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,6 @@
1
+ /**
2
+ * PostCSS Processor Presets
3
+ * @module @ecopages/postcss-processor/presets
4
+ */
5
+ export { tailwindV3Preset } from './tailwind-v3';
6
+ export { tailwindV4Preset, type TailwindV4PresetOptions } from './tailwind-v4';
@@ -0,0 +1,6 @@
1
+ import { tailwindV3Preset } from "./tailwind-v3";
2
+ import { tailwindV4Preset } from "./tailwind-v4";
3
+ export {
4
+ tailwindV3Preset,
5
+ tailwindV4Preset
6
+ };
@@ -0,0 +1,32 @@
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
+ *
16
+ * @example
17
+ * ```typescript
18
+ * import { postcssProcessorPlugin } from '@ecopages/postcss-processor';
19
+ * import { tailwindV3Preset } from '@ecopages/postcss-processor/presets';
20
+ *
21
+ * // Basic usage
22
+ * postcssProcessorPlugin(tailwindV3Preset())
23
+ *
24
+ * // Extend with additional plugins
25
+ * const preset = tailwindV3Preset();
26
+ * postcssProcessorPlugin({
27
+ * ...preset,
28
+ * plugins: { ...preset.plugins, myPlugin: myPlugin() },
29
+ * })
30
+ * ```
31
+ */
32
+ export declare function tailwindV3Preset(): PostCssProcessorPluginConfig;
@@ -0,0 +1,23 @@
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 plugins = {
13
+ "postcss-import": postcssImport(),
14
+ "tailwindcss/nesting": tailwindcssNesting(),
15
+ tailwindcss: tailwindcss(),
16
+ autoprefixer: autoprefixer(autoprefixerOptions),
17
+ cssnano: cssnano()
18
+ };
19
+ return { plugins };
20
+ }
21
+ export {
22
+ tailwindV3Preset
23
+ };
@@ -0,0 +1,45 @@
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
+ *
27
+ * @example
28
+ * ```typescript
29
+ * import { postcssProcessorPlugin } from '@ecopages/postcss-processor';
30
+ * import { tailwindV4Preset } from '@ecopages/postcss-processor/presets';
31
+ *
32
+ * // Basic usage
33
+ * postcssProcessorPlugin(tailwindV4Preset({
34
+ * referencePath: path.resolve(import.meta.dir, 'src/styles/tailwind.css'),
35
+ * }))
36
+ *
37
+ * // Extend with additional plugins
38
+ * const preset = tailwindV4Preset({ referencePath });
39
+ * postcssProcessorPlugin({
40
+ * ...preset,
41
+ * plugins: { ...preset.plugins, myPlugin: myPlugin() },
42
+ * })
43
+ * ```
44
+ */
45
+ export declare function tailwindV4Preset(options: TailwindV4PresetOptions): PostCssProcessorPluginConfig;
@@ -0,0 +1,51 @@
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
+ return {
15
+ plugins: {
16
+ "postcss-import": postcssImport(),
17
+ "postcss-nested": postcssNested(),
18
+ "@tailwindcss/postcss": tailwindcss(),
19
+ autoprefixer: autoprefixer(autoprefixerOptions),
20
+ cssnano: cssnano()
21
+ },
22
+ transformInput: async (contents, filePath) => {
23
+ const css = contents instanceof Buffer ? contents.toString("utf-8") : contents;
24
+ const normalizedFilePath = path.resolve(filePath);
25
+ const normalizedReferencePath = path.resolve(referencePath);
26
+ if (normalizedFilePath === normalizedReferencePath) {
27
+ return css;
28
+ }
29
+ if (/^\s*@reference\s+/m.test(css)) {
30
+ return css;
31
+ }
32
+ const relativePath = path.relative(path.dirname(filePath), referencePath);
33
+ if (css.includes(`@import '${relativePath}'`) || css.includes(`@import "${relativePath}"`)) {
34
+ return css;
35
+ }
36
+ const tailwindImportPattern = /^@import\s+['"]tailwindcss(?:\/[^'"]*)?['"];?\s*$/m;
37
+ if (tailwindImportPattern.test(css)) {
38
+ return css.replace(tailwindImportPattern, `@import '${relativePath}';`);
39
+ }
40
+ if (css.includes("@apply")) {
41
+ return `@reference "${relativePath}";
42
+
43
+ ${css}`;
44
+ }
45
+ return css;
46
+ }
47
+ };
48
+ }
49
+ export {
50
+ tailwindV4Preset
51
+ };
@@ -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 {};
@@ -0,0 +1,22 @@
1
+ import { getFileAsBuffer } from "../postcss-processor";
2
+ const createBunCssLoaderPlugin = ({ name, filter, transform }) => ({
3
+ name,
4
+ setup(build) {
5
+ build.onLoad({ filter }, async (args) => {
6
+ const rawFile = await getFileAsBuffer(args.path);
7
+ const css = await transform({
8
+ contents: rawFile,
9
+ filePath: args.path
10
+ });
11
+ return {
12
+ exports: {
13
+ default: css
14
+ },
15
+ loader: "object"
16
+ };
17
+ });
18
+ }
19
+ });
20
+ export {
21
+ createBunCssLoaderPlugin
22
+ };
@@ -0,0 +1,5 @@
1
+ export type CssTransformInput = {
2
+ contents: string | Buffer;
3
+ filePath: string;
4
+ };
5
+ export type CssTransform = (input: CssTransformInput) => string | Promise<string>;
File without changes
@@ -1,3 +0,0 @@
1
- .base {
2
- color: red;
3
- }
@@ -1,3 +0,0 @@
1
- .test {
2
- @apply bg-red-500;
3
- }
@@ -1,3 +0,0 @@
1
- .test {
2
- @apply bg-reds-500;
3
- }
@@ -1,13 +0,0 @@
1
- $dir: top;
2
- $blue: #056ef0;
3
- $column: 200px;
4
-
5
- .menu_link {
6
- background: $blue;
7
- width: $column;
8
- }
9
-
10
- .menu {
11
- width: calc(4 * $column);
12
- margin-$(dir): 10px;
13
- }
@@ -1,4 +0,0 @@
1
- @import 'base.css';
2
- .main {
3
- background: blue;
4
- }
@@ -1,74 +0,0 @@
1
- import { describe, expect, test, afterAll, beforeAll, vi } from 'vitest';
2
- import path from 'node:path';
3
- import fs from 'node:fs';
4
- import { PostCssProcessorPlugin } from '../plugin';
5
- import type { ClientBridge } from '@ecopages/core/adapters/bun/client-bridge';
6
- import { ConfigBuilder } from '@ecopages/core/config-builder';
7
-
8
- const TMP_DIR = path.join(__dirname, 'tmp_test_hmr');
9
- const SRC_DIR = path.join(TMP_DIR, 'src');
10
- const DIST_DIR = path.join(TMP_DIR, 'dist');
11
-
12
- describe('PostCssProcessorPlugin HMR', () => {
13
- beforeAll(() => {
14
- if (fs.existsSync(TMP_DIR)) {
15
- fs.rmSync(TMP_DIR, { recursive: true, force: true });
16
- }
17
- fs.mkdirSync(SRC_DIR, { recursive: true });
18
- fs.mkdirSync(DIST_DIR, { recursive: true });
19
- });
20
-
21
- afterAll(() => {
22
- fs.rmSync(TMP_DIR, { recursive: true, force: true });
23
- });
24
-
25
- test('handleCssChange should apply transformInput', async () => {
26
- const cssFile = path.join(SRC_DIR, 'style.css');
27
- fs.writeFileSync(cssFile, '.foo { color: red; }');
28
-
29
- const plugin = new PostCssProcessorPlugin({
30
- options: {
31
- filter: /\.css$/,
32
- transformInput: async (content) => {
33
- return `/* prefix */\n${content}`;
34
- },
35
- },
36
- });
37
-
38
- const config = await new ConfigBuilder()
39
- .setRootDir(TMP_DIR)
40
- .setSrcDir('src')
41
- .setDistDir('dist')
42
- .setBaseUrl('http://localhost:3000')
43
- .build();
44
-
45
- plugin.setContext(config);
46
-
47
- const Bridge = {
48
- cssUpdate: () => {},
49
- error: (msg: string) => {
50
- throw new Error(msg);
51
- },
52
- reload: () => {},
53
- } as unknown as ClientBridge;
54
-
55
- const bridgeSpy = vi.spyOn(Bridge, 'cssUpdate');
56
-
57
- const watchConfig = plugin.getWatchConfig();
58
- if (watchConfig && watchConfig.onChange) {
59
- await watchConfig.onChange({ path: cssFile, bridge: Bridge });
60
- } else {
61
- throw new Error('Plugin does not have watch handler');
62
- }
63
-
64
- const outputFile = path.join(DIST_DIR, 'assets', 'style.css');
65
- expect(fs.existsSync(outputFile)).toBe(true);
66
-
67
- const outputContent = fs.readFileSync(outputFile, 'utf-8');
68
- expect(outputContent).toContain('/* prefix */');
69
- expect(outputContent).toContain('.foo { color: red; }');
70
-
71
- expect(bridgeSpy).toHaveBeenCalled();
72
- expect(bridgeSpy).toHaveBeenCalledWith(cssFile);
73
- });
74
- });
@@ -1,106 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
- import path from 'node:path';
3
- import postCssSimpleVars from 'postcss-simple-vars';
4
- import { PostCssProcessor } from '../postcss-processor';
5
- import { tailwindV3Preset } from '../presets/tailwind-v3';
6
-
7
- describe('PostCssProcessor', () => {
8
- test('processPath should return the processed CSS', async () => {
9
- const filePath = path.resolve(__dirname, './css/correct.css');
10
- const expected = '.test{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}';
11
- const { plugins } = tailwindV3Preset();
12
- const result = await PostCssProcessor.processPath(filePath, { plugins: plugins ? Object.values(plugins) : [] });
13
- expect(result).toEqual(expected);
14
- });
15
-
16
- test('processPath should return an empty string when an error occurs during css conversion', async () => {
17
- const filePath = path.resolve(__dirname, './css/error.css');
18
- const expected = '';
19
- const { plugins } = tailwindV3Preset();
20
- const result = await PostCssProcessor.processPath(filePath, { plugins: plugins ? Object.values(plugins) : [] });
21
- expect(result).toEqual(expected);
22
- });
23
-
24
- test('processPath should throw when the file does not exist', async () => {
25
- const filePath = 'fake-path.css';
26
- await expect(PostCssProcessor.processPath(filePath)).rejects.toThrow();
27
- });
28
-
29
- test('processPath should use the custom plugins', async () => {
30
- const filePath = path.resolve(__dirname, './css/external-plugins.css');
31
- const expected = '.menu_link{background:#056ef0;width:200px}.menu{margin-top:10px;width:800px}';
32
- const result = await PostCssProcessor.processPath(filePath, {
33
- plugins: [...Object.values(tailwindV3Preset().plugins ?? {}), postCssSimpleVars()],
34
- });
35
- expect(result).toEqual(expected);
36
- });
37
-
38
- test('processStringOrBuffer should return the processed CSS', async () => {
39
- const string = 'body { @apply bg-white; }';
40
- const expected = 'body{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}';
41
- const { plugins } = tailwindV3Preset();
42
- const result = await PostCssProcessor.processStringOrBuffer(string, {
43
- plugins: plugins ? Object.values(plugins) : [],
44
- });
45
- expect(result).toEqual(expected);
46
- });
47
-
48
- test('processStringOrBuffer should return an empty string when an error occurs during css conversion', async () => {
49
- const string = 'body { @apply bg-whites; }';
50
- const expected = '';
51
- const { plugins } = tailwindV3Preset();
52
- const result = await PostCssProcessor.processStringOrBuffer(string, {
53
- plugins: plugins ? Object.values(plugins) : [],
54
- });
55
- expect(result).toEqual(expected);
56
- });
57
-
58
- test('processStringOrBuffer should return an empty string when the input is empty', async () => {
59
- const string = '';
60
- const expected = '';
61
- const result = await PostCssProcessor.processStringOrBuffer(string);
62
- expect(result).toEqual(expected);
63
- });
64
-
65
- test('processStringOrBuffer should use the custom plugins', async () => {
66
- const string = '$blue: #056ef0; body { background: $blue; }';
67
- const expected = 'body{background:#056ef0}';
68
- const result = await PostCssProcessor.processStringOrBuffer(string, {
69
- plugins: [...Object.values(tailwindV3Preset().plugins ?? {}), postCssSimpleVars()],
70
- });
71
- expect(result).toEqual(expected);
72
- });
73
-
74
- test('processStringOrBuffer should use the transformOutput', async () => {
75
- const string = 'body { background: #056ef0; }';
76
- const expected = '@reference "../app.css";\nbody{background:#056ef0}';
77
- const { plugins } = tailwindV3Preset();
78
- const result = await PostCssProcessor.processStringOrBuffer(string, {
79
- plugins: plugins?.cssnano ? [plugins.cssnano] : [],
80
- transformOutput: (css) => {
81
- return `@reference "../app.css";\n${css}`;
82
- },
83
- });
84
- expect(result).toEqual(expected);
85
- });
86
-
87
- test('processPath should resolve @import', async () => {
88
- const filePath = path.resolve(__dirname, './css/import.css');
89
- const expected = '.base{color:red}.main{background:blue}';
90
- const { plugins } = tailwindV3Preset();
91
- const result = await PostCssProcessor.processPath(filePath, { plugins: plugins ? Object.values(plugins) : [] });
92
- expect(result).toEqual(expected);
93
- });
94
-
95
- test('processStringOrBuffer should resolve @import', async () => {
96
- const string = '@import "base.css"; .main { background: blue; }';
97
- const filePath = path.resolve(__dirname, './css/import.css');
98
- const expected = '.base{color:red}.main{background:blue}';
99
- const { plugins } = tailwindV3Preset();
100
- const result = await PostCssProcessor.processStringOrBuffer(string, {
101
- filePath,
102
- plugins: plugins ? Object.values(plugins) : [],
103
- });
104
- expect(result).toEqual(expected);
105
- });
106
- });
@@ -1,140 +0,0 @@
1
- import { describe, expect, test } from 'vitest';
2
- import path from 'node:path';
3
- import { PostCssProcessor } from '../postcss-processor';
4
- import { tailwindV3Preset } from '../presets/tailwind-v3';
5
- import { tailwindV4Preset } from '../presets/tailwind-v4';
6
-
7
- describe('Presets Verification', () => {
8
- const cssToPrefix = `
9
- .test-prefix {
10
- user-select: none;
11
- backdrop-filter: blur(10px);
12
- appearance: none;
13
- }
14
- `;
15
-
16
- const cssWithImport = `
17
- @import "base.css";
18
- .main { background: blue; }
19
- `;
20
-
21
- test('Tailwind v3 preset should add vendor prefixes', async () => {
22
- const { plugins } = tailwindV3Preset();
23
- /**
24
- * Disable cssnano for readable output logic check, or just check content calls
25
- * By default preset includes cssnano.
26
- */
27
-
28
- const result = await PostCssProcessor.processStringOrBuffer(cssToPrefix, {
29
- plugins: plugins ? Object.values(plugins) : [],
30
- filePath: path.resolve(__dirname, 'style.css'),
31
- });
32
-
33
- /**
34
- * cssnano will minify, so we check minified output
35
- * user-select: none -> -webkit-user-select:none;user-select:none
36
- * backdrop-filter: blur(10px) -> -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)
37
- * appearance: none -> -webkit-appearance:none;-moz-appearance:none;appearance:none
38
- */
39
-
40
- expect(result).toContain('-webkit-user-select:none');
41
- expect(result).toContain('user-select:none');
42
-
43
- expect(result).toContain('-webkit-backdrop-filter:blur(10px)');
44
- expect(result).toContain('backdrop-filter:blur(10px)');
45
-
46
- expect(result).toContain('appearance:none');
47
- });
48
-
49
- test('Tailwind v3 preset should support nesting', async () => {
50
- const { plugins } = tailwindV3Preset();
51
- const cssWithNesting = `
52
- .parent {
53
- & .child { color: blue; }
54
- &__element { color: green; }
55
- }
56
- `;
57
-
58
- const result = await PostCssProcessor.processStringOrBuffer(cssWithNesting, {
59
- plugins: plugins ? Object.values(plugins) : [],
60
- filePath: path.resolve(__dirname, 'style.css'),
61
- });
62
-
63
- expect(result).toContain('.parent .child{color:blue}');
64
- expect(result).toContain('.parent__element{color:green}');
65
- });
66
-
67
- test('Tailwind v4 preset should add vendor prefixes (via Lightning CSS)', async () => {
68
- const preset = tailwindV4Preset({
69
- referencePath: path.resolve(__dirname, '../fixtures/tailwind.css'),
70
- });
71
-
72
- const result = await PostCssProcessor.processStringOrBuffer(cssToPrefix, {
73
- plugins: preset.plugins ? Object.values(preset.plugins) : [],
74
- filePath: path.resolve(__dirname, 'style.css'),
75
- });
76
-
77
- /**
78
- * Lightning CSS used by @tailwindcss/postcss should verify this
79
- * Note: The specific prefixes might depend on the browser targets configured in package.json or defaults
80
- */
81
-
82
- expect(result).toContain('-webkit-user-select:none');
83
- expect(result).toContain('user-select:none');
84
-
85
- expect(result).toContain('-webkit-backdrop-filter:blur(10px)');
86
- expect(result).toContain('backdrop-filter:blur(10px)');
87
-
88
- expect(result).toContain('appearance:none');
89
- });
90
-
91
- test('Tailwind v4 preset should resolve @import', async () => {
92
- const preset = tailwindV4Preset({
93
- referencePath: path.resolve(__dirname, '../fixtures/tailwind.css'),
94
- });
95
-
96
- /**
97
- * Provide a filePath that allows resolving sibling files (like base.css in existing tests/css dir)
98
- * We'll leverage the existing test css files
99
- */
100
- const filePath = path.resolve(__dirname, 'css/import.css');
101
-
102
- const result = await PostCssProcessor.processStringOrBuffer(cssWithImport, {
103
- plugins: preset.plugins ? Object.values(preset.plugins) : [],
104
- filePath,
105
- });
106
-
107
- /**
108
- * Expect content from base.css (.base { color: red }) + main css content
109
- * Minified output expected
110
- */
111
- expect(result).toContain('.base{color:red}');
112
- expect(result).toContain('.main{background:blue}');
113
- });
114
- });
115
-
116
- test('Tailwind v4 preset should support nesting', async () => {
117
- const preset = tailwindV4Preset({
118
- referencePath: path.resolve(__dirname, '../fixtures/tailwind.css'),
119
- });
120
-
121
- const cssWithNesting = `
122
- .parent {
123
- color: red;
124
- & .child {
125
- color: blue;
126
- }
127
- &__element {
128
- color: green;
129
- }
130
- }
131
- `;
132
-
133
- const result = await PostCssProcessor.processStringOrBuffer(cssWithNesting, {
134
- plugins: preset.plugins ? Object.values(preset.plugins) : [],
135
- filePath: path.resolve(__dirname, 'style.css'),
136
- });
137
-
138
- expect(result).toContain('.parent .child{color:blue}');
139
- expect(result).toContain('.parent__element{color:green}');
140
- });