@nifrajs/web 0.1.0-alpha.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.
- package/LICENSE +21 -0
- package/README.md +18 -0
- package/css-modules.d.ts +34 -0
- package/dist/build.d.ts +110 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +238 -0
- package/dist/build.js.map +1 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +204 -0
- package/dist/client.js.map +1 -0
- package/dist/deferred.d.ts +90 -0
- package/dist/deferred.d.ts.map +1 -0
- package/dist/deferred.js +310 -0
- package/dist/deferred.js.map +1 -0
- package/dist/dev-error.d.ts +17 -0
- package/dist/dev-error.d.ts.map +1 -0
- package/dist/dev-error.js +59 -0
- package/dist/dev-error.js.map +1 -0
- package/dist/dev.d.ts +25 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +83 -0
- package/dist/dev.js.map +1 -0
- package/dist/draft.d.ts +58 -0
- package/dist/draft.d.ts.map +1 -0
- package/dist/draft.js +55 -0
- package/dist/draft.js.map +1 -0
- package/dist/fonts-google.d.ts +109 -0
- package/dist/fonts-google.d.ts.map +1 -0
- package/dist/fonts-google.js +291 -0
- package/dist/fonts-google.js.map +1 -0
- package/dist/fonts.d.ts +67 -0
- package/dist/fonts.d.ts.map +1 -0
- package/dist/fonts.js +103 -0
- package/dist/fonts.js.map +1 -0
- package/dist/fs.d.ts +10 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +24 -0
- package/dist/fs.js.map +1 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +784 -0
- package/dist/index.js.map +1 -0
- package/dist/islands.d.ts +37 -0
- package/dist/islands.d.ts.map +1 -0
- package/dist/islands.js +107 -0
- package/dist/islands.js.map +1 -0
- package/dist/isr.d.ts +154 -0
- package/dist/isr.d.ts.map +1 -0
- package/dist/isr.js +331 -0
- package/dist/isr.js.map +1 -0
- package/dist/manifest.d.ts +178 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +213 -0
- package/dist/manifest.js.map +1 -0
- package/dist/plugins/css-modules.d.ts +40 -0
- package/dist/plugins/css-modules.d.ts.map +1 -0
- package/dist/plugins/css-modules.js +0 -0
- package/dist/plugins/css-modules.js.map +1 -0
- package/dist/plugins/kit.d.ts +44 -0
- package/dist/plugins/kit.d.ts.map +1 -0
- package/dist/plugins/kit.js +105 -0
- package/dist/plugins/kit.js.map +1 -0
- package/dist/plugins/scss.d.ts +27 -0
- package/dist/plugins/scss.d.ts.map +1 -0
- package/dist/plugins/scss.js +78 -0
- package/dist/plugins/scss.js.map +1 -0
- package/dist/prerender.d.ts +77 -0
- package/dist/prerender.d.ts.map +1 -0
- package/dist/prerender.js +139 -0
- package/dist/prerender.js.map +1 -0
- package/dist/query.d.ts +62 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +184 -0
- package/dist/query.js.map +1 -0
- package/dist/router.d.ts +169 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +531 -0
- package/dist/router.js.map +1 -0
- package/dist/vite.d.ts +49 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +144 -0
- package/dist/vite.js.map +1 -0
- package/package.json +127 -0
- package/src/build.ts +375 -0
- package/src/client.ts +218 -0
- package/src/deferred.ts +339 -0
- package/src/dev-error.ts +62 -0
- package/src/dev.ts +108 -0
- package/src/draft.ts +82 -0
- package/src/fonts-google.ts +397 -0
- package/src/fonts.ts +147 -0
- package/src/fs.ts +26 -0
- package/src/index.ts +1202 -0
- package/src/islands.ts +121 -0
- package/src/isr.ts +461 -0
- package/src/manifest.ts +366 -0
- package/src/plugins/css-modules.ts +0 -0
- package/src/plugins/kit.ts +123 -0
- package/src/plugins/scss.ts +108 -0
- package/src/prerender.ts +209 -0
- package/src/query.ts +250 -0
- package/src/router.ts +676 -0
- package/src/vite.ts +212 -0
package/src/vite.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web/vite` — a **dev-only** server with true HMR, backed by Vite in middleware mode. nifra's
|
|
3
|
+
* production pipeline stays Bun-native (`@nifrajs/web/build` → `Bun.build`); this is purely for the dev
|
|
4
|
+
* loop. Bun's own HMR is dev-server-only and DCE's `import.meta.hot` under `Bun.build` (nifra's bundler),
|
|
5
|
+
* so HMR comes from Vite + the framework's official Vite plugin (React Fast Refresh, Vue/Svelte/Solid
|
|
6
|
+
* HMR) — which you inject via `plugins` (the same structural-injection idiom as the Bun/codec plugins).
|
|
7
|
+
*
|
|
8
|
+
* Flow: Vite serves + HMR-swaps the client modules (nifra's codegen'd entry + the route files); nifra
|
|
9
|
+
* still **SSRs** each request, and the rendered HTML is run through `vite.transformIndexHtml` so Vite
|
|
10
|
+
* injects its HMR client + the framework's refresh preamble. Node `http` (not `Bun.serve`) because
|
|
11
|
+
* Vite's `middlewares` are Connect-style — it runs fine under Bun.
|
|
12
|
+
*/
|
|
13
|
+
import { writeFileSync } from "node:fs"
|
|
14
|
+
import {
|
|
15
|
+
createServer as createHttpServer,
|
|
16
|
+
type IncomingMessage,
|
|
17
|
+
type ServerResponse,
|
|
18
|
+
} from "node:http"
|
|
19
|
+
import { relative, resolve as resolvePath } from "node:path"
|
|
20
|
+
import { renderDevErrorOverlay } from "./dev-error.ts"
|
|
21
|
+
import { discoverRoutes } from "./fs.ts"
|
|
22
|
+
import { generateClientEntry } from "./index.ts"
|
|
23
|
+
|
|
24
|
+
/** Minimal app surface — `createWebApp(...)` satisfies it. */
|
|
25
|
+
interface FetchApp {
|
|
26
|
+
fetch(request: Request): Response | Promise<Response>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Structural slice of the Vite dev server this module drives (avoids a hard type dep on `vite`).
|
|
30
|
+
interface ViteLike {
|
|
31
|
+
readonly middlewares: (req: IncomingMessage, res: ServerResponse, next: () => void) => void
|
|
32
|
+
transformIndexHtml(url: string, html: string): Promise<string>
|
|
33
|
+
ssrFixStacktrace(err: Error): void
|
|
34
|
+
readonly watcher: { on(event: "change", cb: (path: string) => void): void }
|
|
35
|
+
close(): Promise<void>
|
|
36
|
+
}
|
|
37
|
+
interface ViteModule {
|
|
38
|
+
createServer(config: Record<string, unknown>): Promise<ViteLike>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// `node:http` server type the request handler runs on (also the HMR WebSocket host — see below).
|
|
42
|
+
type NodeHttpServer = ReturnType<typeof createHttpServer>
|
|
43
|
+
|
|
44
|
+
export interface ViteDevServerOptions {
|
|
45
|
+
/** Absolute (or cwd-relative) path to the `routes/` dir. */
|
|
46
|
+
readonly routesDir: string
|
|
47
|
+
/** Client runtime module providing `mountRouter` (e.g. `"@nifrajs/web-react/client"`). */
|
|
48
|
+
readonly clientModule: string
|
|
49
|
+
/**
|
|
50
|
+
* Build the nifra app for the given dev client-entry URL. `importQuery` changes when a route file
|
|
51
|
+
* changes — thread it into `discoverRoutes(routesDir, { importQuery })` so a hard reload SSRs edited
|
|
52
|
+
* modules (HMR handles the live client update; this keeps server-render fresh).
|
|
53
|
+
*/
|
|
54
|
+
readonly createApp: (clientEntry: string, importQuery: string) => FetchApp | Promise<FetchApp>
|
|
55
|
+
/** Vite plugins — inject your framework's official plugin, e.g. `[react()]`. */
|
|
56
|
+
readonly plugins?: readonly unknown[]
|
|
57
|
+
/**
|
|
58
|
+
* Extra `resolve.conditions` prepended ahead of nifra's defaults — some frameworks need their own
|
|
59
|
+
* (e.g. Solid's `"solid"` condition routes `solid-js` to its source/JSX-dev build).
|
|
60
|
+
*/
|
|
61
|
+
readonly conditions?: readonly string[]
|
|
62
|
+
/**
|
|
63
|
+
* Compile-time `define` replacements (e.g. Vue's `__VUE_OPTIONS_API__` flags). Vite already sets
|
|
64
|
+
* `process.env.NODE_ENV` in dev; this is for framework feature flags the plugin doesn't inject.
|
|
65
|
+
*/
|
|
66
|
+
readonly define?: Readonly<Record<string, string>>
|
|
67
|
+
/** Vite project root (default `process.cwd()`). */
|
|
68
|
+
readonly root?: string
|
|
69
|
+
/** Port (default 3000). */
|
|
70
|
+
readonly port?: number
|
|
71
|
+
/**
|
|
72
|
+
* Use polling for the file watcher. Native fs events (fsevents/inotify) are unreliable inside
|
|
73
|
+
* containers, networked filesystems, and some sandboxes — there, HMR silently never fires. Set
|
|
74
|
+
* `true` (or the env var `CHOKIDAR_USEPOLLING=1`) to poll instead. Default: off (native events).
|
|
75
|
+
*/
|
|
76
|
+
readonly poll?: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ViteDevServer {
|
|
80
|
+
readonly port: number
|
|
81
|
+
stop(): Promise<void>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The codegen'd client entry is written here (at the Vite root) so Vite serves + HMRs it.
|
|
85
|
+
const DEV_ENTRY = ".nifra-vite-entry.tsx"
|
|
86
|
+
|
|
87
|
+
const readNodeBody = async (req: IncomingMessage): Promise<Buffer | undefined> => {
|
|
88
|
+
if (req.method === "GET" || req.method === "HEAD") return undefined
|
|
89
|
+
const chunks: Buffer[] = []
|
|
90
|
+
for await (const chunk of req) chunks.push(chunk as Buffer)
|
|
91
|
+
return Buffer.concat(chunks)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Build a Web `Request` from a Node `IncomingMessage` (+ already-read body) for nifra's `app.fetch`. */
|
|
95
|
+
function toWebRequest(req: IncomingMessage, body: Buffer | undefined): Request {
|
|
96
|
+
const headers = new Headers()
|
|
97
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
98
|
+
if (typeof value === "string") headers.set(key, value)
|
|
99
|
+
else if (Array.isArray(value)) for (const v of value) headers.append(key, v)
|
|
100
|
+
}
|
|
101
|
+
const url = `http://${req.headers.host ?? "localhost"}${req.url ?? "/"}`
|
|
102
|
+
// Assemble init via an `unknown`-typed `body` slot: a Buffer is a valid request body at runtime
|
|
103
|
+
// (Bun/undici), but the named `BodyInit` type only exists under the DOM lib (the root program is
|
|
104
|
+
// DOM-free). `ConstructorParameters<typeof Request>[1]` derives the init type from whichever lib is
|
|
105
|
+
// active, so this type-checks in both the bun and DOM tsconfig programs.
|
|
106
|
+
const init: { method: string; headers: Headers; body?: unknown } = {
|
|
107
|
+
method: req.method ?? "GET",
|
|
108
|
+
headers,
|
|
109
|
+
}
|
|
110
|
+
if (body !== undefined) init.body = body
|
|
111
|
+
return new Request(url, init as unknown as ConstructorParameters<typeof Request>[1])
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Start the Vite-backed dev server: Vite serves/HMRs the client; nifra SSRs each request and Vite
|
|
116
|
+
* injects its HMR client + the framework refresh preamble via `transformIndexHtml`.
|
|
117
|
+
*/
|
|
118
|
+
export async function createViteDevServer(options: ViteDevServerOptions): Promise<ViteDevServer> {
|
|
119
|
+
const root = resolvePath(options.root ?? process.cwd())
|
|
120
|
+
const routesDir = resolvePath(options.routesDir)
|
|
121
|
+
const port = options.port ?? 3000
|
|
122
|
+
|
|
123
|
+
// Codegen the client entry with Vite-servable, root-relative specifiers (e.g. `/routes/index.tsx`).
|
|
124
|
+
const manifest = discoverRoutes(routesDir)
|
|
125
|
+
const toUrl = (file: string): string => `/${relative(root, `${routesDir}/${file}`)}`
|
|
126
|
+
writeFileSync(
|
|
127
|
+
resolvePath(root, DEV_ENTRY),
|
|
128
|
+
generateClientEntry(manifest, { clientModule: options.clientModule, resolve: toUrl }),
|
|
129
|
+
)
|
|
130
|
+
const entryUrl = `/${DEV_ENTRY}`
|
|
131
|
+
|
|
132
|
+
// Create our HTTP server FIRST, then hand it to Vite as the HMR WebSocket host (`hmr.server`). In
|
|
133
|
+
// middleware mode Vite would otherwise open a *separate* ws port (24678) — fragile: it conflicts
|
|
134
|
+
// across restarts ("Port undefined is already in use" → the client never connects). Sharing one
|
|
135
|
+
// port means HMR rides the same origin as the app, robust across restarts. The handler closes over
|
|
136
|
+
// `vite`, which is assigned just below before the server starts listening.
|
|
137
|
+
let vite!: ViteLike
|
|
138
|
+
let version = 0
|
|
139
|
+
let app: FetchApp
|
|
140
|
+
|
|
141
|
+
const server: NodeHttpServer = createHttpServer((req, res) => {
|
|
142
|
+
vite.middlewares(req, res, () => {
|
|
143
|
+
// Not a Vite asset → nifra SSR. (`next` runs after Vite declines, so the body is still readable.)
|
|
144
|
+
void (async () => {
|
|
145
|
+
try {
|
|
146
|
+
const body = await readNodeBody(req)
|
|
147
|
+
const nifraRes = await app.fetch(toWebRequest(req, body))
|
|
148
|
+
const contentType = nifraRes.headers.get("content-type") ?? ""
|
|
149
|
+
res.statusCode = nifraRes.status
|
|
150
|
+
if (!contentType.includes("text/html")) {
|
|
151
|
+
// Data / redirect / asset response — pass through untouched.
|
|
152
|
+
for (const [key, value] of nifraRes.headers) res.setHeader(key, value)
|
|
153
|
+
res.end(Buffer.from(await nifraRes.arrayBuffer()))
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
// Inject Vite's HMR client + the framework's refresh preamble into the SSR'd HTML.
|
|
157
|
+
const html = await vite.transformIndexHtml(req.url ?? "/", await nifraRes.text())
|
|
158
|
+
res.setHeader("content-type", "text/html; charset=utf-8")
|
|
159
|
+
res.end(html)
|
|
160
|
+
} catch (err) {
|
|
161
|
+
// Source-map the stack first (Vite maps the bundled frames back to your `.ts`), then render
|
|
162
|
+
// the readable dev overlay instead of a bare text dump. Dev-only — production maps to `_error`.
|
|
163
|
+
if (err instanceof Error) vite.ssrFixStacktrace(err)
|
|
164
|
+
res.statusCode = 500
|
|
165
|
+
res.setHeader("content-type", "text/html; charset=utf-8")
|
|
166
|
+
res.end(renderDevErrorOverlay(err, { method: req.method ?? "GET", url: req.url ?? "/" }))
|
|
167
|
+
}
|
|
168
|
+
})()
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// `conditions: ["bun"]` makes Vite resolve nifra's workspace packages (`@nifrajs/web-react/client`, …) to
|
|
173
|
+
// their TS **source** — so the dev server needs no prior `dist` build of the adapter packages.
|
|
174
|
+
const usePolling = options.poll ?? process.env.CHOKIDAR_USEPOLLING === "1"
|
|
175
|
+
const { createServer } = (await import("vite")) as unknown as ViteModule
|
|
176
|
+
vite = await createServer({
|
|
177
|
+
root,
|
|
178
|
+
appType: "custom",
|
|
179
|
+
server: {
|
|
180
|
+
middlewareMode: true,
|
|
181
|
+
hmr: { server },
|
|
182
|
+
// Explicit watch config; poll when native fs events aren't delivered (containers/sandboxes).
|
|
183
|
+
watch: usePolling ? { usePolling: true, interval: 80 } : {},
|
|
184
|
+
},
|
|
185
|
+
plugins: [...(options.plugins ?? [])],
|
|
186
|
+
resolve: {
|
|
187
|
+
conditions: [...(options.conditions ?? []), "bun", "module", "browser", "development"],
|
|
188
|
+
},
|
|
189
|
+
...(options.define ? { define: options.define } : {}),
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
app = await options.createApp(entryUrl, `v=${version}`)
|
|
193
|
+
|
|
194
|
+
// Keep server-render fresh on edits (Vite already HMRs the client live; this is for a hard reload).
|
|
195
|
+
vite.watcher.on("change", () => {
|
|
196
|
+
version += 1
|
|
197
|
+
Promise.resolve(options.createApp(entryUrl, `v=${version}`))
|
|
198
|
+
.then((next) => {
|
|
199
|
+
app = next
|
|
200
|
+
})
|
|
201
|
+
.catch((err) => console.error("[nifra/web/vite] app re-create failed:", err))
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
await new Promise<void>((done) => server.listen(port, done))
|
|
205
|
+
return {
|
|
206
|
+
port,
|
|
207
|
+
stop: async () => {
|
|
208
|
+
server.close()
|
|
209
|
+
await vite.close()
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
}
|