Skip to content

Commit 1930683

Browse files
committed
fix(optimize-imports): address review nits — TODO comment, type annotation, test names, wildcard dir test
- Add TODO comment on allResolved bail-out suggesting debug logging - Remove redundant ': string[]' annotation on optimizePackageImports (inferred) - Rename namespace re-export test to use generic X/barrel names instead of Slot/lucide-react - Update syntax-error test to match new empty-map-on-parse-error behavior (not null) - Add test for directory-style 'export * from "./components"' where components/index.js exists
1 parent 9f4ca2a commit 1930683

3 files changed

Lines changed: 58 additions & 15 deletions

File tree

packages/vinext/src/config/next-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,7 @@ export async function resolveNextConfig(
506506

507507
// Resolve optimizePackageImports from experimental config
508508
const rawOptimize = experimental?.optimizePackageImports;
509-
const optimizePackageImports: string[] = Array.isArray(rawOptimize)
509+
const optimizePackageImports = Array.isArray(rawOptimize)
510510
? rawOptimize.filter((x): x is string => typeof x === "string")
511511
: [];
512512

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

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,12 @@ function resolvePackageEntry(
284284
* - `import * as X; export { X }` — indirect namespace re-export
285285
* - `export * from "./sub"` — wildcard: recursively parse sub-module and merge exports
286286
*
287+
* Returns an empty map when the file cannot be read or has a parse error, so that
288+
* recursive wildcard calls degrade gracefully without aborting the whole barrel walk.
289+
*
287290
* @param initialContent - Pre-read file content for `filePath`. If provided, skips the
288-
* `readFile` call for the entry file — useful when the caller already has the content.
291+
* `readFile` call for the entry file — avoids a redundant read when the caller
292+
* already has the content in hand.
289293
*/
290294
function buildExportMapFromFile(
291295
filePath: string,
@@ -377,7 +381,16 @@ function buildExportMapFromFile(
377381
if (rawSource.startsWith(".")) {
378382
const subPath = path.resolve(fileDir, rawSource).split(path.sep).join("/");
379383
// Try with the path as-is first, then with common extensions
380-
const candidates = [subPath, `${subPath}.js`, `${subPath}.mjs`, `${subPath}.ts`];
384+
const candidates = [
385+
subPath,
386+
`${subPath}.js`,
387+
`${subPath}.mjs`,
388+
`${subPath}.ts`,
389+
// Directory-style sub-modules: `export * from "./components"` where
390+
// `components/` is a directory with an index file.
391+
`${subPath}/index.js`,
392+
`${subPath}/index.ts`,
393+
];
381394
for (const candidate of candidates) {
382395
if (readFile(candidate) !== null) {
383396
const subMap = buildExportMapFromFile(candidate, readFile, cache, visited);
@@ -463,16 +476,14 @@ export function buildBarrelExportMap(
463476
const cached = exportMapCache.get(entryPath);
464477
if (cached) return cached;
465478

466-
// Verify the entry file is readable and parseable before delegating to the
467-
// recursive helper. This lets us return null (instead of an empty map) for
468-
// unresolvable or unparseable entries.
479+
// Verify the entry file is readable before delegating to the recursive helper.
480+
// This lets us return null (instead of an empty map) for unresolvable entries,
481+
// giving callers a clear signal that the package barrel could not be analyzed.
482+
// Parse errors in the entry file are handled gracefully by buildExportMapFromFile
483+
// (returns an empty map), which causes the transform to leave all imports unchanged —
484+
// the correct safe fallback.
469485
const content = readFile(entryPath);
470486
if (!content) return null;
471-
try {
472-
parseAst(content);
473-
} catch {
474-
return null;
475-
}
476487

477488
const visited = new Set<string>();
478489
// Pass the already-read content so buildExportMapFromFile skips the redundant
@@ -507,6 +518,9 @@ export function createOptimizeImportsPlugin(
507518
// buildStart so the transform loop doesn't allocate template literals per file.
508519
let quotedPackages: string[] = [];
509520

521+
// `satisfies Plugin` gives a structural type-check at the object literal in addition
522+
// to the `: Plugin` return type annotation on the function, catching hook name typos
523+
// or shape mismatches that the return-type check alone would accept silently.
510524
return {
511525
name: "vinext:optimize-imports",
512526
// No enforce — runs after JSX transform so parseAst gets plain JS.
@@ -669,6 +683,7 @@ export function createOptimizeImportsPlugin(
669683
if (!allResolved) break;
670684
}
671685

686+
// TODO: consider debug logging which specifier was unresolved
672687
if (!allResolved || specifiers.length === 0) continue;
673688

674689
// Group specifiers by their resolved source module
@@ -758,5 +773,5 @@ export function createOptimizeImportsPlugin(
758773
};
759774
},
760775
},
761-
};
776+
} satisfies Plugin;
762777
}

tests/optimize-imports.test.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,14 +221,18 @@ export { format };`;
221221
expect(map).toBeNull();
222222
});
223223

224-
it("returns null when entry file has syntax errors", () => {
224+
it("returns an empty map when entry file has syntax errors", () => {
225+
// Parse errors produce an empty map (safe fallback — leaves the import
226+
// unchanged in the transform), not null. Null is only returned when the
227+
// entry path itself cannot be resolved or the file cannot be read.
225228
const entryPath = uniquePath("syntax-error");
226229
const map = buildBarrelExportMap(
227230
"test-pkg",
228231
() => entryPath,
229232
() => "export { unclosed",
230233
);
231-
expect(map).toBeNull();
234+
expect(map).not.toBeNull();
235+
expect(map!.size).toBe(0);
232236
});
233237

234238
it("resolves wildcard export * from './sub' by merging sub-module exports", () => {
@@ -352,6 +356,30 @@ export { Button } from "./button";`;
352356
expect(buttonEntry!.source).toBe("/fake/nested/components/Button");
353357
expect(buttonEntry!.source).not.toBe("/fake/nested/Button");
354358
});
359+
360+
it("resolves wildcard export * from './components' where components/ is a directory with index.js", () => {
361+
// Tests the `/index.js` candidate added to the wildcard resolution path.
362+
// `export * from "./components"` with no extension — the resolver must
363+
// try `components/index.js` when `components.js` does not exist.
364+
const entryPath = "/fake/dir-wildcard/index.js";
365+
const files: Record<string, string> = {
366+
[entryPath]: `export * from "./components";`,
367+
"/fake/dir-wildcard/components/index.js": `export { Button } from "./Button";`,
368+
"/fake/dir-wildcard/components/Button.js": `export function Button() {}`,
369+
};
370+
371+
const map = buildBarrelExportMap(
372+
"test-pkg",
373+
() => entryPath,
374+
(fp) => files[fp] ?? null,
375+
);
376+
377+
expect(map).not.toBeNull();
378+
const buttonEntry = map!.get("Button");
379+
expect(buttonEntry).toBeDefined();
380+
// Must resolve through the directory index, relative to components/
381+
expect(buttonEntry!.source).toBe("/fake/dir-wildcard/components/Button");
382+
});
355383
});
356384

357385
// ── Plugin transform with real FS fixture ─────────────────────
@@ -408,7 +436,7 @@ describe("vinext:optimize-imports transform", () => {
408436
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
409437
});
410438

411-
it("rewrites namespace re-export: import { Slot } from 'pkg' → import * as Slot from 'sub-pkg'", async () => {
439+
it("rewrites namespace re-export: import { X } from 'barrel' → import * as X from 'sub-pkg'", async () => {
412440
// lucide-react is in DEFAULT_OPTIMIZE_PACKAGES. The barrel contents below use
413441
// @radix-ui sub-packages intentionally — this tests the namespace rewrite path
414442
// with an arbitrary barrel; the package name just needs to be in the optimized list.

0 commit comments

Comments
 (0)