@glint/ember-tsc 1.8.2 → 1.8.4
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/lib/config/environment.d.ts +7 -6
- package/lib/config/environment.d.ts.map +1 -1
- package/lib/config/environment.js +8 -23
- package/lib/config/environment.js.map +1 -1
- package/lib/config/types.d.cts +8 -7
- package/lib/config/types.d.cts.map +1 -1
- package/lib/environment-ember-template-imports/-private/environment/index.d.ts.map +1 -1
- package/lib/environment-ember-template-imports/-private/environment/index.js +33 -28
- package/lib/environment-ember-template-imports/-private/environment/index.js.map +1 -1
- package/lib/environment-ember-template-imports/-private/environment/preprocess.d.ts.map +1 -1
- package/lib/environment-ember-template-imports/-private/environment/preprocess.js +18 -4
- package/lib/environment-ember-template-imports/-private/environment/preprocess.js.map +1 -1
- package/lib/environment-ember-template-imports/-private/environment/transform.d.ts.map +1 -1
- package/lib/environment-ember-template-imports/-private/environment/transform.js +39 -34
- package/lib/environment-ember-template-imports/-private/environment/transform.js.map +1 -1
- package/lib/transform/template/glimmer-ast-mapping-tree.d.ts +17 -1
- package/lib/transform/template/glimmer-ast-mapping-tree.d.ts.map +1 -1
- package/lib/transform/template/glimmer-ast-mapping-tree.js +10 -1
- package/lib/transform/template/glimmer-ast-mapping-tree.js.map +1 -1
- package/lib/transform/template/inlining/tagged-strings.js +16 -10
- package/lib/transform/template/inlining/tagged-strings.js.map +1 -1
- package/lib/transform/template/map-template-contents.d.ts +6 -1
- package/lib/transform/template/map-template-contents.d.ts.map +1 -1
- package/lib/transform/template/map-template-contents.js +4 -4
- package/lib/transform/template/map-template-contents.js.map +1 -1
- package/lib/transform/template/rewrite-module.js +6 -6
- package/lib/transform/template/rewrite-module.js.map +1 -1
- package/lib/transform/template/template-to-typescript.d.ts.map +1 -1
- package/lib/transform/template/template-to-typescript.js +39 -9
- package/lib/transform/template/template-to-typescript.js.map +1 -1
- package/lib/transform/template/transformed-module.d.ts.map +1 -1
- package/lib/transform/template/transformed-module.js +34 -0
- package/lib/transform/template/transformed-module.js.map +1 -1
- package/package.json +7 -3
- package/src/config/environment.ts +10 -29
- package/src/config/types.cts +8 -8
- package/src/environment-ember-template-imports/-private/environment/index.ts +33 -28
- package/src/environment-ember-template-imports/-private/environment/preprocess.ts +19 -4
- package/src/environment-ember-template-imports/-private/environment/transform.ts +39 -43
- package/src/transform/template/glimmer-ast-mapping-tree.ts +8 -0
- package/src/transform/template/inlining/tagged-strings.ts +21 -15
- package/src/transform/template/map-template-contents.ts +27 -3
- package/src/transform/template/rewrite-module.ts +6 -6
- package/src/transform/template/template-to-typescript.ts +49 -9
- package/src/transform/template/transformed-module.ts +42 -0
- package/types/-private/dsl/globals.d.ts +5 -10
- package/types/-private/intrinsics/comparison.d.ts +7 -14
- package/types/-private/intrinsics/element.d.ts +22 -2
- package/types/-private/intrinsics/get.d.ts +8 -4
- package/types/-private/intrinsics/truth-helpers.d.ts +26 -9
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
GlintEnvironmentConfig,
|
|
3
3
|
GlintExtensionConfig,
|
|
4
4
|
GlintExtensionsConfig,
|
|
5
|
-
|
|
5
|
+
GlintTemplateConfig,
|
|
6
6
|
SourceKind,
|
|
7
7
|
} from '@glint/ember-tsc/config-types';
|
|
8
8
|
import * as path from 'node:path';
|
|
@@ -20,14 +20,14 @@ export const DEFAULT_EXTENSIONS: GlintExtensionsConfig = {
|
|
|
20
20
|
* glint environments (e.g. ember-template-imports).
|
|
21
21
|
*/
|
|
22
22
|
export class GlintEnvironment {
|
|
23
|
-
private
|
|
23
|
+
private templateConfig: GlintTemplateConfig | undefined;
|
|
24
24
|
private extensionsConfig: GlintExtensionsConfig;
|
|
25
25
|
|
|
26
26
|
public constructor(
|
|
27
27
|
public readonly names: Array<string>,
|
|
28
28
|
config: GlintEnvironmentConfig,
|
|
29
29
|
) {
|
|
30
|
-
this.
|
|
30
|
+
this.templateConfig = config.template;
|
|
31
31
|
// when is this populated? what is config?
|
|
32
32
|
this.extensionsConfig = config.extensions ?? {};
|
|
33
33
|
}
|
|
@@ -58,32 +58,12 @@ export class GlintEnvironment {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
let tags: GlintTagsConfig = {};
|
|
62
61
|
let extensions: GlintExtensionsConfig = { ...DEFAULT_EXTENSIONS };
|
|
63
62
|
|
|
64
63
|
const envUserConfig = { additionalGlobals, additionalSpecialForms };
|
|
65
64
|
|
|
66
65
|
let config = emberTemplateImportsEnvironment(envUserConfig) as GlintEnvironmentConfig;
|
|
67
66
|
|
|
68
|
-
if (config.tags) {
|
|
69
|
-
for (let [importSource, specifiers] of Object.entries(config.tags)) {
|
|
70
|
-
tags[importSource] ??= {};
|
|
71
|
-
for (let [importSpecifier, tagConfig] of Object.entries(specifiers)) {
|
|
72
|
-
if (importSpecifier in tags[importSource]) {
|
|
73
|
-
throw new SilentError(
|
|
74
|
-
'Multiple configured Glint environments attempted to define behavior for the tag `' +
|
|
75
|
-
importSpecifier +
|
|
76
|
-
"` in module '" +
|
|
77
|
-
importSource +
|
|
78
|
-
"'.",
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
tags[importSource][importSpecifier] = tagConfig;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
67
|
if (config.extensions) {
|
|
88
68
|
for (let [extension, extensionConfig] of Object.entries(config.extensions)) {
|
|
89
69
|
if (extension in extensions) {
|
|
@@ -98,7 +78,7 @@ export class GlintEnvironment {
|
|
|
98
78
|
}
|
|
99
79
|
}
|
|
100
80
|
|
|
101
|
-
return new GlintEnvironment(Object.keys(config), {
|
|
81
|
+
return new GlintEnvironment(Object.keys(config), { template: config.template, extensions });
|
|
102
82
|
}
|
|
103
83
|
|
|
104
84
|
public getSourceKind(fileName: string): SourceKind | 'unknown' {
|
|
@@ -118,11 +98,12 @@ export class GlintEnvironment {
|
|
|
118
98
|
}
|
|
119
99
|
|
|
120
100
|
/**
|
|
121
|
-
* Returns
|
|
122
|
-
*
|
|
123
|
-
*
|
|
101
|
+
* Returns the configuration for the built-in `<template>` form — the types
|
|
102
|
+
* module its transformed output references, plus the keyword globals and
|
|
103
|
+
* special forms available inside it. Returns `undefined` if the active
|
|
104
|
+
* environment defines no template form.
|
|
124
105
|
*/
|
|
125
|
-
public
|
|
126
|
-
return this.
|
|
106
|
+
public getTemplateConfig(): GlintTemplateConfig | undefined {
|
|
107
|
+
return this.templateConfig;
|
|
127
108
|
}
|
|
128
109
|
}
|
package/src/config/types.cts
CHANGED
|
@@ -10,7 +10,7 @@ export type GlintConfigInput = {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
export type GlintEnvironmentConfig = {
|
|
13
|
-
|
|
13
|
+
template?: GlintTemplateConfig;
|
|
14
14
|
extensions?: GlintExtensionsConfig;
|
|
15
15
|
};
|
|
16
16
|
|
|
@@ -72,14 +72,14 @@ export type GlintExtensionsConfig = {
|
|
|
72
72
|
[extension: string]: GlintExtensionConfig;
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Configuration for the single, built-in template form (the `<template>` tag in
|
|
77
|
+
* `.gts`/`.gjs` files). Glint no longer supports user-authored or
|
|
78
|
+
* environment-registered template tags, so this is internal compiler
|
|
79
|
+
* configuration rather than a user-facing extension point.
|
|
80
|
+
*/
|
|
81
|
+
export type GlintTemplateConfig = {
|
|
76
82
|
typesModule: string;
|
|
77
83
|
globals?: Array<string>;
|
|
78
84
|
specialForms?: GlintSpecialFormConfig;
|
|
79
85
|
};
|
|
80
|
-
|
|
81
|
-
export type GlintTagsConfig = {
|
|
82
|
-
[importSource: string]: {
|
|
83
|
-
[importSpecifier: string]: GlintTagConfig;
|
|
84
|
-
};
|
|
85
|
-
};
|
|
@@ -97,37 +97,42 @@ export default function emberTemplateImportsEnvironment(
|
|
|
97
97
|
];
|
|
98
98
|
|
|
99
99
|
return {
|
|
100
|
-
|
|
101
|
-
'@glint/ember-tsc
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
100
|
+
template: {
|
|
101
|
+
typesModule: '@glint/ember-tsc/-private/dsl',
|
|
102
|
+
specialForms: {
|
|
103
|
+
globals: {
|
|
104
|
+
if: 'if',
|
|
105
|
+
unless: 'if-not',
|
|
106
|
+
yield: 'yield',
|
|
107
|
+
component: 'bind-invokable',
|
|
108
|
+
modifier: 'bind-invokable',
|
|
109
|
+
helper: 'bind-invokable',
|
|
110
|
+
// The RFC 561 `eq`/`neq` keywords are exactly `===`/`!==` at runtime,
|
|
111
|
+
// so we emit them as the native operators. Beyond matching runtime
|
|
112
|
+
// semantics, this lets TypeScript's control-flow analysis narrow
|
|
113
|
+
// discriminated unions — e.g. `{{#if (eq foo.kind "a")}}` narrows
|
|
114
|
+
// `foo` to the matching variant inside the block, which a
|
|
115
|
+
// boolean-returning helper type cannot do.
|
|
116
|
+
...(hasEmber71BuiltIns()
|
|
117
|
+
? ({ eq: '===', neq: '!==' } satisfies GlintSpecialFormConfig['globals'])
|
|
118
|
+
: {}),
|
|
119
|
+
...additionalGlobalSpecialForms,
|
|
120
|
+
},
|
|
121
|
+
imports: {
|
|
122
|
+
'@ember/helper': {
|
|
123
|
+
array: 'array-literal',
|
|
124
|
+
hash: 'object-literal',
|
|
125
|
+
...additionalSpecialForms.imports?.['@ember/helper'],
|
|
122
126
|
},
|
|
123
|
-
|
|
124
|
-
...globalsForEmber,
|
|
125
|
-
...(hasEmber71BuiltIns() ? globalsForEmber71 : []),
|
|
126
|
-
...Object.keys(additionalGlobalSpecialForms),
|
|
127
|
-
...additionalGlobals,
|
|
128
|
-
],
|
|
127
|
+
...additionalSpecialForms.imports,
|
|
129
128
|
},
|
|
130
129
|
},
|
|
130
|
+
globals: [
|
|
131
|
+
...globalsForEmber,
|
|
132
|
+
...(hasEmber71BuiltIns() ? globalsForEmber71 : []),
|
|
133
|
+
...Object.keys(additionalGlobalSpecialForms),
|
|
134
|
+
...additionalGlobals,
|
|
135
|
+
],
|
|
131
136
|
},
|
|
132
137
|
extensions: {
|
|
133
138
|
'.gts': {
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { GlintExtensionPreprocess } from '@glint/ember-tsc/config-types';
|
|
2
2
|
import { GLOBAL_TAG, PreprocessData, TemplateLocation } from './common.js';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
// A `<template>` becomes a tagged-template expression `___T`...``. In a class
|
|
5
|
+
// body a bare expression is not a valid member, so there it is wrapped in a
|
|
6
|
+
// computed-property name `[___T`...`]` (rewritten to a static block during
|
|
7
|
+
// transform). In every other position it is emitted bare: that keeps a
|
|
8
|
+
// user-authored array such as `[<template/>]` a single array literal, so it
|
|
9
|
+
// can never be confused with — and collapsed into — the wrapper.
|
|
10
|
+
const TAG_OPEN = `${GLOBAL_TAG}\``;
|
|
11
|
+
const TAG_CLOSE = '`';
|
|
6
12
|
|
|
7
13
|
import { Preprocessor } from 'content-tag';
|
|
8
14
|
const p = new Preprocessor();
|
|
@@ -22,15 +28,24 @@ export const preprocess: GlintExtensionPreprocess<PreprocessData> = (source, pat
|
|
|
22
28
|
|
|
23
29
|
contents += source.slice(sourceOffset, startTagOffset);
|
|
24
30
|
|
|
31
|
+
let isClassMember = template.type === 'class-member';
|
|
32
|
+
|
|
33
|
+
if (isClassMember) contents += '[';
|
|
34
|
+
|
|
35
|
+
// `transformedStart`/`transformedEnd` bracket the tag literal itself
|
|
36
|
+
// (`___T`...``), excluding any class-member `[ ]`, so they line up exactly
|
|
37
|
+
// with the tagged-template node's `getStart()`/`getEnd()` during transform.
|
|
25
38
|
let transformedStart = contents.length;
|
|
26
|
-
contents +=
|
|
39
|
+
contents += TAG_OPEN;
|
|
27
40
|
|
|
28
41
|
let templateContent = source.slice(startTagEnd, endTagOffset);
|
|
29
42
|
let escapedContent = templateContent.replaceAll('${{', '\\${{').replaceAll('`', '\\`');
|
|
30
43
|
contents += escapedContent;
|
|
31
|
-
contents +=
|
|
44
|
+
contents += TAG_CLOSE;
|
|
32
45
|
let transformedEnd = contents.length;
|
|
33
46
|
|
|
47
|
+
if (isClassMember) contents += ']';
|
|
48
|
+
|
|
34
49
|
sourceOffset = endTagEnd;
|
|
35
50
|
templateLocations.push({
|
|
36
51
|
startTagOffset,
|
|
@@ -12,7 +12,15 @@ export const transform: GlintExtensionTransform<PreprocessData> = (
|
|
|
12
12
|
let { templateLocations } = data;
|
|
13
13
|
if (!templateLocations.length) return (sf) => sf;
|
|
14
14
|
|
|
15
|
+
// TypeScript 6 changed `getStart()` to require the source file when the node's
|
|
16
|
+
// parent chain is not yet intact. During transformation, `repairAncestry` runs
|
|
17
|
+
// *after* `transformNode`, so parent pointers are not set up at the point where
|
|
18
|
+
// `findTemplateLocation` calls `node.getStart()`. We capture the source file as
|
|
19
|
+
// we enter it so we can pass it explicitly.
|
|
20
|
+
let currentSourceFile: ts.SourceFile | undefined;
|
|
21
|
+
|
|
15
22
|
return function visit(node: ts.Node): ts.Node {
|
|
23
|
+
if (ts.isSourceFile(node)) currentSourceFile = node;
|
|
16
24
|
let visitedNode = ts.visitEachChild(node, visit, context);
|
|
17
25
|
let transformedNode = transformNode(visitedNode);
|
|
18
26
|
return repairAncestry(transformedNode);
|
|
@@ -20,8 +28,29 @@ export const transform: GlintExtensionTransform<PreprocessData> = (
|
|
|
20
28
|
|
|
21
29
|
function transformNode(node: ts.Node): ts.Node {
|
|
22
30
|
if (ts.isSourceFile(node)) {
|
|
23
|
-
//
|
|
31
|
+
// Prepend a synthetic `import { hbs as ___T } from '...'` so the emitted
|
|
32
|
+
// `___T`...`` literals have a tag binding. Downstream `resolveTagInfo`
|
|
33
|
+
// recognizes them as the built-in `<template>` form precisely because
|
|
34
|
+
// this import is compiler-synthesized (it has no source position); their
|
|
35
|
+
// types come from the environment's `getTemplateConfig()`. This is not —
|
|
36
|
+
// and must not be confused with — the unsupported
|
|
37
|
+
// `ember-template-imports` `hbs`.
|
|
24
38
|
return addTagImport(f, node);
|
|
39
|
+
} else if (isETITemplateLiteral(ts, node)) {
|
|
40
|
+
// Correlate every glint tag literal (`___T`...``) back to its original
|
|
41
|
+
// `<template>` source span. This is the only consumer of the location
|
|
42
|
+
// table; it is keyed on the literal's exact start offset, so it matches
|
|
43
|
+
// each real template once and never a surrounding array or expression.
|
|
44
|
+
let location = findTemplateLocation(templateLocations, node, currentSourceFile);
|
|
45
|
+
setEmitMetadata(node, {
|
|
46
|
+
templateLocation: {
|
|
47
|
+
start: location.startTagOffset,
|
|
48
|
+
end: location.endTagOffset + location.endTagLength,
|
|
49
|
+
contentStart: location.startTagOffset + location.startTagLength,
|
|
50
|
+
contentEnd: location.endTagOffset,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
return node;
|
|
25
54
|
} else if (isETIDefaultTemplate(ts, node)) {
|
|
26
55
|
// Annotate that this template is a default export
|
|
27
56
|
setEmitMetadata(node.expression, { prepend: 'export default ' });
|
|
@@ -30,36 +59,12 @@ export const transform: GlintExtensionTransform<PreprocessData> = (
|
|
|
30
59
|
// Annotate that this template is a default export
|
|
31
60
|
setEmitMetadata(node.expression.expression, { prepend: 'export default ' });
|
|
32
61
|
return node;
|
|
33
|
-
} else if (isETITemplateExpression(ts, node)) {
|
|
34
|
-
// Convert '[__T`foo`]' as an expression to just '__T`foo`'
|
|
35
|
-
let location = findTemplateLocation(templateLocations, node);
|
|
36
|
-
|
|
37
|
-
let template = node.elements[0];
|
|
38
|
-
setEmitMetadata(template, {
|
|
39
|
-
templateLocation: {
|
|
40
|
-
start: location.startTagOffset,
|
|
41
|
-
end: location.endTagOffset + location.endTagLength,
|
|
42
|
-
contentStart: location.startTagOffset + location.startTagLength,
|
|
43
|
-
contentEnd: location.endTagOffset,
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
return template;
|
|
47
62
|
} else if (isETITemplateProperty(ts, node)) {
|
|
48
|
-
// Convert '[
|
|
49
|
-
|
|
63
|
+
// Convert '[___T`foo`]' in a class body to 'static { ___T`foo` }'. The
|
|
64
|
+
// tag literal already carries its `templateLocation` from the branch
|
|
65
|
+
// above; here we only add the static-block framing.
|
|
50
66
|
let template = node.name.expression;
|
|
51
|
-
|
|
52
|
-
setEmitMetadata(template, {
|
|
53
|
-
prepend: 'static { ',
|
|
54
|
-
append: ' }',
|
|
55
|
-
templateLocation: {
|
|
56
|
-
start: location.startTagOffset,
|
|
57
|
-
end: location.endTagOffset + location.endTagLength,
|
|
58
|
-
contentStart: location.startTagOffset + location.startTagLength,
|
|
59
|
-
contentEnd: location.endTagOffset,
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
|
|
67
|
+
setEmitMetadata(template, { prepend: 'static { ', append: ' }' });
|
|
63
68
|
return buildStaticBlockForTemplate(f, template);
|
|
64
69
|
}
|
|
65
70
|
|
|
@@ -106,10 +111,6 @@ type ETITemplateLiteral = ts.TaggedTemplateExpression & {
|
|
|
106
111
|
template: ts.NoSubstitutionTemplateLiteral;
|
|
107
112
|
};
|
|
108
113
|
|
|
109
|
-
type ETITemplateExpression = ts.ArrayLiteralExpression & {
|
|
110
|
-
elements: [ETITemplateLiteral];
|
|
111
|
-
};
|
|
112
|
-
|
|
113
114
|
type ETITemplateProperty = ts.PropertyDeclaration & {
|
|
114
115
|
name: ts.ComputedPropertyName & { expression: ETITemplateLiteral };
|
|
115
116
|
};
|
|
@@ -162,14 +163,6 @@ function isETITemplateProperty(ts: TSLib, node: ts.Node): node is ETITemplatePro
|
|
|
162
163
|
);
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
function isETITemplateExpression(ts: TSLib, node: ts.Node): node is ETITemplateExpression {
|
|
166
|
-
return (
|
|
167
|
-
ts.isArrayLiteralExpression(node) &&
|
|
168
|
-
node.elements.length === 1 &&
|
|
169
|
-
isETITemplateLiteral(ts, node.elements[0])
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
166
|
function isETITemplateLiteral(ts: TSLib, node: ts.Node): node is ETITemplateLiteral {
|
|
174
167
|
return (
|
|
175
168
|
ts.isTaggedTemplateExpression(node) &&
|
|
@@ -181,9 +174,12 @@ function isETITemplateLiteral(ts: TSLib, node: ts.Node): node is ETITemplateLite
|
|
|
181
174
|
|
|
182
175
|
function findTemplateLocation(
|
|
183
176
|
locations: Array<TemplateLocation>,
|
|
184
|
-
node:
|
|
177
|
+
node: ETITemplateLiteral,
|
|
178
|
+
sourceFile?: ts.SourceFile,
|
|
185
179
|
): TemplateLocation {
|
|
186
|
-
|
|
180
|
+
// Every emitted tag literal corresponds to exactly one recorded location, so
|
|
181
|
+
// a miss here is a genuine construction bug, not an expected "skip this node".
|
|
182
|
+
let location = locations.find((loc) => loc.transformedStart === node.getStart(sourceFile));
|
|
187
183
|
|
|
188
184
|
if (!location) {
|
|
189
185
|
throw new Error('Internal error: missing location info for template');
|
|
@@ -58,6 +58,14 @@ export default class GlimmerASTMappingTree {
|
|
|
58
58
|
public children: Array<GlimmerASTMappingTree> = [],
|
|
59
59
|
public sourceNode: MappingSource,
|
|
60
60
|
public codeInformation?: CodeInformation,
|
|
61
|
+
/**
|
|
62
|
+
* When set, `toVolarMappings` emits an additional verification-only mapping
|
|
63
|
+
* covering this node's entire generated span (even when it differs in length
|
|
64
|
+
* from the source span). This lets diagnostics that TS anchors on synthetic
|
|
65
|
+
* generated boilerplate — e.g. the `__glintDSL__.resolve(foo)` callee, where
|
|
66
|
+
* a missing-positional-arg error lands — map back to the template (#1168).
|
|
67
|
+
*/
|
|
68
|
+
public wideVerification: boolean = false,
|
|
61
69
|
) {
|
|
62
70
|
children.forEach((child) => (child.parent = this));
|
|
63
71
|
}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
GlintEmitMetadata,
|
|
3
3
|
GlintSpecialForm,
|
|
4
4
|
GlintSpecialFormConfig,
|
|
5
|
-
|
|
5
|
+
GlintTemplateConfig,
|
|
6
6
|
} from '@glint/ember-tsc/config-types';
|
|
7
7
|
import type ts from 'typescript';
|
|
8
8
|
import { GlintEnvironment } from '../../../config/index.js';
|
|
@@ -66,10 +66,10 @@ export function calculateTaggedTemplateSpans(
|
|
|
66
66
|
suffix: script.contents.slice(templateLocation.contentEnd, templateLocation.end),
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
// The `<template>` form's backing import is always compiler-synthesized
|
|
70
|
+
// (see `resolveTagInfo`), so there is never a user-written tag binding to
|
|
71
|
+
// reference in a preamble.
|
|
72
|
+
let preamble: Array<string> = [];
|
|
73
73
|
|
|
74
74
|
let specialForms = collectSpecialForms(importedBindings, info.tagConfig.specialForms ?? {});
|
|
75
75
|
|
|
@@ -164,22 +164,28 @@ function resolveTagInfo(
|
|
|
164
164
|
importedBindings: ImportedBindings,
|
|
165
165
|
tag: ts.Identifier,
|
|
166
166
|
environment: GlintEnvironment,
|
|
167
|
-
): { importedBinding: ImportedBinding; tagConfig:
|
|
167
|
+
): { importedBinding: ImportedBinding; tagConfig: GlintTemplateConfig } | undefined {
|
|
168
168
|
let importedBinding = importedBindings[tag.text];
|
|
169
169
|
if (!importedBinding) {
|
|
170
170
|
return;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
173
|
+
// Only the compiler-generated `<template>` form is processed. Its backing
|
|
174
|
+
// import is synthesized by the gts/gjs transform (see `addTagImport` in
|
|
175
|
+
// `environment-ember-template-imports/-private/environment/transform.ts`) and
|
|
176
|
+
// therefore has no position in the original source. User-authored tagged
|
|
177
|
+
// templates (`hbs`...``) have a real import and are deliberately ignored —
|
|
178
|
+
// Glint no longer supports tagged-string templates.
|
|
179
|
+
if (!importedBinding.synthetic) {
|
|
180
|
+
return;
|
|
182
181
|
}
|
|
182
|
+
|
|
183
|
+
let tagConfig = environment.getTemplateConfig();
|
|
184
|
+
if (!tagConfig) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { importedBinding, tagConfig };
|
|
183
189
|
}
|
|
184
190
|
|
|
185
191
|
type ImportedBinding = { specifier: string; source: string; synthetic: boolean };
|
|
@@ -93,8 +93,18 @@ export type Mapper = {
|
|
|
93
93
|
/**
|
|
94
94
|
* Map all content emitted in the given callback to the span
|
|
95
95
|
* corresponding to the given AST node in the original source.
|
|
96
|
+
*
|
|
97
|
+
* When `wideVerification` is set, an extra verification-only mapping covering
|
|
98
|
+
* the callback's entire generated span is emitted so that diagnostics anchored
|
|
99
|
+
* on synthetic generated boilerplate within it still map back to the node
|
|
100
|
+
* (see `GlimmerASTMappingTree#wideVerification`).
|
|
96
101
|
*/
|
|
97
|
-
forNode(
|
|
102
|
+
forNode(
|
|
103
|
+
node: AST.Node,
|
|
104
|
+
callback: () => void,
|
|
105
|
+
codeFeaturesForNode?: CodeInformation,
|
|
106
|
+
wideVerification?: boolean,
|
|
107
|
+
): void;
|
|
98
108
|
forNodeWithSpan(
|
|
99
109
|
node: AST.Node,
|
|
100
110
|
span: AST.Node['loc'],
|
|
@@ -193,6 +203,7 @@ export function mapTemplateContents(
|
|
|
193
203
|
allowEmpty: boolean,
|
|
194
204
|
callback: () => void,
|
|
195
205
|
codeFeaturesForNode?: CodeInformation,
|
|
206
|
+
wideVerification = false,
|
|
196
207
|
): void => {
|
|
197
208
|
let start = offset;
|
|
198
209
|
let mappings: GlimmerASTMappingTree[] = [];
|
|
@@ -229,6 +240,7 @@ export function mapTemplateContents(
|
|
|
229
240
|
// point in the future but by default tends to make the highlighting in gts files look wrong.
|
|
230
241
|
codeFeaturesForNode ??
|
|
231
242
|
augmentCodeFeaturesWithIgnoreDirectivesSupport(codeFeatures.withoutHighlight, hbsRange),
|
|
243
|
+
wideVerification,
|
|
232
244
|
),
|
|
233
245
|
);
|
|
234
246
|
segmentsStack[0].push(...segments);
|
|
@@ -327,8 +339,20 @@ export function mapTemplateContents(
|
|
|
327
339
|
let source = new Identifier(value);
|
|
328
340
|
captureMapping(hbsRange, source, true, () => mapper.text(value));
|
|
329
341
|
},
|
|
330
|
-
forNode(
|
|
331
|
-
|
|
342
|
+
forNode(
|
|
343
|
+
node: AST.Node,
|
|
344
|
+
callback: () => void,
|
|
345
|
+
codeFeaturesForNode?: CodeInformation,
|
|
346
|
+
wideVerification?: boolean,
|
|
347
|
+
) {
|
|
348
|
+
captureMapping(
|
|
349
|
+
mapper.rangeForNode(node),
|
|
350
|
+
node,
|
|
351
|
+
false,
|
|
352
|
+
callback,
|
|
353
|
+
codeFeaturesForNode,
|
|
354
|
+
wideVerification,
|
|
355
|
+
);
|
|
332
356
|
},
|
|
333
357
|
|
|
334
358
|
forNodeWithSpan(
|
|
@@ -160,12 +160,12 @@ function calculateCorrelatedSpans(
|
|
|
160
160
|
ts.transform(ast, [
|
|
161
161
|
(context) =>
|
|
162
162
|
function visit<T extends ts.Node>(node: T): T {
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
163
|
+
// The gts/gjs preprocessor rewrites every `<template>` into a tagged
|
|
164
|
+
// template expression using a compiler-synthesized tag (see
|
|
165
|
+
// `environment-ember-template-imports/-private/environment/{preprocess,transform}.ts`).
|
|
166
|
+
// We look for those tagged templates here and hand them to
|
|
167
|
+
// `calculateTaggedTemplateSpans`, which only processes the synthesized
|
|
168
|
+
// tag — user-authored `hbs`...`` tagged strings are not supported.
|
|
169
169
|
if (ts.isTaggedTemplateExpression(node)) {
|
|
170
170
|
let meta = emitMetadata.get(node);
|
|
171
171
|
let result = calculateTaggedTemplateSpans(ts, node, meta, script, environment);
|
|
@@ -553,7 +553,17 @@ export function templateToTypescript(
|
|
|
553
553
|
let isGlobal = globals ? globals.includes(name) : true;
|
|
554
554
|
let form = specialForms[name];
|
|
555
555
|
|
|
556
|
-
|
|
556
|
+
// Operator special forms (`===`, `!==`, `&&`, `||`, `!`) emit only the
|
|
557
|
+
// operator expression — never a reference to the keyword itself. For a
|
|
558
|
+
// documented global keyword (e.g. `eq`/`neq`) that would drop hover
|
|
559
|
+
// docs and go-to-definition, so we still emit a discarded reference to
|
|
560
|
+
// it. Narrowing is preserved: the keyword is emitted as the throwaway
|
|
561
|
+
// first operand of a comma expression whose value is the operator
|
|
562
|
+
// expression (`(noop(eq), (a === b))`).
|
|
563
|
+
let isOperatorForm =
|
|
564
|
+
form === '===' || form === '!==' || form === '&&' || form === '||' || form === '!';
|
|
565
|
+
|
|
566
|
+
return { name, form, requiresConsumption: !isGlobal || isOperatorForm };
|
|
557
567
|
}
|
|
558
568
|
}
|
|
559
569
|
|
|
@@ -1051,10 +1061,12 @@ export function templateToTypescript(
|
|
|
1051
1061
|
} else if (position === 'top-level') {
|
|
1052
1062
|
// e.g. top-level mustache `{{someValue}}`
|
|
1053
1063
|
mapper.text('__glintDSL__.emitContent(');
|
|
1054
|
-
|
|
1064
|
+
// Direct mustache invocations opt into wide verification so a missing
|
|
1065
|
+
// required argument is reported (#1168); see `emitResolve`.
|
|
1066
|
+
emitResolve(node, hasParams ? 'resolve' : 'resolveOrReturn', true);
|
|
1055
1067
|
mapper.text(')');
|
|
1056
1068
|
} else {
|
|
1057
|
-
emitResolve(node, hasParams ? 'resolve' : 'resolveOrReturn');
|
|
1069
|
+
emitResolve(node, hasParams ? 'resolve' : 'resolveOrReturn', true);
|
|
1058
1070
|
}
|
|
1059
1071
|
});
|
|
1060
1072
|
}
|
|
@@ -1318,17 +1330,45 @@ export function templateToTypescript(
|
|
|
1318
1330
|
| AST.BlockStatement
|
|
1319
1331
|
| AST.ElementModifierStatement;
|
|
1320
1332
|
|
|
1321
|
-
function emitResolve(
|
|
1333
|
+
function emitResolve(
|
|
1334
|
+
node: CurlyInvocationNode,
|
|
1335
|
+
resolveType: string,
|
|
1336
|
+
wideVerification = false,
|
|
1337
|
+
): void {
|
|
1322
1338
|
// We use forNode here to wrap the emitted resolve expression here so that when
|
|
1323
1339
|
// we convert to Volar mappings, we can create a boundary around
|
|
1324
1340
|
// e.g. "__glintDSL__.resolveOrReturn(expectsAtLeastOneArg)()", which is required because
|
|
1325
1341
|
// this is where TS might generate a diagnostic error.
|
|
1326
1342
|
mapper.forNode(node, () => {
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1343
|
+
if (wideVerification) {
|
|
1344
|
+
// Wrap just the synthetic callee `__glintDSL__.resolve(<path>)` and
|
|
1345
|
+
// map its whole generated span back to the path. TS anchors an
|
|
1346
|
+
// "Expected N arguments, but got M" diagnostic (raised when a required
|
|
1347
|
+
// positional arg is missing) on this callee rather than on the args;
|
|
1348
|
+
// without a covering mapping Volar drops it and the missing-arg goes
|
|
1349
|
+
// unreported (#1168). Mapping to the tight `path` node (rather than the
|
|
1350
|
+
// whole invocation) keeps `@glint-expect-error` suppression scoped
|
|
1351
|
+
// correctly.
|
|
1352
|
+
mapper.forNode(
|
|
1353
|
+
node.path,
|
|
1354
|
+
() => {
|
|
1355
|
+
mapper.text('__glintDSL__.');
|
|
1356
|
+
mapper.text(resolveType);
|
|
1357
|
+
mapper.text('(');
|
|
1358
|
+
emitExpression(node.path);
|
|
1359
|
+
mapper.text(')');
|
|
1360
|
+
},
|
|
1361
|
+
undefined,
|
|
1362
|
+
true,
|
|
1363
|
+
);
|
|
1364
|
+
mapper.text('(');
|
|
1365
|
+
} else {
|
|
1366
|
+
mapper.text('__glintDSL__.');
|
|
1367
|
+
mapper.text(resolveType);
|
|
1368
|
+
mapper.text('(');
|
|
1369
|
+
emitExpression(node.path);
|
|
1370
|
+
mapper.text(')(');
|
|
1371
|
+
}
|
|
1332
1372
|
emitArgs(node.params, node.hash);
|
|
1333
1373
|
mapper.text(')');
|
|
1334
1374
|
});
|
|
@@ -307,6 +307,28 @@ export default class TransformedModule {
|
|
|
307
307
|
});
|
|
308
308
|
};
|
|
309
309
|
|
|
310
|
+
// Push a mapping that covers a generated span whose length differs from the
|
|
311
|
+
// source span it maps back to (via Volar's `generatedLengths`). This lets a
|
|
312
|
+
// diagnostic anchored anywhere within the generated span translate back to
|
|
313
|
+
// the source region even though the two aren't the same length. We restrict
|
|
314
|
+
// it to `verification` so that navigation/completion/hover features (which
|
|
315
|
+
// are served by the narrower, length-aligned mappings) are unaffected.
|
|
316
|
+
const pushWide = (
|
|
317
|
+
sourceOffset: number,
|
|
318
|
+
sourceLength: number,
|
|
319
|
+
generatedOffset: number,
|
|
320
|
+
generatedLength: number,
|
|
321
|
+
codeInformation: CodeInformation | undefined,
|
|
322
|
+
): void => {
|
|
323
|
+
codeMappings.push({
|
|
324
|
+
sourceOffsets: [sourceOffset],
|
|
325
|
+
generatedOffsets: [generatedOffset],
|
|
326
|
+
lengths: [sourceLength],
|
|
327
|
+
generatedLengths: [generatedLength],
|
|
328
|
+
data: { verification: codeInformation?.verification ?? true },
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
|
|
310
332
|
let recurse = (span: CorrelatedSpan, mapping: GlimmerASTMappingTree): void => {
|
|
311
333
|
const children = mapping.children;
|
|
312
334
|
let { originalRange, transformedRange } = mapping;
|
|
@@ -359,6 +381,26 @@ export default class TransformedModule {
|
|
|
359
381
|
// Disregard the "null zone" mappings, i.e. cases where TS code maps to empty HBS code
|
|
360
382
|
if (isNonNullMapping) {
|
|
361
383
|
push(hbsEnd, tsEnd, 0, mapping.codeInformation);
|
|
384
|
+
|
|
385
|
+
// The boundary mappings above only mark the start/end of this node's
|
|
386
|
+
// generated output; the synthetic boilerplate in between (e.g. the
|
|
387
|
+
// `__glintDSL__.resolve(foo)` callee emitted for `{{foo ...}}`) has no
|
|
388
|
+
// covering mapping. Some TS diagnostics anchor on exactly that span —
|
|
389
|
+
// notably "Expected N arguments, but got M" for a missing positional
|
|
390
|
+
// arg, which points at the callee rather than the args. Without a
|
|
391
|
+
// covering verification mapping, Volar can't translate such a
|
|
392
|
+
// diagnostic back to the template and silently drops it (#1168).
|
|
393
|
+
//
|
|
394
|
+
// Only nodes explicitly flagged for this opt in (see `forNode`'s
|
|
395
|
+
// `wideVerification` option): a blanket wide mapping on every parent
|
|
396
|
+
// would re-expose diagnostics suppressed by a narrower
|
|
397
|
+
// `@glint-expect-error`, since an ancestor spanning multiple directive
|
|
398
|
+
// regions carries its own (unsuppressed) verification. We push it
|
|
399
|
+
// *after* the children so the narrower leaf mappings still win for
|
|
400
|
+
// diagnostics that fall squarely within them.
|
|
401
|
+
if (mapping.wideVerification) {
|
|
402
|
+
pushWide(hbsStart, hbsLength, tsStart, tsLength, mapping.codeInformation);
|
|
403
|
+
}
|
|
362
404
|
}
|
|
363
405
|
} else {
|
|
364
406
|
if (hbsLength === tsLength) {
|