@vyckr/tachyon 1.1.11 → 1.2.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.
- package/.env.example +7 -4
- package/LICENSE +21 -0
- package/README.md +210 -90
- package/package.json +50 -33
- package/src/cli/bundle.ts +37 -0
- package/src/cli/serve.ts +100 -0
- package/src/{client/template.js → compiler/render-template.js} +10 -17
- package/src/compiler/template-compiler.ts +419 -0
- package/src/runtime/hot-reload-client.ts +15 -0
- package/src/{client/dev.html → runtime/shells/development.html} +2 -2
- package/src/runtime/shells/not-found.html +73 -0
- package/src/{client/prod.html → runtime/shells/production.html} +1 -1
- package/src/runtime/spa-renderer.ts +439 -0
- package/src/server/console-logger.ts +39 -0
- package/src/server/process-executor.ts +287 -0
- package/src/server/process-pool.ts +80 -0
- package/src/server/route-handler.ts +229 -0
- package/src/server/schema-validator.ts +161 -0
- package/bun.lock +0 -127
- package/components/clicker.html +0 -30
- package/deno.lock +0 -19
- package/go.mod +0 -3
- package/lib/gson-2.3.jar +0 -0
- package/main.js +0 -13
- package/routes/DELETE +0 -18
- package/routes/GET +0 -17
- package/routes/HTML +0 -135
- package/routes/POST +0 -32
- package/routes/SOCKET +0 -26
- package/routes/api/:version/DELETE +0 -10
- package/routes/api/:version/GET +0 -29
- package/routes/api/:version/PATCH +0 -24
- package/routes/api/GET +0 -29
- package/routes/api/POST +0 -16
- package/routes/api/PUT +0 -21
- package/src/client/404.html +0 -7
- package/src/client/dist.ts +0 -20
- package/src/client/hmr.ts +0 -12
- package/src/client/render.ts +0 -417
- package/src/client/routes.json +0 -1
- package/src/client/yon.ts +0 -364
- package/src/router.ts +0 -186
- package/src/serve.ts +0 -147
- package/src/server/logger.ts +0 -31
- package/src/server/tach.ts +0 -238
- package/tests/index.test.ts +0 -110
- package/tests/stream.ts +0 -24
- package/tests/worker.ts +0 -7
- package/tsconfig.json +0 -17
package/.env.example
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# Tachyon environment variables
|
|
2
2
|
PORT=8000
|
|
3
|
-
|
|
3
|
+
TIMEOUT=70
|
|
4
|
+
DEV=
|
|
5
|
+
VALIDATE=
|
|
4
6
|
HOSTNAME=127.0.0.1
|
|
5
7
|
ALLOW_HEADERS=*
|
|
6
|
-
|
|
7
|
-
ALLOW_CREDENTIALS=
|
|
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,154 +1,274 @@
|
|
|
1
1
|
# Tachyon
|
|
2
2
|
|
|
3
|
-
Tachyon is a
|
|
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
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
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
|
+
- **Auth** — built-in Basic Auth and JWT decoding
|
|
16
|
+
- **Streaming** — SSE responses via `Accept: text/event-stream`
|
|
11
17
|
|
|
12
18
|
## Installation
|
|
13
19
|
|
|
14
20
|
```bash
|
|
15
|
-
|
|
21
|
+
npm install @vyckr/tachyon
|
|
16
22
|
```
|
|
17
23
|
|
|
18
|
-
##
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Start the development server (expects routes/ in the current directory)
|
|
28
|
+
tach.serve
|
|
19
29
|
|
|
20
|
-
|
|
30
|
+
# Build front-end assets into dist/
|
|
31
|
+
tach.bundle
|
|
21
32
|
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
|
|
34
|
+
Or via npm scripts if you declare them in your own `package.json`:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"scripts": {
|
|
39
|
+
"start": "tach.serve",
|
|
40
|
+
"bundle": "tach.bundle"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
32
43
|
```
|
|
33
44
|
|
|
34
|
-
|
|
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
|
|
45
|
+
## Configuration
|
|
49
46
|
|
|
47
|
+
Create a `.env` file in your project root. All variables are optional.
|
|
50
48
|
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
```env
|
|
50
|
+
PORT=8000
|
|
51
|
+
HOSTNAME=127.0.0.1
|
|
52
|
+
TIMEOUT=70
|
|
53
|
+
DEV=true
|
|
56
54
|
|
|
57
|
-
|
|
55
|
+
# CORS
|
|
56
|
+
ALLOW_HEADERS=*
|
|
57
|
+
ALLOW_ORIGINS=*
|
|
58
|
+
ALLOW_CREDENTIALS=false
|
|
59
|
+
ALLOW_EXPOSE_HEADERS=*
|
|
60
|
+
ALLOW_MAX_AGE=3600
|
|
61
|
+
ALLOW_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
# Auth
|
|
64
|
+
BASIC_AUTH=username:password
|
|
65
|
+
|
|
66
|
+
# Validation (set to any value to enable)
|
|
67
|
+
VALIDATE=true
|
|
61
68
|
|
|
62
|
-
<
|
|
69
|
+
# Custom route/asset paths (defaults to <cwd>/routes, <cwd>/components, <cwd>/assets)
|
|
70
|
+
ROUTES_PATH=
|
|
71
|
+
COMPONENTS_PATH=
|
|
72
|
+
ASSETS_PATH=
|
|
63
73
|
```
|
|
64
74
|
|
|
65
|
-
|
|
66
|
-
// routes/v1/:collection/GET
|
|
75
|
+
## Route Structure
|
|
67
76
|
|
|
68
|
-
|
|
77
|
+
```
|
|
78
|
+
routes/
|
|
79
|
+
GET → GET /
|
|
80
|
+
POST → POST /
|
|
81
|
+
api/
|
|
82
|
+
GET → GET /api
|
|
83
|
+
:version/
|
|
84
|
+
GET → GET /api/:version
|
|
85
|
+
DELETE → DELETE /api/:version
|
|
86
|
+
dashboard/
|
|
87
|
+
HTML → front-end page at /dashboard
|
|
88
|
+
OPTIONS → schema file (optional, enables validation)
|
|
89
|
+
```
|
|
69
90
|
|
|
70
|
-
|
|
91
|
+
### Requirements
|
|
71
92
|
|
|
72
|
-
|
|
93
|
+
- Every route handler is an **executable file** — include a shebang on the first line
|
|
94
|
+
- The last path segment must be an **uppercase HTTP method** (e.g. `GET`, `POST`, `DELETE`) or `HTML` for a front-end page
|
|
95
|
+
- Dynamic segments start with `:` (e.g. `:version`, `:id`)
|
|
96
|
+
- The first path segment must **not** be dynamic
|
|
97
|
+
- Adjacent dynamic segments are not allowed (e.g. `/:a/:b/GET` is invalid)
|
|
98
|
+
- Node modules must be imported dynamically with the `/modules/` prefix: `await import('/modules/dayjs.js')`
|
|
99
|
+
- Components live in `components/` and must have a `.html` extension
|
|
100
|
+
|
|
101
|
+
### Request Context
|
|
102
|
+
|
|
103
|
+
Every handler receives the full request context on `stdin` as a JSON object:
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"headers": { "content-type": "application/json" },
|
|
108
|
+
"body": { "name": "Alice" },
|
|
109
|
+
"query": { "page": 1 },
|
|
110
|
+
"paths": { "version": "v2" },
|
|
111
|
+
"context": {
|
|
112
|
+
"ipAddress": "127.0.0.1",
|
|
113
|
+
"bearer": { "header": {}, "payload": {}, "signature": "..." }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
73
117
|
|
|
74
|
-
|
|
118
|
+
### Route Handler Examples
|
|
75
119
|
|
|
76
|
-
|
|
120
|
+
**Bun (TypeScript)**
|
|
121
|
+
```typescript
|
|
122
|
+
// routes/v1/:collection/GET
|
|
123
|
+
#!/usr/bin/env bun
|
|
77
124
|
|
|
78
|
-
|
|
125
|
+
const { body, paths, context } = await Bun.stdin.json()
|
|
79
126
|
|
|
80
|
-
|
|
127
|
+
const response = { collection: paths.collection, from: context.ipAddress }
|
|
81
128
|
|
|
82
|
-
|
|
83
|
-
}
|
|
129
|
+
Bun.stdout.write(JSON.stringify(response))
|
|
84
130
|
```
|
|
85
131
|
|
|
132
|
+
**Python**
|
|
86
133
|
```python
|
|
87
134
|
# routes/v1/:collection/POST
|
|
88
|
-
|
|
89
135
|
#!/usr/bin/env python3
|
|
90
|
-
import json
|
|
91
|
-
import sys
|
|
92
|
-
import os
|
|
93
|
-
|
|
94
|
-
print("Executing Python....")
|
|
95
|
-
|
|
96
|
-
ctx = json.loads(sys.stdin.read())
|
|
136
|
+
import json, sys
|
|
97
137
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
file = open(f"/tmp/{os.getpid()}", "w")
|
|
101
|
-
|
|
102
|
-
file.write(json.dumps(ctx))
|
|
103
|
-
|
|
104
|
-
file.close()
|
|
138
|
+
stdin = json.loads(sys.stdin.read())
|
|
139
|
+
sys.stdout.write(json.dumps({ "message": "Hello from Python!" }))
|
|
105
140
|
```
|
|
106
141
|
|
|
142
|
+
**Ruby**
|
|
107
143
|
```ruby
|
|
108
144
|
# routes/v1/:collection/DELETE
|
|
109
|
-
|
|
110
145
|
#!/usr/bin/env ruby
|
|
111
146
|
require 'json'
|
|
112
147
|
|
|
113
|
-
|
|
148
|
+
stdin = JSON.parse(ARGF.read)
|
|
149
|
+
print JSON.generate({ message: "Hello from Ruby!" })
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Schema Validation
|
|
153
|
+
|
|
154
|
+
Place an `OPTIONS` file in any route directory to enable validation:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"POST": {
|
|
159
|
+
"req": {
|
|
160
|
+
"name": "string",
|
|
161
|
+
"age?": 0
|
|
162
|
+
},
|
|
163
|
+
"res": {
|
|
164
|
+
"message": "string"
|
|
165
|
+
},
|
|
166
|
+
"err": {
|
|
167
|
+
"detail": "string"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Nullable fields are suffixed with `?`. Set `VALIDATE=true` in your `.env` to enable.
|
|
114
174
|
|
|
115
|
-
|
|
175
|
+
## Front-end Pages (Yon)
|
|
116
176
|
|
|
117
|
-
|
|
177
|
+
Create an `HTML` file inside any route directory to define a front-end page:
|
|
118
178
|
|
|
119
|
-
|
|
179
|
+
```html
|
|
180
|
+
<!-- routes/HTML -->
|
|
181
|
+
<script>
|
|
182
|
+
document.title = "Home"
|
|
183
|
+
let count = 0
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<h1>Count: {count}</h1>
|
|
187
|
+
<button @click="count++">Increment</button>
|
|
120
188
|
```
|
|
121
189
|
|
|
122
|
-
|
|
190
|
+
### Template Syntax
|
|
191
|
+
|
|
192
|
+
| Syntax | Description |
|
|
193
|
+
|--------|-------------|
|
|
194
|
+
| `{expr}` | Interpolate expression |
|
|
195
|
+
| `@event="handler()"` | Event binding |
|
|
196
|
+
| `:prop="value"` | Bind attribute to expression |
|
|
197
|
+
| `:value="variable"` | Two-way input binding |
|
|
198
|
+
| `<loop :for="...">` | Loop block |
|
|
199
|
+
| `<logic :if="...">` | Conditional block |
|
|
200
|
+
| `<myComp_ prop=val />` | Custom component (trailing `_`) |
|
|
201
|
+
| `<myComp_ lazy />` | Lazy-loaded component (renders when visible) |
|
|
202
|
+
|
|
203
|
+
### Custom Components
|
|
204
|
+
|
|
205
|
+
```html
|
|
206
|
+
<!-- components/counter.html -->
|
|
207
|
+
<script>
|
|
208
|
+
let count = 0
|
|
209
|
+
</script>
|
|
123
210
|
|
|
124
|
-
|
|
125
|
-
bun tach
|
|
211
|
+
<button @click="count++">Clicked {count} times</button>
|
|
126
212
|
```
|
|
127
213
|
|
|
128
|
-
|
|
214
|
+
Use in a page:
|
|
129
215
|
|
|
130
|
-
```
|
|
131
|
-
|
|
216
|
+
```html
|
|
217
|
+
<counter_ />
|
|
132
218
|
```
|
|
133
219
|
|
|
134
|
-
|
|
135
|
-
|
|
220
|
+
### Lazy Loading
|
|
221
|
+
|
|
222
|
+
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.
|
|
223
|
+
|
|
224
|
+
```html
|
|
225
|
+
<!-- Eager (default) — loaded immediately -->
|
|
226
|
+
<counter_ />
|
|
227
|
+
|
|
228
|
+
<!-- Lazy — loaded when visible in the viewport -->
|
|
229
|
+
<counter_ lazy />
|
|
136
230
|
```
|
|
137
231
|
|
|
138
|
-
|
|
139
|
-
|
|
232
|
+
Lazy components are fully interactive once loaded — event delegation and state management work identically to eager components.
|
|
233
|
+
|
|
234
|
+
### NPM Modules in Front-end Code
|
|
235
|
+
|
|
236
|
+
Any package listed in your project's `dependencies` is automatically bundled and served at `/modules/<name>.js`. Import them dynamically in your `<script>` blocks:
|
|
237
|
+
|
|
238
|
+
```html
|
|
239
|
+
<script>
|
|
240
|
+
const { default: dayjs } = await import('/modules/dayjs.js')
|
|
241
|
+
let timestamp = dayjs().format('MMM D, YYYY h:mm A')
|
|
242
|
+
</script>
|
|
243
|
+
|
|
244
|
+
<p>Last updated: {timestamp}</p>
|
|
140
245
|
```
|
|
141
246
|
|
|
142
|
-
|
|
143
|
-
|
|
247
|
+
### Custom 404 Page
|
|
248
|
+
|
|
249
|
+
Place a `404.html` file in your project root to override the default 404 page. It uses the same Yon template syntax:
|
|
250
|
+
|
|
251
|
+
```html
|
|
252
|
+
<!-- 404.html -->
|
|
253
|
+
<script>
|
|
254
|
+
document.title = "Not Found"
|
|
255
|
+
</script>
|
|
256
|
+
|
|
257
|
+
<h1>Oops!</h1>
|
|
258
|
+
<p>This page doesn't exist.</p>
|
|
259
|
+
<a href="/">Go home</a>
|
|
144
260
|
```
|
|
145
261
|
|
|
146
|
-
|
|
262
|
+
If no custom `404.html` is found, Tachyon serves a built-in styled 404 page.
|
|
263
|
+
|
|
264
|
+
## Building for Production
|
|
147
265
|
|
|
148
|
-
```bash
|
|
149
|
-
|
|
266
|
+
```bash
|
|
267
|
+
tach.bundle
|
|
150
268
|
```
|
|
151
269
|
|
|
152
|
-
|
|
270
|
+
Outputs compiled assets to `dist/`.
|
|
271
|
+
|
|
272
|
+
## License
|
|
153
273
|
|
|
154
|
-
|
|
274
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,35 +1,52 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
2
|
+
"name": "@vyckr/tachyon",
|
|
3
|
+
"version": "1.2.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)
|
package/src/cli/serve.ts
ADDED
|
@@ -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
|
+
})
|