Paper trail wrt source map regeneration, AI-supported:
--fix and dedup() rewrite CSS via postcss.parse() + root.toString() (src/index.js), bypassing PostCSS's map machinery. Any style sheet carrying a sourceMappingURL therefore ends up with a map that no longer describes it. Since 1.9.0 both surfaces report this (CLI warning, sourceMapStale); neither fixes it, and that's the intended end state.
Filed as a record: regeneration was designed and prototyped, then backed out. Reopen if someone actually hits the gap—the notes below are what was learned, so a second attempt doesn't start cold.
Why not
- It writes to a file the user never named.
--fix currently touches exactly the paths given on the command line. Regeneration writes to a path parsed out of a comment, which can point anywhere (../../shared/maps/app.css.map, a symlink). Guarding it properly is more code and more edge cases than the feature.
- The result is unverifiable, and it's destructive. A stale map is at least a real map. The replacement is generated, with no way to check its accuracy—notably for nodes CSS Dedup synthesizes, where a merged rule's selector list maps to the last occurrence. If that's subtly wrong the user is worse off and the original is gone.
- Silent quality cliff when the previous map is absent. Verified: if the
.map the annotation points at isn't on disk at rewrite time, PostCSS does not error. It emits a map whose only source is the minified file itself—mapping deduped CSS back to CSS you already have—with sourcesContent populated, so it looks healthy. We'd write that to a path where no file existed before, with no warning and no exit code.
Point 3 also shrinks the audience that motivated the feature. Regeneration only helps if the original map is present at rewrite time, which rules out the "post-processing a bundle I didn't build" case. What remains: you built it, you kept the map, and you can't reorder your pipeline—a group that can mostly use css-dedup/plugin instead.
Current answer for users
Run CSS Dedup before the minifier, or in-pipeline via css-dedup/plugin. The plugin mutates the root in OnceExit, so the host pipeline's process(css, { map }) emits a correct chained map. Documented in the README's source map note.
If revisited: what's already known
Route the writing call through root.toResult({ to, map })—no restructuring needed, since dedupRoot already mutates a parsed root.
- Only the one call site that writes needs it; the other
dedup() calls compute previews that get discarded. Any map option must be forced off there, which is a standing drift risk as call sites are added.
- Chaining is free.
postcss.parse(css, { from }) picks up the previous map from the annotation and reads the external .map off disk, so the generated map reaches the original Sass/Less rather than stopping at the built CSS. Verified.
map: { annotation: false } keeps output byte-identical. clearAnnotation() returns early on annotation: false, so the source's own comment survives verbatim—trailing newline and all—while the map is still generated. Without it, PostCSS strips and re-appends the annotation, changing whitespace and dropping the trailing newline. Verified both ways.
- Never call
process()/toResult() without an explicit map option. MapGenerator enables itself whenever a previous map exists, even with no option passed, and defaults to inline—so a naive call silently converts an external sidecar into a base64 blob. Use map: false when there's no annotation.
- Preserve the annotation URL verbatim; it needn't match the style sheet's name (
styles.min.css → styles.css.map is normal). Capture it from the comment rather than letting PostCSS derive <to>.map.
- Inline (
data:) maps must be re-embedded, so byte counts need remeasuring against the generated CSS—bytes.after from dedupRoot() won't match.
- stdin has no
from, no to, and nowhere to put a .map. Skipping map generation there was the proposed answer.
- Formatting is safe:
--fix on minified input stays minified, since PostCSS infers new nodes' raws from their siblings. Verified.
Out of scope regardless
Generating maps for style sheets that never had one—that's CSS Dedup as a map producer, and the "original" would be a CSS file you still have. Also stripping the sourceMappingURL comment: it doesn't delete the .map, it orphans it, trading degraded debugging for none.
Paper trail wrt source map regeneration, AI-supported:
--fixanddedup()rewrite CSS viapostcss.parse()+root.toString()(src/index.js), bypassing PostCSS's map machinery. Any style sheet carrying asourceMappingURLtherefore ends up with a map that no longer describes it. Since 1.9.0 both surfaces report this (CLI warning,sourceMapStale); neither fixes it, and that's the intended end state.Filed as a record: regeneration was designed and prototyped, then backed out. Reopen if someone actually hits the gap—the notes below are what was learned, so a second attempt doesn't start cold.
Why not
--fixcurrently touches exactly the paths given on the command line. Regeneration writes to a path parsed out of a comment, which can point anywhere (../../shared/maps/app.css.map, a symlink). Guarding it properly is more code and more edge cases than the feature..mapthe annotation points at isn't on disk at rewrite time, PostCSS does not error. It emits a map whose only source is the minified file itself—mapping deduped CSS back to CSS you already have—withsourcesContentpopulated, so it looks healthy. We'd write that to a path where no file existed before, with no warning and no exit code.Point 3 also shrinks the audience that motivated the feature. Regeneration only helps if the original map is present at rewrite time, which rules out the "post-processing a bundle I didn't build" case. What remains: you built it, you kept the map, and you can't reorder your pipeline—a group that can mostly use
css-dedup/plugininstead.Current answer for users
Run CSS Dedup before the minifier, or in-pipeline via
css-dedup/plugin. The plugin mutates the root inOnceExit, so the host pipeline'sprocess(css, { map })emits a correct chained map. Documented in the README's source map note.If revisited: what's already known
Route the writing call through
root.toResult({ to, map })—no restructuring needed, sincededupRootalready mutates a parsed root.dedup()calls compute previews that get discarded. Any map option must be forced off there, which is a standing drift risk as call sites are added.postcss.parse(css, { from })picks up the previous map from the annotation and reads the external.mapoff disk, so the generated map reaches the original Sass/Less rather than stopping at the built CSS. Verified.map: { annotation: false }keeps output byte-identical.clearAnnotation()returns early onannotation: false, so the source's own comment survives verbatim—trailing newline and all—while the map is still generated. Without it, PostCSS strips and re-appends the annotation, changing whitespace and dropping the trailing newline. Verified both ways.process()/toResult()without an explicitmapoption.MapGeneratorenables itself whenever a previous map exists, even with no option passed, and defaults to inline—so a naive call silently converts an external sidecar into a base64 blob. Usemap: falsewhen there's no annotation.styles.min.css→styles.css.mapis normal). Capture it from the comment rather than letting PostCSS derive<to>.map.data:) maps must be re-embedded, so byte counts need remeasuring against the generated CSS—bytes.afterfromdedupRoot()won't match.from, noto, and nowhere to put a.map. Skipping map generation there was the proposed answer.--fixon minified input stays minified, since PostCSS infers new nodes' raws from their siblings. Verified.Out of scope regardless
Generating maps for style sheets that never had one—that's CSS Dedup as a map producer, and the "original" would be a CSS file you still have. Also stripping the
sourceMappingURLcomment: it doesn't delete the.map, it orphans it, trading degraded debugging for none.