Skip to content

Implement a TypeScript 7 content mapper for .gts/.gjs files - #1

Merged
NullVoxPopuli merged 11 commits into
NullVoxPopuli:mainfrom
NullVoxPopuli-ai-agent:ts7-content-mapper
Aug 25, 2026
Merged

Implement a TypeScript 7 content mapper for .gts/.gjs files#1
NullVoxPopuli merged 11 commits into
NullVoxPopuli:mainfrom
NullVoxPopuli-ai-agent:ts7-content-mapper

Conversation

@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Contributor

Native tsc can now type-check <template> tags: with this package registered under contentMappers in tsconfig.json, a TypeScript 7.1 nightly run with --runExternalCode type-checks .gts/.gjs files directly and reports diagnostics at their original template positions. No ember-tsc, no Volar in the CLI path.

{
  "compilerOptions": { "types": ["ember-source/types", "@glint/ember-tsc/types"] },
  "contentMappers": [
    { "package": "ember-content-mapper", "extensions": [".gts", ".gjs"] }
  ]
}

Example output (from diagnostics-tests/):

src/template-errors.gts(5,15): error TS2339: Property 'missingProperty' does not exist on type 'Broken'.
src/unused-directive.gts(4,3): error glint2578: Unused '@glint-expect-error' directive.

How it works

  • lib/server.js speaks the JSON-RPC protocol from Content mappers microsoft/typescript-go#4712 over stdio (initialize / openProject / transform / closeProject), structured after remcohaszing/mdx-content-mapper.
  • transform runs the published Glint transform (rewriteModule from @glint/ember-tsc@1.10), so the emitted TypeScript and its semantics are exactly what Glint's language server produces today.
  • lib/util/mappings.js flattens Glint's Glimmer mapping tree into the protocol's span mappings. Two impedance mismatches handled there:
    • TypeScript forbids overlapping virtual spans, and (until Content mappers round 2 microsoft/TypeScript#63936 lands) overlapping non-identical original spans. Glint's tree nests both by design, so identifier-level leaves keep exact ranges (Verbatim, full feature bits) while boilerplate segments become zero-length Atom anchors that still map diagnostics to the start of the construct that produced them.
    • Volar's coarse CodeInformation switches become SpanMapFeature bitmasks (e.g. Glint's two elementTypes tag-name lookups keep their hover-only / navigation-only split).
  • Directives: Glint v2 never returns module.directives for gts; the semantics are baked into per-node CodeInformation. The mapper recovers them (verification: false nodes, verification.shouldReport closures, placeholder comment nodes) and emits the protocol's native diagnostic directives: @glint-expect-error becomes an Expect region (TypeScript itself reports unused directives as glint2578), @glint-ignore/@glint-nocheck become Ignore regions, and Glint's Volar-era // @ts-expect-error placeholders and auto-import anchor are Ignore-covered so they can't leak TS2578/TS2688.
  • openProject declares dynamicConfig and fingerprints the installed ember-source (it decides whether the 7.1 built-in keywords are globals), watching its package.json. additionalGlobals/additionalSpecialForms pass through contentMappers[].options with option-path diagnostics for invalid values.

Tests

  • fixtures/ + test/fixtures.js: snapshot tests of transform output, span mappings, and directives per fixture (mdx-content-mapper's harness pattern; verbatim mappings are asserted byte-identical, virtual spans asserted disjoint).
  • type-tests/: 18 files ported from typed-ember/glint (ts-gts-7-1-app, @glint/type-test's fccts suite) covering expectTypeOf/to.* assertions, @glint-expect-error usage, 7.1 keyword narrowing, overload resolution, and curried generics. pnpm test:typecheck runs a real typescript@7.1.0-dev nightly over them with --runExternalCode and requires zero diagnostics.
  • diagnostics-tests/: intentionally broken projects whose exact tsc output (positions mapped into .gts sources, unused-directive reporting, content-tag parse errors) is locked in as expected output. Parse errors live in their own project because tsc skips the semantic phase when syntactic diagnostics exist.

One ported file needed {{!-- --}} comment form instead of {{! ... }} for decorative comments containing nested mustaches (array-keyword-preserve-literals.test.gts); published Glint 1.10 rejects the short form there.

Known limitations

  • Needs a TypeScript 7.1 nightly (content mappers merged 2026-08-19); protocol round 2 (Content mappers round 2 microsoft/TypeScript#63936) will remove the protocolVersion field and the original-span overlap restriction, at which point the demotion pass can regain exact ranges for boilerplate-anchored diagnostics.
  • Declaration emit and the LSP path exist protocol-side but only the CLI type-checking path is exercised here.

🤖 Generated with Claude Code

NullVoxPopuli-ai-agent and others added 2 commits August 21, 2026 10:10
Native tsc (7.1 nightlies, --runExternalCode) can now type-check Ember
<template> tags directly: the mapper speaks the JSON-RPC protocol from
microsoft/typescript-go#4712, transforms each file with Glint's
rewriteModule, flattens the Glimmer mapping tree into non-overlapping
span mappings, and translates @glint-expect-error / @glint-ignore /
@glint-nocheck into the protocol's native diagnostic directives.

Structure follows remcohaszing/mdx-content-mapper. Tests are snapshot
fixtures over the transform output plus integration suites type-checked
by a real TypeScript 7 nightly: type-tests/ ports 18 expect-error /
expectTypeOf-style files from typed-ember/glint, and diagnostics-tests/
locks in exact mapped error positions and unused-directive reporting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
examples/nvp-app (pnpm dlx ember.nvp) and examples/cli-app
(pnpm dlx ember-cli@latest new --typescript), each updated to a
TypeScript 7.1 nightly with contentMappers registered in tsconfig.json
and lint:types switched from ember-tsc to tsc --noEmit --runExternalCode.
@glint/tsserver-plugin is dropped since it only loads into a TS 5/6
tsserver. Both apps type-check clean via pnpm test:examples (also in CI),
and examples/README.md covers CLI and VS Code debugging.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Contributor Author

Added examples/: two full apps you can open in an editor and debug, one from pnpm dlx ember.nvp (typescript/qunit/prettier layers) and one from pnpm dlx ember-cli@latest new --typescript. Each is updated to a TS 7.1 nightly with contentMappers in tsconfig.json, lint:types switched to tsc --noEmit --runExternalCode, and @glint/tsserver-plugin removed (it only loads into a TS 5/6 tsserver). Both type-check clean via pnpm test:examples (wired into CI). examples/README.md covers editor setup: the TypeScript Native Preview extension, workspace trust (--runExternalCode is trust-gated in the LSP), and the open-a-.ts-file-first caveat until a VS Code extension registers .gts with the TS extension.

NullVoxPopuli-ai-agent and others added 2 commits August 21, 2026 15:35
Each example app now exercises the surfaces the mapper must handle:
class components with signatures/blocks, template-only components,
untyped .gjs with JSDoc signatures, custom modifiers, helper functions,
.ts barrels importing .gts/.gjs, 7.1 keywords, trackedArray, and .gts
rendering tests. Both apps type-check clean.

Editor fix: the LSP surfaced hint-severity TS6133 for Glint's unused
`const __glintY__ = emitElement(...)` declarations (the CLI never
reports suggestions). TypeScript auto-suppresses declared-but-not-used
diagnostics only in unmapped regions, so gap mappings now leave holes
around those declaration names.

examples/README.md documents Neovim setup: nvim-lspconfig's tsc with
init_options.runExternalCode and glimmer-filetype language-id mapping;
ember.nvim#4 automates it for contentMappers projects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ackages

test-packages/ holds byte-identical copies of five packages from
typed-ember/glint at v1.10.0 (ts-template-imports-app, ts-gts-7-1-app,
ts-special-forms-app, ts-special-forms-pre-7-1-app, ts-extensionless-app);
only package.json and tsconfig.json are adapted. Their glint-expect-error
and ts-expect-error directives, and the deliberate absence of directives
elsewhere, are the assertions: test/typecheck.js compares each package's
tsc --runExternalCode output against expected/<name>.txt. Three packages
type-check clean; the recorded deviations (extensionless imports, .gjs
declaration-file shadowing, per-process ember-source probing, reported
handlebars parse errors) are documented in test-packages/README.md.

Mapper fix motivated by the corpus: Glint's directives also suppress
transform errors (a special-form arity error under @glint-expect-error),
but TypeScript's diagnostic directives only cover its own bind/check
diagnostics, so mapper diagnostics inside directive areas are filtered
in the transform. When emission aborts before producing mapping nodes,
the expect-error area is recovered from the placeholder comment plus
Glint's next-line area-of-effect rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Contributor Author

Two updates from debugging the examples in an editor:

Neovim works now. The red squiggles came from the classic pipeline: ember.nvim attaches ts_ls and injects @glint/tsserver-plugin from the project whenever package.json mentions @glint/ember-tsc; that plugin only loads into a TS 5/6 tsserver and is removed in these apps, so tsserver parsed <template> as plain TS. The fix is TypeScript 7's own LSP (tsc --lsp --stdio) with initializationOptions: { runExternalCode: true }, which is required (without it the server reports "no project found" for .gts) and is the same trust opt-in VS Code sends. Verified end to end in headless nvim: only tsc attaches, a clean application.gts has zero diagnostics, and a planted error reports 2339 at the exact template position. NullVoxPopuli/ember.nvim#4 automates this for any project whose tsconfig declares contentMappers; examples/README.md has the standalone snippet.

The LSP round also caught a mapper bug: hint-severity TS6133 '__glintY__' is declared but its value is never read leaked from Glint's per-element boilerplate (the CLI never reports suggestions, so only editors saw it). TypeScript auto-suppresses declared-but-not-used diagnostics only in unmapped regions, so gap mappings now leave holes around those declaration names.

Also expanded both apps with the usage surfaces the mapper has to handle: class components with signatures and yielded blocks, template-only components, untyped .gjs with a JSDoc signature, a custom ember-modifier, plain-function helpers, .ts barrels importing .gts/.gjs (extension-full specifiers required; extensionless does not resolve through the mapper), 7.1 keywords, trackedArray, and .gts rendering tests. Both apps type-check clean.

Comment thread .github/workflows/ci.yml
Comment thread examples/cli-app/.prettierrc.mjs Outdated

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

we should probably remove prettier/prettierignore, eslint, and stylelint from the test projects (keep them focused on tests)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed in 12d5850: prettier, eslint, and stylelint configs, deps, and scripts are gone from both example apps; each keeps only lint:types.

Comment thread examples/cli-app/.template-lintrc.mjs Outdated

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

template lint should also not be in these test projects

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed along with the rest of the lint tooling in 12d5850.

@NullVoxPopuli-ai-agent

Copy link
Copy Markdown
Contributor Author

Replaced the cherry-picked type-tests/ with direct copies of Glint's test-packages/ (at v1.10.0, byte-identical sources; only package.json/tsconfig.json adapted): ts-template-imports-app, ts-gts-7-1-app, ts-special-forms-app, ts-special-forms-pre-7-1-app, and ts-extensionless-app. The presence and absence of @glint-expect-error / @ts-expect-error in those files are the assertions; test/typecheck.js locks each package's tsc --runExternalCode output to test-packages/expected/<name>.txt, so an empty snapshot means "identical to Glint" and a non-empty one is a documented deviation (test-packages/README.md).

Results: ts-gts-7-1-app, ts-special-forms-app, and ts-special-forms-pre-7-1-app type-check clean. The recorded deviations:

  • Extensionless .gts imports don't resolve through the content mapper (TS2307) — 2 lines in ts-extensionless-app plus 1 in ts-template-imports-app.
  • .gjs with a handwritten .gjs.d.ts: the mapper transforms the .gjs and never consults the adjacent declaration file, so with-declaration-consumer.gts accounts for 7 lines.
  • Shared ember-source probe: the environment probes ember-source from the mapper's install, not per project, so a 6.x-pinned project gets the 7.1-mode transform ({{hash}} → "Property 'hash' does not exist on type 'Keywords & Globals'"). Needs a probe-root parameter in Glint's environment to fix properly.

The corpus also drove a mapper fix: Glint's directives suppress transform errors too (e.g. {{eq}} arity under {{! @glint-expect-error }} in the-7-1-globals-are-not-present.gts), but TS7's diagnostic directives only cover bind/check diagnostics — so the mapper now filters its own diagnostics inside directive areas, recovering the area from the placeholder comment + Glint's next-line area-of-effect rule when a failed emission leaves no mapping nodes.

And it surfaced an upstream bug: ember-tsc silently skips type-checking any template whose handlebars parse fails, which means array-keyword-preserve-literals.test.gts (short {{! }} comments with nested mustaches) is not actually verified in Glint's CI — a planted type error goes unreported there. Filed as typed-ember/glint#1221. The mapper reports the parse error instead; since a parse diagnostic suppresses tsc's semantic phase project-wide, that one file is tsconfig-excluded in our copy so the other 17 files stay verified.

The registry was unreachable when test-packages/ landed (TLS validity
window ahead of this machine's clock), so the lockfile missed the new
importers and --frozen-lockfile installs failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread lib/requests/open-project.js Outdated
Comment thread lib/util/ember-probe.js Outdated
Comment thread lib/constants.js
});
});

describe('@volar/typescript proxyCreateProgram patches', () => {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

why are we testing old volar / ts < 7 stuff?

patching ts isn't possible with ts7, so the comments above in this file confuse me

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right — those tests (plus the *-fixture dirs and extra tsconfigs) are Glint's own vitest harness for its CLI (watch mode, build mode, the tsc source patches), not part of the expect-error corpus. Removed from the copy in 12d5850; test-packages/README.md notes why.

@@ -0,0 +1,19 @@
import '@glint/ember-tsc/types';

// On ember-source < 7.1, `and`/`or`/`not`/`eq`/`neq` are NOT built-in template

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

does this mean that consumers with ember-source < 7.1 will need to have this declare module in their app? previously, this was not needed.

why is it needed now, and can we delete this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No new requirement — that file is byte-identical to upstream Glint's (it ships in glint's own test-packages). It exists because this app opts into additionalSpecialForms operators on ember-source < 7.1: the transform emits a discarded Globals. reference for hover/goto on the keyword, and the declaration satisfies it. Consumers who don't use additionalSpecialForms never need it, same as under ember-tsc today. Deleting it here would diverge from the direct-copy rule (and break the package the same way it would break upstream).

Comment thread README.md
@@ -0,0 +1,97 @@
# ember-content-mapper

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

rewrite this readme using the writing skills you have

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rewritten in 12d5850.

NullVoxPopuli-ai-agent and others added 2 commits August 24, 2026 09:47
- CI: lint and each test suite run as four parallel jobs
  (lint, test-unit, test-typecheck, test-examples).
- Example apps: prettier, eslint, stylelint, and template-lint configs,
  deps, and scripts removed; the apps keep only lint:types.
- test-packages: ts-extensionless-app's __tests__ and *-fixture
  directories removed; they drive Glint's own CLI harness (watch/build
  mode, tsc source patches), which does not apply to TypeScript 7.
- lib/constants.js documents that the enum values are TypeScript's,
  copied from typescript-go#4712.
- The package.json resolution workarounds reference typed-ember/glint#1222,
  which adds the export upstream.
- README rewritten.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
typed-ember/glint#1222 shipped in 1.10.1, so the manifest resolves with
require('@glint/ember-tsc/package.json') and the ember-source probe
anchors its createRequire on the same export. The floor moves to ~1.10.1
everywhere so the export is guaranteed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread test-packages/README.md
@@ -0,0 +1,61 @@
# test-packages

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

rewrite this doc using your writing skills (load them all)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rewritten in eadbf0d.

Comment thread package.json
"packageManager": "pnpm@11.33.0"
"dependencies": {
"@glint/ember-tsc": "~1.10.1",
"typescript": "~5.9.3",

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

why are we using typescript 5.9.3? this whole effort is for TS 7

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

TypeScript 7 does all the type-checking of your project. The 5.9 dependency is the TypeScript JavaScript API that Glint's rewriteModule uses internally to parse the script parts of a .gts file before replacing the templates. The TS 7 npm package is the native Go compiler with no JavaScript API, and @glint/ember-tsc peers on >=5.6, so the mapper carries its own TS 5 purely as a parser. It never checks your code, and it can go once Glint's transform runs against a TS 7 API. Documented in the README ("Why does the mapper depend on TypeScript 5?") in eadbf0d.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

We may need no refactor glint so we don't need ts5 here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and it can be done without touching the existing callers. I analyzed what rewriteModule actually uses the TS AST for and filed typed-ember/glint#1224 with the details: template placement and class-member detection are already in content-tag's parse output, and the import analysis (lexical imports beating globals, glint#1113) only needs an import scanner like es-module-lexer, not a full TS parser. The issue proposes either a parallel entry point or a pluggable script-analysis seam, so the tsserver-plugin/Volar paths keep passing their TSLib unchanged and embedders like this mapper opt into the parser-free path. Once that lands, the typescript@5.9 dependency here disappears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NullVoxPopuli-ai-agent and others added 3 commits August 25, 2026 13:29
The newest marketplace build of the Native Preview extension
(0.20260708.2) predates content mapper support (merged 2026-08-19): it
does not send the runExternalCode opt-in, so VS Code cannot type-check
gts through the mapper yet. The README now separates what works today
(TS 7 server for plain ts/js via useTsgo, Glint extension disabled per
workspace) from the steps that apply once a newer build ships.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Content mapper support works in VS Code with an extension built from
typescript-go main: the client bundle is plain TypeScript and the
native server comes from the published platform package, so no Go
toolchain is needed. Verified against the 2026-08-25 nightly (which
includes protocol round 2): all five test-packages produce output
identical to the recorded expectations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NullVoxPopuli
NullVoxPopuli merged commit 6581d03 into NullVoxPopuli:main Aug 25, 2026
1 check passed
@NullVoxPopuli NullVoxPopuli added the enhancement New feature or request label Aug 26, 2026
@github-actions github-actions Bot mentioned this pull request Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants