Repository navigation
fix(js,ts): require('./x').default reaches an ES module's default export - #2433
Merged
Merged
Conversation
…xport
`const X = require('./x').default` and `const { default: X } = require('./x')`
map to a named import of `default`. That is right for a CommonJS module that
sets the property itself (`exports.default = fn`, or the dual
`module.exports = X; module.exports.default = X`), but a module written as an
ES module and compiled to CommonJS sets it to its default export, which is no
named export. References through the binding stayed unresolved (bitwarden's
`new OsBiometricsServiceMac(...)`) or fell to name matching: the local
variable holding the module, or a namesake method in another package.
findExportedSymbolWalk now falls back to the module's ESM default export when
the named lookup finds no `default`. Only for JS-family refs, only in the
module the require names (`export * from` forwards no default), and only when
that module has an ESM default export at all: a single-file component, or an
`export default` statement. A CommonJS module's `exports.x = function`
declarations are flagged exported, so without that gate the
first-exported-function guess would invent a default for it.
bitwarden +2 edges (the two failed `instantiates` refs). react-native +872
-617: all 617 re-resolved at the same site (541 from the local require
binding to the real function or class, 47 from a namesake method in another
package to the right module, 28 metadata only, 1 from a type signature to the
implementation), 870 of 872 added edges on the declared default or what it
holds. express, eslint, mocha, koa, body-parser, fastify, topcoder,
proshop_mern, next-saas-starter and create-t3-turbo byte-identical. Dead-code
claims unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Summary
extractJSImportsmapsconst X = require('./x').default(andconst { default: X } = require('./x')) as{ exportedName: 'default', isDefault: false }, sofindExportedSymbollooks only for a named export calleddefault. That is right for a CommonJS module that sets the property itself. A module written as an ES module (export default class Foo) has no nameddefault, though, so every reference through the binding stayed unresolved or fell to name matching.bitwarden's desktop app is the reported case.
main-biometrics.service.ts:51-52andos-biometrics-mac.service.spec.ts:105loadexport default class OsBiometricsServiceMacwithrequire("./os-biometrics-mac.service").defaultandnewit, and bothinstantiatesrefs stayedfailed.#2412 fixed the ESM sibling (
import { default as X }) by mapping it as the default import. The require form can't be mapped that way, because.defaultthere is a property ofmodule.exports:exports.default = fn,module.exports.default = X);module.exports = X; module.exports.default = Xsets it to X.Change
The named property is still looked up first. When
findExportedSymbolWalkfinds no nameddefault, it falls back to the module's ESM default export, under three gates:ESM_IMPORT_LANGUAGES). Python'sfrom .mod import defaultis untouched.depth === 0).export * fromnever forwards a default, so the walk through a wildcard re-export doesn't take one.export defaultstatement. A CommonJS module's anonymousexports.x = function () {}is flagged exported, so without this gate the first-exported-function guess would invent a default for a module that has none. That is the expressnormalizeTypebug class from fix(js): resolve calls through CommonJS require bindings #2189.The ESM default lookup itself (component, then the
export default NAMEbinding, then the first exported function or class) is factored intoesmDefaultExportand shared with theisDefaultbranch, sorequire('./x').defaultandimport X from './x'land on the same declaration.layoutComponent(frameworks/react.ts) triescontext.resolveImportfirst. Arequire(…).defaultlayout now resolves there, to the declaration its module read (lazyRouteComponent) found before. ItsisDefault || exportedName === 'default'branch stays as the fallback for when import resolution finds nothing. After #2412, that branch's second half is reached only by the require forms.Other readers of
isDefaultare unchanged. jsx-render's zero-candidate fallback, expo-modules, tier-synthesizer and react-router-synthesizer read the mapping flag, notfindExportedSymbol. topcoder's tworequire('../shared').defaultsites feed only JSX tags (<Application />), which go through jsx-render by name, so that repo is byte-identical.Validation
Fresh
codegraph init -yindexes,scripts/dump-graph.mjsbefore/after, native kernel staged in both arms. The arms are main 19f91e2 and this branch's fix commit, and onlydist/resolution/import-resolver.jsdiffers between them. The main commits merged in since (#2413, #2416, #2417, #2419, #2425, #2426) don't touchimport-resolver.ts, and their resolver hunks are gated to Go, C++ and sync-time navigation.failedrefs nowinstantiatesOsBiometricsServiceMacviaimport{ default: fastify } = require('../../fastify'))defaultalready foundfastifyReact Native's
Librarieshave 238require(…).defaultloads:const X = require(…).defaultbinding to the real function or class;JSTimers.setInterval→ an rn-tester test's method,Animated.delay→IntersectionObserver.delay,Keyboard.dismiss→ a dev-server handler) onto the right module. That is the method itself forKeyboard.dismiss. ForJSTimersandAnimatedit is the exported value, whose members are chosen at run time;importmetadata;log(…)signature in a Flow type to thelogimplementation.export defaultstatement:ExceptionsManager.handleException) or on methods of the class anewinstance has (Keyboard.dismiss);BatchedBridge.registerCallableModule/registerLazyCallableModule) land on the right methods but in the deprecatedtypes_DEPRECATED/modules/BatchedBridge.d.tsdeclare class MessageQueuerather thanMessageQueue.js. That is the existing instance-member type lookup, whichimport BatchedBridge fromgets too.unresolved_refs.Dead-code reports (bitwarden, react-native): no claims lost or gained.
Known limitations, both shared with
import X fromand filed as follow-upsexport default class Foo/export default function foo. So a module that exports a helper above its default class resolves to the helper, for an ESM default import today and now forrequire(…).defaulttoo. Repro on main:export function helper()aboveexport default class Foo, andimport Foo from './x'; new Foo()instantiateshelper. A scan found the shape in 7 react-native, 4 topcoder and 1 bitwarden file. None of them is arequire(…).defaulttarget in this corpus, so no edge above depends on it. The fix (reading the declaration forms indefaultExportBinding) changes ESM default imports everywhere and needs its own validation.export default,codegraph syncleaves the waiting reffailedwhile a fresh index links it. The failed-ref retry keys on the names the changed files declare, and a default binding's local name (X) is not the declaration's (Foo).import Y from './x'behaves the same way on main.Test plan
__tests__/require-default.test.ts(new, 8 tests):.defaultand{ default: X }(bitwarden's shape), and a Svelte component, are red on main and green with the fix;exports.default = fnbeside an anonymousexports.pad = function(the first-export guess would pickpad), the dual export, a CommonJS module with nodefault,export * from, and Pythonfrom .mod import defaultare green on both.export *test; dropping theexport defaultgate fails the no-defaultCommonJS test and the require-binding summary. Dropping the language gate changes nothing, because the target-file check already excludes Python; the Python test pins the behavior.tsc --noEmit,npm run build, kernel rebuilt after merging mainEBUSYtemp-dir cleanups and 2 daemon assertions (mcp-daemon,mcp-writer-lock).function-refCaller/impact graph misses methods passed as first-class references (callbacks), e.g. executor.submit(obj.method, …) #1820 "a module global never binds through a shadow". It hits its own 60 s in-file limit even alone, and passes with the limit raised (219 s on this box).defaultlookup, and none of their fixtures has arequire(…).default.🤖 Generated with Claude Code