@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.
- package/LICENSE +21 -0
- package/dist/cjs/assetPolicy.js +89 -0
- package/dist/cjs/federatedCss.js +57 -0
- package/dist/cjs/node.js +53 -0
- package/dist/cjs/runtimePlugin.js +131 -0
- package/dist/cjs/templatePolicy.js +76 -0
- package/dist/esm/assetPolicy.mjs +48 -0
- package/dist/esm/federatedCss.mjs +19 -0
- package/dist/esm/runtimePlugin.mjs +78 -0
- package/dist/esm/templatePolicy.mjs +38 -0
- package/dist/esm-node/assetPolicy.mjs +49 -0
- package/dist/esm-node/federatedCss.mjs +20 -0
- package/dist/esm-node/node.mjs +11 -0
- package/dist/esm-node/runtimePlugin.mjs +79 -0
- package/dist/esm-node/templatePolicy.mjs +39 -0
- package/dist/types/assetPolicy.d.ts +19 -0
- package/dist/types/federatedCss.d.ts +6 -0
- package/dist/types/node.d.ts +3 -0
- package/dist/types/runtimePlugin.d.ts +7 -0
- package/dist/types/templatePolicy.d.ts +2 -0
- package/package.json +61 -7
- package/rslib.config.mts +19 -0
- package/rstest.config.mts +29 -0
- package/src/assetPolicy.ts +86 -0
- package/src/federatedCss.ts +43 -0
- package/src/node.ts +16 -0
- package/src/runtimePlugin.ts +119 -0
- package/src/templatePolicy.ts +69 -0
- package/tests/federatedCss.test.ts +80 -0
- package/tests/headRuntime.test.tsx +334 -0
- package/tests/nativeAssetPolicy.test.tsx +515 -0
- package/tests/nativeStreamHead.test.tsx +77 -0
- package/tests/nativeStringHead.test.tsx +128 -0
- package/tests/nativeTemplatePolicy.test.tsx +818 -0
- package/tests/runtimePlugin.test.tsx +37 -0
- package/tests/scriptOrder.security.test.ts +42 -0
- package/tsconfig.json +10 -0
- 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,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;
|
package/package.json
CHANGED
|
@@ -1,16 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bleedingdev/modern-js-runtime-renderer-extensions",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
-
"
|
|
11
|
-
"
|
|
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
|
}
|
package/rslib.config.mts
ADDED
|
@@ -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
|
+
}
|