@vyckr/tachyon 1.1.11 → 1.3.0

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 (49) hide show
  1. package/.env.example +7 -4
  2. package/LICENSE +21 -0
  3. package/README.md +230 -87
  4. package/package.json +50 -33
  5. package/src/cli/bundle.ts +37 -0
  6. package/src/cli/serve.ts +100 -0
  7. package/src/{client/template.js → compiler/render-template.js} +10 -17
  8. package/src/compiler/template-compiler.ts +419 -0
  9. package/src/runtime/hot-reload-client.ts +15 -0
  10. package/src/{client/dev.html → runtime/shells/development.html} +2 -2
  11. package/src/runtime/shells/not-found.html +73 -0
  12. package/src/{client/prod.html → runtime/shells/production.html} +1 -1
  13. package/src/runtime/spa-renderer.ts +439 -0
  14. package/src/server/console-logger.ts +39 -0
  15. package/src/server/process-executor.ts +291 -0
  16. package/src/server/process-pool.ts +80 -0
  17. package/src/server/route-handler.ts +230 -0
  18. package/src/server/schema-validator.ts +204 -0
  19. package/bun.lock +0 -127
  20. package/components/clicker.html +0 -30
  21. package/deno.lock +0 -19
  22. package/go.mod +0 -3
  23. package/lib/gson-2.3.jar +0 -0
  24. package/main.js +0 -13
  25. package/routes/DELETE +0 -18
  26. package/routes/GET +0 -17
  27. package/routes/HTML +0 -135
  28. package/routes/POST +0 -32
  29. package/routes/SOCKET +0 -26
  30. package/routes/api/:version/DELETE +0 -10
  31. package/routes/api/:version/GET +0 -29
  32. package/routes/api/:version/PATCH +0 -24
  33. package/routes/api/GET +0 -29
  34. package/routes/api/POST +0 -16
  35. package/routes/api/PUT +0 -21
  36. package/src/client/404.html +0 -7
  37. package/src/client/dist.ts +0 -20
  38. package/src/client/hmr.ts +0 -12
  39. package/src/client/render.ts +0 -417
  40. package/src/client/routes.json +0 -1
  41. package/src/client/yon.ts +0 -364
  42. package/src/router.ts +0 -186
  43. package/src/serve.ts +0 -147
  44. package/src/server/logger.ts +0 -31
  45. package/src/server/tach.ts +0 -238
  46. package/tests/index.test.ts +0 -110
  47. package/tests/stream.ts +0 -24
  48. package/tests/worker.ts +0 -7
  49. package/tsconfig.json +0 -17
package/.env.example CHANGED
@@ -1,10 +1,13 @@
1
1
  # Tachyon environment variables
2
2
  PORT=8000
3
- NODE_ENV=development|production
3
+ TIMEOUT=70
4
+ DEV=
5
+ VALIDATE=
4
6
  HOSTNAME=127.0.0.1
5
7
  ALLOW_HEADERS=*
6
- ALLOW_ORGINS=*
7
- ALLOW_CREDENTIALS=true|false
8
+ ALLOW_ORIGINS=*
9
+ ALLOW_CREDENTIALS=
8
10
  ALLOW_EXPOSE_HEADERS=*
9
11
  ALLOW_MAX_AGE=3600
10
- ALLOW_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS
12
+ ALLOW_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS
13
+ BASIC_AUTH=username:password
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chidelma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,13 +1,20 @@
1
1
  # Tachyon
2
2
 
3
- Tachyon is a simple to use API framework built with TypeScript (Bun). Tachyon aim to provide a simple and intuitive API framework for building serverless applications and abstracts away the complexity of configuations, letting you focus on building your application.
3
+ Tachyon is a **polyglot, file-system-routed full-stack framework for [Bun](https://bun.sh)**. It lets you define API routes as plain executable files written in any language, and build reactive front-end pages with a lightweight HTML template syntax all without configuration.
4
4
 
5
5
  ## Features
6
6
 
7
- - Customizable methods for routes
8
- - Use of file-system based routing
9
- - Hot reloading of routes in development mode
10
- - Supports dynamic routes
7
+ - **File-system routing** — routes are directories; HTTP methods are files
8
+ - **Polyglot handlers** write routes in Bun, Python, Ruby, Go, Rust, Java, or any language with a shebang
9
+ - **Reactive front-end (Yon)** HTML templates with bindings, loops, conditionals, and custom components
10
+ - **Lazy component loading** — defer component rendering until visible with `IntersectionObserver`
11
+ - **NPM dependency bundling** — use npm packages in front-end code via `/modules/` imports
12
+ - **Hot Module Replacement** — watches `routes/` and `components/` and reloads on change
13
+ - **Custom 404 page** — drop a `404.html` in your project root to override the default
14
+ - **Schema validation** — per-route request/response validation via `OPTIONS` files
15
+ - **Status code routing** — map response schemas to HTTP status codes; the framework picks the code automatically
16
+ - **Auth** — built-in Basic Auth and JWT decoding
17
+ - **Streaming** — SSE responses via `Accept: text/event-stream`
11
18
 
12
19
  ## Installation
13
20
 
@@ -15,140 +22,276 @@ Tachyon is a simple to use API framework built with TypeScript (Bun). Tachyon ai
15
22
  bun add @vyckr/tachyon
16
23
  ```
17
24
 
18
- ## Configuration
25
+ ## Quick Start
26
+
27
+ ```bash
28
+ # Start the development server (expects routes/ in the current directory)
29
+ tach.serve
19
30
 
20
- The .env file should be in the root directory of your project. The following environment variables:
31
+ # Build front-end assets into dist/
32
+ tach.bundle
21
33
  ```
22
- # Tachyon environment variables
23
- PORT=8000 (optional)
24
- NODE_ENV=development|production (optional)
25
- HOSTNAME=127.0.0.1 (optional)
26
- ALLOW_HEADERS=* (optional)
27
- ALLOW_ORGINS=* (optional)
28
- ALLOW_CREDENTIALS=true|false (optional)
29
- ALLOW_EXPOSE_HEADERS=* (optional)
30
- ALLOW_MAX_AGE=3600 (optional)
31
- ALLOW_METHODS=GET,POST,PUT,DELETE,PATCH (optional)
34
+
35
+ Or via npm scripts if you declare them in your own `package.json`:
36
+
37
+ ```json
38
+ {
39
+ "scripts": {
40
+ "start": "tach.serve",
41
+ "bundle": "tach.bundle"
42
+ }
43
+ }
32
44
  ```
33
45
 
34
- ### Requirements
35
- - Make sure to have a 'routes' directory in the root of your project
36
- - Dynamic routes should start with a colon `:`
37
- - The first parameter should NOT be a dynamic route (e.g. /:version/doc/GET)
38
- - All dynamic routes should be within odd indexes (e.g. /v1/:path/login/:id/POST)
39
- - The last parameter in the route should always be a capitalized method as a file name without file extension (e.g. /v1/:path/login/:id/name/DELETE)
40
- - Front-end Pages end with capitalized `HTML` filename (e.g. /v1/HTML)
41
- - Node modules should be imported dynamically with `modules` prefix (e.g. const { default: dayjs } = await import(`/modules/dayjs.js`))
42
- - Components should be in the `components` folder and end with `.html` extension (e.g. /components/counter.html)
43
- - First line of the file should be a shebang for the executable file (e.g. #!/usr/bin/env python3)
44
- - Request context can be retrieved by extracting the last element in args and parsing it.
45
- - Response of executable script must be in a String format and must written to the `/tmp` folder with the the process ID as the file name (e.g. `/tmp/1234`).
46
- - Use the exit method of the executable script with a status code to end the process of the executable script
47
-
48
- ### Examples
46
+ ## Configuration
49
47
 
48
+ Create a `.env` file in your project root. All variables are optional.
50
49
 
51
- ```html
52
- <!-- /routes/HTML -->
53
- <script>
54
- // top-level await
55
- const { default: dayjs } = await import("/modules/dayjs.js")
50
+ ```env
51
+ PORT=8000
52
+ HOSTNAME=127.0.0.1
53
+ TIMEOUT=70
54
+ DEV=true
56
55
 
57
- console.log(dayjs().format())
56
+ # CORS
57
+ ALLOW_HEADERS=*
58
+ ALLOW_ORIGINS=*
59
+ ALLOW_CREDENTIALS=false
60
+ ALLOW_EXPOSE_HEADERS=*
61
+ ALLOW_MAX_AGE=3600
62
+ ALLOW_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS
58
63
 
59
- const greeting = "Hello World!"
60
- </script>
64
+ # Auth
65
+ BASIC_AUTH=username:password
66
+
67
+ # Validation (set to any value to enable)
68
+ VALIDATE=true
61
69
 
62
- <p>${greeting}</p>
70
+ # Custom route/asset paths (defaults to <cwd>/routes, <cwd>/components, <cwd>/assets)
71
+ ROUTES_PATH=
72
+ COMPONENTS_PATH=
73
+ ASSETS_PATH=
63
74
  ```
64
75
 
65
- ```typescript
66
- // routes/v1/:collection/GET
76
+ ## Route Structure
67
77
 
68
- #!/usr/bin/env bun
78
+ ```
79
+ routes/
80
+ GET → GET /
81
+ POST → POST /
82
+ api/
83
+ GET → GET /api
84
+ :version/
85
+ GET → GET /api/:version
86
+ DELETE → DELETE /api/:version
87
+ dashboard/
88
+ HTML → front-end page at /dashboard
89
+ OPTIONS → schema file (optional, enables validation)
90
+ ```
69
91
 
70
- for await(const chunk of Bun.stdin.stream()) {
92
+ ### Requirements
71
93
 
72
- console.log("Executing Bun....");
94
+ - Every route handler is an **executable file** — include a shebang on the first line
95
+ - The last path segment must be an **uppercase HTTP method** (e.g. `GET`, `POST`, `DELETE`) or `HTML` for a front-end page
96
+ - Dynamic segments start with `:` (e.g. `:version`, `:id`)
97
+ - The first path segment must **not** be dynamic
98
+ - Adjacent dynamic segments are not allowed (e.g. `/:a/:b/GET` is invalid)
99
+ - Node modules must be imported dynamically with the `/modules/` prefix: `await import('/modules/dayjs.js')`
100
+ - Components live in `components/` and must have a `.html` extension
101
+
102
+ ### Request Context
103
+
104
+ Every handler receives the full request context on `stdin` as a JSON object:
105
+
106
+ ```json
107
+ {
108
+ "headers": { "content-type": "application/json" },
109
+ "body": { "name": "Alice" },
110
+ "query": { "page": 1 },
111
+ "paths": { "version": "v2" },
112
+ "context": {
113
+ "ipAddress": "127.0.0.1",
114
+ "bearer": { "header": {}, "payload": {}, "signature": "..." }
115
+ }
116
+ }
117
+ ```
73
118
 
74
- const data = new TextDecoder().decode(chunk)
119
+ ### Route Handler Examples
75
120
 
76
- const ctx = JSON.parse(data)
121
+ **Bun (TypeScript)**
122
+ ```typescript
123
+ // routes/v1/:collection/GET
124
+ #!/usr/bin/env bun
77
125
 
78
- ctx.message = "Hello from Bun!"
126
+ const { body, paths, context } = await Bun.stdin.json()
79
127
 
80
- const response = JSON.stringify(ctx)
128
+ const response = { collection: paths.collection, from: context.ipAddress }
81
129
 
82
- await Bun.write(`/tmp/${process.pid}`, response)
83
- }
130
+ Bun.stdout.write(JSON.stringify(response))
84
131
  ```
85
132
 
133
+ **Python**
86
134
  ```python
87
135
  # routes/v1/:collection/POST
88
-
89
136
  #!/usr/bin/env python3
90
- import json
91
- import sys
92
- import os
137
+ import json, sys
138
+
139
+ stdin = json.loads(sys.stdin.read())
140
+ sys.stdout.write(json.dumps({ "message": "Hello from Python!" }))
141
+ ```
142
+
143
+ **Ruby**
144
+ ```ruby
145
+ # routes/v1/:collection/DELETE
146
+ #!/usr/bin/env ruby
147
+ require 'json'
93
148
 
94
- print("Executing Python....")
149
+ stdin = JSON.parse(ARGF.read)
150
+ print JSON.generate({ message: "Hello from Ruby!" })
151
+ ```
95
152
 
96
- ctx = json.loads(sys.stdin.read())
153
+ ### Schema Validation
154
+
155
+ Place an `OPTIONS` file in any route directory to enable validation:
156
+
157
+ ```json
158
+ {
159
+ "POST": {
160
+ "req": {
161
+ "name": "string",
162
+ "age?": 0
163
+ },
164
+ "res": {
165
+ "message": "string"
166
+ },
167
+ "err": {
168
+ "detail": "string"
169
+ }
170
+ }
171
+ }
172
+ ```
97
173
 
98
- ctx["message"] = "Hello from Python!"
174
+ Nullable fields are suffixed with `?`. Set `VALIDATE=true` in your `.env` to enable.
99
175
 
100
- file = open(f"/tmp/{os.getpid()}", "w")
176
+ ### Status Code Routing
101
177
 
102
- file.write(json.dumps(ctx))
178
+ Instead of `res`/`err`, you can key response schemas by HTTP status code. Tachyon matches the handler's JSON output against each schema in ascending order — the first match determines the response status code.
103
179
 
104
- file.close()
180
+ ```json
181
+ {
182
+ "POST": {
183
+ "req": { "name": "string" },
184
+ "201": { "id": "string", "name": "string" },
185
+ "400": { "detail": "string" },
186
+ "503": { "detail": "string", "retryAfter": 0 }
187
+ },
188
+ "DELETE": {
189
+ "204": {}
190
+ }
191
+ }
105
192
  ```
106
193
 
107
- ```ruby
108
- # routes/v1/:collection/DELETE
194
+ Handlers write their normal JSON to stdout — no changes required. The framework determines the status code from whichever schema the output matches. If no numeric schemas are defined, the default behaviour applies (stdout → 200, stderr → 500).
109
195
 
110
- #!/usr/bin/env ruby
111
- require 'json'
196
+ When `VALIDATE=true` is set, the matched schema is also used for strict validation.
112
197
 
113
- puts "Executing Ruby...."
198
+ ## Front-end Pages (Yon)
114
199
 
115
- ctx = JSON.parse(ARGF.read)
200
+ Create an `HTML` file inside any route directory to define a front-end page:
116
201
 
117
- ctx["message"] = "Hello from Ruby!"
202
+ ```html
203
+ <!-- routes/HTML -->
204
+ <script>
205
+ document.title = "Home"
206
+ let count = 0
207
+ </script>
118
208
 
119
- File.write("/tmp/#{Process.pid}", JSON.unparse(ctx))
209
+ <h1>Count: {count}</h1>
210
+ <button @click="count++">Increment</button>
120
211
  ```
121
212
 
122
- To run the application, you can use the following command:
213
+ ### Template Syntax
214
+
215
+ | Syntax | Description |
216
+ |--------|-------------|
217
+ | `{expr}` | Interpolate expression |
218
+ | `@event="handler()"` | Event binding |
219
+ | `:prop="value"` | Bind attribute to expression |
220
+ | `:value="variable"` | Two-way input binding |
221
+ | `<loop :for="...">` | Loop block |
222
+ | `<logic :if="...">` | Conditional block |
223
+ | `<myComp_ prop=val />` | Custom component (trailing `_`) |
224
+ | `<myComp_ lazy />` | Lazy-loaded component (renders when visible) |
225
+
226
+ ### Custom Components
123
227
 
124
- ```bash
125
- bun tach
228
+ ```html
229
+ <!-- components/counter.html -->
230
+ <script>
231
+ let count = 0
232
+ </script>
233
+
234
+ <button @click="count++">Clicked {count} times</button>
126
235
  ```
127
236
 
128
- To invoke the API endpoints, you can use the following commands:
237
+ Use in a page:
129
238
 
130
- ```bash
131
- curl -X GET http://localhost:8000/v1/users
239
+ ```html
240
+ <counter_ />
132
241
  ```
133
242
 
134
- ```bash
135
- curl -X POST http://localhost:8000/v1/users -d '{"name": "John Doe", "age": 30}'
243
+ ### Lazy Loading
244
+
245
+ Add the `lazy` attribute to defer a component's loading until it scrolls into view. The component renders a lightweight placeholder and uses `IntersectionObserver` to load the module on demand.
246
+
247
+ ```html
248
+ <!-- Eager (default) — loaded immediately -->
249
+ <counter_ />
250
+
251
+ <!-- Lazy — loaded when visible in the viewport -->
252
+ <counter_ lazy />
136
253
  ```
137
254
 
138
- ```bash
139
- curl -X PATCH http://localhost:8000/v1/users -d '{"name": "Jane Doe", "age": 31}'
255
+ Lazy components are fully interactive once loaded — event delegation and state management work identically to eager components.
256
+
257
+ ### NPM Modules in Front-end Code
258
+
259
+ Any package listed in your project's `dependencies` is automatically bundled and served at `/modules/<name>.js`. Import them dynamically in your `<script>` blocks:
260
+
261
+ ```html
262
+ <script>
263
+ const { default: dayjs } = await import('/modules/dayjs.js')
264
+ let timestamp = dayjs().format('MMM D, YYYY h:mm A')
265
+ </script>
266
+
267
+ <p>Last updated: {timestamp}</p>
140
268
  ```
141
269
 
142
- ```bash
143
- curl -X DELETE http://localhost:8000/v1/users/5e8b0a9c-c0d1-4d3b-a0b1-e2d8e0e9a1c0
270
+ ### Custom 404 Page
271
+
272
+ Place a `404.html` file in your project root to override the default 404 page. It uses the same Yon template syntax:
273
+
274
+ ```html
275
+ <!-- 404.html -->
276
+ <script>
277
+ document.title = "Not Found"
278
+ </script>
279
+
280
+ <h1>Oops!</h1>
281
+ <p>This page doesn't exist.</p>
282
+ <a href="/">Go home</a>
144
283
  ```
145
284
 
146
- To to build front-end assets into a `dist` folder, use the following command:
285
+ If no custom `404.html` is found, Tachyon serves a built-in styled 404 page.
286
+
287
+ ## Building for Production
147
288
 
148
- ```bash
149
- bun yon
289
+ ```bash
290
+ tach.bundle
150
291
  ```
151
292
 
152
- # License
293
+ Outputs compiled assets to `dist/`.
294
+
295
+ ## License
153
296
 
154
- Tachyon is licensed under the MIT License.
297
+ MIT
package/package.json CHANGED
@@ -1,35 +1,52 @@
1
1
  {
2
- "name": "@vyckr/tachyon",
3
- "version": "1.1.11",
4
- "author": "Chidelma",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/Chidelma/Tachyon.git"
8
- },
9
- "devDependencies": {
10
- "@types/bun": "~1.2.8",
11
- "@types/deno": "^2.0.0",
12
- "@types/jsdom": "^21.1.7",
13
- "@types/node": "^20.4.2"
14
- },
15
- "bin": {
16
- "tach": "./src/serve.ts",
17
- "yon": "./src/client/dist.ts"
18
- },
19
- "bugs": {
20
- "url": "https://github.com/Chidelma/Tachyon/issues"
21
- },
22
- "keywords": [
23
- "tachyon",
24
- "api",
25
- "framework",
26
- "typescript",
27
- "bun"
28
- ],
29
- "license": "MIT",
30
- "type": "module",
31
- "dependencies": {
32
- "dayjs": "^1.11.13",
33
- "jsdom": "^26.0.0"
34
- }
2
+ "name": "@vyckr/tachyon",
3
+ "version": "1.3.0",
4
+ "description": "A polyglot, file-system-routed full-stack framework for Bun",
5
+ "author": "Chidelma",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./src/server/route-handler.ts",
9
+ "exports": {
10
+ ".": "./src/server/route-handler.ts",
11
+ "./server": "./src/server/process-executor.ts",
12
+ "./compiler": "./src/compiler/template-compiler.ts"
13
+ },
14
+ "bin": {
15
+ "tach.serve": "./src/cli/serve.ts",
16
+ "tach.bundle": "./src/cli/bundle.ts"
17
+ },
18
+ "scripts": {
19
+ "start": "bun src/cli/serve.ts",
20
+ "bundle": "bun src/cli/bundle.ts",
21
+ "typecheck": "tsc --noEmit",
22
+ "test": "bun test"
23
+ },
24
+ "files": [
25
+ "src/",
26
+ "README.md",
27
+ ".env.example"
28
+ ],
29
+ "keywords": [
30
+ "tachyon",
31
+ "bun",
32
+ "framework",
33
+ "api",
34
+ "polyglot",
35
+ "file-system-routing",
36
+ "typescript"
37
+ ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/Chidelma/Tachyon.git"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/Chidelma/Tachyon/issues"
44
+ },
45
+ "devDependencies": {
46
+ "@types/bun": "^1.3.4",
47
+ "@types/deno": "^2.0.0",
48
+ "@types/node": "^20.4.2",
49
+ "playwright": "^1.59.1",
50
+ "typescript": "^6.0.2"
51
+ }
35
52
  }
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bun
2
+ import Router from "../server/route-handler.js"
3
+ import Yon from "../compiler/template-compiler.js"
4
+ import "../server/console-logger.js"
5
+ import { mkdir } from "node:fs/promises"
6
+
7
+ const start = Date.now()
8
+
9
+ const distPath = `${process.cwd()}/dist`
10
+
11
+ await mkdir(distPath, { recursive: true })
12
+
13
+ await Yon.createStaticRoutes()
14
+
15
+ for (const route in Router.reqRoutes) {
16
+
17
+ // Skip the HMR script — it is a dev-only asset
18
+ if (route.includes('hot-reload-client')) continue
19
+
20
+ const handler = Router.reqRoutes[route]['GET']
21
+
22
+ if (!handler) continue
23
+
24
+ try {
25
+ const res = await handler()
26
+ await Bun.write(Bun.file(`${distPath}${route}`), await res.blob())
27
+ } catch (err) {
28
+ console.error(`Failed to build route ${route}: ${(err as Error).message}`, process.pid)
29
+ }
30
+ }
31
+
32
+ await Bun.write(
33
+ Bun.file(`${distPath}/index.html`),
34
+ await Bun.file(`${import.meta.dir}/../runtime/shells/production.html`).text()
35
+ )
36
+
37
+ console.info(`Built in ${Date.now() - start}ms`, process.pid)
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bun
2
+ import Tach from "../server/process-executor.js"
3
+ import Pool from "../server/process-pool.js"
4
+ import Router from "../server/route-handler.js"
5
+ import Yon from "../compiler/template-compiler.js"
6
+ import "../server/console-logger.js"
7
+ import { watch } from "fs"
8
+ import { access } from "fs/promises"
9
+ import type { Middleware } from "../server/route-handler.js"
10
+
11
+ /** Debounce delay (ms) applied to file-watcher events before triggering an HMR reload */
12
+ const HMR_DEBOUNCE_MS = 1000
13
+
14
+ const start = Date.now()
15
+
16
+ async function pathExists(path: string): Promise<boolean> {
17
+ try { await access(path); return true } catch { return false }
18
+ }
19
+
20
+ async function loadMiddleware() {
21
+ const extensions = ['.ts', '.js']
22
+ for (const ext of extensions) {
23
+ const filePath = `${Router.middlewarePath}${ext}`
24
+ if (await pathExists(filePath)) {
25
+ const mod = await import(filePath)
26
+ Router.middleware = (mod.default ?? mod) as Middleware
27
+ return
28
+ }
29
+ }
30
+ Router.middleware = null
31
+ }
32
+
33
+ async function configureRoutes(isReload = false) {
34
+ if (isReload) Pool.clearWarmedProcesses()
35
+ await loadMiddleware()
36
+ await Router.validateRoutes()
37
+ Tach.createServerRoutes()
38
+ Pool.prewarmAllHandlers()
39
+ await Yon.createStaticRoutes()
40
+ }
41
+
42
+ await configureRoutes()
43
+
44
+ let debounceTimer: Timer
45
+
46
+ const server = Bun.serve({
47
+ idleTimeout: process.env.TIMEOUT ? Number(process.env.TIMEOUT) : 0,
48
+
49
+ fetch(req, server) {
50
+
51
+ if (new URL(req.url).pathname !== "/hmr") {
52
+ return new Response("Not Found", { status: 404 })
53
+ }
54
+
55
+ server.timeout(req, 0)
56
+
57
+ return new Response(new ReadableStream({
58
+ async start(controller) {
59
+
60
+ const onFileChange = () => {
61
+ clearTimeout(debounceTimer)
62
+ debounceTimer = setTimeout(async () => {
63
+ try {
64
+ console.info("HMR Update", process.pid)
65
+ await configureRoutes(true)
66
+ server.reload({ routes: Router.reqRoutes })
67
+ controller.enqueue("\n\n")
68
+ } catch (err) {
69
+ console.error(`HMR reload failed: ${(err as Error).message}`, process.pid)
70
+ }
71
+ }, HMR_DEBOUNCE_MS)
72
+ }
73
+
74
+ if (await pathExists(Router.routesPath)) watch(Router.routesPath, { recursive: true }, onFileChange)
75
+ if (await pathExists(Router.componentsPath)) watch(Router.componentsPath, { recursive: true }, onFileChange)
76
+ }
77
+ }), { headers: { "Content-Type": "text/event-stream" } })
78
+ },
79
+
80
+ routes: Router.reqRoutes,
81
+ port: process.env.PORT || 8080,
82
+ hostname: process.env.HOSTNAME || '0.0.0.0',
83
+ development: !!process.env.DEV,
84
+ })
85
+
86
+ console.info(`Server running on http://${server.hostname}:${server.port} — started in ${Date.now() - start}ms`, process.pid)
87
+
88
+ process.on('SIGINT', () => {
89
+ clearTimeout(debounceTimer)
90
+ Pool.clearWarmedProcesses()
91
+ server.stop()
92
+ process.exit(0)
93
+ })
94
+
95
+ process.on('SIGTERM', () => {
96
+ clearTimeout(debounceTimer)
97
+ Pool.clearWarmedProcesses()
98
+ server.stop()
99
+ process.exit(0)
100
+ })