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
44 changes: 35 additions & 9 deletions 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(path) {
switch (path.node.id.name) {
Expand Down Expand Up @@ -125,6 +135,7 @@ function createHtmlElementsAttributesMap() {
});
}
htmlElementsContent += '}\n';
addTagNameAttributesMapEntry(name, interfaceName);
}

let elementToAttributes = new Map();
Expand Down Expand Up @@ -216,6 +227,7 @@ function createSvgElementAttributesMap() {
}

svgElementsContent += `}\n`;
addTagNameAttributesMapEntry(name, interfaceName);
}

let elementToAttributes = new Map();
Expand Down Expand Up @@ -271,13 +283,27 @@ const filePath = resolve(
'../../packages/template/-private/dsl/elements.d.ts',
);

writeFileSync(
filePath,
[
'// Auto-generated by bin/build-elements.mjs',
'// this server to provide the html attributes for each element',
function defineTagNameAttributesMap(contents) {
return [
'',
createHtmlElementsAttributesMap(),
createSvgElementAttributesMap(),
].join('\n'),
);
`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 prefix = `// Auto-generated by bin/build-elements.mjs
//
`;

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
36 changes: 23 additions & 13 deletions packages/core/src/transform/diagnostics/augmentation.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import type ts from 'typescript';
import ts from 'typescript';
import GlimmerASTMappingTree, { MappingSource } from '../template/glimmer-ast-mapping-tree.js';
import TransformedModule from '../template/transformed-module.js';
import { Diagnostic } from './index.js';

function flattenMessageText(messageText: ts.Diagnostic['messageText']): string {
return ts.flattenDiagnosticMessageText(messageText, '\n');
}

export function augmentDiagnostics<T extends Diagnostic>(
transformedModule: TransformedModule,
diagnostics: T[],
Expand Down Expand Up @@ -77,12 +81,9 @@

function checkGlintLibImports(
diagnostic: Diagnostic,
_mapping: GlimmerASTMappingTree,

Check warning on line 84 in packages/core/src/transform/diagnostics/augmentation.ts

View workflow job for this annotation

GitHub Actions / Lint

'_mapping' is defined but never used
): Diagnostic | undefined {
let messageText =
typeof diagnostic.messageText === 'string'
? diagnostic.messageText
: diagnostic.messageText.messageText;
let messageText = flattenMessageText(diagnostic.messageText);

const typesModules = '@glint/ember-tsc/-private/dsl';

Expand Down Expand Up @@ -138,12 +139,21 @@
parentNode.type === 'ElementNode' &&
!/^(@|\.)/.test(node.name)
) {
// If the assignability issue is on an attribute name and it's not an `@arg`
// or `...attributes`, then it's an HTML attribute type issue.
return addGlintDetails(
diagnostic,
'An Element must be specified in the component signature in order to pass in HTML attributes.',
);
// This particular error generally indicates an elementless component invocation.
// Avoid adding this message for other TS2345 cases (e.g. unknown attrs) so we
// don't obscure the underlying TypeScript diagnostic.
let message = flattenMessageText(diagnostic.messageText);
if (
message.includes("Argument of type 'unknown' is not assignable to parameter of type 'Element'.") ||
message.includes("Type 'unknown' is not assignable to type 'Element'.")
) {
return addGlintDetails(
diagnostic,
'An Element must be specified in the component signature in order to pass in HTML attributes.',
);
}

return;
} else if (
node.type === 'MustacheStatement' &&
(parentNode.type === 'Template' ||
Expand Down Expand Up @@ -213,7 +223,7 @@

return {
...diagnostic,
messageText: `${diagnostic.messageText} ${note}`,
messageText: `${flattenMessageText(diagnostic.messageText)} ${note}`,
};
}
}
Expand Down Expand Up @@ -313,7 +323,7 @@
function addGlintDetails(diagnostic: Diagnostic, details: string): Diagnostic {
return {
...diagnostic,
messageText: `${details}\n${diagnostic.messageText}`,
messageText: `${details}\n${flattenMessageText(diagnostic.messageText)}`,
};
}

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 */
}
}
Loading
Loading