Skip to content

Add a TypeScript 7 backend for Glint type extraction - #48

Merged
johanrd merged 3 commits into
mainfrom
tsgo-backend
Aug 28, 2026
Merged

Add a TypeScript 7 backend for Glint type extraction#48
johanrd merged 3 commits into
mainfrom
tsgo-backend

Conversation

@johanrd

@johanrd johanrd commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Type-aware linting on TypeScript 7. TS7 ships no in-process API, so the existing pipeline (typescript as a library + Glint's rewriteModule) cannot run on it. TS7's typescript/unstable/sync is a synchronous IPC API, which is what html-validate's synchronous transformer needs.

Design

One extraction algorithm (lib/glint.ts) over two backends (lib/backend/):

  • ts6 — the existing pipeline, moved to lib/backend/ts6.ts. It now always adds ember-source/types / @glint/ember-tsc/types to its program, so projects that dropped them from compilerOptions.types for the content mapper keep their types.
  • tsgolib/backend/tsgo.ts. Opens the project with runExternalCode, so the tsconfig's contentMappers (ember-content-mapper) transform .gts inside the compiler; sourceFile.spanMap maps template ranges to the virtual text. No Glint rewrite, no typescript library in-process. The resolver's syntactic parses use the same API through an fs overlay.
  • TsSyntax facade (predicates, enum tables, parseFile, union/literal helpers) replaces typeof TS in lib/resolver/*; boundary casts are confined to tsgo.ts.
  • Selection per tsconfig: tsgo when it declares contentMappers and a TS7 package resolves (typescript 7.x, @typescript/native, typescript-7, or HVE_TSGO=<name>); HVE_TS_BACKEND=tsgo|ts6 forces. Node 22.12+ for require() of the ESM API. Cache entries carry the backend.

Verification

  • pnpm test (ts6) and pnpm test:tsgo (same suite, vitest.tsgo.config.ts): 289 passed, 1 expected fail each. vitest.config.ts excludes ecosystem/**.
  • 244-template app, both backends from dist/: 23 string-literal unions and 24 literals identical as sets, componentAttrMap 272/272, componentTagMap 617 identical + 8 where tsgo resolves a real tag the TS6 program reported as any, 0 missing.

Behaviour differences

  • tsgo returns union members in checker order, so values[0] may be a different (equally valid) member than under TS6.
  • Files outside a tsconfig's include have no project under tsgo, same as tsc; examples/ got its own tsconfig.
  • A signature-less template-only component's Element is null under tsgo (unknown under TS6); both now resolve to transparent.

Cowritten by Claude

Type information now comes from one of two backends behind the same
extraction algorithm:

- ts6: the project's typescript 5/6 as a library plus Glint's
  rewriteModule (the existing pipeline, moved to lib/backend/ts6.ts).
- tsgo: typescript/unstable/sync, TypeScript 7's synchronous IPC API.
  The project is opened with runExternalCode so the tsconfig's
  contentMappers transform .gts files inside the compiler; spanMap maps
  the template back. No Glint rewrite and no typescript library run in
  this process. Selected when the tsconfig declares contentMappers and a
  TypeScript 7 package resolves; HVE_TS_BACKEND forces either.

lib/glint.ts and lib/resolver/* see a TsSyntax facade instead of the
typescript module. The ts6 backend always adds ember-source/types and
@glint/ember-tsc/types to the program so projects that dropped them for
the content mapper keep their types. The cache key includes the backend.

Both vitest lanes (pnpm test, pnpm test:tsgo) pass. On a 244-template
app the backends agree on every string-literal narrowing and component
substitution; tsgo resolves eight sites the TS6 program reported as any.

Cowritten by Claude

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

It introduces a large new backend built on an unstable TypeScript 7 dev API with subtle snapshot-lifetime and span-mapping semantics that cannot be fully verified from the diff and warrant human review.

Pull request overview

This PR adds a second type-extraction backend so the plugin can perform type-aware Ember/Glint linting on TypeScript 7 (tsgo), which ships no in-process library API. It refactors the previously monolithic lib/glint.ts into a shared extraction algorithm over a TypeBackend abstraction (lib/backend/), with the existing TS5/6 + Glint rewriteModule pipeline moved to ts6.ts and a new tsgo.ts backend built on typescript/unstable/sync. A TsSyntax facade replaces direct typeof TS usage across the resolver so the syntactic walks work against either backend.

Changes:

  • Introduce lib/backend/ (types.ts, ts6.ts, tsgo.ts, index.ts) with per-tsconfig backend selection driven by contentMappers detection, TS7 package resolution, and HVE_TS_BACKEND / HVE_TSGO overrides.
  • Refactor lib/glint.ts and the resolver to consume TemplateSites and the TsSyntax/CheckerLike/ProgramLike interfaces instead of the typescript module directly; add the backend to the disk-cache key.
  • Add the test:tsgo suite (vitest.tsgo.config.ts), the typescript-7/ember-content-mapper dev deps, example/fixture tsconfigs declaring contentMappers, and README docs.
File summaries
File Description
lib/backend/types.ts New shared interfaces (TsSyntax, TypeBackend, TemplateSite, etc.)
lib/backend/index.ts Backend selection, tsconfig discovery, syntax fallback
lib/backend/ts6.ts Existing pipeline extracted; always adds ember/glint types
lib/backend/tsgo.ts New TS7 IPC backend: snapshots, span mapping, site collection
lib/glint.ts Refactored to iterate backend sites; adds Element: null handling
lib/cache.ts Adds backend to cache entry + read/write signatures
lib/resolver/walk.ts, template-source.ts, build-maps.ts Switch to TsSyntax/syntaxFor
lib/cache.ts, run.ts Backend threaded through cache key and summary line
test/*.ts, *fixtures Updated call signatures, backend-selection test, contentMappers fixtures
package.json, pnpm-lock.yaml New deps, test:tsgo script, optional typescript peer
README.md, examples/tsconfig.json, vitest*.config.ts Docs and dual-backend test config
Review details

Files not reviewed (1)

  • pnpm-lock.yaml: Generated file
  • Files reviewed: 21/22 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/backend/index.ts Outdated
Comment thread package.json Outdated
- findInnerTypeAtRange: never climb through an object literal. A literal
  or plain path inside a component's named-args object (`{ size: "lg" }`)
  belongs to that invocation, not to a wrapper of its own; the fallback
  could widen to the whole invocation and return a sibling argument's
  type under the literal's key. Regression fixture.
- tsgo parseFile cache: one entry per virtual name, replaced when the
  buffer changes, instead of one per (name, contents) forever.
- Backends expose dispose(); closeBackends() releases every compiler
  process and snapshot for hosts that outlive one run.
- One content-tag parse per opened file instead of two.

Cowritten by Claude
@johanrd johanrd added the enhancement New feature or request label Aug 28, 2026
@johanrd
johanrd merged commit 8ab7dfc into main Aug 28, 2026
2 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 28, 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