@ecopages/postcss-processor 0.2.0-alpha.9 → 0.2.0-beta.0

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/README.md CHANGED
@@ -12,7 +12,7 @@ PostCSS processing pipeline for Ecopages. It provides a processor plugin that se
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- bunx jsr add @ecopages/postcss-processor
15
+ bun add @ecopages/postcss-processor
16
16
  ```
17
17
 
18
18
  ## Usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/postcss-processor",
3
- "version": "0.2.0-alpha.9",
3
+ "version": "0.2.0-beta.0",
4
4
  "description": "Postcss processor, transform string or postcss file to css",
5
5
  "keywords": [
6
6
  "postcss",
@@ -17,17 +17,17 @@
17
17
  "directory": "packages/processors/postcss-processor"
18
18
  },
19
19
  "dependencies": {
20
- "@ecopages/file-system": "0.2.0-alpha.9",
21
- "@ecopages/logger": "latest",
22
- "autoprefixer": "^10.4.0",
23
- "browserslist": "^4.28.1",
24
- "cssnano": "^6.0.0",
25
- "postcss": "^8.4.32",
26
- "postcss-import": "^15.0.0",
20
+ "@ecopages/file-system": "0.2.0-beta.0",
21
+ "@ecopages/logger": "^0.2.3",
22
+ "autoprefixer": "^10.5.0",
23
+ "browserslist": "^4.28.2",
24
+ "cssnano": "^7.1.9",
25
+ "postcss": "^8.5.14",
26
+ "postcss-import": "^16.1.1",
27
27
  "postcss-nested": "^7.0.2"
28
28
  },
29
29
  "peerDependencies": {
30
- "@ecopages/core": "0.2.0-alpha.9",
30
+ "@ecopages/core": "0.2.0-beta.0",
31
31
  "@tailwindcss/postcss": ">=4",
32
32
  "tailwindcss": ">=3"
33
33
  },
package/src/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export * from './plugin';
2
- export * from './postcss-processor';
1
+ export * from './plugin.js';
2
+ export * from './postcss-processor.js';
package/src/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export * from "./plugin";
2
- export * from "./postcss-processor";
1
+ export * from "./plugin.js";
2
+ export * from "./postcss-processor.js";
package/src/plugin.d.ts CHANGED
@@ -2,8 +2,7 @@
2
2
  * PostCssProcessorPlugin
3
3
  * @module @ecopages/postcss-processor
4
4
  */
5
- import { Processor, type ProcessorConfig } from '@ecopages/core/plugins/processor';
6
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
5
+ import { Processor, type EcoBuildPlugin, type ProcessorConfig } from '@ecopages/core/plugins/processor';
7
6
  import type postcss from 'postcss';
8
7
  /**
9
8
  * Record of PostCSS plugins keyed by name
@@ -26,6 +25,13 @@ export interface PostCssProcessorPluginConfig {
26
25
  * Regex filter to match files to process
27
26
  */
28
27
  filter?: RegExp;
28
+ /**
29
+ * CSS entry files to rebuild when a non-CSS dependency changes.
30
+ *
31
+ * Use this when the processor watches template or script files that affect a
32
+ * known stylesheet entry, such as a Tailwind reference file.
33
+ */
34
+ dependencyEntryPaths?: string[];
29
35
  /**
30
36
  * Function to transform the contents of the file.
31
37
  * It can be handy to add a custom header or footer to the file.
@@ -67,11 +73,38 @@ export declare class PostCssProcessorPlugin extends Processor<PostCssProcessorPl
67
73
  private readonly runtimeCssCache;
68
74
  private readonly trackedCssFiles;
69
75
  private watchQueue;
76
+ /**
77
+ * Maps an imported CSS file path → set of tracked CSS entry files that import it.
78
+ * Used to resolve which parent entry files need re-processing when a dependency changes.
79
+ */
80
+ private readonly cssDependencyMap;
70
81
  private getCssFilter;
71
82
  private resolveProcessedCssPath;
72
83
  private readProcessedCssFromDist;
73
84
  private persistProcessedCss;
74
85
  private prewarmRuntimeCssCache;
86
+ /**
87
+ * Regex to match CSS @import statements and extract the path.
88
+ * Handles: @import './foo.css'; @import "./foo.css"; @import url('./foo.css');
89
+ */
90
+ private static readonly CSS_IMPORT_REGEX;
91
+ /**
92
+ * Builds the CSS dependency map by scanning tracked CSS files for @import directives.
93
+ * Maps each imported file to the set of tracked entry files that import it (directly or transitively).
94
+ */
95
+ private buildCssDependencyMap;
96
+ /**
97
+ * Extracts resolved absolute paths of CSS files imported via @import in the given CSS content.
98
+ * Recursively follows imports to capture transitive dependencies.
99
+ * It skips bare module imports like @import 'tailwindcss'.
100
+ * It recursively follows imports to capture transitive dependencies.
101
+ */
102
+ private extractCssImports;
103
+ /**
104
+ * Resolves a changed CSS file to its parent entry file(s) if it is an @import dependency.
105
+ * Returns an empty array if the file is not an import dependency (i.e., it's an entry file itself).
106
+ */
107
+ private resolveEntryFiles;
75
108
  private transformCssSync;
76
109
  private transformCssAsync;
77
110
  matchesFileFilter(filepath: string): boolean;
@@ -79,13 +112,21 @@ export declare class PostCssProcessorPlugin extends Processor<PostCssProcessorPl
79
112
  private refreshConfiguredPlugins;
80
113
  private enqueueWatchTask;
81
114
  private getTrackedCssFiles;
115
+ private getTrackedCssEntryFiles;
116
+ private getDependencyEntryFiles;
82
117
  private handleDependencyChange;
83
118
  constructor(config?: Omit<ProcessorConfig<PostCssProcessorPluginConfig>, 'name' | 'description'>);
84
119
  /**
85
120
  * Handles CSS file changes during development.
86
- * Processes the file and broadcasts a css-update event for hot reloading.
121
+ * If the file is an @import dependency, re-processes the parent entry file(s) instead.
122
+ * Broadcasts a css-update event for hot reloading.
87
123
  */
88
124
  private handleCssChange;
125
+ /**
126
+ * Processes a CSS file and broadcasts a css-update event.
127
+ * Skips broadcast if the processed output hasn't changed.
128
+ */
129
+ private processAndBroadcast;
89
130
  get buildPlugins(): EcoBuildPlugin[];
90
131
  get plugins(): EcoBuildPlugin[];
91
132
  /**
package/src/plugin.js CHANGED
@@ -2,8 +2,8 @@ import path from "node:path";
2
2
  import { fileSystem } from "@ecopages/file-system";
3
3
  import { Processor } from "@ecopages/core/plugins/processor";
4
4
  import { Logger } from "@ecopages/logger";
5
- import { PostCssProcessor } from "./postcss-processor";
6
- import { createCssLoaderPlugin } from "./runtime/css-loader-plugin";
5
+ import { PostCssProcessor } from "./postcss-processor.js";
6
+ import { createCssLoaderPlugin } from "./runtime/css-loader-plugin.js";
7
7
  const logger = new Logger("[@ecopages/postcss-processor]", {
8
8
  debug: process.env.ECOPAGES_LOGGER_DEBUG === "true"
9
9
  });
@@ -17,6 +17,11 @@ class PostCssProcessorPlugin extends Processor {
17
17
  runtimeCssCache = /* @__PURE__ */ new Map();
18
18
  trackedCssFiles = /* @__PURE__ */ new Set();
19
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();
20
25
  getCssFilter() {
21
26
  return this.options?.filter ?? PostCssProcessorPlugin.DEFAULT_OPTIONS.filter;
22
27
  }
@@ -67,6 +72,69 @@ class PostCssProcessorPlugin extends Processor {
67
72
  this.runtimeCssCache.set(filePath, processed);
68
73
  await this.persistProcessedCss(filePath, processed);
69
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);
70
138
  }
71
139
  transformCssSync(input) {
72
140
  const cached = this.runtimeCssCache.get(input.filePath);
@@ -116,11 +184,24 @@ class PostCssProcessorPlugin extends Processor {
116
184
  (filePath) => this.matchesFileFilter(filePath) && fileSystem.exists(filePath)
117
185
  );
118
186
  }
187
+ getTrackedCssEntryFiles() {
188
+ const importedCssFiles = new Set(this.cssDependencyMap.keys());
189
+ return this.getTrackedCssFiles().filter((filePath) => !importedCssFiles.has(filePath));
190
+ }
191
+ getDependencyEntryFiles() {
192
+ const configuredEntryPaths = this.options?.dependencyEntryPaths;
193
+ if (!configuredEntryPaths || configuredEntryPaths.length === 0) {
194
+ return this.getTrackedCssEntryFiles();
195
+ }
196
+ return configuredEntryPaths.filter(
197
+ (filePath) => this.matchesFileFilter(filePath) && fileSystem.exists(filePath)
198
+ );
199
+ }
119
200
  async handleDependencyChange(bridge) {
120
201
  if (!this.context) {
121
202
  return;
122
203
  }
123
- const cssFiles = this.getTrackedCssFiles();
204
+ const cssFiles = this.getDependencyEntryFiles();
124
205
  if (cssFiles.length === 0) {
125
206
  return;
126
207
  }
@@ -169,6 +250,8 @@ class PostCssProcessorPlugin extends Processor {
169
250
  onCreate: async ({ path: path2, bridge }) => {
170
251
  await this.enqueueWatchTask(async () => {
171
252
  if (this.matchesFileFilter(path2)) {
253
+ this.trackedCssFiles.add(path2);
254
+ this.buildCssDependencyMap();
172
255
  await this.handleCssChange(path2, bridge);
173
256
  return;
174
257
  }
@@ -180,6 +263,8 @@ class PostCssProcessorPlugin extends Processor {
180
263
  if (this.matchesFileFilter(path2)) {
181
264
  this.runtimeCssCache.delete(path2);
182
265
  this.trackedCssFiles.delete(path2);
266
+ this.cssDependencyMap.delete(path2);
267
+ this.buildCssDependencyMap();
183
268
  return;
184
269
  }
185
270
  await this.handleDependencyChange(bridge);
@@ -191,9 +276,28 @@ class PostCssProcessorPlugin extends Processor {
191
276
  }
192
277
  /**
193
278
  * Handles CSS file changes during development.
194
- * Processes the file and broadcasts a css-update event for hot reloading.
279
+ * If the file is an @import dependency, re-processes the parent entry file(s) instead.
280
+ * Broadcasts a css-update event for hot reloading.
195
281
  */
196
282
  async handleCssChange(filePath, bridge, refreshPlugins = true) {
283
+ if (!this.context) return;
284
+ if (!fileSystem.exists(filePath)) return;
285
+ const entryFiles = this.resolveEntryFiles(filePath);
286
+ if (entryFiles.length > 0) {
287
+ logger.debug(`CSS dependency changed: ${filePath}, re-processing ${entryFiles.length} parent(s)`);
288
+ for (const entryFile of entryFiles) {
289
+ this.runtimeCssCache.delete(entryFile);
290
+ await this.processAndBroadcast(entryFile, bridge, refreshPlugins);
291
+ }
292
+ return;
293
+ }
294
+ await this.processAndBroadcast(filePath, bridge, refreshPlugins);
295
+ }
296
+ /**
297
+ * Processes a CSS file and broadcasts a css-update event.
298
+ * Skips broadcast if the processed output hasn't changed.
299
+ */
300
+ async processAndBroadcast(filePath, bridge, refreshPlugins = true) {
197
301
  if (!this.context) return;
198
302
  if (!fileSystem.exists(filePath)) return;
199
303
  try {
@@ -281,7 +385,10 @@ class PostCssProcessorPlugin extends Processor {
281
385
  if (foundConfigPath) {
282
386
  try {
283
387
  logger.debug(`Loading PostCSS config from: ${foundConfigPath}`);
284
- const postcssConfigModule = await import(foundConfigPath);
388
+ const postcssConfigModule = await import(
389
+ /* @vite-ignore */
390
+ foundConfigPath
391
+ );
285
392
  const postcssConfig = postcssConfigModule.default || postcssConfigModule;
286
393
  if (postcssConfig && typeof postcssConfig.pluginFactories === "object" && postcssConfig.pluginFactories !== null) {
287
394
  loadedPluginFactories = postcssConfig.pluginFactories;
@@ -2,5 +2,5 @@
2
2
  * PostCSS Processor Presets
3
3
  * @module @ecopages/postcss-processor/presets
4
4
  */
5
- export { tailwindV3Preset } from './tailwind-v3';
6
- export { tailwindV4Preset, type TailwindV4PresetOptions } from './tailwind-v4';
5
+ export { tailwindV3Preset } from './tailwind-v3.js';
6
+ export { tailwindV4Preset, type TailwindV4PresetOptions } from './tailwind-v4.js';
@@ -1,5 +1,5 @@
1
- import { tailwindV3Preset } from "./tailwind-v3";
2
- import { tailwindV4Preset } from "./tailwind-v4";
1
+ import { tailwindV3Preset } from "./tailwind-v3.js";
2
+ import { tailwindV4Preset } from "./tailwind-v4.js";
3
3
  export {
4
4
  tailwindV3Preset,
5
5
  tailwindV4Preset
@@ -11,14 +11,18 @@ function tailwindV4Preset(options) {
11
11
  const autoprefixerOptions = browserslistConfig ? {} : {
12
12
  overrideBrowserslist: [">0.3%", "not ie 11", "not dead", "not op_mini all"]
13
13
  };
14
+ const createTailwindPlugin = () => {
15
+ return tailwindcss({ optimize: false });
16
+ };
14
17
  const pluginFactories = {
15
18
  "postcss-import": () => postcssImport(),
16
19
  "postcss-nested": () => postcssNested(),
17
- "@tailwindcss/postcss": () => tailwindcss({ optimize: false }),
20
+ "@tailwindcss/postcss": createTailwindPlugin,
18
21
  autoprefixer: () => autoprefixer(autoprefixerOptions),
19
22
  cssnano: () => cssnano()
20
23
  };
21
24
  return {
25
+ dependencyEntryPaths: [referencePath],
22
26
  /**
23
27
  * Instantiate the initial plugin list for the active processor instance.
24
28
  * Fresh instances can later be recreated from `pluginFactories`.
@@ -1,5 +1,5 @@
1
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
2
- import type { CssTransform } from './css-runtime-contract';
1
+ import type { EcoBuildPlugin } from '@ecopages/core/plugins/processor';
2
+ import type { CssTransform } from './css-runtime-contract.js';
3
3
  type CssLoaderOptions = {
4
4
  name: string;
5
5
  filter: RegExp;
@@ -1,4 +1,4 @@
1
- import { getFileAsBuffer } from "../postcss-processor";
1
+ import { getFileAsBuffer } from "../postcss-processor.js";
2
2
  const createCssLoaderPlugin = ({ name, filter, transform }) => ({
3
3
  name,
4
4
  setup(build) {
@@ -1,5 +1,5 @@
1
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
2
- import type { CssTransform } from './css-runtime-contract';
1
+ import type { EcoBuildPlugin } from '@ecopages/core/plugins/processor';
2
+ import type { CssTransform } from './css-runtime-contract.js';
3
3
  type BunCssLoaderOptions = {
4
4
  name: string;
5
5
  filter: RegExp;
@@ -1,4 +1,4 @@
1
- import { getFileAsBuffer } from "../postcss-processor";
1
+ import { getFileAsBuffer } from "../postcss-processor.js";
2
2
  const createBunCssLoaderPlugin = ({ name, filter, transform }) => ({
3
3
  name,
4
4
  setup(build) {
package/CHANGELOG.md DELETED
@@ -1,22 +0,0 @@
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
- - Added runtime CSS loader support and a `PostcssProcessor` class so PostCSS processing can be reused outside the plugin DSL.
12
- - Added esbuild build adapter registration and dependency graph integration to the PostCSS processor plugin.
13
-
14
- ### Bug Fixes
15
-
16
- - Rebuilt tracked stylesheets from fresh PostCSS plugin instances on non-CSS source changes so Tailwind-style utility generation picks up template edits without stale caches.
17
- - Applied `transformInput` during direct stylesheet asset processing so Tailwind v4 page CSS keeps injected `@reference` directives and preserves nested BEM selectors in preview/build output.
18
- - Disabled Tailwind v4 PostCSS optimization in the official plugin preset so preview/build no longer rewrites nested BEM selectors into invalid output.
19
-
20
- ### Tests
21
-
22
- - Added processor and preset coverage for the runtime CSS loader and build adapter flow.
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from './plugin';
2
- export * from './postcss-processor';