kilatjs 0.1.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/LICENSE +21 -0
- package/README.md +438 -0
- package/dist/adapters/htmx.d.ts +71 -0
- package/dist/adapters/react.d.ts +7 -0
- package/dist/cli.d.ts +2 -0
- package/dist/core/router.d.ts +36 -0
- package/dist/core/types.d.ts +74 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +1046 -0
- package/dist/server/server.d.ts +17 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
// src/adapters/react.ts
|
|
2
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
class ReactAdapter {
|
|
6
|
+
static async renderToString(component, props) {
|
|
7
|
+
try {
|
|
8
|
+
const element = React.createElement(component, props);
|
|
9
|
+
const html = renderToStaticMarkup(element);
|
|
10
|
+
return html;
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error("Error rendering React component:", error);
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
static createDocument(html, meta = {}, config) {
|
|
17
|
+
const title = meta.title || "KilatJS App";
|
|
18
|
+
const description = meta.description || "";
|
|
19
|
+
const robots = meta.robots || "index,follow";
|
|
20
|
+
let metaTags = "";
|
|
21
|
+
metaTags += `<meta charset="utf-8" />
|
|
22
|
+
`;
|
|
23
|
+
metaTags += `<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
24
|
+
`;
|
|
25
|
+
metaTags += `<meta name="robots" content="${robots}" />
|
|
26
|
+
`;
|
|
27
|
+
if (description) {
|
|
28
|
+
metaTags += `<meta name="description" content="${this.escapeHtml(description)}" />
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
if (meta.canonical) {
|
|
32
|
+
metaTags += `<link rel="canonical" href="${this.escapeHtml(meta.canonical)}" />
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
metaTags += `<meta property="og:title" content="${this.escapeHtml(meta.ogTitle || title)}" />
|
|
36
|
+
`;
|
|
37
|
+
if (meta.ogDescription || description) {
|
|
38
|
+
metaTags += `<meta property="og:description" content="${this.escapeHtml(meta.ogDescription || description)}" />
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
if (meta.ogImage) {
|
|
42
|
+
metaTags += `<meta property="og:image" content="${this.escapeHtml(meta.ogImage)}" />
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
45
|
+
const twitterCard = meta.twitterCard || "summary";
|
|
46
|
+
metaTags += `<meta name="twitter:card" content="${twitterCard}" />
|
|
47
|
+
`;
|
|
48
|
+
const reservedKeys = [
|
|
49
|
+
"title",
|
|
50
|
+
"description",
|
|
51
|
+
"robots",
|
|
52
|
+
"canonical",
|
|
53
|
+
"ogTitle",
|
|
54
|
+
"ogDescription",
|
|
55
|
+
"ogImage",
|
|
56
|
+
"twitterCard"
|
|
57
|
+
];
|
|
58
|
+
Object.entries(meta).forEach(([key, value]) => {
|
|
59
|
+
if (!reservedKeys.includes(key) && typeof value === "string") {
|
|
60
|
+
metaTags += `<meta name="${this.escapeHtml(key)}" content="${this.escapeHtml(value)}" />
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const cssUrl = config?.dev ? `/styles.css?v=${Date.now()}` : "/styles.css";
|
|
65
|
+
metaTags += `<link rel="stylesheet" href="${cssUrl}" />
|
|
66
|
+
`;
|
|
67
|
+
metaTags += `<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
68
|
+
`;
|
|
69
|
+
metaTags += `<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
70
|
+
`;
|
|
71
|
+
metaTags += `<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
|
72
|
+
`;
|
|
73
|
+
return `<!DOCTYPE html>
|
|
74
|
+
<html lang="en">
|
|
75
|
+
<head>
|
|
76
|
+
<title>${this.escapeHtml(title)}</title>
|
|
77
|
+
${metaTags}
|
|
78
|
+
</head>
|
|
79
|
+
<body>
|
|
80
|
+
<div id="root">
|
|
81
|
+
${html}
|
|
82
|
+
</div>
|
|
83
|
+
${config?.dev ? `<script>
|
|
84
|
+
(function() {
|
|
85
|
+
let currentServerId = null;
|
|
86
|
+
let isReconnecting = false;
|
|
87
|
+
|
|
88
|
+
function connect() {
|
|
89
|
+
const source = new EventSource('/_kilat/live-reload');
|
|
90
|
+
|
|
91
|
+
source.onmessage = (event) => {
|
|
92
|
+
const newServerId = event.data;
|
|
93
|
+
if (currentServerId === null) {
|
|
94
|
+
currentServerId = newServerId;
|
|
95
|
+
} else if (currentServerId !== newServerId) {
|
|
96
|
+
// Server ID changed, reload!
|
|
97
|
+
location.reload();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
source.onerror = () => {
|
|
102
|
+
source.close();
|
|
103
|
+
// Try to reconnect in 1s
|
|
104
|
+
setTimeout(connect, 1000);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
connect();
|
|
109
|
+
})();
|
|
110
|
+
</script>` : ""}
|
|
111
|
+
</body>
|
|
112
|
+
</html>`;
|
|
113
|
+
}
|
|
114
|
+
static escapeHtml(str) {
|
|
115
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/adapters/htmx.ts
|
|
120
|
+
class HTMXAdapter {
|
|
121
|
+
static async renderToString(template, props) {
|
|
122
|
+
try {
|
|
123
|
+
const html = await template(props);
|
|
124
|
+
return html;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error("Error rendering HTMX template:", error);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
static isHTMXRequest(request) {
|
|
131
|
+
return request.headers.get("HX-Request") === "true";
|
|
132
|
+
}
|
|
133
|
+
static isBoostedRequest(request) {
|
|
134
|
+
return request.headers.get("HX-Boosted") === "true";
|
|
135
|
+
}
|
|
136
|
+
static getCurrentUrl(request) {
|
|
137
|
+
return request.headers.get("HX-Current-URL");
|
|
138
|
+
}
|
|
139
|
+
static getTrigger(request) {
|
|
140
|
+
return request.headers.get("HX-Trigger");
|
|
141
|
+
}
|
|
142
|
+
static getTriggerName(request) {
|
|
143
|
+
return request.headers.get("HX-Trigger-Name");
|
|
144
|
+
}
|
|
145
|
+
static getTarget(request) {
|
|
146
|
+
return request.headers.get("HX-Target");
|
|
147
|
+
}
|
|
148
|
+
static createResponse(html, options = {}) {
|
|
149
|
+
const headers = new Headers({
|
|
150
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
151
|
+
});
|
|
152
|
+
if (options.retarget) {
|
|
153
|
+
headers.set("HX-Retarget", options.retarget);
|
|
154
|
+
}
|
|
155
|
+
if (options.reswap) {
|
|
156
|
+
headers.set("HX-Reswap", options.reswap);
|
|
157
|
+
}
|
|
158
|
+
if (options.trigger) {
|
|
159
|
+
headers.set("HX-Trigger", options.trigger);
|
|
160
|
+
}
|
|
161
|
+
if (options.triggerAfterSettle) {
|
|
162
|
+
headers.set("HX-Trigger-After-Settle", options.triggerAfterSettle);
|
|
163
|
+
}
|
|
164
|
+
if (options.triggerAfterSwap) {
|
|
165
|
+
headers.set("HX-Trigger-After-Swap", options.triggerAfterSwap);
|
|
166
|
+
}
|
|
167
|
+
if (options.redirect) {
|
|
168
|
+
headers.set("HX-Redirect", options.redirect);
|
|
169
|
+
}
|
|
170
|
+
if (options.refresh) {
|
|
171
|
+
headers.set("HX-Refresh", "true");
|
|
172
|
+
}
|
|
173
|
+
if (options.pushUrl) {
|
|
174
|
+
headers.set("HX-Push-Url", options.pushUrl);
|
|
175
|
+
}
|
|
176
|
+
if (options.replaceUrl) {
|
|
177
|
+
headers.set("HX-Replace-Url", options.replaceUrl);
|
|
178
|
+
}
|
|
179
|
+
return new Response(html, { headers, status: options.status || 200 });
|
|
180
|
+
}
|
|
181
|
+
static redirectResponse(url) {
|
|
182
|
+
return new Response(null, {
|
|
183
|
+
headers: {
|
|
184
|
+
"HX-Redirect": url
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
static refreshResponse() {
|
|
189
|
+
return new Response(null, {
|
|
190
|
+
headers: {
|
|
191
|
+
"HX-Refresh": "true"
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
static createDocument(html, meta = {}, config) {
|
|
196
|
+
const title = meta.title || "KilatJS App";
|
|
197
|
+
const description = meta.description || "";
|
|
198
|
+
const robots = meta.robots || "index,follow";
|
|
199
|
+
let metaTags = "";
|
|
200
|
+
metaTags += `<meta charset="utf-8" />
|
|
201
|
+
`;
|
|
202
|
+
metaTags += `<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
203
|
+
`;
|
|
204
|
+
metaTags += `<meta name="robots" content="${robots}" />
|
|
205
|
+
`;
|
|
206
|
+
if (description) {
|
|
207
|
+
metaTags += `<meta name="description" content="${this.escapeHtml(description)}" />
|
|
208
|
+
`;
|
|
209
|
+
}
|
|
210
|
+
if (meta.canonical) {
|
|
211
|
+
metaTags += `<link rel="canonical" href="${this.escapeHtml(meta.canonical)}" />
|
|
212
|
+
`;
|
|
213
|
+
}
|
|
214
|
+
metaTags += `<meta property="og:title" content="${this.escapeHtml(meta.ogTitle || title)}" />
|
|
215
|
+
`;
|
|
216
|
+
if (meta.ogDescription || description) {
|
|
217
|
+
metaTags += `<meta property="og:description" content="${this.escapeHtml(meta.ogDescription || description)}" />
|
|
218
|
+
`;
|
|
219
|
+
}
|
|
220
|
+
if (meta.ogImage) {
|
|
221
|
+
metaTags += `<meta property="og:image" content="${this.escapeHtml(meta.ogImage)}" />
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
const twitterCard = meta.twitterCard || "summary";
|
|
225
|
+
metaTags += `<meta name="twitter:card" content="${twitterCard}" />
|
|
226
|
+
`;
|
|
227
|
+
const reservedKeys = [
|
|
228
|
+
"title",
|
|
229
|
+
"description",
|
|
230
|
+
"robots",
|
|
231
|
+
"canonical",
|
|
232
|
+
"ogTitle",
|
|
233
|
+
"ogDescription",
|
|
234
|
+
"ogImage",
|
|
235
|
+
"twitterCard"
|
|
236
|
+
];
|
|
237
|
+
Object.entries(meta).forEach(([key, value]) => {
|
|
238
|
+
if (!reservedKeys.includes(key) && typeof value === "string") {
|
|
239
|
+
metaTags += `<meta name="${this.escapeHtml(key)}" content="${this.escapeHtml(value)}" />
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
metaTags += `<link rel="stylesheet" href="/styles.css" />
|
|
244
|
+
`;
|
|
245
|
+
metaTags += `<script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"></script>
|
|
246
|
+
`;
|
|
247
|
+
metaTags += `<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
248
|
+
`;
|
|
249
|
+
metaTags += `<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
250
|
+
`;
|
|
251
|
+
metaTags += `<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
|
252
|
+
`;
|
|
253
|
+
return `<!DOCTYPE html>
|
|
254
|
+
<html lang="en">
|
|
255
|
+
<head>
|
|
256
|
+
<title>${this.escapeHtml(title)}</title>
|
|
257
|
+
${metaTags}
|
|
258
|
+
</head>
|
|
259
|
+
<body hx-boost="true">
|
|
260
|
+
<div id="root">
|
|
261
|
+
${html}
|
|
262
|
+
</div>
|
|
263
|
+
</body>
|
|
264
|
+
</html>`;
|
|
265
|
+
}
|
|
266
|
+
static escapeHtml(str) {
|
|
267
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/core/router.ts
|
|
272
|
+
class Router {
|
|
273
|
+
routes = new Map;
|
|
274
|
+
config;
|
|
275
|
+
staticPaths = new Map;
|
|
276
|
+
serverId = Date.now().toString();
|
|
277
|
+
fsRouter;
|
|
278
|
+
routeCache = new Map;
|
|
279
|
+
preloadedRoutes = new Map;
|
|
280
|
+
routePatterns = [];
|
|
281
|
+
apiRoutes = new Map;
|
|
282
|
+
staticApiResponses = new Map;
|
|
283
|
+
static NOT_FOUND_RESPONSE = new Response("404 Not Found", { status: 404 });
|
|
284
|
+
static METHOD_NOT_ALLOWED_RESPONSE = new Response(JSON.stringify({ error: "Method Not Allowed" }), { status: 405, headers: { "Content-Type": "application/json" } });
|
|
285
|
+
static INTERNAL_ERROR_RESPONSE = new Response(JSON.stringify({ error: "Internal Server Error" }), { status: 500, headers: { "Content-Type": "application/json" } });
|
|
286
|
+
contextPool = [];
|
|
287
|
+
poolIndex = 0;
|
|
288
|
+
constructor(config) {
|
|
289
|
+
this.config = config;
|
|
290
|
+
const routesDir = config.routesDir.startsWith("/") ? config.routesDir : `${process.cwd()}/${config.routesDir}`;
|
|
291
|
+
this.fsRouter = new Bun.FileSystemRouter({
|
|
292
|
+
dir: routesDir,
|
|
293
|
+
style: "nextjs",
|
|
294
|
+
origin: `http://${config.hostname || "localhost"}:${config.port || 3000}`
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
getRoutes() {
|
|
298
|
+
return this.routes;
|
|
299
|
+
}
|
|
300
|
+
getStaticPaths() {
|
|
301
|
+
return this.staticPaths;
|
|
302
|
+
}
|
|
303
|
+
async loadRoutes() {
|
|
304
|
+
if (this.config.dev) {
|
|
305
|
+
this.fsRouter.reload();
|
|
306
|
+
this.routeCache.clear();
|
|
307
|
+
this.preloadedRoutes.clear();
|
|
308
|
+
this.routePatterns.length = 0;
|
|
309
|
+
}
|
|
310
|
+
await this.preloadAllRoutes();
|
|
311
|
+
console.log("\uD83D\uDD04 FileSystemRouter initialized with", this.preloadedRoutes.size, "preloaded routes");
|
|
312
|
+
if (this.config.dev) {
|
|
313
|
+
console.log("\uD83D\uDCCB Preloaded routes:");
|
|
314
|
+
for (const [route, exports] of this.preloadedRoutes.entries()) {
|
|
315
|
+
console.log(` ${route} (${route.includes("[") ? "dynamic" : "static"})`);
|
|
316
|
+
}
|
|
317
|
+
console.log("\uD83D\uDCCB Dynamic route patterns:", this.routePatterns.length);
|
|
318
|
+
for (const pattern of this.routePatterns) {
|
|
319
|
+
console.log(` ${pattern.pattern} -> ${pattern.filePath}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async preloadAllRoutes() {
|
|
324
|
+
const routesDir = this.config.routesDir.startsWith("/") ? this.config.routesDir : `${process.cwd()}/${this.config.routesDir}`;
|
|
325
|
+
await this.scanAndPreloadRoutes(routesDir, "");
|
|
326
|
+
}
|
|
327
|
+
async scanAndPreloadRoutes(dir, basePath) {
|
|
328
|
+
try {
|
|
329
|
+
const proc = Bun.spawn(["find", dir, "-name", "*.ts", "-o", "-name", "*.tsx", "-o", "-name", "*.js", "-o", "-name", "*.jsx"], {
|
|
330
|
+
stdout: "pipe"
|
|
331
|
+
});
|
|
332
|
+
const output = await new Response(proc.stdout).text();
|
|
333
|
+
const files = output.trim().split(`
|
|
334
|
+
`).filter(Boolean);
|
|
335
|
+
for (const filePath of files) {
|
|
336
|
+
const relativePath = filePath.replace(dir, "");
|
|
337
|
+
let routePath = relativePath.replace(/\.(tsx?|jsx?)$/, "");
|
|
338
|
+
if (routePath.endsWith("/index")) {
|
|
339
|
+
routePath = routePath.slice(0, -6) || "/";
|
|
340
|
+
}
|
|
341
|
+
if (!routePath.startsWith("/")) {
|
|
342
|
+
routePath = "/" + routePath;
|
|
343
|
+
}
|
|
344
|
+
const routeType = this.getRouteType(routePath);
|
|
345
|
+
try {
|
|
346
|
+
const routeExports = await import(filePath);
|
|
347
|
+
this.preloadedRoutes.set(routePath, routeExports);
|
|
348
|
+
if (routeType === "api") {
|
|
349
|
+
this.apiRoutes.set(routePath, routeExports);
|
|
350
|
+
}
|
|
351
|
+
if (routePath.includes("[")) {
|
|
352
|
+
const pattern = this.createRoutePattern(routePath);
|
|
353
|
+
if (pattern) {
|
|
354
|
+
this.routePatterns.push({
|
|
355
|
+
pattern: pattern.regex,
|
|
356
|
+
filePath,
|
|
357
|
+
paramNames: pattern.paramNames,
|
|
358
|
+
routeType
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.warn(`Failed to preload route ${filePath}:`, error);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} catch (error) {
|
|
367
|
+
console.warn("Failed to scan routes:", error);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
createRoutePattern(routePath) {
|
|
371
|
+
const paramNames = [];
|
|
372
|
+
let pattern = routePath.replace(/[.*+?^${}|\\]/g, "\\$&");
|
|
373
|
+
pattern = pattern.replace(/\\?\[([^\]]+)\\?\]/g, (match, paramName) => {
|
|
374
|
+
paramNames.push(paramName);
|
|
375
|
+
return "([^/]+)";
|
|
376
|
+
});
|
|
377
|
+
try {
|
|
378
|
+
const regex = new RegExp(`^${pattern}$`);
|
|
379
|
+
console.log(`Created pattern for ${routePath}: ${regex} (params: ${paramNames.join(", ")})`);
|
|
380
|
+
return {
|
|
381
|
+
regex,
|
|
382
|
+
paramNames
|
|
383
|
+
};
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.warn(`Failed to create pattern for ${routePath}:`, error);
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
getRouteType(pathname) {
|
|
390
|
+
if (pathname.startsWith("/api")) {
|
|
391
|
+
return "api";
|
|
392
|
+
}
|
|
393
|
+
if (pathname.includes("[") || pathname.includes(":")) {
|
|
394
|
+
return "dynamic";
|
|
395
|
+
}
|
|
396
|
+
return "static";
|
|
397
|
+
}
|
|
398
|
+
matchRoute(path) {
|
|
399
|
+
if (this.routeCache.has(path)) {
|
|
400
|
+
return this.routeCache.get(path);
|
|
401
|
+
}
|
|
402
|
+
let match = null;
|
|
403
|
+
if (this.preloadedRoutes.has(path)) {
|
|
404
|
+
match = {
|
|
405
|
+
route: path,
|
|
406
|
+
params: {},
|
|
407
|
+
exports: this.preloadedRoutes.get(path),
|
|
408
|
+
routeType: this.getRouteType(path)
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (!match) {
|
|
412
|
+
for (const routePattern of this.routePatterns) {
|
|
413
|
+
const regexMatch = path.match(routePattern.pattern);
|
|
414
|
+
if (regexMatch) {
|
|
415
|
+
const params = {};
|
|
416
|
+
routePattern.paramNames.forEach((name, index) => {
|
|
417
|
+
params[name] = regexMatch[index + 1];
|
|
418
|
+
});
|
|
419
|
+
const routesDir = this.config.routesDir.startsWith("/") ? this.config.routesDir : `${process.cwd()}/${this.config.routesDir}`;
|
|
420
|
+
let routePath = routePattern.filePath.replace(routesDir, "").replace(/\.(tsx?|jsx?)$/, "");
|
|
421
|
+
if (!routePath.startsWith("/")) {
|
|
422
|
+
routePath = "/" + routePath;
|
|
423
|
+
}
|
|
424
|
+
const exports = this.preloadedRoutes.get(routePath);
|
|
425
|
+
if (exports) {
|
|
426
|
+
match = {
|
|
427
|
+
route: path,
|
|
428
|
+
params,
|
|
429
|
+
exports,
|
|
430
|
+
routeType: routePattern.routeType
|
|
431
|
+
};
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
this.routeCache.set(path, match);
|
|
438
|
+
return match;
|
|
439
|
+
}
|
|
440
|
+
pathMatchesPattern(pattern, path) {
|
|
441
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
442
|
+
const pathParts = path.split("/").filter(Boolean);
|
|
443
|
+
if (patternParts.length !== pathParts.length) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
for (let i = 0;i < patternParts.length; i++) {
|
|
447
|
+
const patternPart = patternParts[i];
|
|
448
|
+
const pathPart = pathParts[i];
|
|
449
|
+
if (!patternPart.startsWith("[") && patternPart !== pathPart) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
async handleRequest(request) {
|
|
456
|
+
const url = request.url;
|
|
457
|
+
const pathStart = url.indexOf("/", 8);
|
|
458
|
+
const pathEnd = url.indexOf("?", pathStart);
|
|
459
|
+
const path = pathEnd === -1 ? url.slice(pathStart) : url.slice(pathStart, pathEnd);
|
|
460
|
+
if (path.startsWith("/api/")) {
|
|
461
|
+
return this.handleApiRouteFast(request, path);
|
|
462
|
+
}
|
|
463
|
+
if (path.endsWith(".css") || path === "/favicon.ico" || path === "/_kilat/live-reload") {
|
|
464
|
+
const staticResponse = await this.handleStaticFile(path);
|
|
465
|
+
if (staticResponse) {
|
|
466
|
+
return staticResponse;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (!this.config.dev) {
|
|
470
|
+
const outDir = this.config.outDir || "./dist";
|
|
471
|
+
const filePath = `${outDir}${path === "/" ? "/index.html" : path}`;
|
|
472
|
+
const file = Bun.file(filePath);
|
|
473
|
+
if (await file.exists()) {
|
|
474
|
+
return new Response(file);
|
|
475
|
+
}
|
|
476
|
+
if (!path.endsWith(".html")) {
|
|
477
|
+
const indexFile = Bun.file(`${filePath}/index.html`);
|
|
478
|
+
if (await indexFile.exists()) {
|
|
479
|
+
return new Response(indexFile);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const match = this.matchRoute(path);
|
|
484
|
+
if (!match) {
|
|
485
|
+
return Router.NOT_FOUND_RESPONSE;
|
|
486
|
+
}
|
|
487
|
+
const { params, exports, routeType } = match;
|
|
488
|
+
const context = this.getContextFromPool(request, params, url);
|
|
489
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
490
|
+
const actionHandler = exports[request.method];
|
|
491
|
+
if (actionHandler && typeof actionHandler === "function") {
|
|
492
|
+
try {
|
|
493
|
+
const result = await actionHandler(context);
|
|
494
|
+
this.returnContextToPool(context);
|
|
495
|
+
return result;
|
|
496
|
+
} catch (error) {
|
|
497
|
+
this.returnContextToPool(context);
|
|
498
|
+
console.error(`Error handling ${request.method}:`, error);
|
|
499
|
+
return Router.INTERNAL_ERROR_RESPONSE;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
this.returnContextToPool(context);
|
|
503
|
+
return Router.METHOD_NOT_ALLOWED_RESPONSE;
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
const data = exports.load ? await exports.load(context) : {};
|
|
507
|
+
if (data instanceof Response) {
|
|
508
|
+
this.returnContextToPool(context);
|
|
509
|
+
return data;
|
|
510
|
+
}
|
|
511
|
+
const meta = exports.meta || {};
|
|
512
|
+
const html = await this.renderPage(exports.default, { data, params, state: context.state }, exports.ui, meta);
|
|
513
|
+
const cacheControl = routeType === "dynamic" ? "no-cache" : "public, max-age=3600";
|
|
514
|
+
this.returnContextToPool(context);
|
|
515
|
+
return new Response(html, {
|
|
516
|
+
headers: {
|
|
517
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
518
|
+
"Cache-Control": cacheControl
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
} catch (error) {
|
|
522
|
+
this.returnContextToPool(context);
|
|
523
|
+
if (error instanceof Response) {
|
|
524
|
+
return error;
|
|
525
|
+
}
|
|
526
|
+
console.error("Error rendering page:", error);
|
|
527
|
+
return Router.INTERNAL_ERROR_RESPONSE;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async handleApiRouteFast(request, path) {
|
|
531
|
+
let exports = this.apiRoutes.get(path);
|
|
532
|
+
let params = {};
|
|
533
|
+
if (!exports) {
|
|
534
|
+
for (const routePattern of this.routePatterns) {
|
|
535
|
+
if (routePattern.routeType === "api") {
|
|
536
|
+
const regexMatch = path.match(routePattern.pattern);
|
|
537
|
+
if (regexMatch) {
|
|
538
|
+
routePattern.paramNames.forEach((name, index) => {
|
|
539
|
+
params[name] = regexMatch[index + 1];
|
|
540
|
+
});
|
|
541
|
+
for (const [preloadedPath, preloadedExports] of this.preloadedRoutes.entries()) {
|
|
542
|
+
if (preloadedPath.includes("[") && preloadedPath.startsWith("/api/") && this.pathMatchesPattern(preloadedPath, path)) {
|
|
543
|
+
exports = preloadedExports;
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (exports)
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (!exports) {
|
|
554
|
+
return Router.NOT_FOUND_RESPONSE;
|
|
555
|
+
}
|
|
556
|
+
const method = request.method;
|
|
557
|
+
const handler = exports[method] || exports.default;
|
|
558
|
+
if (!handler || typeof handler !== "function") {
|
|
559
|
+
return Router.METHOD_NOT_ALLOWED_RESPONSE;
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
const context = this.getMinimalApiContext(request, path, params);
|
|
563
|
+
const response = await handler(context);
|
|
564
|
+
if (response instanceof Response) {
|
|
565
|
+
return response;
|
|
566
|
+
}
|
|
567
|
+
return new Response(JSON.stringify(response), {
|
|
568
|
+
headers: { "Content-Type": "application/json" }
|
|
569
|
+
});
|
|
570
|
+
} catch (error) {
|
|
571
|
+
console.error(`API Error [${method} ${path}]:`, error);
|
|
572
|
+
return Router.INTERNAL_ERROR_RESPONSE;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
getContextFromPool(request, params, url) {
|
|
576
|
+
if (this.contextPool.length > 0) {
|
|
577
|
+
const context = this.contextPool.pop();
|
|
578
|
+
context.request = request;
|
|
579
|
+
context.params = params;
|
|
580
|
+
context.query = new URLSearchParams(url.split("?")[1] || "");
|
|
581
|
+
context.state = {};
|
|
582
|
+
return context;
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
request,
|
|
586
|
+
params,
|
|
587
|
+
query: new URLSearchParams(url.split("?")[1] || ""),
|
|
588
|
+
state: {}
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
returnContextToPool(context) {
|
|
592
|
+
if (this.contextPool.length < 100) {
|
|
593
|
+
this.contextPool.push(context);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
getMinimalApiContext(request, path, params = {}) {
|
|
597
|
+
return {
|
|
598
|
+
request,
|
|
599
|
+
params,
|
|
600
|
+
query: new URLSearchParams(request.url.split("?")[1] || ""),
|
|
601
|
+
state: {}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
async handleStaticFile(path) {
|
|
605
|
+
if (path.endsWith(".css")) {
|
|
606
|
+
const cssPath = this.config.tailwind?.cssPath || "./styles.css";
|
|
607
|
+
try {
|
|
608
|
+
const cssFile = Bun.file(cssPath);
|
|
609
|
+
if (await cssFile.exists()) {
|
|
610
|
+
return new Response(cssFile, {
|
|
611
|
+
headers: {
|
|
612
|
+
"Content-Type": "text/css",
|
|
613
|
+
"Cache-Control": "public, max-age=3600"
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
} catch (error) {}
|
|
618
|
+
}
|
|
619
|
+
if (path === "/favicon.ico") {
|
|
620
|
+
return new Response(null, { status: 204 });
|
|
621
|
+
}
|
|
622
|
+
if (path === "/_kilat/live-reload") {
|
|
623
|
+
const serverId = this.serverId;
|
|
624
|
+
return new Response(new ReadableStream({
|
|
625
|
+
start(controller) {
|
|
626
|
+
controller.enqueue(`data: ${serverId}
|
|
627
|
+
|
|
628
|
+
`);
|
|
629
|
+
}
|
|
630
|
+
}), {
|
|
631
|
+
headers: {
|
|
632
|
+
"Content-Type": "text/event-stream",
|
|
633
|
+
"Cache-Control": "no-cache",
|
|
634
|
+
Connection: "keep-alive"
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
create404Response() {
|
|
641
|
+
const html = `<!DOCTYPE html>
|
|
642
|
+
<html lang="en">
|
|
643
|
+
<head>
|
|
644
|
+
<title>404 - Page Not Found</title>
|
|
645
|
+
<meta charset="utf-8" />
|
|
646
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
647
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
648
|
+
</head>
|
|
649
|
+
<body>
|
|
650
|
+
<div id="root">
|
|
651
|
+
<div class="container">
|
|
652
|
+
<h1>404 - Page Not Found</h1>
|
|
653
|
+
<p>The page you're looking for doesn't exist.</p>
|
|
654
|
+
<a href="/">Go back home</a>
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
</body>
|
|
658
|
+
</html>`;
|
|
659
|
+
return new Response(html, {
|
|
660
|
+
status: 404,
|
|
661
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
async renderPage(PageComponent, props, uiFramework = "react", meta = {}) {
|
|
665
|
+
switch (uiFramework) {
|
|
666
|
+
case "react":
|
|
667
|
+
const reactContent = await ReactAdapter.renderToString(PageComponent, props);
|
|
668
|
+
return ReactAdapter.createDocument(reactContent, meta, this.config);
|
|
669
|
+
case "htmx":
|
|
670
|
+
const htmxContent = await HTMXAdapter.renderToString(PageComponent, props);
|
|
671
|
+
return HTMXAdapter.createDocument(htmxContent, meta, this.config);
|
|
672
|
+
default:
|
|
673
|
+
throw new Error(`Unsupported UI framework: ${uiFramework}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// src/server/server.ts
|
|
678
|
+
class KilatServer {
|
|
679
|
+
router;
|
|
680
|
+
config;
|
|
681
|
+
constructor(config) {
|
|
682
|
+
this.config = config;
|
|
683
|
+
this.router = new Router(config);
|
|
684
|
+
}
|
|
685
|
+
async start() {
|
|
686
|
+
if (this.config.tailwind?.enabled) {
|
|
687
|
+
console.log("\uD83C\uDFA8 Building Tailwind CSS...");
|
|
688
|
+
await this.runTailwind(false);
|
|
689
|
+
if (this.config.dev) {
|
|
690
|
+
this.runTailwind(true);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
await this.router.loadRoutes();
|
|
694
|
+
const router = this.router;
|
|
695
|
+
const config = this.config;
|
|
696
|
+
const server = Bun.serve({
|
|
697
|
+
port: config.port || 3000,
|
|
698
|
+
hostname: config.hostname || "localhost",
|
|
699
|
+
async fetch(request) {
|
|
700
|
+
return router.handleRequest(request);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
console.log(`
|
|
704
|
+
\uD83D\uDE80 KilatJS Server Running!
|
|
705
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
706
|
+
Local: http://${server.hostname}:${server.port}
|
|
707
|
+
Mode: ${config.dev ? "Development" : "Production"}
|
|
708
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
709
|
+
`);
|
|
710
|
+
if (config.dev) {
|
|
711
|
+
console.log("\uD83D\uDCC1 Using FileSystemRouter for dynamic route discovery");
|
|
712
|
+
console.log("");
|
|
713
|
+
}
|
|
714
|
+
return server;
|
|
715
|
+
}
|
|
716
|
+
async buildStatic() {
|
|
717
|
+
if (this.config.tailwind?.enabled) {
|
|
718
|
+
await this.runTailwind(false);
|
|
719
|
+
}
|
|
720
|
+
await this.router.loadRoutes();
|
|
721
|
+
console.log(`\uD83D\uDD28 Building for hybrid deployment...
|
|
722
|
+
`);
|
|
723
|
+
await this.ensureDir(this.config.outDir);
|
|
724
|
+
console.log("\uD83D\uDCC4 Static build with FileSystemRouter:");
|
|
725
|
+
console.log("─────────────────────────────────────");
|
|
726
|
+
console.log(" Using Bun's built-in FileSystemRouter for dynamic routing");
|
|
727
|
+
console.log(" Static assets will be copied to output directory");
|
|
728
|
+
await this.copyStaticAssets();
|
|
729
|
+
await this.generateProductionServer();
|
|
730
|
+
console.log(`
|
|
731
|
+
✅ Hybrid Build Complete!
|
|
732
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
733
|
+
Using Bun FileSystemRouter for all routes
|
|
734
|
+
Output: ${import.meta.dir}/../${this.config.outDir}
|
|
735
|
+
|
|
736
|
+
Run 'bun dist/server.js' to start the server.
|
|
737
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
738
|
+
`);
|
|
739
|
+
}
|
|
740
|
+
async generateProductionServer() {
|
|
741
|
+
console.log(`
|
|
742
|
+
⚙️ Generating production server with FileSystemRouter...`);
|
|
743
|
+
const srcDir = this.config.routesDir.replace("/routes", "");
|
|
744
|
+
const srcOutDir = `${this.config.outDir}/src`;
|
|
745
|
+
await this.copyDir(srcDir, srcOutDir);
|
|
746
|
+
console.log(` ✓ src/ (copied with all components and routes)`);
|
|
747
|
+
const serverCode = `#!/usr/bin/env bun
|
|
748
|
+
/**
|
|
749
|
+
* KilatJS Production Server with FileSystemRouter
|
|
750
|
+
* Generated at: ${new Date().toISOString()}
|
|
751
|
+
*
|
|
752
|
+
* Uses Bun's built-in FileSystemRouter for optimal performance
|
|
753
|
+
*/
|
|
754
|
+
|
|
755
|
+
const PORT = process.env.PORT || ${this.config.port || 3000};
|
|
756
|
+
const HOST = process.env.HOST || "${this.config.hostname || "localhost"}";
|
|
757
|
+
const ROOT = import.meta.dir;
|
|
758
|
+
|
|
759
|
+
// Initialize FileSystemRouter
|
|
760
|
+
const fsRouter = new Bun.FileSystemRouter({
|
|
761
|
+
dir: ROOT + "/src/routes",
|
|
762
|
+
style: "nextjs",
|
|
763
|
+
origin: \`http://\${HOST}:\${PORT}\`,
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
function getRouteType(pathname) {
|
|
767
|
+
if (pathname.startsWith("/api")) return "api";
|
|
768
|
+
if (pathname.includes("[") || pathname.includes(":")) return "dynamic";
|
|
769
|
+
return "static";
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// React SSR support
|
|
773
|
+
let React, ReactDOMServer;
|
|
774
|
+
try {
|
|
775
|
+
React = await import("react");
|
|
776
|
+
ReactDOMServer = await import("react-dom/server");
|
|
777
|
+
} catch (e) {
|
|
778
|
+
console.warn("React not available for SSR");
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function renderPage(Component, props, meta = {}) {
|
|
782
|
+
if (!ReactDOMServer || !React) {
|
|
783
|
+
return "<html><body>React not available for SSR</body></html>";
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const content = ReactDOMServer.renderToString(React.createElement(Component, props));
|
|
787
|
+
return \`<!DOCTYPE html>
|
|
788
|
+
<html lang="en">
|
|
789
|
+
<head>
|
|
790
|
+
<meta charset="utf-8" />
|
|
791
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
792
|
+
<title>\${meta.title || "KilatJS App"}</title>
|
|
793
|
+
\${meta.description ? \`<meta name="description" content="\${meta.description}" />\` : ""}
|
|
794
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
795
|
+
</head>
|
|
796
|
+
<body>
|
|
797
|
+
<div id="root">\${content}</div>
|
|
798
|
+
</body>
|
|
799
|
+
</html>\`;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
console.log(\`
|
|
803
|
+
\uD83D\uDE80 KilatJS Production Server (FileSystemRouter)
|
|
804
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
805
|
+
http://\${HOST}:\${PORT}
|
|
806
|
+
Using Bun FileSystemRouter for optimal performance
|
|
807
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
808
|
+
\`);
|
|
809
|
+
|
|
810
|
+
Bun.serve({
|
|
811
|
+
port: PORT,
|
|
812
|
+
hostname: HOST,
|
|
813
|
+
async fetch(req) {
|
|
814
|
+
const url = new URL(req.url);
|
|
815
|
+
const path = url.pathname;
|
|
816
|
+
|
|
817
|
+
// 1. Try static files first
|
|
818
|
+
const staticFile = Bun.file(ROOT + (path === "/" ? "/index.html" : path));
|
|
819
|
+
if (await staticFile.exists()) {
|
|
820
|
+
return new Response(staticFile);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// 2. Try index.html for directories
|
|
824
|
+
const indexFile = Bun.file(ROOT + path + "/index.html");
|
|
825
|
+
if (await indexFile.exists()) {
|
|
826
|
+
return new Response(indexFile);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// 3. Use FileSystemRouter for dynamic routes
|
|
830
|
+
const match = fsRouter.match(path);
|
|
831
|
+
if (!match) {
|
|
832
|
+
console.log(\`No route match found for: \${path}\`);
|
|
833
|
+
return new Response("404 Not Found", { status: 404 });
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
console.log(\`Route matched: \${path} -> \${match.filePath}\`);
|
|
837
|
+
|
|
838
|
+
try {
|
|
839
|
+
// Dynamically import the route
|
|
840
|
+
const routeExports = await import(match.filePath);
|
|
841
|
+
const routeType = getRouteType(match.pathname);
|
|
842
|
+
const params = match.params || {};
|
|
843
|
+
const ctx = {
|
|
844
|
+
request: req,
|
|
845
|
+
params,
|
|
846
|
+
query: url.searchParams,
|
|
847
|
+
state: {}
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
// API routes
|
|
851
|
+
if (routeType === "api") {
|
|
852
|
+
const handler = routeExports[req.method] || routeExports.default;
|
|
853
|
+
if (!handler) {
|
|
854
|
+
return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
|
|
855
|
+
status: 405,
|
|
856
|
+
headers: { "Content-Type": "application/json" }
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const result = await handler(ctx);
|
|
861
|
+
if (result instanceof Response) return result;
|
|
862
|
+
|
|
863
|
+
return new Response(JSON.stringify(result), {
|
|
864
|
+
headers: { "Content-Type": "application/json" }
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Page routes - handle HTTP methods
|
|
869
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
870
|
+
const handler = routeExports[req.method];
|
|
871
|
+
if (handler) return await handler(ctx);
|
|
872
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// GET requests - SSR
|
|
876
|
+
const data = routeExports.load ? await routeExports.load(ctx) : {};
|
|
877
|
+
if (data instanceof Response) return data;
|
|
878
|
+
|
|
879
|
+
const html = await renderPage(
|
|
880
|
+
routeExports.default,
|
|
881
|
+
{ data, params, state: {} },
|
|
882
|
+
routeExports.meta || {}
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
const cacheControl = routeType === "dynamic" ? "no-cache" : "public, max-age=3600";
|
|
886
|
+
|
|
887
|
+
return new Response(html, {
|
|
888
|
+
headers: {
|
|
889
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
890
|
+
"Cache-Control": cacheControl
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
} catch (error) {
|
|
895
|
+
if (error instanceof Response) return error;
|
|
896
|
+
console.error("Route error:", error);
|
|
897
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
`;
|
|
902
|
+
const serverPath = `${this.config.outDir}/server.js`;
|
|
903
|
+
await Bun.write(serverPath, serverCode);
|
|
904
|
+
console.log(` ✓ server.js (FileSystemRouter-based)`);
|
|
905
|
+
}
|
|
906
|
+
formatSize(bytes) {
|
|
907
|
+
if (bytes < 1024)
|
|
908
|
+
return `${bytes} B`;
|
|
909
|
+
if (bytes < 1024 * 1024)
|
|
910
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
911
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
912
|
+
}
|
|
913
|
+
async runTailwind(watch) {
|
|
914
|
+
const { inputPath, cssPath } = this.config.tailwind || {};
|
|
915
|
+
if (!inputPath || !cssPath) {
|
|
916
|
+
console.warn("⚠️ Tailwind enabled but inputPath or cssPath missing");
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const resolvedInputPath = inputPath.startsWith("/") ? inputPath : `${process.cwd()}/${inputPath}`;
|
|
920
|
+
const resolvedCssPath = cssPath.startsWith("/") ? cssPath : `${process.cwd()}/${cssPath}`;
|
|
921
|
+
const args = ["bunx", "@tailwindcss/cli", "-i", resolvedInputPath, "-o", resolvedCssPath];
|
|
922
|
+
if (watch) {
|
|
923
|
+
args.push("--watch");
|
|
924
|
+
}
|
|
925
|
+
console.log(`\uD83C\uDFA8 ${watch ? "Watching" : "Building"} Tailwind CSS...`);
|
|
926
|
+
try {
|
|
927
|
+
const proc = Bun.spawn(args, {
|
|
928
|
+
stdout: "inherit",
|
|
929
|
+
stderr: "inherit",
|
|
930
|
+
cwd: process.cwd()
|
|
931
|
+
});
|
|
932
|
+
if (!watch) {
|
|
933
|
+
await proc.exited;
|
|
934
|
+
}
|
|
935
|
+
} catch (error) {
|
|
936
|
+
console.error("Failed to run Tailwind:", error);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
async copyStaticAssets() {
|
|
940
|
+
console.log(`
|
|
941
|
+
\uD83D\uDCE6 Copying static assets...`);
|
|
942
|
+
const cssPath = this.config.tailwind?.cssPath || "./styles.css";
|
|
943
|
+
const cssFile = Bun.file(cssPath);
|
|
944
|
+
if (await cssFile.exists()) {
|
|
945
|
+
const outputCssPath = `${this.config.outDir}/styles.css`;
|
|
946
|
+
await Bun.write(outputCssPath, cssFile);
|
|
947
|
+
console.log(` ✓ styles.css`);
|
|
948
|
+
}
|
|
949
|
+
if (this.config.publicDir) {
|
|
950
|
+
const publicFile = Bun.file(this.config.publicDir);
|
|
951
|
+
if (await publicFile.exists()) {
|
|
952
|
+
await this.copyDir(this.config.publicDir, this.config.outDir);
|
|
953
|
+
console.log(` ✓ public assets`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
async copyDir(src, dest) {
|
|
958
|
+
try {
|
|
959
|
+
const proc = Bun.spawn(["cp", "-r", src, dest], {
|
|
960
|
+
stdout: "pipe",
|
|
961
|
+
stderr: "pipe"
|
|
962
|
+
});
|
|
963
|
+
await proc.exited;
|
|
964
|
+
} catch (error) {
|
|
965
|
+
console.warn(`Failed to copy ${src} to ${dest}:`, error);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
async ensureDir(dir) {
|
|
969
|
+
try {
|
|
970
|
+
const proc = Bun.spawn(["mkdir", "-p", dir], {
|
|
971
|
+
stdout: "pipe",
|
|
972
|
+
stderr: "pipe"
|
|
973
|
+
});
|
|
974
|
+
await proc.exited;
|
|
975
|
+
} catch (error) {
|
|
976
|
+
console.warn(`Failed to create directory ${dir}:`, error);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
// src/index.ts
|
|
981
|
+
var defaultConfig = {
|
|
982
|
+
routesDir: "./routes",
|
|
983
|
+
outDir: "./dist",
|
|
984
|
+
port: 3000,
|
|
985
|
+
hostname: "localhost",
|
|
986
|
+
dev: false,
|
|
987
|
+
tailwind: {
|
|
988
|
+
enabled: false,
|
|
989
|
+
cssPath: "./styles.css"
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
function createKilat(config = {}) {
|
|
993
|
+
const tailwindConfig = {
|
|
994
|
+
enabled: config.tailwind?.enabled ?? defaultConfig.tailwind?.enabled ?? false,
|
|
995
|
+
inputPath: config.tailwind?.inputPath,
|
|
996
|
+
cssPath: config.tailwind?.cssPath ?? defaultConfig.tailwind?.cssPath ?? "./styles.css",
|
|
997
|
+
configPath: config.tailwind?.configPath ?? defaultConfig.tailwind?.configPath
|
|
998
|
+
};
|
|
999
|
+
const finalConfig = {
|
|
1000
|
+
...defaultConfig,
|
|
1001
|
+
...config,
|
|
1002
|
+
tailwind: tailwindConfig
|
|
1003
|
+
};
|
|
1004
|
+
return new KilatServer(finalConfig);
|
|
1005
|
+
}
|
|
1006
|
+
async function startDevServer(config = {}) {
|
|
1007
|
+
const server = createKilat({ ...config, dev: true });
|
|
1008
|
+
return server.start();
|
|
1009
|
+
}
|
|
1010
|
+
async function buildStatic(config = {}) {
|
|
1011
|
+
const server = createKilat(config);
|
|
1012
|
+
return server.buildStatic();
|
|
1013
|
+
}
|
|
1014
|
+
function defineLoader(loader) {
|
|
1015
|
+
return loader;
|
|
1016
|
+
}
|
|
1017
|
+
function defineConfig(config) {
|
|
1018
|
+
return config;
|
|
1019
|
+
}
|
|
1020
|
+
function defineMeta(meta) {
|
|
1021
|
+
return meta;
|
|
1022
|
+
}
|
|
1023
|
+
var Kilat = {
|
|
1024
|
+
createKilat,
|
|
1025
|
+
startDevServer,
|
|
1026
|
+
buildStatic,
|
|
1027
|
+
defineConfig,
|
|
1028
|
+
defineLoader,
|
|
1029
|
+
defineMeta,
|
|
1030
|
+
defaultConfig
|
|
1031
|
+
};
|
|
1032
|
+
var src_default = Kilat;
|
|
1033
|
+
export {
|
|
1034
|
+
startDevServer,
|
|
1035
|
+
defineMeta,
|
|
1036
|
+
defineLoader,
|
|
1037
|
+
defineConfig,
|
|
1038
|
+
defaultConfig,
|
|
1039
|
+
src_default as default,
|
|
1040
|
+
createKilat,
|
|
1041
|
+
buildStatic,
|
|
1042
|
+
Router,
|
|
1043
|
+
ReactAdapter,
|
|
1044
|
+
KilatServer,
|
|
1045
|
+
HTMXAdapter
|
|
1046
|
+
};
|