Skip to content

fix: resolve tsconfig path aliases containing a colon#780

Merged
privatenumber merged 4 commits into
privatenumber:masterfrom
msmx-mnakagawa:fix/tsconfig-paths-with-colon
May 17, 2026
Merged

fix: resolve tsconfig path aliases containing a colon#780
privatenumber merged 4 commits into
privatenumber:masterfrom
msmx-mnakagawa:fix/tsconfig-paths-with-colon

Conversation

@msmx-mnakagawa

@msmx-mnakagawa msmx-mnakagawa commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #730

  • Resolves tsconfig.paths aliases that contain : in both async and sync ESM resolution.
  • Matches TypeScript's resolver contract: paths applies to non-relative module names, and : is ordinary pattern text.
  • Preserves runtime URL specifiers such as data:, file:, node:, and scheme:// instead of remapping them through paths.
  • No CommonJS behavior change; the CLI signal-test diff only stabilizes the existing CI coverage.

Context

TypeScript supports colon-containing path aliases because it only excludes relative module names from paths matching.

In TypeScript's resolver, paths is attempted when paths && !pathIsRelative(moduleName). pathIsRelative() only matches ./ and ../ style specifiers, so aliases like ns:utils.mjs and a:b/b are eligible non-relative module names. Pattern parsing treats only * specially, which means : is matched as literal text.

Relevant TypeScript source checked against microsoft/TypeScript@f350b52331494b68c90ab02e2b6d0828d2a22a74:

A local ts.resolveModuleName() probe confirmed the same behavior:

  • a:b/b matches a:b/* and resolves to /project/b.ts.
  • ns:utils.mjs matches ns:* and resolves through TS extension substitution to /project/src/utils.mts.

get-tsconfig.resolvePathAlias() also resolves these aliases, so the bug is not that get-tsconfig rejects :. The issue is tsx filtering the specifier before calling resolvePathAlias().

Problem

The ESM hook used requestAcceptsQuery(specifier) as the gate for tsconfig.paths resolution.

That helper treats any : as URL-like, so ns:utils.mjs skipped path alias resolution and was passed directly to Node's ESM loader. Node then rejected it as an unsupported URL scheme instead of letting the configured TypeScript path alias map it to a file.

At the same time, blindly removing the gate would be wrong for a runtime loader: TypeScript can type-resolve data:* through paths, but tsx must still execute real data: imports as data URLs.

Changes

  • Replaces the query-oriented gate with isTsconfigPathAliasSpecifier().
  • Allows non-file, non-relative aliases such as ns:utils.mjs to reach resolvePathAlias().
  • Keeps URL-like runtime specifiers out of paths resolution: data:, file:, node:, and schemes with ://.
  • Applies the same check to the async and sync ESM resolve paths.
  • Adds regression coverage for colon aliases and for preserving data: URL imports.
  • Stabilizes the existing signal relay test with a persistent stdout event iterator so CI does not race on chunk boundaries.

Verification

  • fnm exec --using 24.15.0 pnpm type-check
  • fnm exec --using 24.15.0 pnpm lint
  • CI=true fnm exec --using 24.15.0 pnpm test
  • GitHub Actions passed on Ubuntu and Windows for the latest pushed commit.

@privatenumber privatenumber merged commit 6979f28 into privatenumber:master May 17, 2026
2 checks passed
@privatenumber

Copy link
Copy Markdown
Owner

This issue is now resolved in v4.22.1.

If you're able to, your sponsorship would be very much appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ERR_UNSUPPORTED_ESM_URL_SCHEME error is thrown if path alias contains a colon

2 participants