@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,369 @@
1
+ import React from 'react';
2
+ import { renderToString } from 'react-dom/server';
3
+ import { createHeadRuntime } from '../src/headRuntime';
4
+ import { ensureHelmetContext, getHelmetContext } from '../src/helmetContext';
5
+ import * as rendererHead from '../src/rendererHead';
6
+
7
+ const { Head: Helmet, wrapServerRoot } = createHeadRuntime();
8
+ const getHelmetData = (context: object) => getHelmetContext(context)?.helmet;
9
+
10
+ const createServerContext = () => ({ isBrowser: false }) as any;
11
+
12
+ const renderWithContext = (context: any, node: React.ReactNode) => {
13
+ rendererHead.beginHeadRender(context);
14
+ try {
15
+ const html = renderToString(
16
+ wrapServerRoot(node, {
17
+ runtimeContext: context,
18
+ helmetContext: ensureHelmetContext(context),
19
+ }),
20
+ );
21
+ return rendererHead.completeHeadRender(context, html);
22
+ } catch (error) {
23
+ rendererHead.abortHeadRender(context);
24
+ throw error;
25
+ }
26
+ };
27
+
28
+ describe('server Helmet collection', () => {
29
+ it('keeps independently registered adapters scoped to their own providers', () => {
30
+ const outer = createHeadRuntime();
31
+ const inner = createHeadRuntime();
32
+ const outerContext = {};
33
+ const innerContext = {};
34
+
35
+ renderToString(
36
+ outer.wrapServerRoot(
37
+ inner.wrapServerRoot(
38
+ <>
39
+ <outer.Head>
40
+ <title>outer application</title>
41
+ </outer.Head>
42
+ <inner.Head>
43
+ <title>inner application</title>
44
+ </inner.Head>
45
+ </>,
46
+ {
47
+ runtimeContext: innerContext,
48
+ helmetContext: ensureHelmetContext(innerContext),
49
+ },
50
+ ),
51
+ {
52
+ runtimeContext: outerContext,
53
+ helmetContext: ensureHelmetContext(outerContext),
54
+ },
55
+ ),
56
+ );
57
+
58
+ expect(getHelmetData(outerContext)?.title.toString()).toBe(
59
+ '<title data-rh="true">outer application</title>',
60
+ );
61
+ expect(getHelmetData(innerContext)?.title.toString()).toBe(
62
+ '<title data-rh="true">inner application</title>',
63
+ );
64
+ });
65
+
66
+ it('excludes Helmet records from a discarded Suspense primary branch', () => {
67
+ const context = createServerContext();
68
+ const never = new Promise<never>(() => {});
69
+
70
+ const SuspendForever = (): null => {
71
+ throw never;
72
+ };
73
+
74
+ const html = renderWithContext(
75
+ context,
76
+ <>
77
+ <Helmet>
78
+ <meta name="outside" content="committed" />
79
+ </Helmet>
80
+ <React.Suspense
81
+ fallback={
82
+ <>
83
+ <Helmet>
84
+ <meta name="fallback" content="committed" />
85
+ </Helmet>
86
+ <span>fallback rendered</span>
87
+ </>
88
+ }
89
+ >
90
+ <Helmet>
91
+ <meta name="abandoned-unique" content="ghost" />
92
+ </Helmet>
93
+ <SuspendForever />
94
+ </React.Suspense>
95
+ </>,
96
+ );
97
+
98
+ expect(html).toContain('fallback rendered');
99
+ const meta = getHelmetData(context)!.meta.toString();
100
+ expect(meta).toContain('name="outside"');
101
+ expect(meta).toContain('name="fallback"');
102
+ expect(meta).not.toContain('name="abandoned-unique"');
103
+ });
104
+
105
+ it('collects title/meta during SSR and renders nothing inline', () => {
106
+ const context = createServerContext();
107
+ const html = renderWithContext(
108
+ context,
109
+ <main>
110
+ <Helmet>
111
+ <title>Server Title</title>
112
+ <meta name="description" content="hello" />
113
+ </Helmet>
114
+ body
115
+ </main>,
116
+ );
117
+
118
+ expect(html).not.toContain('Server Title');
119
+ const helmet = getHelmetData(context)!;
120
+ expect(helmet.title.toString()).toBe(
121
+ '<title data-rh="true">Server Title</title>',
122
+ );
123
+ expect(helmet.meta.toString()).toBe(
124
+ '<meta data-rh="true" name="description" content="hello">',
125
+ );
126
+ });
127
+
128
+ it('dedupes meta by primary attribute across nested Helmets, inner wins', () => {
129
+ const context = createServerContext();
130
+ renderWithContext(
131
+ context,
132
+ <>
133
+ <Helmet>
134
+ <meta name="description" content="outer" />
135
+ <meta name="keywords" content="modernjs" />
136
+ </Helmet>
137
+ <section>
138
+ <Helmet>
139
+ <meta name="description" content="inner" />
140
+ </Helmet>
141
+ </section>
142
+ </>,
143
+ );
144
+
145
+ const meta = getHelmetData(context)!.meta.toString();
146
+ expect(meta).toContain('content="inner"');
147
+ expect(meta).not.toContain('content="outer"');
148
+ expect(meta).toContain('content="modernjs"');
149
+ // outer instance tags come first, like react-helmet-async
150
+ expect(meta.indexOf('keywords')).toBeLessThan(meta.indexOf('description'));
151
+ });
152
+
153
+ it('keeps duplicate metas within a single Helmet instance', () => {
154
+ const context = createServerContext();
155
+ renderWithContext(
156
+ context,
157
+ <Helmet>
158
+ <meta name="author" content="a" />
159
+ <meta name="author" content="b" />
160
+ </Helmet>,
161
+ );
162
+
163
+ const meta = getHelmetData(context)!.meta.toString();
164
+ expect(meta).toContain('content="a"');
165
+ expect(meta).toContain('content="b"');
166
+ });
167
+
168
+ it('normalizes charSet when deduping and drops meta without primary attribute', () => {
169
+ const context = createServerContext();
170
+ renderWithContext(
171
+ context,
172
+ <>
173
+ <Helmet>
174
+ <meta charSet="utf-8" />
175
+ <meta content="orphan" />
176
+ </Helmet>
177
+ <Helmet meta={[{ charSet: 'utf-8' } as any]} />
178
+ </>,
179
+ );
180
+
181
+ const meta = getHelmetData(context)!.meta.toString();
182
+ expect(meta.match(/charset/g)).toHaveLength(1);
183
+ expect(meta).not.toContain('orphan');
184
+ });
185
+
186
+ it('applies the innermost title and the innermost titleTemplate', () => {
187
+ const context = createServerContext();
188
+ renderWithContext(
189
+ context,
190
+ <>
191
+ <Helmet titleTemplate="%s | Site">
192
+ <title>Outer</title>
193
+ </Helmet>
194
+ <Helmet>
195
+ <title>Inner</title>
196
+ </Helmet>
197
+ </>,
198
+ );
199
+
200
+ expect(getHelmetData(context)!.title.toString()).toBe(
201
+ '<title data-rh="true">Inner | Site</title>',
202
+ );
203
+ });
204
+
205
+ it('falls back to defaultTitle without applying the template', () => {
206
+ const context = createServerContext();
207
+ renderWithContext(
208
+ context,
209
+ <Helmet defaultTitle="Default" titleTemplate="%s | Site" />,
210
+ );
211
+
212
+ expect(getHelmetData(context)!.title.toString()).toBe(
213
+ '<title data-rh="true">Default</title>',
214
+ );
215
+ });
216
+
217
+ it('maps <html>/<body> children to html/body attributes', () => {
218
+ const context = createServerContext();
219
+ renderWithContext(
220
+ context,
221
+ <Helmet>
222
+ <html lang="en" />
223
+ <body className="dark" />
224
+ </Helmet>,
225
+ );
226
+
227
+ const helmet = getHelmetData(context)!;
228
+ expect(helmet.htmlAttributes.toString()).toBe('lang="en"');
229
+ expect(helmet.bodyAttributes.toString()).toBe('class="dark"');
230
+ });
231
+
232
+ it('keeps one canonical link but all distinct stylesheets', () => {
233
+ const context = createServerContext();
234
+ renderWithContext(
235
+ context,
236
+ <>
237
+ <Helmet>
238
+ <link rel="canonical" href="https://outer.example/" />
239
+ <link rel="stylesheet" href="/a.css" />
240
+ <link rel="stylesheet" href="/b.css" />
241
+ </Helmet>
242
+ <Helmet>
243
+ <link rel="canonical" href="https://inner.example/" />
244
+ </Helmet>
245
+ </>,
246
+ );
247
+
248
+ const link = getHelmetData(context)!.link.toString();
249
+ expect(link).toContain('https://inner.example/');
250
+ expect(link).not.toContain('https://outer.example/');
251
+ expect(link).toContain('/a.css');
252
+ expect(link).toContain('/b.css');
253
+ });
254
+
255
+ it('does not serialize script innerHTML as an attribute', () => {
256
+ const context = createServerContext();
257
+ renderWithContext(
258
+ context,
259
+ <Helmet>
260
+ <script type="application/ld+json">{'{"@context":"a"}'}</script>
261
+ </Helmet>,
262
+ );
263
+
264
+ const script = getHelmetData(context)!.script.toString();
265
+ expect(script).toBe(
266
+ '<script data-rh="true" type="application/ld+json">{"@context":"a"}</script>',
267
+ );
268
+ });
269
+
270
+ it('publishes prioritized SEO tags separately with component parity', () => {
271
+ const context = createServerContext();
272
+ renderWithContext(
273
+ context,
274
+ <Helmet prioritizeSeoTags htmlAttributes={{ class: 'app' } as any}>
275
+ <title>Priority Page</title>
276
+ <meta name="description" content="priority description" />
277
+ <meta name="keywords" content="ordinary keywords" />
278
+ <link rel="canonical" href="https://example.com/page" />
279
+ <link rel="stylesheet" href="/page.css" />
280
+ <script type="application/ld+json">{'{"name":"page"}'}</script>
281
+ <script src="/ordinary.js" />
282
+ </Helmet>,
283
+ );
284
+
285
+ const helmet = getHelmetData(context)!;
286
+ expect(helmet.priority.toString()).toContain('priority description');
287
+ expect(helmet.priority.toString()).toContain('https://example.com/page');
288
+ expect(helmet.priority.toString()).toContain('application/ld+json');
289
+ expect(helmet.meta.toString()).toContain('ordinary keywords');
290
+ expect(helmet.meta.toString()).not.toContain('priority description');
291
+ expect(helmet.link.toString()).toContain('/page.css');
292
+ expect(helmet.link.toString()).not.toContain('https://example.com/page');
293
+ expect(helmet.script.toString()).toContain('/ordinary.js');
294
+ expect(helmet.script.toString()).not.toContain('application/ld+json');
295
+
296
+ const priority = helmet.priority.toComponent();
297
+ expect(priority.map(element => element.type)).toEqual([
298
+ 'meta',
299
+ 'link',
300
+ 'script',
301
+ ]);
302
+ expect(priority[0].props).toMatchObject({
303
+ 'data-rh': true,
304
+ content: 'priority description',
305
+ name: 'description',
306
+ });
307
+ expect(priority[2].props).toMatchObject({
308
+ 'data-rh': true,
309
+ dangerouslySetInnerHTML: { __html: '{"name":"page"}' },
310
+ type: 'application/ld+json',
311
+ });
312
+ expect(helmet.meta.toComponent()[0].props).toMatchObject({
313
+ content: 'ordinary keywords',
314
+ name: 'keywords',
315
+ });
316
+ expect(helmet.title.toComponent()[0].props.children).toBe('Priority Page');
317
+ expect(helmet.htmlAttributes.toComponent()).toEqual({ className: 'app' });
318
+ });
319
+
320
+ it('is idempotent when React replays the tree (streaming SSR retry)', () => {
321
+ const context = createServerContext();
322
+ const app = (
323
+ <>
324
+ <Helmet titleTemplate="%s | Site">
325
+ <title>Page</title>
326
+ <meta name="description" content="stable" />
327
+ <html lang="en" />
328
+ </Helmet>
329
+ <Helmet>
330
+ <meta property="og:title" content="Page" />
331
+ </Helmet>
332
+ </>
333
+ );
334
+
335
+ renderWithContext(context, app);
336
+ const first = getHelmetData(context)!;
337
+ const firstSnapshot = {
338
+ title: first.title.toString(),
339
+ meta: first.meta.toString(),
340
+ htmlAttributes: first.htmlAttributes.toString(),
341
+ };
342
+
343
+ // Simulate a replayed render against the same per-request context.
344
+ renderWithContext(context, app);
345
+ const second = getHelmetData(context)!;
346
+
347
+ expect(second.title.toString()).toBe(firstSnapshot.title);
348
+ expect(second.meta.toString()).toBe(firstSnapshot.meta);
349
+ expect(second.htmlAttributes.toString()).toBe(firstSnapshot.htmlAttributes);
350
+ expect(second.meta.toString().match(/description/g)).toHaveLength(1);
351
+ });
352
+
353
+ it('keeps the innermost base tag only', () => {
354
+ const context = createServerContext();
355
+ renderWithContext(
356
+ context,
357
+ <>
358
+ <Helmet base={{ href: 'https://outer.example/' } as any} />
359
+ <Helmet>
360
+ <base href="https://inner.example/" />
361
+ </Helmet>
362
+ </>,
363
+ );
364
+
365
+ const base = getHelmetData(context)!.base.toString();
366
+ expect(base).toContain('https://inner.example/');
367
+ expect(base).not.toContain('https://outer.example/');
368
+ });
369
+ });
@@ -0,0 +1,117 @@
1
+ import {
2
+ createSafeFailureHttpResult,
3
+ createSafeJsonFailureResponse,
4
+ } from '../src/safeFailure';
5
+
6
+ describe('native safe failure contract compatibility', () => {
7
+ it('should create safe JSON failure envelopes without leaking raw messages', async () => {
8
+ const result = createSafeFailureHttpResult(
9
+ new Error('database password leaked in stack'),
10
+ );
11
+
12
+ expect(result).toEqual({
13
+ status: 500,
14
+ body: {
15
+ success: false,
16
+ error: {
17
+ code: 'INTERNAL_SERVER_ERROR',
18
+ message: 'Internal Server Error',
19
+ status: 500,
20
+ },
21
+ },
22
+ headers: {
23
+ 'content-type': 'application/json; charset=utf-8',
24
+ },
25
+ });
26
+
27
+ const response = createSafeJsonFailureResponse(
28
+ new Error('database password leaked in stack'),
29
+ );
30
+ expect(response.status).toBe(500);
31
+ expect(response.headers.get('content-type')).toContain('application/json');
32
+ await expect(response.json()).resolves.toEqual(result.body);
33
+ });
34
+
35
+ it('should preserve service unavailable status and Retry-After metadata', () => {
36
+ const error = Object.assign(new Error('maintenance window details'), {
37
+ status: 503,
38
+ retryAfterSeconds: 120,
39
+ });
40
+
41
+ expect(createSafeFailureHttpResult(error)).toEqual({
42
+ status: 503,
43
+ body: {
44
+ success: false,
45
+ error: {
46
+ code: 'SERVICE_UNAVAILABLE',
47
+ message: 'Service Unavailable',
48
+ status: 503,
49
+ },
50
+ },
51
+ headers: {
52
+ 'content-type': 'application/json; charset=utf-8',
53
+ 'Retry-After': '120',
54
+ },
55
+ });
56
+ });
57
+
58
+ it.each([
59
+ ['delay-seconds text', '120', '120'],
60
+ [
61
+ 'the largest safely serializable delay',
62
+ Number.MAX_SAFE_INTEGER,
63
+ String(Number.MAX_SAFE_INTEGER),
64
+ ],
65
+ [
66
+ 'an HTTP-date string',
67
+ 'Sun, 30 Aug 2026 20:00:00 GMT',
68
+ 'Sun, 30 Aug 2026 20:00:00 GMT',
69
+ ],
70
+ [
71
+ 'a Date instance',
72
+ new Date(Date.UTC(2026, 7, 30, 20, 0, 0)),
73
+ 'Sun, 30 Aug 2026 20:00:00 GMT',
74
+ ],
75
+ ])('should preserve %s in Retry-After', (_, retryAfter, expected) => {
76
+ const result = createSafeFailureHttpResult({
77
+ status: 503,
78
+ retryAfter,
79
+ });
80
+
81
+ expect(result.headers['Retry-After']).toBe(expected);
82
+ });
83
+
84
+ it.each([
85
+ ['a header-injection payload', '120\r\nX-Injected: true'],
86
+ ['a non-numeric delay', 'after maintenance'],
87
+ ['a delay too large for integer serialization', 1e21],
88
+ ['an invalid HTTP date', 'Wed, 99 Jun 2026 25:61:61 GMT'],
89
+ ['an invalid Date object', new Date(Number.NaN)],
90
+ ])('should ignore %s in Retry-After without throwing', (_, retryAfter) => {
91
+ const error = Object.assign(new Error('maintenance window details'), {
92
+ status: 503,
93
+ retryAfter,
94
+ });
95
+
96
+ expect(createSafeFailureHttpResult(error)).toEqual({
97
+ status: 503,
98
+ body: {
99
+ success: false,
100
+ error: {
101
+ code: 'SERVICE_UNAVAILABLE',
102
+ message: 'Service Unavailable',
103
+ status: 503,
104
+ },
105
+ },
106
+ headers: {
107
+ 'content-type': 'application/json; charset=utf-8',
108
+ },
109
+ });
110
+ expect(() => createSafeJsonFailureResponse(error)).not.toThrow();
111
+ });
112
+
113
+ it('should clamp invalid thrown status values to 500', () => {
114
+ expect(createSafeFailureHttpResult({ status: 200 }).status).toBe(500);
115
+ expect(createSafeFailureHttpResult({ statusCode: 799 }).status).toBe(500);
116
+ });
117
+ });
@@ -0,0 +1,51 @@
1
+ import {
2
+ normalizePreloadBehavior,
3
+ resolvePreloadFromPrefetch,
4
+ } from '../src/prefetchPolicy';
5
+
6
+ const behaviors = ['intent', 'render', 'viewport', 'none'] as const;
7
+
8
+ describe('router prefetch precedence', () => {
9
+ describe.each(behaviors)('React Router prefetch=%s', prefetch => {
10
+ it('inherits prefetch when preload is omitted', () => {
11
+ expect(normalizePreloadBehavior(undefined, prefetch)).toBe(prefetch);
12
+ });
13
+
14
+ it.each([
15
+ ...behaviors,
16
+ false,
17
+ ] as const)('honors explicit preload=%s using none for disabled', preload => {
18
+ expect(normalizePreloadBehavior(preload, prefetch)).toBe(
19
+ preload === false ? 'none' : preload,
20
+ );
21
+ });
22
+ });
23
+
24
+ describe.each([undefined, ...behaviors])('TanStack prefetch=%s', prefetch => {
25
+ it('inherits supported prefetch with viewport as the default', () => {
26
+ expect(resolvePreloadFromPrefetch(prefetch, undefined)).toBe(
27
+ prefetch === 'none' ? false : (prefetch ?? 'viewport'),
28
+ );
29
+ });
30
+
31
+ it.each([
32
+ ...behaviors,
33
+ false,
34
+ null,
35
+ 'custom',
36
+ 0,
37
+ ])('passes explicit preload=%s through unchanged', preload => {
38
+ expect(resolvePreloadFromPrefetch(prefetch, preload)).toBe(preload);
39
+ });
40
+ });
41
+
42
+ it('preserves the routers different defaults', () => {
43
+ expect(normalizePreloadBehavior(undefined, 'render')).toBe('render');
44
+ expect(resolvePreloadFromPrefetch(undefined, undefined)).toBe('viewport');
45
+ });
46
+
47
+ it('preserves object identity for TanStack preload pass-through', () => {
48
+ const preload = {};
49
+ expect(resolvePreloadFromPrefetch('none', preload)).toBe(preload);
50
+ });
51
+ });
@@ -6,13 +6,8 @@ import {
6
6
  completeHeadRender,
7
7
  createHeadChunkProcessor,
8
8
  createWebHeadMarkerStripper,
9
- publishHeadRender,
10
9
  } from '../src';
11
- import {
12
- createNodeHeadMarkerStripper,
13
- createOnceErrorReporter,
14
- pipeNodeHeadStream,
15
- } from '../src/node';
10
+ import { createNodeHeadMarkerStripper, pipeNodeHeadStream } from '../src/node';
16
11
 
17
12
  const marker = (props: Record<string, string>) =>
18
13
  `<template data-modern-helmet="${props['data-modern-helmet']}"></template>`;
@@ -87,41 +82,6 @@ describe('renderer head transactions', () => {
87
82
  }
88
83
  });
89
84
 
90
- it('freezes the published shell and ignores late records', () => {
91
- const context = {};
92
- let published: string[] = [];
93
- beginHeadRender(context);
94
- const shell = collectHeadRecord(
95
- context,
96
- () => 'shell',
97
- records => {
98
- published = records;
99
- },
100
- )!;
101
- const late = collectHeadRecord(
102
- context,
103
- () => 'late',
104
- records => {
105
- published = records;
106
- },
107
- )!;
108
- const processor = createHeadChunkProcessor(context);
109
- expect(processor.push(marker(shell))).toBe('');
110
- publishHeadRender(context);
111
-
112
- expect(
113
- collectHeadRecord(
114
- context,
115
- () => 'late',
116
- records => {
117
- published = records;
118
- },
119
- ),
120
- ).toBeNull();
121
- expect(processor.finish(marker(late))).toBe('');
122
- expect(published).toEqual(['shell']);
123
- });
124
-
125
85
  it('clears the previous snapshot while rendering and restores it on abort', () => {
126
86
  const context = {};
127
87
  let published: string[] = [];
@@ -265,35 +225,4 @@ describe('renderer head transactions', () => {
265
225
  await expect(output).rejects.toThrow('source failed');
266
226
  expect(onError).toHaveBeenCalledTimes(1);
267
227
  });
268
-
269
- it('rejects the destination and reports a stripper error exactly once', async () => {
270
- const source = new PassThrough();
271
- const destination = new PassThrough();
272
- const output = new Response(
273
- Readable.toWeb(destination) as ReadableStream<Uint8Array>,
274
- ).text();
275
- const onError = rstest.fn();
276
- pipeNodeHeadStream({
277
- source,
278
- destination,
279
- context: Object.freeze({}),
280
- onError,
281
- });
282
-
283
- source.end('trigger frozen-context failure');
284
-
285
- await expect(output).rejects.toThrow('object is not extensible');
286
- expect(onError).toHaveBeenCalledTimes(1);
287
- });
288
-
289
- it('reports a shared render and stream failure only once', () => {
290
- const onError = rstest.fn();
291
- const reportError = createOnceErrorReporter(onError);
292
- const failure = new Error('shared failure');
293
-
294
- reportError(failure);
295
- reportError(failure);
296
-
297
- expect(onError).toHaveBeenCalledExactlyOnceWith(failure);
298
- });
299
228
  });