diff --git a/README.md b/README.md index 9b1e070..6d83d19 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# 基于koa实现的简易ssr \ No newline at end of file +# 基于koa实现的简易ssr + +- https://segmentfault.com/a/1190000042389086 \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 456dbfa..71e79af 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/internal/helper/package.json b/internal/helper/package.json new file mode 100644 index 0000000..c16c20e --- /dev/null +++ b/internal/helper/package.json @@ -0,0 +1,8 @@ +{ + "name": "helper", + "exports": { + "./*": { + "import": "./src/*.ts" + } + } +} diff --git a/internal/helper/src/cookie.ts b/internal/helper/src/cookie.ts new file mode 100644 index 0000000..29faaa3 --- /dev/null +++ b/internal/helper/src/cookie.ts @@ -0,0 +1,67 @@ +export type CookieOptions = { + path?: string + domain?: string + expires?: Date | string | number + maxAge?: number + secure?: boolean + httpOnly?: boolean + sameSite?: 'lax' | 'strict' | 'none' +} + +export function serializeCookie(name: string, value: string, options: CookieOptions = {}): string { + const enc = encodeURIComponent + let cookie = `${name}=${enc(value)}` + if (options.maxAge != null) cookie += `; Max-Age=${Math.floor(options.maxAge)}` + if (options.expires != null) { + const date = typeof options.expires === 'number' ? new Date(options.expires) : new Date(options.expires) + cookie += `; Expires=${date.toUTCString()}` + } + if (options.domain) cookie += `; Domain=${options.domain}` + if (options.path) cookie += `; Path=${options.path}` + if (options.secure) cookie += `; Secure` + if (options.httpOnly) cookie += `; HttpOnly` + if (options.sameSite) cookie += `; SameSite=${options.sameSite === 'none' ? 'None' : options.sameSite === 'lax' ? 'Lax' : 'Strict'}` + return cookie +} + +export function parseCookieHeader(header: string | undefined): Record { + const raw = header || '' + const out: Record = {} + raw.split(';').map(s => s.trim()).filter(Boolean).forEach(kv => { + const idx = kv.indexOf('=') + const k = idx >= 0 ? kv.slice(0, idx) : kv + const v = idx >= 0 ? decodeURIComponent(kv.slice(idx + 1)) : '' + out[k] = v + }) + return out +} + +export function parseDocumentCookies(): Record { + // @ts-ignore + if (typeof document === 'undefined') return {} + // @ts-ignore + return parseCookieHeader(document.cookie) +} + + +/** +// server 侧中间件 +import { parseCookieHeader, serializeCookie } from './src/compose/cookieUtils' + +app.use(async (ctx, next) => { + const cookies = parseCookieHeader(ctx.request.headers.cookie as string) + + // 读取 + const token = cookies['demo_token'] + + // 写入(HttpOnly 更安全) + if (!token) { + const setItem = serializeCookie('demo_token', 'from-mw', { + httpOnly: true, path: '/', sameSite: 'lax' + }) + ctx.set('Set-Cookie', [setItem]) + } + + await next() +}) + */ \ No newline at end of file diff --git a/internal/helper/src/env.ts b/internal/helper/src/env.ts new file mode 100644 index 0000000..a208659 --- /dev/null +++ b/internal/helper/src/env.ts @@ -0,0 +1,11 @@ + +const isProduction = process.env.NODE_ENV === 'production' +const port = process.env.PORT || 5173 +const base = process.env.BASE || '/' + + +export const Env = { + isProduction, + port: Number(port), + base, +} \ No newline at end of file diff --git a/internal/helper/src/path.ts b/internal/helper/src/path.ts new file mode 100644 index 0000000..41e8f4c --- /dev/null +++ b/internal/helper/src/path.ts @@ -0,0 +1,23 @@ +import path from "node:path" +import fs from "node:fs/promises" + +const isProduction = process.env.NODE_ENV === 'production' + +export function getPathByRoot(...argus: string[]) { + return path.resolve(import.meta.dir, '../../..', ...argus) +} + +const templateHtml = isProduction + ? await fs.readFile(getPathByRoot('packages', 'client/index.html'), 'utf-8') + : '' + +export function getDevPathFromClient(...argus: string[]) { + return getPathByRoot('packages', 'client', ...argus) +} +export function getDevPathFromServer(...argus: string[]) { + return getPathByRoot('packages', 'server', ...argus) +} + +export function getProdPath(...argus: string[]) { + return getPathByRoot('dist', ...argus) +} diff --git a/internal/x/components/ClientOnly.vue b/internal/x/components/ClientOnly.vue new file mode 100644 index 0000000..479fe11 --- /dev/null +++ b/internal/x/components/ClientOnly.vue @@ -0,0 +1,62 @@ + diff --git a/src/compose/README.md b/internal/x/composables/README.md similarity index 100% rename from src/compose/README.md rename to internal/x/composables/README.md diff --git a/src/compose/cookieUtils.ts b/internal/x/composables/cookieUtils.ts similarity index 100% rename from src/compose/cookieUtils.ts rename to internal/x/composables/cookieUtils.ts diff --git a/src/compose/ssrContext.ts b/internal/x/composables/ssrContext.ts similarity index 84% rename from src/compose/ssrContext.ts rename to internal/x/composables/ssrContext.ts index 0e8703a..60ddc25 100644 --- a/src/compose/ssrContext.ts +++ b/internal/x/composables/ssrContext.ts @@ -3,6 +3,7 @@ export interface SSRContext { cache?: Map cookies?: Record + piniaState?: object setCookies?: string[] [key: string]: any } @@ -10,11 +11,16 @@ export interface SSRContext { export function createSSRContext(): SSRContext { return { cache: new Map(), + piniaState: {}, cookies: {}, setCookies: [] } } +/** + * 将 SSR 上下文注入到 window 对象 + * 在客户端水合时调用 + */ export function hydrateSSRContext(context: SSRContext): void { if (typeof window !== 'undefined') { if (context.cache && Array.isArray(context.cache)) { @@ -24,6 +30,10 @@ export function hydrateSSRContext(context: SSRContext): void { } } +/** + * 清除 SSR 上下文 + * 在客户端水合完成后调用 + */ export function clearSSRContext(): void { if (typeof window !== 'undefined') { delete (window as any).__SSR_CONTEXT__ diff --git a/src/compose/useCookie.ts b/internal/x/composables/useCookie.ts similarity index 100% rename from src/compose/useCookie.ts rename to internal/x/composables/useCookie.ts diff --git a/src/compose/useFetch.ts b/internal/x/composables/useFetch.ts similarity index 89% rename from src/compose/useFetch.ts rename to internal/x/composables/useFetch.ts index e976b0c..3f3150c 100644 --- a/src/compose/useFetch.ts +++ b/internal/x/composables/useFetch.ts @@ -217,33 +217,3 @@ export function useFetch( execute } } - -/** - * 创建 SSR 上下文的辅助函数 - * 在服务端渲染时调用 - */ -// 删除 createSSRContext,这个职责移动到 ssrContext.ts - -/** - * 将 SSR 上下文注入到 window 对象 - * 在客户端水合时调用 - */ -export function hydrateSSRContext(context: SSRContext): void { - if (typeof window !== 'undefined') { - // 确保 Map 对象正确重建 - if (context.cache && Array.isArray(context.cache)) { - context.cache = new Map(context.cache) - } - (window as any).__SSR_CONTEXT__ = context - } -} - -/** - * 清除 SSR 上下文 - * 在客户端水合完成后调用 - */ -export function clearSSRContext(): void { - if (typeof window !== 'undefined') { - delete (window as any).__SSR_CONTEXT__ - } -} diff --git a/internal/x/package.json b/internal/x/package.json new file mode 100644 index 0000000..0e7115f --- /dev/null +++ b/internal/x/package.json @@ -0,0 +1,5 @@ +{ + "name": "x", + "main": "index.ts", + "type": "module" +} diff --git a/package.json b/package.json index abf2fec..61f6249 100644 --- a/package.json +++ b/package.json @@ -2,33 +2,32 @@ "name": "koa-ssr", "type": "module", "workspaces": [ - "packages/*" + "packages/*", + "internal/*" ], "scripts": { - "dev": "bun run --watch server.ts", - "build": "npm run build:client && npm run build:server", - "build:client": "vite build --outDir dist/client", - "build:server": "vite build --ssr src/entry-server.ts --outDir dist/server", - "preview": "cross-env NODE_ENV=production bun run server.ts", - "check": "vue-tsc" + "dev": "bun run --hot packages/server/src/booststap.ts", + "preview": "cross-env NODE_ENV=production bun run packages/server/src/booststap.ts", + "tsc:booststap": "tsc packages/booststap/src/server.ts --outDir dist --module es2022 --target es2022 --lib es2022,dom --moduleResolution bundler --esModuleInterop --skipLibCheck --forceConsistentCasingInFileNames --noEmit false --incremental false", + "tsc:server": "tsc packages/server/src/**/*.ts --outDir dist/server --module es2022 --target es2022 --lib es2022,dom --moduleResolution bundler --esModuleInterop --skipLibCheck --forceConsistentCasingInFileNames --noEmit false --incremental false" }, "devDependencies": { "@types/bun": "latest", - "@types/koa": "^3.0.0", - "@types/koa-send": "^4.1.6", + "@types/koa-compose": "^3.2.8", + "client": "workspace:*", + "core": "workspace:*", "cross-env": "^10.1.0", + "helper": "workspace:*", + "server": "workspace:*", "unplugin-vue-components": "^29.1.0", - "vue-tsc": "^3.1.0" + "vite-plugin-devtools-json": "^1.0.0" }, "peerDependencies": { "typescript": "^5.0.0" }, "dependencies": { - "@vitejs/plugin-vue": "^6.0.1", - "koa": "^3.0.1", - "koa-connect": "^2.1.0", - "koa-send": "^5.0.1", - "vite": "^7.1.7", - "vue": "^3.5.22" + "koa-compose": "^4.1.0", + "pinia": "^3.0.3", + "vite": "^7.1.7" } } diff --git a/packages/client/.gitignore b/packages/client/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/packages/client/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/packages/client/README.md b/packages/client/README.md new file mode 100644 index 0000000..d523fd1 --- /dev/null +++ b/packages/client/README.md @@ -0,0 +1,15 @@ +# client + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.21. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/packages/client/auto-imports.d.ts b/packages/client/auto-imports.d.ts new file mode 100644 index 0000000..055e7ca --- /dev/null +++ b/packages/client/auto-imports.d.ts @@ -0,0 +1,203 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +// biome-ignore lint: disable +export {} +declare global { + const EffectScope: typeof import('vue')['EffectScope'] + const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] + const clearSSRContext: typeof import('../../internal/x/composables/ssrContext')['clearSSRContext'] + const computed: typeof import('vue')['computed'] + const createApp: typeof import('vue')['createApp'] + const createPinia: typeof import('pinia')['createPinia'] + const createSSRContext: typeof import('../../internal/x/composables/ssrContext')['createSSRContext'] + const customRef: typeof import('vue')['customRef'] + const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] + const defineComponent: typeof import('vue')['defineComponent'] + const defineStore: typeof import('pinia')['defineStore'] + const effectScope: typeof import('vue')['effectScope'] + const getActivePinia: typeof import('pinia')['getActivePinia'] + const getCurrentInstance: typeof import('vue')['getCurrentInstance'] + const getCurrentScope: typeof import('vue')['getCurrentScope'] + const getCurrentWatcher: typeof import('vue')['getCurrentWatcher'] + const h: typeof import('vue')['h'] + const hydrateSSRContext: typeof import('../../internal/x/composables/ssrContext')['hydrateSSRContext'] + const inject: typeof import('vue')['inject'] + const isProxy: typeof import('vue')['isProxy'] + const isReactive: typeof import('vue')['isReactive'] + const isReadonly: typeof import('vue')['isReadonly'] + const isRef: typeof import('vue')['isRef'] + const isShallow: typeof import('vue')['isShallow'] + const mapActions: typeof import('pinia')['mapActions'] + const mapGetters: typeof import('pinia')['mapGetters'] + const mapState: typeof import('pinia')['mapState'] + const mapStores: typeof import('pinia')['mapStores'] + const mapWritableState: typeof import('pinia')['mapWritableState'] + const markRaw: typeof import('vue')['markRaw'] + const nextTick: typeof import('vue')['nextTick'] + const onActivated: typeof import('vue')['onActivated'] + const onBeforeMount: typeof import('vue')['onBeforeMount'] + const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] + const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] + const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] + const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] + const onDeactivated: typeof import('vue')['onDeactivated'] + const onErrorCaptured: typeof import('vue')['onErrorCaptured'] + const onMounted: typeof import('vue')['onMounted'] + const onRenderTracked: typeof import('vue')['onRenderTracked'] + const onRenderTriggered: typeof import('vue')['onRenderTriggered'] + const onScopeDispose: typeof import('vue')['onScopeDispose'] + const onServerPrefetch: typeof import('vue')['onServerPrefetch'] + const onUnmounted: typeof import('vue')['onUnmounted'] + const onUpdated: typeof import('vue')['onUpdated'] + const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] + const parseCookieHeader: typeof import('../../internal/x/composables/cookieUtils')['parseCookieHeader'] + const parseDocumentCookies: typeof import('../../internal/x/composables/cookieUtils')['parseDocumentCookies'] + const provide: typeof import('vue')['provide'] + const reactive: typeof import('vue')['reactive'] + const readonly: typeof import('vue')['readonly'] + const ref: typeof import('vue')['ref'] + const render: typeof import('../../internal/x/composables/README.md')['render'] + const resolveComponent: typeof import('vue')['resolveComponent'] + const resolveSSRContext: typeof import('../../internal/x/composables/ssrContext')['resolveSSRContext'] + const serializeCookie: typeof import('../../internal/x/composables/cookieUtils')['serializeCookie'] + const setActivePinia: typeof import('pinia')['setActivePinia'] + const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] + const shallowReactive: typeof import('vue')['shallowReactive'] + const shallowReadonly: typeof import('vue')['shallowReadonly'] + const shallowRef: typeof import('vue')['shallowRef'] + const storeToRefs: typeof import('pinia')['storeToRefs'] + const toRaw: typeof import('vue')['toRaw'] + const toRef: typeof import('vue')['toRef'] + const toRefs: typeof import('vue')['toRefs'] + const toValue: typeof import('vue')['toValue'] + const triggerRef: typeof import('vue')['triggerRef'] + const unref: typeof import('vue')['unref'] + const useAttrs: typeof import('vue')['useAttrs'] + const useAuthStore: typeof import('./src/store/auth')['useAuthStore'] + const useCookie: typeof import('../../internal/x/composables/useCookie')['useCookie'] + const useCssModule: typeof import('vue')['useCssModule'] + const useCssVars: typeof import('vue')['useCssVars'] + const useFetch: typeof import('../../internal/x/composables/useFetch')['useFetch'] + const useGlobal: typeof import('./src/composables/useGlobal/index')['useGlobal'] + const useId: typeof import('vue')['useId'] + const useLink: typeof import('vue-router')['useLink'] + const useModel: typeof import('vue')['useModel'] + const useRoute: typeof import('vue-router')['useRoute'] + const useRouter: typeof import('vue-router')['useRouter'] + const useSlots: typeof import('vue')['useSlots'] + const useTemplateRef: typeof import('vue')['useTemplateRef'] + const watch: typeof import('vue')['watch'] + const watchEffect: typeof import('vue')['watchEffect'] + const watchPostEffect: typeof import('vue')['watchPostEffect'] + const watchSyncEffect: typeof import('vue')['watchSyncEffect'] +} +// for type re-export +declare global { + // @ts-ignore + export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' + import('vue') + // @ts-ignore + export type { CookieOptions } from '../../internal/x/composables/cookieUtils' + import('../../internal/x/composables/cookieUtils') + // @ts-ignore + export type { SSRContext } from '../../internal/x/composables/ssrContext' + import('../../internal/x/composables/ssrContext') +} + +// for vue template auto import +import { UnwrapRef } from 'vue' +declare module 'vue' { + interface GlobalComponents {} + interface ComponentCustomProperties { + readonly EffectScope: UnwrapRef + readonly acceptHMRUpdate: UnwrapRef + readonly clearSSRContext: UnwrapRef + readonly computed: UnwrapRef + readonly createApp: UnwrapRef + readonly createPinia: UnwrapRef + readonly createSSRContext: UnwrapRef + readonly customRef: UnwrapRef + readonly defineAsyncComponent: UnwrapRef + readonly defineComponent: UnwrapRef + readonly defineStore: UnwrapRef + readonly effectScope: UnwrapRef + readonly getActivePinia: UnwrapRef + readonly getCurrentInstance: UnwrapRef + readonly getCurrentScope: UnwrapRef + readonly getCurrentWatcher: UnwrapRef + readonly h: UnwrapRef + readonly hydrateSSRContext: UnwrapRef + readonly inject: UnwrapRef + readonly isProxy: UnwrapRef + readonly isReactive: UnwrapRef + readonly isReadonly: UnwrapRef + readonly isRef: UnwrapRef + readonly isShallow: UnwrapRef + readonly mapActions: UnwrapRef + readonly mapGetters: UnwrapRef + readonly mapState: UnwrapRef + readonly mapStores: UnwrapRef + readonly mapWritableState: UnwrapRef + readonly markRaw: UnwrapRef + readonly nextTick: UnwrapRef + readonly onActivated: UnwrapRef + readonly onBeforeMount: UnwrapRef + readonly onBeforeRouteLeave: UnwrapRef + readonly onBeforeRouteUpdate: UnwrapRef + readonly onBeforeUnmount: UnwrapRef + readonly onBeforeUpdate: UnwrapRef + readonly onDeactivated: UnwrapRef + readonly onErrorCaptured: UnwrapRef + readonly onMounted: UnwrapRef + readonly onRenderTracked: UnwrapRef + readonly onRenderTriggered: UnwrapRef + readonly onScopeDispose: UnwrapRef + readonly onServerPrefetch: UnwrapRef + readonly onUnmounted: UnwrapRef + readonly onUpdated: UnwrapRef + readonly onWatcherCleanup: UnwrapRef + readonly parseCookieHeader: UnwrapRef + readonly parseDocumentCookies: UnwrapRef + readonly provide: UnwrapRef + readonly reactive: UnwrapRef + readonly readonly: UnwrapRef + readonly ref: UnwrapRef + readonly render: UnwrapRef + readonly resolveComponent: UnwrapRef + readonly resolveSSRContext: UnwrapRef + readonly serializeCookie: UnwrapRef + readonly setActivePinia: UnwrapRef + readonly setMapStoreSuffix: UnwrapRef + readonly shallowReactive: UnwrapRef + readonly shallowReadonly: UnwrapRef + readonly shallowRef: UnwrapRef + readonly storeToRefs: UnwrapRef + readonly toRaw: UnwrapRef + readonly toRef: UnwrapRef + readonly toRefs: UnwrapRef + readonly toValue: UnwrapRef + readonly triggerRef: UnwrapRef + readonly unref: UnwrapRef + readonly useAttrs: UnwrapRef + readonly useAuthStore: UnwrapRef + readonly useCookie: UnwrapRef + readonly useCssModule: UnwrapRef + readonly useCssVars: UnwrapRef + readonly useFetch: UnwrapRef + readonly useGlobal: UnwrapRef + readonly useId: UnwrapRef + readonly useLink: UnwrapRef + readonly useModel: UnwrapRef + readonly useRoute: UnwrapRef + readonly useRouter: UnwrapRef + readonly useSlots: UnwrapRef + readonly useTemplateRef: UnwrapRef + readonly watch: UnwrapRef + readonly watchEffect: UnwrapRef + readonly watchPostEffect: UnwrapRef + readonly watchSyncEffect: UnwrapRef + } +} \ No newline at end of file diff --git a/components.d.ts b/packages/client/components.d.ts similarity index 66% rename from components.d.ts rename to packages/client/components.d.ts index eb6c0af..2bb3718 100644 --- a/components.d.ts +++ b/packages/client/components.d.ts @@ -8,10 +8,13 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { - ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default'] + AiDemo: typeof import('./src/components/AiDemo/index.vue')['default'] + ClientOnly: typeof import('./../../internal/x/components/ClientOnly.vue')['default'] CookieDemo: typeof import('./src/components/CookieDemo.vue')['default'] DataFetch: typeof import('./src/components/DataFetch.vue')['default'] HelloWorld: typeof import('./src/components/HelloWorld.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] SimpleTest: typeof import('./src/components/SimpleTest.vue')['default'] } } diff --git a/index.html b/packages/client/index.html similarity index 82% rename from index.html rename to packages/client/index.html index 88db749..0ec77f1 100644 --- a/index.html +++ b/packages/client/index.html @@ -11,7 +11,7 @@
- + \ No newline at end of file diff --git a/packages/client/package.json b/packages/client/package.json new file mode 100644 index 0000000..40cdbff --- /dev/null +++ b/packages/client/package.json @@ -0,0 +1,27 @@ +{ + "name": "client", + "type": "module", + "scripts": { + "build": "bun run build:client && bun run build:server", + "build:client": "vite build --ssrManifest --outDir ../../dist/client --base ./", + "build:server": "vite build --ssr src/entry-server.ts --outDir ../../dist/server", + "check": "vue-tsc" + }, + "devDependencies": { + "unplugin-vue-components": "^29.1.0", + "vue-tsc": "^3.1.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "dompurify": "^3.2.7", + "htmlparser2": "^10.0.0", + "marked": "^16.3.0", + "unplugin-auto-import": "^20.2.0", + "vue": "^3.5.22", + "vue-router": "^4.5.1", + "x": "workspace:*" + } +} diff --git a/public/vite.svg b/packages/client/public/vite.svg similarity index 100% rename from public/vite.svg rename to packages/client/public/vite.svg diff --git a/packages/client/src/App.vue b/packages/client/src/App.vue new file mode 100644 index 0000000..1c0065b --- /dev/null +++ b/packages/client/src/App.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/src/assets/vue.svg b/packages/client/src/assets/vue.svg similarity index 100% rename from src/assets/vue.svg rename to packages/client/src/assets/vue.svg diff --git a/packages/client/src/components/AiDemo/_/VueNodeRenderer.vue b/packages/client/src/components/AiDemo/_/VueNodeRenderer.vue new file mode 100644 index 0000000..e716b7a --- /dev/null +++ b/packages/client/src/components/AiDemo/_/VueNodeRenderer.vue @@ -0,0 +1,56 @@ + + + \ No newline at end of file diff --git a/packages/client/src/components/AiDemo/_/sseData.ts b/packages/client/src/components/AiDemo/_/sseData.ts new file mode 100644 index 0000000..5ec3ce3 --- /dev/null +++ b/packages/client/src/components/AiDemo/_/sseData.ts @@ -0,0 +1,31 @@ +export default [ + { event: "message", answer: "## asdas\n" }, + { event: "message", answer: "**asasa**\n" }, + { event: "message", answer: "![啊啊啊](https://ts1.tc.mm.bing.net/th/id/R-C.823270fc68b9c58f0d9b3feb92b7b172?rik=aubbEBMSC86e%2bw&riu=http%3a%2f%2fimg95.699pic.com%2fphoto%2f50038%2f1181.jpg_wh860.jpg&ehk=iQboj4JMLLfDitOL7VJtSktED0AE%2f7Fyxfik0GTJkyQ%3d&risl=&pid=ImgRaw&r=0)asd\n" }, + { event: "message", answer: "```\nasdaaaasasaasaas\n" }, + { event: "message", answer: "asdsaa\n" }, + { event: "message", answer: "console.log(as)\n" }, + { event: "message", answer: "asa\n```\n\n" }, + { event: "message", answer: "\n\n" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message", answer: "## asdas" }, + { event: "message_end", answer: "## asdas" }, +] \ No newline at end of file diff --git a/packages/client/src/components/AiDemo/index.vue b/packages/client/src/components/AiDemo/index.vue new file mode 100644 index 0000000..9b1636d --- /dev/null +++ b/packages/client/src/components/AiDemo/index.vue @@ -0,0 +1,158 @@ + + + + + + diff --git a/src/components/CookieDemo.vue b/packages/client/src/components/CookieDemo.vue similarity index 93% rename from src/components/CookieDemo.vue rename to packages/client/src/components/CookieDemo.vue index 69320a6..3c65edd 100644 --- a/src/components/CookieDemo.vue +++ b/packages/client/src/components/CookieDemo.vue @@ -13,9 +13,9 @@ + ${preloadLinks} + ` + + return { html, head, setCookies: ssrContext.setCookies || [] } +} + +function renderPreloadLinks(modules: any, manifest: any) { + let links = '' + const seen = new Set() + modules.forEach((id: any) => { + const files = manifest[id] + if (files) { + files.forEach((file: any) => { + if (!seen.has(file)) { + seen.add(file) + const filename = basename(file) + if (manifest[filename]) { + for (const depFile of manifest[filename]) { + links += renderPreloadLink(depFile) + seen.add(depFile) + } + } + links += renderPreloadLink(file) + } + }) + } + }) + return links +} + +function renderPreloadLink(file: string) { + if (file.endsWith('.js')) { + return `` + } else if (file.endsWith('.css')) { + return `` + } else if (file.endsWith('.woff')) { + return ` ` + } else if (file.endsWith('.woff2')) { + return ` ` + } else if (file.endsWith('.gif')) { + return ` ` + } else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) { + return ` ` + } else if (file.endsWith('.png')) { + return ` ` + } else { + return '' + } +} diff --git a/src/main.ts b/packages/client/src/main.ts similarity index 68% rename from src/main.ts rename to packages/client/src/main.ts index 3284bfc..d7f1f4b 100644 --- a/src/main.ts +++ b/packages/client/src/main.ts @@ -1,16 +1,22 @@ import { createSSRApp } from 'vue' import App from './App.vue' +import createSSRRouter from './router'; +import { createPinia } from 'pinia' // SSR requires a fresh app instance per request, therefore we export a function // that creates a fresh app instance. If using Vuex, we'd also be creating a // fresh store here. export function createApp(ssrContext?: any) { const app = createSSRApp(App) - + const router = createSSRRouter() + const pinia = createPinia() + + app.use(router) + app.use(pinia) + // 如果有 SSR 上下文,注入到应用中 if (ssrContext) { app.config.globalProperties.$ssrContext = ssrContext } - - return { app } + return { app, router, pinia } } diff --git a/packages/client/src/pages/about/index.vue b/packages/client/src/pages/about/index.vue new file mode 100644 index 0000000..597f5c1 --- /dev/null +++ b/packages/client/src/pages/about/index.vue @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/packages/client/src/pages/home/index.vue b/packages/client/src/pages/home/index.vue new file mode 100644 index 0000000..573de29 --- /dev/null +++ b/packages/client/src/pages/home/index.vue @@ -0,0 +1,19 @@ + + + diff --git a/packages/client/src/pages/not-found/index.vue b/packages/client/src/pages/not-found/index.vue new file mode 100644 index 0000000..5ef1232 --- /dev/null +++ b/packages/client/src/pages/not-found/index.vue @@ -0,0 +1,5 @@ + diff --git a/packages/client/src/router/index.ts b/packages/client/src/router/index.ts new file mode 100644 index 0000000..82c1eea --- /dev/null +++ b/packages/client/src/router/index.ts @@ -0,0 +1,16 @@ +// src/router.js +import { createRouter, createMemoryHistory, createWebHistory } from 'vue-router'; +import NotFound from '../pages/not-found/index.vue'; + +export default function createSSRRouter() { + return createRouter({ + history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(), // 使用内存模式 + routes: [ + { name: "home", path: '/', meta: { cache: true }, component: () => import('../pages/home/index.vue') }, + { name: "about", path: '/about', meta: { cache: true }, component: () => import('../pages/about/index.vue') }, + + // 404 + { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }, + ], + }); +} \ No newline at end of file diff --git a/packages/client/src/store/auth.ts b/packages/client/src/store/auth.ts new file mode 100644 index 0000000..3589613 --- /dev/null +++ b/packages/client/src/store/auth.ts @@ -0,0 +1,20 @@ +import { defineStore } from "pinia"; + +// export const useAuthStore = defineStore("auth", { +// state: () => ({ +// user: null as null | { name: string }, +// }), +// actions: { +// setUser(user: { name: string }) { +// this.user = user; +// } +// } +// }); +export const useAuthStore = defineStore("auth", () => { + const user = ref(null); + function setUser(u: { name: string }) { + user.value = u; + } + + return { user, setUser }; +}); diff --git a/src/vite-env.d.ts b/packages/client/src/vite-env.d.ts similarity index 100% rename from src/vite-env.d.ts rename to packages/client/src/vite-env.d.ts diff --git a/src/vue.d.ts b/packages/client/src/vue.d.ts similarity index 100% rename from src/vue.d.ts rename to packages/client/src/vue.d.ts diff --git a/tsconfig.json b/packages/client/tsconfig.json similarity index 94% rename from tsconfig.json rename to packages/client/tsconfig.json index ebbb12c..fc06120 100644 --- a/tsconfig.json +++ b/packages/client/tsconfig.json @@ -21,6 +21,6 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "components.d.ts"], + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "components.d.ts", "auto-imports.d.ts"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/tsconfig.node.json b/packages/client/tsconfig.node.json similarity index 100% rename from tsconfig.node.json rename to packages/client/tsconfig.node.json diff --git a/packages/client/vite.config.ts b/packages/client/vite.config.ts new file mode 100644 index 0000000..02f84e2 --- /dev/null +++ b/packages/client/vite.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import Components from 'unplugin-vue-components/vite' +import AutoImport from 'unplugin-auto-import/vite' +import devtoolsJson from 'vite-plugin-devtools-json'; + +// https://vite.dev/config/ +export default defineConfig({ + build: { + emptyOutDir: true + }, + plugins: [ + devtoolsJson(), + vue(), + Components({ + dts: true, + dirs: ['src/components', '../../internal/x/components'], + globsExclude: ["**/_*/**/*"] + }), + AutoImport({ + dts: true, + dtsMode: "overwrite", + ignore: ["**/_*/**/*"], + imports: ['vue', 'vue-router', 'pinia'], + dirs: ['./src/composables/**/*', '../../internal/x/composables/**', "./src/store/**/*"], + vueTemplate: true, + }), + ], +}) diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..30dabbb --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,9 @@ +{ + "name": "core", + "exports": { + "./*": { + "import": "./src/*.ts", + "require": "./src/*.ts" + } + } +} diff --git a/packages/core/src/SsrMiddleWare.ts b/packages/core/src/SsrMiddleWare.ts new file mode 100644 index 0000000..b6e8388 --- /dev/null +++ b/packages/core/src/SsrMiddleWare.ts @@ -0,0 +1,100 @@ +import fs from 'node:fs/promises' +import { getPathByRoot } from "helper/path" +import { parseCookieHeader } from "helper/cookie" +import { Env } from "helper/env" +import { ViteDevServer } from 'vite' +import Send from 'koa-send' +import type Koa from 'koa' +import c2k from 'koa-connect' + +const isProduction = Env.isProduction +const base = Env.base + +const templateHtml = isProduction + ? await fs.readFile(getPathByRoot('dist', 'client/index.html'), 'utf-8') + : '' + +export async function SsrMiddleWare(app: Koa, options?: { onDevViteClose?: Function }) { + let vite: ViteDevServer + if (!isProduction) { + // Dev mode: create Vite server in middleware mode. + const { createServer } = await import('vite') + vite = await createServer({ + server: { middlewareMode: true }, + configFile: getPathByRoot('packages', 'client/vite.config.ts'), + root: getPathByRoot('packages', 'client'), + appType: 'custom', + base, + }) + app.use(c2k(vite.middlewares)) + vite.httpServer?.on("close", () => { + vite.close() + options?.onDevViteClose?.() + }) + } else { + // Production mode: serve pre-built static assets. + app.use(async (ctx, next) => { + if (ctx.originalUrl === "/.well-known/appspecific/com.chrome.devtools.json") return await next() + try { + await Send(ctx, ctx.path, { root: getPathByRoot('dist/client'), index: false }); + if (ctx.status === 404) { + await next() + } + } catch (error) { + if (ctx.status === 404) { + await next() + } else { + throw error + } + } + }) + } + + // Handle every other route with SSR. + app.use(async (ctx, next) => { + if (!ctx.originalUrl.startsWith(base)) return await next() + + try { + const url = ctx.originalUrl.replace(base, '') + let template + let render + let manifest + if (!isProduction) { + // Always read fresh template in development + template = await fs.readFile(getPathByRoot('packages', 'client/index.html'), 'utf-8') + template = await vite.transformIndexHtml(url, template) + manifest = {} + render = (await vite.ssrLoadModule(getPathByRoot('packages', 'client/src/entry-server.ts'))).render + } else { + manifest = await fs.readFile(getPathByRoot('dist', 'client/.vite/ssr-manifest.json'), 'utf-8') + template = templateHtml + // @ts-ignore + render = (await import(getPathByRoot('dist', 'server/entry-server.js'))).render + } + + const cookies = parseCookieHeader(ctx.request.headers['cookie'] as string) + + const rendered = await render(url, manifest, { cookies }) + + const html = template + .replace(``, rendered.head ?? '') + .replace(``, rendered.html ?? '') + + ctx.status = 200 + ctx.set({ 'Content-Type': 'text/html' }) + ctx.body = html + + // 设置服务端渲染期间收集到的 Set-Cookie + const setCookies: string[] = (rendered as any).setCookies || [] + if (setCookies.length > 0) { + ctx.set('Set-Cookie', setCookies) + } + } catch (e: Error | any) { + vite?.ssrFixStacktrace(e) + ctx.status = 500 + console.error(e.stack) + ctx.body = e.stack + } + await next() + }) +} \ No newline at end of file diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 0000000..3d6af73 --- /dev/null +++ b/packages/server/package.json @@ -0,0 +1,20 @@ +{ + "name": "server", + "type": "module", + "exports": { + "./*": { + "import": "./src/*.ts", + "types": "./src/*.d.ts" + } + }, + "scripts": {}, + "devDependencies": { + "@types/koa": "^3.0.0", + "@types/koa-send": "^4.1.6" + }, + "dependencies": { + "koa": "^3.0.1", + "koa-connect": "^2.1.0", + "koa-send": "^5.0.1" + } +} diff --git a/server/main.ts b/packages/server/src/api/main.ts similarity index 67% rename from server/main.ts rename to packages/server/src/api/main.ts index a324dce..3221941 100644 --- a/server/main.ts +++ b/packages/server/src/api/main.ts @@ -1,8 +1,8 @@ -import { parseCookieHeader, serializeCookie } from "../src/compose/cookieUtils"; -import app from "./app"; +import { parseCookieHeader, serializeCookie } from "helper/cookie"; +import app from "../app"; export function bootstrapServer() { - async function fetchFirstSuccess(urls) { + async function fetchFirstSuccess(urls: string[]) { for (const url of urls) { try { const res = await fetch(url, { @@ -32,21 +32,22 @@ export function bootstrapServer() { } app.use(async (ctx, next) => { - const cookies = parseCookieHeader(ctx.request.headers.cookie as string); + // const cookies = parseCookieHeader(ctx.request.headers.cookie as string); // 读取 - const token = cookies["demo_2token"]; + // const token = cookies["demo_2token"]; - // 写入(HttpOnly 更安全) - if (!token) { - const setItem = serializeCookie("demo_2token", "from-mw", { - httpOnly: true, - path: "/", - sameSite: "lax", - }); - ctx.set("Set-Cookie", [setItem]); - } + // // 写入(HttpOnly 更安全) + // if (!token) { + // const setItem = serializeCookie("demo_2token", "from-mw", { + // httpOnly: true, + // path: "/", + // sameSite: "lax", + // }); + // ctx.set("Set-Cookie", [setItem]); + // } if (ctx.originalUrl !== "/api/pics/random") return await next(); + ctx.body = `Hello World` const { type, data } = await fetchFirstSuccess([ "https://api.miaomc.cn/image/get", ]); diff --git a/server/app.ts b/packages/server/src/app.ts similarity index 100% rename from server/app.ts rename to packages/server/src/app.ts diff --git a/packages/server/src/booststap.ts b/packages/server/src/booststap.ts new file mode 100644 index 0000000..c8432ce --- /dev/null +++ b/packages/server/src/booststap.ts @@ -0,0 +1,20 @@ +import app from "./app" +import { bootstrapServer } from "./api/main" +import { SsrMiddleWare } from "core/SsrMiddleWare" +import { Env } from "helper/env" + +bootstrapServer() + +SsrMiddleWare(app, { + onDevViteClose() { + console.log("Vite dev server closed") + if (server) { + server.close() + console.log('Server closed') + } + } +}) + +const server = app.listen(Env.port, () => { + console.log(`Server started at http://localhost:${Env.port}`) +}) diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json new file mode 100644 index 0000000..90b3e96 --- /dev/null +++ b/packages/server/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + // 指定编译成的版本 + "target": "esnext", + // 切换成即将发布的ECMA运行时行为 + "useDefineForClassFields": true, + // 指定ts需要包含的库 + "lib": [ + "esnext" + ], + // 指定编译后的模块系统,如commonjs,umd之类的 + "module": "esnext", + // 跳过库中的类型检查 + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/server.ts b/server.ts deleted file mode 100644 index b2de317..0000000 --- a/server.ts +++ /dev/null @@ -1,84 +0,0 @@ -import fs from 'node:fs/promises' -import c2k from 'koa-connect' -import type { ViteDevServer } from 'vite' -import Send from 'koa-send' -import app from "./server/app" -import { bootstrapServer } from "./server/main" - -// Constants -const isProduction = process.env.NODE_ENV === 'production' -const port = process.env.PORT || 5173 -const base = process.env.BASE || '/' - -bootstrapServer() - -// Cached production assets -const templateHtml = isProduction - ? await fs.readFile('./dist/client/index.html', 'utf-8') - : '' - -let vite: ViteDevServer -if (!isProduction) { - const { createServer } = await import('vite') - vite = await createServer({ - server: { middlewareMode: true }, - appType: 'custom', - base, - }) - app.use(c2k(vite.middlewares)) -} else { - app.use(async (ctx, next) => { - await Send(ctx, ctx.path, { root: './dist/client', index: false }); - if (ctx.status === 404) { - await next() - } - }) -} - -app.use(async (ctx, next) => { - // if (!ctx.originalUrl.startsWith(base)) return await next() - try { - const url = ctx.originalUrl.replace(base, '') - let template - let render - if (!isProduction) { - // Always read fresh template in development - template = await fs.readFile('./index.html', 'utf-8') - template = await vite.transformIndexHtml(url, template) - render = (await vite.ssrLoadModule('/src/entry-server.ts')).render - } else { - template = templateHtml - // @ts-ignore - render = (await import('./dist/server/entry-server.js')).render - } - - // 解析请求 Cookie 到对象(复用通用工具) - const { parseCookieHeader } = await import('./src/compose/cookieUtils') - const cookies = parseCookieHeader(ctx.request.headers['cookie'] as string) - - const rendered = await render(url, { cookies }) - - const html = template - .replace(``, rendered.head ?? '') - .replace(``, rendered.html ?? '') - ctx.status = 200 - ctx.set({ 'Content-Type': 'text/html' }) - ctx.body = html - - // 设置服务端渲染期间收集到的 Set-Cookie - const setCookies: string[] = (rendered as any).setCookies || [] - if (setCookies.length > 0) { - ctx.set('Set-Cookie', setCookies) - } - } catch (e: Error | any) { - vite?.ssrFixStacktrace(e) - ctx.status = 500 - ctx.body = e.stack - } - await next() -}) - -// Start http server -app.listen(port, () => { - console.log(`Server started at http://localhost:${port}`) -}) diff --git a/src/App.vue b/src/App.vue deleted file mode 100644 index 8433d2a..0000000 --- a/src/App.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - - - diff --git a/src/components/ClientOnly.tsx b/src/components/ClientOnly.tsx deleted file mode 100644 index fa64ae8..0000000 --- a/src/components/ClientOnly.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { cloneVNode, createElementBlock, defineComponent, getCurrentInstance, h, InjectionKey, onMounted, provide, shallowRef, SlotsType, VNode } from "vue"; - -export const clientOnlySymbol: InjectionKey = Symbol.for('nuxt:client-only') - -export default defineComponent({ - name: "ClientOnly", - inheritAttrs: false, - props: ['fallback', 'placeholder', 'placeholderTag', 'fallbackTag'], - ...(import.meta.env.DEV && { - slots: Object as SlotsType<{ - default?: () => VNode[] - - /** - * Specify a content to be rendered on the server and displayed until `` is mounted in the browser. - */ - fallback?: () => VNode[] - placeholder?: () => VNode[] - }>, - }), - setup(props, { slots, attrs }) { - const mounted = shallowRef(false) - onMounted(() => { mounted.value = true }) - const vm = getCurrentInstance() - if (vm) { - vm._nuxtClientOnly = true - } - provide(clientOnlySymbol, true) - return () => { - if (mounted.value) { - const vnodes = slots.default?.() - if (vnodes && vnodes.length === 1) { - return [cloneVNode(vnodes[0]!, attrs)] - } - return vnodes - } - const slot = slots.fallback || slots.placeholder - if (slot) { return h(slot) } - const fallbackStr = props.fallback || props.placeholder || '' - const fallbackTag = props.fallbackTag || props.placeholderTag || 'span' - return createElementBlock(fallbackTag, attrs, fallbackStr) - } - } -}) diff --git a/src/entry-client.ts b/src/entry-client.ts deleted file mode 100644 index 94ec873..0000000 --- a/src/entry-client.ts +++ /dev/null @@ -1,21 +0,0 @@ -import './style.css' -import { createApp } from "./main" -import { hydrateSSRContext, clearSSRContext } from './compose/ssrContext' - -// 水合 SSR 上下文(如果存在) -let ssrContext = null -if (typeof window !== 'undefined' && (window as any).__SSR_CONTEXT__) { - ssrContext = (window as any).__SSR_CONTEXT__ - console.log('[Client] 水合 SSR 上下文:', ssrContext) - hydrateSSRContext(ssrContext) -} else { - console.log('[Client] 未找到 SSR 上下文') -} - -// 使用相同的 SSR 上下文创建应用 -const { app } = createApp(ssrContext) - -app.mount('#app') - -// 水合完成后清除 SSR 上下文 -clearSSRContext() diff --git a/src/entry-server.ts b/src/entry-server.ts deleted file mode 100644 index 8eefd4d..0000000 --- a/src/entry-server.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { renderToString } from 'vue/server-renderer' -import { createApp } from './main' -import { createSSRContext } from './compose/ssrContext' - -export async function render(_url: string, init?: { cookies?: Record }) { - // 创建 SSR 上下文,包含数据缓存与 cookies - const ssrContext = createSSRContext() - if (init?.cookies) { - ssrContext.cookies = { ...init.cookies } - } - - // 将 SSR 上下文传递给应用创建函数 - const { app } = createApp(ssrContext) - - // passing SSR context object which will be available via useSSRContext() - // @vitejs/plugin-vue injects code into a component's setup() that registers - // itself on ctx.modules. After the render, ctx.modules would contain all the - // components that have been instantiated during this render call. - const ctx = { cache: ssrContext.cache } - const html = await renderToString(app, ctx) - - // 将 SSR 上下文数据序列化到 HTML 中 - // 使用更安全的方式序列化 Map - const cacheEntries = ssrContext.cache ? Array.from(ssrContext.cache.entries()) : [] - const ssrData = JSON.stringify(cacheEntries) - const cookieInit = JSON.stringify(ssrContext.cookies || {}) - console.log('[SSR] 序列化缓存数据:', cacheEntries) - const head = ` - - ` - - return { html, head, setCookies: ssrContext.setCookies || [] } -} diff --git a/src/style.css b/src/style.css deleted file mode 100644 index f691315..0000000 --- a/src/style.css +++ /dev/null @@ -1,79 +0,0 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -.card { - padding: 2em; -} - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index 348d0dd..0000000 --- a/vite.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' -import Components from 'unplugin-vue-components/vite' - -// https://vite.dev/config/ -export default defineConfig({ - base: './', - plugins: [ - vue(), - Components({ dts: true, - extensions: ['vue', 'tsx'], }) - ], -})