@vyckr/tachyon 1.2.0 → 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.
package/README.md CHANGED
@@ -12,13 +12,14 @@ Tachyon is a **polyglot, file-system-routed full-stack framework for [Bun](https
12
12
  - **Hot Module Replacement** — watches `routes/` and `components/` and reloads on change
13
13
  - **Custom 404 page** — drop a `404.html` in your project root to override the default
14
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
15
16
  - **Auth** — built-in Basic Auth and JWT decoding
16
17
  - **Streaming** — SSE responses via `Accept: text/event-stream`
17
18
 
18
19
  ## Installation
19
20
 
20
21
  ```bash
21
- npm install @vyckr/tachyon
22
+ bun add @vyckr/tachyon
22
23
  ```
23
24
 
24
25
  ## Quick Start
@@ -172,6 +173,28 @@ Place an `OPTIONS` file in any route directory to enable validation:
172
173
 
173
174
  Nullable fields are suffixed with `?`. Set `VALIDATE=true` in your `.env` to enable.
174
175
 
176
+ ### Status Code Routing
177
+
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.
179
+
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
+ }
192
+ ```
193
+
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).
195
+
196
+ When `VALIDATE=true` is set, the matched schema is also used for strict validation.
197
+
175
198
  ## Front-end Pages (Yon)
176
199
 
177
200
  Create an `HTML` file inside any route directory to define a front-end page:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vyckr/tachyon",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "A polyglot, file-system-routed full-stack framework for Bun",
5
5
  "author": "Chidelma",
6
6
  "license": "MIT",
@@ -170,15 +170,19 @@ export default class Tach {
170
170
 
171
171
  const { body, status } = await Tach.getResponse([handler], stdin, context, config)
172
172
 
173
+ const matchedStatus = body ? Validate.matchStatusCode(handler, body) : null
174
+ const finalStatus = matchedStatus ?? status
175
+
173
176
  if (process.env.VALIDATE !== undefined) {
177
+ const ioKey = matchedStatus ? String(matchedStatus) : (status === 200 ? "res" : "err")
174
178
  try {
175
- await Validate.validateData(handler, status === 200 ? "res" : "err", body!)
179
+ await Validate.validateData(handler, ioKey, body!)
176
180
  } catch (e) {
177
181
  return Response.json({ detail: (e as Error).message }, { status: 422, headers: Router.headers })
178
182
  }
179
183
  }
180
184
 
181
- return new Response(body, { status, headers: Router.headers })
185
+ return new Response(body, { status: finalStatus, headers: Router.headers })
182
186
  }
183
187
 
184
188
  /**
@@ -140,7 +140,8 @@ export default class Router {
140
140
  const blob = await request.blob()
141
141
 
142
142
  if (blob.size > 0) {
143
- stdin.body = blob.type.endsWith('json') ? await blob.json() : await blob.text()
143
+ const contentType = request.headers.get('content-type') ?? ''
144
+ stdin.body = contentType.includes('json') ? await blob.json() : await blob.text()
144
145
  }
145
146
 
146
147
  const searchParams = new URL(request.url).searchParams
@@ -122,9 +122,52 @@ export default class Validate {
122
122
  * populated during `validateRoutes()` — instead of re-reading OPTIONS
123
123
  * files from disk.
124
124
  */
125
+ /**
126
+ * Tries to match `body` against each numeric status-code schema defined for
127
+ * the route/method in the OPTIONS file. Returns the first matching code, or
128
+ * `null` if no numeric schemas are defined or none match.
129
+ */
130
+ static matchStatusCode(handler: string, body: string): number | null {
131
+ const parts = handler.split('/')
132
+ const method = parts.pop()!
133
+ const absoluteDir = parts.join('/')
134
+ const relativeRoute = absoluteDir.replace(Router.routesPath, '') || '/'
135
+
136
+ const schema = Router.routeConfigs[relativeRoute] as unknown as Record<string, Record<string, Record<string, unknown>>>
137
+ if (!schema) return null
138
+
139
+ const methodSchema = schema[method]
140
+ if (!methodSchema) return null
141
+
142
+ const statusCodes = Object.keys(methodSchema)
143
+ .filter(k => /^\d{3}$/.test(k))
144
+ .map(Number)
145
+ .sort((a, b) => a - b)
146
+
147
+ if (statusCodes.length === 0) return null
148
+
149
+ let parsed: Record<string, unknown>
150
+ try {
151
+ const p = JSON.parse(body)
152
+ if (typeof p !== 'object' || p === null || Array.isArray(p)) return null
153
+ parsed = p
154
+ } catch {
155
+ return null
156
+ }
157
+
158
+ for (const code of statusCodes) {
159
+ try {
160
+ Validate.validateObject({ ...parsed }, methodSchema[String(code)], relativeRoute, relativeRoute)
161
+ return code
162
+ } catch {}
163
+ }
164
+
165
+ return null
166
+ }
167
+
125
168
  static async validateData(
126
169
  route: string,
127
- io: 'req' | 'err' | 'res',
170
+ io: string,
128
171
  data: Record<string, unknown> | string,
129
172
  ) {
130
173
  const parts = route.split('/')