tagliatelle 1.0.0-beta.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/README.md +488 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +609 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/jsx-runtime.d.ts +31 -0
- package/dist/jsx-runtime.d.ts.map +1 -0
- package/dist/jsx-runtime.js +46 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/router.d.ts +59 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +487 -0
- package/dist/router.js.map +1 -0
- package/dist/security.d.ts +84 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +233 -0
- package/dist/security.js.map +1 -0
- package/dist/tagliatelle.d.ts +90 -0
- package/dist/tagliatelle.d.ts.map +1 -0
- package/dist/tagliatelle.js +684 -0
- package/dist/tagliatelle.js.map +1 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +32 -0
- package/dist/types.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🍝 <Tag>liatelle.js - The Declarative Backend Framework
|
|
3
|
+
*
|
|
4
|
+
* This is the secret sauce that makes JSX work for backend routing.
|
|
5
|
+
* It transforms your beautiful component tree into a blazing-fast Fastify server.
|
|
6
|
+
*/
|
|
7
|
+
import Fastify from 'fastify';
|
|
8
|
+
import { safeMerge, sanitizeErrorMessage, withTimeout } from './security.js';
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
+
// 🎨 CONSOLE COLORS
|
|
11
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
12
|
+
const c = {
|
|
13
|
+
reset: '\x1b[0m',
|
|
14
|
+
bold: '\x1b[1m',
|
|
15
|
+
dim: '\x1b[2m',
|
|
16
|
+
// Colors
|
|
17
|
+
red: '\x1b[31m',
|
|
18
|
+
green: '\x1b[32m',
|
|
19
|
+
yellow: '\x1b[33m',
|
|
20
|
+
blue: '\x1b[34m',
|
|
21
|
+
magenta: '\x1b[35m',
|
|
22
|
+
cyan: '\x1b[36m',
|
|
23
|
+
white: '\x1b[37m',
|
|
24
|
+
gray: '\x1b[90m',
|
|
25
|
+
// Bright colors
|
|
26
|
+
brightRed: '\x1b[91m',
|
|
27
|
+
brightGreen: '\x1b[92m',
|
|
28
|
+
brightYellow: '\x1b[93m',
|
|
29
|
+
brightBlue: '\x1b[94m',
|
|
30
|
+
brightMagenta: '\x1b[95m',
|
|
31
|
+
brightCyan: '\x1b[96m',
|
|
32
|
+
};
|
|
33
|
+
// Method colors
|
|
34
|
+
const methodColor = (method) => {
|
|
35
|
+
switch (method) {
|
|
36
|
+
case 'GET': return c.brightGreen;
|
|
37
|
+
case 'POST': return c.brightYellow;
|
|
38
|
+
case 'PUT': return c.brightBlue;
|
|
39
|
+
case 'PATCH': return c.brightCyan;
|
|
40
|
+
case 'DELETE': return c.brightRed;
|
|
41
|
+
default: return c.white;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
// Status code color
|
|
45
|
+
const statusColor = (code) => {
|
|
46
|
+
if (code < 200)
|
|
47
|
+
return c.gray;
|
|
48
|
+
if (code < 300)
|
|
49
|
+
return c.green;
|
|
50
|
+
if (code < 400)
|
|
51
|
+
return c.cyan;
|
|
52
|
+
if (code < 500)
|
|
53
|
+
return c.yellow;
|
|
54
|
+
return c.red;
|
|
55
|
+
};
|
|
56
|
+
import { COMPONENT_TYPES, } from './types.js';
|
|
57
|
+
import { registerRoutes } from './router.js';
|
|
58
|
+
// Re-export types
|
|
59
|
+
export * from './types.js';
|
|
60
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
61
|
+
// 🍝 JSX FACTORY (Classic Runtime)
|
|
62
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
63
|
+
/**
|
|
64
|
+
* JSX factory function - creates virtual elements from JSX
|
|
65
|
+
* This is called by TypeScript when it transforms JSX
|
|
66
|
+
*/
|
|
67
|
+
export function h(type, props, ...children) {
|
|
68
|
+
return {
|
|
69
|
+
type,
|
|
70
|
+
props: props || {},
|
|
71
|
+
children: children.flat().filter(Boolean)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Fragment support for grouping elements without a wrapper
|
|
76
|
+
*/
|
|
77
|
+
export function Fragment({ children }) {
|
|
78
|
+
return children || [];
|
|
79
|
+
}
|
|
80
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
81
|
+
// 🥣 CONTEXT SYSTEM (Dependency Injection)
|
|
82
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
83
|
+
export class Context {
|
|
84
|
+
values = new Map();
|
|
85
|
+
set(key, value) {
|
|
86
|
+
this.values.set(key, value);
|
|
87
|
+
}
|
|
88
|
+
get(key) {
|
|
89
|
+
return this.values.get(key);
|
|
90
|
+
}
|
|
91
|
+
clone() {
|
|
92
|
+
const newContext = new Context();
|
|
93
|
+
this.values.forEach((value, key) => {
|
|
94
|
+
newContext.set(key, value);
|
|
95
|
+
});
|
|
96
|
+
return newContext;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
100
|
+
// 🍽️ BUILT-IN COMPONENTS
|
|
101
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
102
|
+
export function Server({ port, host, children }) {
|
|
103
|
+
return {
|
|
104
|
+
__tagliatelle: COMPONENT_TYPES.SERVER,
|
|
105
|
+
port: port ?? 3000,
|
|
106
|
+
host: host ?? '0.0.0.0',
|
|
107
|
+
children: children ? [children].flat() : []
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export function Get(props) {
|
|
111
|
+
return {
|
|
112
|
+
__tagliatelle: COMPONENT_TYPES.GET,
|
|
113
|
+
method: 'GET',
|
|
114
|
+
path: props.path,
|
|
115
|
+
handler: props.handler,
|
|
116
|
+
schema: props.schema
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export function Post(props) {
|
|
120
|
+
return {
|
|
121
|
+
__tagliatelle: COMPONENT_TYPES.POST,
|
|
122
|
+
method: 'POST',
|
|
123
|
+
path: props.path,
|
|
124
|
+
handler: props.handler,
|
|
125
|
+
schema: props.schema
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export function Put(props) {
|
|
129
|
+
return {
|
|
130
|
+
__tagliatelle: COMPONENT_TYPES.PUT,
|
|
131
|
+
method: 'PUT',
|
|
132
|
+
path: props.path,
|
|
133
|
+
handler: props.handler,
|
|
134
|
+
schema: props.schema
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export function Delete(props) {
|
|
138
|
+
return {
|
|
139
|
+
__tagliatelle: COMPONENT_TYPES.DELETE,
|
|
140
|
+
method: 'DELETE',
|
|
141
|
+
path: props.path,
|
|
142
|
+
handler: props.handler,
|
|
143
|
+
schema: props.schema
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export function Patch(props) {
|
|
147
|
+
return {
|
|
148
|
+
__tagliatelle: COMPONENT_TYPES.PATCH,
|
|
149
|
+
method: 'PATCH',
|
|
150
|
+
path: props.path,
|
|
151
|
+
handler: props.handler,
|
|
152
|
+
schema: props.schema
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export function Middleware({ use, children }) {
|
|
156
|
+
return {
|
|
157
|
+
__tagliatelle: COMPONENT_TYPES.MIDDLEWARE,
|
|
158
|
+
use,
|
|
159
|
+
children: children ? [children].flat() : []
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export function DB({ provider, children }) {
|
|
163
|
+
return {
|
|
164
|
+
__tagliatelle: COMPONENT_TYPES.DB,
|
|
165
|
+
provider,
|
|
166
|
+
children: children ? [children].flat() : []
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
export function Logger({ level }) {
|
|
170
|
+
return {
|
|
171
|
+
__tagliatelle: COMPONENT_TYPES.LOGGER,
|
|
172
|
+
level: level ?? 'info'
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export function Group({ prefix, children }) {
|
|
176
|
+
return {
|
|
177
|
+
__tagliatelle: COMPONENT_TYPES.GROUP,
|
|
178
|
+
prefix: prefix ?? '',
|
|
179
|
+
children: children ? [children].flat() : []
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
export function Cors({ origin, methods, children }) {
|
|
183
|
+
return {
|
|
184
|
+
__tagliatelle: COMPONENT_TYPES.CORS,
|
|
185
|
+
origin: origin ?? '*',
|
|
186
|
+
methods: methods ?? ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
|
187
|
+
children: children ? [children].flat() : []
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
export function RateLimiter({ max, timeWindow, children }) {
|
|
191
|
+
return {
|
|
192
|
+
__tagliatelle: COMPONENT_TYPES.RATE_LIMITER,
|
|
193
|
+
max: max ?? 100,
|
|
194
|
+
timeWindow: timeWindow ?? '1 minute',
|
|
195
|
+
children: children ? [children].flat() : []
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* File-based routing - Next.js style!
|
|
200
|
+
* Usage: <Routes dir="./routes" prefix="/api" />
|
|
201
|
+
*/
|
|
202
|
+
export function Routes({ dir, prefix }) {
|
|
203
|
+
return {
|
|
204
|
+
__tagliatelle: COMPONENT_TYPES.ROUTES,
|
|
205
|
+
dir,
|
|
206
|
+
prefix: prefix ?? ''
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
210
|
+
// 📤 RESPONSE COMPONENTS (for JSX handlers)
|
|
211
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
212
|
+
/**
|
|
213
|
+
* Response wrapper - contains status, headers, and body
|
|
214
|
+
* Usage: <Response><Status code={201} /><Body data={{...}} /></Response>
|
|
215
|
+
*/
|
|
216
|
+
export function Response({ children }) {
|
|
217
|
+
return {
|
|
218
|
+
__tagliatelle: COMPONENT_TYPES.RESPONSE,
|
|
219
|
+
children: children ? [children].flat() : []
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Set HTTP status code
|
|
224
|
+
* Usage: <Status code={201} />
|
|
225
|
+
*/
|
|
226
|
+
export function Status({ code }) {
|
|
227
|
+
return {
|
|
228
|
+
__tagliatelle: COMPONENT_TYPES.STATUS,
|
|
229
|
+
code
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Set response body (JSON)
|
|
234
|
+
* Usage: <Body data={{ success: true, data: [...] }} />
|
|
235
|
+
*/
|
|
236
|
+
export function Body({ data }) {
|
|
237
|
+
return {
|
|
238
|
+
__tagliatelle: COMPONENT_TYPES.BODY,
|
|
239
|
+
data
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Set response headers
|
|
244
|
+
* Usage: <Headers headers={{ 'X-Custom': 'value' }} />
|
|
245
|
+
*/
|
|
246
|
+
export function Headers({ headers }) {
|
|
247
|
+
return {
|
|
248
|
+
__tagliatelle: COMPONENT_TYPES.HEADERS,
|
|
249
|
+
headers
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Return an error response
|
|
254
|
+
* Usage: <Err code={404} message="Not found" />
|
|
255
|
+
*/
|
|
256
|
+
export function Err({ code, message, details }) {
|
|
257
|
+
return {
|
|
258
|
+
__tagliatelle: COMPONENT_TYPES.ERR,
|
|
259
|
+
code: code ?? 500,
|
|
260
|
+
message,
|
|
261
|
+
details
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Augment handler props with additional data (e.g., user from auth)
|
|
266
|
+
* Usage: <Augment user={{ id: 1, name: "John" }} />
|
|
267
|
+
*/
|
|
268
|
+
export function Augment(props) {
|
|
269
|
+
return {
|
|
270
|
+
__tagliatelle: COMPONENT_TYPES.AUGMENT,
|
|
271
|
+
data: props
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Halt the middleware chain (optionally with error)
|
|
276
|
+
* Usage: <Halt /> or <Halt code={401} message="Unauthorized" />
|
|
277
|
+
*/
|
|
278
|
+
export function Halt({ code, message }) {
|
|
279
|
+
return {
|
|
280
|
+
__tagliatelle: COMPONENT_TYPES.HALT,
|
|
281
|
+
code,
|
|
282
|
+
message
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Resolves a JSX response tree into a response object
|
|
287
|
+
*/
|
|
288
|
+
function resolveResponse(element) {
|
|
289
|
+
const response = {
|
|
290
|
+
statusCode: 200,
|
|
291
|
+
headers: {},
|
|
292
|
+
body: undefined,
|
|
293
|
+
isError: false
|
|
294
|
+
};
|
|
295
|
+
function processNode(node) {
|
|
296
|
+
if (!node)
|
|
297
|
+
return;
|
|
298
|
+
// Handle arrays
|
|
299
|
+
if (Array.isArray(node)) {
|
|
300
|
+
node.forEach(processNode);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Resolve if it's a JSX element
|
|
304
|
+
let resolved = node;
|
|
305
|
+
if (typeof node === 'object' && node !== null && 'type' in node && typeof node.type === 'function') {
|
|
306
|
+
const el = node;
|
|
307
|
+
const props = { ...el.props, children: el.children };
|
|
308
|
+
const componentFn = el.type;
|
|
309
|
+
resolved = componentFn(props);
|
|
310
|
+
}
|
|
311
|
+
// Process TagliatelleComponent
|
|
312
|
+
if (typeof resolved === 'object' && '__tagliatelle' in resolved) {
|
|
313
|
+
const component = resolved;
|
|
314
|
+
switch (component.__tagliatelle) {
|
|
315
|
+
case COMPONENT_TYPES.RESPONSE:
|
|
316
|
+
// Process children
|
|
317
|
+
if (component.children) {
|
|
318
|
+
component.children.forEach(processNode);
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
case COMPONENT_TYPES.STATUS:
|
|
322
|
+
response.statusCode = component.code;
|
|
323
|
+
break;
|
|
324
|
+
case COMPONENT_TYPES.BODY:
|
|
325
|
+
response.body = component.data;
|
|
326
|
+
break;
|
|
327
|
+
case COMPONENT_TYPES.HEADERS:
|
|
328
|
+
Object.assign(response.headers, component.headers);
|
|
329
|
+
break;
|
|
330
|
+
case COMPONENT_TYPES.ERR:
|
|
331
|
+
response.isError = true;
|
|
332
|
+
response.statusCode = component.code;
|
|
333
|
+
const errBody = { error: component.message };
|
|
334
|
+
if (component.details) {
|
|
335
|
+
errBody.details = component.details;
|
|
336
|
+
}
|
|
337
|
+
response.body = errBody;
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
processNode(element);
|
|
343
|
+
return response;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Check if a value is a JSX response element
|
|
347
|
+
*/
|
|
348
|
+
function isJSXResponse(value) {
|
|
349
|
+
if (!value || typeof value !== 'object')
|
|
350
|
+
return false;
|
|
351
|
+
// Check if it's a TagliatelleComponent with response type
|
|
352
|
+
if ('__tagliatelle' in value) {
|
|
353
|
+
const component = value;
|
|
354
|
+
return [
|
|
355
|
+
COMPONENT_TYPES.RESPONSE,
|
|
356
|
+
COMPONENT_TYPES.STATUS,
|
|
357
|
+
COMPONENT_TYPES.BODY,
|
|
358
|
+
COMPONENT_TYPES.HEADERS,
|
|
359
|
+
COMPONENT_TYPES.ERR
|
|
360
|
+
].includes(component.__tagliatelle);
|
|
361
|
+
}
|
|
362
|
+
// Check if it's a JSX element that resolves to a response
|
|
363
|
+
if ('type' in value && typeof value.type === 'function') {
|
|
364
|
+
return true; // We'll resolve and check later
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
369
|
+
// 🚀 THE RENDERER
|
|
370
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
371
|
+
/**
|
|
372
|
+
* Resolves a virtual element into its component output
|
|
373
|
+
* Recursively resolves until we get an actual Tagliatelle component
|
|
374
|
+
*/
|
|
375
|
+
function resolveElement(element) {
|
|
376
|
+
if (!element)
|
|
377
|
+
return null;
|
|
378
|
+
// If it's already a resolved component object
|
|
379
|
+
if (typeof element === 'object' && '__tagliatelle' in element) {
|
|
380
|
+
return element;
|
|
381
|
+
}
|
|
382
|
+
// If it's an array, return as-is
|
|
383
|
+
if (Array.isArray(element)) {
|
|
384
|
+
return element;
|
|
385
|
+
}
|
|
386
|
+
// If it's a JSX element with a function type (component)
|
|
387
|
+
if (typeof element === 'object' && 'type' in element && typeof element.type === 'function') {
|
|
388
|
+
const el = element;
|
|
389
|
+
const props = { ...el.props, children: el.children };
|
|
390
|
+
const componentFn = el.type;
|
|
391
|
+
const result = componentFn(props);
|
|
392
|
+
// Recursively resolve if the result is another element
|
|
393
|
+
const resolved = resolveElement(result);
|
|
394
|
+
// If the resolved result is a tagliatelle component and we had children in the JSX,
|
|
395
|
+
// pass them through
|
|
396
|
+
if (resolved && !Array.isArray(resolved) && '__tagliatelle' in resolved && el.children.length > 0) {
|
|
397
|
+
resolved.children = el.children;
|
|
398
|
+
}
|
|
399
|
+
return resolved;
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Wraps a user handler to work with Fastify's request/reply system
|
|
405
|
+
*/
|
|
406
|
+
function wrapHandler(handler, middlewares, context) {
|
|
407
|
+
return async (request, reply) => {
|
|
408
|
+
// Use our pretty logger
|
|
409
|
+
const prettyLog = context.get('prettyLog') || {
|
|
410
|
+
info: () => { },
|
|
411
|
+
warn: () => { },
|
|
412
|
+
error: () => { },
|
|
413
|
+
debug: () => { },
|
|
414
|
+
};
|
|
415
|
+
// Build props from request
|
|
416
|
+
const props = {
|
|
417
|
+
params: request.params,
|
|
418
|
+
query: request.query,
|
|
419
|
+
body: request.body,
|
|
420
|
+
headers: request.headers,
|
|
421
|
+
request,
|
|
422
|
+
reply,
|
|
423
|
+
db: context.get('db'),
|
|
424
|
+
log: prettyLog,
|
|
425
|
+
};
|
|
426
|
+
// Middleware timeout (30 seconds max)
|
|
427
|
+
const MIDDLEWARE_TIMEOUT = 30000;
|
|
428
|
+
// Execute middleware chain
|
|
429
|
+
for (const mw of middlewares) {
|
|
430
|
+
try {
|
|
431
|
+
// Wrap middleware in timeout to prevent hanging
|
|
432
|
+
const result = await withTimeout(async () => mw(props, request, reply), MIDDLEWARE_TIMEOUT, 'Middleware timeout');
|
|
433
|
+
if (result === false) {
|
|
434
|
+
return; // Middleware halted the chain
|
|
435
|
+
}
|
|
436
|
+
// Middleware can augment props - use safeMerge to prevent prototype pollution
|
|
437
|
+
if (result && typeof result === 'object') {
|
|
438
|
+
safeMerge(props, result);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
const err = error;
|
|
443
|
+
// Sanitize error message to prevent info leakage
|
|
444
|
+
reply.status(err.statusCode ?? 500).send({
|
|
445
|
+
error: sanitizeErrorMessage(err, 'Middleware error')
|
|
446
|
+
});
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
// Execute the actual handler
|
|
452
|
+
const result = await handler(props);
|
|
453
|
+
// If handler already sent response, don't send again
|
|
454
|
+
if (reply.sent)
|
|
455
|
+
return;
|
|
456
|
+
// Check if result is a JSX response
|
|
457
|
+
if (result !== undefined) {
|
|
458
|
+
if (isJSXResponse(result)) {
|
|
459
|
+
// Resolve JSX response tree
|
|
460
|
+
const response = resolveResponse(result);
|
|
461
|
+
// Set headers
|
|
462
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
463
|
+
reply.header(key, value);
|
|
464
|
+
}
|
|
465
|
+
// Send response with status
|
|
466
|
+
reply.status(response.statusCode).send(response.body);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
// Plain object response
|
|
470
|
+
reply.send(result);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
if (!reply.sent) {
|
|
476
|
+
const err = error;
|
|
477
|
+
// Sanitize error to prevent stack trace and info leakage
|
|
478
|
+
reply.status(err.statusCode ?? 500).send({
|
|
479
|
+
error: sanitizeErrorMessage(err, 'Internal server error')
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Processes the virtual DOM tree and configures Fastify
|
|
487
|
+
*/
|
|
488
|
+
async function processTree(element, fastify, context, middlewares = [], prefix = '') {
|
|
489
|
+
const resolved = resolveElement(element);
|
|
490
|
+
if (!resolved)
|
|
491
|
+
return;
|
|
492
|
+
// Handle arrays (fragments)
|
|
493
|
+
if (Array.isArray(resolved)) {
|
|
494
|
+
for (const child of resolved) {
|
|
495
|
+
await processTree(child, fastify, context, middlewares, prefix);
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const component = resolved;
|
|
500
|
+
const type = component.__tagliatelle;
|
|
501
|
+
switch (type) {
|
|
502
|
+
case COMPONENT_TYPES.LOGGER:
|
|
503
|
+
// Fastify has built-in logging, just configure it
|
|
504
|
+
fastify.log.level = component.level;
|
|
505
|
+
break;
|
|
506
|
+
case COMPONENT_TYPES.DB:
|
|
507
|
+
// Initialize database provider
|
|
508
|
+
if (component.provider) {
|
|
509
|
+
try {
|
|
510
|
+
const provider = component.provider;
|
|
511
|
+
const db = await provider();
|
|
512
|
+
context.set('db', db);
|
|
513
|
+
console.log(` ${c.green}✓${c.reset} Database connected`);
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
const err = error;
|
|
517
|
+
console.error(` ${c.red}✗${c.reset} Database failed: ${err.message}`);
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// Process children with db context
|
|
522
|
+
if (component.children) {
|
|
523
|
+
for (const child of component.children) {
|
|
524
|
+
await processTree(child, fastify, context, middlewares, prefix);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
case COMPONENT_TYPES.CORS:
|
|
529
|
+
// Configure CORS
|
|
530
|
+
try {
|
|
531
|
+
const cors = await import('@fastify/cors');
|
|
532
|
+
await fastify.register(cors.default, {
|
|
533
|
+
origin: component.origin,
|
|
534
|
+
methods: component.methods
|
|
535
|
+
});
|
|
536
|
+
console.log(` ${c.green}✓${c.reset} CORS enabled`);
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
console.warn(` ${c.yellow}⚠${c.reset} CORS skipped (install @fastify/cors)`);
|
|
540
|
+
}
|
|
541
|
+
// Process children
|
|
542
|
+
if (component.children) {
|
|
543
|
+
for (const child of component.children) {
|
|
544
|
+
await processTree(child, fastify, context, middlewares, prefix);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
break;
|
|
548
|
+
case COMPONENT_TYPES.MIDDLEWARE:
|
|
549
|
+
// Add middleware to chain for child routes
|
|
550
|
+
const newMiddlewares = [...middlewares, component.use];
|
|
551
|
+
if (component.children) {
|
|
552
|
+
for (const child of component.children) {
|
|
553
|
+
await processTree(child, fastify, context, newMiddlewares, prefix);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
break;
|
|
557
|
+
case COMPONENT_TYPES.GROUP:
|
|
558
|
+
// Add prefix to child routes
|
|
559
|
+
const newPrefix = prefix + (component.prefix ?? '');
|
|
560
|
+
if (component.children) {
|
|
561
|
+
for (const child of component.children) {
|
|
562
|
+
await processTree(child, fastify, context, middlewares, newPrefix);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
break;
|
|
566
|
+
case COMPONENT_TYPES.ROUTES:
|
|
567
|
+
// File-based routing
|
|
568
|
+
const routesDir = component.dir;
|
|
569
|
+
const routesPrefix = prefix + (component.prefix ?? '');
|
|
570
|
+
await registerRoutes(fastify, {
|
|
571
|
+
routesDir,
|
|
572
|
+
prefix: routesPrefix,
|
|
573
|
+
middleware: middlewares
|
|
574
|
+
}, context);
|
|
575
|
+
break;
|
|
576
|
+
case COMPONENT_TYPES.GET:
|
|
577
|
+
case COMPONENT_TYPES.POST:
|
|
578
|
+
case COMPONENT_TYPES.PUT:
|
|
579
|
+
case COMPONENT_TYPES.DELETE:
|
|
580
|
+
case COMPONENT_TYPES.PATCH:
|
|
581
|
+
// Register route with Fastify
|
|
582
|
+
const fullPath = prefix + component.path;
|
|
583
|
+
const method = component.method;
|
|
584
|
+
const routeHandler = wrapHandler(component.handler, middlewares, context);
|
|
585
|
+
if (component.schema) {
|
|
586
|
+
fastify.route({
|
|
587
|
+
method,
|
|
588
|
+
url: fullPath,
|
|
589
|
+
handler: routeHandler,
|
|
590
|
+
schema: component.schema
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
fastify.route({
|
|
595
|
+
method,
|
|
596
|
+
url: fullPath,
|
|
597
|
+
handler: routeHandler
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
const m = component.method;
|
|
601
|
+
console.log(` ${c.dim}├${c.reset} ${methodColor(m)}${m.padEnd(6)}${c.reset} ${c.white}${fullPath}${c.reset}`);
|
|
602
|
+
break;
|
|
603
|
+
default:
|
|
604
|
+
// Unknown component, try to process children
|
|
605
|
+
if (component.children) {
|
|
606
|
+
for (const child of component.children) {
|
|
607
|
+
await processTree(child, fastify, context, middlewares, prefix);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* The main render function - turns your JSX tree into a running server
|
|
614
|
+
*/
|
|
615
|
+
export async function render(element) {
|
|
616
|
+
const resolved = resolveElement(element);
|
|
617
|
+
if (!resolved || Array.isArray(resolved) || resolved.__tagliatelle !== COMPONENT_TYPES.SERVER) {
|
|
618
|
+
throw new Error('<Tag>liatelle: Root element must be a <Server> component!');
|
|
619
|
+
}
|
|
620
|
+
const serverComponent = resolved;
|
|
621
|
+
console.log(`
|
|
622
|
+
${c.yellow} ╔══════════════════════════════════════════════════════════════╗
|
|
623
|
+
║ ${c.reset}${c.bold}🍝 <Tag>liatelle.js${c.reset}${c.yellow} ║
|
|
624
|
+
╚══════════════════════════════════════════════════════════════╝${c.reset}
|
|
625
|
+
`);
|
|
626
|
+
// Create Fastify instance with disabled logger (we do our own)
|
|
627
|
+
const fastify = Fastify({
|
|
628
|
+
logger: false, // Completely disable Fastify's logger
|
|
629
|
+
disableRequestLogging: true // We do our own request logging
|
|
630
|
+
});
|
|
631
|
+
// Create context for dependency injection
|
|
632
|
+
const context = new Context();
|
|
633
|
+
// Create our own pretty logger for handlers to use
|
|
634
|
+
const prettyLog = {
|
|
635
|
+
info: (msg) => console.log(` ${c.dim}ℹ${c.reset} ${msg}`),
|
|
636
|
+
warn: (msg) => console.log(` ${c.yellow}⚠${c.reset} ${msg}`),
|
|
637
|
+
error: (msg) => console.log(` ${c.red}✗${c.reset} ${msg}`),
|
|
638
|
+
debug: (msg) => console.log(` ${c.dim}◦${c.reset} ${c.dim}${msg}${c.reset}`),
|
|
639
|
+
};
|
|
640
|
+
// Make prettyLog available via context
|
|
641
|
+
context.set('prettyLog', prettyLog);
|
|
642
|
+
// Custom request logging hook
|
|
643
|
+
fastify.addHook('onResponse', (request, reply, done) => {
|
|
644
|
+
const method = request.method;
|
|
645
|
+
const url = request.url;
|
|
646
|
+
const status = reply.statusCode;
|
|
647
|
+
const time = reply.elapsedTime?.toFixed(0) || '0';
|
|
648
|
+
const methodStr = `${methodColor(method)}${method.padEnd(6)}${c.reset}`;
|
|
649
|
+
const urlStr = `${c.white}${url}${c.reset}`;
|
|
650
|
+
const statusStr = `${statusColor(status)}${status}${c.reset}`;
|
|
651
|
+
const timeStr = `${c.dim}${time}ms${c.reset}`;
|
|
652
|
+
console.log(` ${methodStr} ${urlStr} ${c.dim}→${c.reset} ${statusStr} ${timeStr}`);
|
|
653
|
+
done();
|
|
654
|
+
});
|
|
655
|
+
// Process the tree
|
|
656
|
+
if (serverComponent.children) {
|
|
657
|
+
for (const child of serverComponent.children) {
|
|
658
|
+
await processTree(child, fastify, context, [], '');
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// Start the server
|
|
662
|
+
const port = serverComponent.port;
|
|
663
|
+
const host = serverComponent.host;
|
|
664
|
+
try {
|
|
665
|
+
await fastify.listen({ port, host });
|
|
666
|
+
console.log(` ${c.green}✓${c.reset} ${c.bold}Server ready${c.reset} ${c.dim}→${c.reset} ${c.cyan}http://${host}:${port}${c.reset}
|
|
667
|
+
`);
|
|
668
|
+
}
|
|
669
|
+
catch (err) {
|
|
670
|
+
console.error(` ${c.red}✗${c.reset} ${c.bold}Failed to start:${c.reset}`, err);
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
return fastify;
|
|
674
|
+
}
|
|
675
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
676
|
+
// 🎁 DEFAULT EXPORT
|
|
677
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
678
|
+
const Tagliatelle = {
|
|
679
|
+
h,
|
|
680
|
+
Fragment,
|
|
681
|
+
render
|
|
682
|
+
};
|
|
683
|
+
export default Tagliatelle;
|
|
684
|
+
//# sourceMappingURL=tagliatelle.js.map
|