@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.
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/bin/ember-tsc.js +4 -0
- package/bin/glint-language-server.js +2 -0
- package/lib/cli/run-volar-tsc.d.ts +2 -0
- package/lib/cli/run-volar-tsc.d.ts.map +1 -0
- package/lib/cli/run-volar-tsc.js +30 -0
- package/lib/cli/run-volar-tsc.js.map +1 -0
- package/lib/config/config.d.ts +15 -0
- package/lib/config/config.d.ts.map +1 -0
- package/lib/config/config.js +21 -0
- package/lib/config/config.js.map +1 -0
- package/lib/config/environment.d.ts +26 -0
- package/lib/config/environment.d.ts.map +1 -0
- package/lib/config/environment.js +96 -0
- package/lib/config/environment.js.map +1 -0
- package/lib/config/index.d.ts +17 -0
- package/lib/config/index.d.ts.map +1 -0
- package/lib/config/index.js +26 -0
- package/lib/config/index.js.map +1 -0
- package/lib/config/loader.d.ts +25 -0
- package/lib/config/loader.d.ts.map +1 -0
- package/lib/config/loader.js +110 -0
- package/lib/config/loader.js.map +1 -0
- package/lib/config/types.cjs +3 -0
- package/lib/config/types.cjs.map +1 -0
- package/lib/config/types.d.cts +60 -0
- package/lib/config/types.d.cts.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/common.d.ts +13 -0
- package/lib/environment-ember-template-imports/-private/environment/common.d.ts.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/common.js +2 -0
- package/lib/environment-ember-template-imports/-private/environment/common.js.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/index.d.ts +3 -0
- package/lib/environment-ember-template-imports/-private/environment/index.d.ts.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/index.js +76 -0
- package/lib/environment-ember-template-imports/-private/environment/index.js.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/preprocess.d.ts +4 -0
- package/lib/environment-ember-template-imports/-private/environment/preprocess.d.ts.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/preprocess.js +73 -0
- package/lib/environment-ember-template-imports/-private/environment/preprocess.js.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/transform.d.ts +4 -0
- package/lib/environment-ember-template-imports/-private/environment/transform.d.ts.map +1 -0
- package/lib/environment-ember-template-imports/-private/environment/transform.js +134 -0
- package/lib/environment-ember-template-imports/-private/environment/transform.js.map +1 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +6 -0
- package/lib/index.js.map +1 -0
- package/lib/plugins/g-compiler-errors.d.ts +12 -0
- package/lib/plugins/g-compiler-errors.d.ts.map +1 -0
- package/lib/plugins/g-compiler-errors.js +58 -0
- package/lib/plugins/g-compiler-errors.js.map +1 -0
- package/lib/plugins/g-template-tag-symbols.d.ts +11 -0
- package/lib/plugins/g-template-tag-symbols.d.ts.map +1 -0
- package/lib/plugins/g-template-tag-symbols.js +48 -0
- package/lib/plugins/g-template-tag-symbols.js.map +1 -0
- package/lib/plugins/utils.d.ts +25 -0
- package/lib/plugins/utils.d.ts.map +1 -0
- package/lib/plugins/utils.js +63 -0
- package/lib/plugins/utils.js.map +1 -0
- package/lib/transform/diagnostics/augmentation.d.ts +4 -0
- package/lib/transform/diagnostics/augmentation.d.ts.map +1 -0
- package/lib/transform/diagnostics/augmentation.js +223 -0
- package/lib/transform/diagnostics/augmentation.js.map +1 -0
- package/lib/transform/diagnostics/index.d.ts +5 -0
- package/lib/transform/diagnostics/index.d.ts.map +1 -0
- package/lib/transform/diagnostics/index.js +2 -0
- package/lib/transform/diagnostics/index.js.map +1 -0
- package/lib/transform/index.d.ts +4 -0
- package/lib/transform/index.d.ts.map +1 -0
- package/lib/transform/index.js +2 -0
- package/lib/transform/index.js.map +1 -0
- package/lib/transform/template/code-features.d.ts +30 -0
- package/lib/transform/template/code-features.d.ts.map +1 -0
- package/lib/transform/template/code-features.js +26 -0
- package/lib/transform/template/code-features.js.map +1 -0
- package/lib/transform/template/glimmer-ast-mapping-tree.d.ts +80 -0
- package/lib/transform/template/glimmer-ast-mapping-tree.d.ts.map +1 -0
- package/lib/transform/template/glimmer-ast-mapping-tree.js +132 -0
- package/lib/transform/template/glimmer-ast-mapping-tree.js.map +1 -0
- package/lib/transform/template/inlining/index.d.ts +16 -0
- package/lib/transform/template/inlining/index.d.ts.map +1 -0
- package/lib/transform/template/inlining/index.js +21 -0
- package/lib/transform/template/inlining/index.js.map +1 -0
- package/lib/transform/template/inlining/tagged-strings.d.ts +8 -0
- package/lib/transform/template/inlining/tagged-strings.d.ts.map +1 -0
- package/lib/transform/template/inlining/tagged-strings.js +140 -0
- package/lib/transform/template/inlining/tagged-strings.js.map +1 -0
- package/lib/transform/template/map-template-contents.d.ts +121 -0
- package/lib/transform/template/map-template-contents.d.ts.map +1 -0
- package/lib/transform/template/map-template-contents.js +287 -0
- package/lib/transform/template/map-template-contents.js.map +1 -0
- package/lib/transform/template/rewrite-module.d.ts +22 -0
- package/lib/transform/template/rewrite-module.d.ts.map +1 -0
- package/lib/transform/template/rewrite-module.js +265 -0
- package/lib/transform/template/rewrite-module.js.map +1 -0
- package/lib/transform/template/scope-stack.d.ts +13 -0
- package/lib/transform/template/scope-stack.d.ts.map +1 -0
- package/lib/transform/template/scope-stack.js +28 -0
- package/lib/transform/template/scope-stack.js.map +1 -0
- package/lib/transform/template/template-to-typescript.d.ts +19 -0
- package/lib/transform/template/template-to-typescript.d.ts.map +1 -0
- package/lib/transform/template/template-to-typescript.js +1095 -0
- package/lib/transform/template/template-to-typescript.js.map +1 -0
- package/lib/transform/template/transformed-module.d.ts +111 -0
- package/lib/transform/template/transformed-module.d.ts.map +1 -0
- package/lib/transform/template/transformed-module.js +287 -0
- package/lib/transform/template/transformed-module.js.map +1 -0
- package/lib/transform/util.d.ts +7 -0
- package/lib/transform/util.d.ts.map +1 -0
- package/lib/transform/util.js +15 -0
- package/lib/transform/util.js.map +1 -0
- package/lib/volar/ember-language-plugin.d.ts +14 -0
- package/lib/volar/ember-language-plugin.d.ts.map +1 -0
- package/lib/volar/ember-language-plugin.js +91 -0
- package/lib/volar/ember-language-plugin.js.map +1 -0
- package/lib/volar/gts-virtual-code.d.ts +83 -0
- package/lib/volar/gts-virtual-code.d.ts.map +1 -0
- package/lib/volar/gts-virtual-code.js +210 -0
- package/lib/volar/gts-virtual-code.js.map +1 -0
- package/lib/volar/language-server.d.ts +2 -0
- package/lib/volar/language-server.d.ts.map +1 -0
- package/lib/volar/language-server.js +214 -0
- package/lib/volar/language-server.js.map +1 -0
- package/lib/volar/script-snapshot.d.ts +17 -0
- package/lib/volar/script-snapshot.d.ts.map +1 -0
- package/lib/volar/script-snapshot.js +24 -0
- package/lib/volar/script-snapshot.js.map +1 -0
- package/package.json +104 -0
- package/src/cli/run-volar-tsc.ts +36 -0
- package/src/config/config.ts +33 -0
- package/src/config/environment.ts +128 -0
- package/src/config/index.ts +30 -0
- package/src/config/loader.ts +143 -0
- package/src/config/types.cts +85 -0
- package/src/environment-ember-template-imports/-private/environment/common.ts +14 -0
- package/src/environment-ember-template-imports/-private/environment/index.ts +83 -0
- package/src/environment-ember-template-imports/-private/environment/preprocess.ts +90 -0
- package/src/environment-ember-template-imports/-private/environment/transform.ts +202 -0
- package/src/index.ts +9 -0
- package/src/plugins/g-compiler-errors.ts +67 -0
- package/src/plugins/g-template-tag-symbols.ts +54 -0
- package/src/plugins/utils.ts +86 -0
- package/src/transform/diagnostics/augmentation.ts +333 -0
- package/src/transform/diagnostics/index.ts +5 -0
- package/src/transform/index.ts +4 -0
- package/src/transform/template/code-features.ts +30 -0
- package/src/transform/template/glimmer-ast-mapping-tree.ts +173 -0
- package/src/transform/template/inlining/index.ts +33 -0
- package/src/transform/template/inlining/tagged-strings.ts +187 -0
- package/src/transform/template/map-template-contents.ts +501 -0
- package/src/transform/template/rewrite-module.ts +372 -0
- package/src/transform/template/scope-stack.ts +34 -0
- package/src/transform/template/template-to-typescript.ts +1476 -0
- package/src/transform/template/transformed-module.ts +431 -0
- package/src/transform/util.ts +24 -0
- package/src/volar/ember-language-plugin.ts +108 -0
- package/src/volar/gts-virtual-code.ts +249 -0
- package/src/volar/language-server.ts +250 -0
- package/src/volar/script-snapshot.ts +27 -0
- package/types/-private/dsl/globals.d.ts +204 -0
- package/types/-private/dsl/index.d.ts +50 -0
- package/types/-private/dsl/integration-declarations.d.ts +143 -0
- package/types/-private/intrinsics/action.d.ts +45 -0
- package/types/-private/intrinsics/concat.d.ts +6 -0
- package/types/-private/intrinsics/each-in.d.ts +24 -0
- package/types/-private/intrinsics/each.d.ts +17 -0
- package/types/-private/intrinsics/fn.d.ts +44 -0
- package/types/-private/intrinsics/get.d.ts +31 -0
- package/types/-private/intrinsics/input.d.ts +24 -0
- package/types/-private/intrinsics/link-to.d.ts +31 -0
- package/types/-private/intrinsics/log.d.ts +6 -0
- package/types/-private/intrinsics/mount.d.ts +9 -0
- package/types/-private/intrinsics/mut.d.ts +14 -0
- package/types/-private/intrinsics/on.d.ts +21 -0
- package/types/-private/intrinsics/outlet.d.ts +8 -0
- package/types/-private/intrinsics/textarea.d.ts +16 -0
- package/types/-private/intrinsics/unbound.d.ts +10 -0
- package/types/-private/intrinsics/unique-id.d.ts +5 -0
- package/types/globals/index.d.ts +3 -0
- 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
|
+
}
|