@modern-js/server-core
Version:
A Progressive React Framework for modern web development.
252 lines (251 loc) • 9.44 kB
JavaScript
import { cutNameByHyphen } from "@modern-js/utils/universal";
import { TrieRouter } from "hono/router/trie-router";
import { X_MODERNJS_RENDER } from "../../constants";
import { uniqueKeyByRoute } from "../../utils";
import { ErrorDigest, createErrorHtml, getPathname, getRuntimeEnv, parseHeaders, parseQuery, sortRoutes } from "../../utils";
import { csrRscRender } from "./csrRscRender";
import { dataHandler } from "./dataHandler";
import { renderRscHandler } from "./renderRscHandler";
import { serverActionHandler } from "./serverActionHandler";
import { ssrRender } from "./ssrRender";
const DYNAMIC_ROUTE_REG = /\/:./;
function getRouter(routes) {
const dynamicRoutes = [];
const normalRoutes = [];
routes.forEach((route) => {
if (DYNAMIC_ROUTE_REG.test(route.urlPath)) {
dynamicRoutes.push(route);
} else {
normalRoutes.push(route);
}
});
const finalRoutes = [
...normalRoutes.sort(sortRoutes),
...dynamicRoutes.sort(sortRoutes)
];
const router = new TrieRouter();
for (const route of finalRoutes) {
const { urlPath: originUrlPath } = route;
const urlPath = originUrlPath.endsWith("/") ? `${originUrlPath}*` : `${originUrlPath}/*`;
router.add("*", urlPath, route);
}
return router;
}
function matchRoute(router, pathname, entryName) {
const matched = router.match("*", pathname);
if (entryName && matched[0].length > 1) {
const matches = matched[0];
const result = matches.find(([route]) => route.entryName === entryName);
return result || [];
} else {
const result = matched[0][0];
return result || [];
}
}
function getHeadersWithoutCookie(headers) {
const _headers = {
...headers,
cookie: void 0
};
delete _headers.cookie;
return _headers;
}
async function createRender({ routes, pwd, metaName, staticGenerate, cacheConfig, forceCSR, config, onFallback }) {
const router = getRouter(routes);
return async (req, { logger, reporter, metrics, monitors, nodeReq, templates, serverManifest, rscClientManifest, rscSSRManifest, rscServerManifest, locals, matchEntryName, matchPathname, loaderContext }) => {
const forMatchpathname = matchPathname !== null && matchPathname !== void 0 ? matchPathname : getPathname(req);
const [routeInfo, params] = matchRoute(router, forMatchpathname, matchEntryName);
const framework = cutNameByHyphen(metaName || "modern-js");
const fallbackHeader = `x-${framework}-ssr-fallback`;
let fallbackReason = null;
const fallbackWrapper = async (reason, error) => {
fallbackReason = reason;
return onFallback === null || onFallback === void 0 ? void 0 : onFallback(reason, {
logger,
reporter,
metrics
}, error);
};
if (!routeInfo) {
return new Response(createErrorHtml(404), {
status: 404,
headers: {
"content-type": "text/html; charset=UTF-8"
}
});
}
const html = templates[uniqueKeyByRoute(routeInfo)];
if (!html) {
return new Response(createErrorHtml(404), {
status: 404,
headers: {
"content-type": "text/html; charset=UTF-8"
}
});
}
const renderMode = await getRenderMode(req, fallbackHeader, routeInfo.isSSR, forceCSR, nodeReq, fallbackWrapper);
const headerData = parseHeaders(req);
const onError = (e, key) => {
monitors === null || monitors === void 0 ? void 0 : monitors.error(`SSR Error - ${key || (e instanceof Error ? e.name : e)}, error = %s, req.url = %s, req.headers = %o`, e instanceof Error ? e.stack || e.message : e, forMatchpathname, getHeadersWithoutCookie(headerData));
};
const onTiming = (name, dur) => {
monitors === null || monitors === void 0 ? void 0 : monitors.timing(name, dur, "SSR");
};
const renderOptions = {
pwd,
html,
routeInfo,
staticGenerate: staticGenerate || false,
config,
nodeReq,
cacheConfig,
reporter,
serverRoutes: routes,
params,
logger,
metrics,
monitors,
locals,
rscClientManifest,
rscSSRManifest,
rscServerManifest,
serverManifest,
loaderContext: loaderContext || /* @__PURE__ */ new Map(),
onError,
onTiming
};
if (fallbackReason) {
renderOptions.html = injectFallbackReasonToHtml({
html: renderOptions.html,
reason: fallbackReason,
framework
});
}
let response;
switch (renderMode) {
case "data":
response = await dataHandler(req, renderOptions) || await renderHandler(req, renderOptions, "ssr", fallbackWrapper, framework);
break;
case "rsc-tree":
response = await renderRscHandler(req, renderOptions);
break;
case "rsc-action":
response = await serverActionHandler(req, renderOptions);
break;
case "ssr":
case "csr":
response = await renderHandler(req, renderOptions, renderMode, fallbackWrapper, framework);
break;
default:
throw new Error(`Unknown render mode: ${renderMode}`);
}
if (fallbackReason) {
response.headers.set(fallbackHeader, `1;reason=${fallbackReason}`);
}
return response;
};
}
async function renderHandler(request, options, mode, fallbackWrapper, framework) {
var _options_config_server;
let response = null;
const { serverManifest } = options;
const ssrByRouteIds = (_options_config_server = options.config.server) === null || _options_config_server === void 0 ? void 0 : _options_config_server.ssrByRouteIds;
const runtimeEnv = getRuntimeEnv();
if (serverManifest.nestedRoutesJson && ssrByRouteIds && (ssrByRouteIds === null || ssrByRouteIds === void 0 ? void 0 : ssrByRouteIds.length) > 0 && runtimeEnv === "node") {
const { nestedRoutesJson } = serverManifest;
const routes = nestedRoutesJson === null || nestedRoutesJson === void 0 ? void 0 : nestedRoutesJson[options.routeInfo.entryName];
if (routes) {
const urlPath = "node:url";
const { pathToFileURL } = await import(urlPath);
const { matchRoutes } = await import(pathToFileURL(require.resolve("@modern-js/runtime-utils/remix-router")).href);
const url = new URL(request.url);
const matchedRoutes = matchRoutes(routes, url.pathname, options.routeInfo.urlPath);
if (!matchedRoutes) {
response = await csrRender(request, options);
} else {
var _lastMatch_route;
const lastMatch = matchedRoutes[matchedRoutes.length - 1];
if (!(lastMatch === null || lastMatch === void 0 ? void 0 : (_lastMatch_route = lastMatch.route) === null || _lastMatch_route === void 0 ? void 0 : _lastMatch_route.id) || !ssrByRouteIds.includes(lastMatch.route.id)) {
response = await csrRender(request, options);
}
}
}
}
if (mode === "ssr" && !response) {
try {
response = await ssrRender(request, options);
} catch (e) {
options.onError(e, ErrorDigest.ERENDER);
await fallbackWrapper("error", e);
response = await csrRender(request, {
...options,
html: injectFallbackReasonToHtml({
html: options.html,
reason: "error",
framework
})
});
}
} else {
response = await csrRender(request, options);
}
const { routeInfo } = options;
applyExtendHeaders(response, routeInfo);
return response;
function applyExtendHeaders(r, route) {
Object.entries(route.responseHeaders || {}).forEach(([k, v]) => {
r.headers.set(k, v);
});
}
}
async function getRenderMode(req, fallbackHeader, isSSR, forceCSR, nodeReq, onFallback) {
const query = parseQuery(req);
if (req.headers.get("x-rsc-action")) {
return "rsc-action";
}
if (req.headers.get("x-rsc-tree")) {
return "rsc-tree";
}
if (isSSR) {
if (query.__loader) {
return "data";
}
const fallbackHeaderValue = req.headers.get(fallbackHeader) || (nodeReq === null || nodeReq === void 0 ? void 0 : nodeReq.headers[fallbackHeader]);
if (forceCSR && (query.csr || fallbackHeaderValue)) {
if (query.csr) {
await (onFallback === null || onFallback === void 0 ? void 0 : onFallback("query"));
} else {
var _fallbackHeaderValue_split_;
const reason = fallbackHeaderValue === null || fallbackHeaderValue === void 0 ? void 0 : (_fallbackHeaderValue_split_ = fallbackHeaderValue.split(";")[1]) === null || _fallbackHeaderValue_split_ === void 0 ? void 0 : _fallbackHeaderValue_split_.split("=")[1];
await (onFallback === null || onFallback === void 0 ? void 0 : onFallback(reason ? `header,${reason}` : "header"));
}
return "csr";
}
return "ssr";
} else {
return "csr";
}
}
function injectFallbackReasonToHtml({ html, reason, framework }) {
const tag = `<script id="__${framework}_ssr_fallback_reason__" type="application/json">${JSON.stringify({
reason
})}</script>`;
return html.replace(/<\/head>/, `${tag}</head>`);
}
async function csrRender(request, options) {
const { html, rscClientManifest } = options;
if (!rscClientManifest || process.env.MODERN_DISABLE_INJECT_RSC_DATA) {
return new Response(html, {
status: 200,
headers: new Headers({
"content-type": "text/html; charset=UTF-8",
[X_MODERNJS_RENDER]: "client"
})
});
} else {
return csrRscRender(request, options);
}
}
export {
createRender
};