@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.11 → 3.9.0-ultramodern.12

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 (66) hide show
  1. package/dist/cjs/localisedUrls/config.js +49 -0
  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 +100 -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/routerState.js +48 -77
  12. package/dist/cjs/routerStatePlugin.js +36 -9
  13. package/dist/esm/localisedUrls/config.mjs +11 -0
  14. package/dist/esm/localisedUrls/index.mjs +8 -0
  15. package/dist/esm/localisedUrls/normalise.mjs +37 -0
  16. package/dist/esm/localisedUrls/pathname.mjs +24 -0
  17. package/dist/esm/localisedUrls/patterns.mjs +152 -0
  18. package/dist/esm/localisedUrls/redirect.mjs +53 -0
  19. package/dist/esm/localisedUrls/resolve.mjs +37 -0
  20. package/dist/esm/localisedUrls/routeIdentity.mjs +54 -0
  21. package/dist/esm/localisedUrls/routes.mjs +176 -0
  22. package/dist/esm/localisedUrls/types.mjs +0 -0
  23. package/dist/esm/routerState.mjs +49 -75
  24. package/dist/esm/routerStatePlugin.mjs +37 -10
  25. package/dist/esm-node/localisedUrls/config.mjs +12 -0
  26. package/dist/esm-node/localisedUrls/index.mjs +9 -0
  27. package/dist/esm-node/localisedUrls/normalise.mjs +38 -0
  28. package/dist/esm-node/localisedUrls/pathname.mjs +25 -0
  29. package/dist/esm-node/localisedUrls/patterns.mjs +153 -0
  30. package/dist/esm-node/localisedUrls/redirect.mjs +54 -0
  31. package/dist/esm-node/localisedUrls/resolve.mjs +38 -0
  32. package/dist/esm-node/localisedUrls/routeIdentity.mjs +55 -0
  33. package/dist/esm-node/localisedUrls/routes.mjs +177 -0
  34. package/dist/esm-node/localisedUrls/types.mjs +1 -0
  35. package/dist/esm-node/routerState.mjs +49 -75
  36. package/dist/esm-node/routerStatePlugin.mjs +37 -10
  37. package/dist/types/localisedUrls/config.d.ts +9 -0
  38. package/dist/types/localisedUrls/index.d.ts +10 -0
  39. package/dist/types/localisedUrls/normalise.d.ts +10 -0
  40. package/dist/types/localisedUrls/pathname.d.ts +4 -0
  41. package/dist/types/localisedUrls/patterns.d.ts +11 -0
  42. package/dist/types/localisedUrls/redirect.d.ts +19 -0
  43. package/dist/types/localisedUrls/resolve.d.ts +7 -0
  44. package/dist/types/localisedUrls/routeIdentity.d.ts +22 -0
  45. package/dist/types/localisedUrls/routes.d.ts +3 -0
  46. package/dist/types/localisedUrls/types.d.ts +14 -0
  47. package/dist/types/routeHydration.d.ts +2 -2
  48. package/dist/types/routerState.d.ts +5 -6
  49. package/dist/types/routerStateTypes.d.ts +47 -17
  50. package/package.json +16 -4
  51. package/src/localisedUrls/config.ts +18 -0
  52. package/src/localisedUrls/index.ts +25 -0
  53. package/src/localisedUrls/normalise.ts +62 -0
  54. package/src/localisedUrls/pathname.ts +60 -0
  55. package/src/localisedUrls/patterns.ts +283 -0
  56. package/src/localisedUrls/redirect.ts +107 -0
  57. package/src/localisedUrls/resolve.ts +106 -0
  58. package/src/localisedUrls/routeIdentity.ts +83 -0
  59. package/src/localisedUrls/routes.ts +451 -0
  60. package/src/localisedUrls/types.ts +18 -0
  61. package/src/routeHydration.ts +2 -2
  62. package/src/routerState.ts +52 -122
  63. package/src/routerStatePlugin.ts +49 -10
  64. package/src/routerStateTypes.ts +47 -17
  65. package/tests/localisedUrls.test.ts +560 -0
  66. package/tests/routerState.test.ts +7 -8
@@ -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,99 +52,49 @@ 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
84
- ? {
85
- hydrationScript: state.hydrationScript ?? hydrationScripts[0],
86
- hydrationScripts,
87
- }
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]) }
88
72
  : {}),
89
- ...(matchedRouteIds ? { matchedRouteIds } : {}),
90
- };
91
- }
92
-
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
-
135
- return {
136
- ...state,
137
- ...(hydrationScripts?.length
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
138
81
  ? {
139
- hydrationScript: state.hydrationScript ?? hydrationScripts[0],
140
- 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
+ }),
141
95
  }
142
96
  : {}),
143
- ...(matchedRouteIds ? { matchedRouteIds } : {}),
144
- ...(serverSnapshot ? { serverSnapshot } : {}),
145
- };
97
+ });
146
98
  }
147
99
 
148
100
  /**
@@ -223,19 +175,13 @@ export function applyRouterRuntimeState<Context extends object>(
223
175
  state: InternalRouterRuntimeState,
224
176
  ) {
225
177
  const previous = routerRuntimeStateExtension.get(runtimeContext);
226
- const normalized = createRouterRuntimeState(state);
227
- routerRuntimeStateExtension.set(runtimeContext, normalized);
228
- if (normalized.serverSnapshot) {
229
- routerServerSnapshotExtension.set(
230
- runtimeContext,
231
- normalized.serverSnapshot,
232
- );
233
- }
178
+ routerRuntimeStateExtension.set(runtimeContext, state);
234
179
  // Only a change of identity is worth a re-render; `RouterWrapper` reapplies
235
180
  // the same instance on every render of the app.
236
181
  if (
237
- previous?.instance !== normalized.instance ||
238
- previous?.framework !== normalized.framework
182
+ previous?.instance !== state.instance ||
183
+ previous?.framework !== state.framework ||
184
+ previous?.navigation !== state.navigation
239
185
  ) {
240
186
  notifyRouterRuntimeState(runtimeContext);
241
187
  }
@@ -247,40 +193,24 @@ export function applyRouterServerPrepareResult<Context extends object>(
247
193
  runtimeContext: Context,
248
194
  result: RouterServerPrepareResult,
249
195
  ) {
250
- const state = createRouterRuntimeState({
196
+ if (result.snapshot) {
197
+ routerServerSnapshotExtension.set(
198
+ runtimeContext,
199
+ createRouterServerSnapshot(result.snapshot),
200
+ );
201
+ }
202
+ return applyRouterRuntimeState(runtimeContext, {
251
203
  ...result.state,
252
204
  cleanup: result.cleanup ?? result.state.cleanup,
253
- serverSnapshot: result.snapshot ?? result.state.serverSnapshot,
254
205
  });
255
- applyRouterRuntimeState(runtimeContext, state);
256
- return runtimeContext;
257
206
  }
258
207
 
259
208
  export function getRouterHydrationScripts(runtimeContext: object) {
260
- const serverSnapshot = getRouterServerSnapshot(runtimeContext);
261
- const runtimeState = getRouterRuntimeState(runtimeContext);
262
- return (
263
- serverSnapshot?.hydrationScripts ??
264
- toHydrationScripts({
265
- hydrationScript: serverSnapshot?.hydrationScript,
266
- }) ??
267
- runtimeState?.hydrationScripts ??
268
- toHydrationScripts({
269
- hydrationScript: runtimeState?.hydrationScript,
270
- }) ??
271
- []
272
- );
209
+ return getRouterServerSnapshot(runtimeContext)?.hydrationScripts ?? [];
273
210
  }
274
211
 
275
212
  export function getRouterMatchedRouteIds(runtimeContext: object) {
276
- const serverSnapshot = getRouterServerSnapshot(runtimeContext);
277
- const runtimeState = getRouterRuntimeState(runtimeContext);
278
- return (
279
- serverSnapshot?.matchedRouteIds ??
280
- getMatchedRouteIdsFromMatches(serverSnapshot?.matches) ??
281
- runtimeState?.matchedRouteIds ??
282
- getMatchedRouteIdsFromMatches(runtimeState?.matches)
283
- );
213
+ return getRouterServerSnapshot(runtimeContext)?.matchedRouteIds;
284
214
  }
285
215
 
286
216
  export async function cleanupRouterRuntimeState(runtimeContext: object) {
@@ -1,8 +1,13 @@
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,
5
6
  } from './routerState';
7
+ import type {
8
+ RouterNavigationCapability,
9
+ RouterNavigationSnapshot,
10
+ } from './routerStateTypes';
6
11
 
7
12
  type ReactRouterServerContext = {
8
13
  statusCode: number;
@@ -19,6 +24,32 @@ type RouterLifecycleEvent = {
19
24
  router?: unknown;
20
25
  };
21
26
 
27
+ function createReactRouterNavigation(
28
+ router: ReturnType<typeof createMemoryRouter>,
29
+ Link: RouterNavigationCapability['Link'],
30
+ ): RouterNavigationCapability {
31
+ let state: typeof router.state | undefined;
32
+ let snapshot: RouterNavigationSnapshot;
33
+ return {
34
+ Link,
35
+ getSnapshot() {
36
+ if (state !== router.state) {
37
+ state = router.state;
38
+ snapshot = {
39
+ location: state.location,
40
+ params: Object.assign(
41
+ {},
42
+ ...state.matches.map(match => match.params),
43
+ ),
44
+ };
45
+ }
46
+ return snapshot;
47
+ },
48
+ subscribe: listener => router.subscribe(listener),
49
+ navigate: (href, options) => router.navigate(href, options),
50
+ };
51
+ }
52
+
22
53
  /** Install fork policy through lifecycle hooks supplied by the native owner. */
23
54
  export function createRouterStatePlugin<Hooks extends Record<string, unknown>>({
24
55
  registryHooks,
@@ -51,7 +82,7 @@ export function createRouterStatePlugin<Hooks extends Record<string, unknown>>({
51
82
  if (!routerContext) {
52
83
  return;
53
84
  }
54
- const serverSnapshot = createRouterServerSnapshot({
85
+ const snapshot = {
55
86
  framework: event.framework,
56
87
  basename: event.basename,
57
88
  statusCode: routerContext.statusCode,
@@ -65,19 +96,27 @@ export function createRouterStatePlugin<Hooks extends Record<string, unknown>>({
65
96
  ? [{ routeId: match.route.id }]
66
97
  : [],
67
98
  ),
68
- });
69
- applyRouterRuntimeState(event.runtimeContext, {
70
- framework: event.framework,
71
- basename: event.basename,
72
- instance: event.router,
73
- matchedRouteIds: serverSnapshot.matchedRouteIds,
74
- serverSnapshot,
99
+ };
100
+ applyRouterServerPrepareResult(event.runtimeContext, {
101
+ snapshot,
102
+ state: {
103
+ framework: event.framework,
104
+ basename: event.basename,
105
+ instance: event.router,
106
+ },
75
107
  });
76
108
  } else if (event.phase === 'client-create') {
109
+ const router = event.router as ReturnType<typeof createMemoryRouter>;
110
+ const { router: api } = event.runtimeContext as {
111
+ router?: { Link?: RouterNavigationCapability['Link'] };
112
+ };
77
113
  applyRouterRuntimeState(event.runtimeContext, {
78
114
  framework: event.framework,
79
115
  basename: event.basename,
80
- instance: event.router,
116
+ instance: router,
117
+ ...(api?.Link
118
+ ? { navigation: createReactRouterNavigation(router, api.Link) }
119
+ : {}),
81
120
  });
82
121
  }
83
122
  });
@@ -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
+ }