@bleedingdev/modern-js-runtime-renderer-extensions 0.0.0 → 3.9.0-ultramodern.11

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 +143 -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 +87 -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 +88 -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 +13 -0
  20. package/dist/types/templatePolicy.d.ts +2 -0
  21. package/package.json +60 -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 +141 -0
  28. package/src/templatePolicy.ts +69 -0
  29. package/tests/federatedCss.test.ts +49 -0
  30. package/tests/headRuntime.test.tsx +163 -0
  31. package/tests/nativeAssetPolicy.test.tsx +377 -0
  32. package/tests/nativeStreamHead.test.tsx +77 -0
  33. package/tests/nativeStringHead.test.tsx +128 -0
  34. package/tests/nativeTemplatePolicy.test.tsx +366 -0
  35. package/tests/rootWrapOrder.test.tsx +70 -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,366 @@
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 buildShellAfterTemplate = (template: string, options: any) =>
37
+ nativeBuildShellAfterTemplate(template, {
38
+ ...options,
39
+ lifecycle: lifecycleFor(options),
40
+ });
41
+ const createDataCollector = (options: any) => {
42
+ const lifecycle = lifecycleFor(options, 'string');
43
+ const collector = new SSRDataCollector({ ...options, lifecycle });
44
+ return {
45
+ effect() {
46
+ collector.effect();
47
+ options.chunkSet.ssrScripts = replaceSSRTemplateChunk(
48
+ {
49
+ name: 'data',
50
+ template: SSR_DATA_PLACEHOLDER,
51
+ placeholder: SSR_DATA_PLACEHOLDER,
52
+ content: options.chunkSet.ssrScripts,
53
+ },
54
+ lifecycle,
55
+ );
56
+ },
57
+ };
58
+ };
59
+
60
+ import { applyRouterRuntimeState } from '@modern-js/runtime-extensions/router-state';
61
+ import { RenderLevel } from '../../plugin-runtime/src/core/constants';
62
+ import { SSR_DATA_PLACEHOLDER } from '../../plugin-runtime/src/core/server/constants';
63
+ import { buildShellAfterTemplate as nativeBuildShellAfterTemplate } from '../../plugin-runtime/src/core/server/stream/afterTemplate';
64
+ import { SSRDataCollector } from '../../plugin-runtime/src/core/server/string/ssrData';
65
+
66
+ const withRouterSnapshot = (
67
+ runtimeContext: Record<string, unknown>,
68
+ serverSnapshot: Record<string, unknown>,
69
+ ) => {
70
+ applyRouterRuntimeState(runtimeContext as any, {
71
+ framework: 'react-router',
72
+ serverSnapshot,
73
+ });
74
+ return runtimeContext;
75
+ };
76
+
77
+ const scriptSrcs = (html: string) =>
78
+ Array.from(
79
+ html.matchAll(/<script\b[^>]*\bsrc=(?:"([^"]+)"|'([^']+)'|([^\s>]+))/g),
80
+ ).map(match => match[1] ?? match[2] ?? match[3]);
81
+
82
+ describe('SSRDataCollector (stream parity)', () => {
83
+ it('should move matched route scripts before the hydration entry script', async () => {
84
+ const html = await buildShellAfterTemplate(
85
+ '<script defer src="/static/js/index.js"></script><!--<?- chunksMap.js ?>-->',
86
+ {
87
+ entryName: 'index',
88
+ renderLevel: RenderLevel.SERVER_RENDER,
89
+ request: new Request('http://localhost/products/shoe'),
90
+ runtimeContext: withRouterSnapshot(
91
+ {
92
+ routeManifest: {
93
+ routeAssets: {
94
+ 'products/$slug': {
95
+ assets: [
96
+ '/static/js/async/products/shared.js',
97
+ '/static/js/async/products/$slug.js',
98
+ ],
99
+ },
100
+ 'async-index': {
101
+ assets: ['/static/js/async/async-index.js'],
102
+ },
103
+ },
104
+ },
105
+ initialData: {},
106
+ __i18nData__: {},
107
+ ssrContext: {
108
+ request: {
109
+ params: {},
110
+ query: {},
111
+ pathname: '/products/shoe',
112
+ host: 'localhost',
113
+ url: 'http://localhost/products/shoe',
114
+ headers: {},
115
+ },
116
+ reporter: { sessionId: 'session-1' },
117
+ },
118
+ },
119
+ {
120
+ matchedRouteIds: ['products/$slug'],
121
+ },
122
+ ) as any,
123
+ ssrConfig: {} as any,
124
+ config: {} as any,
125
+ },
126
+ );
127
+
128
+ expect(scriptSrcs(html)).toEqual([
129
+ '/static/js/async/products/shared.js',
130
+ '/static/js/async/products/$slug.js',
131
+ '/static/js/async/async-index.js',
132
+ '/static/js/index.js',
133
+ ]);
134
+ });
135
+ });
136
+
137
+ import React from 'react';
138
+ import { renderString } from '../../plugin-runtime/src/core/server/string';
139
+
140
+ const TSR_BOOTSTRAP = '<script>window.$_TSR = { router: "hydrated" };</script>';
141
+
142
+ const createRuntimeContext = (options: { withRouterBootstrap: boolean }) => {
143
+ const runtimeContext = {
144
+ isBrowser: false,
145
+ requestContext: {},
146
+ context: {},
147
+ initialData: {},
148
+ __i18nData__: {},
149
+ ssrContext: {
150
+ request: {
151
+ params: {},
152
+ query: {},
153
+ pathname: '/',
154
+ host: 'localhost',
155
+ url: 'http://localhost/',
156
+ headers: {},
157
+ },
158
+ reporter: { sessionId: 'session-1' },
159
+ },
160
+ };
161
+
162
+ if (options.withRouterBootstrap) {
163
+ applyRouterRuntimeState(runtimeContext as any, {
164
+ framework: 'react-router',
165
+ serverSnapshot: {
166
+ hydrationScripts: [TSR_BOOTSTRAP],
167
+ },
168
+ });
169
+ }
170
+
171
+ return runtimeContext;
172
+ };
173
+
174
+ const render = async (
175
+ htmlTemplate: string,
176
+ options: { entryName?: string; withRouterBootstrap?: boolean } = {},
177
+ ) => {
178
+ registerPlugin([rendererHeadPlugin()]);
179
+
180
+ return renderString(
181
+ new Request('http://localhost/'),
182
+ React.createElement('main', null, 'server markup'),
183
+ {
184
+ resource: {
185
+ entryName: options.entryName ?? 'index',
186
+ htmlTemplate,
187
+ routeManifest: {},
188
+ },
189
+ runtimeContext: createRuntimeContext({
190
+ withRouterBootstrap: options.withRouterBootstrap ?? true,
191
+ }),
192
+ config: {},
193
+ onError: () => {},
194
+ onTiming: () => {},
195
+ } as any,
196
+ );
197
+ };
198
+
199
+ describe('renderString template assembly (string-mode script ordering)', () => {
200
+ it('should emit the SSR data + router bootstrap before the entry script', async () => {
201
+ const html = await render(
202
+ [
203
+ '<html><head>',
204
+ '<script src="/static/js/index.js" async></script>',
205
+ '<!--<?- chunksMap.css ?>-->',
206
+ '</head><body><div id="root">',
207
+ '<!--<?- html ?>-->',
208
+ '</div>',
209
+ '<!--<?- chunksMap.js ?>-->',
210
+ SSR_DATA_PLACEHOLDER,
211
+ '</body></html>',
212
+ ].join(''),
213
+ );
214
+
215
+ const entryIndex = html.indexOf(
216
+ '<script src="/static/js/index.js" async></script>',
217
+ );
218
+ const ssrDataIndex = html.indexOf('window._SSR_DATA =');
219
+ const routerBootstrapIndex = html.indexOf('window.$_TSR =');
220
+
221
+ expect(ssrDataIndex).toBeGreaterThan(-1);
222
+ expect(routerBootstrapIndex).toBeGreaterThan(ssrDataIndex);
223
+ expect(entryIndex).toBeGreaterThan(routerBootstrapIndex);
224
+ expect(html).not.toContain(SSR_DATA_PLACEHOLDER);
225
+ });
226
+
227
+ it('should preserve a custom template that omits script markers', async () => {
228
+ const template =
229
+ '<html><head><script async src="/static/js/index.js"></script></head><body>custom shell</body></html>';
230
+ const runtimeContext = createRuntimeContext({ withRouterBootstrap: true });
231
+ runtimeContext.ssrContext.request.headers = {
232
+ 'x-request-id': 'request-1',
233
+ };
234
+
235
+ const html = await renderString(
236
+ new Request('http://localhost/'),
237
+ React.createElement('main', null, 'server markup'),
238
+ {
239
+ resource: {
240
+ entryName: 'index',
241
+ htmlTemplate: template,
242
+ routeManifest: {},
243
+ },
244
+ runtimeContext,
245
+ config: {
246
+ ssr: {
247
+ unsafeHeaders: ['x-request-id'],
248
+ },
249
+ },
250
+ onError: () => {},
251
+ onTiming: () => {},
252
+ } as any,
253
+ );
254
+
255
+ expect(html).toBe(template);
256
+ });
257
+ });
258
+
259
+ import vm from 'node:vm';
260
+
261
+ const createScripts = (options?: {
262
+ useJsonScript?: boolean;
263
+ nonce?: string;
264
+ unsafeHeaders?: string[];
265
+ routerServerSnapshot?: {
266
+ routerData?: {
267
+ loaderData?: Record<string, unknown>;
268
+ errors?: Record<string, unknown>;
269
+ };
270
+ hydrationScript?: string;
271
+ hydrationScripts?: string[];
272
+ };
273
+ }) => {
274
+ const chunkSet = {
275
+ renderLevel: RenderLevel.SERVER_RENDER,
276
+ ssrScripts: '',
277
+ jsChunk: '',
278
+ cssChunk: '',
279
+ };
280
+
281
+ const runtimeContext = {
282
+ initialData: { name: 'modern.js' },
283
+ __i18nData__: {},
284
+ } as any;
285
+ if (options?.routerServerSnapshot) {
286
+ applyRouterRuntimeState(runtimeContext, {
287
+ framework: 'react-router',
288
+ serverSnapshot: options.routerServerSnapshot,
289
+ });
290
+ }
291
+
292
+ const collector = createDataCollector({
293
+ runtimeContext,
294
+ request: new Request('http://localhost/'),
295
+ chunkSet,
296
+ ssrContext: {
297
+ request: {
298
+ params: {},
299
+ query: {},
300
+ pathname: '/',
301
+ host: 'localhost',
302
+ url: 'http://localhost/',
303
+ headers: {
304
+ authorization: 'Bearer secret',
305
+ cookie: 'sid=abc',
306
+ 'x-request-id': 'req-1',
307
+ 'x-internal-secret': 'hidden',
308
+ },
309
+ },
310
+ reporter: { sessionId: 'session-1' },
311
+ } as any,
312
+ ssrConfig: {
313
+ unsafeHeaders: options?.unsafeHeaders,
314
+ } as any,
315
+ nonce: options?.nonce,
316
+ useJsonScript: options?.useJsonScript,
317
+ });
318
+
319
+ collector.effect();
320
+ return chunkSet.ssrScripts;
321
+ };
322
+
323
+ const parseScripts = (html: string) =>
324
+ [
325
+ ...html.matchAll(
326
+ /<script(?<attributes>[^>]*)>(?<body>[\s\S]*?)<\/script>/gu,
327
+ ),
328
+ ].map(match => ({
329
+ attributes: Object.fromEntries(
330
+ [
331
+ ...(match.groups?.attributes ?? '').matchAll(/([\w-]+)="([^"]*)"/gu),
332
+ ].map(attribute => [attribute[1], attribute[2]]),
333
+ ),
334
+ body: match.groups?.body ?? '',
335
+ }));
336
+
337
+ const executeScripts = (html: string) => {
338
+ const browser = {} as Record<string, any>;
339
+ const context = vm.createContext({ window: browser });
340
+ for (const script of parseScripts(html)) {
341
+ vm.runInContext(script.body, context);
342
+ }
343
+ return browser;
344
+ };
345
+
346
+ describe('SSR data script generation', () => {
347
+ it('should use router snapshot data and hydration script when present', () => {
348
+ const browser = executeScripts(
349
+ createScripts({
350
+ routerServerSnapshot: {
351
+ routerData: {
352
+ loaderData: { route: { ok: true } },
353
+ errors: {},
354
+ },
355
+ hydrationScript: '<script>window.__ROUTER_SSR__ = true;</script>',
356
+ },
357
+ }),
358
+ );
359
+
360
+ expect(browser.__ROUTER_SSR__).toBe(true);
361
+ expect(browser._ROUTER_DATA).toEqual({
362
+ errors: {},
363
+ loaderData: { route: { ok: true } },
364
+ });
365
+ });
366
+ });
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { renderToString } from 'react-dom/server';
3
+ import {
4
+ getGlobalInternalRuntimeContext,
5
+ setGlobalContext,
6
+ setGlobalInternalRuntimeContext,
7
+ } from '../../plugin-runtime/src/core/context';
8
+ import { registerPlugin } from '../../plugin-runtime/src/core/plugin';
9
+ import rendererHeadPlugin, {
10
+ RENDERER_HEAD_PRE_PLUGINS,
11
+ } from '../src/runtimePlugin';
12
+
13
+ const previousRuntime = getGlobalInternalRuntimeContext();
14
+
15
+ afterEach(() => {
16
+ setGlobalInternalRuntimeContext(previousRuntime);
17
+ });
18
+
19
+ /** A stand-in for the router: it owns the root for a file-system routes entry. */
20
+ const routerLikePlugin = (content: React.ReactNode) => ({
21
+ name: '@modern-js/plugin-router',
22
+ setup(api: any) {
23
+ api.wrapRoot((App: React.ComponentType<any> | undefined) => {
24
+ const RouterWrapper = (props: any) =>
25
+ App
26
+ ? React.createElement(App, props, content)
27
+ : React.createElement(React.Fragment, null, content);
28
+ return RouterWrapper;
29
+ });
30
+ },
31
+ });
32
+
33
+ const renderRoot = () => {
34
+ const hooks = getGlobalInternalRuntimeContext().hooks;
35
+ // `App` is undefined for a routes entry until the router defines it.
36
+ const Root = hooks.wrapRoot.call(undefined as any);
37
+ return renderToString(React.createElement(Root));
38
+ };
39
+
40
+ describe('renderer head root wrapping', () => {
41
+ beforeEach(() => {
42
+ setGlobalContext({ enableRsc: false });
43
+ });
44
+
45
+ it('declares the runtime plugins it must wrap', () => {
46
+ expect(RENDERER_HEAD_PRE_PLUGINS).toContain('@modern-js/plugin-router');
47
+ expect(rendererHeadPlugin().pre).toEqual(RENDERER_HEAD_PRE_PLUGINS);
48
+ });
49
+
50
+ it('renders the router root when it is registered after the router', () => {
51
+ registerPlugin([
52
+ routerLikePlugin(React.createElement('main', null, 'routed')),
53
+ rendererHeadPlugin(),
54
+ ]);
55
+
56
+ expect(renderRoot()).toContain('routed');
57
+ });
58
+
59
+ it('still renders when its descriptor is emitted before the router', () => {
60
+ // The CLI appends the renderer descriptor first for a plain `appTools()`
61
+ // app. Without an undefined-root fallback this threw
62
+ // "Element type is invalid ... but got: undefined" during SSR.
63
+ registerPlugin([
64
+ rendererHeadPlugin(),
65
+ routerLikePlugin(React.createElement('main', null, 'routed')),
66
+ ]);
67
+
68
+ expect(renderRoot()).toContain('routed');
69
+ });
70
+ });
@@ -0,0 +1,42 @@
1
+ import { createRouteHydrationScriptTags as createTags } from '@modern-js/runtime-extensions';
2
+ import {
3
+ applyRouterRuntimeState,
4
+ getRouterMatchedRouteIds,
5
+ } from '@modern-js/runtime-extensions/router-state';
6
+
7
+ const createRouteHydrationScriptTags = (
8
+ context: any,
9
+ entryName: string,
10
+ options: any,
11
+ ) =>
12
+ createTags(
13
+ context.routeManifest,
14
+ getRouterMatchedRouteIds(context) ?? [],
15
+ entryName,
16
+ options,
17
+ );
18
+
19
+ describe('route hydration script serialization', () => {
20
+ it('quotes and escapes asset and nonce attributes', () => {
21
+ const runtimeContext = {} as any;
22
+ applyRouterRuntimeState(runtimeContext, {
23
+ framework: 'react-router',
24
+ serverSnapshot: { matchedRouteIds: ['route-a'] },
25
+ });
26
+ runtimeContext.routeManifest = {
27
+ routeAssets: {
28
+ 'route-a': {
29
+ assets: ['/route" onload="alert(1)&x=<tag>.js'],
30
+ },
31
+ },
32
+ };
33
+
34
+ expect(
35
+ createRouteHydrationScriptTags(runtimeContext, 'main', {
36
+ nonce: 'nonce"&<value>',
37
+ }),
38
+ ).toBe(
39
+ '<script src="/route&quot; onload=&quot;alert(1)&amp;x=&lt;tag&gt;.js" nonce="nonce&quot;&amp;&lt;value&gt;"></script>',
40
+ );
41
+ });
42
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "@modern-js/tsconfig/base",
3
+ "compilerOptions": {
4
+ "declaration": false,
5
+ "isolatedModules": true,
6
+ "rootDir": "./src",
7
+ "types": ["node"]
8
+ },
9
+ "include": ["src"]
10
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # @bleedingdev/modern-js-runtime-renderer-extensions
2
-
3
- This 0.0.0 version only registers the package name so npm trusted publishing can be configured. It contains no runtime implementation. Use the subsequent UltraModern.js release, published from the repository workflow with provenance.