@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.9 → 3.9.3-ultramodern.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 (124) hide show
  1. package/dist/cjs/{reactContext.js → localisedUrls/config.js} +14 -10
  2. package/dist/cjs/localisedUrls/index.js +95 -0
  3. package/dist/cjs/localisedUrls/normalise.js +87 -0
  4. package/dist/cjs/localisedUrls/pathname.js +68 -0
  5. package/dist/cjs/localisedUrls/patterns.js +214 -0
  6. package/dist/cjs/localisedUrls/redirect.js +104 -0
  7. package/dist/cjs/localisedUrls/resolve.js +78 -0
  8. package/dist/cjs/localisedUrls/routeIdentity.js +92 -0
  9. package/dist/cjs/localisedUrls/routes.js +217 -0
  10. package/dist/cjs/localisedUrls/types.js +18 -0
  11. package/dist/cjs/rendererHead.js +18 -5
  12. package/dist/cjs/request-policy/policyCore.js +1 -1
  13. package/dist/cjs/request-policy/transport.js +2 -2
  14. package/dist/cjs/routerState.js +85 -77
  15. package/dist/cjs/routerStatePlugin.js +63 -9
  16. package/dist/cjs/server-config/serverSource.js +18 -0
  17. package/dist/esm/localisedUrls/config.mjs +11 -0
  18. package/dist/esm/localisedUrls/index.mjs +8 -0
  19. package/dist/esm/localisedUrls/normalise.mjs +37 -0
  20. package/dist/esm/localisedUrls/pathname.mjs +24 -0
  21. package/dist/esm/localisedUrls/patterns.mjs +152 -0
  22. package/dist/esm/localisedUrls/redirect.mjs +57 -0
  23. package/dist/esm/localisedUrls/resolve.mjs +37 -0
  24. package/dist/esm/localisedUrls/routeIdentity.mjs +54 -0
  25. package/dist/esm/localisedUrls/routes.mjs +176 -0
  26. package/dist/esm/localisedUrls/types.mjs +0 -0
  27. package/dist/esm/rendererHead.mjs +14 -5
  28. package/dist/esm/request-policy/policyCore.mjs +1 -1
  29. package/dist/esm/request-policy/transport.mjs +2 -2
  30. package/dist/esm/routerState.mjs +81 -73
  31. package/dist/esm/routerStatePlugin.mjs +62 -11
  32. package/dist/esm/server-config/serverSource.mjs +0 -0
  33. package/dist/esm-node/localisedUrls/config.mjs +12 -0
  34. package/dist/esm-node/localisedUrls/index.mjs +9 -0
  35. package/dist/esm-node/localisedUrls/normalise.mjs +38 -0
  36. package/dist/esm-node/localisedUrls/pathname.mjs +25 -0
  37. package/dist/esm-node/localisedUrls/patterns.mjs +153 -0
  38. package/dist/esm-node/localisedUrls/redirect.mjs +58 -0
  39. package/dist/esm-node/localisedUrls/resolve.mjs +38 -0
  40. package/dist/esm-node/localisedUrls/routeIdentity.mjs +55 -0
  41. package/dist/esm-node/localisedUrls/routes.mjs +177 -0
  42. package/dist/esm-node/localisedUrls/types.mjs +1 -0
  43. package/dist/esm-node/rendererHead.mjs +14 -5
  44. package/dist/esm-node/request-policy/policyCore.mjs +1 -1
  45. package/dist/esm-node/request-policy/transport.mjs +2 -2
  46. package/dist/esm-node/routerState.mjs +81 -73
  47. package/dist/esm-node/routerStatePlugin.mjs +62 -11
  48. package/dist/esm-node/server-config/serverSource.mjs +1 -0
  49. package/dist/types/helmetCompat.d.ts +1 -1
  50. package/dist/types/localisedUrls/config.d.ts +9 -0
  51. package/dist/types/localisedUrls/index.d.ts +10 -0
  52. package/dist/types/localisedUrls/normalise.d.ts +10 -0
  53. package/dist/types/localisedUrls/pathname.d.ts +4 -0
  54. package/dist/types/localisedUrls/patterns.d.ts +11 -0
  55. package/dist/types/localisedUrls/redirect.d.ts +19 -0
  56. package/dist/types/localisedUrls/resolve.d.ts +7 -0
  57. package/dist/types/localisedUrls/routeIdentity.d.ts +22 -0
  58. package/dist/types/localisedUrls/routes.d.ts +3 -0
  59. package/dist/types/localisedUrls/types.d.ts +14 -0
  60. package/dist/types/rendererHead.d.ts +5 -3
  61. package/dist/types/request-policy/types.d.ts +1 -1
  62. package/dist/types/routeHydration.d.ts +2 -2
  63. package/dist/types/routerProvider.d.ts +1 -3
  64. package/dist/types/routerState.d.ts +11 -6
  65. package/dist/types/routerStatePlugin.d.ts +12 -0
  66. package/dist/types/routerStateTypes.d.ts +47 -17
  67. package/dist/types/server-config/index.d.ts +1 -0
  68. package/dist/types/server-config/serverSource.d.ts +16 -0
  69. package/package.json +42 -44
  70. package/src/helmetCompat.ts +1 -1
  71. package/src/localisedUrls/config.ts +18 -0
  72. package/src/localisedUrls/index.ts +25 -0
  73. package/src/localisedUrls/normalise.ts +62 -0
  74. package/src/localisedUrls/pathname.ts +60 -0
  75. package/src/localisedUrls/patterns.ts +283 -0
  76. package/src/localisedUrls/redirect.ts +110 -0
  77. package/src/localisedUrls/resolve.ts +106 -0
  78. package/src/localisedUrls/routeIdentity.ts +83 -0
  79. package/src/localisedUrls/routes.ts +451 -0
  80. package/src/localisedUrls/types.ts +18 -0
  81. package/src/rendererHead.ts +25 -8
  82. package/src/request-policy/policyCore.ts +1 -1
  83. package/src/request-policy/transport.ts +2 -2
  84. package/src/request-policy/types.ts +1 -1
  85. package/src/routeHydration.ts +2 -2
  86. package/src/routerProvider.ts +1 -1
  87. package/src/routerState.ts +127 -115
  88. package/src/routerStatePlugin.ts +106 -10
  89. package/src/routerStateTypes.ts +47 -17
  90. package/src/server-config/index.ts +1 -0
  91. package/src/server-config/serverSource.ts +23 -0
  92. package/dist/cjs/buildIdentity.js +0 -73
  93. package/dist/esm/buildIdentity.mjs +0 -35
  94. package/dist/esm/reactContext.mjs +0 -7
  95. package/dist/esm-node/buildIdentity.mjs +0 -36
  96. package/dist/esm-node/reactContext.mjs +0 -8
  97. package/dist/types/buildIdentity.d.ts +0 -37
  98. package/dist/types/reactContext.d.ts +0 -6
  99. package/rslib.config.mts +0 -19
  100. package/rstest.config.mts +0 -38
  101. package/src/buildIdentity.ts +0 -61
  102. package/src/reactContext.ts +0 -16
  103. package/tests/buildIdentity.test.ts +0 -51
  104. package/tests/contextExtensions.test.ts +0 -27
  105. package/tests/headRuntime.test.tsx +0 -229
  106. package/tests/rendererHead.test.ts +0 -187
  107. package/tests/request-policy/browser-policy.test.ts +0 -108
  108. package/tests/request-policy/client-isolation.test.ts +0 -109
  109. package/tests/request-policy/node-policy.test.ts +0 -447
  110. package/tests/request-policy/policyCore.test.ts +0 -129
  111. package/tests/request-policy/requestContext.test.ts +0 -66
  112. package/tests/request-policy/requestFactory.outbound-contract.test.ts +0 -290
  113. package/tests/request-policy/traceparent.test.ts +0 -52
  114. package/tests/request-policy/transport-retry.test.ts +0 -367
  115. package/tests/routeHydration.test.ts +0 -180
  116. package/tests/routerPrefetchPolicy.test.ts +0 -102
  117. package/tests/routerProvider.test.ts +0 -29
  118. package/tests/routerState.test.ts +0 -78
  119. package/tests/routerStatePlugin.test.ts +0 -63
  120. package/tests/runtimeContextProjection.test.ts +0 -83
  121. package/tests/safeFailure.test.ts +0 -136
  122. package/tests/shellStream.test.ts +0 -122
  123. package/tests/workspaceEvents.test.ts +0 -221
  124. package/tsconfig.json +0 -10
@@ -3,7 +3,6 @@ import { createRuntimeContextExtension } from './contextExtensions';
3
3
  import type {
4
4
  InternalRouterRuntimeState,
5
5
  InternalRouterServerSnapshot,
6
- RouterRouteMatchSnapshot,
7
6
  RouterServerPrepareResult,
8
7
  } from './routerStateTypes';
9
8
 
@@ -13,6 +12,9 @@ export type {
13
12
  InternalRouterServerSnapshot,
14
13
  RouterFramework,
15
14
  RouterLifecyclePhase,
15
+ RouterLinkTarget,
16
+ RouterNavigationCapability,
17
+ RouterNavigationSnapshot,
16
18
  RouterRouteMatchSnapshot,
17
19
  RouterServerPrepareResult,
18
20
  } from './routerStateTypes';
@@ -50,112 +52,138 @@ export function getRouterServerSnapshot(
50
52
  return routerServerSnapshotExtension.get(runtimeContext);
51
53
  }
52
54
 
53
- type RouterSnapshotLike = Partial<InternalRouterServerSnapshot>;
54
-
55
- function toHydrationScripts(state: {
56
- hydrationScript?: string;
57
- hydrationScripts?: string[];
58
- }) {
59
- if (state.hydrationScripts?.length) {
60
- return state.hydrationScripts;
61
- }
62
-
63
- return state.hydrationScript ? [state.hydrationScript] : undefined;
64
- }
65
-
66
- function getMatchedRouteIdsFromMatches(matches?: RouterRouteMatchSnapshot[]) {
67
- const routeIds = matches
68
- ?.map(match => match.assetRouteId ?? match.routeId)
69
- .filter((routeId): routeId is string => typeof routeId === 'string');
70
-
71
- return routeIds?.length ? routeIds : undefined;
72
- }
73
-
55
+ /** Capture prepared metadata once; live router updates cannot rewrite it. */
74
56
  export function createRouterServerSnapshot(
75
- state: RouterSnapshotLike,
57
+ snapshot: InternalRouterServerSnapshot,
76
58
  ): InternalRouterServerSnapshot {
77
- const hydrationScripts = toHydrationScripts(state);
59
+ const matches = snapshot.matches?.map(match =>
60
+ Object.freeze({
61
+ ...match,
62
+ ...(match.params ? { params: Object.freeze({ ...match.params }) } : {}),
63
+ }),
64
+ );
78
65
  const matchedRouteIds =
79
- state.matchedRouteIds ?? getMatchedRouteIdsFromMatches(state.matches);
80
-
81
- return {
82
- ...state,
83
- ...(hydrationScripts?.length
66
+ snapshot.matchedRouteIds ??
67
+ matches?.map(match => match.assetRouteId ?? match.routeId);
68
+ return Object.freeze({
69
+ ...snapshot,
70
+ ...(snapshot.hydrationScripts
71
+ ? { hydrationScripts: Object.freeze([...snapshot.hydrationScripts]) }
72
+ : {}),
73
+ ...(matchedRouteIds
74
+ ? { matchedRouteIds: Object.freeze([...matchedRouteIds]) }
75
+ : {}),
76
+ ...(matches ? { matches: Object.freeze(matches) } : {}),
77
+ ...(snapshot.errors
78
+ ? { errors: Object.freeze({ ...snapshot.errors }) }
79
+ : {}),
80
+ ...(snapshot.routerData
84
81
  ? {
85
- hydrationScript: state.hydrationScript ?? hydrationScripts[0],
86
- hydrationScripts,
82
+ routerData: Object.freeze({
83
+ ...snapshot.routerData,
84
+ ...(snapshot.routerData.loaderData
85
+ ? {
86
+ loaderData: Object.freeze({
87
+ ...snapshot.routerData.loaderData,
88
+ }),
89
+ }
90
+ : {}),
91
+ ...(snapshot.routerData.errors
92
+ ? { errors: Object.freeze({ ...snapshot.routerData.errors }) }
93
+ : {}),
94
+ }),
87
95
  }
88
96
  : {}),
89
- ...(matchedRouteIds ? { matchedRouteIds } : {}),
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Listeners per runtime context, so a consumer can react to the slot being
102
+ * filled in. A router provider publishes its instance during render - TanStack
103
+ * installs it when `RouterWrapper` renders - which is after anything that
104
+ * wraps the app has already evaluated. Without a notification those wrappers
105
+ * would hold a routerless view of the app for the rest of the session.
106
+ *
107
+ * Keyed weakly: the runtime context outlives neither the app nor this map.
108
+ */
109
+ const routerRuntimeStateListeners = new WeakMap<object, Set<() => void>>();
110
+
111
+ /**
112
+ * Observe the router runtime-state slot for the given runtime context.
113
+ * Returns an unsubscribe function; safe to call for a context that never
114
+ * receives a router.
115
+ */
116
+ export function subscribeRouterRuntimeState(
117
+ runtimeContext: object,
118
+ listener: () => void,
119
+ ): () => void {
120
+ if (!runtimeContext || typeof runtimeContext !== 'object') {
121
+ return () => undefined;
122
+ }
123
+ let listeners = routerRuntimeStateListeners.get(runtimeContext);
124
+ if (!listeners) {
125
+ listeners = new Set();
126
+ routerRuntimeStateListeners.set(runtimeContext, listeners);
127
+ }
128
+ listeners.add(listener);
129
+ return () => {
130
+ listeners?.delete(listener);
90
131
  };
91
132
  }
92
133
 
93
- export function createRouterRuntimeState(
94
- state: InternalRouterRuntimeState,
95
- ): InternalRouterRuntimeState {
96
- const hasSnapshotState =
97
- Boolean(state.serverSnapshot) ||
98
- Boolean(state.hydrationScript) ||
99
- Boolean(state.hydrationScripts?.length) ||
100
- Boolean(state.matchedRouteIds?.length) ||
101
- Boolean(state.matches?.length);
102
- const serverSnapshot = state.serverSnapshot
103
- ? createRouterServerSnapshot({
104
- ...state.serverSnapshot,
105
- framework: state.serverSnapshot.framework ?? state.framework,
106
- basename: state.serverSnapshot.basename ?? state.basename,
107
- hydrationScript:
108
- state.serverSnapshot.hydrationScript ?? state.hydrationScript,
109
- hydrationScripts:
110
- state.serverSnapshot.hydrationScripts ?? state.hydrationScripts,
111
- matchedRouteIds:
112
- state.serverSnapshot.matchedRouteIds ?? state.matchedRouteIds,
113
- matches: state.serverSnapshot.matches ?? state.matches,
114
- })
115
- : hasSnapshotState
116
- ? createRouterServerSnapshot({
117
- framework: state.framework,
118
- basename: state.basename,
119
- hydrationScript: state.hydrationScript,
120
- hydrationScripts: state.hydrationScripts,
121
- matchedRouteIds: state.matchedRouteIds,
122
- matches: state.matches,
123
- })
124
- : undefined;
125
- const hydrationScripts = toHydrationScripts({
126
- hydrationScript: state.hydrationScript ?? serverSnapshot?.hydrationScript,
127
- hydrationScripts:
128
- state.hydrationScripts ?? serverSnapshot?.hydrationScripts,
129
- });
130
- const matchedRouteIds =
131
- state.matchedRouteIds ??
132
- serverSnapshot?.matchedRouteIds ??
133
- getMatchedRouteIdsFromMatches(state.matches);
134
+ /** Contexts with a notification already scheduled, so a burst coalesces. */
135
+ const pendingRouterRuntimeStateNotifications = new WeakSet<object>();
134
136
 
135
- return {
136
- ...state,
137
- ...(hydrationScripts?.length
138
- ? {
139
- hydrationScript: state.hydrationScript ?? hydrationScripts[0],
140
- hydrationScripts,
141
- }
142
- : {}),
143
- ...(matchedRouteIds ? { matchedRouteIds } : {}),
144
- ...(serverSnapshot ? { serverSnapshot } : {}),
137
+ function notifyRouterRuntimeState(runtimeContext: object) {
138
+ const listeners = routerRuntimeStateListeners.get(runtimeContext);
139
+ if (!listeners?.size) {
140
+ return;
141
+ }
142
+ if (pendingRouterRuntimeStateNotifications.has(runtimeContext)) {
143
+ return;
144
+ }
145
+ pendingRouterRuntimeStateNotifications.add(runtimeContext);
146
+ // Deferred deliberately. A router provider publishes its instance from
147
+ // inside its own render - `RouterWrapper` does - so calling observers
148
+ // synchronously would schedule an update on a component that is rendering,
149
+ // which React reports as "Cannot update a component while rendering a
150
+ // different component". Delivering after the current render lands keeps the
151
+ // notification correct and silent.
152
+ const deliver = () => {
153
+ pendingRouterRuntimeStateNotifications.delete(runtimeContext);
154
+ const current = routerRuntimeStateListeners.get(runtimeContext);
155
+ if (!current?.size) {
156
+ return;
157
+ }
158
+ for (const listener of [...current]) {
159
+ try {
160
+ listener();
161
+ } catch {
162
+ // One bad observer must not stop the router from being published.
163
+ }
164
+ }
145
165
  };
166
+ if (typeof queueMicrotask === 'function') {
167
+ queueMicrotask(deliver);
168
+ } else {
169
+ setTimeout(deliver, 0);
170
+ }
146
171
  }
147
172
 
148
173
  export function applyRouterRuntimeState<Context extends object>(
149
174
  runtimeContext: Context,
150
175
  state: InternalRouterRuntimeState,
151
176
  ) {
152
- const normalized = createRouterRuntimeState(state);
153
- routerRuntimeStateExtension.set(runtimeContext, normalized);
154
- if (normalized.serverSnapshot) {
155
- routerServerSnapshotExtension.set(
156
- runtimeContext,
157
- normalized.serverSnapshot,
158
- );
177
+ const previous = routerRuntimeStateExtension.get(runtimeContext);
178
+ routerRuntimeStateExtension.set(runtimeContext, state);
179
+ // Only a change of identity is worth a re-render; `RouterWrapper` reapplies
180
+ // the same instance on every render of the app.
181
+ if (
182
+ previous?.instance !== state.instance ||
183
+ previous?.framework !== state.framework ||
184
+ previous?.navigation !== state.navigation
185
+ ) {
186
+ notifyRouterRuntimeState(runtimeContext);
159
187
  }
160
188
 
161
189
  return runtimeContext;
@@ -165,40 +193,24 @@ export function applyRouterServerPrepareResult<Context extends object>(
165
193
  runtimeContext: Context,
166
194
  result: RouterServerPrepareResult,
167
195
  ) {
168
- const state = createRouterRuntimeState({
196
+ if (result.snapshot) {
197
+ routerServerSnapshotExtension.set(
198
+ runtimeContext,
199
+ createRouterServerSnapshot(result.snapshot),
200
+ );
201
+ }
202
+ return applyRouterRuntimeState(runtimeContext, {
169
203
  ...result.state,
170
204
  cleanup: result.cleanup ?? result.state.cleanup,
171
- serverSnapshot: result.snapshot ?? result.state.serverSnapshot,
172
205
  });
173
- applyRouterRuntimeState(runtimeContext, state);
174
- return runtimeContext;
175
206
  }
176
207
 
177
208
  export function getRouterHydrationScripts(runtimeContext: object) {
178
- const serverSnapshot = getRouterServerSnapshot(runtimeContext);
179
- const runtimeState = getRouterRuntimeState(runtimeContext);
180
- return (
181
- serverSnapshot?.hydrationScripts ??
182
- toHydrationScripts({
183
- hydrationScript: serverSnapshot?.hydrationScript,
184
- }) ??
185
- runtimeState?.hydrationScripts ??
186
- toHydrationScripts({
187
- hydrationScript: runtimeState?.hydrationScript,
188
- }) ??
189
- []
190
- );
209
+ return getRouterServerSnapshot(runtimeContext)?.hydrationScripts ?? [];
191
210
  }
192
211
 
193
212
  export function getRouterMatchedRouteIds(runtimeContext: object) {
194
- const serverSnapshot = getRouterServerSnapshot(runtimeContext);
195
- const runtimeState = getRouterRuntimeState(runtimeContext);
196
- return (
197
- serverSnapshot?.matchedRouteIds ??
198
- getMatchedRouteIdsFromMatches(serverSnapshot?.matches) ??
199
- runtimeState?.matchedRouteIds ??
200
- getMatchedRouteIdsFromMatches(runtimeState?.matches)
201
- );
213
+ return getRouterServerSnapshot(runtimeContext)?.matchedRouteIds;
202
214
  }
203
215
 
204
216
  export async function cleanupRouterRuntimeState(runtimeContext: object) {
@@ -1,8 +1,15 @@
1
+ import type { createMemoryRouter } from '@modern-js/runtime-utils/router';
1
2
  import { createRouterPrefetchPolicy } from './routerPrefetchPolicy';
2
3
  import {
3
4
  applyRouterRuntimeState,
4
- createRouterServerSnapshot,
5
+ applyRouterServerPrepareResult,
6
+ getRouterRuntimeState,
7
+ getRouterServerSnapshot,
5
8
  } from './routerState';
9
+ import type {
10
+ RouterNavigationCapability,
11
+ RouterNavigationSnapshot,
12
+ } from './routerStateTypes';
6
13
 
7
14
  type ReactRouterServerContext = {
8
15
  statusCode: number;
@@ -19,6 +26,47 @@ type RouterLifecycleEvent = {
19
26
  router?: unknown;
20
27
  };
21
28
 
29
+ type PreparedRequest<RuntimeContext extends object = object> = {
30
+ runtimeContext: RuntimeContext;
31
+ routerResult?: {
32
+ statusCode?: number;
33
+ errors?: Record<string, unknown> | null;
34
+ };
35
+ };
36
+
37
+ export const ROUTER_CLEANUP_ERROR =
38
+ 'An error occurs during router runtime cleanup';
39
+
40
+ // Provider wrappers and application composition may share the same native API.
41
+ // Install their common request policy once for that runtime instance.
42
+ const installedRouterPolicies = new WeakSet<object>();
43
+
44
+ function createReactRouterNavigation(
45
+ router: ReturnType<typeof createMemoryRouter>,
46
+ Link: RouterNavigationCapability['Link'],
47
+ ): RouterNavigationCapability {
48
+ let state: typeof router.state | undefined;
49
+ let snapshot: RouterNavigationSnapshot;
50
+ return {
51
+ Link,
52
+ getSnapshot() {
53
+ if (state !== router.state) {
54
+ state = router.state;
55
+ snapshot = {
56
+ location: state.location,
57
+ params: Object.assign(
58
+ {},
59
+ ...state.matches.map(match => match.params),
60
+ ),
61
+ };
62
+ }
63
+ return snapshot;
64
+ },
65
+ subscribe: listener => router.subscribe(listener),
66
+ navigate: (href, options) => router.navigate(href, options),
67
+ };
68
+ }
69
+
22
70
  /** Install fork policy through lifecycle hooks supplied by the native owner. */
23
71
  export function createRouterStatePlugin<Hooks extends Record<string, unknown>>({
24
72
  registryHooks,
@@ -30,16 +78,56 @@ export function createRouterStatePlugin<Hooks extends Record<string, unknown>>({
30
78
  registryHooks,
31
79
  setup(api: {
32
80
  onBeforeRender: (callback: (context: object) => void) => unknown;
81
+ onRenderPrepared: (
82
+ callback: <RuntimeContext extends object>(
83
+ info: PreparedRequest<RuntimeContext>,
84
+ ) => PreparedRequest<RuntimeContext>,
85
+ ) => unknown;
86
+ onRequestEnd: (
87
+ callback: (info: { runtimeContext: object }) => Promise<void>,
88
+ ) => unknown;
33
89
  onAfterCreateRouter: (
34
90
  callback: (event: RouterLifecycleEvent) => void,
35
91
  ) => unknown;
36
92
  }) {
93
+ if (installedRouterPolicies.has(api)) {
94
+ return;
95
+ }
96
+ installedRouterPolicies.add(api);
37
97
  api.onBeforeRender(context => {
38
98
  const runtimeContext = context as {
39
99
  linkPrefetchPolicy?: ReturnType<typeof createRouterPrefetchPolicy>;
40
100
  };
41
101
  runtimeContext.linkPrefetchPolicy ??= createRouterPrefetchPolicy();
42
102
  });
103
+ api.onRenderPrepared(info => {
104
+ const snapshot = getRouterServerSnapshot(info.runtimeContext);
105
+ if (!snapshot) {
106
+ return info;
107
+ }
108
+ return {
109
+ ...info,
110
+ routerResult: {
111
+ statusCode: snapshot.statusCode ?? info.routerResult?.statusCode,
112
+ errors: snapshot.errors ?? info.routerResult?.errors,
113
+ },
114
+ };
115
+ });
116
+ api.onRequestEnd(async ({ runtimeContext }) => {
117
+ try {
118
+ await getRouterRuntimeState(runtimeContext)?.cleanup?.();
119
+ } catch (error) {
120
+ const { ssrContext } = runtimeContext as {
121
+ ssrContext?: {
122
+ onError: (error: unknown, message: string) => void;
123
+ };
124
+ };
125
+ if (!ssrContext) {
126
+ throw error;
127
+ }
128
+ ssrContext.onError(error, ROUTER_CLEANUP_ERROR);
129
+ }
130
+ });
43
131
  api.onAfterCreateRouter(event => {
44
132
  if (event.framework !== 'react-router') {
45
133
  return;
@@ -51,7 +139,7 @@ export function createRouterStatePlugin<Hooks extends Record<string, unknown>>({
51
139
  if (!routerContext) {
52
140
  return;
53
141
  }
54
- const serverSnapshot = createRouterServerSnapshot({
142
+ const snapshot = {
55
143
  framework: event.framework,
56
144
  basename: event.basename,
57
145
  statusCode: routerContext.statusCode,
@@ -65,19 +153,27 @@ export function createRouterStatePlugin<Hooks extends Record<string, unknown>>({
65
153
  ? [{ routeId: match.route.id }]
66
154
  : [],
67
155
  ),
68
- });
69
- applyRouterRuntimeState(event.runtimeContext, {
70
- framework: event.framework,
71
- basename: event.basename,
72
- instance: event.router,
73
- matchedRouteIds: serverSnapshot.matchedRouteIds,
74
- serverSnapshot,
156
+ };
157
+ applyRouterServerPrepareResult(event.runtimeContext, {
158
+ snapshot,
159
+ state: {
160
+ framework: event.framework,
161
+ basename: event.basename,
162
+ instance: event.router,
163
+ },
75
164
  });
76
165
  } else if (event.phase === 'client-create') {
166
+ const router = event.router as ReturnType<typeof createMemoryRouter>;
167
+ const { router: api } = event.runtimeContext as {
168
+ router?: { Link?: RouterNavigationCapability['Link'] };
169
+ };
77
170
  applyRouterRuntimeState(event.runtimeContext, {
78
171
  framework: event.framework,
79
172
  basename: event.basename,
80
- instance: event.router,
173
+ instance: router,
174
+ ...(api?.Link
175
+ ? { navigation: createReactRouterNavigation(router, api.Link) }
176
+ : {}),
81
177
  });
82
178
  }
83
179
  });
@@ -1,3 +1,5 @@
1
+ import type React from 'react';
2
+
1
3
  export type BuiltInRouterFramework = 'react-router' | 'tanstack';
2
4
 
3
5
  export type RouterFramework = BuiltInRouterFramework | (string & {});
@@ -10,37 +12,65 @@ export interface RouterRouteMatchSnapshot {
10
12
  }
11
13
 
12
14
  export interface InternalRouterServerSnapshot {
13
- framework?: RouterFramework;
14
- basename?: string;
15
- statusCode?: number;
16
- errors?: Record<string, unknown>;
17
- routerData?: {
18
- loaderData?: Record<string, unknown>;
19
- errors?: Record<string, unknown>;
15
+ readonly framework?: RouterFramework;
16
+ readonly basename?: string;
17
+ readonly statusCode?: number;
18
+ readonly errors?: Record<string, unknown>;
19
+ readonly routerData?: {
20
+ readonly loaderData?: Record<string, unknown>;
21
+ readonly errors?: Record<string, unknown>;
20
22
  };
21
- hydrationScript?: string;
22
- hydrationScripts?: string[];
23
- matchedRouteIds?: string[];
24
- matches?: RouterRouteMatchSnapshot[];
23
+ readonly hydrationScripts?: readonly string[];
24
+ readonly matchedRouteIds?: readonly string[];
25
+ readonly matches?: readonly RouterRouteMatchSnapshot[];
25
26
  }
26
27
 
27
28
  export interface InternalRouterRuntimeState {
28
29
  framework: RouterFramework;
29
30
  basename?: string;
30
31
  instance?: unknown;
31
- hydrationScript?: string;
32
- hydrationScripts?: string[];
33
- matchedRouteIds?: string[];
34
- matches?: RouterRouteMatchSnapshot[];
35
- serverSnapshot?: InternalRouterServerSnapshot;
32
+ navigation?: RouterNavigationCapability;
36
33
  cleanup?: () => void | Promise<void>;
37
34
  }
38
35
 
39
36
  export interface RouterServerPrepareResult {
40
37
  state: InternalRouterRuntimeState;
41
38
  snapshot?: InternalRouterServerSnapshot;
42
- redirect?: Response;
43
39
  cleanup?: () => void | Promise<void>;
44
40
  }
45
41
 
46
42
  export type RouterLifecyclePhase = 'ssr-prepare' | 'client-create' | 'hydrate';
43
+
44
+ /** Provider-owned browser navigation. Consumers never inspect router internals. */
45
+ export interface RouterNavigationSnapshot {
46
+ location: { pathname: string; search: string; hash: string };
47
+ params: Record<string, string>;
48
+ }
49
+
50
+ export interface RouterLinkTarget {
51
+ pathname: string;
52
+ href: string;
53
+ search?: Record<string, unknown>;
54
+ hash?: string;
55
+ hashScrollIntoView?: boolean | ScrollIntoViewOptions;
56
+ prefetch?: 'intent' | 'render' | 'viewport' | 'none';
57
+ preload?: unknown;
58
+ }
59
+
60
+ export interface RouterNavigationCapability {
61
+ getSnapshot: () => RouterNavigationSnapshot;
62
+ subscribe: (listener: () => void) => () => void;
63
+ navigate: (
64
+ href: string,
65
+ options?: { replace?: boolean; state?: unknown },
66
+ ) => void | Promise<void>;
67
+ Link: React.ComponentType<{
68
+ to: string;
69
+ children?: React.ReactNode;
70
+ [key: string]: unknown;
71
+ }>;
72
+ createLinkProps?: (target: RouterLinkTarget) => {
73
+ to: string;
74
+ [key: string]: unknown;
75
+ };
76
+ }
@@ -1,2 +1,3 @@
1
1
  export type * from './bffRuntime';
2
+ export type * from './serverSource';
2
3
  export type * from './serverTelemetry';
@@ -0,0 +1,23 @@
1
+ /** Context passed to function items of a server `source.globalVars` chain. */
2
+ export interface ServerGlobalVarsContext {
3
+ env: string;
4
+ target: 'web' | 'node' | 'web-worker';
5
+ }
6
+
7
+ export type ServerGlobalVars = Record<string, unknown>;
8
+
9
+ type ServerGlobalVarsChainItem =
10
+ | ServerGlobalVars
11
+ | ((
12
+ config: ServerGlobalVars,
13
+ context: ServerGlobalVarsContext,
14
+ ) => ServerGlobalVars | void);
15
+
16
+ /** Source options that server code reads from the application config. */
17
+ export interface ServerSourceConfigExtension {
18
+ /**
19
+ * Compile-time constants replaced in server code, such as BFF handlers.
20
+ * Server code resolves the chain with `{ env: 'server', target: 'node' }`.
21
+ */
22
+ globalVars?: ServerGlobalVarsChainItem | ServerGlobalVarsChainItem[];
23
+ }
@@ -1,73 +0,0 @@
1
- "use strict";
2
- var __webpack_require__ = {};
3
- (()=>{
4
- __webpack_require__.d = (exports1, getters, values)=>{
5
- var define = (defs, kind)=>{
6
- for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
7
- enumerable: true,
8
- [kind]: defs[key]
9
- });
10
- };
11
- define(getters, "get");
12
- define(values, "value");
13
- };
14
- })();
15
- (()=>{
16
- __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
17
- })();
18
- (()=>{
19
- __webpack_require__.r = (exports1)=>{
20
- if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
21
- value: 'Module'
22
- });
23
- Object.defineProperty(exports1, '__esModule', {
24
- value: true
25
- });
26
- };
27
- })();
28
- var __webpack_exports__ = {};
29
- __webpack_require__.r(__webpack_exports__);
30
- function readBuildValue(read, fallback) {
31
- try {
32
- return read();
33
- } catch (error) {
34
- if (null !== error && 'object' == typeof error && 'name' in error && 'ReferenceError' === error.name) return fallback;
35
- throw error;
36
- }
37
- }
38
- function resolveUltramodernBuildArtifact(artifact, readers) {
39
- const buildMarker = readers ? readBuildValue(readers.buildMarker, artifact.deliveryUnit.buildMarker) : artifact.deliveryUnit.buildMarker;
40
- const sourceRevision = readers ? readBuildValue(readers.sourceRevision, artifact.deliveryUnit.sourceRevision) : artifact.deliveryUnit.sourceRevision;
41
- const identity = {
42
- build: buildMarker,
43
- buildMarker,
44
- sourceRevision
45
- };
46
- return {
47
- ...artifact,
48
- deliveryUnit: {
49
- ...artifact.deliveryUnit,
50
- ...identity
51
- },
52
- surfaces: {
53
- api: {
54
- ...artifact.surfaces.api,
55
- ...identity
56
- },
57
- ui: {
58
- ...artifact.surfaces.ui,
59
- ...identity
60
- }
61
- }
62
- };
63
- }
64
- __webpack_require__.d(__webpack_exports__, {
65
- resolveUltramodernBuildArtifact: ()=>resolveUltramodernBuildArtifact
66
- });
67
- exports.resolveUltramodernBuildArtifact = __webpack_exports__.resolveUltramodernBuildArtifact;
68
- for(var __rspack_i in __webpack_exports__)if (-1 === [
69
- "resolveUltramodernBuildArtifact"
70
- ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
71
- Object.defineProperty(exports, '__esModule', {
72
- value: true
73
- });
@@ -1,35 +0,0 @@
1
- function readBuildValue(read, fallback) {
2
- try {
3
- return read();
4
- } catch (error) {
5
- if (null !== error && 'object' == typeof error && 'name' in error && 'ReferenceError' === error.name) return fallback;
6
- throw error;
7
- }
8
- }
9
- function resolveUltramodernBuildArtifact(artifact, readers) {
10
- const buildMarker = readers ? readBuildValue(readers.buildMarker, artifact.deliveryUnit.buildMarker) : artifact.deliveryUnit.buildMarker;
11
- const sourceRevision = readers ? readBuildValue(readers.sourceRevision, artifact.deliveryUnit.sourceRevision) : artifact.deliveryUnit.sourceRevision;
12
- const identity = {
13
- build: buildMarker,
14
- buildMarker,
15
- sourceRevision
16
- };
17
- return {
18
- ...artifact,
19
- deliveryUnit: {
20
- ...artifact.deliveryUnit,
21
- ...identity
22
- },
23
- surfaces: {
24
- api: {
25
- ...artifact.surfaces.api,
26
- ...identity
27
- },
28
- ui: {
29
- ...artifact.surfaces.ui,
30
- ...identity
31
- }
32
- }
33
- };
34
- }
35
- export { resolveUltramodernBuildArtifact };