@glint/ember-tsc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/bin/ember-tsc.js +4 -0
  4. package/bin/glint-language-server.js +2 -0
  5. package/lib/cli/run-volar-tsc.d.ts +2 -0
  6. package/lib/cli/run-volar-tsc.d.ts.map +1 -0
  7. package/lib/cli/run-volar-tsc.js +30 -0
  8. package/lib/cli/run-volar-tsc.js.map +1 -0
  9. package/lib/config/config.d.ts +15 -0
  10. package/lib/config/config.d.ts.map +1 -0
  11. package/lib/config/config.js +21 -0
  12. package/lib/config/config.js.map +1 -0
  13. package/lib/config/environment.d.ts +26 -0
  14. package/lib/config/environment.d.ts.map +1 -0
  15. package/lib/config/environment.js +96 -0
  16. package/lib/config/environment.js.map +1 -0
  17. package/lib/config/index.d.ts +17 -0
  18. package/lib/config/index.d.ts.map +1 -0
  19. package/lib/config/index.js +26 -0
  20. package/lib/config/index.js.map +1 -0
  21. package/lib/config/loader.d.ts +25 -0
  22. package/lib/config/loader.d.ts.map +1 -0
  23. package/lib/config/loader.js +110 -0
  24. package/lib/config/loader.js.map +1 -0
  25. package/lib/config/types.cjs +3 -0
  26. package/lib/config/types.cjs.map +1 -0
  27. package/lib/config/types.d.cts +60 -0
  28. package/lib/config/types.d.cts.map +1 -0
  29. package/lib/environment-ember-template-imports/-private/environment/common.d.ts +13 -0
  30. package/lib/environment-ember-template-imports/-private/environment/common.d.ts.map +1 -0
  31. package/lib/environment-ember-template-imports/-private/environment/common.js +2 -0
  32. package/lib/environment-ember-template-imports/-private/environment/common.js.map +1 -0
  33. package/lib/environment-ember-template-imports/-private/environment/index.d.ts +3 -0
  34. package/lib/environment-ember-template-imports/-private/environment/index.d.ts.map +1 -0
  35. package/lib/environment-ember-template-imports/-private/environment/index.js +76 -0
  36. package/lib/environment-ember-template-imports/-private/environment/index.js.map +1 -0
  37. package/lib/environment-ember-template-imports/-private/environment/preprocess.d.ts +4 -0
  38. package/lib/environment-ember-template-imports/-private/environment/preprocess.d.ts.map +1 -0
  39. package/lib/environment-ember-template-imports/-private/environment/preprocess.js +73 -0
  40. package/lib/environment-ember-template-imports/-private/environment/preprocess.js.map +1 -0
  41. package/lib/environment-ember-template-imports/-private/environment/transform.d.ts +4 -0
  42. package/lib/environment-ember-template-imports/-private/environment/transform.d.ts.map +1 -0
  43. package/lib/environment-ember-template-imports/-private/environment/transform.js +134 -0
  44. package/lib/environment-ember-template-imports/-private/environment/transform.js.map +1 -0
  45. package/lib/index.d.ts +7 -0
  46. package/lib/index.d.ts.map +1 -0
  47. package/lib/index.js +6 -0
  48. package/lib/index.js.map +1 -0
  49. package/lib/plugins/g-compiler-errors.d.ts +12 -0
  50. package/lib/plugins/g-compiler-errors.d.ts.map +1 -0
  51. package/lib/plugins/g-compiler-errors.js +58 -0
  52. package/lib/plugins/g-compiler-errors.js.map +1 -0
  53. package/lib/plugins/g-template-tag-symbols.d.ts +11 -0
  54. package/lib/plugins/g-template-tag-symbols.d.ts.map +1 -0
  55. package/lib/plugins/g-template-tag-symbols.js +48 -0
  56. package/lib/plugins/g-template-tag-symbols.js.map +1 -0
  57. package/lib/plugins/utils.d.ts +25 -0
  58. package/lib/plugins/utils.d.ts.map +1 -0
  59. package/lib/plugins/utils.js +63 -0
  60. package/lib/plugins/utils.js.map +1 -0
  61. package/lib/transform/diagnostics/augmentation.d.ts +4 -0
  62. package/lib/transform/diagnostics/augmentation.d.ts.map +1 -0
  63. package/lib/transform/diagnostics/augmentation.js +223 -0
  64. package/lib/transform/diagnostics/augmentation.js.map +1 -0
  65. package/lib/transform/diagnostics/index.d.ts +5 -0
  66. package/lib/transform/diagnostics/index.d.ts.map +1 -0
  67. package/lib/transform/diagnostics/index.js +2 -0
  68. package/lib/transform/diagnostics/index.js.map +1 -0
  69. package/lib/transform/index.d.ts +4 -0
  70. package/lib/transform/index.d.ts.map +1 -0
  71. package/lib/transform/index.js +2 -0
  72. package/lib/transform/index.js.map +1 -0
  73. package/lib/transform/template/code-features.d.ts +30 -0
  74. package/lib/transform/template/code-features.d.ts.map +1 -0
  75. package/lib/transform/template/code-features.js +26 -0
  76. package/lib/transform/template/code-features.js.map +1 -0
  77. package/lib/transform/template/glimmer-ast-mapping-tree.d.ts +80 -0
  78. package/lib/transform/template/glimmer-ast-mapping-tree.d.ts.map +1 -0
  79. package/lib/transform/template/glimmer-ast-mapping-tree.js +132 -0
  80. package/lib/transform/template/glimmer-ast-mapping-tree.js.map +1 -0
  81. package/lib/transform/template/inlining/index.d.ts +16 -0
  82. package/lib/transform/template/inlining/index.d.ts.map +1 -0
  83. package/lib/transform/template/inlining/index.js +21 -0
  84. package/lib/transform/template/inlining/index.js.map +1 -0
  85. package/lib/transform/template/inlining/tagged-strings.d.ts +8 -0
  86. package/lib/transform/template/inlining/tagged-strings.d.ts.map +1 -0
  87. package/lib/transform/template/inlining/tagged-strings.js +140 -0
  88. package/lib/transform/template/inlining/tagged-strings.js.map +1 -0
  89. package/lib/transform/template/map-template-contents.d.ts +121 -0
  90. package/lib/transform/template/map-template-contents.d.ts.map +1 -0
  91. package/lib/transform/template/map-template-contents.js +287 -0
  92. package/lib/transform/template/map-template-contents.js.map +1 -0
  93. package/lib/transform/template/rewrite-module.d.ts +22 -0
  94. package/lib/transform/template/rewrite-module.d.ts.map +1 -0
  95. package/lib/transform/template/rewrite-module.js +265 -0
  96. package/lib/transform/template/rewrite-module.js.map +1 -0
  97. package/lib/transform/template/scope-stack.d.ts +13 -0
  98. package/lib/transform/template/scope-stack.d.ts.map +1 -0
  99. package/lib/transform/template/scope-stack.js +28 -0
  100. package/lib/transform/template/scope-stack.js.map +1 -0
  101. package/lib/transform/template/template-to-typescript.d.ts +19 -0
  102. package/lib/transform/template/template-to-typescript.d.ts.map +1 -0
  103. package/lib/transform/template/template-to-typescript.js +1095 -0
  104. package/lib/transform/template/template-to-typescript.js.map +1 -0
  105. package/lib/transform/template/transformed-module.d.ts +111 -0
  106. package/lib/transform/template/transformed-module.d.ts.map +1 -0
  107. package/lib/transform/template/transformed-module.js +287 -0
  108. package/lib/transform/template/transformed-module.js.map +1 -0
  109. package/lib/transform/util.d.ts +7 -0
  110. package/lib/transform/util.d.ts.map +1 -0
  111. package/lib/transform/util.js +15 -0
  112. package/lib/transform/util.js.map +1 -0
  113. package/lib/volar/ember-language-plugin.d.ts +14 -0
  114. package/lib/volar/ember-language-plugin.d.ts.map +1 -0
  115. package/lib/volar/ember-language-plugin.js +91 -0
  116. package/lib/volar/ember-language-plugin.js.map +1 -0
  117. package/lib/volar/gts-virtual-code.d.ts +83 -0
  118. package/lib/volar/gts-virtual-code.d.ts.map +1 -0
  119. package/lib/volar/gts-virtual-code.js +210 -0
  120. package/lib/volar/gts-virtual-code.js.map +1 -0
  121. package/lib/volar/language-server.d.ts +2 -0
  122. package/lib/volar/language-server.d.ts.map +1 -0
  123. package/lib/volar/language-server.js +214 -0
  124. package/lib/volar/language-server.js.map +1 -0
  125. package/lib/volar/script-snapshot.d.ts +17 -0
  126. package/lib/volar/script-snapshot.d.ts.map +1 -0
  127. package/lib/volar/script-snapshot.js +24 -0
  128. package/lib/volar/script-snapshot.js.map +1 -0
  129. package/package.json +104 -0
  130. package/src/cli/run-volar-tsc.ts +36 -0
  131. package/src/config/config.ts +33 -0
  132. package/src/config/environment.ts +128 -0
  133. package/src/config/index.ts +30 -0
  134. package/src/config/loader.ts +143 -0
  135. package/src/config/types.cts +85 -0
  136. package/src/environment-ember-template-imports/-private/environment/common.ts +14 -0
  137. package/src/environment-ember-template-imports/-private/environment/index.ts +83 -0
  138. package/src/environment-ember-template-imports/-private/environment/preprocess.ts +90 -0
  139. package/src/environment-ember-template-imports/-private/environment/transform.ts +202 -0
  140. package/src/index.ts +9 -0
  141. package/src/plugins/g-compiler-errors.ts +67 -0
  142. package/src/plugins/g-template-tag-symbols.ts +54 -0
  143. package/src/plugins/utils.ts +86 -0
  144. package/src/transform/diagnostics/augmentation.ts +333 -0
  145. package/src/transform/diagnostics/index.ts +5 -0
  146. package/src/transform/index.ts +4 -0
  147. package/src/transform/template/code-features.ts +30 -0
  148. package/src/transform/template/glimmer-ast-mapping-tree.ts +173 -0
  149. package/src/transform/template/inlining/index.ts +33 -0
  150. package/src/transform/template/inlining/tagged-strings.ts +187 -0
  151. package/src/transform/template/map-template-contents.ts +501 -0
  152. package/src/transform/template/rewrite-module.ts +372 -0
  153. package/src/transform/template/scope-stack.ts +34 -0
  154. package/src/transform/template/template-to-typescript.ts +1476 -0
  155. package/src/transform/template/transformed-module.ts +431 -0
  156. package/src/transform/util.ts +24 -0
  157. package/src/volar/ember-language-plugin.ts +108 -0
  158. package/src/volar/gts-virtual-code.ts +249 -0
  159. package/src/volar/language-server.ts +250 -0
  160. package/src/volar/script-snapshot.ts +27 -0
  161. package/types/-private/dsl/globals.d.ts +204 -0
  162. package/types/-private/dsl/index.d.ts +50 -0
  163. package/types/-private/dsl/integration-declarations.d.ts +143 -0
  164. package/types/-private/intrinsics/action.d.ts +45 -0
  165. package/types/-private/intrinsics/concat.d.ts +6 -0
  166. package/types/-private/intrinsics/each-in.d.ts +24 -0
  167. package/types/-private/intrinsics/each.d.ts +17 -0
  168. package/types/-private/intrinsics/fn.d.ts +44 -0
  169. package/types/-private/intrinsics/get.d.ts +31 -0
  170. package/types/-private/intrinsics/input.d.ts +24 -0
  171. package/types/-private/intrinsics/link-to.d.ts +31 -0
  172. package/types/-private/intrinsics/log.d.ts +6 -0
  173. package/types/-private/intrinsics/mount.d.ts +9 -0
  174. package/types/-private/intrinsics/mut.d.ts +14 -0
  175. package/types/-private/intrinsics/on.d.ts +21 -0
  176. package/types/-private/intrinsics/outlet.d.ts +8 -0
  177. package/types/-private/intrinsics/textarea.d.ts +16 -0
  178. package/types/-private/intrinsics/unbound.d.ts +10 -0
  179. package/types/-private/intrinsics/unique-id.d.ts +5 -0
  180. package/types/globals/index.d.ts +3 -0
  181. package/types/silent-error.d.ts +4 -0
@@ -0,0 +1,1476 @@
1
+ import { AST } from '@glimmer/syntax';
2
+ import { GlintEmitMetadata, GlintSpecialForm } from '@glint/ember-tsc/config-types';
3
+ import { assert, unreachable } from '../util.js';
4
+ import { TextContent } from './glimmer-ast-mapping-tree.js';
5
+ import { EmbeddingSyntax, mapTemplateContents, RewriteResult } from './map-template-contents.js';
6
+ import ScopeStack from './scope-stack.js';
7
+
8
+ const SPLATTRIBUTES = '...attributes';
9
+
10
+ export type TemplateToTypescriptOptions = {
11
+ typesModule: string;
12
+ meta?: GlintEmitMetadata | undefined;
13
+ globals?: Array<string> | undefined;
14
+ backingValue?: string;
15
+ preamble?: Array<string>;
16
+ embeddingSyntax?: EmbeddingSyntax;
17
+ useJsDoc?: boolean;
18
+ specialForms?: Record<string, GlintSpecialForm>;
19
+ };
20
+
21
+ /**
22
+ * NOTE: this is tech debt. Tho solving it will require more work than polyfilling old behavior of path.parts
23
+ * @param node
24
+ * @returns
25
+ */
26
+ function getPathParts(node: AST.PathExpression): string[] {
27
+ // The original code which used old @glimmer/syntax used node.parts,
28
+ // which never included the @ of the path.
29
+ let atLess = node.head.original.replace(/^@/, '');
30
+
31
+ // The original path.parts array did not include "this" in the parts.
32
+ if (atLess === 'this') return node.tail;
33
+
34
+ return [atLess, ...node.tail];
35
+ }
36
+
37
+ /**
38
+ * Given the text contents of a template, returns a TypeScript representation
39
+ * of that template's contents, as well as a mapping of offsets and ranges between
40
+ * the original and transformed contents.
41
+ */
42
+ export function templateToTypescript(
43
+ originalTemplate: string,
44
+ {
45
+ typesModule,
46
+ globals,
47
+ meta,
48
+ backingValue,
49
+ preamble = [],
50
+ embeddingSyntax = { prefix: '', suffix: '' },
51
+ specialForms = {},
52
+ useJsDoc = false,
53
+ }: TemplateToTypescriptOptions,
54
+ ): RewriteResult {
55
+ let { prefix, suffix } = embeddingSyntax;
56
+ let template = `${''.padEnd(prefix.length)}${originalTemplate}${''.padEnd(suffix.length)}`;
57
+
58
+ return mapTemplateContents(originalTemplate, { embeddingSyntax }, (ast, mapper) => {
59
+ let { rangeForNode } = mapper;
60
+ let scope = new ScopeStack([]);
61
+ let inHtmlContext: 'svg' | 'math' | 'default' = 'default';
62
+
63
+ emitTemplateBoilerplate(() => {
64
+ for (let statement of ast?.body ?? []) {
65
+ emitTopLevelStatement(statement);
66
+ }
67
+ });
68
+
69
+ return;
70
+
71
+ function emitTopLevelStatement(node: AST.TopLevelStatement): void {
72
+ switch (node.type) {
73
+ case 'Block':
74
+ throw new Error(`Internal error: unexpected top-level ${node.type}`);
75
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
76
+ // @ts-expect-error
77
+ case 'PartialStatement':
78
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
79
+ // @ts-expect-error
80
+ throw new Error(`Internal error: unexpected top-level ${node.type}`);
81
+
82
+ case 'TextNode':
83
+ return emitTopLevelTextNode(node);
84
+
85
+ case 'CommentStatement':
86
+ case 'MustacheCommentStatement':
87
+ return emitComment(node);
88
+
89
+ case 'MustacheStatement':
90
+ return emitTopLevelMustacheStatement(node);
91
+
92
+ case 'BlockStatement':
93
+ return emitBlockStatement(node);
94
+
95
+ case 'ElementNode':
96
+ return emitElementNode(node);
97
+
98
+ default:
99
+ unreachable(node);
100
+ }
101
+ }
102
+
103
+ function emitTemplateBoilerplate(emitBody: () => void): void {
104
+ if (meta?.prepend) {
105
+ mapper.text(meta.prepend);
106
+ }
107
+
108
+ if (useJsDoc) {
109
+ mapper.text(`(/** @type {typeof import(`);
110
+ if (ast) {
111
+ mapper.forNode(ast, () => {
112
+ mapper.text(`"${typesModule}"`);
113
+ });
114
+ } else {
115
+ mapper.text(`"${typesModule}"`);
116
+ }
117
+ mapper.text(`)} */ ({}))`);
118
+ } else {
119
+ mapper.text(`({} as typeof import(`);
120
+ if (ast) {
121
+ mapper.forNode(ast, () => {
122
+ mapper.text(`"${typesModule}"`);
123
+ });
124
+ } else {
125
+ mapper.text(`"${typesModule}"`);
126
+ }
127
+ mapper.text(`))`);
128
+ }
129
+
130
+ if (backingValue) {
131
+ mapper.text(`.templateForBackingValue(${backingValue}, function(__glintRef__`);
132
+ } else {
133
+ mapper.text(`.templateExpression(function(__glintRef__`);
134
+ }
135
+
136
+ if (useJsDoc) {
137
+ mapper.text(`, /** @type {typeof import("${typesModule}")} */ __glintDSL__) {`);
138
+ } else {
139
+ mapper.text(`, __glintDSL__: typeof import("${typesModule}")) {`);
140
+ }
141
+
142
+ mapper.newline();
143
+ mapper.indent();
144
+
145
+ for (let line of preamble) {
146
+ mapper.text(line);
147
+ mapper.newline();
148
+ }
149
+
150
+ if (ast) {
151
+ mapper.forNode(ast, emitBody);
152
+ }
153
+
154
+ // Ensure the context and lib variables are always consumed to prevent
155
+ // an unused variable warning
156
+ mapper.text('__glintRef__; __glintDSL__;');
157
+ mapper.newline();
158
+
159
+ mapper.emitDirectivePlaceholders();
160
+
161
+ mapper.dedent();
162
+ mapper.text('})');
163
+
164
+ if (meta?.append) {
165
+ mapper.text(meta.append);
166
+ }
167
+ }
168
+
169
+ function emitTopLevelTextNode(node: AST.TextNode): void {
170
+ // We don't need to emit any code for text nodes, but we want to track
171
+ // where they are so we know NOT to try and suggest global completions
172
+ // in "text space" where it wouldn't make sense.
173
+ mapper.nothing(node, new TextContent());
174
+ }
175
+
176
+ function emitComment(node: AST.MustacheCommentStatement | AST.CommentStatement): void {
177
+ let text = node.value.trim();
178
+ const directiveRegex = /^@glint-([a-z-]+)/i;
179
+ let match = directiveRegex.exec(text);
180
+ if (!match) {
181
+ return mapper.nothing(node);
182
+ }
183
+
184
+ emitDirective(match, node);
185
+ }
186
+
187
+ function emitDirective(
188
+ match: RegExpExecArray,
189
+ node: AST.CommentStatement | AST.MustacheCommentStatement,
190
+ ): void {
191
+ let kind = match[1];
192
+ let location = rangeForNode(node);
193
+ if (kind === 'ignore' || kind === 'expect-error') {
194
+ mapper.directive(kind, node, location, mapper.rangeForLine(node.loc.end.line + 1));
195
+ } else if (kind === 'nocheck') {
196
+ mapper.directive('ignore', node, location, { start: 0, end: template.length - 1 });
197
+ } else if (kind === 'in-svg') {
198
+ inHtmlContext = 'svg';
199
+ } else if (kind === 'in-mathml') {
200
+ inHtmlContext = 'math';
201
+ } else if (kind === 'out-svg' || kind === 'out-mathml') {
202
+ inHtmlContext = 'default';
203
+ } else {
204
+ // Push an error on the record
205
+ mapper.error(`Unknown directive @glint-${kind}`, location);
206
+ }
207
+ }
208
+
209
+ function emitTopLevelMustacheStatement(node: AST.MustacheStatement): void {
210
+ emitMustacheStatement(node, 'top-level');
211
+ mapper.text(';');
212
+ mapper.newline();
213
+ }
214
+
215
+ // Captures the context in which a given invocation (i.e. a mustache or
216
+ // sexpr) is being performed. Certain keywords like `yield` are only
217
+ // valid in certain positions, and whether a param-less mustache implicitly
218
+ // evaluates a helper or returns it also depends on the location it's in.
219
+ type InvokePosition = 'top-level' | 'attr' | 'arg' | 'concat' | 'sexpr';
220
+
221
+ function emitSpecialFormExpression(
222
+ formInfo: SpecialFormInfo,
223
+ node: AST.MustacheStatement | AST.SubExpression,
224
+ position: InvokePosition,
225
+ ): void {
226
+ if (formInfo.requiresConsumption) {
227
+ mapper.text('(__glintDSL__.noop(');
228
+ emitExpression(node.path);
229
+ mapper.text('), ');
230
+ }
231
+
232
+ switch (formInfo.form) {
233
+ case 'yield':
234
+ emitYieldExpression(formInfo, node, position);
235
+ break;
236
+
237
+ case 'if':
238
+ emitIfExpression(formInfo, node);
239
+ break;
240
+
241
+ case 'if-not':
242
+ emitIfNotExpression(formInfo, node);
243
+ break;
244
+
245
+ case 'object-literal':
246
+ emitObjectExpression(formInfo, node);
247
+ break;
248
+
249
+ case 'array-literal':
250
+ emitArrayExpression(formInfo, node);
251
+ break;
252
+
253
+ case 'bind-invokable':
254
+ emitBindInvokableExpression(formInfo, node, position);
255
+ break;
256
+
257
+ case '===':
258
+ case '!==':
259
+ emitBinaryOperatorExpression(formInfo, node);
260
+ break;
261
+
262
+ case '&&':
263
+ case '||':
264
+ emitLogicalExpression(formInfo, node);
265
+ break;
266
+
267
+ case '!':
268
+ emitUnaryOperatorExpression(formInfo, node);
269
+ break;
270
+
271
+ default:
272
+ mapper.error(`${formInfo.name} is not valid in inline form`, rangeForNode(node));
273
+ mapper.text('undefined');
274
+ }
275
+
276
+ if (formInfo.requiresConsumption) {
277
+ mapper.text(')');
278
+ }
279
+ }
280
+
281
+ function emitBindInvokableExpression(
282
+ formInfo: SpecialFormInfo,
283
+ node: AST.MustacheStatement | AST.SubExpression,
284
+ position: InvokePosition,
285
+ ): void {
286
+ mapper.forNode(node, () => {
287
+ assert(
288
+ node.params.length >= 1,
289
+ () => `{{${formInfo.name}}} requires at least one positional argument`,
290
+ );
291
+
292
+ assert(
293
+ node.params.length === 1 || node.hash.pairs.length === 0,
294
+ () =>
295
+ `Due to TypeScript inference limitations, {{${formInfo.name}}} can only pre-bind ` +
296
+ `either named or positional arguments in a single pass. You can instead break the ` +
297
+ `binding into two parts, e.g. ` +
298
+ `{{${formInfo.name} (${formInfo.name} ... posA posB) namedA=true namedB=true}}`,
299
+ );
300
+
301
+ if (position === 'top-level') {
302
+ mapper.text('__glintDSL__.emitContent(');
303
+ }
304
+
305
+ // Treat the first argument to a bind-invokable expression (`{{component}}`,
306
+ // `{{helper}}`, etc) as special: we wrap it in a `resolve` call so that the
307
+ // type machinery for those helpers can always operate against the resolved value.
308
+ // We wrap the `resolveForBind` call in an IIFE to prevent "backpressure" in
309
+ // type inference from the subsequent arguments that are being passed: the bound
310
+ // invokable is the source of record for its own type and we don't want inference
311
+ // from the `resolveForBind` call to be affected by other (potentially incorrect)
312
+ // parameter types.
313
+ mapper.text('__glintDSL__.resolve(');
314
+ emitExpression(node.path);
315
+ mapper.text(')((() => __glintDSL__.resolveForBind(');
316
+ emitExpression(node.params[0]);
317
+ mapper.text('))(), ');
318
+ emitArgs(node.params.slice(1), node.hash);
319
+ mapper.text(')');
320
+
321
+ if (position === 'top-level') {
322
+ mapper.text(')');
323
+ }
324
+ });
325
+ }
326
+
327
+ function emitObjectExpression(
328
+ formInfo: SpecialFormInfo,
329
+ node: AST.MustacheStatement | AST.SubExpression,
330
+ ): void {
331
+ mapper.forNode(node, () => {
332
+ assert(
333
+ node.params.length === 0,
334
+ () => `{{${formInfo.name}}} only accepts named parameters`,
335
+ );
336
+
337
+ if (!node.hash.pairs.length) {
338
+ mapper.text('{}');
339
+ return;
340
+ }
341
+
342
+ mapper.text('({');
343
+ mapper.indent();
344
+ mapper.newline();
345
+
346
+ let start = template.indexOf('hash', rangeForNode(node).start) + 4;
347
+ for (let pair of node.hash.pairs) {
348
+ start = template.indexOf(pair.key, start);
349
+ emitHashKey(pair.key, start);
350
+ mapper.text(': ');
351
+ emitExpression(pair.value);
352
+ mapper.text(',');
353
+ mapper.newline();
354
+ }
355
+
356
+ mapper.dedent();
357
+ mapper.text('})');
358
+ });
359
+ }
360
+
361
+ function emitArrayExpression(
362
+ formInfo: SpecialFormInfo,
363
+ node: AST.MustacheStatement | AST.SubExpression,
364
+ ): void {
365
+ mapper.forNode(node, () => {
366
+ assert(
367
+ node.hash.pairs.length === 0,
368
+ () => `{{${formInfo.name}}} only accepts positional parameters`,
369
+ );
370
+
371
+ mapper.text('[');
372
+
373
+ for (let [index, param] of node.params.entries()) {
374
+ emitExpression(param);
375
+
376
+ if (index < node.params.length - 1) {
377
+ mapper.text(', ');
378
+ }
379
+ }
380
+
381
+ mapper.text(']');
382
+ });
383
+ }
384
+
385
+ function emitIfExpression(
386
+ formInfo: SpecialFormInfo,
387
+ node: AST.MustacheStatement | AST.SubExpression,
388
+ ): void {
389
+ mapper.forNode(node, () => {
390
+ assert(
391
+ node.params.length >= 2,
392
+ () => `{{${formInfo.name}}} requires at least two parameters`,
393
+ );
394
+
395
+ mapper.text('(');
396
+ emitExpression(node.params[0]);
397
+ mapper.text(') ? (');
398
+ emitExpression(node.params[1]);
399
+ mapper.text(') : (');
400
+
401
+ if (node.params[2]) {
402
+ emitExpression(node.params[2]);
403
+ } else {
404
+ mapper.text('undefined');
405
+ }
406
+
407
+ mapper.text(')');
408
+ });
409
+ }
410
+
411
+ function emitIfNotExpression(
412
+ formInfo: SpecialFormInfo,
413
+ node: AST.MustacheStatement | AST.SubExpression,
414
+ ): void {
415
+ mapper.forNode(node, () => {
416
+ assert(
417
+ node.params.length >= 2,
418
+ () => `{{${formInfo.name}}} requires at least two parameters`,
419
+ );
420
+
421
+ mapper.text('!(');
422
+ emitExpression(node.params[0]);
423
+ mapper.text(') ? (');
424
+ emitExpression(node.params[1]);
425
+ mapper.text(') : (');
426
+
427
+ if (node.params[2]) {
428
+ emitExpression(node.params[2]);
429
+ } else {
430
+ mapper.text('undefined');
431
+ }
432
+
433
+ mapper.text(')');
434
+ });
435
+ }
436
+
437
+ function emitBinaryOperatorExpression(
438
+ formInfo: SpecialFormInfo,
439
+ node: AST.MustacheStatement | AST.SubExpression,
440
+ ): void {
441
+ mapper.forNode(node, () => {
442
+ assert(
443
+ node.hash.pairs.length === 0,
444
+ () => `{{${formInfo.name}}} only accepts positional parameters`,
445
+ );
446
+ assert(
447
+ node.params.length === 2,
448
+ () => `{{${formInfo.name}}} requires exactly two parameters`,
449
+ );
450
+
451
+ const [left, right] = node.params;
452
+
453
+ mapper.text('(');
454
+ emitExpression(left);
455
+ mapper.text(` ${formInfo.form} `);
456
+ emitExpression(right);
457
+ mapper.text(')');
458
+ });
459
+ }
460
+
461
+ function emitLogicalExpression(
462
+ formInfo: SpecialFormInfo,
463
+ node: AST.MustacheStatement | AST.SubExpression,
464
+ ): void {
465
+ mapper.forNode(node, () => {
466
+ assert(
467
+ node.hash.pairs.length === 0,
468
+ () => `{{${formInfo.name}}} only accepts positional parameters`,
469
+ );
470
+ assert(
471
+ node.params.length >= 2,
472
+ () => `{{${formInfo.name}}} requires at least two parameters`,
473
+ );
474
+
475
+ mapper.text('(');
476
+ for (const [index, param] of node.params.entries()) {
477
+ emitExpression(param);
478
+
479
+ if (index < node.params.length - 1) {
480
+ mapper.text(` ${formInfo.form} `);
481
+ }
482
+ }
483
+ mapper.text(')');
484
+ });
485
+ }
486
+
487
+ function emitUnaryOperatorExpression(
488
+ formInfo: SpecialFormInfo,
489
+ node: AST.MustacheStatement | AST.SubExpression,
490
+ ): void {
491
+ mapper.forNode(node, () => {
492
+ assert(
493
+ node.hash.pairs.length === 0,
494
+ () => `{{${formInfo.name}}} only accepts positional parameters`,
495
+ );
496
+ assert(
497
+ node.params.length === 1,
498
+ () => `{{${formInfo.name}}} requires exactly one parameter`,
499
+ );
500
+
501
+ const [param] = node.params;
502
+
503
+ mapper.text(formInfo.form);
504
+ emitExpression(param);
505
+ });
506
+ }
507
+
508
+ type SpecialFormInfo = {
509
+ form: GlintSpecialForm;
510
+ name: string;
511
+ requiresConsumption: boolean;
512
+ };
513
+
514
+ function checkSpecialForm(node: AST.CallNode): SpecialFormInfo | null {
515
+ if (
516
+ node.path.type === 'PathExpression' &&
517
+ node.path.head.type === 'VarHead' &&
518
+ !node.path.tail.length
519
+ ) {
520
+ let name = node.path.head.name;
521
+ if (typeof specialForms[name] === 'string' && !scope.hasBinding(name)) {
522
+ let isGlobal = globals ? globals.includes(name) : true;
523
+ let form = specialForms[name];
524
+
525
+ return { name, form, requiresConsumption: !isGlobal };
526
+ }
527
+ }
528
+
529
+ return null;
530
+ }
531
+
532
+ function emitExpression(node: AST.Expression): void {
533
+ switch (node.type) {
534
+ case 'PathExpression':
535
+ return emitPath(node);
536
+
537
+ case 'SubExpression':
538
+ return emitSubExpression(node);
539
+
540
+ case 'BooleanLiteral':
541
+ case 'NullLiteral':
542
+ case 'NumberLiteral':
543
+ case 'StringLiteral':
544
+ case 'UndefinedLiteral':
545
+ return emitLiteral(node);
546
+
547
+ default:
548
+ unreachable(node);
549
+ }
550
+ }
551
+
552
+ function emitElementNode(node: AST.ElementNode): void {
553
+ let firstCharacter = node.tag.charAt(0);
554
+ if (
555
+ firstCharacter.toUpperCase() === firstCharacter ||
556
+ node.tag.includes('.') ||
557
+ scope.hasBinding(node.tag)
558
+ ) {
559
+ emitComponent(node);
560
+ } else {
561
+ emitPlainElement(node);
562
+ }
563
+ }
564
+
565
+ function emitConcatStatement(node: AST.ConcatStatement): void {
566
+ mapper.forNode(node, () => {
567
+ mapper.text('`');
568
+ for (let part of node.parts) {
569
+ if (part.type === 'MustacheStatement') {
570
+ mapper.text('$' + '{');
571
+ emitMustacheStatement(part, 'concat');
572
+ mapper.text('}');
573
+ }
574
+ }
575
+ mapper.text('`');
576
+ });
577
+ }
578
+
579
+ function emitIdentifierReference(name: string, hbsOffset: number): void {
580
+ if (treatAsGlobal(name)) {
581
+ mapper.text('__glintDSL__.Globals["');
582
+ mapper.identifier(JSON.stringify(name).slice(1, -1), hbsOffset, name.length);
583
+ mapper.text('"]');
584
+ } else {
585
+ mapper.identifier(makeJSSafe(name), hbsOffset, name.length);
586
+ }
587
+ }
588
+
589
+ function treatAsGlobal(name: string): boolean {
590
+ if (globals) {
591
+ // If we have a known set of global identifiers, we should only treat
592
+ // members of that set as global, unless the identifier is in scope,
593
+ // and assume everything else is local. This is typically true in
594
+ // environments that capture scope, like strict-mode Ember.
595
+ return globals.includes(name) && !scope.hasBinding(name);
596
+ } else {
597
+ // Otherwise, we assume everything is global unless we can see it
598
+ // in scope as a block variable. This is the case in resolver-based
599
+ // environments like loose-mode Ember.
600
+ return !scope.hasBinding(name);
601
+ }
602
+ }
603
+
604
+ function tagNameToPathContents(node: AST.ElementNode): {
605
+ start: number;
606
+ kind: PathKind;
607
+ path: Array<string>;
608
+ } {
609
+ let tagName = node.tag;
610
+ let start = template.indexOf(tagName, rangeForNode(node).start);
611
+
612
+ if (tagName.startsWith('@')) {
613
+ return {
614
+ start,
615
+ kind: 'arg',
616
+ path: tagName.slice(1).split('.'),
617
+ };
618
+ } else if (tagName.startsWith('this.')) {
619
+ return {
620
+ start,
621
+ kind: 'this',
622
+ path: tagName.slice('this.'.length).split('.'),
623
+ };
624
+ } else {
625
+ return {
626
+ start,
627
+ kind: 'free',
628
+ path: tagName.split('.'),
629
+ };
630
+ }
631
+ }
632
+
633
+ function emitComponent(node: AST.ElementNode): void {
634
+ mapper.forNode(node, () => {
635
+ let { start, path, kind } = tagNameToPathContents(node);
636
+
637
+ for (let comment of node.comments) {
638
+ emitComment(comment);
639
+ }
640
+
641
+ mapper.text('{');
642
+ mapper.newline();
643
+ mapper.indent();
644
+
645
+ // Resolve the component and stash into the `__glintY__` variable for later invocation.
646
+ mapper.text('const __glintY__ = __glintDSL__.emitComponent(');
647
+
648
+ // Error boundary: "Expected 1 arguments, but got 0." e.g. when invoking `<ComponentThatHasArgs />`
649
+ mapper.forNode(node.path, () => {
650
+ mapper.text('__glintDSL__.resolve(');
651
+ emitPathContents(path, start, kind);
652
+ mapper.text(')');
653
+ });
654
+
655
+ // "Call" the component, optionally passing args if they are provided in the template.
656
+ mapper.text('(');
657
+
658
+ let dataAttrs = node.attributes.filter(({ name }) => name.startsWith('@'));
659
+ if (dataAttrs.length) {
660
+ // Error boundary: "Expected 0 arguments, but got 1." e.g. when invoking `<ComponentThatHasNoArgs @foo={{bar}} />`
661
+ mapper.forNodeWithSpan(node, node.openTag, () => {
662
+ mapper.text('{ ');
663
+
664
+ for (let attr of dataAttrs) {
665
+ mapper.forNode(attr, () => {
666
+ mapper.newline();
667
+
668
+ const attrStartOffset = attr.loc.getStart().offset!;
669
+ emitHashKey(attr.name.slice(1), attrStartOffset + prefix.length + 1);
670
+ mapper.text(': ');
671
+
672
+ switch (attr.value.type) {
673
+ case 'TextNode':
674
+ mapper.text(JSON.stringify(attr.value.chars));
675
+ break;
676
+ case 'ConcatStatement':
677
+ emitConcatStatement(attr.value);
678
+ break;
679
+ case 'MustacheStatement':
680
+ emitMustacheStatement(attr.value, 'arg');
681
+ break;
682
+ default:
683
+ unreachable(attr.value);
684
+ }
685
+ });
686
+
687
+ start = rangeForNode(attr.value).end;
688
+ mapper.text(', ');
689
+ }
690
+
691
+ mapper.text('...__glintDSL__.NamedArgsMarker }');
692
+ });
693
+ }
694
+
695
+ mapper.text('));');
696
+ mapper.newline();
697
+
698
+ emitAttributesAndModifiers(node);
699
+
700
+ if (!node.selfClosing) {
701
+ let blocks = determineBlockChildren(node);
702
+ if (blocks.type === 'named') {
703
+ for (const child of blocks.children) {
704
+ if (child.type === 'CommentStatement' || child.type === 'MustacheCommentStatement') {
705
+ emitComment(child);
706
+ continue;
707
+ }
708
+
709
+ let childStart = rangeForNode(child).start;
710
+ let nameStart = template.indexOf(child.tag, childStart) + ':'.length;
711
+ let blockParamsStart = template.indexOf('|', childStart);
712
+ let name = child.tag.slice(1);
713
+
714
+ mapper.forNode(child, () =>
715
+ emitBlockContents(
716
+ name,
717
+ nameStart,
718
+ child.blockParams,
719
+ blockParamsStart,
720
+ child.children,
721
+ ),
722
+ );
723
+ }
724
+ } else {
725
+ let blockParamsStart = template.indexOf('|', rangeForNode(node).start);
726
+ emitBlockContents(
727
+ 'default',
728
+ undefined,
729
+ node.blockParams,
730
+ blockParamsStart,
731
+ blocks.children,
732
+ );
733
+ }
734
+ }
735
+
736
+ mapper.dedent();
737
+ mapper.text('}');
738
+ mapper.newline();
739
+ });
740
+ }
741
+
742
+ function isAllowedAmongNamedBlocks(node: AST.Node): boolean {
743
+ return (
744
+ (node.type === 'TextNode' && node.chars.trim() === '') ||
745
+ node.type === 'CommentStatement' ||
746
+ node.type === 'MustacheCommentStatement'
747
+ );
748
+ }
749
+
750
+ function isNamedBlock(node: AST.Node): boolean {
751
+ return node.type === 'ElementNode' && node.tag.startsWith(':');
752
+ }
753
+
754
+ type NamedBlockChild = AST.ElementNode | AST.CommentStatement | AST.MustacheCommentStatement;
755
+ type BlockChildren =
756
+ | { type: 'named'; children: NamedBlockChild[] }
757
+ | { type: 'default'; children: AST.TopLevelStatement[] };
758
+
759
+ function determineBlockChildren(node: AST.ElementNode): BlockChildren {
760
+ let named = 0;
761
+ let other = 0;
762
+
763
+ for (let child of node.children) {
764
+ if (isAllowedAmongNamedBlocks(child)) {
765
+ continue;
766
+ }
767
+
768
+ if (isNamedBlock(child)) {
769
+ named += 1;
770
+ } else {
771
+ other += 1;
772
+ }
773
+ }
774
+
775
+ if (named === 0) {
776
+ return { type: 'default', children: node.children };
777
+ } else if (other === 0) {
778
+ return {
779
+ type: 'named',
780
+ children: node.children.filter(
781
+ // Filter out ignorable content between named blocks
782
+ (child): child is NamedBlockChild => child.type === 'ElementNode',
783
+ // ||
784
+ // child.type === 'CommentStatement' ||
785
+ // child.type === 'MustacheCommentStatement',
786
+ ),
787
+ };
788
+ } else {
789
+ // If we get here, meaningful content was mixed with named blocks,
790
+ // so it's worth doing the additional work to produce errors for
791
+ // those nodes
792
+ for (let child of node.children) {
793
+ if (!isNamedBlock(child)) {
794
+ mapper.forNode(child, () =>
795
+ assert(
796
+ isAllowedAmongNamedBlocks(child),
797
+ 'Named blocks may not be mixed with other content',
798
+ ),
799
+ );
800
+ }
801
+ }
802
+
803
+ return { type: 'named', children: [] };
804
+ }
805
+ }
806
+
807
+ function emitPlainElement(node: AST.ElementNode): void {
808
+ mapper.forNode(node, () => {
809
+ if (node.tag === 'svg') {
810
+ inHtmlContext = 'svg';
811
+ }
812
+
813
+ if (node.tag === 'math') {
814
+ inHtmlContext = 'math';
815
+ }
816
+
817
+ for (let comment of node.comments) {
818
+ emitComment(comment);
819
+ }
820
+
821
+ mapper.text('{');
822
+ mapper.newline();
823
+ mapper.indent();
824
+
825
+ if (inHtmlContext === 'default') {
826
+ mapper.text('const __glintY__ = __glintDSL__.emitElement("');
827
+ } else if (inHtmlContext === 'svg') {
828
+ mapper.text('const __glintY__ = __glintDSL__.emitSVGElement("');
829
+ } else if (inHtmlContext === 'math') {
830
+ mapper.text('const __glintY__ = __glintDSL__.emitMathMlElement("');
831
+ }
832
+ mapper.forNode(node.path, () => {
833
+ mapper.text(node.tag);
834
+ });
835
+ mapper.text('");');
836
+ mapper.newline();
837
+
838
+ emitAttributesAndModifiers(node);
839
+
840
+ for (let child of node.children) {
841
+ emitTopLevelStatement(child);
842
+ }
843
+
844
+ if (node.tag === 'svg' || node.tag === 'math') {
845
+ inHtmlContext = 'default';
846
+ }
847
+
848
+ mapper.dedent();
849
+ mapper.text('}');
850
+ mapper.newline();
851
+ });
852
+ }
853
+
854
+ function emitAttributesAndModifiers(node: AST.ElementNode): void {
855
+ emitSplattributes(node);
856
+ emitPlainAttributes(node);
857
+ emitModifiers(node);
858
+ }
859
+
860
+ function emitPlainAttributes(node: AST.ElementNode): void {
861
+ let attributes = node.attributes.filter(
862
+ (attr) => !attr.name.startsWith('@') && attr.name !== SPLATTRIBUTES,
863
+ );
864
+
865
+ // Only emit `applyAttributes` if there are attributes to apply.
866
+ if (attributes.length === 0) return;
867
+
868
+ const attrsSpan = node.openTag
869
+ .withStart(node.path.loc.getEnd())
870
+ .withEnd(node.openTag.getEnd().move(-1));
871
+ mapper.forNodeWithSpan(node, attrsSpan, () => {
872
+ let isFirstAttribute = true;
873
+
874
+ for (let attr of attributes) {
875
+ if (isFirstAttribute) {
876
+ isFirstAttribute = false;
877
+
878
+ mapper.text('__glintDSL__.applyAttributes(');
879
+
880
+ // We map the `__glintY__.element` arg to the first attribute node, which has the effect
881
+ // such that diagnostics due to passing attributes to invalid elements will show up
882
+ // on the attribute, rather than on the whole element.
883
+ mapper.forNode(attr, () => {
884
+ mapper.text('__glintY__.element');
885
+ });
886
+
887
+ mapper.text(', {');
888
+ }
889
+
890
+ mapper.newline();
891
+ mapper.indent();
892
+
893
+ mapper.forNode(attr, () => {
894
+ const attrStartOffset = attr.loc.getStart().offset!;
895
+ emitHashKey(attr.name, attrStartOffset + prefix.length);
896
+ mapper.text(': ');
897
+
898
+ if (attr.value.type === 'MustacheStatement') {
899
+ emitMustacheStatement(attr.value, 'attr');
900
+ } else if (attr.value.type === 'ConcatStatement') {
901
+ emitConcatStatement(attr.value);
902
+ } else {
903
+ mapper.text(JSON.stringify(attr.value.chars));
904
+ }
905
+
906
+ mapper.text(',');
907
+ mapper.newline();
908
+ });
909
+ }
910
+ mapper.newline();
911
+ });
912
+ mapper.newline();
913
+ mapper.dedent();
914
+ mapper.text('});');
915
+ mapper.newline();
916
+ }
917
+
918
+ function emitSplattributes(node: AST.ElementNode): void {
919
+ let splattributes = node.attributes.find((attr) => attr.name === SPLATTRIBUTES);
920
+ if (!splattributes) return;
921
+
922
+ assert(
923
+ splattributes.value.type === 'TextNode' && splattributes.value.chars === '',
924
+ '`...attributes` cannot accept a value',
925
+ );
926
+
927
+ mapper.forNode(splattributes, () => {
928
+ mapper.text('__glintDSL__.applySplattributes(__glintRef__.element, __glintY__.element);');
929
+ });
930
+
931
+ mapper.newline();
932
+ }
933
+
934
+ function emitModifiers(node: AST.ElementNode): void {
935
+ for (let modifier of node.modifiers) {
936
+ mapper.forNode(modifier, () => {
937
+ mapper.text('__glintDSL__.applyModifier(');
938
+
939
+ mapper.forNode(modifier, () => {
940
+ mapper.text('__glintDSL__.resolve(');
941
+ emitExpression(modifier.path);
942
+ mapper.text(')');
943
+ });
944
+
945
+ mapper.text('(__glintY__.element, ');
946
+ emitArgs(modifier.params, modifier.hash);
947
+ mapper.text('));');
948
+ mapper.newline();
949
+ });
950
+ }
951
+ }
952
+
953
+ function emitMustacheStatement(node: AST.MustacheStatement, position: InvokePosition): void {
954
+ let specialFormInfo = checkSpecialForm(node);
955
+ if (specialFormInfo) {
956
+ emitSpecialFormExpression(specialFormInfo, node, position);
957
+ return;
958
+ } else if (node.path.type !== 'PathExpression' && node.path.type !== 'SubExpression') {
959
+ // This assertion is currently meaningless, as @glimmer/syntax silently drops
960
+ // any named or positional parameters passed in a literal mustache
961
+ assert(
962
+ node.params.length === 0 && node.hash.pairs.length === 0,
963
+ 'Literals do not accept params',
964
+ );
965
+
966
+ emitLiteral(node.path);
967
+ return;
968
+ }
969
+
970
+ mapper.forNode(node, () => {
971
+ // If a mustache has parameters, we know it must be an invocation; if
972
+ // not, it depends on where it appears. In arg position, it's always
973
+ // passed directly as a value; otherwise it's invoked if it's a
974
+ // component/helper, and returned as a value otherwise.
975
+ let hasParams = Boolean(node.hash.pairs.length || node.params.length);
976
+ if (!hasParams && position === 'arg' && !isGlobal(node.path)) {
977
+ emitExpression(node.path);
978
+ } else if (position === 'top-level') {
979
+ // e.g. top-level mustache `{{someValue}}`
980
+ mapper.text('__glintDSL__.emitContent(');
981
+ emitResolve(node, hasParams ? 'resolve' : 'resolveOrReturn');
982
+ mapper.text(')');
983
+ } else {
984
+ emitResolve(node, hasParams ? 'resolve' : 'resolveOrReturn');
985
+ }
986
+ });
987
+ }
988
+
989
+ function isGlobal(path: AST.Expression): boolean {
990
+ return Boolean(
991
+ path.type === 'PathExpression' &&
992
+ path.head.type === 'VarHead' &&
993
+ globals?.includes(path.head.name) &&
994
+ !scope.hasBinding(path.head.name),
995
+ );
996
+ }
997
+
998
+ function emitYieldExpression(
999
+ formInfo: SpecialFormInfo,
1000
+ node: AST.MustacheStatement | AST.SubExpression,
1001
+ position: InvokePosition,
1002
+ ): void {
1003
+ mapper.forNode(node, () => {
1004
+ assert(
1005
+ position === 'top-level',
1006
+ () => `{{${formInfo.name}}} may only appear as a top-level statement`,
1007
+ );
1008
+
1009
+ let to = 'default';
1010
+ let toPair = node.hash.pairs.find((pair) => pair.key === 'to');
1011
+ if (toPair) {
1012
+ assert(
1013
+ toPair.value.type === 'StringLiteral',
1014
+ () => `Named block {{${formInfo.name}}}s must have a literal block name`,
1015
+ );
1016
+ to = toPair.value.value;
1017
+ }
1018
+
1019
+ if (to === 'inverse') {
1020
+ to = 'else';
1021
+ }
1022
+
1023
+ mapper.text('__glintDSL__.yieldToBlock(__glintRef__, ');
1024
+ mapper.text(JSON.stringify(to));
1025
+ mapper.text(')(');
1026
+
1027
+ for (let [index, param] of node.params.entries()) {
1028
+ if (index) {
1029
+ mapper.text(', ');
1030
+ }
1031
+
1032
+ emitExpression(param);
1033
+ }
1034
+
1035
+ mapper.text(')');
1036
+ });
1037
+ }
1038
+
1039
+ function emitSpecialFormStatement(formInfo: SpecialFormInfo, node: AST.BlockStatement): void {
1040
+ if (formInfo.requiresConsumption) {
1041
+ emitExpression(node.path);
1042
+ mapper.text(';');
1043
+ mapper.newline();
1044
+ }
1045
+
1046
+ switch (formInfo.form) {
1047
+ case 'if':
1048
+ emitIfStatement(formInfo, node);
1049
+ break;
1050
+
1051
+ case 'if-not':
1052
+ emitUnlessStatement(formInfo, node);
1053
+ break;
1054
+
1055
+ case 'bind-invokable':
1056
+ mapper.error(
1057
+ `The {{${formInfo.name}}} helper can't be used directly in block form under Glint. ` +
1058
+ `Consider first binding the result to a variable, e.g. '{{#let (${formInfo.name} ...) as |...|}}' ` +
1059
+ `and then using the bound value.`,
1060
+ rangeForNode(node.path),
1061
+ );
1062
+ break;
1063
+
1064
+ default:
1065
+ mapper.error(`${formInfo.name} is not valid in block form`, rangeForNode(node.path));
1066
+ }
1067
+ }
1068
+
1069
+ function emitIfStatement(formInfo: SpecialFormInfo, node: AST.BlockStatement): void {
1070
+ mapper.forNode(node, () => {
1071
+ assert(
1072
+ node.params.length === 1,
1073
+ () => `{{#${formInfo.name}}} requires exactly one condition`,
1074
+ );
1075
+
1076
+ mapper.text('if (');
1077
+ emitExpression(node.params[0]);
1078
+ mapper.text(') {');
1079
+ mapper.newline();
1080
+ mapper.indent();
1081
+
1082
+ for (let statement of node.program.body) {
1083
+ emitTopLevelStatement(statement);
1084
+ }
1085
+
1086
+ if (node.inverse) {
1087
+ mapper.dedent();
1088
+ mapper.text('} else {');
1089
+ mapper.indent();
1090
+ mapper.newline();
1091
+
1092
+ for (let statement of node.inverse.body) {
1093
+ emitTopLevelStatement(statement);
1094
+ }
1095
+ }
1096
+
1097
+ mapper.dedent();
1098
+ mapper.text('}');
1099
+ mapper.newline();
1100
+ });
1101
+ }
1102
+
1103
+ function emitUnlessStatement(formInfo: SpecialFormInfo, node: AST.BlockStatement): void {
1104
+ mapper.forNode(node, () => {
1105
+ assert(
1106
+ node.params.length === 1,
1107
+ () => `{{#${formInfo.name}}} requires exactly one condition`,
1108
+ );
1109
+
1110
+ mapper.text('if (!(');
1111
+ emitExpression(node.params[0]);
1112
+ mapper.text(')) {');
1113
+ mapper.newline();
1114
+ mapper.indent();
1115
+
1116
+ for (let statement of node.program.body) {
1117
+ emitTopLevelStatement(statement);
1118
+ }
1119
+
1120
+ if (node.inverse) {
1121
+ mapper.dedent();
1122
+ mapper.text('} else {');
1123
+ mapper.indent();
1124
+ mapper.newline();
1125
+
1126
+ for (let statement of node.inverse.body) {
1127
+ emitTopLevelStatement(statement);
1128
+ }
1129
+ }
1130
+
1131
+ mapper.dedent();
1132
+ mapper.text('}');
1133
+ mapper.newline();
1134
+ });
1135
+ }
1136
+
1137
+ function emitBlockStatement(node: AST.BlockStatement): void {
1138
+ let specialFormInfo = checkSpecialForm(node);
1139
+ if (specialFormInfo) {
1140
+ emitSpecialFormStatement(specialFormInfo, node);
1141
+ return;
1142
+ }
1143
+
1144
+ mapper.forNode(node, () => {
1145
+ mapper.text('{');
1146
+ mapper.newline();
1147
+ mapper.indent();
1148
+
1149
+ mapper.text('const __glintY__ = __glintDSL__.emitComponent(');
1150
+ emitResolve(node, 'resolve');
1151
+ mapper.text(');');
1152
+ mapper.newline();
1153
+
1154
+ emitBlock('default', node.program);
1155
+
1156
+ if (node.inverse) {
1157
+ emitBlock('else', node.inverse);
1158
+ }
1159
+
1160
+ // TODO: emit something corresponding to `{{/foo}}` like we do
1161
+ // for angle bracket components, so that symbol renames propagate?
1162
+ // A little hairier (ha) for mustaches, since they
1163
+ if (node.path.type === 'PathExpression') {
1164
+ let start = template.lastIndexOf(node.path.original, rangeForNode(node).end);
1165
+ emitPathContents(getPathParts(node.path), start, determinePathKind(node.path));
1166
+ mapper.text(';');
1167
+ mapper.newline();
1168
+ }
1169
+
1170
+ mapper.dedent();
1171
+ mapper.text('}');
1172
+ });
1173
+
1174
+ mapper.newline();
1175
+ }
1176
+
1177
+ function emitBlock(name: string, node: AST.Block): void {
1178
+ let paramsStart = template.lastIndexOf(
1179
+ '|',
1180
+ template.lastIndexOf('|', rangeForNode(node).end) - 1,
1181
+ );
1182
+
1183
+ emitBlockContents(name, undefined, node.blockParams, paramsStart, node.body);
1184
+ }
1185
+
1186
+ function emitBlockContents(
1187
+ name: string,
1188
+ nameOffset: number | undefined,
1189
+ blockParams: string[],
1190
+ blockParamsOffset: number,
1191
+ children: AST.TopLevelStatement[],
1192
+ ): void {
1193
+ assert(
1194
+ blockParams.every((name) => !name.includes('-')),
1195
+ 'Block params must be valid TypeScript identifiers',
1196
+ );
1197
+
1198
+ scope.push(blockParams);
1199
+
1200
+ mapper.text('{');
1201
+ mapper.newline();
1202
+ mapper.indent();
1203
+
1204
+ mapper.text('const [');
1205
+
1206
+ let start = blockParamsOffset;
1207
+ for (let [index, param] of blockParams.entries()) {
1208
+ if (index) mapper.text(', ');
1209
+
1210
+ start = template.indexOf(param, start);
1211
+ mapper.identifier(makeJSSafe(param), start, param.length);
1212
+ }
1213
+
1214
+ mapper.text('] = __glintY__.blockParams');
1215
+ emitPropertyAccesss(name, { offset: nameOffset, synthetic: true });
1216
+ mapper.text(';');
1217
+ mapper.newline();
1218
+
1219
+ for (let statement of children) {
1220
+ emitTopLevelStatement(statement);
1221
+ }
1222
+
1223
+ mapper.dedent();
1224
+ mapper.text('}');
1225
+ mapper.newline();
1226
+ scope.pop();
1227
+ }
1228
+
1229
+ function emitSubExpression(node: AST.SubExpression): void {
1230
+ let specialFormInfo = checkSpecialForm(node);
1231
+ if (specialFormInfo) {
1232
+ emitSpecialFormExpression(specialFormInfo, node, 'sexpr');
1233
+ return;
1234
+ }
1235
+
1236
+ mapper.forNode(node, () => {
1237
+ emitResolve(node, 'resolve');
1238
+ });
1239
+ }
1240
+
1241
+ /** An AST node that represents an invocation of some template entity in curlies */
1242
+ type CurlyInvocationNode =
1243
+ | AST.MustacheStatement
1244
+ | AST.SubExpression
1245
+ | AST.BlockStatement
1246
+ | AST.ElementModifierStatement;
1247
+
1248
+ function emitResolve(node: CurlyInvocationNode, resolveType: string): void {
1249
+ // We use forNode here to wrap the emitted resolve expression here so that when
1250
+ // we convert to Volar mappings, we can create a boundary around
1251
+ // e.g. "__glintDSL__.resolveOrReturn(expectsAtLeastOneArg)()", which is required because
1252
+ // this is where TS might generate a diagnostic error.
1253
+ mapper.forNode(node, () => {
1254
+ mapper.text('__glintDSL__.');
1255
+ mapper.text(resolveType);
1256
+ mapper.text('(');
1257
+ emitExpression(node.path);
1258
+ mapper.text(')(');
1259
+ emitArgs(node.params, node.hash);
1260
+ mapper.text(')');
1261
+ });
1262
+ }
1263
+
1264
+ function emitArgs(positional: Array<AST.Expression>, named: AST.Hash): void {
1265
+ // Emit positional args
1266
+ for (let [index, param] of positional.entries()) {
1267
+ if (index) {
1268
+ mapper.text(', ');
1269
+ }
1270
+
1271
+ emitExpression(param);
1272
+ }
1273
+
1274
+ // Emit named args
1275
+ if (named.pairs.length) {
1276
+ if (positional.length) {
1277
+ mapper.text(', ');
1278
+ }
1279
+
1280
+ // TS diagnostic error boundary
1281
+ mapper.forNode(named, () => {
1282
+ mapper.text('{ ');
1283
+
1284
+ let { start } = rangeForNode(named);
1285
+ for (let [index, pair] of named.pairs.entries()) {
1286
+ start = template.indexOf(pair.key, start);
1287
+ emitHashKey(pair.key, start);
1288
+ mapper.text(': ');
1289
+ emitExpression(pair.value);
1290
+
1291
+ if (index === named.pairs.length - 1) {
1292
+ mapper.text(' ');
1293
+ }
1294
+
1295
+ start = rangeForNode(pair.value).end;
1296
+ mapper.text(', ');
1297
+ }
1298
+
1299
+ mapper.text('...__glintDSL__.NamedArgsMarker }');
1300
+ });
1301
+ }
1302
+ }
1303
+
1304
+ type PathKind = 'this' | 'arg' | 'free';
1305
+
1306
+ function emitPath(node: AST.PathExpression): void {
1307
+ mapper.forNode(node, () => {
1308
+ let { start } = rangeForNode(node);
1309
+ emitPathContents(getPathParts(node), start, determinePathKind(node));
1310
+ });
1311
+ }
1312
+
1313
+ function determinePathKind(node: AST.PathExpression): PathKind {
1314
+ switch (node.head.type) {
1315
+ case 'AtHead':
1316
+ return 'arg';
1317
+ case 'ThisHead':
1318
+ return 'this';
1319
+ case 'VarHead':
1320
+ return 'free';
1321
+ }
1322
+ }
1323
+
1324
+ function emitPathContents(parts: string[], start: number, kind: PathKind): void {
1325
+ if (kind === 'this') {
1326
+ let thisStart = template.indexOf('this', start);
1327
+ mapper.text('__glintRef__.');
1328
+ mapper.identifier('this', thisStart);
1329
+ start = template.indexOf('.', thisStart) + 1;
1330
+ } else if (kind === 'arg') {
1331
+ mapper.text('__glintRef__.args');
1332
+ start = template.indexOf('@', start) + 1;
1333
+ }
1334
+
1335
+ let head = parts[0];
1336
+ if (!head) return;
1337
+
1338
+ start = template.indexOf(head, start);
1339
+
1340
+ // The first segment of a non-this, non-arg path must resolve
1341
+ // to some in-scope identifier.
1342
+ if (kind === 'free') {
1343
+ emitIdentifierReference(head, start);
1344
+ } else {
1345
+ emitPropertyAccesss(head, { offset: start, optional: false });
1346
+ }
1347
+
1348
+ start += head.length;
1349
+
1350
+ for (let i = 1; i < parts.length; i++) {
1351
+ let part = parts[i];
1352
+ start = template.indexOf(part, start);
1353
+ emitPropertyAccesss(part, { offset: start, optional: true });
1354
+ start += part.length;
1355
+ }
1356
+ }
1357
+
1358
+ type PropertyAccessOptions = {
1359
+ offset?: number;
1360
+ optional?: boolean;
1361
+ synthetic?: boolean;
1362
+ };
1363
+
1364
+ function emitPropertyAccesss(
1365
+ name: string,
1366
+ { offset, optional, synthetic }: PropertyAccessOptions = {},
1367
+ ): void {
1368
+ // Synthetic accesses should always use `[]` notation to avoid incidentally triggering
1369
+ // `noPropertyAccessFromIndexSignature`. Emitting `{{foo.bar}}` property accesses, however,
1370
+ // should use `.` notation for exactly the same reason.
1371
+ if (!synthetic && isSafeKey(name)) {
1372
+ mapper.text(optional ? '?.' : '.');
1373
+ if (offset) {
1374
+ mapper.identifier(name, offset);
1375
+ } else {
1376
+ mapper.text(name);
1377
+ }
1378
+ } else {
1379
+ mapper.text(optional ? '?.[' : '[');
1380
+ if (offset) {
1381
+ emitIdentifierString(name, offset);
1382
+ } else {
1383
+ mapper.text(JSON.stringify(name));
1384
+ }
1385
+ mapper.text(']');
1386
+ }
1387
+ }
1388
+
1389
+ function emitHashKey(name: string, start: number): void {
1390
+ if (isSafeKey(name)) {
1391
+ mapper.identifier(name, start);
1392
+ } else {
1393
+ emitIdentifierString(name, start);
1394
+ }
1395
+ }
1396
+
1397
+ function emitIdentifierString(name: string, start: number): void {
1398
+ mapper.text('"');
1399
+ mapper.identifier(JSON.stringify(name).slice(1, -1), start, name.length);
1400
+ mapper.text('"');
1401
+ }
1402
+
1403
+ function emitLiteral(node: AST.Literal): void {
1404
+ mapper.forNode(node, () =>
1405
+ mapper.text(node.value === undefined ? 'undefined' : JSON.stringify(node.value)),
1406
+ );
1407
+ }
1408
+
1409
+ function isSafeKey(key: string): boolean {
1410
+ return /^[a-z_$][a-z0-9_$]*$/i.test(key);
1411
+ }
1412
+ });
1413
+ }
1414
+
1415
+ const JSKeywords = new Set([
1416
+ 'await',
1417
+ 'break',
1418
+ 'case',
1419
+ 'catch',
1420
+ 'class',
1421
+ 'const',
1422
+ 'continue',
1423
+ 'debugger',
1424
+ 'default',
1425
+ 'delete',
1426
+ 'do',
1427
+ 'else',
1428
+ 'enum',
1429
+ 'eval',
1430
+ 'export',
1431
+ 'extends',
1432
+ 'false',
1433
+ 'finally',
1434
+ 'for',
1435
+ 'function',
1436
+ 'if',
1437
+ 'implements',
1438
+ 'import',
1439
+ 'in',
1440
+ 'instanceof',
1441
+ 'interface',
1442
+ 'let',
1443
+ 'new',
1444
+ 'null',
1445
+ 'package',
1446
+ 'private',
1447
+ 'protected',
1448
+ 'public',
1449
+ 'return',
1450
+ 'static',
1451
+ 'super',
1452
+ 'switch',
1453
+ 'this',
1454
+ 'throw',
1455
+ 'true',
1456
+ 'try',
1457
+ 'typeof',
1458
+ 'undefined',
1459
+ 'var',
1460
+ 'void',
1461
+ 'while',
1462
+ 'with',
1463
+ 'yield',
1464
+ ]);
1465
+
1466
+ function isJSKeyword(token: string): boolean {
1467
+ return JSKeywords.has(token);
1468
+ }
1469
+
1470
+ function makeJSSafe(identifier: string): string {
1471
+ if (isJSKeyword(identifier) || identifier.startsWith('__')) {
1472
+ return `__${identifier}`;
1473
+ }
1474
+
1475
+ return identifier;
1476
+ }