@bleedingdev/modern-js-runtime-renderer-extensions 3.9.0-ultramodern.8 → 3.9.3-ultramodern.1

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.
@@ -37,6 +37,7 @@ var __webpack_require__ = {};
37
37
  var __webpack_exports__ = {};
38
38
  __webpack_require__.r(__webpack_exports__);
39
39
  __webpack_require__.d(__webpack_exports__, {
40
+ RENDERER_HEAD_PRE_PLUGINS: ()=>RENDERER_HEAD_PRE_PLUGINS,
40
41
  createRendererHeadPlugin: ()=>createRendererHeadPlugin,
41
42
  default: ()=>runtimePlugin,
42
43
  rendererHeadPlugin: ()=>rendererHeadPlugin
@@ -57,14 +58,15 @@ function createRendererHeadPlugin(options = {}) {
57
58
  api.resolveComponent((component, { name })=>'head.Helmet' === name ? head.Head : component);
58
59
  api.transformRuntimeContext(context_projection_namespaceObject.projectRuntimeContext);
59
60
  api.wrapRoot((Root)=>{
60
- const HeadRoot = (props)=>head.wrapClientRoot(external_react_default().createElement(Root, props));
61
+ const HeadRoot = (props)=>head.wrapClientRoot(Root ? external_react_default().createElement(Root, props) : props?.children ?? null);
61
62
  return HeadRoot;
62
63
  });
63
- const createState = (runtimeContext)=>{
64
+ const createState = ({ runtimeContext, monitors })=>{
64
65
  const helmetContext = (0, helmet_context_namespaceObject.ensureHelmetContext)(runtimeContext);
66
+ const reportLateHead = 'production' === process.env.NODE_ENV ? (message)=>monitors.warn(message) : (message)=>monitors.error(message);
65
67
  const lifecycle = {
66
68
  beforeReact () {
67
- (0, runtime_extensions_namespaceObject.beginHeadRender)(runtimeContext);
69
+ (0, runtime_extensions_namespaceObject.beginHeadRender)(runtimeContext, reportLateHead);
68
70
  },
69
71
  completedBody (html, { phase }) {
70
72
  if ('complete' === phase) return (0, runtime_extensions_namespaceObject.completeHeadRender)(runtimeContext, html);
@@ -89,7 +91,7 @@ function createRendererHeadPlugin(options = {}) {
89
91
  };
90
92
  };
91
93
  api.extendStringSSRCollectors(({ render })=>{
92
- const state = createState(render.runtimeContext);
94
+ const state = createState(render);
93
95
  return {
94
96
  ...state.lifecycle,
95
97
  ...(0, external_assetPolicy_js_namespaceObject.createAssetPolicy)(render),
@@ -102,7 +104,7 @@ function createRendererHeadPlugin(options = {}) {
102
104
  const { runtimeContext, terminalMarker } = info;
103
105
  const processNodeStream = options.processNodeStream;
104
106
  if ('node' === info.platform && void 0 === processNodeStream) throw new Error('Node head rendering requires the Node runtime-renderer-extensions entry');
105
- const state = createState(runtimeContext);
107
+ const state = createState(info);
106
108
  return {
107
109
  ...state.lifecycle,
108
110
  ...(0, external_assetPolicy_js_namespaceObject.createAssetPolicy)(info),
@@ -116,12 +118,23 @@ function createRendererHeadPlugin(options = {}) {
116
118
  }
117
119
  };
118
120
  }
119
- const rendererHeadPlugin = ()=>createRendererHeadPlugin();
121
+ const RENDERER_HEAD_PRE_PLUGINS = [
122
+ '@modern-js/plugin-router',
123
+ '@modern-js/plugin-tanstack',
124
+ '@modern-js/plugin-state',
125
+ '@modern-js/plugin-i18n'
126
+ ];
127
+ const rendererHeadPlugin = ()=>({
128
+ ...createRendererHeadPlugin(),
129
+ pre: RENDERER_HEAD_PRE_PLUGINS
130
+ });
120
131
  const runtimePlugin = rendererHeadPlugin;
132
+ exports.RENDERER_HEAD_PRE_PLUGINS = __webpack_exports__.RENDERER_HEAD_PRE_PLUGINS;
121
133
  exports.createRendererHeadPlugin = __webpack_exports__.createRendererHeadPlugin;
122
134
  exports["default"] = __webpack_exports__["default"];
123
135
  exports.rendererHeadPlugin = __webpack_exports__.rendererHeadPlugin;
124
136
  for(var __rspack_i in __webpack_exports__)if (-1 === [
137
+ "RENDERER_HEAD_PRE_PLUGINS",
125
138
  "createRendererHeadPlugin",
126
139
  "default",
127
140
  "rendererHeadPlugin"
@@ -13,14 +13,15 @@ function createRendererHeadPlugin(options = {}) {
13
13
  api.resolveComponent((component, { name })=>'head.Helmet' === name ? head.Head : component);
14
14
  api.transformRuntimeContext(projectRuntimeContext);
15
15
  api.wrapRoot((Root)=>{
16
- const HeadRoot = (props)=>head.wrapClientRoot(react.createElement(Root, props));
16
+ const HeadRoot = (props)=>head.wrapClientRoot(Root ? react.createElement(Root, props) : props?.children ?? null);
17
17
  return HeadRoot;
18
18
  });
19
- const createState = (runtimeContext)=>{
19
+ const createState = ({ runtimeContext, monitors })=>{
20
20
  const helmetContext = ensureHelmetContext(runtimeContext);
21
+ const reportLateHead = 'production' === process.env.NODE_ENV ? (message)=>monitors.warn(message) : (message)=>monitors.error(message);
21
22
  const lifecycle = {
22
23
  beforeReact () {
23
- beginHeadRender(runtimeContext);
24
+ beginHeadRender(runtimeContext, reportLateHead);
24
25
  },
25
26
  completedBody (html, { phase }) {
26
27
  if ('complete' === phase) return completeHeadRender(runtimeContext, html);
@@ -45,7 +46,7 @@ function createRendererHeadPlugin(options = {}) {
45
46
  };
46
47
  };
47
48
  api.extendStringSSRCollectors(({ render })=>{
48
- const state = createState(render.runtimeContext);
49
+ const state = createState(render);
49
50
  return {
50
51
  ...state.lifecycle,
51
52
  ...createAssetPolicy(render),
@@ -58,7 +59,7 @@ function createRendererHeadPlugin(options = {}) {
58
59
  const { runtimeContext, terminalMarker } = info;
59
60
  const processNodeStream = options.processNodeStream;
60
61
  if ('node' === info.platform && void 0 === processNodeStream) throw new Error('Node head rendering requires the Node runtime-renderer-extensions entry');
61
- const state = createState(runtimeContext);
62
+ const state = createState(info);
62
63
  return {
63
64
  ...state.lifecycle,
64
65
  ...createAssetPolicy(info),
@@ -72,7 +73,16 @@ function createRendererHeadPlugin(options = {}) {
72
73
  }
73
74
  };
74
75
  }
75
- const rendererHeadPlugin = ()=>createRendererHeadPlugin();
76
+ const RENDERER_HEAD_PRE_PLUGINS = [
77
+ '@modern-js/plugin-router',
78
+ '@modern-js/plugin-tanstack',
79
+ '@modern-js/plugin-state',
80
+ '@modern-js/plugin-i18n'
81
+ ];
82
+ const rendererHeadPlugin = ()=>({
83
+ ...createRendererHeadPlugin(),
84
+ pre: RENDERER_HEAD_PRE_PLUGINS
85
+ });
76
86
  const runtimePlugin = rendererHeadPlugin;
77
87
  export default runtimePlugin;
78
- export { createRendererHeadPlugin, rendererHeadPlugin };
88
+ export { RENDERER_HEAD_PRE_PLUGINS, createRendererHeadPlugin, rendererHeadPlugin };
@@ -14,14 +14,15 @@ function createRendererHeadPlugin(options = {}) {
14
14
  api.resolveComponent((component, { name })=>'head.Helmet' === name ? head.Head : component);
15
15
  api.transformRuntimeContext(projectRuntimeContext);
16
16
  api.wrapRoot((Root)=>{
17
- const HeadRoot = (props)=>head.wrapClientRoot(react.createElement(Root, props));
17
+ const HeadRoot = (props)=>head.wrapClientRoot(Root ? react.createElement(Root, props) : props?.children ?? null);
18
18
  return HeadRoot;
19
19
  });
20
- const createState = (runtimeContext)=>{
20
+ const createState = ({ runtimeContext, monitors })=>{
21
21
  const helmetContext = ensureHelmetContext(runtimeContext);
22
+ const reportLateHead = 'production' === process.env.NODE_ENV ? (message)=>monitors.warn(message) : (message)=>monitors.error(message);
22
23
  const lifecycle = {
23
24
  beforeReact () {
24
- beginHeadRender(runtimeContext);
25
+ beginHeadRender(runtimeContext, reportLateHead);
25
26
  },
26
27
  completedBody (html, { phase }) {
27
28
  if ('complete' === phase) return completeHeadRender(runtimeContext, html);
@@ -46,7 +47,7 @@ function createRendererHeadPlugin(options = {}) {
46
47
  };
47
48
  };
48
49
  api.extendStringSSRCollectors(({ render })=>{
49
- const state = createState(render.runtimeContext);
50
+ const state = createState(render);
50
51
  return {
51
52
  ...state.lifecycle,
52
53
  ...createAssetPolicy(render),
@@ -59,7 +60,7 @@ function createRendererHeadPlugin(options = {}) {
59
60
  const { runtimeContext, terminalMarker } = info;
60
61
  const processNodeStream = options.processNodeStream;
61
62
  if ('node' === info.platform && void 0 === processNodeStream) throw new Error('Node head rendering requires the Node runtime-renderer-extensions entry');
62
- const state = createState(runtimeContext);
63
+ const state = createState(info);
63
64
  return {
64
65
  ...state.lifecycle,
65
66
  ...createAssetPolicy(info),
@@ -73,7 +74,16 @@ function createRendererHeadPlugin(options = {}) {
73
74
  }
74
75
  };
75
76
  }
76
- const rendererHeadPlugin = ()=>createRendererHeadPlugin();
77
+ const RENDERER_HEAD_PRE_PLUGINS = [
78
+ '@modern-js/plugin-router',
79
+ '@modern-js/plugin-tanstack',
80
+ '@modern-js/plugin-state',
81
+ '@modern-js/plugin-i18n'
82
+ ];
83
+ const rendererHeadPlugin = ()=>({
84
+ ...createRendererHeadPlugin(),
85
+ pre: RENDERER_HEAD_PRE_PLUGINS
86
+ });
77
87
  const runtimePlugin = rendererHeadPlugin;
78
88
  export default runtimePlugin;
79
- export { createRendererHeadPlugin, rendererHeadPlugin };
89
+ export { RENDERER_HEAD_PRE_PLUGINS, createRendererHeadPlugin, rendererHeadPlugin };
@@ -3,5 +3,11 @@ export type RendererHeadPluginOptions = {
3
3
  processNodeStream?: (source: NodeJS.ReadWriteStream, context: object, terminalMarker: string) => NodeJS.ReadWriteStream;
4
4
  };
5
5
  export declare function createRendererHeadPlugin(options?: RendererHeadPluginOptions): RuntimePlugin<{}>;
6
+ /**
7
+ * The plugins whose root this one wraps. Declaring them keeps the head
8
+ * provider outside their trees no matter what order the CLI emitted the
9
+ * runtime descriptors in.
10
+ */
11
+ export declare const RENDERER_HEAD_PRE_PLUGINS: string[];
6
12
  export declare const rendererHeadPlugin: () => RuntimePlugin<{}>;
7
13
  export default rendererHeadPlugin;
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@bleedingdev/modern-js-runtime-renderer-extensions",
3
3
  "description": "UltraModern.js head rendering plugin registered through native runtime hooks.",
4
- "homepage": "https://github.com/BleedingDev/ultramodern.js#readme",
4
+ "homepage": "https://github.com/ultramodernjs/ultramodern.js#readme",
5
5
  "bugs": {
6
- "url": "https://github.com/BleedingDev/ultramodern.js/issues"
6
+ "url": "https://github.com/ultramodernjs/ultramodern.js/issues"
7
7
  },
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/BleedingDev/ultramodern.js.git",
10
+ "url": "git+https://github.com/ultramodernjs/ultramodern.js.git",
11
11
  "directory": "packages/runtime/renderer-extensions"
12
12
  },
13
13
  "license": "MIT",
14
- "version": "3.9.0-ultramodern.8",
14
+ "version": "3.9.3-ultramodern.1",
15
15
  "engines": {
16
- "node": ">=26.7.0"
16
+ "node": ">=26.10.0"
17
17
  },
18
18
  "types": "./dist/types/runtimePlugin.d.ts",
19
19
  "main": "./dist/cjs/node.js",
@@ -36,25 +36,23 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.8",
40
- "@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.8"
39
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.3-ultramodern.1",
40
+ "@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.3-ultramodern.1"
41
41
  },
42
42
  "devDependencies": {
43
- "@modern-js/render": "npm:@bleedingdev/modern-js-render@3.9.0-ultramodern.8",
44
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.8",
45
- "@modern-js/tsconfig": "npm:@bleedingdev/modern-js-tsconfig@3.9.0-ultramodern.8",
46
- "@rslib/core": "1.0.0",
43
+ "@modern-js/render": "npm:@bleedingdev/modern-js-render@3.9.3-ultramodern.1",
44
+ "@modern-js/tsconfig": "npm:@bleedingdev/modern-js-tsconfig@3.9.3-ultramodern.1",
45
+ "@rslib/core": "1.0.3",
47
46
  "@scripts/rstest-config": "2.66.0",
48
- "@testing-library/dom": "^10.4.1",
47
+ "@testing-library/dom": "^10.4.2",
49
48
  "@testing-library/react": "^16.3.3",
50
- "@types/node": "^26.4.1",
51
- "@types/react": "^19.2.18",
52
- "@types/react-dom": "^19.2.7",
53
- "@typescript/native-preview": "7.0.0-dev.20260707.2",
49
+ "@types/node": "^26.6.3",
50
+ "@types/react": "^19.3.0",
51
+ "@types/react-dom": "^19.3.0",
54
52
  "esbuild": "^0.28.2",
55
- "react": "^19.2.8",
56
- "react-dom": "^19.2.8",
57
- "typescript": "^7.0.2"
53
+ "react": "^19.3.0",
54
+ "react-dom": "^19.3.0",
55
+ "typescript": "7.0.2"
58
56
  },
59
57
  "peerDependencies": {
60
58
  "react": "^19.2.8"
@@ -63,8 +61,15 @@
63
61
  "publishConfig": {
64
62
  "access": "public"
65
63
  },
64
+ "files": [
65
+ "dist",
66
+ "src"
67
+ ],
66
68
  "scripts": {
67
69
  "build": "rslib build",
68
70
  "test": "rstest"
71
+ },
72
+ "ultramodern": {
73
+ "sourceName": "@modern-js/runtime-renderer-extensions"
69
74
  }
70
75
  }
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  Collector,
3
3
  RuntimePlugin,
4
+ SSRRenderInfo,
4
5
  SSRRenderLifecycle,
5
6
  StreamSSRExtender,
6
7
  } from '@modern-js/plugin/runtime';
@@ -39,15 +40,27 @@ export function createRendererHeadPlugin(
39
40
  api.transformRuntimeContext(projectRuntimeContext);
40
41
  api.wrapRoot(Root => {
41
42
  const HeadRoot = (props: React.ComponentProps<typeof Root>) =>
42
- head.wrapClientRoot(React.createElement(Root, props));
43
+ // A root is only defined once the plugin that owns it (the router,
44
+ // for a file-system routes entry) has run. This plugin must keep
45
+ // working when it is registered before that one: it then wraps the
46
+ // children the later plugin hands it instead of a missing root.
47
+ head.wrapClientRoot(
48
+ Root
49
+ ? React.createElement(Root, props)
50
+ : ((props as { children?: React.ReactNode })?.children ?? null),
51
+ );
43
52
  return HeadRoot;
44
53
  });
45
54
 
46
- const createState = (runtimeContext: object) => {
55
+ const createState = ({ runtimeContext, monitors }: SSRRenderInfo) => {
47
56
  const helmetContext = ensureHelmetContext(runtimeContext);
57
+ const reportLateHead =
58
+ process.env.NODE_ENV === 'production'
59
+ ? (message: string) => monitors.warn(message)
60
+ : (message: string) => monitors.error(message);
48
61
  const lifecycle: SSRRenderLifecycle = {
49
62
  beforeReact() {
50
- beginHeadRender(runtimeContext);
63
+ beginHeadRender(runtimeContext, reportLateHead);
51
64
  },
52
65
  completedBody(html, { phase }) {
53
66
  if (phase === 'complete') {
@@ -72,7 +85,7 @@ export function createRendererHeadPlugin(
72
85
  };
73
86
 
74
87
  api.extendStringSSRCollectors(({ render }): Collector => {
75
- const state = createState(render.runtimeContext);
88
+ const state = createState(render);
76
89
  return {
77
90
  ...state.lifecycle,
78
91
  ...createAssetPolicy(render),
@@ -89,7 +102,7 @@ export function createRendererHeadPlugin(
89
102
  'Node head rendering requires the Node runtime-renderer-extensions entry',
90
103
  );
91
104
  }
92
- const state = createState(runtimeContext);
105
+ const state = createState(info);
93
106
  return {
94
107
  ...state.lifecycle,
95
108
  ...createAssetPolicy(info),
@@ -113,7 +126,21 @@ export function createRendererHeadPlugin(
113
126
  };
114
127
  }
115
128
 
116
- export const rendererHeadPlugin = (): RuntimePlugin<{}> =>
117
- createRendererHeadPlugin();
129
+ /**
130
+ * The plugins whose root this one wraps. Declaring them keeps the head
131
+ * provider outside their trees no matter what order the CLI emitted the
132
+ * runtime descriptors in.
133
+ */
134
+ export const RENDERER_HEAD_PRE_PLUGINS = [
135
+ '@modern-js/plugin-router',
136
+ '@modern-js/plugin-tanstack',
137
+ '@modern-js/plugin-state',
138
+ '@modern-js/plugin-i18n',
139
+ ];
140
+
141
+ export const rendererHeadPlugin = (): RuntimePlugin<{}> => ({
142
+ ...createRendererHeadPlugin(),
143
+ pre: RENDERER_HEAD_PRE_PLUGINS,
144
+ });
118
145
 
119
146
  export default rendererHeadPlugin;
package/rslib.config.mts DELETED
@@ -1,19 +0,0 @@
1
- import { RSLIB_CODE_ENTRY_GLOB, rslibConfig } from '@modern-js/rslib';
2
- import { defineConfig } from '@rslib/core';
3
-
4
- export default defineConfig({
5
- ...rslibConfig,
6
- lib: rslibConfig.lib.map(lib =>
7
- lib.id === 'esm-web'
8
- ? {
9
- ...lib,
10
- source: {
11
- ...lib.source,
12
- entry: {
13
- index: [RSLIB_CODE_ENTRY_GLOB, '!./src/node.ts'],
14
- },
15
- },
16
- }
17
- : lib,
18
- ),
19
- });
package/rstest.config.mts DELETED
@@ -1,29 +0,0 @@
1
- import type { ProjectConfig } from '@rstest/core';
2
- import { withTestPreset } from '@scripts/rstest-config';
3
-
4
- const commonConfig: ProjectConfig = {
5
- root: __dirname,
6
- globals: true,
7
- setupFiles: ['@scripts/rstest-config/setup.ts'],
8
- tools: {
9
- swc: {
10
- jsc: {
11
- transform: {
12
- react: {
13
- runtime: 'automatic',
14
- },
15
- },
16
- },
17
- },
18
- },
19
- };
20
-
21
- export default {
22
- projects: [
23
- withTestPreset({
24
- name: 'renderer-extensions-node',
25
- testEnvironment: 'node',
26
- extends: commonConfig,
27
- }),
28
- ],
29
- };
@@ -1,49 +0,0 @@
1
- import {
2
- attributesToString,
3
- hasStylesheetLink,
4
- } from '../../plugin-runtime/src/core/server/utils';
5
- import { createFederatedCssLinks as createLinks } from '../src/federatedCss';
6
-
7
- const createFederatedCssLinks = (
8
- assets: Parameters<typeof createLinks>[0],
9
- options: Parameters<typeof createLinks>[1],
10
- ) => createLinks(assets, options, { attributesToString, hasStylesheetLink });
11
-
12
- describe('createFederatedCssLinks', () => {
13
- it('does not dedupe a stylesheet against a longer href substring', () => {
14
- const html = createFederatedCssLinks(['/remote/theme.css'], {
15
- template: '<link href="/remote/theme.css?v=next" rel="stylesheet" />',
16
- });
17
-
18
- expect(html).toBe('<link href="/remote/theme.css" rel="stylesheet" />');
19
- });
20
-
21
- it('escapes federated css asset urls before interpolating href attributes', () => {
22
- const html = createFederatedCssLinks(
23
- ['/remote/theme" onload="alert(1)&x=<tag>.css'],
24
- {
25
- template: '',
26
- },
27
- );
28
-
29
- expect(html).toBe(
30
- '<link href="/remote/theme&quot; onload=&quot;alert(1)&amp;x=&lt;tag&gt;.css" rel="stylesheet" />',
31
- );
32
- expect(html).not.toContain('onload="alert(1)');
33
- });
34
-
35
- it('escapes extra attributes and prevents duplicate renderer-owned attributes', () => {
36
- const html = createFederatedCssLinks(['/remote/theme.css'], {
37
- template: '',
38
- attributes: {
39
- nonce: 'nonce"&<value>',
40
- href: 'https://attacker.example/override.css',
41
- rel: 'preload',
42
- },
43
- });
44
-
45
- expect(html).toBe(
46
- '<link nonce="nonce&quot;&amp;&lt;value&gt;" href="/remote/theme.css" rel="stylesheet" />',
47
- );
48
- });
49
- });
@@ -1,163 +0,0 @@
1
- import { getHelmetContext } from '@modern-js/runtime-extensions/helmet-context';
2
- import React, { useContext } from 'react';
3
- import { renderToReadableStream, renderToString } from 'react-dom/server';
4
- import {
5
- getGlobalEnableRsc,
6
- getGlobalInternalRuntimeContext,
7
- getInitialContext,
8
- InternalRuntimeContext,
9
- setGlobalContext,
10
- setGlobalInternalRuntimeContext,
11
- } from '../../plugin-runtime/src/core/context';
12
- import { registerPlugin } from '../../plugin-runtime/src/core/plugin';
13
- import {
14
- wrapRuntimeContextProvider as nativeWrapRuntimeContextProvider,
15
- wrapRuntimeComponentResolver,
16
- } from '../../plugin-runtime/src/core/react/wrapper';
17
- import { Helmet } from '../../plugin-runtime/src/exports/head';
18
- import rendererHeadPlugin from '../src/runtimePlugin';
19
-
20
- const previousRuntime = getGlobalInternalRuntimeContext();
21
- const previousRsc = getGlobalEnableRsc();
22
- beforeEach(() => {
23
- setGlobalContext({ enableRsc: false });
24
- registerPlugin([rendererHeadPlugin()]);
25
- });
26
- afterEach(() => {
27
- setGlobalInternalRuntimeContext(previousRuntime);
28
- setGlobalContext({ enableRsc: previousRsc });
29
- });
30
-
31
- // Exercise the unchanged native provider/head entry points through registered
32
- // collector providers. Transaction/lifecycle rendering is covered separately.
33
- function wrapRuntimeContextProvider(
34
- app: React.ReactElement,
35
- context: ReturnType<typeof getInitialContext>,
36
- ) {
37
- const hooks = getGlobalInternalRuntimeContext().hooks;
38
- const App = hooks.wrapRoot.call(() => app);
39
- let root = nativeWrapRuntimeContextProvider(<App />, context);
40
- if (getGlobalEnableRsc() && !context.isBrowser) {
41
- root = wrapRuntimeComponentResolver(root);
42
- }
43
- const collectors = hooks.extendStringSSRCollectors.call({
44
- chunkSet: { renderLevel: 0, ssrScripts: '', jsChunk: '', cssChunk: '' },
45
- render: {
46
- runtimeContext: context,
47
- request: new Request('https://example.test/tractors'),
48
- platform: 'node',
49
- mode: 'string',
50
- isRsc: getGlobalEnableRsc() === true,
51
- },
52
- });
53
- for (const collector of collectors) root = collector.collect?.(root) ?? root;
54
- return root;
55
- }
56
-
57
- describe('registered native runtime/head compatibility', () => {
58
- it('keeps overlapping requests on their original head contexts after projection', async () => {
59
- const projectedContexts: object[] = [];
60
- getGlobalInternalRuntimeContext().pluginAPI?.transformRuntimeContext(
61
- (projection: any) => {
62
- const internalContext = { ...projection.internalContext };
63
- delete internalContext[
64
- Symbol.for('@modern-js/runtime:context-extensions')
65
- ];
66
- projectedContexts.push(internalContext);
67
- return { ...projection, internalContext };
68
- },
69
- );
70
- const SuspendedHead = ({
71
- ready,
72
- title,
73
- }: {
74
- ready: Promise<void>;
75
- title: string;
76
- }) => {
77
- React.use(ready);
78
- return (
79
- <Helmet>
80
- <title>{title}</title>
81
- </Helmet>
82
- );
83
- };
84
- const requests = ['first request', 'second request'].map(title => {
85
- let release = () => {};
86
- const ready = new Promise<void>(resolve => {
87
- release = resolve;
88
- });
89
- const context = getInitialContext(false);
90
- return { title, ready, release, context };
91
- });
92
- const streams = await Promise.all(
93
- requests.map(({ title, ready, context }) =>
94
- renderToReadableStream(
95
- wrapRuntimeContextProvider(
96
- <React.Suspense fallback="pending">
97
- <SuspendedHead ready={ready} title={title} />
98
- </React.Suspense>,
99
- context,
100
- ),
101
- ),
102
- ),
103
- );
104
- requests[1].release();
105
- requests[0].release();
106
- await Promise.all(
107
- streams.map(async stream => {
108
- await stream.allReady;
109
- return new Response(stream).text();
110
- }),
111
- );
112
-
113
- for (const { context, title } of requests) {
114
- expect(getHelmetContext(context)?.helmet?.title.toString()).toBe(
115
- `<title data-rh="true">${title}</title>`,
116
- );
117
- }
118
- expect(projectedContexts).toHaveLength(2);
119
- for (const context of projectedContexts) {
120
- expect(getHelmetContext(context)).toBeUndefined();
121
- }
122
- });
123
-
124
- it('keeps response functions outside the RSC internal context', () => {
125
- let internalValue: ReturnType<typeof getInitialContext> | undefined;
126
-
127
- const Probe = () => {
128
- internalValue = useContext(InternalRuntimeContext);
129
- return null;
130
- };
131
-
132
- const response = {
133
- setHeader: rstest.fn(),
134
- status: rstest.fn(),
135
- locals: { tenant: 'tractor-store' },
136
- };
137
- const context = getInitialContext(false);
138
- context.ssrContext = {
139
- request: {
140
- params: { category: 'compact' },
141
- pathname: '/tractors',
142
- query: { sort: 'price' },
143
- headers: { accept: 'text/html' },
144
- host: 'example.test',
145
- url: 'https://example.test/tractors?sort=price',
146
- },
147
- response,
148
- };
149
-
150
- setGlobalContext({ enableRsc: true });
151
- try {
152
- renderToString(wrapRuntimeContextProvider(<Probe />, context));
153
- } finally {
154
- setGlobalContext({ enableRsc: false });
155
- }
156
-
157
- expect(
158
- Object.values(internalValue?.requestContext.response ?? {}).some(
159
- value => typeof value === 'function',
160
- ),
161
- ).toBe(false);
162
- });
163
- });