PostCSS dependency messages: who owns cache invalidation for dir-dependency entries across runners
0 reputation · 06 Nov 2025, 10:09 UTC
0 reputation · 06 Nov 2025, 10:09 UTC
I'm writing a PostCSS plugin that scans a folder of template files and registers them as build inputs. The documented approach is to push { type: 'dir-dependency', dir: ..., glob: ... } objects onto result.messages so the runner (postcss-loader, gulp-postcss, etc.) knows to recompile when anything in that directory changes.
What I can't pin down is the boundary of responsibility afterwards. PostCSS itself only standardizes the message format; watching, caching, and re-running all live in the runner. The docs don't appear to define how a runner should treat a dir-dependency when deciding whether a cached transform result is still valid — for example, whether a new file matching the glob must invalidate the cache, or whether only mtime changes on existing files count. I also see conflicting guidance on whether dir should be absolute or relative to the from option, and some runners historically ignored the glob field entirely.
Assume PostCSS 8.x and a webpack/postcss-loader setup, plugin written with an async OnceExit hook.
Is there any runner-agnostic convention for cache invalidation of dir-dependency messages, or must the plugin defensively register every matched file as an individual dependency instead? And is absolute vs. relative pathing specified anywhere, or purely per-runner behavior I need to test?
35275 reputation · 06 Nov 2025, 21:32 UTC
There is no runner-agnostic invalidation contract beyond the message format itself. PostCSS 8 standardizes only the shape of { type: 'dir-dependency', dir, glob } on result.messages; watching, caching, and deciding what counts as a change are entirely the runner's job. Your plugin declares intent; it cannot enforce behavior. In practice, postcss-loader translates dir-dependency into webpack's addContextDependency, which watches directory membership — so a new file matching the directory (not just mtime changes on existing files) should trigger a rebuild. But that semantic is webpack's, not PostCSS's.
Think of the message as a hint with an implied contract:
OnceExit is fine — messages just need to be on the result before the runner reads them). Nothing else.result.messages, register each entry with its watcher/cache layer, and re-run the transform when the watched unit changes. For dir-dependency the watched unit is directory membership plus contents, so add/remove/rename must invalidate — otherwise the message type would be indistinguishable from a plain dependency.Because PostCSS never executes the watching, a runner that ignores dir-dependency (or ignores glob) fails silently — you get stale output with no error. That is the real risk you're identifying, and it's a runner bug/limitation, not something fixable from inside the plugin.
The format docs historically describe dir as a directory path without mandating resolution rules, and runners register it against their own watcher. The safe, de-facto convention is: always emit absolute paths, resolved from result.opts.from (falling back to process.cwd() if from is undefined):
const path = require('path');
const base = result.opts.from ? path.dirname(result.opts.from) : process.cwd();
result.messages.push({
type: 'dir-dependency',
dir: path.resolve(base, templatesDir),
glob: '**/*.html'
});Relative paths resolve against whatever the runner happens to use as its working directory, which differs between postcss-loader, gulp-postcss, and the PostCSS CLI. Absolute paths remove that variable entirely.
dependency fallback?For a webpack/postcss-loader setup on maintained versions: no — context dependencies are handled, and adding hundreds of individual dependency messages adds watcher overhead for no gain. The defensive enumeration is only worth it if you must support runners with known spotty dir-dependency support (some older gulp/grunt wrappers). A pragmatic middle ground: emit the dir-dependency, and document which runners you've verified.
Don't trust the matrix — test the runner you ship with:
dir is absolute.One caveat: in one-shot (non-watch) builds there is no cache to invalidate, so none of this matters until a persistent cache or watcher is involved — with webpack's persistent caching, though, the context dependency is exactly what keeps the cache honest across restarts.
Use comments to ask for clarification. Post a solution as an answer.
5,730 reputation · 06 Nov 2025, 22:03 UTC
PostCSS 8.x treats result.messages as an output contract only. The core pipeline returns the Result and does not watch files, cache transforms, or invalidate anything. Cache invalidation for type: 'dir-dependency' is owned entirely by the runner that consumes the Result.
That means the glob field is advisory and not interpreted by PostCSS. Runners may normalize dir to absolute paths, deduplicate entries, or ignore glob for performance, so the effective invalidation granularity is runner-specific. If deterministic invalidation is required across runners, emitting individual dependency messages for the matched files is the only portable fallback.