diff --git a/README.md b/README.md
index 5764c61..7918d2f 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,13 @@
+## ssr
+
+https://stackblitz.com/edit/bluwy-create-vite-extra-fuc1kcnj?file=index.html,server.js,src%2FApp.vue,src%2Fcomponents%2FHelloWorld.vue,src%2Fassets%2Fvue.svg
+
+bun run server.ts
+
+bunx tsc .\server.ts --module NodeNext --target esnext --moduleResolution nodenext
+
+
+
## koa3-demo
diff --git a/bun.lockb b/bun.lockb
index 8f97feb..daa08b1 100644
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/client/App.vue b/client/App.vue
new file mode 100644
index 0000000..5cc7b23
--- /dev/null
+++ b/client/App.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
diff --git a/client/assets/vue.svg b/client/assets/vue.svg
new file mode 100644
index 0000000..770e9d3
--- /dev/null
+++ b/client/assets/vue.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/components/HelloWorld.vue b/client/components/HelloWorld.vue
new file mode 100644
index 0000000..63f7e72
--- /dev/null
+++ b/client/components/HelloWorld.vue
@@ -0,0 +1,38 @@
+
+
+
+ {{ msg }}
+
+
+
+
+ Edit
+ components/HelloWorld.vue to test HMR
+
+
+
+
+ Check out
+ create-vue, the official Vue + Vite starter
+
+
+ Install
+ Volar
+ in your IDE for a better DX
+
+ Click on the Vite and Vue logos to learn more
+
+
+
diff --git a/client/entry-client.ts b/client/entry-client.ts
new file mode 100644
index 0000000..ae68fdd
--- /dev/null
+++ b/client/entry-client.ts
@@ -0,0 +1,6 @@
+import './style.css'
+import { createApp } from "./main"
+
+const { app } = createApp()
+
+app.mount('#app')
diff --git a/client/entry-server.ts b/client/entry-server.ts
new file mode 100644
index 0000000..bbafdd3
--- /dev/null
+++ b/client/entry-server.ts
@@ -0,0 +1,15 @@
+import { renderToString } from 'vue/server-renderer'
+import { createApp } from './main'
+
+export async function render(_url: string) {
+ const { app } = createApp()
+
+ // 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 = {}
+ const html = await renderToString(app, ctx)
+
+ return { html }
+}
diff --git a/client/main.ts b/client/main.ts
new file mode 100644
index 0000000..ff091f8
--- /dev/null
+++ b/client/main.ts
@@ -0,0 +1,10 @@
+import { createSSRApp } from 'vue'
+import App from './App.vue'
+
+// 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() {
+ const app = createSSRApp(App)
+ return { app }
+}
diff --git a/client/style.css b/client/style.css
new file mode 100644
index 0000000..f691315
--- /dev/null
+++ b/client/style.css
@@ -0,0 +1,79 @@
+: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/client/vite-env.d.ts b/client/vite-env.d.ts
new file mode 100644
index 0000000..323c78a
--- /dev/null
+++ b/client/vite-env.d.ts
@@ -0,0 +1,7 @@
+///
+
+declare module '*.vue' {
+ import type { DefineComponent } from 'vue'
+ const component: DefineComponent<{}, {}, any>
+ export default component
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..70c1f19
--- /dev/null
+++ b/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Vite + Vue + TS
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 199afea..7545092 100644
--- a/package.json
+++ b/package.json
@@ -4,23 +4,19 @@
"version": "0.0.1-alpha",
"type": "module",
"scripts": {
- "dev": "bun --hot src/main.js",
- "start": "cross-env NODE_ENV=production bun run src/main.js",
- "build": "vite build",
- "migrate:make": "npx knex migrate:make ",
- "migrate": "npx knex migrate:latest",
- "seed:make": "npx knex seed:make ",
- "seed": "npx knex seed:run ",
- "dev:init": "bun run scripts/init.js",
- "init": "cross-env NODE_ENV=production bun run scripts/init.js",
- "test": "bun test",
- "test:db:run": "bun run scripts/run-db-tests.js",
- "test:db:benchmark": "bun run scripts/db-benchmark.js"
+ "dev": "bun run server.ts",
+ "build": "npm run build:client && npm run build:server",
+ "build:client": "vite build --outDir dist/client",
+ "build:server": "vite build --ssr client/entry-server.ts --outDir dist/server",
+ "preview": "cross-env NODE_ENV=production node server",
+ "check": "vue-tsc"
},
"devDependencies": {
"@types/bun": "latest",
+ "@types/koa": "^3.0.0",
"@types/node": "^24.0.1",
"cross-env": "^10.0.0",
+ "koa-send": "^5.0.1",
"module-alias": "^2.2.3",
"node-gyp": "^11.4.2",
"vite": "^7.0.0",
@@ -28,6 +24,7 @@
},
"dependencies": {
"@koa/etag": "^5.0.1",
+ "@vitejs/plugin-vue": "^6.0.1",
"bcryptjs": "^3.0.2",
"consolidate": "^1.0.4",
"extend-shallow": "^3.0.2",
@@ -39,6 +36,7 @@
"knex": "^3.1.0",
"koa": "^3.0.0",
"koa-bodyparser": "^4.4.1",
+ "koa-connect": "^2.1.0",
"koa-helmet": "^8.0.1",
"koa-ratelimit": "^6.0.0",
"koa-session": "^7.0.2",
@@ -62,6 +60,6 @@
"services": "./src/services"
},
"peerDependencies": {
- "typescript": "^5.0.0"
+ "typescript": "^5.9.2"
}
}
\ No newline at end of file
diff --git a/public/vite.svg b/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..76512b4
--- /dev/null
+++ b/server.js
@@ -0,0 +1,66 @@
+import fs from 'node:fs/promises';
+import Koa from "koa";
+import c2k from 'koa-connect';
+import Send from 'koa-send';
+// Constants
+const isProduction = process.env.NODE_ENV === 'production';
+const port = process.env.PORT || 5173;
+const base = process.env.BASE || '/';
+// Cached production assets
+const templateHtml = isProduction
+ ? await fs.readFile('./dist/client/index.html', 'utf-8')
+ : '';
+const app = new Koa();
+// Add Vite or respective production middlewares
+/** @type {import('vite').ViteDevServer | undefined} */
+let vite;
+if (!isProduction) {
+ const { createServer } = await import('vite');
+ vite = await createServer({
+ server: { middlewareMode: true },
+ appType: 'custom',
+ base,
+ });
+ app.use(c2k(vite.middlewares));
+}
+else {
+ app.use(Send({ root: 'dist/client', index: false }));
+}
+app.use(async (ctx, next) => {
+ try {
+ const url = ctx.path.replace(base, '');
+ /** @type {string} */
+ let template;
+ /** @type {import('./client/entry-server.ts').render} */
+ 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('/client/entry-server.ts')).render;
+ }
+ else {
+ template = templateHtml;
+ // @ts-ignore
+ render = (await import('./dist/server/entry-server.js')).render;
+ }
+ const rendered = await render(url);
+ const html = template
+ .replace(``, rendered.head ?? '')
+ .replace(``, rendered.html ?? '');
+ ctx.status = 200;
+ ctx.set({ 'Content-Type': 'text/html' });
+ ctx.body = html;
+ }
+ catch (e) {
+ vite?.ssrFixStacktrace(e);
+ console.log(e.stack);
+ 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/server.ts b/server.ts
new file mode 100644
index 0000000..21162b0
--- /dev/null
+++ b/server.ts
@@ -0,0 +1,71 @@
+import fs from 'node:fs/promises'
+import Koa from "koa"
+import c2k from 'koa-connect'
+import { ViteDevServer } from 'vite'
+import Send from 'koa-send'
+
+// Constants
+const isProduction = process.env.NODE_ENV === 'production'
+const port = process.env.PORT || 5173
+const base = process.env.BASE || '/'
+
+// Cached production assets
+const templateHtml = isProduction
+ ? await fs.readFile('./dist/client/index.html', 'utf-8')
+ : ''
+
+const app = new Koa()
+
+// Add Vite or respective production middlewares
+/** @type {import('vite').ViteDevServer | undefined} */
+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(Send({ root: 'dist/client', index: false }))
+}
+app.use(async (ctx, next) => {
+ try {
+ const url = ctx.path.replace(base, '')
+ /** @type {string} */
+ let template
+ /** @type {import('./client/entry-server.ts').render} */
+ 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('/client/entry-server.ts')).render
+ } else {
+ template = templateHtml
+ // @ts-ignore
+ render = (await import('./dist/server/entry-server.js')).render
+ }
+
+ const rendered = await render(url)
+
+ const html = template
+ .replace(``, rendered.head ?? '')
+ .replace(``, rendered.html ?? '')
+ ctx.status = 200
+ ctx.set({ 'Content-Type': 'text/html' })
+ ctx.body = html
+ } catch (e: Error | any) {
+ vite?.ssrFixStacktrace(e)
+ console.log(e.stack)
+ 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/vite.config copy.ts b/vite.config copy.ts
new file mode 100644
index 0000000..ef21700
--- /dev/null
+++ b/vite.config copy.ts
@@ -0,0 +1,82 @@
+import { dirname, resolve } from "node:path"
+import { fileURLToPath } from "node:url"
+import module from "node:module"
+import { defineConfig } from "vite"
+import pkg from "./package.json"
+import { viteStaticCopy } from "vite-plugin-static-copy"
+
+const __dirname = dirname(fileURLToPath(import.meta.url))
+
+function getExternal(): string[] {
+ return [...Object.keys(pkg.dependencies || {}), ...module.builtinModules]
+}
+
+export default defineConfig({
+ publicDir: false,
+ resolve: {
+ alias: {
+ "@": resolve(__dirname, "src"),
+ db: resolve(__dirname, "src/db"),
+ config: resolve(__dirname, "src/config"),
+ utils: resolve(__dirname, "src/utils"),
+ },
+ },
+ build: {
+ lib: {
+ entry: resolve(__dirname, "src/main.js"),
+ formats: ["es"],
+ fileName: () => `[name].js`,
+ },
+ outDir: resolve(__dirname, "dist"),
+ rollupOptions: {
+ external: getExternal(),
+ // watch: {
+ // include: "src/**",
+ // exclude: "node_modules/**",
+ // },
+ output: {
+ preserveModules: true,
+ preserveModulesRoot: "src",
+ inlineDynamicImports: false,
+ },
+ },
+ },
+ plugins: [
+ viteStaticCopy({
+ targets: [
+ {
+ src: "public",
+ dest: "",
+ },
+ {
+ src: "src/views",
+ dest: "",
+ },
+ {
+ src: "src/db/migrations",
+ dest: "db",
+ },
+ {
+ src: "src/db/seeds",
+ dest: "db",
+ },
+ {
+ src: "entrypoint.sh",
+ dest: "",
+ },
+ {
+ src: "package.json",
+ dest: "",
+ },
+ {
+ src: "knexfile.mjs",
+ dest: "",
+ },
+ {
+ src: "bun.lockb",
+ dest: "",
+ },
+ ],
+ }),
+ ],
+})
diff --git a/vite.config.ts b/vite.config.ts
index ef21700..bbcf80c 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,82 +1,7 @@
-import { dirname, resolve } from "node:path"
-import { fileURLToPath } from "node:url"
-import module from "node:module"
-import { defineConfig } from "vite"
-import pkg from "./package.json"
-import { viteStaticCopy } from "vite-plugin-static-copy"
-
-const __dirname = dirname(fileURLToPath(import.meta.url))
-
-function getExternal(): string[] {
- return [...Object.keys(pkg.dependencies || {}), ...module.builtinModules]
-}
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+// https://vite.dev/config/
export default defineConfig({
- publicDir: false,
- resolve: {
- alias: {
- "@": resolve(__dirname, "src"),
- db: resolve(__dirname, "src/db"),
- config: resolve(__dirname, "src/config"),
- utils: resolve(__dirname, "src/utils"),
- },
- },
- build: {
- lib: {
- entry: resolve(__dirname, "src/main.js"),
- formats: ["es"],
- fileName: () => `[name].js`,
- },
- outDir: resolve(__dirname, "dist"),
- rollupOptions: {
- external: getExternal(),
- // watch: {
- // include: "src/**",
- // exclude: "node_modules/**",
- // },
- output: {
- preserveModules: true,
- preserveModulesRoot: "src",
- inlineDynamicImports: false,
- },
- },
- },
- plugins: [
- viteStaticCopy({
- targets: [
- {
- src: "public",
- dest: "",
- },
- {
- src: "src/views",
- dest: "",
- },
- {
- src: "src/db/migrations",
- dest: "db",
- },
- {
- src: "src/db/seeds",
- dest: "db",
- },
- {
- src: "entrypoint.sh",
- dest: "",
- },
- {
- src: "package.json",
- dest: "",
- },
- {
- src: "knexfile.mjs",
- dest: "",
- },
- {
- src: "bun.lockb",
- dest: "",
- },
- ],
- }),
- ],
+ plugins: [vue()],
})