Skip to content

Commit 927fb04

Browse files
committed
fix(optimize-imports): address review nits — wildcard extensions, double-read, readFile hoisting, subpkg registration guard
- Add /index.mjs, /index.tsx, and .tsx to wildcard candidate list for ESM-first and TypeScript-first packages - Pass candidateContent to buildExportMapFromFile in wildcard loop to avoid reading each sub-module file twice - Add comment clarifying the no-op resolveEntry callback pattern - Hoist readFileSafe closure outside the import-declaration for-loop (one allocation per transform, not per barrel import) - Gate subpkgOrigin registration loop with registeredBarrels Set so 50 files importing the same barrel don't each iterate the full export map
1 parent 1930683 commit 927fb04

1 file changed

Lines changed: 52 additions & 24 deletions

File tree

packages/vinext/src/plugins/optimize-imports.ts

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)