@ecopages/postcss-processor 0.2.0-alpha.1
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/LICENSE +21 -0
- package/README.md +136 -0
- package/package.json +82 -0
- package/src/index.ts +2 -0
- package/src/plugin.ts +354 -0
- package/src/postcss-processor.ts +157 -0
- package/src/presets/index.ts +7 -0
- package/src/presets/tailwind-v3.ts +61 -0
- package/src/presets/tailwind-v4.ts +113 -0
- package/src/runtime/css-loader-plugin.ts +37 -0
- package/src/runtime/css-loader.bun.ts +30 -0
- package/src/runtime/css-runtime-contract.ts +6 -0
- package/src/test/css/base.css +3 -0
- package/src/test/css/correct.css +3 -0
- package/src/test/css/error.css +3 -0
- package/src/test/css/external-plugins.css +13 -0
- package/src/test/css/import.css +4 -0
- package/src/test/plugin.test.ts +74 -0
- package/src/test/postcss-processor.test.ts +106 -0
- package/src/test/presets.test.ts +140 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present Andrea Zanenghi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# PostCSS Processor
|
|
2
|
+
|
|
3
|
+
This module provides a PostCSS processor plugin for Ecopages and utility functions for processing CSS files and strings using PostCSS. It includes built-in presets for Tailwind CSS (v3 and v4).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Ecopages Processor Plugin**: Seamless integration with Ecopages build system.
|
|
8
|
+
- **Tailwind Presets**: Ready-to-use configurations for Tailwind CSS v3 and v4.
|
|
9
|
+
- **Automatic Configuration**: Detects `postcss.config.{js,ts,etc}` automatically.
|
|
10
|
+
- **Standalone Usage**: Process CSS files or strings directly.
|
|
11
|
+
- **Bun Loader**: Automatically registers a Bun loader for importing CSS in TS/JS files.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bunx jsr add @ecopages/postcss-processor
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage with Ecopages
|
|
20
|
+
|
|
21
|
+
Integrate the processor into your `eco.config.ts` using one of the available presets.
|
|
22
|
+
|
|
23
|
+
### Tailwind v3 Preset
|
|
24
|
+
|
|
25
|
+
Includes `tailwindcss`, `autoprefixer`, `postcss-import`, `cssnano`.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bun add -D tailwindcss@3.4.19
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// eco.config.ts
|
|
33
|
+
import { ConfigBuilder } from '@ecopages/core';
|
|
34
|
+
import { postcssProcessorPlugin } from '@ecopages/postcss-processor';
|
|
35
|
+
import { tailwindV3Preset } from '@ecopages/postcss-processor/presets/tailwind-v3';
|
|
36
|
+
|
|
37
|
+
const config = await new ConfigBuilder().setProcessors([postcssProcessorPlugin(tailwindV3Preset())]).build();
|
|
38
|
+
|
|
39
|
+
export default config;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Tailwind v4 Preset (Recommended)
|
|
43
|
+
|
|
44
|
+
Includes `@tailwindcss/postcss`, `autoprefixer`, `postcss-nested`, `cssnano`, and handles `@reference` injection for `@apply`.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bun add -D @tailwindcss/postcss tailwindcss
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// eco.config.ts
|
|
52
|
+
import path from 'node:path';
|
|
53
|
+
import { ConfigBuilder } from '@ecopages/core';
|
|
54
|
+
import { postcssProcessorPlugin } from '@ecopages/postcss-processor';
|
|
55
|
+
import { tailwindV4Preset } from '@ecopages/postcss-processor/presets/tailwind-v4';
|
|
56
|
+
|
|
57
|
+
const config = await new ConfigBuilder()
|
|
58
|
+
.setProcessors([
|
|
59
|
+
postcssProcessorPlugin(
|
|
60
|
+
tailwindV4Preset({
|
|
61
|
+
referencePath: path.resolve(import.meta.dir, 'src/styles/app.css'),
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
])
|
|
65
|
+
.build();
|
|
66
|
+
|
|
67
|
+
export default config;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Browser Support
|
|
71
|
+
|
|
72
|
+
By default, the presets target a broad range of modern browsers (`>0.3%, not ie 11, not dead, not op_mini all`).
|
|
73
|
+
|
|
74
|
+
To override this, add a `browserslist` configuration to your `package.json` or create a `.browserslistrc` file in your project root. The processor will automatically detect and use your custom configuration.
|
|
75
|
+
|
|
76
|
+
### Custom Configuration
|
|
77
|
+
|
|
78
|
+
You can also use a standard `postcss.config.js` file or pass plugins manually.
|
|
79
|
+
|
|
80
|
+
**Using `postcss.config.js`:**
|
|
81
|
+
Create the file in your root, and simply add `postcssProcessorPlugin()` to your config without arguments.
|
|
82
|
+
|
|
83
|
+
**Manual Configuration:**
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { postcssProcessorPlugin } from '@ecopages/postcss-processor';
|
|
87
|
+
import myPlugin from 'postcss-my-plugin';
|
|
88
|
+
|
|
89
|
+
postcssProcessorPlugin({
|
|
90
|
+
plugins: {
|
|
91
|
+
'my-plugin': myPlugin(),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Advanced Configuration:**
|
|
97
|
+
|
|
98
|
+
For advanced use cases, use transformation hooks to modify CSS before or after processing:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { ConfigBuilder } from '@ecopages/core';
|
|
102
|
+
import { postcssProcessorPlugin } from '@ecopages/postcss-processor';
|
|
103
|
+
|
|
104
|
+
const config = await new ConfigBuilder()
|
|
105
|
+
.setProcessors([
|
|
106
|
+
postcssProcessorPlugin({
|
|
107
|
+
// Define a filter for files to process (defaults to /\.css$/)
|
|
108
|
+
filter: /\.css$/,
|
|
109
|
+
// Provide a function to transform input before processing
|
|
110
|
+
transformInput: async (css) => `/* My Custom Header */\n${css}`,
|
|
111
|
+
// Provide a function to transform output after processing
|
|
112
|
+
transformOutput: async (css) => css.replace('blue', 'red'),
|
|
113
|
+
// Explicitly provide plugins (overrides defaults)
|
|
114
|
+
plugins: {
|
|
115
|
+
/* custom plugins */
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
])
|
|
119
|
+
.build();
|
|
120
|
+
|
|
121
|
+
export default config;
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Standalone Usage
|
|
125
|
+
|
|
126
|
+
You can use the underlying processor functions directly:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { PostCssProcessor } from '@ecopages/postcss-processor';
|
|
130
|
+
|
|
131
|
+
// Process a file
|
|
132
|
+
const css = await PostCssProcessor.processPath('path/to/file.css');
|
|
133
|
+
|
|
134
|
+
// Process a string
|
|
135
|
+
const result = await PostCssProcessor.processStringOrBuffer('.class { @apply bg-red-500; }', { filePath: 'style.css' });
|
|
136
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ecopages/postcss-processor",
|
|
3
|
+
"version": "0.2.0-alpha.1",
|
|
4
|
+
"description": "Postcss processor, transform string or postcss file to css",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"postcss",
|
|
7
|
+
"processor",
|
|
8
|
+
"css"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"main": "./src/postcss-processor.ts",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"types": "./src/postcss-processor.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/ecopages/ecopages.git",
|
|
20
|
+
"directory": "packages/processors/postcss-processor"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"release:jsr": "bunx jsr publish"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@ecopages/core": "workspace:*",
|
|
28
|
+
"@ecopages/file-system": "workspace:*",
|
|
29
|
+
"@ecopages/logger": "latest",
|
|
30
|
+
"autoprefixer": "^10.4.0",
|
|
31
|
+
"browserslist": "^4.28.1",
|
|
32
|
+
"cssnano": "^6.0.0",
|
|
33
|
+
"postcss": "^8.4.32",
|
|
34
|
+
"postcss-import": "^15.0.0",
|
|
35
|
+
"postcss-nested": "^7.0.2"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@tailwindcss/postcss": ">=4",
|
|
39
|
+
"tailwindcss": ">=3"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"@tailwindcss/postcss": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
45
|
+
"tailwindcss": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
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
|
+
"exports": {
|
|
57
|
+
".": {
|
|
58
|
+
"default": "./src/index.ts",
|
|
59
|
+
"types": "./src/index.ts"
|
|
60
|
+
},
|
|
61
|
+
"./postcss-processor": {
|
|
62
|
+
"default": "./src/postcss-processor.ts",
|
|
63
|
+
"types": "./src/postcss-processor.ts"
|
|
64
|
+
},
|
|
65
|
+
"./plugin": {
|
|
66
|
+
"default": "./src/plugin.ts",
|
|
67
|
+
"types": "./src/plugin.ts"
|
|
68
|
+
},
|
|
69
|
+
"./presets": {
|
|
70
|
+
"default": "./src/presets/index.ts",
|
|
71
|
+
"types": "./src/presets/index.ts"
|
|
72
|
+
},
|
|
73
|
+
"./presets/tailwind-v3": {
|
|
74
|
+
"default": "./src/presets/tailwind-v3.ts",
|
|
75
|
+
"types": "./src/presets/tailwind-v3.ts"
|
|
76
|
+
},
|
|
77
|
+
"./presets/tailwind-v4": {
|
|
78
|
+
"default": "./src/presets/tailwind-v4.ts",
|
|
79
|
+
"types": "./src/presets/tailwind-v4.ts"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/index.ts
ADDED
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostCssProcessorPlugin
|
|
3
|
+
* @module @ecopages/postcss-processor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import type { IClientBridge } from '@ecopages/core';
|
|
8
|
+
import { fileSystem } from '@ecopages/file-system';
|
|
9
|
+
import { Processor, type ProcessorConfig } from '@ecopages/core/plugins/processor';
|
|
10
|
+
import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
|
|
11
|
+
import { Logger } from '@ecopages/logger';
|
|
12
|
+
import type postcss from 'postcss';
|
|
13
|
+
import { PostCssProcessor } from './postcss-processor';
|
|
14
|
+
import { createCssLoaderPlugin } from './runtime/css-loader-plugin';
|
|
15
|
+
import type { CssTransformInput } from './runtime/css-runtime-contract';
|
|
16
|
+
|
|
17
|
+
const logger = new Logger('[@ecopages/postcss-processor]', {
|
|
18
|
+
debug: process.env.ECOPAGES_LOGGER_DEBUG === 'true',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Record of PostCSS plugins keyed by name
|
|
23
|
+
*/
|
|
24
|
+
export type PluginsRecord = Record<string, postcss.AcceptedPlugin>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration for the PostCSS processor
|
|
28
|
+
*/
|
|
29
|
+
export interface PostCssProcessorPluginConfig {
|
|
30
|
+
/**
|
|
31
|
+
* Regex filter to match files to process
|
|
32
|
+
*/
|
|
33
|
+
filter?: RegExp;
|
|
34
|
+
/**
|
|
35
|
+
* Function to transform the contents of the file.
|
|
36
|
+
* It can be handy to add a custom header or footer to the file.
|
|
37
|
+
* Useful for injecting Tailwind v4 `@reference` directives.
|
|
38
|
+
* @param contents The contents of the file
|
|
39
|
+
* @param filePath The absolute path to the CSS file being processed
|
|
40
|
+
* @returns The transformed contents
|
|
41
|
+
*/
|
|
42
|
+
transformInput?: (contents: string | Buffer, filePath: string) => string | Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Function to transform the output CSS after PostCSS processing.
|
|
45
|
+
* It can be handy to add a custom header or footer to the processed CSS.
|
|
46
|
+
* @param css The processed CSS
|
|
47
|
+
* @returns The transformed CSS
|
|
48
|
+
*/
|
|
49
|
+
transformOutput?: (css: string) => Promise<string> | string;
|
|
50
|
+
/**
|
|
51
|
+
* Custom PostCSS plugins to use instead of the default ones
|
|
52
|
+
* @default undefined (uses default plugins)
|
|
53
|
+
*/
|
|
54
|
+
plugins?: PluginsRecord;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* PostCssProcessorPlugin
|
|
59
|
+
* A Processor for transforming CSS files.
|
|
60
|
+
*/
|
|
61
|
+
export class PostCssProcessorPlugin extends Processor<PostCssProcessorPluginConfig> {
|
|
62
|
+
static DEFAULT_OPTIONS: Required<Pick<PostCssProcessorPluginConfig, 'filter'>> = {
|
|
63
|
+
filter: /\.css$/,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
private postcssPlugins: postcss.AcceptedPlugin[] = [];
|
|
67
|
+
private readonly runtimeCssCache = new Map<string, string>();
|
|
68
|
+
|
|
69
|
+
private getCssFilter(): RegExp {
|
|
70
|
+
return this.options?.filter ?? PostCssProcessorPlugin.DEFAULT_OPTIONS.filter;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private resolveProcessedCssPath(filePath: string): string | null {
|
|
74
|
+
if (!this.context) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const relativePath = path.relative(this.context.srcDir, filePath);
|
|
79
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return path.join(this.context.distDir, 'assets', relativePath);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private readProcessedCssFromDist(filePath: string): string | null {
|
|
87
|
+
const outputPath = this.resolveProcessedCssPath(filePath);
|
|
88
|
+
if (!outputPath || !fileSystem.exists(outputPath)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return fileSystem.readFileAsBuffer(outputPath).toString('utf-8');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async persistProcessedCss(filePath: string, css: string): Promise<void> {
|
|
96
|
+
const outputPath = this.resolveProcessedCssPath(filePath);
|
|
97
|
+
if (!outputPath) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fileSystem.ensureDir(path.dirname(outputPath));
|
|
102
|
+
fileSystem.write(outputPath, css);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async prewarmRuntimeCssCache(): Promise<void> {
|
|
106
|
+
if (!this.context) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const sourceFiles = await fileSystem.glob(['**/*.{css,scss,sass,less}'], {
|
|
111
|
+
cwd: this.context.srcDir,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
for (const relativePath of sourceFiles) {
|
|
115
|
+
const filePath = path.join(this.context.srcDir, relativePath);
|
|
116
|
+
if (!this.matchesFileFilter(filePath)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const rawContents = await fileSystem.readFile(filePath);
|
|
121
|
+
let transformedInput = rawContents;
|
|
122
|
+
|
|
123
|
+
if (this.options?.transformInput) {
|
|
124
|
+
transformedInput = await this.options.transformInput(rawContents, filePath);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const processed = await this.process(transformedInput, filePath);
|
|
128
|
+
this.runtimeCssCache.set(filePath, processed);
|
|
129
|
+
await this.persistProcessedCss(filePath, processed);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private transformCssSync(input: CssTransformInput): string {
|
|
134
|
+
const cached = this.runtimeCssCache.get(input.filePath);
|
|
135
|
+
if (cached) {
|
|
136
|
+
return cached;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const persisted = this.readProcessedCssFromDist(input.filePath);
|
|
140
|
+
if (persisted) {
|
|
141
|
+
this.runtimeCssCache.set(input.filePath, persisted);
|
|
142
|
+
return persisted;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const { contents } = input;
|
|
146
|
+
return typeof contents === 'string' ? contents : contents.toString('utf-8');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async transformCssAsync(input: CssTransformInput): Promise<string> {
|
|
150
|
+
const { contents, filePath } = input;
|
|
151
|
+
let transformed: string = typeof contents === 'string' ? contents : contents.toString('utf-8');
|
|
152
|
+
|
|
153
|
+
if (this.options?.transformInput) {
|
|
154
|
+
const result = this.options.transformInput(contents, filePath);
|
|
155
|
+
transformed =
|
|
156
|
+
typeof (result as unknown as Record<string, unknown>).then === 'function'
|
|
157
|
+
? await (result as Promise<string>)
|
|
158
|
+
: (result as string);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const processed = await this.process(transformed, filePath);
|
|
162
|
+
this.runtimeCssCache.set(filePath, processed);
|
|
163
|
+
await this.persistProcessedCss(filePath, processed);
|
|
164
|
+
return processed;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
override matchesFileFilter(filepath: string): boolean {
|
|
168
|
+
const filter = this.options?.filter ?? PostCssProcessorPlugin.DEFAULT_OPTIONS.filter;
|
|
169
|
+
return filter.test(filepath);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
constructor(
|
|
173
|
+
config: Omit<ProcessorConfig<PostCssProcessorPluginConfig>, 'name' | 'description'> = {
|
|
174
|
+
options: PostCssProcessorPlugin.DEFAULT_OPTIONS,
|
|
175
|
+
},
|
|
176
|
+
) {
|
|
177
|
+
super({
|
|
178
|
+
name: 'ecopages-postcss-processor',
|
|
179
|
+
description: 'A Processor for transforming CSS files using PostCSS.',
|
|
180
|
+
capabilities: [
|
|
181
|
+
{
|
|
182
|
+
kind: 'stylesheet',
|
|
183
|
+
extensions: ['*.css'],
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
watch: {
|
|
187
|
+
paths: [],
|
|
188
|
+
extensions: ['.css', '.scss', '.sass', '.less'],
|
|
189
|
+
onChange: async ({ path, bridge }) => {
|
|
190
|
+
await this.handleCssChange(path, bridge);
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
...config,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Handles CSS file changes during development.
|
|
199
|
+
* Processes the file and broadcasts a css-update event for hot reloading.
|
|
200
|
+
*/
|
|
201
|
+
private async handleCssChange(filePath: string, bridge: IClientBridge): Promise<void> {
|
|
202
|
+
if (!this.context) return;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
let content = await fileSystem.readFile(filePath);
|
|
206
|
+
|
|
207
|
+
if (this.options?.transformInput) {
|
|
208
|
+
content = await this.options.transformInput(content, filePath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const processed = await this.process(content, filePath);
|
|
212
|
+
this.runtimeCssCache.set(filePath, processed);
|
|
213
|
+
await this.persistProcessedCss(filePath, processed);
|
|
214
|
+
|
|
215
|
+
bridge.cssUpdate(filePath);
|
|
216
|
+
|
|
217
|
+
logger.debug(`Processed CSS: ${filePath}`);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
220
|
+
logger.error(`Failed to process CSS: ${filePath}`, errorMessage);
|
|
221
|
+
bridge.error(errorMessage);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
get buildPlugins(): EcoBuildPlugin[] {
|
|
226
|
+
return [
|
|
227
|
+
createCssLoaderPlugin({
|
|
228
|
+
name: 'postcss-processor-build-loader',
|
|
229
|
+
filter: this.getCssFilter(),
|
|
230
|
+
transform: this.transformCssAsync.bind(this),
|
|
231
|
+
}),
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
get plugins(): EcoBuildPlugin[] {
|
|
236
|
+
return [
|
|
237
|
+
createCssLoaderPlugin({
|
|
238
|
+
name: 'postcss-processor-runtime-loader',
|
|
239
|
+
filter: this.getCssFilter(),
|
|
240
|
+
transform: this.transformCssSync.bind(this),
|
|
241
|
+
}),
|
|
242
|
+
];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Setup the PostCSS processor.
|
|
247
|
+
*/
|
|
248
|
+
async setup(): Promise<void> {
|
|
249
|
+
await this.collectPostcssPlugins();
|
|
250
|
+
await this.prewarmRuntimeCssCache();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get the PostCSS plugins from the options or a config file.
|
|
255
|
+
* Searches for postcss.config.{js,cjs,mjs,ts} in the root directory.
|
|
256
|
+
*/
|
|
257
|
+
private async collectPostcssPlugins(): Promise<void> {
|
|
258
|
+
if (!this.context) {
|
|
259
|
+
throw new Error('Context must be set');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const configExtensions = ['js', 'cjs', 'mjs', 'ts'];
|
|
263
|
+
let foundConfigPath: string | undefined;
|
|
264
|
+
let loadedPlugins: postcss.AcceptedPlugin[] | undefined;
|
|
265
|
+
|
|
266
|
+
for (const ext of configExtensions) {
|
|
267
|
+
const configPath = path.join(this.context.rootDir, `postcss.config.${ext}`);
|
|
268
|
+
if (fileSystem.exists(configPath)) {
|
|
269
|
+
foundConfigPath = configPath;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (foundConfigPath) {
|
|
275
|
+
try {
|
|
276
|
+
logger.debug(`Loading PostCSS config from: ${foundConfigPath}`);
|
|
277
|
+
|
|
278
|
+
const postcssConfigModule = await import(foundConfigPath);
|
|
279
|
+
const postcssConfig = postcssConfigModule.default || postcssConfigModule;
|
|
280
|
+
|
|
281
|
+
if (postcssConfig && typeof postcssConfig.plugins === 'object' && postcssConfig.plugins !== null) {
|
|
282
|
+
if (Array.isArray(postcssConfig.plugins)) {
|
|
283
|
+
loadedPlugins = postcssConfig.plugins;
|
|
284
|
+
} else {
|
|
285
|
+
loadedPlugins = Object.values(postcssConfig.plugins as PluginsRecord);
|
|
286
|
+
}
|
|
287
|
+
logger.debug(`Successfully loaded ${loadedPlugins?.length ?? 0} plugins from config file.`);
|
|
288
|
+
} else {
|
|
289
|
+
logger.warn(
|
|
290
|
+
`PostCSS config file found (${foundConfigPath}), but no valid 'plugins' export detected.`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
} catch (error: any) {
|
|
294
|
+
logger.error(`Error loading PostCSS config from ${foundConfigPath}: ${error.message}`, error);
|
|
295
|
+
loadedPlugins = undefined;
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
logger.debug('No PostCSS config file found in root directory.');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (loadedPlugins) {
|
|
302
|
+
this.postcssPlugins = loadedPlugins;
|
|
303
|
+
} else if (this.options?.plugins) {
|
|
304
|
+
logger.debug('Using PostCSS plugins provided in processor options.');
|
|
305
|
+
this.postcssPlugins = Object.values(this.options.plugins);
|
|
306
|
+
} else {
|
|
307
|
+
logger.warn(
|
|
308
|
+
'No PostCSS plugins configured. Use a preset like tailwindV3Preset() or tailwindV4Preset(), ' +
|
|
309
|
+
'provide plugins via options, or create a postcss.config file.',
|
|
310
|
+
);
|
|
311
|
+
this.postcssPlugins = [];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!this.postcssPlugins || this.postcssPlugins.length === 0) {
|
|
315
|
+
logger.warn('No PostCSS plugins configured or loaded. CSS processing might be minimal.');
|
|
316
|
+
this.postcssPlugins = [];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Process CSS content
|
|
322
|
+
* @param fileAsString CSS content as string
|
|
323
|
+
* @param filePath Optional file path for resolving relative imports
|
|
324
|
+
* @returns Processed CSS
|
|
325
|
+
*/
|
|
326
|
+
async process(fileAsString: string, filePath?: string): Promise<string> {
|
|
327
|
+
return await PostCssProcessor.processStringOrBuffer(fileAsString, {
|
|
328
|
+
filePath,
|
|
329
|
+
plugins: this.postcssPlugins,
|
|
330
|
+
transformOutput: this.options?.transformOutput,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
processSync(fileAsString: string, filePath?: string): string {
|
|
335
|
+
return PostCssProcessor.processStringOrBufferSync(fileAsString, {
|
|
336
|
+
filePath,
|
|
337
|
+
plugins: this.postcssPlugins,
|
|
338
|
+
transformOutput: this.options?.transformOutput,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Teardown the PostCSS processor.
|
|
344
|
+
*/
|
|
345
|
+
async teardown(): Promise<void> {
|
|
346
|
+
logger.debug('Tearing down PostCSS processor');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export const postcssProcessorPlugin = (config?: PostCssProcessorPluginConfig): PostCssProcessorPlugin => {
|
|
351
|
+
return new PostCssProcessorPlugin({
|
|
352
|
+
options: config,
|
|
353
|
+
});
|
|
354
|
+
};
|