Skip to content

Source map regeneration: evaluated, not planned #25

Description

@j9t

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

  1. 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.
  2. 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.
  3. 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.cssstyles.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions