@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.4 → 3.9.0-ultramodern.5
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.
- package/dist/cjs/contextExtensions.js +74 -0
- package/dist/cjs/headRuntime.js +91 -0
- package/dist/cjs/helmetContext.js +54 -0
- package/dist/cjs/index.js +18 -4
- package/dist/cjs/prefetchPolicy.js +52 -0
- package/dist/cjs/request-policy/browser.js +139 -0
- package/dist/cjs/request-policy/client.js +234 -0
- package/dist/cjs/request-policy/node.js +149 -0
- package/dist/cjs/request-policy/policyCore.js +266 -0
- package/dist/cjs/request-policy/requestContext.js +137 -0
- package/dist/cjs/request-policy/traceparent.js +56 -0
- package/dist/cjs/request-policy/transport.js +234 -0
- package/dist/cjs/request-policy/types.js +57 -0
- package/dist/cjs/routeHydration.js +127 -0
- package/dist/cjs/routerPrefetchPolicy.js +153 -0
- package/dist/cjs/routerProvider.js +170 -0
- package/dist/cjs/routerState.js +167 -0
- package/dist/cjs/routerStatePlugin.js +86 -0
- package/dist/cjs/routerStateTypes.js +18 -0
- package/dist/cjs/runtimeContextProjection.js +112 -0
- package/dist/cjs/server-config/bffRuntime.js +18 -0
- package/dist/cjs/server-config/index.js +18 -0
- package/dist/cjs/server-config/serverTelemetry.js +18 -0
- package/dist/cjs/shellStream.js +34 -2
- package/dist/cjs/workspaceEvents.js +159 -0
- package/dist/esm/contextExtensions.mjs +33 -0
- package/dist/esm/headRuntime.mjs +43 -0
- package/dist/esm/helmetContext.mjs +13 -0
- package/dist/esm/index.mjs +2 -0
- package/dist/esm/prefetchPolicy.mjs +11 -0
- package/dist/esm/request-policy/browser.mjs +11 -0
- package/dist/esm/request-policy/client.mjs +195 -0
- package/dist/esm/request-policy/policyCore.mjs +171 -0
- package/dist/esm/request-policy/requestContext.mjs +75 -0
- package/dist/esm/request-policy/traceparent.mjs +18 -0
- package/dist/esm/request-policy/transport.mjs +196 -0
- package/dist/esm/request-policy/types.mjs +10 -0
- package/dist/esm/routeHydration.mjs +77 -0
- package/dist/esm/routerPrefetchPolicy.mjs +115 -0
- package/dist/esm/routerProvider.mjs +117 -0
- package/dist/esm/routerState.mjs +105 -0
- package/dist/esm/routerStatePlugin.mjs +48 -0
- package/dist/esm/routerStateTypes.mjs +0 -0
- package/dist/esm/runtimeContextProjection.mjs +74 -0
- package/dist/esm/server-config/bffRuntime.mjs +0 -0
- package/dist/esm/server-config/index.mjs +0 -0
- package/dist/esm/server-config/serverTelemetry.mjs +0 -0
- package/dist/esm/shellStream.mjs +34 -2
- package/dist/esm/workspaceEvents.mjs +66 -0
- package/dist/esm-node/contextExtensions.mjs +34 -0
- package/dist/esm-node/headRuntime.mjs +44 -0
- package/dist/esm-node/helmetContext.mjs +14 -0
- package/dist/esm-node/index.mjs +2 -0
- package/dist/esm-node/prefetchPolicy.mjs +12 -0
- package/dist/esm-node/request-policy/browser.mjs +12 -0
- package/dist/esm-node/request-policy/client.mjs +196 -0
- package/dist/esm-node/request-policy/node.mjs +22 -0
- package/dist/esm-node/request-policy/policyCore.mjs +172 -0
- package/dist/esm-node/request-policy/requestContext.mjs +76 -0
- package/dist/esm-node/request-policy/traceparent.mjs +19 -0
- package/dist/esm-node/request-policy/transport.mjs +197 -0
- package/dist/esm-node/request-policy/types.mjs +11 -0
- package/dist/esm-node/routeHydration.mjs +78 -0
- package/dist/esm-node/routerPrefetchPolicy.mjs +116 -0
- package/dist/esm-node/routerProvider.mjs +118 -0
- package/dist/esm-node/routerState.mjs +106 -0
- package/dist/esm-node/routerStatePlugin.mjs +49 -0
- package/dist/esm-node/routerStateTypes.mjs +1 -0
- package/dist/esm-node/runtimeContextProjection.mjs +75 -0
- package/dist/esm-node/server-config/bffRuntime.mjs +1 -0
- package/dist/esm-node/server-config/index.mjs +1 -0
- package/dist/esm-node/server-config/serverTelemetry.mjs +1 -0
- package/dist/esm-node/shellStream.mjs +34 -2
- package/dist/esm-node/workspaceEvents.mjs +67 -0
- package/dist/types/contextExtensions.d.ts +34 -0
- package/dist/types/headRuntime.d.ts +12 -0
- package/dist/types/helmetContext.d.ts +10 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/prefetchPolicy.d.ts +4 -0
- package/dist/types/request-policy/browser.d.ts +10 -0
- package/dist/types/request-policy/client.d.ts +14 -0
- package/dist/types/request-policy/node.d.ts +10 -0
- package/dist/types/request-policy/policyCore.d.ts +107 -0
- package/dist/types/request-policy/requestContext.d.ts +22 -0
- package/dist/types/request-policy/traceparent.d.ts +10 -0
- package/dist/types/request-policy/transport.d.ts +12 -0
- package/dist/types/request-policy/types.d.ts +144 -0
- package/dist/types/routeHydration.d.ts +24 -0
- package/dist/types/routerPrefetchPolicy.d.ts +28 -0
- package/dist/types/routerProvider.d.ts +77 -0
- package/dist/types/routerState.d.ts +12 -0
- package/dist/types/routerStatePlugin.d.ts +19 -0
- package/dist/types/routerStateTypes.d.ts +40 -0
- package/dist/types/runtimeContextProjection.d.ts +30 -0
- package/dist/types/server-config/bffRuntime.d.ts +122 -0
- package/dist/types/server-config/index.d.ts +2 -0
- package/dist/types/server-config/serverTelemetry.d.ts +322 -0
- package/dist/types/workspaceEvents.d.ts +62 -0
- package/package.json +198 -13
- package/rslib.config.mts +17 -2
- package/rstest.config.mts +33 -2
- package/src/contextExtensions.ts +81 -0
- package/src/headRuntime.tsx +77 -0
- package/src/helmetContext.ts +31 -0
- package/src/index.ts +2 -0
- package/src/prefetchPolicy.ts +33 -0
- package/src/request-policy/browser.ts +22 -0
- package/src/request-policy/client.ts +404 -0
- package/src/request-policy/node.ts +41 -0
- package/src/request-policy/policyCore.ts +424 -0
- package/src/request-policy/requestContext.ts +149 -0
- package/src/request-policy/traceparent.ts +43 -0
- package/src/request-policy/transport.ts +349 -0
- package/src/request-policy/types.ts +229 -0
- package/src/routeHydration.ts +208 -0
- package/src/routerPrefetchPolicy.ts +195 -0
- package/src/routerProvider.ts +340 -0
- package/src/routerState.ts +208 -0
- package/src/routerStatePlugin.ts +86 -0
- package/src/routerStateTypes.ts +46 -0
- package/src/runtimeContextProjection.ts +105 -0
- package/src/server-config/bffRuntime.ts +136 -0
- package/src/server-config/index.ts +2 -0
- package/src/server-config/serverTelemetry.ts +337 -0
- package/src/shellStream.ts +55 -25
- package/src/workspaceEvents.ts +324 -0
- package/tests/buildIdentity.test.ts +2 -4
- package/tests/contextExtensions.test.ts +71 -0
- package/tests/headRuntime.test.tsx +369 -0
- package/tests/nativeSafeFailureCompatibility.test.ts +117 -0
- package/tests/prefetchPolicy.test.ts +51 -0
- package/tests/rendererHead.test.ts +1 -72
- package/tests/request-policy/browser-policy.test.ts +608 -0
- package/tests/request-policy/client-isolation.test.ts +149 -0
- package/tests/request-policy/identityBinding.test.ts +75 -0
- package/tests/request-policy/node-policy.test.ts +741 -0
- package/tests/request-policy/policyCore.test.ts +250 -0
- package/tests/request-policy/requestContext.test.ts +80 -0
- package/tests/request-policy/requestFactory.outbound-contract.test.ts +691 -0
- package/tests/request-policy/traceparent.test.ts +55 -0
- package/tests/request-policy/transport-retry.test.ts +370 -0
- package/tests/routeHydration.test.ts +180 -0
- package/tests/routerPrefetchPolicy.test.ts +102 -0
- package/tests/routerProvider.test.ts +38 -0
- package/tests/routerState.test.ts +157 -0
- package/tests/routerStatePlugin.test.ts +108 -0
- package/tests/runtimeContextProjection.test.ts +135 -0
- package/tests/shellStream.test.ts +37 -0
- package/tests/workspaceEvents.test.ts +391 -0
- package/dist/cjs/rscHtmlStream.js +0 -128
- package/dist/esm/rscHtmlStream.mjs +0 -86
- package/dist/esm-node/rscHtmlStream.mjs +0 -87
- package/dist/types/rscHtmlStream.d.ts +0 -4
- package/src/rscHtmlStream.ts +0 -142
- package/tests/rscHtmlStream.test.ts +0 -153
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// @effect-diagnostics globalConsole:off
|
|
2
|
+
import { merge } from '@modern-js/runtime-utils/merge';
|
|
3
|
+
|
|
4
|
+
/** Structural plugin contract; the composing integration supplies native hooks. */
|
|
5
|
+
export type RouterProviderPlugin = {
|
|
6
|
+
name?: string;
|
|
7
|
+
registryHooks?: Record<string, unknown>;
|
|
8
|
+
setup?: (api: any) => unknown;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Guard for the wrapper plugin (`router/internal`): a resolved provider is
|
|
14
|
+
* only invoked through `setup`, so registry hooks outside the canonical
|
|
15
|
+
* router hook contract cannot be registered for it. Returns the offending
|
|
16
|
+
* hook names and warns once so they are surfaced instead of silently dropped.
|
|
17
|
+
*/
|
|
18
|
+
export function reportUnsupportedProviderRegistryHooks(
|
|
19
|
+
providerPlugin: {
|
|
20
|
+
name?: string;
|
|
21
|
+
registryHooks?: Record<string, unknown>;
|
|
22
|
+
},
|
|
23
|
+
routerProviderRegistryHooks: Record<string, unknown>,
|
|
24
|
+
): string[] {
|
|
25
|
+
const unsupportedHookNames = Object.keys(
|
|
26
|
+
providerPlugin.registryHooks ?? {},
|
|
27
|
+
).filter(hookName => !(hookName in routerProviderRegistryHooks));
|
|
28
|
+
|
|
29
|
+
if (unsupportedHookNames.length > 0) {
|
|
30
|
+
console.warn(
|
|
31
|
+
`[@modern-js/runtime] The router provider "${providerPlugin.name}" declares registry hooks outside the router hook contract: ${unsupportedHookNames.join(
|
|
32
|
+
', ',
|
|
33
|
+
)}. These hooks are not registered when the provider is resolved through \`runtime.router.framework\` — declare them on a separate runtime plugin instead.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return unsupportedHookNames;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type RouterProviderFactory = (userConfig?: any) => RouterProviderPlugin;
|
|
41
|
+
|
|
42
|
+
export type RouterProviderRegistration = {
|
|
43
|
+
name: string;
|
|
44
|
+
factory: RouterProviderFactory;
|
|
45
|
+
isDefault?: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* App-owned provider catalog. Provider factories close over their module graph,
|
|
50
|
+
* so they must be resolved from the runtime wrapper that owns the app instead
|
|
51
|
+
* of from a page-global first-registration winner.
|
|
52
|
+
*/
|
|
53
|
+
export type RouterProviderRealm = Readonly<{
|
|
54
|
+
defaultProvider?: string;
|
|
55
|
+
get: (name: string) => RouterProviderFactory | undefined;
|
|
56
|
+
names: () => readonly string[];
|
|
57
|
+
}>;
|
|
58
|
+
|
|
59
|
+
export function createRouterProviderRealm(
|
|
60
|
+
registrations: readonly RouterProviderRegistration[],
|
|
61
|
+
): RouterProviderRealm {
|
|
62
|
+
const providers = new Map<string, RouterProviderFactory>();
|
|
63
|
+
let defaultProvider: string | undefined;
|
|
64
|
+
|
|
65
|
+
for (const registration of registrations) {
|
|
66
|
+
if (registration.name.length === 0) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'[@modern-js/runtime] A router provider realm cannot contain an unnamed provider.',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (providers.has(registration.name)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`[@modern-js/runtime] Router provider "${registration.name}" is declared more than once in the same runtime realm.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (
|
|
77
|
+
registration.isDefault === true &&
|
|
78
|
+
defaultProvider !== undefined &&
|
|
79
|
+
defaultProvider !== registration.name
|
|
80
|
+
) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`[@modern-js/runtime] Router provider realm declares both "${defaultProvider}" and "${registration.name}" as defaults.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
providers.set(registration.name, registration.factory);
|
|
87
|
+
if (registration.isDefault === true) {
|
|
88
|
+
defaultProvider = registration.name;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const providerNames = Object.freeze([...providers.keys()]);
|
|
93
|
+
return Object.freeze({
|
|
94
|
+
...(defaultProvider !== undefined ? { defaultProvider } : {}),
|
|
95
|
+
get: (name: string) => providers.get(name),
|
|
96
|
+
names: () => providerNames,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type RouterProviderRegistry = {
|
|
101
|
+
providers: Map<string, RouterProviderFactory>;
|
|
102
|
+
defaultProvider?: string;
|
|
103
|
+
/** Names that were registered by more than one module instance. */
|
|
104
|
+
duplicateProviders: Set<string>;
|
|
105
|
+
/** Duplicate names already reported when the compatibility fallback won. */
|
|
106
|
+
warnedDuplicates: Set<string>;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Versioned compatibility-registry key. The unsuffixed key
|
|
111
|
+
* ('@modern-js/runtime:router-providers') is owned by older published copies
|
|
112
|
+
* of this module whose `registerRouterProvider` *throws* on a same-name
|
|
113
|
+
* re-registration. `:v2` added keep-first registration but also enforced one
|
|
114
|
+
* non-default provider across the whole JavaScript realm. `:v3` removes that
|
|
115
|
+
* page-global app invariant: explicit runtime realms own provider selection,
|
|
116
|
+
* while this registry remains only a mixed-version fallback. New copies read
|
|
117
|
+
* but never mutate the v2 slot so older published copies remain compatible.
|
|
118
|
+
*/
|
|
119
|
+
const REGISTRY_SLOT: unique symbol = Symbol.for(
|
|
120
|
+
'@modern-js/runtime:router-providers:v3',
|
|
121
|
+
);
|
|
122
|
+
const LEGACY_V2_REGISTRY_SLOT: unique symbol = Symbol.for(
|
|
123
|
+
'@modern-js/runtime:router-providers:v2',
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
function getRegistry(): RouterProviderRegistry {
|
|
127
|
+
const host = globalThis as { [REGISTRY_SLOT]?: RouterProviderRegistry };
|
|
128
|
+
host[REGISTRY_SLOT] ??= {
|
|
129
|
+
providers: new Map(),
|
|
130
|
+
duplicateProviders: new Set(),
|
|
131
|
+
warnedDuplicates: new Set(),
|
|
132
|
+
};
|
|
133
|
+
// Defense in depth within the v3 key: if a future v3-keyed copy of this
|
|
134
|
+
// module ever predates a later-added field (mixed v3 minors in a Module
|
|
135
|
+
// Federation setup), heal the shape instead of crashing on `undefined`.
|
|
136
|
+
host[REGISTRY_SLOT].duplicateProviders ??= new Set();
|
|
137
|
+
host[REGISTRY_SLOT].warnedDuplicates ??= new Set();
|
|
138
|
+
return host[REGISTRY_SLOT];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getLegacyV2Registry():
|
|
142
|
+
| Pick<RouterProviderRegistry, 'providers' | 'defaultProvider'>
|
|
143
|
+
| undefined {
|
|
144
|
+
const host = globalThis as {
|
|
145
|
+
[LEGACY_V2_REGISTRY_SLOT]?: Pick<
|
|
146
|
+
RouterProviderRegistry,
|
|
147
|
+
'providers' | 'defaultProvider'
|
|
148
|
+
>;
|
|
149
|
+
};
|
|
150
|
+
return host[LEGACY_V2_REGISTRY_SLOT];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function registerRouterProvider(
|
|
154
|
+
name: string,
|
|
155
|
+
factory: RouterProviderFactory,
|
|
156
|
+
options: { isDefault?: boolean } = {},
|
|
157
|
+
): void {
|
|
158
|
+
const registry = getRegistry();
|
|
159
|
+
const existing = registry.providers.get(name);
|
|
160
|
+
|
|
161
|
+
if (existing !== undefined) {
|
|
162
|
+
// Keep-first semantics. A same-name re-registration with a *different*
|
|
163
|
+
// factory is almost always two bundled copies of the same provider
|
|
164
|
+
// module — e.g. a Module Federation remote that does not share
|
|
165
|
+
// '@modern-js/plugin-tanstack/runtime' evaluates its own copy, which
|
|
166
|
+
// creates a fresh factory function per evaluation. That must not crash
|
|
167
|
+
// the app; the first registration remains available for compatibility.
|
|
168
|
+
// Merely loading independent app-owned realms is valid, so defer the
|
|
169
|
+
// warning until a caller actually consumes this ambiguous fallback.
|
|
170
|
+
if (existing !== factory) {
|
|
171
|
+
registry.duplicateProviders.add(name);
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (options.isDefault === true) {
|
|
177
|
+
registry.defaultProvider = name;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
registry.providers.set(name, factory);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function resolveRouterProvider(
|
|
184
|
+
framework?: string,
|
|
185
|
+
options: {
|
|
186
|
+
/**
|
|
187
|
+
* Provider factories owned by the resolving app/runtime wrapper. A realm
|
|
188
|
+
* is the exclusive provider source when supplied because its factories
|
|
189
|
+
* close over the correct app module graph.
|
|
190
|
+
*/
|
|
191
|
+
realm?: RouterProviderRealm;
|
|
192
|
+
/**
|
|
193
|
+
* Legacy single-provider override retained for published wrappers. New
|
|
194
|
+
* integrations should pass an app-owned `realm`, which supports every
|
|
195
|
+
* provider rather than only the default.
|
|
196
|
+
*/
|
|
197
|
+
localDefault?: { name: string; factory: RouterProviderFactory };
|
|
198
|
+
} = {},
|
|
199
|
+
): RouterProviderFactory {
|
|
200
|
+
if (options.realm !== undefined) {
|
|
201
|
+
const name = framework || options.realm.defaultProvider;
|
|
202
|
+
const realmProviderNames = options.realm.names();
|
|
203
|
+
|
|
204
|
+
if (name === undefined) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`[@modern-js/runtime] The app-owned router provider realm does not declare a default provider. Available realm providers: ${
|
|
207
|
+
realmProviderNames.join(', ') || '(none)'
|
|
208
|
+
}.`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const factory = options.realm.get(name);
|
|
213
|
+
if (factory === undefined) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`[@modern-js/runtime] Router provider "${name}" is not registered in the app-owned router provider realm. ` +
|
|
216
|
+
`Available realm providers: ${
|
|
217
|
+
realmProviderNames.join(', ') || '(none)'
|
|
218
|
+
}. Compatibility registry fallback is disabled for app-owned realms.`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return factory;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const registry = getRegistry();
|
|
226
|
+
const legacyV2Registry = getLegacyV2Registry();
|
|
227
|
+
// `||` on purpose: a falsy framework value (empty string from env
|
|
228
|
+
// templating, `false`, `undefined`) falls back to the default provider
|
|
229
|
+
// instead of erroring on an unknown framework "". Prefer the older
|
|
230
|
+
// localDefault contract before compatibility slots.
|
|
231
|
+
const name =
|
|
232
|
+
framework ||
|
|
233
|
+
options.localDefault?.name ||
|
|
234
|
+
registry.defaultProvider ||
|
|
235
|
+
legacyV2Registry?.defaultProvider;
|
|
236
|
+
|
|
237
|
+
if (name === undefined) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
'[@modern-js/runtime] No default router provider is registered. This is a bug in the runtime setup.',
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (
|
|
244
|
+
options.localDefault !== undefined &&
|
|
245
|
+
name === options.localDefault.name
|
|
246
|
+
) {
|
|
247
|
+
return options.localDefault.factory;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const factory = registry.providers.get(name);
|
|
251
|
+
if (factory !== undefined) {
|
|
252
|
+
if (
|
|
253
|
+
registry.duplicateProviders.has(name) &&
|
|
254
|
+
!registry.warnedDuplicates.has(name)
|
|
255
|
+
) {
|
|
256
|
+
registry.warnedDuplicates.add(name);
|
|
257
|
+
console.warn(
|
|
258
|
+
`[@modern-js/runtime] The router provider "${name}" was registered more than once with different module instances, and a router wrapper without an app-owned provider realm is resolving the mixed-version compatibility fallback. The fallback is keeping the first registration. ` +
|
|
259
|
+
'Modern router wrappers isolate app-owned realms automatically. ' +
|
|
260
|
+
'For an older wrapper, share the provider runtime between Module Federation host and remotes or upgrade every app to a realm-aware runtime.',
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return factory;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const legacyV2Factory = legacyV2Registry?.providers.get(name);
|
|
267
|
+
if (legacyV2Factory !== undefined) {
|
|
268
|
+
return legacyV2Factory;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (name === 'tanstack') {
|
|
272
|
+
throw new Error(
|
|
273
|
+
'[@modern-js/runtime] `runtime.router.framework` is set to "tanstack", but no TanStack router provider is registered. ' +
|
|
274
|
+
'Install @modern-js/plugin-tanstack, add `tanstackRouterPlugin()` to the `plugins` array in modern.config.ts, ' +
|
|
275
|
+
"and make sure '@modern-js/plugin-tanstack/runtime' is imported (e.g. in modern.runtime.ts).",
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
throw new Error(
|
|
280
|
+
`[@modern-js/runtime] Unknown router framework "${name}". ` +
|
|
281
|
+
`Registered providers: ${
|
|
282
|
+
[
|
|
283
|
+
...new Set([
|
|
284
|
+
...registry.providers.keys(),
|
|
285
|
+
...(legacyV2Registry?.providers.keys() ?? []),
|
|
286
|
+
]),
|
|
287
|
+
].join(', ') || '(none)'
|
|
288
|
+
}. Install and register the plugin that provides this router framework.`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Test-only escape hatch: compatibility registries live on `globalThis`, so
|
|
294
|
+
* unit tests need a way to restore a pristine state between cases.
|
|
295
|
+
*/
|
|
296
|
+
export function unsafe_resetRouterProvidersForTesting(): void {
|
|
297
|
+
const host = globalThis as {
|
|
298
|
+
[REGISTRY_SLOT]?: RouterProviderRegistry;
|
|
299
|
+
[LEGACY_V2_REGISTRY_SLOT]?: unknown;
|
|
300
|
+
};
|
|
301
|
+
delete host[REGISTRY_SLOT];
|
|
302
|
+
delete host[LEGACY_V2_REGISTRY_SLOT];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Compose app-owned providers without importing the native runtime graph. */
|
|
306
|
+
export function createRouterPlugin<Hooks extends Record<string, unknown>>({
|
|
307
|
+
defaultProvider,
|
|
308
|
+
registryHooks,
|
|
309
|
+
localProviders = [],
|
|
310
|
+
}: {
|
|
311
|
+
defaultProvider: { name: string; factory: RouterProviderFactory };
|
|
312
|
+
registryHooks: Hooks;
|
|
313
|
+
localProviders?: readonly Omit<RouterProviderRegistration, 'isDefault'>[];
|
|
314
|
+
}) {
|
|
315
|
+
registerRouterProvider(defaultProvider.name, defaultProvider.factory, {
|
|
316
|
+
isDefault: true,
|
|
317
|
+
});
|
|
318
|
+
const realm = createRouterProviderRealm([
|
|
319
|
+
{ ...defaultProvider, isDefault: true },
|
|
320
|
+
...localProviders,
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
return (userConfig: Record<string, any> = {}) => ({
|
|
324
|
+
name: '@modern-js/plugin-router',
|
|
325
|
+
registryHooks,
|
|
326
|
+
setup(api: {
|
|
327
|
+
getRuntimeConfig: () => { router?: Record<string, unknown> };
|
|
328
|
+
[key: string]: any;
|
|
329
|
+
}) {
|
|
330
|
+
const mergedConfig = merge(
|
|
331
|
+
api.getRuntimeConfig().router || {},
|
|
332
|
+
userConfig,
|
|
333
|
+
);
|
|
334
|
+
const factory = resolveRouterProvider(mergedConfig.framework, { realm });
|
|
335
|
+
const provider = factory(userConfig);
|
|
336
|
+
reportUnsupportedProviderRegistryHooks(provider, registryHooks);
|
|
337
|
+
provider.setup?.(api);
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// @effect-diagnostics asyncFunction:off strictBooleanExpressions:off
|
|
2
|
+
import { createRuntimeContextExtension } from './contextExtensions';
|
|
3
|
+
import type {
|
|
4
|
+
InternalRouterRuntimeState,
|
|
5
|
+
InternalRouterServerSnapshot,
|
|
6
|
+
RouterRouteMatchSnapshot,
|
|
7
|
+
RouterServerPrepareResult,
|
|
8
|
+
} from './routerStateTypes';
|
|
9
|
+
|
|
10
|
+
export type {
|
|
11
|
+
BuiltInRouterFramework,
|
|
12
|
+
InternalRouterRuntimeState,
|
|
13
|
+
InternalRouterServerSnapshot,
|
|
14
|
+
RouterFramework,
|
|
15
|
+
RouterLifecyclePhase,
|
|
16
|
+
RouterRouteMatchSnapshot,
|
|
17
|
+
RouterServerPrepareResult,
|
|
18
|
+
} from './routerStateTypes';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Router runtime state is shared by every router provider (react-router,
|
|
22
|
+
* @modern-js/plugin-tanstack, ...) and consumed by the SSR pipeline. It lives
|
|
23
|
+
* in the runtime-context extension slot instead of ad-hoc fields on
|
|
24
|
+
* `TInternalRuntimeContext`.
|
|
25
|
+
*/
|
|
26
|
+
const routerRuntimeStateExtension =
|
|
27
|
+
createRuntimeContextExtension<InternalRouterRuntimeState>(
|
|
28
|
+
'@modern-js/runtime:router-runtime-state',
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The server snapshot is tracked separately: a later
|
|
33
|
+
* `applyRouterRuntimeState` call without a snapshot must not clear a
|
|
34
|
+
* previously captured one.
|
|
35
|
+
*/
|
|
36
|
+
const routerServerSnapshotExtension =
|
|
37
|
+
createRuntimeContextExtension<InternalRouterServerSnapshot>(
|
|
38
|
+
'@modern-js/runtime:router-server-snapshot',
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export function getRouterRuntimeState(
|
|
42
|
+
runtimeContext: object,
|
|
43
|
+
): InternalRouterRuntimeState | undefined {
|
|
44
|
+
return routerRuntimeStateExtension.get(runtimeContext);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getRouterServerSnapshot(
|
|
48
|
+
runtimeContext: object,
|
|
49
|
+
): InternalRouterServerSnapshot | undefined {
|
|
50
|
+
return routerServerSnapshotExtension.get(runtimeContext);
|
|
51
|
+
}
|
|
52
|
+
|
|
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
|
+
|
|
74
|
+
export function createRouterServerSnapshot(
|
|
75
|
+
state: RouterSnapshotLike,
|
|
76
|
+
): InternalRouterServerSnapshot {
|
|
77
|
+
const hydrationScripts = toHydrationScripts(state);
|
|
78
|
+
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
|
+
}
|
|
88
|
+
: {}),
|
|
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
|
|
138
|
+
? {
|
|
139
|
+
hydrationScript: state.hydrationScript ?? hydrationScripts[0],
|
|
140
|
+
hydrationScripts,
|
|
141
|
+
}
|
|
142
|
+
: {}),
|
|
143
|
+
...(matchedRouteIds ? { matchedRouteIds } : {}),
|
|
144
|
+
...(serverSnapshot ? { serverSnapshot } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function applyRouterRuntimeState<Context extends object>(
|
|
149
|
+
runtimeContext: Context,
|
|
150
|
+
state: InternalRouterRuntimeState,
|
|
151
|
+
) {
|
|
152
|
+
const normalized = createRouterRuntimeState(state);
|
|
153
|
+
routerRuntimeStateExtension.set(runtimeContext, normalized);
|
|
154
|
+
if (normalized.serverSnapshot) {
|
|
155
|
+
routerServerSnapshotExtension.set(
|
|
156
|
+
runtimeContext,
|
|
157
|
+
normalized.serverSnapshot,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return runtimeContext;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function applyRouterServerPrepareResult<Context extends object>(
|
|
165
|
+
runtimeContext: Context,
|
|
166
|
+
result: RouterServerPrepareResult,
|
|
167
|
+
) {
|
|
168
|
+
const state = createRouterRuntimeState({
|
|
169
|
+
...result.state,
|
|
170
|
+
cleanup: result.cleanup ?? result.state.cleanup,
|
|
171
|
+
serverSnapshot: result.snapshot ?? result.state.serverSnapshot,
|
|
172
|
+
});
|
|
173
|
+
applyRouterRuntimeState(runtimeContext, state);
|
|
174
|
+
return runtimeContext;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
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
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
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
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function cleanupRouterRuntimeState(runtimeContext: object) {
|
|
205
|
+
try {
|
|
206
|
+
await getRouterRuntimeState(runtimeContext)?.cleanup?.();
|
|
207
|
+
} catch {}
|
|
208
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createRouterPrefetchPolicy } from './routerPrefetchPolicy';
|
|
2
|
+
import {
|
|
3
|
+
applyRouterRuntimeState,
|
|
4
|
+
createRouterServerSnapshot,
|
|
5
|
+
} from './routerState';
|
|
6
|
+
|
|
7
|
+
type ReactRouterServerContext = {
|
|
8
|
+
statusCode: number;
|
|
9
|
+
errors?: Record<string, unknown> | null;
|
|
10
|
+
loaderData: Record<string, unknown>;
|
|
11
|
+
matches: { route: { id?: string } }[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type RouterLifecycleEvent = {
|
|
15
|
+
framework: string;
|
|
16
|
+
phase: string;
|
|
17
|
+
runtimeContext: object;
|
|
18
|
+
basename?: string;
|
|
19
|
+
router?: unknown;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Install fork policy through lifecycle hooks supplied by the native owner. */
|
|
23
|
+
export function createRouterStatePlugin<Hooks extends Record<string, unknown>>({
|
|
24
|
+
registryHooks,
|
|
25
|
+
}: {
|
|
26
|
+
registryHooks: Hooks;
|
|
27
|
+
}) {
|
|
28
|
+
return {
|
|
29
|
+
name: '@modern-js/router-runtime-policy',
|
|
30
|
+
registryHooks,
|
|
31
|
+
setup(api: {
|
|
32
|
+
onBeforeRender: (callback: (context: object) => void) => unknown;
|
|
33
|
+
onAfterCreateRouter: (
|
|
34
|
+
callback: (event: RouterLifecycleEvent) => void,
|
|
35
|
+
) => unknown;
|
|
36
|
+
}) {
|
|
37
|
+
api.onBeforeRender(context => {
|
|
38
|
+
const runtimeContext = context as {
|
|
39
|
+
linkPrefetchPolicy?: ReturnType<typeof createRouterPrefetchPolicy>;
|
|
40
|
+
};
|
|
41
|
+
runtimeContext.linkPrefetchPolicy ??= createRouterPrefetchPolicy();
|
|
42
|
+
});
|
|
43
|
+
api.onAfterCreateRouter(event => {
|
|
44
|
+
if (event.framework !== 'react-router') {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (event.phase === 'ssr-prepare') {
|
|
48
|
+
const { routerContext } = event.runtimeContext as {
|
|
49
|
+
routerContext?: ReactRouterServerContext;
|
|
50
|
+
};
|
|
51
|
+
if (!routerContext) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const serverSnapshot = createRouterServerSnapshot({
|
|
55
|
+
framework: event.framework,
|
|
56
|
+
basename: event.basename,
|
|
57
|
+
statusCode: routerContext.statusCode,
|
|
58
|
+
errors: routerContext.errors ?? undefined,
|
|
59
|
+
routerData: {
|
|
60
|
+
loaderData: routerContext.loaderData,
|
|
61
|
+
errors: routerContext.errors ?? undefined,
|
|
62
|
+
},
|
|
63
|
+
matches: routerContext.matches.flatMap(match =>
|
|
64
|
+
typeof match.route.id === 'string'
|
|
65
|
+
? [{ routeId: match.route.id }]
|
|
66
|
+
: [],
|
|
67
|
+
),
|
|
68
|
+
});
|
|
69
|
+
applyRouterRuntimeState(event.runtimeContext, {
|
|
70
|
+
framework: event.framework,
|
|
71
|
+
basename: event.basename,
|
|
72
|
+
instance: event.router,
|
|
73
|
+
matchedRouteIds: serverSnapshot.matchedRouteIds,
|
|
74
|
+
serverSnapshot,
|
|
75
|
+
});
|
|
76
|
+
} else if (event.phase === 'client-create') {
|
|
77
|
+
applyRouterRuntimeState(event.runtimeContext, {
|
|
78
|
+
framework: event.framework,
|
|
79
|
+
basename: event.basename,
|
|
80
|
+
instance: event.router,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type BuiltInRouterFramework = 'react-router' | 'tanstack';
|
|
2
|
+
|
|
3
|
+
export type RouterFramework = BuiltInRouterFramework | (string & {});
|
|
4
|
+
|
|
5
|
+
export interface RouterRouteMatchSnapshot {
|
|
6
|
+
routeId: string;
|
|
7
|
+
assetRouteId?: string;
|
|
8
|
+
pathname?: string;
|
|
9
|
+
params?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
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>;
|
|
20
|
+
};
|
|
21
|
+
hydrationScript?: string;
|
|
22
|
+
hydrationScripts?: string[];
|
|
23
|
+
matchedRouteIds?: string[];
|
|
24
|
+
matches?: RouterRouteMatchSnapshot[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface InternalRouterRuntimeState {
|
|
28
|
+
framework: RouterFramework;
|
|
29
|
+
basename?: string;
|
|
30
|
+
instance?: unknown;
|
|
31
|
+
hydrationScript?: string;
|
|
32
|
+
hydrationScripts?: string[];
|
|
33
|
+
matchedRouteIds?: string[];
|
|
34
|
+
matches?: RouterRouteMatchSnapshot[];
|
|
35
|
+
serverSnapshot?: InternalRouterServerSnapshot;
|
|
36
|
+
cleanup?: () => void | Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RouterServerPrepareResult {
|
|
40
|
+
state: InternalRouterRuntimeState;
|
|
41
|
+
snapshot?: InternalRouterServerSnapshot;
|
|
42
|
+
redirect?: Response;
|
|
43
|
+
cleanup?: () => void | Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type RouterLifecyclePhase = 'ssr-prepare' | 'client-create' | 'hydrate';
|