@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.
- package/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- 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
|
+
}
|