Repository navigation
fix(js,ts): import { default as X } is the module's default import - #2412
Merged
Merged
Conversation
bulletproof-react's router imports its app shell as
`import { default as AppRoot, ErrorBoundary as AppRootErrorBoundary }
from './routes/app/root'`. The import mapping recorded AppRoot as a
named import of an export called `default`, which no module declares,
so resolveViaImport found nothing for it and every reference fell
through to name matching. The route /app bound to AppRoot by
exact-match, and in a project where another file declares an AppRoot
(or a default export is imported under a name another file uses) the
call, route or JSX attribute bound to that file's symbol instead.
The mapping now records `{ default as X }` as the default import, the
same as `import X from`, so every reader of isDefault agrees with it:
the import resolver finds the module's default export, the binding's
own `imports` reference links the module file as a default import's
does, and the JSX-child, Expo native-module, HTTP-client and styled
Link lookups read it as the default import. Re-exports
(`export { default as X } from`) were already chased as the default.
`require('./x').default` is unchanged: `.default` there is a property
of module.exports, which a CommonJS module can set by name.
alan2207/bulletproof-react: 4 edges re-resolved at the same sites (the
route /app reaches AppRoot by import; three binding imports link their
module file). bitwarden/clients: 2 failed Storybook imports now link
their module. bradtraversy/proshop_mern, leerob/next-saas-starter and
t3-oss/create-t3-turbo: byte-identical. Dead-code reports unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-7c9188 # Conflicts: # CHANGELOG.md
…-7c9188 # Conflicts: # CHANGELOG.md
This was referenced Oct 7, 2026
Open
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
alan2207/bulletproof-react's router (
apps/react-vite/src/app/router.tsx) imports its app shell as:extractJSImportsrecordedAppRootas{ exportedName: 'default', isDefault: false }, a named import of an export calleddefault. No module declares one, sofindExportedSymbolfound nothing,resolveImportreturned null forAppRoot, and every reference to it fell through to name matching. In bulletproof-react the route/appbinds toAppRootbyexact-match. When another file declares a symbol with the same name, the reference can bind to that one instead. The new test's fixture reproduces this on main:/app→legacy.tsx:AppRootrenderShell()'s call →legacy.tsx:AppRoot/preferences,Component: Settingsandsidebar={Settings}→legacy.tsx:Settingsinstead of the default exportSettingsRouteFix
extractJSImportsnow records an aliased named import whose imported name isdefaultas the default import (isDefault: true), which is the mappingimport X fromalready gets.I fixed the mapping rather than teaching
findExportedSymbolto readexportedName === 'default'as the default, because then every other reader ofImportMapping.isDefaultagrees with it too:resolveViaImport→findExportedSymboltakes the default-export path:export default …, a binding exported as default, a component file, or CommonJSmodule.exports =.resolveModuleImportToFilelinks the binding's ownimportsreference to the module file, as it does for a default import.expo-modules:export default requireNativeModule(…);tier-synthesizer: a default-exported HTTP client;react-router-synthesizer: a default-exportedstyled(Link);Svelte, Vue and Astro script blocks go through the same function, so they're covered.
Checked and left unchanged:
export { default as X } from './x'.extractReExportsalready recordsoriginalName: 'default', and the re-export chase callsfindExportedSymbolwithisDefault: rex.originalName === 'default'(Re-export barrels unresolved after #130: default re-exports (export { default as X } from './x.svelte'), bare './' index imports, and package-subpath barrels - callers/callees/impact miss the edge (false 0 callers) #629).const X = require('./x').default. It still maps as a named memberdefault. Here.defaultis a property ofmodule.exports: a module compiled from ESM sets it to the default export, but a CommonJS module can also setexports.defaultitself, so reading it as the ESM default would break that case. See the follow-ups below.Overlap with #2400
layoutComponentinframeworks/react.tsworks around this bug. WhenresolveImportfails for a layout's name, it reads the module of a mapping withisDefault || exportedName === 'default'. With this fixresolveImportsucceeds first for{ default as X }. That fallback is now reached only when the default export can't be resolved, or forrequire(…).default, so I left it as is.Validation
mainat5ace306bagainst this branch. Both arms had the native kernel staged, since TS/JS are kernel-routed as in a release. Each repo was indexed fresh (init -y) and compared withscripts/dump-graph.mjs.In bulletproof-react:
/app→AppRootnow resolves byimportinstead ofexact-match. The target is the same.importsbindingsAppRoot,DiscussionRouteandDiscussionsRoute(the last two from the discussions route tests) now link their module file instead of the function, byimportinstead ofexact-match. Default imports already link this way.In bitwarden, two Storybook files import
{ default as baseComponentDefinition } from './access-selector.stories'. That default export is an anonymous object, so these were failed refs. They now link the module file.No edge is lost. Every edge removed in bulletproof-react is the same source site, re-resolved. Dead-code reports (
buildDeadCodeReport, with and without exported symbols) are identical on bulletproof-react and bitwarden.I surveyed the local validation kits (about 100 JS/TS repos). Project-local
{ default as X }imports appear only in these two repos, plus one string fixture in qwik. Imports from a package, like{ default as dayjs } from 'dayjs', don't change.Tests
__tests__/import-default-as.test.ts:{ default as X }alone;import type { default as X }andimport { type default as X };import A, { default as B };defaultsanddefaultThemestay named;legacy.tsxdeclares its ownAppRootandSettings. It checks:elementandComponent;<Route element>;sidebar={Settings});memocomponent imported under another name;importsedge.Results:
mainat31c3328d, 8 of the new test's 10 cases fail. All 10 pass with the fix. The 2 that pass on main guard behaviour that must not change.npx vitest run --testTimeout=120000) ran on the Windows dev box while another workload held it at 100% CPU: 6345 passed and 18 failed.EBUSYteardown errors, in the daemon, sync, watcher, MCP-lifecycle and viewer-steps suites.beforeAllhooks insyncandui-steps-api-servers.sync.test.tspasses on its own. The other two pass with their limits raised locally; the Caller/impact graph misses methods passed as first-class references (callbacks), e.g. executor.submit(obj.method, …) #1820 case took 115 s under that load.31c3328dwith the fix, the 17 related test files pass serially, 253 tests in all. They cover imports, React Router, CommonJS, Expo modules, JSX-child disambiguation, cross-tier steps, stores, sync import retries and kernel TS/JS parity.tsconfig.The A/B arms were built on
5ace306b. The branch was then rebased onto31c3328d(#2403), which changes Liquid sync and reference tails, not JS/TS resolution.Since then, main has merged #2423 (React Router JSX index routes and layouts) and #2424 (the JSX render pass on Node 22). I re-ran the bulletproof-react A/B on
855eca26and got the same 4 edges re-resolved at the same sites, with nothing else changed. I also re-ranimport-default-as,react-router,react-router-nested-lazy,jsx-render-work,js-local-binding-backtracking,jsx-child-disambiguationandimport-type-modifieron the merged branch: 100 tests pass.Not in this PR
Two separate gaps turned up along the way. I've filed each as its own task.
jsxChildtakes a single same-named candidate even when the file imports the tag from a module that doesn't declare it.jsx-renderedges cross from one app into another. For example, react-vite'sCreateCommentrenders nextjs-app'sButton,TextareaandForm.<Settings />afterimport { default as Settings } from './settings'renders another file'sSettingswhen one exists.import Settings fromdoes the same, so this isn't specific to this fix.const X = require('./x').defaultstill links nothing when./xis a compiled ES module. For example, bitwarden'snew OsBiometricsServiceMac(…)inmain-biometrics.service.ts. Fixing it has to keep a CommonJS module's ownexports.defaultworking.🤖 Generated with Claude Code