vinext 0.1.1 → 0.1.2

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 (147) hide show
  1. package/README.md +2 -5
  2. package/dist/build/client-build-config.d.ts +7 -1
  3. package/dist/build/client-build-config.js +9 -1
  4. package/dist/check.js +4 -3
  5. package/dist/client/navigation-runtime.d.ts +3 -2
  6. package/dist/client/window-next.d.ts +6 -4
  7. package/dist/config/config-matchers.d.ts +11 -4
  8. package/dist/config/config-matchers.js +15 -2
  9. package/dist/config/next-config.d.ts +13 -0
  10. package/dist/config/next-config.js +2 -0
  11. package/dist/deploy.js +9 -2
  12. package/dist/entries/app-rsc-entry.js +7 -1
  13. package/dist/entries/pages-client-entry.js +1 -1
  14. package/dist/entries/pages-server-entry.js +7 -6
  15. package/dist/index.d.ts +0 -2
  16. package/dist/index.js +86 -78
  17. package/dist/plugins/dynamic-preload-metadata.d.ts +13 -0
  18. package/dist/plugins/dynamic-preload-metadata.js +415 -0
  19. package/dist/plugins/og-assets.js +2 -2
  20. package/dist/plugins/optimize-imports.d.ts +8 -4
  21. package/dist/plugins/optimize-imports.js +16 -12
  22. package/dist/plugins/sass.d.ts +53 -24
  23. package/dist/plugins/sass.js +249 -1
  24. package/dist/plugins/wasm-module-import.d.ts +15 -0
  25. package/dist/plugins/wasm-module-import.js +50 -0
  26. package/dist/routing/app-route-graph.d.ts +23 -1
  27. package/dist/routing/app-route-graph.js +47 -8
  28. package/dist/routing/file-matcher.js +1 -1
  29. package/dist/server/app-browser-entry.js +108 -213
  30. package/dist/server/app-browser-error.d.ts +4 -1
  31. package/dist/server/app-browser-error.js +7 -1
  32. package/dist/server/app-browser-history-controller.d.ts +104 -0
  33. package/dist/server/app-browser-history-controller.js +210 -0
  34. package/dist/server/app-browser-navigation-controller.d.ts +3 -2
  35. package/dist/server/app-browser-navigation-controller.js +10 -7
  36. package/dist/server/app-browser-rsc-redirect.d.ts +11 -2
  37. package/dist/server/app-browser-rsc-redirect.js +30 -8
  38. package/dist/server/app-browser-state.js +4 -7
  39. package/dist/server/app-browser-visible-commit.js +1 -1
  40. package/dist/server/app-fallback-renderer.d.ts +2 -1
  41. package/dist/server/app-fallback-renderer.js +3 -1
  42. package/dist/server/app-middleware.js +1 -0
  43. package/dist/server/app-optimistic-routing.js +22 -1
  44. package/dist/server/app-page-boundary-render.d.ts +2 -1
  45. package/dist/server/app-page-boundary-render.js +4 -2
  46. package/dist/server/app-page-cache.js +9 -7
  47. package/dist/server/app-page-dispatch.d.ts +8 -0
  48. package/dist/server/app-page-dispatch.js +18 -5
  49. package/dist/server/app-page-element-builder.d.ts +22 -2
  50. package/dist/server/app-page-element-builder.js +37 -8
  51. package/dist/server/app-page-execution.d.ts +1 -1
  52. package/dist/server/app-page-execution.js +32 -17
  53. package/dist/server/app-page-render.d.ts +1 -1
  54. package/dist/server/app-page-render.js +7 -14
  55. package/dist/server/app-page-request.d.ts +1 -0
  56. package/dist/server/app-page-request.js +3 -2
  57. package/dist/server/app-page-response.js +1 -1
  58. package/dist/server/app-page-route-wiring.d.ts +3 -1
  59. package/dist/server/app-page-route-wiring.js +8 -7
  60. package/dist/server/app-page-stream.d.ts +1 -6
  61. package/dist/server/app-page-stream.js +1 -4
  62. package/dist/server/app-route-handler-response.js +11 -10
  63. package/dist/server/app-route-handler-runtime.js +12 -1
  64. package/dist/server/app-rsc-handler.js +1 -1
  65. package/dist/server/app-rsc-response-finalizer.js +1 -1
  66. package/dist/server/app-server-action-execution.d.ts +11 -0
  67. package/dist/server/app-server-action-execution.js +5 -2
  68. package/dist/server/app-ssr-entry.js +2 -2
  69. package/dist/server/app-ssr-stream.js +9 -1
  70. package/dist/server/dev-lockfile.js +2 -1
  71. package/dist/server/dev-server.js +43 -12
  72. package/dist/server/headers.d.ts +8 -1
  73. package/dist/server/headers.js +8 -1
  74. package/dist/server/instrumentation-runtime.d.ts +6 -0
  75. package/dist/server/instrumentation-runtime.js +8 -0
  76. package/dist/server/isr-decision.d.ts +79 -0
  77. package/dist/server/isr-decision.js +70 -0
  78. package/dist/server/metadata-route-response.js +5 -3
  79. package/dist/server/middleware-runtime.d.ts +13 -0
  80. package/dist/server/middleware-runtime.js +11 -7
  81. package/dist/server/middleware.js +1 -0
  82. package/dist/server/navigation-planner.d.ts +62 -1
  83. package/dist/server/navigation-planner.js +188 -0
  84. package/dist/server/navigation-trace.d.ts +11 -1
  85. package/dist/server/navigation-trace.js +11 -1
  86. package/dist/server/normalize-path.d.ts +0 -8
  87. package/dist/server/normalize-path.js +3 -1
  88. package/dist/server/otel-tracer-extension.d.ts +45 -0
  89. package/dist/server/otel-tracer-extension.js +89 -0
  90. package/dist/server/pages-api-route.d.ts +14 -3
  91. package/dist/server/pages-api-route.js +6 -1
  92. package/dist/server/pages-asset-tags.d.ts +15 -4
  93. package/dist/server/pages-asset-tags.js +18 -12
  94. package/dist/server/pages-data-route.js +5 -1
  95. package/dist/server/pages-node-compat.d.ts +3 -11
  96. package/dist/server/pages-node-compat.js +174 -121
  97. package/dist/server/pages-page-data.d.ts +28 -0
  98. package/dist/server/pages-page-data.js +61 -17
  99. package/dist/server/pages-page-handler.d.ts +1 -0
  100. package/dist/server/pages-page-handler.js +22 -6
  101. package/dist/server/pages-page-response.d.ts +45 -1
  102. package/dist/server/pages-page-response.js +66 -5
  103. package/dist/server/pages-readiness.d.ts +1 -1
  104. package/dist/server/pages-request-pipeline.d.ts +15 -1
  105. package/dist/server/pages-request-pipeline.js +23 -2
  106. package/dist/server/prod-server.d.ts +39 -1
  107. package/dist/server/prod-server.js +98 -34
  108. package/dist/shims/cache-runtime.js +9 -2
  109. package/dist/shims/dynamic-preload-chunks.d.ts +8 -0
  110. package/dist/shims/dynamic-preload-chunks.js +77 -0
  111. package/dist/shims/dynamic.d.ts +4 -0
  112. package/dist/shims/dynamic.js +4 -2
  113. package/dist/shims/error-boundary.d.ts +4 -4
  114. package/dist/shims/error.js +37 -11
  115. package/dist/shims/fetch-cache.d.ts +9 -1
  116. package/dist/shims/fetch-cache.js +11 -1
  117. package/dist/shims/head.js +6 -1
  118. package/dist/shims/headers.d.ts +16 -2
  119. package/dist/shims/headers.js +37 -1
  120. package/dist/shims/image-config.js +7 -1
  121. package/dist/shims/internal/app-route-detection.d.ts +6 -3
  122. package/dist/shims/internal/app-route-detection.js +10 -6
  123. package/dist/shims/internal/app-router-context.d.ts +5 -0
  124. package/dist/shims/metadata.d.ts +6 -2
  125. package/dist/shims/metadata.js +32 -14
  126. package/dist/shims/navigation.d.ts +7 -16
  127. package/dist/shims/navigation.js +33 -16
  128. package/dist/shims/router.js +28 -1
  129. package/dist/shims/script-nonce-context.d.ts +1 -1
  130. package/dist/shims/script-nonce-context.js +11 -3
  131. package/dist/shims/server.d.ts +17 -1
  132. package/dist/shims/server.js +31 -6
  133. package/dist/shims/slot.js +1 -1
  134. package/dist/shims/unified-request-context.js +1 -0
  135. package/dist/typegen.js +1 -0
  136. package/dist/utils/client-build-manifest.js +15 -5
  137. package/dist/utils/client-runtime-metadata.d.ts +45 -0
  138. package/dist/utils/client-runtime-metadata.js +63 -0
  139. package/dist/utils/hash.d.ts +17 -1
  140. package/dist/utils/hash.js +36 -1
  141. package/dist/utils/lazy-chunks.d.ts +27 -1
  142. package/dist/utils/lazy-chunks.js +65 -1
  143. package/dist/utils/manifest-paths.d.ts +20 -2
  144. package/dist/utils/manifest-paths.js +38 -3
  145. package/dist/utils/path.d.ts +2 -1
  146. package/dist/utils/path.js +5 -1
  147. package/package.json +2 -2
@@ -0,0 +1,104 @@
1
+ import { BfcacheIdMap, HistoryTraversalIntent } from "./app-history-state.js";
2
+ import { AppRouterState } from "./app-browser-state.js";
3
+ import { HistoryUpdateMode } from "./app-browser-navigation-controller.js";
4
+
5
+ //#region src/server/app-browser-history-controller.d.ts
6
+ /**
7
+ * Visible router-state metadata at the instant a hash-only navigation commits.
8
+ * `null` means the browser router tree has not committed yet, so the controller
9
+ * falls back to reading the same facts off the live history entry.
10
+ */
11
+ type VisibleNavigationMetadata = {
12
+ bfcacheIds: BfcacheIdMap | null;
13
+ previousNextUrl: string | null;
14
+ };
15
+ type AppBrowserHistoryControllerDeps = {
16
+ initialHistoryState: unknown;
17
+ maxHistoryStateSnapshots: number; /** Reads `window.history.state`. Injected so the controller stays unit-testable. */
18
+ readHistoryState: () => unknown; /** Reads `window.location.href`. Injected so the controller stays unit-testable. */
19
+ readCurrentHref: () => string; /** Wraps `pushHistoryStateWithoutNotify(state, "", href)`. */
20
+ pushHistoryState: (state: unknown, href: string) => void; /** Wraps `replaceHistoryStateWithoutNotify(state, "", href)`. */
21
+ replaceHistoryState: (state: unknown, href: string) => void;
22
+ readVisibleNavigationMetadata: () => VisibleNavigationMetadata | null;
23
+ };
24
+ /**
25
+ * Candidate visible state resolved from a restorable history snapshot, handed to
26
+ * the entry's approved-visible-restore callback. The controller resolves the
27
+ * candidate and owns the traversal-index commit; the entry owns the actual
28
+ * `AppBrowserNavigationController.restoreHistorySnapshotVisibleState()` call and
29
+ * the `ApprovedVisibleCommit` boundary.
30
+ */
31
+ type RestorableSnapshotCandidate = {
32
+ state: AppRouterState;
33
+ beforeCommit: () => void;
34
+ };
35
+ type RestoreHistorySnapshotOptions = {
36
+ historyState: unknown;
37
+ stageClientParams: (params: Record<string, string | string[]>) => void;
38
+ approveVisibleRestore: (candidate: RestorableSnapshotCandidate) => boolean;
39
+ };
40
+ type CommitNavigationHistoryOptions = {
41
+ bfcacheIds: BfcacheIdMap;
42
+ href: string;
43
+ historyUpdateMode: HistoryUpdateMode | undefined;
44
+ previousNextUrl: string | null;
45
+ targetHistoryIndex?: number | null;
46
+ stageClientParams: () => void;
47
+ };
48
+ /**
49
+ * Owns App Router browser-history metadata and traversal bookkeeping behind a
50
+ * typed seam: traversal index allocation/commit, push/replace/traverse/hash-only
51
+ * history-state writes, BFCache epoch/snapshot invalidation through
52
+ * `RestorableClientStateController`, and restorable-snapshot candidate
53
+ * resolution.
54
+ *
55
+ * Ownership boundary: this is not a second router or visible-state authority. It
56
+ * resolves history facts and delegates visible restoration through an injected
57
+ * approved-commit callback. It never sets router state directly, never imports
58
+ * `applyApprovedVisibleCommit()`, and never bypasses the `ApprovedVisibleCommit`
59
+ * boundary owned by `AppBrowserNavigationController`.
60
+ */
61
+ declare class AppBrowserHistoryController {
62
+ #private;
63
+ constructor(deps: AppBrowserHistoryControllerDeps);
64
+ get currentHistoryTraversalIndex(): number | null;
65
+ allocateNavigationHistoryTraversalIndex(historyUpdateMode: HistoryUpdateMode | undefined): number | null;
66
+ commitHistoryTraversalIndex(index: number | null): void;
67
+ commitTraversalIndexFromHistoryState(historyState: unknown): void;
68
+ resolveTraversalIntent(historyState: unknown): HistoryTraversalIntent;
69
+ readCurrentBfcacheVersionHistoryIds(historyState: unknown): BfcacheIdMap | null;
70
+ isCacheInvalidationGuarded(): boolean;
71
+ isCurrentBfcacheVersion(historyState: unknown): boolean;
72
+ beginCacheInvalidationGuard(): () => void;
73
+ invalidateRestorableClientState(): void;
74
+ rememberHistoryStateSnapshot(state: AppRouterState): void;
75
+ commitHashOnlyNavigation(href: string, historyUpdateMode: HistoryUpdateMode, scroll: boolean): void;
76
+ /**
77
+ * Writes the history entry for an approved push/replace/traverse commit and
78
+ * advances the traversal index. `stageClientParams` runs at the exact point it
79
+ * ran inline in the browser-entry commit effect so client-param staging stays
80
+ * ordered relative to the history write. Mirrors Next.js committing tree state
81
+ * into the history entry during the navigation commit.
82
+ */
83
+ commitNavigationHistory(options: CommitNavigationHistoryOptions): void;
84
+ syncCurrentHistoryStatePreviousNextUrl(previousNextUrl: string | null, bfcacheIds?: BfcacheIdMap | null): void;
85
+ /** Initial history write performed before hydration starts. */
86
+ writeBootstrapHistoryMetadata(): void;
87
+ /** History write performed on the first committed (hydrated) render. */
88
+ writeHydratedHistoryMetadata(options: {
89
+ bfcacheIds: BfcacheIdMap;
90
+ previousNextUrl: string | null;
91
+ }): void;
92
+ /**
93
+ * Resolves a restorable snapshot candidate for the given history entry and
94
+ * commits the traversal index after, and only after, the injected
95
+ * approved-visible-restore callback succeeds. The traversal-index commit and
96
+ * client-param staging run inside `beforeCommit`, which the
97
+ * `AppBrowserNavigationController` invokes only once the `ApprovedVisibleCommit`
98
+ * is approved. Returns false when no snapshot is restorable or the restore is
99
+ * not approved.
100
+ */
101
+ restoreHistorySnapshot(options: RestoreHistorySnapshotOptions): boolean;
102
+ }
103
+ //#endregion
104
+ export { AppBrowserHistoryController, RestorableSnapshotCandidate };
@@ -0,0 +1,210 @@
1
+ import { RestorableClientStateController, createHistoryStateWithNavigationMetadata, readHistoryStateBfcacheIds, readHistoryStatePreviousNextUrl, readHistoryStateTraversalIndex, resolveHistoryTraversalIntent } from "./app-history-state.js";
2
+ //#region src/server/app-browser-history-controller.ts
3
+ function stripVinextScrollState(state) {
4
+ if (!state || typeof state !== "object") return state;
5
+ const nextState = {};
6
+ for (const [key, value] of Object.entries(state)) {
7
+ if (key === "__vinext_scrollX" || key === "__vinext_scrollY") continue;
8
+ nextState[key] = value;
9
+ }
10
+ return Object.keys(nextState).length > 0 ? nextState : null;
11
+ }
12
+ /**
13
+ * Owns App Router browser-history metadata and traversal bookkeeping behind a
14
+ * typed seam: traversal index allocation/commit, push/replace/traverse/hash-only
15
+ * history-state writes, BFCache epoch/snapshot invalidation through
16
+ * `RestorableClientStateController`, and restorable-snapshot candidate
17
+ * resolution.
18
+ *
19
+ * Ownership boundary: this is not a second router or visible-state authority. It
20
+ * resolves history facts and delegates visible restoration through an injected
21
+ * approved-commit callback. It never sets router state directly, never imports
22
+ * `applyApprovedVisibleCommit()`, and never bypasses the `ApprovedVisibleCommit`
23
+ * boundary owned by `AppBrowserNavigationController`.
24
+ */
25
+ var AppBrowserHistoryController = class {
26
+ #restorableClientState;
27
+ #readHistoryState;
28
+ #readCurrentHref;
29
+ #pushHistoryState;
30
+ #replaceHistoryState;
31
+ #readVisibleNavigationMetadata;
32
+ #currentHistoryTraversalIndex;
33
+ #nextHistoryTraversalIndex;
34
+ constructor(deps) {
35
+ this.#readHistoryState = deps.readHistoryState;
36
+ this.#readCurrentHref = deps.readCurrentHref;
37
+ this.#pushHistoryState = deps.pushHistoryState;
38
+ this.#replaceHistoryState = deps.replaceHistoryState;
39
+ this.#readVisibleNavigationMetadata = deps.readVisibleNavigationMetadata;
40
+ this.#restorableClientState = new RestorableClientStateController({
41
+ initialHistoryState: deps.initialHistoryState,
42
+ maxHistoryStateSnapshots: deps.maxHistoryStateSnapshots
43
+ });
44
+ this.#currentHistoryTraversalIndex = readHistoryStateTraversalIndex(deps.initialHistoryState) ?? 0;
45
+ this.#nextHistoryTraversalIndex = this.#currentHistoryTraversalIndex;
46
+ }
47
+ get currentHistoryTraversalIndex() {
48
+ return this.#currentHistoryTraversalIndex;
49
+ }
50
+ allocateNavigationHistoryTraversalIndex(historyUpdateMode) {
51
+ switch (historyUpdateMode) {
52
+ case "push": return this.#nextHistoryTraversalIndex + 1;
53
+ case "replace": return this.#currentHistoryTraversalIndex;
54
+ case void 0: return null;
55
+ default: throw new Error("[vinext] Unknown history update mode: " + String(historyUpdateMode));
56
+ }
57
+ }
58
+ commitHistoryTraversalIndex(index) {
59
+ this.#currentHistoryTraversalIndex = index;
60
+ if (index !== null) this.#nextHistoryTraversalIndex = Math.max(this.#nextHistoryTraversalIndex, index);
61
+ }
62
+ commitTraversalIndexFromHistoryState(historyState) {
63
+ this.commitHistoryTraversalIndex(readHistoryStateTraversalIndex(historyState));
64
+ }
65
+ resolveTraversalIntent(historyState) {
66
+ return resolveHistoryTraversalIntent({
67
+ currentHistoryIndex: this.#currentHistoryTraversalIndex,
68
+ historyState
69
+ });
70
+ }
71
+ readCurrentBfcacheVersionHistoryIds(historyState) {
72
+ return this.#restorableClientState.readCurrentBfcacheVersionHistoryIds(historyState);
73
+ }
74
+ isCacheInvalidationGuarded() {
75
+ return this.#restorableClientState.isCacheInvalidationGuarded();
76
+ }
77
+ isCurrentBfcacheVersion(historyState) {
78
+ return this.#restorableClientState.isCurrentBfcacheVersion(historyState);
79
+ }
80
+ beginCacheInvalidationGuard() {
81
+ return this.#restorableClientState.beginCacheInvalidationGuard();
82
+ }
83
+ invalidateRestorableClientState() {
84
+ this.#restorableClientState.invalidateClientState();
85
+ }
86
+ rememberHistoryStateSnapshot(state) {
87
+ this.#restorableClientState.rememberHistoryStateSnapshot({
88
+ historyIndex: this.#currentHistoryTraversalIndex,
89
+ state
90
+ });
91
+ }
92
+ commitHashOnlyNavigation(href, historyUpdateMode, scroll) {
93
+ const navigationHistoryIndex = this.allocateNavigationHistoryTraversalIndex(historyUpdateMode);
94
+ const historyState = this.#readHistoryState();
95
+ const visible = this.#readVisibleNavigationMetadata();
96
+ const previousNextUrl = visible ? visible.previousNextUrl : readHistoryStatePreviousNextUrl(historyState);
97
+ const bfcacheIds = visible ? visible.bfcacheIds : this.#restorableClientState.readCurrentBfcacheVersionHistoryIds(historyState);
98
+ const nextHistoryState = createHistoryStateWithNavigationMetadata(this.#createHashOnlyNavigationBaseHistoryState(historyUpdateMode, scroll), {
99
+ bfcacheIds,
100
+ bfcacheVersion: bfcacheIds === null ? void 0 : this.#restorableClientState.currentBfcacheVersion,
101
+ previousNextUrl,
102
+ traversalIndex: navigationHistoryIndex
103
+ });
104
+ if (historyUpdateMode === "replace") this.#replaceHistoryState(nextHistoryState, href);
105
+ else this.#pushHistoryState(nextHistoryState, href);
106
+ this.commitHistoryTraversalIndex(navigationHistoryIndex);
107
+ }
108
+ #createHashOnlyNavigationBaseHistoryState(historyUpdateMode, scroll) {
109
+ if (historyUpdateMode !== "replace") return null;
110
+ const historyState = this.#readHistoryState();
111
+ return scroll ? stripVinextScrollState(historyState) : historyState;
112
+ }
113
+ /**
114
+ * Writes the history entry for an approved push/replace/traverse commit and
115
+ * advances the traversal index. `stageClientParams` runs at the exact point it
116
+ * ran inline in the browser-entry commit effect so client-param staging stays
117
+ * ordered relative to the history write. Mirrors Next.js committing tree state
118
+ * into the history entry during the navigation commit.
119
+ */
120
+ commitNavigationHistory(options) {
121
+ const currentHref = this.#readCurrentHref();
122
+ const origin = new URL(currentHref).origin;
123
+ const targetHref = new URL(options.href, origin).href;
124
+ const preserveExistingState = options.historyUpdateMode === "replace";
125
+ const navigationHistoryIndex = options.targetHistoryIndex !== void 0 ? options.targetHistoryIndex : this.allocateNavigationHistoryTraversalIndex(options.historyUpdateMode);
126
+ const historyState = createHistoryStateWithNavigationMetadata(preserveExistingState ? this.#readHistoryState() : null, {
127
+ bfcacheIds: options.bfcacheIds,
128
+ bfcacheVersion: this.#restorableClientState.currentBfcacheVersion,
129
+ previousNextUrl: options.previousNextUrl,
130
+ traversalIndex: navigationHistoryIndex
131
+ });
132
+ let wroteHistoryState = false;
133
+ if (options.historyUpdateMode === "replace" && currentHref !== targetHref) {
134
+ options.stageClientParams();
135
+ this.#replaceHistoryState(historyState, options.href);
136
+ wroteHistoryState = true;
137
+ this.commitHistoryTraversalIndex(navigationHistoryIndex);
138
+ } else if (options.historyUpdateMode === "push" && currentHref !== targetHref) {
139
+ options.stageClientParams();
140
+ this.#pushHistoryState(historyState, options.href);
141
+ wroteHistoryState = true;
142
+ this.commitHistoryTraversalIndex(navigationHistoryIndex);
143
+ }
144
+ if (!wroteHistoryState) {
145
+ this.syncCurrentHistoryStatePreviousNextUrl(options.previousNextUrl, options.bfcacheIds);
146
+ options.stageClientParams();
147
+ if (options.targetHistoryIndex !== void 0) this.commitHistoryTraversalIndex(options.targetHistoryIndex);
148
+ }
149
+ }
150
+ syncCurrentHistoryStatePreviousNextUrl(previousNextUrl, bfcacheIds) {
151
+ if (this.#isHistoryStateNavigationMetadataInSync(this.#readHistoryState(), previousNextUrl, bfcacheIds)) return;
152
+ const nextHistoryState = createHistoryStateWithNavigationMetadata(this.#readHistoryState(), {
153
+ bfcacheIds,
154
+ bfcacheVersion: bfcacheIds === void 0 ? void 0 : this.#restorableClientState.currentBfcacheVersion,
155
+ previousNextUrl
156
+ });
157
+ this.#replaceHistoryState(nextHistoryState, this.#readCurrentHref());
158
+ if (this.#isHistoryStateNavigationMetadataInSync(this.#readHistoryState(), previousNextUrl, bfcacheIds)) return;
159
+ this.#replaceHistoryState(nextHistoryState, this.#readCurrentHref());
160
+ }
161
+ #isHistoryStateNavigationMetadataInSync(state, previousNextUrl, bfcacheIds) {
162
+ return readHistoryStatePreviousNextUrl(state) === previousNextUrl && (bfcacheIds === void 0 || areBfcacheIdMapsEqual(readHistoryStateBfcacheIds(state), bfcacheIds) && this.#restorableClientState.isCurrentBfcacheVersion(state));
163
+ }
164
+ /** Initial history write performed before hydration starts. */
165
+ writeBootstrapHistoryMetadata() {
166
+ this.#replaceHistoryState(createHistoryStateWithNavigationMetadata(this.#readHistoryState(), {
167
+ previousNextUrl: null,
168
+ traversalIndex: this.#currentHistoryTraversalIndex
169
+ }), this.#readCurrentHref());
170
+ }
171
+ /** History write performed on the first committed (hydrated) render. */
172
+ writeHydratedHistoryMetadata(options) {
173
+ this.#replaceHistoryState(createHistoryStateWithNavigationMetadata(this.#readHistoryState(), {
174
+ bfcacheIds: options.bfcacheIds,
175
+ bfcacheVersion: this.#restorableClientState.currentBfcacheVersion,
176
+ previousNextUrl: options.previousNextUrl,
177
+ traversalIndex: this.#currentHistoryTraversalIndex
178
+ }), this.#readCurrentHref());
179
+ }
180
+ /**
181
+ * Resolves a restorable snapshot candidate for the given history entry and
182
+ * commits the traversal index after, and only after, the injected
183
+ * approved-visible-restore callback succeeds. The traversal-index commit and
184
+ * client-param staging run inside `beforeCommit`, which the
185
+ * `AppBrowserNavigationController` invokes only once the `ApprovedVisibleCommit`
186
+ * is approved. Returns false when no snapshot is restorable or the restore is
187
+ * not approved.
188
+ */
189
+ restoreHistorySnapshot(options) {
190
+ const decision = this.#restorableClientState.resolveHistoryStateSnapshotRestore(options.historyState);
191
+ if (decision.kind === "skip") return false;
192
+ return options.approveVisibleRestore({
193
+ state: decision.state,
194
+ beforeCommit: () => {
195
+ this.commitHistoryTraversalIndex(decision.targetHistoryIndex);
196
+ options.stageClientParams(decision.state.navigationSnapshot.params);
197
+ }
198
+ });
199
+ }
200
+ };
201
+ function areBfcacheIdMapsEqual(a, b) {
202
+ if (a === b) return true;
203
+ if (a === null || b === null) return false;
204
+ const aEntries = Object.entries(a);
205
+ const bEntries = Object.entries(b);
206
+ if (aEntries.length !== bEntries.length) return false;
207
+ return aEntries.every(([key, value]) => b[key] === value);
208
+ }
209
+ //#endregion
210
+ export { AppBrowserHistoryController };
@@ -1,9 +1,10 @@
1
1
  import { RouteManifest } from "../routing/app-route-graph.js";
2
2
  import { AppRouterScrollIntent } from "../shims/app-router-scroll-state.js";
3
+ import { NavigationRuntimeVisibleCommitMode } from "../client/navigation-runtime.js";
3
4
  import { ServerActionRevalidationKind } from "./app-browser-action-result.js";
4
5
  import { AppElements } from "./app-elements-wire.js";
5
6
  import { OperationLane } from "./navigation-planner.js";
6
- import { ClientNavigationRenderSnapshot, commitClientNavigationState } from "../shims/navigation.js";
7
+ import { ClientNavigationRenderSnapshot, commitClientNavigationState, createSnapshotPathAndSearch } from "../shims/navigation.js";
7
8
  import { AppNavigationPayloadOrigin, AppRouterState } from "./app-browser-state.js";
8
9
  import { Dispatch, ReactNode } from "react";
9
10
 
@@ -75,6 +76,7 @@ type BrowserNavigationController = {
75
76
  targetHistoryIndex?: number | null;
76
77
  targetHref: string;
77
78
  navId: number;
79
+ visibleCommitMode?: NavigationRuntimeVisibleCommitMode;
78
80
  }): Promise<NavigationPayloadOutcome>;
79
81
  commitSameUrlNavigatePayload(nextElements: Promise<AppElements>, navigationSnapshot: ClientNavigationRenderSnapshot, returnValue?: {
80
82
  ok: boolean;
@@ -99,7 +101,6 @@ type BrowserNavigationController = {
99
101
  }): ReactNode;
100
102
  };
101
103
  declare function clearHardNavigationLoopGuard(): void;
102
- declare function createSnapshotPathAndSearch(snapshot: ClientNavigationRenderSnapshot): string;
103
104
  declare function createBasePathStrippedPathAndSearch(url: URL, basePath: string): string;
104
105
  declare function createAppBrowserNavigationController(deps?: BrowserNavigationControllerDeps): BrowserNavigationController;
105
106
  //#endregion
@@ -1,10 +1,11 @@
1
1
  import { stripBasePath } from "../utils/base-path.js";
2
2
  import { claimAppRouterScrollIntentForCommit, consumeAppRouterScrollIntent } from "../shims/app-router-scroll-state.js";
3
- import { activateNavigationSnapshot, clearPendingPathname, commitClientNavigationState } from "../shims/navigation.js";
3
+ import { activateNavigationSnapshot, clearPendingPathname, commitClientNavigationState, createSnapshotPathAndSearch } from "../shims/navigation.js";
4
4
  import { shouldScheduleRefreshForDiscardedServerAction } from "./app-browser-action-result.js";
5
5
  import { FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN, createPendingNavigationCommit } from "./app-browser-state.js";
6
6
  import { applyApprovedVisibleCommit, approveHmrVisibleCommit, approvePendingNavigationCommit, resolveAndClassifyNavigationCommit } from "./app-browser-visible-commit.js";
7
7
  import { startTransition, useLayoutEffect } from "react";
8
+ import { flushSync } from "react-dom";
8
9
  //#region src/server/app-browser-navigation-controller.ts
9
10
  const HARD_NAVIGATION_LOOP_GUARD_KEY = "__vinext_hard_navigation_target__";
10
11
  function normalizeBrowserHref(href) {
@@ -50,10 +51,6 @@ function performHardNavigationWithLoopGuard(href, mode = "assign") {
50
51
  else window.location.assign(href);
51
52
  return true;
52
53
  }
53
- function createSnapshotPathAndSearch(snapshot) {
54
- const query = snapshot.searchParams.toString();
55
- return query === "" ? snapshot.pathname : `${snapshot.pathname}?${query}`;
56
- }
57
54
  function createBasePathStrippedPathAndSearch(url, basePath) {
58
55
  const pathname = stripBasePath(url.pathname, basePath);
59
56
  const query = new URLSearchParams(url.search).toString();
@@ -221,12 +218,18 @@ function createAppBrowserNavigationController(deps = {}) {
221
218
  }, [renderId]);
222
219
  return children;
223
220
  }
224
- function dispatchApprovedVisibleCommit(commit, pendingRouterState) {
221
+ function dispatchApprovedVisibleCommit(commit, pendingRouterState, visibleCommitMode) {
225
222
  const setter = getBrowserRouterStateSetter();
226
223
  if (pendingRouterState) {
227
224
  resolvePendingBrowserRouterState(pendingRouterState, commit);
228
225
  return;
229
226
  }
227
+ if (visibleCommitMode === "synchronous") {
228
+ flushSync(() => {
229
+ setter(applyApprovedVisibleCommit(getBrowserRouterState(), commit));
230
+ });
231
+ return;
232
+ }
230
233
  startTransition(() => {
231
234
  setter(applyApprovedVisibleCommit(getBrowserRouterState(), commit));
232
235
  });
@@ -349,7 +352,7 @@ function createAppBrowserNavigationController(deps = {}) {
349
352
  claimAppRouterScrollIntentForCommit(options.scrollIntent, renderId);
350
353
  activateNavigationSnapshot();
351
354
  snapshotActivated = true;
352
- dispatchApprovedVisibleCommit(approvedCommit, options.pendingRouterState);
355
+ dispatchApprovedVisibleCommit(approvedCommit, options.pendingRouterState, options.visibleCommitMode ?? "transition");
353
356
  } catch (error) {
354
357
  pendingNavigationPrePaintEffects.delete(renderId);
355
358
  pendingNavigationCommits.delete(renderId);
@@ -1,7 +1,7 @@
1
1
  //#region src/server/app-browser-rsc-redirect.d.ts
2
- declare const MAX_RSC_REDIRECT_DEPTH = 10;
3
2
  type RscRedirectHistoryUpdateMode = "push" | "replace" | undefined;
4
3
  type RscRedirectLifecycleDecision = {
4
+ href: string;
5
5
  kind: "no-redirect";
6
6
  } | {
7
7
  href: string;
@@ -24,5 +24,14 @@ declare function resolveRscRedirectLifecycleHop(options: {
24
24
  requestPreviousNextUrl: string | null;
25
25
  responseUrl: string;
26
26
  }): RscRedirectLifecycleDecision;
27
+ declare function resolveStreamedRscRedirectLifecycleHop(options: {
28
+ currentHref: string;
29
+ historyUpdateMode: Exclude<RscRedirectHistoryUpdateMode, undefined>;
30
+ maxRedirectDepth?: number;
31
+ origin: string;
32
+ redirectDepth: number;
33
+ requestPreviousNextUrl: string | null;
34
+ streamedRedirectTarget: string;
35
+ }): RscRedirectLifecycleDecision;
27
36
  //#endregion
28
- export { MAX_RSC_REDIRECT_DEPTH, resolveRscRedirectLifecycleHop };
37
+ export { resolveRscRedirectLifecycleHop, resolveStreamedRscRedirectLifecycleHop };
@@ -6,17 +6,23 @@ function toVisibleAppHref(href, origin) {
6
6
  stripRscCacheBustingSearchParam(url);
7
7
  return `${stripRscSuffix(url.pathname)}${url.search}${url.hash}`;
8
8
  }
9
- function resolveRscRedirectLifecycleHop(options) {
10
- const responseUrl = new URL(options.responseUrl, options.origin);
11
- if (responseUrl.origin !== options.origin) return {
12
- href: responseUrl.href,
9
+ function toStreamedRedirectVisibleAppHref(href, origin) {
10
+ const url = new URL(href, origin);
11
+ return `${url.pathname}${url.search}${url.hash}`;
12
+ }
13
+ function resolveRedirectLifecycleHopFromTarget(options) {
14
+ if (options.targetUrl.origin !== options.origin) return {
15
+ href: options.targetUrl.href,
13
16
  kind: "terminal-hard-navigation",
14
17
  reason: "externalRedirect",
15
18
  redirectDepth: options.redirectDepth
16
19
  };
17
- const redirectedHref = resolveHardNavigationTargetFromRscResponse(responseUrl.href, options.currentHref, options.origin);
18
- if (redirectedHref === toVisibleAppHref(options.currentHref, options.origin)) return { kind: "no-redirect" };
19
- const maxRedirectDepth = options.maxRedirectDepth ?? 10;
20
+ const redirectedHref = options.redirectedHref;
21
+ if (redirectedHref === toVisibleAppHref(options.currentHref, options.origin)) return {
22
+ href: redirectedHref,
23
+ kind: "no-redirect"
24
+ };
25
+ const maxRedirectDepth = options.maxRedirectDepth ?? MAX_RSC_REDIRECT_DEPTH;
20
26
  if (options.redirectDepth >= maxRedirectDepth) return {
21
27
  href: redirectedHref,
22
28
  kind: "terminal-hard-navigation",
@@ -31,5 +37,21 @@ function resolveRscRedirectLifecycleHop(options) {
31
37
  redirectDepth: options.redirectDepth + 1
32
38
  };
33
39
  }
40
+ function resolveRscRedirectLifecycleHop(options) {
41
+ const responseUrl = new URL(options.responseUrl, options.origin);
42
+ return resolveRedirectLifecycleHopFromTarget({
43
+ ...options,
44
+ redirectedHref: resolveHardNavigationTargetFromRscResponse(responseUrl.href, options.currentHref, options.origin),
45
+ targetUrl: responseUrl
46
+ });
47
+ }
48
+ function resolveStreamedRscRedirectLifecycleHop(options) {
49
+ const streamedRedirectUrl = new URL(options.streamedRedirectTarget, options.origin);
50
+ return resolveRedirectLifecycleHopFromTarget({
51
+ ...options,
52
+ redirectedHref: toStreamedRedirectVisibleAppHref(options.streamedRedirectTarget, options.origin),
53
+ targetUrl: streamedRedirectUrl
54
+ });
55
+ }
34
56
  //#endregion
35
- export { MAX_RSC_REDIRECT_DEPTH, resolveRscRedirectLifecycleHop };
57
+ export { resolveRscRedirectLifecycleHop, resolveStreamedRscRedirectLifecycleHop };
@@ -7,9 +7,10 @@ import { getMountedSlotIds, getMountedSlotIdsHeader } from "./app-elements.js";
7
7
  import "./app-bfcache-id.js";
8
8
  import { createHistoryStateWithNavigationMetadata, createHistoryStateWithPreviousNextUrl, isBfcacheSegmentId, isHistoryStateBfcacheVersionCurrent, readHistoryStateBfcacheIds, readHistoryStateBfcacheVersion, readHistoryStatePreviousNextUrl, readHistoryStateTraversalIndex, resolveHistoryTraversalIntent } from "./app-history-state.js";
9
9
  import { createRscRequestHeaders } from "./app-rsc-cache-busting.js";
10
- import { createCacheEntryReuseProof } from "./cache-proof.js";
11
10
  import { NavigationTraceReasonCodes, createNavigationLifecycleTraceFields, createNavigationTrace } from "./navigation-trace.js";
12
11
  import { navigationPlanner, resolveDefaultOrUnmatchedSlotPersistenceForLayouts } from "./navigation-planner.js";
12
+ import { createSnapshotPathAndSearch } from "../shims/navigation.js";
13
+ import { createCacheEntryReuseProof } from "./cache-proof.js";
13
14
  //#region src/server/app-browser-state.ts
14
15
  const FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN = { origin: "fresh" };
15
16
  const VISITED_CACHE_APP_NAVIGATION_PAYLOAD_ORIGIN = { origin: "visited-cache" };
@@ -222,10 +223,6 @@ function createPendingNavigationTraceFields(options) {
222
223
  ...options.targetHref !== void 0 ? { targetHref: options.targetHref } : {}
223
224
  };
224
225
  }
225
- function createNavigationSnapshotUrl(snapshot) {
226
- const query = snapshot.searchParams.toString();
227
- return query === "" ? snapshot.pathname : `${snapshot.pathname}?${query}`;
228
- }
229
226
  function createMountedParallelSlotSnapshots(elements) {
230
227
  const snapshots = [];
231
228
  for (const slotId of getMountedSlotIds(elements)) {
@@ -239,7 +236,7 @@ function createMountedParallelSlotSnapshots(elements) {
239
236
  return snapshots;
240
237
  }
241
238
  function createVisibleRouteSnapshot(state) {
242
- const displayUrl = createNavigationSnapshotUrl(state.navigationSnapshot);
239
+ const displayUrl = createSnapshotPathAndSearch(state.navigationSnapshot);
243
240
  const matchedUrl = normalizeNavigationSnapshotMatchedUrl(state.navigationSnapshot.pathname);
244
241
  return {
245
242
  displayUrl,
@@ -257,7 +254,7 @@ function createVisibleRouteSnapshot(state) {
257
254
  };
258
255
  }
259
256
  function createPendingRouteSnapshot(pending) {
260
- const displayUrl = createNavigationSnapshotUrl(pending.action.navigationSnapshot);
257
+ const displayUrl = createSnapshotPathAndSearch(pending.action.navigationSnapshot);
261
258
  const matchedUrl = normalizeNavigationSnapshotMatchedUrl(pending.action.navigationSnapshot.pathname);
262
259
  return {
263
260
  displayUrl,
@@ -1,7 +1,7 @@
1
1
  import { normalizeAppElementsSlotBindings } from "./app-elements-wire.js";
2
2
  import "./app-elements.js";
3
- import { mergeElements } from "../shims/slot.js";
4
3
  import { NavigationTraceReasonCodes, NavigationTraceTransactionCodes, createNavigationTrace, prependNavigationTraceEntry } from "./navigation-trace.js";
4
+ import { mergeElements } from "../shims/slot.js";
5
5
  import { createPendingNavigationCommit, preserveBfcacheIdsForMergedElements, resolvePendingNavigationCommitDispositionDecision } from "./app-browser-state.js";
6
6
  //#region src/server/app-browser-visible-commit.ts
7
7
  const approvedVisibleCommitBrand = Symbol("ApprovedVisibleCommit");
@@ -52,7 +52,8 @@ type AppFallbackRendererOptions<TModule extends AppPageModule = AppPageModule> =
52
52
  loadGlobalNotFoundModule?: (() => Promise<TModule | null | undefined>) | null;
53
53
  makeThenableParams: (params: AppPageParams) => unknown;
54
54
  metadataRoutes: MetadataFileRoute[]; /** Configured next.config `basePath`, threaded into file-based metadata href emission. */
55
- basePath?: string;
55
+ basePath?: string; /** Configured next.config `trailingSlash`, threaded into canonical URL rendering. */
56
+ trailingSlash?: boolean;
56
57
  resolveChildSegments: (routeSegments: readonly string[], treePosition: number, params: AppPageParams) => string[];
57
58
  rootBoundaries: AppFallbackRendererRootBoundaries<TModule>;
58
59
  rscRenderer: (element: ReactNode | AppElements, options: {
@@ -7,7 +7,7 @@ const EMPTY_MW_CTX = {
7
7
  status: null
8
8
  };
9
9
  function createAppFallbackRenderer(options) {
10
- const { basePath = "", clearRequestContext, createRscOnErrorHandler: buildRscOnErrorHandler, fontProviders, getNavigationContext, globalErrorModule, loadGlobalNotFoundModule, makeThenableParams, metadataRoutes, resolveChildSegments, rootBoundaries, rscRenderer, sanitizer, ssrLoader } = options;
10
+ const { basePath = "", clearRequestContext, createRscOnErrorHandler: buildRscOnErrorHandler, fontProviders, getNavigationContext, globalErrorModule, loadGlobalNotFoundModule, makeThenableParams, metadataRoutes, resolveChildSegments, rootBoundaries, rscRenderer, sanitizer, ssrLoader, trailingSlash } = options;
11
11
  const { rootForbiddenModule, rootLayouts, rootNotFoundModule, rootUnauthorizedModule } = rootBoundaries;
12
12
  const effectiveGlobalErrorModule = globalErrorModule ?? DEFAULT_GLOBAL_ERROR_MODULE;
13
13
  const effectiveRootNotFoundModule = rootNotFoundModule ?? DEFAULT_NOT_FOUND_MODULE;
@@ -58,6 +58,7 @@ function createAppFallbackRenderer(options) {
58
58
  }
59
59
  return renderAppPageHttpAccessFallback({
60
60
  basePath,
61
+ trailingSlash,
61
62
  boundaryComponent: opts?.boundaryComponent ?? null,
62
63
  boundaryModule: opts?.boundaryModule ?? null,
63
64
  buildFontLinkHeader: fontProviders.buildFontLinkHeader,
@@ -96,6 +97,7 @@ function createAppFallbackRenderer(options) {
96
97
  renderErrorBoundary(route, error, isRscRequest, request, matchedParams, scriptNonce, middlewareContext, callContext) {
97
98
  return renderAppPageErrorBoundary({
98
99
  basePath,
100
+ trailingSlash,
99
101
  buildFontLinkHeader: fontProviders.buildFontLinkHeader,
100
102
  clearRequestContext,
101
103
  createRscOnErrorHandler(pathname, routePath) {
@@ -115,6 +115,7 @@ async function applyAppMiddleware(options) {
115
115
  if (!forwarded.applied) {
116
116
  const result = await executeMiddleware({
117
117
  basePath: options.basePath,
118
+ hadBasePath: true,
118
119
  i18nConfig: options.i18nConfig,
119
120
  isDataRequest: options.isDataRequest,
120
121
  isProxy: options.isProxy,
@@ -1,5 +1,6 @@
1
1
  import { buildParams, decodeMatchedParams, splitPathnameForRouteMatch } from "../routing/utils.js";
2
2
  import { stripBasePath } from "../utils/base-path.js";
3
+ import { matchRoutePattern } from "../routing/route-pattern.js";
3
4
  import { isUnknownRecord } from "../utils/record.js";
4
5
  import { AppElementsWire } from "./app-elements-wire.js";
5
6
  import "./app-elements.js";
@@ -135,6 +136,20 @@ function matchOptimisticRouteManifestRoute(options) {
135
136
  decodeMatchedParams(match.params);
136
137
  return match;
137
138
  }
139
+ function mergeParams(target, source) {
140
+ for (const [key, value] of Object.entries(source)) target[key] = value;
141
+ }
142
+ function resolveOptimisticNavigationParams(options) {
143
+ const navigationParams = { ...options.match.params };
144
+ for (const binding of options.routeManifest.segmentGraph.slotBindings.values()) {
145
+ if (binding.routeId !== options.match.route.id || binding.state !== "active") continue;
146
+ const patternParts = binding.slotPatternParts;
147
+ if (!patternParts) continue;
148
+ const matched = matchRoutePattern(options.urlParts, patternParts);
149
+ if (matched) mergeParams(navigationParams, matched);
150
+ }
151
+ return navigationParams;
152
+ }
138
153
  function elementHasSuspenseFallback(value, depth = 0) {
139
154
  if (depth > 100) return false;
140
155
  if (Array.isArray(value)) return value.some((entry) => elementHasSuspenseFallback(entry, depth + 1));
@@ -183,6 +198,8 @@ function createOptimisticRouteElements(template) {
183
198
  }
184
199
  function resolveOptimisticNavigationPayload(options) {
185
200
  if (options.interceptionContext !== null) return null;
201
+ const urlParts = hrefToRouteParts(options.href, options.basePath);
202
+ if (urlParts === null) return null;
186
203
  const match = matchOptimisticRouteManifestRoute({
187
204
  basePath: options.basePath,
188
205
  href: options.href,
@@ -198,7 +215,11 @@ function resolveOptimisticNavigationPayload(options) {
198
215
  if (template.mountedSlotsHeader !== options.mountedSlotsHeader) return null;
199
216
  return {
200
217
  elements: createOptimisticRouteElements(template),
201
- params: match.params,
218
+ params: resolveOptimisticNavigationParams({
219
+ match,
220
+ routeManifest: options.routeManifest,
221
+ urlParts
222
+ }),
202
223
  template
203
224
  };
204
225
  }
@@ -41,7 +41,8 @@ type AppPageBoundaryRenderCommonOptions<TModule extends AppPageModule = AppPageM
41
41
  makeThenableParams: (params: AppPageParams) => unknown;
42
42
  middlewareContext: AppPageMiddlewareContext;
43
43
  metadataRoutes: MetadataFileRoute[]; /** Configured next.config `basePath`, threaded into file-based metadata href emission. */
44
- basePath?: string;
44
+ basePath?: string; /** Configured next.config `trailingSlash`, threaded into canonical URL rendering. */
45
+ trailingSlash?: boolean;
45
46
  renderToReadableStream: (element: ReactNode | AppElements, options: {
46
47
  onError: AppPageBoundaryOnError;
47
48
  }) => ReadableStream<Uint8Array>;
@@ -168,7 +168,8 @@ async function renderAppPageHttpAccessFallback(options) {
168
168
  if (metadata) headElements.push(createElement(MetadataHead, {
169
169
  key: "metadata",
170
170
  metadata,
171
- pathname
171
+ pathname,
172
+ trailingSlash: options.trailingSlash
172
173
  }));
173
174
  headElements.push(createElement(ViewportHead, {
174
175
  key: "viewport",
@@ -231,7 +232,8 @@ async function renderAppPageErrorBoundary(options) {
231
232
  if (metadata) headElements.push(createElement(MetadataHead, {
232
233
  key: "metadata",
233
234
  metadata,
234
- pathname
235
+ pathname,
236
+ trailingSlash: options.trailingSlash
235
237
  }));
236
238
  headElements.push(createElement(ViewportHead, {
237
239
  key: "viewport",