Skip to content

[typings-generator] Emit declaration source maps for generated typings - #5907

Open
mikedelgaudio wants to merge 1 commit into
microsoft:mainfrom
mikedelgaudio:feature/typings-generator-declaration-maps
Open

[typings-generator] Emit declaration source maps for generated typings#5907
mikedelgaudio wants to merge 1 commit into
microsoft:mainfrom
mikedelgaudio:feature/typings-generator-declaration-maps

Conversation

@mikedelgaudio

@mikedelgaudio mikedelgaudio commented Jul 29, 2026

Copy link
Copy Markdown

Summary

Fixes #5906

Generated typings are merged into the source tree via the TypeScript rootDirs option, so the
language service only ever sees the generated .d.ts. As a result, "go to definition" on a
localized string — or on its import specifier — navigates to the generated declaration file instead
of the .resx that actually declares the string. The generated file is a build artifact, so this is
a dead end for the developer.

This adds opt-in declaration source map (.d.ts.map) emit to @rushstack/typings-generator, which
is the mechanism TypeScript already uses to redirect "go to definition" from a .d.ts to real
source. With it enabled, alt-clicking a localized string opens the .resx on the
<data name="..."> element that declares it.

The feature is off by default and no map is emitted unless it is enabled and the parser supplies
source positions, so existing behavior is unchanged.

Details

The change is made in typings-generator rather than in each consuming plugin, because the
generator composes its output line by line and therefore already knows the exact position of every
declaration it emits. Solving it downstream would require re-parsing the generator's own output with
a regex, which would silently break whenever the emit format changed.

Scope note: this PR wires the feature up for localization typings only.
@rushstack/heft-static-asset-typings-plugin already builds on typings-generator, so it should be
able to adopt this by supplying positions and exposing the option.
@rushstack/heft-sass-plugin is a separate case — it emits its own .d.ts directly from
SassProcessor and does not depend on typings-generator, so SCSS would need a follow-up change.
serializeDeclarationMap is exported so that plugin can reuse the encoder rather than reimplement
it. I'm happy to send that follow-up if the approach here looks right.

Package Change
@rushstack/typings-generator New DeclarationMap.ts (source map v3 + Base64 VLQ encoding) and serializeDeclarationMap. New opt-in generateDeclarationMaps option on ITypingsGeneratorBaseOptions. Parsers may now return IGeneratedTypings (typings text plus declaration positions) instead of a bare string. StringValuesTypingsGenerator records each declaration's position as it emits the line.
@rushstack/localization-utilities ILocalizedString.sourcePosition, populated by parseResx from the xmldoc element.
@rushstack/heft-localization-typings-plugin generateDeclarationMaps option, plus its config schema entry.

Notes for reviewers:

  • Backwards compatible. The parser return type is widened from string | undefined to
    string | IGeneratedTypings | undefined, so existing parsers that return a string continue to
    compile and behave identically. sourcePosition and generateDeclarationMaps are both optional.
  • parseResx now always reports sourcePosition, regardless of whether declaration maps are
    enabled. This keeps the parser free of any dependency on generator configuration, at the cost of
    adding an optional field to its output (hence the snapshot update below). Happy to gate it behind
    an option instead if you would prefer the parsed output to stay unchanged by default.
  • Multiple output folders. The map is serialized once per output folder so the relative path
    back to the source is correct for secondary folders, not just the primary one.
  • An explicit //# sourceMappingURL= comment is emitted rather than relying on tsserver's
    adjacent-file probing. This is deliberate — it makes resolution unambiguous — but it does mean the
    comment appears in the generated output, so please flag it if you would prefer the implicit form.
  • column: 0 is used for .resx positions. xmldoc reports column at the end of the
    element's open tag rather than its start (verified empirically), so the start of the line is used
    instead. The line number is exact, which is what determines where the editor lands.

How it was tested

Validated against main at e9ed0088 on Linux / Node 22.

  • Added DeclarationMap.test.ts — 7 new unit tests covering VLQ encoding, mapping serialization,
    multi-folder relative paths, and the disabled / no-positions cases.

  • rush build and rush test are clean for all three packages:

    Package Tests
    @rushstack/typings-generator 26 passing (7 new + 19 existing StringValuesTypingsGenerator, unmodified)
    @rushstack/localization-utilities 18 passing
    @rushstack/heft-localization-typings-plugin no test files
  • Snapshot update: parseResx now reports sourcePosition for each string, which changes the
    parsed output shape, so the 10 snapshots in
    libraries/localization-utilities/src/parsers/test/__snapshots__/parseResx.test.ts.snap were
    regenerated. The only delta in each is the added sourcePosition object — no values changed.

  • End-to-end against a real .resx using the built library, then queried a live tsserver against
    a fixture configured with rootDirs:

    import specifier  -> Strings.resx:1
    AddComment        -> Strings.resx:3   (<data name="AddComment">)
    CopySelection     -> Strings.resx:7   (<data name="CopySelection">)
    
  • Verified the multi-folder case resolves to ../src/Strings.resx for the primary output folder and
    ../../src/Strings.resx for a secondary one.

  • Confirmed that with generateDeclarationMaps unset, no .d.ts.map is emitted and the generated
    .d.ts is byte-identical to before this change.

Impacted documentation

  • heft-localization-typings-plugin gains a generateDeclarationMaps config option, so the plugin's
    configuration docs would need a new entry. The JSON schema in
    src/schemas/heft-localization-typings-plugin.schema.json has been updated in this PR.
  • rush change files are included for all three packages, and the API review files
    (common/reviews/api/typings-generator.api.md, localization-utilities.api.md) are 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.
@mikedelgaudio

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="Microsoft"

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.

[typings-generator] Support declaration source maps so "go to definition" resolves to the source file

1 participant