@bleedingdev/modern-js-runtime-extensions 3.8.3-ultramodern.6 → 3.9.0-ultramodern.10
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/buildIdentity.js +73 -0
- 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/localisedLoaderIdentity.js +51 -0
- package/dist/cjs/prefetchPolicy.js +52 -0
- package/dist/cjs/reactContext.js +45 -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 +105 -0
- package/dist/cjs/routerState.js +204 -0
- package/dist/cjs/routerStatePlugin.js +86 -0
- package/dist/cjs/routerStateTypes.js +18 -0
- package/dist/cjs/rsc-disabled/rscDisabledClientBrowserRuntime.js +69 -0
- package/dist/cjs/rsc-disabled/rscDisabledClientEdgeRuntime.js +61 -0
- package/dist/cjs/rsc-disabled/rscDisabledClientNodeRuntime.js +65 -0
- package/dist/cjs/rsc-disabled/rscDisabledRuntimeError.js +42 -0
- package/dist/cjs/rsc-disabled/rscDisabledServerEdgeRuntime.js +93 -0
- package/dist/cjs/rsc-disabled/rscDisabledServerNodeRuntime.js +101 -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/buildIdentity.mjs +35 -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/localisedLoaderIdentity.mjs +13 -0
- package/dist/esm/prefetchPolicy.mjs +11 -0
- package/dist/esm/reactContext.mjs +7 -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 +58 -0
- package/dist/esm/routerState.mjs +139 -0
- package/dist/esm/routerStatePlugin.mjs +48 -0
- package/dist/esm/routerStateTypes.mjs +0 -0
- package/dist/esm/rsc-disabled/rscDisabledClientBrowserRuntime.mjs +10 -0
- package/dist/esm/rsc-disabled/rscDisabledClientEdgeRuntime.mjs +8 -0
- package/dist/esm/rsc-disabled/rscDisabledClientNodeRuntime.mjs +9 -0
- package/dist/esm/rsc-disabled/rscDisabledRuntimeError.mjs +4 -0
- package/dist/esm/rsc-disabled/rscDisabledServerEdgeRuntime.mjs +16 -0
- package/dist/esm/rsc-disabled/rscDisabledServerNodeRuntime.mjs +18 -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/buildIdentity.mjs +36 -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/localisedLoaderIdentity.mjs +14 -0
- package/dist/esm-node/prefetchPolicy.mjs +12 -0
- package/dist/esm-node/reactContext.mjs +8 -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 +59 -0
- package/dist/esm-node/routerState.mjs +140 -0
- package/dist/esm-node/routerStatePlugin.mjs +49 -0
- package/dist/esm-node/routerStateTypes.mjs +1 -0
- package/dist/esm-node/rsc-disabled/rscDisabledClientBrowserRuntime.mjs +11 -0
- package/dist/esm-node/rsc-disabled/rscDisabledClientEdgeRuntime.mjs +9 -0
- package/dist/esm-node/rsc-disabled/rscDisabledClientNodeRuntime.mjs +10 -0
- package/dist/esm-node/rsc-disabled/rscDisabledRuntimeError.mjs +5 -0
- package/dist/esm-node/rsc-disabled/rscDisabledServerEdgeRuntime.mjs +17 -0
- package/dist/esm-node/rsc-disabled/rscDisabledServerNodeRuntime.mjs +19 -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/buildIdentity.d.ts +37 -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 +6 -4
- package/dist/types/localisedLoaderIdentity.d.ts +10 -0
- package/dist/types/node.d.ts +1 -1
- package/dist/types/prefetchPolicy.d.ts +4 -0
- package/dist/types/reactContext.d.ts +6 -0
- package/dist/types/rendererHead.d.ts +1 -1
- 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 +55 -0
- package/dist/types/routerState.d.ts +18 -0
- package/dist/types/routerStatePlugin.d.ts +19 -0
- package/dist/types/routerStateTypes.d.ts +40 -0
- package/dist/types/rsc-disabled/rscDisabledClientBrowserRuntime.d.ts +9 -0
- package/dist/types/rsc-disabled/rscDisabledClientEdgeRuntime.d.ts +7 -0
- package/dist/types/rsc-disabled/rscDisabledClientNodeRuntime.d.ts +8 -0
- package/dist/types/rsc-disabled/rscDisabledRuntimeError.d.ts +1 -0
- package/dist/types/rsc-disabled/rscDisabledServerEdgeRuntime.d.ts +15 -0
- package/dist/types/rsc-disabled/rscDisabledServerNodeRuntime.d.ts +17 -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 +240 -15
- package/rslib.config.mts +17 -2
- package/rstest.config.mts +33 -2
- package/src/buildIdentity.ts +61 -0
- 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/localisedLoaderIdentity.ts +30 -0
- package/src/prefetchPolicy.ts +33 -0
- package/src/reactContext.ts +16 -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 +160 -0
- package/src/routerState.ts +290 -0
- package/src/routerStatePlugin.ts +86 -0
- package/src/routerStateTypes.ts +46 -0
- package/src/rsc-disabled/rscDisabledClientBrowserRuntime.ts +10 -0
- package/src/rsc-disabled/rscDisabledClientEdgeRuntime.ts +8 -0
- package/src/rsc-disabled/rscDisabledClientNodeRuntime.ts +9 -0
- package/src/rsc-disabled/rscDisabledRuntimeError.ts +3 -0
- package/src/rsc-disabled/rscDisabledServerEdgeRuntime.ts +16 -0
- package/src/rsc-disabled/rscDisabledServerNodeRuntime.ts +18 -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 +51 -0
- package/tests/contextExtensions.test.ts +27 -0
- package/tests/headRuntime.test.tsx +229 -0
- package/tests/rendererHead.test.ts +1 -113
- package/tests/request-policy/browser-policy.test.ts +108 -0
- package/tests/request-policy/client-isolation.test.ts +109 -0
- package/tests/request-policy/node-policy.test.ts +447 -0
- package/tests/request-policy/policyCore.test.ts +129 -0
- package/tests/request-policy/requestContext.test.ts +66 -0
- package/tests/request-policy/requestFactory.outbound-contract.test.ts +290 -0
- package/tests/request-policy/traceparent.test.ts +52 -0
- package/tests/request-policy/transport-retry.test.ts +367 -0
- package/tests/routeHydration.test.ts +180 -0
- package/tests/routerPrefetchPolicy.test.ts +102 -0
- package/tests/routerProvider.test.ts +29 -0
- package/tests/routerState.test.ts +78 -0
- package/tests/routerStatePlugin.test.ts +63 -0
- package/tests/runtimeContextProjection.test.ts +83 -0
- package/tests/safeFailure.test.ts +10 -28
- package/tests/shellStream.test.ts +37 -0
- package/tests/workspaceEvents.test.ts +221 -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,208 @@
|
|
|
1
|
+
import { escapeHtmlAttribute } from './html';
|
|
2
|
+
|
|
3
|
+
type RouteAssetManifest = {
|
|
4
|
+
assets?: string[];
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type RouteManifestLike = {
|
|
8
|
+
routeAssets?: Record<string, RouteAssetManifest | undefined>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type ScriptTagMatch = {
|
|
12
|
+
index: number;
|
|
13
|
+
tag: string;
|
|
14
|
+
src: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ScriptChunkLike = {
|
|
18
|
+
filename?: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function getScriptTags(template: string): ScriptTagMatch[] {
|
|
23
|
+
const scriptRegExp = /<script\b[^>]*\bsrc=(["'])(.*?)\1[^>]*><\/script>/g;
|
|
24
|
+
return Array.from(template.matchAll(scriptRegExp)).map(match => ({
|
|
25
|
+
index: match.index ?? 0,
|
|
26
|
+
tag: match[0],
|
|
27
|
+
src: match[2],
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getAssetBasename(src: string) {
|
|
32
|
+
const withoutQuery = src.split(/[?#]/)[0];
|
|
33
|
+
return withoutQuery.split('/').pop() || withoutQuery;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isEntryScript(src: string, entryName: string, asyncEntry: boolean) {
|
|
37
|
+
const basename = getAssetBasename(src);
|
|
38
|
+
const prefix = asyncEntry ? `async-${entryName}` : entryName;
|
|
39
|
+
return (
|
|
40
|
+
basename === `${prefix}.js` ||
|
|
41
|
+
basename.startsWith(`${prefix}.`) ||
|
|
42
|
+
basename.startsWith(`${prefix}-`)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const dedupeByUrl = <T extends ScriptChunkLike>(chunks: T[]) => {
|
|
47
|
+
const seen = new Set<string>();
|
|
48
|
+
return chunks.filter(chunk => {
|
|
49
|
+
if (chunk.url === undefined || chunk.url === '' || seen.has(chunk.url)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
seen.add(chunk.url);
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const isAsyncEntryScriptChunk = (chunk: ScriptChunkLike, entryName: string) => {
|
|
58
|
+
if (chunk.url === undefined || !chunk.url.endsWith('.js')) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const asyncEntryName = `async-${entryName}`;
|
|
63
|
+
const filename = chunk.filename ?? chunk.url;
|
|
64
|
+
const basename = filename.split('/').pop() ?? filename;
|
|
65
|
+
return (
|
|
66
|
+
basename === `${asyncEntryName}.js` ||
|
|
67
|
+
basename.startsWith(`${asyncEntryName}.`) ||
|
|
68
|
+
basename.startsWith(`${asyncEntryName}-`)
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function getMatchedRouteChunks<T>(
|
|
73
|
+
routeManifest: RouteManifestLike | undefined,
|
|
74
|
+
matchedRouteIds: string[],
|
|
75
|
+
routeAssetToChunk: (asset: string) => T,
|
|
76
|
+
): T[] {
|
|
77
|
+
const routeAssets = routeManifest?.routeAssets;
|
|
78
|
+
if (routeAssets === undefined) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return matchedRouteIds
|
|
83
|
+
.flatMap(routeId => routeAssets[routeId]?.assets ?? [])
|
|
84
|
+
.map(routeAssetToChunk);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const orderHydrationScriptChunks = <T extends ScriptChunkLike>({
|
|
88
|
+
asyncEntryChunks,
|
|
89
|
+
collectedChunks,
|
|
90
|
+
matchedRouteChunks,
|
|
91
|
+
entryName,
|
|
92
|
+
}: {
|
|
93
|
+
asyncEntryChunks: T[];
|
|
94
|
+
collectedChunks: T[];
|
|
95
|
+
matchedRouteChunks: T[];
|
|
96
|
+
entryName: string;
|
|
97
|
+
}) => {
|
|
98
|
+
const asyncEntryScriptChunks: T[] = [];
|
|
99
|
+
const asyncEntryDependencyChunks: T[] = [];
|
|
100
|
+
|
|
101
|
+
for (const chunk of asyncEntryChunks) {
|
|
102
|
+
if (isAsyncEntryScriptChunk(chunk, entryName)) {
|
|
103
|
+
asyncEntryScriptChunks.push(chunk);
|
|
104
|
+
} else {
|
|
105
|
+
asyncEntryDependencyChunks.push(chunk);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return dedupeByUrl([
|
|
110
|
+
...asyncEntryDependencyChunks,
|
|
111
|
+
...collectedChunks,
|
|
112
|
+
...matchedRouteChunks,
|
|
113
|
+
...asyncEntryScriptChunks,
|
|
114
|
+
]);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export function injectBeforeHydrationEntryScript(
|
|
118
|
+
template: string,
|
|
119
|
+
scripts: string,
|
|
120
|
+
entryName = 'index',
|
|
121
|
+
) {
|
|
122
|
+
if (scripts === '') {
|
|
123
|
+
return template;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const scriptTags = getScriptTags(template);
|
|
127
|
+
const target =
|
|
128
|
+
scriptTags.find(match => isEntryScript(match.src, entryName, false)) ??
|
|
129
|
+
scriptTags.find(match => isEntryScript(match.src, entryName, true));
|
|
130
|
+
|
|
131
|
+
if (target === undefined) {
|
|
132
|
+
return template;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return `${template.slice(0, target.index)}${scripts}${template.slice(
|
|
136
|
+
target.index,
|
|
137
|
+
)}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function replaceChunkJsPlaceholder(
|
|
141
|
+
template: string,
|
|
142
|
+
scripts: string,
|
|
143
|
+
entryName: string | undefined,
|
|
144
|
+
placeholder: string,
|
|
145
|
+
) {
|
|
146
|
+
if (!template.includes(placeholder)) {
|
|
147
|
+
return template;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (scripts === '') {
|
|
151
|
+
return template.replace(placeholder, () => '');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const withoutPlaceholder = template.replace(placeholder, () => '');
|
|
155
|
+
const withEarlyScripts = injectBeforeHydrationEntryScript(
|
|
156
|
+
withoutPlaceholder,
|
|
157
|
+
scripts,
|
|
158
|
+
entryName,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (withEarlyScripts !== withoutPlaceholder) {
|
|
162
|
+
return withEarlyScripts;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return template.replace(placeholder, () => scripts);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function createRouteHydrationScriptTags(
|
|
169
|
+
routeManifest: RouteManifestLike | undefined,
|
|
170
|
+
matchedRouteIds: string[],
|
|
171
|
+
entryName: string,
|
|
172
|
+
options: {
|
|
173
|
+
nonce?: string;
|
|
174
|
+
template?: string;
|
|
175
|
+
} = {},
|
|
176
|
+
) {
|
|
177
|
+
const { nonce, template } = options;
|
|
178
|
+
const routeAssets = routeManifest?.routeAssets;
|
|
179
|
+
if (routeAssets === undefined) {
|
|
180
|
+
return '';
|
|
181
|
+
}
|
|
182
|
+
const existingScriptSrcs =
|
|
183
|
+
template === undefined
|
|
184
|
+
? new Set<string>()
|
|
185
|
+
: new Set(getScriptTags(template).map(({ src }) => src));
|
|
186
|
+
|
|
187
|
+
const jsAssets = Array.from(
|
|
188
|
+
new Set(
|
|
189
|
+
[
|
|
190
|
+
...getMatchedRouteChunks(
|
|
191
|
+
routeManifest,
|
|
192
|
+
matchedRouteIds,
|
|
193
|
+
asset => asset,
|
|
194
|
+
),
|
|
195
|
+
...(routeAssets[`async-${entryName}`]?.assets ?? []),
|
|
196
|
+
].filter((asset: string) => asset.endsWith('.js')),
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
return jsAssets
|
|
200
|
+
.filter(asset => !existingScriptSrcs.has(asset))
|
|
201
|
+
.map(
|
|
202
|
+
asset =>
|
|
203
|
+
`<script src="${escapeHtmlAttribute(asset)}"${
|
|
204
|
+
nonce ? ` nonce="${escapeHtmlAttribute(nonce)}"` : ''
|
|
205
|
+
}></script>`,
|
|
206
|
+
)
|
|
207
|
+
.join(' ');
|
|
208
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { getNavigationWarmupCacheKey } from './navigationWarmup';
|
|
2
|
+
import { normalizePreloadBehavior } from './prefetchPolicy';
|
|
3
|
+
|
|
4
|
+
type PrefetchBehavior = 'intent' | 'render' | 'viewport' | 'none';
|
|
5
|
+
type PrefetchState = { code: boolean; data: boolean };
|
|
6
|
+
|
|
7
|
+
type NetworkInformationLike = {
|
|
8
|
+
saveData?: boolean;
|
|
9
|
+
effectiveType?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type WarmupTask = {
|
|
13
|
+
key: string;
|
|
14
|
+
run: () => Promise<unknown>;
|
|
15
|
+
cancelled: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const INTENT_DELAY = 100;
|
|
19
|
+
const VIEWPORT_ROOT_MARGIN = '200px';
|
|
20
|
+
const MAX_CONCURRENT_WARMUPS = 4;
|
|
21
|
+
const WARMUP_TTL = 30_000;
|
|
22
|
+
|
|
23
|
+
/** Policy-only contract: native links supply elements, route inputs and loads. */
|
|
24
|
+
export function createRouterPrefetchPolicy() {
|
|
25
|
+
const cache = new Map<string, number>();
|
|
26
|
+
const queue: WarmupTask[] = [];
|
|
27
|
+
let activeWarmups = 0;
|
|
28
|
+
|
|
29
|
+
const canWarmup = () => {
|
|
30
|
+
const navigator = globalThis.navigator as
|
|
31
|
+
| (Navigator & {
|
|
32
|
+
connection?: NetworkInformationLike;
|
|
33
|
+
mozConnection?: NetworkInformationLike;
|
|
34
|
+
webkitConnection?: NetworkInformationLike;
|
|
35
|
+
})
|
|
36
|
+
| undefined;
|
|
37
|
+
const connection =
|
|
38
|
+
navigator?.connection ||
|
|
39
|
+
navigator?.mozConnection ||
|
|
40
|
+
navigator?.webkitConnection;
|
|
41
|
+
return (
|
|
42
|
+
!connection?.saveData &&
|
|
43
|
+
connection?.effectiveType !== 'slow-2g' &&
|
|
44
|
+
connection?.effectiveType !== '2g'
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const runNext = () => {
|
|
49
|
+
while (activeWarmups < MAX_CONCURRENT_WARMUPS && queue.length > 0) {
|
|
50
|
+
const task = queue.shift()!;
|
|
51
|
+
if (task.cancelled) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
activeWarmups += 1;
|
|
55
|
+
task
|
|
56
|
+
.run()
|
|
57
|
+
.catch(() => {
|
|
58
|
+
cache.delete(task.key);
|
|
59
|
+
})
|
|
60
|
+
.finally(() => {
|
|
61
|
+
activeWarmups -= 1;
|
|
62
|
+
runNext();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
canWarmup,
|
|
69
|
+
allowData(route: unknown) {
|
|
70
|
+
return (
|
|
71
|
+
(route as { handle?: { navigationWarmup?: { data?: boolean } } })
|
|
72
|
+
?.handle?.navigationWarmup?.data !== false
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
observe({
|
|
76
|
+
element,
|
|
77
|
+
prefetch: inputPrefetch,
|
|
78
|
+
preload: inputPreload,
|
|
79
|
+
notify,
|
|
80
|
+
}: {
|
|
81
|
+
element: HTMLAnchorElement | null;
|
|
82
|
+
prefetch?: PrefetchBehavior;
|
|
83
|
+
preload?: PrefetchBehavior | false;
|
|
84
|
+
notify: (state: PrefetchState) => void;
|
|
85
|
+
}) {
|
|
86
|
+
const prefetch = inputPrefetch ?? 'render';
|
|
87
|
+
const preload = normalizePreloadBehavior(inputPreload, prefetch);
|
|
88
|
+
let state: PrefetchState = {
|
|
89
|
+
code: prefetch === 'render' || preload === 'render',
|
|
90
|
+
data: prefetch === 'render',
|
|
91
|
+
};
|
|
92
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
93
|
+
let observer: IntersectionObserver | undefined;
|
|
94
|
+
const update = (next: PrefetchState) => {
|
|
95
|
+
state = next;
|
|
96
|
+
notify(state);
|
|
97
|
+
};
|
|
98
|
+
const cancelTimer = () => {
|
|
99
|
+
if (timer !== undefined) {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
timer = undefined;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
notify(state);
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
element &&
|
|
108
|
+
(prefetch === 'viewport' || preload === 'viewport') &&
|
|
109
|
+
typeof IntersectionObserver !== 'undefined'
|
|
110
|
+
) {
|
|
111
|
+
observer = new IntersectionObserver(
|
|
112
|
+
entries => {
|
|
113
|
+
if (!entries.some(entry => entry.isIntersecting)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
update({
|
|
117
|
+
code: true,
|
|
118
|
+
data: state.data || prefetch === 'viewport',
|
|
119
|
+
});
|
|
120
|
+
observer?.disconnect();
|
|
121
|
+
},
|
|
122
|
+
{ rootMargin: VIEWPORT_ROOT_MARGIN },
|
|
123
|
+
);
|
|
124
|
+
observer.observe(element);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
onIntent() {
|
|
129
|
+
if (prefetch !== 'intent' && preload !== 'intent') {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (timer !== undefined) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
timer = setTimeout(() => {
|
|
136
|
+
timer = undefined;
|
|
137
|
+
update({ code: true, data: state.data || prefetch === 'intent' });
|
|
138
|
+
}, INTENT_DELAY);
|
|
139
|
+
},
|
|
140
|
+
onCancel() {
|
|
141
|
+
if (prefetch === 'intent' || preload === 'intent') {
|
|
142
|
+
cancelTimer();
|
|
143
|
+
update({ code: false, data: false });
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
dispose() {
|
|
147
|
+
cancelTimer();
|
|
148
|
+
observer?.disconnect();
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
schedule({
|
|
153
|
+
runtimeContext,
|
|
154
|
+
chunkLoader,
|
|
155
|
+
publicPath,
|
|
156
|
+
key: operationKey,
|
|
157
|
+
run,
|
|
158
|
+
}: {
|
|
159
|
+
runtimeContext: object;
|
|
160
|
+
chunkLoader: object;
|
|
161
|
+
publicPath: string;
|
|
162
|
+
key: string;
|
|
163
|
+
run: () => Promise<unknown>;
|
|
164
|
+
}) {
|
|
165
|
+
if (!canWarmup()) {
|
|
166
|
+
return () => {};
|
|
167
|
+
}
|
|
168
|
+
const now = performance.now();
|
|
169
|
+
for (const [key, timestamp] of cache) {
|
|
170
|
+
if (now - timestamp > WARMUP_TTL) {
|
|
171
|
+
cache.delete(key);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const key = getNavigationWarmupCacheKey(
|
|
175
|
+
runtimeContext,
|
|
176
|
+
chunkLoader,
|
|
177
|
+
publicPath,
|
|
178
|
+
operationKey,
|
|
179
|
+
);
|
|
180
|
+
if (cache.has(key)) {
|
|
181
|
+
return () => {};
|
|
182
|
+
}
|
|
183
|
+
cache.set(key, now);
|
|
184
|
+
const task: WarmupTask = { key, run, cancelled: false };
|
|
185
|
+
queue.push(task);
|
|
186
|
+
runNext();
|
|
187
|
+
return () => {
|
|
188
|
+
task.cancelled = true;
|
|
189
|
+
if (queue.includes(task)) {
|
|
190
|
+
cache.delete(key);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
export function resolveRouterProvider(
|
|
101
|
+
framework: string | undefined,
|
|
102
|
+
options: { realm: RouterProviderRealm },
|
|
103
|
+
): RouterProviderFactory {
|
|
104
|
+
const name = framework || options.realm.defaultProvider;
|
|
105
|
+
const realmProviderNames = options.realm.names();
|
|
106
|
+
|
|
107
|
+
if (name === undefined) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`[@modern-js/runtime] The app-owned router provider realm does not declare a default provider. Available realm providers: ${
|
|
110
|
+
realmProviderNames.join(', ') || '(none)'
|
|
111
|
+
}.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const factory = options.realm.get(name);
|
|
116
|
+
if (factory === undefined) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`[@modern-js/runtime] Router provider "${name}" is not registered in the app-owned router provider realm. ` +
|
|
119
|
+
`Available realm providers: ${
|
|
120
|
+
realmProviderNames.join(', ') || '(none)'
|
|
121
|
+
}.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return factory;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Compose app-owned providers without importing the native runtime graph. */
|
|
129
|
+
export function createRouterPlugin<Hooks extends Record<string, unknown>>({
|
|
130
|
+
defaultProvider,
|
|
131
|
+
registryHooks,
|
|
132
|
+
localProviders = [],
|
|
133
|
+
}: {
|
|
134
|
+
defaultProvider: { name: string; factory: RouterProviderFactory };
|
|
135
|
+
registryHooks: Hooks;
|
|
136
|
+
localProviders?: readonly Omit<RouterProviderRegistration, 'isDefault'>[];
|
|
137
|
+
}) {
|
|
138
|
+
const realm = createRouterProviderRealm([
|
|
139
|
+
{ ...defaultProvider, isDefault: true },
|
|
140
|
+
...localProviders,
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
return (userConfig: Record<string, any> = {}) => ({
|
|
144
|
+
name: '@modern-js/plugin-router',
|
|
145
|
+
registryHooks,
|
|
146
|
+
setup(api: {
|
|
147
|
+
getRuntimeConfig: () => { router?: Record<string, unknown> };
|
|
148
|
+
[key: string]: any;
|
|
149
|
+
}) {
|
|
150
|
+
const mergedConfig = merge(
|
|
151
|
+
api.getRuntimeConfig().router || {},
|
|
152
|
+
userConfig,
|
|
153
|
+
);
|
|
154
|
+
const factory = resolveRouterProvider(mergedConfig.framework, { realm });
|
|
155
|
+
const provider = factory(userConfig);
|
|
156
|
+
reportUnsupportedProviderRegistryHooks(provider, registryHooks);
|
|
157
|
+
provider.setup?.(api);
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|