@netrojs/fnetro 0.1.2
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.
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/client.ts +307 -0
- package/core.ts +734 -0
- package/dist/client.d.ts +196 -0
- package/dist/client.js +673 -0
- package/dist/core.d.ts +200 -0
- package/dist/core.js +495 -0
- package/dist/server.d.ts +231 -0
- package/dist/server.js +720 -0
- package/package.json +91 -0
- package/server.ts +415 -0
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@netrojs/fnetro",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Full-stack Hono framework — SSR, SPA, Vue-like reactivity, route groups, middleware and API routes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "MD Ashikur Rahman<info@netrosolutions.com>",
|
|
8
|
+
"homepage": "https://github.com/netrosolutions/fnetro",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/netrosolutions/fnetro.git",
|
|
12
|
+
"directory": "packages/fnetro"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/netrosolutions/fnetro/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"fnetro",
|
|
19
|
+
"hono",
|
|
20
|
+
"framework",
|
|
21
|
+
"fullstack",
|
|
22
|
+
"ssr",
|
|
23
|
+
"spa",
|
|
24
|
+
"reactivity",
|
|
25
|
+
"signals",
|
|
26
|
+
"jsx",
|
|
27
|
+
"tsx",
|
|
28
|
+
"node",
|
|
29
|
+
"bun",
|
|
30
|
+
"deno",
|
|
31
|
+
"cloudflare-workers",
|
|
32
|
+
"edge"
|
|
33
|
+
],
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/core.d.ts",
|
|
37
|
+
"import": "./dist/core.js"
|
|
38
|
+
},
|
|
39
|
+
"./core": {
|
|
40
|
+
"types": "./dist/core.d.ts",
|
|
41
|
+
"import": "./dist/core.js"
|
|
42
|
+
},
|
|
43
|
+
"./server": {
|
|
44
|
+
"types": "./dist/server.d.ts",
|
|
45
|
+
"import": "./dist/server.js"
|
|
46
|
+
},
|
|
47
|
+
"./client": {
|
|
48
|
+
"types": "./dist/client.d.ts",
|
|
49
|
+
"import": "./dist/client.js"
|
|
50
|
+
},
|
|
51
|
+
"./vite": {
|
|
52
|
+
"types": "./dist/server.d.ts",
|
|
53
|
+
"import": "./dist/server.js"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"main": "./dist/core.js",
|
|
57
|
+
"types": "./dist/core.d.ts",
|
|
58
|
+
"files": [
|
|
59
|
+
"dist",
|
|
60
|
+
"core.ts",
|
|
61
|
+
"server.ts",
|
|
62
|
+
"client.ts",
|
|
63
|
+
"README.md",
|
|
64
|
+
"LICENSE"
|
|
65
|
+
],
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsup",
|
|
68
|
+
"build:watch": "tsup --watch",
|
|
69
|
+
"typecheck": "tsc --noEmit",
|
|
70
|
+
"clean": "rimraf dist",
|
|
71
|
+
"prepublishOnly": "npm run clean && npm run build && npm run typecheck"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"hono": ">=4.0.0",
|
|
75
|
+
"vite": ">=5.0.0"
|
|
76
|
+
},
|
|
77
|
+
"peerDependenciesMeta": {
|
|
78
|
+
"vite": {
|
|
79
|
+
"optional": true
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@hono/node-server": "^1.19.11",
|
|
84
|
+
"@types/node": "^25.5.0",
|
|
85
|
+
"hono": "^4.12.8",
|
|
86
|
+
"rimraf": "^6.1.3",
|
|
87
|
+
"tsup": "^8.5.1",
|
|
88
|
+
"typescript": "^5.9.3",
|
|
89
|
+
"vite": "^8.0.0"
|
|
90
|
+
}
|
|
91
|
+
}
|
package/server.ts
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
// FNetro · server.ts
|
|
3
|
+
// Hono server integration · SSR renderer · Vite plugin (dual-build)
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import { Hono } from 'hono'
|
|
7
|
+
import { jsx } from 'hono/jsx'
|
|
8
|
+
import { renderToString } from 'hono/jsx/dom/server'
|
|
9
|
+
import {
|
|
10
|
+
resolveRoutes,
|
|
11
|
+
SPA_HEADER, STATE_KEY, PARAMS_KEY,
|
|
12
|
+
type AppConfig, type ResolvedRoute, type LayoutDef,
|
|
13
|
+
type PageDef, type ApiRouteDef,
|
|
14
|
+
} from './core'
|
|
15
|
+
import type { Plugin, InlineConfig } from 'vite'
|
|
16
|
+
import type { MiddlewareHandler } from 'hono'
|
|
17
|
+
|
|
18
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
19
|
+
// § 1 Path matching
|
|
20
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
21
|
+
|
|
22
|
+
interface CompiledPath {
|
|
23
|
+
re: RegExp
|
|
24
|
+
keys: string[]
|
|
25
|
+
original: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compilePath(path: string): CompiledPath {
|
|
29
|
+
const keys: string[] = []
|
|
30
|
+
const src = path
|
|
31
|
+
.replace(/\[\.\.\.([^\]]+)\]/g, (_: string, k: string) => { keys.push(k); return '(.*)' }) // [...slug]
|
|
32
|
+
.replace(/\[([^\]]+)\]/g, (_: string, k: string) => { keys.push(k); return '([^/]+)' }) // [id]
|
|
33
|
+
.replace(/\*/g, '(.*)')
|
|
34
|
+
return { re: new RegExp(`^${src}$`), keys, original: path }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function matchPath(compiled: CompiledPath, pathname: string): Record<string, string> | null {
|
|
38
|
+
const m = pathname.match(compiled.re)
|
|
39
|
+
if (!m) return null
|
|
40
|
+
const params: Record<string, string> = {}
|
|
41
|
+
compiled.keys.forEach((k, i) => { params[k] = decodeURIComponent(m[i + 1]) })
|
|
42
|
+
return params
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
46
|
+
// § 2 SSR Renderer
|
|
47
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
48
|
+
// § 2 SSR Renderer
|
|
49
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
50
|
+
|
|
51
|
+
/** Build the outer HTML shell as a plain string — faster than JSX for static structure */
|
|
52
|
+
function buildShell(opts: {
|
|
53
|
+
title: string
|
|
54
|
+
stateJson: string
|
|
55
|
+
paramsJson: string
|
|
56
|
+
pageHtml: string
|
|
57
|
+
}): string {
|
|
58
|
+
return `<!DOCTYPE html>
|
|
59
|
+
<html lang="en">
|
|
60
|
+
<head>
|
|
61
|
+
<meta charset="UTF-8">
|
|
62
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
63
|
+
<title>${escHtml(opts.title)}</title>
|
|
64
|
+
<link rel="stylesheet" href="/assets/style.css">
|
|
65
|
+
</head>
|
|
66
|
+
<body>
|
|
67
|
+
<div id="fnetro-app">${opts.pageHtml}</div>
|
|
68
|
+
<script>window.${STATE_KEY}=${opts.stateJson};window.${PARAMS_KEY}=${opts.paramsJson};</script>
|
|
69
|
+
<script type="module" src="/assets/client.js"></script>
|
|
70
|
+
</body>
|
|
71
|
+
</html>`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function escHtml(s: string): string {
|
|
75
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function renderInner(
|
|
79
|
+
route: ResolvedRoute,
|
|
80
|
+
data: object,
|
|
81
|
+
url: string,
|
|
82
|
+
params: Record<string, string>,
|
|
83
|
+
appLayout: LayoutDef | undefined
|
|
84
|
+
): Promise<string> {
|
|
85
|
+
const pageNode = (jsx as any)(route.page.Page, { ...data, url, params })
|
|
86
|
+
|
|
87
|
+
const layout = route.layout !== undefined ? route.layout : appLayout
|
|
88
|
+
const wrapped = layout
|
|
89
|
+
? (jsx as any)(layout.Component, { url, params, children: pageNode })
|
|
90
|
+
: pageNode
|
|
91
|
+
|
|
92
|
+
return renderToString(wrapped as any)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function renderFullPage(
|
|
96
|
+
route: ResolvedRoute,
|
|
97
|
+
data: object,
|
|
98
|
+
url: string,
|
|
99
|
+
params: Record<string, string>,
|
|
100
|
+
appLayout: LayoutDef | undefined,
|
|
101
|
+
title = 'FNetro'
|
|
102
|
+
): Promise<string> {
|
|
103
|
+
const pageHtml = await renderInner(route, data, url, params, appLayout)
|
|
104
|
+
return buildShell({
|
|
105
|
+
title,
|
|
106
|
+
stateJson: JSON.stringify({ [url]: data }),
|
|
107
|
+
paramsJson: JSON.stringify(params),
|
|
108
|
+
pageHtml,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
113
|
+
// § 3 createFNetro — assemble the Hono app
|
|
114
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
115
|
+
|
|
116
|
+
export interface FNetroApp {
|
|
117
|
+
/** The underlying Hono instance — add raw routes, custom error handlers, etc. */
|
|
118
|
+
app: Hono
|
|
119
|
+
/** Hono fetch handler — export this as default for edge runtimes */
|
|
120
|
+
handler: Hono['fetch']
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function createFNetro(config: AppConfig): FNetroApp {
|
|
124
|
+
const app = new Hono()
|
|
125
|
+
|
|
126
|
+
// Static assets
|
|
127
|
+
app.use('/assets/*', async (c, next) => {
|
|
128
|
+
// In production served by Vite build output; delegate to next in dev
|
|
129
|
+
await next()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Global middleware
|
|
133
|
+
;(config.middleware ?? []).forEach(mw => app.use('*', mw))
|
|
134
|
+
|
|
135
|
+
// Resolve all routes
|
|
136
|
+
const { pages, apis } = resolveRoutes(config.routes, {
|
|
137
|
+
layout: config.layout,
|
|
138
|
+
middleware: [],
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// Pre-compile paths
|
|
142
|
+
const compiled = pages.map(r => ({
|
|
143
|
+
route: r,
|
|
144
|
+
compiled: compilePath(r.fullPath),
|
|
145
|
+
}))
|
|
146
|
+
|
|
147
|
+
// Register API routes
|
|
148
|
+
apis.forEach(api => {
|
|
149
|
+
const sub = new Hono()
|
|
150
|
+
api.register(sub, config.middleware ?? [])
|
|
151
|
+
app.route(api.path, sub)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Page handler (catch-all, after API routes)
|
|
155
|
+
app.all('*', async (c) => {
|
|
156
|
+
const url = new URL(c.req.url)
|
|
157
|
+
const pathname = url.pathname
|
|
158
|
+
const isSPA = c.req.header(SPA_HEADER) === '1'
|
|
159
|
+
|
|
160
|
+
// Find matching page
|
|
161
|
+
let matched: { route: ResolvedRoute; params: Record<string, string> } | null = null
|
|
162
|
+
for (const { route, compiled: cp } of compiled) {
|
|
163
|
+
const params = matchPath(cp, pathname)
|
|
164
|
+
if (params !== null) {
|
|
165
|
+
matched = { route, params }
|
|
166
|
+
break
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!matched) {
|
|
171
|
+
if (config.notFound) {
|
|
172
|
+
const html = await renderToString(jsx(config.notFound as any, {}))
|
|
173
|
+
return c.html(`<!DOCTYPE html><html><body>${html}</body></html>`, 404)
|
|
174
|
+
}
|
|
175
|
+
return c.text('Not Found', 404)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const { route, params } = matched
|
|
179
|
+
|
|
180
|
+
// Expose params via c.req — patch temporarily
|
|
181
|
+
const origParam = c.req.param.bind(c.req)
|
|
182
|
+
;(c.req as any).param = (key?: string) =>
|
|
183
|
+
key ? (params[key] ?? origParam(key)) : { ...params, ...origParam() }
|
|
184
|
+
|
|
185
|
+
// Run route-level middleware chain (mirrors Hono's own onion model)
|
|
186
|
+
let earlyResponse: Response | undefined
|
|
187
|
+
const handlers = [...route.middleware]
|
|
188
|
+
let idx = 0
|
|
189
|
+
|
|
190
|
+
const runMiddleware = async (): Promise<void> => {
|
|
191
|
+
const mw = handlers[idx++]
|
|
192
|
+
if (!mw) return
|
|
193
|
+
const res = await mw(c, runMiddleware)
|
|
194
|
+
// If middleware returned a Response and didn't call next(), use it
|
|
195
|
+
if (res instanceof Response && !earlyResponse) earlyResponse = res
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await runMiddleware()
|
|
199
|
+
|
|
200
|
+
if (earlyResponse) return earlyResponse
|
|
201
|
+
|
|
202
|
+
// Run loader
|
|
203
|
+
const data = route.page.loader ? await route.page.loader(c) : {}
|
|
204
|
+
const safeData = data ?? {}
|
|
205
|
+
|
|
206
|
+
if (isSPA) {
|
|
207
|
+
// SPA navigation — return JSON
|
|
208
|
+
const html = await renderInner(route, safeData, pathname, params, config.layout)
|
|
209
|
+
return c.json({
|
|
210
|
+
html,
|
|
211
|
+
state: safeData,
|
|
212
|
+
params,
|
|
213
|
+
url: pathname,
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Full SSR
|
|
218
|
+
const fullHtml = await renderFullPage(route, safeData, pathname, params, config.layout)
|
|
219
|
+
return c.html(fullHtml)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
return { app, handler: app.fetch }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
226
|
+
// § 4 Universal serve() — auto-detects Node / Bun / Deno / edge
|
|
227
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
228
|
+
|
|
229
|
+
export type Runtime = 'node' | 'bun' | 'deno' | 'edge' | 'unknown'
|
|
230
|
+
|
|
231
|
+
export function detectRuntime(): Runtime {
|
|
232
|
+
if (typeof (globalThis as any).Bun !== 'undefined') return 'bun'
|
|
233
|
+
if (typeof (globalThis as any).Deno !== 'undefined') return 'deno'
|
|
234
|
+
if (typeof process !== 'undefined' && process.versions?.node) return 'node'
|
|
235
|
+
return 'edge'
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface ServeOptions {
|
|
239
|
+
app: FNetroApp
|
|
240
|
+
port?: number
|
|
241
|
+
hostname?: string
|
|
242
|
+
/** Override auto-detected runtime. */
|
|
243
|
+
runtime?: Runtime
|
|
244
|
+
/** Static assets root directory (served at /assets/*). @default './dist' */
|
|
245
|
+
staticDir?: string
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function serve(opts: ServeOptions): Promise<void> {
|
|
249
|
+
const runtime = opts.runtime ?? detectRuntime()
|
|
250
|
+
const port = opts.port ?? Number((globalThis as any).process?.env?.PORT ?? 3000)
|
|
251
|
+
const hostname = opts.hostname ?? '0.0.0.0'
|
|
252
|
+
const staticDir = opts.staticDir ?? './dist'
|
|
253
|
+
const addr = `http://${hostname === '0.0.0.0' ? 'localhost' : hostname}:${port}`
|
|
254
|
+
const logReady = () => console.log(`\n🔥 FNetro [${runtime}] ready → ${addr}\n`)
|
|
255
|
+
|
|
256
|
+
switch (runtime) {
|
|
257
|
+
case 'node': {
|
|
258
|
+
const [{ serve: nodeServe }, { serveStatic }] = await Promise.all([
|
|
259
|
+
import('@hono/node-server'),
|
|
260
|
+
import('@hono/node-server/serve-static'),
|
|
261
|
+
])
|
|
262
|
+
opts.app.app.use('/assets/*', serveStatic({ root: staticDir }))
|
|
263
|
+
nodeServe({ fetch: opts.app.handler, port, hostname })
|
|
264
|
+
logReady()
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
case 'bun': {
|
|
268
|
+
;(globalThis as any).Bun.serve({ fetch: opts.app.handler, port, hostname })
|
|
269
|
+
logReady()
|
|
270
|
+
break
|
|
271
|
+
}
|
|
272
|
+
case 'deno': {
|
|
273
|
+
;(globalThis as any).Deno.serve({ port, hostname }, opts.app.handler)
|
|
274
|
+
logReady()
|
|
275
|
+
break
|
|
276
|
+
}
|
|
277
|
+
default:
|
|
278
|
+
console.warn('[fnetro] serve() is a no-op on edge runtimes. Export `app.handler` instead.')
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
283
|
+
// § 5 Vite plugin — automatic dual build (server + client)
|
|
284
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
285
|
+
|
|
286
|
+
export interface FNetroPluginOptions {
|
|
287
|
+
/**
|
|
288
|
+
* Server entry file (exports the Hono app / calls serve()).
|
|
289
|
+
* @default 'app/server.ts'
|
|
290
|
+
*/
|
|
291
|
+
serverEntry?: string
|
|
292
|
+
/**
|
|
293
|
+
* Client entry file (calls boot()).
|
|
294
|
+
* @default 'app/client.ts'
|
|
295
|
+
*/
|
|
296
|
+
clientEntry?: string
|
|
297
|
+
/**
|
|
298
|
+
* Output directory for the server bundle.
|
|
299
|
+
* @default 'dist/server'
|
|
300
|
+
*/
|
|
301
|
+
serverOutDir?: string
|
|
302
|
+
/**
|
|
303
|
+
* Output directory for client assets (JS, CSS).
|
|
304
|
+
* @default 'dist/assets'
|
|
305
|
+
*/
|
|
306
|
+
clientOutDir?: string
|
|
307
|
+
/**
|
|
308
|
+
* External packages for the server bundle.
|
|
309
|
+
* Node built-ins are always external.
|
|
310
|
+
*/
|
|
311
|
+
serverExternal?: string[]
|
|
312
|
+
/**
|
|
313
|
+
* Emit type declarations for framework types.
|
|
314
|
+
* @default false
|
|
315
|
+
*/
|
|
316
|
+
dts?: boolean
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const NODE_BUILTINS = /^node:|^(assert|buffer|child_process|cluster|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|perf_hooks|process|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|trace_events|tty|url|util|v8|vm|worker_threads|zlib)$/
|
|
320
|
+
|
|
321
|
+
export function fnetroVitePlugin(opts: FNetroPluginOptions = {}): Plugin[] {
|
|
322
|
+
const {
|
|
323
|
+
serverEntry = 'app/server.ts',
|
|
324
|
+
clientEntry = 'app/client.ts',
|
|
325
|
+
serverOutDir = 'dist/server',
|
|
326
|
+
clientOutDir = 'dist/assets',
|
|
327
|
+
serverExternal = [],
|
|
328
|
+
} = opts
|
|
329
|
+
|
|
330
|
+
let isServerBuild = true // first pass = server
|
|
331
|
+
|
|
332
|
+
const sharedEsbuild = {
|
|
333
|
+
jsx: 'automatic' as const,
|
|
334
|
+
jsxImportSource: 'hono/jsx',
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Common JSX transform for all .tsx files
|
|
338
|
+
const jsxPlugin: Plugin = {
|
|
339
|
+
name: 'fnetro:jsx',
|
|
340
|
+
config: () => ({ esbuild: sharedEsbuild }),
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Server build plugin
|
|
344
|
+
const serverPlugin: Plugin = {
|
|
345
|
+
name: 'fnetro:server',
|
|
346
|
+
apply: 'build',
|
|
347
|
+
enforce: 'pre',
|
|
348
|
+
|
|
349
|
+
config() {
|
|
350
|
+
// No alias needed: hono/jsx and hono/jsx/dom produce compatible nodes.
|
|
351
|
+
// renderToString (server) and render() (client) both accept them.
|
|
352
|
+
return {
|
|
353
|
+
build: {
|
|
354
|
+
outDir: serverOutDir,
|
|
355
|
+
ssr: true,
|
|
356
|
+
target: 'node18',
|
|
357
|
+
lib: {
|
|
358
|
+
entry: serverEntry,
|
|
359
|
+
formats: ['es'],
|
|
360
|
+
fileName: 'server',
|
|
361
|
+
},
|
|
362
|
+
rollupOptions: {
|
|
363
|
+
external: (id: string) =>
|
|
364
|
+
NODE_BUILTINS.test(id) ||
|
|
365
|
+
id === '@hono/node-server' ||
|
|
366
|
+
serverExternal.includes(id),
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
esbuild: sharedEsbuild,
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
async closeBundle() {
|
|
374
|
+
console.log('\n⚡ FNetro: building client bundle…\n')
|
|
375
|
+
|
|
376
|
+
const { build } = await import('vite')
|
|
377
|
+
await build({
|
|
378
|
+
configFile: false,
|
|
379
|
+
esbuild: sharedEsbuild,
|
|
380
|
+
build: {
|
|
381
|
+
outDir: clientOutDir,
|
|
382
|
+
lib: {
|
|
383
|
+
entry: clientEntry,
|
|
384
|
+
formats: ['es'],
|
|
385
|
+
fileName: 'client',
|
|
386
|
+
},
|
|
387
|
+
rollupOptions: {
|
|
388
|
+
output: { entryFileNames: '[name].js' },
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
} satisfies InlineConfig)
|
|
392
|
+
|
|
393
|
+
console.log('\n✅ FNetro: both bundles ready\n')
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return [jsxPlugin, serverPlugin]
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
401
|
+
// § 6 Re-export core for convenience when only server.ts is imported
|
|
402
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
403
|
+
export {
|
|
404
|
+
definePage, defineGroup, defineLayout, defineMiddleware, defineApiRoute,
|
|
405
|
+
ref, shallowRef, reactive, shallowReactive, readonly,
|
|
406
|
+
computed, effect, watch, watchEffect, effectScope,
|
|
407
|
+
toRef, toRefs, unref, isRef, isReactive, isReadonly, markRaw, toRaw,
|
|
408
|
+
triggerRef, use, useLocalRef, useLocalReactive,
|
|
409
|
+
SPA_HEADER, STATE_KEY,
|
|
410
|
+
} from './core'
|
|
411
|
+
export type {
|
|
412
|
+
AppConfig, PageDef, GroupDef, LayoutDef, ApiRouteDef, MiddlewareDef,
|
|
413
|
+
Ref, ComputedRef, WritableComputedRef, WatchSource, WatchOptions,
|
|
414
|
+
LoaderCtx, FNetroMiddleware, AnyJSX,
|
|
415
|
+
} from './core'
|