@buenojs/bueno 0.8.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 (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,611 @@
1
+ /**
2
+ * File-based Routing Implementation
3
+ *
4
+ * Provides Next.js-style file-based routing:
5
+ * - pages/ directory scanning
6
+ * - Dynamic routes: [id].tsx, [...slug].tsx
7
+ * - Route generation from file structure
8
+ * - Integration with existing Router
9
+ */
10
+
11
+ import { createLogger, type Logger } from "../logger/index.js";
12
+ import { Router } from "../router/index.js";
13
+ import type {
14
+ FileRouterConfig,
15
+ PartialFileRouterConfig,
16
+ RouteDefinition,
17
+ RouteMatch,
18
+ DynamicRoute,
19
+ RouteHandler,
20
+ RouteMiddleware,
21
+ FileRouteOptions,
22
+ RouteType,
23
+ } from "./types.js";
24
+ import type { SSRPage } from "./types.js";
25
+
26
+ // ============= Constants =============
27
+
28
+ const DEFAULT_PAGES_DIR = "pages";
29
+ const DEFAULT_API_DIR = "api";
30
+ const SUPPORTED_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
31
+
32
+ // ============= File Router Class =============
33
+
34
+ /**
35
+ * File-based router that scans directories and generates routes
36
+ *
37
+ * Features:
38
+ * - Next.js-style file-based routing
39
+ * - Dynamic routes with [param] syntax
40
+ * - Catch-all routes with [...param] syntax
41
+ * - API routes in pages/api/
42
+ * - Hot reload support
43
+ */
44
+ export class FileRouter {
45
+ private config: FileRouterConfig;
46
+ private logger: Logger;
47
+ private routes: Map<string, RouteDefinition> = new Map();
48
+ private dynamicRoutes: DynamicRoute[] = [];
49
+ private router: Router;
50
+ private pageModules: Map<string, SSRPage> = new Map();
51
+ private apiHandlers: Map<string, Record<string, RouteHandler>> = new Map();
52
+
53
+ constructor(config: PartialFileRouterConfig = {}) {
54
+ this.config = this.normalizeConfig(config);
55
+ this.logger = createLogger({
56
+ level: "debug",
57
+ pretty: true,
58
+ context: { component: "FileRouter" },
59
+ });
60
+ this.router = new Router();
61
+ }
62
+
63
+ /**
64
+ * Normalize partial config to full config with defaults
65
+ */
66
+ private normalizeConfig(config: PartialFileRouterConfig): FileRouterConfig {
67
+ return {
68
+ pagesDir: config.pagesDir ?? DEFAULT_PAGES_DIR,
69
+ apiDir: config.apiDir ?? DEFAULT_API_DIR,
70
+ rootDir: config.rootDir ?? process.cwd(),
71
+ extensions: config.extensions ?? SUPPORTED_EXTENSIONS,
72
+ watch: config.watch ?? false,
73
+ ignore: config.ignore ?? ["node_modules", ".git", "dist", "build"],
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Initialize the file router by scanning the pages directory
79
+ */
80
+ async init(): Promise<void> {
81
+ this.logger.info(`Initializing file router from: ${this.config.pagesDir}`);
82
+ await this.scanPagesDirectory();
83
+ this.buildRouter();
84
+ this.logger.info(`Loaded ${this.routes.size} routes`);
85
+ }
86
+
87
+ /**
88
+ * Scan the pages directory for route files
89
+ */
90
+ private async scanPagesDirectory(): Promise<void> {
91
+ const pagesPath = this.config.pagesDir;
92
+ const glob = new Bun.Glob(`**/*{${this.config.extensions.join(",")}}`);
93
+
94
+ try {
95
+ for await (const file of glob.scan(pagesPath)) {
96
+ await this.processRouteFile(file, pagesPath);
97
+ }
98
+ } catch (error) {
99
+ this.logger.error(`Failed to scan pages directory: ${pagesPath}`, error);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Process a single route file
105
+ */
106
+ private async processRouteFile(filePath: string, basePath: string): Promise<void> {
107
+ const fullPath = `${basePath}/${filePath}`;
108
+ const routePath = this.filePathToRoute(filePath);
109
+
110
+ // Determine route type
111
+ const routeType = this.getRouteType(filePath);
112
+
113
+ // Parse route parameters
114
+ const { pattern, params } = this.parseRoutePattern(routePath);
115
+
116
+ // Create route definition
117
+ const route: RouteDefinition = {
118
+ id: this.generateRouteId(filePath),
119
+ path: routePath,
120
+ pattern,
121
+ filePath: fullPath,
122
+ type: routeType,
123
+ params,
124
+ regex: this.routeToRegex(routePath),
125
+ };
126
+
127
+ // Store route
128
+ this.routes.set(routePath, route);
129
+
130
+ // If dynamic route, add to dynamic routes list
131
+ if (params.length > 0) {
132
+ this.dynamicRoutes.push({
133
+ ...route,
134
+ paramNames: params,
135
+ });
136
+ }
137
+
138
+ this.logger.debug(`Processed route: ${routePath} (${routeType})`);
139
+ }
140
+
141
+ /**
142
+ * Convert file path to route path
143
+ */
144
+ private filePathToRoute(filePath: string): string {
145
+ // Remove file extension
146
+ let route = filePath.replace(/\.(tsx?|jsx?)$/, "");
147
+
148
+ // Convert index to root
149
+ if (route === "index") {
150
+ return "/";
151
+ }
152
+
153
+ // Handle nested index files
154
+ if (route.endsWith("/index")) {
155
+ route = route.replace(/\/index$/, "");
156
+ }
157
+
158
+ // Ensure leading slash
159
+ if (!route.startsWith("/")) {
160
+ route = "/" + route;
161
+ }
162
+
163
+ return route;
164
+ }
165
+
166
+ /**
167
+ * Determine route type from file path
168
+ */
169
+ private getRouteType(filePath: string): RouteType {
170
+ if (filePath.startsWith(`${this.config.apiDir}/`) || filePath.startsWith("api/")) {
171
+ return "api";
172
+ }
173
+ return "page";
174
+ }
175
+
176
+ /**
177
+ * Parse route pattern and extract parameters
178
+ */
179
+ private parseRoutePattern(routePath: string): { pattern: string; params: string[] } {
180
+ const params: string[] = [];
181
+ let pattern = routePath;
182
+
183
+ // Match [param] - single parameter
184
+ const singleParamRegex = /\[([^\]]+)\]/g;
185
+ let match;
186
+
187
+ while ((match = singleParamRegex.exec(routePath)) !== null) {
188
+ const paramName = match[1];
189
+ if (!paramName.startsWith("...")) {
190
+ params.push(paramName);
191
+ }
192
+ }
193
+
194
+ // Match [...param] - catch-all parameter
195
+ const catchAllRegex = /\[\.\.\.([^\]]+)\]/g;
196
+ while ((match = catchAllRegex.exec(routePath)) !== null) {
197
+ params.push(match[1]);
198
+ }
199
+
200
+ return { pattern, params };
201
+ }
202
+
203
+ /**
204
+ * Convert route path to regex
205
+ */
206
+ private routeToRegex(routePath: string): RegExp {
207
+ let regex = routePath;
208
+
209
+ // Escape special regex characters except our param syntax
210
+ regex = regex.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
211
+
212
+ // Replace catch-all params [...param] with capture group
213
+ regex = regex.replace(/\\\[\\\.\.\.([^\]]+)\\\]/g, "(.*)");
214
+
215
+ // Replace single params [param] with capture group
216
+ regex = regex.replace(/\\\[([^\]]+)\\\]/g, "([^/]+)");
217
+
218
+ // Ensure exact match
219
+ return new RegExp(`^${regex}$`);
220
+ }
221
+
222
+ /**
223
+ * Generate unique route ID
224
+ */
225
+ private generateRouteId(filePath: string): string {
226
+ return filePath.replace(/[\/\\.]/g, "-").replace(/^-/, "");
227
+ }
228
+
229
+ /**
230
+ * Build the router from collected routes
231
+ */
232
+ private buildRouter(): void {
233
+ // Sort routes by specificity (static routes first, then dynamic)
234
+ const sortedRoutes = this.sortRoutesBySpecificity();
235
+
236
+ for (const route of sortedRoutes) {
237
+ if (route.type === "page") {
238
+ this.router.get(route.path, this.createPageHandler(route) as import("../types").RouteHandler);
239
+ } else if (route.type === "api") {
240
+ this.registerApiRoute(route);
241
+ }
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Sort routes by specificity
247
+ * Static routes come before dynamic routes
248
+ */
249
+ private sortRoutesBySpecificity(): RouteDefinition[] {
250
+ return Array.from(this.routes.values()).sort((a, b) => {
251
+ // Static routes first
252
+ if (a.params.length === 0 && b.params.length > 0) return -1;
253
+ if (a.params.length > 0 && b.params.length === 0) return 1;
254
+
255
+ // More specific routes first (fewer params)
256
+ if (a.params.length !== b.params.length) {
257
+ return a.params.length - b.params.length;
258
+ }
259
+
260
+ // Alphabetical by path
261
+ return a.path.localeCompare(b.path);
262
+ });
263
+ }
264
+
265
+ /**
266
+ * Create page handler for a route
267
+ */
268
+ private createPageHandler(route: RouteDefinition): RouteHandler {
269
+ return async (request: Request) => {
270
+ const module = await this.loadPageModule(route.filePath);
271
+ if (!module) {
272
+ return new Response("Page not found", { status: 404 });
273
+ }
274
+
275
+ // Extract params from request
276
+ const url = new URL(request.url);
277
+ const params = this.extractParams(url.pathname, route);
278
+
279
+ // Call the page's render method or default handler
280
+ if (module.render) {
281
+ const ctx = this.createContext(request, params);
282
+ const result = await module.render(ctx);
283
+ return new Response(result.html, {
284
+ headers: { "Content-Type": "text/html" },
285
+ status: result.status,
286
+ });
287
+ }
288
+
289
+ return new Response("Page handler not implemented", { status: 500 });
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Register API route with all HTTP methods
295
+ */
296
+ private async registerApiRoute(route: RouteDefinition): Promise<void> {
297
+ const module = await this.loadApiModule(route.filePath);
298
+ if (!module) return;
299
+
300
+ const handlers: Record<string, RouteHandler> = {};
301
+ const methodMap: Record<string, (pattern: string, handler: import("../types").RouteHandler) => void> = {
302
+ GET: this.router.get.bind(this.router),
303
+ POST: this.router.post.bind(this.router),
304
+ PUT: this.router.put.bind(this.router),
305
+ PATCH: this.router.patch.bind(this.router),
306
+ DELETE: this.router.delete.bind(this.router),
307
+ HEAD: this.router.head.bind(this.router),
308
+ OPTIONS: this.router.options.bind(this.router),
309
+ };
310
+
311
+ for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] as const) {
312
+ if (module[method]) {
313
+ handlers[method] = module[method];
314
+ methodMap[method](route.path, this.createApiHandler(route, method) as import("../types").RouteHandler);
315
+ }
316
+ }
317
+
318
+ this.apiHandlers.set(route.path, handlers);
319
+ }
320
+
321
+ /**
322
+ * Create API handler for a route and method
323
+ */
324
+ private createApiHandler(route: RouteDefinition, method: string): RouteHandler {
325
+ return async (request: Request) => {
326
+ const module = await this.loadApiModule(route.filePath);
327
+ if (!module || !module[method]) {
328
+ return new Response("Method not allowed", { status: 405 });
329
+ }
330
+
331
+ const url = new URL(request.url);
332
+ const params = this.extractParams(url.pathname, route);
333
+ const ctx = this.createContext(request, params);
334
+
335
+ return module[method](ctx as unknown as Request);
336
+ };
337
+ }
338
+
339
+ /**
340
+ * Load page module dynamically
341
+ */
342
+ private async loadPageModule(filePath: string): Promise<SSRPage | null> {
343
+ if (this.pageModules.has(filePath)) {
344
+ return this.pageModules.get(filePath)!;
345
+ }
346
+
347
+ try {
348
+ const module = await import(filePath);
349
+ this.pageModules.set(filePath, module.default || module);
350
+ return module.default || module;
351
+ } catch (error) {
352
+ this.logger.error(`Failed to load page module: ${filePath}`, error);
353
+ return null;
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Load API module dynamically
359
+ */
360
+ private async loadApiModule(filePath: string): Promise<Record<string, RouteHandler> | null> {
361
+ try {
362
+ const module = await import(filePath);
363
+ return module;
364
+ } catch (error) {
365
+ this.logger.error(`Failed to load API module: ${filePath}`, error);
366
+ return null;
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Extract params from URL using route definition
372
+ */
373
+ private extractParams(pathname: string, route: RouteDefinition): Record<string, string> {
374
+ const params: Record<string, string> = {};
375
+ const match = pathname.match(route.regex);
376
+
377
+ if (match && route.params.length > 0) {
378
+ for (let i = 0; i < route.params.length; i++) {
379
+ params[route.params[i]] = match[i + 1];
380
+ }
381
+ }
382
+
383
+ return params;
384
+ }
385
+
386
+ /**
387
+ * Create context for request
388
+ */
389
+ private createContext(request: Request, params: Record<string, string> = {}): import("./types").SSRContext {
390
+ const url = new URL(request.url);
391
+ return {
392
+ request,
393
+ url: request.url,
394
+ pathname: url.pathname,
395
+ query: url.searchParams,
396
+ params,
397
+ headers: new Headers(),
398
+ status: 200,
399
+ head: [],
400
+ body: [],
401
+ data: {},
402
+ modules: new Set<string>(),
403
+ };
404
+ }
405
+
406
+ /**
407
+ * Match a request to a route
408
+ */
409
+ match(method: string, pathname: string): RouteMatch | null {
410
+ // Try static routes first
411
+ const staticRoute = this.routes.get(pathname);
412
+ if (staticRoute) {
413
+ return {
414
+ route: staticRoute,
415
+ params: {},
416
+ };
417
+ }
418
+
419
+ // Try dynamic routes
420
+ for (const route of this.dynamicRoutes) {
421
+ const match = pathname.match(route.regex);
422
+ if (match) {
423
+ const params: Record<string, string> = {};
424
+ for (let i = 0; i < route.paramNames.length; i++) {
425
+ params[route.paramNames[i]] = match[i + 1];
426
+ }
427
+ return {
428
+ route,
429
+ params,
430
+ };
431
+ }
432
+ }
433
+
434
+ return null;
435
+ }
436
+
437
+ /**
438
+ * Handle incoming request
439
+ */
440
+ async handle(request: Request): Promise<Response> {
441
+ const url = new URL(request.url);
442
+ const match = this.match(request.method, url.pathname);
443
+
444
+ if (!match) {
445
+ return new Response("Not Found", { status: 404 });
446
+ }
447
+
448
+ const handler = this.createHandler(match.route, request.method);
449
+ return handler(request);
450
+ }
451
+
452
+ /**
453
+ * Create handler for route
454
+ */
455
+ private createHandler(route: RouteDefinition, method: string): RouteHandler {
456
+ if (route.type === "api") {
457
+ return this.createApiHandler(route, method);
458
+ }
459
+ return this.createPageHandler(route);
460
+ }
461
+
462
+ /**
463
+ * Get all routes
464
+ */
465
+ getRoutes(): RouteDefinition[] {
466
+ return Array.from(this.routes.values());
467
+ }
468
+
469
+ /**
470
+ * Get route by path
471
+ */
472
+ getRoute(path: string): RouteDefinition | undefined {
473
+ return this.routes.get(path);
474
+ }
475
+
476
+ /**
477
+ * Get dynamic routes
478
+ */
479
+ getDynamicRoutes(): DynamicRoute[] {
480
+ return this.dynamicRoutes;
481
+ }
482
+
483
+ /**
484
+ * Generate URL for a route
485
+ */
486
+ generateUrl(routeId: string, params: Record<string, string> = {}): string | null {
487
+ for (const route of this.routes.values()) {
488
+ if (route.id === routeId) {
489
+ let url = route.path;
490
+ for (const [key, value] of Object.entries(params)) {
491
+ url = url.replace(`[${key}]`, value);
492
+ url = url.replace(`[...${key}]`, value);
493
+ }
494
+ return url;
495
+ }
496
+ }
497
+ return null;
498
+ }
499
+
500
+ /**
501
+ * Reload routes (for hot reload)
502
+ */
503
+ async reload(): Promise<void> {
504
+ this.logger.info("Reloading routes...");
505
+ this.routes.clear();
506
+ this.dynamicRoutes = [];
507
+ this.pageModules.clear();
508
+ this.apiHandlers.clear();
509
+ this.router = new Router();
510
+ await this.init();
511
+ }
512
+
513
+ /**
514
+ * Get the underlying router
515
+ */
516
+ getRouter(): Router {
517
+ return this.router;
518
+ }
519
+
520
+ /**
521
+ * Get configuration
522
+ */
523
+ getConfig(): FileRouterConfig {
524
+ return { ...this.config };
525
+ }
526
+ }
527
+
528
+ // ============= Factory Function =============
529
+
530
+ /**
531
+ * Create a file router
532
+ */
533
+ export function createFileRouter(config: PartialFileRouterConfig = {}): FileRouter {
534
+ return new FileRouter(config);
535
+ }
536
+
537
+ // ============= Utility Functions =============
538
+
539
+ /**
540
+ * Check if a path is a dynamic route
541
+ */
542
+ export function isDynamicRoute(path: string): boolean {
543
+ return /\[.*\]/.test(path);
544
+ }
545
+
546
+ /**
547
+ * Check if a path is a catch-all route
548
+ */
549
+ export function isCatchAllRoute(path: string): boolean {
550
+ return /\[\.\.\..*\]/.test(path);
551
+ }
552
+
553
+ /**
554
+ * Get parameter names from route path
555
+ */
556
+ export function getRouteParams(path: string): string[] {
557
+ const params: string[] = [];
558
+ const regex = /\[([^\]]+)\]/g;
559
+ let match;
560
+
561
+ while ((match = regex.exec(path)) !== null) {
562
+ let param = match[1];
563
+ if (param.startsWith("...")) {
564
+ param = param.slice(3);
565
+ }
566
+ params.push(param);
567
+ }
568
+
569
+ return params;
570
+ }
571
+
572
+ /**
573
+ * Normalize route path
574
+ */
575
+ export function normalizeRoutePath(path: string): string {
576
+ // Remove trailing slash except for root
577
+ if (path !== "/" && path.endsWith("/")) {
578
+ path = path.slice(0, -1);
579
+ }
580
+
581
+ // Ensure leading slash
582
+ if (!path.startsWith("/")) {
583
+ path = "/" + path;
584
+ }
585
+
586
+ return path;
587
+ }
588
+
589
+ /**
590
+ * Compare route specificity
591
+ * Returns negative if a is more specific, positive if b is more specific
592
+ */
593
+ export function compareRouteSpecificity(a: string, b: string): number {
594
+ const aParams = getRouteParams(a);
595
+ const bParams = getRouteParams(b);
596
+
597
+ // Fewer params = more specific
598
+ if (aParams.length !== bParams.length) {
599
+ return aParams.length - bParams.length;
600
+ }
601
+
602
+ // Catch-all routes are less specific
603
+ const aCatchAll = isCatchAllRoute(a);
604
+ const bCatchAll = isCatchAllRoute(b);
605
+
606
+ if (aCatchAll && !bCatchAll) return 1;
607
+ if (!aCatchAll && bCatchAll) return -1;
608
+
609
+ // Alphabetical comparison
610
+ return a.localeCompare(b);
611
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Framework Build Configurations
3
+ *
4
+ * Exports framework-specific build configurations for all supported frameworks.
5
+ * Each framework module provides configuration for JSX runtime, plugins, and defines.
6
+ */
7
+
8
+ import type { FrontendFramework, FrameworkBuildConfig } from "../types.js";
9
+
10
+ // Import framework configurations for internal use
11
+ import { getReactBuildConfig, reactFrameworkMeta } from "./react.js";
12
+ import { getVueBuildConfig, vueFrameworkMeta } from "./vue.js";
13
+ import { getSvelteBuildConfig, svelteFrameworkMeta } from "./svelte.js";
14
+ import { getSolidBuildConfig, solidFrameworkMeta } from "./solid.js";
15
+
16
+ // Re-export framework configurations
17
+ export { getReactBuildConfig, isReactComponent, getReactRefreshPreamble, reactFrameworkMeta } from "./react.js";
18
+ export { getVueBuildConfig, isVueComponent, isVueJsx, getVueBlockTypes, vueFrameworkMeta } from "./vue.js";
19
+ export { getSvelteBuildConfig, isSvelteComponent, getSveltePreprocessConfig, getSvelteCompilerOptions, svelteFrameworkMeta } from "./svelte.js";
20
+ export { getSolidBuildConfig, isSolidComponent, getSolidRefreshPreamble, getSolidTransformOptions, solidFrameworkMeta } from "./solid.js";
21
+
22
+ // Framework metadata types
23
+ export interface FrameworkMeta {
24
+ name: FrontendFramework;
25
+ displayName: string;
26
+ fileExtensions: string[];
27
+ componentExtensions: string[];
28
+ needsRefreshRuntime: boolean;
29
+ supportsHMR: boolean;
30
+ supportsSSR: boolean;
31
+ }
32
+
33
+ /**
34
+ * Get framework build configuration by framework name
35
+ */
36
+ export function getFrameworkConfig(framework: FrontendFramework): FrameworkBuildConfig {
37
+ switch (framework) {
38
+ case "react":
39
+ return getReactBuildConfig();
40
+ case "vue":
41
+ return getVueBuildConfig();
42
+ case "svelte":
43
+ return getSvelteBuildConfig();
44
+ case "solid":
45
+ return getSolidBuildConfig();
46
+ default:
47
+ // Default to React configuration
48
+ return getReactBuildConfig();
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Get framework metadata by framework name
54
+ */
55
+ export function getFrameworkMeta(framework: FrontendFramework): FrameworkMeta {
56
+ switch (framework) {
57
+ case "react":
58
+ return reactFrameworkMeta;
59
+ case "vue":
60
+ return vueFrameworkMeta;
61
+ case "svelte":
62
+ return svelteFrameworkMeta;
63
+ case "solid":
64
+ return solidFrameworkMeta;
65
+ default:
66
+ return reactFrameworkMeta;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Detect framework from file extension
72
+ */
73
+ export function detectFrameworkFromExtension(filePath: string): FrontendFramework | null {
74
+ if (filePath.endsWith(".vue")) {
75
+ return "vue";
76
+ }
77
+ if (filePath.endsWith(".svelte")) {
78
+ return "svelte";
79
+ }
80
+ // Both React and Solid use .jsx/.tsx, need package.json to distinguish
81
+ // Default to React for JSX files
82
+ if (/\.(j|t)sx$/.test(filePath)) {
83
+ return "react";
84
+ }
85
+ return null;
86
+ }
87
+
88
+ /**
89
+ * Get all supported file extensions
90
+ */
91
+ export function getAllSupportedExtensions(): string[] {
92
+ return [
93
+ ...reactFrameworkMeta.fileExtensions,
94
+ ...vueFrameworkMeta.fileExtensions,
95
+ ...svelteFrameworkMeta.fileExtensions,
96
+ ...solidFrameworkMeta.fileExtensions,
97
+ ];
98
+ }
99
+
100
+ /**
101
+ * Check if a file extension is supported
102
+ */
103
+ export function isSupportedExtension(filePath: string): boolean {
104
+ const extensions = getAllSupportedExtensions();
105
+ return extensions.some((ext) => filePath.endsWith(ext));
106
+ }