@openrewrite/rewrite 8.91.1 → 8.91.2

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 (96) hide show
  1. package/dist/javascript/add-import.d.ts +68 -34
  2. package/dist/javascript/add-import.d.ts.map +1 -1
  3. package/dist/javascript/add-import.js +322 -160
  4. package/dist/javascript/add-import.js.map +1 -1
  5. package/dist/javascript/amd.d.ts +138 -0
  6. package/dist/javascript/amd.d.ts.map +1 -0
  7. package/dist/javascript/amd.js +768 -0
  8. package/dist/javascript/amd.js.map +1 -0
  9. package/dist/javascript/binding.d.ts +94 -0
  10. package/dist/javascript/binding.d.ts.map +1 -0
  11. package/dist/javascript/binding.js +335 -0
  12. package/dist/javascript/binding.js.map +1 -0
  13. package/dist/javascript/index.d.ts +8 -1
  14. package/dist/javascript/index.d.ts.map +1 -1
  15. package/dist/javascript/index.js +26 -1
  16. package/dist/javascript/index.js.map +1 -1
  17. package/dist/javascript/parser-utils.d.ts.map +1 -1
  18. package/dist/javascript/parser-utils.js +2 -0
  19. package/dist/javascript/parser-utils.js.map +1 -1
  20. package/dist/javascript/parser.d.ts +7 -1
  21. package/dist/javascript/parser.d.ts.map +1 -1
  22. package/dist/javascript/parser.js +7 -1
  23. package/dist/javascript/parser.js.map +1 -1
  24. package/dist/javascript/recipes/change-import.d.ts +2 -4
  25. package/dist/javascript/recipes/change-import.d.ts.map +1 -1
  26. package/dist/javascript/recipes/change-import.js +8 -240
  27. package/dist/javascript/recipes/change-import.js.map +1 -1
  28. package/dist/javascript/remove-import.d.ts +2 -28
  29. package/dist/javascript/remove-import.d.ts.map +1 -1
  30. package/dist/javascript/remove-import.js +19 -57
  31. package/dist/javascript/remove-import.js.map +1 -1
  32. package/dist/javascript/scope.d.ts +49 -0
  33. package/dist/javascript/scope.d.ts.map +1 -0
  34. package/dist/javascript/scope.js +324 -0
  35. package/dist/javascript/scope.js.map +1 -0
  36. package/dist/javascript/templating/bindings.d.ts +0 -11
  37. package/dist/javascript/templating/bindings.d.ts.map +1 -1
  38. package/dist/javascript/templating/bindings.js +0 -29
  39. package/dist/javascript/templating/bindings.js.map +1 -1
  40. package/dist/javascript/templating/comparator.d.ts +10 -0
  41. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  42. package/dist/javascript/templating/comparator.js +43 -14
  43. package/dist/javascript/templating/comparator.js.map +1 -1
  44. package/dist/javascript/templating/engine.d.ts +17 -4
  45. package/dist/javascript/templating/engine.d.ts.map +1 -1
  46. package/dist/javascript/templating/engine.js +43 -17
  47. package/dist/javascript/templating/engine.js.map +1 -1
  48. package/dist/javascript/templating/index.d.ts +1 -1
  49. package/dist/javascript/templating/index.d.ts.map +1 -1
  50. package/dist/javascript/templating/index.js.map +1 -1
  51. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  52. package/dist/javascript/templating/pattern.js +2 -2
  53. package/dist/javascript/templating/pattern.js.map +1 -1
  54. package/dist/javascript/templating/placeholder-replacement.d.ts +2 -0
  55. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  56. package/dist/javascript/templating/placeholder-replacement.js +10 -5
  57. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  58. package/dist/javascript/templating/precedence.d.ts +12 -4
  59. package/dist/javascript/templating/precedence.d.ts.map +1 -1
  60. package/dist/javascript/templating/precedence.js +18 -11
  61. package/dist/javascript/templating/precedence.js.map +1 -1
  62. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  63. package/dist/javascript/templating/rewrite.js +7 -1
  64. package/dist/javascript/templating/rewrite.js.map +1 -1
  65. package/dist/javascript/templating/template.d.ts +7 -1
  66. package/dist/javascript/templating/template.d.ts.map +1 -1
  67. package/dist/javascript/templating/template.js +48 -28
  68. package/dist/javascript/templating/template.js.map +1 -1
  69. package/dist/javascript/templating/types.d.ts +10 -18
  70. package/dist/javascript/templating/types.d.ts.map +1 -1
  71. package/dist/javascript/templating/utils.d.ts +1 -1
  72. package/dist/javascript/templating/utils.d.ts.map +1 -1
  73. package/dist/javascript/templating/utils.js +3 -2
  74. package/dist/javascript/templating/utils.js.map +1 -1
  75. package/dist/rewrite-javascript-version.txt +1 -1
  76. package/package.json +2 -2
  77. package/src/javascript/add-import.ts +342 -198
  78. package/src/javascript/amd.ts +915 -0
  79. package/src/javascript/binding.ts +449 -0
  80. package/src/javascript/index.ts +12 -1
  81. package/src/javascript/parser-utils.ts +2 -0
  82. package/src/javascript/parser.ts +13 -0
  83. package/src/javascript/recipes/change-import.ts +10 -260
  84. package/src/javascript/remove-import.ts +20 -56
  85. package/src/javascript/scope.ts +338 -0
  86. package/src/javascript/templating/bindings.ts +0 -30
  87. package/src/javascript/templating/comparator.ts +52 -14
  88. package/src/javascript/templating/engine.ts +70 -28
  89. package/src/javascript/templating/index.ts +0 -1
  90. package/src/javascript/templating/pattern.ts +4 -2
  91. package/src/javascript/templating/placeholder-replacement.ts +16 -6
  92. package/src/javascript/templating/precedence.ts +21 -13
  93. package/src/javascript/templating/rewrite.ts +9 -1
  94. package/src/javascript/templating/template.ts +62 -29
  95. package/src/javascript/templating/types.ts +11 -18
  96. package/src/javascript/templating/utils.ts +4 -2
@@ -25,7 +25,6 @@ export type {
25
25
  MatchOptions,
26
26
  TemplateParameter,
27
27
  TemplateOptions,
28
- ModuleBinding,
29
28
  RewriteRule,
30
29
  TryOnOptions,
31
30
  RewriteConfig,
@@ -236,7 +236,8 @@ export class Pattern {
236
236
  // A capture's type reaches the parse as a declaration, so it shapes the tree the same
237
237
  // way an explicit context statement does and belongs in the key alongside one.
238
238
  [...contextStatements, ...TemplateEngine.capturePreamble(this.captures)],
239
- this._options.dependencies || {}
239
+ this._options.dependencies || {},
240
+ this._options.types
240
241
  );
241
242
 
242
243
  // Level 2: Global cache (fast path - shared with Template)
@@ -247,7 +248,8 @@ export class Pattern {
247
248
  this.templateParts,
248
249
  this.captures,
249
250
  contextStatements,
250
- this._options.dependencies || {}
251
+ this._options.dependencies || {},
252
+ this._options.types
251
253
  );
252
254
  globalAstCache.set(cacheKey, tree);
253
255
  }
@@ -13,7 +13,7 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import {Cursor, isTree} from '../..';
16
+ import {Cursor, isTree, Markers} from '../..';
17
17
  import {J} from '../../java';
18
18
  import {JS} from '..';
19
19
  import {JavaScriptVisitor} from '../visitor';
@@ -42,7 +42,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
42
42
  const replacement = this.replacePlaceholder(tree);
43
43
  if (replacement !== tree) {
44
44
  // `this.cursor` is still the enclosing template node: `super.visit()` has not pushed this one
45
- return maybeParenthesize(enclosingTree(parent ?? this.cursor), tree.id, replacement, true) as R;
45
+ return maybeParenthesize(enclosingTree(parent ?? this.cursor), tree.id, replacement, tree.markers) as R;
46
46
  }
47
47
  }
48
48
 
@@ -181,6 +181,16 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
181
181
  sourcePrefix : this.mergePrefix(sourcePrefix, templatePrefix);
182
182
  }
183
183
 
184
+ /** As `mergePrefix`, for markers: everything the value was matched with, plus the kinds only the slot wrote. */
185
+ private mergeMarkers(sourceMarkers: Markers, templateMarkers: Markers): Markers {
186
+ const added = templateMarkers.markers.filter(
187
+ template => !sourceMarkers.markers.some(source => source.kind === template.kind));
188
+ return added.length === 0 ? sourceMarkers : {
189
+ ...sourceMarkers,
190
+ markers: [...sourceMarkers.markers, ...added]
191
+ };
192
+ }
193
+
184
194
  /**
185
195
  * Expands variadic placeholders in a list of elements.
186
196
  *
@@ -386,7 +396,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
386
396
  if (isTree(propertyValue)) {
387
397
  const propValueAsJ = propertyValue as J;
388
398
  return produce(propValueAsJ, draft => {
389
- draft.markers = placeholder.markers;
399
+ draft.markers = this.mergeMarkers(propValueAsJ.markers, placeholder.markers);
390
400
  draft.prefix = this.mergePrefix(propValueAsJ.prefix, placeholder.prefix);
391
401
  });
392
402
  }
@@ -421,7 +431,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
421
431
  const matchedNode = this.values.get(name);
422
432
  if (matchedNode && !Array.isArray(matchedNode)) {
423
433
  return produce(matchedNode, draft => {
424
- draft.markers = placeholder.markers;
434
+ draft.markers = this.mergeMarkers(matchedNode.markers, placeholder.markers);
425
435
  draft.prefix = this.mergePrefix(matchedNode.prefix, placeholder.prefix);
426
436
  });
427
437
  }
@@ -438,7 +448,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
438
448
  // Extract the element from the J.RightPadded wrapper
439
449
  const element = param.value.element as J;
440
450
  return produce(element, draft => {
441
- draft.markers = placeholder.markers;
451
+ draft.markers = this.mergeMarkers(element.markers, placeholder.markers);
442
452
  draft.prefix = this.mergePrefix(element.prefix, placeholder.prefix);
443
453
  });
444
454
  }
@@ -458,7 +468,7 @@ export class PlaceholderReplacementVisitor extends JavaScriptVisitor<any> {
458
468
  if (isTree(param.value)) {
459
469
  // Return the AST node, preserving comments from the source
460
470
  return produce(param.value as J, draft => {
461
- draft.markers = placeholder.markers;
471
+ draft.markers = this.mergeMarkers(param.value.markers, placeholder.markers);
462
472
  draft.prefix = this.mergePrefix(param.value.prefix, placeholder.prefix);
463
473
  });
464
474
  }
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import {isTree} from '../..';
17
17
  import {emptySpace, J} from '../../java';
18
- import {emptyMarkers, Marker, markers} from '../../markers';
18
+ import {emptyMarkers, Marker, markers, Markers} from '../../markers';
19
19
  import {randomId} from '../../uuid';
20
20
  import {JS} from '..';
21
21
 
@@ -319,9 +319,12 @@ export function requiredPrecedence(parent: J, childId: string): number | undefin
319
319
  return slotConstraints(parent, childId)?.precedence;
320
320
  }
321
321
 
322
- /** Parenthesizes `expression` if the slot of `parent` holding `childId` would otherwise reparse it. */
322
+ /**
323
+ * Parenthesizes `expression` if the slot of `parent` holding `childId` would otherwise reparse it.
324
+ * `slotMarkers` are the markers the slot contributed; {@link parenthesize} says what they decide.
325
+ */
323
326
  export function maybeParenthesize(parent: J | undefined, childId: string, expression: J,
324
- slotOwnsTrailingMarkers: boolean = false): J {
327
+ slotMarkers?: Markers): J {
325
328
  if (!parent) {
326
329
  return expression;
327
330
  }
@@ -334,10 +337,10 @@ export function maybeParenthesize(parent: J | undefined, childId: string, expres
334
337
  // A statement wrapper is transparent here: the parentheses belong around the expression
335
338
  if (expression.kind === JS.Kind.ExpressionStatement) {
336
339
  const inner = (expression as JS.ExpressionStatement).expression;
337
- const wrapped = wrapIfNeeded(parent, childId, constraints, inner, slotOwnsTrailingMarkers);
340
+ const wrapped = wrapIfNeeded(parent, childId, constraints, inner, slotMarkers);
338
341
  return wrapped === inner ? expression : {...expression, expression: wrapped} as JS.ExpressionStatement;
339
342
  }
340
- return wrapIfNeeded(parent, childId, constraints, expression, slotOwnsTrailingMarkers);
343
+ return wrapIfNeeded(parent, childId, constraints, expression, slotMarkers);
341
344
  }
342
345
 
343
346
  /** The nearest enclosing LST node in a cursor path, skipping the padding wrappers visitors push. */
@@ -352,18 +355,23 @@ export function enclosingTree(cursor: { value: any, parent?: any } | undefined):
352
355
  return undefined;
353
356
  }
354
357
 
355
- /** Wraps in `J.Parentheses`, moving the prefix out so the surrounding whitespace survives. */
356
- export function parenthesize(expression: J, slotOwnsTrailingMarkers: boolean = false): J.Parentheses<J> {
357
- const trailing = slotOwnsTrailingMarkers ? expression.markers.markers.filter(isTrailingMarker) : [];
358
- const inner = trailing.length === 0 ? expression : {
358
+ /**
359
+ * Wraps in `J.Parentheses`, moving the prefix out so the surrounding whitespace survives. A trailing marker
360
+ * `slotMarkers` also carries belongs to the slot around the expression, so it moves out too: `${x}!` with `x`
361
+ * bound to `a + b` gives `(a + b)!`, while a bare `${x}` bound to `a!` gives `(a!)`.
362
+ */
363
+ export function parenthesize(expression: J, slotMarkers?: Markers): J.Parentheses<J> {
364
+ const hoisted = expression.markers.markers.filter(
365
+ m => isTrailingMarker(m) && slotMarkers?.markers.some(slot => slot.kind === m.kind));
366
+ const inner = hoisted.length === 0 ? expression : {
359
367
  ...expression,
360
- markers: markers(...expression.markers.markers.filter(m => !isTrailingMarker(m)))
368
+ markers: markers(...expression.markers.markers.filter(m => !hoisted.includes(m)))
361
369
  };
362
370
  return {
363
371
  kind: J.Kind.Parentheses,
364
372
  id: randomId(),
365
373
  prefix: expression.prefix,
366
- markers: trailing.length === 0 ? emptyMarkers : markers(...trailing),
374
+ markers: hoisted.length === 0 ? emptyMarkers : markers(...hoisted),
367
375
  tree: {
368
376
  kind: J.Kind.RightPadded,
369
377
  element: {...inner, prefix: emptySpace},
@@ -374,7 +382,7 @@ export function parenthesize(expression: J, slotOwnsTrailingMarkers: boolean = f
374
382
  }
375
383
 
376
384
  function wrapIfNeeded(parent: J, childId: string, constraints: SlotConstraints, expression: J,
377
- slotOwnsTrailingMarkers: boolean): J {
385
+ slotMarkers: Markers | undefined): J {
378
386
  if (precedenceOf(expression) < constraints.precedence ||
379
387
  (constraints.noCallShape && isCallShaped(expression)) ||
380
388
  (constraints.noOptionalChain && hasOptionalChain(expression)) ||
@@ -383,7 +391,7 @@ function wrapIfNeeded(parent: J, childId: string, constraints: SlotConstraints,
383
391
  (constraints.followedByDot && isDotAdjacentNumber(expression)) ||
384
392
  mixesNullishWithLogical(parent, expression) ||
385
393
  wouldFuseSigns(parent, childId, expression)) {
386
- return parenthesize(expression, slotOwnsTrailingMarkers);
394
+ return parenthesize(expression, slotMarkers);
387
395
  }
388
396
  return expression;
389
397
  }
@@ -58,7 +58,15 @@ class RewriteRuleImpl implements RewriteRule {
58
58
  let result: J | undefined;
59
59
 
60
60
  const template = typeof this.after === 'function' ? this.after(match) : this.after;
61
- const bindings = options?.bindings ?? (options?.visitor && template.resolveBindings(options.visitor));
61
+ const bindings = options?.bindings ??
62
+ (options?.visitor ? await template.resolveBindings(options.visitor) : undefined);
63
+ // Applying without them would splice the context's own names in unbound, which
64
+ // reads as a working edit and is not one.
65
+ if (bindings === undefined && await template.bindsModules()) {
66
+ throw new Error(
67
+ "Template binds modules in its context, so applying it needs their local names. " +
68
+ "Pass {visitor: this} to tryOn, or bindings you resolved yourself.");
69
+ }
62
70
  result = await template.apply(node, cursor,
63
71
  { values: match, format: this.format, bindings: bindings || undefined });
64
72
 
@@ -16,8 +16,8 @@
16
16
  import {Cursor, Tree} from '../..';
17
17
  import {J} from '../../java';
18
18
  import {ApplyOptions, Parameter, TemplateOptions, TemplateParameter} from './types';
19
- import {bindingContextStatement, isResolvable} from './bindings';
20
- import {maybeAddImport} from '../add-import';
19
+ import {maybeBind} from '../binding';
20
+ import {ContextBinding} from './engine';
21
21
  import {JavaScriptVisitor} from '../visitor';
22
22
  import {MatchResult} from './pattern';
23
23
  import {generateCacheKey, globalAstCache, WRAPPERS_MAP_SYMBOL} from './utils';
@@ -177,6 +177,7 @@ export class TemplateBuilder {
177
177
  export class Template {
178
178
  private options: TemplateOptions = {};
179
179
  private _cachedTemplate?: J;
180
+ private _contextBindings?: Promise<ContextBinding[]>;
180
181
 
181
182
  /**
182
183
  * Creates a new template.
@@ -224,6 +225,7 @@ export class Template {
224
225
  this.options = {...this.options, ...options};
225
226
  // Invalidate cache when configuration changes
226
227
  this._cachedTemplate = undefined;
228
+ this._contextBindings = undefined;
227
229
  return this;
228
230
  }
229
231
 
@@ -240,20 +242,16 @@ export class Template {
240
242
  * @returns The cached or newly computed template tree
241
243
  * @internal
242
244
  */
243
- private async getTemplateTree(): Promise<JS.CompilationUnit> {
245
+ private async getTemplateTree(): Promise<J> {
244
246
  // Level 1: Instance cache (fastest path)
245
247
  if (this._cachedTemplate) {
246
- return this._cachedTemplate as JS.CompilationUnit;
248
+ return this._cachedTemplate;
247
249
  }
248
250
 
249
251
  // Generate cache key for global lookup
250
252
  // For raw() parameters, we need to include their code values in the key
251
253
  // since they're spliced at construction time, not application time
252
- const contextStatements = [
253
- ...(this.options.context || this.options.imports || []),
254
- ...Object.entries(this.options.bindings ?? {})
255
- .map(([name, b]) => bindingContextStatement(name, b, this.options.dependencies ?? {}))
256
- ];
254
+ const contextStatements = this.options.context || this.options.imports || [];
257
255
  const parametersKey = this.parameters.map((p, i) => {
258
256
  const value = p.value;
259
257
  // Include raw code values in the cache key using the symbol
@@ -267,14 +265,15 @@ export class Template {
267
265
  parametersKey,
268
266
  // As in Pattern.getAstPattern: a parameter's type reaches the parse as a declaration
269
267
  [...contextStatements, ...TemplateEngine.parameterPreamble(this.parameters)],
270
- this.options.dependencies || {}
268
+ this.options.dependencies || {},
269
+ this.options.types
271
270
  );
272
271
 
273
272
  // Level 2: Global cache (fast path - shared with Pattern)
274
273
  const cached = globalAstCache.get(cacheKey);
275
274
  if (cached) {
276
- this._cachedTemplate = cached as JS.CompilationUnit;
277
- return cached as JS.CompilationUnit;
275
+ this._cachedTemplate = cached;
276
+ return cached;
278
277
  }
279
278
 
280
279
  // Level 3: Compute via TemplateEngine (slow path)
@@ -282,8 +281,9 @@ export class Template {
282
281
  this.templateParts,
283
282
  this.parameters,
284
283
  contextStatements,
285
- this.options.dependencies || {}
286
- ) as JS.CompilationUnit;
284
+ this.options.dependencies || {},
285
+ this.options.types
286
+ );
287
287
 
288
288
  // Cache in both levels
289
289
  globalAstCache.set(cacheKey, result);
@@ -298,18 +298,47 @@ export class Template {
298
298
  * cannot resolve is bound whether or not the template goes on to reference it, so call this
299
299
  * where the template is known to apply — {@link RewriteRule.tryOn} does, once a pattern matched.
300
300
  */
301
- resolveBindings(visitor: JavaScriptVisitor<any>): Record<string, string> {
301
+ async resolveBindings(visitor: JavaScriptVisitor<any>): Promise<Record<string, string>> {
302
302
  const resolved: Record<string, string> = {};
303
- const dependencies = this.options.dependencies ?? {};
304
- for (const [name, binding] of Object.entries(this.options.bindings ?? {})) {
305
- // Recognising the reference the template splices in takes attribution, which only the
306
- // import form of a context statement carries. Without one there is nothing to look for.
307
- const onlyIfReferenced = isResolvable(binding.module, dependencies);
308
- resolved[name] = maybeAddImport(visitor, {...binding, preferredName: name, onlyIfReferenced});
303
+ for (const binding of await this.contextBindings()) {
304
+ // Recognising the reference the template splices in takes attribution, so a module the
305
+ // workspace could not resolve is bound whether or not the template turns out to use it.
306
+ const onlyIfReferenced = binding.attributed;
307
+ const bound = maybeBind(visitor, {
308
+ module: binding.module!,
309
+ member: binding.member,
310
+ typeOnly: binding.typeOnly,
311
+ preferredName: binding.name,
312
+ onlyIfReferenced
313
+ });
314
+ // An unresolved binding is left out rather than recorded as `undefined`, so `apply()`'s
315
+ // own "applied without a local name" check catches it, same as a caller-omitted one.
316
+ if (bound !== undefined) {
317
+ resolved[binding.name] = bound;
318
+ }
309
319
  }
310
320
  return resolved;
311
321
  }
312
322
 
323
+ /** Whether the context binds a module, which applying this template therefore has to resolve. */
324
+ async bindsModules(): Promise<boolean> {
325
+ return (await this.contextBindings()).length > 0;
326
+ }
327
+
328
+ /** What this template's context statements bind, which is what it needs bound in the target file. */
329
+ private contextBindings(): Promise<ContextBinding[]> {
330
+ return this._contextBindings ??= this.deriveContextBindings();
331
+ }
332
+
333
+ private deriveContextBindings(): Promise<ContextBinding[]> {
334
+ return TemplateEngine.getContextBindings(
335
+ this.templateParts, this.parameters,
336
+ this.options.context || this.options.imports || [],
337
+ this.options.dependencies || {},
338
+ this.options.types
339
+ );
340
+ }
341
+
313
342
  /**
314
343
  * Applies this template and returns the resulting tree.
315
344
  *
@@ -372,17 +401,21 @@ export class Template {
372
401
  }
373
402
  }
374
403
 
375
- const declared = this.options.bindings ?? {};
376
404
  const renames: Record<string, string> = {};
377
405
  const modules: Record<string, string> = {};
378
- for (const [name, binding] of Object.entries(declared)) {
379
- const bound = options?.bindings?.[name];
380
- if (bound === undefined) {
381
- throw new Error(`Template declares a binding for '${name}' but was applied without a local name for it. ` +
382
- `Pass bindings: template.resolveBindings(visitor) to apply().`);
406
+ // Supplying names is what asks for the context's modules to be bound in the file being
407
+ // edited; without them a context import only types the template.
408
+ if (options?.bindings !== undefined) {
409
+ for (const binding of await this.contextBindings()) {
410
+ const bound = options.bindings[binding.name];
411
+ if (bound === undefined) {
412
+ throw new Error(`Template binds '${binding.module}' in its context, but no local name was ` +
413
+ `given for '${binding.name}'. Either pass bindings from resolveBindings(visitor), or — if it ` +
414
+ `already did — binding was refused, which an AMD block or a file requiring its modules can do.`);
415
+ }
416
+ renames[binding.name] = bound;
417
+ modules[binding.name] = binding.module!;
383
418
  }
384
- renames[name] = bound;
385
- modules[name] = binding.module;
386
419
  }
387
420
 
388
421
  // Use instance-level cache to get the template tree
@@ -18,7 +18,6 @@ import {J, Type} from '../../java';
18
18
  import type {Pattern} from "./pattern";
19
19
  import type {Template} from "./template";
20
20
  import type {CaptureValue, RawCode} from "./capture";
21
- import type {AddImportOptions} from "../add-import";
22
21
  import type {JavaScriptVisitor} from "../visitor";
23
22
 
24
23
  /**
@@ -350,6 +349,12 @@ export interface PatternOptions {
350
349
  */
351
350
  dependencies?: Record<string, string>;
352
351
 
352
+ /**
353
+ * Type packages to load whose declarations nothing imports, as {@link TemplateOptions.types}.
354
+ * A pattern matches on attribution, so a module typed only ambiently needs this to match.
355
+ */
356
+ types?: string[];
357
+
353
358
  /**
354
359
  * When true, allows patterns without type annotations to match code with type annotations.
355
360
  * This enables more flexible pattern matching during development or when full type attribution
@@ -485,27 +490,15 @@ export interface TemplateOptions {
485
490
  dependencies?: Record<string, string>;
486
491
 
487
492
  /**
488
- * Modules the template's code refers to, keyed by the identifier its source uses for each.
489
- * The key is a preferred name: {@link Template.resolveBindings} deconflicts it against the
490
- * file, and applying the template rewrites the template's references to whatever it settled on.
493
+ * Type packages to load whose declarations nothing imports. TypeScript reads `@types/*` on its
494
+ * own and everything else only when named here, so a package declaring its modules ambiently —
495
+ * rather than at a path matching the specifier resolves only with this set.
491
496
  *
492
- * @example
493
- * ```typescript
494
- * template`Theming.setTheme(${capture('theme')})`
495
- * .configure({bindings: {Theming: {module: 'sap/ui/core/Theming', member: 'default'}}})
496
- * ```
497
+ * @example `{dependencies: {'@sapui5/types': '^1.120.0'}, types: ['@sapui5/types']}`
497
498
  */
498
- bindings?: Record<string, ModuleBinding>;
499
+ types?: string[];
499
500
  }
500
501
 
501
- /**
502
- * A module a template's code depends on. The local name comes from the key it is declared under,
503
- * and {@link Template.resolveBindings} settles the rest. `member` and `typeOnly` shape the import
504
- * it creates, so a caller passing {@link ApplyOptions.bindings} of its own reads neither.
505
- */
506
- export type ModuleBinding = Omit<AddImportOptions,
507
- 'module' | 'alias' | 'preferredName' | 'onlyIfReferenced' | 'sideEffectOnly'> & { module: string };
508
-
509
502
  /**
510
503
  * Options for template application.
511
504
  */
@@ -107,13 +107,15 @@ export function generateCacheKey(
107
107
  templateParts: string[] | TemplateStringsArray,
108
108
  itemsKey: string,
109
109
  contextStatements: string[],
110
- dependencies: Record<string, string>
110
+ dependencies: Record<string, string>,
111
+ types?: string[]
111
112
  ): string {
112
113
  return [
113
114
  Array.from(templateParts).join('|'),
114
115
  itemsKey,
115
116
  contextStatements.join(';'),
116
- JSON.stringify(dependencies)
117
+ JSON.stringify(dependencies),
118
+ JSON.stringify(types ?? null)
117
119
  ].join('::');
118
120
  }
119
121