Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 19 additions & 16 deletions packages/vinext/src/build/client-build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,25 @@ function getPackageName(id: string): string | null {
*/
export function createClientManualChunks(shimsDir: string, preserveRouteBoundaries = false) {
return function clientManualChunks(id: string): string | undefined {
// Check the shims prefix before the node_modules branch: for an installed
// copy the shims live under <app>/node_modules/vinext/dist/shims/, so the
// node_modules early return would otherwise swallow them and the "vinext"
// chunk would never form, leaving the shims to graph-based splitting that
// can place them in a static import cycle with the browser entry chunk.
//
// `shimsDir` is slash-normalized with a trailing slash; the bundler-provided
// id can carry native backslashes on Windows, so slash it before matching.
const slashedId = toSlash(id);
if (slashedId.startsWith(shimsDir)) {
if (preserveRouteBoundaries) {
const relativeId = slashedId.slice(shimsDir.length).split("?", 1)[0] ?? "";
const extensionIndex = relativeId.lastIndexOf(".");
const shimName = extensionIndex === -1 ? relativeId : relativeId.slice(0, extensionIndex);
if (ROUTE_OWNED_CLIENT_SHIMS.has(shimName)) return undefined;
}
return "vinext";
}

// React framework — always loaded, shared across all pages.
// Isolating React into its own chunk is the single highest-value
// split: it's ~130KB compressed, loaded on every page, and its
Expand All @@ -129,9 +148,6 @@ export function createClientManualChunks(shimsDir: string, preserveRouteBoundari
// example that does). Split those entrypoints into their own chunk
// so the server renderer loads lazily, only on routes that use it,
// instead of weighing down first paint on every page.
// Windows ids carry backslashes, so slash-normalize before matching
// the "react-dom/" separator (same convention as getPackageName).
const slashedId = toSlash(id);
const sub = slashedId.slice(slashedId.lastIndexOf("react-dom/") + "react-dom/".length);
if (
sub.startsWith("server.") ||
Expand All @@ -152,19 +168,6 @@ export function createClientManualChunks(shimsDir: string, preserveRouteBoundari
return undefined;
}

// `shimsDir` is slash-normalized with a trailing slash; the bundler-provided
// id can carry native backslashes on Windows, so slash it before matching.
const slashedId = toSlash(id);
if (slashedId.startsWith(shimsDir)) {
if (preserveRouteBoundaries) {
const relativeId = slashedId.slice(shimsDir.length).split("?", 1)[0] ?? "";
const extensionIndex = relativeId.lastIndexOf(".");
const shimName = extensionIndex === -1 ? relativeId : relativeId.slice(0, extensionIndex);
if (ROUTE_OWNED_CLIENT_SHIMS.has(shimName)) return undefined;
}
return "vinext";
}

return undefined;
};
}
Expand Down
37 changes: 37 additions & 0 deletions tests/build-optimization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,43 @@ describe("clientManualChunks", () => {
});
});

describe("createClientManualChunks (installed layout)", () => {
// The shimsDir MUST contain node_modules — that's the regression: an installed
// copy's shims were swallowed by the node_modules early return.
const installedShimsDir = "/app/node_modules/vinext/dist/shims/";

it("groups shims under node_modules into the vinext chunk", () => {
const chunks = createClientManualChunks(installedShimsDir);
expect(chunks("/app/node_modules/vinext/dist/shims/slot.js")).toBe("vinext");
expect(chunks("/app/node_modules/vinext/dist/shims/navigation-context-state.js")).toBe(
"vinext",
);
expect(chunks("/app/node_modules/vinext/dist/shims/slot.js?v=abc")).toBe("vinext");
});

it("still excludes route-owned shims when preserving route boundaries", () => {
const chunks = createClientManualChunks(installedShimsDir, true);
expect(chunks("/app/node_modules/vinext/dist/shims/link.js")).toBeUndefined();
expect(
chunks("/app/node_modules/vinext/dist/shims/internal/hybrid-client-route-owner.js"),
).toBeUndefined();
expect(chunks("/app/node_modules/vinext/dist/shims/slot.js")).toBe("vinext");
});

it("leaves framework and vendor grouping untouched", () => {
const chunks = createClientManualChunks(installedShimsDir);
expect(chunks("/app/node_modules/react/index.js")).toBe("framework");
expect(chunks("/app/node_modules/scheduler/index.js")).toBe("framework");
expect(chunks("/app/node_modules/react-dom/client.js")).toBe("framework");
expect(chunks("/app/node_modules/react-dom/server.browser.js")).toBe("react-dom-server");
expect(chunks("/app/node_modules/.pnpm/react@19.2.8/node_modules/react/index.js")).toBe(
"framework",
);
expect(chunks("/app/node_modules/lodash/map.js")).toBeUndefined();
expect(chunks("/app/src/components/Button.tsx")).toBeUndefined();
});
});

// ─── optimizeDeps.exclude — prevents esbuild scanning virtual module imports ─

describe("optimizeDeps.exclude for vinext", () => {
Expand Down
Loading