@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.
Files changed (155) hide show
  1. package/dist/cjs/contextExtensions.js +74 -0
  2. package/dist/cjs/headRuntime.js +91 -0
  3. package/dist/cjs/helmetContext.js +54 -0
  4. package/dist/cjs/index.js +18 -4
  5. package/dist/cjs/prefetchPolicy.js +52 -0
  6. package/dist/cjs/request-policy/browser.js +139 -0
  7. package/dist/cjs/request-policy/client.js +234 -0
  8. package/dist/cjs/request-policy/node.js +149 -0
  9. package/dist/cjs/request-policy/policyCore.js +266 -0
  10. package/dist/cjs/request-policy/requestContext.js +137 -0
  11. package/dist/cjs/request-policy/traceparent.js +56 -0
  12. package/dist/cjs/request-policy/transport.js +234 -0
  13. package/dist/cjs/request-policy/types.js +57 -0
  14. package/dist/cjs/routeHydration.js +127 -0
  15. package/dist/cjs/routerPrefetchPolicy.js +153 -0
  16. package/dist/cjs/routerProvider.js +170 -0
  17. package/dist/cjs/routerState.js +167 -0
  18. package/dist/cjs/routerStatePlugin.js +86 -0
  19. package/dist/cjs/routerStateTypes.js +18 -0
  20. package/dist/cjs/runtimeContextProjection.js +112 -0
  21. package/dist/cjs/server-config/bffRuntime.js +18 -0
  22. package/dist/cjs/server-config/index.js +18 -0
  23. package/dist/cjs/server-config/serverTelemetry.js +18 -0
  24. package/dist/cjs/shellStream.js +34 -2
  25. package/dist/cjs/workspaceEvents.js +159 -0
  26. package/dist/esm/contextExtensions.mjs +33 -0
  27. package/dist/esm/headRuntime.mjs +43 -0
  28. package/dist/esm/helmetContext.mjs +13 -0
  29. package/dist/esm/index.mjs +2 -0
  30. package/dist/esm/prefetchPolicy.mjs +11 -0
  31. package/dist/esm/request-policy/browser.mjs +11 -0
  32. package/dist/esm/request-policy/client.mjs +195 -0
  33. package/dist/esm/request-policy/policyCore.mjs +171 -0
  34. package/dist/esm/request-policy/requestContext.mjs +75 -0
  35. package/dist/esm/request-policy/traceparent.mjs +18 -0
  36. package/dist/esm/request-policy/transport.mjs +196 -0
  37. package/dist/esm/request-policy/types.mjs +10 -0
  38. package/dist/esm/routeHydration.mjs +77 -0
  39. package/dist/esm/routerPrefetchPolicy.mjs +115 -0
  40. package/dist/esm/routerProvider.mjs +117 -0
  41. package/dist/esm/routerState.mjs +105 -0
  42. package/dist/esm/routerStatePlugin.mjs +48 -0
  43. package/dist/esm/routerStateTypes.mjs +0 -0
  44. package/dist/esm/runtimeContextProjection.mjs +74 -0
  45. package/dist/esm/server-config/bffRuntime.mjs +0 -0
  46. package/dist/esm/server-config/index.mjs +0 -0
  47. package/dist/esm/server-config/serverTelemetry.mjs +0 -0
  48. package/dist/esm/shellStream.mjs +34 -2
  49. package/dist/esm/workspaceEvents.mjs +66 -0
  50. package/dist/esm-node/contextExtensions.mjs +34 -0
  51. package/dist/esm-node/headRuntime.mjs +44 -0
  52. package/dist/esm-node/helmetContext.mjs +14 -0
  53. package/dist/esm-node/index.mjs +2 -0
  54. package/dist/esm-node/prefetchPolicy.mjs +12 -0
  55. package/dist/esm-node/request-policy/browser.mjs +12 -0
  56. package/dist/esm-node/request-policy/client.mjs +196 -0
  57. package/dist/esm-node/request-policy/node.mjs +22 -0
  58. package/dist/esm-node/request-policy/policyCore.mjs +172 -0
  59. package/dist/esm-node/request-policy/requestContext.mjs +76 -0
  60. package/dist/esm-node/request-policy/traceparent.mjs +19 -0
  61. package/dist/esm-node/request-policy/transport.mjs +197 -0
  62. package/dist/esm-node/request-policy/types.mjs +11 -0
  63. package/dist/esm-node/routeHydration.mjs +78 -0
  64. package/dist/esm-node/routerPrefetchPolicy.mjs +116 -0
  65. package/dist/esm-node/routerProvider.mjs +118 -0
  66. package/dist/esm-node/routerState.mjs +106 -0
  67. package/dist/esm-node/routerStatePlugin.mjs +49 -0
  68. package/dist/esm-node/routerStateTypes.mjs +1 -0
  69. package/dist/esm-node/runtimeContextProjection.mjs +75 -0
  70. package/dist/esm-node/server-config/bffRuntime.mjs +1 -0
  71. package/dist/esm-node/server-config/index.mjs +1 -0
  72. package/dist/esm-node/server-config/serverTelemetry.mjs +1 -0
  73. package/dist/esm-node/shellStream.mjs +34 -2
  74. package/dist/esm-node/workspaceEvents.mjs +67 -0
  75. package/dist/types/contextExtensions.d.ts +34 -0
  76. package/dist/types/headRuntime.d.ts +12 -0
  77. package/dist/types/helmetContext.d.ts +10 -0
  78. package/dist/types/index.d.ts +2 -0
  79. package/dist/types/prefetchPolicy.d.ts +4 -0
  80. package/dist/types/request-policy/browser.d.ts +10 -0
  81. package/dist/types/request-policy/client.d.ts +14 -0
  82. package/dist/types/request-policy/node.d.ts +10 -0
  83. package/dist/types/request-policy/policyCore.d.ts +107 -0
  84. package/dist/types/request-policy/requestContext.d.ts +22 -0
  85. package/dist/types/request-policy/traceparent.d.ts +10 -0
  86. package/dist/types/request-policy/transport.d.ts +12 -0
  87. package/dist/types/request-policy/types.d.ts +144 -0
  88. package/dist/types/routeHydration.d.ts +24 -0
  89. package/dist/types/routerPrefetchPolicy.d.ts +28 -0
  90. package/dist/types/routerProvider.d.ts +77 -0
  91. package/dist/types/routerState.d.ts +12 -0
  92. package/dist/types/routerStatePlugin.d.ts +19 -0
  93. package/dist/types/routerStateTypes.d.ts +40 -0
  94. package/dist/types/runtimeContextProjection.d.ts +30 -0
  95. package/dist/types/server-config/bffRuntime.d.ts +122 -0
  96. package/dist/types/server-config/index.d.ts +2 -0
  97. package/dist/types/server-config/serverTelemetry.d.ts +322 -0
  98. package/dist/types/workspaceEvents.d.ts +62 -0
  99. package/package.json +198 -13
  100. package/rslib.config.mts +17 -2
  101. package/rstest.config.mts +33 -2
  102. package/src/contextExtensions.ts +81 -0
  103. package/src/headRuntime.tsx +77 -0
  104. package/src/helmetContext.ts +31 -0
  105. package/src/index.ts +2 -0
  106. package/src/prefetchPolicy.ts +33 -0
  107. package/src/request-policy/browser.ts +22 -0
  108. package/src/request-policy/client.ts +404 -0
  109. package/src/request-policy/node.ts +41 -0
  110. package/src/request-policy/policyCore.ts +424 -0
  111. package/src/request-policy/requestContext.ts +149 -0
  112. package/src/request-policy/traceparent.ts +43 -0
  113. package/src/request-policy/transport.ts +349 -0
  114. package/src/request-policy/types.ts +229 -0
  115. package/src/routeHydration.ts +208 -0
  116. package/src/routerPrefetchPolicy.ts +195 -0
  117. package/src/routerProvider.ts +340 -0
  118. package/src/routerState.ts +208 -0
  119. package/src/routerStatePlugin.ts +86 -0
  120. package/src/routerStateTypes.ts +46 -0
  121. package/src/runtimeContextProjection.ts +105 -0
  122. package/src/server-config/bffRuntime.ts +136 -0
  123. package/src/server-config/index.ts +2 -0
  124. package/src/server-config/serverTelemetry.ts +337 -0
  125. package/src/shellStream.ts +55 -25
  126. package/src/workspaceEvents.ts +324 -0
  127. package/tests/buildIdentity.test.ts +2 -4
  128. package/tests/contextExtensions.test.ts +71 -0
  129. package/tests/headRuntime.test.tsx +369 -0
  130. package/tests/nativeSafeFailureCompatibility.test.ts +117 -0
  131. package/tests/prefetchPolicy.test.ts +51 -0
  132. package/tests/rendererHead.test.ts +1 -72
  133. package/tests/request-policy/browser-policy.test.ts +608 -0
  134. package/tests/request-policy/client-isolation.test.ts +149 -0
  135. package/tests/request-policy/identityBinding.test.ts +75 -0
  136. package/tests/request-policy/node-policy.test.ts +741 -0
  137. package/tests/request-policy/policyCore.test.ts +250 -0
  138. package/tests/request-policy/requestContext.test.ts +80 -0
  139. package/tests/request-policy/requestFactory.outbound-contract.test.ts +691 -0
  140. package/tests/request-policy/traceparent.test.ts +55 -0
  141. package/tests/request-policy/transport-retry.test.ts +370 -0
  142. package/tests/routeHydration.test.ts +180 -0
  143. package/tests/routerPrefetchPolicy.test.ts +102 -0
  144. package/tests/routerProvider.test.ts +38 -0
  145. package/tests/routerState.test.ts +157 -0
  146. package/tests/routerStatePlugin.test.ts +108 -0
  147. package/tests/runtimeContextProjection.test.ts +135 -0
  148. package/tests/shellStream.test.ts +37 -0
  149. package/tests/workspaceEvents.test.ts +391 -0
  150. package/dist/cjs/rscHtmlStream.js +0 -128
  151. package/dist/esm/rscHtmlStream.mjs +0 -86
  152. package/dist/esm-node/rscHtmlStream.mjs +0 -87
  153. package/dist/types/rscHtmlStream.d.ts +0 -4
  154. package/src/rscHtmlStream.ts +0 -142
  155. 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
+ }