@bleedingdev/modern-js-runtime-renderer-extensions 0.0.0 → 3.9.0-ultramodern.10

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 +143 -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 +87 -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 +88 -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 +13 -0
  20. package/dist/types/templatePolicy.d.ts +2 -0
  21. package/package.json +60 -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 +141 -0
  28. package/src/templatePolicy.ts +69 -0
  29. package/tests/federatedCss.test.ts +49 -0
  30. package/tests/headRuntime.test.tsx +163 -0
  31. package/tests/nativeAssetPolicy.test.tsx +377 -0
  32. package/tests/nativeStreamHead.test.tsx +77 -0
  33. package/tests/nativeStringHead.test.tsx +128 -0
  34. package/tests/nativeTemplatePolicy.test.tsx +366 -0
  35. package/tests/rootWrapOrder.test.tsx +70 -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,49 @@
1
+ import "node:module";
2
+ import { getMatchedRouteChunks, orderHydrationScriptChunks } from "@modern-js/runtime-extensions";
3
+ import { getRouterMatchedRouteIds } from "@modern-js/runtime-extensions/router-state";
4
+ function getRendererResources(render) {
5
+ const resource = render.resource;
6
+ const context = render.runtimeContext;
7
+ return {
8
+ resource,
9
+ entryName: resource?.entryName ?? 'index',
10
+ routeManifest: 'string' === render.mode ? resource?.routeManifest : context.routeManifest
11
+ };
12
+ }
13
+ function createAssetPolicy(render) {
14
+ const { routeManifest, entryName } = getRendererResources(render);
15
+ return {
16
+ transformAssets (assets, info) {
17
+ const matchedRouteIds = getRouterMatchedRouteIds(render.runtimeContext) ?? [];
18
+ if ('loadable' === info.source) {
19
+ const matched = getMatchedRouteChunks(routeManifest, matchedRouteIds, info.createAsset);
20
+ if ('style' === info.kind) return [
21
+ ...assets,
22
+ ...matched
23
+ ];
24
+ const asyncEntryChunks = [
25
+ ...info.groups.find((group)=>'async-entry' === group.name)?.assets ?? []
26
+ ];
27
+ const collectedChunks = [
28
+ ...info.groups.find((group)=>'loadable' === group.name)?.assets ?? []
29
+ ];
30
+ return orderHydrationScriptChunks({
31
+ asyncEntryChunks,
32
+ collectedChunks,
33
+ matchedRouteChunks: matched,
34
+ entryName
35
+ });
36
+ }
37
+ if ('style' !== info.kind || 0 === matchedRouteIds.length) return assets;
38
+ const routeAssets = routeManifest?.routeAssets;
39
+ if (void 0 === routeAssets) return assets;
40
+ const matched = matchedRouteIds.flatMap((routeId)=>routeAssets[routeId]?.referenceCssAssets ?? []).map(info.createAsset);
41
+ const entry = info.groups.find((group)=>'async-entry' === group.name)?.assets ?? [];
42
+ return [
43
+ ...matched,
44
+ ...entry
45
+ ];
46
+ }
47
+ };
48
+ }
49
+ export { createAssetPolicy, getRendererResources };
@@ -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,88 @@
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(Root ? react.createElement(Root, props) : props?.children ?? null);
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 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
+ });
86
+ const runtimePlugin = rendererHeadPlugin;
87
+ export default runtimePlugin;
88
+ export { RENDERER_HEAD_PRE_PLUGINS, 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,13 @@
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
+ /**
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[];
12
+ export declare const rendererHeadPlugin: () => RuntimePlugin<{}>;
13
+ 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,69 @@
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.10",
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.10",
40
+ "@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.10"
41
+ },
42
+ "devDependencies": {
43
+ "@modern-js/render": "npm:@bleedingdev/modern-js-render@3.9.0-ultramodern.10",
44
+ "@modern-js/tsconfig": "npm:@bleedingdev/modern-js-tsconfig@3.9.0-ultramodern.10",
45
+ "@rslib/core": "1.0.0",
46
+ "@scripts/rstest-config": "2.66.0",
47
+ "@testing-library/dom": "^10.4.1",
48
+ "@testing-library/react": "^16.3.3",
49
+ "@types/node": "^26.4.1",
50
+ "@types/react": "^19.2.18",
51
+ "@types/react-dom": "^19.2.7",
52
+ "@typescript/native-preview": "7.0.0-dev.20260707.2",
53
+ "esbuild": "^0.28.2",
54
+ "react": "^19.2.8",
55
+ "react-dom": "^19.2.8",
56
+ "typescript": "^7.0.2"
57
+ },
58
+ "peerDependencies": {
59
+ "react": "^19.2.8"
60
+ },
61
+ "sideEffects": false,
13
62
  "publishConfig": {
14
63
  "access": "public"
64
+ },
65
+ "scripts": {
66
+ "build": "rslib build",
67
+ "test": "rstest"
15
68
  }
16
69
  }
@@ -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;