Skip to content

[api] Provide module resolution overrides - #64299

Open
Andrew Branch (andrewbranch) wants to merge 24 commits into
microsoft:mainfrom
andrewbranch:api-module-resolution
Open

Andrew Branch (andrewbranch) wants to merge 24 commits into
microsoft:mainfrom
andrewbranch:api-module-resolution

Conversation

@andrewbranch

@andrewbranch Andrew Branch (andrewbranch) commented Sep 16, 2026

Copy link
Copy Markdown
Member

Fixes #64069

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:
const dirname = fileURLToPath(new URL(".", import.meta.url));
const r1 = resolver.resolveModuleName("typescript", dirname, ModuleKind.ESNext);

// Resolve against an existing snapshot:
const r2 = 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 directory
        name: "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 directory
        name: "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 run
    entries: [/* ... */]
  },
  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.
    return defaultResolver.resolveModuleName(name, containingDirectory, mode, { snapshot });
  },
});

using program = api.createProgram(rootNames, compilerOptions, {
  moduleResolver: customResolver
});

Add snapshot-scoped reusable module resolvers and serializable resolution overrides for standalone resolution and createProgram.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
# Conflicts:
#	packages/typescript/src/api/async/api.ts
#	packages/typescript/src/api/proto.generated.ts
#	packages/typescript/src/api/sync/api.ts
#	packages/typescript/test/async/api.test.ts
#	packages/typescript/test/sync/api-generators.test.ts
#	packages/typescript/test/sync/api.test.ts
#	tools/gen-proto/main.go
#	tsc/internal/api/proto.go
#	tsc/internal/api/session.go
#	tsc/internal/project/refcountcache_test.go
#	tsc/internal/project/snapshot.go
#	tsc/internal/project/snapshothost.go
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Public LSP integration, callback lifecycle, handle ownership, and resolution metadata have unresolved correctness issues.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds snapshot-scoped module resolution APIs and program-level resolution overrides.

Changes:

  • Adds reusable resolution sets, resolvers, callbacks, and fallback behavior.
  • Integrates overrides into compiler program creation and snapshot updates.
  • Exposes synchronous/asynchronous TypeScript APIs with tests.
File summaries
File Description
tsc/internal/project/snapshot.go Exposes snapshot resolution-host functionality.
tsc/internal/project/projectcollectionbuilder.go Applies resolution providers to synthetic projects.
tsc/internal/project/project.go Stores providers and passes them to programs.
tsc/internal/module/types.go Defines the resolution-provider interface.
tsc/internal/module/resolver.go Adds directory-based module resolution.
tsc/internal/lsp/server.go Connects API sessions for callbacks.
tsc/internal/compiler/program.go Tracks providers and resolution errors.
tsc/internal/compiler/filesparser.go Propagates resolution errors.
tsc/internal/compiler/fileloader.go Invokes resolution overrides while loading imports.
tsc/internal/api/session.go Handles new module-resolution requests and providers.
tsc/internal/api/session_module_resolution_test.go Tests resolution sets, callbacks, and programs.
tsc/internal/api/session_createprogram_test.go Updates request-conversion tests.
tsc/internal/api/server.go Initializes callback connectivity.
tsc/internal/api/proto.go Defines module-resolution protocol messages.
tsc/internal/api/module_resolution.go Implements API resolution behavior.
tools/gen-proto/main.go Generates the resolution-mode API type.
packages/typescript/test/sync/api.test.ts Tests synchronous provider reconfiguration.
packages/typescript/test/sync/api-generators.test.ts Tests generator-method parity.
packages/typescript/test/async/api.test.ts Tests asynchronous provider reconfiguration.
packages/typescript/src/api/syncChannel.ts Supports callback unregistration.
packages/typescript/src/api/sync/client.ts Registers synchronous callbacks.
packages/typescript/src/api/sync/api.ts Exposes synchronous resolution APIs.
packages/typescript/src/api/proto.generated.ts Adds generated protocol definitions.
packages/typescript/src/api/async/client.ts Registers asynchronous callbacks.
packages/typescript/src/api/async/api.ts Exposes asynchronous resolution APIs.
Review details

Suppressed comments (4)

packages/typescript/src/api/async/api.ts:265

  • 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.
    if (input instanceof ModuleResolutionSet) {
        input.ensureNotDisposed();
        return { set: input.id };

packages/typescript/src/api/sync/api.ts:282

  • 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.
    if (input instanceof ModuleResolutionSet) {
        input.ensureNotDisposed();
        return { set: input.id };

packages/typescript/src/api/async/api.ts:273

  • 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.
    client.registerCallback(name, params => {
  • Files reviewed: 25/25 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread tsc/internal/api/module_resolution.go Outdated
Comment thread packages/typescript/src/api/async/api.ts
Comment thread packages/typescript/src/api/sync/api.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Callback errors, callback lifetimes, cross-client handles, and concurrent disposal remain incorrect.

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.
  • Files reviewed: 25/25 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread tsc/internal/api/session.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Override ordering, handle ownership, and callback-cache lifetime can currently produce incorrect behavior or unbounded retention.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 3 High severity · 3 Medium severity

Open (6)

Comment thread packages/typescript/src/api/async/api.ts
Comment thread packages/typescript/src/api/async/api.ts Outdated
Comment thread packages/typescript/src/api/async/api.ts
Comment thread packages/typescript/src/api/async/api.ts Outdated
Comment thread tsc/internal/api/module_resolution.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Cross-session handles can silently target unrelated resources, and LSP snapshot updates do not propagate resolver callback failures.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 4 High severity · 1 Medium severity

Open (5)
Resolved since last review (5)

Comment thread packages/typescript/src/api/async/api.ts
Comment thread packages/typescript/src/api/sync/api.ts
Comment thread packages/typescript/src/api/sync/api.ts
Comment thread packages/typescript/src/api/sync/api.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Snapshot tracking, cross-session handle validation, and concurrent resolver disposal contain correctness and resource-lifecycle defects.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 High severity

Open (2)
Resolved since last review (5)
Previously missed (1)

In code that hasn't changed since last review

Medium severity 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.

Comment thread packages/typescript/src/api/async/api.ts
Comment thread packages/typescript/src/api/sync/api.ts

@jakebailey Jake Bailey (jakebailey) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excited for static resolution.

Comment thread tsc/internal/api/module_resolution.go Outdated
"github.com/microsoft/TypeScript/tsc/internal/tspath"
)

type moduleResolutionMatchKey struct {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense for this to live in module so that Go API users could provide static resolution? (Maybe eliminating the factory?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Conceptually, I certainly enjoy the idea that Program does not create a resolver, but rather uses one its been given.)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Resolver option preservation, callback error propagation, and cross-session handle validation remain incorrect.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 High severity

Open (2)
Resolved since last review (2)

Comment thread tsc/internal/api/module_resolution.go Outdated
Comment thread tsc/internal/compiler/fileloader.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Snapshot tracking, API ownership validation, and retained resolver-context lifetime have unresolved correctness issues.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 High severity

Open (2)
Resolved since last review (2)

Comment thread packages/typescript/src/api/async/api.ts
Comment on lines +66 to +68
return resolver, func() {
f.session.releaseProgramResolutionContext(contextID)
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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.

@jakebailey Jake Bailey (jakebailey) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API] No module resolution API: no counterpart to ts.resolveModuleName

3 participants