镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Add content mapper output extensions - #64581

Merged
Andrew Branch (andrewbranch) merged 14 commits into
microsoft:mainfrom
andrewbranch:content-mapper-output-extensions
Oct 13, 2026
Merged

Andrew Branch (andrewbranch) merged 14 commits into
microsoft:mainfrom
andrewbranch:content-mapper-output-extensions

Conversation

@andrewbranch

Copy link
Copy Markdown
Member

Fixes #64053

This makes TypeScript content mapping a better citizen in a system where another tool is going to emit content mapped files to disk. The current system basically assumes a bundler or running the source in place.

A content mapper's package.json can now specify typescript.contentMapper.outputExtensions, a mapping from a content mapped file's original extension to the output extension that another tool will emit:

{
  "name": "ember-content-mapper",
  "typescript": {
    "contentMapper": {
      "exec": ["node", "server.js"],
      "outputExtensions": {
        ".gts": ".js"
      }
    }
  }
}

The same configuration is supported in tsconfig.json as an override (the entire mapping is overridden; keys are not merged):

{
  "contentMappers": [
    {
      "package": "ember-content-mapper",
      "extensions": [".gts"],
      "outputExtensions": {
        ".gts": ".js"
      }
    }
  ]
}

This causes:

  • The declaration file output for Component.gts to be Component.d.ts instead of Component.d.gts.ts, because there is expected to be a Component.js (that someone else will create).

  • ⚠️ The program that uses this content mapper must set rewriteRelativeImportExtensions: true. Why? If configuration indicates that Page.gts is going to be emitted as Page.js, then a plain TypeScript file in the same program that imports it:

    // routes.ts
    import Page from "./Page.gts";

    must be emitted as

    // routes.js
    import Page from "./Page.js";

    which is exactly what rewriteRelativeImportExtensions does.

    The alternative would be to import from "./Page.js" or even "./Page" depending on module resolution settings, i.e., an output-compatible path. That request is Content mappers: let registered extensions take part in extensionless module lookup #64549, and while this PR allows us space to consider that in the future, I strongly prefer imports that will trigger a content mapper be immediately recognizable by file extension.

Copilot AI balanced review requested due to automatic review settings October 1, 2026 23:07
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Oct 1, 2026
@typescript-automation typescript-automation Bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Oct 1, 2026

This comment was marked as resolved.

This comment was marked as resolved.

Preserve source phase import diagnostic codes and renumber content mapper output diagnostics; regenerate outputs and update the baseline.

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

This comment was marked as resolved.

This comment was marked as low quality.

Preserve content mapper output extensions in the relocated VS Code extension API.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adapt content mapper rewrites, output paths, hosts, and regression tests to strongly typed paths and case sensitivity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread tsc/internal/checker/nodebuilderimpl.go Outdated

@jakebailey Jake Bailey (jakebailey) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the casing stuff might be out of date after the typed paths PR?

Comment thread tsc/internal/compiler/emitter.go Outdated
Comment thread tsc/internal/core/core.go Outdated
Comment thread tsc/internal/outputpaths/outputpaths.go Outdated
Comment thread tsc/internal/printer/helpers.go
@leonidaz

Copy link
Copy Markdown

Andrew Branch (@andrewbranch) Thanks for working on this. The output-extension support is a useful step forward.

I strongly prefer imports that will trigger a content mapper be immediately recognizable by file extension.

I understand the preference for making the source format visible, but I don’t think that should rule out extensionless resolution when a project explicitly configures it and its build tool supports it.

TypeScript already allows import Card from "./Card" to resolve to Card.tsx in the appropriate resolution modes. That import does not reveal that the source contains JSX or requires JSX processing. Why should components authored in a registered, mapper-supported format have to expose that implementation detail in every import? The mechanisms differ, but the developer-facing expectation seems equivalent. [TypeScript’s extension-substitution rules](https://www.typescriptlang.org/docs/handbook/modules/reference.html#file-extension-substitution)

More fundamentally, moduleResolution: "bundler" is intended to model the bundler’s resolution behavior. When the bundler is configured to resolve ./Card to Card.tsrx, rejecting that import in TypeScript creates a mismatch between the type checker and a working build. This is particularly difficult to justify for noEmit projects, where TypeScript is not responsible for producing runtime imports. [Extensionless-path documentation](https://www.typescriptlang.org/docs/handbook/modules/reference.html#extensionless-relative-paths)

The proposal in #64549 is deliberately limited to registered extensions and contexts that already support extensionless lookup. The resolver would use the registered extension list; the mapper would still process the file after resolution selects it. Node ESM’s explicit-extension requirements would remain intact.

There are practical benefits beyond shorter imports. Extensionless paths let a component change source format without requiring changes throughout its import graph. They also preserve existing conventions in projects whose language tools already support this through Volar, avoiding import rewrites solely to migrate to TypeScript 7.

The precedence and declaration-emission questions deserve explicit answers. However, they seem like reasons to define the feature’s constraints rather than require source extensions universally, especially when declaration emit is irrelevant to a noEmit application.

I really hope you would consider an opt-in setting on a contentMappers entry. That would let projects request behavior consistent with their build tooling while keeping explicit extensions available as a project convention.

Preserve the async disposal diagnostic code and renumber content mapper diagnostics; regenerate outputs and update the affected baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Preserve custom-resolution validation behavior, renumber content mapper diagnostics, and regenerate the affected baseline.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread tsc/internal/checker/checker.go
nodebuilder.FlagsNoTruncation

const declarationEmitInternalNodeBuilderFlags = nodebuilder.InternalFlagsAllowUnresolvedNames
const declarationEmitInternalNodeBuilderFlags = nodebuilder.InternalFlagsAllowUnresolvedNames | nodebuilder.InternalFlagsRewriteModuleSpecifiers

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When would we ever want that flag off if it's config driven anyway?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We don't want the flag enabled in the type printback node builders for diagnostics / LSP (types should display as import("./Card.astro"), not import("./Card.js"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Curious if this needs to not be internal for those printing types back for other reasons, but maybe not

Comment thread tsc/internal/tspath/extensionrewrite.go
Comment thread tsc/internal/contentmapper/contentmapper.go Outdated

@jakebailey Jake Bailey (jakebailey) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Code LGTM if the design is what we all decided on.

@github-project-automation github-project-automation Bot moved this from Not started to Needs merge in PR Backlog Oct 13, 2026

func TestResolveContentMapperManifestRejectsMalformedOutputExtensions(t *testing.T) {
t.Parallel()
for _, value := range []string{`"invalid"`, `null`, `[]`, `{".vue": 1}`, `{".vue": null}`, `{".vue/foo": ".js"}`, `{".vue\\foo": ".js"}`, `{".vue": ".js/foo"}`, `{".vue": ".js\\foo"}`} {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We actually discussed and said that null was an acceptable output value.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is in the package.json, not the tsconfig. f78aff7 adds null handling for tsconfig.

@andrewbranch
Andrew Branch (andrewbranch) added this pull request to the merge queue Oct 13, 2026
Merged via the queue into microsoft:main with commit c9b2c8e Oct 13, 2026
29 checks passed
@andrewbranch
Andrew Branch (andrewbranch) deleted the content-mapper-output-extensions branch October 13, 2026 23:38
@github-project-automation github-project-automation Bot moved this from Needs merge to Done in PR Backlog Oct 13, 2026
Colin McDonnell (colinhacks) added a commit to colinhacks/motherload that referenced this pull request Oct 20, 2026
…, the maintainer's decision of 2026-10-19: microsoft/TypeScript#64581, merged 2026-10-13 and carried by the release-7.1 branch cut as 7.1.0-beta on 2026-10-16, refuses a content mapper extension that ends in a built-in one (`.ts`, `.js`, `.json` and their kin) with TS18066, the error `.json` itself gets. Measured on 2026-10-19 with a probe mapper registering `.schema.json`, `.sql` and `.pg.sql`: 7.1.0-dev.20260929.1 accepted all three, and 7.1.0-dev.20261019.1 refused `.schema.json` alone; the pinned 7.1.0-dev.20261002.1 predates the rule. `.jsonschema` was chosen over `.schema.jsonc`, over asking TypeScript for an exemption and over keeping `.schema.json` until the next TypeScript bump.

src/formats.ts reads the new suffix (its FILTER and `formatOf`), as do src/plugin.ts, src/schema.ts, src/index.ts, `*.jsonschema?raw` in src/client.d.ts and the site's remark-mapper.ts; the three schema files move to lab/basic/user.jsonschema, tests/fixtures/user.jsonschema and tests/fixtures/errors/broken.jsonschema, and lab/basic, the error fixture and the tests register and expect it. The README and the site give the reason and the VS Code setting that shows the files as JSON, `"files.associations": { "*.jsonschema": "json" }`, since VS Code's JSON language does not list `.jsonschema`. DESIGN.md gains a section on the suffix and corrects its notes on plain `.json` and the TypeScript build; PLAN.md records the decision, corrects its content mapper facts, and moves its open question about Vite to `.json5`, since Vite's JSON plugin claims ids matching `\.(?:json|json5)(?:$|\?)`, which `.jsonschema` does not.

Verified on 2026-10-19 on macOS: `nub run typecheck` passes and `nub run test` passes 57 of 57 on the pinned nightly, as before the change; on 7.1.0-dev.20261019.1 the three projects of `nub run typecheck` pass and tests/fixtures/errors gives the same diagnostics as on the pinned build, `broken.jsonschema(1,1): error motherload4` among them; and `nub run build` in site/ passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Colin McDonnell (colinhacks) added a commit to colinhacks/motherload that referenced this pull request Oct 20, 2026
… 2026-10-20, replacing the `.jsonschema` of 2026-10-19: TypeScript nightlies after 2026-10-13 refuse `.schema.json` (microsoft/TypeScript#64581, TS18066), and `.schema.jsonc` keeps "schema" in the name and is shown by editors as JSON with comments, since VS Code's jsonc language lists `.jsonc`. A schema is therefore read as JSONC: src/schema.ts parses it with `parseJsoncFile`, comments and trailing commas allowed, and the strict `parseJsonFile`, which only the schema loader called, is gone with its test; tests/schema.test.ts checks that a schema with a comment and trailing commas types and validates. `formatOf` already tests the schema suffix before `.jsonc`, so a schema reaches the schema loader and any other `.jsonc` file the data loader; FILTER, src/client.d.ts (`*.schema.jsonc?raw`), the site's remark-mapper.ts, the three fixture files, lab/basic, the error fixture, the README and the site follow, the site's schema examples now fenced as `jsonc`.

DESIGN.md's section on the suffix records the decision and one cost measured on 2026-10-20 with `import user from "./user.schema.jsonc"` in lab/basic: Node with no loader stops with ERR_UNKNOWN_FILE_EXTENSION, but nub 0.9.6 and Bun 1.4.3 with no loader read the file as JSONC data, so the default export is the schema object and `user.parse` is undefined though the types say it is a validator; with motherload/register (Node and nub through --import, Bun through --preload) all three give the validator module. PLAN.md records the decision and adds that cost to its open question on nub's built-in loaders.

Verified on 2026-10-20 on macOS: `nub run typecheck` passes and `nub run test` passes 57 of 57 on the pinned 7.1.0-dev.20261002.1; on 7.1.0-dev.20261019.1 the three projects of `nub run typecheck` pass and tests/fixtures/errors gives the same diagnostics as on the pinned build, `broken.schema.jsonc(1,1): error motherload4` among them; and `nub run build` in site/ passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Colin McDonnell (colinhacks) added a commit to colinhacks/motherload that referenced this pull request Oct 21, 2026
…de `.schema.jsonc`, read as JSON with comments, the maintainer's direction of 2026-10-20: "I think we can continue supporting it for .schema.jsonc and .schema.json5. Kind of nice to be able to have comments as well." The suffix names the dialect: `parseSchemaFile` in src/schema.ts reads `.schema.json5` with the `.json5` loader's parser and `.schema.jsonc` with the `.jsonc` loader's, both texts of the module (types and code) alike, and the same text under the other suffix is a syntax error. `formatOf` and FILTER take the new suffix before `.json5`; src/plugin.ts names it, src/client.d.ts types `*.schema.json5?raw`, and the site's remark-mapper.ts lists it.

TypeScript's rule from microsoft/TypeScript#64581 reserves `/\.(?:[cm]?[jt]s|[jt]sx|json)$/i`, so neither `.jsonc` nor `.json5` is built-in. Measured on 7.1.0-dev.20261019.1: lab/basic, which now registers both schema suffixes, type-checks; tests/fixtures/errors reports its new broken.schema.json5 at (3,44); the root and lab/universal projects pass; and an entry registering `.schema.json` and `.schema.json5` gets TS18066 for the first alone.

`.jsonschema` stays an alternative not taken (DESIGN.md): no VS Code language and no Linguist language lists it, so it is plain text on GitHub and in an editor without files.associations, and VS Code's JSON service, which checks a `.schema.jsonc` file against the draft its `$schema` names, never runs on it. VS Code has no JSON5 language either, so a `.schema.json5` file gets no schema checking in the editor; Linguist highlights both.

Runtimes, measured on 2026-10-20 with `import point from "./point.schema.json5"`: Node 26.11.1 and nub 0.9.6 with no loader stop with ERR_UNKNOWN_FILE_EXTENSION (nub did the same for `.jsonc`, `.json5` and `.schema.jsonc` in that run, unlike DESIGN.md's earlier record, now noted in PLAN.md's open question on nub); Bun 1.4.3 loads it as JSON5 data; with motherload/register all three give the validator. Vite, read on its main branch and not run: its JSON transform is now rolldown's native plugin, which takes only `.json` ids, but `isJSONRequest` still sends a `.json5` or `.schema.json5` id through `ssrTransformJSON` in a module runner with `json.stringify: true`; PLAN.md's open Vite question now says so.

Tests: lab/basic imports point.schema.json5 (unquoted keys, single quotes, a hex `maxLength`, trailing commas) with exact-type assertions and runs it under the preload and from the esbuild bundle (15 inputs, none from node_modules); tests/schema.test.ts checks JSON5 in both texts, the suffix choosing the dialect, and a placed syntax error; formats.test.ts and the `?raw` extension list cover the suffix. DESIGN.md's suffix section, PLAN.md, AGENTS.md, the README and the docs page (a `.schema.json5` section with a hover from the mapper, and the setup's extension list) follow.

Verified on 2026-10-20 on macOS: `nub run typecheck` passes and `nub run test` passes 58 of 58 on the pinned 7.1.0-dev.20261002.1; `nub run build` in site/ passes, its new hover reading `const origin: Point`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Will Cory (roninjin10) pushed a commit to smithersai/vibelang that referenced this pull request Oct 26, 2026
The mapper was tested against 7.1.0-dev.20261010.1, a nightly older than
the fork's own upstream base (257e874, 2026-10-14). That nightly predates
content mapper output extensions (microsoft/TypeScript#64581), so the
mapper's protocol code and the compiler it was tested against differed.

Pin typescript 7.1.0-beta (published 2026-10-20). Its content mapper code
is the fork base's: none of the 15 upstream commits between them touch
contentmapper, spanmap or ipc.

e2e.sh gains a project whose .vibe files compile to .js: with
outputExtensions { ".vibe": ".js" } and rewriteRelativeImportExtensions,
stock tsc emits main.js importing ./hello.js and hello.d.ts,
vibelang-mapper --emit writes hello.js, and node runs it. On the old pin
tsc ignores outputExtensions and main.js imports ./hello.vibe.

The parity harness moves by one case: declaration-nested-linked now
reports TS2883 under stock tsc, from upstream #64599 (package reachability
in declaration emit), which 7.1.0-dev.20261015.1 already has.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
NullVoxPopuli added a commit to NullVoxPopuli/ember-content-mapper that referenced this pull request Oct 29, 2026
TypeScript 7.1.0-dev.20261014.1 adds `outputExtensions` to content mapper
entries (microsoft/TypeScript#64581, fixing #64053). The library example
opts in, so counter.gts emits counter.d.ts and declarations/ matches dist/.

- Bump the TypeScript 7 nightly to 7.1.0-dev.20261014.1.
- Document "Publishing a library" in the README and point the migration
  guide at it.
- Add a declaration-output-extensions fixture and test.

The mapper's package.json does not set a default: apps would then need
rewriteRelativeImportExtensions, which reports TS2877 for non-relative
extension imports such as #app/components/counter.gts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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

Status: Done

Development

Successfully merging this pull request may close these issues.

content-mapper generates inconsistent declaration extensions, making management of package.json#exports hard / verbose

5 participants