@bleedingdev/modern-js-runtime-renderer-extensions 0.0.0 → 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 (38) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/assetPolicy.js +89 -0
  3. package/dist/cjs/federatedCss.js +57 -0
  4. package/dist/cjs/node.js +53 -0
  5. package/dist/cjs/runtimePlugin.js +131 -0
  6. package/dist/cjs/templatePolicy.js +76 -0
  7. package/dist/esm/assetPolicy.mjs +48 -0
  8. package/dist/esm/federatedCss.mjs +19 -0
  9. package/dist/esm/runtimePlugin.mjs +78 -0
  10. package/dist/esm/templatePolicy.mjs +38 -0
  11. package/dist/esm-node/assetPolicy.mjs +49 -0
  12. package/dist/esm-node/federatedCss.mjs +20 -0
  13. package/dist/esm-node/node.mjs +11 -0
  14. package/dist/esm-node/runtimePlugin.mjs +79 -0
  15. package/dist/esm-node/templatePolicy.mjs +39 -0
  16. package/dist/types/assetPolicy.d.ts +19 -0
  17. package/dist/types/federatedCss.d.ts +6 -0
  18. package/dist/types/node.d.ts +3 -0
  19. package/dist/types/runtimePlugin.d.ts +7 -0
  20. package/dist/types/templatePolicy.d.ts +2 -0
  21. package/package.json +61 -7
  22. package/rslib.config.mts +19 -0
  23. package/rstest.config.mts +29 -0
  24. package/src/assetPolicy.ts +86 -0
  25. package/src/federatedCss.ts +43 -0
  26. package/src/node.ts +16 -0
  27. package/src/runtimePlugin.ts +119 -0
  28. package/src/templatePolicy.ts +69 -0
  29. package/tests/federatedCss.test.ts +80 -0
  30. package/tests/headRuntime.test.tsx +334 -0
  31. package/tests/nativeAssetPolicy.test.tsx +515 -0
  32. package/tests/nativeStreamHead.test.tsx +77 -0
  33. package/tests/nativeStringHead.test.tsx +128 -0
  34. package/tests/nativeTemplatePolicy.test.tsx +818 -0
  35. package/tests/runtimePlugin.test.tsx +37 -0
  36. package/tests/scriptOrder.security.test.ts +42 -0
  37. package/tsconfig.json +10 -0
  38. package/README.md +0 -3
@@ -0,0 +1,515 @@
1
+ import { getGlobalInternalRuntimeContext } from '../../plugin-runtime/src/core/context';
2
+ import { registerPlugin } from '../../plugin-runtime/src/core/plugin';
3
+ import {
4
+ createSSRRenderLifecycle,
5
+ replaceSSRTemplateChunk,
6
+ } from '../../plugin-runtime/src/core/server/shared';
7
+ import { rendererHeadPlugin } from '../src/node';
8
+
9
+ const lifecycleFor = (options: any, mode: 'string' | 'stream' = 'stream') => {
10
+ registerPlugin([rendererHeadPlugin()]);
11
+ const hooks = getGlobalInternalRuntimeContext().hooks;
12
+ const render = {
13
+ runtimeContext: options.runtimeContext,
14
+ request: options.request ?? new Request('http://localhost/'),
15
+ resource: options.resource ?? {
16
+ entryName: options.entryName,
17
+ routeManifest: options.routeManifest,
18
+ moduleFederationCssAssets: options.moduleFederationCssAssets,
19
+ },
20
+ config: options.config ?? {},
21
+ platform: 'node' as const,
22
+ mode,
23
+ isRsc: false,
24
+ };
25
+ return createSSRRenderLifecycle(
26
+ mode === 'string'
27
+ ? (hooks.extendStringSSRCollectors
28
+ .call({ chunkSet: options.chunkSet, render })
29
+ .filter(Boolean) as any)
30
+ : (hooks.extendStreamSSR
31
+ .call({ ...render, terminalMarker: '<!--end-->' })
32
+ .filter(Boolean) as any),
33
+ );
34
+ };
35
+
36
+ const buildShellBeforeTemplate = (template: string, options: any) =>
37
+ nativeBuildShellBeforeTemplate(template, {
38
+ ...options,
39
+ lifecycle: lifecycleFor(options),
40
+ });
41
+ const buildWorkerShellBeforeTemplate = (template: string, options: any) =>
42
+ nativeBuildWorkerShellBeforeTemplate(template, {
43
+ ...options,
44
+ lifecycle: lifecycleFor(options),
45
+ });
46
+
47
+ import { applyRouterRuntimeState } from '@modern-js/runtime-extensions/router-state';
48
+ import { CHUNK_CSS_PLACEHOLDER } from '../../plugin-runtime/src/core/server/constants';
49
+ import { buildShellBeforeTemplate as nativeBuildShellBeforeTemplate } from '../../plugin-runtime/src/core/server/stream/beforeTemplate';
50
+ import { buildShellBeforeTemplate as nativeBuildWorkerShellBeforeTemplate } from '../../plugin-runtime/src/core/server/stream/beforeTemplate.worker';
51
+
52
+ const withRouterSnapshot = (
53
+ runtimeContext: Record<string, unknown>,
54
+ serverSnapshot: Record<string, unknown>,
55
+ ) => {
56
+ applyRouterRuntimeState(runtimeContext as any, {
57
+ framework: 'react-router',
58
+ serverSnapshot,
59
+ });
60
+ return runtimeContext;
61
+ };
62
+
63
+ describe('buildShellBeforeTemplate', () => {
64
+ it.each([
65
+ ['node', buildShellBeforeTemplate],
66
+ ['worker', buildWorkerShellBeforeTemplate],
67
+ ])('preserves the complete %s CSS priority order without duplicate assets', async (_runtime, buildTemplate) => {
68
+ const runtimeContext = withRouterSnapshot(
69
+ {
70
+ routeManifest: {
71
+ routeAssets: {
72
+ 'route-a': {
73
+ referenceCssAssets: ['/assets/route-a.css', '/assets/shared.css'],
74
+ },
75
+ 'route-b': {
76
+ referenceCssAssets: ['/assets/route-b.css'],
77
+ },
78
+ 'async-main': {
79
+ referenceCssAssets: ['/assets/async-main.css'],
80
+ },
81
+ },
82
+ },
83
+ },
84
+ {
85
+ matchedRouteIds: ['route-a', 'route-b'],
86
+ },
87
+ );
88
+ const helmetStylesheet =
89
+ '<link href="/assets/helmet.css" rel="stylesheet" data-rh="true">';
90
+ const helmetData = {
91
+ bodyAttributes: '',
92
+ htmlAttributes: '',
93
+ base: '',
94
+ priority: '',
95
+ link: helmetStylesheet,
96
+ meta: '',
97
+ noscript: '',
98
+ script: '',
99
+ style: '',
100
+ title: '',
101
+ } as any;
102
+
103
+ const styledComponentsStyleTags =
104
+ '<style data-styled="true">.styled{color:red}</style>';
105
+ const orderedFragments = [
106
+ '<link href="/assets/route-a.css" rel="stylesheet" />',
107
+ '<link href="/assets/shared.css" rel="stylesheet" />',
108
+ '<link href="/assets/route-b.css" rel="stylesheet" />',
109
+ '<link href="/assets/async-main.css" rel="stylesheet" />',
110
+ styledComponentsStyleTags,
111
+ '<link href="/assets/federated.css" rel="stylesheet" />',
112
+ helmetStylesheet,
113
+ ];
114
+ const expectedHtml = `<html><head>${orderedFragments
115
+ .slice(0, -1)
116
+ .join('')} ${helmetStylesheet}\n</head><body></body></html>`;
117
+ const html = await buildTemplate(
118
+ `<html><head>${CHUNK_CSS_PLACEHOLDER}</head><body></body></html>`,
119
+ {
120
+ entryName: 'main',
121
+ runtimeContext: runtimeContext as any,
122
+ config: {} as any,
123
+ styledComponentsStyleTags,
124
+ helmetData,
125
+ moduleFederationCssAssets: [
126
+ '/assets/shared.css',
127
+ '/assets/federated.css',
128
+ ],
129
+ },
130
+ );
131
+
132
+ expect(html).toBe(expectedHtml);
133
+ expect(orderedFragments.map(fragment => html.indexOf(fragment))).toEqual(
134
+ orderedFragments.map(fragment => expectedHtml.indexOf(fragment)),
135
+ );
136
+ expect(html.split(orderedFragments[1])).toHaveLength(2);
137
+ });
138
+
139
+ it('should use shared matched route ids from the router snapshot for css injection', async () => {
140
+ for (const buildTemplate of [
141
+ buildShellBeforeTemplate,
142
+ buildWorkerShellBeforeTemplate,
143
+ ]) {
144
+ const html = await buildTemplate(
145
+ `<html><head>${CHUNK_CSS_PLACEHOLDER}</head><body></body></html>`,
146
+ {
147
+ entryName: 'main',
148
+ runtimeContext: withRouterSnapshot(
149
+ {
150
+ routeManifest: {
151
+ routeAssets: {
152
+ 'route-a': {
153
+ referenceCssAssets: ['/assets/route-a.css'],
154
+ },
155
+ },
156
+ },
157
+ },
158
+ {
159
+ matchedRouteIds: ['route-a'],
160
+ },
161
+ ) as any,
162
+ config: {} as any,
163
+ },
164
+ );
165
+
166
+ expect(html).toContain('/assets/route-a.css');
167
+ }
168
+ });
169
+
170
+ it('should derive css route ids from generic match snapshots', async () => {
171
+ const html = await buildShellBeforeTemplate(
172
+ `<html><head>${CHUNK_CSS_PLACEHOLDER}</head><body></body></html>`,
173
+ {
174
+ entryName: 'main',
175
+ runtimeContext: withRouterSnapshot(
176
+ {
177
+ routeManifest: {
178
+ routeAssets: {
179
+ 'asset-route': {
180
+ referenceCssAssets: ['/assets/asset-route.css'],
181
+ },
182
+ legacy: {
183
+ referenceCssAssets: ['/assets/legacy.css'],
184
+ },
185
+ },
186
+ },
187
+ },
188
+ {
189
+ matches: [{ routeId: 'router-route', assetRouteId: 'asset-route' }],
190
+ },
191
+ ) as any,
192
+ config: {} as any,
193
+ },
194
+ );
195
+
196
+ expect(html).toContain('/assets/asset-route.css');
197
+ expect(html).not.toContain('/assets/legacy.css');
198
+ });
199
+ });
200
+
201
+ const createRouteHydrationScriptTags = (
202
+ runtimeContext: any,
203
+ entryName: string,
204
+ options: any = {},
205
+ ) => {
206
+ const lifecycle = lifecycleFor({
207
+ runtimeContext,
208
+ entryName,
209
+ config: options,
210
+ });
211
+ return replaceSSRTemplateChunk(
212
+ {
213
+ name: 'scripts',
214
+ template: `${options.template ?? ''}<!--assets-->`,
215
+ placeholder: '<!--assets-->',
216
+ content: '',
217
+ },
218
+ lifecycle,
219
+ ).replace(options.template ?? '', '');
220
+ };
221
+ const createLoadableCollector = (options: any) => {
222
+ const lifecycle = lifecycleFor(options, 'string');
223
+ const collector = new NativeLoadableCollector({ ...options, lifecycle });
224
+ const effect = collector.effect.bind(collector);
225
+ collector.effect = async () => {
226
+ await effect();
227
+ options.chunkSet.cssChunk = replaceSSRTemplateChunk(
228
+ {
229
+ name: 'styles',
230
+ template: `${options.template}<!--assets-->`,
231
+ placeholder: '<!--assets-->',
232
+ content: options.chunkSet.cssChunk,
233
+ ...collector.stylesheetInfo,
234
+ },
235
+ lifecycle,
236
+ ).slice(options.template.length);
237
+ };
238
+ return collector;
239
+ };
240
+
241
+ import { orderHydrationScriptChunks } from '@modern-js/runtime-extensions';
242
+ import React from 'react';
243
+ import { RenderLevel } from '../../plugin-runtime/src/core/constants';
244
+ import { LoadableCollector as NativeLoadableCollector } from '../../plugin-runtime/src/core/server/string/loadable';
245
+
246
+ const createRuntimeContextWithMatchedRoutes = (matchedRouteIds: string[]) => {
247
+ const runtimeContext = {} as any;
248
+ applyRouterRuntimeState(runtimeContext, {
249
+ framework: 'react-router',
250
+ serverSnapshot: { matchedRouteIds },
251
+ });
252
+ return runtimeContext;
253
+ };
254
+
255
+ const chunk = (url: string, filename = url) => ({
256
+ url,
257
+ filename,
258
+ path: url,
259
+ });
260
+
261
+ const scriptUrls = (chunks: Array<{ url?: string }>) =>
262
+ chunks.map(item => item.url).filter(Boolean);
263
+
264
+ describe('createRouteHydrationScriptTags', () => {
265
+ it('does not treat substring matches in the template as existing scripts', () => {
266
+ const runtimeContext = createRuntimeContextWithMatchedRoutes(['route-a']);
267
+ runtimeContext.routeManifest = {
268
+ routeAssets: {
269
+ 'route-a': {
270
+ assets: ['/static/js/route-a.js'],
271
+ },
272
+ },
273
+ };
274
+
275
+ const scripts = createRouteHydrationScriptTags(runtimeContext, 'index', {
276
+ template:
277
+ '<html><head><link href="/static/js/route-a.js.map" rel="prefetch" /></head></html>',
278
+ });
279
+
280
+ expect(scripts).toBe('<script src="/static/js/route-a.js"></script>');
281
+ });
282
+ });
283
+
284
+ describe('LoadableCollector federated css', () => {
285
+ it('appends deduped module federation css after local route css', async () => {
286
+ const chunkSet = {
287
+ renderLevel: RenderLevel.CLIENT_RENDER,
288
+ ssrScripts: '',
289
+ jsChunk: '',
290
+ cssChunk: '',
291
+ };
292
+
293
+ const collector = createLoadableCollector({
294
+ runtimeContext: createRuntimeContextWithMatchedRoutes(['route-a']),
295
+ template:
296
+ '<html><head><link href="https://remote.example.com/already.css" rel="stylesheet" /></head></html>',
297
+ entryName: 'main',
298
+ chunkSet,
299
+ config: {},
300
+ routeManifest: {
301
+ routeAssets: {
302
+ 'route-a': {
303
+ assets: ['/static/css/route-a.css', '/static/css/shared.css'],
304
+ },
305
+ },
306
+ },
307
+ moduleFederationCssAssets: [
308
+ '/static/css/shared.css',
309
+ 'https://remote.example.com/expose.css',
310
+ 'https://remote.example.com/expose.css',
311
+ 'https://remote.example.com/already.css',
312
+ ],
313
+ });
314
+
315
+ (collector as any).extractor = {
316
+ chunks: ['local'],
317
+ getChunkAssets: () => [
318
+ chunk('/static/css/local.css'),
319
+ chunk('/static/css/shared.css'),
320
+ ],
321
+ getScriptTags: () => '',
322
+ };
323
+ await collector.effect();
324
+
325
+ expect(chunkSet.cssChunk).toBe(
326
+ '<link href="/static/css/local.css" rel="stylesheet" /><link href="/static/css/shared.css" rel="stylesheet" /><link href="/static/css/route-a.css" rel="stylesheet" /><link href="https://remote.example.com/expose.css" rel="stylesheet" />',
327
+ );
328
+ });
329
+
330
+ it('orders matched route scripts before the async entry script', () => {
331
+ const ordered = orderHydrationScriptChunks({
332
+ entryName: 'index',
333
+ asyncEntryChunks: [
334
+ chunk('/static/js/async/vendor.js'),
335
+ chunk('/static/js/async/async-index-123.js'),
336
+ ],
337
+ collectedChunks: [chunk('/static/js/async/loadable-child.js')],
338
+ matchedRouteChunks: [
339
+ chunk('/static/js/async/(lang)/page.js'),
340
+ chunk('/static/js/async/(lang)/checkout/page.js'),
341
+ chunk('/static/js/async/(lang)/page.js'),
342
+ ],
343
+ });
344
+
345
+ expect(scriptUrls(ordered)).toEqual([
346
+ '/static/js/async/vendor.js',
347
+ '/static/js/async/loadable-child.js',
348
+ '/static/js/async/(lang)/page.js',
349
+ '/static/js/async/(lang)/checkout/page.js',
350
+ '/static/js/async/async-index-123.js',
351
+ ]);
352
+ });
353
+
354
+ it('does not fall back to runtime route manifest when options omit routeManifest', async () => {
355
+ const chunkSet = {
356
+ renderLevel: RenderLevel.CLIENT_RENDER,
357
+ ssrScripts: '',
358
+ jsChunk: '',
359
+ cssChunk: '',
360
+ };
361
+ const runtimeContext = createRuntimeContextWithMatchedRoutes(['route-a']);
362
+ runtimeContext.routeManifest = {
363
+ routeAssets: {
364
+ 'route-a': {
365
+ assets: ['/static/css/route-a.css'],
366
+ },
367
+ },
368
+ };
369
+
370
+ const collector = createLoadableCollector({
371
+ runtimeContext,
372
+ template: '<html><head></head></html>',
373
+ entryName: 'main',
374
+ chunkSet,
375
+ config: {},
376
+ });
377
+
378
+ collector.collect(React.createElement('div'));
379
+ await collector.effect();
380
+
381
+ expect(chunkSet.cssChunk).toBe('');
382
+ });
383
+ });
384
+
385
+ test('registered asset policy orders native loadable output before the async entry', async () => {
386
+ const chunkSet = {
387
+ renderLevel: RenderLevel.CLIENT_RENDER,
388
+ ssrScripts: '',
389
+ jsChunk: '',
390
+ cssChunk: '',
391
+ };
392
+ const collector = createLoadableCollector({
393
+ runtimeContext: createRuntimeContextWithMatchedRoutes(['route-a']),
394
+ template: '',
395
+ entryName: 'index',
396
+ chunkSet,
397
+ config: { enableAsyncEntry: true },
398
+ routeManifest: { routeAssets: { 'route-a': { assets: ['/route-a.js'] } } },
399
+ });
400
+ (collector as any).extractor = {
401
+ chunks: ['child'],
402
+ getScriptTags: () => '',
403
+ getChunkAssets: (ids: string[]) =>
404
+ ids.includes('async-index')
405
+ ? [chunk('/vendor.js'), chunk('/async-index.js')]
406
+ : [chunk('/child.js')],
407
+ };
408
+ await collector.effect();
409
+ expect(
410
+ Array.from(chunkSet.jsChunk.matchAll(/src="([^"]+)"/g), match => match[1]),
411
+ ).toEqual(['/vendor.js', '/child.js', '/route-a.js', '/async-index.js']);
412
+ });
413
+
414
+ describe('SSR hydration helper matrix', () => {
415
+ const createRuntimeContext = (
416
+ matchedRouteIds: string[],
417
+ routeAssets: Record<string, { assets?: string[] }>,
418
+ ) => {
419
+ const runtimeContext = {
420
+ routeManifest: {
421
+ routeAssets,
422
+ },
423
+ } as any;
424
+
425
+ applyRouterRuntimeState(runtimeContext, {
426
+ framework: 'custom-router',
427
+ matchedRouteIds,
428
+ } as any);
429
+
430
+ return runtimeContext;
431
+ };
432
+
433
+ it('dedupes hydration script chunks and generated tags by the exact script src', () => {
434
+ expect(
435
+ orderHydrationScriptChunks({
436
+ entryName: 'main',
437
+ asyncEntryChunks: [
438
+ {
439
+ filename: 'async-main.123.js',
440
+ url: '/assets/async-main.123.js',
441
+ },
442
+ {
443
+ filename: 'vendor.js',
444
+ url: '/assets/vendor.js',
445
+ },
446
+ {
447
+ filename: 'vendor-copy.js',
448
+ url: '/assets/vendor.js',
449
+ },
450
+ ],
451
+ collectedChunks: [
452
+ {
453
+ filename: 'route.js',
454
+ url: '/assets/route.js',
455
+ },
456
+ ],
457
+ matchedRouteChunks: [
458
+ {
459
+ filename: 'route-copy.js',
460
+ url: '/assets/route.js',
461
+ },
462
+ {
463
+ filename: 'vendor-query.js',
464
+ url: '/assets/vendor.js?cache=1',
465
+ },
466
+ ],
467
+ }),
468
+ ).toEqual([
469
+ {
470
+ filename: 'vendor.js',
471
+ url: '/assets/vendor.js',
472
+ },
473
+ {
474
+ filename: 'route.js',
475
+ url: '/assets/route.js',
476
+ },
477
+ {
478
+ filename: 'vendor-query.js',
479
+ url: '/assets/vendor.js?cache=1',
480
+ },
481
+ {
482
+ filename: 'async-main.123.js',
483
+ url: '/assets/async-main.123.js',
484
+ },
485
+ ]);
486
+
487
+ const runtimeContext = createRuntimeContext(['routes/dashboard'], {
488
+ 'routes/dashboard': {
489
+ assets: [
490
+ '/assets/dashboard.js',
491
+ '/assets/shared.js',
492
+ '/assets/dashboard.css',
493
+ '/assets/dashboard.js',
494
+ ],
495
+ },
496
+ 'async-main': {
497
+ assets: [
498
+ '/assets/async-main.js',
499
+ '/assets/shared.js',
500
+ '/assets/async-main.css',
501
+ ],
502
+ },
503
+ });
504
+
505
+ expect(
506
+ createRouteHydrationScriptTags(runtimeContext, 'main', {
507
+ nonce: 'nonce-1',
508
+ template:
509
+ '<script defer src="/assets/dashboard.js"></script><script src="/assets/shared.js?cache=1"></script>',
510
+ }),
511
+ ).toBe(
512
+ '<script src="/assets/shared.js" nonce="nonce-1"></script> <script src="/assets/async-main.js" nonce="nonce-1"></script>',
513
+ );
514
+ });
515
+ });
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ import { registerPlugin } from '../../plugin-runtime/src/core/plugin';
3
+ import { renderStreaming } from '../../plugin-runtime/src/core/server/stream';
4
+ import { Helmet } from '../../plugin-runtime/src/exports/head';
5
+ import { rendererHeadPlugin } from '../src/node';
6
+
7
+ const createRuntimeContext = () => ({
8
+ isBrowser: false,
9
+ requestContext: {},
10
+ context: {},
11
+ initialData: {},
12
+ __i18nData__: {},
13
+ routeManifest: {},
14
+ ssrContext: {
15
+ request: {
16
+ params: {},
17
+ query: {},
18
+ pathname: '/',
19
+ host: 'localhost',
20
+ url: 'http://localhost/',
21
+ headers: {},
22
+ },
23
+ reporter: { sessionId: 'session-1' },
24
+ },
25
+ });
26
+
27
+ describe('streaming Helmet collection', () => {
28
+ it('publishes only Helmet markers present in the completed shell', async () => {
29
+ registerPlugin([rendererHeadPlugin()]);
30
+
31
+ const AbandonPrimary = (): null => {
32
+ throw new Error('abandon primary');
33
+ };
34
+ const runtimeContext = createRuntimeContext();
35
+ const stream = await renderStreaming(
36
+ new Request('http://localhost/'),
37
+ <>
38
+ <Helmet>
39
+ <meta name="outside" content="committed" />
40
+ </Helmet>
41
+ <React.Suspense
42
+ fallback={
43
+ <>
44
+ <Helmet>
45
+ <meta name="fallback" content="committed" />
46
+ </Helmet>
47
+ fallback rendered
48
+ </>
49
+ }
50
+ >
51
+ <Helmet>
52
+ <meta name="abandoned-unique" content="ghost" />
53
+ </Helmet>
54
+ <AbandonPrimary />
55
+ </React.Suspense>
56
+ </>,
57
+ {
58
+ resource: {
59
+ entryName: 'index',
60
+ htmlTemplate:
61
+ '<html><head></head><body><!--<?- html ?>--></body></html>',
62
+ routeManifest: {},
63
+ },
64
+ runtimeContext,
65
+ config: {},
66
+ onError: () => {},
67
+ onTiming: () => {},
68
+ } as any,
69
+ );
70
+
71
+ const html = await new Response(stream).text();
72
+ expect(html).toContain('name="outside" content="committed"');
73
+ expect(html).toContain('name="fallback" content="committed"');
74
+ expect(html).not.toContain('abandoned-unique');
75
+ expect(html).not.toContain('data-modern-helmet');
76
+ });
77
+ });