Refactor server structure and enhance middleware functionality
- Consolidated middleware setup into a dedicated setup file for better organization. - Introduced API proxy middleware to handle requests to the backend API. - Registered well-known, merge, and SSR routes in separate files for improved modularity. - Removed unused HTML and SSR layout files to streamline the codebase. - Implemented a utility for HTML escaping to prevent XSS vulnerabilities. - Updated the main server entry point to utilize the new middleware and route structure.
This commit is contained in:
171
src/index.tsx
171
src/index.tsx
@@ -1,161 +1,24 @@
|
||||
import { serializeQueryCache } from '@pinia/colada';
|
||||
import { renderSSRHead } from '@unhead/vue/server';
|
||||
import { Hono } from 'hono';
|
||||
import { contextStorage } from 'hono/context-storage';
|
||||
import { cors } from "hono/cors";
|
||||
import { streamText } from 'hono/streaming';
|
||||
import isMobile from 'is-mobile';
|
||||
import { renderToWebStream } from 'vue/server-renderer';
|
||||
import { buildBootstrapScript } from './lib/manifest';
|
||||
import { styleTags } from './lib/primePassthrough';
|
||||
import { createApp } from './main';
|
||||
import { useAuthStore } from './stores/auth';
|
||||
// @ts-ignore
|
||||
import Base from '@primevue/core/base';
|
||||
import { baseAPIURL } from './api/httpClientAdapter.server';
|
||||
import { createManifest, getListFiles, saveManifest, validateChunkUrls } from './server/modules/merge';
|
||||
const app = new Hono()
|
||||
const defaultNames = ['primitive', 'semantic', 'global', 'base', 'ripple-directive']
|
||||
// app.use(renderer)
|
||||
app.use('*', contextStorage());
|
||||
app.use(cors(), async (c, next) => {
|
||||
c.set("fetch", app.request.bind(app));
|
||||
const ua = c.req.header("User-Agent")
|
||||
if (!ua) {
|
||||
return c.json({ error: "User-Agent header is missing" }, 400);
|
||||
};
|
||||
c.set("isMobile", isMobile({ ua }));
|
||||
await next();
|
||||
}, async (c, next) => {
|
||||
const path = c.req.path
|
||||
|
||||
if (path !== '/r' && !path.startsWith('/r/')) {
|
||||
return await next()
|
||||
}
|
||||
const url = new URL(c.req.url)
|
||||
url.host = baseAPIURL.replace(/^https?:\/\//, '')
|
||||
url.protocol = 'https:'
|
||||
url.pathname = path.replace(/^\/r/, '') || '/'
|
||||
url.port = ''
|
||||
// console.log("url", url.toString())
|
||||
// console.log("c.req.raw", c.req.raw)
|
||||
const headers = new Headers(c.req.header());
|
||||
headers.delete("host");
|
||||
headers.delete("connection");
|
||||
import { setupMiddlewares } from './server/middlewares/setup';
|
||||
import { apiProxyMiddleware } from './server/middlewares/apiProxy';
|
||||
import { registerWellKnownRoutes } from './server/routes/wellKnown';
|
||||
import { registerMergeRoutes } from './server/routes/merge';
|
||||
import { registerManifestRoutes } from './server/routes/manifest';
|
||||
import { registerSSRRoutes } from './server/routes/ssr';
|
||||
|
||||
return fetch(url.toString(), {
|
||||
method: c.req.method,
|
||||
headers: headers,
|
||||
body: c.req.raw.body,
|
||||
// @ts-ignore
|
||||
duplex: 'half',
|
||||
credentials: 'include'
|
||||
});
|
||||
});
|
||||
app.get("/.well-known/*", (c) => {
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
app.post('/merge', async (c, next) => {
|
||||
const headers = new Headers(c.req.header());
|
||||
headers.delete("host");
|
||||
headers.delete("connection");
|
||||
return fetch(`${baseAPIURL}/me`, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
credentials: 'include'
|
||||
}).then(res => res.json()).then((r) => {
|
||||
if (r.data?.user) {
|
||||
return next();
|
||||
}
|
||||
else {
|
||||
throw new Error("Unauthorized");
|
||||
}}).catch(() => {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
});
|
||||
}, async (c) => {
|
||||
try {
|
||||
const body = await c.req.json()
|
||||
const { filename, chunks } = body
|
||||
if (!filename || !Array.isArray(chunks) || chunks.length === 0) {
|
||||
return c.json({ error: 'invalid payload' }, 400)
|
||||
}
|
||||
const hostError = validateChunkUrls(chunks)
|
||||
if (hostError) return c.json({ error: hostError }, 400)
|
||||
const app = new Hono();
|
||||
|
||||
const manifest = createManifest(filename, chunks)
|
||||
await saveManifest(manifest)
|
||||
// Global middlewares
|
||||
setupMiddlewares(app);
|
||||
|
||||
return c.json({
|
||||
status: 'ok',
|
||||
id: manifest.id,
|
||||
filename: manifest.filename,
|
||||
total_parts: manifest.total_parts,
|
||||
})
|
||||
} catch (e: any) {
|
||||
return c.json({ error: e?.message ?? String(e) }, 500)
|
||||
}
|
||||
})
|
||||
app.get('/manifest/:id', async (c) => {
|
||||
const manifest = await getListFiles()
|
||||
if (!manifest) {
|
||||
return c.json({ error: "Manifest not found" }, 404)
|
||||
}
|
||||
return c.json(manifest)
|
||||
})
|
||||
app.get("*", async (c) => {
|
||||
const nonce = crypto.randomUUID();
|
||||
const url = new URL(c.req.url);
|
||||
const { app, router, head, pinia, bodyClass, queryCache } = createApp();
|
||||
app.provide("honoContext", c);
|
||||
const auth = useAuthStore();
|
||||
auth.$reset();
|
||||
// auth.initialized = false;
|
||||
await auth.init();
|
||||
await router.push(url.pathname);
|
||||
await router.isReady();
|
||||
let usedStyles = new Set<String>();
|
||||
Base.setLoadedStyleName = async (name: string) => usedStyles.add(name)
|
||||
return streamText(c, async (stream) => {
|
||||
c.header("Content-Type", "text/html; charset=utf-8");
|
||||
c.header("Content-Encoding", "Identity");
|
||||
const ctx: Record<string, any> = {};
|
||||
const appStream = renderToWebStream(app, ctx);
|
||||
// console.log("ctx: ", );
|
||||
await stream.write("<!DOCTYPE html><html lang='en'><head>");
|
||||
await stream.write("<base href='" + url.origin + "'/>");
|
||||
// API proxy middleware (handles /r/*)
|
||||
app.use(apiProxyMiddleware);
|
||||
|
||||
await renderSSRHead(head).then((headString) => stream.write(headString.headTags.replace(/\n/g, "")));
|
||||
// await stream.write(`<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"rel="stylesheet"></link>`);
|
||||
await stream.write(`<link rel="preconnect" href="https://fonts.googleapis.com">`);
|
||||
await stream.write(`<link href="https://fonts.googleapis.com/css2?family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700&display=swap" rel="stylesheet">`);
|
||||
await stream.write('<link rel="icon" href="/favicon.ico" />');
|
||||
await stream.write(buildBootstrapScript());
|
||||
if (usedStyles.size > 0) {
|
||||
defaultNames.forEach(name => usedStyles.add(name));
|
||||
}
|
||||
await Promise.all(styleTags.filter(tag => usedStyles.has(tag.name.replace(/-(variables|style)$/, ""))).map(tag => stream.write(`<style type="text/css" data-primevue-style-id="${tag.name}">${tag.value}</style>`)));
|
||||
await stream.write(`</head><body class='${bodyClass}'>`);
|
||||
// await stream.pipe(createTextTransformStreamClass(appStream, (text) => text.replace('<div id="anchor-header" class="p-4"></div>', `<div id="anchor-header" class="p-4">${ctx.teleports["#anchor-header"] || ""}</div>`).replace('<div id="anchor-top"></div>', `<div id="anchor-top">${ctx.teleports["#anchor-top"] || ""}</div>`)));
|
||||
await stream.pipe(appStream);
|
||||
delete ctx.teleports
|
||||
delete ctx.__teleportBuffers
|
||||
delete ctx.modules;
|
||||
Object.assign(ctx, { $p: pinia.state.value, $colada: serializeQueryCache(queryCache) });
|
||||
await stream.write(`<script type="application/json" data-ssr="true" id="__APP_DATA__" nonce="${nonce}">${htmlEscape((JSON.stringify(ctx)))}</script>`);
|
||||
await stream.write("</body></html>");
|
||||
});
|
||||
})
|
||||
const ESCAPE_LOOKUP: { [match: string]: string } = {
|
||||
"&": "\\u0026",
|
||||
">": "\\u003e",
|
||||
"<": "\\u003c",
|
||||
"\u2028": "\\u2028",
|
||||
"\u2029": "\\u2029",
|
||||
};
|
||||
// Routes
|
||||
registerWellKnownRoutes(app);
|
||||
registerMergeRoutes(app);
|
||||
registerManifestRoutes(app);
|
||||
registerSSRRoutes(app);
|
||||
|
||||
const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
|
||||
|
||||
function htmlEscape(str: string): string {
|
||||
return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
|
||||
}
|
||||
export default app
|
||||
export default app;
|
||||
|
||||
29
src/server/middlewares/apiProxy.ts
Normal file
29
src/server/middlewares/apiProxy.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { baseAPIURL } from '@/api/httpClientAdapter.server';
|
||||
import type { Context, Next } from 'hono';
|
||||
|
||||
export async function apiProxyMiddleware(c: Context, next: Next) {
|
||||
const path = c.req.path;
|
||||
|
||||
if (path !== '/r' && !path.startsWith('/r/')) {
|
||||
return await next();
|
||||
}
|
||||
|
||||
const url = new URL(c.req.url);
|
||||
url.host = baseAPIURL.replace(/^https?:\/\//, '');
|
||||
url.protocol = 'https:';
|
||||
url.pathname = path.replace(/^\/r/, '') || '/';
|
||||
url.port = '';
|
||||
|
||||
const headers = new Headers(c.req.header());
|
||||
headers.delete("host");
|
||||
headers.delete("connection");
|
||||
|
||||
return fetch(url.toString(), {
|
||||
method: c.req.method,
|
||||
headers: headers,
|
||||
body: c.req.raw.body,
|
||||
// @ts-ignore
|
||||
duplex: 'half',
|
||||
credentials: 'include'
|
||||
});
|
||||
}
|
||||
20
src/server/middlewares/setup.ts
Normal file
20
src/server/middlewares/setup.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { contextStorage } from 'hono/context-storage';
|
||||
import { cors } from 'hono/cors';
|
||||
import isMobile from 'is-mobile';
|
||||
import type { Hono } from 'hono';
|
||||
|
||||
export function setupMiddlewares(app: Hono) {
|
||||
app.use('*', contextStorage());
|
||||
|
||||
app.use(cors(), async (c, next) => {
|
||||
c.set("fetch", app.request.bind(app));
|
||||
|
||||
const ua = c.req.header("User-Agent");
|
||||
if (!ua) {
|
||||
return c.json({ error: "User-Agent header is missing" }, 400);
|
||||
}
|
||||
|
||||
c.set("isMobile", isMobile({ ua }));
|
||||
await next();
|
||||
});
|
||||
}
|
||||
12
src/server/routes/manifest.ts
Normal file
12
src/server/routes/manifest.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { getListFiles } from '@/server/modules/merge';
|
||||
import type { Hono } from 'hono';
|
||||
|
||||
export function registerManifestRoutes(app: Hono) {
|
||||
app.get('/manifest/:id', async (c) => {
|
||||
const manifest = await getListFiles();
|
||||
if (!manifest) {
|
||||
return c.json({ error: "Manifest not found" }, 404);
|
||||
}
|
||||
return c.json(manifest);
|
||||
});
|
||||
}
|
||||
57
src/server/routes/merge.ts
Normal file
57
src/server/routes/merge.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { baseAPIURL } from '@/api/httpClientAdapter.server';
|
||||
import {
|
||||
createManifest,
|
||||
saveManifest,
|
||||
validateChunkUrls
|
||||
} from '@/server/modules/merge';
|
||||
import type { Hono, MiddlewareHandler } from 'hono';
|
||||
|
||||
const authMiddleware: MiddlewareHandler = async (c, next) => {
|
||||
const headers = new Headers(c.req.header());
|
||||
headers.delete("host");
|
||||
headers.delete("connection");
|
||||
|
||||
try {
|
||||
const res = await fetch(`${baseAPIURL}/me`, {
|
||||
method: 'GET',
|
||||
headers: headers,
|
||||
credentials: 'include'
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.data?.user) {
|
||||
return await next();
|
||||
}
|
||||
throw new Error("Unauthorized");
|
||||
} catch {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
};
|
||||
|
||||
export function registerMergeRoutes(app: Hono) {
|
||||
app.post('/merge', authMiddleware, async (c) => {
|
||||
try {
|
||||
const body = await c.req.json();
|
||||
const { filename, chunks } = body;
|
||||
|
||||
if (!filename || !Array.isArray(chunks) || chunks.length === 0) {
|
||||
return c.json({ error: 'invalid payload' }, 400);
|
||||
}
|
||||
|
||||
const hostError = validateChunkUrls(chunks);
|
||||
if (hostError) return c.json({ error: hostError }, 400);
|
||||
|
||||
const manifest = createManifest(filename, chunks);
|
||||
await saveManifest(manifest);
|
||||
|
||||
return c.json({
|
||||
status: 'ok',
|
||||
id: manifest.id,
|
||||
filename: manifest.filename,
|
||||
total_parts: manifest.total_parts,
|
||||
});
|
||||
} catch (e: any) {
|
||||
return c.json({ error: e?.message ?? String(e) }, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
97
src/server/routes/ssr.ts
Normal file
97
src/server/routes/ssr.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { serializeQueryCache } from '@pinia/colada';
|
||||
import { renderSSRHead } from '@unhead/vue/server';
|
||||
import { streamText } from 'hono/streaming';
|
||||
import { renderToWebStream } from 'vue/server-renderer';
|
||||
// @ts-ignore
|
||||
import Base from '@primevue/core/base';
|
||||
|
||||
import { createApp } from '@/main';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { buildBootstrapScript } from '@/lib/manifest';
|
||||
import { styleTags } from '@/lib/primePassthrough';
|
||||
import { htmlEscape } from '@/server/utils/htmlEscape';
|
||||
import type { Hono } from 'hono';
|
||||
|
||||
const DEFAULT_STYLE_NAMES = ['primitive', 'semantic', 'global', 'base', 'ripple-directive'];
|
||||
|
||||
export function registerSSRRoutes(app: Hono) {
|
||||
app.get("*", async (c) => {
|
||||
const nonce = crypto.randomUUID();
|
||||
const url = new URL(c.req.url);
|
||||
|
||||
const { app: vueApp, router, head, pinia, bodyClass, queryCache } = createApp();
|
||||
|
||||
vueApp.provide("honoContext", c);
|
||||
|
||||
const auth = useAuthStore();
|
||||
auth.$reset();
|
||||
await auth.init();
|
||||
|
||||
await router.push(url.pathname);
|
||||
await router.isReady();
|
||||
|
||||
const usedStyles = new Set<string>();
|
||||
Base.setLoadedStyleName = async (name: string) => usedStyles.add(name);
|
||||
|
||||
return streamText(c, async (stream) => {
|
||||
c.header("Content-Type", "text/html; charset=utf-8");
|
||||
c.header("Content-Encoding", "Identity");
|
||||
|
||||
const ctx: Record<string, any> = {};
|
||||
const appStream = renderToWebStream(vueApp, ctx);
|
||||
|
||||
// HTML Head
|
||||
await stream.write("<!DOCTYPE html><html lang='en'><head>");
|
||||
await stream.write("<base href='" + url.origin + "'/>");
|
||||
|
||||
// SSR Head tags
|
||||
const headResult = await renderSSRHead(head);
|
||||
await stream.write(headResult.headTags.replace(/\n/g, ""));
|
||||
|
||||
// Fonts & Favicon
|
||||
await stream.write(`<link rel="preconnect" href="https://fonts.googleapis.com">`);
|
||||
await stream.write(`<link href="https://fonts.googleapis.com/css2?family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700&display=swap" rel="stylesheet">`);
|
||||
await stream.write('<link rel="icon" href="/favicon.ico" />');
|
||||
|
||||
// Bootstrap scripts
|
||||
await stream.write(buildBootstrapScript());
|
||||
|
||||
// PrimeVue styles
|
||||
if (usedStyles.size > 0) {
|
||||
DEFAULT_STYLE_NAMES.forEach(name => usedStyles.add(name));
|
||||
}
|
||||
|
||||
const activeStyles = styleTags.filter(tag =>
|
||||
usedStyles.has(tag.name.replace(/-(variables|style)$/, ""))
|
||||
);
|
||||
|
||||
for (const tag of activeStyles) {
|
||||
await stream.write(`<style type="text/css" data-primevue-style-id="${tag.name}">${tag.value}</style>`);
|
||||
}
|
||||
|
||||
// Body start
|
||||
await stream.write(`</head><body class='${bodyClass}'>`);
|
||||
|
||||
// App content
|
||||
await stream.pipe(appStream);
|
||||
|
||||
// Cleanup context
|
||||
delete ctx.teleports;
|
||||
delete ctx.__teleportBuffers;
|
||||
delete ctx.modules;
|
||||
|
||||
// Inject state
|
||||
Object.assign(ctx, {
|
||||
$p: pinia.state.value,
|
||||
$colada: serializeQueryCache(queryCache)
|
||||
});
|
||||
|
||||
// App data script
|
||||
const appDataScript = `<script type="application/json" data-ssr="true" id="__APP_DATA__" nonce="${nonce}">${htmlEscape(JSON.stringify(ctx))}</script>`;
|
||||
await stream.write(appDataScript);
|
||||
|
||||
// Close HTML
|
||||
await stream.write("</body></html>");
|
||||
});
|
||||
});
|
||||
}
|
||||
7
src/server/routes/wellKnown.ts
Normal file
7
src/server/routes/wellKnown.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { Hono } from 'hono';
|
||||
|
||||
export function registerWellKnownRoutes(app: Hono) {
|
||||
app.get("/.well-known/*", (c) => {
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
}
|
||||
13
src/server/utils/htmlEscape.ts
Normal file
13
src/server/utils/htmlEscape.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
const ESCAPE_LOOKUP: { [match: string]: string } = {
|
||||
"&": "\\u0026",
|
||||
">": "\\u003e",
|
||||
"<": "\\u003c",
|
||||
"\u2028": "\\u2028",
|
||||
"\u2029": "\\u2029",
|
||||
};
|
||||
|
||||
const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
|
||||
|
||||
export function htmlEscape(str: string): string {
|
||||
return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* @module
|
||||
* html Helper for Hono.
|
||||
*/
|
||||
|
||||
import { escapeToBuffer, raw, resolveCallbackSync, stringBufferToString } from 'hono/utils/html'
|
||||
import type { HtmlEscaped, HtmlEscapedString, StringBufferWithCallbacks } from 'hono/utils/html'
|
||||
|
||||
|
||||
export const html = (
|
||||
strings: TemplateStringsArray,
|
||||
...values: unknown[]
|
||||
): HtmlEscapedString | Promise<HtmlEscapedString> => {
|
||||
const buffer: StringBufferWithCallbacks = [''] as StringBufferWithCallbacks
|
||||
|
||||
for (let i = 0, len = strings.length - 1; i < len; i++) {
|
||||
buffer[0] += strings[i]
|
||||
|
||||
const children = Array.isArray(values[i])
|
||||
? (values[i] as Array<unknown>).flat(Infinity)
|
||||
: [values[i]]
|
||||
for (let i = 0, len = children.length; i < len; i++) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const child = children[i] as any
|
||||
if (typeof child === 'string') {
|
||||
escapeToBuffer(child, buffer)
|
||||
} else if (typeof child === 'number') {
|
||||
;(buffer[0] as string) += child
|
||||
} else if (typeof child === 'boolean' || child === null || child === undefined) {
|
||||
continue
|
||||
} else if (typeof child === 'object' && (child as HtmlEscaped).isEscaped) {
|
||||
if ((child as HtmlEscapedString).callbacks) {
|
||||
buffer.unshift('', child)
|
||||
} else {
|
||||
const tmp = child.toString()
|
||||
if (tmp instanceof Promise) {
|
||||
buffer.unshift('', tmp)
|
||||
} else {
|
||||
buffer[0] += tmp
|
||||
}
|
||||
}
|
||||
} else if (child instanceof Promise) {
|
||||
buffer.unshift('', child)
|
||||
} else {
|
||||
escapeToBuffer(child.toString(), buffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer[0] += strings.at(-1) as string
|
||||
|
||||
return buffer.length === 1
|
||||
? 'callbacks' in buffer
|
||||
? raw(resolveCallbackSync(raw(buffer[0], buffer.callbacks)))
|
||||
: raw(buffer[0])
|
||||
: stringBufferToString(buffer, buffer.callbacks)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { createContext, jsx, Suspense } from "hono/jsx";
|
||||
import { renderToReadableStream, StreamingContext } from "hono/jsx/streaming";
|
||||
import { HtmlEscapedCallback, HtmlEscapedString, raw } from "hono/utils/html";
|
||||
// import { jsxs } from "hono/jsx-renderer";
|
||||
import { Context } from "hono";
|
||||
import type {
|
||||
FC,
|
||||
Context as JSXContext,
|
||||
JSXNode
|
||||
} from "hono/jsx";
|
||||
import { jsxTemplate } from "hono/jsx/jsx-runtime";
|
||||
export const RequestContext: JSXContext<Context<any, any, {}> | null> =
|
||||
createContext<Context | null>(null);
|
||||
export function renderSSRLayout(c: Context, appStream: ReadableStream) {
|
||||
const body = jsxTemplate`${raw("<!DOCTYPE html>")}${_c(
|
||||
RequestContext.Provider,
|
||||
{ value: c },
|
||||
// currentLayout as any
|
||||
_c(
|
||||
"html",
|
||||
{ lang: "en" },
|
||||
_c(
|
||||
"head",
|
||||
null,
|
||||
raw('<meta charset="UTF-8"/>'),
|
||||
raw(
|
||||
'<meta name="viewport" content="width=device-width, initial-scale=1.0"/>'
|
||||
),
|
||||
raw('<link rel="icon" href="/favicon.ico" />'),
|
||||
raw(`<base href="${new URL(c.req.url).origin}/"/>`)
|
||||
),
|
||||
_c(
|
||||
"body",
|
||||
{
|
||||
class:
|
||||
"font-sans bg-[#f9fafd] text-gray-800 antialiased flex flex-col",
|
||||
},
|
||||
_c(
|
||||
StreamingContext,
|
||||
{ value: { scriptNonce: "random-nonce-value" } },
|
||||
_c(
|
||||
Suspense,
|
||||
{ fallback: _c("div", { class: "loading" }, raw("Loading...")) },
|
||||
raw(appStream.getReader())
|
||||
)
|
||||
),
|
||||
_c("script", {
|
||||
dangerouslySetInnerHTML: {
|
||||
__html: `window.__SSR_STATE__ = ${JSON.stringify(
|
||||
JSON.stringify(c.get("ssrContext") || {})
|
||||
)};`,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
)}`;
|
||||
return renderToReadableStream(body);
|
||||
}
|
||||
function _c(
|
||||
tag: string | FC<any>,
|
||||
props: any,
|
||||
...children: (JSXNode | HtmlEscapedCallback | HtmlEscapedString | null)[]
|
||||
): JSXNode {
|
||||
return jsx(tag, props, ...(children as any));
|
||||
}
|
||||
Reference in New Issue
Block a user