vinext 0.0.28 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/build/report.d.ts +117 -0
  2. package/dist/build/report.d.ts.map +1 -0
  3. package/dist/build/report.js +303 -0
  4. package/dist/build/report.js.map +1 -0
  5. package/dist/cli.js +106 -9
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cloudflare/kv-cache-handler.d.ts.map +1 -1
  8. package/dist/cloudflare/kv-cache-handler.js +14 -12
  9. package/dist/cloudflare/kv-cache-handler.js.map +1 -1
  10. package/dist/cloudflare/tpr.d.ts +10 -0
  11. package/dist/cloudflare/tpr.d.ts.map +1 -1
  12. package/dist/cloudflare/tpr.js +36 -41
  13. package/dist/cloudflare/tpr.js.map +1 -1
  14. package/dist/config/next-config.d.ts.map +1 -1
  15. package/dist/config/next-config.js +16 -0
  16. package/dist/config/next-config.js.map +1 -1
  17. package/dist/entries/app-rsc-entry.d.ts.map +1 -1
  18. package/dist/entries/app-rsc-entry.js +19 -24
  19. package/dist/entries/app-rsc-entry.js.map +1 -1
  20. package/dist/entries/pages-server-entry.d.ts.map +1 -1
  21. package/dist/entries/pages-server-entry.js +94 -44
  22. package/dist/entries/pages-server-entry.js.map +1 -1
  23. package/dist/index.d.ts +17 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +111 -38
  26. package/dist/index.js.map +1 -1
  27. package/dist/routing/app-router.d.ts +2 -0
  28. package/dist/routing/app-router.d.ts.map +1 -1
  29. package/dist/routing/app-router.js +92 -79
  30. package/dist/routing/app-router.js.map +1 -1
  31. package/dist/routing/pages-router.d.ts.map +1 -1
  32. package/dist/routing/pages-router.js +17 -57
  33. package/dist/routing/pages-router.js.map +1 -1
  34. package/dist/routing/route-trie.d.ts +57 -0
  35. package/dist/routing/route-trie.d.ts.map +1 -0
  36. package/dist/routing/route-trie.js +160 -0
  37. package/dist/routing/route-trie.js.map +1 -0
  38. package/dist/routing/route-validation.d.ts.map +1 -1
  39. package/dist/routing/route-validation.js +13 -1
  40. package/dist/routing/route-validation.js.map +1 -1
  41. package/dist/routing/utils.d.ts +19 -0
  42. package/dist/routing/utils.d.ts.map +1 -1
  43. package/dist/routing/utils.js +47 -0
  44. package/dist/routing/utils.js.map +1 -1
  45. package/dist/server/api-handler.d.ts.map +1 -1
  46. package/dist/server/api-handler.js +28 -13
  47. package/dist/server/api-handler.js.map +1 -1
  48. package/dist/server/dev-server.d.ts.map +1 -1
  49. package/dist/server/dev-server.js +58 -6
  50. package/dist/server/dev-server.js.map +1 -1
  51. package/dist/server/image-optimization.d.ts.map +1 -1
  52. package/dist/server/image-optimization.js +1 -1
  53. package/dist/server/image-optimization.js.map +1 -1
  54. package/dist/server/instrumentation.d.ts.map +1 -1
  55. package/dist/server/instrumentation.js +17 -8
  56. package/dist/server/instrumentation.js.map +1 -1
  57. package/dist/server/middleware-codegen.d.ts +10 -0
  58. package/dist/server/middleware-codegen.d.ts.map +1 -1
  59. package/dist/server/middleware-codegen.js +39 -0
  60. package/dist/server/middleware-codegen.js.map +1 -1
  61. package/dist/server/middleware.d.ts.map +1 -1
  62. package/dist/server/middleware.js +2 -1
  63. package/dist/server/middleware.js.map +1 -1
  64. package/dist/server/prod-server.d.ts +8 -2
  65. package/dist/server/prod-server.d.ts.map +1 -1
  66. package/dist/server/prod-server.js +60 -20
  67. package/dist/server/prod-server.js.map +1 -1
  68. package/dist/shims/headers.d.ts.map +1 -1
  69. package/dist/shims/headers.js +2 -5
  70. package/dist/shims/headers.js.map +1 -1
  71. package/dist/shims/link.d.ts.map +1 -1
  72. package/dist/shims/link.js +11 -43
  73. package/dist/shims/link.js.map +1 -1
  74. package/dist/shims/metadata.d.ts +56 -0
  75. package/dist/shims/metadata.d.ts.map +1 -1
  76. package/dist/shims/metadata.js +66 -0
  77. package/dist/shims/metadata.js.map +1 -1
  78. package/dist/shims/navigation.d.ts +2 -0
  79. package/dist/shims/navigation.d.ts.map +1 -1
  80. package/dist/shims/navigation.js +41 -29
  81. package/dist/shims/navigation.js.map +1 -1
  82. package/dist/shims/router.d.ts.map +1 -1
  83. package/dist/shims/router.js +13 -19
  84. package/dist/shims/router.js.map +1 -1
  85. package/dist/shims/url-utils.d.ts +20 -6
  86. package/dist/shims/url-utils.d.ts.map +1 -1
  87. package/dist/shims/url-utils.js +79 -0
  88. package/dist/shims/url-utils.js.map +1 -1
  89. package/package.json +2 -2
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Build report — prints a Next.js-style route table after `vinext build`.
3
+ *
4
+ * Classifies every discovered route as:
5
+ * ○ Static — confirmed static: force-static or revalidate=Infinity
6
+ * ◐ ISR — statically rendered, revalidated on a timer (revalidate=N)
7
+ * ƒ Dynamic — confirmed dynamic: force-dynamic, revalidate=0, or getServerSideProps
8
+ * ? Unknown — no explicit config; likely dynamic but not confirmed
9
+ * λ API — API route handler
10
+ *
11
+ * Classification uses regex-based static source analysis (no module
12
+ * execution). Vite's parseAst() is NOT used because it doesn't handle
13
+ * TypeScript syntax.
14
+ *
15
+ * Limitation: without running the build, we cannot detect dynamic API usage
16
+ * (headers(), cookies(), connection(), etc.) that implicitly forces a route
17
+ * dynamic. Routes without explicit `export const dynamic` or
18
+ * `export const revalidate` are classified as "unknown" rather than "static"
19
+ * to avoid false confidence.
20
+ */
21
+ import type { Route } from "../routing/pages-router.js";
22
+ import type { AppRoute } from "../routing/app-router.js";
23
+ export type RouteType = "static" | "isr" | "ssr" | "unknown" | "api";
24
+ export interface RouteRow {
25
+ pattern: string;
26
+ type: RouteType;
27
+ /** Only set for `isr` routes. */
28
+ revalidate?: number;
29
+ }
30
+ /**
31
+ * Returns true if the source code contains a named export with the given name.
32
+ * Handles all three common export forms:
33
+ * export function foo() {}
34
+ * export const foo = ...
35
+ * export { foo }
36
+ */
37
+ export declare function hasNamedExport(code: string, name: string): boolean;
38
+ /**
39
+ * Extracts the string value of `export const <name> = "value"`.
40
+ * Handles optional TypeScript type annotations:
41
+ * export const dynamic: string = "force-dynamic"
42
+ * Returns null if the export is absent or not a string literal.
43
+ */
44
+ export declare function extractExportConstString(code: string, name: string): string | null;
45
+ /**
46
+ * Extracts the numeric value of `export const <name> = <number>`.
47
+ * Supports integers, decimals, negative values, and `Infinity`.
48
+ * Handles optional TypeScript type annotations.
49
+ * Returns null if the export is absent or not a number.
50
+ */
51
+ export declare function extractExportConstNumber(code: string, name: string): number | null;
52
+ /**
53
+ * Extracts the `revalidate` value from inside a `getStaticProps` return object.
54
+ * Looks for: revalidate: <number> or revalidate: false or revalidate: Infinity
55
+ *
56
+ * Returns:
57
+ * number — a positive revalidation interval (enables ISR)
58
+ * 0 — treat as SSR (revalidate every request)
59
+ * false — fully static (no revalidation)
60
+ * Infinity — fully static (treated same as false by Next.js)
61
+ * null — no `revalidate` key found (fully static)
62
+ */
63
+ export declare function extractGetStaticPropsRevalidate(code: string): number | false | null;
64
+ /**
65
+ * Classifies a Pages Router page file by reading its source and examining
66
+ * which data-fetching exports it contains.
67
+ *
68
+ * API routes (files under pages/api/) are always `api`.
69
+ */
70
+ export declare function classifyPagesRoute(filePath: string): {
71
+ type: RouteType;
72
+ revalidate?: number;
73
+ };
74
+ /**
75
+ * Classifies an App Router route.
76
+ *
77
+ * @param pagePath Absolute path to the page.tsx (null for API-only routes)
78
+ * @param routePath Absolute path to the route.ts handler (null for page routes)
79
+ * @param isDynamic Whether the URL pattern contains dynamic segments
80
+ */
81
+ export declare function classifyAppRoute(pagePath: string | null, routePath: string | null, isDynamic: boolean): {
82
+ type: RouteType;
83
+ revalidate?: number;
84
+ };
85
+ /**
86
+ * Builds a sorted list of RouteRow objects from the discovered routes.
87
+ * Routes are sorted alphabetically by path, matching filesystem order.
88
+ */
89
+ export declare function buildReportRows(options: {
90
+ pageRoutes?: Route[];
91
+ apiRoutes?: Route[];
92
+ appRoutes?: AppRoute[];
93
+ }): RouteRow[];
94
+ /**
95
+ * Formats a list of RouteRows into a Next.js-style build report string.
96
+ *
97
+ * Example output:
98
+ * Route (pages)
99
+ * ┌ ○ /
100
+ * ├ ◐ /blog/:slug (60s)
101
+ * ├ ƒ /dashboard
102
+ * └ λ /api/posts
103
+ *
104
+ * ○ Static ◐ ISR ƒ Dynamic λ API
105
+ */
106
+ export declare function formatBuildReport(rows: RouteRow[], routerLabel?: string): string;
107
+ /**
108
+ * Scans the project at `root`, classifies all routes, and prints the
109
+ * Next.js-style build report to stdout.
110
+ *
111
+ * Called at the end of `vinext build` in cli.ts.
112
+ */
113
+ export declare function printBuildReport(options: {
114
+ root: string;
115
+ pageExtensions?: string[];
116
+ }): Promise<void>;
117
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/build/report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAIzD,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,KAAK,GAAG,KAAK,GAAG,SAAS,GAAG,KAAK,CAAC;AAErE,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,SAAS,CAAC;IAChB,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAclE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOlF;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQlF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,IAAI,CAYnF;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG;IACpD,IAAI,EAAE,SAAS,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAgCA;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,SAAS,EAAE,OAAO,GACjB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CA0C1C;AAID;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE;IACvC,UAAU,CAAC,EAAE,KAAK,EAAE,CAAC;IACrB,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC;IACpB,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;CACxB,GAAG,QAAQ,EAAE,CAqBb;AAoBD;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,WAAW,SAAQ,GAAG,MAAM,CAsC/E;AAcD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B,GAAG,OAAO,CAAC,IAAI,CAAC,CA6BhB"}
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Build report — prints a Next.js-style route table after `vinext build`.
3
+ *
4
+ * Classifies every discovered route as:
5
+ * ○ Static — confirmed static: force-static or revalidate=Infinity
6
+ * ◐ ISR — statically rendered, revalidated on a timer (revalidate=N)
7
+ * ƒ Dynamic — confirmed dynamic: force-dynamic, revalidate=0, or getServerSideProps
8
+ * ? Unknown — no explicit config; likely dynamic but not confirmed
9
+ * λ API — API route handler
10
+ *
11
+ * Classification uses regex-based static source analysis (no module
12
+ * execution). Vite's parseAst() is NOT used because it doesn't handle
13
+ * TypeScript syntax.
14
+ *
15
+ * Limitation: without running the build, we cannot detect dynamic API usage
16
+ * (headers(), cookies(), connection(), etc.) that implicitly forces a route
17
+ * dynamic. Routes without explicit `export const dynamic` or
18
+ * `export const revalidate` are classified as "unknown" rather than "static"
19
+ * to avoid false confidence.
20
+ */
21
+ import fs from "node:fs";
22
+ import path from "node:path";
23
+ // ─── Regex-based export detection ────────────────────────────────────────────
24
+ /**
25
+ * Returns true if the source code contains a named export with the given name.
26
+ * Handles all three common export forms:
27
+ * export function foo() {}
28
+ * export const foo = ...
29
+ * export { foo }
30
+ */
31
+ export function hasNamedExport(code, name) {
32
+ // Function / generator / async function declaration
33
+ const fnRe = new RegExp(`(?:^|\\n)\\s*export\\s+(?:async\\s+)?function\\s+${name}\\b`);
34
+ if (fnRe.test(code))
35
+ return true;
36
+ // Variable declaration (const / let / var)
37
+ const varRe = new RegExp(`(?:^|\\n)\\s*export\\s+(?:const|let|var)\\s+${name}\\s*[=:]`);
38
+ if (varRe.test(code))
39
+ return true;
40
+ // Re-export specifier: export { foo } or export { foo as bar }
41
+ const reRe = new RegExp(`export\\s*\\{[^}]*\\b${name}\\b[^}]*\\}`);
42
+ if (reRe.test(code))
43
+ return true;
44
+ return false;
45
+ }
46
+ /**
47
+ * Extracts the string value of `export const <name> = "value"`.
48
+ * Handles optional TypeScript type annotations:
49
+ * export const dynamic: string = "force-dynamic"
50
+ * Returns null if the export is absent or not a string literal.
51
+ */
52
+ export function extractExportConstString(code, name) {
53
+ const re = new RegExp(`(?:^|\\n)\\s*export\\s+const\\s+${name}\\s*(?::[^=]+)?\\s*=\\s*['"]([^'"]+)['"]`, "m");
54
+ const m = re.exec(code);
55
+ return m ? m[1] : null;
56
+ }
57
+ /**
58
+ * Extracts the numeric value of `export const <name> = <number>`.
59
+ * Supports integers, decimals, negative values, and `Infinity`.
60
+ * Handles optional TypeScript type annotations.
61
+ * Returns null if the export is absent or not a number.
62
+ */
63
+ export function extractExportConstNumber(code, name) {
64
+ const re = new RegExp(`(?:^|\\n)\\s*export\\s+const\\s+${name}\\s*(?::[^=]+)?\\s*=\\s*(-?\\d+(?:\\.\\d+)?|Infinity)`, "m");
65
+ const m = re.exec(code);
66
+ if (!m)
67
+ return null;
68
+ return m[1] === "Infinity" ? Infinity : parseFloat(m[1]);
69
+ }
70
+ /**
71
+ * Extracts the `revalidate` value from inside a `getStaticProps` return object.
72
+ * Looks for: revalidate: <number> or revalidate: false or revalidate: Infinity
73
+ *
74
+ * Returns:
75
+ * number — a positive revalidation interval (enables ISR)
76
+ * 0 — treat as SSR (revalidate every request)
77
+ * false — fully static (no revalidation)
78
+ * Infinity — fully static (treated same as false by Next.js)
79
+ * null — no `revalidate` key found (fully static)
80
+ */
81
+ export function extractGetStaticPropsRevalidate(code) {
82
+ // TODO: This regex matches `revalidate:` anywhere in the file, not scoped to
83
+ // the getStaticProps return object. A config object or comment elsewhere in
84
+ // the file (e.g. `const defaults = { revalidate: 30 }`) could produce a false
85
+ // positive. Rare in practice, but a proper AST-based approach would be more
86
+ // accurate.
87
+ const re = /\brevalidate\s*:\s*(-?\d+(?:\.\d+)?|Infinity|false)\b/;
88
+ const m = re.exec(code);
89
+ if (!m)
90
+ return null;
91
+ if (m[1] === "false")
92
+ return false;
93
+ if (m[1] === "Infinity")
94
+ return Infinity;
95
+ return parseFloat(m[1]);
96
+ }
97
+ // ─── Route classification ─────────────────────────────────────────────────────
98
+ /**
99
+ * Classifies a Pages Router page file by reading its source and examining
100
+ * which data-fetching exports it contains.
101
+ *
102
+ * API routes (files under pages/api/) are always `api`.
103
+ */
104
+ export function classifyPagesRoute(filePath) {
105
+ // API routes are identified by their path
106
+ const normalized = filePath.replace(/\\/g, "/");
107
+ if (normalized.includes("/pages/api/")) {
108
+ return { type: "api" };
109
+ }
110
+ let code;
111
+ try {
112
+ code = fs.readFileSync(filePath, "utf8");
113
+ }
114
+ catch {
115
+ return { type: "unknown" };
116
+ }
117
+ if (hasNamedExport(code, "getServerSideProps")) {
118
+ return { type: "ssr" };
119
+ }
120
+ if (hasNamedExport(code, "getStaticProps")) {
121
+ const revalidate = extractGetStaticPropsRevalidate(code);
122
+ if (revalidate === null || revalidate === false || revalidate === Infinity) {
123
+ return { type: "static" };
124
+ }
125
+ if (revalidate === 0) {
126
+ return { type: "ssr" };
127
+ }
128
+ // Positive number → ISR
129
+ return { type: "isr", revalidate };
130
+ }
131
+ return { type: "static" };
132
+ }
133
+ /**
134
+ * Classifies an App Router route.
135
+ *
136
+ * @param pagePath Absolute path to the page.tsx (null for API-only routes)
137
+ * @param routePath Absolute path to the route.ts handler (null for page routes)
138
+ * @param isDynamic Whether the URL pattern contains dynamic segments
139
+ */
140
+ export function classifyAppRoute(pagePath, routePath, isDynamic) {
141
+ // Route handlers with no page component → API
142
+ if (routePath !== null && pagePath === null) {
143
+ return { type: "api" };
144
+ }
145
+ const filePath = pagePath ?? routePath;
146
+ if (!filePath)
147
+ return { type: "unknown" };
148
+ let code;
149
+ try {
150
+ code = fs.readFileSync(filePath, "utf8");
151
+ }
152
+ catch {
153
+ return { type: "unknown" };
154
+ }
155
+ // Check `export const dynamic`
156
+ const dynamicValue = extractExportConstString(code, "dynamic");
157
+ if (dynamicValue === "force-dynamic") {
158
+ return { type: "ssr" };
159
+ }
160
+ if (dynamicValue === "force-static" || dynamicValue === "error") {
161
+ // "error" enforces static rendering — it throws if dynamic APIs are used,
162
+ // so the page is statically rendered (same as force-static for classification).
163
+ return { type: "static" };
164
+ }
165
+ // Check `export const revalidate`
166
+ const revalidateValue = extractExportConstNumber(code, "revalidate");
167
+ if (revalidateValue !== null) {
168
+ if (revalidateValue === Infinity)
169
+ return { type: "static" };
170
+ if (revalidateValue === 0)
171
+ return { type: "ssr" };
172
+ if (revalidateValue > 0)
173
+ return { type: "isr", revalidate: revalidateValue };
174
+ }
175
+ // Fall back to isDynamic flag (dynamic URL segments without explicit config)
176
+ if (isDynamic)
177
+ return { type: "ssr" };
178
+ // No explicit config and no dynamic URL segments — we can't confirm static
179
+ // without running the build (dynamic API calls like headers() are invisible
180
+ // to static analysis). Report as unknown rather than falsely claiming static.
181
+ return { type: "unknown" };
182
+ }
183
+ // ─── Row building ─────────────────────────────────────────────────────────────
184
+ /**
185
+ * Builds a sorted list of RouteRow objects from the discovered routes.
186
+ * Routes are sorted alphabetically by path, matching filesystem order.
187
+ */
188
+ export function buildReportRows(options) {
189
+ const rows = [];
190
+ for (const route of options.pageRoutes ?? []) {
191
+ const { type, revalidate } = classifyPagesRoute(route.filePath);
192
+ rows.push({ pattern: route.pattern, type, revalidate });
193
+ }
194
+ for (const route of options.apiRoutes ?? []) {
195
+ rows.push({ pattern: route.pattern, type: "api" });
196
+ }
197
+ for (const route of options.appRoutes ?? []) {
198
+ const { type, revalidate } = classifyAppRoute(route.pagePath, route.routePath, route.isDynamic);
199
+ rows.push({ pattern: route.pattern, type, revalidate });
200
+ }
201
+ // Sort purely by path — mirrors filesystem order, matching Next.js output style
202
+ rows.sort((a, b) => a.pattern.localeCompare(b.pattern));
203
+ return rows;
204
+ }
205
+ // ─── Formatting ───────────────────────────────────────────────────────────────
206
+ const SYMBOLS = {
207
+ static: "○",
208
+ isr: "◐",
209
+ ssr: "ƒ",
210
+ unknown: "?",
211
+ api: "λ",
212
+ };
213
+ const LABELS = {
214
+ static: "Static",
215
+ isr: "ISR",
216
+ ssr: "Dynamic",
217
+ unknown: "Unknown",
218
+ api: "API",
219
+ };
220
+ /**
221
+ * Formats a list of RouteRows into a Next.js-style build report string.
222
+ *
223
+ * Example output:
224
+ * Route (pages)
225
+ * ┌ ○ /
226
+ * ├ ◐ /blog/:slug (60s)
227
+ * ├ ƒ /dashboard
228
+ * └ λ /api/posts
229
+ *
230
+ * ○ Static ◐ ISR ƒ Dynamic λ API
231
+ */
232
+ export function formatBuildReport(rows, routerLabel = "app") {
233
+ if (rows.length === 0)
234
+ return "";
235
+ const lines = [];
236
+ lines.push(` Route (${routerLabel})`);
237
+ // Determine padding width from the longest pattern
238
+ const maxPatternLen = Math.max(...rows.map((r) => r.pattern.length));
239
+ rows.forEach((row, i) => {
240
+ const isLast = i === rows.length - 1;
241
+ const corner = i === 0 ? "┌" : isLast ? "└" : "├";
242
+ const sym = SYMBOLS[row.type];
243
+ const suffix = row.type === "isr" && row.revalidate !== undefined ? ` (${row.revalidate}s)` : "";
244
+ const padding = " ".repeat(maxPatternLen - row.pattern.length);
245
+ lines.push(` ${corner} ${sym} ${row.pattern}${padding}${suffix}`);
246
+ });
247
+ lines.push("");
248
+ // Legend — only include types that appear in this report, sorted alphabetically by label
249
+ const usedTypes = [...new Set(rows.map((r) => r.type))].sort((a, b) => LABELS[a].localeCompare(LABELS[b]));
250
+ lines.push(" " + usedTypes.map((t) => `${SYMBOLS[t]} ${LABELS[t]}`).join(" "));
251
+ // Explanatory note — only shown when unknown routes are present
252
+ if (usedTypes.includes("unknown")) {
253
+ lines.push("");
254
+ lines.push(" ? Some routes could not be classified. vinext currently uses static analysis");
255
+ lines.push(" and cannot detect dynamic API usage (headers(), cookies(), etc.) at build time.");
256
+ lines.push(" Automatic classification will be improved in a future release.");
257
+ }
258
+ return lines.join("\n");
259
+ }
260
+ // ─── Directory detection ──────────────────────────────────────────────────────
261
+ function findDir(root, ...candidates) {
262
+ for (const candidate of candidates) {
263
+ const full = path.join(root, candidate);
264
+ if (fs.existsSync(full) && fs.statSync(full).isDirectory())
265
+ return full;
266
+ }
267
+ return null;
268
+ }
269
+ // ─── Main entry point ─────────────────────────────────────────────────────────
270
+ /**
271
+ * Scans the project at `root`, classifies all routes, and prints the
272
+ * Next.js-style build report to stdout.
273
+ *
274
+ * Called at the end of `vinext build` in cli.ts.
275
+ */
276
+ export async function printBuildReport(options) {
277
+ const { root } = options;
278
+ const appDir = findDir(root, "app", "src/app");
279
+ const pagesDir = findDir(root, "pages", "src/pages");
280
+ if (!appDir && !pagesDir)
281
+ return;
282
+ if (appDir) {
283
+ // Dynamic import to avoid loading routing code unless needed
284
+ const { appRouter } = await import("../routing/app-router.js");
285
+ const routes = await appRouter(appDir, options.pageExtensions);
286
+ const rows = buildReportRows({ appRoutes: routes });
287
+ if (rows.length > 0) {
288
+ console.log("\n" + formatBuildReport(rows, "app"));
289
+ }
290
+ }
291
+ if (pagesDir) {
292
+ const { pagesRouter, apiRouter } = await import("../routing/pages-router.js");
293
+ const [pageRoutes, apiRoutes] = await Promise.all([
294
+ pagesRouter(pagesDir, options.pageExtensions),
295
+ apiRouter(pagesDir, options.pageExtensions),
296
+ ]);
297
+ const rows = buildReportRows({ pageRoutes, apiRoutes });
298
+ if (rows.length > 0) {
299
+ console.log("\n" + formatBuildReport(rows, "pages"));
300
+ }
301
+ }
302
+ }
303
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/build/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Build report — prints a Next.js-style route table after `vinext build`.\n *\n * Classifies every discovered route as:\n * ○ Static — confirmed static: force-static or revalidate=Infinity\n * ◐ ISR — statically rendered, revalidated on a timer (revalidate=N)\n * ƒ Dynamic — confirmed dynamic: force-dynamic, revalidate=0, or getServerSideProps\n * ? Unknown — no explicit config; likely dynamic but not confirmed\n * λ API — API route handler\n *\n * Classification uses regex-based static source analysis (no module\n * execution). Vite's parseAst() is NOT used because it doesn't handle\n * TypeScript syntax.\n *\n * Limitation: without running the build, we cannot detect dynamic API usage\n * (headers(), cookies(), connection(), etc.) that implicitly forces a route\n * dynamic. Routes without explicit `export const dynamic` or\n * `export const revalidate` are classified as \"unknown\" rather than \"static\"\n * to avoid false confidence.\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport type { Route } from \"../routing/pages-router.js\";\nimport type { AppRoute } from \"../routing/app-router.js\";\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type RouteType = \"static\" | \"isr\" | \"ssr\" | \"unknown\" | \"api\";\n\nexport interface RouteRow {\n pattern: string;\n type: RouteType;\n /** Only set for `isr` routes. */\n revalidate?: number;\n}\n\n// ─── Regex-based export detection ────────────────────────────────────────────\n\n/**\n * Returns true if the source code contains a named export with the given name.\n * Handles all three common export forms:\n * export function foo() {}\n * export const foo = ...\n * export { foo }\n */\nexport function hasNamedExport(code: string, name: string): boolean {\n // Function / generator / async function declaration\n const fnRe = new RegExp(`(?:^|\\\\n)\\\\s*export\\\\s+(?:async\\\\s+)?function\\\\s+${name}\\\\b`);\n if (fnRe.test(code)) return true;\n\n // Variable declaration (const / let / var)\n const varRe = new RegExp(`(?:^|\\\\n)\\\\s*export\\\\s+(?:const|let|var)\\\\s+${name}\\\\s*[=:]`);\n if (varRe.test(code)) return true;\n\n // Re-export specifier: export { foo } or export { foo as bar }\n const reRe = new RegExp(`export\\\\s*\\\\{[^}]*\\\\b${name}\\\\b[^}]*\\\\}`);\n if (reRe.test(code)) return true;\n\n return false;\n}\n\n/**\n * Extracts the string value of `export const <name> = \"value\"`.\n * Handles optional TypeScript type annotations:\n * export const dynamic: string = \"force-dynamic\"\n * Returns null if the export is absent or not a string literal.\n */\nexport function extractExportConstString(code: string, name: string): string | null {\n const re = new RegExp(\n `(?:^|\\\\n)\\\\s*export\\\\s+const\\\\s+${name}\\\\s*(?::[^=]+)?\\\\s*=\\\\s*['\"]([^'\"]+)['\"]`,\n \"m\",\n );\n const m = re.exec(code);\n return m ? m[1] : null;\n}\n\n/**\n * Extracts the numeric value of `export const <name> = <number>`.\n * Supports integers, decimals, negative values, and `Infinity`.\n * Handles optional TypeScript type annotations.\n * Returns null if the export is absent or not a number.\n */\nexport function extractExportConstNumber(code: string, name: string): number | null {\n const re = new RegExp(\n `(?:^|\\\\n)\\\\s*export\\\\s+const\\\\s+${name}\\\\s*(?::[^=]+)?\\\\s*=\\\\s*(-?\\\\d+(?:\\\\.\\\\d+)?|Infinity)`,\n \"m\",\n );\n const m = re.exec(code);\n if (!m) return null;\n return m[1] === \"Infinity\" ? Infinity : parseFloat(m[1]);\n}\n\n/**\n * Extracts the `revalidate` value from inside a `getStaticProps` return object.\n * Looks for: revalidate: <number> or revalidate: false or revalidate: Infinity\n *\n * Returns:\n * number — a positive revalidation interval (enables ISR)\n * 0 — treat as SSR (revalidate every request)\n * false — fully static (no revalidation)\n * Infinity — fully static (treated same as false by Next.js)\n * null — no `revalidate` key found (fully static)\n */\nexport function extractGetStaticPropsRevalidate(code: string): number | false | null {\n // TODO: This regex matches `revalidate:` anywhere in the file, not scoped to\n // the getStaticProps return object. A config object or comment elsewhere in\n // the file (e.g. `const defaults = { revalidate: 30 }`) could produce a false\n // positive. Rare in practice, but a proper AST-based approach would be more\n // accurate.\n const re = /\\brevalidate\\s*:\\s*(-?\\d+(?:\\.\\d+)?|Infinity|false)\\b/;\n const m = re.exec(code);\n if (!m) return null;\n if (m[1] === \"false\") return false;\n if (m[1] === \"Infinity\") return Infinity;\n return parseFloat(m[1]);\n}\n\n// ─── Route classification ─────────────────────────────────────────────────────\n\n/**\n * Classifies a Pages Router page file by reading its source and examining\n * which data-fetching exports it contains.\n *\n * API routes (files under pages/api/) are always `api`.\n */\nexport function classifyPagesRoute(filePath: string): {\n type: RouteType;\n revalidate?: number;\n} {\n // API routes are identified by their path\n const normalized = filePath.replace(/\\\\/g, \"/\");\n if (normalized.includes(\"/pages/api/\")) {\n return { type: \"api\" };\n }\n\n let code: string;\n try {\n code = fs.readFileSync(filePath, \"utf8\");\n } catch {\n return { type: \"unknown\" };\n }\n\n if (hasNamedExport(code, \"getServerSideProps\")) {\n return { type: \"ssr\" };\n }\n\n if (hasNamedExport(code, \"getStaticProps\")) {\n const revalidate = extractGetStaticPropsRevalidate(code);\n\n if (revalidate === null || revalidate === false || revalidate === Infinity) {\n return { type: \"static\" };\n }\n if (revalidate === 0) {\n return { type: \"ssr\" };\n }\n // Positive number → ISR\n return { type: \"isr\", revalidate };\n }\n\n return { type: \"static\" };\n}\n\n/**\n * Classifies an App Router route.\n *\n * @param pagePath Absolute path to the page.tsx (null for API-only routes)\n * @param routePath Absolute path to the route.ts handler (null for page routes)\n * @param isDynamic Whether the URL pattern contains dynamic segments\n */\nexport function classifyAppRoute(\n pagePath: string | null,\n routePath: string | null,\n isDynamic: boolean,\n): { type: RouteType; revalidate?: number } {\n // Route handlers with no page component → API\n if (routePath !== null && pagePath === null) {\n return { type: \"api\" };\n }\n\n const filePath = pagePath ?? routePath;\n if (!filePath) return { type: \"unknown\" };\n\n let code: string;\n try {\n code = fs.readFileSync(filePath, \"utf8\");\n } catch {\n return { type: \"unknown\" };\n }\n\n // Check `export const dynamic`\n const dynamicValue = extractExportConstString(code, \"dynamic\");\n if (dynamicValue === \"force-dynamic\") {\n return { type: \"ssr\" };\n }\n if (dynamicValue === \"force-static\" || dynamicValue === \"error\") {\n // \"error\" enforces static rendering — it throws if dynamic APIs are used,\n // so the page is statically rendered (same as force-static for classification).\n return { type: \"static\" };\n }\n\n // Check `export const revalidate`\n const revalidateValue = extractExportConstNumber(code, \"revalidate\");\n if (revalidateValue !== null) {\n if (revalidateValue === Infinity) return { type: \"static\" };\n if (revalidateValue === 0) return { type: \"ssr\" };\n if (revalidateValue > 0) return { type: \"isr\", revalidate: revalidateValue };\n }\n\n // Fall back to isDynamic flag (dynamic URL segments without explicit config)\n if (isDynamic) return { type: \"ssr\" };\n\n // No explicit config and no dynamic URL segments — we can't confirm static\n // without running the build (dynamic API calls like headers() are invisible\n // to static analysis). Report as unknown rather than falsely claiming static.\n return { type: \"unknown\" };\n}\n\n// ─── Row building ─────────────────────────────────────────────────────────────\n\n/**\n * Builds a sorted list of RouteRow objects from the discovered routes.\n * Routes are sorted alphabetically by path, matching filesystem order.\n */\nexport function buildReportRows(options: {\n pageRoutes?: Route[];\n apiRoutes?: Route[];\n appRoutes?: AppRoute[];\n}): RouteRow[] {\n const rows: RouteRow[] = [];\n\n for (const route of options.pageRoutes ?? []) {\n const { type, revalidate } = classifyPagesRoute(route.filePath);\n rows.push({ pattern: route.pattern, type, revalidate });\n }\n\n for (const route of options.apiRoutes ?? []) {\n rows.push({ pattern: route.pattern, type: \"api\" });\n }\n\n for (const route of options.appRoutes ?? []) {\n const { type, revalidate } = classifyAppRoute(route.pagePath, route.routePath, route.isDynamic);\n rows.push({ pattern: route.pattern, type, revalidate });\n }\n\n // Sort purely by path — mirrors filesystem order, matching Next.js output style\n rows.sort((a, b) => a.pattern.localeCompare(b.pattern));\n\n return rows;\n}\n\n// ─── Formatting ───────────────────────────────────────────────────────────────\n\nconst SYMBOLS: Record<RouteType, string> = {\n static: \"○\",\n isr: \"◐\",\n ssr: \"ƒ\",\n unknown: \"?\",\n api: \"λ\",\n};\n\nconst LABELS: Record<RouteType, string> = {\n static: \"Static\",\n isr: \"ISR\",\n ssr: \"Dynamic\",\n unknown: \"Unknown\",\n api: \"API\",\n};\n\n/**\n * Formats a list of RouteRows into a Next.js-style build report string.\n *\n * Example output:\n * Route (pages)\n * ┌ ○ /\n * ├ ◐ /blog/:slug (60s)\n * ├ ƒ /dashboard\n * └ λ /api/posts\n *\n * ○ Static ◐ ISR ƒ Dynamic λ API\n */\nexport function formatBuildReport(rows: RouteRow[], routerLabel = \"app\"): string {\n if (rows.length === 0) return \"\";\n\n const lines: string[] = [];\n lines.push(` Route (${routerLabel})`);\n\n // Determine padding width from the longest pattern\n const maxPatternLen = Math.max(...rows.map((r) => r.pattern.length));\n\n rows.forEach((row, i) => {\n const isLast = i === rows.length - 1;\n const corner = i === 0 ? \"┌\" : isLast ? \"└\" : \"├\";\n const sym = SYMBOLS[row.type];\n const suffix =\n row.type === \"isr\" && row.revalidate !== undefined ? ` (${row.revalidate}s)` : \"\";\n const padding = \" \".repeat(maxPatternLen - row.pattern.length);\n lines.push(` ${corner} ${sym} ${row.pattern}${padding}${suffix}`);\n });\n\n lines.push(\"\");\n\n // Legend — only include types that appear in this report, sorted alphabetically by label\n const usedTypes = [...new Set(rows.map((r) => r.type))].sort((a, b) =>\n LABELS[a].localeCompare(LABELS[b]),\n );\n lines.push(\" \" + usedTypes.map((t) => `${SYMBOLS[t]} ${LABELS[t]}`).join(\" \"));\n\n // Explanatory note — only shown when unknown routes are present\n if (usedTypes.includes(\"unknown\")) {\n lines.push(\"\");\n lines.push(\" ? Some routes could not be classified. vinext currently uses static analysis\");\n lines.push(\n \" and cannot detect dynamic API usage (headers(), cookies(), etc.) at build time.\",\n );\n lines.push(\" Automatic classification will be improved in a future release.\");\n }\n\n return lines.join(\"\\n\");\n}\n\n// ─── Directory detection ──────────────────────────────────────────────────────\n\nfunction findDir(root: string, ...candidates: string[]): string | null {\n for (const candidate of candidates) {\n const full = path.join(root, candidate);\n if (fs.existsSync(full) && fs.statSync(full).isDirectory()) return full;\n }\n return null;\n}\n\n// ─── Main entry point ─────────────────────────────────────────────────────────\n\n/**\n * Scans the project at `root`, classifies all routes, and prints the\n * Next.js-style build report to stdout.\n *\n * Called at the end of `vinext build` in cli.ts.\n */\nexport async function printBuildReport(options: {\n root: string;\n pageExtensions?: string[];\n}): Promise<void> {\n const { root } = options;\n\n const appDir = findDir(root, \"app\", \"src/app\");\n const pagesDir = findDir(root, \"pages\", \"src/pages\");\n\n if (!appDir && !pagesDir) return;\n\n if (appDir) {\n // Dynamic import to avoid loading routing code unless needed\n const { appRouter } = await import(\"../routing/app-router.js\");\n const routes = await appRouter(appDir, options.pageExtensions);\n const rows = buildReportRows({ appRoutes: routes });\n if (rows.length > 0) {\n console.log(\"\\n\" + formatBuildReport(rows, \"app\"));\n }\n }\n\n if (pagesDir) {\n const { pagesRouter, apiRouter } = await import(\"../routing/pages-router.js\");\n const [pageRoutes, apiRoutes] = await Promise.all([\n pagesRouter(pagesDir, options.pageExtensions),\n apiRouter(pagesDir, options.pageExtensions),\n ]);\n const rows = buildReportRows({ pageRoutes, apiRoutes });\n if (rows.length > 0) {\n console.log(\"\\n\" + formatBuildReport(rows, \"pages\"));\n }\n }\n}\n"]}
package/dist/cli.js CHANGED
@@ -12,6 +12,7 @@
12
12
  * needed for most Next.js apps.
13
13
  */
14
14
  import vinext, { clientOutputConfig, clientTreeshakeConfig } from "./index.js";
15
+ import { printBuildReport } from "./build/report.js";
15
16
  import path from "node:path";
16
17
  import fs from "node:fs";
17
18
  import { pathToFileURL } from "node:url";
@@ -66,6 +67,9 @@ function parseArgs(args) {
66
67
  if (arg === "--help" || arg === "-h") {
67
68
  result.help = true;
68
69
  }
70
+ else if (arg === "--verbose") {
71
+ result.verbose = true;
72
+ }
69
73
  else if (arg === "--turbopack") {
70
74
  result.turbopack = true; // no-op, accepted for script compat
71
75
  }
@@ -87,12 +91,92 @@ function parseArgs(args) {
87
91
  }
88
92
  return result;
89
93
  }
90
- // ─── Auto-configuration ───────────────────────────────────────────────────────
94
+ // ─── Build logger ─────────────────────────────────────────────────────────────
91
95
  /**
92
- * Build the Vite config automatically. If a vite.config.ts exists in the
93
- * project, Vite will merge our config with it (theirs takes precedence).
94
- * If there's no vite.config, this provides everything needed.
96
+ * Create a custom Vite logger for build output.
97
+ *
98
+ * By default Vite/Rollup emit a lot of build noise: version banners, progress
99
+ * lines, chunk size tables, minChunkSize diagnostics, and various internal
100
+ * warnings that are either not actionable or already handled by vinext at
101
+ * runtime. This logger suppresses all of that while keeping the things that
102
+ * actually matter:
103
+ *
104
+ * KEPT
105
+ * ✓ N modules transformed. — confirms the transform phase completed
106
+ * ✓ built in Xs — build timing (useful perf signal)
107
+ * Genuine warnings/errors — anything the user may need to act on
108
+ *
109
+ * SUPPRESSED (info)
110
+ * vite vX.Y.Z building... — Vite version banner
111
+ * transforming... / rendering chunks... / computing gzip size...
112
+ * Initially, there are N chunks... — Rollup minChunkSize diagnostics
113
+ * After merging chunks, there are...
114
+ * X are below minChunkSize.
115
+ * Blank lines
116
+ * Chunk/asset size table rows — e.g. " dist/client/assets/foo.js 42 kB"
117
+ * [rsc] / [ssr] / [client] / [worker] — RSC plugin env section headers
118
+ *
119
+ * SUPPRESSED (warn)
120
+ * "dynamic import will not move module into another chunk" — internal chunking note
121
+ * "X is not exported by virtual:vinext-*" — handled gracefully at runtime
95
122
  */
123
+ function createBuildLogger(vite) {
124
+ const logger = vite.createLogger("info", { allowClearScreen: false });
125
+ const originalInfo = logger.info.bind(logger);
126
+ const originalWarn = logger.warn.bind(logger);
127
+ // Strip ANSI escape codes for pattern matching (keep originals for output).
128
+ const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, ""); // eslint-disable-line no-control-regex
129
+ logger.info = (msg, options) => {
130
+ const plain = strip(msg);
131
+ // Always keep timing lines ("✓ built in 1.23s", "✓ 75 modules transformed.").
132
+ if (plain.trimStart().startsWith("✓")) {
133
+ originalInfo(msg, options);
134
+ return;
135
+ }
136
+ // Vite version banner: "vite v6.x.x building for production..."
137
+ if (/^vite v\d/.test(plain.trim()))
138
+ return;
139
+ // Rollup progress noise: "transforming...", "rendering chunks...", "computing gzip size..."
140
+ if (/^(transforming|rendering chunks|computing gzip size)/.test(plain.trim()))
141
+ return;
142
+ // Rollup minChunkSize diagnostics:
143
+ // "Initially, there are\n36 chunks, of which\n..."
144
+ // "After merging chunks, there are\n..."
145
+ // "X are below minChunkSize."
146
+ if (/^(Initially,|After merging|are below minChunkSize)/.test(plain.trim()))
147
+ return;
148
+ // Blank / whitespace-only separator lines.
149
+ if (/^\s*$/.test(plain))
150
+ return;
151
+ // Chunk/asset size table rows — e.g.:
152
+ // " dist/client/assets/foo.js 42.10 kB │ gzip: 6.74 kB" (TTY: indented)
153
+ // "dist/client/assets/foo.js 42.10 kB │ gzip: 6.74 kB" (non-TTY: at column 0)
154
+ // Both start with "dist/" (possibly preceded by whitespace).
155
+ if (/^\s*(dist\/|\.\/)/.test(plain))
156
+ return;
157
+ // @vitejs/plugin-rsc environment section headers ("[rsc]", "[ssr]", "[client]", "[worker]").
158
+ if (/^\s*\[(rsc|ssr|client|worker)\]/.test(plain))
159
+ return;
160
+ originalInfo(msg, options);
161
+ };
162
+ logger.warn = (msg, options) => {
163
+ const plain = strip(msg);
164
+ // Rollup: "dynamic import will not move module into another chunk" — this is
165
+ // emitted as a [plugin vite:reporter] warning with long absolute file paths.
166
+ // It's an internal chunking note, not actionable.
167
+ if (plain.includes("dynamic import will not move module into another chunk"))
168
+ return;
169
+ // Rollup: "X is not exported by Y" from virtual entry modules — these come from
170
+ // Rollup's static analysis of the generated virtual:vinext-server-entry when the
171
+ // user's middleware doesn't export the expected names. The vinext runtime handles
172
+ // missing exports gracefully, so this is noise.
173
+ if (plain.includes("is not exported by") && plain.includes("virtual:vinext"))
174
+ return;
175
+ originalWarn(msg, options);
176
+ };
177
+ return logger;
178
+ }
179
+ // ─── Auto-configuration ───────────────────────────────────────────────────────
96
180
  function hasAppDir() {
97
181
  return (fs.existsSync(path.join(process.cwd(), "app")) ||
98
182
  fs.existsSync(path.join(process.cwd(), "src", "app")));
@@ -102,7 +186,12 @@ function hasViteConfig() {
102
186
  fs.existsSync(path.join(process.cwd(), "vite.config.js")) ||
103
187
  fs.existsSync(path.join(process.cwd(), "vite.config.mjs")));
104
188
  }
105
- function buildViteConfig(overrides = {}) {
189
+ /**
190
+ * Build the Vite config automatically. If a vite.config.ts exists in the
191
+ * project, Vite will merge our config with it (theirs takes precedence).
192
+ * If there's no vite.config, this provides everything needed.
193
+ */
194
+ function buildViteConfig(overrides = {}, logger) {
106
195
  const hasConfig = hasViteConfig();
107
196
  // If a vite.config exists, let Vite load it — only set root and overrides.
108
197
  // The user's config already has vinext() + rsc() plugins configured.
@@ -111,6 +200,7 @@ function buildViteConfig(overrides = {}) {
111
200
  if (hasConfig) {
112
201
  return {
113
202
  root: process.cwd(),
203
+ ...(logger ? { customLogger: logger } : {}),
114
204
  ...overrides,
115
205
  };
116
206
  }
@@ -127,6 +217,7 @@ function buildViteConfig(overrides = {}) {
127
217
  resolve: {
128
218
  dedupe: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
129
219
  },
220
+ ...(logger ? { customLogger: logger } : {}),
130
221
  ...overrides,
131
222
  };
132
223
  return config;
@@ -193,6 +284,10 @@ async function buildApp() {
193
284
  const vite = await loadVite();
194
285
  console.log(`\n vinext build (Vite ${getViteVersion()})\n`);
195
286
  const isApp = hasAppDir();
287
+ // In verbose mode, skip the custom logger so raw Vite/Rollup output is shown.
288
+ const logger = parsed.verbose
289
+ ? vite.createLogger("info", { allowClearScreen: false })
290
+ : createBuildLogger(vite);
196
291
  // For App Router: upgrade React if needed for react-server-dom-webpack compatibility.
197
292
  // Without this, builds with react<19.2.4 produce a Worker that crashes at
198
293
  // runtime with "Cannot read properties of undefined (reading 'moduleMap')".
@@ -207,7 +302,7 @@ async function buildApp() {
207
302
  }
208
303
  if (isApp) {
209
304
  // App Router: use createBuilder for multi-environment RSC builds
210
- const config = buildViteConfig();
305
+ const config = buildViteConfig({}, logger);
211
306
  const builder = await vite.createBuilder(config);
212
307
  await builder.buildApp();
213
308
  }
@@ -227,7 +322,7 @@ async function buildApp() {
227
322
  treeshake: clientTreeshakeConfig,
228
323
  },
229
324
  },
230
- }));
325
+ }, logger));
231
326
  console.log(" Building server...");
232
327
  await vite.build(buildViteConfig({
233
328
  build: {
@@ -239,8 +334,9 @@ async function buildApp() {
239
334
  },
240
335
  },
241
336
  },
242
- }));
337
+ }, logger));
243
338
  }
339
+ await printBuildReport({ root: process.cwd() });
244
340
  console.log("\n Build complete. Run `vinext start` to start the production server.\n");
245
341
  }
246
342
  async function start() {
@@ -377,7 +473,8 @@ function printHelp(cmd) {
377
473
  runs the appropriate multi-environment build via Vite.
378
474
 
379
475
  Options:
380
- -h, --help Show this help
476
+ --verbose Show full Vite/Rollup build output (suppressed by default)
477
+ -h, --help Show this help
381
478
  `);
382
479
  return;
383
480
  }