Repository navigation
fix(js,ts): a catch-all or node_modules path alias no longer makes package imports look local - #2437
Merged
Merged
Conversation
…ckage imports look local The out-of-repo import guard (`isOutOfRepoImport`) never fired in two setups: - A catch-all `paths` key. The guard started with `isExternalImport`, which treats an import as local when it starts with a root alias's prefix, and a `"*"` key's prefix is empty. On cord-field (`"*": ["./typings/*"]`), 169 imports of `Typography` from `@mui/material` bound to the project's own `Typography`. - An alias that lands on disk but outside the index. `fileExists` falls back to `fs.existsSync`, which also answers for a directory or a file under `node_modules`. home-assistant's `"lit/decorators": ["./node_modules/lit/decorators.js"]` bound 4,916 `@property()` decorators to an unrelated `property` field, and topcoder's `config` package landed on its `config/` folder. The guard now skips the alias-prefix test (it already asks `resolveImportPath` whether an alias maps the import to a file) and counts that resolution only when it lands on an indexed file. Other callers of `isExternalImport` are unchanged: resolving an import still needs the prefix test so `"*": ["src/*"]` aliases resolve at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…si-255b83 # Conflicts: # CHANGELOG.md
This was referenced Oct 7, 2026
…si-255b83 # Conflicts: # CHANGELOG.md
This was referenced Oct 7, 2026
Merged
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.
Problem
The out-of-repo import guard (#2120, #2128) keeps a name that a file imports from an npm package from binding to a project symbol that only shares the name. In two setups it never fired.
1. A catch-all
pathskey.isOutOfRepoImportstarts withisExternalImport. That call treats an import as local when it starts with a root alias's prefix, so that alias resolution gets a chance to run. A"*"key has an empty prefix, so every package import counted as local."baseUrl": "src","*": ["./typings/*"]) bound 169 imports ofTypographyfrom@mui/materialtosrc/components/Typography/Typography.stories.tsx.2. An alias that lands on disk but outside the index.
resolveImportPath"resolves" such an import, becausefileExistsfalls back tofs.existsSync. That also returns true for a directory or for a file undernode_modules."lit/decorators": ["./node_modules/lit/decorators.js"](and 44 aliases like it): on main, 4,916 lit@property()decorators bind toZWaveJSNodeConfigParam::property, and 3,098@state()decorators to unrelatedstatefields.node_modules/*.import config from 'config'lands on the repo'sconfig/folder.Fix
isOutOfRepoImport(src/resolution/index.ts) now:isExternalImportwithout the alias-prefix test (new optionaliasPrefixes: false). The prefix test is redundant here because the guard then asksresolveImportPathwhether an alias maps the specifier to a file;knownFiles).The result:
package.jsonchain declares, which no alias maps to an indexed file, is outside the repo, whatever the aliases..d.tsfor an untyped package, orcomponents/Buttonthrough"*": ["src/*"].The other
isExternalImportcallers are unchanged:resolveImportPathneeds the prefix test, or"*": ["src/*"]-style aliases would stop resolving.isBoundToOutOfRepoImport, the inheritance gate, keeps its old answer. In these repos the name-matcher guard now declines the package-boundextends/implementsrefs, and none of those edges remain.Tests
__tests__/catch-all-path-alias.test.ts, three cases:~/…imports still bind, and so does a package whose types the catch-all finds in the project."*": ["src/*"]. A project import still resolves through the catch-all, and a package import doesn't bind to a namesake.node_modules/*catch-all, and itsconfig/folder.All three fail on main. The third also fails with only the prefix half of the fix, which pins the indexed-file check. The TypeScript build is clean.
Test runs on the merged branch (Windows, a shared box at 100% CPU):
#1820module-global test hitting its own 60 s limit. With the limit raised it passes alone (99 s).#1820again (99 s with its limit raised);#1356disconnect test (2.3 s, under its own 20 s limit).Validation
I compared main with this branch on 18 repos, each indexed fresh (
init -y), with edges compared by qualified-name keys. Each repo was indexed twice:node_modulesstubbed on disk to emulate an installed checkout: a folder for every declared package and every imported specifier, plus each alias target file.The first run was on main at 22a7ba8. After merging main at ed199e6, which includes #2412, #2415 and #2423, I re-indexed the 13 repos that moved or are JS controls. Their removed edge sets came out line-for-line the same. The other 5 controls were byte-identical on 22a7ba8.
"*": ["./typings/*"]"*": ["src/components/*", "src/*"]"*": ["src/shared/*", "src/*", "node_modules/*"]"*": ["src/*"]"*": ["src/*"]"*": ["src/*", "src/shared/*", "test/*"]cors()edge, now from a framework resolver instead of name matching)"*"node_modulesnode_modulesunresolved_refsasfailed.node_modulesexisted, it changed nothing on topcoder or home-assistant, which is why the indexed-file check is part of the fix.node_modules, itsreact-nativeimport falls through to the repo's own rootreact-native.ts, which is also what TypeScript would pick.Every removed edge, triaged
All 9,072 removed edges bind a name the file imports from a declared package:
Typography,IconButton,Form→ stories and wrapper components; lodashmerge→ the project's ownmerge. happa: grommetText×99 → a projectText. topcoder:configfromtopcoder-react-utils×133 → a jest config constant. home-assistant: the 8,014 lit decorators. 39 of these hit the file's ownconst x = require('pkg')binding, the CommonJS shape #2120 already declines.Themefrom@mui/material→ adeclare module '@emotion/react'augmentation. legend-state's 11 self-loops: a property's react-native type reference resolved to the property itself.DateTime×32, MUIPalette, react-routerNavigateProps. happa: grommetFormFieldProps, styled-componentsDefaultTheme. TypeScript merges these into the package's type, but the import names the package.MsgReaderfrom@freiraum/msgreader,MammothOptionsfrommammoth→typings/…/index.d.ts. TypeScript resolves these imports there.Main already declines the last two groups in every repo without a catch-all; they bound here only by name. This PR applies the guard's existing rule to catch-all repos rather than changing it. Binding these imports to the repo's own
declare moduledeclarations is a follow-up.Not in this PR (filed as follow-ups)
jsx-rendersynthesizer andmatchFunctionRefnever consult the guard, alias or not.<Typography>from@mui/materialstill gets ajsx-renderedge to the project'sTypography: 313 such edges on cord-field, and 33 on outline, a control.declare module 'x') could bind to that declaration. Related:declare functionsignatures in.d.tsfiles are not extracted.🤖 Generated with Claude Code