Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"version": "0.2.0",
"configurations": [
{
{
// For this to work, make sure you're running `tsc --build --watch` at the root, AND
// `pnpm bundle:watch` from within vscode directory.
"name": "Debug Extension (TS Plugin, .gts)",
"name": "Debug Extension (TS Plugin, .gts, ember-app)",
"type": "extensionHost",
"request": "launch",
// "preLaunchTask": "npm: build",
Expand All @@ -24,6 +24,29 @@
"${workspaceFolder}/packages/vscode/__fixtures__/ember-app"
]
},
{
// For this to work, make sure you're running `tsc --build --watch` at the root, AND
// `pnpm bundle:watch` from within vscode directory.
"name": "Debug Extension (TS Plugin, .gts, ts-template-imports-app)",
"type": "extensionHost",
"request": "launch",
// "preLaunchTask": "npm: build",
"autoAttachChildProcesses": true,
"runtimeExecutable": "${execPath}",
"outFiles": [
"${workspaceFolder}/**/*.js",
"!**/node_modules/**"
],
"args": [
"--extensionDevelopmentPath=${workspaceFolder}/packages/vscode",
// Disable v1 vscode glint
"--disable-extension",
"typed-ember.glint-vscode",
// comment to activate your local extensions
// "--disable-extensions",
"${workspaceFolder}/test-packages/ts-template-imports-app"
]
},
{
"name": "Attach to TS Server",
"type": "node",
Expand Down
32 changes: 31 additions & 1 deletion bin/build-elements.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ const htmlElementsMap = new Map([[GLOBAL_HTML_ATTRIBUTES_NAME, 'HTMLElement']]);
const svgElementsMap = new Map([[GLOBAL_SVG_ATTRIBUTES_NAME, 'SVGElement']]);
const mathmlElementsMap = new Map();

const tagNameAttributesMap = [];

function addTagNameAttributesMapEntry(name, interfaceName) {
if (name === 'GlobalHTMLAttributes' || name === 'GlobalSVGAttributes') {
return;
}

tagNameAttributesMap.push(` ['${name}']: ${interfaceName};`);
}

traverse(ast, {
TSInterfaceDeclaration: function (path) {
if (path.node.id.name === 'HTMLElementTagNameMap') {
Expand Down Expand Up @@ -123,6 +133,7 @@ interface GlintHtmlElementAttributesMap {\n`;
});
}
htmlElementsContent += '}\n';
addTagNameAttributesMapEntry(name, interfaceName);
}

function addMapEntry(type) {
Expand Down Expand Up @@ -193,6 +204,7 @@ interface GlintSvgElementAttributesMap {\n`;
});
}
svgElementsContent += `}\n`;
addTagNameAttributesMapEntry(name, interfaceName);
}

function addMapEntry(type) {
Expand Down Expand Up @@ -232,5 +244,23 @@ const filePath = resolve(
fileURLToPath(import.meta.url),
'../../packages/template/-private/dsl/elements.d.ts',
);
const content = prefix + createHtmlElementsAttributesMap() + createSvgElementAttributesMap();

function defineTagNameAttributesMap(contents) {
return [
'',
`global {`,
`/* These are not all the elements, but they are the ones with types of their own, beyond HTMLElement/SVGElement */`,
`interface GlintTagNameAttributesMap {`,
contents.join('\n'),
`}`,
`}`,
`\n`,
].join('\n');
}

const content =
prefix +
createHtmlElementsAttributesMap() +
createSvgElementAttributesMap() +
defineTagNameAttributesMap(tagNameAttributesMap);
writeFileSync(filePath, content);
33 changes: 33 additions & 0 deletions docs/glint-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,36 @@ declare global {
}
}
```

### Custom Elements / WebComponent types

To add custom elements, there are 3 global interfaces you must merge with to register the custom element:
- The Invocation Registry, `GlintCustomElements` -- this is how Glint translates element names in HTML into types.
- The Type Registry, `GlintElementRegistry` -- this is how Glint converts the name of your custom element to the actual element type
- The Attribute Registry, `GlintHtmlElementAttributesMap` -- this is how Glint type-checks your props and attributes passed to your custom element.


To specify all 3, it would look something like this:

```ts
import '@glint/template';

import type { MyCustomElementClass, MyCustomElementProps } from './wherever.ts';

declare global {
interface GlintCustomElements {
'my-custom-element-emit-element': MyCustomElement;
}

interface GlintElementRegistry {
MyCustomElement: MyCustomElement;
}

interface GlintHtmlElementAttributesMap {
MyCustomElement: {
propNum: number;
propStr: string;
};
}
}
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@babel/core": "^7.26.10",
"@babel/parser": "^7.27.0",
"@glimmer/component": "^2.0.0",
"@glint/ember-tsc": "workspace:*",
"@glint/tsserver-plugin": "workspace:*",
"@typescript-eslint/eslint-plugin": "^5.42.1",
"@typescript-eslint/parser": "^5.42.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -877,11 +877,11 @@ export function templateToTypescript(

mapper.text('__glintDSL__.applyAttributes(');

// We map the `__glintY__.element` arg to the first attribute node, which has the effect
// We map the `__glintY__` arg to the first attribute node, which has the effect
// such that diagnostics due to passing attributes to invalid elements will show up
// on the attribute, rather than on the whole element.
mapper.forNode(attr, () => {
mapper.text('__glintY__.element');
mapper.text('__glintY__');
});

mapper.text(', {');
Expand Down
45 changes: 45 additions & 0 deletions packages/template/-private/dsl/custom-elements.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
declare global {
/**
* Map of element tag names to their type. Used by `emitElement` via `ElementForTagName`.
*
* By default, this interface is empty; to add custom elements, you can
* augment it in your own project like so:
* ```ts
* declare global {
* interface GlintCustomElementRegistry {
* 'my-custom-element': MyCustomElementClass;
* }
* }
* ```
*
*/
interface GlintCustomElementMap {
/* intentionally empty, as there are no custom elements by default */
}
/**
* Map of custom element class names to their attributes type.
*
* This is a separate interface, because there isn't a TypeScript mechanism
* to get the list of attributes and properties assignable to a given element type.
*
* You _could_ set loose values such as `typeof YourELement`, but then you'll have things
* that don't make sense to assign in the template, such as methods (toString, etc)
*
* ```ts
* declare global {
* interface GlintCustomElementAttributesMap {
* // ok, with caveats, easiest.
* 'MyCustomElementClass': typeof MyCustomElementClass;
* // better, but more verbose
* 'MyCustomElementClass': {
* propNum: number;
* propStr: string;
* // etc
* }
* }
* ```
*/
interface GlintCustomElementAttributesMap {
/* intentionally empty, as there are no custom elements by default */
}
}
125 changes: 125 additions & 0 deletions packages/template/-private/dsl/elements.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4396,3 +4396,128 @@ interface GlintSvgElementAttributesMap {
['SVGElement']: GlobalSVGAttributes;
}
}

global {
interface GlintTagNameAttributesMap {
['a']: HTMLAnchorElementAttributes;
['area']: HTMLAreaElementAttributes;
['audio']: HTMLAudioElementAttributes;
['base']: HTMLBaseElementAttributes;
['blockquote']: HTMLQuoteElementAttributes;
['br']: HTMLBRElementAttributes;
['button']: HTMLButtonElementAttributes;
['canvas']: HTMLCanvasElementAttributes;
['caption']: HTMLTableCaptionElementAttributes;
['col']: HTMLTableColElementAttributes;
['data']: HTMLDataElementAttributes;
['del']: HTMLModElementAttributes;
['details']: HTMLDetailsElementAttributes;
['dialog']: HTMLDialogElementAttributes;
['div']: HTMLDivElementAttributes;
['dl']: HTMLDListElementAttributes;
['embed']: HTMLEmbedElementAttributes;
['fieldset']: HTMLFieldSetElementAttributes;
['form']: HTMLFormElementAttributes;
['h1']: HTMLHeadingElementAttributes;
['head']: HTMLHeadElementAttributes;
['hr']: HTMLHRElementAttributes;
['iframe']: HTMLIFrameElementAttributes;
['img']: HTMLImageElementAttributes;
['input']: HTMLInputElementAttributes;
['label']: HTMLLabelElementAttributes;
['legend']: HTMLLegendElementAttributes;
['li']: HTMLLIElementAttributes;
['link']: HTMLLinkElementAttributes;
['map']: HTMLMapElementAttributes;
['menu']: HTMLMenuElementAttributes;
['meta']: HTMLMetaElementAttributes;
['meter']: HTMLMeterElementAttributes;
['object']: HTMLObjectElementAttributes;
['ol']: HTMLOListElementAttributes;
['optgroup']: HTMLOptGroupElementAttributes;
['option']: HTMLOptionElementAttributes;
['output']: HTMLOutputElementAttributes;
['p']: HTMLParagraphElementAttributes;
['pre']: HTMLPreElementAttributes;
['progress']: HTMLProgressElementAttributes;
['script']: HTMLScriptElementAttributes;
['select']: HTMLSelectElementAttributes;
['slot']: HTMLSlotElementAttributes;
['source']: HTMLSourceElementAttributes;
['style']: HTMLStyleElementAttributes;
['table']: HTMLTableElementAttributes;
['tbody']: HTMLTableSectionElementAttributes;
['td']: HTMLTableCellElementAttributes;
['template']: HTMLTemplateElementAttributes;
['textarea']: HTMLTextAreaElementAttributes;
['time']: HTMLTimeElementAttributes;
['tr']: HTMLTableRowElementAttributes;
['track']: HTMLTrackElementAttributes;
['ul']: HTMLUListElementAttributes;
['video']: HTMLVideoElementAttributes;
['a']: SVGAElementAttributes;
['animate']: SVGAnimateElementAttributes;
['animateMotion']: SVGAnimateMotionElementAttributes;
['animateTransform']: SVGAnimateTransformElementAttributes;
['circle']: SVGCircleElementAttributes;
['clipPath']: SVGClipPathElementAttributes;
['defs']: SVGDefsElementAttributes;
['desc']: SVGDescElementAttributes;
['ellipse']: SVGEllipseElementAttributes;
['feBlend']: SVGFEBlendElementAttributes;
['feColorMatrix']: SVGFEColorMatrixElementAttributes;
['feComponentTransfer']: SVGFEComponentTransferElementAttributes;
['feComposite']: SVGFECompositeElementAttributes;
['feConvolveMatrix']: SVGFEConvolveMatrixElementAttributes;
['feDiffuseLighting']: SVGFEDiffuseLightingElementAttributes;
['feDisplacementMap']: SVGFEDisplacementMapElementAttributes;
['feDistantLight']: SVGFEDistantLightElementAttributes;
['feDropShadow']: SVGFEDropShadowElementAttributes;
['feFlood']: SVGFEFloodElementAttributes;
['feFuncA']: SVGFEFuncAElementAttributes;
['feFuncB']: SVGFEFuncBElementAttributes;
['feFuncG']: SVGFEFuncGElementAttributes;
['feFuncR']: SVGFEFuncRElementAttributes;
['feGaussianBlur']: SVGFEGaussianBlurElementAttributes;
['feImage']: SVGFEImageElementAttributes;
['feMerge']: SVGFEMergeElementAttributes;
['feMergeNode']: SVGFEMergeNodeElementAttributes;
['feMorphology']: SVGFEMorphologyElementAttributes;
['feOffset']: SVGFEOffsetElementAttributes;
['fePointLight']: SVGFEPointLightElementAttributes;
['feSpecularLighting']: SVGFESpecularLightingElementAttributes;
['feSpotLight']: SVGFESpotLightElementAttributes;
['feTile']: SVGFETileElementAttributes;
['feTurbulence']: SVGFETurbulenceElementAttributes;
['filter']: SVGFilterElementAttributes;
['foreignObject']: SVGForeignObjectElementAttributes;
['g']: SVGGElementAttributes;
['image']: SVGImageElementAttributes;
['line']: SVGLineElementAttributes;
['linearGradient']: SVGLinearGradientElementAttributes;
['marker']: SVGMarkerElementAttributes;
['mask']: SVGMaskElementAttributes;
['metadata']: SVGMetadataElementAttributes;
['mpath']: SVGMPathElementAttributes;
['path']: SVGPathElementAttributes;
['pattern']: SVGPatternElementAttributes;
['polygon']: SVGPolygonElementAttributes;
['polyline']: SVGPolylineElementAttributes;
['radialGradient']: SVGRadialGradientElementAttributes;
['rect']: SVGRectElementAttributes;
['script']: SVGScriptElementAttributes;
['set']: SVGSetElementAttributes;
['stop']: SVGStopElementAttributes;
['style']: SVGStyleElementAttributes;
['svg']: SVGSVGElementAttributes;
['switch']: SVGSwitchElementAttributes;
['symbol']: SVGSymbolElementAttributes;
['text']: SVGTextElementAttributes;
['textPath']: SVGTextPathElementAttributes;
['title']: SVGTitleElementAttributes;
['tspan']: SVGTSpanElementAttributes;
['use']: SVGUseElementAttributes;
['view']: SVGViewElementAttributes;
}
}

19 changes: 13 additions & 6 deletions packages/template/-private/dsl/emit.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AttrValue, ContentValue } from '..';
import { ContentValue } from '..';
import {
ComponentReturn,
AnyContext,
Expand All @@ -11,6 +11,7 @@ import {
} from '../integration';
import {
AttributesForElement,
AttributesForTagName,
ElementForTagName,
MathMlElementForTagName,
SVGElementForTagName,
Expand Down Expand Up @@ -52,20 +53,22 @@ export declare function emitContent(value: ContentValue): void;
export declare function emitElement<Name extends string | 'math' | 'svg'>(
name: Name,
): {
name: Name;
element: Name extends 'math'
? MathMlElementForTagName<'math'>
: Name extends 'svg'
? SVGElementForTagName<'svg'>
: ElementForTagName<Name>;
attributes: AttributesForTagName<Name>;
};

export declare function emitSVGElement<Name extends keyof SVGElementTagNameMap>(
name: Name,
): { element: SVGElementForTagName<Name> };
): { name: Name; element: SVGElementForTagName<Name> };

export declare function emitMathMlElement<Name extends keyof MathMLElementTagNameMap>(
name: Name,
): { element: MathMlElementForTagName<Name> };
): { name: Name; element: MathMlElementForTagName<Name> };

/*
* Emits the given value as an entity that expects to receive blocks
Expand Down Expand Up @@ -154,9 +157,13 @@ export declare function applySplattributes<
* <div foo={{bar}}></div>
* <AnotherComponent foo={{bar}} />
*/
export declare function applyAttributes<T extends Element>(
element: T,
attrs: Partial<AttributesForElement<T>>,
export declare function applyAttributes<
T extends Element | { name: string; element: unknown; attributes: unknown },
>(
invoked: T,
attrs: T extends Element
? { [K in string & keyof AttributesForElement<T>]?: AttributesForElement<T>[K] }
: { [K in string & keyof T['attributes']]?: T['attributes'][K] },
): void;

/*
Expand Down
Loading
Loading