@@ -380,20 +380,32 @@ function buildExportMapFromFile(
380380 // export * from "./sub" — wildcard: recursively merge sub-module exports
381381 if ( rawSource . startsWith ( "." ) ) {
382382 const subPath = path . resolve ( fileDir , rawSource ) . split ( path . sep ) . join ( "/" ) ;
383- // Try with the path as-is first, then with common extensions
383+ // Try with the path as-is first, then with common extensions.
384+ // Include .tsx for TypeScript-first internal libraries, and /index.mjs
385+ // for ESM-first packages where the directory index is a .mjs file.
384386 const candidates = [
385387 subPath ,
386388 `${ subPath } .js` ,
387389 `${ subPath } .mjs` ,
388390 `${ subPath } .ts` ,
391+ `${ subPath } .tsx` ,
389392 // Directory-style sub-modules: `export * from "./components"` where
390393 // `components/` is a directory with an index file.
391394 `${ subPath } /index.js` ,
395+ `${ subPath } /index.mjs` ,
392396 `${ subPath } /index.ts` ,
397+ `${ subPath } /index.tsx` ,
393398 ] ;
394399 for ( const candidate of candidates ) {
395- if ( readFile ( candidate ) !== null ) {
396- const subMap = buildExportMapFromFile ( candidate , readFile , cache , visited ) ;
400+ const candidateContent = readFile ( candidate ) ;
401+ if ( candidateContent !== null ) {
402+ const subMap = buildExportMapFromFile (
403+ candidate ,
404+ readFile ,
405+ cache ,
406+ visited ,
407+ candidateContent ,
408+ ) ;
397409 for ( const [ name , entry ] of subMap ) {
398410 if ( ! exportMap . has ( name ) ) {
399411 exportMap . set ( name , entry ) ;
@@ -517,6 +529,10 @@ export function createOptimizeImportsPlugin(
517529 // Pre-built quoted forms used for the per-file quick-check. Computed once in
518530 // buildStart so the transform loop doesn't allocate template literals per file.
519531 let quotedPackages : string [ ] = [ ] ;
532+ // Tracks barrel entries whose sub-package origins have already been registered,
533+ // so repeated imports of the same barrel (across many files) don't redundantly
534+ // iterate the full export map.
535+ const registeredBarrels = new Set < string > ( ) ;
520536
521537 // `satisfies Plugin` gives a structural type-check at the object literal in addition
522538 // to the `: Plugin` return type annotation on the function, catching hook name typos
@@ -543,6 +559,7 @@ export function createOptimizeImportsPlugin(
543559 entryPathCache . clear ( ) ;
544560 barrelCaches . exportMapCache . clear ( ) ;
545561 barrelCaches . subpkgOrigin . clear ( ) ;
562+ registeredBarrels . clear ( ) ;
546563 } ,
547564
548565 async resolveId ( source ) {
@@ -599,6 +616,16 @@ export function createOptimizeImportsPlugin(
599616 const s = new MagicString ( code ) ;
600617 let hasChanges = false ;
601618 const root = getRoot ( ) ;
619+ // Hoist the readFile closure so a single function is reused across all
620+ // import declarations in the file, rather than allocating an identical
621+ // closure for each barrel import encountered.
622+ const readFileSafe = ( filepath : string ) : string | null => {
623+ try {
624+ return fs . readFileSync ( filepath , "utf-8" ) ;
625+ } catch {
626+ return null ;
627+ }
628+ } ;
602629
603630 for ( const node of ast . body as AstBodyNode [ ] ) {
604631 if ( node . type !== "ImportDeclaration" ) continue ;
@@ -619,14 +646,10 @@ export function createOptimizeImportsPlugin(
619646 }
620647 const exportMap = buildBarrelExportMap (
621648 importSource ,
649+ // Entry already resolved above via entryPathCache; the callback is a
650+ // no-op resolver that simply returns the pre-resolved barrelEntry.
622651 ( ) => barrelEntry ,
623- ( filepath ) => {
624- try {
625- return fs . readFileSync ( filepath , "utf-8" ) ;
626- } catch {
627- return null ;
628- }
629- } ,
652+ readFileSafe ,
630653 barrelCaches . exportMapCache ,
631654 ) ;
632655 if ( ! exportMap || ! barrelEntry ) continue ;
@@ -635,20 +658,25 @@ export function createOptimizeImportsPlugin(
635658 // the barrel's context (needed for pnpm strict hoisting).
636659 // Only bare specifiers (npm packages) need this — absolute paths are
637660 // already fully resolved and don't require context-aware resolution.
638- for ( const entry of exportMap . values ( ) ) {
639- if (
640- ! entry . source . startsWith ( "/" ) &&
641- ! entry . source . startsWith ( "." ) &&
642- ! barrelCaches . subpkgOrigin . has ( entry . source )
643- ) {
644- // First barrel to register this specifier wins. This is safe because the
645- // sub-package specifier (e.g. "@radix-ui/react-slot") resolves to the same
646- // path regardless of which barrel we resolve from — only the importer context
647- // differs, not the target. In pnpm strict mode there is exactly one copy of
648- // each sub-package, so any barrel's context reaches the same file.
649- // (In a monorepo with nested node_modules, two barrels could in theory see
650- // different versions; that edge case is out of scope for the default package list.)
651- barrelCaches . subpkgOrigin . set ( entry . source , barrelEntry ) ;
661+ // Gate with registeredBarrels so files that all import from the same
662+ // barrel don't each re-iterate the full export map.
663+ if ( ! registeredBarrels . has ( barrelEntry ) ) {
664+ registeredBarrels . add ( barrelEntry ) ;
665+ for ( const entry of exportMap . values ( ) ) {
666+ if (
667+ ! entry . source . startsWith ( "/" ) &&
668+ ! entry . source . startsWith ( "." ) &&
669+ ! barrelCaches . subpkgOrigin . has ( entry . source )
670+ ) {
671+ // First barrel to register this specifier wins. This is safe because the
672+ // sub-package specifier (e.g. "@radix-ui/react-slot") resolves to the same
673+ // path regardless of which barrel we resolve from — only the importer context
674+ // differs, not the target. In pnpm strict mode there is exactly one copy of
675+ // each sub-package, so any barrel's context reaches the same file.
676+ // (In a monorepo with nested node_modules, two barrels could in theory see
677+ // different versions; that edge case is out of scope for the default package list.)
678+ barrelCaches . subpkgOrigin . set ( entry . source , barrelEntry ) ;
679+ }
652680 }
653681 }
654682
0 commit comments