Conversation
Two things blocked type-aware recipes for SAP UI5, and neither was about
being unable to read UI5's declarations.
A package that declares its modules ambiently was never in scope. The
parser's `types` defaults to `["*"]`, which globs `node_modules/@types`
and nothing else, so `@openui5/types` — which declares `"sap/m/Button"`
rather than shipping something resolvable by path — installed cleanly and
then sat unreferenced, leaving every `Button` unattributed. `npm()` now
names the packages a fixture's own manifest declares, keeping `"*"`
alongside so `@types` reads as before. A test that declared a dependency
in order to use it gets that dependency's declarations.
AMD factory parameters carried no type at all. `sap.ui.define([...],
function (Button) {…})` pairs the nth dependency with the nth parameter
positionally, at loader runtime, so nothing declares the pairing for a
checker to follow, and everything reached through the parameter went
unattributed — which is most of the UI5 migration surface. The parser now
declares that pairing itself, as an overload whose `dependencies` is the
literal tuple the call site passes, so it matches that call and no other.
The declarations go in a file beside the source rather than in it: no
position in the parsed text moves, a relative specifier resolves as it
does for the source, and the checker's own inference carries the type
through the factory body from there. `require` is left alone, already
being declared globally by `@types/node`.
`main` reworked how programs are built while this branch was open: #8733 reads the project's tsconfig and builds one `ts.Program` per config group rather than one per parse batch. The AMD overload declarations move onto that structure — each is added to the root names of the program its own source belongs to, and the compiler host serves them per group.
`sap.ui.define(["sap/m/Button"], function (Button) {…})` attributed its factory parameter only where `@openui5/types` reached the program through a reference directive. Named in `types`, which is how a project states it and how a `tsconfig` carries it, the loader's own `sap.ui.define` overloads win instead and the parameter stays `any` — so the generated overload did nothing for a UI5 source as one is actually written.
The generated declarations now reference each named type package themselves. The AMD test drops the `/// <reference types>` line its fixture carried, which is what let this through: no fixture reached the overload the way a UI5 source does.
`TemplateOptions.types` passes through to `compilerOptions.types`, where an explicit list replaces the default rather than extending it. A template naming an ambient package — the documented use, `types: ['@sapui5/types']` — therefore lost every `@types/*` global along with it: `process.cwd()` came back unattributed in a workspace that installs `@types/node`. The engine keeps `*` alongside whatever the caller names, which is what the option's own documentation describes. An empty list still names nothing and loads nothing, which is the one way to state that.
…ackages `npm()` names the declaration packages a fixture's manifest declares, as the parser's default. A project config merges over that default and carries `types` only where it states one, so a fixture writing a `tsconfig.json` that says nothing about `types` was still given its manifest's packages, with `"types": ["*"]` the only way to decline. Which ambient declarations are in scope is among the options a project decides, so the manifest names them only for a fixture carrying no config. A fixture that carries one is left to say what it means to say — and a spec helper for `tsconfig.json`, when there is one, then composes rather than collides.
This was referenced Sep 8, 2026
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type-aware recipes for SAP UI5 were blocked on two separate things, neither of which was an inability to read UI5's declarations.
Ambient packages were never in scope. The parser's
typesdefaults to["*"], which globsnode_modules/@typesand nothing else.@openui5/typesdeclares"sap/m/Button"ambiently rather than shipping something resolvable by path, so it installed cleanly and its declarations then sat on disk unreferenced — everyButtoncame out<unknown>.npm()now names the packages a fixture's own manifest declares, keeping"*"alongside so@typesreads as before. Declaring the dependency is the whole setup.AMD factory parameters carried no type.
sap.ui.define(["sap/m/Button"], function (Button) {…})pairs the nth dependency with the nth parameter positionally, at loader runtime, so nothing declares that pairing for a checker to follow. The parser now declares it, generating an overload whosedependenciesis the literal tuple the call site passes:It matches that call and no other, and TypeScript's own inference carries the type through the factory body from there —
new Button(), the variable it initialises, and the calls on that variable all attribute with no propagation added to the type mapper.The declarations go in a file beside the source rather than in it. That keeps every position in the parsed text where it was; transpiling the AMD block into an ES module before type-checking, which is what the UI5 linter does, would mean remapping the whole LST. Sitting beside the source also lets a relative specifier resolve exactly as it does for the source.
requireis left alone deliberately —@types/nodedeclares it globally, and an overload there would catch every CommonJS call in the file.This is worth closing because the mechanical fix for most UI5 findings is to add a module to the
sap.ui.definearray and rewrite the reference to the injected parameter, and every one of those rewrites needs the parameter's type.Tests
type-mapping-ambient-modules.test.tscovers both gaps, alongside a reference-directive control that passed before this change.ui5-setup.test.tspins what aMethodMatchercan select once the types are present, including two limits worth knowing: a module path does not glob (sap/m/*selects nothing, since package patterns split on dots), and matching follows the receiver's own type rather than walking to the type that declares the member.Both suites share one manifest verbatim, so they reuse a single cached install.
Full suite: 2255 passing. The three failures in
test/rpc/install-recipes.test.tsarenpm install @openrewrite/recipes-nodejs@latestexiting 1, and reproduce identically onmain.