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
349 changes: 312 additions & 37 deletions packages/vinext/src/plugins/optimize-imports.ts

Large diffs are not rendered by default.

188 changes: 188 additions & 0 deletions tests/optimize-imports-build.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { mkdtemp, rm } from "node:fs/promises";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { createBuilder } from "vite";
import { describe, expect, it } from "vite-plus/test";
import vinext from "../packages/vinext/src/index.js";

function symlinkWorkspacePackage(root: string, packageName: string) {
const source = path.resolve(import.meta.dirname, "../node_modules", packageName);
const target = path.join(root, "node_modules", packageName);
fs.mkdirSync(path.dirname(target), { recursive: true });
fs.symlinkSync(source, target, "junction");
}

async function withTempDir<T>(prefix: string, run: (tmpDir: string) => Promise<T>): Promise<T> {
const tmpDir = await mkdtemp(path.join(os.tmpdir(), prefix));
fs.mkdirSync(path.join(tmpDir, "node_modules"), { recursive: true });
symlinkWorkspacePackage(tmpDir, "react");
symlinkWorkspacePackage(tmpDir, "react-dom");
symlinkWorkspacePackage(tmpDir, "react-server-dom-webpack");
try {
return await run(tmpDir);
} finally {
await rm(tmpDir, { recursive: true, force: true });
}
}

function writeFixtureFile(root: string, filePath: string, content: string) {
const absPath = path.join(root, filePath);
fs.mkdirSync(path.dirname(absPath), { recursive: true });
fs.writeFileSync(absPath, content);
}

function readTextFilesRecursive(root: string): string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: readTextFilesRecursive follows symlinks (the react/react-dom symlinks created by symlinkWorkspacePackage) and would recursively read all .js files under the real react package if they end up in the dist/ tree. In practice this is fine because the build output won't contain symlinks to workspace packages in the output dir, but if the build ever emits a symlink (e.g., Vite preserves symlinks in some configurations), this could produce unexpectedly large buildOutput strings and slow assertions.

Not blocking — the current build test works correctly.

let output = "";
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
const entryPath = path.join(root, entry.name);
if (entry.isDirectory()) {
output += readTextFilesRecursive(entryPath);
continue;
}
if (!entry.name.endsWith(".js")) continue;
output += fs.readFileSync(entryPath, "utf-8");
}
return output;
}

async function buildApp(root: string) {
const rscOutDir = path.join(root, "dist", "server");
const ssrOutDir = path.join(root, "dist", "server", "ssr");
const clientOutDir = path.join(root, "dist", "client");

const builder = await createBuilder({
root,
configFile: false,
plugins: [vinext({ appDir: root, rscOutDir, ssrOutDir, clientOutDir })],
logLevel: "silent",
});

await builder.buildApp();
}

describe("optimizePackageImports production builds", () => {
it("builds an App Router app when an optimized antd barrel resolves through a use-client export-star boundary", async () => {
// issue-845 repro scaffold and package pins are recorded in:
// .sisyphus/evidence/task-1-parity-matrix.md
await withTempDir("vinext-optimize-imports-build-", async (root) => {
writeFixtureFile(
root,
"package.json",
JSON.stringify(
{ name: "vinext-optimize-imports-build", private: true, type: "module" },
null,
2,
),
);
writeFixtureFile(
root,
"tsconfig.json",
JSON.stringify(
{
compilerOptions: {
target: "ES2022",
module: "ESNext",
moduleResolution: "bundler",
jsx: "react-jsx",
strict: true,
skipLibCheck: true,
types: ["vite/client", "@vitejs/plugin-rsc/types"],
},
include: ["app", "*.ts", "*.tsx"],
},
null,
2,
),
);

writeFixtureFile(
root,
"app/layout.tsx",
`import type { ReactNode } from "react";

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
`,
);
writeFixtureFile(
root,
"app/page.tsx",
`import AntdDemo from "./components/AntdDemo";

export default function HomePage() {
return <AntdDemo />;
}
`,
);
writeFixtureFile(
root,
"app/components/AntdDemo.tsx",
`"use client";

import { Button } from "antd";

export default function AntdDemo() {
return <Button />;
}
`,
);

writeFixtureFile(
root,
"node_modules/antd/package.json",
JSON.stringify(
{
name: "antd",
version: "6.3.5",
type: "module",
main: "./index.js",
},
null,
2,
),
);
writeFixtureFile(
root,
"node_modules/antd/index.js",
`export { Button } from "./es/button/index.js";
`,
);
writeFixtureFile(
root,
"node_modules/antd/es/button/index.js",
`"use client";

export * from "./button.js";
export { default } from "./button.js";
`,
);
writeFixtureFile(
root,
"node_modules/antd/es/button/button.js",
`export function Button() {
return null;
}

export default Button;
`,
);

await buildApp(root);

expect(fs.existsSync(path.join(root, "dist", "server", "index.js"))).toBe(true);
Comment thread
llc1123 marked this conversation as resolved.
expect(fs.existsSync(path.join(root, "dist", "server", "ssr", "index.js"))).toBe(true);
expect(fs.existsSync(path.join(root, "dist", "client"))).toBe(true);

const buildOutput = readTextFilesRecursive(path.join(root, "dist"));
expect(buildOutput).not.toContain(`from "antd"`);
expect(buildOutput).not.toContain("/es/button/index.js");
expect(buildOutput).toContain("function Button");
});
}, 60_000);
});
56 changes: 56 additions & 0 deletions tests/optimize-imports-export-map-bindings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, it, expect } from "vite-plus/test";
import { buildBarrelExportMap } from "../packages/vinext/src/plugins/optimize-imports.js";

let testId = 0;
function uniquePath(name: string): string {
return `/fake/${name}-${++testId}/entry.js`;
}

describe("buildBarrelExportMap binding re-export cases", () => {
it("handles import * as X; export { X }", async () => {
const entryPath = uniquePath("import-ns-reexport");
const barrelCode = `import * as AlertDialog from "@radix-ui/react-alert-dialog";\nexport { AlertDialog };`;
const map = await buildBarrelExportMap(
"test-pkg",
() => entryPath,
() => Promise.resolve(barrelCode),
);
expect(map).not.toBeNull();
expect(map!.get("AlertDialog")).toEqual({
source: "@radix-ui/react-alert-dialog",
isNamespace: true,
});
});

it("handles import { X }; export { X }", async () => {
const entryPath = uniquePath("import-named-reexport");
const barrelCode = `import { format } from "date-fns/format";\nexport { format };`;
const map = await buildBarrelExportMap(
"test-pkg",
() => entryPath,
() => Promise.resolve(barrelCode),
);
expect(map).not.toBeNull();
expect(map!.get("format")).toEqual({
source: "date-fns/format",
isNamespace: false,
originalName: "format",
});
});

it("handles export { Local as Public } for same-file declarations", async () => {
const entryPath = uniquePath("local-alias-reexport");
const barrelCode = `const Mo = {};\nexport { Mo as Listbox };`;
const map = await buildBarrelExportMap(
"test-pkg",
() => entryPath,
() => Promise.resolve(barrelCode),
);
expect(map).not.toBeNull();
expect(map!.get("Listbox")).toEqual({
source: entryPath,
isNamespace: false,
originalName: "Listbox",
});
});
});
48 changes: 48 additions & 0 deletions tests/optimize-imports-export-map-failures.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, it, expect } from "vite-plus/test";
import {
buildBarrelExportMap,
DEFAULT_OPTIMIZE_PACKAGES,
} from "../packages/vinext/src/plugins/optimize-imports.js";

let testId = 0;
function uniquePath(name: string): string {
return `/fake/${name}-${++testId}/entry.js`;
}

describe("buildBarrelExportMap failure and baseline cases", () => {
it("returns null when entry cannot be resolved", async () => {
const map = await buildBarrelExportMap(
"nonexistent-pkg",
() => null,
() => Promise.resolve(null),
);
expect(map).toBeNull();
});

it("returns null when entry file cannot be read", async () => {
const entryPath = uniquePath("unreadable");
const map = await buildBarrelExportMap(
"test-pkg",
() => entryPath,
() => Promise.resolve(null),
);
expect(map).toBeNull();
});

it("returns an empty map when entry file has syntax errors", async () => {
const entryPath = uniquePath("syntax-error");
const map = await buildBarrelExportMap(
"test-pkg",
() => entryPath,
() => Promise.resolve("export { unclosed"),
);
expect(map).not.toBeNull();
expect(map!.size).toBe(0);
});

it("DEFAULT_OPTIMIZE_PACKAGES includes expected packages", () => {
expect(DEFAULT_OPTIMIZE_PACKAGES).toContain("lucide-react");
expect(DEFAULT_OPTIMIZE_PACKAGES).toContain("radix-ui");
expect(DEFAULT_OPTIMIZE_PACKAGES).toContain("antd");
});
});
Loading
Loading