@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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/css-modules.d.ts +34 -0
  4. package/dist/build.d.ts +110 -0
  5. package/dist/build.d.ts.map +1 -0
  6. package/dist/build.js +238 -0
  7. package/dist/build.js.map +1 -0
  8. package/dist/client.d.ts +32 -0
  9. package/dist/client.d.ts.map +1 -0
  10. package/dist/client.js +204 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/deferred.d.ts +90 -0
  13. package/dist/deferred.d.ts.map +1 -0
  14. package/dist/deferred.js +310 -0
  15. package/dist/deferred.js.map +1 -0
  16. package/dist/dev-error.d.ts +17 -0
  17. package/dist/dev-error.d.ts.map +1 -0
  18. package/dist/dev-error.js +59 -0
  19. package/dist/dev-error.js.map +1 -0
  20. package/dist/dev.d.ts +25 -0
  21. package/dist/dev.d.ts.map +1 -0
  22. package/dist/dev.js +83 -0
  23. package/dist/dev.js.map +1 -0
  24. package/dist/draft.d.ts +58 -0
  25. package/dist/draft.d.ts.map +1 -0
  26. package/dist/draft.js +55 -0
  27. package/dist/draft.js.map +1 -0
  28. package/dist/fonts-google.d.ts +109 -0
  29. package/dist/fonts-google.d.ts.map +1 -0
  30. package/dist/fonts-google.js +291 -0
  31. package/dist/fonts-google.js.map +1 -0
  32. package/dist/fonts.d.ts +67 -0
  33. package/dist/fonts.d.ts.map +1 -0
  34. package/dist/fonts.js +103 -0
  35. package/dist/fonts.js.map +1 -0
  36. package/dist/fs.d.ts +10 -0
  37. package/dist/fs.d.ts.map +1 -0
  38. package/dist/fs.js +24 -0
  39. package/dist/fs.js.map +1 -0
  40. package/dist/index.d.ts +280 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +784 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/islands.d.ts +37 -0
  45. package/dist/islands.d.ts.map +1 -0
  46. package/dist/islands.js +107 -0
  47. package/dist/islands.js.map +1 -0
  48. package/dist/isr.d.ts +154 -0
  49. package/dist/isr.d.ts.map +1 -0
  50. package/dist/isr.js +331 -0
  51. package/dist/isr.js.map +1 -0
  52. package/dist/manifest.d.ts +178 -0
  53. package/dist/manifest.d.ts.map +1 -0
  54. package/dist/manifest.js +213 -0
  55. package/dist/manifest.js.map +1 -0
  56. package/dist/plugins/css-modules.d.ts +40 -0
  57. package/dist/plugins/css-modules.d.ts.map +1 -0
  58. package/dist/plugins/css-modules.js +0 -0
  59. package/dist/plugins/css-modules.js.map +1 -0
  60. package/dist/plugins/kit.d.ts +44 -0
  61. package/dist/plugins/kit.d.ts.map +1 -0
  62. package/dist/plugins/kit.js +105 -0
  63. package/dist/plugins/kit.js.map +1 -0
  64. package/dist/plugins/scss.d.ts +27 -0
  65. package/dist/plugins/scss.d.ts.map +1 -0
  66. package/dist/plugins/scss.js +78 -0
  67. package/dist/plugins/scss.js.map +1 -0
  68. package/dist/prerender.d.ts +77 -0
  69. package/dist/prerender.d.ts.map +1 -0
  70. package/dist/prerender.js +139 -0
  71. package/dist/prerender.js.map +1 -0
  72. package/dist/query.d.ts +62 -0
  73. package/dist/query.d.ts.map +1 -0
  74. package/dist/query.js +184 -0
  75. package/dist/query.js.map +1 -0
  76. package/dist/router.d.ts +169 -0
  77. package/dist/router.d.ts.map +1 -0
  78. package/dist/router.js +531 -0
  79. package/dist/router.js.map +1 -0
  80. package/dist/vite.d.ts +49 -0
  81. package/dist/vite.d.ts.map +1 -0
  82. package/dist/vite.js +144 -0
  83. package/dist/vite.js.map +1 -0
  84. package/package.json +127 -0
  85. package/src/build.ts +375 -0
  86. package/src/client.ts +218 -0
  87. package/src/deferred.ts +339 -0
  88. package/src/dev-error.ts +62 -0
  89. package/src/dev.ts +108 -0
  90. package/src/draft.ts +82 -0
  91. package/src/fonts-google.ts +397 -0
  92. package/src/fonts.ts +147 -0
  93. package/src/fs.ts +26 -0
  94. package/src/index.ts +1202 -0
  95. package/src/islands.ts +121 -0
  96. package/src/isr.ts +461 -0
  97. package/src/manifest.ts +366 -0
  98. package/src/plugins/css-modules.ts +0 -0
  99. package/src/plugins/kit.ts +123 -0
  100. package/src/plugins/scss.ts +108 -0
  101. package/src/prerender.ts +209 -0
  102. package/src/query.ts +250 -0
  103. package/src/router.ts +676 -0
  104. 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
+ }