@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,187 @@
1
+ import {
2
+ GlintEmitMetadata,
3
+ GlintSpecialForm,
4
+ GlintSpecialFormConfig,
5
+ GlintTagConfig,
6
+ } from '@glint/ember-tsc/config-types';
7
+ import type ts from 'typescript';
8
+ import { GlintEnvironment } from '../../../config/index.js';
9
+ import { assert, TSLib } from '../../util.js';
10
+ import { templateToTypescript } from '../template-to-typescript.js';
11
+ import { Directive, Range, SourceFile, TransformError } from '../transformed-module.js';
12
+ import { CorrelatedSpansResult, isEmbeddedInClass, PartialCorrelatedSpan } from './index.js';
13
+
14
+ export function calculateTaggedTemplateSpans(
15
+ ts: TSLib,
16
+ node: ts.TaggedTemplateExpression,
17
+ meta: GlintEmitMetadata | undefined,
18
+ script: SourceFile,
19
+ environment: GlintEnvironment,
20
+ ): CorrelatedSpansResult {
21
+ let directives: Array<Directive> = [];
22
+ let errors: Array<TransformError> = [];
23
+ let partialSpans: Array<PartialCorrelatedSpan> = [];
24
+ let tag = node.tag;
25
+
26
+ if (!ts.isIdentifier(tag)) {
27
+ return { errors, directives, partialSpans };
28
+ }
29
+
30
+ let importedBindings = collectImportedBindings(ts, tag.getSourceFile());
31
+ let info = resolveTagInfo(importedBindings, tag, environment);
32
+ if (info) {
33
+ assert(
34
+ ts.isNoSubstitutionTemplateLiteral(node.template),
35
+ 'No interpolated values in template strings',
36
+ );
37
+
38
+ let { typesModule, globals } = info.tagConfig;
39
+ let template = node.template.rawText ?? node.template.text;
40
+
41
+ // environment-specific transforms may emit templateLocation in meta, in
42
+ // which case we use that. Otherwise we use the reported location from the
43
+ // node itself (which is presumably correct because no transform has messed
44
+ // with it).
45
+ let templateLocation = meta?.templateLocation ?? {
46
+ start: node.getStart(),
47
+ end: node.getEnd(),
48
+ contentStart: node.template.getStart() + 1,
49
+ contentEnd: node.template.getEnd() - 1,
50
+ };
51
+
52
+ let embeddingSyntax = {
53
+ prefix: script.contents.slice(templateLocation.start, templateLocation.contentStart),
54
+ suffix: script.contents.slice(templateLocation.contentEnd, templateLocation.end),
55
+ };
56
+
57
+ let preamble = [];
58
+ if (!info.importedBinding.synthetic) {
59
+ preamble.push(`${tag.text};`);
60
+ }
61
+
62
+ let specialForms = collectSpecialForms(importedBindings, info.tagConfig.specialForms ?? {});
63
+ let transformedTemplate = templateToTypescript(template, {
64
+ typesModule: typesModule,
65
+ meta,
66
+ preamble,
67
+ globals,
68
+ embeddingSyntax,
69
+ specialForms,
70
+ backingValue: isEmbeddedInClass(ts, node) ? 'this' : undefined,
71
+ useJsDoc: environment.isUntypedScript(script.filename),
72
+ });
73
+
74
+ for (let { message, location } of transformedTemplate.errors) {
75
+ if (location) {
76
+ errors.push({
77
+ source: script,
78
+ message,
79
+ location: addOffset(location, templateLocation.start),
80
+ });
81
+ } else {
82
+ errors.push({
83
+ source: script,
84
+ message,
85
+ location: {
86
+ start: tag.getStart(),
87
+ end: tag.getEnd(),
88
+ },
89
+ });
90
+ }
91
+ }
92
+
93
+ if (transformedTemplate.result) {
94
+ partialSpans.push({
95
+ originalFile: script,
96
+ originalStart: templateLocation.start,
97
+ originalLength: templateLocation.end - templateLocation.start,
98
+ insertionPoint: templateLocation.start,
99
+ transformedSource: transformedTemplate.result.code,
100
+ glimmerAstMapping: transformedTemplate.result.mapping,
101
+ });
102
+ }
103
+ }
104
+
105
+ return { errors, directives, partialSpans };
106
+ }
107
+
108
+ function addOffset(location: Range, offset: number): Range {
109
+ return {
110
+ start: location.start + offset,
111
+ end: location.end + offset,
112
+ };
113
+ }
114
+
115
+ function collectSpecialForms(
116
+ importedBindings: ImportedBindings,
117
+ config: GlintSpecialFormConfig,
118
+ ): Record<string, GlintSpecialForm> {
119
+ let specialForms: Record<string, GlintSpecialForm> = { ...config.globals };
120
+ if (config.imports) {
121
+ for (let [name, { specifier, source }] of Object.entries(importedBindings)) {
122
+ let formForImport = config.imports[source]?.[specifier];
123
+ if (formForImport) {
124
+ specialForms[name] = formForImport;
125
+ }
126
+ }
127
+ }
128
+ return specialForms;
129
+ }
130
+
131
+ function resolveTagInfo(
132
+ importedBindings: ImportedBindings,
133
+ tag: ts.Identifier,
134
+ environment: GlintEnvironment,
135
+ ): { importedBinding: ImportedBinding; tagConfig: GlintTagConfig } | undefined {
136
+ let importedBinding = importedBindings[tag.text];
137
+ if (!importedBinding) {
138
+ return;
139
+ }
140
+
141
+ for (let [importSource, tags] of Object.entries(environment.getConfiguredTemplateTags())) {
142
+ for (let [importSpecifier, tagConfig] of Object.entries(tags)) {
143
+ if (
144
+ importSource === importedBinding.source &&
145
+ importSpecifier === importedBinding.specifier
146
+ ) {
147
+ return { importedBinding, tagConfig };
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ type ImportedBinding = { specifier: string; source: string; synthetic: boolean };
154
+ type ImportedBindings = Record<string, ImportedBinding>;
155
+
156
+ function collectImportedBindings(ts: TSLib, sourceFile: ts.SourceFile): ImportedBindings {
157
+ let result: ImportedBindings = {};
158
+ for (let statement of sourceFile.statements) {
159
+ if (ts.isImportDeclaration(statement)) {
160
+ assert(ts.isStringLiteral(statement.moduleSpecifier));
161
+
162
+ let { importClause } = statement;
163
+ if (!importClause) continue;
164
+
165
+ let synthetic = statement.pos === statement.end;
166
+
167
+ if (importClause.name) {
168
+ result[importClause.name.text] = {
169
+ specifier: 'default',
170
+ source: statement.moduleSpecifier.text,
171
+ synthetic,
172
+ };
173
+ }
174
+
175
+ if (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings)) {
176
+ for (let binding of importClause.namedBindings.elements) {
177
+ result[binding.name.text] = {
178
+ specifier: binding.propertyName?.text ?? binding.name.text,
179
+ source: statement.moduleSpecifier.text,
180
+ synthetic,
181
+ };
182
+ }
183
+ }
184
+ }
185
+ }
186
+ return result;
187
+ }
@@ -0,0 +1,501 @@
1
+ import { AST, preprocess } from '@glimmer/syntax';
2
+ import { CodeInformation } from '@volar/language-server/node.js';
3
+ import { assert } from '../util.js';
4
+ import { codeFeatures } from './code-features.js';
5
+ import GlimmerASTMappingTree, {
6
+ MappingSource,
7
+ TemplateEmbedding,
8
+ } from './glimmer-ast-mapping-tree.js';
9
+ import { Directive, DirectiveKind, Range } from './transformed-module.js';
10
+
11
+ /**
12
+ * @glimmer/syntax parses identifiers as strings. Aside from meaning
13
+ * we often have to reverse engineer location information for them
14
+ * by hand, it also means we can't treat mappings from identifiers
15
+ * consistently with how we treat mappings from other AST nodes.
16
+ *
17
+ * This class just gives us a uniform way to store identifiers
18
+ * or other nodes as the `source` for a mapping.
19
+ */
20
+ export class Identifier {
21
+ public readonly type = 'Identifier';
22
+ public constructor(public readonly name: string) {}
23
+ }
24
+
25
+ export type Mapper = {
26
+ /**
27
+ * Given a @glimmer/syntax AST node, returns the corresponding start
28
+ * and end offsets of that node in the original source.
29
+ */
30
+ rangeForNode: (node: AST.Node, span?: AST.Node['loc']) => Range;
31
+
32
+ /**
33
+ * Given a 0-based line number, returns the corresponding start and
34
+ * end offsets for that line.
35
+ */
36
+ rangeForLine: (line: number) => Range;
37
+
38
+ /**
39
+ * Captures the existence of a directive specified by the given source
40
+ * node and affecting the given range of text.
41
+ */
42
+ directive: (
43
+ type: DirectiveKind,
44
+ commentNode: AST.CommentStatement | AST.MustacheCommentStatement,
45
+ location: Range,
46
+ areaOfEffect: Range,
47
+ ) => void;
48
+
49
+ /**
50
+ * Emits placeholder `ts-expect-error`s for corresponding `@glint-expect-error` directives.
51
+ * This is called at the end of the template transformation to ensure that we're at a
52
+ * top-level / statement-level point in the transformed code, which is a requirement for
53
+ * the `ts-expect-error` placeholder diagnostics to be emitted.
54
+ */
55
+ emitDirectivePlaceholders: () => void;
56
+
57
+ /**
58
+ * Records an error at the given location.
59
+ */
60
+ error: (message: string, location: Range) => void;
61
+
62
+ /** Emit a newline in the transformed source */
63
+ newline(): void;
64
+
65
+ /** Increase the indent level for future emitted content */
66
+ indent(): void;
67
+
68
+ /** Decrease the indent level for future emitted content */
69
+ dedent(): void;
70
+
71
+ /** Append the given raw text to the transformed source */
72
+ text(value: string): void;
73
+
74
+ /**
75
+ * Append the given raw text to the transformed source, creating
76
+ * a 0-length mapping for it in the output.
77
+ */
78
+ synthetic(value: string): void;
79
+
80
+ /**
81
+ * Essentially the inverse of `emit.synthetic`, this notes the
82
+ * presence of a template AST node at a given location while not
83
+ * emitting anything in the resulting TS translation.
84
+ */
85
+ nothing(node: AST.Node, source?: MappingSource): void;
86
+
87
+ /**
88
+ * Append the given value to the transformed source, mapping
89
+ * that span back to the given offset in the original source.
90
+ */
91
+ identifier(value: string, hbsOffset: number, hbsLength?: number): void;
92
+
93
+ /**
94
+ * Map all content emitted in the given callback to the span
95
+ * corresponding to the given AST node in the original source.
96
+ */
97
+ forNode(node: AST.Node, callback: () => void, codeFeaturesForNode?: CodeInformation): void;
98
+ forNodeWithSpan(
99
+ node: AST.Node,
100
+ span: AST.Node['loc'],
101
+ callback: () => void,
102
+ codeFeaturesForNode?: CodeInformation,
103
+ ): void;
104
+ };
105
+
106
+ type LocalDirective = Omit<Directive, 'source'>;
107
+
108
+ /** The result of rewriting a template */
109
+ export type RewriteResult = {
110
+ /**
111
+ * Any errors discovered during rewriting, along with their location
112
+ * in terms of the original source.
113
+ */
114
+ errors: Array<{ message: string; location: Range | undefined }>;
115
+
116
+ /**
117
+ * The source code and a `MappingTree` resulting from rewriting a
118
+ * template. If the template contains unrecoverable syntax errors,
119
+ * this may be undefined.
120
+ */
121
+ result?: {
122
+ code: string;
123
+ directives: Array<LocalDirective>;
124
+ mapping: GlimmerASTMappingTree;
125
+ };
126
+ };
127
+
128
+ /**
129
+ * Syntax surrounding the contents of a template that marks it as
130
+ * embedded within the surrounding context, like the `hbs` tag and
131
+ * backticks on a tagged string or the `<template>` markers in a
132
+ * `.gts`/`.gjs` file.
133
+ */
134
+ export type EmbeddingSyntax = {
135
+ prefix: string;
136
+ suffix: string;
137
+ };
138
+
139
+ export type MapTemplateContentsOptions = {
140
+ embeddingSyntax: EmbeddingSyntax;
141
+ };
142
+
143
+ /**
144
+ * Given the text of a handlebars template (either standalone .hbs file, or the contents
145
+ * of an embedded `<template>...</template>` within a .gts file), invokes the given callback
146
+ * with a set of tools to emit mapped contents corresponding to
147
+ * that template, tracking the text emitted in order to provide
148
+ * a mapping of ranges in the input to ranges in the output.
149
+ */
150
+ export function mapTemplateContents(
151
+ template: string,
152
+ { embeddingSyntax }: MapTemplateContentsOptions,
153
+ callback: (ast: AST.Template | null, mapper: Mapper) => void,
154
+ ): RewriteResult {
155
+ let ast: AST.Template | null = null;
156
+ let errors: Array<{ message: string; location: Range | undefined }> = [];
157
+ let lineOffsets = calculateLineOffsets(template, embeddingSyntax.prefix.length);
158
+ try {
159
+ ast = preprocess(template);
160
+ } catch (error) {
161
+ let message = getErrorMessage(error);
162
+ let location: Range | undefined;
163
+ if (isHBSSyntaxError(error)) {
164
+ location = {
165
+ start: lineOffsets[error.hash.loc.first_line] + error.hash.loc.first_column,
166
+ end: lineOffsets[error.hash.loc.last_line] + error.hash.loc.last_column,
167
+ };
168
+ } else {
169
+ let match = /line (\d+) : column (\d+)/.exec(message);
170
+ if (match) {
171
+ let offset = lineOffsets[Number(match[1])] + Number(match[2]);
172
+ location = { start: offset, end: offset };
173
+ }
174
+ }
175
+
176
+ errors.push({ message, location });
177
+ }
178
+
179
+ let segmentsStack: string[][] = [[]];
180
+ let mappingsStack: GlimmerASTMappingTree[][] = [[]];
181
+ let indent = '';
182
+ let offset = 0;
183
+ let directives: Array<LocalDirective> = [];
184
+
185
+ // Associates all content emitted during the given callback with the
186
+ // given range in the template source and corresponding AST node.
187
+ // If an exception is thrown while executing the callback, the error
188
+ // will be captured and associated with the given range, and no content
189
+ // will be emitted.
190
+ let captureMapping = (
191
+ hbsRange: Range,
192
+ source: MappingSource,
193
+ allowEmpty: boolean,
194
+ callback: () => void,
195
+ codeFeaturesForNode?: CodeInformation,
196
+ ): void => {
197
+ let start = offset;
198
+ let mappings: GlimmerASTMappingTree[] = [];
199
+ let segments: string[] = [];
200
+
201
+ segmentsStack.unshift(segments);
202
+ mappingsStack.unshift(mappings);
203
+ try {
204
+ callback();
205
+ } catch (error) {
206
+ errors.push({ message: getErrorMessage(error), location: hbsRange });
207
+ offset = start;
208
+ }
209
+ mappingsStack.shift();
210
+ segmentsStack.shift();
211
+
212
+ // If the offset didn't change (either because nothing was emitted
213
+ // or because an exception was thrown), don't add a new node to the
214
+ // mapping tree or flush any new content.
215
+ if (start !== offset || allowEmpty) {
216
+ let end = offset;
217
+ let tsRange = { start, end };
218
+
219
+ mappingsStack[0].push(
220
+ new GlimmerASTMappingTree(
221
+ tsRange,
222
+ hbsRange,
223
+ mappings,
224
+ source,
225
+
226
+ // Prevent TS's semantic classifications (used for semantic highlighting, see
227
+ // https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide) from being
228
+ // source-mapped back to the glimmer template. These might be useful to reinstate at some
229
+ // point in the future but by default tends to make the highlighting in gts files look wrong.
230
+ codeFeaturesForNode ??
231
+ augmentCodeFeaturesWithIgnoreDirectivesSupport(codeFeatures.withoutHighlight, hbsRange),
232
+ ),
233
+ );
234
+ segmentsStack[0].push(...segments);
235
+ }
236
+ };
237
+
238
+ /**
239
+ * This function will conditionally augment the CodeInformation object
240
+ * that's passed in for each mapping as a means to implement support for
241
+ * `@glint-expect-error` directives.
242
+ */
243
+ function augmentCodeFeaturesWithIgnoreDirectivesSupport(
244
+ features: CodeInformation,
245
+ hbsRange: Range,
246
+ ): CodeInformation {
247
+ if (features.verification) {
248
+ // If this code span requests verification (e.g. TS type-checking), then
249
+ // we potentially need to decorate the `verification` value that we pass
250
+ // back to Volar, in case we have active `glint-ignore/expect-error` directives
251
+ // in active effect.
252
+
253
+ let activeDirective: LocalDirective | undefined;
254
+ for (let directive of directives) {
255
+ if (
256
+ directive.areaOfEffect.start <= hbsRange.start &&
257
+ directive.areaOfEffect.end > hbsRange.start
258
+ ) {
259
+ if (!activeDirective) {
260
+ activeDirective = directive;
261
+ } else {
262
+ // More than one directive applies here; glint-nocheck, if present, always wins.
263
+ if (directive.kind === 'nocheck') {
264
+ activeDirective = directive;
265
+ }
266
+ }
267
+ }
268
+ }
269
+
270
+ if (!activeDirective) {
271
+ return features;
272
+ }
273
+
274
+ if (activeDirective.kind === 'ignore' || activeDirective.kind === 'nocheck') {
275
+ // We are currently in an ignored region of code, so don't
276
+ // even bother performing any type-checking: override verification (i.e. type-checking) to false
277
+ // for this mapping (note that the whole generated TS file will be type-checked but any
278
+ // diagnostics in this region will be suppressed by Volar)
279
+ return {
280
+ ...features,
281
+ verification: false,
282
+ };
283
+ } else if (activeDirective.kind === 'expect-error') {
284
+ const expectErrorDirective = activeDirective;
285
+ return {
286
+ ...features,
287
+ verification: {
288
+ shouldReport: () => {
289
+ // Keep track of any errors/diagnostics reported within this region of code...
290
+ expectErrorDirective.errorCount++;
291
+
292
+ // ...and suppress them from bubbling up as diagnostics/errrors/warnings.
293
+ return false;
294
+ },
295
+ },
296
+ };
297
+ }
298
+ }
299
+ return features;
300
+ }
301
+
302
+ let mapper: Mapper = {
303
+ indent() {
304
+ indent += ' ';
305
+ },
306
+ dedent() {
307
+ indent = indent.slice(2);
308
+ },
309
+ newline() {
310
+ offset += 1;
311
+ segmentsStack[0].push('\n');
312
+ },
313
+ text(value: string) {
314
+ offset += value.length;
315
+ segmentsStack[0].push(value);
316
+ },
317
+ synthetic(value: string) {
318
+ if (value.length) {
319
+ mapper.identifier(value, 0, 0);
320
+ }
321
+ },
322
+ nothing(node: AST.Node, source: MappingSource = node) {
323
+ captureMapping(mapper.rangeForNode(node), source, true, () => {});
324
+ },
325
+ identifier(value: string, hbsOffset: number, hbsLength = value.length) {
326
+ let hbsRange = { start: hbsOffset, end: hbsOffset + hbsLength };
327
+ let source = new Identifier(value);
328
+ captureMapping(hbsRange, source, true, () => mapper.text(value));
329
+ },
330
+ forNode(node: AST.Node, callback: () => void, codeFeaturesForNode?: CodeInformation) {
331
+ captureMapping(mapper.rangeForNode(node), node, false, callback, codeFeaturesForNode);
332
+ },
333
+
334
+ forNodeWithSpan(
335
+ node: AST.Node,
336
+ span: AST.Node['loc'],
337
+ callback: () => void,
338
+ codeFeaturesForNode?: CodeInformation,
339
+ ) {
340
+ captureMapping(mapper.rangeForNode(node, span), node, false, callback, codeFeaturesForNode);
341
+ },
342
+
343
+ error(message: string, location: Range) {
344
+ errors.push({ message, location });
345
+ },
346
+
347
+ directive(
348
+ kind: DirectiveKind,
349
+ commentNode: AST.CommentStatement | AST.MustacheCommentStatement,
350
+ location: Range,
351
+ areaOfEffect: Range,
352
+ ) {
353
+ const directive: LocalDirective = {
354
+ kind,
355
+ commentNode,
356
+ location,
357
+ areaOfEffect,
358
+ errorCount: 0,
359
+ };
360
+
361
+ directives.push(directive);
362
+ },
363
+
364
+ emitDirectivePlaceholders(): void {
365
+ if (directives.length) {
366
+ mapper.text('// begin directive placeholders');
367
+ mapper.newline();
368
+ }
369
+
370
+ for (let directive of directives) {
371
+ if (directive.kind !== 'expect-error') {
372
+ continue;
373
+ }
374
+
375
+ mapper.forNode(
376
+ directive.commentNode,
377
+ () => {
378
+ mapper.text(`// @ts-expect-error ${directive.kind}`);
379
+ },
380
+ {
381
+ ...codeFeatures.withoutHighlight,
382
+ verification: {
383
+ shouldReport: () => {
384
+ // This determines whether we raise the placeholder "unused ts-expect-error" diagnostic.
385
+ // If errors were encountered in the region covered by the directive, then we suppress
386
+ // the "unused ts-expect-error" diagnostic here.
387
+ return directive.errorCount === 0;
388
+ },
389
+ },
390
+ },
391
+ );
392
+ mapper.newline();
393
+ mapper.text(';');
394
+ mapper.newline();
395
+ }
396
+
397
+ if (directives.length) {
398
+ mapper.text('// end directive placeholders');
399
+ mapper.newline();
400
+ }
401
+ },
402
+
403
+ rangeForNode: buildRangeForNode(lineOffsets),
404
+
405
+ rangeForLine: (line: number): Range => ({
406
+ start: lineOffsets[line],
407
+ end: lineOffsets[line + 1] ?? template.length,
408
+ }),
409
+ };
410
+
411
+ callback(ast, mapper);
412
+
413
+ assert(segmentsStack.length === 1);
414
+
415
+ let code = segmentsStack[0].join('');
416
+
417
+ let mapping = new GlimmerASTMappingTree(
418
+ { start: 0, end: code.length },
419
+ {
420
+ start: 0,
421
+ end: embeddingSyntax.prefix.length + template.length + embeddingSyntax.suffix.length,
422
+ },
423
+ mappingsStack[0],
424
+ new TemplateEmbedding(),
425
+ codeFeatures.all,
426
+ );
427
+
428
+ return { errors, result: { code, directives, mapping } };
429
+ }
430
+
431
+ const LEADING_WHITESPACE = /^\s+/;
432
+ const TRAILING_WHITESPACE = /\s+$/;
433
+
434
+ function calculateLineOffsets(template: string, contentOffset: number): Array<number> {
435
+ let lines = template.split('\n');
436
+ let total = contentOffset;
437
+ let offsets = [contentOffset];
438
+
439
+ for (let [index, line] of lines.entries()) {
440
+ // lines from @glimmer/syntax are 1-indexed
441
+ offsets[index + 1] = total;
442
+ total += line.length + 1;
443
+ }
444
+
445
+ return offsets;
446
+ }
447
+
448
+ function buildRangeForNode(
449
+ offsets: Array<number>,
450
+ ): (node: AST.Node, span?: AST.Node['loc']) => Range {
451
+ return (node, span) => {
452
+ let { loc } = node;
453
+ if (span) {
454
+ loc = span;
455
+ }
456
+ let start = offsets[loc.start.line] + loc.start.column;
457
+ let end = offsets[loc.end.line] + loc.end.column;
458
+
459
+ // This makes error reporting for illegal text nodes (e.g. alongside named blocks)
460
+ // a bit nicer by only highlighting the content rather than all the surrounding
461
+ // newlines and attendant whitespace
462
+ if (node.type === 'TextNode') {
463
+ let leading = LEADING_WHITESPACE.exec(node.chars)?.[0].length ?? 0;
464
+ let trailing = TRAILING_WHITESPACE.exec(node.chars)?.[0].length ?? 0;
465
+
466
+ if (leading !== node.chars.length) {
467
+ start += leading;
468
+ end -= trailing;
469
+ }
470
+ }
471
+
472
+ return { start, end };
473
+ };
474
+ }
475
+
476
+ interface HBSSyntaxError extends Error {
477
+ hash: {
478
+ text: string;
479
+ token: string;
480
+ line: number;
481
+ loc: {
482
+ first_line: number;
483
+ last_line: number;
484
+ first_column: number;
485
+ last_column: number;
486
+ };
487
+ };
488
+ }
489
+
490
+ function getErrorMessage(error: unknown): string {
491
+ return (error as any)?.message ?? '(unknown error)';
492
+ }
493
+
494
+ function isHBSSyntaxError(error: unknown): error is HBSSyntaxError {
495
+ if (typeof error === 'object' && !!error && 'hash' in error) {
496
+ let { hash } = error as any;
497
+ return typeof hash?.loc === 'object';
498
+ }
499
+
500
+ return false;
501
+ }