cloudflare/vinext

Public

mirrored from https://github.com/cloudflare/vinextAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.0.9

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

benchmarks/generate-app.mjs

493lines · modecode

1#!/usr/bin/env node
2/**
3 * Generate the shared 33-route benchmark app.
4 * Shared between the Next.js and vinext benchmark projects.
5 */
6import { mkdirSync, writeFileSync, rmSync } from "node:fs";
7import { join, dirname } from "node:path";
8
9// Seeded PRNG (mulberry32) for deterministic source generation across runs.
10function mulberry32(seed) {
11 let s = seed | 0;
12 return () => {
13 s = (s + 0x6d2b79f5) | 0;
14 let t = Math.imul(s ^ (s >>> 15), 1 | s);
15 t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
16 return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
17 };
18}
19const random = mulberry32(42);
20
21const APP = join(dirname(new URL(import.meta.url).pathname), "app");
22
23// Clean and recreate
24rmSync(APP, { recursive: true, force: true });
25
26function write(rel, content) {
27 const p = join(APP, rel);
28 mkdirSync(dirname(p), { recursive: true });
29 writeFileSync(p, content.trimStart() + "\n");
30}
31
32// ─── Root layout ───────────────────────────────────────────────────────────────
33write("layout.tsx", `
34// Force all pages to be dynamically rendered (no static pre-rendering).
35// Without this, Next.js detects most pages as static and pre-renders them at
36// build time — work that vinext doesn't do. This benchmark is designed to
37// compare build/compilation speed, not static generation, so we opt out of
38// pre-rendering to keep the comparison apples-to-apples.
39export const dynamic = "force-dynamic";
40
41export const metadata = {
42 title: { default: "Benchmark App", template: "%s | Benchmark" },
43 description: "A realistic benchmark app for comparing Next.js and vinext",
44};
45
46export default function RootLayout({ children }: { children: React.ReactNode }) {
47 return (
48 <html lang="en">
49 <body>
50 <nav style={{ padding: "1rem", borderBottom: "1px solid #eee", display: "flex", gap: "1rem" }}>
51 <a href="/">Home</a>
52 <a href="/products">Products</a>
53 <a href="/blog">Blog</a>
54 <a href="/dashboard">Dashboard</a>
55 <a href="/about">About</a>
56 <a href="/docs">Docs</a>
57 <a href="/settings">Settings</a>
58 </nav>
59 <main style={{ padding: "1rem" }}>{children}</main>
60 </body>
61 </html>
62 );
63}
64`);
65
66// ─── Home page ─────────────────────────────────────────────────────────────────
67write("page.tsx", `
68export default function Home() {
69 const now = new Date().toISOString();
70 return (
71 <div>
72 <h1>Benchmark App</h1>
73 <p>Server-rendered at {now}</p>
74 <p>This is a realistic benchmark app with 33 routes, nested layouts, dynamic routes, and client components.</p>
75 </div>
76 );
77}
78`);
79
80// ─── Shared client components ──────────────────────────────────────────────────
81write("_components/counter.tsx", `
82"use client";
83import { useState } from "react";
84
85export function Counter({ label = "Count" }: { label?: string }) {
86 const [count, setCount] = useState(0);
87 return (
88 <div style={{ padding: "0.5rem", border: "1px solid #ddd", borderRadius: "4px", display: "inline-block" }}>
89 <span>{label}: {count}</span>
90 <button onClick={() => setCount(c => c + 1)} style={{ marginLeft: "0.5rem" }}>+</button>
91 </div>
92 );
93}
94`);
95
96write("_components/timer.tsx", `
97"use client";
98import { useState, useEffect } from "react";
99
100export function Timer() {
101 const [elapsed, setElapsed] = useState(0);
102 useEffect(() => {
103 const id = setInterval(() => setElapsed(e => e + 1), 1000);
104 return () => clearInterval(id);
105 }, []);
106 return <span>Uptime: {elapsed}s</span>;
107}
108`);
109
110write("_components/search.tsx", `
111"use client";
112import { useState } from "react";
113
114export function Search({ placeholder = "Search..." }: { placeholder?: string }) {
115 const [query, setQuery] = useState("");
116 return (
117 <div style={{ marginBottom: "1rem" }}>
118 <input
119 type="text"
120 value={query}
121 onChange={e => setQuery(e.target.value)}
122 placeholder={placeholder}
123 style={{ padding: "0.5rem", border: "1px solid #ddd", borderRadius: "4px", width: "300px" }}
124 />
125 {query && <p style={{ fontSize: "0.8rem", color: "#666" }}>Searching for: {query}</p>}
126 </div>
127 );
128}
129`);
130
131// ─── About ─────────────────────────────────────────────────────────────────────
132write("about/page.tsx", `
133export const metadata = { title: "About" };
134export default function AboutPage() {
135 return (
136 <div>
137 <h1>About</h1>
138 <p>This is a benchmark application for comparing Next.js and vinext (Vite) performance.</p>
139 <p>It includes 33 routes with nested layouts, dynamic routes, server components, client components, and metadata.</p>
140 </div>
141 );
142}
143`);
144
145// ─── Products section (dynamic + layout) ───────────────────────────────────────
146write("products/layout.tsx", `
147export default function ProductsLayout({ children }: { children: React.ReactNode }) {
148 return (
149 <div>
150 <div style={{ padding: "0.5rem", background: "#f0f0f0", marginBottom: "1rem" }}>
151 <strong>Products</strong> — <a href="/products">All</a>
152 </div>
153 {children}
154 </div>
155 );
156}
157`);
158
159// Pre-compute product prices at generation time with seeded PRNG for reproducibility.
160const productPrices = Array.from({ length: 20 }, () => (Math.round(random() * 10000) / 100).toFixed(2));
161
162write("products/page.tsx", `
163import Link from "next/link";
164export const metadata = { title: "Products" };
165
166const products = [
167${productPrices.map((price, i) => ` { id: ${i + 1}, name: "Product ${i + 1}", price: ${price} },`).join("\n")}
168];
169
170export default function ProductsPage() {
171 return (
172 <div>
173 <h1>Products</h1>
174 <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: "1rem" }}>
175 {products.map(p => (
176 <Link key={p.id} href={\`/products/\${p.id}\`} style={{ padding: "1rem", border: "1px solid #ddd", textDecoration: "none", color: "inherit" }}>
177 <h3>{p.name}</h3>
178 <p>\${p.price}</p>
179 </Link>
180 ))}
181 </div>
182 </div>
183 );
184}
185`);
186
187write("products/[id]/page.tsx", `
188export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
189 const { id } = await params;
190 return { title: \`Product \${id}\` };
191}
192export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
193 const { id } = await params;
194 return (
195 <div>
196 <h1>Product {id}</h1>
197 <p>This is the detail page for product {id}. Rendered at {new Date().toISOString()}</p>
198 </div>
199 );
200}
201`);
202
203// ─── Blog section (25 posts) ───────────────────────────────────────────────────
204write("blog/layout.tsx", `
205import { Search } from "../_components/search";
206export default function BlogLayout({ children }: { children: React.ReactNode }) {
207 return (
208 <div>
209 <div style={{ padding: "0.5rem", background: "#f5f5ff", marginBottom: "1rem" }}>
210 <strong>Blog</strong> — <a href="/blog">All Posts</a>
211 </div>
212 <Search placeholder="Search posts..." />
213 {children}
214 </div>
215 );
216}
217`);
218
219write("blog/page.tsx", `
220import Link from "next/link";
221export const metadata = { title: "Blog" };
222const posts = Array.from({ length: 25 }, (_, i) => ({
223 slug: \`post-\${i + 1}\`,
224 title: \`Blog Post \${i + 1}: \${["React Patterns", "Server Components", "Caching", "Deployment", "Performance"][i % 5]}\`,
225 date: new Date(2025, 0, i + 1).toLocaleDateString(),
226}));
227export default function BlogPage() {
228 return (
229 <div>
230 <h1>Blog</h1>
231 {posts.map(post => (
232 <article key={post.slug} style={{ marginBottom: "1rem", paddingBottom: "1rem", borderBottom: "1px solid #eee" }}>
233 <Link href={\`/blog/\${post.slug}\`}><h2>{post.title}</h2></Link>
234 <time>{post.date}</time>
235 </article>
236 ))}
237 </div>
238 );
239}
240`);
241
242write("blog/[slug]/page.tsx", `
243export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
244 const { slug } = await params;
245 return { title: slug.replace(/-/g, " ") };
246}
247export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
248 const { slug } = await params;
249 const paragraphs = Array.from({ length: 5 }, (_, i) =>
250 \`Paragraph \${i + 1} of "\${slug}". Lorem ipsum dolor sit amet, consectetur adipiscing elit.\`
251 );
252 return (
253 <div>
254 <h1>{slug.replace(/-/g, " ")}</h1>
255 <time>{new Date().toLocaleDateString()}</time>
256 {paragraphs.map((p, i) => <p key={i}>{p}</p>)}
257 </div>
258 );
259}
260`);
261
262// ─── Dashboard (nested layout + client components) ─────────────────────────────
263write("dashboard/layout.tsx", `
264import { Timer } from "../_components/timer";
265export default function DashboardLayout({ children }: { children: React.ReactNode }) {
266 return (
267 <div style={{ display: "grid", gridTemplateColumns: "200px 1fr" }}>
268 <aside style={{ padding: "1rem", borderRight: "1px solid #eee" }}>
269 <h3>Dashboard</h3>
270 <nav style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
271 <a href="/dashboard">Overview</a>
272 <a href="/dashboard/analytics">Analytics</a>
273 <a href="/dashboard/users">Users</a>
274 <a href="/dashboard/settings">Settings</a>
275 </nav>
276 <div style={{ marginTop: "1rem", fontSize: "0.8rem", color: "#666" }}><Timer /></div>
277 </aside>
278 <div style={{ padding: "1rem" }}>{children}</div>
279 </div>
280 );
281}
282`);
283
284write("dashboard/page.tsx", `
285import { Counter } from "../_components/counter";
286export const metadata = { title: "Dashboard" };
287export default function DashboardPage() {
288 const stats = [
289 { label: "Total Users", value: "12,345" },
290 { label: "Revenue", value: "$98,765" },
291 { label: "Orders", value: "3,456" },
292 { label: "Conversion", value: "3.2%" },
293 ];
294 return (
295 <div>
296 <h1>Dashboard Overview</h1>
297 <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: "1rem", marginBottom: "2rem" }}>
298 {stats.map(s => (
299 <div key={s.label} style={{ padding: "1rem", border: "1px solid #ddd", borderRadius: "8px" }}>
300 <div style={{ fontSize: "0.8rem", color: "#666" }}>{s.label}</div>
301 <div style={{ fontSize: "1.5rem", fontWeight: "bold" }}>{s.value}</div>
302 </div>
303 ))}
304 </div>
305 <Counter label="Page Views" />
306 </div>
307 );
308}
309`);
310
311// Pre-compute analytics data at generation time with seeded PRNG for reproducibility.
312const analyticsRows = Array.from({ length: 10 }, () => ({
313 views: Math.floor(random() * 10000),
314 bounce: Math.floor(random() * 100),
315}));
316
317write("dashboard/analytics/page.tsx", `
318export const metadata = { title: "Analytics" };
319
320const rows = [
321${analyticsRows.map((r, i) => ` { page: "/page-${i + 1}", views: ${r.views}, bounce: ${r.bounce} },`).join("\n")}
322];
323
324export default function AnalyticsPage() {
325 return (
326 <div>
327 <h1>Analytics</h1>
328 <table style={{ width: "100%", borderCollapse: "collapse" }}>
329 <thead><tr><th>Page</th><th>Views</th><th>Bounce Rate</th></tr></thead>
330 <tbody>{rows.map(r => (
331 <tr key={r.page} style={{ borderBottom: "1px solid #eee" }}>
332 <td style={{ padding: "0.5rem" }}>{r.page}</td>
333 <td style={{ padding: "0.5rem" }}>{r.views.toLocaleString()}</td>
334 <td style={{ padding: "0.5rem" }}>{r.bounce}%</td>
335 </tr>
336 ))}</tbody>
337 </table>
338 </div>
339 );
340}
341`);
342
343write("dashboard/users/page.tsx", `
344export const metadata = { title: "Users" };
345export default function UsersPage() {
346 const users = Array.from({ length: 15 }, (_, i) => ({
347 id: i + 1, name: \`User \${i + 1}\`, email: \`user\${i + 1}@example.com\`, role: i % 3 === 0 ? "Admin" : "User",
348 }));
349 return (
350 <div>
351 <h1>Users</h1>
352 <table style={{ width: "100%", borderCollapse: "collapse" }}>
353 <thead><tr><th>ID</th><th>Name</th><th>Email</th><th>Role</th></tr></thead>
354 <tbody>{users.map(u => (
355 <tr key={u.id} style={{ borderBottom: "1px solid #eee" }}>
356 <td style={{ padding: "0.5rem" }}>{u.id}</td><td style={{ padding: "0.5rem" }}>{u.name}</td>
357 <td style={{ padding: "0.5rem" }}>{u.email}</td><td style={{ padding: "0.5rem" }}>{u.role}</td>
358 </tr>
359 ))}</tbody>
360 </table>
361 </div>
362 );
363}
364`);
365
366write("dashboard/settings/page.tsx", `
367import { Counter } from "../../_components/counter";
368export const metadata = { title: "Dashboard Settings" };
369export default function DashboardSettingsPage() {
370 return (<div><h1>Dashboard Settings</h1><p>Configure dashboard preferences.</p><Counter label="Saves" /></div>);
371}
372`);
373
374// ─── Docs (catch-all) ──────────────────────────────────────────────────────────
375write("docs/page.tsx", `
376import Link from "next/link";
377export const metadata = { title: "Documentation" };
378const sections = ["getting-started", "installation", "configuration", "api-reference", "deployment", "troubleshooting", "migration", "plugins"];
379export default function DocsIndex() {
380 return (<div><h1>Documentation</h1><ul>{sections.map(s => <li key={s}><Link href={\`/docs/\${s}\`}>{s.replace(/-/g, " ")}</Link></li>)}</ul></div>);
381}
382`);
383
384write("docs/[...slug]/page.tsx", `
385export async function generateMetadata({ params }: { params: Promise<{ slug: string[] }> }) {
386 const { slug } = await params;
387 return { title: \`Docs: \${slug.join(" / ")}\` };
388}
389export default async function DocPage({ params }: { params: Promise<{ slug: string[] }> }) {
390 const { slug } = await params;
391 return (<div><h1>Docs: {slug.join(" / ")}</h1>{Array.from({ length: 3 }, (_, i) => <p key={i}>Section {i + 1} content.</p>)}</div>);
392}
393`);
394
395// ─── Settings (nested layout) ──────────────────────────────────────────────────
396write("settings/layout.tsx", `
397export default function SettingsLayout({ children }: { children: React.ReactNode }) {
398 return (
399 <div>
400 <div style={{ display: "flex", gap: "1rem", padding: "0.5rem", background: "#fafafa", marginBottom: "1rem" }}>
401 <a href="/settings">General</a>
402 <a href="/settings/profile">Profile</a>
403 <a href="/settings/notifications">Notifications</a>
404 <a href="/settings/billing">Billing</a>
405 </div>
406 {children}
407 </div>
408 );
409}
410`);
411
412for (const page of ["", "profile", "notifications", "billing"]) {
413 const name = page || "General";
414 const dir = page ? `settings/${page}` : "settings";
415 write(`${dir}/page.tsx`, `
416export const metadata = { title: "Settings - ${name}" };
417export default function Settings${name.charAt(0).toUpperCase() + name.slice(1)}Page() {
418 return (<div><h1>Settings: ${name}</h1><p>Configure ${name.toLowerCase()} settings.</p></div>);
419}
420 `);
421}
422
423// ─── API routes ────────────────────────────────────────────────────────────────
424write("api/health/route.ts", `
425export function GET() {
426 return Response.json({ status: "ok", timestamp: Date.now() });
427}
428`);
429
430write("api/data/route.ts", `
431export function GET() {
432 const data = Array.from({ length: 100 }, (_, i) => ({ id: i + 1, value: Math.random(), label: \`Item \${i + 1}\` }));
433 return Response.json(data);
434}
435`);
436
437// ─── Additional static pages (volume) ──────────────────────────────────────────
438const staticPages = [
439 "features", "pricing", "team", "careers", "contact",
440 "faq", "terms", "privacy", "changelog", "roadmap",
441 "support", "community", "partners", "press", "security",
442];
443
444for (const page of staticPages) {
445 const title = page.charAt(0).toUpperCase() + page.slice(1);
446 write(`${page}/page.tsx`, `
447export const metadata = { title: "${title}" };
448export default function ${title}Page() {
449 return (
450 <div>
451 <h1>${title}</h1>
452 <p>This is the ${page} page with information about ${page}.</p>
453 ${page === "faq" ? `
454 <div>
455 {Array.from({ length: 10 }, (_, i) => (
456 <details key={i} style={{ marginBottom: "0.5rem" }}>
457 <summary>Question {i + 1}?</summary>
458 <p>Answer to question {i + 1}.</p>
459 </details>
460 ))}
461 </div>` : `
462 <p>More content for the ${page} section would go here.</p>`}
463 </div>
464 );
465}
466 `);
467}
468
469// Count results
470import { readdirSync, statSync } from "node:fs";
471function countFiles(dir, name) {
472 let count = 0;
473 for (const entry of readdirSync(dir)) {
474 const full = join(dir, entry);
475 if (statSync(full).isDirectory()) count += countFiles(full, name);
476 else if (entry === name) count++;
477 }
478 return count;
479}
480
481const pages = countFiles(APP, "page.tsx");
482const routes = countFiles(APP, "route.ts");
483console.log(`Generated benchmark app: ${pages} pages + ${routes} API routes = ${pages + routes} total routes`);
484
485// Copy to each benchmark project (symlinks don't work with Turbopack)
486import { cpSync } from "node:fs";
487const BASE = dirname(new URL(import.meta.url).pathname);
488for (const project of ["nextjs", "vinext", "vinext-rolldown"]) {
489 const dest = join(BASE, project, "app");
490 rmSync(dest, { recursive: true, force: true });
491 cpSync(APP, dest, { recursive: true });
492 console.log(` Copied to ${project}/app`);
493}
494