@bleedingdev/modern-js-runtime-renderer-extensions 3.9.0-ultramodern.17 → 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.
- package/dist/cjs/runtimePlugin.js +5 -4
- package/dist/esm/runtimePlugin.mjs +5 -4
- package/dist/esm-node/runtimePlugin.mjs +5 -4
- package/package.json +5 -5
- package/rstest.config.mts +12 -0
- package/src/runtimePlugin.ts +9 -4
- package/tests/headRuntime.test.tsx +1 -0
- package/tests/nativeAssetPolicy.test.tsx +1 -0
- package/tests/nativeStreamHead.test.tsx +106 -13
- package/tests/nativeTemplatePolicy.test.tsx +1 -0
|
@@ -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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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.
|
|
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.
|
|
40
|
-
"@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.
|
|
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.
|
|
44
|
-
"@modern-js/tsconfig": "npm:@bleedingdev/modern-js-tsconfig@3.9.0-ultramodern.
|
|
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: {
|
package/src/runtimePlugin.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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(
|
|
105
|
+
const state = createState(info);
|
|
101
106
|
return {
|
|
102
107
|
...state.lifecycle,
|
|
103
108
|
...createAssetPolicy(info),
|
|
@@ -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 (
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
});
|