Skip to content

Allow content mapper registrations to filter their files with an include glob, inherit default extension from manifest - #65047

Open
Andrew Branch (andrewbranch) wants to merge 8 commits into
microsoft:mainfrom
andrewbranch:content-mapper-include-glob
Open

Andrew Branch (andrewbranch) wants to merge 8 commits into
microsoft:mainfrom
andrewbranch:content-mapper-include-glob

Conversation

@andrewbranch

@andrewbranch Andrew Branch (andrewbranch) commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Fixes #64362

Limitations

Normally content mappers that specify outputExtensions participate fully in rewriteRelativeImportExtensions. As an example, the Ember content mapper registers "outputExtensions": { ".gts": ".js" } because an external build tool compiles .gts files to .js outputs. This controls two behaviors by TypeScript: (1) the declaration file output for a Component.gts file will be Component.d.ts, to correspond with the Component.js file produced by the external tool; and (2) the build will require --rewriteRelativeImportExtensions to be enabled so an import of

import Component from "./Component.gts";

can be rewritten as import Component from "./Component.js" in TypeScript's output. Dynamic imports like

await import(path + "/Component.gts");

also get routed through a helper that replaces the trailing .gts with .js.

This logic gets more complicated when a content mapper that uses outputExtensions also uses an include filter, because it's harder to tell, in a single-file emit context, whether the file resolved by a given import is governed by a content mapper registration that contributes a module specifier rewrite.

Restrictions on imports affected by rootDirs and moduleSuffixes

These two options can cause false negative or false positives on whether a module specifier should be rewritten that can only be resolved by probing module resolution, which breaks the rule that module specifiers must be safely rewritable by looking only at configuration and the single file being processed. An single example exercising the ambiguity created by each:

{
  "compilerOptions": {
    "rootDirs": ["src", "types"],
    "moduleSuffixes": [".ios", ".android", ""],
  },
  "contentMappers": [{
    "package": "example-mapper",
    "extensions": [".foo"],
    "include": ["src", "**/*.android.foo"],
    "outputExtensions": { ".foo": ".js" },
  }]
}
// src/main.ts
import Component from "./Component.foo";

To decide whether "./Component.foo" should be rewritten to "./Component.js", you have to know what file it actually resolved to:

  • If it's found in src, it should be rewritten
  • but if it's found in types, it shouldn't,
  • UNLESS types/Component.ios.foo does not exist AND types/Component.android.foo does exist

Since this is undecidable in a single-file context, we use the following process to decide whether to issue a diagnostic:

  1. Assume a naive path-based resolution of "./Component.foo" from src to be src/Component.foo
  2. Test that naive resolution against the content mapper's include—it passes, so assume that rewriting should happen
  3. Check what the actual resolved file from the program was and test that against the content mapper's include
  4. If the answer from (3) disagrees with the answer from (2), issue an error.

This process means you can use moduleSuffixes and rootDirs in combination with an extension-rewriting content mapper, just not in a way where the presence or absence of other files on disk can change the answer of whether a given import resolves to a content-mapped file or not.

Restrictions on dynamic imports

Non-literal import calls and require calls get a runtime helper with rewriteRelativeImportExtensions such that await import(dir + "./Component.foo") can have its .foo suffix rewritten to .js at runtime. If a content mapper uses both outputExtensions and an include, the include glob would have to be evaluated at runtime inside that helper to determine whether the rewriting should happen. That would be more complex and expensive than what it's worth, so using a dynamic import under these circumstances requires setting --disableDynamicImportRewriting (#65289) to make it explicit that you are opting out of getting the runtime rewriting helper.

This comment was marked as resolved.

This comment was marked as resolved.

Use manifest extensions when a registration omits them, while preserving explicit overrides. Carry these defaults through extension contributions, validation, and config schemas.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Narrow mapper claims with tsconfig-style include patterns. Use the winning mapper consistently for discovery, resolution, language services, and output naming, and reject incompatible output mappings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

# Conflicts:
#	packages/typescript/src/vscode/extensionApi.ts
#	packages/vscode-typescript/src/contentMapperContributions.ts
#	packages/vscode-typescript/test/contentMapperContributions.test.ts
#	tools/scripts/lsp/generate.mts
#	tools/scripts/tsc/options-schema.ts
#	tools/scripts/tsc/options.test.ts
#	tsc/internal/diagnostics/diagnosticMessages.json

This comment was marked as resolved.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Declaration emit and resolution use different suffixes for overlapping filtered extensions, preventing declaration-only consumers from resolving imports even when the emitted declaration exists.

1 open issue
4 resolved since last review
2 files not reviewed
  • tsc/internal/diagnostics/diagnostics_generated.go: Generated file
  • tsc/internal/lsp/lsproto/lsp_generated.go: Generated file

Review effort: Balanced

Comment thread tsc/internal/outputpaths/outputpaths.go Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed because mapper inclusion and extension-fallback interactions can break module resolution, while relative-pattern registrations introduce a separate client compatibility risk.

No open issues 🎉

1 resolved since last review
2 files not reviewed
  • tsc/internal/diagnostics/diagnostics_generated.go: Generated file
  • tsc/internal/lsp/lsproto/lsp_generated.go: Generated file

Review effort: Balanced

@andrewbranch
Andrew Branch (andrewbranch) marked this pull request as ready for review November 10, 2026 00:31

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Content mappers: more granular resolution, and default extensions in the manifest

2 participants