@modern-js/runtime 3.0.1 → 3.0.2

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.
@@ -60,7 +60,9 @@ const genRenderCode = ({ srcDirectory, internalSrcAlias, metaName, entry, custom
60
60
  return `import { createRoot } from '@${metaName}/runtime/react';
61
61
  import { render } from '@${metaName}/runtime/browser';
62
62
 
63
- ${enableRsc ? `import { RscClientRoot, createFromReadableStream, rscStream, callServer } from '@${metaName}/runtime/rsc/client';` : ''}
63
+ ${enableRsc ? `import { RscClientRoot, createFromReadableStream, rscStream, callServer, setServerCallback } from '@${metaName}/runtime/rsc/client';` : ''}
64
+
65
+ ${enableRsc ? "setServerCallback(callServer);" : ''}
64
66
 
65
67
  ${enableRsc ? `const data = createFromReadableStream(rscStream, {
66
68
  callServer: callServer,
@@ -85,9 +87,12 @@ const entryForCSRWithRSC = ({ metaName, entryName, urlPath = '/', mountId = 'roo
85
87
  isRedirectResponse,
86
88
  rscStream,
87
89
  callServer,
90
+ setServerCallback,
88
91
  createFromReadableStream
89
92
  } from '@${metaName}/runtime/rsc/client';
90
93
 
94
+ setServerCallback(callServer);
95
+
91
96
  const handleRedirectResponse = (res: Response) => {
92
97
  const { headers } = res;
93
98
  const location = headers.get('X-Modernjs-Redirect');
@@ -82,8 +82,7 @@ export const requestHandler = createRequestHandler(handleRequest, {
82
82
  const handleRSCRequest = async (request, ServerRoot, options) => {
83
83
  const { serverPayload } = options;
84
84
  const stream = renderRsc({
85
- element: options.rscRoot,
86
- clientManifest: options.rscClientManifest!,
85
+ element: options.rscRoot
87
86
  });
88
87
 
89
88
  return new Response(stream);
@@ -106,32 +105,13 @@ const entryForCSRWithRSC = ({ metaName, entryName })=>`
106
105
  import {
107
106
  createRequestHandler,
108
107
  } from '@${metaName}/runtime/ssr/server';
109
- import { renderRsc, processRSCStream } from '@${metaName}/runtime/rsc/server'
108
+ import { renderCSRWithRSC, renderRsc } from '@${metaName}/runtime/rsc/server';
110
109
  export { handleAction } from '@${metaName}/runtime/rsc/server';
111
110
 
112
111
  const handleCSRRender = async (request, ServerRoot, options) => {
113
- const rscPayloadStream = renderRsc({
114
- element: options.rscRoot,
115
- clientManifest: options.rscClientManifest!,
116
- });
117
- const stream = new ReadableStream({
118
- start(controller) {
119
- const encoder = new TextEncoder();
120
-
121
- controller.enqueue(encoder.encode(options.html));
122
-
123
- processRSCStream(rscPayloadStream, controller, encoder)
124
- .catch(err => {
125
- controller.error(err);
126
- });
127
- }
128
- });
129
-
130
- return new Response(stream, {
131
- status: 200,
132
- headers: new Headers({
133
- 'content-type': 'text/html; charset=UTF-8',
134
- }),
112
+ return renderCSRWithRSC({
113
+ html: options.html,
114
+ rscRoot: options.rscRoot,
135
115
  });
136
116
  }
137
117
 
@@ -142,7 +122,6 @@ const entryForCSRWithRSC = ({ metaName, entryName })=>`
142
122
  const handleRequest = async (request, ServerRoot, options) => {
143
123
  const stream = renderRsc({
144
124
  element: options.rscRoot,
145
- clientManifest: options.rscClientManifest!,
146
125
  });
147
126
 
148
127
  return new Response(stream);
@@ -40,6 +40,7 @@ const node_namespaceObject = require("@modern-js/runtime-utils/node");
40
40
  const request_namespaceObject = require("@modern-js/runtime-utils/universal/request");
41
41
  const external_react_namespaceObject = require("react");
42
42
  var external_react_default = /*#__PURE__*/ __webpack_require__.n(external_react_namespaceObject);
43
+ const rsc_router_js_namespaceObject = require("../../router/runtime/rsc-router.js");
43
44
  const index_js_namespaceObject = require("../context/index.js");
44
45
  const runtime_js_namespaceObject = require("../context/runtime.js");
45
46
  const serverPayload_index_js_namespaceObject = require("../context/serverPayload/index.js");
@@ -65,6 +66,14 @@ async function handleRSCRequest(request, Root, context, options, handleRequest)
65
66
  runtimeContext: context
66
67
  });
67
68
  }
69
+ const isRedirectStatus = (status)=>status >= 300 && status <= 399;
70
+ const processRedirect = (headers, status, ctx)=>{
71
+ if (ctx.enableRsc && ctx.isRSCNavigation) return (0, rsc_router_js_namespaceObject.handleRSCRedirect)(headers, ctx.basename, status);
72
+ return new Response(null, {
73
+ status,
74
+ headers
75
+ });
76
+ };
68
77
  function createSSRContext(request, options) {
69
78
  const { config, loaderContext, onError, onTiming, locals, resource, params, responseProxy, reporter } = options;
70
79
  const { nonce, useJsonScript } = config;
@@ -155,26 +164,22 @@ const createRequestHandler = async (handleRequest, createRequestOptions)=>{
155
164
  ssrContext,
156
165
  isBrowser: false
157
166
  });
158
- const getRedirectResponse = (result)=>{
159
- if ("u" > typeof Response && result instanceof Response && result.status >= 300 && result.status <= 399) {
160
- const { status } = result;
161
- const redirectUrl = result.headers.get('Location') || '/';
162
- const { ssrContext } = context;
163
- if (ssrContext) return new Response(null, {
164
- status,
165
- headers: {
166
- Location: redirectUrl
167
- }
168
- });
169
- }
167
+ const redirectCtx = {
168
+ enableRsc: !!createRequestOptions?.enableRsc,
169
+ isRSCNavigation: 'true' === request.headers.get('x-rsc-tree'),
170
+ basename: ssrContext.baseUrl || '/'
170
171
  };
171
172
  const beforeRenderResult = await runBeforeRender(context);
172
173
  if (context.routerContext?.statusCode && context.routerContext?.statusCode !== 200) context.ssrContext?.response.status(context.routerContext?.statusCode);
173
174
  const errors = Object.values(context.routerContext?.errors || {});
174
175
  if (errors.length > 0) options.onError(errors[0], external_tracer_js_namespaceObject.SSRErrors.LOADER_ERROR);
175
- const redirectResponse = getRedirectResponse(beforeRenderResult);
176
- if (redirectResponse) if (createRequestOptions?.enableRsc) return beforeRenderResult || redirectResponse;
177
- else return redirectResponse;
176
+ if ("u" > typeof Response && beforeRenderResult instanceof Response && isRedirectStatus(beforeRenderResult.status)) {
177
+ if (beforeRenderResult.headers.has('X-Modernjs-Redirect')) return beforeRenderResult;
178
+ const redirectUrl = beforeRenderResult.headers.get('Location') || '/';
179
+ return processRedirect(new Headers({
180
+ Location: redirectUrl
181
+ }), beforeRenderResult.status, redirectCtx);
182
+ }
178
183
  if (!createRequestOptions?.enableRsc) {
179
184
  const { htmlTemplate } = options.resource;
180
185
  options.resource.htmlTemplate = htmlTemplate.replace('</head>', `${external_constants_js_namespaceObject.CHUNK_CSS_PLACEHOLDER}</head>`);
@@ -184,6 +189,7 @@ const createRequestHandler = async (handleRequest, createRequestOptions)=>{
184
189
  ...options,
185
190
  runtimeContext: context
186
191
  });
192
+ if (-1 !== responseProxy.status && isRedirectStatus(responseProxy.status) && responseProxy.headers.Location) return processRedirect(new Headers(responseProxy.headers), responseProxy.status, redirectCtx);
187
193
  Object.entries(responseProxy.headers).forEach(([key, value])=>{
188
194
  response.headers.set(key, value);
189
195
  });
@@ -48,10 +48,9 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
48
48
  try {
49
49
  const readableOriginal = await (0, ssr_namespaceObject.renderSSRStream)(rootElement, {
50
50
  request,
51
- clientManifest: options.rscClientManifest,
52
- ssrManifest: options.rscSSRManifest,
53
51
  nonce: config.nonce,
54
52
  rscRoot: rscRoot,
53
+ routes: runtimeContext.routes,
55
54
  onError (error) {
56
55
  options.onError?.(error);
57
56
  }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, definition)=>{
5
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
+ enumerable: true,
7
+ get: definition[key]
8
+ });
9
+ };
10
+ })();
11
+ (()=>{
12
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
13
+ })();
14
+ (()=>{
15
+ __webpack_require__.r = (exports1)=>{
16
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
17
+ value: 'Module'
18
+ });
19
+ Object.defineProperty(exports1, '__esModule', {
20
+ value: true
21
+ });
22
+ };
23
+ })();
24
+ var __webpack_exports__ = {};
25
+ __webpack_require__.r(__webpack_exports__);
26
+ __webpack_require__.d(__webpack_exports__, {
27
+ CSSLinks: ()=>CSSLinks
28
+ });
29
+ const jsx_runtime_namespaceObject = require("react/jsx-runtime");
30
+ require("react");
31
+ function CSSLinks({ cssFiles }) {
32
+ if (0 === cssFiles.length) return null;
33
+ return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(jsx_runtime_namespaceObject.Fragment, {
34
+ children: cssFiles.map((css)=>/*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("link", {
35
+ href: css,
36
+ rel: "stylesheet"
37
+ }, css))
38
+ });
39
+ }
40
+ exports.CSSLinks = __webpack_exports__.CSSLinks;
41
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
42
+ "CSSLinks"
43
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
44
+ Object.defineProperty(exports, '__esModule', {
45
+ value: true
46
+ });
@@ -44,11 +44,27 @@ const client_namespaceObject = require("@modern-js/render/client");
44
44
  const router_namespaceObject = require("@modern-js/runtime-utils/router");
45
45
  const external_react_namespaceObject = require("react");
46
46
  var external_react_default = /*#__PURE__*/ __webpack_require__.n(external_react_namespaceObject);
47
+ const external_CSSLinks_js_namespaceObject = require("./CSSLinks.js");
47
48
  const safeUse = (promise)=>{
48
49
  if ('function' == typeof external_react_default().use) return external_react_default().use(promise);
49
50
  return null;
50
51
  };
51
- const createServerPayload = (routerContext, routes)=>({
52
+ function collectCssFilesFromRoutes(matches, routes) {
53
+ const cssFiles = [];
54
+ for (const match of matches){
55
+ const route = findRouteInTree(routes, match.route.id);
56
+ if (!route) continue;
57
+ const component = route.Component;
58
+ if (component && 'function' == typeof component && 'entryCssFiles' in component) {
59
+ const css = component.entryCssFiles;
60
+ if (Array.isArray(css)) cssFiles.push(...css);
61
+ }
62
+ }
63
+ return Array.from(new Set(cssFiles));
64
+ }
65
+ const createServerPayload = (routerContext, routes)=>{
66
+ const cssFiles = collectCssFilesFromRoutes(routerContext.matches, routes);
67
+ return {
52
68
  type: 'render',
53
69
  actionData: routerContext.actionData,
54
70
  errors: routerContext.errors,
@@ -60,7 +76,7 @@ const createServerPayload = (routerContext, routes)=>({
60
76
  let processedElement;
61
77
  if (element) {
62
78
  const ElementComponent = element.type;
63
- processedElement = /*#__PURE__*/ external_react_default().createElement(ElementComponent, {
79
+ const elementProps = {
64
80
  loaderData: routerContext?.loaderData?.[match.route.id],
65
81
  actionData: routerContext?.actionData?.[match.route.id],
66
82
  params: match.params,
@@ -74,7 +90,12 @@ const createServerPayload = (routerContext, routes)=>({
74
90
  handle: route.handle
75
91
  };
76
92
  })
77
- });
93
+ };
94
+ const routeElement = /*#__PURE__*/ external_react_default().createElement(ElementComponent, elementProps);
95
+ const isLeafRoute = index === routerContext.matches.length - 1;
96
+ processedElement = isLeafRoute && cssFiles.length > 0 ? /*#__PURE__*/ external_react_default().createElement(external_react_default().Fragment, null, /*#__PURE__*/ external_react_default().createElement(external_CSSLinks_js_namespaceObject.CSSLinks, {
97
+ cssFiles
98
+ }), routeElement) : routeElement;
78
99
  }
79
100
  return {
80
101
  element: processedElement,
@@ -93,7 +114,8 @@ const createServerPayload = (routerContext, routes)=>({
93
114
  pathnameBase: match.pathnameBase
94
115
  };
95
116
  })
96
- });
117
+ };
118
+ };
97
119
  const handleRSCRedirect = (headers, basename, status)=>{
98
120
  const newHeaders = new Headers(headers);
99
121
  let redirectUrl = headers.get('Location');
@@ -202,17 +224,19 @@ const createClientRouterFromPayload = (payload, originalRoutes, basename = '')=>
202
224
  clientLoaderResults.forEach(({ routeId, result })=>{
203
225
  results[routeId] = result;
204
226
  });
227
+ if (!res.body) throw new Error('Response body is null');
205
228
  const payload = await (0, client_namespaceObject.createFromReadableStream)(res.body);
206
- if (void 0 === payload.type || 'render' !== payload.type) throw new Error('Unexpected payload type');
229
+ if ('object' != typeof payload || null === payload || void 0 === payload.type || 'render' !== payload.type) throw new Error('Unexpected payload type');
230
+ const serverPayload = payload;
207
231
  matches.forEach((match)=>{
208
232
  const routeId = match.route.id;
209
- const matchedRoute = payload.routes.find((route)=>route.id === routeId);
233
+ const matchedRoute = serverPayload.routes.find((route)=>route.id === routeId);
210
234
  if (matchedRoute) router.patchRoutes(matchedRoute.parentId, [
211
235
  matchedRoute
212
236
  ], true);
213
- if (payload.loaderData?.[routeId]) results[routeId] = {
237
+ if (serverPayload.loaderData?.[routeId]) results[routeId] = {
214
238
  type: 'data',
215
- result: payload.loaderData[routeId]
239
+ result: serverPayload.loaderData[routeId]
216
240
  };
217
241
  });
218
242
  return results;
@@ -15,7 +15,9 @@ const genRenderCode = ({ srcDirectory, internalSrcAlias, metaName, entry, custom
15
15
  return `import { createRoot } from '@${metaName}/runtime/react';
16
16
  import { render } from '@${metaName}/runtime/browser';
17
17
 
18
- ${enableRsc ? `import { RscClientRoot, createFromReadableStream, rscStream, callServer } from '@${metaName}/runtime/rsc/client';` : ''}
18
+ ${enableRsc ? `import { RscClientRoot, createFromReadableStream, rscStream, callServer, setServerCallback } from '@${metaName}/runtime/rsc/client';` : ''}
19
+
20
+ ${enableRsc ? "setServerCallback(callServer);" : ''}
19
21
 
20
22
  ${enableRsc ? `const data = createFromReadableStream(rscStream, {
21
23
  callServer: callServer,
@@ -40,9 +42,12 @@ const entryForCSRWithRSC = ({ metaName, entryName, urlPath = '/', mountId = 'roo
40
42
  isRedirectResponse,
41
43
  rscStream,
42
44
  callServer,
45
+ setServerCallback,
43
46
  createFromReadableStream
44
47
  } from '@${metaName}/runtime/rsc/client';
45
48
 
49
+ setServerCallback(callServer);
50
+
46
51
  const handleRedirectResponse = (res: Response) => {
47
52
  const { headers } = res;
48
53
  const location = headers.get('X-Modernjs-Redirect');
@@ -53,8 +53,7 @@ export const requestHandler = createRequestHandler(handleRequest, {
53
53
  const handleRSCRequest = async (request, ServerRoot, options) => {
54
54
  const { serverPayload } = options;
55
55
  const stream = renderRsc({
56
- element: options.rscRoot,
57
- clientManifest: options.rscClientManifest!,
56
+ element: options.rscRoot
58
57
  });
59
58
 
60
59
  return new Response(stream);
@@ -77,32 +76,13 @@ const entryForCSRWithRSC = ({ metaName, entryName })=>`
77
76
  import {
78
77
  createRequestHandler,
79
78
  } from '@${metaName}/runtime/ssr/server';
80
- import { renderRsc, processRSCStream } from '@${metaName}/runtime/rsc/server'
79
+ import { renderCSRWithRSC, renderRsc } from '@${metaName}/runtime/rsc/server';
81
80
  export { handleAction } from '@${metaName}/runtime/rsc/server';
82
81
 
83
82
  const handleCSRRender = async (request, ServerRoot, options) => {
84
- const rscPayloadStream = renderRsc({
85
- element: options.rscRoot,
86
- clientManifest: options.rscClientManifest!,
87
- });
88
- const stream = new ReadableStream({
89
- start(controller) {
90
- const encoder = new TextEncoder();
91
-
92
- controller.enqueue(encoder.encode(options.html));
93
-
94
- processRSCStream(rscPayloadStream, controller, encoder)
95
- .catch(err => {
96
- controller.error(err);
97
- });
98
- }
99
- });
100
-
101
- return new Response(stream, {
102
- status: 200,
103
- headers: new Headers({
104
- 'content-type': 'text/html; charset=UTF-8',
105
- }),
83
+ return renderCSRWithRSC({
84
+ html: options.html,
85
+ rscRoot: options.rscRoot,
106
86
  });
107
87
  }
108
88
 
@@ -113,7 +93,6 @@ const entryForCSRWithRSC = ({ metaName, entryName })=>`
113
93
  const handleRequest = async (request, ServerRoot, options) => {
114
94
  const stream = renderRsc({
115
95
  element: options.rscRoot,
116
- clientManifest: options.rscClientManifest!,
117
96
  });
118
97
 
119
98
  return new Response(stream);
@@ -2,6 +2,7 @@ import { jsx } from "react/jsx-runtime";
2
2
  import { storage } from "@modern-js/runtime-utils/node";
3
3
  import { getPathname, parseCookie, parseHeaders, parseQuery } from "@modern-js/runtime-utils/universal/request";
4
4
  import react, { Fragment } from "react";
5
+ import { handleRSCRedirect } from "../../router/runtime/rsc-router.mjs";
5
6
  import { getGlobalInternalRuntimeContext, getGlobalRSCRoot } from "../context/index.mjs";
6
7
  import { getInitialContext } from "../context/runtime.mjs";
7
8
  import { getServerPayload } from "../context/serverPayload/index.mjs";
@@ -27,6 +28,14 @@ async function handleRSCRequest(request, Root, context, options, handleRequest)
27
28
  runtimeContext: context
28
29
  });
29
30
  }
31
+ const isRedirectStatus = (status)=>status >= 300 && status <= 399;
32
+ const processRedirect = (headers, status, ctx)=>{
33
+ if (ctx.enableRsc && ctx.isRSCNavigation) return handleRSCRedirect(headers, ctx.basename, status);
34
+ return new Response(null, {
35
+ status,
36
+ headers
37
+ });
38
+ };
30
39
  function createSSRContext(request, options) {
31
40
  const { config, loaderContext, onError, onTiming, locals, resource, params, responseProxy, reporter } = options;
32
41
  const { nonce, useJsonScript } = config;
@@ -117,26 +126,22 @@ const createRequestHandler = async (handleRequest, createRequestOptions)=>{
117
126
  ssrContext,
118
127
  isBrowser: false
119
128
  });
120
- const getRedirectResponse = (result)=>{
121
- if ("u" > typeof Response && result instanceof Response && result.status >= 300 && result.status <= 399) {
122
- const { status } = result;
123
- const redirectUrl = result.headers.get('Location') || '/';
124
- const { ssrContext } = context;
125
- if (ssrContext) return new Response(null, {
126
- status,
127
- headers: {
128
- Location: redirectUrl
129
- }
130
- });
131
- }
129
+ const redirectCtx = {
130
+ enableRsc: !!createRequestOptions?.enableRsc,
131
+ isRSCNavigation: 'true' === request.headers.get('x-rsc-tree'),
132
+ basename: ssrContext.baseUrl || '/'
132
133
  };
133
134
  const beforeRenderResult = await runBeforeRender(context);
134
135
  if (context.routerContext?.statusCode && context.routerContext?.statusCode !== 200) context.ssrContext?.response.status(context.routerContext?.statusCode);
135
136
  const errors = Object.values(context.routerContext?.errors || {});
136
137
  if (errors.length > 0) options.onError(errors[0], SSRErrors.LOADER_ERROR);
137
- const redirectResponse = getRedirectResponse(beforeRenderResult);
138
- if (redirectResponse) if (createRequestOptions?.enableRsc) return beforeRenderResult || redirectResponse;
139
- else return redirectResponse;
138
+ if ("u" > typeof Response && beforeRenderResult instanceof Response && isRedirectStatus(beforeRenderResult.status)) {
139
+ if (beforeRenderResult.headers.has('X-Modernjs-Redirect')) return beforeRenderResult;
140
+ const redirectUrl = beforeRenderResult.headers.get('Location') || '/';
141
+ return processRedirect(new Headers({
142
+ Location: redirectUrl
143
+ }), beforeRenderResult.status, redirectCtx);
144
+ }
140
145
  if (!createRequestOptions?.enableRsc) {
141
146
  const { htmlTemplate } = options.resource;
142
147
  options.resource.htmlTemplate = htmlTemplate.replace('</head>', `${CHUNK_CSS_PLACEHOLDER}</head>`);
@@ -146,6 +151,7 @@ const createRequestHandler = async (handleRequest, createRequestOptions)=>{
146
151
  ...options,
147
152
  runtimeContext: context
148
153
  });
154
+ if (-1 !== responseProxy.status && isRedirectStatus(responseProxy.status) && responseProxy.headers.Location) return processRedirect(new Headers(responseProxy.headers), responseProxy.status, redirectCtx);
149
155
  Object.entries(responseProxy.headers).forEach(([key, value])=>{
150
156
  response.headers.set(key, value);
151
157
  });
@@ -20,10 +20,9 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
20
20
  try {
21
21
  const readableOriginal = await renderSSRStream(rootElement, {
22
22
  request,
23
- clientManifest: options.rscClientManifest,
24
- ssrManifest: options.rscSSRManifest,
25
23
  nonce: config.nonce,
26
24
  rscRoot: rscRoot,
25
+ routes: runtimeContext.routes,
27
26
  onError (error) {
28
27
  options.onError?.(error);
29
28
  }
@@ -0,0 +1,12 @@
1
+ import { Fragment, jsx } from "react/jsx-runtime";
2
+ import "react";
3
+ function CSSLinks({ cssFiles }) {
4
+ if (0 === cssFiles.length) return null;
5
+ return /*#__PURE__*/ jsx(Fragment, {
6
+ children: cssFiles.map((css)=>/*#__PURE__*/ jsx("link", {
7
+ href: css,
8
+ rel: "stylesheet"
9
+ }, css))
10
+ });
11
+ }
12
+ export { CSSLinks };
@@ -2,11 +2,27 @@ import { Fragment, jsx } from "react/jsx-runtime";
2
2
  import { ElementsContext, createFromReadableStream } from "@modern-js/render/client";
3
3
  import { StaticRouterProvider, createBrowserRouter, createStaticRouter, redirect } from "@modern-js/runtime-utils/router";
4
4
  import react from "react";
5
+ import { CSSLinks } from "./CSSLinks.mjs";
5
6
  const safeUse = (promise)=>{
6
7
  if ('function' == typeof react.use) return react.use(promise);
7
8
  return null;
8
9
  };
9
- const createServerPayload = (routerContext, routes)=>({
10
+ function collectCssFilesFromRoutes(matches, routes) {
11
+ const cssFiles = [];
12
+ for (const match of matches){
13
+ const route = findRouteInTree(routes, match.route.id);
14
+ if (!route) continue;
15
+ const component = route.Component;
16
+ if (component && 'function' == typeof component && 'entryCssFiles' in component) {
17
+ const css = component.entryCssFiles;
18
+ if (Array.isArray(css)) cssFiles.push(...css);
19
+ }
20
+ }
21
+ return Array.from(new Set(cssFiles));
22
+ }
23
+ const createServerPayload = (routerContext, routes)=>{
24
+ const cssFiles = collectCssFilesFromRoutes(routerContext.matches, routes);
25
+ return {
10
26
  type: 'render',
11
27
  actionData: routerContext.actionData,
12
28
  errors: routerContext.errors,
@@ -18,7 +34,7 @@ const createServerPayload = (routerContext, routes)=>({
18
34
  let processedElement;
19
35
  if (element) {
20
36
  const ElementComponent = element.type;
21
- processedElement = /*#__PURE__*/ react.createElement(ElementComponent, {
37
+ const elementProps = {
22
38
  loaderData: routerContext?.loaderData?.[match.route.id],
23
39
  actionData: routerContext?.actionData?.[match.route.id],
24
40
  params: match.params,
@@ -32,7 +48,12 @@ const createServerPayload = (routerContext, routes)=>({
32
48
  handle: route.handle
33
49
  };
34
50
  })
35
- });
51
+ };
52
+ const routeElement = /*#__PURE__*/ react.createElement(ElementComponent, elementProps);
53
+ const isLeafRoute = index === routerContext.matches.length - 1;
54
+ processedElement = isLeafRoute && cssFiles.length > 0 ? /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(CSSLinks, {
55
+ cssFiles
56
+ }), routeElement) : routeElement;
36
57
  }
37
58
  return {
38
59
  element: processedElement,
@@ -51,7 +72,8 @@ const createServerPayload = (routerContext, routes)=>({
51
72
  pathnameBase: match.pathnameBase
52
73
  };
53
74
  })
54
- });
75
+ };
76
+ };
55
77
  const handleRSCRedirect = (headers, basename, status)=>{
56
78
  const newHeaders = new Headers(headers);
57
79
  let redirectUrl = headers.get('Location');
@@ -160,17 +182,19 @@ const createClientRouterFromPayload = (payload, originalRoutes, basename = '')=>
160
182
  clientLoaderResults.forEach(({ routeId, result })=>{
161
183
  results[routeId] = result;
162
184
  });
185
+ if (!res.body) throw new Error('Response body is null');
163
186
  const payload = await createFromReadableStream(res.body);
164
- if (void 0 === payload.type || 'render' !== payload.type) throw new Error('Unexpected payload type');
187
+ if ('object' != typeof payload || null === payload || void 0 === payload.type || 'render' !== payload.type) throw new Error('Unexpected payload type');
188
+ const serverPayload = payload;
165
189
  matches.forEach((match)=>{
166
190
  const routeId = match.route.id;
167
- const matchedRoute = payload.routes.find((route)=>route.id === routeId);
191
+ const matchedRoute = serverPayload.routes.find((route)=>route.id === routeId);
168
192
  if (matchedRoute) router.patchRoutes(matchedRoute.parentId, [
169
193
  matchedRoute
170
194
  ], true);
171
- if (payload.loaderData?.[routeId]) results[routeId] = {
195
+ if (serverPayload.loaderData?.[routeId]) results[routeId] = {
172
196
  type: 'data',
173
- result: payload.loaderData[routeId]
197
+ result: serverPayload.loaderData[routeId]
174
198
  };
175
199
  });
176
200
  return results;
@@ -16,7 +16,9 @@ const genRenderCode = ({ srcDirectory, internalSrcAlias, metaName, entry, custom
16
16
  return `import { createRoot } from '@${metaName}/runtime/react';
17
17
  import { render } from '@${metaName}/runtime/browser';
18
18
 
19
- ${enableRsc ? `import { RscClientRoot, createFromReadableStream, rscStream, callServer } from '@${metaName}/runtime/rsc/client';` : ''}
19
+ ${enableRsc ? `import { RscClientRoot, createFromReadableStream, rscStream, callServer, setServerCallback } from '@${metaName}/runtime/rsc/client';` : ''}
20
+
21
+ ${enableRsc ? "setServerCallback(callServer);" : ''}
20
22
 
21
23
  ${enableRsc ? `const data = createFromReadableStream(rscStream, {
22
24
  callServer: callServer,
@@ -41,9 +43,12 @@ const entryForCSRWithRSC = ({ metaName, entryName, urlPath = '/', mountId = 'roo
41
43
  isRedirectResponse,
42
44
  rscStream,
43
45
  callServer,
46
+ setServerCallback,
44
47
  createFromReadableStream
45
48
  } from '@${metaName}/runtime/rsc/client';
46
49
 
50
+ setServerCallback(callServer);
51
+
47
52
  const handleRedirectResponse = (res: Response) => {
48
53
  const { headers } = res;
49
54
  const location = headers.get('X-Modernjs-Redirect');
@@ -54,8 +54,7 @@ export const requestHandler = createRequestHandler(handleRequest, {
54
54
  const handleRSCRequest = async (request, ServerRoot, options) => {
55
55
  const { serverPayload } = options;
56
56
  const stream = renderRsc({
57
- element: options.rscRoot,
58
- clientManifest: options.rscClientManifest!,
57
+ element: options.rscRoot
59
58
  });
60
59
 
61
60
  return new Response(stream);
@@ -78,32 +77,13 @@ const entryForCSRWithRSC = ({ metaName, entryName })=>`
78
77
  import {
79
78
  createRequestHandler,
80
79
  } from '@${metaName}/runtime/ssr/server';
81
- import { renderRsc, processRSCStream } from '@${metaName}/runtime/rsc/server'
80
+ import { renderCSRWithRSC, renderRsc } from '@${metaName}/runtime/rsc/server';
82
81
  export { handleAction } from '@${metaName}/runtime/rsc/server';
83
82
 
84
83
  const handleCSRRender = async (request, ServerRoot, options) => {
85
- const rscPayloadStream = renderRsc({
86
- element: options.rscRoot,
87
- clientManifest: options.rscClientManifest!,
88
- });
89
- const stream = new ReadableStream({
90
- start(controller) {
91
- const encoder = new TextEncoder();
92
-
93
- controller.enqueue(encoder.encode(options.html));
94
-
95
- processRSCStream(rscPayloadStream, controller, encoder)
96
- .catch(err => {
97
- controller.error(err);
98
- });
99
- }
100
- });
101
-
102
- return new Response(stream, {
103
- status: 200,
104
- headers: new Headers({
105
- 'content-type': 'text/html; charset=UTF-8',
106
- }),
84
+ return renderCSRWithRSC({
85
+ html: options.html,
86
+ rscRoot: options.rscRoot,
107
87
  });
108
88
  }
109
89
 
@@ -114,7 +94,6 @@ const entryForCSRWithRSC = ({ metaName, entryName })=>`
114
94
  const handleRequest = async (request, ServerRoot, options) => {
115
95
  const stream = renderRsc({
116
96
  element: options.rscRoot,
117
- clientManifest: options.rscClientManifest!,
118
97
  });
119
98
 
120
99
  return new Response(stream);
@@ -3,6 +3,7 @@ import { jsx } from "react/jsx-runtime";
3
3
  import { storage } from "@modern-js/runtime-utils/node";
4
4
  import { getPathname, parseCookie, parseHeaders, parseQuery } from "@modern-js/runtime-utils/universal/request";
5
5
  import react, { Fragment } from "react";
6
+ import { handleRSCRedirect } from "../../router/runtime/rsc-router.mjs";
6
7
  import { getGlobalInternalRuntimeContext, getGlobalRSCRoot } from "../context/index.mjs";
7
8
  import { getInitialContext } from "../context/runtime.mjs";
8
9
  import { getServerPayload } from "../context/serverPayload/index.mjs";
@@ -28,6 +29,14 @@ async function handleRSCRequest(request, Root, context, options, handleRequest)
28
29
  runtimeContext: context
29
30
  });
30
31
  }
32
+ const isRedirectStatus = (status)=>status >= 300 && status <= 399;
33
+ const processRedirect = (headers, status, ctx)=>{
34
+ if (ctx.enableRsc && ctx.isRSCNavigation) return handleRSCRedirect(headers, ctx.basename, status);
35
+ return new Response(null, {
36
+ status,
37
+ headers
38
+ });
39
+ };
31
40
  function createSSRContext(request, options) {
32
41
  const { config, loaderContext, onError, onTiming, locals, resource, params, responseProxy, reporter } = options;
33
42
  const { nonce, useJsonScript } = config;
@@ -118,26 +127,22 @@ const createRequestHandler = async (handleRequest, createRequestOptions)=>{
118
127
  ssrContext,
119
128
  isBrowser: false
120
129
  });
121
- const getRedirectResponse = (result)=>{
122
- if ("u" > typeof Response && result instanceof Response && result.status >= 300 && result.status <= 399) {
123
- const { status } = result;
124
- const redirectUrl = result.headers.get('Location') || '/';
125
- const { ssrContext } = context;
126
- if (ssrContext) return new Response(null, {
127
- status,
128
- headers: {
129
- Location: redirectUrl
130
- }
131
- });
132
- }
130
+ const redirectCtx = {
131
+ enableRsc: !!createRequestOptions?.enableRsc,
132
+ isRSCNavigation: 'true' === request.headers.get('x-rsc-tree'),
133
+ basename: ssrContext.baseUrl || '/'
133
134
  };
134
135
  const beforeRenderResult = await runBeforeRender(context);
135
136
  if (context.routerContext?.statusCode && context.routerContext?.statusCode !== 200) context.ssrContext?.response.status(context.routerContext?.statusCode);
136
137
  const errors = Object.values(context.routerContext?.errors || {});
137
138
  if (errors.length > 0) options.onError(errors[0], SSRErrors.LOADER_ERROR);
138
- const redirectResponse = getRedirectResponse(beforeRenderResult);
139
- if (redirectResponse) if (createRequestOptions?.enableRsc) return beforeRenderResult || redirectResponse;
140
- else return redirectResponse;
139
+ if ("u" > typeof Response && beforeRenderResult instanceof Response && isRedirectStatus(beforeRenderResult.status)) {
140
+ if (beforeRenderResult.headers.has('X-Modernjs-Redirect')) return beforeRenderResult;
141
+ const redirectUrl = beforeRenderResult.headers.get('Location') || '/';
142
+ return processRedirect(new Headers({
143
+ Location: redirectUrl
144
+ }), beforeRenderResult.status, redirectCtx);
145
+ }
141
146
  if (!createRequestOptions?.enableRsc) {
142
147
  const { htmlTemplate } = options.resource;
143
148
  options.resource.htmlTemplate = htmlTemplate.replace('</head>', `${CHUNK_CSS_PLACEHOLDER}</head>`);
@@ -147,6 +152,7 @@ const createRequestHandler = async (handleRequest, createRequestOptions)=>{
147
152
  ...options,
148
153
  runtimeContext: context
149
154
  });
155
+ if (-1 !== responseProxy.status && isRedirectStatus(responseProxy.status) && responseProxy.headers.Location) return processRedirect(new Headers(responseProxy.headers), responseProxy.status, redirectCtx);
150
156
  Object.entries(responseProxy.headers).forEach(([key, value])=>{
151
157
  response.headers.set(key, value);
152
158
  });
@@ -21,10 +21,9 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
21
21
  try {
22
22
  const readableOriginal = await renderSSRStream(rootElement, {
23
23
  request,
24
- clientManifest: options.rscClientManifest,
25
- ssrManifest: options.rscSSRManifest,
26
24
  nonce: config.nonce,
27
25
  rscRoot: rscRoot,
26
+ routes: runtimeContext.routes,
28
27
  onError (error) {
29
28
  options.onError?.(error);
30
29
  }
@@ -0,0 +1,13 @@
1
+ import "node:module";
2
+ import { Fragment, jsx } from "react/jsx-runtime";
3
+ import "react";
4
+ function CSSLinks({ cssFiles }) {
5
+ if (0 === cssFiles.length) return null;
6
+ return /*#__PURE__*/ jsx(Fragment, {
7
+ children: cssFiles.map((css)=>/*#__PURE__*/ jsx("link", {
8
+ href: css,
9
+ rel: "stylesheet"
10
+ }, css))
11
+ });
12
+ }
13
+ export { CSSLinks };
@@ -3,11 +3,27 @@ import { Fragment, jsx } from "react/jsx-runtime";
3
3
  import { ElementsContext, createFromReadableStream } from "@modern-js/render/client";
4
4
  import { StaticRouterProvider, createBrowserRouter, createStaticRouter, redirect } from "@modern-js/runtime-utils/router";
5
5
  import react from "react";
6
+ import { CSSLinks } from "./CSSLinks.mjs";
6
7
  const safeUse = (promise)=>{
7
8
  if ('function' == typeof react.use) return react.use(promise);
8
9
  return null;
9
10
  };
10
- const createServerPayload = (routerContext, routes)=>({
11
+ function collectCssFilesFromRoutes(matches, routes) {
12
+ const cssFiles = [];
13
+ for (const match of matches){
14
+ const route = findRouteInTree(routes, match.route.id);
15
+ if (!route) continue;
16
+ const component = route.Component;
17
+ if (component && 'function' == typeof component && 'entryCssFiles' in component) {
18
+ const css = component.entryCssFiles;
19
+ if (Array.isArray(css)) cssFiles.push(...css);
20
+ }
21
+ }
22
+ return Array.from(new Set(cssFiles));
23
+ }
24
+ const createServerPayload = (routerContext, routes)=>{
25
+ const cssFiles = collectCssFilesFromRoutes(routerContext.matches, routes);
26
+ return {
11
27
  type: 'render',
12
28
  actionData: routerContext.actionData,
13
29
  errors: routerContext.errors,
@@ -19,7 +35,7 @@ const createServerPayload = (routerContext, routes)=>({
19
35
  let processedElement;
20
36
  if (element) {
21
37
  const ElementComponent = element.type;
22
- processedElement = /*#__PURE__*/ react.createElement(ElementComponent, {
38
+ const elementProps = {
23
39
  loaderData: routerContext?.loaderData?.[match.route.id],
24
40
  actionData: routerContext?.actionData?.[match.route.id],
25
41
  params: match.params,
@@ -33,7 +49,12 @@ const createServerPayload = (routerContext, routes)=>({
33
49
  handle: route.handle
34
50
  };
35
51
  })
36
- });
52
+ };
53
+ const routeElement = /*#__PURE__*/ react.createElement(ElementComponent, elementProps);
54
+ const isLeafRoute = index === routerContext.matches.length - 1;
55
+ processedElement = isLeafRoute && cssFiles.length > 0 ? /*#__PURE__*/ react.createElement(react.Fragment, null, /*#__PURE__*/ react.createElement(CSSLinks, {
56
+ cssFiles
57
+ }), routeElement) : routeElement;
37
58
  }
38
59
  return {
39
60
  element: processedElement,
@@ -52,7 +73,8 @@ const createServerPayload = (routerContext, routes)=>({
52
73
  pathnameBase: match.pathnameBase
53
74
  };
54
75
  })
55
- });
76
+ };
77
+ };
56
78
  const handleRSCRedirect = (headers, basename, status)=>{
57
79
  const newHeaders = new Headers(headers);
58
80
  let redirectUrl = headers.get('Location');
@@ -161,17 +183,19 @@ const createClientRouterFromPayload = (payload, originalRoutes, basename = '')=>
161
183
  clientLoaderResults.forEach(({ routeId, result })=>{
162
184
  results[routeId] = result;
163
185
  });
186
+ if (!res.body) throw new Error('Response body is null');
164
187
  const payload = await createFromReadableStream(res.body);
165
- if (void 0 === payload.type || 'render' !== payload.type) throw new Error('Unexpected payload type');
188
+ if ('object' != typeof payload || null === payload || void 0 === payload.type || 'render' !== payload.type) throw new Error('Unexpected payload type');
189
+ const serverPayload = payload;
166
190
  matches.forEach((match)=>{
167
191
  const routeId = match.route.id;
168
- const matchedRoute = payload.routes.find((route)=>route.id === routeId);
192
+ const matchedRoute = serverPayload.routes.find((route)=>route.id === routeId);
169
193
  if (matchedRoute) router.patchRoutes(matchedRoute.parentId, [
170
194
  matchedRoute
171
195
  ], true);
172
- if (payload.loaderData?.[routeId]) results[routeId] = {
196
+ if (serverPayload.loaderData?.[routeId]) results[routeId] = {
173
197
  type: 'data',
174
- result: payload.loaderData[routeId]
198
+ result: serverPayload.loaderData[routeId]
175
199
  };
176
200
  });
177
201
  return results;
@@ -8,6 +8,7 @@ export type PayloadRoute = {
8
8
  hasAction: boolean;
9
9
  hasErrorBoundary: boolean;
10
10
  hasLoader: boolean;
11
+ hasClientLoader?: boolean;
11
12
  id: string;
12
13
  index?: boolean;
13
14
  params: Record<string, string>;
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ /**
3
+ * Server Component that renders CSS link tags
4
+ * These will be serialized into RSC payload and automatically handled by React on client
5
+ * React will automatically hoist these link tags to the head
6
+ */
7
+ export declare function CSSLinks({ cssFiles }: {
8
+ cssFiles: string[];
9
+ }): React.JSX.Element | null;
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "modern",
16
16
  "modern.js"
17
17
  ],
18
- "version": "3.0.1",
18
+ "version": "3.0.2",
19
19
  "engines": {
20
20
  "node": ">=20"
21
21
  },
@@ -28,6 +28,10 @@
28
28
  "react-server": "./dist/esm/react-server.mjs",
29
29
  "default": "./dist/esm/index.mjs"
30
30
  },
31
+ "./react-server": {
32
+ "types": "./dist/types/react-server.d.ts",
33
+ "default": "./dist/esm/react-server.mjs"
34
+ },
31
35
  "./internal": {
32
36
  "types": "./dist/types/internal.d.ts",
33
37
  "default": "./dist/esm/internal.mjs"
@@ -131,6 +135,9 @@
131
135
  ".": [
132
136
  "./dist/types/index.d.ts"
133
137
  ],
138
+ "react-server": [
139
+ "./dist/types/react-server.d.ts"
140
+ ],
134
141
  "internal": [
135
142
  "./dist/types/internal.d.ts"
136
143
  ],
@@ -197,7 +204,7 @@
197
204
  "@loadable/component": "5.16.7",
198
205
  "@loadable/server": "5.16.7",
199
206
  "@swc/helpers": "^0.5.17",
200
- "@swc/plugin-loadable-components": "^11.4.0",
207
+ "@swc/plugin-loadable-components": "^11.5.0",
201
208
  "@types/loadable__component": "^5.13.10",
202
209
  "@types/react-helmet": "^6.1.11",
203
210
  "cookie": "0.7.2",
@@ -208,12 +215,12 @@
208
215
  "isbot": "3.8.0",
209
216
  "react-helmet": "^6.1.0",
210
217
  "react-is": "^18.3.1",
211
- "@modern-js/plugin": "3.0.1",
212
- "@modern-js/render": "3.0.1",
213
- "@modern-js/plugin-data-loader": "3.0.1",
214
- "@modern-js/runtime-utils": "3.0.1",
215
- "@modern-js/types": "3.0.1",
216
- "@modern-js/utils": "3.0.1"
218
+ "@modern-js/plugin": "3.0.2",
219
+ "@modern-js/render": "3.0.2",
220
+ "@modern-js/runtime-utils": "3.0.2",
221
+ "@modern-js/plugin-data-loader": "3.0.2",
222
+ "@modern-js/types": "3.0.2",
223
+ "@modern-js/utils": "3.0.2"
217
224
  },
218
225
  "peerDependencies": {
219
226
  "react": ">=17.0.2",
@@ -221,8 +228,8 @@
221
228
  },
222
229
  "devDependencies": {
223
230
  "@remix-run/web-fetch": "^4.1.3",
224
- "@rsbuild/core": "2.0.0-beta.2",
225
- "@rslib/core": "0.19.4",
231
+ "@rsbuild/core": "2.0.0-beta.4",
232
+ "@rslib/core": "0.19.5",
226
233
  "@testing-library/dom": "^10.4.1",
227
234
  "@testing-library/react": "^16.3.2",
228
235
  "@types/cookie": "0.6.0",
@@ -233,7 +240,7 @@
233
240
  "react-dom": "^19.2.4",
234
241
  "ts-node": "^10.9.2",
235
242
  "typescript": "^5",
236
- "@modern-js/app-tools": "3.0.1",
243
+ "@modern-js/app-tools": "3.0.2",
237
244
  "@modern-js/rslib": "2.68.10",
238
245
  "@scripts/rstest-config": "2.66.0"
239
246
  },