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.
@@ -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