From ff1d4902bcdf67d2dd4819333bfd0745c8d7548f Mon Sep 17 00:00:00 2001 From: "Mr.Dat" Date: Thu, 26 Feb 2026 18:38:37 +0700 Subject: [PATCH] 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. --- AGENTS.md | 426 ++++++++++++++++------------- src/index.tsx | 171 ++---------- src/server/middlewares/apiProxy.ts | 29 ++ src/server/middlewares/setup.ts | 20 ++ src/server/routes/manifest.ts | 12 + src/server/routes/merge.ts | 57 ++++ src/server/routes/ssr.ts | 97 +++++++ src/server/routes/wellKnown.ts | 7 + src/server/utils/htmlEscape.ts | 13 + src/worker/html.ts | 56 ---- src/worker/ssrLayout.ts | 65 ----- 11 files changed, 488 insertions(+), 465 deletions(-) create mode 100644 src/server/middlewares/apiProxy.ts create mode 100644 src/server/middlewares/setup.ts create mode 100644 src/server/routes/manifest.ts create mode 100644 src/server/routes/merge.ts create mode 100644 src/server/routes/ssr.ts create mode 100644 src/server/routes/wellKnown.ts create mode 100644 src/server/utils/htmlEscape.ts delete mode 100644 src/worker/html.ts delete mode 100644 src/worker/ssrLayout.ts diff --git a/AGENTS.md b/AGENTS.md index c55c688..205d22f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,82 +1,101 @@ -# Holistream - AI Agent Guide +# AGENTS.md -This document provides comprehensive guidance for AI coding agents working on the Holistream project. +This file provides guidance for AI coding agents working with the Holistream codebase. ## Project Overview -**Holistream** is a Vue 3 streaming application with Server-Side Rendering (SSR) deployed on Cloudflare Workers. It provides video upload, management, and streaming capabilities with a focus on performance and user experience. +**Holistream** is a Vue 3 streaming application with Server-Side Rendering (SSR) deployed on Cloudflare Workers. It provides video upload, management, and streaming capabilities for content creators. -- **Name**: holistream -- **Type**: ES Module JavaScript/TypeScript project -- **Package Manager**: Bun (uses `bun.lock`) +### Key Characteristics + +- **Type**: Full-stack web application with SSR +- **Primary Language**: TypeScript +- **Package Manager**: Bun (evident from `bun.lock`) +- **Deployment Target**: Cloudflare Workers ## Technology Stack -| Category | Technology | -|----------|------------| -| **Framework** | Vue 3.5+ with Composition API | -| **Rendering** | SSR (Server-Side Rendering) with Vue's `createSSRApp` | -| **Router** | Vue Router 5 with SSR-aware history | -| **Server** | Hono framework on Cloudflare Workers | -| **Build Tool** | Vite 7 with custom SSR plugin | -| **Styling** | UnoCSS (Tailwind-like utility-first CSS) | -| **UI Components** | PrimeVue 4 with Aura theme | -| **State Management** | Pinia 3 + Pinia Colada for server state | -| **HTTP Client** | Auto-generated from OpenAPI/Swagger spec | -| **Head Management** | @unhead/vue for SEO | -| **Icons** | Custom Vue icon components | -| **Validation** | Zod v4 | -| **Utils** | VueUse, clsx, tailwind-merge | +| Category | Technology | Version | +|----------|------------|---------| +| Framework | Vue | 3.5.27 | +| Router | Vue Router | 5.0.2 | +| Server Framework | Hono | 4.11.7 | +| Build Tool | Vite | 7.3.1 | +| CSS Framework | UnoCSS | 66.6.0 | +| UI Components | PrimeVue | 4.5.4 | +| State Management | Pinia | 3.0.4 | +| Server State | Pinia Colada | 0.21.2 | +| Meta/SEO | @unhead/vue | 2.1.2 | +| Utilities | VueUse | 14.2.0 | +| Validation | Zod | 4.3.6 | +| Deployment | Wrangler | 4.62.0 | ## Project Structure ``` +. ├── src/ │ ├── api/ # API client and HTTP adapters -│ │ ├── client.ts # Auto-generated API client from Swagger -│ │ ├── httpClientAdapter.client.ts # Browser fetch adapter -│ │ ├── httpClientAdapter.server.ts # SSR fetch adapter with cookie forwarding -│ │ └── rpc/ # RPC utilities +│ │ ├── client.ts # Auto-generated API client from OpenAPI spec +│ │ ├── httpClientAdapter.client.ts # Client-side fetch adapter +│ │ └── httpClientAdapter.server.ts # Server-side fetch adapter │ ├── client.ts # Client entry point (hydration) -│ ├── components/ # Vue components (auto-imported) -│ │ ├── icons/ # Custom SVG icon components +│ ├── components/ # Vue components │ │ ├── dashboard/ # Dashboard-specific components -│ │ └── ui/ # UI primitive components +│ │ ├── icons/ # Custom icon components +│ │ ├── ui/ # UI primitive components +│ │ ├── ClientOnly.tsx # SSR-safe client-only wrapper +│ │ ├── DashboardLayout.vue # Main dashboard layout +│ │ ├── GlobalUploadIndicator.vue +│ │ ├── NotificationDrawer.vue +│ │ └── RootLayout.vue # Root application layout │ ├── composables/ # Vue composables -│ │ └── useUploadQueue.ts # File upload queue management +│ │ └── useUploadQueue.ts # Upload queue management │ ├── index.tsx # Server entry point (Hono app) │ ├── lib/ # Utility libraries -│ │ ├── utils.ts # Helper functions (cn, formatBytes, etc.) -│ │ ├── liteMqtt.ts # MQTT client for real-time notifications -│ │ ├── swr/ # SWR cache implementation -│ │ └── directives/ # Vue custom directives +│ │ ├── constants.ts # Application constants +│ │ ├── directives/ # Custom Vue directives +│ │ ├── hoc/ # Higher-order components +│ │ ├── interface.ts # TypeScript interfaces +│ │ ├── liteMqtt.ts # MQTT client (browser) +│ │ ├── manifest.ts # Vite manifest utilities +│ │ ├── PiniaSharedState.ts # Pinia state hydration plugin +│ │ ├── primePassthrough.ts # PrimeVue theme configuration +│ │ ├── replateStreamText.ts +│ │ └── utils.ts # Utility functions (cn, formatters, etc.) │ ├── main.ts # App factory function -│ ├── routes/ # Route components -│ │ ├── auth/ # Login, signup, forgot password -│ │ ├── home/ # Landing, terms, privacy -│ │ ├── notification/ # Notification center +│ ├── mocks/ # Mock data for development +│ ├── routes/ # Route components (page components) +│ │ ├── auth/ # Authentication pages +│ │ ├── home/ # Public pages (landing, terms, privacy) +│ │ ├── notification/ # Notification page │ │ ├── overview/ # Dashboard overview │ │ ├── plans/ # Payments & plans │ │ ├── profile/ # User profile -│ │ ├── upload/ # Video upload interface -│ │ ├── video/ # Video list and detail -│ │ └── index.ts # Router configuration -│ ├── server/ # Server-specific modules +│ │ ├── upload/ # Video upload +│ │ ├── video/ # Video management +│ │ ├── index.ts # Router configuration +│ │ └── NotFound.vue # 404 page +│ ├── server/ # Server-side utilities +│ │ └── modules/ +│ │ └── merge.ts # Video chunk merge logic │ ├── stores/ # Pinia stores -│ │ └── auth.ts # Authentication state +│ │ └── auth.ts # Authentication store │ ├── type.d.ts # TypeScript declarations │ └── worker/ # Worker utilities -├── public/ # Static assets (favicons, etc.) -├── docs.json # OpenAPI/Swagger specification -├── package.json # Dependencies and scripts -├── tsconfig.json # TypeScript configuration -├── vite.config.ts # Vite + Cloudflare plugin config -├── wrangler.jsonc # Cloudflare Workers configuration +│ ├── html.ts +│ └── ssrLayout.ts +├── bootstrap_btn.ts # Bootstrap button preset for UnoCSS +├── ssrPlugin.ts # Custom Vite SSR plugin ├── uno.config.ts # UnoCSS configuration -├── ssrPlugin.ts # Custom SSR build plugin -├── bootstrap_btn.ts # Custom UnoCSS preset for Bootstrap buttons -├── auto-imports.d.ts # Generated auto-import declarations -└── components.d.ts # Generated component declarations +├── vite.config.ts # Vite configuration +├── wrangler.jsonc # Cloudflare Workers configuration +├── tsconfig.json # TypeScript configuration +├── package.json # Package dependencies +├── bun.lock # Bun lock file +├── docs.json # OpenAPI/Swagger spec for API +├── auto-imports.d.ts # Auto-generated type declarations +└── components.d.ts # Auto-generated component declarations ``` ## Build and Development Commands @@ -85,10 +104,10 @@ This document provides comprehensive guidance for AI coding agents working on th # Install dependencies bun install -# Development server with hot reload +# Start development server with hot reload bun dev -# Production build (client + worker) +# Build for production (client + worker) bun run build # Preview production build locally @@ -104,45 +123,49 @@ bun run cf-typegen bun run tail ``` +> **Note**: While npm commands work (`npm run dev`, etc.), the project uses Bun as its primary package manager. + ## Architecture Details ### SSR Architecture -The application uses a custom SSR setup (`ssrPlugin.ts`): +The application uses a custom SSR setup defined in `ssrPlugin.ts`: -1. **Build Order**: Client bundle is built FIRST, then Worker bundle -2. **Manifest Injection**: Vite manifest is injected into server build for asset rendering -3. **Environment-based Module Resolution**: Different implementations for `@httpClientAdapter` and `@liteMqtt` based on SSR context +1. **Build Order**: Client bundle is built FIRST, then the Worker bundle +2. **Manifest Injection**: Vite manifest is injected into the server build for asset rendering +3. **Environment-based Resolution**: `httpClientAdapter` and `liteMqtt` resolve to different implementations based on SSR context -**Entry Points**: +**Entry Points:** - **Server**: `src/index.tsx` - Hono app that renders Vue SSR stream -- **Client**: `src/client.ts` - Hydrates the SSR-rendered app -- **Factory**: `src/main.ts` - Creates app instance (used by both) +- **Client**: `src/client.ts` - Hydrates the SSR-rendered application +- **App Factory**: `src/main.ts` - Creates the Vue app instance (used by both) -### Module Aliases - -| Alias | Resolution | -|-------|------------| -| `@/` | `./src/` | -| `@httpClientAdapter` | `src/api/httpClientAdapter.server.ts` (SSR) or `.client.ts` (browser) | -| `@liteMqtt` | `src/lib/liteMqtt.server.ts` (SSR) or `.ts` (browser) | - -### State Management Pattern +### State Management with SSR Uses **Pinia Colada** for server state with SSR hydration: -- Queries are fetched server-side and serialized to `window.__APP_DATA__` -- Client hydrates the query cache on startup via `hydrateQueryCache()` -- Pinia state is similarly serialized and restored via `PiniaSharedState` plugin +- Server-side queries are fetched and serialized to `window.__APP_DATA__` +- Client hydrates the query cache via `hydrateQueryCache()` +- Pinia state is serialized and restored via `PiniaSharedState` plugin + +### Module Aliases + +Configured in `tsconfig.json` and `vite.config.ts`: + +| Alias | Resolution | +|-------|------------| +| `@/` | `src/` | +| `@httpClientAdapter` | `src/api/httpClientAdapter.server.ts` (SSR) or `.client.ts` (browser) | +| `@liteMqtt` | `src/lib/liteMqtt.server.ts` (SSR) or `.ts` (browser) | ### API Client Architecture -The API client (`src/api/client.ts`) is auto-generated from OpenAPI/Swagger spec (`docs.json`): +The API client (`src/api/client.ts`) is **auto-generated** from the OpenAPI spec (`docs.json`): -- **Base URL**: `r` (proxied to `https://api.pipic.fun`) -- **Server Adapter** (`httpClientAdapter.server.ts`): Forwards cookies, merges headers, calls backend API -- **Client Adapter** (`httpClientAdapter.client.ts`): Standard fetch with credentials -- **Proxy Route**: `/r/*` paths proxy to `https://api.pipic.fun` +- Uses `customFetch` adapter that differs between client/server +- **Server adapter** (`httpClientAdapter.server.ts`): Forwards cookies, merges headers, proxies to `api.pipic.fun` +- **Client adapter** (`httpClientAdapter.client.ts`): Standard fetch with credentials +- API proxy route: `/r/*` paths proxy to `https://api.pipic.fun` ### Routing Structure @@ -150,22 +173,21 @@ Routes are defined in `src/routes/index.ts` with three main layout groups: 1. **Public** (`/`): Landing page, terms, privacy 2. **Auth** (`/login`, `/sign-up`, `/forgot`): Authentication pages (redirects if logged in) -3. **Dashboard**: Protected routes requiring auth +3. **Dashboard**: Protected routes requiring authentication - `/overview` - Main dashboard - - `/upload` - Video upload + - `/upload` - Video upload with queue management - `/video` - Video list - `/video/:id` - Video detail/edit - - `/payments-and-plans` - Billing - - `/notification` - Notifications - - `/profile` - User settings + - `/payments-and-plans` - Billing management + - `/notification`, `/profile` - User settings Route meta supports `@unhead/vue` for SEO: -```typescript -meta: { - head: { - title: "Page Title", - meta: [{ name: "description", content: "..." }] - } +```ts +meta: { + head: { + title: "Page Title", + meta: [{ name: "description", content: "..." }] + } } ``` @@ -173,137 +195,166 @@ meta: { Configuration in `uno.config.ts`: -- **Presets**: Wind4 (Tailwind), Typography, Attributify, Bootstrap buttons -- **Custom Colors**: - - `primary`: #14a74b (green) - - `accent`: #14a74b - - `secondary`: #fd7906 (orange) - - `success`, `info`, `warning`, `danger` +- **Presets**: Wind4 (Tailwind-like), Typography, Attributify, Bootstrap buttons +- **Custom Colors**: + - `primary` (#14a74b) + - `secondary` (#fd7906) + - `accent`, `success`, `info`, `warning`, `danger` - **Shortcuts**: `press-animated` for button press effects -- **Transformers**: `transformerCompileClass` (prefix: `_`), `transformerVariantGroup` +- **Transformers**: + - `transformerCompileClass` (prefix: `_` for compiled classes) + - `transformerVariantGroup` -Use `cn()` from `src/lib/utils.ts` for conditional class merging (clsx + tailwind-merge): -```typescript -import { cn } from '@/lib/utils'; -const className = cn('base-class', condition && 'conditional-class'); -``` +Use `cn()` from `src/lib/utils.ts` for conditional class merging (combines `clsx` + `tailwind-merge`). ### Component Auto-Import -Components in `src/components/` are auto-imported via `unplugin-vue-components`: +Components are auto-imported via `unplugin-vue-components`: - PrimeVue components resolved via `PrimeVueResolver` -- Custom icons in `src/components/icons/` are auto-imported - Vue/Pinia/Vue Router APIs auto-imported via `unplugin-auto-import` +- Type declarations auto-generated to `components.d.ts` and `auto-imports.d.ts` -No need to manually import `ref`, `computed`, `onMounted`, etc. +## Development Guidelines + +### Code Style + +- **TypeScript**: Strict mode enabled +- **JSX/TSX**: Supported for components (import source: `vue`) +- **CSS**: Use UnoCSS utility classes; custom CSS in component ``))); - await stream.write(``); - // await stream.pipe(createTextTransformStreamClass(appStream, (text) => text.replace('
', `
${ctx.teleports["#anchor-header"] || ""}
`).replace('
', `
${ctx.teleports["#anchor-top"] || ""}
`))); - 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(``); - await stream.write(""); - }); -}) -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; diff --git a/src/server/middlewares/apiProxy.ts b/src/server/middlewares/apiProxy.ts new file mode 100644 index 0000000..f8e9e5f --- /dev/null +++ b/src/server/middlewares/apiProxy.ts @@ -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' + }); +} diff --git a/src/server/middlewares/setup.ts b/src/server/middlewares/setup.ts new file mode 100644 index 0000000..fa7f03f --- /dev/null +++ b/src/server/middlewares/setup.ts @@ -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(); + }); +} diff --git a/src/server/routes/manifest.ts b/src/server/routes/manifest.ts new file mode 100644 index 0000000..d04c415 --- /dev/null +++ b/src/server/routes/manifest.ts @@ -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); + }); +} diff --git a/src/server/routes/merge.ts b/src/server/routes/merge.ts new file mode 100644 index 0000000..f9b46a5 --- /dev/null +++ b/src/server/routes/merge.ts @@ -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); + } + }); +} diff --git a/src/server/routes/ssr.ts b/src/server/routes/ssr.ts new file mode 100644 index 0000000..06116c4 --- /dev/null +++ b/src/server/routes/ssr.ts @@ -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(); + 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 = {}; + const appStream = renderToWebStream(vueApp, ctx); + + // HTML Head + await stream.write(""); + await stream.write(""); + + // SSR Head tags + const headResult = await renderSSRHead(head); + await stream.write(headResult.headTags.replace(/\n/g, "")); + + // Fonts & Favicon + await stream.write(``); + await stream.write(``); + await stream.write(''); + + // 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(``); + } + + // Body start + await stream.write(``); + + // 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 = ``; + await stream.write(appDataScript); + + // Close HTML + await stream.write(""); + }); + }); +} diff --git a/src/server/routes/wellKnown.ts b/src/server/routes/wellKnown.ts new file mode 100644 index 0000000..0d8b035 --- /dev/null +++ b/src/server/routes/wellKnown.ts @@ -0,0 +1,7 @@ +import type { Hono } from 'hono'; + +export function registerWellKnownRoutes(app: Hono) { + app.get("/.well-known/*", (c) => { + return c.json({ ok: true }); + }); +} diff --git a/src/server/utils/htmlEscape.ts b/src/server/utils/htmlEscape.ts new file mode 100644 index 0000000..5f1161c --- /dev/null +++ b/src/server/utils/htmlEscape.ts @@ -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]); +} diff --git a/src/worker/html.ts b/src/worker/html.ts deleted file mode 100644 index 3283b89..0000000 --- a/src/worker/html.ts +++ /dev/null @@ -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 => { - 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).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) -} diff --git a/src/worker/ssrLayout.ts b/src/worker/ssrLayout.ts deleted file mode 100644 index 647447a..0000000 --- a/src/worker/ssrLayout.ts +++ /dev/null @@ -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 | null> = - createContext(null); -export function renderSSRLayout(c: Context, appStream: ReadableStream) { - const body = jsxTemplate`${raw("")}${_c( - RequestContext.Provider, - { value: c }, - // currentLayout as any - _c( - "html", - { lang: "en" }, - _c( - "head", - null, - raw(''), - raw( - '' - ), - raw(''), - raw(``) - ), - _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, - props: any, - ...children: (JSXNode | HtmlEscapedCallback | HtmlEscapedString | null)[] -): JSXNode { - return jsx(tag, props, ...(children as any)); -}