@netrojs/create-vono 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ import { reactive, ref } from 'vue'
3
+ import { usePageData } from '@netrojs/vono/client'
4
+
5
+ interface SettingsData {
6
+ settings: {
7
+ siteName: string
8
+ siteUrl: string
9
+ analyticsId: string
10
+ emailNotifs: boolean
11
+ maintenanceMode: boolean
12
+ }
13
+ }
14
+
15
+ const data = usePageData<SettingsData>()
16
+
17
+ // Editable local copy — reactive so template updates instantly
18
+ const form = reactive({ ...data.settings })
19
+ const saved = ref(false)
20
+ const saving = ref(false)
21
+
22
+ async function save() {
23
+ saving.value = true
24
+ // Simulate an API call
25
+ await new Promise(r => setTimeout(r, 600))
26
+ saving.value = false
27
+ saved.value = true
28
+ setTimeout(() => { saved.value = false }, 2500)
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <div class="dash-section settings-form">
34
+ <div class="form-group">
35
+ <label class="form-label" for="siteName">Site name</label>
36
+ <input id="siteName" v-model="form.siteName" class="form-input" type="text">
37
+ </div>
38
+
39
+ <div class="form-group">
40
+ <label class="form-label" for="siteUrl">Site URL</label>
41
+ <input id="siteUrl" v-model="form.siteUrl" class="form-input" type="url">
42
+ </div>
43
+
44
+ <div class="form-group">
45
+ <label class="form-label" for="analyticsId">Analytics ID</label>
46
+ <input id="analyticsId" v-model="form.analyticsId" class="form-input" type="text" placeholder="G-XXXXXXXXXX">
47
+ </div>
48
+
49
+ <div class="form-group form-toggle">
50
+ <label class="form-label">
51
+ <input v-model="form.emailNotifs" type="checkbox">
52
+ Email notifications
53
+ </label>
54
+ </div>
55
+
56
+ <div class="form-group form-toggle">
57
+ <label class="form-label">
58
+ <input v-model="form.maintenanceMode" type="checkbox">
59
+ Maintenance mode
60
+ <span v-if="form.maintenanceMode" class="tag tag-warn">⚠️ Site is offline to visitors</span>
61
+ </label>
62
+ </div>
63
+
64
+ <div class="form-footer">
65
+ <button class="btn btn-primary" :disabled="saving" @click="save">
66
+ {{ saving ? 'Saving…' : 'Save settings' }}
67
+ </button>
68
+ <Transition name="fade">
69
+ <span v-if="saved" class="save-confirm">✅ Saved!</span>
70
+ </Transition>
71
+ </div>
72
+ </div>
73
+ </template>
@@ -0,0 +1,94 @@
1
+ <script setup lang="ts">
2
+ import { ref, onMounted } from 'vue'
3
+ import { RouterLink } from 'vue-router'
4
+ import { usePageData } from '@netrojs/vono/client'
5
+ import type { HomeData } from '../routes'
6
+
7
+ // Full type inference — HomeData is derived from the loader via InferPageData<T>
8
+ const data = usePageData<HomeData>()
9
+
10
+ // Client-only state — demonstrates ref + onMounted working after SSR hydration
11
+ const activeFeature = ref(0)
12
+ const mounted = ref(false)
13
+
14
+ onMounted(() => {
15
+ mounted.value = true
16
+ // Rotate the highlighted feature card every 3s
17
+ setInterval(() => {
18
+ activeFeature.value = (activeFeature.value + 1) % data.features.length
19
+ }, 3000)
20
+ })
21
+ </script>
22
+
23
+ <template>
24
+ <!-- ── Hero ──────────────────────────────────────────────────────────── -->
25
+ <section class="hero">
26
+ <div class="hero-badge">Full-stack · Vue 3 + Hono</div>
27
+ <h1 class="hero-headline">{{ data.headline }}</h1>
28
+ <p class="hero-sub">{{ data.subline }}</p>
29
+ <div class="hero-actions">
30
+ <RouterLink to="/blog" class="btn btn-primary">Read the Blog →</RouterLink>
31
+ <RouterLink to="/dashboard" class="btn btn-ghost">Dashboard Demo</RouterLink>
32
+ </div>
33
+
34
+ <!-- Stat chips — data from the loader, typed -->
35
+ <div class="stat-row">
36
+ <div class="stat-chip">⚡ {{ data.stats.ssr }}</div>
37
+ <div class="stat-chip">📦 {{ data.stats.bundle }}</div>
38
+ <div class="stat-chip">🔒 {{ data.stats.dx }}</div>
39
+ </div>
40
+ </section>
41
+
42
+ <!-- ── Feature grid ───────────────────────────────────────────────────── -->
43
+ <section class="section">
44
+ <h2 class="section-title">Everything you need</h2>
45
+ <div class="feature-grid">
46
+ <div
47
+ v-for="(f, i) in data.features"
48
+ :key="f.title"
49
+ class="feature-card"
50
+ :class="{ highlight: mounted && i === activeFeature }"
51
+ >
52
+ <span class="feature-icon">{{ f.icon }}</span>
53
+ <h3 class="feature-title">{{ f.title }}</h3>
54
+ <p class="feature-desc">{{ f.desc }}</p>
55
+ </div>
56
+ </div>
57
+ </section>
58
+
59
+ <!-- ── Recent posts ───────────────────────────────────────────────────── -->
60
+ <section class="section">
61
+ <h2 class="section-title">Recent posts</h2>
62
+ <div class="post-list">
63
+ <RouterLink
64
+ v-for="post in data.recentPosts"
65
+ :key="post.slug"
66
+ :to="`/blog/${post.slug}`"
67
+ class="post-card"
68
+ >
69
+ <div class="post-meta">
70
+ <span>{{ post.date }}</span>
71
+ <span class="tag" v-for="tag in post.tags.slice(0, 2)" :key="tag">#{{ tag }}</span>
72
+ </div>
73
+ <h3 class="post-card-title">{{ post.title }}</h3>
74
+ <p class="post-card-excerpt">{{ post.excerpt }}</p>
75
+ <span class="post-views">{{ post.views.toLocaleString() }} views</span>
76
+ </RouterLink>
77
+ </div>
78
+ <RouterLink to="/blog" class="btn btn-ghost" style="margin-top:1.5rem">All posts →</RouterLink>
79
+ </section>
80
+
81
+ <!-- ── Code example ───────────────────────────────────────────────────── -->
82
+ <section class="section">
83
+ <h2 class="section-title">Type-safe in 3 lines</h2>
84
+ <pre class="code-block"><code>// routes.ts
85
+ export const postPage = definePage({
86
+ loader: async (c) =&gt; fetchPost(c.req.param('slug')),
87
+ component: () =&gt; import('./pages/post.vue'),
88
+ })
89
+ export type PostData = InferPageData&lt;typeof postPage&gt;
90
+
91
+ // pages/post.vue
92
+ const data = usePageData&lt;PostData&gt;() // ✅ fully typed, zero duplication</code></pre>
93
+ </section>
94
+ </template>
@@ -0,0 +1,71 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+
5
+ const router = useRouter()
6
+ const email = ref('')
7
+ const password = ref('')
8
+ const error = ref<string | null>(null)
9
+ const loading = ref(false)
10
+
11
+ async function login() {
12
+ if (!email.value || !password.value) {
13
+ error.value = 'Please enter your email and password.'
14
+ return
15
+ }
16
+ loading.value = true
17
+ error.value = null
18
+
19
+ // Stub: set a cookie and redirect. Replace with a real auth call.
20
+ await new Promise(r => setTimeout(r, 500))
21
+ document.cookie = 'session=demo; path=/'
22
+ await router.push('/dashboard')
23
+ }
24
+ </script>
25
+
26
+ <template>
27
+ <div class="login-shell">
28
+ <div class="login-card">
29
+ <div class="login-logo">◈ Vono</div>
30
+ <h1 class="login-title">Sign in</h1>
31
+ <p class="login-sub">Dashboard is protected by server middleware.</p>
32
+
33
+ <div v-if="error" class="alert-error">{{ error }}</div>
34
+
35
+ <div class="form-group">
36
+ <label class="form-label" for="email">Email</label>
37
+ <input
38
+ id="email"
39
+ v-model="email"
40
+ class="form-input"
41
+ type="email"
42
+ placeholder="you@example.com"
43
+ autocomplete="email"
44
+ @keyup.enter="login"
45
+ >
46
+ </div>
47
+
48
+ <div class="form-group">
49
+ <label class="form-label" for="password">Password</label>
50
+ <input
51
+ id="password"
52
+ v-model="password"
53
+ class="form-input"
54
+ type="password"
55
+ placeholder="••••••••"
56
+ autocomplete="current-password"
57
+ @keyup.enter="login"
58
+ >
59
+ </div>
60
+
61
+ <button class="btn btn-primary" style="width:100%;margin-top:.5rem" :disabled="loading" @click="login">
62
+ {{ loading ? 'Signing in…' : 'Sign in' }}
63
+ </button>
64
+
65
+ <p class="login-hint">
66
+ Demo: any email + password sets a <code>session=demo</code> cookie
67
+ and grants dashboard access (the auth guard stub accepts it).
68
+ </p>
69
+ </div>
70
+ </div>
71
+ </template>
@@ -0,0 +1,272 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // app/routes.ts · Vono demo — full-featured route definitions
3
+ //
4
+ // Demonstrates:
5
+ // • definePage() with typed loaders — types flow into usePageData<T>()
6
+ // • InferPageData<T> for a single-source-of-truth type pattern
7
+ // • defineGroup() with shared layout + prefix + middleware
8
+ // • defineLayout() for per-section layouts
9
+ // • defineApiRoute() for Hono JSON APIs co-located with pages
10
+ // • Async loaders → automatic code splitting per route chunk
11
+ // • Dynamic params → [slug], [id] in paths
12
+ // • Per-page SEO → static + dynamic (function) forms
13
+ // • Server-side middleware → auth guard on protected routes
14
+ // ─────────────────────────────────────────────────────────────────────────────
15
+
16
+ import {
17
+ definePage,
18
+ defineGroup,
19
+ defineLayout,
20
+ defineApiRoute,
21
+ type InferPageData,
22
+ } from '@netrojs/vono'
23
+ import type { LoaderCtx } from '@netrojs/vono'
24
+ import RootLayout from './layouts/RootLayout.vue'
25
+ import DashboardLayout from './layouts/DashboardLayout.vue'
26
+
27
+ // ── Shared data types (co-located with routes for InferPageData) ──────────────
28
+
29
+ export interface Post {
30
+ id: number
31
+ slug: string
32
+ title: string
33
+ excerpt: string
34
+ body: string
35
+ author: string
36
+ date: string
37
+ tags: string[]
38
+ views: number
39
+ }
40
+
41
+ export interface DashboardStats {
42
+ totalUsers: number
43
+ totalPosts: number
44
+ totalViews: number
45
+ recentSignups: number
46
+ trend: Array<{ day: string; users: number; views: number }>
47
+ }
48
+
49
+ // ── In-memory mock data (replace with a real DB in production) ───────────────
50
+
51
+ const POSTS: Post[] = [
52
+ {
53
+ id: 1, slug: 'getting-started', title: 'Getting Started with Vono',
54
+ excerpt: 'Learn how to build full-stack Vue apps with streaming SSR and Hono.',
55
+ body: 'Vono combines Hono\'s blazing-fast server with Vue 3\'s reactive UI layer...',
56
+ author: 'Alice', date: '2025-03-01', tags: ['vue', 'ssr', 'hono'], views: 1024,
57
+ },
58
+ {
59
+ id: 2, slug: 'type-safe-loaders', title: 'Type-Safe Loaders & usePageData',
60
+ excerpt: 'One type definition — inferred from your loader, available everywhere.',
61
+ body: 'The InferPageData<T> helper extracts the return type from definePage()...',
62
+ author: 'Bob', date: '2025-03-10', tags: ['typescript', 'dx'], views: 832,
63
+ },
64
+ {
65
+ id: 3, slug: 'streaming-ssr', title: 'Streaming SSR Deep Dive',
66
+ excerpt: 'Why renderToWebStream gives you dramatically lower TTFB than buffered SSR.',
67
+ body: 'With renderToWebStream the browser receives <head> immediately...',
68
+ author: 'Carol', date: '2025-03-18', tags: ['performance', 'ssr'], views: 2048,
69
+ },
70
+ ]
71
+
72
+ // ── Auth helper (stub) ────────────────────────────────────────────────────────
73
+
74
+ function isAuthenticated(c: LoaderCtx): boolean {
75
+ // Real app: check a JWT cookie / session token
76
+ return c.req.header('cookie')?.includes('session=demo') ?? false
77
+ }
78
+
79
+ // ── Layouts ───────────────────────────────────────────────────────────────────
80
+
81
+ export const rootLayout = defineLayout(RootLayout)
82
+ export const dashboardLayout = defineLayout(DashboardLayout)
83
+
84
+ // ── API routes (Hono handlers) ────────────────────────────────────────────────
85
+
86
+ const postsApi = defineApiRoute('/api/posts', (app) => {
87
+ app.get('/', (c) => c.json({ posts: POSTS.map(({ body: _, ...p }) => p) }))
88
+ app.get('/:slug', (c) => {
89
+ const post = POSTS.find(p => p.slug === c.req.param('slug'))
90
+ return post ? c.json(post) : c.json({ error: 'Not found' }, 404)
91
+ })
92
+ })
93
+
94
+ const statsApi = defineApiRoute('/api/stats', (app) => {
95
+ app.get('/', (_c) => c.json({
96
+ totalUsers: 4200,
97
+ totalPosts: POSTS.length,
98
+ totalViews: POSTS.reduce((s, p) => s + p.views, 0),
99
+ }))
100
+ })
101
+ // (Fix the accidental `c` → use the handler param)
102
+ const statsApiFixed = defineApiRoute('/api/stats', (app) => {
103
+ app.get('/', (c) => c.json({
104
+ totalUsers: 4200,
105
+ totalPosts: POSTS.length,
106
+ totalViews: POSTS.reduce((s, p) => s + p.views, 0),
107
+ }))
108
+ })
109
+
110
+ // ── Public pages ──────────────────────────────────────────────────────────────
111
+
112
+ export const homePage = definePage({
113
+ path: '/',
114
+ layout: rootLayout,
115
+ seo: {
116
+ title: 'Vono Demo — Vue 3 + Hono Full-Stack Framework',
117
+ description: 'A complex demo showcasing streaming SSR, SPA navigation, type-safe loaders, code splitting, and Hono middleware — all in one TypeScript-first framework.',
118
+ ogType: 'website',
119
+ twitterCard: 'summary_large_image',
120
+ },
121
+ loader: async () => ({
122
+ headline: 'Build faster with Vono',
123
+ subline: 'Streaming SSR · SPA · Type-safe loaders · Hono middleware · Multi-runtime',
124
+ stats: {
125
+ ssr: '< 10ms TTFB on cold start',
126
+ bundle: '< 50 kB client JS (gzipped)',
127
+ dx: 'One loader type — used everywhere',
128
+ },
129
+ recentPosts: POSTS.map(({ body: _, ...p }) => p),
130
+ features: [
131
+ { icon: '⚡', title: 'Streaming SSR', desc: 'renderToWebStream flushes <head> instantly — CSS & scripts load while the body streams.' },
132
+ { icon: '🔀', title: 'SPA Navigation', desc: 'Vue Router on the client. Navigating between pages fetches JSON — no full reload.' },
133
+ { icon: '🔒', title: 'Type-safe Loaders', desc: 'InferPageData<T> derives component types from your loader. Zero duplication.' },
134
+ { icon: '✂️', title: 'Code Splitting', desc: 'Pass () => import() as component. Vono resolves it for SSR, splits it for the client.' },
135
+ { icon: '🔍', title: 'Full SEO', desc: 'Title, description, OG, Twitter Cards, JSON-LD — synced on every navigation.' },
136
+ { icon: '🛡️', title: 'Server Middleware', desc: 'Hono middleware per app, per group, per route — with a client-side counterpart.' },
137
+ { icon: '🗂️', title: 'Route Groups', desc: 'defineGroup() lets you share a prefix, layout, and middleware across multiple routes.' },
138
+ { icon: '🚀', title: 'Multi-runtime', desc: 'Node.js, Bun, Deno, Cloudflare Workers — same codebase, zero adapter code.' },
139
+ ],
140
+ }),
141
+ component: () => import('./pages/home.vue'),
142
+ })
143
+ export type HomeData = InferPageData<typeof homePage>
144
+
145
+ // ── Blog pages ────────────────────────────────────────────────────────────────
146
+
147
+ export const blogListPage = definePage({
148
+ path: '/blog',
149
+ layout: rootLayout,
150
+ seo: {
151
+ title: 'Blog — Vono Demo',
152
+ description: 'Articles about Vue 3, Hono, SSR, TypeScript, and full-stack development.',
153
+ },
154
+ loader: async () => ({
155
+ posts: POSTS.map(({ body: _, ...p }) => p),
156
+ }),
157
+ component: () => import('./pages/blog/index.vue'),
158
+ })
159
+ export type BlogListData = InferPageData<typeof blogListPage>
160
+
161
+ export const blogPostPage = definePage({
162
+ path: '/blog/[slug]',
163
+ layout: rootLayout,
164
+ // Dynamic SEO: receives the loader output + params at render time
165
+ seo: (data, _params) => ({
166
+ title: `${data.post?.title ?? 'Post'} — Vono Blog`,
167
+ description: data.post?.excerpt,
168
+ ogType: 'article',
169
+ ogImage: `/og/blog/${data.post?.slug}.png`,
170
+ }),
171
+ loader: async (c) => {
172
+ const slug = c.req.param('slug')
173
+ const post = POSTS.find(p => p.slug === slug) ?? null
174
+ return { post }
175
+ },
176
+ component: () => import('./pages/blog/[slug].vue'),
177
+ })
178
+ export type BlogPostData = InferPageData<typeof blogPostPage>
179
+
180
+ // ── Dashboard (protected) ─────────────────────────────────────────────────────
181
+ //
182
+ // defineGroup() applies:
183
+ // - prefix → all routes under /dashboard
184
+ // - layout → DashboardLayout (sidebar + header)
185
+ // - middleware → auth guard; returns 401 JSON if not authenticated
186
+
187
+ const authGuard = async (c: LoaderCtx, next: () => Promise<void>): Promise<void | Response> => {
188
+ if (!isAuthenticated(c)) {
189
+ // For SPA navigations return JSON; for full-page loads redirect
190
+ const isSPA = c.req.header('x-vono-spa') === '1'
191
+ if (isSPA) return c.json({ error: 'Unauthorized' }, 401) as unknown as Response
192
+ return c.redirect('/login') as unknown as Response
193
+ }
194
+ await next()
195
+ }
196
+
197
+ export const dashboardGroup = defineGroup({
198
+ prefix: '/dashboard',
199
+ layout: dashboardLayout,
200
+ middleware: [authGuard],
201
+ routes: [
202
+ definePage({
203
+ path: '', // resolves to /dashboard
204
+ seo: { title: 'Dashboard — Vono Demo' },
205
+ loader: async (): Promise<DashboardStats> => ({
206
+ totalUsers: 4200,
207
+ totalPosts: POSTS.length,
208
+ totalViews: POSTS.reduce((s, p) => s + p.views, 0),
209
+ recentSignups: 38,
210
+ trend: [
211
+ { day: 'Mon', users: 120, views: 880 },
212
+ { day: 'Tue', users: 145, views: 1020 },
213
+ { day: 'Wed', users: 98, views: 760 },
214
+ { day: 'Thu', users: 210, views: 1450 },
215
+ { day: 'Fri', users: 175, views: 1230 },
216
+ { day: 'Sat', users: 90, views: 620 },
217
+ { day: 'Sun', users: 60, views: 490 },
218
+ ],
219
+ }),
220
+ component: () => import('./pages/dashboard/index.vue'),
221
+ }),
222
+
223
+ definePage({
224
+ path: '/posts',
225
+ seo: { title: 'Manage Posts — Vono Demo' },
226
+ loader: async () => ({ posts: POSTS }),
227
+ component: () => import('./pages/dashboard/posts.vue'),
228
+ }),
229
+
230
+ definePage({
231
+ path: '/settings',
232
+ seo: { title: 'Settings — Vono Demo' },
233
+ loader: async () => ({
234
+ settings: {
235
+ siteName: 'Vono Demo',
236
+ siteUrl: 'https://demo.vono.dev',
237
+ analyticsId: 'G-XXXXXXXXXX',
238
+ emailNotifs: true,
239
+ maintenanceMode: false,
240
+ },
241
+ }),
242
+ component: () => import('./pages/dashboard/settings.vue'),
243
+ }),
244
+ ],
245
+ })
246
+
247
+ // ── Auth pages (no layout) ────────────────────────────────────────────────────
248
+
249
+ export const loginPage = definePage({
250
+ path: '/login',
251
+ layout: false, // no layout — full-page auth screen
252
+ seo: { title: 'Sign in — Vono Demo' },
253
+ loader: async () => ({ error: null as string | null }),
254
+ component: () => import('./pages/login.vue'),
255
+ })
256
+ export type LoginData = InferPageData<typeof loginPage>
257
+
258
+ // ── 404 page (handled by createVono's notFound option) ─────────────────────
259
+ // Exported so app.ts can reference it directly.
260
+ export { default as NotFoundPage } from './pages/404.vue'
261
+
262
+ // ── Master route list ─────────────────────────────────────────────────────────
263
+
264
+ export const routes = [
265
+ postsApi,
266
+ statsApiFixed,
267
+ homePage,
268
+ blogListPage,
269
+ blogPostPage,
270
+ dashboardGroup,
271
+ loginPage,
272
+ ]