vinext 0.1.5 → 0.1.6

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 (69) hide show
  1. package/README.md +2 -2
  2. package/dist/build/google-fonts/find-font-files-in-css.d.ts +16 -0
  3. package/dist/build/google-fonts/find-font-files-in-css.js +28 -0
  4. package/dist/build/report.d.ts +1 -2
  5. package/dist/build/report.js +2 -11
  6. package/dist/build/run-prerender.js +1 -1
  7. package/dist/check.js +3 -3
  8. package/dist/client/pages-router-link-navigation.d.ts +10 -0
  9. package/dist/client/pages-router-link-navigation.js +3 -3
  10. package/dist/deploy.js +3 -3
  11. package/dist/entries/app-rsc-entry.js +1 -0
  12. package/dist/entries/app-rsc-manifest.js +76 -46
  13. package/dist/entries/pages-server-entry.js +1 -0
  14. package/dist/index.js +69 -54
  15. package/dist/init.d.ts +5 -4
  16. package/dist/init.js +27 -8
  17. package/dist/plugins/fonts.js +3 -1
  18. package/dist/plugins/middleware-export-validation.d.ts +5 -0
  19. package/dist/plugins/middleware-export-validation.js +36 -0
  20. package/dist/plugins/og-asset-ownership.d.ts +27 -0
  21. package/dist/plugins/og-asset-ownership.js +285 -0
  22. package/dist/plugins/og-assets.js +22 -4
  23. package/dist/server/accept-encoding.d.ts +29 -0
  24. package/dist/server/accept-encoding.js +95 -0
  25. package/dist/server/app-browser-entry.js +8 -5
  26. package/dist/server/app-browser-navigation-controller.js +1 -1
  27. package/dist/server/app-browser-rsc-redirect.d.ts +2 -1
  28. package/dist/server/app-browser-rsc-redirect.js +8 -1
  29. package/dist/server/app-browser-state.js +1 -1
  30. package/dist/server/app-browser-visible-commit.js +1 -1
  31. package/dist/server/app-middleware.d.ts +1 -0
  32. package/dist/server/app-middleware.js +1 -0
  33. package/dist/server/app-page-dispatch.d.ts +2 -2
  34. package/dist/server/app-page-head.js +2 -2
  35. package/dist/server/app-page-request.d.ts +7 -0
  36. package/dist/server/app-page-request.js +19 -1
  37. package/dist/server/app-prerender-endpoints.d.ts +3 -3
  38. package/dist/server/app-prerender-endpoints.js +3 -4
  39. package/dist/server/app-route-module-loader.d.ts +62 -14
  40. package/dist/server/app-route-module-loader.js +61 -10
  41. package/dist/server/app-rsc-handler.d.ts +7 -6
  42. package/dist/server/app-rsc-handler.js +31 -26
  43. package/dist/server/app-rsc-route-matching.d.ts +9 -0
  44. package/dist/server/app-rsc-route-matching.js +13 -0
  45. package/dist/server/app-ssr-entry.js +2 -2
  46. package/dist/server/headers.d.ts +5 -1
  47. package/dist/server/headers.js +5 -1
  48. package/dist/server/metadata-route-response.d.ts +3 -3
  49. package/dist/server/middleware-runtime.d.ts +2 -1
  50. package/dist/server/middleware-runtime.js +19 -8
  51. package/dist/server/middleware.d.ts +2 -2
  52. package/dist/server/middleware.js +26 -11
  53. package/dist/server/pages-request-pipeline.d.ts +3 -1
  54. package/dist/server/pages-request-pipeline.js +4 -2
  55. package/dist/server/prod-server.d.ts +1 -1
  56. package/dist/server/prod-server.js +85 -72
  57. package/dist/shims/font-google-base.d.ts +1 -0
  58. package/dist/shims/font-google-base.js +6 -21
  59. package/dist/shims/internal/interpolate-as.d.ts +25 -0
  60. package/dist/shims/internal/interpolate-as.js +196 -0
  61. package/dist/shims/link.js +24 -1
  62. package/dist/shims/navigation.js +1 -1
  63. package/dist/shims/navigation.react-server.js +1 -1
  64. package/dist/shims/router.d.ts +1 -3
  65. package/dist/shims/router.js +62 -18
  66. package/dist/typegen.js +2 -10
  67. package/dist/utils/project.d.ts +7 -1
  68. package/dist/utils/project.js +16 -2
  69. package/package.json +3 -3
@@ -0,0 +1,196 @@
1
+ import { removeTrailingSlash } from "../../utils/base-path.js";
2
+ //#region src/shims/internal/interpolate-as.ts
3
+ /**
4
+ * Port of Next.js' Pages Router `interpolateAs` helper, plus the minimal
5
+ * subset of its transitive dependencies (`getRouteRegex` + `getRouteMatcher`
6
+ * and the parameter-pattern parsers they rely on).
7
+ *
8
+ * Upstream sources:
9
+ * - packages/next/src/shared/lib/router/utils/interpolate-as.ts
10
+ * - packages/next/src/shared/lib/router/utils/route-regex.ts
11
+ * - packages/next/src/shared/lib/router/utils/route-matcher.ts
12
+ * - packages/next/src/shared/lib/router/utils/get-dynamic-param.ts
13
+ * - packages/next/src/shared/lib/router/utils/remove-trailing-slash.ts
14
+ * - packages/next/src/shared/lib/escape-regexp.ts
15
+ *
16
+ * Used by the Pages Router shim to project a route pattern + `as` pathname +
17
+ * query back into a fully-interpolated browser URL (the `<Link href as>`
18
+ * masking path). Interception-route markers (`(.)`, `(..)`, `(...)`,
19
+ * `(..)(..)`) are intentionally omitted — they only apply to App Router and
20
+ * never appear in Pages Router patterns.
21
+ */
22
+ function normalizeQuery(query) {
23
+ const normalized = {};
24
+ if (!query) return normalized;
25
+ for (const [key, value] of Object.entries(query)) {
26
+ if (value === void 0) continue;
27
+ normalized[key] = Array.isArray(value) ? value.map(String) : String(value);
28
+ }
29
+ return normalized;
30
+ }
31
+ function parseRouteHrefQuery(routeHref, queryIndex, hashIndex) {
32
+ const query = {};
33
+ if (queryIndex === -1 || hashIndex !== -1 && queryIndex > hashIndex) return query;
34
+ const searchEnd = hashIndex === -1 ? routeHref.length : hashIndex;
35
+ for (const [key, value] of new URLSearchParams(routeHref.slice(queryIndex + 1, searchEnd))) {
36
+ const existing = query[key];
37
+ if (existing === void 0) query[key] = value;
38
+ else if (Array.isArray(existing)) existing.push(value);
39
+ else query[key] = [existing, value];
40
+ }
41
+ return query;
42
+ }
43
+ const reHasRegExp = /[|\\{}()[\]^$+*?.-]/;
44
+ const reReplaceRegExp = /[|\\{}()[\]^$+*?.-]/g;
45
+ function escapeStringRegexp(str) {
46
+ if (reHasRegExp.test(str)) return str.replace(reReplaceRegExp, "\\$&");
47
+ return str;
48
+ }
49
+ /**
50
+ * Regular expression pattern used to match route parameters.
51
+ * Matches both single parameters and parameter groups.
52
+ * Examples:
53
+ * - `[[...slug]]` matches parameter group with key 'slug', repeat: true, optional: true
54
+ * - `[...slug]` matches parameter group with key 'slug', repeat: true, optional: false
55
+ * - `[[foo]]` matches parameter with key 'foo', repeat: false, optional: true
56
+ * - `[bar]` matches parameter with key 'bar', repeat: false, optional: false
57
+ */
58
+ const PARAMETER_PATTERN = /^([^[]*)\[((?:\[[^\]]*\])|[^\]]+)\](.*)$/;
59
+ /**
60
+ * Parses a matched parameter from the PARAMETER_PATTERN regex.
61
+ * Examples:
62
+ * - `[...slug]` -> `{ key: 'slug', repeat: true, optional: true }`
63
+ * - `...slug` -> `{ key: 'slug', repeat: true, optional: false }`
64
+ * - `[foo]` -> `{ key: 'foo', repeat: false, optional: true }`
65
+ * - `bar` -> `{ key: 'bar', repeat: false, optional: false }`
66
+ */
67
+ function parseMatchedParameter(param) {
68
+ const optional = param.startsWith("[") && param.endsWith("]");
69
+ if (optional) param = param.slice(1, -1);
70
+ const repeat = param.startsWith("...");
71
+ if (repeat) param = param.slice(3);
72
+ return {
73
+ key: param,
74
+ repeat,
75
+ optional
76
+ };
77
+ }
78
+ function getParametrizedRoute(route) {
79
+ const groups = {};
80
+ let groupIndex = 1;
81
+ const segments = [];
82
+ for (const segment of removeTrailingSlash(route).slice(1).split("/")) {
83
+ const paramMatches = segment.match(PARAMETER_PATTERN);
84
+ if (paramMatches && paramMatches[2]) {
85
+ const { key, repeat, optional } = parseMatchedParameter(paramMatches[2]);
86
+ groups[key] = {
87
+ pos: groupIndex++,
88
+ repeat,
89
+ optional
90
+ };
91
+ const s = repeat ? optional ? "(?:/(.+?))?" : "/(.+?)" : "/([^/]+?)";
92
+ segments.push(s);
93
+ } else segments.push(`/${escapeStringRegexp(segment)}`);
94
+ }
95
+ return {
96
+ parameterizedRoute: segments.join(""),
97
+ groups
98
+ };
99
+ }
100
+ /**
101
+ * From a normalized route this function generates a regular expression and a
102
+ * corresponding groups object intended to be used to store matching groups
103
+ * from the regular expression.
104
+ */
105
+ function getRouteRegex(normalizedRoute) {
106
+ const { parameterizedRoute, groups } = getParametrizedRoute(normalizedRoute);
107
+ return {
108
+ re: new RegExp(`^${parameterizedRoute}(?:/)?$`),
109
+ groups
110
+ };
111
+ }
112
+ /**
113
+ * Compile a route regex into a function that extracts decoded params from a
114
+ * pathname. Returns `false` if the pathname does not match the route.
115
+ *
116
+ * The `safeRouteMatcher`/`stripParameterSeparators` wrapper that Next.js
117
+ * applies (for adjacent-parameter normalization) is omitted — Pages Router
118
+ * patterns never contain adjacent parameters, so the wrapper is a no-op for
119
+ * this caller.
120
+ */
121
+ function getRouteMatcher({ re, groups }) {
122
+ return (pathname) => {
123
+ const routeMatch = re.exec(pathname);
124
+ if (!routeMatch) return false;
125
+ const decode = (param) => {
126
+ try {
127
+ return decodeURIComponent(param);
128
+ } catch {
129
+ throw new Error("failed to decode param");
130
+ }
131
+ };
132
+ const params = {};
133
+ for (const [key, group] of Object.entries(groups)) {
134
+ const match = routeMatch[group.pos];
135
+ if (match !== void 0) if (group.repeat) params[key] = match.split("/").map((entry) => decode(entry));
136
+ else params[key] = decode(match);
137
+ }
138
+ return params;
139
+ };
140
+ }
141
+ /**
142
+ * Project a `(route, asPathname, query)` triple back into a fully-interpolated
143
+ * browser URL. Used by the `<Link href as>` masking path in the Pages Router
144
+ * shim: extract param values from the rendered `as` path when it differs from
145
+ * the route pattern, otherwise fall back to reading them from the href query.
146
+ *
147
+ * Returns `{ params, result }`. `result` is the interpolated URL, or the empty
148
+ * string when one or more required params could not be resolved. Callers warn
149
+ * (in dev) on the empty-string case; this helper deliberately stays silent —
150
+ * matching Next.js' behavior, where `<Link>` itself owns the dev warning.
151
+ */
152
+ function interpolateAs(route, asPathname, query) {
153
+ let interpolatedRoute = "";
154
+ const dynamicRegex = getRouteRegex(route);
155
+ const dynamicGroups = dynamicRegex.groups;
156
+ const dynamicMatches = (asPathname !== route ? getRouteMatcher(dynamicRegex)(asPathname) : "") || query;
157
+ interpolatedRoute = route;
158
+ const params = Object.keys(dynamicGroups);
159
+ if (!params.every((param) => {
160
+ let value = dynamicMatches[param] || "";
161
+ const { repeat, optional } = dynamicGroups[param];
162
+ let replaced = `[${repeat ? "..." : ""}${param}]`;
163
+ if (optional) replaced = `${!value ? "/" : ""}[${replaced}]`;
164
+ if (repeat && !Array.isArray(value)) value = [value];
165
+ return (optional || param in dynamicMatches) && (interpolatedRoute = interpolatedRoute.replace(replaced, repeat ? value.map((segment) => encodeURIComponent(segment)).join("/") : encodeURIComponent(value)) || "/");
166
+ })) interpolatedRoute = "";
167
+ return {
168
+ params,
169
+ result: interpolatedRoute
170
+ };
171
+ }
172
+ /**
173
+ * Resolve a bracket-pattern route href against its displayed href. Query
174
+ * values can be supplied directly (object-form hrefs) or parsed from the route
175
+ * href (string-form hrefs). A `?` after `#` is part of the fragment, not a
176
+ * query delimiter.
177
+ */
178
+ function interpolateDynamicRouteHref(routeHref, asHref, queryInput) {
179
+ if (!routeHref.includes("[")) return null;
180
+ const hashIndex = routeHref.indexOf("#");
181
+ const queryIndex = routeHref.indexOf("?");
182
+ const pathEnd = [hashIndex, queryIndex].filter((index) => index !== -1).reduce((earliest, index) => Math.min(earliest, index), routeHref.length);
183
+ const routePathname = routeHref.slice(0, pathEnd);
184
+ const trailing = routeHref.slice(pathEnd);
185
+ const asPathname = asHref.split(/[?#]/, 1)[0];
186
+ const query = queryInput ? normalizeQuery(queryInput) : parseRouteHrefQuery(routeHref, queryIndex, hashIndex);
187
+ const { result, params } = interpolateAs(routePathname, asPathname, query);
188
+ return {
189
+ href: result ? `${result}${trailing}` : "",
190
+ params,
191
+ query,
192
+ routePathname
193
+ };
194
+ }
195
+ //#endregion
196
+ export { interpolateDynamicRouteHref };
@@ -12,6 +12,7 @@ import { appendSearchParamsToUrl, urlQueryToSearchParams } from "../utils/query.
12
12
  import { resolveHybridClientRouteOwner } from "./internal/hybrid-client-route-owner.js";
13
13
  import { markAppRouteDetectedOnPrefetch } from "./internal/app-route-detection.js";
14
14
  import { isAbsoluteOrProtocolRelativeUrl, normalizePathTrailingSlash, toBrowserNavigationHref, toSameOriginAppPath, withBasePath } from "./url-utils.js";
15
+ import { interpolateDynamicRouteHref } from "./internal/interpolate-as.js";
15
16
  import { getCurrentBrowserLocale } from "./client-locale.js";
16
17
  import { getCurrentRoutePathnameForWarning } from "./internal/route-pattern-for-warning.js";
17
18
  import { createRscRequestHeaders, createRscRequestUrl, stripRscCacheBustingSearchParam, stripRscSuffix } from "../server/app-rsc-cache-busting.js";
@@ -391,11 +392,30 @@ function applyLocaleToHref(href, locale) {
391
392
  }
392
393
  return addLocalePrefix(href, resolvedLocale, defaultLocale);
393
394
  }
395
+ /**
396
+ * For the `<Link href="/blog/[slug]" as="/blog/test-post">` case, project the
397
+ * bracket-pattern href + the resolved `as` back into a concrete route URL the
398
+ * Pages Router can fetch (`/blog/test-post`). Returns null when:
399
+ * - `href` has no bracket params (already concrete; the existing forwarding
400
+ * path works as-is)
401
+ * - interpolation fails because a required param could not be resolved
402
+ * (caller falls back to `as`, matching pre-PR behavior)
403
+ *
404
+ * The query for interpolation is the href's own query — `as` is the matcher
405
+ * input rather than the source of param values. For string hrefs the search
406
+ * portion is parsed into the query record; for object hrefs we take
407
+ * `href.query` directly.
408
+ */
409
+ function resolveConcreteRouteHref(href, as) {
410
+ if (typeof as !== "string") return null;
411
+ return interpolateDynamicRouteHref(typeof href === "string" ? href : resolveHref(href), as, typeof href === "string" ? void 0 : href.query)?.href || null;
412
+ }
394
413
  const Link = forwardRef(function Link({ href, as, replace = false, prefetch: prefetchProp, scroll = true, shallow = false, children: childrenProp, onClick, onMouseEnter, onTouchStart, onNavigate, unstable_dynamicOnHover = false, legacyBehavior = false, passHref = false, ...rest }, forwardedRef) {
395
414
  const { locale, ...restWithoutLocale } = rest;
396
415
  let children = childrenProp;
397
416
  if (legacyBehavior && (typeof childrenProp === "string" || typeof childrenProp === "number")) children = React.createElement("a", null, childrenProp);
398
417
  const rawResolvedHref = as ?? resolveHref(href);
418
+ const routeHrefRaw = resolveConcreteRouteHref(href, as) ?? (typeof href === "string" ? href : resolveHref(href));
399
419
  const resolvedHref = typeof rawResolvedHref === "string" ? warnAndNormalizeRepeatedSlashesInHref(rawResolvedHref) : rawResolvedHref;
400
420
  const isDangerous = typeof resolvedHref === "string" && isDangerousScheme(resolvedHref);
401
421
  const normalizedHref = normalizePathTrailingSlash(applyLocaleToHref(isDangerous ? "/" : resolvedHref, locale), __trailingSlash);
@@ -508,6 +528,8 @@ const Link = forwardRef(function Link({ href, as, replace = false, prefetch: pre
508
528
  e.preventDefault();
509
529
  const hasAppNavigationRuntime = Boolean(getNavigationRuntime()?.functions.navigate);
510
530
  const pagesNavigateHref = resolvedHref.startsWith("?") ? resolvePagesLinkNavigationHref(resolvedHref, locale) : navigateHref;
531
+ const pagesAsForLink = typeof as === "string" && typeof routeHrefRaw === "string" && as !== routeHrefRaw ? pagesNavigateHref : void 0;
532
+ const pagesHrefForLink = pagesAsForLink === void 0 ? pagesNavigateHref : routeHrefRaw;
511
533
  const absoluteFullHref = toBrowserNavigationHref(hasAppNavigationRuntime ? navigateHref : pagesNavigateHref, window.location.href, __basePath);
512
534
  if (onNavigate) try {
513
535
  const navUrl = new URL(absoluteFullHref, window.location.origin);
@@ -546,7 +568,8 @@ const Link = forwardRef(function Link({ href, as, replace = false, prefetch: pre
546
568
  router: Router && "reload" in Router ? Router : void 0,
547
569
  loadRouter: async () => (await import("next/router.js")).default,
548
570
  navigation: {
549
- href: pagesNavigateHref,
571
+ href: pagesHrefForLink,
572
+ as: pagesAsForLink,
550
573
  replace,
551
574
  scroll,
552
575
  shallow,
@@ -16,8 +16,8 @@ import { VINEXT_RSC_COMPATIBILITY_ID_HEADER, createRscRequestHeaders, createRscR
16
16
  import { getNavigationRuntime, hasAppNavigationRuntime } from "../client/navigation-runtime.js";
17
17
  import { notifyAppRouterTransitionStart } from "../client/instrumentation-client-state.js";
18
18
  import { PUBLIC_INITIAL_BFCACHE_ID } from "../server/app-bfcache-id.js";
19
- import { createExternalHistoryStatePreservingMetadata, createHashOnlyHistoryStatePreservingNavigationMetadata } from "../server/app-history-state.js";
20
19
  import { resolveManifestNavigationInterceptionContext } from "../server/app-browser-interception-context.js";
20
+ import { createExternalHistoryStatePreservingMetadata, createHashOnlyHistoryStatePreservingNavigationMetadata } from "../server/app-history-state.js";
21
21
  import { hasPendingAppRouterPageRedirect } from "../server/app-browser-mpa-navigation.js";
22
22
  import { navigationPlanner } from "../server/navigation-planner.js";
23
23
  import { ReadonlyURLSearchParams } from "./readonly-url-search-params.js";
@@ -1,8 +1,8 @@
1
1
  import { GLOBAL_ACCESSORS_KEY, ServerInsertedHTMLContext, _registerStateAccessors, clearServerInsertedHTML, flushServerInsertedHTML, getLayoutSegmentContext, getNavigationContext, renderServerInsertedHTML, setNavigationContext } from "./navigation-context-state.js";
2
2
  import { BailoutToCSRError, DynamicServerError, HTTP_ERROR_FALLBACK_ERROR_CODE, RedirectType, forbidden, getAccessFallbackHTTPStatus, isBailoutToCSRError, isDynamicServerError, isHTTPAccessFallbackError, isNextRouterError, isRedirectError, notFound, permanentRedirect, redirect, unauthorized, unstable_rethrow } from "./navigation-errors.js";
3
3
  import "./navigation-server.js";
4
- import { ReadonlyURLSearchParams } from "./readonly-url-search-params.js";
5
4
  import { throwClientHookError } from "./client-hook-error.js";
5
+ import { ReadonlyURLSearchParams } from "./readonly-url-search-params.js";
6
6
  //#region src/shims/navigation.react-server.ts
7
7
  function usePathname() {
8
8
  throwClientHookError("usePathname()");
@@ -6,9 +6,7 @@ import { ComponentType, ReactElement } from "react";
6
6
  type BeforePopStateCallback = (state: {
7
7
  url: string;
8
8
  as: string;
9
- options: {
10
- shallow: boolean;
11
- };
9
+ options: TransitionOptions;
12
10
  }) => boolean;
13
11
  type NextRouter = {
14
12
  /** Current pathname */pathname: string; /** Current route pattern (e.g., "/posts/[id]") */
@@ -19,6 +19,7 @@ import { installWindowNext } from "../client/window-next.js";
19
19
  import { getWindowOrigin, isAbsoluteOrProtocolRelativeUrl, isHashOnlyBrowserUrlChange, normalizePathTrailingSlash, toBrowserNavigationHref, toSameOriginAppPath, withBasePath } from "./url-utils.js";
20
20
  import { scrollToHashTarget } from "./hash-scroll.js";
21
21
  import { installPagesRouterRuntime, setPagesRouterPopStateHandler, setStampInitialHistoryState } from "./pages-router-runtime.js";
22
+ import { interpolateDynamicRouteHref } from "./internal/interpolate-as.js";
22
23
  import { getCurrentBrowserLocale } from "./client-locale.js";
23
24
  import { Component, Fragment, createElement, useContext, useEffect, useLayoutEffect, useMemo, useState } from "react";
24
25
  //#region src/shims/router.ts
@@ -1118,7 +1119,7 @@ async function navigateClientHtml(url, fetchUrl, controller, navId, assertStillC
1118
1119
  * fixups). The JSON path derives its own URL from the browser-facing `url`
1119
1120
  * because the data endpoint speaks the unprefixed path.
1120
1121
  */
1121
- async function navigateClient(url, fetchUrl = url, options = {}) {
1122
+ async function navigateClient(url, fetchUrl = url, options = {}, routeUrl = url) {
1122
1123
  if (typeof window === "undefined") return;
1123
1124
  const previousAbortController = routerRuntimeState.activeAbortController;
1124
1125
  if (previousAbortController) queueMicrotask(() => previousAbortController.abort());
@@ -1135,7 +1136,7 @@ async function navigateClient(url, fetchUrl = url, options = {}) {
1135
1136
  else {
1136
1137
  let browserUrl = url;
1137
1138
  let htmlFetchUrl = fetchUrl;
1138
- let dataTarget = resolvePagesDataNavigationTarget(browserUrl, __basePath);
1139
+ let dataTarget = resolvePagesDataNavigationTarget(routeUrl, __basePath);
1139
1140
  let middlewareDataResponse;
1140
1141
  if (!dataTarget) {
1141
1142
  let middlewareEffect;
@@ -1178,9 +1179,9 @@ async function navigateClient(url, fetchUrl = url, options = {}) {
1178
1179
  * - "failed" — genuine error, caller should return false (hard nav is already
1179
1180
  * scheduled as recovery)
1180
1181
  */
1181
- async function runNavigateClient(fullUrl, resolvedUrl, fetchUrl = fullUrl, options = {}) {
1182
+ async function runNavigateClient(fullUrl, resolvedUrl, fetchUrl = fullUrl, options = {}, routeUrl = fullUrl) {
1182
1183
  try {
1183
- await navigateClient(fullUrl, fetchUrl, options);
1184
+ await navigateClient(fullUrl, fetchUrl, options, routeUrl);
1184
1185
  return "completed";
1185
1186
  } catch (err) {
1186
1187
  routerEvents.emit("routeChangeError", err, resolvedUrl, { shallow: false });
@@ -1252,9 +1253,9 @@ function updateHistory(mode, fullUrl, navState) {
1252
1253
  const previousKey = getRouterStateKey(window.history.state);
1253
1254
  const key = mode === "push" ? createHistoryKey() : previousKey ?? routerRuntimeState.currentHistoryKey ?? createHistoryKey();
1254
1255
  const state = {
1255
- url: navState?.url ?? fullUrl,
1256
- as: navState?.as ?? fullUrl,
1257
- options: navState?.options ?? {},
1256
+ url: navState.url,
1257
+ as: navState.as,
1258
+ options: navState.options,
1258
1259
  __N: true,
1259
1260
  key
1260
1261
  };
@@ -1300,8 +1301,13 @@ async function performNavigation(url, as, options, mode, onStateUpdate) {
1300
1301
  assertSafeNavigationUrl(resolveUrl(url));
1301
1302
  if (as !== void 0) assertSafeNavigationUrl(String(as));
1302
1303
  const navigationLocale = resolveTransitionLocale(options?.locale);
1303
- let resolved = resolveNavigationTarget(url, as, navigationLocale, as === void 0 && options?.locale !== void 0 && typeof url !== "string" && url.pathname === void 0 && (url.query !== void 0 && Object.keys(url.query).length > 0 || typeof url.search === "string" && url.search.length > 0 || typeof url.hash === "string" && url.hash.length > 0));
1304
- if (as === void 0 && (typeof url === "string" && options?._vinextInterpolateDynamicRoute === true || typeof url !== "string" && url.pathname === void 0 && (url.query !== void 0 && Object.keys(url.query).length > 0 || typeof url.search === "string" && url.search.length > 0))) resolved = interpolateCurrentDynamicRoute(resolved);
1304
+ const replaceInheritedLocale = as === void 0 && options?.locale !== void 0 && typeof url !== "string" && url.pathname === void 0 && (url.query !== void 0 && Object.keys(url.query).length > 0 || typeof url.search === "string" && url.search.length > 0 || typeof url.hash === "string" && url.hash.length > 0);
1305
+ let resolved = resolveNavigationTarget(url, as, navigationLocale, replaceInheritedLocale);
1306
+ let resolvedRoute = applyNavigationLocale(resolveUrl(url), navigationLocale, replaceInheritedLocale);
1307
+ if (as === void 0 && (typeof url === "string" && options?._vinextInterpolateDynamicRoute === true || typeof url !== "string" && url.pathname === void 0 && (url.query !== void 0 && Object.keys(url.query).length > 0 || typeof url.search === "string" && url.search.length > 0))) {
1308
+ resolved = interpolateCurrentDynamicRoute(resolved);
1309
+ resolvedRoute = interpolateCurrentDynamicRoute(resolvedRoute);
1310
+ }
1305
1311
  if (isExternalUrl(resolved)) {
1306
1312
  const localPath = toSameOriginAppPath(resolved, __basePath);
1307
1313
  if (localPath == null) {
@@ -1311,10 +1317,36 @@ async function performNavigation(url, as, options, mode, onStateUpdate) {
1311
1317
  }
1312
1318
  resolved = localPath;
1313
1319
  }
1320
+ if (isExternalUrl(resolvedRoute)) {
1321
+ const localPath = toSameOriginAppPath(resolvedRoute, __basePath);
1322
+ if (localPath != null) resolvedRoute = localPath;
1323
+ }
1314
1324
  resolved = normalizePathTrailingSlash(resolved, __trailingSlash);
1325
+ resolvedRoute = normalizePathTrailingSlash(resolvedRoute, __trailingSlash);
1326
+ let interpolatedRoute = resolvedRoute;
1327
+ if (resolvedRoute.includes("[")) {
1328
+ const projection = interpolateDynamicRouteHref(resolvedRoute, resolved, typeof url === "string" ? void 0 : url.query);
1329
+ if (projection?.href) {
1330
+ interpolatedRoute = projection.href;
1331
+ if (as === void 0 && stripHash(resolved).split("?", 1)[0] === projection.routePathname) {
1332
+ const remaining = new URLSearchParams();
1333
+ const consumed = new Set(projection.params);
1334
+ for (const [k, v] of Object.entries(projection.query)) {
1335
+ if (consumed.has(k)) continue;
1336
+ if (v === void 0) continue;
1337
+ if (Array.isArray(v)) v.forEach((entry) => remaining.append(k, entry));
1338
+ else remaining.append(k, v);
1339
+ }
1340
+ const searchStr = remaining.toString();
1341
+ const hashStr = extractHash(resolved);
1342
+ resolved = normalizePathTrailingSlash(`${projection.href.split(/[?#]/, 1)[0]}${searchStr ? `?${searchStr}` : ""}${hashStr}`, __trailingSlash);
1343
+ }
1344
+ } else interpolatedRoute = resolved;
1345
+ }
1315
1346
  const full = normalizePathTrailingSlash(toBrowserNavigationHref(resolved, window.location.href, __basePath), __trailingSlash);
1347
+ const fullRouteUrl = interpolatedRoute !== resolved ? normalizePathTrailingSlash(toBrowserNavigationHref(interpolatedRoute, window.location.href, __basePath), __trailingSlash) : full;
1316
1348
  const errorRouteHtmlFetchUrl = resolvePagesErrorHtmlFetchUrl(url, navigationLocale);
1317
- const htmlFetchUrl = errorRouteHtmlFetchUrl ?? getPagesHtmlFetchUrl(full, navigationLocale);
1349
+ const htmlFetchUrl = errorRouteHtmlFetchUrl ?? getPagesHtmlFetchUrl(fullRouteUrl, navigationLocale);
1318
1350
  const shallow = options?.shallow ?? false;
1319
1351
  const doScroll = options?.scroll !== false;
1320
1352
  const hash = extractHash(resolved);
@@ -1330,15 +1362,17 @@ async function performNavigation(url, as, options, mode, onStateUpdate) {
1330
1362
  mode,
1331
1363
  scroll: scrollTarget
1332
1364
  };
1365
+ const currentLocale = getCurrentUrlLocale();
1366
+ if (mode === "push" && interpolatedRoute !== resolved && stripHash(full) === routerRuntimeState.lastPathnameAndSearch && navigationLocale === currentLocale) mode = "replace";
1333
1367
  const navStateOptions = { shallow };
1334
1368
  if (navigationLocale !== void 0) navStateOptions.locale = navigationLocale;
1335
1369
  const resolvedNoHash = stripHash(resolved);
1336
1370
  const navState = {
1337
- url: resolvedNoHash,
1371
+ url: stripHash(interpolatedRoute),
1338
1372
  as: resolvedNoHash,
1339
1373
  options: navStateOptions
1340
1374
  };
1341
- if (options?._h !== 1 && isHashOnlyChange(full)) {
1375
+ if (options?._h !== 1 && interpolatedRoute === resolved && isHashOnlyChange(full)) {
1342
1376
  if (mode === "push") saveScrollPosition();
1343
1377
  const eventUrl = resolveHashUrl(full);
1344
1378
  routerEvents.emit("hashChangeStart", eventUrl, { shallow });
@@ -1363,7 +1397,7 @@ async function performNavigation(url, as, options, mode, onStateUpdate) {
1363
1397
  routerEvents.emit("beforeHistoryChange", resolved, { shallow });
1364
1398
  updateHistory(mode, full, navState);
1365
1399
  if (!shallow) {
1366
- const result = await runNavigateClient(full, resolved, htmlFetchUrl, navigateOptions);
1400
+ const result = await runNavigateClient(full, resolved, htmlFetchUrl, navigateOptions, fullRouteUrl);
1367
1401
  if (result === "cancelled") return true;
1368
1402
  if (result === "failed") return false;
1369
1403
  } else if (doScroll) if (hash) scrollToHashTarget(hash);
@@ -1479,7 +1513,7 @@ function PagesRouterProvider({ children }) {
1479
1513
  return AppRouterContext ? createElement(AppRouterContext.Provider, { value: appRouter }, content) : content;
1480
1514
  }
1481
1515
  function isNextRouterState(state) {
1482
- return typeof state === "object" && state !== null && "__N" in state && state.__N === true;
1516
+ return typeof state === "object" && state !== null && "__N" in state && state.__N === true && "url" in state && typeof state.url === "string" && "as" in state && typeof state.as === "string" && "options" in state && typeof state.options === "object" && state.options !== null;
1483
1517
  }
1484
1518
  function getRouterStateKey(state) {
1485
1519
  if (!isNextRouterState(state)) return void 0;
@@ -1508,11 +1542,16 @@ function handlePagesRouterPopState(e) {
1508
1542
  };
1509
1543
  }
1510
1544
  if (routerRuntimeState.beforePopStateCb !== void 0) {
1511
- if (!routerRuntimeState.beforePopStateCb({
1545
+ const beforePopStateState = isNextRouterState(state) ? {
1546
+ url: state.url,
1547
+ as: state.as,
1548
+ options: state.options
1549
+ } : {
1512
1550
  url: appUrl,
1513
1551
  as: appUrl,
1514
1552
  options: { shallow: false }
1515
- })) return;
1553
+ };
1554
+ if (!routerRuntimeState.beforePopStateCb({ ...beforePopStateState })) return;
1516
1555
  }
1517
1556
  if (targetKey !== void 0) routerRuntimeState.currentHistoryKey = targetKey;
1518
1557
  routerRuntimeState.lastPathnameAndSearch = browserUrl;
@@ -1524,7 +1563,8 @@ function handlePagesRouterPopState(e) {
1524
1563
  dispatchNavigateEvent();
1525
1564
  return;
1526
1565
  }
1527
- const effectiveLocale = (isNextRouterState(state) ? state.options?.locale : void 0) ?? window.__VINEXT_LOCALE__;
1566
+ const stateLocale = isNextRouterState(state) ? state.options?.locale : void 0;
1567
+ const effectiveLocale = typeof stateLocale === "string" ? stateLocale : window.__VINEXT_LOCALE__;
1528
1568
  const fullAppUrl = appUrl + window.location.hash;
1529
1569
  routerEvents.emit("routeChangeStart", fullAppUrl, { shallow: false });
1530
1570
  routerEvents.emit("beforeHistoryChange", fullAppUrl, { shallow: false });
@@ -1533,7 +1573,11 @@ function handlePagesRouterPopState(e) {
1533
1573
  x: 0,
1534
1574
  y: 0
1535
1575
  } : readScrollPosition(state);
1536
- if (await runNavigateClient(browserUrl, fullAppUrl, getPagesHtmlFetchUrl(browserUrl, effectiveLocale), { scroll: scrollTarget }) === "completed") {
1576
+ const stateRouteUrl = (() => {
1577
+ if (isNextRouterState(state) && typeof state.url === "string" && typeof state.as === "string" && state.url !== state.as) return normalizePathTrailingSlash(withBasePath(state.url, __basePath), __trailingSlash);
1578
+ return browserUrl;
1579
+ })();
1580
+ if (await runNavigateClient(browserUrl, fullAppUrl, getPagesHtmlFetchUrl(stateRouteUrl, effectiveLocale), { scroll: scrollTarget }, stateRouteUrl) === "completed") {
1537
1581
  routerEvents.emit("routeChangeComplete", fullAppUrl, { shallow: false });
1538
1582
  dispatchNavigateEvent();
1539
1583
  }
package/dist/typegen.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { findDir } from "./utils/project.js";
1
2
  import { decodeRouteSegment, isInvisibleSegment } from "./routing/utils.js";
2
3
  import { patternToNextFormat } from "./routing/route-validation.js";
3
4
  import { compareStrings } from "./utils/compare.js";
@@ -15,7 +16,7 @@ import "./.next/types/routes.d.ts";
15
16
  `;
16
17
  async function generateRouteTypes(options) {
17
18
  const root = path.resolve(options.root);
18
- const appDir = options.appDir ? path.resolve(options.appDir) : await findAppDir(root);
19
+ const appDir = options.appDir ? path.resolve(options.appDir) : findDir(root, "app", path.join("src", "app"));
19
20
  const outPath = path.join(root, ".next", "types", "routes.d.ts");
20
21
  const content = appDir ? renderRouteTypes(await collectRouteTypeModel(appDir, options.pageExtensions)) : renderRouteTypes(emptyRouteTypeModel());
21
22
  await fs.mkdir(path.dirname(outPath), { recursive: true });
@@ -84,15 +85,6 @@ async function collectRouteTypeModel(appDir, pageExtensions) {
84
85
  for (const slotNames of model.layoutSlots.values()) slotNames.sort(compareStrings);
85
86
  return model;
86
87
  }
87
- async function findAppDir(root) {
88
- for (const rel of ["app", path.join("src", "app")]) {
89
- const candidate = path.join(root, rel);
90
- try {
91
- if ((await fs.stat(candidate)).isDirectory()) return candidate;
92
- } catch {}
93
- }
94
- return null;
95
- }
96
88
  function renderRouteTypes(model) {
97
89
  const allRoutes = uniqueSorted([
98
90
  ...model.pageRoutes,
@@ -62,9 +62,15 @@ declare function findInNodeModules(start: string, subPath: string): string | nul
62
62
  * Check if a vite.config file exists in the project root.
63
63
  */
64
64
  declare function hasViteConfig(root: string): boolean;
65
+ /**
66
+ * Return the first candidate directory (resolved against `root`) that exists,
67
+ * or null. Used to locate conventional `app`/`pages` directories that may live
68
+ * at the root or under `src/`.
69
+ */
70
+ declare function findDir(root: string, ...candidates: string[]): string | null;
65
71
  /**
66
72
  * Check if the project uses App Router (has an app/ directory).
67
73
  */
68
74
  declare function hasAppDir(root: string): boolean;
69
75
  //#endregion
70
- export { detectPackageManager, detectPackageManagerName, ensureESModule, ensureViteConfigCompatibility, findInNodeModules, hasAppDir, hasViteConfig, renameCJSConfigs };
76
+ export { detectPackageManager, detectPackageManagerName, ensureESModule, ensureViteConfigCompatibility, findDir, findInNodeModules, hasAppDir, hasViteConfig, renameCJSConfigs };
@@ -190,10 +190,24 @@ function hasViteConfig(root) {
190
190
  return fs.existsSync(path.join(root, "vite.config.ts")) || fs.existsSync(path.join(root, "vite.config.js")) || fs.existsSync(path.join(root, "vite.config.mjs"));
191
191
  }
192
192
  /**
193
+ * Return the first candidate directory (resolved against `root`) that exists,
194
+ * or null. Used to locate conventional `app`/`pages` directories that may live
195
+ * at the root or under `src/`.
196
+ */
197
+ function findDir(root, ...candidates) {
198
+ for (const candidate of candidates) {
199
+ const full = path.join(root, candidate);
200
+ try {
201
+ if (fs.statSync(full).isDirectory()) return full;
202
+ } catch {}
203
+ }
204
+ return null;
205
+ }
206
+ /**
193
207
  * Check if the project uses App Router (has an app/ directory).
194
208
  */
195
209
  function hasAppDir(root) {
196
- return fs.existsSync(path.join(root, "app")) || fs.existsSync(path.join(root, "src", "app"));
210
+ return findDir(root, "app", path.join("src", "app")) !== null;
197
211
  }
198
212
  //#endregion
199
- export { detectPackageManager, detectPackageManagerName, ensureESModule, ensureViteConfigCompatibility, findInNodeModules, hasAppDir, hasViteConfig, renameCJSConfigs };
213
+ export { detectPackageManager, detectPackageManagerName, ensureESModule, ensureViteConfigCompatibility, findDir, findInNodeModules, hasAppDir, hasViteConfig, renameCJSConfigs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vinext",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Run Next.js apps on Vite. Drop-in replacement for the next CLI.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -103,8 +103,8 @@
103
103
  "@vitejs/plugin-react": "^6.0.1",
104
104
  "@vitejs/plugin-rsc": "^0.5.27",
105
105
  "react-server-dom-webpack": "^19.2.7",
106
- "vite": "npm:@voidzero-dev/vite-plus-core@0.1.24",
107
- "vite-plus": "0.1.24",
106
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.2.1",
107
+ "vite-plus": "0.2.1",
108
108
  "@vinext/cloudflare": "0.1.2"
109
109
  },
110
110
  "peerDependencies": {