-
Notifications
You must be signed in to change notification settings - Fork 880
/
template.ts
165 lines (156 loc) · 4.32 KB
/
template.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
/**
* @fileoverview
*
* Utilities for analyzing lit-html templates.
*/
import type ts from 'typescript';
type TypeScript = typeof ts;
/**
* Returns true if the specifier is know to export the Lit html template tag.
*
* This can be used in a hueristic to determine if a template is a lit-html
* template.
*/
export const isKnownLitModuleSpecifier = (specifier: string): boolean => {
return (
specifier === 'lit' ||
specifier === 'lit-html' ||
specifier === 'lit-element'
);
};
/**
* Returns true if the given node is a tagged template expression with the
* lit-html template tag.
*/
export const isLitTaggedTemplateExpression = (
node: ts.Node,
ts: TypeScript,
checker: ts.TypeChecker
): node is ts.TaggedTemplateExpression => {
if (!ts.isTaggedTemplateExpression(node)) {
return false;
}
if (ts.isIdentifier(node.tag)) {
return isResolvedIdentifierLitHtmlTemplate(node.tag, ts, checker);
}
if (ts.isPropertyAccessExpression(node.tag)) {
return isResolvedPropertyAccessExpressionLitHtmlNamespace(
node.tag,
ts,
checker
);
}
return false;
};
/**
* Resolve a common pattern of using the `html` identifier of a lit namespace
* import.
*
* E.g.:
*
* ```ts
* import * as identifier from 'lit';
* identifier.html`<p>I am compiled!</p>`;
* ```
*/
const isResolvedPropertyAccessExpressionLitHtmlNamespace = (
node: ts.PropertyAccessExpression,
ts: TypeScript,
checker: ts.TypeChecker
): boolean => {
// Ensure propertyAccessExpression ends with `.html`.
if (ts.isIdentifier(node.name) && node.name.text !== 'html') {
return false;
}
// Expect a namespace preceding `html`, `<namespace>.html`.
if (!ts.isIdentifier(node.expression)) {
return false;
}
// Resolve the namespace if it has been aliased.
const symbol = checker.getSymbolAtLocation(node.expression);
if (!symbol) {
return false;
}
const namespaceImport = symbol.declarations?.[0];
if (!namespaceImport || !ts.isNamespaceImport(namespaceImport)) {
return false;
}
const importDeclaration = namespaceImport.parent.parent;
const specifier = importDeclaration.moduleSpecifier;
if (!ts.isStringLiteral(specifier)) {
return false;
}
return isKnownLitModuleSpecifier(specifier.text);
};
/**
* Resolve the tag function identifier back to an import, returning true if
* the original reference was the `html` export from `lit` or `lit-html`.
*
* This check handles: aliasing and reassigning the import.
*
* ```ts
* import {html as h} from 'lit';
* h``;
* // isResolvedIdentifierLitHtmlTemplate(<h ast node>) returns true
* ```
*
* ```ts
* import {html} from 'lit-html/static.js';
* html`false`;
* // isResolvedIdentifierLitHtmlTemplate(<html ast node>) returns false
* ```
*
* @param node a TaggedTemplateExpression tag
*/
const isResolvedIdentifierLitHtmlTemplate = (
node: ts.Identifier,
ts: TypeScript,
checker: ts.TypeChecker
): boolean => {
const symbol = checker.getSymbolAtLocation(node);
if (!symbol) {
return false;
}
const templateImport = symbol.declarations?.[0];
if (!templateImport || !ts.isImportSpecifier(templateImport)) {
return false;
}
// An import specifier has the following structures:
//
// `import {<propertyName> as <name>} from <moduleSpecifier>;`
// `import {<name>} from <moduleSpecifier>;`
//
// This check allows aliasing `html` by ensuring propertyName is `html`.
// Thus `{html as myHtml}` is a valid template that can be compiled.
// Otherwise a compilable template must be a direct import of lit's `html`
// tag function.
if (
(templateImport.propertyName &&
templateImport.propertyName.text !== 'html') ||
(!templateImport.propertyName && templateImport.name.text !== 'html')
) {
return false;
}
const namedImport = templateImport.parent;
if (!ts.isNamedImports(namedImport)) {
return false;
}
const importClause = namedImport.parent;
if (!ts.isImportClause(importClause)) {
return false;
}
const importDeclaration = importClause.parent;
if (!ts.isImportDeclaration(importDeclaration)) {
return false;
}
const specifier = importDeclaration.moduleSpecifier;
if (!ts.isStringLiteral(specifier)) {
return false;
}
return isKnownLitModuleSpecifier(specifier.text);
};