This commit is contained in:
2025-12-31 01:09:34 +07:00
commit 772e84c761
25 changed files with 1774 additions and 0 deletions

52
src/index.tsx Normal file
View File

@@ -0,0 +1,52 @@
import { Hono } from 'hono'
import { renderer } from './renderer'
import { createApp } from './main';
import { renderToWebStream } from 'vue/server-renderer';
import { streamText } from 'hono/streaming';
import { renderSSRHead } from '@unhead/vue/server';
const app = new Hono()
// app.use(renderer)
// app.get('/', (c) => {
// return c.text('Hello World!')
// // return c.render(<h1>Hello!</h1>)
// })
app.get("*", async (c) => {
const url = new URL(c.req.url);
const { app, router, head } = createApp();
router.push(url.pathname);
await router.isReady();
return streamText(c, async (stream) => {
c.header("Content-Type", "text/html; charset=utf-8");
c.header("Content-Encoding", "Identity");
const ctx = {};
const appStream = renderToWebStream(app, ctx);
await stream.write("<!DOCTYPE html><html lang='en'><head>");
await stream.write("<base href='" + url.origin + "'/>");
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("</head><body class='font-sans bg-[#f9fafd] text-gray-800 antialiased flex flex-col'>");
await stream.pipe(appStream);
let json = htmlEscape(JSON.stringify(JSON.stringify(ctx)));
await stream.write(`<script>window.__SSR_STATE__ = JSON.parse(${json});</script>`);
await stream.write("</body></html>");
});
// return c.body(renderToWebStream(app, {}));
})
const ESCAPE_LOOKUP: { [match: string]: string } = {
"&": "\\u0026",
">": "\\u003e",
"<": "\\u003c",
"\u2028": "\\u2028",
"\u2029": "\\u2029",
};
const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
function htmlEscape(str: string): string {
return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
}
export default app

View File

@@ -0,0 +1,36 @@
import { defineComponent, onErrorCaptured, ref } from "vue";
export function withErrorBoundary(WrappedComponent: any) {
return defineComponent({
name: `WithErrorBoundary(${WrappedComponent.name})`,
render() {
if (this.hasError) {
return (
<div class="flex items-center justify-center min-h-screen-sm">
<div class="p-8 space-y-lg max-w-lg w-full">
<p>
<b>500.&nbsp;</b>
<ins class="text-gray-500 decoration-none">Đã xảy ra lỗi.</ins>
</p>
<div class="font-thin">
<p>Máy chủ đang gặp sự cố tạm thời không thể xử yêu cầu của bạn. Vui lòng <a class="underline text-primary" href="/">thử lại</a> sau vài phút.</p>
</div>
</div>
</div>
);
}
return <WrappedComponent {...this.$props} v-slots={this.$slots} />;
},
setup() {
const hasError = ref(false);
const error = ref<Error | null>(null);
onErrorCaptured((err) => {
hasError.value = true;
error.value = err as Error;
console.error("Error captured in withErrorBoundary:", err);
return false; // Prevent further propagation
});
return { hasError, error, reset: () => { hasError.value = false; error.value = null; } };
},
});
}

View File

@@ -0,0 +1,77 @@
import SWRVCache, { type ICacheItem } from '..'
import type { IKey } from '../../types'
/**
* LocalStorage cache adapter for swrv data cache.
* https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
*/
export default class LocalStorageCache extends SWRVCache<any> {
private STORAGE_KEY
constructor (key = 'swrv', ttl = 0) {
super(ttl)
this.STORAGE_KEY = key
}
private encode (storage: any) { return JSON.stringify(storage) }
private decode (storage: any) { return JSON.parse(storage) }
get (k: IKey): ICacheItem<IKey> {
const item = localStorage.getItem(this.STORAGE_KEY)
if (item) {
const _key = this.serializeKey(k)
const itemParsed: ICacheItem<any> = JSON.parse(item)[_key]
if (itemParsed?.expiresAt === null) {
itemParsed.expiresAt = Infinity // localStorage sets Infinity to 'null'
}
return itemParsed
}
return undefined as any
}
set (k: string, v: any, ttl: number) {
let payload = {}
const _key = this.serializeKey(k)
const timeToLive = ttl || this.ttl
const storage = localStorage.getItem(this.STORAGE_KEY)
const now = Date.now()
const item = {
data: v,
createdAt: now,
expiresAt: timeToLive ? now + timeToLive : Infinity
}
if (storage) {
payload = this.decode(storage)
(payload as any)[_key] = item
} else {
payload = { [_key]: item }
}
this.dispatchExpire(timeToLive, item, _key)
localStorage.setItem(this.STORAGE_KEY, this.encode(payload))
}
dispatchExpire (ttl: number, item: any, serializedKey: string) {
ttl && setTimeout(() => {
const current = Date.now()
const hasExpired = current >= item.expiresAt
if (hasExpired) this.delete(serializedKey)
}, ttl)
}
delete (serializedKey: string) {
const storage = localStorage.getItem(this.STORAGE_KEY)
let payload = {} as Record<string, any>
if (storage) {
payload = this.decode(storage)
delete payload[serializedKey]
}
localStorage.setItem(this.STORAGE_KEY, this.encode(payload))
}
}

72
src/lib/swr/cache/index.ts vendored Normal file
View File

@@ -0,0 +1,72 @@
import type { IKey } from '../types'
import hash from '../lib/hash'
export interface ICacheItem<Data> {
data: Data,
createdAt: number,
expiresAt: number
}
function serializeKeyDefault (key: IKey): string {
if (typeof key === 'function') {
try {
key = key()
} catch (err) {
// dependencies not ready
key = ''
}
}
if (Array.isArray(key)) {
key = hash(key)
} else {
// convert null to ''
key = String(key || '')
}
return key
}
export default class SWRVCache<CacheData> {
protected ttl: number
private items?: Map<string, ICacheItem<CacheData>>
constructor (ttl = 0) {
this.items = new Map()
this.ttl = ttl
}
serializeKey (key: IKey): string {
return serializeKeyDefault(key)
}
get (k: string): ICacheItem<CacheData> {
const _key = this.serializeKey(k)
return this.items!.get(_key)!
}
set (k: string, v: any, ttl: number) {
const _key = this.serializeKey(k)
const timeToLive = ttl || this.ttl
const now = Date.now()
const item = {
data: v,
createdAt: now,
expiresAt: timeToLive ? now + timeToLive : Infinity
}
this.dispatchExpire(timeToLive, item, _key)
this.items!.set(_key, item)
}
dispatchExpire (ttl: number, item: any, serializedKey: string) {
ttl && setTimeout(() => {
const current = Date.now()
const hasExpired = current >= item.expiresAt
if (hasExpired) this.delete(serializedKey)
}, ttl)
}
delete (serializedKey: string) {
this.items!.delete(serializedKey)
}
}

8
src/lib/swr/index.ts Normal file
View File

@@ -0,0 +1,8 @@
import SWRVCache from './cache'
import useSWRV, { mutate } from './use-swrv'
export {
type IConfig
} from './types'
export { mutate, SWRVCache }
export default useSWRV

44
src/lib/swr/lib/hash.ts Normal file
View File

@@ -0,0 +1,44 @@
// From https://github.com/vercel/swr/blob/master/src/libs/hash.ts
// use WeakMap to store the object->key mapping
// so the objects can be garbage collected.
// WeakMap uses a hashtable under the hood, so the lookup
// complexity is almost O(1).
const table = new WeakMap()
// counter of the key
let counter = 0
// hashes an array of objects and returns a string
export default function hash (args: any[]): string {
if (!args.length) return ''
let key = 'arg'
for (let i = 0; i < args.length; ++i) {
let _hash
if (
args[i] === null ||
(typeof args[i] !== 'object' && typeof args[i] !== 'function')
) {
// need to consider the case that args[i] is a string:
// args[i] _hash
// "undefined" -> '"undefined"'
// undefined -> 'undefined'
// 123 -> '123'
// null -> 'null'
// "null" -> '"null"'
if (typeof args[i] === 'string') {
_hash = '"' + args[i] + '"'
} else {
_hash = String(args[i])
}
} else {
if (!table.has(args[i])) {
_hash = counter
table.set(args[i], counter++)
} else {
_hash = table.get(args[i])
}
}
key += '@' + _hash
}
return key
}

View File

@@ -0,0 +1,27 @@
function isOnline (): boolean {
if (typeof navigator.onLine !== 'undefined') {
return navigator.onLine
}
// always assume it's online
return true
}
function isDocumentVisible (): boolean {
if (
typeof document !== 'undefined' &&
typeof document.visibilityState !== 'undefined'
) {
return document.visibilityState !== 'hidden'
}
// always assume it's visible
return true
}
const fetcher = (url: string | Request) => fetch(url).then(res => res.json())
export default {
isOnline,
isDocumentVisible,
fetcher
}

42
src/lib/swr/types.ts Normal file
View File

@@ -0,0 +1,42 @@
import type { Ref, WatchSource } from 'vue'
import SWRVCache from './cache'
import LocalStorageCache from './cache/adapters/localStorage'
export type fetcherFn<Data> = (...args: any) => Data | Promise<Data>
export interface IConfig<
Data = any,
Fn extends fetcherFn<Data> = fetcherFn<Data>
> {
refreshInterval?: number
cache?: LocalStorageCache | SWRVCache<any>
dedupingInterval?: number
ttl?: number
serverTTL?: number
revalidateOnFocus?: boolean
revalidateDebounce?: number
shouldRetryOnError?: boolean
errorRetryInterval?: number
errorRetryCount?: number
fetcher?: Fn,
isOnline?: () => boolean
isDocumentVisible?: () => boolean
}
export interface revalidateOptions {
shouldRetryOnError?: boolean,
errorRetryCount?: number,
forceRevalidate?: boolean,
}
export interface IResponse<Data = any, Error = any> {
data: Ref<Data | undefined>
error: Ref<Error | undefined>
isValidating: Ref<boolean>
isLoading: Ref<boolean>
mutate: (data?: fetcherFn<Data>, opts?: revalidateOptions) => Promise<void>
}
export type keyType = string | any[] | null | undefined
export type IKey = keyType | WatchSource<keyType>

470
src/lib/swr/use-swrv.ts Normal file
View File

@@ -0,0 +1,470 @@
/** ____
*--------------/ \.------------------/
* / swrv \. / //
* / / /\. / //
* / _____/ / \. /
* / / ____/ . \. /
* / \ \_____ \. /
* / . \_____ \ \ / //
* \ _____/ / ./ / //
* \ / _____/ ./ /
* \ / / . ./ /
* \ / / ./ /
* . \/ ./ / //
* \ ./ / //
* \.. / /
* . ||| /
* ||| /
* . ||| / //
* ||| / //
* ||| /
*/
import {
reactive,
watch,
ref,
toRefs,
// isRef,
onMounted,
onUnmounted,
getCurrentInstance,
isReadonly,
onServerPrefetch,
isRef,
useSSRContext,
type FunctionPlugin,
inject
} from 'vue'
import webPreset from './lib/web-preset'
import SWRVCache from './cache'
import type { IConfig, IKey, IResponse, fetcherFn, revalidateOptions } from './types'
import { tinyassert } from "@hiogawa/utils";
type StateRef<Data, Error> = {
data: Data, error: Error, isValidating: boolean, isLoading: boolean, revalidate: Function, key: any
};
const DATA_CACHE = new SWRVCache<Omit<IResponse, 'mutate'>>()
const REF_CACHE = new SWRVCache<StateRef<any, any>[]>()
const PROMISES_CACHE = new SWRVCache<Omit<IResponse, 'mutate'>>()
const defaultConfig: IConfig = {
cache: DATA_CACHE,
refreshInterval: 0,
ttl: 0,
serverTTL: 1000,
dedupingInterval: 2000,
revalidateOnFocus: true,
revalidateDebounce: 0,
shouldRetryOnError: true,
errorRetryInterval: 5000,
errorRetryCount: 5,
fetcher: webPreset.fetcher,
isOnline: webPreset.isOnline,
isDocumentVisible: webPreset.isDocumentVisible
}
/**
* Cache the refs for later revalidation
*/
function setRefCache(key: string, theRef: StateRef<any, any>, ttl: number) {
const refCacheItem = REF_CACHE.get(key)
if (refCacheItem) {
refCacheItem.data.push(theRef)
} else {
// #51 ensures ref cache does not evict too soon
const gracePeriod = 5000
REF_CACHE.set(key, [theRef], ttl > 0 ? ttl + gracePeriod : ttl)
}
}
function onErrorRetry(revalidate: (any: any, opts: revalidateOptions) => void, errorRetryCount: number, config: IConfig): void {
if (!(config as any).isDocumentVisible()) {
return
}
if (config.errorRetryCount !== undefined && errorRetryCount > config.errorRetryCount) {
return
}
const count = Math.min(errorRetryCount || 0, (config as any).errorRetryCount)
const timeout = count * (config as any).errorRetryInterval
setTimeout(() => {
revalidate(null, { errorRetryCount: count + 1, shouldRetryOnError: true })
}, timeout)
}
/**
* Main mutation function for receiving data from promises to change state and
* set data cache
*/
const mutate = async <Data>(key: string, res: Promise<Data> | Data, cache = DATA_CACHE, ttl = defaultConfig.ttl) => {
let data, error, isValidating
if (isPromise(res)) {
try {
data = await res
} catch (err) {
error = err
}
} else {
data = res
}
// eslint-disable-next-line prefer-const
isValidating = false
const newData = { data, error, isValidating }
if (typeof data !== 'undefined') {
try {
cache.set(key, newData, Number(ttl))
} catch (err) {
console.error('swrv(mutate): failed to set cache', err)
}
}
/**
* Revalidate all swrv instances with new data
*/
const stateRef = REF_CACHE.get(key)
if (stateRef && stateRef.data.length) {
// This filter fixes #24 race conditions to only update ref data of current
// key, while data cache will continue to be updated if revalidation is
// fired
let refs = stateRef.data.filter(r => r.key === key)
refs.forEach((r, idx) => {
if (typeof newData.data !== 'undefined') {
r.data = newData.data
}
r.error = newData.error
r.isValidating = newData.isValidating
r.isLoading = newData.isValidating
const isLast = idx === refs.length - 1
if (!isLast) {
// Clean up refs that belonged to old keys
delete refs[idx]
}
})
refs = refs.filter(Boolean)
}
return newData
}
/* Stale-While-Revalidate hook to handle fetching, caching, validation, and more... */
function useSWRV<Data = any, Error = any>(
key: IKey
): IResponse<Data, Error>
function useSWRV<Data = any, Error = any>(
key: IKey,
fn: fetcherFn<Data> | undefined | null,
config?: IConfig
): IResponse<Data, Error>
function useSWRV<Data = any, Error = any>(...args: any[]): IResponse<Data, Error> {
const injectedConfig = inject<Partial<IConfig> | null>('swrv-config', null)
tinyassert(injectedConfig, 'Injected swrv-config must be an object')
let key: IKey
let fn: fetcherFn<Data> | undefined | null
let config: IConfig = { ...defaultConfig, ...injectedConfig }
let unmounted = false
let isHydrated = false
const instance = getCurrentInstance() as any
const vm = instance?.proxy || instance // https://github.com/vuejs/composition-api/pull/520
if (!vm) {
console.error('Could not get current instance, check to make sure that `useSwrv` is declared in the top level of the setup function.')
throw new Error('Could not get current instance')
}
const IS_SERVER = typeof window === 'undefined' || false
// #region ssr
const isSsrHydration = Boolean(
!IS_SERVER &&
window !== undefined && (window as any).__SSR_STATE__.swrv)
// #endregion
if (args.length >= 1) {
key = args[0]
}
if (args.length >= 2) {
fn = args[1]
}
if (args.length > 2) {
config = {
...config,
...args[2]
}
}
const ttl = IS_SERVER ? config.serverTTL : config.ttl
const keyRef = typeof key === 'function' ? (key as any) : ref(key)
if (typeof fn === 'undefined') {
// use the global fetcher
fn = config.fetcher
}
let stateRef: StateRef<Data, Error> | null = null
// #region ssr
if (isSsrHydration) {
// component was ssrHydrated, so make the ssr reactive as the initial data
const swrvState = (window as any).__SSR_STATE__.swrv || []
const swrvKey = nanoHex(vm.$.type.__name ?? vm.$.type.name)
if (swrvKey !== undefined && swrvKey !== null) {
const nodeState = swrvState[swrvKey] || []
const instanceState = nodeState[nanoHex(isRef(keyRef) ? keyRef.value : keyRef())]
if (instanceState) {
stateRef = reactive(instanceState)
isHydrated = true
}
}
}
// #endregion
if (!stateRef) {
stateRef = reactive({
data: undefined,
error: undefined,
isValidating: true,
isLoading: true,
key: null
}) as StateRef<Data, Error>
}
/**
* Revalidate the cache, mutate data
*/
const revalidate = async (data?: fetcherFn<Data>, opts?: revalidateOptions) => {
const isFirstFetch = stateRef.data === undefined
const keyVal = keyRef.value
if (!keyVal) { return }
const cacheItem = config.cache!.get(keyVal)
const newData = cacheItem && cacheItem.data
stateRef.isValidating = true
stateRef.isLoading = !newData
if (newData) {
stateRef.data = newData.data
stateRef.error = newData.error
}
const fetcher = data || fn
if (
!fetcher ||
(!(config as any).isDocumentVisible() && !isFirstFetch) ||
(opts?.forceRevalidate !== undefined && !opts?.forceRevalidate)
) {
stateRef.isValidating = false
stateRef.isLoading = false
return
}
// Dedupe items that were created in the last interval #76
if (cacheItem) {
const shouldRevalidate = Boolean(
((Date.now() - cacheItem.createdAt) >= (config as any).dedupingInterval) || opts?.forceRevalidate
)
if (!shouldRevalidate) {
stateRef.isValidating = false
stateRef.isLoading = false
return
}
}
const trigger = async () => {
const promiseFromCache = PROMISES_CACHE.get(keyVal)
if (!promiseFromCache) {
const fetcherArgs = Array.isArray(keyVal) ? keyVal : [keyVal]
const newPromise = fetcher(...fetcherArgs)
PROMISES_CACHE.set(keyVal, newPromise, (config as any).dedupingInterval)
await mutate(keyVal, newPromise, (config as any).cache, ttl)
} else {
await mutate(keyVal, promiseFromCache.data, (config as any).cache, ttl)
}
stateRef.isValidating = false
stateRef.isLoading = false
PROMISES_CACHE.delete(keyVal)
if (stateRef.error !== undefined) {
const shouldRetryOnError = !unmounted && config.shouldRetryOnError && (opts ? opts.shouldRetryOnError : true)
if (shouldRetryOnError) {
onErrorRetry(revalidate, opts ? Number(opts.errorRetryCount) : 1, config)
}
}
}
if (newData && config.revalidateDebounce) {
setTimeout(async () => {
if (!unmounted) {
await trigger()
}
}, config.revalidateDebounce)
} else {
await trigger()
}
}
const revalidateCall = async () => revalidate(null as any, { shouldRetryOnError: false })
let timer: any = null
/**
* Setup polling
*/
onMounted(() => {
const tick = async () => {
// component might un-mount during revalidate, so do not set a new timeout
// if this is the case, but continue to revalidate since promises can't
// be cancelled and new hook instances might rely on promise/data cache or
// from pre-fetch
if (!stateRef.error && (config as any).isOnline()) {
// if API request errored, we stop polling in this round
// and let the error retry function handle it
await revalidate()
} else {
if (timer) {
clearTimeout(timer)
}
}
if (config.refreshInterval && !unmounted) {
timer = setTimeout(tick, config.refreshInterval)
}
}
if (config.refreshInterval) {
timer = setTimeout(tick, config.refreshInterval)
}
if (config.revalidateOnFocus) {
document.addEventListener('visibilitychange', revalidateCall, false)
window.addEventListener('focus', revalidateCall, false)
}
})
/**
* Teardown
*/
onUnmounted(() => {
unmounted = true
if (timer) {
clearTimeout(timer)
}
if (config.revalidateOnFocus) {
document.removeEventListener('visibilitychange', revalidateCall, false)
window.removeEventListener('focus', revalidateCall, false)
}
const refCacheItem = REF_CACHE.get(keyRef.value)
if (refCacheItem) {
refCacheItem.data = refCacheItem.data.filter((ref) => ref !== stateRef)
}
})
// #region ssr
if (IS_SERVER) {
const ssrContext = useSSRContext()
// make sure srwv exists in ssrContext
let swrvRes: Record<string, any> = {}
if (ssrContext) {
swrvRes = ssrContext.swrv = ssrContext.swrv || swrvRes
}
const ssrKey = nanoHex(vm.$.type.__name ?? vm.$.type.name)
// if (!vm.$vnode || (vm.$node && !vm.$node.data)) {
// vm.$vnode = {
// data: { attrs: { 'data-swrv-key': ssrKey } }
// }
// }
// const attrs = (vm.$vnode.data.attrs = vm.$vnode.data.attrs || {})
// attrs['data-swrv-key'] = ssrKey
// // Nuxt compatibility
// if (vm.$ssrContext && vm.$ssrContext.nuxt) {
// vm.$ssrContext.nuxt.swrv = swrvRes
// }
if (ssrContext) {
ssrContext.swrv = swrvRes
}
onServerPrefetch(async () => {
await revalidate()
if (!swrvRes[ssrKey]) swrvRes[ssrKey] = {}
swrvRes[ssrKey][nanoHex(keyRef.value)] = {
data: stateRef.data,
error: stateRef.error,
isValidating: stateRef.isValidating
}
})
}
// #endregion
/**
* Revalidate when key dependencies change
*/
try {
watch(keyRef, (val) => {
if (!isReadonly(keyRef)) {
keyRef.value = val
}
stateRef.key = val
stateRef.isValidating = Boolean(val)
setRefCache(keyRef.value, stateRef, Number(ttl))
if (!IS_SERVER && !isHydrated && keyRef.value) {
revalidate()
}
isHydrated = false
}, {
immediate: true
})
} catch {
// do nothing
}
const res: IResponse = {
...toRefs(stateRef),
mutate: (data?: fetcherFn<Data>, opts?: revalidateOptions) => revalidate(data, {
...opts,
forceRevalidate: true
})
}
return res
}
function isPromise<T>(p: any): p is Promise<T> {
return p !== null && typeof p === 'object' && typeof p.then === 'function'
}
/**
* string to hex 8 chars
* @param name string
* @returns string
*/
function nanoHex(name: string): string {
try {
let hash = 0
for (let i = 0; i < name.length; i++) {
const chr = name.charCodeAt(i)
hash = ((hash << 5) - hash) + chr
hash |= 0 // Convert to 32bit integer
}
let hex = (hash >>> 0).toString(16)
while (hex.length < 8) {
hex = '0' + hex
}
return hex
} catch {
console.error("err name: ", name)
return '0000'
}
}
export const vueSWR = (swrvConfig: Partial<IConfig> = defaultConfig): FunctionPlugin => (app) => {
app.config.globalProperties.$swrv = useSWRV
// app.provide('swrv', useSWRV)
app.provide('swrv-config', swrvConfig)
}
export { mutate }
export default useSWRV

7
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,7 @@
import type { ClassValue } from "clsx";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

17
src/main.ts Normal file
View File

@@ -0,0 +1,17 @@
import { createHead as CSRHead } from "@unhead/vue/client";
import { createHead as SSRHead } from "@unhead/vue/server";
import { createSSRApp } from 'vue';
import { RouterView } from 'vue-router';
import { withErrorBoundary } from './lib/hoc/withErrorBoundary';
import { vueSWR } from './lib/swr/use-swrv';
import router from './routes';
// import { appComponents } from './components'
export function createApp() {
const app = createSSRApp(withErrorBoundary(RouterView))
const head = import.meta.env.SSR ? SSRHead() : CSRHead()
app.use(head)
app.use(vueSWR({revalidateOnFocus: false}))
app.use(router)
return { app, router, head }
}

14
src/renderer.tsx Normal file
View File

@@ -0,0 +1,14 @@
import { jsxRenderer } from 'hono/jsx-renderer'
import { Link, ViteClient } from 'vite-ssr-components/hono'
export const renderer = jsxRenderer(({ children }) => {
return (
<html>
<head>
<ViteClient />
<Link href="/src/style.css" rel="stylesheet" />
</head>
<body>{children}</body>
</html>
)
})

7
src/routes/home/Home.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<div>Home</div>
</template>
<script setup lang="ts">
</script>

25
src/routes/index.ts Normal file
View File

@@ -0,0 +1,25 @@
import { type ReactiveHead, type ResolvableValue } from "@unhead/vue";
import {
createMemoryHistory,
createRouter,
createWebHistory,
type RouteRecordRaw,
} from "vue-router";
type RouteData = RouteRecordRaw & {
meta?: ResolvableValue<ReactiveHead>;
children?: RouteData[];
};
const routes: RouteData[] = [
{
path: "/",
component: () => import("./home/Home.vue")
},
];
const router = createRouter({
history: import.meta.env.SSR
? createMemoryHistory() // server
: createWebHistory(), // client
routes,
});
export default router;

3
src/style.css Normal file
View File

@@ -0,0 +1,3 @@
h1 {
font-family: Arial, Helvetica, sans-serif;
}