@bleedingdev/modern-js-runtime-renderer-extensions 3.9.0-ultramodern.16 → 3.9.0-ultramodern.18

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.
@@ -61,11 +61,12 @@ function createRendererHeadPlugin(options = {}) {
61
61
  const HeadRoot = (props)=>head.wrapClientRoot(Root ? external_react_default().createElement(Root, props) : props?.children ?? null);
62
62
  return HeadRoot;
63
63
  });
64
- const createState = (runtimeContext)=>{
64
+ const createState = ({ runtimeContext, monitors })=>{
65
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);
66
67
  const lifecycle = {
67
68
  beforeReact () {
68
- (0, runtime_extensions_namespaceObject.beginHeadRender)(runtimeContext);
69
+ (0, runtime_extensions_namespaceObject.beginHeadRender)(runtimeContext, reportLateHead);
69
70
  },
70
71
  completedBody (html, { phase }) {
71
72
  if ('complete' === phase) return (0, runtime_extensions_namespaceObject.completeHeadRender)(runtimeContext, html);
@@ -90,7 +91,7 @@ function createRendererHeadPlugin(options = {}) {
90
91
  };
91
92
  };
92
93
  api.extendStringSSRCollectors(({ render })=>{
93
- const state = createState(render.runtimeContext);
94
+ const state = createState(render);
94
95
  return {
95
96
  ...state.lifecycle,
96
97
  ...(0, external_assetPolicy_js_namespaceObject.createAssetPolicy)(render),
@@ -103,7 +104,7 @@ function createRendererHeadPlugin(options = {}) {
103
104
  const { runtimeContext, terminalMarker } = info;
104
105
  const processNodeStream = options.processNodeStream;
105
106
  if ('node' === info.platform && void 0 === processNodeStream) throw new Error('Node head rendering requires the Node runtime-renderer-extensions entry');
106
- const state = createState(runtimeContext);
107
+ const state = createState(info);
107
108
  return {
108
109
  ...state.lifecycle,
109
110
  ...(0, external_assetPolicy_js_namespaceObject.createAssetPolicy)(info),
@@ -16,11 +16,12 @@ function createRendererHeadPlugin(options = {}) {
16
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),
@@ -17,11 +17,12 @@ function createRendererHeadPlugin(options = {}) {
17
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),
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "directory": "packages/runtime/renderer-extensions"
12
12
  },
13
13
  "license": "MIT",
14
- "version": "3.9.0-ultramodern.16",
14
+ "version": "3.9.0-ultramodern.18",
15
15
  "engines": {
16
16
  "node": ">=26.7.0"
17
17
  },
@@ -36,12 +36,12 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.16",
40
- "@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.16"
39
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.18",
40
+ "@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.18"
41
41
  },
42
42
  "devDependencies": {
43
- "@modern-js/render": "npm:@bleedingdev/modern-js-render@3.9.0-ultramodern.16",
44
- "@modern-js/tsconfig": "npm:@bleedingdev/modern-js-tsconfig@3.9.0-ultramodern.16",
43
+ "@modern-js/render": "npm:@bleedingdev/modern-js-render@3.9.0-ultramodern.18",
44
+ "@modern-js/tsconfig": "npm:@bleedingdev/modern-js-tsconfig@3.9.0-ultramodern.18",
45
45
  "@rslib/core": "1.0.2",
46
46
  "@scripts/rstest-config": "2.66.0",
47
47
  "@testing-library/dom": "^10.4.2",
package/rstest.config.mts CHANGED
@@ -1,3 +1,4 @@
1
+ import path from 'node:path';
1
2
  import type { ProjectConfig } from '@rstest/core';
2
3
  import { withTestPreset } from '@scripts/rstest-config';
3
4
 
@@ -5,6 +6,17 @@ const commonConfig: ProjectConfig = {
5
6
  root: __dirname,
6
7
  globals: true,
7
8
  setupFiles: ['@scripts/rstest-config/setup.ts'],
9
+ resolve: {
10
+ alias: {
11
+ // The plugin-runtime sources under test reach their contexts through the
12
+ // package self-reference; resolve it to the same source module the tests
13
+ // import, as plugin-runtime's own tsconfig paths do.
14
+ '@modern-js/runtime/context$': path.join(
15
+ __dirname,
16
+ '../plugin-runtime/src/core/context/index.ts',
17
+ ),
18
+ },
19
+ },
8
20
  tools: {
9
21
  swc: {
10
22
  jsc: {
@@ -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';
@@ -51,11 +52,15 @@ export function createRendererHeadPlugin(
51
52
  return HeadRoot;
52
53
  });
53
54
 
54
- const createState = (runtimeContext: object) => {
55
+ const createState = ({ runtimeContext, monitors }: SSRRenderInfo) => {
55
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);
56
61
  const lifecycle: SSRRenderLifecycle = {
57
62
  beforeReact() {
58
- beginHeadRender(runtimeContext);
63
+ beginHeadRender(runtimeContext, reportLateHead);
59
64
  },
60
65
  completedBody(html, { phase }) {
61
66
  if (phase === 'complete') {
@@ -80,7 +85,7 @@ export function createRendererHeadPlugin(
80
85
  };
81
86
 
82
87
  api.extendStringSSRCollectors(({ render }): Collector => {
83
- const state = createState(render.runtimeContext);
88
+ const state = createState(render);
84
89
  return {
85
90
  ...state.lifecycle,
86
91
  ...createAssetPolicy(render),
@@ -97,7 +102,7 @@ export function createRendererHeadPlugin(
97
102
  'Node head rendering requires the Node runtime-renderer-extensions entry',
98
103
  );
99
104
  }
100
- const state = createState(runtimeContext);
105
+ const state = createState(info);
101
106
  return {
102
107
  ...state.lifecycle,
103
108
  ...createAssetPolicy(info),
@@ -48,6 +48,7 @@ function wrapRuntimeContextProvider(
48
48
  platform: 'node',
49
49
  mode: 'string',
50
50
  isRsc: getGlobalEnableRsc() === true,
51
+ monitors: console,
51
52
  },
52
53
  });
53
54
  for (const collector of collectors) root = collector.collect?.(root) ?? root;
@@ -21,6 +21,7 @@ const lifecycleFor = (options: any, mode: 'string' | 'stream' = 'stream') => {
21
21
  platform: 'node' as const,
22
22
  mode,
23
23
  isRsc: false,
24
+ monitors: console,
24
25
  };
25
26
  return createSSRRenderLifecycle(
26
27
  mode === 'string'
@@ -1,4 +1,7 @@
1
+ import { LATE_HEAD_MESSAGE } from '@modern-js/runtime-extensions';
1
2
  import React from 'react';
3
+ import { storage } from '../../../toolkit/runtime-utils/src/node';
4
+ import { defaultMonitors } from '../../plugin-runtime/src/core/context/monitors/default';
2
5
  import { registerPlugin } from '../../plugin-runtime/src/core/plugin';
3
6
  import { renderStreaming } from '../../plugin-runtime/src/core/server/stream';
4
7
  import { Helmet } from '../../plugin-runtime/src/exports/head';
@@ -24,20 +27,40 @@ const createRuntimeContext = () => ({
24
27
  },
25
28
  });
26
29
 
27
- const renderDocument = async (root: React.ReactElement) => {
30
+ const renderDocument = async (
31
+ root: React.ReactElement,
32
+ {
33
+ headers = {},
34
+ onShell,
35
+ }: { headers?: Record<string, string>; onShell?: () => void } = {},
36
+ ) => {
28
37
  registerPlugin([rendererHeadPlugin()]);
29
- const stream = await renderStreaming(new Request('http://localhost/'), root, {
30
- resource: {
31
- entryName: 'index',
32
- htmlTemplate: '<html><head></head><body><!--<?- html ?>--></body></html>',
33
- routeManifest: {},
34
- },
35
- runtimeContext: createRuntimeContext(),
36
- config: {},
37
- onError: () => {},
38
- onTiming: () => {},
39
- } as any);
40
- return new Response(stream).text();
38
+ return storage.run({}, async () => {
39
+ const stream = await renderStreaming(
40
+ new Request('http://localhost/', { headers }),
41
+ root,
42
+ {
43
+ resource: {
44
+ entryName: 'index',
45
+ htmlTemplate:
46
+ '<html><head></head><body><!--<?- html ?>--></body></html>',
47
+ routeManifest: {},
48
+ },
49
+ runtimeContext: createRuntimeContext(),
50
+ config: {},
51
+ onError: () => {},
52
+ onTiming: () => {},
53
+ } as any,
54
+ );
55
+ const reader = stream.pipeThrough(new TextDecoderStream()).getReader();
56
+ let html = '';
57
+ for (;;) {
58
+ const { done, value } = await reader.read();
59
+ if (done) return html;
60
+ if (html === '') onShell?.();
61
+ html += value;
62
+ }
63
+ });
41
64
  };
42
65
 
43
66
  describe('streaming Helmet collection', () => {
@@ -98,4 +121,74 @@ describe('streaming Helmet collection', () => {
98
121
  );
99
122
  expect(html).toContain('<main>Route content. ');
100
123
  });
124
+
125
+ describe('a Helmet in a boundary that completes after the shell', () => {
126
+ const createLateTree = () => {
127
+ let resolve!: () => void;
128
+ const ready = new Promise<void>(done => {
129
+ resolve = done;
130
+ });
131
+ let settled = false;
132
+ ready.then(() => {
133
+ settled = true;
134
+ });
135
+ const Late = () => {
136
+ if (!settled) throw ready;
137
+ return (
138
+ <Helmet>
139
+ <title>Late title</title>
140
+ </Helmet>
141
+ );
142
+ };
143
+ // A root-level Suspense could hold the document preamble, so React
144
+ // would wait for it; apps render routes inside a container.
145
+ const tree = (
146
+ <div id="app">
147
+ <Helmet>
148
+ <meta name="shell" content="committed" />
149
+ </Helmet>
150
+ <React.Suspense fallback="loading">
151
+ <Late />
152
+ </React.Suspense>
153
+ </div>
154
+ );
155
+ return { tree, resolve };
156
+ };
157
+
158
+ afterEach(() => {
159
+ rstest.restoreAllMocks();
160
+ });
161
+
162
+ it('reports the dropped head tags', async () => {
163
+ const error = rstest
164
+ .spyOn(defaultMonitors, 'error')
165
+ .mockImplementation(() => {});
166
+ const { tree, resolve } = createLateTree();
167
+ const html = await renderDocument(tree, { onShell: resolve });
168
+
169
+ const head = html.slice(0, html.indexOf('</head>'));
170
+ expect(head).toContain('name="shell" content="committed"');
171
+ expect(html).toContain('loading');
172
+ expect(head).not.toContain('Late title');
173
+ expect(error).toHaveBeenCalledTimes(1);
174
+ expect(error).toHaveBeenCalledWith(LATE_HEAD_MESSAGE);
175
+ expect(LATE_HEAD_MESSAGE).toContain('<Helmet>');
176
+ });
177
+
178
+ it('gives a bot the full head', async () => {
179
+ const error = rstest.spyOn(defaultMonitors, 'error');
180
+ const warn = rstest.spyOn(defaultMonitors, 'warn');
181
+ const { tree, resolve } = createLateTree();
182
+ setTimeout(resolve, 0);
183
+ const html = await renderDocument(tree, {
184
+ headers: { 'user-agent': 'Googlebot/2.1' },
185
+ });
186
+
187
+ const head = html.slice(0, html.indexOf('</head>'));
188
+ expect(head).toContain('name="shell" content="committed"');
189
+ expect(head).toMatch(/<title[^>]*>Late title<\/title>/);
190
+ expect(error).not.toHaveBeenCalled();
191
+ expect(warn).not.toHaveBeenCalled();
192
+ });
193
+ });
101
194
  });
@@ -21,6 +21,7 @@ const lifecycleFor = (options: any, mode: 'string' | 'stream' = 'stream') => {
21
21
  platform: 'node' as const,
22
22
  mode,
23
23
  isRsc: false,
24
+ monitors: console,
24
25
  };
25
26
  return createSSRRenderLifecycle(
26
27
  mode === 'string'