@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,20 @@
1
+ import "node:module";
2
+ import { escapeHtmlAttribute } from "@modern-js/runtime-extensions";
3
+ const createFederatedCssLinks = (assets, options, { attributesToString, hasStylesheetLink })=>{
4
+ if (void 0 === assets || 0 === assets.length) return '';
5
+ const seen = new Set(options.existingAssets || []);
6
+ const attributes = attributesToString(Object.fromEntries(Object.entries(options.attributes || {}).filter(([name])=>![
7
+ 'href',
8
+ 'rel'
9
+ ].includes(name.toLowerCase()))));
10
+ const links = [];
11
+ for (const asset of assets){
12
+ const href = escapeHtmlAttribute(asset);
13
+ if (!('' === asset || seen.has(asset) || hasStylesheetLink(options.template, asset) || hasStylesheetLink(options.template, href))) {
14
+ seen.add(asset);
15
+ links.push(`<link${attributes} href="${href}" rel="stylesheet" />`);
16
+ }
17
+ }
18
+ return links.join('');
19
+ };
20
+ export { createFederatedCssLinks };
@@ -0,0 +1,11 @@
1
+ import "node:module";
2
+ import { createNodeHeadMarkerStripper } from "@modern-js/runtime-extensions/node";
3
+ import { createRendererHeadPlugin } from "./runtimePlugin.mjs";
4
+ const rendererHeadPlugin = ()=>createRendererHeadPlugin({
5
+ processNodeStream (source, context, terminalMarker) {
6
+ return source.pipe(createNodeHeadMarkerStripper(context, terminalMarker));
7
+ }
8
+ });
9
+ const node = rendererHeadPlugin;
10
+ export default node;
11
+ export { createRendererHeadPlugin, rendererHeadPlugin };
@@ -0,0 +1,79 @@
1
+ import "node:module";
2
+ import { abortHeadRender, beginHeadRender, completeHeadRender, createConservingWebShellStream, publishHeadRender } from "@modern-js/runtime-extensions";
3
+ import { projectRuntimeContext } from "@modern-js/runtime-extensions/context-projection";
4
+ import { createHeadRuntime } from "@modern-js/runtime-extensions/head-runtime";
5
+ import { ensureHelmetContext } from "@modern-js/runtime-extensions/helmet-context";
6
+ import react from "react";
7
+ import { createAssetPolicy } from "./assetPolicy.mjs";
8
+ import { createTemplatePolicy } from "./templatePolicy.mjs";
9
+ function createRendererHeadPlugin(options = {}) {
10
+ return {
11
+ name: '@modern-js/runtime-renderer-extensions',
12
+ setup (api) {
13
+ const head = createHeadRuntime();
14
+ api.resolveComponent((component, { name })=>'head.Helmet' === name ? head.Head : component);
15
+ api.transformRuntimeContext(projectRuntimeContext);
16
+ api.wrapRoot((Root)=>{
17
+ const HeadRoot = (props)=>head.wrapClientRoot(react.createElement(Root, props));
18
+ return HeadRoot;
19
+ });
20
+ const createState = (runtimeContext)=>{
21
+ const helmetContext = ensureHelmetContext(runtimeContext);
22
+ const lifecycle = {
23
+ beforeReact () {
24
+ beginHeadRender(runtimeContext);
25
+ },
26
+ completedBody (html, { phase }) {
27
+ if ('complete' === phase) return completeHeadRender(runtimeContext, html);
28
+ publishHeadRender(runtimeContext);
29
+ return html;
30
+ },
31
+ getHeadData () {
32
+ return helmetContext.helmet ?? void 0;
33
+ },
34
+ onTerminal (terminal) {
35
+ if ('complete' !== terminal.status) abortHeadRender(runtimeContext);
36
+ }
37
+ };
38
+ return {
39
+ lifecycle,
40
+ wrap (root) {
41
+ return head.wrapServerRoot(root, {
42
+ runtimeContext,
43
+ helmetContext
44
+ });
45
+ }
46
+ };
47
+ };
48
+ api.extendStringSSRCollectors(({ render })=>{
49
+ const state = createState(render.runtimeContext);
50
+ return {
51
+ ...state.lifecycle,
52
+ ...createAssetPolicy(render),
53
+ ...createTemplatePolicy(render),
54
+ collect: state.wrap,
55
+ effect () {}
56
+ };
57
+ });
58
+ api.extendStreamSSR((info)=>{
59
+ const { runtimeContext, terminalMarker } = info;
60
+ const processNodeStream = options.processNodeStream;
61
+ 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
+ return {
64
+ ...state.lifecycle,
65
+ ...createAssetPolicy(info),
66
+ ...createTemplatePolicy(info),
67
+ modifyRootElement: state.wrap,
68
+ streamPhase: 'body',
69
+ processStream: void 0 === processNodeStream ? void 0 : (source)=>processNodeStream(source, runtimeContext, terminalMarker),
70
+ processReadableStream: (source)=>createConservingWebShellStream(source, runtimeContext, terminalMarker)
71
+ };
72
+ });
73
+ }
74
+ };
75
+ }
76
+ const rendererHeadPlugin = ()=>createRendererHeadPlugin();
77
+ const runtimePlugin = rendererHeadPlugin;
78
+ export default runtimePlugin;
79
+ export { createRendererHeadPlugin, rendererHeadPlugin };
@@ -0,0 +1,39 @@
1
+ import "node:module";
2
+ import { createRouteHydrationScriptTags, replaceChunkJsPlaceholder } from "@modern-js/runtime-extensions";
3
+ import { getRouterHydrationScripts, getRouterMatchedRouteIds, getRouterServerSnapshot } from "@modern-js/runtime-extensions/router-state";
4
+ import { getRendererResources } from "./assetPolicy.mjs";
5
+ import { createFederatedCssLinks } from "./federatedCss.mjs";
6
+ function createTemplatePolicy(render) {
7
+ const { resource, routeManifest, entryName } = getRendererResources(render);
8
+ const config = render.config;
9
+ return {
10
+ getRouterData () {
11
+ return getRouterServerSnapshot(render.runtimeContext)?.routerData || void 0;
12
+ },
13
+ transformTemplateChunk (chunk, formatting) {
14
+ if ('styles' === chunk.name) return {
15
+ ...chunk,
16
+ content: chunk.content + createFederatedCssLinks(resource?.moduleFederationCssAssets, {
17
+ template: chunk.template,
18
+ attributes: chunk.attributes,
19
+ existingAssets: chunk.emittedAssets
20
+ }, formatting)
21
+ };
22
+ let content = chunk.content;
23
+ if ("scripts" === chunk.name && 'stream' === render.mode) content = createRouteHydrationScriptTags(routeManifest, getRouterMatchedRouteIds(render.runtimeContext) ?? [], entryName, {
24
+ nonce: config?.nonce,
25
+ template: chunk.template
26
+ });
27
+ else if ('data' === chunk.name) {
28
+ const scripts = getRouterHydrationScripts(render.runtimeContext);
29
+ if (scripts.length > 0) content += `\n${scripts.join('\n')}`;
30
+ }
31
+ return {
32
+ ...chunk,
33
+ content: '',
34
+ template: replaceChunkJsPlaceholder(chunk.template, content, entryName, chunk.placeholder)
35
+ };
36
+ }
37
+ };
38
+ }
39
+ export { createTemplatePolicy };
@@ -0,0 +1,19 @@
1
+ import type { SSRRenderInfo, SSRRenderLifecycle } from '@modern-js/plugin/runtime';
2
+ export type RendererRouteManifest = {
3
+ routeAssets?: Record<string, {
4
+ assets?: string[];
5
+ referenceCssAssets?: string[];
6
+ } | undefined>;
7
+ };
8
+ export type RendererResource = {
9
+ entryName?: string;
10
+ htmlTemplate?: string;
11
+ routeManifest?: RendererRouteManifest;
12
+ moduleFederationCssAssets?: string[];
13
+ };
14
+ export declare function getRendererResources(render: SSRRenderInfo): {
15
+ resource: RendererResource | undefined;
16
+ entryName: string;
17
+ routeManifest: RendererRouteManifest | undefined;
18
+ };
19
+ export declare function createAssetPolicy(render: SSRRenderInfo): Pick<SSRRenderLifecycle, 'transformAssets'>;
@@ -0,0 +1,6 @@
1
+ import type { SSRHtmlFormatting } from '@modern-js/plugin/runtime';
2
+ export declare const createFederatedCssLinks: (assets: string[] | undefined, options: {
3
+ template: string;
4
+ attributes?: Readonly<Record<string, unknown>>;
5
+ existingAssets?: Iterable<string>;
6
+ }, { attributesToString, hasStylesheetLink }: SSRHtmlFormatting) => string;
@@ -0,0 +1,3 @@
1
+ export { createRendererHeadPlugin, type RendererHeadPluginOptions, } from './runtimePlugin.js';
2
+ export declare const rendererHeadPlugin: () => import("@modern-js/plugin/runtime").RuntimePlugin<{}>;
3
+ export default rendererHeadPlugin;
@@ -0,0 +1,7 @@
1
+ import type { RuntimePlugin } from '@modern-js/plugin/runtime';
2
+ export type RendererHeadPluginOptions = {
3
+ processNodeStream?: (source: NodeJS.ReadWriteStream, context: object, terminalMarker: string) => NodeJS.ReadWriteStream;
4
+ };
5
+ export declare function createRendererHeadPlugin(options?: RendererHeadPluginOptions): RuntimePlugin<{}>;
6
+ export declare const rendererHeadPlugin: () => RuntimePlugin<{}>;
7
+ export default rendererHeadPlugin;
@@ -0,0 +1,2 @@
1
+ import type { SSRRenderInfo, SSRRenderLifecycle } from '@modern-js/plugin/runtime';
2
+ export declare function createTemplatePolicy(render: SSRRenderInfo): Pick<SSRRenderLifecycle, 'transformTemplateChunk' | 'getRouterData'>;
package/package.json CHANGED
@@ -1,16 +1,70 @@
1
1
  {
2
2
  "name": "@bleedingdev/modern-js-runtime-renderer-extensions",
3
- "version": "0.0.0",
4
- "description": "Package-name registration for the forthcoming UltraModern.js release. This setup version contains no runtime implementation.",
5
- "license": "MIT",
3
+ "description": "UltraModern.js head rendering plugin registered through native runtime hooks.",
4
+ "homepage": "https://github.com/BleedingDev/ultramodern.js#readme",
5
+ "bugs": {
6
+ "url": "https://github.com/BleedingDev/ultramodern.js/issues"
7
+ },
6
8
  "repository": {
7
9
  "type": "git",
8
- "url": "git+https://github.com/BleedingDev/ultramodern.js.git"
10
+ "url": "git+https://github.com/BleedingDev/ultramodern.js.git",
11
+ "directory": "packages/runtime/renderer-extensions"
12
+ },
13
+ "license": "MIT",
14
+ "version": "3.9.0-ultramodern.5",
15
+ "engines": {
16
+ "node": ">=26.7.0"
17
+ },
18
+ "types": "./dist/types/runtimePlugin.d.ts",
19
+ "main": "./dist/cjs/node.js",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/types/runtimePlugin.d.ts",
23
+ "node": {
24
+ "import": "./dist/esm-node/node.mjs",
25
+ "require": "./dist/cjs/node.js"
26
+ },
27
+ "default": "./dist/esm/runtimePlugin.mjs"
28
+ }
9
29
  },
10
- "files": [
11
- "README.md"
12
- ],
30
+ "typesVersions": {
31
+ "*": {
32
+ ".": [
33
+ "./dist/types/runtimePlugin.d.ts",
34
+ "./src/runtimePlugin.ts"
35
+ ]
36
+ }
37
+ },
38
+ "dependencies": {
39
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.5",
40
+ "@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.5"
41
+ },
42
+ "devDependencies": {
43
+ "@modern-js/render": "npm:@bleedingdev/modern-js-render@3.9.0-ultramodern.5",
44
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.5",
45
+ "@modern-js/tsconfig": "npm:@bleedingdev/modern-js-tsconfig@3.9.0-ultramodern.5",
46
+ "@rslib/core": "1.0.0",
47
+ "@scripts/rstest-config": "2.66.0",
48
+ "@testing-library/dom": "^10.4.1",
49
+ "@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",
54
+ "esbuild": "^0.28.2",
55
+ "react": "^19.2.8",
56
+ "react-dom": "^19.2.8",
57
+ "typescript": "^7.0.2"
58
+ },
59
+ "peerDependencies": {
60
+ "react": "^19.2.8"
61
+ },
62
+ "sideEffects": false,
13
63
  "publishConfig": {
14
64
  "access": "public"
65
+ },
66
+ "scripts": {
67
+ "build": "rslib build",
68
+ "test": "rstest"
15
69
  }
16
70
  }
@@ -0,0 +1,19 @@
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
+ });
@@ -0,0 +1,29 @@
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
+ };
@@ -0,0 +1,86 @@
1
+ import type {
2
+ SSRRenderInfo,
3
+ SSRRenderLifecycle,
4
+ } from '@modern-js/plugin/runtime';
5
+ import {
6
+ getMatchedRouteChunks,
7
+ orderHydrationScriptChunks,
8
+ } from '@modern-js/runtime-extensions';
9
+ import { getRouterMatchedRouteIds } from '@modern-js/runtime-extensions/router-state';
10
+
11
+ export type RendererRouteManifest = {
12
+ routeAssets?: Record<
13
+ string,
14
+ | {
15
+ assets?: string[];
16
+ referenceCssAssets?: string[];
17
+ }
18
+ | undefined
19
+ >;
20
+ };
21
+
22
+ export type RendererResource = {
23
+ entryName?: string;
24
+ htmlTemplate?: string;
25
+ routeManifest?: RendererRouteManifest;
26
+ moduleFederationCssAssets?: string[];
27
+ };
28
+
29
+ export function getRendererResources(render: SSRRenderInfo) {
30
+ const resource = render.resource as RendererResource | undefined;
31
+ const context = render.runtimeContext as {
32
+ routeManifest?: RendererRouteManifest;
33
+ };
34
+ return {
35
+ resource,
36
+ entryName: resource?.entryName ?? 'index',
37
+ // String mode deliberately honors an explicitly omitted resource manifest.
38
+ routeManifest:
39
+ render.mode === 'string'
40
+ ? resource?.routeManifest
41
+ : context.routeManifest,
42
+ };
43
+ }
44
+
45
+ export function createAssetPolicy(
46
+ render: SSRRenderInfo,
47
+ ): Pick<SSRRenderLifecycle, 'transformAssets'> {
48
+ const { routeManifest, entryName } = getRendererResources(render);
49
+ return {
50
+ transformAssets(assets, info) {
51
+ const matchedRouteIds =
52
+ getRouterMatchedRouteIds(render.runtimeContext) ?? [];
53
+ if (info.source === 'loadable') {
54
+ const matched = getMatchedRouteChunks(
55
+ routeManifest,
56
+ matchedRouteIds,
57
+ info.createAsset,
58
+ );
59
+ if (info.kind === 'style') return [...assets, ...matched];
60
+ const asyncEntryChunks = [
61
+ ...(info.groups.find(group => group.name === 'async-entry')?.assets ??
62
+ []),
63
+ ];
64
+ const collectedChunks = [
65
+ ...(info.groups.find(group => group.name === 'loadable')?.assets ??
66
+ []),
67
+ ];
68
+ return orderHydrationScriptChunks({
69
+ asyncEntryChunks,
70
+ collectedChunks,
71
+ matchedRouteChunks: matched,
72
+ entryName,
73
+ });
74
+ }
75
+ if (info.kind !== 'style' || matchedRouteIds.length === 0) return assets;
76
+ const routeAssets = routeManifest?.routeAssets;
77
+ if (routeAssets === undefined) return assets;
78
+ const matched = matchedRouteIds
79
+ .flatMap(routeId => routeAssets[routeId]?.referenceCssAssets ?? [])
80
+ .map(info.createAsset);
81
+ const entry =
82
+ info.groups.find(group => group.name === 'async-entry')?.assets ?? [];
83
+ return [...matched, ...entry];
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,43 @@
1
+ import type { SSRHtmlFormatting } from '@modern-js/plugin/runtime';
2
+ import { escapeHtmlAttribute } from '@modern-js/runtime-extensions';
3
+
4
+ export const createFederatedCssLinks = (
5
+ assets: string[] | undefined,
6
+ options: {
7
+ template: string;
8
+ attributes?: Readonly<Record<string, unknown>>;
9
+ existingAssets?: Iterable<string>;
10
+ },
11
+ { attributesToString, hasStylesheetLink }: SSRHtmlFormatting,
12
+ ) => {
13
+ if (assets === undefined || assets.length === 0) {
14
+ return '';
15
+ }
16
+
17
+ const seen = new Set(options.existingAssets || []);
18
+ const attributes = attributesToString(
19
+ Object.fromEntries(
20
+ Object.entries(options.attributes || {}).filter(
21
+ ([name]) => !['href', 'rel'].includes(name.toLowerCase()),
22
+ ),
23
+ ),
24
+ );
25
+ const links: string[] = [];
26
+
27
+ for (const asset of assets) {
28
+ const href = escapeHtmlAttribute(asset);
29
+ if (
30
+ asset === '' ||
31
+ seen.has(asset) ||
32
+ hasStylesheetLink(options.template, asset) ||
33
+ hasStylesheetLink(options.template, href)
34
+ ) {
35
+ continue;
36
+ }
37
+
38
+ seen.add(asset);
39
+ links.push(`<link${attributes} href="${href}" rel="stylesheet" />`);
40
+ }
41
+
42
+ return links.join('');
43
+ };
package/src/node.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { createNodeHeadMarkerStripper } from '@modern-js/runtime-extensions/node';
2
+ import { createRendererHeadPlugin } from './runtimePlugin';
3
+
4
+ export {
5
+ createRendererHeadPlugin,
6
+ type RendererHeadPluginOptions,
7
+ } from './runtimePlugin';
8
+
9
+ export const rendererHeadPlugin = () =>
10
+ createRendererHeadPlugin({
11
+ processNodeStream(source, context, terminalMarker) {
12
+ return source.pipe(createNodeHeadMarkerStripper(context, terminalMarker));
13
+ },
14
+ });
15
+
16
+ export default rendererHeadPlugin;
@@ -0,0 +1,119 @@
1
+ import type {
2
+ Collector,
3
+ RuntimePlugin,
4
+ SSRRenderLifecycle,
5
+ StreamSSRExtender,
6
+ } from '@modern-js/plugin/runtime';
7
+ import {
8
+ abortHeadRender,
9
+ beginHeadRender,
10
+ completeHeadRender,
11
+ createConservingWebShellStream,
12
+ publishHeadRender,
13
+ } from '@modern-js/runtime-extensions';
14
+ import { projectRuntimeContext } from '@modern-js/runtime-extensions/context-projection';
15
+ import { createHeadRuntime } from '@modern-js/runtime-extensions/head-runtime';
16
+ import { ensureHelmetContext } from '@modern-js/runtime-extensions/helmet-context';
17
+ import React from 'react';
18
+ import { createAssetPolicy } from './assetPolicy';
19
+ import { createTemplatePolicy } from './templatePolicy';
20
+
21
+ export type RendererHeadPluginOptions = {
22
+ processNodeStream?: (
23
+ source: NodeJS.ReadWriteStream,
24
+ context: object,
25
+ terminalMarker: string,
26
+ ) => NodeJS.ReadWriteStream;
27
+ };
28
+
29
+ export function createRendererHeadPlugin(
30
+ options: RendererHeadPluginOptions = {},
31
+ ): RuntimePlugin<{}> {
32
+ return {
33
+ name: '@modern-js/runtime-renderer-extensions',
34
+ setup(api) {
35
+ const head = createHeadRuntime();
36
+ api.resolveComponent((component, { name }) =>
37
+ name === 'head.Helmet' ? head.Head : component,
38
+ );
39
+ api.transformRuntimeContext(projectRuntimeContext);
40
+ api.wrapRoot(Root => {
41
+ const HeadRoot = (props: React.ComponentProps<typeof Root>) =>
42
+ head.wrapClientRoot(React.createElement(Root, props));
43
+ return HeadRoot;
44
+ });
45
+
46
+ const createState = (runtimeContext: object) => {
47
+ const helmetContext = ensureHelmetContext(runtimeContext);
48
+ const lifecycle: SSRRenderLifecycle = {
49
+ beforeReact() {
50
+ beginHeadRender(runtimeContext);
51
+ },
52
+ completedBody(html, { phase }) {
53
+ if (phase === 'complete') {
54
+ return completeHeadRender(runtimeContext, html);
55
+ }
56
+ publishHeadRender(runtimeContext);
57
+ return html;
58
+ },
59
+ getHeadData() {
60
+ return helmetContext.helmet ?? undefined;
61
+ },
62
+ onTerminal(terminal) {
63
+ if (terminal.status !== 'complete') abortHeadRender(runtimeContext);
64
+ },
65
+ };
66
+ return {
67
+ lifecycle,
68
+ wrap(root: React.ReactNode) {
69
+ return head.wrapServerRoot(root, { runtimeContext, helmetContext });
70
+ },
71
+ };
72
+ };
73
+
74
+ api.extendStringSSRCollectors(({ render }): Collector => {
75
+ const state = createState(render.runtimeContext);
76
+ return {
77
+ ...state.lifecycle,
78
+ ...createAssetPolicy(render),
79
+ ...createTemplatePolicy(render),
80
+ collect: state.wrap,
81
+ effect() {},
82
+ };
83
+ });
84
+ api.extendStreamSSR((info): StreamSSRExtender => {
85
+ const { runtimeContext, terminalMarker } = info;
86
+ const processNodeStream = options.processNodeStream;
87
+ if (info.platform === 'node' && processNodeStream === undefined) {
88
+ throw new Error(
89
+ 'Node head rendering requires the Node runtime-renderer-extensions entry',
90
+ );
91
+ }
92
+ const state = createState(runtimeContext);
93
+ return {
94
+ ...state.lifecycle,
95
+ ...createAssetPolicy(info),
96
+ ...createTemplatePolicy(info),
97
+ modifyRootElement: state.wrap,
98
+ streamPhase: 'body',
99
+ processStream:
100
+ processNodeStream === undefined
101
+ ? undefined
102
+ : source =>
103
+ processNodeStream(source, runtimeContext, terminalMarker),
104
+ processReadableStream: source =>
105
+ createConservingWebShellStream(
106
+ source,
107
+ runtimeContext,
108
+ terminalMarker,
109
+ ),
110
+ };
111
+ });
112
+ },
113
+ };
114
+ }
115
+
116
+ export const rendererHeadPlugin = (): RuntimePlugin<{}> =>
117
+ createRendererHeadPlugin();
118
+
119
+ export default rendererHeadPlugin;
@@ -0,0 +1,69 @@
1
+ import type {
2
+ SSRRenderInfo,
3
+ SSRRenderLifecycle,
4
+ } from '@modern-js/plugin/runtime';
5
+ import {
6
+ createRouteHydrationScriptTags,
7
+ replaceChunkJsPlaceholder,
8
+ } from '@modern-js/runtime-extensions';
9
+ import {
10
+ getRouterHydrationScripts,
11
+ getRouterMatchedRouteIds,
12
+ getRouterServerSnapshot,
13
+ } from '@modern-js/runtime-extensions/router-state';
14
+ import { getRendererResources } from './assetPolicy';
15
+ import { createFederatedCssLinks } from './federatedCss';
16
+
17
+ export function createTemplatePolicy(
18
+ render: SSRRenderInfo,
19
+ ): Pick<SSRRenderLifecycle, 'transformTemplateChunk' | 'getRouterData'> {
20
+ const { resource, routeManifest, entryName } = getRendererResources(render);
21
+ const config = render.config as { nonce?: string } | undefined;
22
+ return {
23
+ getRouterData() {
24
+ return (
25
+ getRouterServerSnapshot(render.runtimeContext)?.routerData || undefined
26
+ );
27
+ },
28
+ transformTemplateChunk(chunk, formatting) {
29
+ if (chunk.name === 'styles') {
30
+ return {
31
+ ...chunk,
32
+ content:
33
+ chunk.content +
34
+ createFederatedCssLinks(
35
+ resource?.moduleFederationCssAssets,
36
+ {
37
+ template: chunk.template,
38
+ attributes: chunk.attributes,
39
+ existingAssets: chunk.emittedAssets,
40
+ },
41
+ formatting,
42
+ ),
43
+ };
44
+ }
45
+ let content = chunk.content;
46
+ if (chunk.name === 'scripts' && render.mode === 'stream') {
47
+ content = createRouteHydrationScriptTags(
48
+ routeManifest,
49
+ getRouterMatchedRouteIds(render.runtimeContext) ?? [],
50
+ entryName,
51
+ { nonce: config?.nonce, template: chunk.template },
52
+ );
53
+ } else if (chunk.name === 'data') {
54
+ const scripts = getRouterHydrationScripts(render.runtimeContext);
55
+ if (scripts.length > 0) content += `\n${scripts.join('\n')}`;
56
+ }
57
+ return {
58
+ ...chunk,
59
+ content: '',
60
+ template: replaceChunkJsPlaceholder(
61
+ chunk.template,
62
+ content,
63
+ entryName,
64
+ chunk.placeholder,
65
+ ),
66
+ };
67
+ },
68
+ };
69
+ }