UNPKG

@modern-js/server-core

Version:

A Progressive React Framework for modern web development.

254 lines (253 loc) • 9.73 kB
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, forceCSRMap, 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" } }); } var _forceCSRMap_get; const finalForceCSR = routeInfo.entryName ? (_forceCSRMap_get = forceCSRMap === null || forceCSRMap === void 0 ? void 0 : forceCSRMap.get(routeInfo.entryName)) !== null && _forceCSRMap_get !== void 0 ? _forceCSRMap_get : forceCSR : forceCSR; const renderMode = await getRenderMode(req, fallbackHeader, routeInfo.isSSR, finalForceCSR, 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 };