Skip to content

[heft-sass-plugin] Emit declaration source maps for generated typings - #5909

Open
mikedelgaudio wants to merge 2 commits into
microsoft:mainfrom
mikedelgaudio:feature/heft-sass-plugin-declaration-maps
Open

[heft-sass-plugin] Emit declaration source maps for generated typings#5909
mikedelgaudio wants to merge 2 commits into
microsoft:mainfrom
mikedelgaudio:feature/heft-sass-plugin-declaration-maps

Conversation

@mikedelgaudio

Copy link
Copy Markdown

Summary

Fixes #5908

Sass typings are written into a folder that rootDirs merges into the source tree, so the language
service only ever sees the generated .d.ts. "Go to definition" on a CSS module class therefore
stops at the generated declaration instead of opening the rule that declares it.

This adds an opt-in generateDeclarationMaps option that emits a .d.ts.map beside each generated
typings file. It is off by default, and when disabled the output is unchanged.

This is the Sass counterpart to #5906 / #5907, which cover localization typings. It is stacked on
#5907
— the shared declaration-map primitives live in @rushstack/typings-generator, so this
branch contains that commit and should merge after it.

Details

The interesting part is obtaining accurate positions. Sass is compiled before PostCSS runs, so
PostCSS positions refer to the compiled CSS rather than the stylesheet. The chain is:

  1. Request a source map from Sass when the option is enabled.
  2. Record where each class selector appears in the compiled CSS, using a PostCSS plugin registered
    before postcss-modules (which rewrites class names).
  3. Translate that position back through the Sass source map to the original stylesheet.
  4. createDTS reports the line it emits each declaration on, so the generated side needs no parsing
    of its own output.

Because the lookup happens in compiled-CSS order, this gets two cases right that a text search over
the stylesheet cannot:

  • A class declared in an @imported partial resolves into that partial. It does not appear in
    the importing file at all.
  • A class restated inside a @media or theme block still resolves to its primary rule, with no
    indentation heuristics.

Shared vs. Sass-specific code

Per the two questions raised in #5908, and to avoid a third copy of this logic appearing downstream:

Package Change
@rushstack/typings-generator serializeDeclarationMap now accepts multiple sources, and IDeclarationMapping gains an optional sourceIndex — needed because Sass declarations can originate from several files. Adds decodeMappings and originalPositionFor alongside the existing encoder.
@rushstack/heft-sass-plugin New SassDeclarationMaps.ts with the PostCSS recorder and the resolve-through-Sass-map step, exported from the package index so other Sass typings generators can reuse it. Adds a dependency on typings-generator.

Notes for reviewers:

  • Backwards compatible. serializeDeclarationMap's sources parameter widens from string to
    string | readonly string[], and sourceIndex defaults to 0, so existing callers are
    unaffected. The heft-localization-typings-plugin path is unchanged.
  • sourceMapIncludeSources is not forced on. Enabling declaration maps requests a Sass source
    map, but only embeds sources when sourceMap: true was already set, to avoid inflating output.
  • Happy to split the typings-generator half into its own PR if you would prefer to review the
    shared primitives separately from the Sass plugin.

How it was tested

Validated on Linux / Node 22.

  • rush build and rush test clean for heft-sass-plugin, typings-generator, and
    localization-utilities.
  • heft-sass-plugin: 60/60 tests pass, including 3 new cases that decode the emitted map and
    assert the resolved line:
    • each declaration resolves to the rule that declares it;
    • a class declared in a partial resolves into _partial-with-class.scss, not the importing file;
    • a class restated inside @media resolves to its primary rule, not the restatement;
    • no .d.ts.map is emitted when the option is disabled.
  • typings-generator: the 26 existing tests from [typings-generator] Emit declaration source maps for generated typings #5907 still pass unmodified, confirming the
    multi-source change did not regress the single-source path.
  • Separately verified the same approach against a live tsserver in a large internal monorepo:
    go-to-definition on a CSS module class resolved to the .scss rule rather than the generated
    .d.ts.

Impacted documentation

  • heft-sass-plugin gains a generateDeclarationMaps option; the plugin's configuration docs would
    need a new entry. The JSON schema is updated in this PR.
  • rush change files are included, and common/reviews/api/typings-generator.api.md is updated.

Generated typings such as .resx and .scss declarations are merged into the
source tree via "rootDirs", so the TypeScript language service only ever sees
the generated .d.ts. Alt-clicking a localized string therefore navigates to the
generated declaration rather than the file that declares it.

Add opt-in declaration source map generation to TypingsGenerator. The generator
already composes its output line by line, so it knows the exact position of
every emitted declaration and does not need to parse its own output. The map is
serialized per output folder so that the relative path back to the source is
correct for secondary folders as well.

StringValuesTypingsGenerator records those positions from the new optional
IStringValueTyping.sourcePosition, parseResx populates it from the xmldoc
element, and heft-localization-typings-plugin exposes a generateDeclarationMaps
option. Parsers that do not supply positions are unaffected, and no map is
emitted unless the feature is enabled and positions are available.
Sass typings are merged into the source tree via rootDirs, so the language
service only sees the generated .d.ts and go-to-definition on a CSS module class
stops there instead of opening the rule that declares it.

Add an opt-in generateDeclarationMaps option that emits a .d.ts.map beside each
generated typings file. Positions are obtained by recording where each class
selector appears in the compiled CSS, before postcss-modules rewrites names, and
translating that position back through the Sass source map. A class declared in
an imported partial therefore resolves into that partial, and a class restated
inside a media query still resolves to its top-level rule.

The shared pieces live in typings-generator: serializeDeclarationMap now accepts
multiple sources, and decodeMappings/originalPositionFor are exported for
generators that compile their input. The Sass-specific helpers are exported from
heft-sass-plugin so that other Sass typings generators can reuse them rather
than reimplement the same chain.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Needs triage

Development

Successfully merging this pull request may close these issues.

[heft-sass-plugin] Emit declaration source maps so "go to definition" resolves to the stylesheet

1 participant