fix(aliases): a paths wildcard no longer matches an import that leaves nothing for its * (#2969)

A tsconfig/jsconfig `paths` key with text on both sides of its `*`, like
"@x/*/index", matched an import that starts with the prefix and ends with
the suffix only by counting one character twice: `@x/index` starts with
`@x/` and ends with `/index` through its single `/`. applyAliases tested
startsWith and endsWith independently and sliced an empty capture, so the
import and every call through it linked to `widgets/index`, a file the
import never named.

The pattern is now skipped when the specifier is shorter than the prefix
and the suffix together, which is TypeScript's own isPatternMatch rule. A
less specific pattern can still match, and an empty capture at exactly
prefix + suffix length stays allowed, as tsc allows it.

Validation: new path-alias-overlap tests (4) fail 3 of 4 on main and pass
with the fix; the exact-length case passes on both. A/B against main on
bulletproof-react, excalidraw, outline and vben (all with paths aliases)
and gin and flask: byte-identical, as no paths key in them has text after
its `*` (0 of 44 configs with paths in our local clones). outline index
median of 3 interleaved: 19,165 ms main, 18,546 ms fix. Full suite: 9,226
passed, 0 failed, 85 skipped.

Reimplements PR #882 by @Dashsoap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-28 01:48:20 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 7973df0d09
commit 77ee3484be
3 changed files with 103 additions and 0 deletions
+6
View File
@@ -353,6 +353,12 @@ export function applyAliases(
let captured = '';
if (pat.hasWildcard) {
// The prefix and the suffix may not share characters. `@x/*/index`
// both starts and ends `@x/index` only by counting its one `/` twice,
// which leaves nothing for `*` to stand on. tsc's `isPatternMatch`
// wants the specifier at least as long as the two together, so this
// pattern is skipped and a less specific one may still match.
if (importPath.length < pat.prefix.length + pat.suffix.length) continue;
captured = importPath.slice(pat.prefix.length, importPath.length - pat.suffix.length);
} else if (importPath !== pat.prefix) {
// Literal pattern must match exactly.