Add type-system support for custom elements / web components - #1212
Add type-system support for custom elements / web components#1212NullVoxPopuli-ai-agent wants to merge 6 commits into
Conversation
Adds two global registries to @glint/template: - GlintCustomElementTagNameMap: tag name -> element instance type (mirroring HTMLElementTagNameMap), giving registered custom elements a real element type for modifiers, ...attributes, and hover info. - GlintCustomElementAttributesMap: tag name -> accepted attributes, checked strictly when registered. Plain elements now resolve their attributes from the tag name they were written with (via a new applyTagAttributes DSL function and name/attributes members on the emitElement result) rather than from the element instance type, so attributes can be registered for a tag without registering the element type. Components keep resolving attributes from their signature's Element type, now with a reverse lookup into the custom element registry and a graceful fallback for unregistered Element subclasses (previously the lookup produced 'never', rejecting every attribute). Resolves typed-ember#808 Supersedes typed-ember#1016 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ffac5c2 to
625ed63
Compare
TypeScript's excess-property check only reports the first unknown key in
an object literal, so emitting all of an element's attributes as a single
applyAttributes/applyTagAttributes call meant that e.g.
<my-custom-element foo="hi" bar="hi"></my-custom-element>
only flagged 'foo'. Each attribute is now applied in its own call, giving
every attribute its own excess-property check (and its own diagnostic).
This also applies to built-in elements and component attributes, which
had the same first-error-only behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Follow-up pushed: TypeScript's excess-property check only reports the first unknown key per object literal, so |
Hovering an element's tag name now shows its element type, and go-to-definition resolves to its HTMLElementTagNameMap / GlintCustomElementTagNameMap entry. This applies to built-in elements too, which previously had no type information on hover (typed-ember#808). The transform emits discarded references to a new value-level elementTypes lookup, with the template's tag name mapped onto them: __glintDSL__.noop(__glintDSL__.elementTypes.my_element); __glintDSL__.noop(__glintDSL__.elementTypes["my-element"]); Two forms are needed because the features have conflicting constraints: - Hover requires a same-length *identifier* (hyphens -> underscores, like the each-in -> Globals.each_in keyword aliases): a quoted key's quickinfo textSpan includes the quotes, which have no source counterpart, so Volar drops the hover result. - Go-to-definition requires the *raw quoted key*: TypeScript retains no declaration links through the key-remapped mapped type backing the identifier form. Each reference's Volar mapping is scoped to just its feature (hover vs navigation) so the two can't produce duplicate or misanchored results. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Also pushed: hovering an element's tag name now shows its element type, and go-to-definition resolves to its Implementation note: the transform emits two discarded |
Hovering an element's tag name previously surfaced the raw quickinfo of
the discarded elementTypes lookup the transform emits, displayed
property-style: '(property) my_custom_element: MyCustomElement'. The
tsserver plugin now rewrites that quickinfo — when the hovered position
maps to a plain element's tag name and the display has the expected
'(property) <tag-name-ish>: ...' shape — to show only the type:
'MyCustomElement'. Components and every other hover are untouched.
Notes:
- The mapping tree can hold sibling twins of a node where the first twin
has no children, and narrowestMappingForOriginalRange returns the
first containing child — so the plugin locates the smallest containing
mapping itself.
- The property name may appear as an identifier (my_custom_element), a
quoted key ('foo-bar' or "div"), depending on which elementTypes
entry served the hover; all forms are matched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Hover polish per feedback: tag-name hover now shows just the element type ( |
Hovering (or invoking go-to-definition on) an attribute of a plain element surfaced the private __glintY__ binding: the applyAttributes / applyTagAttributes *target argument* is mapped onto the attribute node (so invalid-element diagnostics anchor there), and that mapping carried full language-feature capabilities. It is now scoped to verification only — and captureMapping applies the @glint-ignore/expect-error directive support to explicitly-provided code features too, so the diagnostics that mapping anchors remain suppressible. With the pollution gone, attribute-name hover/definition come from the object-literal key mapping: definition resolves to the attribute's declaration (e.g. a GlintCustomElementAttributesMap entry), and hover works for identifier-safe names. Quoted keys (hyphenated names like prop-num) still yielded no hover — TypeScript's quickinfo textSpan for a quoted key includes the quotes, which have no source counterpart, so Volar drops the result. The tsserver plugin now recovers those: it locates the generated key via the mapping tree, resolves the contextual property with the type checker, and synthesizes a property-style quickinfo with a source-space span. Note: program positions in TS plugin mode are shifted by the source text's length relative to transformedContents offsets (Volar's leading-offset scheme — the target script is source + generated concatenated); the recovery accounts for that. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Attribute-name hover/goto fixed (was surfacing the private |
Editors resolve definitions through tsserver's definitionAndBoundSpan command (VS Code's TS integration and most LSP clients), not the plain definition command the earlier tests exercised. Volar's proxy for definitionAndBoundSpan drops the ENTIRE result when the origin (bound) span can't be translated back to the template — which is the case for the quoted elementTypes["my-element"] keys serving element tag names and for quoted attribute keys, since the quotes have no source counterpart. So go-to-definition on <my-custom-element> (or a hyphenated attribute) worked in the harness but returned nothing in VS Code and Neovim. The tsserver plugin now falls back when the decorated definitionAndBoundSpan comes up empty inside a template: it takes the definitions from getDefinitionAtPosition (whose per-target translation works) and synthesizes the bound span from the mapping tree (the tag / attribute name range). The definition tests now issue definitionAndBoundSpan so they exercise the same path editors use, and assert the bound span too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Fixed go-to-definition on |
Resolves #808. Supersedes #1016 (rebuilt on current
main, which has moved ~360 commits since that branch's base).What this adds
Two global registries in
@glint/template, both empty by default and populated by projects viadeclare global:The registries are independent, and everything unregistered behaves exactly as it does today:
...attributes, hover)ElementElementAugmenting the DOM's standard
HTMLElementTagNameMap(the Lit convention) also works for the element-type half;GlintCustomElementTagNameMapexists for projects that prefer not to extend the DOM's own registry.How it works
emitElement(...)now returns{ name, element, attributes }(previously just{ element }), andElementForTagNamefalls back toGlintCustomElementTagNameMapbeforeElement.applyTagAttributes(__glintY__, { ... })DSL call whose attrs type comes from the newAttributesForTagName<Name>. This is what lets attributes-only registrations work — there's no element type to reverse-look-up.applyAttributes(__glintY__.element, { ... })and resolve attributes from the signature'sElementtype, so all existing elementless-component diagnostics/augmentations are byte-for-byte unchanged.AttributesForElement<T>additionally does a reverse lookup into the custom-element registry, so a component withElement: MyCustomElementgets the same attribute checking.AttributesForElement<T>also gains a graceful fallback: anHTMLElement/SVGElementsubclass that isn't in any registry now accepts arbitrary attributes. Previously the type-name lookup producednever, which rejected every attribute (includingclass) for such elements.Differences from #1016
#1016 was exploratory and internally inconsistent (it carried four different registry names across code/docs/fixtures, and a generated
GlintTagNameAttributesMapwhose duplicate HTML/SVG keys —a,script,style,title— can't compile). This PR keeps its core architecture (name-keyed registries,name/attributeson theemitElementresult, tag-name-based attribute resolution) with these deliberate changes:GlintCustomElementTagNameMap,GlintCustomElementAttributesMap), both keyed by tag name; no generated tag-name map is needed, sobin/build-elements.mjsandelements.d.tsare untouched.Record<string, AttrValue>), matching today's behavior, rather than being narrowed to global HTML attributes — no breaking change for existing users of custom elements.applyAttributes(__glintY__.element, ...)emit instead of switching everything toapplyAttributes(__glintY__, ...), which avoids the nested/uglier "An Element must be specified..." diagnostics that Add support for typed custom-elements and web-components #1016 had to counteract with message-text sniffing in the diagnostic augmentation layer.Testing
packages/template/__tests__/custom-elements.test.ts(+ registry in a separate module, proving cross-file registration),private/ElementForTagName.test.ts, additions toattributes.test.ts/AttributesForElement.test.ts.test-packages/ts-template-imports-app/src/custom-elements.gtswith@glint-expect-errorassertions (verified they're load-bearing: making a flagged line valid fails with "Unused '@ts-expect-error' directive").test:typecheck/test:tscsuites,package-test-corevitest (247 tests),ts-extensionless-app,v2-ts-ember-addontestem, VS Code TS-plugin smoke tests, andpnpm lintall pass.🤖 Generated with Claude Code