@netrojs/fnetro 0.2.0 → 0.2.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.
Files changed (2) hide show
  1. package/README.md +536 -324
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,35 +1,50 @@
1
- # @netrojs/fnetro
1
+ # FNetro
2
2
 
3
- > Full-stack [Hono](https://hono.dev) framework powered by **SolidJS v1.9+** — SSR, SPA, SEO, server & client middleware, TypeScript-first.
3
+ > Full-stack [Hono](https://hono.dev) framework powered by **SolidJS v1.9+** —
4
+ > SSR · SPA · SEO · server & client middleware · multi-runtime · TypeScript-first.
4
5
 
5
- [![npm version](https://img.shields.io/npm/v/@netrojs/fnetro)](https://www.npmjs.com/package/@netrojs/fnetro)
6
+ [![CI](https://github.com/netrosolutions/fnetro/actions/workflows/ci.yml/badge.svg)](https://github.com/netrosolutions/fnetro/actions/workflows/ci.yml)
7
+ [![npm @netrojs/fnetro](https://img.shields.io/npm/v/@netrojs/fnetro?label=%40netrojs%2Ffnetro)](https://www.npmjs.com/package/@netrojs/fnetro)
8
+ [![npm create-fnetro](https://img.shields.io/npm/v/@netrojs/create-fnetro?label=%40netrojs%2Fcreate-fnetro)](https://www.npmjs.com/package/@netrojs/create-fnetro)
6
9
  [![license](https://img.shields.io/npm/l/@netrojs/fnetro)](./LICENSE)
7
10
 
8
11
  ---
9
12
 
10
13
  ## Table of contents
11
14
 
12
- 1. [Quick start](#quick-start)
13
- 2. [Installation](#installation)
14
- 3. [Project structure](#project-structure)
15
- 4. [Core concepts](#core-concepts)
16
- 5. [Routing](#routing)
15
+ 1. [Packages](#packages)
16
+ 2. [Quick start](#quick-start)
17
+ 3. [How it works](#how-it-works)
18
+ 4. [Routing](#routing)
17
19
  - [definePage](#definepage)
18
20
  - [defineGroup](#definegroup)
19
21
  - [defineLayout](#definelayout)
20
22
  - [defineApiRoute](#defineapiroute)
21
- 6. [Loaders](#loaders)
22
- 7. [SEO](#seo)
23
- 8. [Middleware](#middleware)
23
+ 5. [Loaders](#loaders)
24
+ 6. [SEO](#seo)
25
+ 7. [Middleware](#middleware)
24
26
  - [Server middleware](#server-middleware)
25
27
  - [Client middleware](#client-middleware)
26
- 9. [SolidJS reactivity](#solidjs-reactivity)
27
- 10. [Navigation](#navigation)
28
- 11. [Asset handling](#asset-handling)
29
- 12. [Multi-runtime `serve()`](#multi-runtime-serve)
30
- 13. [Vite plugin](#vite-plugin)
28
+ 8. [SolidJS reactivity](#solidjs-reactivity)
29
+ 9. [Navigation](#navigation)
30
+ 10. [Asset handling](#asset-handling)
31
+ 11. [Multi-runtime serve()](#multi-runtime-serve)
32
+ 12. [Vite plugin](#vite-plugin)
33
+ 13. [Project structure](#project-structure)
31
34
  14. [TypeScript](#typescript)
32
- 15. [API reference](#api-reference)
35
+ 15. [create-fnetro CLI](#create-fnetro-cli)
36
+ 16. [API reference](#api-reference)
37
+ 17. [Monorepo development](#monorepo-development)
38
+ 18. [Publishing & releases](#publishing--releases)
39
+
40
+ ---
41
+
42
+ ## Packages
43
+
44
+ | Package | Description |
45
+ |---|---|
46
+ | [`@netrojs/fnetro`](./packages/fnetro) | Core framework — SSR renderer, SPA routing, SEO, middleware, Vite plugin |
47
+ | [`@netrojs/create-fnetro`](./packages/create-fnetro) | Interactive project scaffolding CLI |
33
48
 
34
49
  ---
35
50
 
@@ -42,76 +57,43 @@ npm install
42
57
  npm run dev
43
58
  ```
44
59
 
45
- ---
46
-
47
- ## Installation
60
+ Or with other package managers:
48
61
 
49
62
  ```bash
50
- # npm
51
- npm install @netrojs/fnetro solid-js hono
52
-
53
- # Dev deps (build toolchain)
54
- npm install -D vite vite-plugin-solid @hono/vite-dev-server typescript
55
- ```
56
-
57
- For Node.js runtime add:
58
- ```bash
59
- npm install -D @hono/node-server
60
- ```
61
-
62
- ### Peer dependencies
63
-
64
- | Package | Version | Required? |
65
- |---|---|---|
66
- | `solid-js` | `>=1.9.11` | ✅ Always |
67
- | `hono` | `>=4.0.0` | ✅ Always |
68
- | `vite` | `>=5.0.0` | Build only |
69
- | `vite-plugin-solid` | `>=2.11.11` | Build only |
70
-
71
- ---
72
-
73
- ## Project structure
74
-
75
- ```
76
- my-app/
77
- ├── app.ts # Shared FNetro app — used by dev server and server.ts
78
- ├── server.ts # Production server entry — calls serve()
79
- ├── client.ts # Browser entry — calls boot()
80
- ├── app/
81
- │ ├── layouts.tsx # defineLayout() — shared nav/footer shell
82
- │ └── routes/
83
- │ ├── home.tsx # definePage({ path: '/' })
84
- │ ├── about.tsx # definePage({ path: '/about' })
85
- │ └── api.ts # defineApiRoute('/api', ...)
86
- ├── public/
87
- │ └── style.css # Static assets served at /
88
- ├── vite.config.ts
89
- └── tsconfig.json
63
+ pnpm create @netrojs/fnetro@latest my-app
64
+ bun create @netrojs/fnetro my-app
65
+ deno run -A npm:create-fnetro my-app
90
66
  ```
91
67
 
92
68
  ---
93
69
 
94
- ## Core concepts
95
-
96
- FNetro is built on three files:
97
-
98
- | File | Purpose |
99
- |---|---|
100
- | `@netrojs/fnetro` (core) | Route builders, SEO types, path matching utilities |
101
- | `@netrojs/fnetro/server` | Hono app factory, SSR renderer, Vite plugin, `serve()` |
102
- | `@netrojs/fnetro/client` | SolidJS hydration, SPA routing, client middleware |
103
-
104
- **Data flow:**
105
-
106
- ```
107
- Request
108
- → Hono middleware
109
- Route match
110
- → Loader runs (server-side)
111
- SolidJS SSR renders HTML
112
- → HTML + state injected into shell
113
- Client hydrates
114
- → SPA navigation takes over (no full page reloads)
70
+ ## How it works
71
+
72
+ ```
73
+ Browser Server (Hono)
74
+ ──────────────────────────────── ─────────────────────────────────────
75
+ Global middleware
76
+
77
+ Route match ([id], [...slug], *)
78
+
79
+ Route middleware
80
+
81
+ Loader (async, type-safe)
82
+
83
+ SSR ────── SolidJS renderToStringAsync()
84
+ │ ↓
85
+ HTML + hydration script ◄─┘ SEO <head> injection
86
+
87
+ HTML shell (state + params + seo embedded)
88
+
89
+ SPA ────── JSON payload (state + seo only)
90
+
91
+ hydrate() ◄───────────────┘
92
+
93
+ Client middleware chain
94
+
95
+ SolidJS reactive component tree
96
+ (module-level signals persist across navigations)
115
97
  ```
116
98
 
117
99
  ---
@@ -120,77 +102,68 @@ Request
120
102
 
121
103
  ### `definePage`
122
104
 
123
- Define a page with a path, optional loader, optional SEO, and a SolidJS component.
105
+ Define a route with an optional SSR loader, SEO config, and a SolidJS component.
124
106
 
125
107
  ```tsx
126
- // app/routes/home.tsx
108
+ // app/routes/post.tsx
127
109
  import { definePage } from '@netrojs/fnetro'
128
110
 
129
111
  export default definePage({
130
- path: '/',
112
+ path: '/posts/[slug]',
131
113
 
132
- // Optional server-side data loader
133
114
  loader: async (c) => {
134
- const data = await fetchSomeData()
135
- return { items: data }
115
+ const slug = c.req.param('slug')
116
+ const post = await db.posts.findBySlug(slug)
117
+ if (!post) return c.notFound()
118
+ return { post }
136
119
  },
137
120
 
138
- // Optional SEO (see § SEO)
139
- seo: {
140
- title: 'Home — My App',
141
- description: 'Welcome to my app.',
142
- },
121
+ seo: (data) => ({
122
+ title: `${data.post.title} — My Blog`,
123
+ description: data.post.excerpt,
124
+ ogImage: data.post.coverUrl,
125
+ twitterCard: 'summary_large_image',
126
+ }),
143
127
 
144
- // SolidJS component receives loader data + url + params
145
- Page({ items, url, params }) {
146
- return (
147
- <ul>
148
- {items.map(item => <li>{item.name}</li>)}
149
- </ul>
150
- )
128
+ Page({ post, url, params }) {
129
+ return <article>{post.title}</article>
151
130
  },
152
131
  })
153
132
  ```
154
133
 
155
- **Dynamic segments** use `[param]` syntax:
156
-
157
- ```ts
158
- // matches /posts/hello-world → params.slug = 'hello-world'
159
- definePage({ path: '/posts/[slug]', ... })
134
+ **Path patterns:**
160
135
 
161
- // catch-all: matches /files/a/b/c params.rest = 'a/b/c'
162
- definePage({ path: '/files/[...rest]', ... })
163
- ```
136
+ | Pattern | Matches | `params` |
137
+ |---|---|---|
138
+ | `/posts/[slug]` | `/posts/hello-world` | `{ slug: 'hello-world' }` |
139
+ | `/files/[...rest]` | `/files/a/b/c` | `{ rest: 'a/b/c' }` |
140
+ | `/shop/*` | `/shop/anything` | *(positional)* |
164
141
 
165
142
  ---
166
143
 
167
144
  ### `defineGroup`
168
145
 
169
- Group routes under a shared prefix, layout, and middleware.
146
+ Group routes under a shared URL prefix, layout, and middleware.
170
147
 
171
148
  ```ts
172
149
  import { defineGroup } from '@netrojs/fnetro'
173
- import { requireAuth } from './middleware/auth'
174
- import { AdminLayout } from './layouts'
175
- import dashboard from './routes/admin/dashboard'
176
- import users from './routes/admin/users'
177
150
 
178
151
  export const adminGroup = defineGroup({
179
152
  prefix: '/admin',
180
- layout: AdminLayout,
181
- middleware: [requireAuth],
182
- routes: [dashboard, users],
153
+ layout: AdminLayout, // optional — overrides app default
154
+ middleware: [requireAuth, auditLog],
155
+ routes: [dashboard, users, settings],
183
156
  })
184
157
  ```
185
158
 
186
- Groups can be nested:
159
+ Groups nest arbitrarily:
187
160
 
188
161
  ```ts
189
162
  defineGroup({
190
163
  prefix: '/api',
191
164
  routes: [
192
- defineGroup({ prefix: '/v1', routes: [v1Routes] }),
193
- defineGroup({ prefix: '/v2', routes: [v2Routes] }),
165
+ defineGroup({ prefix: '/v1', routes: [v1] }),
166
+ defineGroup({ prefix: '/v2', routes: [v2] }),
194
167
  ],
195
168
  })
196
169
  ```
@@ -199,18 +172,18 @@ defineGroup({
199
172
 
200
173
  ### `defineLayout`
201
174
 
202
- Create a shared layout component that wraps page content.
175
+ Wrap every page with a shared shell (nav, footer, providers).
203
176
 
204
177
  ```tsx
205
178
  import { defineLayout } from '@netrojs/fnetro'
206
179
  import { createSignal } from 'solid-js'
207
180
 
208
- const [mobileOpen, setMobileOpen] = createSignal(false)
181
+ // Module-level signal persists across SPA navigations
182
+ const [sidebarOpen, setSidebarOpen] = createSignal(false)
209
183
 
210
184
  export const RootLayout = defineLayout(({ children, url, params }) => (
211
185
  <div class="app">
212
- <nav class="navbar">
213
- <a href="/" class="logo">My App</a>
186
+ <nav>
214
187
  <a href="/" class={url === '/' ? 'active' : ''}>Home</a>
215
188
  <a href="/about" class={url === '/about' ? 'active' : ''}>About</a>
216
189
  </nav>
@@ -220,21 +193,21 @@ export const RootLayout = defineLayout(({ children, url, params }) => (
220
193
  ))
221
194
  ```
222
195
 
223
- **Per-page layout override:**
196
+ **Per-page override:**
224
197
 
225
198
  ```ts
226
- // Use a different layout for this page
199
+ // Use a different layout
227
200
  definePage({ path: '/landing', layout: LandingLayout, Page: ... })
228
201
 
229
- // No layout for this page
230
- definePage({ path: '/embed', layout: false, Page: ... })
202
+ // Disable layout entirely
203
+ definePage({ path: '/embed', layout: false, Page: ... })
231
204
  ```
232
205
 
233
206
  ---
234
207
 
235
208
  ### `defineApiRoute`
236
209
 
237
- Mount raw Hono routes at a path. Full Hono API available.
210
+ Mount raw Hono sub-routes. Full Hono API — REST, RPC, WebSocket, streaming.
238
211
 
239
212
  ```ts
240
213
  import { defineApiRoute } from '@netrojs/fnetro'
@@ -242,28 +215,23 @@ import { zValidator } from '@hono/zod-validator'
242
215
  import { z } from 'zod'
243
216
 
244
217
  export const api = defineApiRoute('/api', (app) => {
245
- app.get('/health', (c) => c.json({ status: 'ok', ts: Date.now() }))
218
+ app.get('/health', (c) =>
219
+ c.json({ status: 'ok', ts: Date.now() }),
220
+ )
246
221
 
247
222
  app.get('/users/:id', async (c) => {
248
223
  const user = await db.users.find(c.req.param('id'))
249
- if (!user) return c.json({ error: 'Not found' }, 404)
250
- return c.json(user)
224
+ return user ? c.json(user) : c.json({ error: 'not found' }, 404)
251
225
  })
252
226
 
253
227
  app.post(
254
228
  '/items',
255
229
  zValidator('json', z.object({ name: z.string().min(1) })),
256
230
  async (c) => {
257
- const body = c.req.valid('json')
258
- const item = await db.items.create(body)
231
+ const item = await db.items.create(c.req.valid('json'))
259
232
  return c.json(item, 201)
260
233
  },
261
234
  )
262
-
263
- // WebSocket example
264
- app.get('/ws', upgradeWebSocket(() => ({
265
- onMessage(e, ws) { ws.send(`Echo: ${e.data}`) },
266
- })))
267
235
  })
268
236
  ```
269
237
 
@@ -271,45 +239,37 @@ export const api = defineApiRoute('/api', (app) => {
271
239
 
272
240
  ## Loaders
273
241
 
274
- Loaders run **server-side on every request** (both SSR and SPA navigation).
275
- The return value is serialized to JSON and passed to the Page component as props.
242
+ Loaders run **on the server on every request** both initial SSR and SPA navigations. The return value is JSON-serialised and injected as page props.
276
243
 
277
244
  ```ts
278
245
  definePage({
279
- path: '/posts/[slug]',
246
+ path: '/dashboard',
280
247
 
281
248
  loader: async (c) => {
282
- // c is a Hono Context — access headers, cookies, query params, etc.
283
- const slug = c.req.param('slug')
284
- const token = getCookie(c, 'session')
285
- const post = await db.posts.findBySlug(slug)
249
+ // Full Hono Context — headers, cookies, query params, env bindings
250
+ const session = getCookie(c, 'session')
251
+ if (!session) return c.redirect('/login')
286
252
 
287
- if (!post) {
288
- // Return a 404 response from the loader
289
- return c.notFound()
290
- }
291
-
292
- return { post }
253
+ const user = await auth.verify(session)
254
+ const stats = await db.stats.forUser(user.id)
255
+ return { user, stats }
293
256
  },
294
257
 
295
- Page({ post }) { ... },
258
+ Page({ user, stats }) { /* typed */ },
296
259
  })
297
260
  ```
298
261
 
299
262
  **Type-safe loaders:**
300
263
 
301
264
  ```ts
302
- interface PostData {
303
- post: Post
304
- author: User
305
- }
265
+ interface DashboardData { user: User; stats: Stats }
306
266
 
307
- definePage<PostData>({
308
- loader: async (c): Promise<PostData> => ({
309
- post: await db.posts.find(c.req.param('id')),
310
- author: await db.users.find(post.authorId),
267
+ definePage<DashboardData>({
268
+ loader: async (c): Promise<DashboardData> => ({
269
+ user: await getUser(c),
270
+ stats: await getStats(c),
311
271
  }),
312
- Page({ post, author }) { /* fully typed */ },
272
+ Page({ user, stats }) { /* DashboardData & { url, params } */ },
313
273
  })
314
274
  ```
315
275
 
@@ -318,10 +278,10 @@ definePage<PostData>({
318
278
  ## SEO
319
279
 
320
280
  Every page can declare `seo` as a **static object** or a **function of loader data**.
321
- App-level `seo` provides defaults; page-level values override them.
281
+ App-level `seo` provides global defaults; page-level values override them.
322
282
 
323
283
  ```ts
324
- // app.ts — global defaults
284
+ // app.ts — global defaults applied to every page
325
285
  createFNetro({
326
286
  seo: {
327
287
  ogType: 'website',
@@ -329,42 +289,49 @@ createFNetro({
329
289
  twitterCard: 'summary_large_image',
330
290
  twitterSite: '@myapp',
331
291
  robots: 'index, follow',
292
+ themeColor: '#0d0f14',
332
293
  },
333
294
  routes: [...],
334
295
  })
296
+ ```
335
297
 
336
- // app/routes/post.tsx — page-level (merges with app defaults)
298
+ ```ts
299
+ // app/routes/post.tsx — page overrides (merged with app defaults)
337
300
  definePage({
338
301
  path: '/posts/[slug]',
339
- loader: async (c) => ({ post: await getPost(c.req.param('slug')) }),
302
+ loader: (c) => ({ post: await getPost(c.req.param('slug')) }),
340
303
 
341
- // Function form — receives loader data and params
342
304
  seo: (data, params) => ({
343
305
  title: `${data.post.title} — My Blog`,
344
306
  description: data.post.excerpt,
345
- canonical: `https://myapp.com/posts/${params.slug}`,
307
+ canonical: `https://example.com/posts/${params.slug}`,
346
308
  ogTitle: data.post.title,
347
309
  ogDescription: data.post.excerpt,
348
- ogImage: data.post.coverImageUrl,
310
+ ogImage: data.post.coverUrl,
349
311
  ogImageWidth: '1200',
350
312
  ogImageHeight: '630',
351
313
  twitterTitle: data.post.title,
352
- twitterImage: data.post.coverImageUrl,
314
+ twitterImage: data.post.coverUrl,
353
315
  jsonLd: {
354
- '@context': 'https://schema.org',
355
- '@type': 'Article',
356
- headline: data.post.title,
357
- author: { '@type': 'Person', name: data.post.authorName },
316
+ '@context': 'https://schema.org',
317
+ '@type': 'Article',
318
+ headline: data.post.title,
319
+ author: { '@type': 'Person', name: data.post.authorName },
358
320
  datePublished: data.post.publishedAt,
321
+ image: data.post.coverUrl,
359
322
  },
323
+ extra: [
324
+ { name: 'article:author', content: data.post.authorName },
325
+ ],
360
326
  }),
327
+
361
328
  Page({ post }) { ... },
362
329
  })
363
330
  ```
364
331
 
365
332
  ### All SEO fields
366
333
 
367
- | Field | HTML output |
334
+ | Field | `<head>` output |
368
335
  |---|---|
369
336
  | `title` | `<title>` |
370
337
  | `description` | `<meta name="description">` |
@@ -389,10 +356,11 @@ definePage({
389
356
  | `twitterTitle` | `<meta name="twitter:title">` |
390
357
  | `twitterDescription` | `<meta name="twitter:description">` |
391
358
  | `twitterImage` | `<meta name="twitter:image">` |
359
+ | `twitterImageAlt` | `<meta name="twitter:image:alt">` |
392
360
  | `jsonLd` | `<script type="application/ld+json">` |
393
- | `extra` | Custom `<meta>` tags |
361
+ | `extra` | Arbitrary `<meta>` tags |
394
362
 
395
- **Client-side SEO sync:** On SPA navigation, all `<meta>` tags and `document.title` are updated automatically — no full reload needed.
363
+ On SPA navigation, all `<meta>` tags and `document.title` are updated automatically — no full reload.
396
364
 
397
365
  ---
398
366
 
@@ -400,29 +368,29 @@ definePage({
400
368
 
401
369
  ### Server middleware
402
370
 
403
- Hono middleware applied at three levels:
371
+ Hono middleware at three levels — global, group, and page.
404
372
 
405
373
  ```ts
406
374
  import { createFNetro } from '@netrojs/fnetro/server'
407
- import { cors } from 'hono/cors'
408
- import { logger } from 'hono/logger'
409
- import { bearerAuth } from 'hono/bearer-auth'
375
+ import { cors } from 'hono/cors'
376
+ import { logger } from 'hono/logger'
377
+ import { bearerAuth } from 'hono/bearer-auth'
410
378
 
411
- // 1. Global — runs before every route
412
379
  const fnetro = createFNetro({
413
- middleware: [logger(), cors({ origin: 'https://myapp.com' })],
380
+ // 1. Global runs on every request
381
+ middleware: [logger(), cors({ origin: 'https://example.com' })],
414
382
 
415
383
  routes: [
416
- // 2. Group-level — runs for all routes in the group
384
+ // 2. Group-level — runs for every route in the group
417
385
  defineGroup({
418
- prefix: '/dashboard',
386
+ prefix: '/admin',
419
387
  middleware: [bearerAuth({ token: process.env.API_KEY! })],
420
388
  routes: [
421
389
  // 3. Page-level — runs for this route only
422
390
  definePage({
423
- path: '/settings',
391
+ path: '/reports',
424
392
  middleware: [rateLimiter({ max: 10, window: '1m' })],
425
- Page: Settings,
393
+ Page: Reports,
426
394
  }),
427
395
  ],
428
396
  }),
@@ -445,73 +413,61 @@ const requireAuth: HonoMiddleware = async (c, next) => {
445
413
 
446
414
  ### Client middleware
447
415
 
448
- Client middleware runs before every **SPA navigation**. Register with `useClientMiddleware()` **before** calling `boot()`.
416
+ Runs before every **SPA navigation**. Register with `useClientMiddleware()` **before** `boot()`.
449
417
 
450
418
  ```ts
451
419
  // client.ts
452
420
  import { boot, useClientMiddleware, navigate } from '@netrojs/fnetro/client'
453
421
 
454
- // Analytics
422
+ // Analytics — fires after navigation completes
455
423
  useClientMiddleware(async (url, next) => {
456
424
  await next()
457
- analytics.track('pageview', { url })
425
+ analytics.page({ url })
458
426
  })
459
427
 
460
- // Auth guard
428
+ // Auth guard — redirects before navigation
461
429
  useClientMiddleware(async (url, next) => {
462
- const protectedPaths = ['/dashboard', '/settings', '/profile']
463
- const isProtected = protectedPaths.some(p => url.startsWith(p))
464
-
465
- if (isProtected && !isAuthenticated()) {
466
- await navigate(`/login?redirect=${encodeURIComponent(url)}`)
467
- return // cancel original navigation
430
+ if (!isLoggedIn() && url.startsWith('/dashboard')) {
431
+ await navigate('/login?redirect=' + encodeURIComponent(url))
432
+ return // cancel the original navigation
468
433
  }
469
-
470
434
  await next()
471
435
  })
472
436
 
473
437
  // Loading indicator
474
438
  useClientMiddleware(async (url, next) => {
475
- showLoadingBar()
476
- try {
477
- await next()
478
- } finally {
479
- hideLoadingBar()
480
- }
439
+ NProgress.start()
440
+ try { await next() }
441
+ finally { NProgress.done() }
481
442
  })
482
443
 
483
444
  boot({ routes, layout })
484
445
  ```
485
446
 
486
- Middleware runs in registration order. The chain is `mw1 → mw2 → ... → actual navigation`.
447
+ The chain runs in registration order: `mw1 → mw2 → ... → fetch + render`. Omitting `next()` in any middleware cancels the navigation.
487
448
 
488
449
  ---
489
450
 
490
451
  ## SolidJS reactivity
491
452
 
492
- Use SolidJS primitives directly — no FNetro wrappers needed.
453
+ Use SolidJS primitives directly — no FNetro wrappers.
454
+
455
+ **Module-level signals** persist across SPA navigations (they live for the lifetime of the page JS):
493
456
 
494
457
  ```tsx
495
- import { createSignal, createMemo, createEffect, For, Show } from 'solid-js'
496
- import { createStore, produce } from 'solid-js/store'
458
+ import { createSignal, createMemo, createEffect, For } from 'solid-js'
497
459
  import { definePage } from '@netrojs/fnetro'
498
460
 
499
- // Module-level signals persist across SPA navigations
500
461
  const [count, setCount] = createSignal(0)
501
462
  const doubled = createMemo(() => count() * 2)
502
463
 
503
464
  export default definePage({
504
465
  path: '/counter',
505
466
  Page() {
506
- // Effects run automatically when signals they read change
507
- createEffect(() => {
508
- document.title = `Count: ${count()}`
509
- })
510
-
467
+ createEffect(() => { document.title = `Count: ${count()}` })
511
468
  return (
512
469
  <div>
513
- <p>Count: {count()}</p>
514
- <p>Doubled: {doubled()}</p>
470
+ <p>{count()} × 2 = {doubled()}</p>
515
471
  <button onClick={() => setCount(n => n + 1)}>+</button>
516
472
  </div>
517
473
  )
@@ -519,20 +475,15 @@ export default definePage({
519
475
  })
520
476
  ```
521
477
 
522
- **Store example:**
478
+ **Stores** for structured reactive state:
523
479
 
524
480
  ```tsx
525
481
  import { createStore, produce } from 'solid-js/store'
526
482
 
527
483
  interface Todo { id: number; text: string; done: boolean }
528
-
529
484
  const [todos, setTodos] = createStore<{ items: Todo[] }>({ items: [] })
530
485
 
531
- function addTodo(text: string) {
532
- setTodos('items', l => [...l, { id: Date.now(), text, done: false }])
533
- }
534
-
535
- function toggleTodo(id: number) {
486
+ function toggle(id: number) {
536
487
  setTodos('items', t => t.id === id, produce(t => { t.done = !t.done }))
537
488
  }
538
489
 
@@ -540,18 +491,16 @@ export default definePage({
540
491
  path: '/todos',
541
492
  Page() {
542
493
  return (
543
- <ul>
544
- <For each={todos.items}>
545
- {todo => (
546
- <li
547
- style={{ 'text-decoration': todo.done ? 'line-through' : 'none' }}
548
- onClick={() => toggleTodo(todo.id)}
549
- >
550
- {todo.text}
551
- </li>
552
- )}
553
- </For>
554
- </ul>
494
+ <For each={todos.items}>
495
+ {(todo) => (
496
+ <li
497
+ style={{ 'text-decoration': todo.done ? 'line-through' : 'none' }}
498
+ onClick={() => toggle(todo.id)}
499
+ >
500
+ {todo.text}
501
+ </li>
502
+ )}
503
+ </For>
555
504
  )
556
505
  },
557
506
  })
@@ -561,18 +510,15 @@ export default definePage({
561
510
 
562
511
  ## Navigation
563
512
 
564
- ### Link-based (automatic)
513
+ ### Links automatic interception
565
514
 
566
- Any `<a href="...">` pointing to a registered route is intercepted automatically no special component needed.
515
+ Any `<a href="...">` pointing to a registered route is intercepted automatically. No special component needed.
567
516
 
568
517
  ```tsx
569
- // These all work — SPA navigation, no full reload
570
- <a href="/about">About</a>
571
- <a href="/posts/hello">Read post</a>
572
-
573
- // Opt out with data-no-spa or rel="external"
574
- <a href="/legacy" data-no-spa>Legacy page</a>
575
- <a href="https://external.com" rel="external">External</a>
518
+ <a href="/about">About</a> {/* SPA navigation */}
519
+ <a href="/posts/hello">Post</a> {/* → SPA navigation */}
520
+ <a href="/legacy" data-no-spa>Legacy</a> {/* → full page load */}
521
+ <a href="https://example.com" rel="external">External</a> {/* → full page load */}
576
522
  ```
577
523
 
578
524
  ### Programmatic navigation
@@ -580,14 +526,9 @@ Any `<a href="...">` pointing to a registered route is intercepted automatically
580
526
  ```ts
581
527
  import { navigate } from '@netrojs/fnetro/client'
582
528
 
583
- // Push to history (default)
584
- await navigate('/about')
585
-
586
- // Replace current history entry
587
- await navigate('/login', { replace: true })
588
-
589
- // Prevent scroll-to-top
590
- await navigate('/modal', { scroll: false })
529
+ await navigate('/about') // push history
530
+ await navigate('/login', { replace: true }) // replace history entry
531
+ await navigate('/modal', { scroll: false }) // skip scroll-to-top
591
532
  ```
592
533
 
593
534
  ### Prefetch
@@ -595,18 +536,10 @@ await navigate('/modal', { scroll: false })
595
536
  ```ts
596
537
  import { prefetch } from '@netrojs/fnetro/client'
597
538
 
598
- // On hover/focus warms the loader cache
599
- prefetch('/about')
539
+ prefetch('/about') // warm the loader cache on hover / focus
600
540
  ```
601
541
 
602
- Hover-based prefetching is enabled by default in `boot()`:
603
-
604
- ```ts
605
- boot({
606
- prefetchOnHover: true, // default: true
607
- routes,
608
- })
609
- ```
542
+ Hover-based prefetching is automatic when `prefetchOnHover: true` (the default) is set in `boot()`.
610
543
 
611
544
  ---
612
545
 
@@ -614,46 +547,44 @@ boot({
614
547
 
615
548
  ### Development
616
549
 
617
- `@hono/vite-dev-server` injects Vite's dev client automatically. No asset configuration needed.
550
+ `@hono/vite-dev-server` injects Vite's dev client and HMR scripts automatically. No asset config needed.
618
551
 
619
552
  ### Production
620
553
 
621
- The Vite plugin produces a `manifest.json` alongside the client bundle. The server reads it at startup to inject correct hashed URLs into every HTML response.
554
+ `vite build` produces a `manifest.json` alongside the hashed client bundle. The server reads the manifest at startup to resolve the correct filenames.
622
555
 
623
556
  ```ts
624
- // app.ts — production configuration
557
+ // app.ts
625
558
  createFNetro({
626
559
  routes,
627
560
  assets: {
628
- // Path to the directory containing manifest.json
629
- manifestDir: 'dist/assets',
630
- // Key in the manifest (usually the entry filename)
631
- manifestEntry: 'client.ts',
561
+ manifestDir: 'dist/assets', // directory containing manifest.json
562
+ manifestEntry: 'client.ts', // key in the manifest (your client entry)
632
563
  },
633
564
  })
634
565
  ```
635
566
 
636
- **Manual asset paths** (edge runtimes / when manifest is unavailable):
567
+ **Manual override** (edge runtimes / CDN-hosted assets):
637
568
 
638
569
  ```ts
639
570
  createFNetro({
640
571
  assets: {
641
- scripts: ['/assets/client-abc123.js'],
642
- styles: ['/assets/style-def456.css'],
572
+ scripts: ['https://cdn.example.com/client-abc123.js'],
573
+ styles: ['https://cdn.example.com/style-def456.css'],
643
574
  },
644
575
  })
645
576
  ```
646
577
 
647
- **Public directory:** Static files in `public/` (images, fonts, robots.txt) are served at the root path by the Node.js `serve()` helper automatically.
578
+ **Public directory** static files in `public/` (images, fonts, `robots.txt`, `favicon.ico`) are served at `/` by the Node.js `serve()` helper automatically.
648
579
 
649
580
  ---
650
581
 
651
- ## Multi-runtime `serve()`
582
+ ## Multi-runtime serve()
652
583
 
653
584
  ```ts
654
585
  import { serve } from '@netrojs/fnetro/server'
655
586
 
656
- // Auto-detects the runtime
587
+ // Auto-detects Node.js, Bun, or Deno
657
588
  await serve({ app: fnetro })
658
589
 
659
590
  // Explicit configuration
@@ -661,16 +592,18 @@ await serve({
661
592
  app: fnetro,
662
593
  port: 3000,
663
594
  hostname: '0.0.0.0',
664
- runtime: 'node', // 'node' | 'bun' | 'deno'
665
- staticDir: './dist', // where dist/assets/ lives
595
+ runtime: 'node', // 'node' | 'bun' | 'deno' | 'edge'
596
+ staticDir: './dist', // root for /assets/* and /* static files
666
597
  })
667
598
  ```
668
599
 
669
- **Edge runtimes** (Cloudflare Workers, Deno Deploy, etc.) — just export the handler:
600
+ **Edge runtimes** (Cloudflare Workers, Deno Deploy, Fastly, etc.):
670
601
 
671
602
  ```ts
672
603
  // server.ts
673
604
  import { fnetro } from './app'
605
+
606
+ // Export the Hono fetch handler — the platform calls it directly
674
607
  export default { fetch: fnetro.handler }
675
608
  ```
676
609
 
@@ -680,25 +613,25 @@ export default { fetch: fnetro.handler }
680
613
 
681
614
  ```ts
682
615
  // vite.config.ts
683
- import { defineConfig } from 'vite'
616
+ import { defineConfig } from 'vite'
684
617
  import { fnetroVitePlugin } from '@netrojs/fnetro/vite'
685
- import devServer from '@hono/vite-dev-server'
618
+ import devServer from '@hono/vite-dev-server'
686
619
 
687
620
  export default defineConfig({
688
621
  plugins: [
689
- // Handles JSX transform (vite-plugin-solid) + production dual build
622
+ // Handles: SolidJS JSX transform, SSR server build, client bundle + manifest
690
623
  fnetroVitePlugin({
691
- serverEntry: 'app/server.ts', // default: 'app/server.ts'
692
- clientEntry: 'client.ts', // default: 'client.ts'
693
- serverOutDir: 'dist/server', // default: 'dist/server'
694
- clientOutDir: 'dist/assets', // default: 'dist/assets'
695
- // Extra packages to externalize in the server bundle
696
- serverExternal: ['@myorg/db'],
697
- // Options forwarded to vite-plugin-solid
698
- solidOptions: { extensions: ['.mdx'] },
624
+ serverEntry: 'server.ts', // default: 'server.ts'
625
+ clientEntry: 'client.ts', // default: 'client.ts'
626
+ serverOutDir: 'dist/server', // default: 'dist/server'
627
+ clientOutDir: 'dist/assets', // default: 'dist/assets'
628
+ serverExternal: ['@myorg/db'], // extra server-bundle externals
629
+ solidOptions: {}, // forwarded to vite-plugin-solid
699
630
  }),
700
631
 
701
- // Dev server — serves the app with hot-reload
632
+ // Dev: serves the FNetro app through Vite with hot-reload
633
+ // app.ts default export must be the Hono *instance* (fnetro.app),
634
+ // NOT fnetro.handler (plain function, no .fetch property).
702
635
  devServer({ entry: 'app.ts' }),
703
636
  ],
704
637
  })
@@ -709,26 +642,65 @@ export default defineConfig({
709
642
  ```
710
643
  dist/
711
644
  ├── server/
712
- │ └── server.js # SSR server bundle
645
+ │ └── server.js # SSR server bundle (ESM)
713
646
  └── assets/
714
- ├── manifest.json # Asset manifest (for hashed URL resolution)
715
- ├── client-abc123.js # Hydration bundle
716
- └── style-def456.css # CSS (if imported in JS)
647
+ ├── manifest.json # Vite asset manifest (for hashed URL resolution)
648
+ ├── client-[hash].js # Hydration + SPA bundle
649
+ └── style-[hash].css # CSS (when imported from JS)
717
650
  ```
718
651
 
719
652
  ---
720
653
 
654
+ ## Project structure
655
+
656
+ ```
657
+ my-app/
658
+
659
+ ├── app.ts # Shared FNetro app — used by dev server AND server.ts
660
+ │ # Default export must be fnetro.app (Hono instance)
661
+
662
+ ├── server.ts # Production entry — imports app.ts, calls serve()
663
+ ├── client.ts # Browser entry — registers middleware, calls boot()
664
+
665
+ ├── app/
666
+ │ ├── layouts.tsx # defineLayout() — root shell (nav, footer)
667
+ │ └── routes/
668
+ │ ├── home.tsx # definePage({ path: '/' })
669
+ │ ├── about.tsx # definePage({ path: '/about' })
670
+ │ ├── api.ts # defineApiRoute('/api', fn)
671
+ │ └── posts/
672
+ │ ├── index.tsx # /posts
673
+ │ └── [slug].tsx # /posts/:slug
674
+
675
+ ├── public/
676
+ │ ├── style.css # Global CSS (served at /style.css)
677
+ │ └── favicon.ico
678
+
679
+ ├── vite.config.ts
680
+ ├── tsconfig.json
681
+ └── package.json
682
+ ```
683
+
684
+ ### `app.ts` vs `server.ts`
685
+
686
+ | File | Purpose |
687
+ |---|---|
688
+ | `app.ts` | Creates the FNetro app. Exports `fnetro` (named) and `fnetro.app` (default). Used by `@hono/vite-dev-server` in dev and imported by `server.ts` in production. |
689
+ | `server.ts` | Production-only entry point. Imports from `app.ts` and calls `serve()`. Never imported by the dev server. |
690
+
691
+ ---
692
+
721
693
  ## TypeScript
722
694
 
723
- `tsconfig.json` for a FNetro project:
695
+ `tsconfig.json` for any FNetro project:
724
696
 
725
697
  ```json
726
698
  {
727
699
  "compilerOptions": {
728
- "target": "ESNext",
700
+ "target": "ES2022",
729
701
  "module": "ESNext",
730
702
  "moduleResolution": "bundler",
731
- "lib": ["ESNext", "DOM"],
703
+ "lib": ["ES2022", "DOM"],
732
704
  "jsx": "preserve",
733
705
  "jsxImportSource": "solid-js",
734
706
  "strict": true,
@@ -742,53 +714,293 @@ dist/
742
714
  }
743
715
  ```
744
716
 
717
+ > **Important:** `jsxImportSource` must be `"solid-js"` — not `"hono/jsx"`. FNetro v0.2+ uses SolidJS for all rendering.
718
+
719
+ ---
720
+
721
+ ## create-fnetro CLI
722
+
723
+ Scaffold a new project interactively or from CI:
724
+
725
+ ```bash
726
+ npm create @netrojs/fnetro@latest [project-name] [flags]
727
+ ```
728
+
729
+ ### Interactive mode
730
+
731
+ Running without flags opens a step-by-step prompt:
732
+
733
+ ```
734
+ ⬡ create-fnetro
735
+ Full-stack Hono + SolidJS — SSR · SPA · SEO · TypeScript
736
+
737
+ ✔ Project name: … my-app
738
+ ✔ Target runtime: › Node.js
739
+ ✔ Template: › Minimal
740
+ ✔ Package manager: › npm
741
+ ✔ Install dependencies now? … yes
742
+ ✔ Initialize a git repository? … yes
743
+ ```
744
+
745
+ ### CLI flags (non-interactive / CI)
746
+
747
+ | Flag | Values | Default |
748
+ |---|---|---|
749
+ | `--ci` | — | `false` |
750
+ | `--runtime` | `node` `bun` `deno` `cloudflare` `generic` | `node` |
751
+ | `--template` | `minimal` `full` | `minimal` |
752
+ | `--pkg-manager` | `npm` `pnpm` `yarn` `bun` `deno` | `npm` |
753
+ | `--no-install` | — | installs |
754
+ | `--no-git` | — | initialises |
755
+
756
+ ```bash
757
+ # Non-interactive CI scaffold
758
+ npm create @netrojs/fnetro@latest my-app \
759
+ --ci \
760
+ --runtime node \
761
+ --template full \
762
+ --pkg-manager pnpm \
763
+ --no-git
764
+ ```
765
+
766
+ ### Templates
767
+
768
+ **`minimal`** — production-ready starter:
769
+ ```
770
+ app.ts server.ts client.ts
771
+ app/layouts.tsx
772
+ app/routes/home.tsx # GET /
773
+ app/routes/about.tsx # GET /about
774
+ app/routes/api.ts # GET /api/health GET /api/hello
775
+ public/style.css
776
+ ```
777
+
778
+ **`full`** — includes SolidJS signal demo, dynamic routes, and shared store:
779
+ ```
780
+ (everything in minimal, plus)
781
+ app/store.ts # createSignal + createStore examples
782
+ app/routes/counter.tsx # GET /counter — signals demo
783
+ app/routes/posts/index.tsx # GET /posts — SSR list
784
+ app/routes/posts/[slug].tsx # GET /posts/:slug — dynamic SSR + SEO
785
+ ```
786
+
787
+ ### Supported runtimes
788
+
789
+ | Runtime | Dev command | Prod server |
790
+ |---|---|---|
791
+ | `node` | `vite` (via `@hono/vite-dev-server`) | `@hono/node-server` |
792
+ | `bun` | `bun --bun vite` | `Bun.serve` |
793
+ | `deno` | `deno run -A npm:vite` | `Deno.serve` |
794
+ | `cloudflare` | `wrangler dev` | Cloudflare Workers |
795
+ | `generic` | `vite` | WinterCG `export default { fetch }` |
796
+
745
797
  ---
746
798
 
747
799
  ## API reference
748
800
 
749
801
  ### `@netrojs/fnetro` (core)
750
802
 
751
- | Export | Description |
752
- |---|---|
753
- | `definePage(def)` | Define a page route |
754
- | `defineGroup(def)` | Define a route group |
755
- | `defineLayout(Component)` | Define a layout component |
756
- | `defineApiRoute(path, register)` | Define raw Hono sub-routes |
757
- | `resolveRoutes(routes, opts)` | Internal: flatten route tree |
758
- | `compilePath(path)` | Internal: compile a path pattern |
759
- | `matchPath(compiled, pathname)` | Internal: match a compiled path |
760
- | `SPA_HEADER` | `'x-fnetro-spa'` |
761
- | `STATE_KEY` | `'__FNETRO_STATE__'` |
762
- | `PARAMS_KEY` | `'__FNETRO_PARAMS__'` |
763
- | `SEO_KEY` | `'__FNETRO_SEO__'` |
764
-
765
- **Types:** `AppConfig`, `PageDef<T>`, `GroupDef`, `LayoutDef`, `ApiRouteDef`, `Route`, `PageProps<T>`, `LayoutProps`, `SEOMeta`, `HonoMiddleware`, `LoaderCtx`, `ClientMiddleware`, `ResolvedRoute`, `CompiledPath`
803
+ **Functions:**
804
+
805
+ | Export | Signature | Description |
806
+ |---|---|---|
807
+ | `definePage` | `<T>(def) → PageDef<T>` | Define a page route |
808
+ | `defineGroup` | `(def) → GroupDef` | Group routes under a prefix |
809
+ | `defineLayout` | `(Component) → LayoutDef` | Wrap pages in a shared shell |
810
+ | `defineApiRoute` | `(path, register) → ApiRouteDef` | Mount raw Hono sub-routes |
811
+ | `compilePath` | `(path) → CompiledPath` | Compile a path pattern to a regex |
812
+ | `matchPath` | `(compiled, pathname) → params \| null` | Match a compiled path |
813
+ | `resolveRoutes` | `(routes, opts) → { pages, apis }` | Flatten a route tree |
814
+
815
+ **Constants:** `SPA_HEADER` · `STATE_KEY` · `PARAMS_KEY` · `SEO_KEY`
816
+
817
+ **Types:** `AppConfig` · `PageDef<T>` · `GroupDef` · `LayoutDef` · `ApiRouteDef` · `Route` · `PageProps<T>` · `LayoutProps` · `SEOMeta` · `HonoMiddleware` · `LoaderCtx` · `ClientMiddleware` · `ResolvedRoute` · `CompiledPath`
766
818
 
767
819
  ---
768
820
 
769
821
  ### `@netrojs/fnetro/server`
770
822
 
771
- | Export | Description |
772
- |---|---|
773
- | `createFNetro(config)` | Create the FNetro/Hono app |
774
- | `serve(opts)` | Start server for Node/Bun/Deno |
775
- | `detectRuntime()` | Auto-detect the current JS runtime |
776
- | `fnetroVitePlugin(opts?)` | Vite plugin for SSR + client builds |
823
+ **Functions:**
824
+
825
+ | Export | Signature | Description |
826
+ |---|---|---|
827
+ | `createFNetro` | `(config: FNetroOptions) → FNetroApp` | Build the Hono app |
828
+ | `serve` | `(opts: ServeOptions) → Promise<void>` | Start server for Node/Bun/Deno |
829
+ | `detectRuntime` | `() → Runtime` | Auto-detect the current JS runtime |
830
+ | `fnetroVitePlugin` | `(opts?) → Plugin[]` | Vite plugin for dual build |
831
+
832
+ **`FNetroOptions`** (extends `AppConfig`):
777
833
 
778
- **Types:** `FNetroOptions`, `FNetroApp`, `ServeOptions`, `Runtime`, `AssetConfig`, `FNetroPluginOptions`
834
+ ```ts
835
+ interface FNetroOptions {
836
+ layout?: LayoutDef // default layout for all pages
837
+ seo?: SEOMeta // global SEO defaults
838
+ middleware?: HonoMiddleware[] // global Hono middleware
839
+ routes: Route[] // top-level routes
840
+ notFound?: Component // 404 component
841
+ htmlAttrs?: Record<string,string> // attributes on <html>
842
+ head?: string // raw HTML appended to <head>
843
+ assets?: AssetConfig // production asset config
844
+ }
845
+ ```
846
+
847
+ **`AssetConfig`:**
848
+
849
+ ```ts
850
+ interface AssetConfig {
851
+ scripts?: string[] // explicit script URLs
852
+ styles?: string[] // explicit stylesheet URLs
853
+ manifestDir?: string // directory containing manifest.json
854
+ manifestEntry?: string // manifest key for client entry (default: 'client.ts')
855
+ }
856
+ ```
857
+
858
+ **`ServeOptions`:**
859
+
860
+ ```ts
861
+ interface ServeOptions {
862
+ app: FNetroApp
863
+ port?: number // default: process.env.PORT ?? 3000
864
+ hostname?: string // default: '0.0.0.0'
865
+ runtime?: Runtime // default: auto-detected
866
+ staticDir?: string // default: './dist'
867
+ }
868
+ ```
869
+
870
+ **`FNetroPluginOptions`:**
871
+
872
+ ```ts
873
+ interface FNetroPluginOptions {
874
+ serverEntry?: string // default: 'server.ts'
875
+ clientEntry?: string // default: 'client.ts'
876
+ serverOutDir?: string // default: 'dist/server'
877
+ clientOutDir?: string // default: 'dist/assets'
878
+ serverExternal?: string[] // extra server-bundle externals
879
+ solidOptions?: object // passed to vite-plugin-solid
880
+ }
881
+ ```
779
882
 
780
883
  ---
781
884
 
782
885
  ### `@netrojs/fnetro/client`
783
886
 
784
- | Export | Description |
785
- |---|---|
786
- | `boot(options)` | Hydrate SSR HTML and start SPA |
787
- | `navigate(to, opts?)` | Programmatic SPA navigation |
788
- | `prefetch(url)` | Pre-warm the loader cache |
789
- | `useClientMiddleware(fn)` | Register client navigation middleware |
887
+ **Functions:**
888
+
889
+ | Export | Signature | Description |
890
+ |---|---|---|
891
+ | `boot` | `(opts: BootOptions) → Promise<void>` | Hydrate SSR and start SPA |
892
+ | `navigate` | `(to, opts?) Promise<void>` | Programmatic navigation |
893
+ | `prefetch` | `(url) → void` | Warm loader cache |
894
+ | `useClientMiddleware` | `(fn: ClientMiddleware) → void` | Register nav middleware |
895
+
896
+ **`BootOptions`** (extends `AppConfig`):
897
+
898
+ ```ts
899
+ interface BootOptions extends AppConfig {
900
+ prefetchOnHover?: boolean // default: true
901
+ }
902
+ ```
903
+
904
+ **`NavigateOptions`:**
905
+
906
+ ```ts
907
+ interface NavigateOptions {
908
+ replace?: boolean // replaceState instead of pushState
909
+ scroll?: boolean // scroll to top after navigation (default: true)
910
+ }
911
+ ```
912
+
913
+ **`ClientMiddleware`:**
914
+
915
+ ```ts
916
+ type ClientMiddleware = (
917
+ url: string,
918
+ next: () => Promise<void>,
919
+ ) => Promise<void>
920
+ ```
921
+
922
+ ---
923
+
924
+ ## Monorepo development
925
+
926
+ ```bash
927
+ # Clone and install
928
+ git clone https://github.com/netrosolutions/fnetro.git
929
+ cd fnetro
930
+ npm install # hoists all workspace deps to root node_modules
931
+
932
+ # Build both packages
933
+ npm run build
934
+
935
+ # Typecheck both packages
936
+ npm run typecheck
937
+
938
+ # Clean all dist/ directories
939
+ npm run clean
790
940
 
791
- **Types:** `BootOptions`, `NavigateOptions`
941
+ # Watch mode (fnetro package)
942
+ npm run build:watch --workspace=packages/fnetro
943
+ ```
944
+
945
+ ### Workspace structure
946
+
947
+ ```
948
+ fnetro/ root (private monorepo)
949
+ ├── packages/
950
+ │ ├── fnetro/ @netrojs/fnetro
951
+ │ │ ├── core.ts Shared types, path matching, constants
952
+ │ │ ├── server.ts Hono factory, SSR renderer, Vite plugin, serve()
953
+ │ │ ├── client.ts SolidJS hydration, SPA router, client middleware
954
+ │ │ └── tsup.config.ts Build config (3 separate entry points)
955
+ │ └── create-fnetro/ @netrojs/create-fnetro
956
+ │ └── src/index.ts CLI scaffolding tool
957
+ ├── .changeset/ Changeset version files
958
+ │ └── config.json
959
+ └── .github/
960
+ └── workflows/
961
+ ├── ci.yml Typecheck, build, scaffold smoke tests
962
+ └── release.yml Changeset-driven versioning + npm publish
963
+ ```
964
+
965
+ ---
966
+
967
+ ## Publishing & releases
968
+
969
+ This monorepo uses [Changesets](https://github.com/changesets/changesets) for versioning and publishing.
970
+
971
+ ### Day-to-day workflow
972
+
973
+ **1. Make changes** to `packages/fnetro` and/or `packages/create-fnetro`.
974
+
975
+ **2. Add a changeset** describing the change:
976
+ ```bash
977
+ npm run changeset
978
+ # → prompts you to select packages and bump type (patch/minor/major)
979
+ # → writes a .changeset/*.md file — commit this with your changes
980
+ ```
981
+
982
+ **3. Open a PR.** CI runs typecheck, build, and scaffold smoke tests on Node 18 / 20 / 22 / 24.
983
+
984
+ **4. Merge to `main`.** The `release.yml` workflow runs automatically:
985
+ - If `.changeset/*.md` files exist → opens / updates a **"Version Packages"** PR that bumps versions and updates `CHANGELOG.md`
986
+ - If the "Version Packages" PR is merged → **publishes both packages to npm** with provenance attestation and creates a GitHub Release
987
+
988
+ ### Manual release
989
+
990
+ ```bash
991
+ # Dry run — see what would be published
992
+ npm run release:dry
993
+
994
+ # Full release (build + changeset publish)
995
+ npm run release
996
+ ```
997
+
998
+ ### Secrets required
999
+
1000
+ | Secret | Description |
1001
+ |---|---|
1002
+ | `NPM_TOKEN` | npm automation token (requires publish permission for `@netrojs`) |
1003
+ | `GITHUB_TOKEN` | Provided automatically by GitHub Actions |
792
1004
 
793
1005
  ---
794
1006
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netrojs/fnetro",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Full-stack Hono framework — SolidJS SSR/SPA, SEO, middleware, route groups, API routes",
5
5
  "type": "module",
6
6
  "license": "MIT",