You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This adds a way to perform isolated, arbitrary resolveModuleName calls, and provides two ways of overriding module resolution in API programs.
Creating and using a module resolver
// Create a resolver with compiler options:
using resolver=api.createModuleResolver(compilerOptions);// Resolve against the host file system:constdirname=fileURLToPath(newURL(".",import.meta.url));constr1=resolver.resolveModuleName("typescript",dirname,ModuleKind.ESNext);// Resolve against an existing snapshot:constr2=resolver.resolveModuleName("typescript",dirname,ModuleKind.ESNext,{ snapshot });
Customizing its behavior
// Specify static resolutions:
using resolver=api.createModuleResolver(compilerOptions,{moduleResolutions: {// Resolutions not supplied in 'entries' will be attempted by the default resolver.// Use "unresolved" to indicate that modules not found in 'entries' should be treated as unresolved.fallback: "resolve",// Entries are tried in order of specificity first (directory is more specific// than mode), then in the order they are listed.entries: [{// Applies to ESM-mode imports of "typescript" from the specified containing directoryname: "typescript",containingDirectory: "/project",mode: ModuleKind.ESNext,result: {resolvedFileName: "/.cache/typescript-1/lib/typescript.d.ts"}},{// Applies to imports of "typescript" from files directly inside the specified containing directoryname: "typescript",containingDirectory: "/project",result: {resolvedFileName: "/.cache/typescript-2/lib/typescript.d.ts"}},{// Applies to all other imports of "typescript"name: "typescript",result: {resolvedFileName: "/.cache/typescript-3/lib/typescript.d.ts"}},]}});// Specify a callback:
using resolver=api.createModuleResolver(compilerOptions,{resolveModuleName: (name,containingDirectory,mode)=>{return{resolvedFileName: "./all-my-modules-resolve-here.ts"};}});// Specify both: static resolutions checked first, then the callback is used as a fallback.
using resolver=api.createModuleResolver(compilerOptions,{moduleResolutions: {fallback: "resolve",// Must be "resolve" for the callback to ever runentries: [/* ... */]},resolveModuleName: (name,containingDirectory,mode)=>{return{resolvedFileName: "./all-my-other-modules-resolve-here.ts"};}});
Providing a resolver to a program
using defaultResolver=api.createModuleResolver(compilerOptions);
using customResolver=api.createModuleResolver(compilerOptions,{moduleResolutions: {fallback: "resolve",entries: [/* ... */]},resolveModuleName: (name,containingDirectory,mode,{ snapshot })=>{if(shouldCustomize(name)){return{/* ... */};}// During program construction, `snapshot` represents the in-progress snapshot build.// Passing it along to `defaultResolver` ensures it sees the same file system contents// as the program build, and tracks every touched file as a dependency that could// invalidate the program.returndefaultResolver.resolveModuleName(name,containingDirectory,mode,{ snapshot });},});
using program=api.createProgram(rootNames,compilerOptions,{moduleResolver: customResolver});
Cover reusable sets, changed and removed providers, repeated inline specs, and callbacks under the snapshot reconfiguration model.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Exercise synthetic-program reconfiguration end to end for reusable sets, changed and removed providers, repeated inline specs, and callbacks.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A ModuleResolutionSet is scoped to the API session that created it, but this only checks disposal before serializing its numeric ID. If a set from another API instance is passed here, both sessions can have (for example) set ID 1, causing the request to silently use an unrelated resolution set. Validate that the set belongs to the current client/session before sending its ID.
A ModuleResolutionSet is scoped to the API session that created it, but this only checks disposal before serializing its numeric ID. If a set from another API instance is passed here, both sessions can have (for example) set ID 1, causing the request to silently use an unrelated resolution set. Validate that the set belongs to the current client/session before sending its ID.
The disposer returned by client.registerCallback is discarded. Every snapshot creation/reconfiguration and module-resolver creation with a callback therefore leaves a handler that captures the user callback until the entire API connection closes, even after the owning snapshots are disposed. Track the disposer and unregister once no live snapshot or resolver can reference this callback.
client.registerCallback(name, params => {
packages/typescript/src/api/sync/api.ts:290
The disposer returned by client.registerCallback is discarded. Every snapshot creation/reconfiguration and module-resolver creation with a callback therefore leaves a handler that captures the user callback until the entire API connection closes, even after the owning snapshots are disposed. Track the disposer and unregister once no live snapshot or resolver can reference this callback.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (3)
Previously missed (3) — in code that hasn't changed since the last review.
packages/typescript/src/api/async/api.ts:268
This accepts a ModuleResolutionSet created by any API instance. Since set IDs are allocated per server session, passing a set from API A into API B can either report “not found” or, if B has allocated the same numeric ID, silently use B's unrelated resolutions. Validate that the set belongs to the current client before serializing its handle. packages/typescript/src/api/async/api.ts:283
The disposer returned by client.registerCallback is discarded, so every callback supplied while creating or reconfiguring programs/resolvers remains registered and retains its closure until the whole connection closes. Repeated snapshot updates therefore grow the callback registry without bound, including when the API request fails. Track the disposer with the provider's owning snapshot/resolver and unregister it once no live snapshot can use that provider. packages/typescript/src/api/async/api.ts:912
disposed is set only after the release request completes, so two concurrent calls to dispose() both send releaseModuleResolutionSet; the second request then fails because the server handle is already gone. Memoize the in-flight disposal promise, as Snapshot.dispose does, to keep disposal idempotent under concurrency.
Make async module resolver disposal concurrency-safe
packages/typescript/src/api/async/api.ts:973
Concurrent calls to this intended-idempotent async dispose() both observe disposed === false and send releaseModuleResolver; the second request then fails because the server registration was already removed. Cache an in-flight disposal promise, as Snapshot.dispose() does, so all callers await the same release.
The reason will be displayed to describe this comment to others. Learn more.
The factory is kind of needed if you want to provide static resolution that falls back to dynamic resolution, while sharing a single resolver and cache for the full program build, because currently, the program still uses its own resolver unconditionally for certain things, namely
ResolveTypeReferenceDirective
GetPackageScopeForPath
PackageJsonCacheEntries
ResolvePackageDirectory
But thinking about this more, I think ProgramOptions should just take a module.Resolver-like interface that handles all of these things, such that the program never creates its own fallback resolver. Also, I meant to support static and callback resolveTypeReferenceDirective from the JS API and forgot. It's not quite clear to me whether package.jsons need to be statically providable too, but I guess that can be added later.
So I think the plan will be
Make ProgramOptions take an interface that can replace *module.Resolver instead of module.ResolutionProviderFactory. (Maybe the interface is module.Resolver with implementations *module.DynamicResolver and `*module.StaticResolver?)
Move the static resolver to public module API as one concrete implementation of that interface.
The reason will be displayed to describe this comment to others. Learn more.
Ugh, the trouble with this is that the module resolver's host is wrapped with the projectReferenceDtsFakingHost, which is internal to Program and only created right before module resolution begins. So there's really no way to provide a custom resolver and have access to correct fallback behavior without ProgramOptions taking a factory function like (fallback module.Resolver) module.Resolver. I think that's reasonable, though.
The reason will be displayed to describe this comment to others. Learn more.
I don't know how Program.ResolveModuleName got introduced but that's not a thing—it had no callers and I just deleted it. If the scenario described were possible, it would indicate that our snapshot model was flawed, not that I released a resource too early. Module resolution can't happen after program construction is completed.
The reason will be displayed to describe this comment to others. Learn more.
Needs a main merge, but, pretty neat!
This branch has not been deployed
No deployments
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
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.
Fixes #64069
This adds a way to perform isolated, arbitrary
resolveModuleNamecalls, and provides two ways of overriding module resolution in API programs.Creating and using a module resolver
Customizing its behavior
Providing a resolver to a program