loly 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/bin/loly.js ADDED
@@ -0,0 +1,3015 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { config } from "dotenv";
5
+
6
+ // src/server/dev-server.ts
7
+ import { join as join3 } from "path";
8
+ import { existsSync as existsSync5 } from "fs";
9
+ import chokidar from "chokidar";
10
+
11
+ // src/router/scanner.ts
12
+ import { glob } from "glob";
13
+ import { relative, dirname, sep } from "path";
14
+
15
+ // src/constants/routes.ts
16
+ var ROUTE_FILE_NAMES = [
17
+ "page.tsx",
18
+ "page.ts",
19
+ "layout.tsx",
20
+ "layout.ts",
21
+ "route.ts",
22
+ "route.tsx"
23
+ ];
24
+
25
+ // src/constants/directories.ts
26
+ var DIRECTORIES = {
27
+ SRC: "src",
28
+ APP: "app",
29
+ DIST: "dist",
30
+ CLIENT: "client",
31
+ SERVER: "server",
32
+ PUBLIC: "public",
33
+ LOLY: ".loly"
34
+ };
35
+
36
+ // src/constants/files.ts
37
+ var FILE_NAMES = {
38
+ MANIFEST: "manifest.json",
39
+ BOOTSTRAP: "bootstrap.ts",
40
+ GLOBALS_CSS: "globals.css",
41
+ ROUTES_MANIFEST: "routes-manifest.json"
42
+ };
43
+
44
+ // src/constants/http.ts
45
+ var HTTP_STATUS = {
46
+ OK: 200,
47
+ BAD_REQUEST: 400,
48
+ NOT_FOUND: 404,
49
+ METHOD_NOT_ALLOWED: 405,
50
+ REQUEST_TIMEOUT: 408,
51
+ INTERNAL_ERROR: 500
52
+ };
53
+ var HTTP_HEADERS = {
54
+ CONTENT_TYPE_JSON: "application/json",
55
+ CONTENT_TYPE_HTML: "text/html; charset=utf-8"
56
+ };
57
+
58
+ // src/constants/server.ts
59
+ var SERVER = {
60
+ DEFAULT_PORT: 3e3,
61
+ RSC_ENDPOINT: "/__loly/rsc",
62
+ IMAGE_ENDPOINT: "/_loly/image",
63
+ ASYNC_ENDPOINT: "/__loly/async",
64
+ APP_CONTAINER_ID: "app"
65
+ };
66
+
67
+ // src/constants/node-builtins.ts
68
+ var NODE_BUILTINS = [
69
+ "fs",
70
+ "path",
71
+ "url",
72
+ "util",
73
+ "stream",
74
+ "events",
75
+ "crypto",
76
+ "http",
77
+ "https",
78
+ "os",
79
+ "querystring",
80
+ "buffer",
81
+ "constants",
82
+ "child_process",
83
+ "module",
84
+ "fs/promises",
85
+ "worker_threads",
86
+ "zlib",
87
+ "inspector",
88
+ "perf_hooks"
89
+ ];
90
+
91
+ // src/constants/errors.ts
92
+ var ERROR_MESSAGES = {
93
+ CONTEXT_NOT_INITIALIZED: "[loly-core] Framework context not initialized. Call setContext() first.",
94
+ APP_DIR_NOT_FOUND: (dir) => `[loly-core] src/app/ directory not found in ${dir}. Please ensure your project has a src/app/ directory.`,
95
+ ROUTE_NOT_FOUND: (pathname) => `Route not found: ${pathname}`,
96
+ PAGE_COMPONENT_NOT_FOUND: (path4) => `[loly-core] Page component not found in ${path4}`,
97
+ PAGE_COMPONENT_MUST_BE_FUNCTION: "[loly-core] Page component must be a function",
98
+ COMPONENT_NOT_FOUND: (route) => `Component not found for route: ${route}`,
99
+ APP_CONTAINER_NOT_FOUND: "[loly-core] App container not found (#app)",
100
+ CLIENT_BUILD_DIR_NOT_FOUND: (dir) => `[loly-core] Client build directory not found: ${dir}. Run 'loly build' first or the client scripts won't load.`,
101
+ ROUTES_MANIFEST_NOT_FOUND: (path4) => `[loly-core] Routes manifest not found: ${path4}. Run 'loly build' first.`,
102
+ DIRECTORY_NOT_FOUND: (dir) => `[loly-core] Directory not found: ${dir}`,
103
+ CLIENT_BUILD_FAILED: "Client build failed",
104
+ SERVER_BUILD_FAILED: "Server build failed",
105
+ FAILED_TO_LOAD_MODULE: (path4) => `[loly-core] Failed to load module: ${path4}`,
106
+ FAILED_TO_LOAD_ROUTE_COMPONENT: (path4) => `[bootstrap] Failed to load component for route ${path4}`,
107
+ FAILED_TO_LOAD_NESTED_LAYOUT: (path4) => `[loly-core] Failed to load nested layout at ${path4}`,
108
+ FAILED_TO_READ_CLIENT_MANIFEST: "[loly-core] Failed to read client manifest:",
109
+ FAILED_TO_PARSE_ISLAND_DATA: "[loly-core] Failed to parse island data:",
110
+ FATAL_BOOTSTRAP_ERROR: "[bootstrap] Fatal error during bootstrap:",
111
+ UNEXPECTED_ERROR: "[loly-core] Unexpected error:",
112
+ ERROR_STARTING_DEV_SERVER: "[loly-core] Error starting dev server:",
113
+ ERROR_STARTING_PROD_SERVER: "[loly-core] Error starting production server:",
114
+ ERROR_BUILDING_PROJECT: "[loly-core] Error building project:",
115
+ ERROR_HANDLING_REQUEST: "[loly-core] Error handling request:",
116
+ ERROR_RENDERING_PAGE: "[loly-core] Error rendering page:",
117
+ RSC_ENDPOINT_ERROR: "[loly-core] RSC endpoint error:"
118
+ };
119
+
120
+ // src/utils/path.ts
121
+ function normalizePath(path4) {
122
+ if (path4 === "/") return "/";
123
+ return path4.replace(/\/$/, "") || "/";
124
+ }
125
+ function normalizeUrlPath(pathname) {
126
+ return normalizePath(pathname);
127
+ }
128
+
129
+ // src/utils/module-loader.ts
130
+ import { resolve } from "path";
131
+ import { existsSync } from "fs";
132
+ var tsxRegistered = false;
133
+ var tsxRegistrationPromise = null;
134
+ async function ensureTsxRegistered() {
135
+ if (tsxRegistered) {
136
+ return;
137
+ }
138
+ if (tsxRegistrationPromise) {
139
+ await tsxRegistrationPromise;
140
+ return;
141
+ }
142
+ tsxRegistrationPromise = (async () => {
143
+ try {
144
+ await import("tsx/esm");
145
+ tsxRegistered = true;
146
+ } catch (error) {
147
+ console.warn(
148
+ "[loly-core] tsx not available, TypeScript files won't load in development"
149
+ );
150
+ tsxRegistered = true;
151
+ }
152
+ })();
153
+ await tsxRegistrationPromise;
154
+ }
155
+ function resolveModuleUrl(path4) {
156
+ if (path4.startsWith("file://")) {
157
+ return path4;
158
+ }
159
+ const absolutePath = resolve(path4);
160
+ const normalizedPath = absolutePath.replace(/\\/g, "/");
161
+ if (normalizedPath.startsWith("/")) {
162
+ return `file://${normalizedPath}`;
163
+ }
164
+ return `file:///${normalizedPath}`;
165
+ }
166
+ async function loadModule(filePath, compiledPath) {
167
+ let pathToLoad = filePath;
168
+ if (compiledPath && existsSync(compiledPath)) {
169
+ pathToLoad = compiledPath;
170
+ }
171
+ try {
172
+ if (pathToLoad.endsWith(".ts") || pathToLoad.endsWith(".tsx")) {
173
+ await ensureTsxRegistered();
174
+ }
175
+ const fileUrl = resolveModuleUrl(pathToLoad);
176
+ const module = await import(fileUrl);
177
+ return module;
178
+ } catch (error) {
179
+ console.error(ERROR_MESSAGES.FAILED_TO_LOAD_MODULE(pathToLoad), error);
180
+ throw error;
181
+ }
182
+ }
183
+
184
+ // src/router/scanner.ts
185
+ function parseSegment(segment) {
186
+ if (segment.startsWith("(") && segment.endsWith(")")) {
187
+ return {
188
+ segment,
189
+ // Keep original with parentheses: (admin)
190
+ isDynamic: false,
191
+ isCatchAll: false,
192
+ isOptional: true
193
+ };
194
+ }
195
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
196
+ const paramName = segment.slice(4, -1);
197
+ return {
198
+ segment,
199
+ isDynamic: true,
200
+ isCatchAll: true,
201
+ isOptional: false,
202
+ paramName
203
+ };
204
+ }
205
+ if (segment.startsWith("[[...") && segment.endsWith("]]")) {
206
+ const paramName = segment.slice(5, -2);
207
+ return {
208
+ segment,
209
+ isDynamic: true,
210
+ isCatchAll: true,
211
+ isOptional: true,
212
+ paramName
213
+ };
214
+ }
215
+ if (segment.startsWith("[") && segment.endsWith("]")) {
216
+ const paramName = segment.slice(1, -1);
217
+ return {
218
+ segment,
219
+ isDynamic: true,
220
+ isCatchAll: false,
221
+ isOptional: false,
222
+ paramName
223
+ };
224
+ }
225
+ return {
226
+ segment,
227
+ isDynamic: false,
228
+ isCatchAll: false,
229
+ isOptional: false
230
+ };
231
+ }
232
+ function segmentsToUrlPath(segments) {
233
+ const parts = [];
234
+ for (const seg of segments) {
235
+ if (seg.isOptional) {
236
+ continue;
237
+ }
238
+ if (seg.isCatchAll) {
239
+ parts.push(`*${seg.paramName || "slug"}`);
240
+ } else if (seg.isDynamic) {
241
+ parts.push(`:${seg.paramName || "param"}`);
242
+ } else {
243
+ parts.push(seg.segment);
244
+ }
245
+ }
246
+ const path4 = "/" + parts.join("/");
247
+ return normalizeUrlPath(path4);
248
+ }
249
+ var HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
250
+ async function analyzeApiRoute(filePath) {
251
+ try {
252
+ const module = await loadModule(filePath);
253
+ const exports = Object.keys(module);
254
+ const httpMethods = [];
255
+ for (const exportName of exports) {
256
+ const upperExport = exportName.toUpperCase();
257
+ if (HTTP_METHODS.includes(upperExport) && typeof module[exportName] === "function") {
258
+ httpMethods.push(upperExport);
259
+ }
260
+ }
261
+ return httpMethods;
262
+ } catch (error) {
263
+ console.warn(`[loly-core] Failed to analyze API route ${filePath}:`, error);
264
+ return [];
265
+ }
266
+ }
267
+ async function scanRoutes(appDir) {
268
+ const routes = [];
269
+ const routeMap = /* @__PURE__ */ new Map();
270
+ const apiRouteMap = /* @__PURE__ */ new Map();
271
+ const patterns = ROUTE_FILE_NAMES.map((file) => `**/${file}`);
272
+ const files = await glob(patterns, { cwd: appDir, absolute: true });
273
+ let rootLayout;
274
+ for (const file of files) {
275
+ const absolutePath = file;
276
+ const relativePath = relative(appDir, absolutePath);
277
+ const dir = dirname(relativePath);
278
+ const fileName = file.split(sep).pop() || "";
279
+ let type;
280
+ if (fileName.startsWith("page.")) {
281
+ type = "page";
282
+ } else if (fileName.startsWith("layout.")) {
283
+ type = "layout";
284
+ } else if (fileName.startsWith("route.")) {
285
+ type = "route";
286
+ } else {
287
+ continue;
288
+ }
289
+ const pathSegments = dir === "." ? [] : dir.split(sep).filter(Boolean);
290
+ const segments = pathSegments.map(parseSegment);
291
+ const isRootLayout = type === "layout" && dir === ".";
292
+ let urlPath = "";
293
+ let isApiRoute = false;
294
+ let httpMethods = void 0;
295
+ if (type === "page") {
296
+ urlPath = segmentsToUrlPath(segments);
297
+ } else if (type === "route") {
298
+ httpMethods = await analyzeApiRoute(absolutePath);
299
+ if (httpMethods.length > 0) {
300
+ isApiRoute = true;
301
+ urlPath = segmentsToUrlPath(segments);
302
+ }
303
+ }
304
+ const routeFile = {
305
+ filePath: absolutePath,
306
+ segments,
307
+ type,
308
+ isRootLayout,
309
+ urlPath,
310
+ isApiRoute: isApiRoute || void 0,
311
+ httpMethods
312
+ };
313
+ routes.push(routeFile);
314
+ if (isRootLayout) {
315
+ rootLayout = routeFile;
316
+ }
317
+ if (type === "page") {
318
+ routeMap.set(urlPath, routeFile);
319
+ } else if (isApiRoute && urlPath) {
320
+ apiRouteMap.set(urlPath, routeFile);
321
+ }
322
+ }
323
+ return {
324
+ routes,
325
+ rootLayout,
326
+ routeMap,
327
+ apiRouteMap
328
+ };
329
+ }
330
+
331
+ // src/utils/route-component-factory.ts
332
+ import { resolve as resolve2, join } from "path";
333
+ import { existsSync as existsSync2 } from "fs";
334
+ var RouteComponentFactory = class {
335
+ constructor() {
336
+ this.componentCache = /* @__PURE__ */ new Map();
337
+ this.layoutCache = /* @__PURE__ */ new Map();
338
+ }
339
+ /**
340
+ * Load a route component
341
+ * @param route - Route file information
342
+ * @param compiledPath - Optional compiled path for production
343
+ * @returns The loaded component module
344
+ */
345
+ async loadRouteComponent(route, compiledPath) {
346
+ const cacheKey = compiledPath || route.filePath;
347
+ if (this.componentCache.has(cacheKey)) {
348
+ return this.componentCache.get(cacheKey);
349
+ }
350
+ const sourcePath = resolve2(route.filePath);
351
+ const module = await loadModule(sourcePath, compiledPath);
352
+ const component = module.default;
353
+ if (!component) {
354
+ throw new Error(
355
+ `[loly-core] Component not found in ${route.filePath}`
356
+ );
357
+ }
358
+ this.componentCache.set(cacheKey, component);
359
+ return component;
360
+ }
361
+ /**
362
+ * Load a layout component
363
+ * @param layout - Layout route file information
364
+ * @param compiledPath - Optional compiled path for production
365
+ * @returns The loaded layout component module
366
+ */
367
+ async loadLayoutComponent(layout, compiledPath) {
368
+ const cacheKey = compiledPath || layout.filePath;
369
+ if (this.layoutCache.has(cacheKey)) {
370
+ const cached = this.layoutCache.get(cacheKey);
371
+ return cached?.[0];
372
+ }
373
+ const sourcePath = resolve2(layout.filePath);
374
+ const module = await loadModule(sourcePath, compiledPath);
375
+ const layoutComponent = module.default;
376
+ if (!layoutComponent) {
377
+ throw new Error(
378
+ `[loly-core] Layout component not found in ${layout.filePath}`
379
+ );
380
+ }
381
+ this.layoutCache.set(cacheKey, [layoutComponent]);
382
+ return layoutComponent;
383
+ }
384
+ /**
385
+ * Load all layouts from root to route
386
+ * @param route - Target route file
387
+ * @param config - Route configuration
388
+ * @param appDir - Application directory
389
+ * @param manifestRoute - Optional manifest route data
390
+ * @returns Array of layout components in order (root to route)
391
+ */
392
+ async loadLayouts(route, config2, appDir, manifestRoute) {
393
+ const layouts = [];
394
+ const configWithManifest = config2;
395
+ const routesManifest = configWithManifest.routesManifest;
396
+ if (config2.rootLayout) {
397
+ const rootLayoutManifest = configWithManifest.rootLayoutManifest;
398
+ const layoutSourcePath = resolve2(config2.rootLayout.filePath);
399
+ const layoutCompiledPath = rootLayoutManifest?.serverPath ? resolve2(rootLayoutManifest.serverPath) : void 0;
400
+ const rootLayout = await this.loadLayoutComponent(
401
+ config2.rootLayout,
402
+ layoutCompiledPath
403
+ );
404
+ layouts.push(rootLayout);
405
+ }
406
+ const allRouteSegments = route.segments;
407
+ for (let i = 0; i < allRouteSegments.length; i++) {
408
+ const segmentPath = allRouteSegments.slice(0, i + 1);
409
+ let layoutManifest;
410
+ if (routesManifest) {
411
+ layoutManifest = routesManifest.find((r) => {
412
+ if (r.type !== "layout") return false;
413
+ if (r.isRootLayout) return false;
414
+ if (r.segments.length !== segmentPath.length) return false;
415
+ return r.segments.every((seg, idx) => {
416
+ const routeSeg = segmentPath[idx];
417
+ if (seg.segment === routeSeg.segment) return true;
418
+ if (seg.isDynamic && routeSeg.isDynamic && seg.paramName === routeSeg.paramName) {
419
+ return true;
420
+ }
421
+ if (seg.isOptional && routeSeg.isOptional && !seg.isDynamic && !routeSeg.isDynamic) {
422
+ const segName = seg.segment.replace(/^\(|\)$/g, "");
423
+ const routeSegName = routeSeg.segment.replace(/^\(|\)$/g, "");
424
+ if (segName === routeSegName) return true;
425
+ }
426
+ return false;
427
+ });
428
+ });
429
+ }
430
+ if (layoutManifest) {
431
+ const layoutSourcePath = resolve2(layoutManifest.sourcePath);
432
+ const layoutCompiledPath = layoutManifest.serverPath ? resolve2(layoutManifest.serverPath) : void 0;
433
+ try {
434
+ const layoutFile = {
435
+ filePath: layoutSourcePath,
436
+ segments: layoutManifest.segments,
437
+ type: "layout",
438
+ isRootLayout: false,
439
+ urlPath: ""
440
+ };
441
+ const layout = await this.loadLayoutComponent(
442
+ layoutFile,
443
+ layoutCompiledPath
444
+ );
445
+ layouts.push(layout);
446
+ } catch (error) {
447
+ console.warn(
448
+ `[loly-core] Failed to load nested layout at ${layoutSourcePath}:`,
449
+ error
450
+ );
451
+ }
452
+ } else {
453
+ const layoutPathParts = segmentPath.map((s) => s.segment);
454
+ const layoutDir = join(appDir, ...layoutPathParts);
455
+ const layoutFilePath = join(layoutDir, "layout.tsx");
456
+ const layoutFilePathAlt = join(layoutDir, "layout.ts");
457
+ const actualPath = existsSync2(layoutFilePath) ? layoutFilePath : existsSync2(layoutFilePathAlt) ? layoutFilePathAlt : null;
458
+ if (actualPath) {
459
+ try {
460
+ const layoutFile = {
461
+ filePath: actualPath,
462
+ segments: segmentPath,
463
+ type: "layout",
464
+ isRootLayout: false,
465
+ urlPath: ""
466
+ };
467
+ const layout = await this.loadLayoutComponent(layoutFile);
468
+ layouts.push(layout);
469
+ } catch (error) {
470
+ console.warn(
471
+ `[loly-core] Failed to load nested layout at ${actualPath}:`,
472
+ error
473
+ );
474
+ }
475
+ }
476
+ }
477
+ }
478
+ return layouts;
479
+ }
480
+ /**
481
+ * Clear component cache
482
+ */
483
+ clearCache() {
484
+ this.componentCache.clear();
485
+ this.layoutCache.clear();
486
+ }
487
+ };
488
+
489
+ // src/server/rendering/strategies.ts
490
+ import { renderToHtmlStream, createHead } from "loly-jsx";
491
+
492
+ // src/server/async-registry.ts
493
+ var asyncTaskRegistry = /* @__PURE__ */ new Map();
494
+ var DEFAULT_MAX_AGE = 5 * 60 * 1e3;
495
+ function registerAsyncTask(id, fn, props) {
496
+ asyncTaskRegistry.set(id, {
497
+ id,
498
+ fn,
499
+ props,
500
+ createdAt: Date.now()
501
+ });
502
+ }
503
+ async function resolveAsyncTask(id) {
504
+ const task = asyncTaskRegistry.get(id);
505
+ if (!task) {
506
+ throw new Error(`Async task not found: ${id}`);
507
+ }
508
+ return task.fn();
509
+ }
510
+
511
+ // src/server/rendering/strategies.ts
512
+ import { resolve as resolve3 } from "path";
513
+
514
+ // src/router/matcher.ts
515
+ function matchRoute(pathname, config2, loadComponent) {
516
+ const normalized = normalizeUrlPath(pathname);
517
+ if (config2.routeMap.has(normalized)) {
518
+ const route = config2.routeMap.get(normalized);
519
+ return { route, params: {} };
520
+ }
521
+ for (const route of config2.routes) {
522
+ if (route.type !== "page") continue;
523
+ const match = matchRoutePattern(normalized, route);
524
+ if (match) {
525
+ return { route, params: match.params };
526
+ }
527
+ }
528
+ return null;
529
+ }
530
+ function matchRoutePattern(pathname, route) {
531
+ const pathSegments = pathname === "/" ? [""] : pathname.split("/").filter(Boolean);
532
+ const routeSegments = route.segments.filter((s) => !s.isOptional);
533
+ if (routeSegments.length === 0 && pathSegments.length === 1 && pathSegments[0] === "") {
534
+ return { params: {} };
535
+ }
536
+ const params = {};
537
+ let pathIdx = 0;
538
+ let routeIdx = 0;
539
+ while (pathIdx < pathSegments.length && routeIdx < routeSegments.length) {
540
+ const routeSeg = routeSegments[routeIdx];
541
+ const pathSeg = pathSegments[pathIdx];
542
+ if (routeSeg.isCatchAll) {
543
+ const remaining = pathSegments.slice(pathIdx);
544
+ if (routeSeg.paramName) {
545
+ params[routeSeg.paramName] = remaining.join("/");
546
+ }
547
+ return { params };
548
+ }
549
+ if (routeSeg.isDynamic) {
550
+ if (routeSeg.paramName) {
551
+ params[routeSeg.paramName] = decodeURIComponent(pathSeg);
552
+ }
553
+ pathIdx++;
554
+ routeIdx++;
555
+ continue;
556
+ }
557
+ if (routeSeg.segment === pathSeg) {
558
+ pathIdx++;
559
+ routeIdx++;
560
+ continue;
561
+ }
562
+ return null;
563
+ }
564
+ if (pathIdx === pathSegments.length && routeIdx === routeSegments.length) {
565
+ return { params };
566
+ }
567
+ return null;
568
+ }
569
+
570
+ // src/context.ts
571
+ import { join as join2 } from "path";
572
+ var globalContext = null;
573
+ function setContext(context) {
574
+ globalContext = context;
575
+ }
576
+ function getContext() {
577
+ if (!globalContext) {
578
+ throw new Error(ERROR_MESSAGES.CONTEXT_NOT_INITIALIZED);
579
+ }
580
+ return globalContext;
581
+ }
582
+ function getProjectDir() {
583
+ return getContext().projectDir;
584
+ }
585
+ function getAppDir() {
586
+ return getContext().appDir;
587
+ }
588
+ function getSrcDir() {
589
+ return getContext().srcDir;
590
+ }
591
+ function getOutDir() {
592
+ return getContext().outDir;
593
+ }
594
+ function getAppDirPath(projectDir) {
595
+ return join2(projectDir, DIRECTORIES.SRC, DIRECTORIES.APP);
596
+ }
597
+ function getClientDir() {
598
+ return join2(getOutDir(), DIRECTORIES.CLIENT);
599
+ }
600
+ function getServerOutDir() {
601
+ return join2(getOutDir(), DIRECTORIES.SERVER);
602
+ }
603
+ function getManifestPath() {
604
+ return join2(getClientDir(), FILE_NAMES.MANIFEST);
605
+ }
606
+ function getRoutesManifestPath() {
607
+ return join2(getProjectDir(), DIRECTORIES.LOLY, FILE_NAMES.ROUTES_MANIFEST);
608
+ }
609
+ function getBootstrapPath() {
610
+ return join2(getProjectDir(), DIRECTORIES.LOLY, FILE_NAMES.BOOTSTRAP);
611
+ }
612
+ function getGlobalsCssPath() {
613
+ return join2(getClientDir(), FILE_NAMES.GLOBALS_CSS);
614
+ }
615
+ function getPublicDir() {
616
+ return join2(getProjectDir(), DIRECTORIES.PUBLIC);
617
+ }
618
+ function getRouteConfig() {
619
+ return getContext().routeConfig || null;
620
+ }
621
+ function setRouteConfig(config2) {
622
+ const context = getContext();
623
+ context.routeConfig = config2;
624
+ }
625
+
626
+ // src/utils/html.ts
627
+ function generateErrorHtml(status, message) {
628
+ const statusText = status === HTTP_STATUS.NOT_FOUND ? "404 - Not Found" : status === HTTP_STATUS.INTERNAL_ERROR ? "500 - Internal Server Error" : `${status} - Error`;
629
+ return `<!doctype html><html><head><title>${statusText}</title></head><body><h1>${statusText}</h1>${message ? `<p>${message}</p>` : ""}</body></html>`;
630
+ }
631
+ function htmlStringToStream(html) {
632
+ const encoder = new TextEncoder();
633
+ return new ReadableStream({
634
+ start(controller) {
635
+ controller.enqueue(encoder.encode(html));
636
+ controller.close();
637
+ }
638
+ });
639
+ }
640
+ function extractIslandDataFromHtml(html) {
641
+ const islandData = {};
642
+ const islandDataRegex = /<script>window\.__LOLY_ISLAND_DATA__=window\.__LOLY_ISLAND_DATA__\|\|{};window\.__LOLY_ISLAND_DATA__\[([^\]]+)\]=([^<]+);<\/script>/gs;
643
+ const htmlWithoutScripts = html.replace(
644
+ islandDataRegex,
645
+ (match, id, data) => {
646
+ try {
647
+ let islandId;
648
+ const trimmedId = id.trim();
649
+ if (trimmedId.startsWith('"') || trimmedId.startsWith("'")) {
650
+ islandId = JSON.parse(trimmedId);
651
+ } else if (/^\d+$/.test(trimmedId)) {
652
+ islandId = trimmedId;
653
+ } else {
654
+ islandId = trimmedId;
655
+ }
656
+ const trimmedData = data.trim().replace(/;?\s*$/, "");
657
+ const islandProps = JSON.parse(trimmedData);
658
+ islandData[islandId] = islandProps;
659
+ } catch (e) {
660
+ console.warn(
661
+ "[loly-core] Failed to parse island data:",
662
+ e,
663
+ { id, data: data?.substring(0, 100) }
664
+ );
665
+ }
666
+ return "";
667
+ }
668
+ );
669
+ return { htmlWithoutScripts, islandData };
670
+ }
671
+
672
+ // src/utils/assets.ts
673
+ import { existsSync as existsSync3, readFileSync } from "fs";
674
+ function getMainScriptName() {
675
+ const manifestPath = getManifestPath();
676
+ if (!existsSync3(manifestPath)) {
677
+ return "main.js";
678
+ }
679
+ try {
680
+ const manifestContent = readFileSync(manifestPath, "utf-8");
681
+ const manifest = JSON.parse(manifestContent);
682
+ const mainEntry = manifest["main"];
683
+ if (mainEntry?.file) {
684
+ return mainEntry.file;
685
+ }
686
+ } catch (error) {
687
+ console.warn("[loly-core] Failed to read client manifest:", error);
688
+ }
689
+ return "main.js";
690
+ }
691
+ function generateAssetTags(manifestRoute, publicPath = "/") {
692
+ let scripts = "";
693
+ let styles = "";
694
+ let preloads = "";
695
+ const mainScriptName = getMainScriptName();
696
+ const mainScriptPath = mainScriptName.startsWith("/") ? mainScriptName : `${publicPath}${mainScriptName}`;
697
+ preloads = `<link rel="modulepreload" href="${mainScriptPath}" crossorigin>`;
698
+ scripts = `<script type="module" src="${mainScriptPath}"></script>`;
699
+ const globalsCssPath = getGlobalsCssPath();
700
+ if (existsSync3(globalsCssPath)) {
701
+ const cssPath = `${publicPath}globals.css`;
702
+ styles += `
703
+ <link rel="stylesheet" href="${cssPath}">`;
704
+ }
705
+ if (manifestRoute?.clientChunks) {
706
+ const { js = [], css = [] } = manifestRoute.clientChunks;
707
+ for (const jsFile of js) {
708
+ const scriptPath = jsFile.startsWith("/") ? jsFile : `${publicPath}${jsFile}`;
709
+ scripts += `
710
+ <script type="module" src="${scriptPath}"></script>`;
711
+ }
712
+ for (const cssFile of css) {
713
+ const cssPath = cssFile.startsWith("/") ? cssFile : `${publicPath}${cssFile}`;
714
+ styles += `
715
+ <link rel="stylesheet" href="${cssPath}">`;
716
+ }
717
+ }
718
+ return { scripts, styles, preloads };
719
+ }
720
+ function getClientAssetsForRoute(manifestRoute) {
721
+ return generateAssetTags(manifestRoute, "/");
722
+ }
723
+
724
+ // src/utils/errors.ts
725
+ function handleRouteError(error, pathname) {
726
+ if (error.message.includes("Route not found")) {
727
+ return {
728
+ html: htmlStringToStream(generateErrorHtml(HTTP_STATUS.NOT_FOUND, "")),
729
+ status: HTTP_STATUS.NOT_FOUND
730
+ };
731
+ }
732
+ console.error(ERROR_MESSAGES.ERROR_RENDERING_PAGE, error);
733
+ return {
734
+ html: htmlStringToStream(generateErrorHtml(HTTP_STATUS.INTERNAL_ERROR, "")),
735
+ status: HTTP_STATUS.INTERNAL_ERROR
736
+ };
737
+ }
738
+
739
+ // src/utils/env.ts
740
+ function getPublicEnv() {
741
+ const publicEnv = {};
742
+ if (typeof process !== "undefined" && process.env) {
743
+ for (const key in process.env) {
744
+ if (key.startsWith("LOLY_PUBLIC_")) {
745
+ const clientKey = key.replace(/^LOLY_PUBLIC_/, "");
746
+ publicEnv[clientKey] = process.env[key] || "";
747
+ }
748
+ }
749
+ }
750
+ return publicEnv;
751
+ }
752
+ function generatePublicEnvScript() {
753
+ const publicEnv = getPublicEnv();
754
+ const envJson = JSON.stringify(publicEnv);
755
+ return `<script>window.__LOLY_ENV__=Object.freeze(${envJson});</script>`;
756
+ }
757
+
758
+ // src/server/rendering/strategies.ts
759
+ var RSCStrategy = class {
760
+ constructor(routeFactory2) {
761
+ this.routeFactory = routeFactory2;
762
+ }
763
+ async render(context, config2) {
764
+ const { pathname } = context;
765
+ const appDir = getAppDir();
766
+ const configWithManifest = config2;
767
+ const manifestRouteMap = configWithManifest.routeMapManifest;
768
+ const manifestRoute = manifestRouteMap?.get(pathname);
769
+ const match = matchRoute(pathname, config2, async (route2) => {
770
+ const routeManifest = manifestRouteMap?.get(pathname) || Array.from(manifestRouteMap?.values() || []).find(
771
+ (r) => r.urlPath === route2.urlPath
772
+ );
773
+ const compiledPath = routeManifest?.serverPath ? resolve3(routeManifest.serverPath) : void 0;
774
+ return await this.routeFactory.loadRouteComponent(route2, compiledPath);
775
+ });
776
+ if (!match) {
777
+ throw new Error(ERROR_MESSAGES.ROUTE_NOT_FOUND(pathname));
778
+ }
779
+ const { route, params } = match;
780
+ context.params = params;
781
+ const layouts = await this.routeFactory.loadLayouts(
782
+ route,
783
+ config2,
784
+ appDir,
785
+ manifestRoute
786
+ );
787
+ const pageCompiledPath = manifestRoute?.serverPath ? resolve3(manifestRoute.serverPath) : void 0;
788
+ const PageComponent = await this.routeFactory.loadRouteComponent(
789
+ route,
790
+ pageCompiledPath
791
+ );
792
+ const pageProps = {
793
+ params,
794
+ searchParams: context.searchParams
795
+ };
796
+ let pageContent;
797
+ if (typeof PageComponent === "function") {
798
+ const result = PageComponent(pageProps);
799
+ pageContent = result instanceof Promise ? await result : result;
800
+ } else {
801
+ throw new Error(ERROR_MESSAGES.PAGE_COMPONENT_MUST_BE_FUNCTION);
802
+ }
803
+ let content = pageContent;
804
+ for (let i = layouts.length - 1; i >= 0; i--) {
805
+ const Layout = layouts[i];
806
+ if (typeof Layout === "function") {
807
+ const layoutResult = Layout({ children: content, params });
808
+ content = layoutResult instanceof Promise ? await layoutResult : layoutResult;
809
+ }
810
+ }
811
+ const { renderToString: renderToString2 } = await import("loly-jsx");
812
+ const htmlWithScripts = await renderToString2(content);
813
+ const { htmlWithoutScripts, islandData } = extractIslandDataFromHtml(htmlWithScripts);
814
+ const { scripts, styles, preloads } = getClientAssetsForRoute(manifestRoute);
815
+ return { html: htmlWithoutScripts, scripts, styles, preloads, islandData };
816
+ }
817
+ };
818
+ var SSRStrategy = class {
819
+ constructor(routeFactory2) {
820
+ this.routeFactory = routeFactory2;
821
+ }
822
+ async render(context, config2) {
823
+ const { pathname } = context;
824
+ const appDir = getAppDir();
825
+ const configWithManifest = config2;
826
+ const manifestRouteMap = configWithManifest.routeMapManifest;
827
+ const manifestRoute = manifestRouteMap?.get(pathname);
828
+ const match = matchRoute(pathname, config2, async (route2) => {
829
+ const routeManifest = manifestRouteMap?.get(pathname) || Array.from(manifestRouteMap?.values() || []).find(
830
+ (r) => r.urlPath === route2.urlPath
831
+ );
832
+ const compiledPath = routeManifest?.serverPath ? resolve3(routeManifest.serverPath) : void 0;
833
+ return await this.routeFactory.loadRouteComponent(route2, compiledPath);
834
+ });
835
+ if (!match) {
836
+ return {
837
+ html: htmlStringToStream(generateErrorHtml(HTTP_STATUS.NOT_FOUND, "")),
838
+ status: HTTP_STATUS.NOT_FOUND
839
+ };
840
+ }
841
+ const { route, params } = match;
842
+ context.params = params;
843
+ try {
844
+ const layouts = await this.routeFactory.loadLayouts(
845
+ route,
846
+ config2,
847
+ appDir,
848
+ manifestRoute
849
+ );
850
+ const pageCompiledPath = manifestRoute?.serverPath ? resolve3(manifestRoute.serverPath) : void 0;
851
+ const PageComponent = await this.routeFactory.loadRouteComponent(
852
+ route,
853
+ pageCompiledPath
854
+ );
855
+ const pageProps = {
856
+ params,
857
+ searchParams: context.searchParams
858
+ };
859
+ let pageContent;
860
+ if (typeof PageComponent === "function") {
861
+ const result = PageComponent(pageProps);
862
+ pageContent = result instanceof Promise ? await result : result;
863
+ } else {
864
+ throw new Error(ERROR_MESSAGES.PAGE_COMPONENT_MUST_BE_FUNCTION);
865
+ }
866
+ let content = pageContent;
867
+ for (let i = layouts.length - 1; i >= 0; i--) {
868
+ const Layout = layouts[i];
869
+ if (typeof Layout === "function") {
870
+ const layoutResult = Layout({ children: content, params });
871
+ content = layoutResult instanceof Promise ? await layoutResult : layoutResult;
872
+ }
873
+ }
874
+ const head = createHead();
875
+ head.setTitle("Loly App");
876
+ const { scripts, styles, preloads } = getClientAssetsForRoute(manifestRoute);
877
+ const envScript = generatePublicEnvScript();
878
+ const headParts = [head.toString()];
879
+ if (preloads) headParts.push(preloads);
880
+ headParts.push(envScript);
881
+ const headContent = headParts.join("\n");
882
+ const htmlStream = renderToHtmlStream({
883
+ view: content,
884
+ head: headContent,
885
+ scripts,
886
+ styles,
887
+ appId: SERVER.APP_CONTAINER_ID,
888
+ onAsyncTask: (id, fn, props) => {
889
+ registerAsyncTask(id, fn, props);
890
+ }
891
+ });
892
+ return {
893
+ html: htmlStream,
894
+ status: HTTP_STATUS.OK
895
+ };
896
+ } catch (error) {
897
+ return handleRouteError(error, context.pathname);
898
+ }
899
+ }
900
+ };
901
+
902
+ // src/server/ssr.ts
903
+ var routeFactory = new RouteComponentFactory();
904
+ async function renderPageContent(context, config2, appDir) {
905
+ const rscStrategy = new RSCStrategy(routeFactory);
906
+ return await rscStrategy.render(context, config2);
907
+ }
908
+ async function renderPageStream(context, config2, appDir) {
909
+ const ssrStrategy = new SSRStrategy(routeFactory);
910
+ return await ssrStrategy.render(context, config2);
911
+ }
912
+
913
+ // src/server/base-server.ts
914
+ import express2 from "express";
915
+
916
+ // src/utils/express-setup.ts
917
+ import express from "express";
918
+ import { Readable } from "stream";
919
+ import { existsSync as existsSync4 } from "fs";
920
+
921
+ // src/server/handlers/image.ts
922
+ import crypto2 from "crypto";
923
+
924
+ // src/server/utils/image-optimizer.ts
925
+ import sharp from "sharp";
926
+ import fs2 from "fs";
927
+ import path3 from "path";
928
+
929
+ // src/server/utils/image-validation.ts
930
+ import path from "path";
931
+ function isRemoteUrl(url) {
932
+ return url.startsWith("http://") || url.startsWith("https://");
933
+ }
934
+ function sanitizeImagePath(imagePath) {
935
+ const normalized = path.normalize(imagePath).replace(/^(\.\.(\/|\\|$))+/, "");
936
+ return normalized.replace(/^[/\\]+/, "");
937
+ }
938
+ function patternToRegex(pattern) {
939
+ const parts = [];
940
+ if (pattern.protocol) {
941
+ parts.push(pattern.protocol === "https" ? "https" : "http");
942
+ } else {
943
+ parts.push("https?");
944
+ }
945
+ parts.push("://");
946
+ let hostnamePattern = pattern.hostname.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^.]*");
947
+ parts.push(hostnamePattern);
948
+ if (pattern.port) {
949
+ parts.push(`:${pattern.port}`);
950
+ }
951
+ if (pattern.pathname) {
952
+ let pathnamePattern = pattern.pathname.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
953
+ parts.push(pathnamePattern);
954
+ } else {
955
+ parts.push(".*");
956
+ }
957
+ const regexSource = `^${parts.join("")}`;
958
+ return new RegExp(regexSource);
959
+ }
960
+ function validateRemoteUrl(url, config2) {
961
+ if (!config2 || !config2.remotePatterns && !config2.domains) {
962
+ return true;
963
+ }
964
+ try {
965
+ const urlObj = new URL(url);
966
+ const protocol = urlObj.protocol.replace(":", "");
967
+ const hostname = urlObj.hostname;
968
+ const port = urlObj.port || "";
969
+ const pathname = urlObj.pathname;
970
+ if (config2.remotePatterns && config2.remotePatterns.length > 0) {
971
+ for (const pattern of config2.remotePatterns) {
972
+ const regex = patternToRegex(pattern);
973
+ const testUrl = `${protocol}://${hostname}${port ? `:${port}` : ""}${pathname}`;
974
+ if (regex.test(testUrl)) {
975
+ if (pattern.protocol && pattern.protocol !== protocol) {
976
+ continue;
977
+ }
978
+ if (pattern.port && pattern.port !== port) {
979
+ continue;
980
+ }
981
+ return true;
982
+ }
983
+ }
984
+ }
985
+ if (config2.domains && config2.domains.length > 0) {
986
+ for (const domain of config2.domains) {
987
+ const domainPattern = domain.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^.]*");
988
+ const regex = new RegExp(`^${domainPattern}$`);
989
+ if (regex.test(hostname)) {
990
+ if (process.env.NODE_ENV === "production" && protocol !== "https") {
991
+ continue;
992
+ }
993
+ return true;
994
+ }
995
+ }
996
+ }
997
+ return false;
998
+ } catch (error) {
999
+ return false;
1000
+ }
1001
+ }
1002
+ function validateImageDimensions(width, height, config2) {
1003
+ const maxWidth = config2?.maxWidth || 3840;
1004
+ const maxHeight = config2?.maxHeight || 3840;
1005
+ if (width !== void 0 && (width <= 0 || width > maxWidth)) {
1006
+ return {
1007
+ valid: false,
1008
+ error: `Image width must be between 1 and ${maxWidth}, got ${width}`
1009
+ };
1010
+ }
1011
+ if (height !== void 0 && (height <= 0 || height > maxHeight)) {
1012
+ return {
1013
+ valid: false,
1014
+ error: `Image height must be between 1 and ${maxHeight}, got ${height}`
1015
+ };
1016
+ }
1017
+ return { valid: true };
1018
+ }
1019
+ function validateQuality(quality) {
1020
+ if (quality === void 0) {
1021
+ return { valid: true };
1022
+ }
1023
+ if (typeof quality !== "number" || quality < 1 || quality > 100) {
1024
+ return {
1025
+ valid: false,
1026
+ error: `Image quality must be between 1 and 100, got ${quality}`
1027
+ };
1028
+ }
1029
+ return { valid: true };
1030
+ }
1031
+
1032
+ // src/server/utils/image-cache.ts
1033
+ import fs from "fs";
1034
+ import path2 from "path";
1035
+ import crypto from "crypto";
1036
+ var ImageLRUCache = class {
1037
+ constructor(maxSize = 50) {
1038
+ this.maxSize = maxSize;
1039
+ this.cache = /* @__PURE__ */ new Map();
1040
+ }
1041
+ /**
1042
+ * Get an image from cache
1043
+ */
1044
+ get(key) {
1045
+ if (!this.cache.has(key)) {
1046
+ return void 0;
1047
+ }
1048
+ const value = this.cache.get(key);
1049
+ this.cache.delete(key);
1050
+ this.cache.set(key, value);
1051
+ return value;
1052
+ }
1053
+ /**
1054
+ * Set an image in cache
1055
+ */
1056
+ set(key, value) {
1057
+ if (this.cache.has(key)) {
1058
+ this.cache.delete(key);
1059
+ } else if (this.cache.size >= this.maxSize) {
1060
+ const firstKey = this.cache.keys().next().value;
1061
+ if (firstKey) {
1062
+ this.cache.delete(firstKey);
1063
+ }
1064
+ }
1065
+ this.cache.set(key, value);
1066
+ }
1067
+ /**
1068
+ * Check if key exists in cache
1069
+ */
1070
+ has(key) {
1071
+ return this.cache.has(key);
1072
+ }
1073
+ /**
1074
+ * Clear the cache
1075
+ */
1076
+ clear() {
1077
+ this.cache.clear();
1078
+ }
1079
+ /**
1080
+ * Get cache size
1081
+ */
1082
+ size() {
1083
+ return this.cache.size;
1084
+ }
1085
+ };
1086
+ var globalLRUCache = null;
1087
+ function getLRUCache(maxSize) {
1088
+ if (!globalLRUCache) {
1089
+ globalLRUCache = new ImageLRUCache(maxSize);
1090
+ }
1091
+ return globalLRUCache;
1092
+ }
1093
+ function generateCacheKey(src, width, height, quality, format) {
1094
+ const data = `${src}-${width || ""}-${height || ""}-${quality || ""}-${format || ""}`;
1095
+ return crypto.createHash("sha256").update(data).digest("hex");
1096
+ }
1097
+ function getCacheDir() {
1098
+ const projectDir = getProjectDir();
1099
+ return path2.join(projectDir, DIRECTORIES.LOLY, "cache", "images");
1100
+ }
1101
+ function ensureCacheDir(cacheDir) {
1102
+ if (!fs.existsSync(cacheDir)) {
1103
+ fs.mkdirSync(cacheDir, { recursive: true });
1104
+ }
1105
+ }
1106
+ function getCachedImagePath(cacheKey, extension, cacheDir) {
1107
+ return path2.join(cacheDir, `${cacheKey}.${extension}`);
1108
+ }
1109
+ function hasCachedImage(cacheKey, extension, cacheDir) {
1110
+ const cachedPath = getCachedImagePath(cacheKey, extension, cacheDir);
1111
+ return fs.existsSync(cachedPath);
1112
+ }
1113
+ function readCachedImage(cacheKey, extension, cacheDir) {
1114
+ const cachedPath = getCachedImagePath(cacheKey, extension, cacheDir);
1115
+ try {
1116
+ if (fs.existsSync(cachedPath)) {
1117
+ return fs.readFileSync(cachedPath);
1118
+ }
1119
+ } catch (error) {
1120
+ console.warn(`[image-optimizer] Failed to read cached image: ${cachedPath}`, error);
1121
+ }
1122
+ return null;
1123
+ }
1124
+ function writeCachedImage(cacheKey, extension, cacheDir, imageBuffer) {
1125
+ ensureCacheDir(cacheDir);
1126
+ const cachedPath = getCachedImagePath(cacheKey, extension, cacheDir);
1127
+ try {
1128
+ fs.writeFileSync(cachedPath, imageBuffer);
1129
+ } catch (error) {
1130
+ console.warn(`[image-optimizer] Failed to write cached image: ${cachedPath}`, error);
1131
+ }
1132
+ }
1133
+ function getImageMimeType(format) {
1134
+ const formatMap = {
1135
+ webp: "image/webp",
1136
+ avif: "image/avif",
1137
+ jpeg: "image/jpeg",
1138
+ jpg: "image/jpeg",
1139
+ png: "image/png",
1140
+ gif: "image/gif",
1141
+ svg: "image/svg+xml"
1142
+ };
1143
+ const normalized = format.toLowerCase();
1144
+ return formatMap[normalized] || "image/jpeg";
1145
+ }
1146
+ function getImageExtension(format) {
1147
+ const formatMap = {
1148
+ "image/webp": "webp",
1149
+ "image/avif": "avif",
1150
+ "image/jpeg": "jpg",
1151
+ "image/png": "png",
1152
+ "image/gif": "gif",
1153
+ "image/svg+xml": "svg",
1154
+ webp: "webp",
1155
+ avif: "avif",
1156
+ jpeg: "jpg",
1157
+ jpg: "jpg",
1158
+ png: "png",
1159
+ gif: "gif",
1160
+ svg: "svg"
1161
+ };
1162
+ const normalized = format.toLowerCase();
1163
+ return formatMap[normalized] || "jpg";
1164
+ }
1165
+ function cleanupCacheByAge(cacheDir, maxAgeDays = 30) {
1166
+ const result = { deleted: 0, freed: 0 };
1167
+ const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
1168
+ const now = Date.now();
1169
+ try {
1170
+ if (!fs.existsSync(cacheDir)) {
1171
+ return result;
1172
+ }
1173
+ const files = fs.readdirSync(cacheDir);
1174
+ for (const file of files) {
1175
+ const filePath = path2.join(cacheDir, file);
1176
+ try {
1177
+ const stat = fs.statSync(filePath);
1178
+ if (stat.isFile()) {
1179
+ const age = now - stat.mtime.getTime();
1180
+ if (age > maxAgeMs) {
1181
+ const size = stat.size;
1182
+ fs.unlinkSync(filePath);
1183
+ result.deleted++;
1184
+ result.freed += size;
1185
+ }
1186
+ }
1187
+ } catch (error) {
1188
+ continue;
1189
+ }
1190
+ }
1191
+ } catch (error) {
1192
+ console.warn(`[image-cache] Failed to cleanup cache by age: ${cacheDir}`, error);
1193
+ }
1194
+ return result;
1195
+ }
1196
+ function cleanupCacheBySize(cacheDir, maxSizeMB = 500) {
1197
+ const result = { deleted: 0, freed: 0 };
1198
+ const maxSizeBytes = maxSizeMB * 1024 * 1024;
1199
+ try {
1200
+ if (!fs.existsSync(cacheDir)) {
1201
+ return result;
1202
+ }
1203
+ const files = fs.readdirSync(cacheDir);
1204
+ const fileInfos = [];
1205
+ for (const file of files) {
1206
+ const filePath = path2.join(cacheDir, file);
1207
+ try {
1208
+ const stat = fs.statSync(filePath);
1209
+ if (stat.isFile()) {
1210
+ fileInfos.push({
1211
+ path: filePath,
1212
+ mtime: stat.mtime.getTime(),
1213
+ size: stat.size
1214
+ });
1215
+ }
1216
+ } catch (error) {
1217
+ continue;
1218
+ }
1219
+ }
1220
+ const totalSize = fileInfos.reduce((sum, info) => sum + info.size, 0);
1221
+ if (totalSize <= maxSizeBytes) {
1222
+ return result;
1223
+ }
1224
+ fileInfos.sort((a, b) => a.mtime - b.mtime);
1225
+ let currentSize = totalSize;
1226
+ for (const fileInfo of fileInfos) {
1227
+ if (currentSize <= maxSizeBytes) {
1228
+ break;
1229
+ }
1230
+ try {
1231
+ fs.unlinkSync(fileInfo.path);
1232
+ result.deleted++;
1233
+ result.freed += fileInfo.size;
1234
+ currentSize -= fileInfo.size;
1235
+ } catch (error) {
1236
+ continue;
1237
+ }
1238
+ }
1239
+ } catch (error) {
1240
+ console.warn(`[image-cache] Failed to cleanup cache by size: ${cacheDir}`, error);
1241
+ }
1242
+ return result;
1243
+ }
1244
+ function cleanupCache(config2) {
1245
+ const cacheDir = getCacheDir();
1246
+ const maxSizeMB = config2?.maxSizeMB ?? 500;
1247
+ const maxAgeDays = config2?.maxAgeDays ?? 30;
1248
+ const ageResult = cleanupCacheByAge(cacheDir, maxAgeDays);
1249
+ const sizeResult = cleanupCacheBySize(cacheDir, maxSizeMB);
1250
+ return {
1251
+ deleted: ageResult.deleted + sizeResult.deleted,
1252
+ freed: ageResult.freed + sizeResult.freed
1253
+ };
1254
+ }
1255
+
1256
+ // src/server/utils/image-optimizer.ts
1257
+ var DEFAULT_IMAGE_CONFIG = {
1258
+ maxWidth: 3840,
1259
+ maxHeight: 3840,
1260
+ quality: 70,
1261
+ formats: ["image/avif", "image/webp"],
1262
+ minimumCacheTTL: 31536e3,
1263
+ // 1 año (en lugar de 60)
1264
+ cacheMaxSize: 500,
1265
+ cacheMaxAge: 30,
1266
+ cacheLRUSize: 50,
1267
+ cacheCleanupInterval: 100
1268
+ };
1269
+ var requestCount = 0;
1270
+ async function downloadRemoteImage(url, timeout = 1e4) {
1271
+ const controller = new AbortController();
1272
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1273
+ try {
1274
+ const response = await fetch(url, {
1275
+ signal: controller.signal,
1276
+ headers: {
1277
+ "User-Agent": "Loly-Image-Optimizer/1.0"
1278
+ }
1279
+ });
1280
+ clearTimeout(timeoutId);
1281
+ if (!response.ok) {
1282
+ throw new Error(`Failed to download image: ${response.status} ${response.statusText}`);
1283
+ }
1284
+ const arrayBuffer = await response.arrayBuffer();
1285
+ return Buffer.from(arrayBuffer);
1286
+ } catch (error) {
1287
+ clearTimeout(timeoutId);
1288
+ if (error instanceof Error && error.name === "AbortError") {
1289
+ throw new Error(`Image download timeout after ${timeout}ms`);
1290
+ }
1291
+ throw error;
1292
+ }
1293
+ }
1294
+ function readLocalImage(src, projectRoot) {
1295
+ const sanitized = sanitizeImagePath(src);
1296
+ const publicDir = getPublicDir();
1297
+ const publicPath = path3.join(publicDir, sanitized);
1298
+ if (fs2.existsSync(publicPath)) {
1299
+ return fs2.readFileSync(publicPath);
1300
+ }
1301
+ if (src.startsWith("/")) {
1302
+ const absolutePath = path3.join(projectRoot, sanitized);
1303
+ if (fs2.existsSync(absolutePath)) {
1304
+ return fs2.readFileSync(absolutePath);
1305
+ }
1306
+ }
1307
+ throw new Error(`Image not found: ${src}`);
1308
+ }
1309
+ function determineOutputFormat(sourceFormat, requestedFormat, config2) {
1310
+ if (sourceFormat === "svg") {
1311
+ return "svg";
1312
+ }
1313
+ if (requestedFormat && requestedFormat !== "auto") {
1314
+ return requestedFormat;
1315
+ }
1316
+ const supportedFormats = config2.formats || ["image/webp"];
1317
+ if (supportedFormats.includes("image/avif")) {
1318
+ return "avif";
1319
+ }
1320
+ if (supportedFormats.includes("image/webp")) {
1321
+ return "webp";
1322
+ }
1323
+ return sourceFormat === "svg" ? "jpeg" : sourceFormat;
1324
+ }
1325
+ async function optimizeImage(options, config2) {
1326
+ const imageConfig = config2 || DEFAULT_IMAGE_CONFIG;
1327
+ const projectRoot = getProjectDir();
1328
+ const dimValidation = validateImageDimensions(options.width, options.height, imageConfig);
1329
+ if (!dimValidation.valid) {
1330
+ throw new Error(dimValidation.error);
1331
+ }
1332
+ const qualityValidation = validateQuality(options.quality);
1333
+ if (!qualityValidation.valid) {
1334
+ throw new Error(qualityValidation.error);
1335
+ }
1336
+ if (isRemoteUrl(options.src)) {
1337
+ if (!validateRemoteUrl(options.src, imageConfig)) {
1338
+ throw new Error(`Remote image domain not allowed: ${options.src}`);
1339
+ }
1340
+ }
1341
+ const sourceFormat = path3.extname(options.src).slice(1).toLowerCase() || "jpeg";
1342
+ const outputFormat = determineOutputFormat(sourceFormat, options.format, imageConfig);
1343
+ const cacheKey = generateCacheKey(
1344
+ options.src,
1345
+ options.width,
1346
+ options.height,
1347
+ options.quality || imageConfig.quality || 75,
1348
+ outputFormat
1349
+ );
1350
+ const cacheDir = getCacheDir();
1351
+ const extension = getImageExtension(outputFormat);
1352
+ const fullCacheKey = `${cacheKey}.${extension}`;
1353
+ const lruSize = imageConfig.cacheLRUSize || DEFAULT_IMAGE_CONFIG.cacheLRUSize || 50;
1354
+ const lruCache = getLRUCache(lruSize);
1355
+ const lruCached = lruCache.get(fullCacheKey);
1356
+ if (lruCached) {
1357
+ const metadata2 = await sharp(lruCached).metadata();
1358
+ return {
1359
+ buffer: lruCached,
1360
+ format: outputFormat,
1361
+ mimeType: getImageMimeType(outputFormat),
1362
+ width: metadata2.width || options.width || 0,
1363
+ height: metadata2.height || options.height || 0
1364
+ };
1365
+ }
1366
+ if (hasCachedImage(cacheKey, extension, cacheDir)) {
1367
+ const cached = readCachedImage(cacheKey, extension, cacheDir);
1368
+ if (cached) {
1369
+ lruCache.set(fullCacheKey, cached);
1370
+ const metadata2 = await sharp(cached).metadata();
1371
+ return {
1372
+ buffer: cached,
1373
+ format: outputFormat,
1374
+ mimeType: getImageMimeType(outputFormat),
1375
+ width: metadata2.width || options.width || 0,
1376
+ height: metadata2.height || options.height || 0
1377
+ };
1378
+ }
1379
+ }
1380
+ let imageBuffer;
1381
+ if (isRemoteUrl(options.src)) {
1382
+ imageBuffer = await downloadRemoteImage(options.src);
1383
+ } else {
1384
+ imageBuffer = readLocalImage(options.src, projectRoot);
1385
+ }
1386
+ if (outputFormat === "svg" || sourceFormat === "svg") {
1387
+ if (!imageConfig.dangerouslyAllowSVG) {
1388
+ throw new Error("SVG images are not allowed. Set images.dangerouslyAllowSVG to true to enable.");
1389
+ }
1390
+ return {
1391
+ buffer: imageBuffer,
1392
+ format: "svg",
1393
+ mimeType: "image/svg+xml",
1394
+ width: options.width || 0,
1395
+ height: options.height || 0
1396
+ };
1397
+ }
1398
+ let sharpInstance = sharp(imageBuffer);
1399
+ const metadata = await sharpInstance.metadata();
1400
+ if (options.width || options.height) {
1401
+ const fit = options.fit || "cover";
1402
+ sharpInstance = sharpInstance.resize(options.width, options.height, {
1403
+ fit,
1404
+ withoutEnlargement: true
1405
+ });
1406
+ }
1407
+ const quality = options.quality || imageConfig.quality || 75;
1408
+ switch (outputFormat) {
1409
+ case "webp":
1410
+ sharpInstance = sharpInstance.webp({ quality });
1411
+ break;
1412
+ case "avif":
1413
+ sharpInstance = sharpInstance.avif({ quality });
1414
+ break;
1415
+ case "jpeg":
1416
+ case "jpg":
1417
+ sharpInstance = sharpInstance.jpeg({ quality });
1418
+ break;
1419
+ case "png":
1420
+ sharpInstance = sharpInstance.png({ quality: Math.round(quality / 100 * 9) });
1421
+ break;
1422
+ default:
1423
+ sharpInstance = sharpInstance.jpeg({ quality });
1424
+ }
1425
+ const optimizedBuffer = await sharpInstance.toBuffer();
1426
+ const finalMetadata = await sharp(optimizedBuffer).metadata();
1427
+ lruCache.set(fullCacheKey, optimizedBuffer);
1428
+ writeCachedImage(cacheKey, extension, cacheDir, optimizedBuffer);
1429
+ requestCount++;
1430
+ const cleanupInterval = imageConfig.cacheCleanupInterval || DEFAULT_IMAGE_CONFIG.cacheCleanupInterval || 100;
1431
+ if (requestCount >= cleanupInterval) {
1432
+ requestCount = 0;
1433
+ setImmediate(() => {
1434
+ try {
1435
+ const cleanupResult = cleanupCache({
1436
+ maxSizeMB: imageConfig.cacheMaxSize || DEFAULT_IMAGE_CONFIG.cacheMaxSize,
1437
+ maxAgeDays: imageConfig.cacheMaxAge || DEFAULT_IMAGE_CONFIG.cacheMaxAge
1438
+ });
1439
+ if (cleanupResult.deleted > 0) {
1440
+ console.log(
1441
+ `[image-cache] Cleaned up ${cleanupResult.deleted} files, freed ${(cleanupResult.freed / 1024 / 1024).toFixed(2)} MB`
1442
+ );
1443
+ }
1444
+ } catch (error) {
1445
+ console.warn("[image-cache] Cleanup failed:", error);
1446
+ }
1447
+ });
1448
+ }
1449
+ return {
1450
+ buffer: optimizedBuffer,
1451
+ format: outputFormat,
1452
+ mimeType: getImageMimeType(outputFormat),
1453
+ width: finalMetadata.width || options.width || metadata.width || 0,
1454
+ height: finalMetadata.height || options.height || metadata.height || 0
1455
+ };
1456
+ }
1457
+
1458
+ // src/server/handlers/image.ts
1459
+ async function handleImageRequest(options) {
1460
+ const { req, res, config: config2 } = options;
1461
+ try {
1462
+ const src = req.query.src;
1463
+ const width = req.query.w ? parseInt(req.query.w, 10) : void 0;
1464
+ const height = req.query.h ? parseInt(req.query.h, 10) : void 0;
1465
+ const quality = req.query.q ? parseInt(req.query.q, 10) : void 0;
1466
+ const format = req.query.format;
1467
+ const fit = req.query.fit;
1468
+ if (!src) {
1469
+ res.status(400).json({
1470
+ error: "Missing required parameter: src"
1471
+ });
1472
+ return;
1473
+ }
1474
+ if (typeof src !== "string") {
1475
+ res.status(400).json({
1476
+ error: "Parameter 'src' must be a string"
1477
+ });
1478
+ return;
1479
+ }
1480
+ const etagInput = `${src}-${width || ""}-${height || ""}-${quality || ""}-${format || ""}-${fit || ""}`;
1481
+ const etag = `"${crypto2.createHash("md5").update(etagInput).digest("hex")}"`;
1482
+ const ifNoneMatch = req.headers["if-none-match"];
1483
+ if (ifNoneMatch === etag) {
1484
+ res.status(304).end();
1485
+ return;
1486
+ }
1487
+ const result = await optimizeImage(
1488
+ {
1489
+ src,
1490
+ width,
1491
+ height,
1492
+ quality,
1493
+ format,
1494
+ fit
1495
+ },
1496
+ config2
1497
+ );
1498
+ const imageConfig = config2 || {};
1499
+ const cacheTTL = imageConfig.minimumCacheTTL ?? DEFAULT_IMAGE_CONFIG.minimumCacheTTL;
1500
+ res.setHeader("Content-Type", result.mimeType);
1501
+ res.setHeader("Content-Length", result.buffer.length);
1502
+ res.setHeader("Cache-Control", `public, max-age=${cacheTTL}, immutable`);
1503
+ res.setHeader("ETag", etag);
1504
+ res.setHeader("X-Content-Type-Options", "nosniff");
1505
+ res.send(result.buffer);
1506
+ } catch (error) {
1507
+ if (error instanceof Error) {
1508
+ if (error.message.includes("not allowed")) {
1509
+ res.status(403).json({
1510
+ error: "Forbidden",
1511
+ message: error.message
1512
+ });
1513
+ return;
1514
+ }
1515
+ if (error.message.includes("not found") || error.message.includes("Image not found")) {
1516
+ res.status(404).json({
1517
+ error: "Not Found",
1518
+ message: error.message
1519
+ });
1520
+ return;
1521
+ }
1522
+ if (error.message.includes("must be")) {
1523
+ res.status(400).json({
1524
+ error: "Bad Request",
1525
+ message: error.message
1526
+ });
1527
+ return;
1528
+ }
1529
+ if (error.message.includes("timeout") || error.message.includes("download")) {
1530
+ res.status(504).json({
1531
+ error: "Gateway Timeout",
1532
+ message: error.message
1533
+ });
1534
+ return;
1535
+ }
1536
+ }
1537
+ console.error("[image-optimizer] Error processing image:", error);
1538
+ res.status(500).json({
1539
+ error: "Internal Server Error",
1540
+ message: "Failed to process image"
1541
+ });
1542
+ }
1543
+ }
1544
+
1545
+ // src/router/api-matcher.ts
1546
+ function matchRoutePattern2(pathname, route) {
1547
+ const pathSegments = pathname === "/" ? [""] : pathname.split("/").filter(Boolean);
1548
+ const routeSegments = route.segments.filter((s) => !s.isOptional);
1549
+ if (routeSegments.length === 0 && pathSegments.length === 1 && pathSegments[0] === "") {
1550
+ return { params: {} };
1551
+ }
1552
+ const params = {};
1553
+ let pathIdx = 0;
1554
+ let routeIdx = 0;
1555
+ while (pathIdx < pathSegments.length && routeIdx < routeSegments.length) {
1556
+ const routeSeg = routeSegments[routeIdx];
1557
+ const pathSeg = pathSegments[pathIdx];
1558
+ if (routeSeg.isCatchAll) {
1559
+ const remaining = pathSegments.slice(pathIdx);
1560
+ if (routeSeg.paramName) {
1561
+ params[routeSeg.paramName] = remaining.join("/");
1562
+ }
1563
+ return { params };
1564
+ }
1565
+ if (routeSeg.isDynamic) {
1566
+ if (routeSeg.paramName) {
1567
+ params[routeSeg.paramName] = decodeURIComponent(pathSeg);
1568
+ }
1569
+ pathIdx++;
1570
+ routeIdx++;
1571
+ continue;
1572
+ }
1573
+ if (routeSeg.segment === pathSeg) {
1574
+ pathIdx++;
1575
+ routeIdx++;
1576
+ continue;
1577
+ }
1578
+ return null;
1579
+ }
1580
+ if (pathIdx === pathSegments.length && routeIdx === routeSegments.length) {
1581
+ return { params };
1582
+ }
1583
+ return null;
1584
+ }
1585
+ function matchApiRoute(pathname, config2) {
1586
+ const normalized = normalizeUrlPath(pathname);
1587
+ if (config2.apiRouteMap && config2.apiRouteMap.has(normalized)) {
1588
+ const route = config2.apiRouteMap.get(normalized);
1589
+ return { route, params: {} };
1590
+ }
1591
+ if (config2.apiRouteMap) {
1592
+ for (const route of config2.apiRouteMap.values()) {
1593
+ const match = matchRoutePattern2(normalized, route);
1594
+ if (match) {
1595
+ return { route, params: match.params };
1596
+ }
1597
+ }
1598
+ }
1599
+ for (const route of config2.routes) {
1600
+ if (!route.isApiRoute || !route.urlPath) continue;
1601
+ const match = matchRoutePattern2(normalized, route);
1602
+ if (match) {
1603
+ return { route, params: match.params };
1604
+ }
1605
+ }
1606
+ return null;
1607
+ }
1608
+
1609
+ // src/utils/api-route-loader.ts
1610
+ var HTTP_METHODS2 = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
1611
+ async function loadApiRoute(route, compiledPath) {
1612
+ if (!route.isApiRoute) {
1613
+ throw new Error(`[loly-core] Route ${route.filePath} is not an API route`);
1614
+ }
1615
+ const sourcePath = route.filePath;
1616
+ const module = await loadModule(sourcePath, compiledPath);
1617
+ const handlers = /* @__PURE__ */ new Map();
1618
+ for (const exportName of Object.keys(module)) {
1619
+ const upperExport = exportName.toUpperCase();
1620
+ if (HTTP_METHODS2.includes(upperExport) && typeof module[exportName] === "function") {
1621
+ handlers.set(upperExport, module[exportName]);
1622
+ }
1623
+ }
1624
+ if (handlers.size === 0) {
1625
+ throw new Error(
1626
+ `[loly-core] No valid HTTP method handlers found in API route ${route.filePath}`
1627
+ );
1628
+ }
1629
+ return { handlers };
1630
+ }
1631
+ function hasMethod(loadedRoute, method) {
1632
+ return loadedRoute.handlers.has(method.toUpperCase());
1633
+ }
1634
+ function getHandler(loadedRoute, method) {
1635
+ return loadedRoute.handlers.get(method.toUpperCase());
1636
+ }
1637
+
1638
+ // src/server/handlers/api-route.ts
1639
+ function createApiRouteHandler() {
1640
+ return async (pathname, method, req, res, config2) => {
1641
+ try {
1642
+ const match = matchApiRoute(pathname, config2);
1643
+ if (!match) {
1644
+ return false;
1645
+ }
1646
+ const { route, params } = match;
1647
+ const loadedRoute = await loadApiRoute(route);
1648
+ const upperMethod = method.toUpperCase();
1649
+ if (!hasMethod(loadedRoute, upperMethod)) {
1650
+ res.status(HTTP_STATUS.METHOD_NOT_ALLOWED).json({ error: `Method ${method} not allowed` });
1651
+ return true;
1652
+ }
1653
+ const handler = getHandler(loadedRoute, upperMethod);
1654
+ if (!handler) {
1655
+ res.status(HTTP_STATUS.METHOD_NOT_ALLOWED).json({ error: `Method ${method} not allowed` });
1656
+ return true;
1657
+ }
1658
+ req.params = params;
1659
+ await handler(req, res);
1660
+ return true;
1661
+ } catch (error) {
1662
+ console.error("[loly-core] Error handling API route:", error);
1663
+ if (!res.headersSent) {
1664
+ res.status(HTTP_STATUS.INTERNAL_ERROR).json({
1665
+ error: "Internal Server Error",
1666
+ message: error instanceof Error ? error.message : String(error)
1667
+ });
1668
+ return true;
1669
+ }
1670
+ return false;
1671
+ }
1672
+ };
1673
+ }
1674
+
1675
+ // src/server/handlers/async-handler.ts
1676
+ import { renderToString } from "loly-jsx";
1677
+ async function handleAsyncRequest(req, res) {
1678
+ try {
1679
+ const asyncId = req.params.id;
1680
+ if (!asyncId) {
1681
+ res.status(HTTP_STATUS.BAD_REQUEST).json({
1682
+ html: null,
1683
+ error: "Missing async ID"
1684
+ });
1685
+ return;
1686
+ }
1687
+ const timeoutPromise = new Promise((_, reject) => {
1688
+ setTimeout(() => reject(new Error("Timeout")), 3e4);
1689
+ });
1690
+ try {
1691
+ const resolvedVNode = await Promise.race([resolveAsyncTask(asyncId), timeoutPromise]);
1692
+ const html = await renderToString(resolvedVNode);
1693
+ res.status(HTTP_STATUS.OK).json({
1694
+ html,
1695
+ error: null
1696
+ });
1697
+ } catch (error) {
1698
+ if (error instanceof Error && error.message === "Timeout") {
1699
+ res.status(HTTP_STATUS.REQUEST_TIMEOUT).json({
1700
+ html: null,
1701
+ error: "Async task timeout"
1702
+ });
1703
+ return;
1704
+ }
1705
+ if (error instanceof Error && error.message.includes("not found")) {
1706
+ res.status(HTTP_STATUS.NOT_FOUND).json({
1707
+ html: null,
1708
+ error: error.message
1709
+ });
1710
+ return;
1711
+ }
1712
+ console.error("[loly] Error resolving async task:", error);
1713
+ res.status(HTTP_STATUS.INTERNAL_ERROR).json({
1714
+ html: null,
1715
+ error: error instanceof Error ? error.message : "Unknown error"
1716
+ });
1717
+ }
1718
+ } catch (error) {
1719
+ console.error("[loly] Error handling async request:", error);
1720
+ res.status(HTTP_STATUS.INTERNAL_ERROR).json({
1721
+ html: null,
1722
+ error: error instanceof Error ? error.message : "Unknown error"
1723
+ });
1724
+ }
1725
+ }
1726
+
1727
+ // src/utils/express-setup.ts
1728
+ import compression from "compression";
1729
+ function setupExpressMiddleware(app) {
1730
+ app.use(compression({
1731
+ level: 6,
1732
+ // Compression level (1-9, 6 is good balance)
1733
+ threshold: 1024,
1734
+ // Only compress responses > 1KB
1735
+ filter: (req, res) => {
1736
+ if (req.headers["x-no-compression"]) {
1737
+ return false;
1738
+ }
1739
+ return compression.filter(req, res);
1740
+ }
1741
+ }));
1742
+ app.use((req, res, next) => {
1743
+ const url = new URL(req.url, `http://${req.headers.host}`);
1744
+ req.query = Object.fromEntries(url.searchParams);
1745
+ next();
1746
+ });
1747
+ }
1748
+ function setupStaticFiles(app, clientDir, publicDir) {
1749
+ if (existsSync4(clientDir)) {
1750
+ app.use(express.static(clientDir));
1751
+ } else {
1752
+ console.warn(ERROR_MESSAGES.CLIENT_BUILD_DIR_NOT_FOUND(clientDir));
1753
+ }
1754
+ if (existsSync4(publicDir)) {
1755
+ app.use(express.static(publicDir));
1756
+ }
1757
+ }
1758
+ function setupRSCEndpoint(app, handler) {
1759
+ app.get(SERVER.RSC_ENDPOINT, async (req, res) => {
1760
+ try {
1761
+ const pathname = req.query.path || "/";
1762
+ const search = req.query.search || "";
1763
+ const searchParams = search ? Object.fromEntries(new URLSearchParams(search)) : {};
1764
+ const result = await handler(pathname, searchParams);
1765
+ res.setHeader("Content-Type", HTTP_HEADERS.CONTENT_TYPE_JSON);
1766
+ res.json({
1767
+ pathname,
1768
+ params: result.params,
1769
+ searchParams,
1770
+ html: result.html,
1771
+ scripts: result.scripts,
1772
+ styles: result.styles,
1773
+ preloads: result.preloads,
1774
+ islandData: result.islandData
1775
+ });
1776
+ } catch (error) {
1777
+ console.error(ERROR_MESSAGES.RSC_ENDPOINT_ERROR, error);
1778
+ if (error.message.includes("Route not found")) {
1779
+ res.status(HTTP_STATUS.NOT_FOUND).json({ error: "Route not found" });
1780
+ } else {
1781
+ res.status(HTTP_STATUS.INTERNAL_ERROR).json({
1782
+ error: "Internal Server Error"
1783
+ });
1784
+ }
1785
+ }
1786
+ });
1787
+ }
1788
+ function setupImageEndpoint(app, config2) {
1789
+ app.get(SERVER.IMAGE_ENDPOINT, async (req, res) => {
1790
+ await handleImageRequest({ req, res, config: config2 });
1791
+ });
1792
+ }
1793
+ function setupAsyncEndpoint(app) {
1794
+ app.get(`${SERVER.ASYNC_ENDPOINT}/:id`, async (req, res) => {
1795
+ await handleAsyncRequest(req, res);
1796
+ });
1797
+ }
1798
+ function setupApiRoutes(app, getConfig) {
1799
+ app.use(express.json({ limit: "10mb" }));
1800
+ app.use(express.urlencoded({ extended: true, limit: "10mb" }));
1801
+ const apiHandler = createApiRouteHandler();
1802
+ app.use(async (req, res, next) => {
1803
+ const config2 = getConfig();
1804
+ if (!config2) {
1805
+ return next();
1806
+ }
1807
+ const url = new URL(req.url, `http://${req.headers.host}`);
1808
+ const pathname = url.pathname;
1809
+ const method = req.method;
1810
+ if (config2.apiRouteMap && config2.apiRouteMap.size > 0) {
1811
+ try {
1812
+ const handled = await apiHandler(pathname, method, req, res, config2);
1813
+ if (handled) {
1814
+ return;
1815
+ }
1816
+ } catch (error) {
1817
+ if (!res.headersSent) {
1818
+ return next();
1819
+ }
1820
+ return;
1821
+ }
1822
+ }
1823
+ next();
1824
+ });
1825
+ }
1826
+ function setupSSREndpoint(app, handler) {
1827
+ app.get("*", async (req, res) => {
1828
+ try {
1829
+ const url = new URL(req.url, `http://${req.headers.host}`);
1830
+ const result = await handler(
1831
+ url.pathname,
1832
+ Object.fromEntries(url.searchParams)
1833
+ );
1834
+ res.status(result.status);
1835
+ res.setHeader("Content-Type", HTTP_HEADERS.CONTENT_TYPE_HTML);
1836
+ const nodeStream = Readable.fromWeb(result.html);
1837
+ nodeStream.pipe(res);
1838
+ nodeStream.on("error", (err) => {
1839
+ if (!res.headersSent) {
1840
+ console.error(ERROR_MESSAGES.ERROR_HANDLING_REQUEST, err);
1841
+ res.status(HTTP_STATUS.INTERNAL_ERROR).send("Internal Server Error");
1842
+ }
1843
+ });
1844
+ } catch (error) {
1845
+ console.error(ERROR_MESSAGES.ERROR_HANDLING_REQUEST, error);
1846
+ res.status(HTTP_STATUS.INTERNAL_ERROR).send("Internal Server Error");
1847
+ }
1848
+ });
1849
+ }
1850
+
1851
+ // src/server/base-server.ts
1852
+ var BaseServer = class {
1853
+ constructor(port = SERVER.DEFAULT_PORT) {
1854
+ this.app = express2();
1855
+ this.port = port;
1856
+ }
1857
+ /**
1858
+ * Template method - defines the algorithm structure
1859
+ */
1860
+ async setup() {
1861
+ this.setupMiddleware();
1862
+ this.setupStaticFiles();
1863
+ this.setupImageEndpoint();
1864
+ this.setupAsyncEndpoint();
1865
+ this.setupApiRoutes();
1866
+ this.setupRSCEndpoint();
1867
+ this.setupSSREndpoint();
1868
+ }
1869
+ /**
1870
+ * Setup Express middleware
1871
+ */
1872
+ setupMiddleware() {
1873
+ setupExpressMiddleware(this.app);
1874
+ }
1875
+ /**
1876
+ * Setup static file serving
1877
+ */
1878
+ setupStaticFiles() {
1879
+ setupStaticFiles(this.app, getClientDir(), getPublicDir());
1880
+ }
1881
+ /**
1882
+ * Setup image optimization endpoint
1883
+ */
1884
+ setupImageEndpoint() {
1885
+ setupImageEndpoint(this.app);
1886
+ }
1887
+ /**
1888
+ * Setup async component endpoint
1889
+ */
1890
+ setupAsyncEndpoint() {
1891
+ setupAsyncEndpoint(this.app);
1892
+ }
1893
+ /**
1894
+ * Setup API routes
1895
+ */
1896
+ setupApiRoutes() {
1897
+ setupApiRoutes(this.app, () => {
1898
+ try {
1899
+ return getRouteConfig();
1900
+ } catch {
1901
+ return null;
1902
+ }
1903
+ });
1904
+ }
1905
+ /**
1906
+ * Setup RSC endpoint
1907
+ */
1908
+ setupRSCEndpoint() {
1909
+ setupRSCEndpoint(this.app, this.createRSCHandler());
1910
+ }
1911
+ /**
1912
+ * Setup SSR endpoint
1913
+ */
1914
+ setupSSREndpoint() {
1915
+ setupSSREndpoint(this.app, this.createSSRHandler());
1916
+ }
1917
+ /**
1918
+ * Start the server
1919
+ */
1920
+ async start() {
1921
+ await this.setup();
1922
+ this.app.listen(this.port, () => {
1923
+ console.log(
1924
+ `[loly-core] Server running at http://localhost:${this.port}`
1925
+ );
1926
+ });
1927
+ }
1928
+ /**
1929
+ * Get the Express app instance
1930
+ */
1931
+ getApp() {
1932
+ return this.app;
1933
+ }
1934
+ };
1935
+
1936
+ // src/server/dev-server.ts
1937
+ var routeConfig = null;
1938
+ var routeConfigPromise = null;
1939
+ async function reloadRoutes(appDir) {
1940
+ const config2 = await scanRoutes(appDir);
1941
+ routeConfig = config2;
1942
+ return config2;
1943
+ }
1944
+ async function getRouteConfig2(appDir) {
1945
+ if (routeConfig) {
1946
+ return routeConfig;
1947
+ }
1948
+ if (routeConfigPromise) {
1949
+ return routeConfigPromise;
1950
+ }
1951
+ routeConfigPromise = reloadRoutes(appDir);
1952
+ return routeConfigPromise;
1953
+ }
1954
+ var DevServer = class extends BaseServer {
1955
+ constructor(options) {
1956
+ super(options.port);
1957
+ this.watcher = null;
1958
+ this.projectDir = options.projectDir;
1959
+ }
1960
+ /**
1961
+ * Initialize server context and load routes
1962
+ */
1963
+ async initialize() {
1964
+ const appDir = getAppDirPath(this.projectDir);
1965
+ const srcDir = join3(this.projectDir, DIRECTORIES.SRC);
1966
+ const outDir = join3(this.projectDir, DIRECTORIES.LOLY, DIRECTORIES.DIST);
1967
+ if (!existsSync5(appDir)) {
1968
+ throw new Error(ERROR_MESSAGES.APP_DIR_NOT_FOUND(this.projectDir));
1969
+ }
1970
+ setContext({
1971
+ projectDir: this.projectDir,
1972
+ appDir,
1973
+ srcDir,
1974
+ outDir,
1975
+ buildMode: "development",
1976
+ isDev: true,
1977
+ serverPort: this.port
1978
+ });
1979
+ const initialConfig = await reloadRoutes(getAppDir());
1980
+ setRouteConfig(initialConfig);
1981
+ }
1982
+ /**
1983
+ * Create RSC handler
1984
+ */
1985
+ createRSCHandler() {
1986
+ return async (pathname, searchParams) => {
1987
+ const config2 = await getRouteConfig2(getAppDir());
1988
+ const context = {
1989
+ pathname,
1990
+ searchParams,
1991
+ params: {}
1992
+ };
1993
+ const result = await renderPageContent(context, config2, getAppDir());
1994
+ return {
1995
+ ...result,
1996
+ params: context.params
1997
+ };
1998
+ };
1999
+ }
2000
+ /**
2001
+ * Create SSR handler
2002
+ */
2003
+ createSSRHandler() {
2004
+ return async (pathname, searchParams) => {
2005
+ const config2 = await getRouteConfig2(getAppDir());
2006
+ const context = {
2007
+ pathname,
2008
+ searchParams,
2009
+ params: {}
2010
+ };
2011
+ return await renderPageStream(context, config2, getAppDir());
2012
+ };
2013
+ }
2014
+ /**
2015
+ * Load route configuration
2016
+ */
2017
+ async loadRouteConfig() {
2018
+ return await getRouteConfig2(getAppDir());
2019
+ }
2020
+ /**
2021
+ * Setup hot reload watcher
2022
+ */
2023
+ setupHotReload() {
2024
+ this.watcher = chokidar.watch(getAppDir(), {
2025
+ ignored: /node_modules/,
2026
+ persistent: true
2027
+ });
2028
+ const reloadHandler = async () => {
2029
+ routeConfig = null;
2030
+ routeConfigPromise = null;
2031
+ await reloadRoutes(getAppDir());
2032
+ };
2033
+ this.watcher.on("change", reloadHandler);
2034
+ this.watcher.on("add", reloadHandler);
2035
+ this.watcher.on("unlink", reloadHandler);
2036
+ }
2037
+ /**
2038
+ * Start the development server with hot reload
2039
+ */
2040
+ async start() {
2041
+ await this.initialize();
2042
+ await this.setup();
2043
+ this.setupHotReload();
2044
+ this.getApp().listen(this.port, () => {
2045
+ console.log(
2046
+ `[loly-core] Dev server running at http://localhost:${this.port}`
2047
+ );
2048
+ });
2049
+ }
2050
+ /**
2051
+ * Stop the server and cleanup
2052
+ */
2053
+ async stop() {
2054
+ if (this.watcher) {
2055
+ await this.watcher.close();
2056
+ this.watcher = null;
2057
+ }
2058
+ }
2059
+ };
2060
+ async function startDevServer(options) {
2061
+ const server = new DevServer(options);
2062
+ await server.start();
2063
+ }
2064
+
2065
+ // src/server/prod-server.ts
2066
+ import { join as join4 } from "path";
2067
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
2068
+ function loadRouteConfigFromManifest() {
2069
+ const manifestPath = getRoutesManifestPath();
2070
+ if (!existsSync6(manifestPath)) {
2071
+ throw new Error(ERROR_MESSAGES.ROUTES_MANIFEST_NOT_FOUND(manifestPath));
2072
+ }
2073
+ const manifestContent = readFileSync2(manifestPath, "utf-8");
2074
+ const manifest = JSON.parse(manifestContent);
2075
+ const routes = manifest.routes.map((route) => ({
2076
+ filePath: route.serverPath || route.sourcePath,
2077
+ // Prefer compiled path
2078
+ segments: route.segments,
2079
+ type: route.type,
2080
+ isRootLayout: route.isRootLayout,
2081
+ urlPath: route.urlPath,
2082
+ isApiRoute: route.isApiRoute || void 0,
2083
+ httpMethods: route.httpMethods || void 0
2084
+ }));
2085
+ const rootLayout = manifest.rootLayout ? {
2086
+ filePath: manifest.rootLayout.serverPath || manifest.rootLayout.sourcePath,
2087
+ segments: manifest.rootLayout.segments,
2088
+ type: manifest.rootLayout.type,
2089
+ isRootLayout: manifest.rootLayout.isRootLayout,
2090
+ urlPath: manifest.rootLayout.urlPath
2091
+ } : void 0;
2092
+ const routeMap = /* @__PURE__ */ new Map();
2093
+ const apiRouteMap = /* @__PURE__ */ new Map();
2094
+ for (const routeEntry of manifest.routeMap || []) {
2095
+ const route = {
2096
+ filePath: routeEntry.serverPath || routeEntry.sourcePath,
2097
+ segments: routeEntry.segments,
2098
+ type: routeEntry.type,
2099
+ isRootLayout: routeEntry.isRootLayout,
2100
+ urlPath: routeEntry.urlPath,
2101
+ isApiRoute: routeEntry.isApiRoute || void 0,
2102
+ httpMethods: routeEntry.httpMethods || void 0
2103
+ };
2104
+ if (routeEntry.type === "page") {
2105
+ routeMap.set(routeEntry.path, route);
2106
+ } else if (routeEntry.isApiRoute && routeEntry.urlPath) {
2107
+ apiRouteMap.set(routeEntry.urlPath, route);
2108
+ }
2109
+ }
2110
+ const routeMapManifest = /* @__PURE__ */ new Map();
2111
+ for (const routeEntry of manifest.routeMap || []) {
2112
+ routeMapManifest.set(routeEntry.path, routeEntry);
2113
+ }
2114
+ return {
2115
+ routes,
2116
+ rootLayout,
2117
+ routeMap,
2118
+ apiRouteMap,
2119
+ routeMapManifest,
2120
+ // Include manifest data for accessing compiled paths and chunks
2121
+ rootLayoutManifest: manifest.rootLayout,
2122
+ // Include root layout manifest
2123
+ routesManifest: manifest.routes
2124
+ // Include all routes from manifest for layout lookup
2125
+ };
2126
+ }
2127
+ var ProdServer = class extends BaseServer {
2128
+ constructor(options) {
2129
+ super(options.port);
2130
+ this.config = null;
2131
+ this.projectDir = options.projectDir;
2132
+ }
2133
+ /**
2134
+ * Initialize server context and load routes from manifest
2135
+ */
2136
+ async initialize() {
2137
+ const appDir = getAppDirPath(this.projectDir);
2138
+ const srcDir = join4(this.projectDir, DIRECTORIES.SRC);
2139
+ const outDir = join4(this.projectDir, DIRECTORIES.LOLY, DIRECTORIES.DIST);
2140
+ if (!existsSync6(appDir)) {
2141
+ throw new Error(ERROR_MESSAGES.APP_DIR_NOT_FOUND(this.projectDir));
2142
+ }
2143
+ setContext({
2144
+ projectDir: this.projectDir,
2145
+ appDir,
2146
+ srcDir,
2147
+ outDir,
2148
+ buildMode: "production",
2149
+ isDev: false,
2150
+ serverPort: this.port
2151
+ });
2152
+ this.config = loadRouteConfigFromManifest();
2153
+ setRouteConfig(this.config);
2154
+ }
2155
+ /**
2156
+ * Create RSC handler
2157
+ */
2158
+ createRSCHandler() {
2159
+ return async (pathname, searchParams) => {
2160
+ if (!this.config) {
2161
+ this.config = await this.loadRouteConfig();
2162
+ }
2163
+ const context = {
2164
+ pathname,
2165
+ searchParams,
2166
+ params: {}
2167
+ };
2168
+ const result = await renderPageContent(context, this.config, getAppDir());
2169
+ return {
2170
+ ...result,
2171
+ params: context.params
2172
+ };
2173
+ };
2174
+ }
2175
+ /**
2176
+ * Create SSR handler
2177
+ */
2178
+ createSSRHandler() {
2179
+ return async (pathname, searchParams) => {
2180
+ if (!this.config) {
2181
+ this.config = await this.loadRouteConfig();
2182
+ }
2183
+ const context = {
2184
+ pathname,
2185
+ searchParams,
2186
+ params: {}
2187
+ };
2188
+ return await renderPageStream(context, this.config, getAppDir());
2189
+ };
2190
+ }
2191
+ /**
2192
+ * Load route configuration from manifest
2193
+ */
2194
+ async loadRouteConfig() {
2195
+ return loadRouteConfigFromManifest();
2196
+ }
2197
+ /**
2198
+ * Start the production server
2199
+ */
2200
+ async start() {
2201
+ await this.initialize();
2202
+ await this.setup();
2203
+ this.getApp().listen(this.port, () => {
2204
+ console.log(
2205
+ `[loly-core] Production server running at http://localhost:${this.port}`
2206
+ );
2207
+ });
2208
+ }
2209
+ };
2210
+ async function startProdServer(options) {
2211
+ const server = new ProdServer(options);
2212
+ await server.start();
2213
+ }
2214
+
2215
+ // src/build/index.ts
2216
+ import rspackBuild from "@rspack/core";
2217
+ import { join as join9 } from "path";
2218
+ import { existsSync as existsSync10 } from "fs";
2219
+
2220
+ // src/build/manifest.ts
2221
+ import { writeFileSync, mkdirSync, existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
2222
+ import { join as join5, dirname as dirname2, relative as relative2 } from "path";
2223
+ function getServerCompiledPath(sourcePath, appDir) {
2224
+ const relativePath = relative2(appDir, sourcePath);
2225
+ const relativeWithoutExt = relativePath.replace(/\.(tsx?|jsx?)$/, ".js");
2226
+ const serverDistDir = getServerOutDir();
2227
+ return join5(serverDistDir, relativeWithoutExt).replace(/\\/g, "/");
2228
+ }
2229
+ function getClientChunksForRoute(routeIndex, clientManifestPath) {
2230
+ if (!existsSync7(clientManifestPath)) {
2231
+ return null;
2232
+ }
2233
+ try {
2234
+ const manifestContent = readFileSync3(clientManifestPath, "utf-8");
2235
+ const manifest = JSON.parse(manifestContent);
2236
+ const chunkName = `route-${routeIndex}`;
2237
+ const chunk = manifest[chunkName];
2238
+ if (!chunk) {
2239
+ return null;
2240
+ }
2241
+ const js = [];
2242
+ const css = [];
2243
+ if (chunk.file) {
2244
+ js.push(chunk.file);
2245
+ }
2246
+ if (chunk.css && chunk.css.length > 0) {
2247
+ css.push(...chunk.css);
2248
+ }
2249
+ if (chunk.imports && chunk.imports.length > 0) {
2250
+ for (const importName of chunk.imports) {
2251
+ const importChunk = manifest[importName];
2252
+ if (importChunk?.file) {
2253
+ js.push(importChunk.file);
2254
+ }
2255
+ if (importChunk?.css && importChunk.css.length > 0) {
2256
+ css.push(...importChunk.css);
2257
+ }
2258
+ }
2259
+ }
2260
+ return { js, css };
2261
+ } catch (error) {
2262
+ return null;
2263
+ }
2264
+ }
2265
+ function generateRoutesManifest(config2, projectDir, appDir) {
2266
+ const manifestDir = join5(getProjectDir(), DIRECTORIES.LOLY);
2267
+ if (!existsSync7(manifestDir)) {
2268
+ mkdirSync(manifestDir, { recursive: true });
2269
+ }
2270
+ const clientManifestPath = getManifestPath();
2271
+ const hasClientManifest = existsSync7(clientManifestPath);
2272
+ const pageRoutes = config2.routes.filter((r) => r.type === "page");
2273
+ let routeIndex = 0;
2274
+ const routesData = config2.routes.map((route) => {
2275
+ const sourcePath = route.filePath;
2276
+ const serverPath = getServerCompiledPath(sourcePath, appDir);
2277
+ let clientChunks = null;
2278
+ if (route.type === "page" && hasClientManifest) {
2279
+ clientChunks = getClientChunksForRoute(routeIndex, clientManifestPath);
2280
+ routeIndex++;
2281
+ }
2282
+ return {
2283
+ sourcePath,
2284
+ // Original TSX path (for development)
2285
+ serverPath,
2286
+ // Compiled server path (for production)
2287
+ segments: route.segments,
2288
+ type: route.type,
2289
+ isRootLayout: route.isRootLayout,
2290
+ urlPath: route.urlPath,
2291
+ isApiRoute: route.isApiRoute || void 0,
2292
+ httpMethods: route.httpMethods || void 0,
2293
+ clientChunks: clientChunks || void 0
2294
+ // Client chunks for this route
2295
+ };
2296
+ });
2297
+ const rootLayoutData = config2.rootLayout ? {
2298
+ sourcePath: config2.rootLayout.filePath,
2299
+ serverPath: getServerCompiledPath(
2300
+ config2.rootLayout.filePath,
2301
+ appDir
2302
+ ),
2303
+ segments: config2.rootLayout.segments,
2304
+ type: config2.rootLayout.type,
2305
+ isRootLayout: config2.rootLayout.isRootLayout,
2306
+ urlPath: config2.rootLayout.urlPath
2307
+ } : void 0;
2308
+ routeIndex = 0;
2309
+ const routeMapData = Array.from(config2.routeMap.entries()).map(
2310
+ ([path4, route]) => {
2311
+ const sourcePath = route.filePath;
2312
+ const serverPath = getServerCompiledPath(sourcePath, appDir);
2313
+ let clientChunks = null;
2314
+ if (hasClientManifest) {
2315
+ clientChunks = getClientChunksForRoute(routeIndex, clientManifestPath);
2316
+ routeIndex++;
2317
+ }
2318
+ return {
2319
+ path: path4,
2320
+ sourcePath,
2321
+ serverPath,
2322
+ segments: route.segments,
2323
+ type: route.type,
2324
+ isRootLayout: route.isRootLayout,
2325
+ urlPath: route.urlPath,
2326
+ isApiRoute: route.isApiRoute || void 0,
2327
+ httpMethods: route.httpMethods || void 0,
2328
+ clientChunks: clientChunks || void 0
2329
+ };
2330
+ }
2331
+ );
2332
+ if (config2.apiRouteMap) {
2333
+ for (const [path4, route] of config2.apiRouteMap.entries()) {
2334
+ const sourcePath = route.filePath;
2335
+ const serverPath = getServerCompiledPath(sourcePath, appDir);
2336
+ routeMapData.push({
2337
+ path: path4,
2338
+ sourcePath,
2339
+ serverPath,
2340
+ segments: route.segments,
2341
+ type: route.type,
2342
+ isRootLayout: route.isRootLayout,
2343
+ urlPath: route.urlPath,
2344
+ isApiRoute: route.isApiRoute || void 0,
2345
+ httpMethods: route.httpMethods || void 0,
2346
+ clientChunks: void 0
2347
+ // API routes don't have client chunks
2348
+ });
2349
+ }
2350
+ }
2351
+ const manifest = {
2352
+ routes: routesData,
2353
+ rootLayout: rootLayoutData,
2354
+ routeMap: routeMapData
2355
+ };
2356
+ const manifestPath = getRoutesManifestPath();
2357
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
2358
+ }
2359
+ function generateBootstrap(projectDir, routes) {
2360
+ const bootstrapDir = join5(getProjectDir(), DIRECTORIES.LOLY);
2361
+ if (!existsSync7(bootstrapDir)) {
2362
+ mkdirSync(bootstrapDir, { recursive: true });
2363
+ }
2364
+ const routeImports = routes.map((route, index) => {
2365
+ const routePath = route.filePath.replace(/\\/g, "/");
2366
+ const projectPath = getProjectDir().replace(/\\/g, "/");
2367
+ let relativePath = routePath.replace(projectPath, "").replace(/^\//, "");
2368
+ relativePath = `../${relativePath}`;
2369
+ return ` {
2370
+ path: "${route.urlPath}",
2371
+ component: async () => {
2372
+ const mod = await import(/* webpackChunkName: "route-${index}" */ "${relativePath}");
2373
+ return mod.default || mod;
2374
+ },
2375
+ }`;
2376
+ }).join(",\n");
2377
+ const bootstrapContent = `import { bootstrapClient } from "loly/client";
2378
+
2379
+ const routes = [
2380
+ ${routeImports}
2381
+ ];
2382
+
2383
+ const notFoundRoute = null;
2384
+ const errorRoute = null;
2385
+
2386
+ try {
2387
+ bootstrapClient({
2388
+ routes,
2389
+ notFoundRoute,
2390
+ errorRoute,
2391
+ });
2392
+ } catch (error) {
2393
+ console.error("[bootstrap] Fatal error during bootstrap:", error);
2394
+ throw error;
2395
+ }
2396
+ `;
2397
+ const bootstrapPath = getBootstrapPath();
2398
+ writeFileSync(bootstrapPath, bootstrapContent);
2399
+ }
2400
+
2401
+ // src/build/client.ts
2402
+ import { join as join6, normalize as normalize2, relative as relative3 } from "path";
2403
+
2404
+ // src/build/server-only.ts
2405
+ import { readFileSync as readFileSync4 } from "fs";
2406
+ import { glob as glob2 } from "glob";
2407
+ import { normalize } from "path";
2408
+ function hasUseServerDirective(filePath) {
2409
+ try {
2410
+ const content = readFileSync4(filePath, "utf-8");
2411
+ const trimmed = content.trim();
2412
+ return trimmed.startsWith('"use server"') || trimmed.startsWith("'use server'") || trimmed.startsWith('"use server";') || trimmed.startsWith("'use server';");
2413
+ } catch {
2414
+ return false;
2415
+ }
2416
+ }
2417
+ async function scanServerOnlyFiles(srcDir) {
2418
+ const serverOnlyFiles = /* @__PURE__ */ new Set();
2419
+ if (!srcDir) {
2420
+ return serverOnlyFiles;
2421
+ }
2422
+ try {
2423
+ const patterns = ["**/*.{ts,tsx,js,jsx}"];
2424
+ const files = await glob2(patterns, {
2425
+ cwd: srcDir,
2426
+ absolute: true,
2427
+ ignore: ["**/node_modules/**", "**/.loly/**", "**/dist/**", "**/.next/**"]
2428
+ });
2429
+ for (const file of files) {
2430
+ if (hasUseServerDirective(file)) {
2431
+ const normalizedPath = normalize(file);
2432
+ serverOnlyFiles.add(normalizedPath);
2433
+ }
2434
+ }
2435
+ } catch (error) {
2436
+ console.warn("[loly-core] Failed to scan for server-only files:", error);
2437
+ }
2438
+ return serverOnlyFiles;
2439
+ }
2440
+
2441
+ // src/build/client.ts
2442
+ function generateServerOnlyStubCode() {
2443
+ return `
2444
+ const SERVER_ONLY_ERROR = "This module is marked with 'use server' and can only be imported in server components.";
2445
+
2446
+ function createErrorFunction(propName) {
2447
+ const errorFn = function (...args) {
2448
+ throw new Error(SERVER_ONLY_ERROR + " Attempted to call: " + propName);
2449
+ };
2450
+ errorFn.toString = () => "[ServerOnlyStub:" + propName + "]";
2451
+ errorFn[Symbol.toStringTag] = "Function";
2452
+ errorFn.then = () => Promise.reject(new Error(SERVER_ONLY_ERROR + " Attempted to await: " + propName));
2453
+ errorFn.catch = () => Promise.reject(new Error(SERVER_ONLY_ERROR + " Attempted to catch: " + propName));
2454
+ return errorFn;
2455
+ }
2456
+
2457
+ function createServerOnlyProxy() {
2458
+ return new Proxy({}, {
2459
+ get(_target, prop) {
2460
+ return createErrorFunction(String(prop));
2461
+ },
2462
+ set() {
2463
+ throw new Error(SERVER_ONLY_ERROR);
2464
+ },
2465
+ has() {
2466
+ return true;
2467
+ },
2468
+ ownKeys() {
2469
+ return [];
2470
+ },
2471
+ getOwnPropertyDescriptor(_target, prop) {
2472
+ return {
2473
+ enumerable: true,
2474
+ configurable: true,
2475
+ get: () => createErrorFunction(String(prop)),
2476
+ };
2477
+ },
2478
+ });
2479
+ }
2480
+
2481
+ const serverOnlyStub = createServerOnlyProxy();
2482
+ export default serverOnlyStub;
2483
+ `;
2484
+ }
2485
+ function matchServerOnlyFile(request, serverOnlyFiles, srcDir) {
2486
+ const normalizedSrcDir = normalize2(srcDir).replace(/\\/g, "/");
2487
+ const normalizedRequest = request.replace(/\\/g, "/");
2488
+ for (const serverOnlyPath of serverOnlyFiles) {
2489
+ const normalizedPath = normalize2(serverOnlyPath).replace(/\\/g, "/");
2490
+ if (normalizedRequest === normalizedPath) {
2491
+ return serverOnlyPath;
2492
+ }
2493
+ const relativeFromSrc = normalizedPath.replace(normalizedSrcDir + "/", "").replace(/^\/+/, "");
2494
+ const relativeWithoutExt = relativeFromSrc.replace(/\.(ts|tsx|js|jsx)$/, "");
2495
+ if (normalizedRequest === relativeFromSrc || normalizedRequest === relativeWithoutExt) {
2496
+ return serverOnlyPath;
2497
+ }
2498
+ if (normalizedRequest.endsWith("/" + relativeFromSrc) || normalizedRequest.endsWith("/" + relativeWithoutExt)) {
2499
+ return serverOnlyPath;
2500
+ }
2501
+ try {
2502
+ const relativePath = relative3(srcDir, serverOnlyPath).replace(/\\/g, "/");
2503
+ const relativePathWithoutExt = relativePath.replace(/\.(ts|tsx|js|jsx)$/, "");
2504
+ if (normalizedRequest === relativePath || normalizedRequest === relativePathWithoutExt) {
2505
+ return serverOnlyPath;
2506
+ }
2507
+ if (normalizedRequest.endsWith("/" + relativePath) || normalizedRequest.endsWith("/" + relativePathWithoutExt)) {
2508
+ return serverOnlyPath;
2509
+ }
2510
+ const requestParts = normalizedRequest.split("/").filter((p) => p && p !== ".");
2511
+ const relativeParts = relativePathWithoutExt.split("/").filter((p) => p && p !== ".");
2512
+ if (relativeParts.length > 0 && requestParts.length >= relativeParts.length) {
2513
+ const requestEnd = requestParts.slice(-relativeParts.length).join("/");
2514
+ const relativeEnd = relativeParts.join("/");
2515
+ if (requestEnd === relativeEnd) {
2516
+ return serverOnlyPath;
2517
+ }
2518
+ }
2519
+ } catch {
2520
+ }
2521
+ const filename = normalizedPath.split("/").pop();
2522
+ if (filename) {
2523
+ const filenameWithoutExt = filename.replace(/\.(ts|tsx|js|jsx)$/, "");
2524
+ if (normalizedRequest === filename || normalizedRequest === filenameWithoutExt) {
2525
+ return serverOnlyPath;
2526
+ }
2527
+ if (normalizedRequest.endsWith("/" + filename) || normalizedRequest.endsWith("/" + filenameWithoutExt)) {
2528
+ return serverOnlyPath;
2529
+ }
2530
+ }
2531
+ try {
2532
+ if (!normalizedRequest.startsWith("/") && !normalizedRequest.startsWith(".")) {
2533
+ if (normalizedRequest === relativeWithoutExt || normalizedRequest === relativeFromSrc) {
2534
+ return serverOnlyPath;
2535
+ }
2536
+ }
2537
+ } catch {
2538
+ }
2539
+ }
2540
+ return null;
2541
+ }
2542
+ async function createClientConfig(options) {
2543
+ const { mode = "production" } = options;
2544
+ const bootstrapEntry = getBootstrapPath();
2545
+ const srcDir = getSrcDir();
2546
+ const serverOnlyFiles = await scanServerOnlyFiles(srcDir);
2547
+ const stubCode = generateServerOnlyStubCode();
2548
+ const entries = {
2549
+ main: bootstrapEntry
2550
+ };
2551
+ return {
2552
+ mode,
2553
+ entry: entries,
2554
+ output: {
2555
+ path: getClientDir(),
2556
+ filename: "[name].[contenthash].js",
2557
+ chunkFilename: "[name].[contenthash].js",
2558
+ clean: true,
2559
+ publicPath: "/"
2560
+ },
2561
+ resolve: {
2562
+ extensions: [".ts", ".tsx", ".js", ".jsx"],
2563
+ alias: {
2564
+ "loly": join6(
2565
+ getProjectDir(),
2566
+ "node_modules",
2567
+ "loly",
2568
+ "dist",
2569
+ "client.js"
2570
+ )
2571
+ }
2572
+ },
2573
+ externals: [
2574
+ "express",
2575
+ "chokidar",
2576
+ "glob",
2577
+ "@rspack/core",
2578
+ "@rspack/cli",
2579
+ "tsx",
2580
+ ({ request, contextInfo }) => {
2581
+ if (!request) return false;
2582
+ if (request.startsWith("node:")) return request;
2583
+ if (NODE_BUILTINS.includes(request)) return `node:${request}`;
2584
+ return false;
2585
+ }
2586
+ ],
2587
+ externalsType: "module",
2588
+ module: {
2589
+ rules: [
2590
+ {
2591
+ test: /\.tsx?$/,
2592
+ use: [
2593
+ {
2594
+ loader: "builtin:swc-loader",
2595
+ options: {
2596
+ jsc: {
2597
+ parser: {
2598
+ syntax: "typescript",
2599
+ tsx: true,
2600
+ decorators: false,
2601
+ dynamicImport: true
2602
+ },
2603
+ transform: {
2604
+ react: {
2605
+ runtime: "automatic",
2606
+ importSource: "loly-jsx",
2607
+ throwIfNamespace: false
2608
+ }
2609
+ },
2610
+ target: "es2020"
2611
+ },
2612
+ module: {
2613
+ type: "es6"
2614
+ }
2615
+ }
2616
+ }
2617
+ ]
2618
+ },
2619
+ {
2620
+ test: /\.css$/,
2621
+ type: "css"
2622
+ }
2623
+ ]
2624
+ },
2625
+ optimization: {
2626
+ minimize: mode === "production",
2627
+ splitChunks: {
2628
+ chunks: "all"
2629
+ },
2630
+ usedExports: true,
2631
+ sideEffects: false,
2632
+ // More aggressive tree shaking - assume no side effects
2633
+ providedExports: true,
2634
+ concatenateModules: true,
2635
+ // Better for tree shaking
2636
+ removeEmptyChunks: true,
2637
+ mergeDuplicateChunks: true
2638
+ },
2639
+ plugins: [
2640
+ {
2641
+ name: "server-only-plugin",
2642
+ apply(compiler) {
2643
+ compiler.hooks.normalModuleFactory.tap("server-only-plugin", (factory) => {
2644
+ factory.hooks.beforeResolve.tap("server-only-plugin", (data) => {
2645
+ if (!data.request) return;
2646
+ const request = data.request;
2647
+ if (request.startsWith("data:")) {
2648
+ return;
2649
+ }
2650
+ const matchedFile = matchServerOnlyFile(request, serverOnlyFiles, srcDir);
2651
+ if (matchedFile) {
2652
+ const encodedStub = Buffer.from(stubCode.trim()).toString("base64");
2653
+ data.request = `data:text/javascript;base64,${encodedStub}`;
2654
+ }
2655
+ });
2656
+ });
2657
+ }
2658
+ },
2659
+ {
2660
+ name: "manifest-plugin",
2661
+ apply(compiler) {
2662
+ compiler.hooks.emit.tap("manifest-plugin", (compilation) => {
2663
+ const manifest = {};
2664
+ const entrypoints = compilation.entrypoints || /* @__PURE__ */ new Map();
2665
+ const entryChunkNames = /* @__PURE__ */ new Set();
2666
+ for (const [name, entrypoint] of entrypoints) {
2667
+ entryChunkNames.add(name);
2668
+ const chunks2 = entrypoint.chunks || [];
2669
+ for (const chunk of chunks2) {
2670
+ const chunkName = chunk.name || chunk.id || "unknown";
2671
+ entryChunkNames.add(chunkName);
2672
+ }
2673
+ }
2674
+ const chunks = compilation.chunks || [];
2675
+ for (const chunk of chunks) {
2676
+ const name = chunk.name || chunk.id || "unknown";
2677
+ const files = [];
2678
+ const cssFiles = [];
2679
+ const chunkFiles = chunk.files || [];
2680
+ for (const file of chunkFiles) {
2681
+ if (file.endsWith(".css")) {
2682
+ cssFiles.push(file);
2683
+ } else {
2684
+ files.push(file);
2685
+ }
2686
+ }
2687
+ const isEntry = entryChunkNames.has(name) || chunk.hasEntryModule && chunk.hasEntryModule() || name === "main";
2688
+ manifest[name] = {
2689
+ file: files[0] || "",
2690
+ isEntry,
2691
+ imports: [],
2692
+ css: cssFiles.length > 0 ? cssFiles : void 0
2693
+ };
2694
+ }
2695
+ const manifestJson = JSON.stringify(manifest, null, 2);
2696
+ compilation.emitAsset("manifest.json", {
2697
+ source: () => manifestJson,
2698
+ size: () => Buffer.byteLength(manifestJson, "utf8")
2699
+ });
2700
+ });
2701
+ }
2702
+ }
2703
+ ],
2704
+ devtool: mode === "production" ? false : "source-map"
2705
+ };
2706
+ }
2707
+
2708
+ // src/build/server.ts
2709
+ import { join as join7 } from "path";
2710
+ import { existsSync as existsSync8, readdirSync } from "fs";
2711
+ function collectAppSources(appDir) {
2712
+ const entries = [];
2713
+ function walk(dir) {
2714
+ if (!existsSync8(dir)) return;
2715
+ const items = readdirSync(dir, { withFileTypes: true });
2716
+ for (const item of items) {
2717
+ const full = join7(dir, item.name);
2718
+ if (item.isDirectory()) {
2719
+ if (item.name === "node_modules" || item.name.startsWith(".")) {
2720
+ continue;
2721
+ }
2722
+ walk(full);
2723
+ continue;
2724
+ }
2725
+ if (item.isFile()) {
2726
+ const isRouteFile = ROUTE_FILE_NAMES.some(
2727
+ (routeFile) => item.name === routeFile
2728
+ );
2729
+ if (isRouteFile && (full.endsWith(".ts") || full.endsWith(".tsx"))) {
2730
+ if (full.endsWith(".d.ts")) continue;
2731
+ entries.push(full);
2732
+ }
2733
+ }
2734
+ }
2735
+ }
2736
+ walk(appDir);
2737
+ return entries;
2738
+ }
2739
+ async function buildServerFiles(options) {
2740
+ const { mode = "production" } = options;
2741
+ const appDir = getAppDir();
2742
+ const serverOutDir = getServerOutDir();
2743
+ if (existsSync8(serverOutDir)) {
2744
+ const { rmSync } = await import("fs");
2745
+ rmSync(serverOutDir, { recursive: true, force: true });
2746
+ }
2747
+ const entryPoints = collectAppSources(appDir);
2748
+ if (entryPoints.length === 0) {
2749
+ return;
2750
+ }
2751
+ try {
2752
+ const { build } = await import("esbuild");
2753
+ await build({
2754
+ entryPoints,
2755
+ outdir: serverOutDir,
2756
+ outbase: appDir,
2757
+ // Preserve directory structure
2758
+ format: "esm",
2759
+ platform: "node",
2760
+ target: "es2020",
2761
+ bundle: true,
2762
+ // Bundle to resolve relative imports
2763
+ packages: "external",
2764
+ // Externalize node_modules packages
2765
+ external: [
2766
+ // Explicitly externalize loly-jsx to avoid bundling it
2767
+ "loly-jsx",
2768
+ "loly-jsx/*",
2769
+ "loly-jsx/jsx-runtime",
2770
+ "loly-jsx/render/*",
2771
+ "loly-jsx/router"
2772
+ ],
2773
+ sourcemap: true,
2774
+ jsx: "automatic",
2775
+ jsxImportSource: "loly-jsx",
2776
+ logLevel: mode === "production" ? "warning" : "info"
2777
+ });
2778
+ } catch (error) {
2779
+ console.error(`[loly-core] Failed to build server files:`, error);
2780
+ throw error;
2781
+ }
2782
+ }
2783
+
2784
+ // src/build/css-processor.ts
2785
+ import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, copyFileSync } from "fs";
2786
+ import { join as join8, dirname as dirname3 } from "path";
2787
+ import { pathToFileURL } from "url";
2788
+ import { createRequire } from "module";
2789
+ async function processCssWithPostCSS(options) {
2790
+ const { inputPath, outputPath, projectDir } = options;
2791
+ let postcss;
2792
+ try {
2793
+ const postcssModule = await import("postcss");
2794
+ postcss = postcssModule.default || postcssModule;
2795
+ } catch {
2796
+ mkdirSync2(dirname3(outputPath), { recursive: true });
2797
+ copyFileSync(inputPath, outputPath);
2798
+ return;
2799
+ }
2800
+ const configPaths = [
2801
+ join8(projectDir, "postcss.config.js"),
2802
+ join8(projectDir, "postcss.config.mjs"),
2803
+ join8(projectDir, "postcss.config.cjs")
2804
+ ];
2805
+ const configPath = configPaths.find((p) => existsSync9(p));
2806
+ if (!configPath) {
2807
+ mkdirSync2(dirname3(outputPath), { recursive: true });
2808
+ copyFileSync(inputPath, outputPath);
2809
+ return;
2810
+ }
2811
+ const configUrl = pathToFileURL(configPath).href;
2812
+ const configModule = await import(configUrl);
2813
+ const config2 = configModule.default || configModule;
2814
+ const css = readFileSync5(inputPath, "utf-8");
2815
+ let plugins = config2.plugins || [];
2816
+ const projectRequire = createRequire(join8(projectDir, "package.json"));
2817
+ if (plugins && typeof plugins === "object" && !Array.isArray(plugins)) {
2818
+ const pluginArray = [];
2819
+ for (const [name, options2] of Object.entries(plugins)) {
2820
+ try {
2821
+ const pluginPath = projectRequire.resolve(name);
2822
+ const pluginUrl = pathToFileURL(pluginPath).href;
2823
+ const pluginModule = await import(pluginUrl);
2824
+ const plugin = pluginModule.default || pluginModule;
2825
+ const pluginOptions = options2 && typeof options2 === "object" && Object.keys(options2).length > 0 ? options2 : void 0;
2826
+ pluginArray.push(pluginOptions ? plugin(pluginOptions) : plugin());
2827
+ } catch (err) {
2828
+ console.warn(`[loly-core] Failed to load PostCSS plugin "${name}":`, err);
2829
+ throw err;
2830
+ }
2831
+ }
2832
+ plugins = pluginArray;
2833
+ }
2834
+ const result = await postcss(plugins).process(css, {
2835
+ from: inputPath,
2836
+ to: outputPath
2837
+ });
2838
+ mkdirSync2(dirname3(outputPath), { recursive: true });
2839
+ writeFileSync2(outputPath, result.css);
2840
+ if (result.map) {
2841
+ writeFileSync2(outputPath + ".map", result.map.toString());
2842
+ }
2843
+ }
2844
+
2845
+ // src/build/index.ts
2846
+ async function buildProject(options) {
2847
+ const {
2848
+ projectDir,
2849
+ outDir = join9(projectDir, DIRECTORIES.LOLY, DIRECTORIES.DIST)
2850
+ } = options;
2851
+ const appDir = getAppDirPath(projectDir);
2852
+ const srcDir = join9(projectDir, DIRECTORIES.SRC);
2853
+ if (!existsSync10(appDir)) {
2854
+ throw new Error(ERROR_MESSAGES.APP_DIR_NOT_FOUND(projectDir));
2855
+ }
2856
+ setContext({
2857
+ projectDir,
2858
+ appDir,
2859
+ srcDir,
2860
+ outDir,
2861
+ buildMode: options.mode || "production",
2862
+ isDev: options.mode === "development"
2863
+ });
2864
+ const routeConfig2 = await scanRoutes(getAppDir());
2865
+ setRouteConfig(routeConfig2);
2866
+ const pageRoutes = routeConfig2.routes.filter((r) => r.type === "page");
2867
+ generateBootstrap(getProjectDir(), pageRoutes);
2868
+ const clientConfig = await createClientConfig(options);
2869
+ try {
2870
+ await new Promise((resolve6, reject) => {
2871
+ const compiler = rspackBuild(clientConfig);
2872
+ compiler.run((err, stats) => {
2873
+ if (err) {
2874
+ console.error("[loly-core] Client build error:", err);
2875
+ reject(err);
2876
+ return;
2877
+ }
2878
+ if (stats && stats.hasErrors()) {
2879
+ console.error("[loly-core] Client build has errors");
2880
+ console.error(stats.compilation.errors);
2881
+ reject(new Error(ERROR_MESSAGES.CLIENT_BUILD_FAILED));
2882
+ return;
2883
+ }
2884
+ resolve6();
2885
+ });
2886
+ });
2887
+ } catch (error) {
2888
+ console.error(ERROR_MESSAGES.CLIENT_BUILD_FAILED, error);
2889
+ throw error;
2890
+ }
2891
+ const globalsCssPath = existsSync10(join9(getSrcDir(), FILE_NAMES.GLOBALS_CSS)) ? join9(getSrcDir(), FILE_NAMES.GLOBALS_CSS) : existsSync10(join9(getAppDir(), FILE_NAMES.GLOBALS_CSS)) ? join9(getAppDir(), FILE_NAMES.GLOBALS_CSS) : null;
2892
+ const clientOutDir = getClientDir();
2893
+ if (globalsCssPath && existsSync10(globalsCssPath)) {
2894
+ const destCssPath = join9(clientOutDir, FILE_NAMES.GLOBALS_CSS);
2895
+ await processCssWithPostCSS({
2896
+ inputPath: globalsCssPath,
2897
+ outputPath: destCssPath,
2898
+ projectDir: getProjectDir()
2899
+ });
2900
+ }
2901
+ try {
2902
+ await buildServerFiles(options);
2903
+ } catch (error) {
2904
+ console.error(ERROR_MESSAGES.SERVER_BUILD_FAILED, error);
2905
+ throw error;
2906
+ }
2907
+ generateRoutesManifest(routeConfig2, getProjectDir(), getAppDir());
2908
+ }
2909
+
2910
+ // src/cli/index.ts
2911
+ import { resolve as resolve5 } from "path";
2912
+ import { existsSync as existsSync11 } from "fs";
2913
+ function loadEnvFile(projectDir) {
2914
+ const envPath = resolve5(projectDir, ".env");
2915
+ if (existsSync11(envPath)) {
2916
+ config({ path: envPath });
2917
+ }
2918
+ const envLocalPath = resolve5(projectDir, ".env.local");
2919
+ if (existsSync11(envLocalPath)) {
2920
+ config({ path: envLocalPath, override: true });
2921
+ }
2922
+ }
2923
+ var commands = {
2924
+ dev: async (args) => {
2925
+ const projectDir = args[0] ? resolve5(args[0]) : process.cwd();
2926
+ if (!existsSync11(projectDir)) {
2927
+ console.error(`[loly-core] Directory not found: ${projectDir}`);
2928
+ process.exit(1);
2929
+ }
2930
+ loadEnvFile(projectDir);
2931
+ const port = args[1] ? parseInt(args[1], 10) : void 0;
2932
+ try {
2933
+ await startDevServer({
2934
+ projectDir,
2935
+ port
2936
+ });
2937
+ } catch (error) {
2938
+ console.error("[loly-core] Error starting dev server:", error);
2939
+ process.exit(1);
2940
+ }
2941
+ },
2942
+ build: async (args) => {
2943
+ const projectDir = args[0] ? resolve5(args[0]) : process.cwd();
2944
+ if (!existsSync11(projectDir)) {
2945
+ console.error(`[loly-core] Directory not found: ${projectDir}`);
2946
+ process.exit(1);
2947
+ }
2948
+ loadEnvFile(projectDir);
2949
+ const mode = args.includes("--dev") ? "development" : "production";
2950
+ const outDir = args.find((arg) => arg.startsWith("--out-dir="))?.split("=")[1];
2951
+ try {
2952
+ await buildProject({
2953
+ projectDir,
2954
+ outDir: outDir ? resolve5(outDir) : void 0,
2955
+ mode
2956
+ });
2957
+ } catch (error) {
2958
+ console.error("[loly-core] Error building project:", error);
2959
+ process.exit(1);
2960
+ }
2961
+ },
2962
+ start: async (args) => {
2963
+ const projectDir = args[0] ? resolve5(args[0]) : process.cwd();
2964
+ if (!existsSync11(projectDir)) {
2965
+ console.error(`[loly-core] Directory not found: ${projectDir}`);
2966
+ process.exit(1);
2967
+ }
2968
+ loadEnvFile(projectDir);
2969
+ const port = args[1] ? parseInt(args[1], 10) : void 0;
2970
+ try {
2971
+ await startProdServer({
2972
+ projectDir,
2973
+ port
2974
+ });
2975
+ } catch (error) {
2976
+ console.error("[loly-core] Error starting production server:", error);
2977
+ process.exit(1);
2978
+ }
2979
+ }
2980
+ };
2981
+ async function main() {
2982
+ const args = process.argv.slice(2);
2983
+ const command = args[0];
2984
+ if (!command || !(command in commands)) {
2985
+ console.log(`
2986
+ Usage: loly <command> [options]
2987
+
2988
+ Commands:
2989
+ dev [dir] [port] Start development server
2990
+ --stream Use streaming SSR
2991
+
2992
+ build [dir] Build for production
2993
+ --dev Build in development mode
2994
+ --out-dir=<path> Output directory
2995
+
2996
+ start [dir] [port] Start production server
2997
+ --stream Use streaming SSR
2998
+
2999
+ Examples:
3000
+ loly dev
3001
+ loly dev . 3000
3002
+ loly build
3003
+ loly build . --out-dir=dist
3004
+ loly start
3005
+ loly start . 3000
3006
+ `);
3007
+ process.exit(1);
3008
+ }
3009
+ const commandArgs = args.slice(1);
3010
+ await commands[command](commandArgs);
3011
+ }
3012
+ main().catch((error) => {
3013
+ console.error("[loly-core] Unexpected error:", error);
3014
+ process.exit(1);
3015
+ });