@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.4 → 3.9.0-ultramodern.6

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 +105 -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 +58 -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 +59 -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 +55 -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 +160 -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 +29 -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,324 @@
1
+ export type UltramodernPerformanceReadinessSignalId =
2
+ | 'bfcache'
3
+ | 'core-web-vitals-rum'
4
+ | 'duplicate-prefetch-warmup'
5
+ | 'cache-policy-sanity'
6
+ | 'save-data-behavior'
7
+ | 'cloudflare-ssr-cache-hints';
8
+
9
+ export type UltramodernWorkspaceLocale = 'en' | 'cs';
10
+
11
+ export type UltramodernPerformanceReadinessSignalStatus =
12
+ | 'pass'
13
+ | 'warn'
14
+ | 'fail';
15
+
16
+ export const ultramodernWorkspaceEventNames = {
17
+ navigate: 'ultramodern:navigate',
18
+ performanceSignal: 'ultramodern:performance-signal',
19
+ remoteReady: 'ultramodern:remote-ready',
20
+ routeSettled: 'ultramodern:route-settled',
21
+ } as const;
22
+
23
+ export type UltramodernWorkspaceEventName =
24
+ (typeof ultramodernWorkspaceEventNames)[keyof typeof ultramodernWorkspaceEventNames];
25
+
26
+ export interface UltramodernNavigatePayload {
27
+ to: string;
28
+ replace?: boolean;
29
+ state?: Record<string, unknown>;
30
+ }
31
+
32
+ export interface UltramodernRouteSettledPayload {
33
+ pathname: string;
34
+ locale?: UltramodernWorkspaceLocale;
35
+ title?: string;
36
+ }
37
+
38
+ export interface UltramodernRemoteReadyPayload {
39
+ appId: string;
40
+ build?: string;
41
+ surface?: string;
42
+ version?: string;
43
+ }
44
+
45
+ export interface UltramodernPerformanceSignalPayload {
46
+ signalId: UltramodernPerformanceReadinessSignalId;
47
+ status: UltramodernPerformanceReadinessSignalStatus;
48
+ durationMs?: number;
49
+ detail?: Record<string, unknown>;
50
+ }
51
+
52
+ export interface UltramodernWorkspaceEventPayloadMap {
53
+ 'ultramodern:navigate': UltramodernNavigatePayload;
54
+ 'ultramodern:performance-signal': UltramodernPerformanceSignalPayload;
55
+ 'ultramodern:remote-ready': UltramodernRemoteReadyPayload;
56
+ 'ultramodern:route-settled': UltramodernRouteSettledPayload;
57
+ }
58
+
59
+ export class UltramodernWorkspaceEventValidationError {
60
+ readonly message: string;
61
+ readonly name = 'UltramodernWorkspaceEventValidationError';
62
+ readonly eventName: UltramodernWorkspaceEventName;
63
+ readonly payload: unknown;
64
+
65
+ constructor(eventName: UltramodernWorkspaceEventName, payload: unknown) {
66
+ this.message = `Invalid payload for UltraModern workspace event "${eventName}"`;
67
+ this.eventName = eventName;
68
+ this.payload = payload;
69
+ }
70
+ }
71
+
72
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
73
+ typeof value === 'object' && value !== null && !Array.isArray(value);
74
+
75
+ const isNonEmptyString = (value: unknown): value is string =>
76
+ typeof value === 'string' && value.trim().length > 0;
77
+
78
+ const isNonNegativeNumber = (value: unknown): value is number =>
79
+ typeof value === 'number' && Number.isFinite(value) && value >= 0;
80
+
81
+ const isUltramodernWorkspaceLocale = (
82
+ value: unknown,
83
+ ): value is UltramodernWorkspaceLocale => value === 'en' || value === 'cs';
84
+
85
+ const isPerformanceReadinessSignalId = (
86
+ value: unknown,
87
+ ): value is UltramodernPerformanceReadinessSignalId =>
88
+ value === 'bfcache' ||
89
+ value === 'core-web-vitals-rum' ||
90
+ value === 'duplicate-prefetch-warmup' ||
91
+ value === 'cache-policy-sanity' ||
92
+ value === 'save-data-behavior' ||
93
+ value === 'cloudflare-ssr-cache-hints';
94
+
95
+ const isPerformanceReadinessSignalStatus = (
96
+ value: unknown,
97
+ ): value is UltramodernPerformanceReadinessSignalStatus =>
98
+ value === 'pass' || value === 'warn' || value === 'fail';
99
+
100
+ const hasOptionalString = (value: Record<string, unknown>, key: string) =>
101
+ value[key] === undefined || isNonEmptyString(value[key]);
102
+
103
+ const hasOptionalBoolean = (value: Record<string, unknown>, key: string) =>
104
+ value[key] === undefined || typeof value[key] === 'boolean';
105
+
106
+ const hasOptionalRecord = (value: Record<string, unknown>, key: string) =>
107
+ value[key] === undefined || isRecord(value[key]);
108
+
109
+ const hasOptionalNonNegativeNumber = (
110
+ value: Record<string, unknown>,
111
+ key: string,
112
+ ) => value[key] === undefined || isNonNegativeNumber(value[key]);
113
+
114
+ const hasOptionalLocale = (value: Record<string, unknown>, key: string) =>
115
+ value[key] === undefined || isUltramodernWorkspaceLocale(value[key]);
116
+
117
+ export const isUltramodernNavigatePayload = (
118
+ payload: unknown,
119
+ ): payload is UltramodernNavigatePayload =>
120
+ isRecord(payload) &&
121
+ isNonEmptyString(payload.to) &&
122
+ hasOptionalBoolean(payload, 'replace') &&
123
+ hasOptionalRecord(payload, 'state');
124
+
125
+ export const isUltramodernRouteSettledPayload = (
126
+ payload: unknown,
127
+ ): payload is UltramodernRouteSettledPayload =>
128
+ isRecord(payload) &&
129
+ isNonEmptyString(payload.pathname) &&
130
+ hasOptionalLocale(payload, 'locale') &&
131
+ hasOptionalString(payload, 'title');
132
+
133
+ export const isUltramodernRemoteReadyPayload = (
134
+ payload: unknown,
135
+ ): payload is UltramodernRemoteReadyPayload =>
136
+ isRecord(payload) &&
137
+ isNonEmptyString(payload.appId) &&
138
+ hasOptionalString(payload, 'build') &&
139
+ hasOptionalString(payload, 'surface') &&
140
+ hasOptionalString(payload, 'version');
141
+
142
+ export const isUltramodernPerformanceSignalPayload = (
143
+ payload: unknown,
144
+ ): payload is UltramodernPerformanceSignalPayload =>
145
+ isRecord(payload) &&
146
+ isPerformanceReadinessSignalId(payload.signalId) &&
147
+ isPerformanceReadinessSignalStatus(payload.status) &&
148
+ hasOptionalNonNegativeNumber(payload, 'durationMs') &&
149
+ hasOptionalRecord(payload, 'detail');
150
+
151
+ const ultramodernWorkspaceEventValidators: {
152
+ [Name in UltramodernWorkspaceEventName]: (
153
+ payload: unknown,
154
+ ) => payload is UltramodernWorkspaceEventPayloadMap[Name];
155
+ } = {
156
+ [ultramodernWorkspaceEventNames.navigate]: isUltramodernNavigatePayload,
157
+ [ultramodernWorkspaceEventNames.performanceSignal]:
158
+ isUltramodernPerformanceSignalPayload,
159
+ [ultramodernWorkspaceEventNames.remoteReady]: isUltramodernRemoteReadyPayload,
160
+ [ultramodernWorkspaceEventNames.routeSettled]:
161
+ isUltramodernRouteSettledPayload,
162
+ };
163
+
164
+ export const isUltramodernWorkspaceEventPayload = <
165
+ Name extends UltramodernWorkspaceEventName,
166
+ >(
167
+ eventName: Name,
168
+ payload: unknown,
169
+ ): payload is UltramodernWorkspaceEventPayloadMap[Name] =>
170
+ ultramodernWorkspaceEventValidators[eventName](payload);
171
+
172
+ export const assertUltramodernWorkspaceEventPayload = <
173
+ Name extends UltramodernWorkspaceEventName,
174
+ >(
175
+ eventName: Name,
176
+ payload: unknown,
177
+ ): UltramodernWorkspaceEventPayloadMap[Name] => {
178
+ if (!isUltramodernWorkspaceEventPayload(eventName, payload)) {
179
+ throw new UltramodernWorkspaceEventValidationError(eventName, payload);
180
+ }
181
+
182
+ return payload;
183
+ };
184
+
185
+ export const createUltramodernWorkspaceEvent = <
186
+ Name extends UltramodernWorkspaceEventName,
187
+ >(
188
+ eventName: Name,
189
+ payload: UltramodernWorkspaceEventPayloadMap[Name],
190
+ ): CustomEvent<UltramodernWorkspaceEventPayloadMap[Name]> =>
191
+ new CustomEvent(eventName, {
192
+ bubbles: true,
193
+ composed: true,
194
+ detail: assertUltramodernWorkspaceEventPayload(eventName, payload),
195
+ });
196
+
197
+ export const dispatchUltramodernWorkspaceEvent = <
198
+ Name extends UltramodernWorkspaceEventName,
199
+ >(
200
+ target: EventTarget,
201
+ eventName: Name,
202
+ payload: UltramodernWorkspaceEventPayloadMap[Name],
203
+ ) => target.dispatchEvent(createUltramodernWorkspaceEvent(eventName, payload));
204
+
205
+ export const onUltramodernWorkspaceEvent = <
206
+ Name extends UltramodernWorkspaceEventName,
207
+ >(
208
+ target: EventTarget,
209
+ eventName: Name,
210
+ handler: (
211
+ payload: UltramodernWorkspaceEventPayloadMap[Name],
212
+ event: CustomEvent<UltramodernWorkspaceEventPayloadMap[Name]>,
213
+ ) => void,
214
+ ) => {
215
+ const listener = (event: Event) => {
216
+ if (!('detail' in event)) {
217
+ throw new UltramodernWorkspaceEventValidationError(eventName, undefined);
218
+ }
219
+
220
+ const customEvent = event as CustomEvent<unknown>;
221
+ handler(
222
+ assertUltramodernWorkspaceEventPayload(eventName, customEvent.detail),
223
+ customEvent as CustomEvent<UltramodernWorkspaceEventPayloadMap[Name]>,
224
+ );
225
+ };
226
+
227
+ target.addEventListener(eventName, listener);
228
+
229
+ return () => {
230
+ target.removeEventListener(eventName, listener);
231
+ };
232
+ };
233
+
234
+ export const dispatchUltramodernNavigate = (
235
+ target: EventTarget,
236
+ payload: UltramodernNavigatePayload,
237
+ ) =>
238
+ dispatchUltramodernWorkspaceEvent(
239
+ target,
240
+ ultramodernWorkspaceEventNames.navigate,
241
+ payload,
242
+ );
243
+
244
+ export const dispatchUltramodernRouteSettled = (
245
+ target: EventTarget,
246
+ payload: UltramodernRouteSettledPayload,
247
+ ) =>
248
+ dispatchUltramodernWorkspaceEvent(
249
+ target,
250
+ ultramodernWorkspaceEventNames.routeSettled,
251
+ payload,
252
+ );
253
+
254
+ export const dispatchUltramodernRemoteReady = (
255
+ target: EventTarget,
256
+ payload: UltramodernRemoteReadyPayload,
257
+ ) =>
258
+ dispatchUltramodernWorkspaceEvent(
259
+ target,
260
+ ultramodernWorkspaceEventNames.remoteReady,
261
+ payload,
262
+ );
263
+
264
+ export const dispatchUltramodernPerformanceSignal = (
265
+ target: EventTarget,
266
+ payload: UltramodernPerformanceSignalPayload,
267
+ ) =>
268
+ dispatchUltramodernWorkspaceEvent(
269
+ target,
270
+ ultramodernWorkspaceEventNames.performanceSignal,
271
+ payload,
272
+ );
273
+
274
+ export const onUltramodernNavigate = (
275
+ target: EventTarget,
276
+ handler: (
277
+ payload: UltramodernNavigatePayload,
278
+ event: CustomEvent<UltramodernNavigatePayload>,
279
+ ) => void,
280
+ ) =>
281
+ onUltramodernWorkspaceEvent(
282
+ target,
283
+ ultramodernWorkspaceEventNames.navigate,
284
+ handler,
285
+ );
286
+
287
+ export const onUltramodernRouteSettled = (
288
+ target: EventTarget,
289
+ handler: (
290
+ payload: UltramodernRouteSettledPayload,
291
+ event: CustomEvent<UltramodernRouteSettledPayload>,
292
+ ) => void,
293
+ ) =>
294
+ onUltramodernWorkspaceEvent(
295
+ target,
296
+ ultramodernWorkspaceEventNames.routeSettled,
297
+ handler,
298
+ );
299
+
300
+ export const onUltramodernRemoteReady = (
301
+ target: EventTarget,
302
+ handler: (
303
+ payload: UltramodernRemoteReadyPayload,
304
+ event: CustomEvent<UltramodernRemoteReadyPayload>,
305
+ ) => void,
306
+ ) =>
307
+ onUltramodernWorkspaceEvent(
308
+ target,
309
+ ultramodernWorkspaceEventNames.remoteReady,
310
+ handler,
311
+ );
312
+
313
+ export const onUltramodernPerformanceSignal = (
314
+ target: EventTarget,
315
+ handler: (
316
+ payload: UltramodernPerformanceSignalPayload,
317
+ event: CustomEvent<UltramodernPerformanceSignalPayload>,
318
+ ) => void,
319
+ ) =>
320
+ onUltramodernWorkspaceEvent(
321
+ target,
322
+ ultramodernWorkspaceEventNames.performanceSignal,
323
+ handler,
324
+ );
@@ -28,10 +28,6 @@ const readers = {
28
28
  sourceRevision: () => 'compiled-revision',
29
29
  };
30
30
 
31
- it('retains generation identity when the module is evaluated without a compiler', () => {
32
- expect(resolveUltramodernBuildArtifact(artifact)).toEqual(artifact);
33
- });
34
-
35
31
  it('applies injected identity to every surface without mutating the artifact', () => {
36
32
  const resolved = resolveUltramodernBuildArtifact(artifact, readers);
37
33
  for (const record of [
@@ -52,6 +48,8 @@ it('applies injected identity to every surface without mutating the artifact', (
52
48
  });
53
49
 
54
50
  it('falls back only for missing compiler references and preserves each supplied value', () => {
51
+ expect(resolveUltramodernBuildArtifact(artifact)).toEqual(artifact);
52
+
55
53
  const result = resolveUltramodernBuildArtifact(artifact, {
56
54
  buildMarker: readers.buildMarker,
57
55
  sourceRevision: () => {
@@ -0,0 +1,71 @@
1
+ import { createRuntimeContextExtension } from '../src/contextExtensions';
2
+
3
+ describe('runtime context extensions', () => {
4
+ it('stores and retrieves typed values per context object', () => {
5
+ const extension = createRuntimeContextExtension<{ value: number }>(
6
+ 'test:isolation-a',
7
+ );
8
+ const contextA = {};
9
+ const contextB = {};
10
+
11
+ extension.set(contextA, { value: 1 });
12
+ extension.set(contextB, { value: 2 });
13
+
14
+ expect(extension.get(contextA)).toEqual({ value: 1 });
15
+ expect(extension.get(contextB)).toEqual({ value: 2 });
16
+
17
+ extension.remove(contextA);
18
+ expect(extension.get(contextA)).toBeUndefined();
19
+ expect(extension.get(contextB)).toEqual({ value: 2 });
20
+ });
21
+
22
+ it('isolates extensions with different ids on the same context', () => {
23
+ const first = createRuntimeContextExtension<string>('test:first');
24
+ const second = createRuntimeContextExtension<string>('test:second');
25
+ const context = {};
26
+
27
+ first.set(context, 'one');
28
+ second.set(context, 'two');
29
+
30
+ expect(first.get(context)).toBe('one');
31
+ expect(second.get(context)).toBe('two');
32
+ });
33
+
34
+ it('shares state between extensions created with the same id', () => {
35
+ const a = createRuntimeContextExtension<string>('test:shared');
36
+ const b = createRuntimeContextExtension<string>('test:shared');
37
+ const context = {};
38
+
39
+ a.set(context, 'value');
40
+ expect(b.get(context)).toBe('value');
41
+ });
42
+
43
+ it('returns undefined for contexts without any extensions', () => {
44
+ const extension = createRuntimeContextExtension<string>('test:empty');
45
+ expect(extension.get({})).toBeUndefined();
46
+ });
47
+
48
+ it('does not leak into string-key enumeration of the context', () => {
49
+ const extension = createRuntimeContextExtension<string>('test:hidden');
50
+ const context = {};
51
+ const keysBefore = Object.keys(context);
52
+
53
+ extension.set(context, 'secret');
54
+
55
+ expect(Object.keys(context)).toEqual(keysBefore);
56
+ expect(JSON.stringify(context)).not.toContain('secret');
57
+ for (const key in context) {
58
+ expect(typeof key).toBe('string');
59
+ expect((context as Record<string, unknown>)[key]).not.toBe('secret');
60
+ }
61
+ });
62
+
63
+ it('survives object spreads so SSR context copies keep their extensions', () => {
64
+ const extension = createRuntimeContextExtension<string>('test:spread');
65
+ const context = {};
66
+ extension.set(context, 'carried');
67
+
68
+ const copy = { ...context };
69
+ expect(extension.get(copy)).toBe('carried');
70
+ });
71
+ });