vinext 1.2.0 → 1.2.1

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 (110) hide show
  1. package/README.md +1 -1
  2. package/dist/build/output-root-externals.d.ts +21 -0
  3. package/dist/build/output-root-externals.js +40 -0
  4. package/dist/build/prerender-paths.js +1 -1
  5. package/dist/build/prerender.js +1 -1
  6. package/dist/check.js +5 -6
  7. package/dist/cli-dev-config.js +3 -3
  8. package/dist/cli.js +1 -1
  9. package/dist/config/config-matchers.js +12 -7
  10. package/dist/index.js +113 -94
  11. package/dist/init.d.ts +1 -1
  12. package/dist/init.js +86 -6
  13. package/dist/plugins/ast-utils.d.ts +53 -2
  14. package/dist/plugins/ast-utils.js +90 -2
  15. package/dist/plugins/cloudflare-dev-module-urls.d.ts +41 -0
  16. package/dist/plugins/cloudflare-dev-module-urls.js +271 -0
  17. package/dist/plugins/commonjs-esm-facade.js +2 -2
  18. package/dist/plugins/css-module-imports.js +13 -20
  19. package/dist/plugins/dynamic-preload-metadata.js +2 -3
  20. package/dist/plugins/extensionless-dynamic-import.js +2 -3
  21. package/dist/plugins/fonts.d.ts +1 -1
  22. package/dist/plugins/fonts.js +3 -3
  23. package/dist/plugins/ignore-dynamic-requests.js +2 -3
  24. package/dist/plugins/import-meta-url.js +3 -4
  25. package/dist/plugins/instrumentation-client.js +3 -2
  26. package/dist/plugins/middleware-export-validation.js +2 -3
  27. package/dist/plugins/og-harfbuzz.js +2 -3
  28. package/dist/plugins/optimize-imports.js +3 -4
  29. package/dist/plugins/pages-node-externals.js +2 -3
  30. package/dist/plugins/remove-console.js +2 -18
  31. package/dist/plugins/require-condition-resolution.js +3 -3
  32. package/dist/plugins/require-context.js +2 -3
  33. package/dist/plugins/strip-server-exports.js +3 -4
  34. package/dist/plugins/styled-jsx.js +2 -3
  35. package/dist/plugins/use-cache-callable.d.ts +21 -2
  36. package/dist/plugins/use-cache-callable.js +76 -33
  37. package/dist/plugins/worker-image-imports.js +30 -33
  38. package/dist/plugins/worker-use-server.js +38 -14
  39. package/dist/server/api-handler.js +1 -1
  40. package/dist/server/app-action-forwarding.js +2 -2
  41. package/dist/server/app-browser-entry.js +3 -3
  42. package/dist/server/app-browser-interception-context.js +1 -1
  43. package/dist/server/app-browser-prefetch-response.js +2 -2
  44. package/dist/server/app-browser-state.js +1 -1
  45. package/dist/server/app-browser-supplemental-refresh.js +1 -1
  46. package/dist/server/app-optimistic-routing.js +1 -1
  47. package/dist/server/app-page-dispatch.js +1 -1
  48. package/dist/server/app-page-render.js +1 -1
  49. package/dist/server/app-pages-bridge.js +1 -1
  50. package/dist/server/app-prerender-endpoints.js +1 -1
  51. package/dist/server/app-render-abort-error.d.ts +19 -0
  52. package/dist/server/app-render-abort-error.js +31 -1
  53. package/dist/server/app-request-stage-independent-entry.js +5 -5
  54. package/dist/server/app-response-stage-entry.js +2 -2
  55. package/dist/server/app-route-handler-runtime.js +1 -1
  56. package/dist/server/app-router-entry.js +5 -5
  57. package/dist/server/app-rsc-cache-busting.d.ts +16 -0
  58. package/dist/server/app-rsc-cache-busting.js +29 -12
  59. package/dist/server/app-rsc-handler.js +5 -5
  60. package/dist/server/app-rsc-request-normalization.js +3 -3
  61. package/dist/server/app-rsc-response-stage.js +2 -2
  62. package/dist/server/app-server-action-execution.js +1 -1
  63. package/dist/server/app-ssr-entry.js +5 -4
  64. package/dist/server/app-visited-response-cache.js +2 -2
  65. package/dist/server/config-headers.js +1 -1
  66. package/dist/server/defer-until-stream-consumed.d.ts +2 -1
  67. package/dist/server/defer-until-stream-consumed.js +3 -1
  68. package/dist/server/dev-server.js +1 -1
  69. package/dist/server/image-optimization.js +1 -0
  70. package/dist/server/metadata-route-response.js +2 -2
  71. package/dist/server/middleware-path-to-regexp.js +1 -1
  72. package/dist/server/middleware-runtime.js +5 -5
  73. package/dist/server/navigation-planner.js +1 -1
  74. package/dist/server/open-redirect.d.ts +11 -4
  75. package/dist/server/open-redirect.js +11 -4
  76. package/dist/server/pages-api-route.js +2 -2
  77. package/dist/server/pages-data-route.js +2 -1
  78. package/dist/server/pages-page-data.js +1 -1
  79. package/dist/server/pages-page-handler.js +1 -1
  80. package/dist/server/pages-page-response.js +1 -1
  81. package/dist/server/pages-request-pipeline.js +5 -5
  82. package/dist/server/pages-request-stage-entry.js +4 -4
  83. package/dist/server/pages-revalidate.js +2 -2
  84. package/dist/server/pages-router-entry.js +1 -1
  85. package/dist/server/prod-server.js +5 -5
  86. package/dist/server/rsc-stream-hints.d.ts +1 -1
  87. package/dist/server/rsc-stream-hints.js +21 -3
  88. package/dist/server/worker-image-optimization.js +19 -6
  89. package/dist/shims/head.js +1 -1
  90. package/dist/shims/headers.js +2 -2
  91. package/dist/shims/internal/hybrid-client-route-owner-direct.js +1 -1
  92. package/dist/shims/internal/hybrid-client-route-owner.js +1 -0
  93. package/dist/shims/link-prefetch.js +5 -4
  94. package/dist/shims/link.js +2 -2
  95. package/dist/shims/navigation.js +4 -5
  96. package/dist/shims/router.js +9 -12
  97. package/dist/shims/server.d.ts +11 -4
  98. package/dist/shims/server.js +41 -5
  99. package/dist/shims/url-utils.d.ts +7 -0
  100. package/dist/shims/url-utils.js +12 -3
  101. package/dist/utils/domain-locale.js +1 -1
  102. package/dist/utils/plugin-rsc-version.d.ts +2 -0
  103. package/dist/utils/plugin-rsc-version.js +28 -2
  104. package/dist/utils/react-version.d.ts +7 -0
  105. package/dist/utils/react-version.js +39 -16
  106. package/package.json +6 -6
  107. package/dist/build/nitro-route-rules.d.ts +0 -47
  108. package/dist/build/nitro-route-rules.js +0 -79
  109. package/dist/plugins/cache-flight-codec.d.ts +0 -19
  110. package/dist/plugins/cache-flight-codec.js +0 -68
@@ -1,9 +1,9 @@
1
1
  import { isUnknownRecord } from "../utils/record.js";
2
2
  import { parseCookieHeader } from "../utils/parse-cookie.js";
3
3
  import { isExternalUrl } from "../utils/external-url.js";
4
+ import { hasBasePath, removeTrailingSlash, stripBasePath } from "../utils/base-path.js";
4
5
  import { substituteDestinationParams } from "../config/destination-params.js";
5
6
  import { sortRoutes, splitPathSegments } from "../routing/utils.js";
6
- import { hasBasePath, removeTrailingSlash, stripBasePath } from "../utils/base-path.js";
7
7
  import { assertSafeNavigationUrl } from "./url-safety.js";
8
8
  import { addLocalePrefix, getDomainLocaleUrl, getLocalePathPrefix } from "../utils/domain-locale.js";
9
9
  import { AppRouterContext } from "./internal/app-router-context.js";
@@ -935,7 +935,6 @@ function isAppComponent(value) {
935
935
  function resolveSameOriginRedirectedUrl(responseUrl) {
936
936
  const appPath = toSameOriginAppPath(responseUrl, __basePath);
937
937
  if (appPath === null) return null;
938
- if (appPath.startsWith("//")) return normalizePathTrailingSlash(responseUrl, __trailingSlash);
939
938
  return normalizePathTrailingSlash(appPath, __trailingSlash);
940
939
  }
941
940
  function stripLocalePrefixForApiRedirect(appPath) {
@@ -957,19 +956,13 @@ function resolveLocalRedirectUrl(location) {
957
956
  let appPath;
958
957
  if (location.startsWith("/") && !location.startsWith("//")) try {
959
958
  const parsed = new URL(location, "http://vinext.local");
960
- appPath = stripBasePath(parsed.pathname, __basePath) + parsed.search + parsed.hash;
959
+ const pathname = stripBasePath(parsed.pathname, __basePath);
960
+ appPath = pathname.startsWith("//") ? toSameOriginAppPath(new URL(location, window.location.href).href, __basePath) : pathname + parsed.search + parsed.hash;
961
961
  } catch {
962
962
  appPath = location;
963
963
  }
964
964
  else appPath = toSameOriginAppPath(location, __basePath);
965
965
  if (appPath === null) return null;
966
- if (appPath.startsWith("//")) try {
967
- const browserUrl = new URL(location, window.location.href);
968
- if (browserUrl.origin !== getWindowOrigin()) return null;
969
- return normalizePathTrailingSlash(browserUrl.href, __trailingSlash);
970
- } catch {
971
- return null;
972
- }
973
966
  return normalizePathTrailingSlash(stripLocalePrefixForApiRedirect(appPath), __trailingSlash);
974
967
  }
975
968
  function hasClientRewriteRules() {
@@ -1816,6 +1809,10 @@ async function performNavigation(url, as, options, mode, onStateUpdate, redirect
1816
1809
  const absoluteHistoryUrl = sameOriginPath === null ? void 0 : origin && inputHref.startsWith(origin) ? inputHref.slice(origin.length) : sameOriginPath;
1817
1810
  const currentRoute = window.__NEXT_DATA__?.page ?? stripBasePath(window.location.pathname, __basePath);
1818
1811
  const currentAs = stripBasePath(window.location.pathname, __basePath) + window.location.search;
1812
+ const resolveRelativeRoutePath = (href) => href === "" || /^[/?#]/.test(href) || isAbsoluteUrl(href) ? href : resolvePagesRouterHref(href, {
1813
+ pathname: currentRoute,
1814
+ asPath: currentAs
1815
+ }, __basePath);
1819
1816
  const preparedHistoryHref = (isAbsoluteUrl(inputHref) ? toSameOriginAppPath(inputHref, __basePath) : null) ?? absoluteHistoryUrl ?? resolvePagesRouterHref(inputHref, {
1820
1817
  pathname: currentRoute,
1821
1818
  asPath: currentAs
@@ -1825,12 +1822,12 @@ async function performNavigation(url, as, options, mode, onStateUpdate, redirect
1825
1822
  const replaceInheritedLocale = options?.locale !== void 0 && (!as ? urlInheritsCurrentPath || typeof url !== "string" && inheritsVisiblePath(url) : typeof as !== "string" && inheritsVisiblePath(as));
1826
1823
  const credentialedHistoryHref = credentialedSameOriginHref(url);
1827
1824
  const credentialedDisplayHref = credentialedSameOriginHref(as) ?? (!as ? credentialedHistoryHref : void 0);
1828
- as = as ? resolveUrl(as, true) : void 0;
1825
+ as = as ? resolveRelativeRoutePath(resolveUrl(as, true)) : void 0;
1829
1826
  assertSafeNavigationUrl(resolveUrl(url));
1830
1827
  if (as !== void 0) assertSafeNavigationUrl(as);
1831
1828
  const isHydrationQueryUpdate = options?._h === 1;
1832
1829
  const navigationLocale = resolveTransitionLocale(options?.locale);
1833
- const unlocalizedRouteHref = resolveUrl(isQueryOnlyString ? `${stripBasePath(window.location.pathname, __basePath)}${url}` : url);
1830
+ const unlocalizedRouteHref = resolveRelativeRoutePath(resolveUrl(isQueryOnlyString ? `${stripBasePath(window.location.pathname, __basePath)}${url}` : url));
1834
1831
  const localRouteHref = isExternalUrl(unlocalizedRouteHref) ? toSameOriginAppPath(unlocalizedRouteHref, __basePath) : unlocalizedRouteHref;
1835
1832
  const implicitDynamicRoute = !isHydrationQueryUpdate && as === void 0 && localRouteHref !== null ? resolveDynamicRouteHref(localRouteHref) : null;
1836
1833
  const routeForNavigation = implicitDynamicRoute?.as ?? unlocalizedRouteHref;
@@ -42,13 +42,20 @@ export declare class NextRequest extends Request {
42
42
  get page(): void;
43
43
  get ua(): void;
44
44
  /**
45
- * Client IP address. Prefers Cloudflare's trusted CF-Connecting-IP header
46
- * over the spoofable X-Forwarded-For. Returns undefined if unavailable.
45
+ * Client IP address. Next.js removed `ip` in v15; vinext keeps it as an
46
+ * extension. Returns undefined unless the value comes from a trusted source.
47
+ *
48
+ * On Cloudflare Workers (the request carries `cf` metadata), Cloudflare's
49
+ * edge sets `CF-Connecting-IP`. Elsewhere these headers are client-supplied
50
+ * and are only honored behind a trusted proxy (`VINEXT_TRUST_PROXY` /
51
+ * `VINEXT_TRUSTED_HOSTS`), matching the `X-Forwarded-*` policy.
47
52
  */
48
53
  get ip(): string | undefined;
49
54
  /**
50
- * Geolocation data. Platform-dependent (e.g., Cloudflare, Vercel).
51
- * Returns undefined if not available.
55
+ * Geolocation data. Next.js removed `geo` in v15; vinext keeps it as an
56
+ * extension. Read from Cloudflare's `request.cf` metadata on Workers, or
57
+ * from platform geo headers behind a trusted proxy. Returns undefined
58
+ * otherwise, because any client can send these headers.
52
59
  */
53
60
  get geo(): {
54
61
  city?: string;
@@ -3,10 +3,11 @@ import { MIDDLEWARE_SET_COOKIE_HEADER } from "../utils/protocol-headers.js";
3
3
  import { encodeMiddlewareRequestHeaders } from "../utils/middleware-request-headers.js";
4
4
  import { parseEdgeRequestCookieHeader } from "../utils/parse-cookie.js";
5
5
  import { hasBasePath, removeTrailingSlash, stripBasePath } from "../utils/base-path.js";
6
- import { getRequestExecutionContext } from "./request-context.js";
7
6
  import { MIDDLEWARE_NEXT_HEADER, MIDDLEWARE_REWRITE_HEADER } from "../server/headers.js";
7
+ import { getRequestExecutionContext } from "./request-context.js";
8
8
  import { validateCookieAttributeValue, validateCookieName } from "./internal/cookie-serialize.js";
9
9
  import { assertSafeNavigationUrl } from "./url-safety.js";
10
+ import { trustProxy } from "../server/proxy-trust.js";
10
11
  import { userAgent, userAgentFromString } from "./user-agent.js";
11
12
  //#region src/shims/server.ts
12
13
  /**
@@ -97,6 +98,19 @@ function initWithoutInputBody(input, init) {
97
98
  body: null
98
99
  };
99
100
  }
101
+ /**
102
+ * workerd's `request.cf` metadata. Only the Workers runtime sets it, so its
103
+ * presence means the request came through Cloudflare's edge rather than
104
+ * straight from a client.
105
+ */
106
+ function readCloudflareMetadata(request) {
107
+ const cf = Reflect.get(request, "cf");
108
+ return cf !== null && typeof cf === "object" ? cf : void 0;
109
+ }
110
+ function readStringField(source, key) {
111
+ const value = Reflect.get(source, key);
112
+ return typeof value === "string" && value !== "" ? value : void 0;
113
+ }
100
114
  var NextRequest = class extends Request {
101
115
  _nextUrl;
102
116
  _url;
@@ -155,17 +169,39 @@ var NextRequest = class extends Request {
155
169
  throw new Error("NextRequest.ua has been removed; use userAgent() instead");
156
170
  }
157
171
  /**
158
- * Client IP address. Prefers Cloudflare's trusted CF-Connecting-IP header
159
- * over the spoofable X-Forwarded-For. Returns undefined if unavailable.
172
+ * Client IP address. Next.js removed `ip` in v15; vinext keeps it as an
173
+ * extension. Returns undefined unless the value comes from a trusted source.
174
+ *
175
+ * On Cloudflare Workers (the request carries `cf` metadata), Cloudflare's
176
+ * edge sets `CF-Connecting-IP`. Elsewhere these headers are client-supplied
177
+ * and are only honored behind a trusted proxy (`VINEXT_TRUST_PROXY` /
178
+ * `VINEXT_TRUSTED_HOSTS`), matching the `X-Forwarded-*` policy.
160
179
  */
161
180
  get ip() {
181
+ if (readCloudflareMetadata(this)) return this.headers.get("cf-connecting-ip") ?? void 0;
182
+ if (!trustProxy) return void 0;
162
183
  return this.headers.get("cf-connecting-ip") ?? this.headers.get("x-real-ip") ?? this.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? void 0;
163
184
  }
164
185
  /**
165
- * Geolocation data. Platform-dependent (e.g., Cloudflare, Vercel).
166
- * Returns undefined if not available.
186
+ * Geolocation data. Next.js removed `geo` in v15; vinext keeps it as an
187
+ * extension. Read from Cloudflare's `request.cf` metadata on Workers, or
188
+ * from platform geo headers behind a trusted proxy. Returns undefined
189
+ * otherwise, because any client can send these headers.
167
190
  */
168
191
  get geo() {
192
+ const cf = readCloudflareMetadata(this);
193
+ if (cf) {
194
+ const country = readStringField(cf, "country");
195
+ if (!country) return void 0;
196
+ return {
197
+ country,
198
+ city: readStringField(cf, "city"),
199
+ region: readStringField(cf, "region"),
200
+ latitude: readStringField(cf, "latitude"),
201
+ longitude: readStringField(cf, "longitude")
202
+ };
203
+ }
204
+ if (!trustProxy) return void 0;
169
205
  const country = this.headers.get("cf-ipcountry") ?? this.headers.get("x-vercel-ip-country") ?? void 0;
170
206
  if (!country) return void 0;
171
207
  return {
@@ -2,6 +2,13 @@
2
2
  export declare function isAbsoluteUrl(url: string): boolean;
3
3
  export declare function isAbsoluteOrProtocolRelativeUrl(url: string): boolean;
4
4
  export declare function getWindowOrigin(): string | null;
5
+ /**
6
+ * A same-origin pathname can start with "//" (e.g. `https://app.com//evil.com`),
7
+ * and as a local path that is a protocol-relative reference to another host.
8
+ * Collapse the leading slashes: Next.js does the same for these URLs, on the
9
+ * client (`resolveHref`) or with its server's repeated-slash 308.
10
+ */
11
+ export declare function collapseLeadingSlashes(pathname: string): string;
5
12
  /**
6
13
  * If `url` is an absolute same-origin URL, return the local path
7
14
  * (pathname + search + hash). Returns null for truly external URLs
@@ -25,6 +25,15 @@ function getWindowOrigin() {
25
25
  }
26
26
  }
27
27
  /**
28
+ * A same-origin pathname can start with "//" (e.g. `https://app.com//evil.com`),
29
+ * and as a local path that is a protocol-relative reference to another host.
30
+ * Collapse the leading slashes: Next.js does the same for these URLs, on the
31
+ * client (`resolveHref`) or with its server's repeated-slash 308.
32
+ */
33
+ function collapseLeadingSlashes(pathname) {
34
+ return pathname.replace(/^\/{2,}/, "/");
35
+ }
36
+ /**
28
37
  * If `url` is an absolute same-origin URL, return the local path
29
38
  * (pathname + search + hash). Returns null for truly external URLs
30
39
  * or on the server (where origin is unknown).
@@ -34,7 +43,7 @@ function toSameOriginPath(url) {
34
43
  if (!origin) return null;
35
44
  try {
36
45
  const parsed = url.startsWith("//") ? new URL(url, origin) : new URL(url);
37
- if (parsed.origin === origin) return parsed.pathname + parsed.search + parsed.hash;
46
+ if (parsed.origin === origin) return collapseLeadingSlashes(parsed.pathname) + parsed.search + parsed.hash;
38
47
  } catch {}
39
48
  return null;
40
49
  }
@@ -49,7 +58,7 @@ function toSameOriginAppPath(url, basePath) {
49
58
  try {
50
59
  const parsed = new URL(localPath, "http://vinext.local");
51
60
  if (!hasBasePath(parsed.pathname, basePath)) return null;
52
- return stripBasePath(parsed.pathname, basePath) + parsed.search + parsed.hash;
61
+ return collapseLeadingSlashes(stripBasePath(parsed.pathname, basePath)) + parsed.search + parsed.hash;
53
62
  } catch {
54
63
  return localPath;
55
64
  }
@@ -164,4 +173,4 @@ function isHashOnlyBrowserUrlChange(href, currentHref, basePath = "") {
164
173
  }
165
174
  }
166
175
  //#endregion
167
- export { getWindowOrigin, isAbsoluteOrProtocolRelativeUrl, isAbsoluteUrl, isHashOnlyBrowserUrlChange, normalizePathTrailingSlash, resolveRelativeHref, toBrowserNavigationHref, toCanonicalBrowserNavigationHref, toSameOriginAppPath, toSameOriginPath, withBasePath };
176
+ export { collapseLeadingSlashes, getWindowOrigin, isAbsoluteOrProtocolRelativeUrl, isAbsoluteUrl, isHashOnlyBrowserUrlChange, normalizePathTrailingSlash, resolveRelativeHref, toBrowserNavigationHref, toCanonicalBrowserNavigationHref, toSameOriginAppPath, toSameOriginPath, withBasePath };
@@ -42,7 +42,7 @@ function getDomainLocaleUrl(url, locale, { basePath, currentHostname, domainItem
42
42
  const targetDomain = detectDomainLocale(domainItems, void 0, locale);
43
43
  if (!targetDomain) return void 0;
44
44
  const currentDomain = detectDomainLocale(domainItems, currentHostname ?? void 0);
45
- const localizedPath = addLocalePrefix(url, locale, targetDomain.defaultLocale);
45
+ const localizedPath = addLocalePrefix(/^[/?#]/.test(url) ? url : `/${url}`, locale, targetDomain.defaultLocale);
46
46
  if (currentDomain && normalizeDomainHostname(currentDomain.domain) === normalizeDomainHostname(targetDomain.domain)) return;
47
47
  return `${`http${targetDomain.http ? "" : "s"}://`}${targetDomain.domain}${prefixBasePath(localizedPath, basePath)}`;
48
48
  }
@@ -1,4 +1,6 @@
1
1
  //#region src/utils/plugin-rsc-version.d.ts
2
2
  /** Return the installed @vitejs/plugin-rsc version, or throw if it is unsupported. */
3
3
  export declare function assertPluginRscVersion(resolvedRscPath: string): string;
4
+ /** Whether the package owning `dir` must install its own react-server-dom-webpack. */
5
+ export declare function needsAppReactServerDom(dir: string, pluginRscVersion: string): boolean;
4
6
  //#endregion
@@ -1,4 +1,7 @@
1
- import { findPackageVersion, isVersionBelow } from "./react-version.js";
1
+ import path from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
2
+ import { MINIMUM_REACT_VERSION, findPackageVersion, isVersionBelow } from "./react-version.js";
3
+ import { createRequire } from "node:module";
4
+ import fs from "node:fs";
2
5
  //#region src/utils/plugin-rsc-version.ts
3
6
  const MINIMUM_PLUGIN_RSC_VERSION = [
4
7
  0,
@@ -11,5 +14,28 @@ function assertPluginRscVersion(resolvedRscPath) {
11
14
  if (!version || isVersionBelow(version, MINIMUM_PLUGIN_RSC_VERSION)) throw new Error(`vinext: App Router requires @vitejs/plugin-rsc ${MINIMUM_PLUGIN_RSC_VERSION.join(".")} or newer (found ${version ?? "an unknown version"}). Upgrade @vitejs/plugin-rsc.`);
12
15
  return version;
13
16
  }
17
+ const FIRST_PLUGIN_RSC_WITH_SUPPORTED_RSDW = [
18
+ 0,
19
+ 5,
20
+ 38
21
+ ];
22
+ /** Whether the package owning `dir` must install its own react-server-dom-webpack. */
23
+ function needsAppReactServerDom(dir, pluginRscVersion) {
24
+ if (!isVersionBelow(pluginRscVersion, FIRST_PLUGIN_RSC_WITH_SUPPORTED_RSDW)) return false;
25
+ let manifest = path.join(dir, "package.json");
26
+ while (!fs.existsSync(manifest)) {
27
+ const parent = path.dirname(path.dirname(manifest));
28
+ if (parent === path.dirname(manifest)) return true;
29
+ manifest = path.join(parent, "package.json");
30
+ }
31
+ try {
32
+ const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8"));
33
+ if (!pkg.dependencies?.["react-server-dom-webpack"] && !pkg.devDependencies?.["react-server-dom-webpack"]) return true;
34
+ const version = findPackageVersion(createRequire(manifest).resolve("react-server-dom-webpack"), "react-server-dom-webpack");
35
+ return !version || isVersionBelow(version, MINIMUM_REACT_VERSION);
36
+ } catch {
37
+ return true;
38
+ }
39
+ }
14
40
  //#endregion
15
- export { assertPluginRscVersion };
41
+ export { assertPluginRscVersion, needsAppReactServerDom };
@@ -1,7 +1,14 @@
1
1
  //#region src/utils/react-version.d.ts
2
+ /**
3
+ * Lowest React accepted for App Router builds. react-server-dom-webpack 19.3.0
4
+ * shares typed-array buffers across Flight chunks, fixed in 19.3.1.
5
+ */
6
+ export declare const MINIMUM_REACT_VERSION: [number, number, number];
2
7
  export declare function getReactUpgradeDeps(root: string, options?: {
3
8
  fromManifest?: boolean;
9
+ installing?: boolean;
4
10
  }): string[];
11
+ export declare function reactUpgradeRequiredMessage(installCommand: string, upgrades: string[], pluginRscVersion?: string): string;
5
12
  export declare function isVersionBelow(version: string, minimum: [number, number, number]): boolean;
6
13
  /** Walk up from a resolved module entry to find its package version. */
7
14
  export declare function findPackageVersion(resolvedEntry: string, packageName: string): string | null;
@@ -12,17 +12,27 @@ function getDependencyUpgradeDeps(root, recommendations) {
12
12
  }
13
13
  return [];
14
14
  }
15
+ /**
16
+ * Lowest React accepted for App Router builds. react-server-dom-webpack 19.3.0
17
+ * shares typed-array buffers across Flight chunks, fixed in 19.3.1.
18
+ */
19
+ const MINIMUM_REACT_VERSION = [
20
+ 19,
21
+ 3,
22
+ 1
23
+ ];
15
24
  function getReactUpgradeDeps(root, options = {}) {
16
25
  if (options.fromManifest) {
17
26
  const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf-8"));
18
- for (const name of ["react", "react-dom"]) {
27
+ for (const name of [
28
+ "react",
29
+ "react-dom",
30
+ "react-server-dom-webpack"
31
+ ]) {
19
32
  const declared = pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
20
- const version = /^[~^]?([0-9]+(?:\.[0-9]+){0,2}(?:-[0-9A-Za-z.*-]+)?)(?:\.[x*])?$/.exec(declared ?? "")?.[1];
21
- if (version && (version.includes("-") || isVersionBelow(version, [
22
- 19,
23
- 2,
24
- 6
25
- ]))) {
33
+ const version = (options.installing ? /^([0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?)$/ : /^[~^]?([0-9]+(?:\.[0-9]+){0,2}(?:-[0-9A-Za-z.*-]+)?)(?:\.[x*])?$/).exec(declared ?? "")?.[1];
34
+ const canary = !options.installing && version?.includes("-");
35
+ if (version && (canary || isVersionBelow(version, MINIMUM_REACT_VERSION))) {
26
36
  const upgrades = ["react@latest", "react-dom@latest"];
27
37
  const rsdw = pkg.dependencies?.["react-server-dom-webpack"] ?? pkg.devDependencies?.["react-server-dom-webpack"];
28
38
  if (rsdw && rsdw !== "latest") upgrades.push("react-server-dom-webpack@latest");
@@ -31,14 +41,27 @@ function getReactUpgradeDeps(root, options = {}) {
31
41
  }
32
42
  return [];
33
43
  }
34
- return getDependencyUpgradeDeps(root, { react: {
35
- minimumVersion: [
36
- 19,
37
- 2,
38
- 6
39
- ],
40
- upgrades: ["react@latest", "react-dom@latest"]
41
- } });
44
+ return getDependencyUpgradeDeps(root, {
45
+ "react-server-dom-webpack": {
46
+ minimumVersion: MINIMUM_REACT_VERSION,
47
+ upgrades: [
48
+ "react@latest",
49
+ "react-dom@latest",
50
+ "react-server-dom-webpack@latest"
51
+ ]
52
+ },
53
+ react: {
54
+ minimumVersion: MINIMUM_REACT_VERSION,
55
+ upgrades: ["react@latest", "react-dom@latest"]
56
+ },
57
+ "react-dom": {
58
+ minimumVersion: MINIMUM_REACT_VERSION,
59
+ upgrades: ["react@latest", "react-dom@latest"]
60
+ }
61
+ });
62
+ }
63
+ function reactUpgradeRequiredMessage(installCommand, upgrades, pluginRscVersion) {
64
+ return `vinext: App Router requires react, react-dom, and react-server-dom-webpack ${MINIMUM_REACT_VERSION.join(".")} or later.\n` + (pluginRscVersion ? `@vitejs/plugin-rsc ${pluginRscVersion} bundles an older react-server-dom-webpack, so install the latest in your app.\n` : "") + `Run: ${installCommand} ${upgrades.join(" ")}`;
42
65
  }
43
66
  function isVersionBelow(version, minimum) {
44
67
  const current = version.split(".").map((part) => parseInt(part, 10));
@@ -59,4 +82,4 @@ function findPackageVersion(resolvedEntry, packageName) {
59
82
  return null;
60
83
  }
61
84
  //#endregion
62
- export { findPackageVersion, getReactUpgradeDeps, isVersionBelow };
85
+ export { MINIMUM_REACT_VERSION, findPackageVersion, getReactUpgradeDeps, isVersionBelow, reactUpgradeRequiredMessage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vinext",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Run Next.js apps on Vite. Drop-in replacement for the next CLI.",
5
5
  "homepage": "https://vinext.dev/",
6
6
  "license": "MIT",
@@ -238,12 +238,12 @@
238
238
  "@types/react-dom": "^19.2.3",
239
239
  "@types/ua-parser-js": "0.7.36",
240
240
  "@vitejs/plugin-react": "^6.1.0",
241
- "@vitejs/plugin-rsc": "^0.5.37",
241
+ "@vitejs/plugin-rsc": "^0.5.38",
242
242
  "am-i-vibing": "^0.5.0",
243
243
  "ignore": "5.3.2",
244
244
  "image-size": "2.0.2",
245
245
  "pathslash": "^0.1.0",
246
- "react-server-dom-webpack": "^19.2.7",
246
+ "react-server-dom-webpack": "^19.3.1",
247
247
  "ua-parser-js": "1.0.35",
248
248
  "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
249
249
  "vite-plus": "1.1.0"
@@ -252,9 +252,9 @@
252
252
  "@mdx-js/rollup": "^3.0.0",
253
253
  "@vitejs/plugin-react": "^5.1.4 || ^6.0.0",
254
254
  "@vitejs/plugin-rsc": "^0.5.37",
255
- "react": "^19.2.6",
256
- "react-dom": "^19.2.6",
257
- "react-server-dom-webpack": "^19.2.6",
255
+ "react": "^19.3.1",
256
+ "react-dom": "^19.3.1",
257
+ "react-server-dom-webpack": "^19.3.1",
258
258
  "vite": "^8.0.0"
259
259
  },
260
260
  "peerDependenciesMeta": {
@@ -1,47 +0,0 @@
1
- import { RouteRow } from "./report.js";
2
- //#region src/build/nitro-route-rules.d.ts
3
- export type NitroRouteRuleConfig = Record<string, unknown> & {
4
- swr?: boolean | number;
5
- cache?: unknown;
6
- static?: boolean;
7
- isr?: boolean | number;
8
- prerender?: boolean;
9
- };
10
- type NitroRouteRules = Record<string, {
11
- swr: number;
12
- }>;
13
- /**
14
- * Scans the filesystem for route files and generates Nitro `routeRules` for ISR routes.
15
- *
16
- * Note: this duplicates the filesystem scanning that `printBuildReport` also performs.
17
- * The `nitro.setup` hook runs during Nitro initialization (before the build), while
18
- * `printBuildReport` runs after the build, so sharing results is non-trivial. This is
19
- * a future optimization target.
20
- *
21
- * Unlike `printBuildReport`, this path does not receive `prerenderResult`, so routes
22
- * classified as `unknown` by static analysis (which `printBuildReport` might upgrade
23
- * to `static` via speculative prerender) are skipped here.
24
- */
25
- export declare function collectNitroRouteRules(options: {
26
- appDir?: string | null;
27
- pagesDir?: string | null;
28
- pageExtensions: string[];
29
- }): Promise<NitroRouteRules>;
30
- export declare function generateNitroRouteRules(rows: RouteRow[]): NitroRouteRules;
31
- /**
32
- * Converts vinext's internal `:param` route syntax to Nitro's rou3
33
- * pattern format. Nitro uses `rou3` for routeRules matching, which
34
- * supports `*` (single-segment) and `**` (multi-segment) wildcards.
35
- *
36
- * /blog/:slug -> /blog/* (single segment)
37
- * /docs/:slug+ -> /docs/** (one or more segments — catch-all)
38
- * /docs/:slug* -> /docs/** (zero or more segments — optional catch-all)
39
- * /about -> /about (unchanged)
40
- * /:a/:b produces `/*`/`/*` (consecutive single-segment params)
41
- */
42
- export declare function convertToNitroPattern(pattern: string): string;
43
- export declare function mergeNitroRouteRules(existingRouteRules: Record<string, NitroRouteRuleConfig> | undefined, generatedRouteRules: NitroRouteRules): {
44
- routeRules: Record<string, NitroRouteRuleConfig>;
45
- skippedRoutes: string[];
46
- };
47
- //#endregion
@@ -1,79 +0,0 @@
1
- import { appRouter } from "../routing/app-router.js";
2
- import { apiRouter, pagesRouter } from "../routing/pages-router.js";
3
- import { buildReportRows } from "./report.js";
4
- //#region src/build/nitro-route-rules.ts
5
- /**
6
- * Scans the filesystem for route files and generates Nitro `routeRules` for ISR routes.
7
- *
8
- * Note: this duplicates the filesystem scanning that `printBuildReport` also performs.
9
- * The `nitro.setup` hook runs during Nitro initialization (before the build), while
10
- * `printBuildReport` runs after the build, so sharing results is non-trivial. This is
11
- * a future optimization target.
12
- *
13
- * Unlike `printBuildReport`, this path does not receive `prerenderResult`, so routes
14
- * classified as `unknown` by static analysis (which `printBuildReport` might upgrade
15
- * to `static` via speculative prerender) are skipped here.
16
- */
17
- async function collectNitroRouteRules(options) {
18
- const { appDir, pageExtensions, pagesDir } = options;
19
- let appRoutes = [];
20
- let pageRoutes = [];
21
- let apiRoutes = [];
22
- if (appDir) appRoutes = await appRouter(appDir, pageExtensions);
23
- if (pagesDir) {
24
- const [pages, apis] = await Promise.all([pagesRouter(pagesDir, pageExtensions), apiRouter(pagesDir, pageExtensions)]);
25
- pageRoutes = pages;
26
- apiRoutes = apis;
27
- }
28
- return generateNitroRouteRules(buildReportRows({
29
- appRoutes,
30
- pageRoutes,
31
- apiRoutes
32
- }));
33
- }
34
- function generateNitroRouteRules(rows) {
35
- const rules = {};
36
- for (const row of rows) if (row.type === "isr" && typeof row.revalidate === "number" && Number.isFinite(row.revalidate) && row.revalidate > 0) rules[convertToNitroPattern(row.pattern)] = { swr: row.revalidate };
37
- return rules;
38
- }
39
- /**
40
- * Converts vinext's internal `:param` route syntax to Nitro's rou3
41
- * pattern format. Nitro uses `rou3` for routeRules matching, which
42
- * supports `*` (single-segment) and `**` (multi-segment) wildcards.
43
- *
44
- * /blog/:slug -> /blog/* (single segment)
45
- * /docs/:slug+ -> /docs/** (one or more segments — catch-all)
46
- * /docs/:slug* -> /docs/** (zero or more segments — optional catch-all)
47
- * /about -> /about (unchanged)
48
- * /:a/:b produces `/*`/`/*` (consecutive single-segment params)
49
- */
50
- function convertToNitroPattern(pattern) {
51
- return pattern.split("/").map((segment) => {
52
- if (segment.startsWith(":")) return segment.endsWith("+") || segment.endsWith("*") ? "**" : "*";
53
- return segment;
54
- }).join("/");
55
- }
56
- function mergeNitroRouteRules(existingRouteRules, generatedRouteRules) {
57
- const routeRules = { ...existingRouteRules };
58
- const skippedRoutes = [];
59
- for (const [route, generatedRule] of Object.entries(generatedRouteRules)) {
60
- const existingRule = routeRules[route];
61
- if (existingRule && hasUserDefinedCacheRule(existingRule)) {
62
- skippedRoutes.push(route);
63
- continue;
64
- }
65
- routeRules[route] = {
66
- ...existingRule,
67
- ...generatedRule
68
- };
69
- }
70
- return {
71
- routeRules,
72
- skippedRoutes
73
- };
74
- }
75
- function hasUserDefinedCacheRule(rule) {
76
- return rule.swr !== void 0 || rule.cache !== void 0 || rule.static !== void 0 || rule.isr !== void 0 || rule.prerender !== void 0;
77
- }
78
- //#endregion
79
- export { collectNitroRouteRules, convertToNitroPattern, generateNitroRouteRules, mergeNitroRouteRules };
@@ -1,19 +0,0 @@
1
- import { MagicStringTransformResult } from "./transform-result.js";
2
- import { Plugin } from "vite";
3
- //#region src/plugins/cache-flight-codec.d.ts
4
- /**
5
- * Version of the Flight codec package that owns `id`. plugin-rsc resolves the
6
- * app's own react-server-dom-webpack when installed and its vendored copy
7
- * otherwise; reading the codec file's own package.json follows that choice.
8
- */
9
- export declare function readFlightCodecVersion(id: string): string;
10
- /**
11
- * Backport React 19.3's root-element temporary reference branch to older Flight
12
- * codecs, so a "use cache" argument like `Promise<ReactElement>` encodes as a
13
- * temporary reference instead of throwing. Remove it once React 19.3 is the
14
- * minimum supported version.
15
- * https://github.com/facebook/react/blob/v19.3.0/packages/react-client/src/ReactFlightReplyClient.js#L486
16
- */
17
- export declare function patchCacheFlightCodec(code: string, id: string, version?: string): MagicStringTransformResult<import("magic-string").SourceMap> | null;
18
- export declare function cacheFlightCodecPlugin(): Plugin;
19
- //#endregion
@@ -1,68 +0,0 @@
1
- import path from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
2
- import { magicStringTransformResult } from "./transform-result.js";
3
- import fs from "node:fs";
4
- import MagicString from "magic-string";
5
- //#region src/plugins/cache-flight-codec.ts
6
- const CLIENT_CODEC = /\/react-server-dom-(?:webpack|turbopack)-client\.edge\.(?:development|production)\.js$/;
7
- const ELEMENT_CASE = /case REACT_ELEMENT_TYPE:[\s\S]*?throw Error\(\s*"React Element cannot be passed/;
8
- /**
9
- * Version of the Flight codec package that owns `id`. plugin-rsc resolves the
10
- * app's own react-server-dom-webpack when installed and its vendored copy
11
- * otherwise; reading the codec file's own package.json follows that choice.
12
- */
13
- function readFlightCodecVersion(id) {
14
- for (let dir = path.dirname(id);; dir = path.dirname(dir)) {
15
- const manifest = path.join(dir, "package.json");
16
- if (fs.existsSync(manifest)) {
17
- const pkg = JSON.parse(fs.readFileSync(manifest, "utf8"));
18
- if (/^react-server-dom-(?:webpack|turbopack)$/.test(pkg.name ?? "") && pkg.version) return pkg.version;
19
- }
20
- if (path.dirname(dir) === dir) throw new Error(`vinext: cannot find the react-server-dom package version for ${id}`);
21
- }
22
- }
23
- /** React 19.3 ships the branch; 0.0.0-* experimental builds track main. */
24
- function needsElementTemporaryReferenceBackport(version) {
25
- const match = /^(\d+)\.(\d+)\./.exec(version);
26
- if (!match || version.startsWith("0.0.0-")) return false;
27
- const [major, minor] = [Number(match[1]), Number(match[2])];
28
- return major < 19 || major === 19 && minor < 3;
29
- }
30
- /**
31
- * Backport React 19.3's root-element temporary reference branch to older Flight
32
- * codecs, so a "use cache" argument like `Promise<ReactElement>` encodes as a
33
- * temporary reference instead of throwing. Remove it once React 19.3 is the
34
- * minimum supported version.
35
- * https://github.com/facebook/react/blob/v19.3.0/packages/react-client/src/ReactFlightReplyClient.js#L486
36
- */
37
- function patchCacheFlightCodec(code, id, version = readFlightCodecVersion(id)) {
38
- if (!needsElementTemporaryReferenceBackport(version)) return null;
39
- const elementCase = ELEMENT_CASE.exec(code);
40
- if (!elementCase) throw new Error(`vinext: react-server-dom-webpack ${version} (${id}) is not supported. Upgrade react, react-dom, and react-server-dom-webpack to 19.3 or later. If your app does not depend on react-server-dom-webpack directly, upgrade @vitejs/plugin-rsc to 0.5.35 or later instead.`);
41
- const output = new MagicString(code);
42
- output.appendLeft(elementCase.index + elementCase[0].lastIndexOf("throw Error("), "if (void 0 !== temporaryReferences && modelRoot === value) return (modelRoot = null), \"$T\";\n");
43
- return magicStringTransformResult(output, {
44
- hires: "boundary",
45
- source: id
46
- });
47
- }
48
- function cacheFlightCodecPlugin() {
49
- const transform = {
50
- filter: { id: CLIENT_CODEC },
51
- handler: (code, id) => patchCacheFlightCodec(code, id)
52
- };
53
- return {
54
- name: "vinext:cache-flight-codec",
55
- enforce: "pre",
56
- applyToEnvironment: (environment) => environment.name === "rsc",
57
- configEnvironment(name) {
58
- if (name !== "rsc") return;
59
- return { optimizeDeps: { rolldownOptions: { plugins: [{
60
- name: "vinext:cache-flight-codec-optimizer",
61
- transform
62
- }] } } };
63
- },
64
- transform
65
- };
66
- }
67
- //#endregion
68
- export { cacheFlightCodecPlugin, patchCacheFlightCodec, readFlightCodecVersion };