@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.
Files changed (50) hide show
  1. package/lib/config/environment.d.ts +7 -6
  2. package/lib/config/environment.d.ts.map +1 -1
  3. package/lib/config/environment.js +8 -23
  4. package/lib/config/environment.js.map +1 -1
  5. package/lib/config/types.d.cts +8 -7
  6. package/lib/config/types.d.cts.map +1 -1
  7. package/lib/environment-ember-template-imports/-private/environment/index.d.ts.map +1 -1
  8. package/lib/environment-ember-template-imports/-private/environment/index.js +33 -28
  9. package/lib/environment-ember-template-imports/-private/environment/index.js.map +1 -1
  10. package/lib/environment-ember-template-imports/-private/environment/preprocess.d.ts.map +1 -1
  11. package/lib/environment-ember-template-imports/-private/environment/preprocess.js +18 -4
  12. package/lib/environment-ember-template-imports/-private/environment/preprocess.js.map +1 -1
  13. package/lib/environment-ember-template-imports/-private/environment/transform.d.ts.map +1 -1
  14. package/lib/environment-ember-template-imports/-private/environment/transform.js +39 -34
  15. package/lib/environment-ember-template-imports/-private/environment/transform.js.map +1 -1
  16. package/lib/transform/template/glimmer-ast-mapping-tree.d.ts +17 -1
  17. package/lib/transform/template/glimmer-ast-mapping-tree.d.ts.map +1 -1
  18. package/lib/transform/template/glimmer-ast-mapping-tree.js +10 -1
  19. package/lib/transform/template/glimmer-ast-mapping-tree.js.map +1 -1
  20. package/lib/transform/template/inlining/tagged-strings.js +16 -10
  21. package/lib/transform/template/inlining/tagged-strings.js.map +1 -1
  22. package/lib/transform/template/map-template-contents.d.ts +6 -1
  23. package/lib/transform/template/map-template-contents.d.ts.map +1 -1
  24. package/lib/transform/template/map-template-contents.js +4 -4
  25. package/lib/transform/template/map-template-contents.js.map +1 -1
  26. package/lib/transform/template/rewrite-module.js +6 -6
  27. package/lib/transform/template/rewrite-module.js.map +1 -1
  28. package/lib/transform/template/template-to-typescript.d.ts.map +1 -1
  29. package/lib/transform/template/template-to-typescript.js +39 -9
  30. package/lib/transform/template/template-to-typescript.js.map +1 -1
  31. package/lib/transform/template/transformed-module.d.ts.map +1 -1
  32. package/lib/transform/template/transformed-module.js +34 -0
  33. package/lib/transform/template/transformed-module.js.map +1 -1
  34. package/package.json +7 -3
  35. package/src/config/environment.ts +10 -29
  36. package/src/config/types.cts +8 -8
  37. package/src/environment-ember-template-imports/-private/environment/index.ts +33 -28
  38. package/src/environment-ember-template-imports/-private/environment/preprocess.ts +19 -4
  39. package/src/environment-ember-template-imports/-private/environment/transform.ts +39 -43
  40. package/src/transform/template/glimmer-ast-mapping-tree.ts +8 -0
  41. package/src/transform/template/inlining/tagged-strings.ts +21 -15
  42. package/src/transform/template/map-template-contents.ts +27 -3
  43. package/src/transform/template/rewrite-module.ts +6 -6
  44. package/src/transform/template/template-to-typescript.ts +49 -9
  45. package/src/transform/template/transformed-module.ts +42 -0
  46. package/types/-private/dsl/globals.d.ts +5 -10
  47. package/types/-private/intrinsics/comparison.d.ts +7 -14
  48. package/types/-private/intrinsics/element.d.ts +22 -2
  49. package/types/-private/intrinsics/get.d.ts +8 -4
  50. 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
- GlintTagsConfig,
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 tagConfig: GlintTagsConfig;
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.tagConfig = config.tags ?? {};
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), { tags, extensions });
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 an array of template tags that should be rewritten according to this
122
- * config object, along with an import specifier indicating where the template types
123
- * for each tag can be found.
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 getConfiguredTemplateTags(): GlintTagsConfig {
126
- return this.tagConfig;
106
+ public getTemplateConfig(): GlintTemplateConfig | undefined {
107
+ return this.templateConfig;
127
108
  }
128
109
  }
@@ -10,7 +10,7 @@ export type GlintConfigInput = {
10
10
  };
11
11
 
12
12
  export type GlintEnvironmentConfig = {
13
- tags?: GlintTagsConfig;
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
- export type GlintTagConfig = {
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
- tags: {
101
- '@glint/ember-tsc/environment-ember-template-imports/-private/tag': {
102
- hbs: {
103
- typesModule: '@glint/ember-tsc/-private/dsl',
104
- specialForms: {
105
- globals: {
106
- if: 'if',
107
- unless: 'if-not',
108
- yield: 'yield',
109
- component: 'bind-invokable',
110
- modifier: 'bind-invokable',
111
- helper: 'bind-invokable',
112
- ...additionalGlobalSpecialForms,
113
- },
114
- imports: {
115
- '@ember/helper': {
116
- array: 'array-literal',
117
- hash: 'object-literal',
118
- ...additionalSpecialForms.imports?.['@ember/helper'],
119
- },
120
- ...additionalSpecialForms.imports,
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
- globals: [
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
- const TEMPLATE_START = `[${GLOBAL_TAG}\``;
5
- const TEMPLATE_END = '`]';
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 += TEMPLATE_START;
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 += TEMPLATE_END;
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
- // Add `import { hbs as __T } from 'ember-template-imports'` to the file
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 '[__T`foo`]' in a class body to 'static { __T`foo` }'
49
- let location = findTemplateLocation(templateLocations, node);
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: ETITemplateExpression | ETITemplateProperty,
177
+ node: ETITemplateLiteral,
178
+ sourceFile?: ts.SourceFile,
185
179
  ): TemplateLocation {
186
- let location = locations.find((loc) => loc.transformedStart === node.getStart());
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
- GlintTagConfig,
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
- let preamble = [];
70
- if (!info.importedBinding.synthetic) {
71
- preamble.push(`${tag.text};`);
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: GlintTagConfig } | undefined {
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
- for (let [importSource, tags] of Object.entries(environment.getConfiguredTemplateTags())) {
174
- for (let [importSpecifier, tagConfig] of Object.entries(tags)) {
175
- if (
176
- importSource === importedBinding.source &&
177
- importSpecifier === importedBinding.specifier
178
- ) {
179
- return { importedBinding, tagConfig };
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(node: AST.Node, callback: () => void, codeFeaturesForNode?: CodeInformation): void;
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(node: AST.Node, callback: () => void, codeFeaturesForNode?: CodeInformation) {
331
- captureMapping(mapper.rangeForNode(node), node, false, callback, codeFeaturesForNode);
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
- // Here we look for ```hbs``` tagged template expressions, originally introduced
164
- // in the now-removed GlimmerX environment. We can consider getting rid of this, but
165
- // then again there are still some use cases in the wild (e.g. Glimmer Next / GXT)
166
- // where have tagged templates closing over outer scope is desirable:
167
- // https://github.com/lifeart/glimmer-next/tree/master/glint-environment-gxt
168
- // https://discord.com/channels/480462759797063690/717767358743183412/1259061848632721480
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
- return { name, form, requiresConsumption: !isGlobal };
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
- emitResolve(node, hasParams ? 'resolve' : 'resolveOrReturn');
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(node: CurlyInvocationNode, resolveType: string): void {
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
- mapper.text('__glintDSL__.');
1328
- mapper.text(resolveType);
1329
- mapper.text('(');
1330
- emitExpression(node.path);
1331
- mapper.text(')(');
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) {