vinext 0.0.20 → 0.0.22

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 (54) hide show
  1. package/dist/deploy.d.ts.map +1 -1
  2. package/dist/deploy.js +6 -3
  3. package/dist/deploy.js.map +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +233 -22
  6. package/dist/index.js.map +1 -1
  7. package/dist/routing/app-router.d.ts.map +1 -1
  8. package/dist/routing/app-router.js +1 -41
  9. package/dist/routing/app-router.js.map +1 -1
  10. package/dist/routing/pages-router.d.ts.map +1 -1
  11. package/dist/routing/pages-router.js +1 -27
  12. package/dist/routing/pages-router.js.map +1 -1
  13. package/dist/routing/utils.d.ts +25 -0
  14. package/dist/routing/utils.d.ts.map +1 -0
  15. package/dist/routing/utils.js +70 -0
  16. package/dist/routing/utils.js.map +1 -0
  17. package/dist/server/app-dev-server.d.ts.map +1 -1
  18. package/dist/server/app-dev-server.js +140 -6
  19. package/dist/server/app-dev-server.js.map +1 -1
  20. package/dist/server/dev-server.d.ts.map +1 -1
  21. package/dist/server/dev-server.js +77 -4
  22. package/dist/server/dev-server.js.map +1 -1
  23. package/dist/server/middleware.d.ts.map +1 -1
  24. package/dist/server/middleware.js +13 -4
  25. package/dist/server/middleware.js.map +1 -1
  26. package/dist/server/prod-server.d.ts.map +1 -1
  27. package/dist/server/prod-server.js +7 -0
  28. package/dist/server/prod-server.js.map +1 -1
  29. package/dist/server/request-log.d.ts +34 -0
  30. package/dist/server/request-log.d.ts.map +1 -0
  31. package/dist/server/request-log.js +65 -0
  32. package/dist/server/request-log.js.map +1 -0
  33. package/dist/shims/cache-runtime.d.ts.map +1 -1
  34. package/dist/shims/cache-runtime.js +5 -1
  35. package/dist/shims/cache-runtime.js.map +1 -1
  36. package/dist/shims/cache.d.ts +6 -0
  37. package/dist/shims/cache.d.ts.map +1 -1
  38. package/dist/shims/cache.js +22 -2
  39. package/dist/shims/cache.js.map +1 -1
  40. package/dist/shims/head.d.ts +11 -0
  41. package/dist/shims/head.d.ts.map +1 -1
  42. package/dist/shims/head.js +21 -0
  43. package/dist/shims/head.js.map +1 -1
  44. package/dist/shims/headers.d.ts +8 -0
  45. package/dist/shims/headers.d.ts.map +1 -1
  46. package/dist/shims/headers.js +41 -0
  47. package/dist/shims/headers.js.map +1 -1
  48. package/dist/shims/script.d.ts.map +1 -1
  49. package/dist/shims/script.js +7 -1
  50. package/dist/shims/script.js.map +1 -1
  51. package/dist/shims/server.d.ts.map +1 -1
  52. package/dist/shims/server.js +2 -1
  53. package/dist/shims/server.js.map +1 -1
  54. package/package.json +1 -1
@@ -294,6 +294,48 @@ function rscOnError(error) {
294
294
  if (error && typeof error === "object" && "digest" in error) {
295
295
  return String(error.digest);
296
296
  }
297
+
298
+ // In dev, detect the "Only plain objects" RSC serialization error and emit
299
+ // an actionable hint. This error occurs when a Server Component passes a
300
+ // class instance, ES module namespace object, or null-prototype object as a
301
+ // prop to a Client Component.
302
+ //
303
+ // Root cause: Vite bundles modules as true ESM (module namespace objects
304
+ // have a null-like internal slot), while Next.js's webpack build produces
305
+ // plain CJS-wrapped objects with __esModule:true. React's RSC serializer
306
+ // accepts the latter as plain objects but rejects the former — which means
307
+ // code that accidentally passes "import * as X" works in webpack/Next.js
308
+ // but correctly fails in vinext.
309
+ //
310
+ // Common triggers:
311
+ // - "import * as utils from './utils'" passed as a prop
312
+ // - class instances (new Foo()) passed as props
313
+ // - Date / Map / Set instances passed as props
314
+ // - Objects with Object.create(null) (null prototype)
315
+ if (
316
+ process.env.NODE_ENV !== "production" &&
317
+ error instanceof Error &&
318
+ error.message.includes("Only plain objects, and a few built-ins, can be passed to Client Components")
319
+ ) {
320
+ console.error(
321
+ "[vinext] RSC serialization error: a non-plain object was passed from a Server Component to a Client Component.\\n" +
322
+ "\\n" +
323
+ "Common causes:\\n" +
324
+ " * Passing a module namespace (import * as X) directly as a prop.\\n" +
325
+ " Unlike Next.js (webpack), Vite produces real ESM module namespace objects\\n" +
326
+ " which are not serializable. Fix: pass individual values instead,\\n" +
327
+ " e.g. <Comp value={module.value} />\\n" +
328
+ " * Passing a class instance (new Foo()) as a prop.\\n" +
329
+ " Fix: convert to a plain object, e.g. { id: foo.id, name: foo.name }\\n" +
330
+ " * Passing a Date, Map, or Set. Use .toISOString(), [...map.entries()], etc.\\n" +
331
+ " * Passing Object.create(null). Use { ...obj } to restore a prototype.\\n" +
332
+ "\\n" +
333
+ "Original error:",
334
+ error.message,
335
+ );
336
+ return undefined;
337
+ }
338
+
297
339
  // In production, generate a digest hash for non-navigation errors
298
340
  if (process.env.NODE_ENV === "production" && error) {
299
341
  const msg = error instanceof Error ? error.message : String(error);
@@ -1212,6 +1254,12 @@ export default async function handler(request) {
1212
1254
  }
1213
1255
 
1214
1256
  async function _handleRequest(request, __reqCtx) {
1257
+ const __reqStart = process.env.NODE_ENV !== "production" ? performance.now() : 0;
1258
+ let __compileEnd;
1259
+ let __renderEnd;
1260
+ // __reqStart is included in the timing header so the Node logging middleware
1261
+ // can compute true compile time as: handlerStart - middlewareStart.
1262
+ // Format: "handlerStart,compileMs,renderMs" - all as integers (ms). Dev-only.
1215
1263
  const url = new URL(request.url);
1216
1264
 
1217
1265
  // ── Cross-origin request protection ─────────────────────────────────
@@ -1258,7 +1306,11 @@ async function _handleRequest(request, __reqCtx) {
1258
1306
 
1259
1307
  // ── Apply redirects from next.config.js ───────────────────────────────
1260
1308
  if (__configRedirects.length) {
1261
- const __redir = __applyConfigRedirects(pathname, __reqCtx);
1309
+ // Strip .rsc suffix before matching redirect rules - RSC (client-side nav) requests
1310
+ // arrive as /some/path.rsc but redirect patterns are defined without it (e.g.
1311
+ // /some/path). Without this, soft-nav fetches bypass all config redirects.
1312
+ const __redirPathname = pathname.endsWith(".rsc") ? pathname.slice(0, -4) : pathname;
1313
+ const __redir = __applyConfigRedirects(__redirPathname, __reqCtx);
1262
1314
  if (__redir) {
1263
1315
  const __redirDest = __sanitizeDestination(
1264
1316
  __basePath && !__redir.destination.startsWith(__basePath)
@@ -1274,7 +1326,9 @@ async function _handleRequest(request, __reqCtx) {
1274
1326
 
1275
1327
  // ── Apply beforeFiles rewrites from next.config.js ────────────────────
1276
1328
  if (__configRewrites.beforeFiles && __configRewrites.beforeFiles.length) {
1277
- const __rewritten = __applyConfigRewrites(pathname, __configRewrites.beforeFiles, __reqCtx);
1329
+ // Strip .rsc suffix before matching rewrite rules — same reason as redirects above.
1330
+ const __rewritePathname = pathname.endsWith(".rsc") ? pathname.slice(0, -4) : pathname;
1331
+ const __rewritten = __applyConfigRewrites(__rewritePathname, __configRewrites.beforeFiles, __reqCtx);
1278
1332
  if (__rewritten) {
1279
1333
  if (__isExternalUrl(__rewritten)) {
1280
1334
  setHeadersContext(null);
@@ -2056,6 +2110,9 @@ async function _handleRequest(request, __reqCtx) {
2056
2110
  console.error = _origConsoleError;
2057
2111
  }
2058
2112
 
2113
+ // Mark end of compile phase: route matching, middleware, tree building are done.
2114
+ if (process.env.NODE_ENV !== "production") __compileEnd = performance.now();
2115
+
2059
2116
  // Render to RSC stream
2060
2117
  const rscStream = renderToReadableStream(element, { onError: rscOnError });
2061
2118
 
@@ -2084,6 +2141,21 @@ async function _handleRequest(request, __reqCtx) {
2084
2141
  responseHeaders[key] = value;
2085
2142
  }
2086
2143
  }
2144
+ // Attach internal timing header so the dev server middleware can log it.
2145
+ // Format: "handlerStart,compileMs,renderMs"
2146
+ // handlerStart - absolute performance.now() when _handleRequest began,
2147
+ // used by the logging middleware to compute true compile
2148
+ // time as (handlerStart - middlewareReqStart).
2149
+ // compileMs - time inside the handler before renderToReadableStream.
2150
+ // -1 sentinel means compile time is not measured.
2151
+ // renderMs - -1 sentinel for RSC-only (soft-nav) responses, since
2152
+ // rendering is handled asynchronously by the client. The
2153
+ // logging middleware computes render time as totalMs - compileMs.
2154
+ if (process.env.NODE_ENV !== "production") {
2155
+ const handlerStart = Math.round(__reqStart);
2156
+ const compileMs = __compileEnd !== undefined ? Math.round(__compileEnd - __reqStart) : -1;
2157
+ responseHeaders["x-vinext-timing"] = handlerStart + "," + compileMs + ",-1";
2158
+ }
2087
2159
  return new Response(rscStream, { status: _middlewareRewriteStatus || 200, headers: responseHeaders });
2088
2160
  }
2089
2161
 
@@ -2110,6 +2182,8 @@ async function _handleRequest(request, __reqCtx) {
2110
2182
  try {
2111
2183
  const ssrEntry = await import.meta.viteRsc.loadModule("ssr", "index");
2112
2184
  htmlStream = await ssrEntry.handleSsr(rscStream, _getNavigationContext(), fontData);
2185
+ // Shell render complete; Suspense boundaries stream asynchronously
2186
+ if (process.env.NODE_ENV !== "production") __renderEnd = performance.now();
2113
2187
  } catch (ssrErr) {
2114
2188
  const specialResponse = await handleRenderError(ssrErr);
2115
2189
  if (specialResponse) return specialResponse;
@@ -2140,6 +2214,22 @@ async function _handleRequest(request, __reqCtx) {
2140
2214
  response.headers.append(key, value);
2141
2215
  }
2142
2216
  }
2217
+ // Attach internal timing header so the dev server middleware can log it.
2218
+ // Format: "handlerStart,compileMs,renderMs"
2219
+ // handlerStart - absolute performance.now() when _handleRequest began,
2220
+ // used by the logging middleware to compute true compile
2221
+ // time as (handlerStart - middlewareReqStart).
2222
+ // compileMs - time inside the handler before renderToReadableStream.
2223
+ // renderMs - time from renderToReadableStream to handleSsr completion,
2224
+ // or -1 sentinel if not measured (falls back to totalMs - compileMs).
2225
+ if (process.env.NODE_ENV !== "production") {
2226
+ const handlerStart = Math.round(__reqStart);
2227
+ const compileMs = __compileEnd !== undefined ? Math.round(__compileEnd - __reqStart) : -1;
2228
+ const renderMs = __renderEnd !== undefined && __compileEnd !== undefined
2229
+ ? Math.round(__renderEnd - __compileEnd)
2230
+ : -1;
2231
+ response.headers.set("x-vinext-timing", handlerStart + "," + compileMs + "," + renderMs);
2232
+ }
2143
2233
  // Apply custom status code from middleware rewrite
2144
2234
  if (_middlewareRewriteStatus) {
2145
2235
  return new Response(response.body, {
@@ -2257,6 +2347,20 @@ async function collectStreamChunks(stream) {
2257
2347
  return chunks;
2258
2348
  }
2259
2349
 
2350
+ // React 19 dev-mode workaround (see VinextFlightRoot in handleSsr):
2351
+ //
2352
+ // In dev, Flight error decoding in react-server-dom-webpack/client.edge
2353
+ // can hit resolveErrorDev() which (via React's dev error stack capture)
2354
+ // expects a non-null hooks dispatcher.
2355
+ //
2356
+ // Vinext previously called createFromReadableStream() outside of any React render.
2357
+ // When an RSC stream contains an error, dev-mode decoding could crash with:
2358
+ // - "Invalid hook call"
2359
+ // - "Cannot read properties of null (reading 'useContext')"
2360
+ //
2361
+ // Fix: call createFromReadableStream() lazily inside a React component render.
2362
+ // This mirrors Next.js behavior and ensures the dispatcher is set.
2363
+
2260
2364
  /**
2261
2365
  * Create a TransformStream that appends RSC chunks as inline <script> tags
2262
2366
  * to the HTML stream. This allows progressive hydration — the browser receives
@@ -2391,7 +2495,18 @@ export async function handleSsr(rscStream, navContext, fontData) {
2391
2495
  // immediately in the HTML shell, then stream in resolved content as RSC
2392
2496
  // chunks arrive. Awaiting here would block until all async server components
2393
2497
  // complete, collapsing the streaming behavior.
2394
- const root = createFromReadableStream(ssrStream);
2498
+ // Lazily create the Flight root inside render so React's hook dispatcher is set
2499
+ // (avoids React 19 dev-mode resolveErrorDev() crash). VinextFlightRoot returns
2500
+ // a thenable (not a ReactNode), which React 19 consumes via its internal
2501
+ // thenable-as-child suspend/resume behavior. This matches Next.js's approach.
2502
+ let flightRoot;
2503
+ function VinextFlightRoot() {
2504
+ if (!flightRoot) {
2505
+ flightRoot = createFromReadableStream(ssrStream);
2506
+ }
2507
+ return flightRoot;
2508
+ }
2509
+ const root = _ssrCE(VinextFlightRoot);
2395
2510
 
2396
2511
  // Wrap with ServerInsertedHTMLContext.Provider so libraries that use
2397
2512
  // useContext(ServerInsertedHTMLContext) (Apollo Client, styled-components,
@@ -2446,11 +2561,11 @@ export async function handleSsr(rscStream, navContext, fontData) {
2446
2561
  const insertedElements = flushServerInsertedHTML();
2447
2562
 
2448
2563
  // Render the inserted elements to HTML strings
2449
- const { createElement, Fragment } = await import("react");
2564
+ const { Fragment } = await import("react");
2450
2565
  let insertedHTML = "";
2451
2566
  for (const el of insertedElements) {
2452
2567
  try {
2453
- insertedHTML += renderToStaticMarkup(createElement(Fragment, null, el));
2568
+ insertedHTML += renderToStaticMarkup(_ssrCE(Fragment, null, el));
2454
2569
  } catch {
2455
2570
  // Skip elements that can't be rendered
2456
2571
  }
@@ -2865,7 +2980,12 @@ async function main() {
2865
2980
  // Checks the prefetch cache (populated by <Link> IntersectionObserver and
2866
2981
  // router.prefetch()) before making a network request. This makes navigation
2867
2982
  // near-instant for prefetched routes.
2868
- window.__VINEXT_RSC_NAVIGATE__ = async function navigateRsc(href) {
2983
+ window.__VINEXT_RSC_NAVIGATE__ = async function navigateRsc(href, __redirectDepth) {
2984
+ if ((__redirectDepth || 0) > 10) {
2985
+ console.error("[vinext] Too many RSC redirects — aborting navigation to prevent infinite loop.");
2986
+ window.location.href = href;
2987
+ return;
2988
+ }
2869
2989
  try {
2870
2990
  const url = new URL(href, window.location.origin);
2871
2991
  const rscUrl = toRscUrl(url.pathname + url.search);
@@ -2891,6 +3011,20 @@ async function main() {
2891
3011
  });
2892
3012
  }
2893
3013
 
3014
+ // Detect if fetch followed a redirect: compare the final response URL to
3015
+ // what we requested. If they differ, the server issued a 3xx — push the
3016
+ // canonical destination URL into history before rendering.
3017
+ const __finalUrl = new URL(navResponse.url);
3018
+ const __requestedUrl = new URL(rscUrl, window.location.origin);
3019
+ if (__finalUrl.pathname !== __requestedUrl.pathname) {
3020
+ // Strip .rsc suffix from the final URL to get the page path for history.
3021
+ // Use replaceState instead of pushState: the caller (navigateImpl) already
3022
+ // pushed the pre-redirect URL; replacing it avoids a stale history entry.
3023
+ const __destPath = __finalUrl.pathname.replace(/\\.rsc$/, "") + __finalUrl.search;
3024
+ window.history.replaceState(null, "", __destPath);
3025
+ return window.__VINEXT_RSC_NAVIGATE__(__destPath, (__redirectDepth || 0) + 1);
3026
+ }
3027
+
2894
3028
  // Update useParams() with route params from the server before re-rendering
2895
3029
  const navParamsHeader = navResponse.headers.get("X-Vinext-Params");
2896
3030
  if (navParamsHeader) {