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/dist/index.js ADDED
@@ -0,0 +1,3826 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/router/scanner.ts
9
+ import { glob } from "glob";
10
+ import { relative, dirname, sep } from "path";
11
+
12
+ // src/constants/routes.ts
13
+ var ROUTE_FILE_NAMES = [
14
+ "page.tsx",
15
+ "page.ts",
16
+ "layout.tsx",
17
+ "layout.ts",
18
+ "route.ts",
19
+ "route.tsx"
20
+ ];
21
+
22
+ // src/constants/directories.ts
23
+ var DIRECTORIES = {
24
+ SRC: "src",
25
+ APP: "app",
26
+ DIST: "dist",
27
+ CLIENT: "client",
28
+ SERVER: "server",
29
+ PUBLIC: "public",
30
+ LOLY: ".loly"
31
+ };
32
+
33
+ // src/constants/files.ts
34
+ var FILE_NAMES = {
35
+ MANIFEST: "manifest.json",
36
+ BOOTSTRAP: "bootstrap.ts",
37
+ GLOBALS_CSS: "globals.css",
38
+ ROUTES_MANIFEST: "routes-manifest.json"
39
+ };
40
+
41
+ // src/constants/http.ts
42
+ var HTTP_STATUS = {
43
+ OK: 200,
44
+ BAD_REQUEST: 400,
45
+ NOT_FOUND: 404,
46
+ METHOD_NOT_ALLOWED: 405,
47
+ REQUEST_TIMEOUT: 408,
48
+ INTERNAL_ERROR: 500
49
+ };
50
+ var HTTP_HEADERS = {
51
+ CONTENT_TYPE_JSON: "application/json",
52
+ CONTENT_TYPE_HTML: "text/html; charset=utf-8"
53
+ };
54
+
55
+ // src/constants/server.ts
56
+ var SERVER = {
57
+ DEFAULT_PORT: 3e3,
58
+ RSC_ENDPOINT: "/__loly/rsc",
59
+ IMAGE_ENDPOINT: "/_loly/image",
60
+ ASYNC_ENDPOINT: "/__loly/async",
61
+ APP_CONTAINER_ID: "app"
62
+ };
63
+
64
+ // src/constants/node-builtins.ts
65
+ var NODE_BUILTINS = [
66
+ "fs",
67
+ "path",
68
+ "url",
69
+ "util",
70
+ "stream",
71
+ "events",
72
+ "crypto",
73
+ "http",
74
+ "https",
75
+ "os",
76
+ "querystring",
77
+ "buffer",
78
+ "constants",
79
+ "child_process",
80
+ "module",
81
+ "fs/promises",
82
+ "worker_threads",
83
+ "zlib",
84
+ "inspector",
85
+ "perf_hooks"
86
+ ];
87
+
88
+ // src/constants/errors.ts
89
+ var ERROR_MESSAGES = {
90
+ CONTEXT_NOT_INITIALIZED: "[loly-core] Framework context not initialized. Call setContext() first.",
91
+ APP_DIR_NOT_FOUND: (dir) => `[loly-core] src/app/ directory not found in ${dir}. Please ensure your project has a src/app/ directory.`,
92
+ ROUTE_NOT_FOUND: (pathname) => `Route not found: ${pathname}`,
93
+ PAGE_COMPONENT_NOT_FOUND: (path4) => `[loly-core] Page component not found in ${path4}`,
94
+ PAGE_COMPONENT_MUST_BE_FUNCTION: "[loly-core] Page component must be a function",
95
+ COMPONENT_NOT_FOUND: (route) => `Component not found for route: ${route}`,
96
+ APP_CONTAINER_NOT_FOUND: "[loly-core] App container not found (#app)",
97
+ 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.`,
98
+ ROUTES_MANIFEST_NOT_FOUND: (path4) => `[loly-core] Routes manifest not found: ${path4}. Run 'loly build' first.`,
99
+ DIRECTORY_NOT_FOUND: (dir) => `[loly-core] Directory not found: ${dir}`,
100
+ CLIENT_BUILD_FAILED: "Client build failed",
101
+ SERVER_BUILD_FAILED: "Server build failed",
102
+ FAILED_TO_LOAD_MODULE: (path4) => `[loly-core] Failed to load module: ${path4}`,
103
+ FAILED_TO_LOAD_ROUTE_COMPONENT: (path4) => `[bootstrap] Failed to load component for route ${path4}`,
104
+ FAILED_TO_LOAD_NESTED_LAYOUT: (path4) => `[loly-core] Failed to load nested layout at ${path4}`,
105
+ FAILED_TO_READ_CLIENT_MANIFEST: "[loly-core] Failed to read client manifest:",
106
+ FAILED_TO_PARSE_ISLAND_DATA: "[loly-core] Failed to parse island data:",
107
+ FATAL_BOOTSTRAP_ERROR: "[bootstrap] Fatal error during bootstrap:",
108
+ UNEXPECTED_ERROR: "[loly-core] Unexpected error:",
109
+ ERROR_STARTING_DEV_SERVER: "[loly-core] Error starting dev server:",
110
+ ERROR_STARTING_PROD_SERVER: "[loly-core] Error starting production server:",
111
+ ERROR_BUILDING_PROJECT: "[loly-core] Error building project:",
112
+ ERROR_HANDLING_REQUEST: "[loly-core] Error handling request:",
113
+ ERROR_RENDERING_PAGE: "[loly-core] Error rendering page:",
114
+ RSC_ENDPOINT_ERROR: "[loly-core] RSC endpoint error:"
115
+ };
116
+
117
+ // src/utils/path.ts
118
+ function normalizePath(path4) {
119
+ if (path4 === "/") return "/";
120
+ return path4.replace(/\/$/, "") || "/";
121
+ }
122
+ function normalizeUrlPath(pathname) {
123
+ return normalizePath(pathname);
124
+ }
125
+
126
+ // src/utils/module-loader.ts
127
+ import { resolve } from "path";
128
+ import { existsSync } from "fs";
129
+ var tsxRegistered = false;
130
+ var tsxRegistrationPromise = null;
131
+ async function ensureTsxRegistered() {
132
+ if (tsxRegistered) {
133
+ return;
134
+ }
135
+ if (tsxRegistrationPromise) {
136
+ await tsxRegistrationPromise;
137
+ return;
138
+ }
139
+ tsxRegistrationPromise = (async () => {
140
+ try {
141
+ await import("tsx/esm");
142
+ tsxRegistered = true;
143
+ } catch (error) {
144
+ console.warn(
145
+ "[loly-core] tsx not available, TypeScript files won't load in development"
146
+ );
147
+ tsxRegistered = true;
148
+ }
149
+ })();
150
+ await tsxRegistrationPromise;
151
+ }
152
+ function resolveModuleUrl(path4) {
153
+ if (path4.startsWith("file://")) {
154
+ return path4;
155
+ }
156
+ const absolutePath = resolve(path4);
157
+ const normalizedPath = absolutePath.replace(/\\/g, "/");
158
+ if (normalizedPath.startsWith("/")) {
159
+ return `file://${normalizedPath}`;
160
+ }
161
+ return `file:///${normalizedPath}`;
162
+ }
163
+ async function loadModule(filePath, compiledPath) {
164
+ let pathToLoad = filePath;
165
+ if (compiledPath && existsSync(compiledPath)) {
166
+ pathToLoad = compiledPath;
167
+ }
168
+ try {
169
+ if (pathToLoad.endsWith(".ts") || pathToLoad.endsWith(".tsx")) {
170
+ await ensureTsxRegistered();
171
+ }
172
+ const fileUrl = resolveModuleUrl(pathToLoad);
173
+ const module = await import(fileUrl);
174
+ return module;
175
+ } catch (error) {
176
+ console.error(ERROR_MESSAGES.FAILED_TO_LOAD_MODULE(pathToLoad), error);
177
+ throw error;
178
+ }
179
+ }
180
+
181
+ // src/router/scanner.ts
182
+ function parseSegment(segment) {
183
+ if (segment.startsWith("(") && segment.endsWith(")")) {
184
+ return {
185
+ segment,
186
+ // Keep original with parentheses: (admin)
187
+ isDynamic: false,
188
+ isCatchAll: false,
189
+ isOptional: true
190
+ };
191
+ }
192
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
193
+ const paramName = segment.slice(4, -1);
194
+ return {
195
+ segment,
196
+ isDynamic: true,
197
+ isCatchAll: true,
198
+ isOptional: false,
199
+ paramName
200
+ };
201
+ }
202
+ if (segment.startsWith("[[...") && segment.endsWith("]]")) {
203
+ const paramName = segment.slice(5, -2);
204
+ return {
205
+ segment,
206
+ isDynamic: true,
207
+ isCatchAll: true,
208
+ isOptional: true,
209
+ paramName
210
+ };
211
+ }
212
+ if (segment.startsWith("[") && segment.endsWith("]")) {
213
+ const paramName = segment.slice(1, -1);
214
+ return {
215
+ segment,
216
+ isDynamic: true,
217
+ isCatchAll: false,
218
+ isOptional: false,
219
+ paramName
220
+ };
221
+ }
222
+ return {
223
+ segment,
224
+ isDynamic: false,
225
+ isCatchAll: false,
226
+ isOptional: false
227
+ };
228
+ }
229
+ function segmentsToUrlPath(segments) {
230
+ const parts = [];
231
+ for (const seg of segments) {
232
+ if (seg.isOptional) {
233
+ continue;
234
+ }
235
+ if (seg.isCatchAll) {
236
+ parts.push(`*${seg.paramName || "slug"}`);
237
+ } else if (seg.isDynamic) {
238
+ parts.push(`:${seg.paramName || "param"}`);
239
+ } else {
240
+ parts.push(seg.segment);
241
+ }
242
+ }
243
+ const path4 = "/" + parts.join("/");
244
+ return normalizeUrlPath(path4);
245
+ }
246
+ var HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
247
+ async function analyzeApiRoute(filePath) {
248
+ try {
249
+ const module = await loadModule(filePath);
250
+ const exports = Object.keys(module);
251
+ const httpMethods = [];
252
+ for (const exportName of exports) {
253
+ const upperExport = exportName.toUpperCase();
254
+ if (HTTP_METHODS.includes(upperExport) && typeof module[exportName] === "function") {
255
+ httpMethods.push(upperExport);
256
+ }
257
+ }
258
+ return httpMethods;
259
+ } catch (error) {
260
+ console.warn(`[loly-core] Failed to analyze API route ${filePath}:`, error);
261
+ return [];
262
+ }
263
+ }
264
+ async function scanRoutes(appDir) {
265
+ const routes = [];
266
+ const routeMap = /* @__PURE__ */ new Map();
267
+ const apiRouteMap = /* @__PURE__ */ new Map();
268
+ const patterns = ROUTE_FILE_NAMES.map((file) => `**/${file}`);
269
+ const files = await glob(patterns, { cwd: appDir, absolute: true });
270
+ let rootLayout;
271
+ for (const file of files) {
272
+ const absolutePath = file;
273
+ const relativePath = relative(appDir, absolutePath);
274
+ const dir = dirname(relativePath);
275
+ const fileName = file.split(sep).pop() || "";
276
+ let type;
277
+ if (fileName.startsWith("page.")) {
278
+ type = "page";
279
+ } else if (fileName.startsWith("layout.")) {
280
+ type = "layout";
281
+ } else if (fileName.startsWith("route.")) {
282
+ type = "route";
283
+ } else {
284
+ continue;
285
+ }
286
+ const pathSegments = dir === "." ? [] : dir.split(sep).filter(Boolean);
287
+ const segments = pathSegments.map(parseSegment);
288
+ const isRootLayout = type === "layout" && dir === ".";
289
+ let urlPath = "";
290
+ let isApiRoute = false;
291
+ let httpMethods = void 0;
292
+ if (type === "page") {
293
+ urlPath = segmentsToUrlPath(segments);
294
+ } else if (type === "route") {
295
+ httpMethods = await analyzeApiRoute(absolutePath);
296
+ if (httpMethods.length > 0) {
297
+ isApiRoute = true;
298
+ urlPath = segmentsToUrlPath(segments);
299
+ }
300
+ }
301
+ const routeFile = {
302
+ filePath: absolutePath,
303
+ segments,
304
+ type,
305
+ isRootLayout,
306
+ urlPath,
307
+ isApiRoute: isApiRoute || void 0,
308
+ httpMethods
309
+ };
310
+ routes.push(routeFile);
311
+ if (isRootLayout) {
312
+ rootLayout = routeFile;
313
+ }
314
+ if (type === "page") {
315
+ routeMap.set(urlPath, routeFile);
316
+ } else if (isApiRoute && urlPath) {
317
+ apiRouteMap.set(urlPath, routeFile);
318
+ }
319
+ }
320
+ return {
321
+ routes,
322
+ rootLayout,
323
+ routeMap,
324
+ apiRouteMap
325
+ };
326
+ }
327
+
328
+ // src/router/matcher.ts
329
+ function matchRoute(pathname, config, loadComponent3) {
330
+ const normalized = normalizeUrlPath(pathname);
331
+ if (config.routeMap.has(normalized)) {
332
+ const route = config.routeMap.get(normalized);
333
+ return { route, params: {} };
334
+ }
335
+ for (const route of config.routes) {
336
+ if (route.type !== "page") continue;
337
+ const match = matchRoutePattern(normalized, route);
338
+ if (match) {
339
+ return { route, params: match.params };
340
+ }
341
+ }
342
+ return null;
343
+ }
344
+ function matchRoutePattern(pathname, route) {
345
+ const pathSegments = pathname === "/" ? [""] : pathname.split("/").filter(Boolean);
346
+ const routeSegments = route.segments.filter((s) => !s.isOptional);
347
+ if (routeSegments.length === 0 && pathSegments.length === 1 && pathSegments[0] === "") {
348
+ return { params: {} };
349
+ }
350
+ const params = {};
351
+ let pathIdx = 0;
352
+ let routeIdx = 0;
353
+ while (pathIdx < pathSegments.length && routeIdx < routeSegments.length) {
354
+ const routeSeg = routeSegments[routeIdx];
355
+ const pathSeg = pathSegments[pathIdx];
356
+ if (routeSeg.isCatchAll) {
357
+ const remaining = pathSegments.slice(pathIdx);
358
+ if (routeSeg.paramName) {
359
+ params[routeSeg.paramName] = remaining.join("/");
360
+ }
361
+ return { params };
362
+ }
363
+ if (routeSeg.isDynamic) {
364
+ if (routeSeg.paramName) {
365
+ params[routeSeg.paramName] = decodeURIComponent(pathSeg);
366
+ }
367
+ pathIdx++;
368
+ routeIdx++;
369
+ continue;
370
+ }
371
+ if (routeSeg.segment === pathSeg) {
372
+ pathIdx++;
373
+ routeIdx++;
374
+ continue;
375
+ }
376
+ return null;
377
+ }
378
+ if (pathIdx === pathSegments.length && routeIdx === routeSegments.length) {
379
+ return { params };
380
+ }
381
+ return null;
382
+ }
383
+ function extractParams(pathname, route) {
384
+ const match = matchRoutePattern(pathname, route);
385
+ return match?.params || {};
386
+ }
387
+
388
+ // src/server/dev-server.ts
389
+ import { join as join3 } from "path";
390
+ import { existsSync as existsSync5 } from "fs";
391
+ import chokidar from "chokidar";
392
+
393
+ // src/utils/route-component-factory.ts
394
+ import { resolve as resolve2, join } from "path";
395
+ import { existsSync as existsSync2 } from "fs";
396
+ var RouteComponentFactory = class {
397
+ constructor() {
398
+ this.componentCache = /* @__PURE__ */ new Map();
399
+ this.layoutCache = /* @__PURE__ */ new Map();
400
+ }
401
+ /**
402
+ * Load a route component
403
+ * @param route - Route file information
404
+ * @param compiledPath - Optional compiled path for production
405
+ * @returns The loaded component module
406
+ */
407
+ async loadRouteComponent(route, compiledPath) {
408
+ const cacheKey = compiledPath || route.filePath;
409
+ if (this.componentCache.has(cacheKey)) {
410
+ return this.componentCache.get(cacheKey);
411
+ }
412
+ const sourcePath = resolve2(route.filePath);
413
+ const module = await loadModule(sourcePath, compiledPath);
414
+ const component = module.default;
415
+ if (!component) {
416
+ throw new Error(
417
+ `[loly-core] Component not found in ${route.filePath}`
418
+ );
419
+ }
420
+ this.componentCache.set(cacheKey, component);
421
+ return component;
422
+ }
423
+ /**
424
+ * Load a layout component
425
+ * @param layout - Layout route file information
426
+ * @param compiledPath - Optional compiled path for production
427
+ * @returns The loaded layout component module
428
+ */
429
+ async loadLayoutComponent(layout, compiledPath) {
430
+ const cacheKey = compiledPath || layout.filePath;
431
+ if (this.layoutCache.has(cacheKey)) {
432
+ const cached = this.layoutCache.get(cacheKey);
433
+ return cached?.[0];
434
+ }
435
+ const sourcePath = resolve2(layout.filePath);
436
+ const module = await loadModule(sourcePath, compiledPath);
437
+ const layoutComponent = module.default;
438
+ if (!layoutComponent) {
439
+ throw new Error(
440
+ `[loly-core] Layout component not found in ${layout.filePath}`
441
+ );
442
+ }
443
+ this.layoutCache.set(cacheKey, [layoutComponent]);
444
+ return layoutComponent;
445
+ }
446
+ /**
447
+ * Load all layouts from root to route
448
+ * @param route - Target route file
449
+ * @param config - Route configuration
450
+ * @param appDir - Application directory
451
+ * @param manifestRoute - Optional manifest route data
452
+ * @returns Array of layout components in order (root to route)
453
+ */
454
+ async loadLayouts(route, config, appDir, manifestRoute) {
455
+ const layouts = [];
456
+ const configWithManifest = config;
457
+ const routesManifest = configWithManifest.routesManifest;
458
+ if (config.rootLayout) {
459
+ const rootLayoutManifest = configWithManifest.rootLayoutManifest;
460
+ const layoutSourcePath = resolve2(config.rootLayout.filePath);
461
+ const layoutCompiledPath = rootLayoutManifest?.serverPath ? resolve2(rootLayoutManifest.serverPath) : void 0;
462
+ const rootLayout = await this.loadLayoutComponent(
463
+ config.rootLayout,
464
+ layoutCompiledPath
465
+ );
466
+ layouts.push(rootLayout);
467
+ }
468
+ const allRouteSegments = route.segments;
469
+ for (let i = 0; i < allRouteSegments.length; i++) {
470
+ const segmentPath = allRouteSegments.slice(0, i + 1);
471
+ let layoutManifest;
472
+ if (routesManifest) {
473
+ layoutManifest = routesManifest.find((r) => {
474
+ if (r.type !== "layout") return false;
475
+ if (r.isRootLayout) return false;
476
+ if (r.segments.length !== segmentPath.length) return false;
477
+ return r.segments.every((seg, idx) => {
478
+ const routeSeg = segmentPath[idx];
479
+ if (seg.segment === routeSeg.segment) return true;
480
+ if (seg.isDynamic && routeSeg.isDynamic && seg.paramName === routeSeg.paramName) {
481
+ return true;
482
+ }
483
+ if (seg.isOptional && routeSeg.isOptional && !seg.isDynamic && !routeSeg.isDynamic) {
484
+ const segName = seg.segment.replace(/^\(|\)$/g, "");
485
+ const routeSegName = routeSeg.segment.replace(/^\(|\)$/g, "");
486
+ if (segName === routeSegName) return true;
487
+ }
488
+ return false;
489
+ });
490
+ });
491
+ }
492
+ if (layoutManifest) {
493
+ const layoutSourcePath = resolve2(layoutManifest.sourcePath);
494
+ const layoutCompiledPath = layoutManifest.serverPath ? resolve2(layoutManifest.serverPath) : void 0;
495
+ try {
496
+ const layoutFile = {
497
+ filePath: layoutSourcePath,
498
+ segments: layoutManifest.segments,
499
+ type: "layout",
500
+ isRootLayout: false,
501
+ urlPath: ""
502
+ };
503
+ const layout = await this.loadLayoutComponent(
504
+ layoutFile,
505
+ layoutCompiledPath
506
+ );
507
+ layouts.push(layout);
508
+ } catch (error) {
509
+ console.warn(
510
+ `[loly-core] Failed to load nested layout at ${layoutSourcePath}:`,
511
+ error
512
+ );
513
+ }
514
+ } else {
515
+ const layoutPathParts = segmentPath.map((s) => s.segment);
516
+ const layoutDir = join(appDir, ...layoutPathParts);
517
+ const layoutFilePath = join(layoutDir, "layout.tsx");
518
+ const layoutFilePathAlt = join(layoutDir, "layout.ts");
519
+ const actualPath = existsSync2(layoutFilePath) ? layoutFilePath : existsSync2(layoutFilePathAlt) ? layoutFilePathAlt : null;
520
+ if (actualPath) {
521
+ try {
522
+ const layoutFile = {
523
+ filePath: actualPath,
524
+ segments: segmentPath,
525
+ type: "layout",
526
+ isRootLayout: false,
527
+ urlPath: ""
528
+ };
529
+ const layout = await this.loadLayoutComponent(layoutFile);
530
+ layouts.push(layout);
531
+ } catch (error) {
532
+ console.warn(
533
+ `[loly-core] Failed to load nested layout at ${actualPath}:`,
534
+ error
535
+ );
536
+ }
537
+ }
538
+ }
539
+ }
540
+ return layouts;
541
+ }
542
+ /**
543
+ * Clear component cache
544
+ */
545
+ clearCache() {
546
+ this.componentCache.clear();
547
+ this.layoutCache.clear();
548
+ }
549
+ };
550
+
551
+ // src/server/rendering/strategies.ts
552
+ import { renderToHtmlStream, createHead } from "loly-jsx";
553
+
554
+ // src/server/async-registry.ts
555
+ var asyncTaskRegistry = /* @__PURE__ */ new Map();
556
+ var DEFAULT_MAX_AGE = 5 * 60 * 1e3;
557
+ function registerAsyncTask(id, fn, props) {
558
+ asyncTaskRegistry.set(id, {
559
+ id,
560
+ fn,
561
+ props,
562
+ createdAt: Date.now()
563
+ });
564
+ }
565
+ async function resolveAsyncTask(id) {
566
+ const task = asyncTaskRegistry.get(id);
567
+ if (!task) {
568
+ throw new Error(`Async task not found: ${id}`);
569
+ }
570
+ return task.fn();
571
+ }
572
+
573
+ // src/server/rendering/strategies.ts
574
+ import { resolve as resolve3 } from "path";
575
+
576
+ // src/context.ts
577
+ import { join as join2 } from "path";
578
+ var globalContext = null;
579
+ function setContext(context) {
580
+ globalContext = context;
581
+ }
582
+ function getContext() {
583
+ if (!globalContext) {
584
+ throw new Error(ERROR_MESSAGES.CONTEXT_NOT_INITIALIZED);
585
+ }
586
+ return globalContext;
587
+ }
588
+ function getProjectDir() {
589
+ return getContext().projectDir;
590
+ }
591
+ function getAppDir() {
592
+ return getContext().appDir;
593
+ }
594
+ function getSrcDir() {
595
+ return getContext().srcDir;
596
+ }
597
+ function getOutDir() {
598
+ return getContext().outDir;
599
+ }
600
+ function getAppDirPath(projectDir) {
601
+ return join2(projectDir, DIRECTORIES.SRC, DIRECTORIES.APP);
602
+ }
603
+ function getClientDir() {
604
+ return join2(getOutDir(), DIRECTORIES.CLIENT);
605
+ }
606
+ function getServerOutDir() {
607
+ return join2(getOutDir(), DIRECTORIES.SERVER);
608
+ }
609
+ function getManifestPath() {
610
+ return join2(getClientDir(), FILE_NAMES.MANIFEST);
611
+ }
612
+ function getRoutesManifestPath() {
613
+ return join2(getProjectDir(), DIRECTORIES.LOLY, FILE_NAMES.ROUTES_MANIFEST);
614
+ }
615
+ function getBootstrapPath() {
616
+ return join2(getProjectDir(), DIRECTORIES.LOLY, FILE_NAMES.BOOTSTRAP);
617
+ }
618
+ function getGlobalsCssPath() {
619
+ return join2(getClientDir(), FILE_NAMES.GLOBALS_CSS);
620
+ }
621
+ function getPublicDir() {
622
+ return join2(getProjectDir(), DIRECTORIES.PUBLIC);
623
+ }
624
+ function getRouteConfig() {
625
+ return getContext().routeConfig || null;
626
+ }
627
+ function setRouteConfig(config) {
628
+ const context = getContext();
629
+ context.routeConfig = config;
630
+ }
631
+
632
+ // src/utils/html.ts
633
+ function generateErrorHtml(status, message) {
634
+ const statusText = status === HTTP_STATUS.NOT_FOUND ? "404 - Not Found" : status === HTTP_STATUS.INTERNAL_ERROR ? "500 - Internal Server Error" : `${status} - Error`;
635
+ return `<!doctype html><html><head><title>${statusText}</title></head><body><h1>${statusText}</h1>${message ? `<p>${message}</p>` : ""}</body></html>`;
636
+ }
637
+ function htmlStringToStream(html) {
638
+ const encoder = new TextEncoder();
639
+ return new ReadableStream({
640
+ start(controller) {
641
+ controller.enqueue(encoder.encode(html));
642
+ controller.close();
643
+ }
644
+ });
645
+ }
646
+ function extractIslandDataFromHtml(html) {
647
+ const islandData = {};
648
+ const islandDataRegex = /<script>window\.__LOLY_ISLAND_DATA__=window\.__LOLY_ISLAND_DATA__\|\|{};window\.__LOLY_ISLAND_DATA__\[([^\]]+)\]=([^<]+);<\/script>/gs;
649
+ const htmlWithoutScripts = html.replace(
650
+ islandDataRegex,
651
+ (match, id, data) => {
652
+ try {
653
+ let islandId;
654
+ const trimmedId = id.trim();
655
+ if (trimmedId.startsWith('"') || trimmedId.startsWith("'")) {
656
+ islandId = JSON.parse(trimmedId);
657
+ } else if (/^\d+$/.test(trimmedId)) {
658
+ islandId = trimmedId;
659
+ } else {
660
+ islandId = trimmedId;
661
+ }
662
+ const trimmedData = data.trim().replace(/;?\s*$/, "");
663
+ const islandProps = JSON.parse(trimmedData);
664
+ islandData[islandId] = islandProps;
665
+ } catch (e) {
666
+ console.warn(
667
+ "[loly-core] Failed to parse island data:",
668
+ e,
669
+ { id, data: data?.substring(0, 100) }
670
+ );
671
+ }
672
+ return "";
673
+ }
674
+ );
675
+ return { htmlWithoutScripts, islandData };
676
+ }
677
+
678
+ // src/utils/assets.ts
679
+ import { existsSync as existsSync3, readFileSync } from "fs";
680
+ function getMainScriptName() {
681
+ const manifestPath = getManifestPath();
682
+ if (!existsSync3(manifestPath)) {
683
+ return "main.js";
684
+ }
685
+ try {
686
+ const manifestContent = readFileSync(manifestPath, "utf-8");
687
+ const manifest = JSON.parse(manifestContent);
688
+ const mainEntry = manifest["main"];
689
+ if (mainEntry?.file) {
690
+ return mainEntry.file;
691
+ }
692
+ } catch (error) {
693
+ console.warn("[loly-core] Failed to read client manifest:", error);
694
+ }
695
+ return "main.js";
696
+ }
697
+ function generateAssetTags(manifestRoute, publicPath = "/") {
698
+ let scripts = "";
699
+ let styles = "";
700
+ let preloads = "";
701
+ const mainScriptName = getMainScriptName();
702
+ const mainScriptPath = mainScriptName.startsWith("/") ? mainScriptName : `${publicPath}${mainScriptName}`;
703
+ preloads = `<link rel="modulepreload" href="${mainScriptPath}" crossorigin>`;
704
+ scripts = `<script type="module" src="${mainScriptPath}"></script>`;
705
+ const globalsCssPath = getGlobalsCssPath();
706
+ if (existsSync3(globalsCssPath)) {
707
+ const cssPath = `${publicPath}globals.css`;
708
+ styles += `
709
+ <link rel="stylesheet" href="${cssPath}">`;
710
+ }
711
+ if (manifestRoute?.clientChunks) {
712
+ const { js = [], css = [] } = manifestRoute.clientChunks;
713
+ for (const jsFile of js) {
714
+ const scriptPath = jsFile.startsWith("/") ? jsFile : `${publicPath}${jsFile}`;
715
+ scripts += `
716
+ <script type="module" src="${scriptPath}"></script>`;
717
+ }
718
+ for (const cssFile of css) {
719
+ const cssPath = cssFile.startsWith("/") ? cssFile : `${publicPath}${cssFile}`;
720
+ styles += `
721
+ <link rel="stylesheet" href="${cssPath}">`;
722
+ }
723
+ }
724
+ return { scripts, styles, preloads };
725
+ }
726
+ function getClientAssetsForRoute(manifestRoute) {
727
+ return generateAssetTags(manifestRoute, "/");
728
+ }
729
+
730
+ // src/utils/errors.ts
731
+ function handleRouteError(error, pathname) {
732
+ if (error.message.includes("Route not found")) {
733
+ return {
734
+ html: htmlStringToStream(generateErrorHtml(HTTP_STATUS.NOT_FOUND, "")),
735
+ status: HTTP_STATUS.NOT_FOUND
736
+ };
737
+ }
738
+ console.error(ERROR_MESSAGES.ERROR_RENDERING_PAGE, error);
739
+ return {
740
+ html: htmlStringToStream(generateErrorHtml(HTTP_STATUS.INTERNAL_ERROR, "")),
741
+ status: HTTP_STATUS.INTERNAL_ERROR
742
+ };
743
+ }
744
+
745
+ // src/utils/env.ts
746
+ function getPublicEnv() {
747
+ const publicEnv = {};
748
+ if (typeof process !== "undefined" && process.env) {
749
+ for (const key in process.env) {
750
+ if (key.startsWith("LOLY_PUBLIC_")) {
751
+ const clientKey = key.replace(/^LOLY_PUBLIC_/, "");
752
+ publicEnv[clientKey] = process.env[key] || "";
753
+ }
754
+ }
755
+ }
756
+ return publicEnv;
757
+ }
758
+ function generatePublicEnvScript() {
759
+ const publicEnv = getPublicEnv();
760
+ const envJson = JSON.stringify(publicEnv);
761
+ return `<script>window.__LOLY_ENV__=Object.freeze(${envJson});</script>`;
762
+ }
763
+ function getEnv(key) {
764
+ if (typeof window !== "undefined" && window.__LOLY_ENV__) {
765
+ return window.__LOLY_ENV__[key];
766
+ }
767
+ if (typeof process !== "undefined" && process.env) {
768
+ return process.env[key] || process.env[`LOLY_PUBLIC_${key}`];
769
+ }
770
+ return void 0;
771
+ }
772
+
773
+ // src/server/rendering/strategies.ts
774
+ var RSCStrategy = class {
775
+ constructor(routeFactory2) {
776
+ this.routeFactory = routeFactory2;
777
+ }
778
+ async render(context, config) {
779
+ const { pathname } = context;
780
+ const appDir = getAppDir();
781
+ const configWithManifest = config;
782
+ const manifestRouteMap = configWithManifest.routeMapManifest;
783
+ const manifestRoute = manifestRouteMap?.get(pathname);
784
+ const match = matchRoute(pathname, config, async (route2) => {
785
+ const routeManifest = manifestRouteMap?.get(pathname) || Array.from(manifestRouteMap?.values() || []).find(
786
+ (r) => r.urlPath === route2.urlPath
787
+ );
788
+ const compiledPath = routeManifest?.serverPath ? resolve3(routeManifest.serverPath) : void 0;
789
+ return await this.routeFactory.loadRouteComponent(route2, compiledPath);
790
+ });
791
+ if (!match) {
792
+ throw new Error(ERROR_MESSAGES.ROUTE_NOT_FOUND(pathname));
793
+ }
794
+ const { route, params } = match;
795
+ context.params = params;
796
+ const layouts = await this.routeFactory.loadLayouts(
797
+ route,
798
+ config,
799
+ appDir,
800
+ manifestRoute
801
+ );
802
+ const pageCompiledPath = manifestRoute?.serverPath ? resolve3(manifestRoute.serverPath) : void 0;
803
+ const PageComponent = await this.routeFactory.loadRouteComponent(
804
+ route,
805
+ pageCompiledPath
806
+ );
807
+ const pageProps = {
808
+ params,
809
+ searchParams: context.searchParams
810
+ };
811
+ let pageContent;
812
+ if (typeof PageComponent === "function") {
813
+ const result = PageComponent(pageProps);
814
+ pageContent = result instanceof Promise ? await result : result;
815
+ } else {
816
+ throw new Error(ERROR_MESSAGES.PAGE_COMPONENT_MUST_BE_FUNCTION);
817
+ }
818
+ let content = pageContent;
819
+ for (let i = layouts.length - 1; i >= 0; i--) {
820
+ const Layout = layouts[i];
821
+ if (typeof Layout === "function") {
822
+ const layoutResult = Layout({ children: content, params });
823
+ content = layoutResult instanceof Promise ? await layoutResult : layoutResult;
824
+ }
825
+ }
826
+ const { renderToString: renderToString2 } = await import("loly-jsx");
827
+ const htmlWithScripts = await renderToString2(content);
828
+ const { htmlWithoutScripts, islandData } = extractIslandDataFromHtml(htmlWithScripts);
829
+ const { scripts, styles, preloads } = getClientAssetsForRoute(manifestRoute);
830
+ return { html: htmlWithoutScripts, scripts, styles, preloads, islandData };
831
+ }
832
+ };
833
+ var SSRStrategy = class {
834
+ constructor(routeFactory2) {
835
+ this.routeFactory = routeFactory2;
836
+ }
837
+ async render(context, config) {
838
+ const { pathname } = context;
839
+ const appDir = getAppDir();
840
+ const configWithManifest = config;
841
+ const manifestRouteMap = configWithManifest.routeMapManifest;
842
+ const manifestRoute = manifestRouteMap?.get(pathname);
843
+ const match = matchRoute(pathname, config, async (route2) => {
844
+ const routeManifest = manifestRouteMap?.get(pathname) || Array.from(manifestRouteMap?.values() || []).find(
845
+ (r) => r.urlPath === route2.urlPath
846
+ );
847
+ const compiledPath = routeManifest?.serverPath ? resolve3(routeManifest.serverPath) : void 0;
848
+ return await this.routeFactory.loadRouteComponent(route2, compiledPath);
849
+ });
850
+ if (!match) {
851
+ return {
852
+ html: htmlStringToStream(generateErrorHtml(HTTP_STATUS.NOT_FOUND, "")),
853
+ status: HTTP_STATUS.NOT_FOUND
854
+ };
855
+ }
856
+ const { route, params } = match;
857
+ context.params = params;
858
+ try {
859
+ const layouts = await this.routeFactory.loadLayouts(
860
+ route,
861
+ config,
862
+ appDir,
863
+ manifestRoute
864
+ );
865
+ const pageCompiledPath = manifestRoute?.serverPath ? resolve3(manifestRoute.serverPath) : void 0;
866
+ const PageComponent = await this.routeFactory.loadRouteComponent(
867
+ route,
868
+ pageCompiledPath
869
+ );
870
+ const pageProps = {
871
+ params,
872
+ searchParams: context.searchParams
873
+ };
874
+ let pageContent;
875
+ if (typeof PageComponent === "function") {
876
+ const result = PageComponent(pageProps);
877
+ pageContent = result instanceof Promise ? await result : result;
878
+ } else {
879
+ throw new Error(ERROR_MESSAGES.PAGE_COMPONENT_MUST_BE_FUNCTION);
880
+ }
881
+ let content = pageContent;
882
+ for (let i = layouts.length - 1; i >= 0; i--) {
883
+ const Layout = layouts[i];
884
+ if (typeof Layout === "function") {
885
+ const layoutResult = Layout({ children: content, params });
886
+ content = layoutResult instanceof Promise ? await layoutResult : layoutResult;
887
+ }
888
+ }
889
+ const head = createHead();
890
+ head.setTitle("Loly App");
891
+ const { scripts, styles, preloads } = getClientAssetsForRoute(manifestRoute);
892
+ const envScript = generatePublicEnvScript();
893
+ const headParts = [head.toString()];
894
+ if (preloads) headParts.push(preloads);
895
+ headParts.push(envScript);
896
+ const headContent = headParts.join("\n");
897
+ const htmlStream = renderToHtmlStream({
898
+ view: content,
899
+ head: headContent,
900
+ scripts,
901
+ styles,
902
+ appId: SERVER.APP_CONTAINER_ID,
903
+ onAsyncTask: (id, fn, props) => {
904
+ registerAsyncTask(id, fn, props);
905
+ }
906
+ });
907
+ return {
908
+ html: htmlStream,
909
+ status: HTTP_STATUS.OK
910
+ };
911
+ } catch (error) {
912
+ return handleRouteError(error, context.pathname);
913
+ }
914
+ }
915
+ };
916
+ var RenderingStrategyFactory = class {
917
+ constructor(routeFactory2) {
918
+ this.routeFactory = routeFactory2;
919
+ }
920
+ /**
921
+ * Get SSR rendering strategy
922
+ * @returns SSR rendering strategy
923
+ */
924
+ getSSRStrategy() {
925
+ return new SSRStrategy(this.routeFactory);
926
+ }
927
+ /**
928
+ * Get RSC rendering strategy
929
+ * @returns RSC rendering strategy
930
+ */
931
+ getRSCStrategy() {
932
+ return new RSCStrategy(this.routeFactory);
933
+ }
934
+ /**
935
+ * Get rendering strategy based on type
936
+ * @param type - Strategy type: 'ssr' or 'rsc'
937
+ * @returns Appropriate rendering strategy (SSR only, RSC must be accessed via getRSCStrategy)
938
+ */
939
+ getStrategy(type) {
940
+ if (type === "ssr") {
941
+ return new SSRStrategy(this.routeFactory);
942
+ }
943
+ throw new Error(`Invalid strategy type: ${type}`);
944
+ }
945
+ };
946
+
947
+ // src/server/ssr.ts
948
+ var routeFactory = new RouteComponentFactory();
949
+ async function renderPageContent(context, config, appDir) {
950
+ const rscStrategy = new RSCStrategy(routeFactory);
951
+ return await rscStrategy.render(context, config);
952
+ }
953
+ async function renderPageStream(context, config, appDir) {
954
+ const ssrStrategy = new SSRStrategy(routeFactory);
955
+ return await ssrStrategy.render(context, config);
956
+ }
957
+
958
+ // src/server/base-server.ts
959
+ import express2 from "express";
960
+
961
+ // src/utils/express-setup.ts
962
+ import express from "express";
963
+ import { Readable } from "stream";
964
+ import { existsSync as existsSync4 } from "fs";
965
+
966
+ // src/server/handlers/image.ts
967
+ import crypto2 from "crypto";
968
+
969
+ // src/server/utils/image-optimizer.ts
970
+ import sharp from "sharp";
971
+ import fs2 from "fs";
972
+ import path3 from "path";
973
+
974
+ // src/server/utils/image-validation.ts
975
+ import path from "path";
976
+ function isRemoteUrl(url) {
977
+ return url.startsWith("http://") || url.startsWith("https://");
978
+ }
979
+ function sanitizeImagePath(imagePath) {
980
+ const normalized = path.normalize(imagePath).replace(/^(\.\.(\/|\\|$))+/, "");
981
+ return normalized.replace(/^[/\\]+/, "");
982
+ }
983
+ function patternToRegex(pattern) {
984
+ const parts = [];
985
+ if (pattern.protocol) {
986
+ parts.push(pattern.protocol === "https" ? "https" : "http");
987
+ } else {
988
+ parts.push("https?");
989
+ }
990
+ parts.push("://");
991
+ let hostnamePattern = pattern.hostname.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^.]*");
992
+ parts.push(hostnamePattern);
993
+ if (pattern.port) {
994
+ parts.push(`:${pattern.port}`);
995
+ }
996
+ if (pattern.pathname) {
997
+ let pathnamePattern = pattern.pathname.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
998
+ parts.push(pathnamePattern);
999
+ } else {
1000
+ parts.push(".*");
1001
+ }
1002
+ const regexSource = `^${parts.join("")}`;
1003
+ return new RegExp(regexSource);
1004
+ }
1005
+ function validateRemoteUrl(url, config) {
1006
+ if (!config || !config.remotePatterns && !config.domains) {
1007
+ return true;
1008
+ }
1009
+ try {
1010
+ const urlObj = new URL(url);
1011
+ const protocol = urlObj.protocol.replace(":", "");
1012
+ const hostname = urlObj.hostname;
1013
+ const port = urlObj.port || "";
1014
+ const pathname = urlObj.pathname;
1015
+ if (config.remotePatterns && config.remotePatterns.length > 0) {
1016
+ for (const pattern of config.remotePatterns) {
1017
+ const regex = patternToRegex(pattern);
1018
+ const testUrl = `${protocol}://${hostname}${port ? `:${port}` : ""}${pathname}`;
1019
+ if (regex.test(testUrl)) {
1020
+ if (pattern.protocol && pattern.protocol !== protocol) {
1021
+ continue;
1022
+ }
1023
+ if (pattern.port && pattern.port !== port) {
1024
+ continue;
1025
+ }
1026
+ return true;
1027
+ }
1028
+ }
1029
+ }
1030
+ if (config.domains && config.domains.length > 0) {
1031
+ for (const domain of config.domains) {
1032
+ const domainPattern = domain.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^.]*");
1033
+ const regex = new RegExp(`^${domainPattern}$`);
1034
+ if (regex.test(hostname)) {
1035
+ if (process.env.NODE_ENV === "production" && protocol !== "https") {
1036
+ continue;
1037
+ }
1038
+ return true;
1039
+ }
1040
+ }
1041
+ }
1042
+ return false;
1043
+ } catch (error) {
1044
+ return false;
1045
+ }
1046
+ }
1047
+ function validateImageDimensions(width, height, config) {
1048
+ const maxWidth = config?.maxWidth || 3840;
1049
+ const maxHeight = config?.maxHeight || 3840;
1050
+ if (width !== void 0 && (width <= 0 || width > maxWidth)) {
1051
+ return {
1052
+ valid: false,
1053
+ error: `Image width must be between 1 and ${maxWidth}, got ${width}`
1054
+ };
1055
+ }
1056
+ if (height !== void 0 && (height <= 0 || height > maxHeight)) {
1057
+ return {
1058
+ valid: false,
1059
+ error: `Image height must be between 1 and ${maxHeight}, got ${height}`
1060
+ };
1061
+ }
1062
+ return { valid: true };
1063
+ }
1064
+ function validateQuality(quality) {
1065
+ if (quality === void 0) {
1066
+ return { valid: true };
1067
+ }
1068
+ if (typeof quality !== "number" || quality < 1 || quality > 100) {
1069
+ return {
1070
+ valid: false,
1071
+ error: `Image quality must be between 1 and 100, got ${quality}`
1072
+ };
1073
+ }
1074
+ return { valid: true };
1075
+ }
1076
+
1077
+ // src/server/utils/image-cache.ts
1078
+ import fs from "fs";
1079
+ import path2 from "path";
1080
+ import crypto from "crypto";
1081
+ var ImageLRUCache = class {
1082
+ constructor(maxSize = 50) {
1083
+ this.maxSize = maxSize;
1084
+ this.cache = /* @__PURE__ */ new Map();
1085
+ }
1086
+ /**
1087
+ * Get an image from cache
1088
+ */
1089
+ get(key) {
1090
+ if (!this.cache.has(key)) {
1091
+ return void 0;
1092
+ }
1093
+ const value = this.cache.get(key);
1094
+ this.cache.delete(key);
1095
+ this.cache.set(key, value);
1096
+ return value;
1097
+ }
1098
+ /**
1099
+ * Set an image in cache
1100
+ */
1101
+ set(key, value) {
1102
+ if (this.cache.has(key)) {
1103
+ this.cache.delete(key);
1104
+ } else if (this.cache.size >= this.maxSize) {
1105
+ const firstKey = this.cache.keys().next().value;
1106
+ if (firstKey) {
1107
+ this.cache.delete(firstKey);
1108
+ }
1109
+ }
1110
+ this.cache.set(key, value);
1111
+ }
1112
+ /**
1113
+ * Check if key exists in cache
1114
+ */
1115
+ has(key) {
1116
+ return this.cache.has(key);
1117
+ }
1118
+ /**
1119
+ * Clear the cache
1120
+ */
1121
+ clear() {
1122
+ this.cache.clear();
1123
+ }
1124
+ /**
1125
+ * Get cache size
1126
+ */
1127
+ size() {
1128
+ return this.cache.size;
1129
+ }
1130
+ };
1131
+ var globalLRUCache = null;
1132
+ function getLRUCache(maxSize) {
1133
+ if (!globalLRUCache) {
1134
+ globalLRUCache = new ImageLRUCache(maxSize);
1135
+ }
1136
+ return globalLRUCache;
1137
+ }
1138
+ function generateCacheKey(src, width, height, quality, format) {
1139
+ const data = `${src}-${width || ""}-${height || ""}-${quality || ""}-${format || ""}`;
1140
+ return crypto.createHash("sha256").update(data).digest("hex");
1141
+ }
1142
+ function getCacheDir() {
1143
+ const projectDir = getProjectDir();
1144
+ return path2.join(projectDir, DIRECTORIES.LOLY, "cache", "images");
1145
+ }
1146
+ function ensureCacheDir(cacheDir) {
1147
+ if (!fs.existsSync(cacheDir)) {
1148
+ fs.mkdirSync(cacheDir, { recursive: true });
1149
+ }
1150
+ }
1151
+ function getCachedImagePath(cacheKey, extension, cacheDir) {
1152
+ return path2.join(cacheDir, `${cacheKey}.${extension}`);
1153
+ }
1154
+ function hasCachedImage(cacheKey, extension, cacheDir) {
1155
+ const cachedPath = getCachedImagePath(cacheKey, extension, cacheDir);
1156
+ return fs.existsSync(cachedPath);
1157
+ }
1158
+ function readCachedImage(cacheKey, extension, cacheDir) {
1159
+ const cachedPath = getCachedImagePath(cacheKey, extension, cacheDir);
1160
+ try {
1161
+ if (fs.existsSync(cachedPath)) {
1162
+ return fs.readFileSync(cachedPath);
1163
+ }
1164
+ } catch (error) {
1165
+ console.warn(`[image-optimizer] Failed to read cached image: ${cachedPath}`, error);
1166
+ }
1167
+ return null;
1168
+ }
1169
+ function writeCachedImage(cacheKey, extension, cacheDir, imageBuffer) {
1170
+ ensureCacheDir(cacheDir);
1171
+ const cachedPath = getCachedImagePath(cacheKey, extension, cacheDir);
1172
+ try {
1173
+ fs.writeFileSync(cachedPath, imageBuffer);
1174
+ } catch (error) {
1175
+ console.warn(`[image-optimizer] Failed to write cached image: ${cachedPath}`, error);
1176
+ }
1177
+ }
1178
+ function getImageMimeType(format) {
1179
+ const formatMap = {
1180
+ webp: "image/webp",
1181
+ avif: "image/avif",
1182
+ jpeg: "image/jpeg",
1183
+ jpg: "image/jpeg",
1184
+ png: "image/png",
1185
+ gif: "image/gif",
1186
+ svg: "image/svg+xml"
1187
+ };
1188
+ const normalized = format.toLowerCase();
1189
+ return formatMap[normalized] || "image/jpeg";
1190
+ }
1191
+ function getImageExtension(format) {
1192
+ const formatMap = {
1193
+ "image/webp": "webp",
1194
+ "image/avif": "avif",
1195
+ "image/jpeg": "jpg",
1196
+ "image/png": "png",
1197
+ "image/gif": "gif",
1198
+ "image/svg+xml": "svg",
1199
+ webp: "webp",
1200
+ avif: "avif",
1201
+ jpeg: "jpg",
1202
+ jpg: "jpg",
1203
+ png: "png",
1204
+ gif: "gif",
1205
+ svg: "svg"
1206
+ };
1207
+ const normalized = format.toLowerCase();
1208
+ return formatMap[normalized] || "jpg";
1209
+ }
1210
+ function cleanupCacheByAge(cacheDir, maxAgeDays = 30) {
1211
+ const result = { deleted: 0, freed: 0 };
1212
+ const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
1213
+ const now = Date.now();
1214
+ try {
1215
+ if (!fs.existsSync(cacheDir)) {
1216
+ return result;
1217
+ }
1218
+ const files = fs.readdirSync(cacheDir);
1219
+ for (const file of files) {
1220
+ const filePath = path2.join(cacheDir, file);
1221
+ try {
1222
+ const stat = fs.statSync(filePath);
1223
+ if (stat.isFile()) {
1224
+ const age = now - stat.mtime.getTime();
1225
+ if (age > maxAgeMs) {
1226
+ const size = stat.size;
1227
+ fs.unlinkSync(filePath);
1228
+ result.deleted++;
1229
+ result.freed += size;
1230
+ }
1231
+ }
1232
+ } catch (error) {
1233
+ continue;
1234
+ }
1235
+ }
1236
+ } catch (error) {
1237
+ console.warn(`[image-cache] Failed to cleanup cache by age: ${cacheDir}`, error);
1238
+ }
1239
+ return result;
1240
+ }
1241
+ function cleanupCacheBySize(cacheDir, maxSizeMB = 500) {
1242
+ const result = { deleted: 0, freed: 0 };
1243
+ const maxSizeBytes = maxSizeMB * 1024 * 1024;
1244
+ try {
1245
+ if (!fs.existsSync(cacheDir)) {
1246
+ return result;
1247
+ }
1248
+ const files = fs.readdirSync(cacheDir);
1249
+ const fileInfos = [];
1250
+ for (const file of files) {
1251
+ const filePath = path2.join(cacheDir, file);
1252
+ try {
1253
+ const stat = fs.statSync(filePath);
1254
+ if (stat.isFile()) {
1255
+ fileInfos.push({
1256
+ path: filePath,
1257
+ mtime: stat.mtime.getTime(),
1258
+ size: stat.size
1259
+ });
1260
+ }
1261
+ } catch (error) {
1262
+ continue;
1263
+ }
1264
+ }
1265
+ const totalSize = fileInfos.reduce((sum, info) => sum + info.size, 0);
1266
+ if (totalSize <= maxSizeBytes) {
1267
+ return result;
1268
+ }
1269
+ fileInfos.sort((a, b) => a.mtime - b.mtime);
1270
+ let currentSize = totalSize;
1271
+ for (const fileInfo of fileInfos) {
1272
+ if (currentSize <= maxSizeBytes) {
1273
+ break;
1274
+ }
1275
+ try {
1276
+ fs.unlinkSync(fileInfo.path);
1277
+ result.deleted++;
1278
+ result.freed += fileInfo.size;
1279
+ currentSize -= fileInfo.size;
1280
+ } catch (error) {
1281
+ continue;
1282
+ }
1283
+ }
1284
+ } catch (error) {
1285
+ console.warn(`[image-cache] Failed to cleanup cache by size: ${cacheDir}`, error);
1286
+ }
1287
+ return result;
1288
+ }
1289
+ function cleanupCache(config) {
1290
+ const cacheDir = getCacheDir();
1291
+ const maxSizeMB = config?.maxSizeMB ?? 500;
1292
+ const maxAgeDays = config?.maxAgeDays ?? 30;
1293
+ const ageResult = cleanupCacheByAge(cacheDir, maxAgeDays);
1294
+ const sizeResult = cleanupCacheBySize(cacheDir, maxSizeMB);
1295
+ return {
1296
+ deleted: ageResult.deleted + sizeResult.deleted,
1297
+ freed: ageResult.freed + sizeResult.freed
1298
+ };
1299
+ }
1300
+
1301
+ // src/server/utils/image-optimizer.ts
1302
+ var DEFAULT_IMAGE_CONFIG = {
1303
+ maxWidth: 3840,
1304
+ maxHeight: 3840,
1305
+ quality: 70,
1306
+ formats: ["image/avif", "image/webp"],
1307
+ minimumCacheTTL: 31536e3,
1308
+ // 1 año (en lugar de 60)
1309
+ cacheMaxSize: 500,
1310
+ cacheMaxAge: 30,
1311
+ cacheLRUSize: 50,
1312
+ cacheCleanupInterval: 100
1313
+ };
1314
+ var requestCount = 0;
1315
+ async function downloadRemoteImage(url, timeout = 1e4) {
1316
+ const controller = new AbortController();
1317
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1318
+ try {
1319
+ const response = await fetch(url, {
1320
+ signal: controller.signal,
1321
+ headers: {
1322
+ "User-Agent": "Loly-Image-Optimizer/1.0"
1323
+ }
1324
+ });
1325
+ clearTimeout(timeoutId);
1326
+ if (!response.ok) {
1327
+ throw new Error(`Failed to download image: ${response.status} ${response.statusText}`);
1328
+ }
1329
+ const arrayBuffer = await response.arrayBuffer();
1330
+ return Buffer.from(arrayBuffer);
1331
+ } catch (error) {
1332
+ clearTimeout(timeoutId);
1333
+ if (error instanceof Error && error.name === "AbortError") {
1334
+ throw new Error(`Image download timeout after ${timeout}ms`);
1335
+ }
1336
+ throw error;
1337
+ }
1338
+ }
1339
+ function readLocalImage(src, projectRoot) {
1340
+ const sanitized = sanitizeImagePath(src);
1341
+ const publicDir = getPublicDir();
1342
+ const publicPath = path3.join(publicDir, sanitized);
1343
+ if (fs2.existsSync(publicPath)) {
1344
+ return fs2.readFileSync(publicPath);
1345
+ }
1346
+ if (src.startsWith("/")) {
1347
+ const absolutePath = path3.join(projectRoot, sanitized);
1348
+ if (fs2.existsSync(absolutePath)) {
1349
+ return fs2.readFileSync(absolutePath);
1350
+ }
1351
+ }
1352
+ throw new Error(`Image not found: ${src}`);
1353
+ }
1354
+ function determineOutputFormat(sourceFormat, requestedFormat, config) {
1355
+ if (sourceFormat === "svg") {
1356
+ return "svg";
1357
+ }
1358
+ if (requestedFormat && requestedFormat !== "auto") {
1359
+ return requestedFormat;
1360
+ }
1361
+ const supportedFormats = config.formats || ["image/webp"];
1362
+ if (supportedFormats.includes("image/avif")) {
1363
+ return "avif";
1364
+ }
1365
+ if (supportedFormats.includes("image/webp")) {
1366
+ return "webp";
1367
+ }
1368
+ return sourceFormat === "svg" ? "jpeg" : sourceFormat;
1369
+ }
1370
+ async function optimizeImage(options, config) {
1371
+ const imageConfig = config || DEFAULT_IMAGE_CONFIG;
1372
+ const projectRoot = getProjectDir();
1373
+ const dimValidation = validateImageDimensions(options.width, options.height, imageConfig);
1374
+ if (!dimValidation.valid) {
1375
+ throw new Error(dimValidation.error);
1376
+ }
1377
+ const qualityValidation = validateQuality(options.quality);
1378
+ if (!qualityValidation.valid) {
1379
+ throw new Error(qualityValidation.error);
1380
+ }
1381
+ if (isRemoteUrl(options.src)) {
1382
+ if (!validateRemoteUrl(options.src, imageConfig)) {
1383
+ throw new Error(`Remote image domain not allowed: ${options.src}`);
1384
+ }
1385
+ }
1386
+ const sourceFormat = path3.extname(options.src).slice(1).toLowerCase() || "jpeg";
1387
+ const outputFormat = determineOutputFormat(sourceFormat, options.format, imageConfig);
1388
+ const cacheKey = generateCacheKey(
1389
+ options.src,
1390
+ options.width,
1391
+ options.height,
1392
+ options.quality || imageConfig.quality || 75,
1393
+ outputFormat
1394
+ );
1395
+ const cacheDir = getCacheDir();
1396
+ const extension = getImageExtension(outputFormat);
1397
+ const fullCacheKey = `${cacheKey}.${extension}`;
1398
+ const lruSize = imageConfig.cacheLRUSize || DEFAULT_IMAGE_CONFIG.cacheLRUSize || 50;
1399
+ const lruCache = getLRUCache(lruSize);
1400
+ const lruCached = lruCache.get(fullCacheKey);
1401
+ if (lruCached) {
1402
+ const metadata2 = await sharp(lruCached).metadata();
1403
+ return {
1404
+ buffer: lruCached,
1405
+ format: outputFormat,
1406
+ mimeType: getImageMimeType(outputFormat),
1407
+ width: metadata2.width || options.width || 0,
1408
+ height: metadata2.height || options.height || 0
1409
+ };
1410
+ }
1411
+ if (hasCachedImage(cacheKey, extension, cacheDir)) {
1412
+ const cached = readCachedImage(cacheKey, extension, cacheDir);
1413
+ if (cached) {
1414
+ lruCache.set(fullCacheKey, cached);
1415
+ const metadata2 = await sharp(cached).metadata();
1416
+ return {
1417
+ buffer: cached,
1418
+ format: outputFormat,
1419
+ mimeType: getImageMimeType(outputFormat),
1420
+ width: metadata2.width || options.width || 0,
1421
+ height: metadata2.height || options.height || 0
1422
+ };
1423
+ }
1424
+ }
1425
+ let imageBuffer;
1426
+ if (isRemoteUrl(options.src)) {
1427
+ imageBuffer = await downloadRemoteImage(options.src);
1428
+ } else {
1429
+ imageBuffer = readLocalImage(options.src, projectRoot);
1430
+ }
1431
+ if (outputFormat === "svg" || sourceFormat === "svg") {
1432
+ if (!imageConfig.dangerouslyAllowSVG) {
1433
+ throw new Error("SVG images are not allowed. Set images.dangerouslyAllowSVG to true to enable.");
1434
+ }
1435
+ return {
1436
+ buffer: imageBuffer,
1437
+ format: "svg",
1438
+ mimeType: "image/svg+xml",
1439
+ width: options.width || 0,
1440
+ height: options.height || 0
1441
+ };
1442
+ }
1443
+ let sharpInstance = sharp(imageBuffer);
1444
+ const metadata = await sharpInstance.metadata();
1445
+ if (options.width || options.height) {
1446
+ const fit = options.fit || "cover";
1447
+ sharpInstance = sharpInstance.resize(options.width, options.height, {
1448
+ fit,
1449
+ withoutEnlargement: true
1450
+ });
1451
+ }
1452
+ const quality = options.quality || imageConfig.quality || 75;
1453
+ switch (outputFormat) {
1454
+ case "webp":
1455
+ sharpInstance = sharpInstance.webp({ quality });
1456
+ break;
1457
+ case "avif":
1458
+ sharpInstance = sharpInstance.avif({ quality });
1459
+ break;
1460
+ case "jpeg":
1461
+ case "jpg":
1462
+ sharpInstance = sharpInstance.jpeg({ quality });
1463
+ break;
1464
+ case "png":
1465
+ sharpInstance = sharpInstance.png({ quality: Math.round(quality / 100 * 9) });
1466
+ break;
1467
+ default:
1468
+ sharpInstance = sharpInstance.jpeg({ quality });
1469
+ }
1470
+ const optimizedBuffer = await sharpInstance.toBuffer();
1471
+ const finalMetadata = await sharp(optimizedBuffer).metadata();
1472
+ lruCache.set(fullCacheKey, optimizedBuffer);
1473
+ writeCachedImage(cacheKey, extension, cacheDir, optimizedBuffer);
1474
+ requestCount++;
1475
+ const cleanupInterval = imageConfig.cacheCleanupInterval || DEFAULT_IMAGE_CONFIG.cacheCleanupInterval || 100;
1476
+ if (requestCount >= cleanupInterval) {
1477
+ requestCount = 0;
1478
+ setImmediate(() => {
1479
+ try {
1480
+ const cleanupResult = cleanupCache({
1481
+ maxSizeMB: imageConfig.cacheMaxSize || DEFAULT_IMAGE_CONFIG.cacheMaxSize,
1482
+ maxAgeDays: imageConfig.cacheMaxAge || DEFAULT_IMAGE_CONFIG.cacheMaxAge
1483
+ });
1484
+ if (cleanupResult.deleted > 0) {
1485
+ console.log(
1486
+ `[image-cache] Cleaned up ${cleanupResult.deleted} files, freed ${(cleanupResult.freed / 1024 / 1024).toFixed(2)} MB`
1487
+ );
1488
+ }
1489
+ } catch (error) {
1490
+ console.warn("[image-cache] Cleanup failed:", error);
1491
+ }
1492
+ });
1493
+ }
1494
+ return {
1495
+ buffer: optimizedBuffer,
1496
+ format: outputFormat,
1497
+ mimeType: getImageMimeType(outputFormat),
1498
+ width: finalMetadata.width || options.width || metadata.width || 0,
1499
+ height: finalMetadata.height || options.height || metadata.height || 0
1500
+ };
1501
+ }
1502
+
1503
+ // src/server/handlers/image.ts
1504
+ async function handleImageRequest(options) {
1505
+ const { req, res, config } = options;
1506
+ try {
1507
+ const src = req.query.src;
1508
+ const width = req.query.w ? parseInt(req.query.w, 10) : void 0;
1509
+ const height = req.query.h ? parseInt(req.query.h, 10) : void 0;
1510
+ const quality = req.query.q ? parseInt(req.query.q, 10) : void 0;
1511
+ const format = req.query.format;
1512
+ const fit = req.query.fit;
1513
+ if (!src) {
1514
+ res.status(400).json({
1515
+ error: "Missing required parameter: src"
1516
+ });
1517
+ return;
1518
+ }
1519
+ if (typeof src !== "string") {
1520
+ res.status(400).json({
1521
+ error: "Parameter 'src' must be a string"
1522
+ });
1523
+ return;
1524
+ }
1525
+ const etagInput = `${src}-${width || ""}-${height || ""}-${quality || ""}-${format || ""}-${fit || ""}`;
1526
+ const etag = `"${crypto2.createHash("md5").update(etagInput).digest("hex")}"`;
1527
+ const ifNoneMatch = req.headers["if-none-match"];
1528
+ if (ifNoneMatch === etag) {
1529
+ res.status(304).end();
1530
+ return;
1531
+ }
1532
+ const result = await optimizeImage(
1533
+ {
1534
+ src,
1535
+ width,
1536
+ height,
1537
+ quality,
1538
+ format,
1539
+ fit
1540
+ },
1541
+ config
1542
+ );
1543
+ const imageConfig = config || {};
1544
+ const cacheTTL = imageConfig.minimumCacheTTL ?? DEFAULT_IMAGE_CONFIG.minimumCacheTTL;
1545
+ res.setHeader("Content-Type", result.mimeType);
1546
+ res.setHeader("Content-Length", result.buffer.length);
1547
+ res.setHeader("Cache-Control", `public, max-age=${cacheTTL}, immutable`);
1548
+ res.setHeader("ETag", etag);
1549
+ res.setHeader("X-Content-Type-Options", "nosniff");
1550
+ res.send(result.buffer);
1551
+ } catch (error) {
1552
+ if (error instanceof Error) {
1553
+ if (error.message.includes("not allowed")) {
1554
+ res.status(403).json({
1555
+ error: "Forbidden",
1556
+ message: error.message
1557
+ });
1558
+ return;
1559
+ }
1560
+ if (error.message.includes("not found") || error.message.includes("Image not found")) {
1561
+ res.status(404).json({
1562
+ error: "Not Found",
1563
+ message: error.message
1564
+ });
1565
+ return;
1566
+ }
1567
+ if (error.message.includes("must be")) {
1568
+ res.status(400).json({
1569
+ error: "Bad Request",
1570
+ message: error.message
1571
+ });
1572
+ return;
1573
+ }
1574
+ if (error.message.includes("timeout") || error.message.includes("download")) {
1575
+ res.status(504).json({
1576
+ error: "Gateway Timeout",
1577
+ message: error.message
1578
+ });
1579
+ return;
1580
+ }
1581
+ }
1582
+ console.error("[image-optimizer] Error processing image:", error);
1583
+ res.status(500).json({
1584
+ error: "Internal Server Error",
1585
+ message: "Failed to process image"
1586
+ });
1587
+ }
1588
+ }
1589
+
1590
+ // src/router/api-matcher.ts
1591
+ function matchRoutePattern2(pathname, route) {
1592
+ const pathSegments = pathname === "/" ? [""] : pathname.split("/").filter(Boolean);
1593
+ const routeSegments = route.segments.filter((s) => !s.isOptional);
1594
+ if (routeSegments.length === 0 && pathSegments.length === 1 && pathSegments[0] === "") {
1595
+ return { params: {} };
1596
+ }
1597
+ const params = {};
1598
+ let pathIdx = 0;
1599
+ let routeIdx = 0;
1600
+ while (pathIdx < pathSegments.length && routeIdx < routeSegments.length) {
1601
+ const routeSeg = routeSegments[routeIdx];
1602
+ const pathSeg = pathSegments[pathIdx];
1603
+ if (routeSeg.isCatchAll) {
1604
+ const remaining = pathSegments.slice(pathIdx);
1605
+ if (routeSeg.paramName) {
1606
+ params[routeSeg.paramName] = remaining.join("/");
1607
+ }
1608
+ return { params };
1609
+ }
1610
+ if (routeSeg.isDynamic) {
1611
+ if (routeSeg.paramName) {
1612
+ params[routeSeg.paramName] = decodeURIComponent(pathSeg);
1613
+ }
1614
+ pathIdx++;
1615
+ routeIdx++;
1616
+ continue;
1617
+ }
1618
+ if (routeSeg.segment === pathSeg) {
1619
+ pathIdx++;
1620
+ routeIdx++;
1621
+ continue;
1622
+ }
1623
+ return null;
1624
+ }
1625
+ if (pathIdx === pathSegments.length && routeIdx === routeSegments.length) {
1626
+ return { params };
1627
+ }
1628
+ return null;
1629
+ }
1630
+ function matchApiRoute(pathname, config) {
1631
+ const normalized = normalizeUrlPath(pathname);
1632
+ if (config.apiRouteMap && config.apiRouteMap.has(normalized)) {
1633
+ const route = config.apiRouteMap.get(normalized);
1634
+ return { route, params: {} };
1635
+ }
1636
+ if (config.apiRouteMap) {
1637
+ for (const route of config.apiRouteMap.values()) {
1638
+ const match = matchRoutePattern2(normalized, route);
1639
+ if (match) {
1640
+ return { route, params: match.params };
1641
+ }
1642
+ }
1643
+ }
1644
+ for (const route of config.routes) {
1645
+ if (!route.isApiRoute || !route.urlPath) continue;
1646
+ const match = matchRoutePattern2(normalized, route);
1647
+ if (match) {
1648
+ return { route, params: match.params };
1649
+ }
1650
+ }
1651
+ return null;
1652
+ }
1653
+
1654
+ // src/utils/api-route-loader.ts
1655
+ var HTTP_METHODS2 = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
1656
+ async function loadApiRoute(route, compiledPath) {
1657
+ if (!route.isApiRoute) {
1658
+ throw new Error(`[loly-core] Route ${route.filePath} is not an API route`);
1659
+ }
1660
+ const sourcePath = route.filePath;
1661
+ const module = await loadModule(sourcePath, compiledPath);
1662
+ const handlers = /* @__PURE__ */ new Map();
1663
+ for (const exportName of Object.keys(module)) {
1664
+ const upperExport = exportName.toUpperCase();
1665
+ if (HTTP_METHODS2.includes(upperExport) && typeof module[exportName] === "function") {
1666
+ handlers.set(upperExport, module[exportName]);
1667
+ }
1668
+ }
1669
+ if (handlers.size === 0) {
1670
+ throw new Error(
1671
+ `[loly-core] No valid HTTP method handlers found in API route ${route.filePath}`
1672
+ );
1673
+ }
1674
+ return { handlers };
1675
+ }
1676
+ function hasMethod(loadedRoute, method) {
1677
+ return loadedRoute.handlers.has(method.toUpperCase());
1678
+ }
1679
+ function getHandler(loadedRoute, method) {
1680
+ return loadedRoute.handlers.get(method.toUpperCase());
1681
+ }
1682
+
1683
+ // src/server/handlers/api-route.ts
1684
+ function createApiRouteHandler() {
1685
+ return async (pathname, method, req, res, config) => {
1686
+ try {
1687
+ const match = matchApiRoute(pathname, config);
1688
+ if (!match) {
1689
+ return false;
1690
+ }
1691
+ const { route, params } = match;
1692
+ const loadedRoute = await loadApiRoute(route);
1693
+ const upperMethod = method.toUpperCase();
1694
+ if (!hasMethod(loadedRoute, upperMethod)) {
1695
+ res.status(HTTP_STATUS.METHOD_NOT_ALLOWED).json({ error: `Method ${method} not allowed` });
1696
+ return true;
1697
+ }
1698
+ const handler = getHandler(loadedRoute, upperMethod);
1699
+ if (!handler) {
1700
+ res.status(HTTP_STATUS.METHOD_NOT_ALLOWED).json({ error: `Method ${method} not allowed` });
1701
+ return true;
1702
+ }
1703
+ req.params = params;
1704
+ await handler(req, res);
1705
+ return true;
1706
+ } catch (error) {
1707
+ console.error("[loly-core] Error handling API route:", error);
1708
+ if (!res.headersSent) {
1709
+ res.status(HTTP_STATUS.INTERNAL_ERROR).json({
1710
+ error: "Internal Server Error",
1711
+ message: error instanceof Error ? error.message : String(error)
1712
+ });
1713
+ return true;
1714
+ }
1715
+ return false;
1716
+ }
1717
+ };
1718
+ }
1719
+
1720
+ // src/server/handlers/async-handler.ts
1721
+ import { renderToString } from "loly-jsx";
1722
+ async function handleAsyncRequest(req, res) {
1723
+ try {
1724
+ const asyncId = req.params.id;
1725
+ if (!asyncId) {
1726
+ res.status(HTTP_STATUS.BAD_REQUEST).json({
1727
+ html: null,
1728
+ error: "Missing async ID"
1729
+ });
1730
+ return;
1731
+ }
1732
+ const timeoutPromise = new Promise((_, reject) => {
1733
+ setTimeout(() => reject(new Error("Timeout")), 3e4);
1734
+ });
1735
+ try {
1736
+ const resolvedVNode = await Promise.race([resolveAsyncTask(asyncId), timeoutPromise]);
1737
+ const html = await renderToString(resolvedVNode);
1738
+ res.status(HTTP_STATUS.OK).json({
1739
+ html,
1740
+ error: null
1741
+ });
1742
+ } catch (error) {
1743
+ if (error instanceof Error && error.message === "Timeout") {
1744
+ res.status(HTTP_STATUS.REQUEST_TIMEOUT).json({
1745
+ html: null,
1746
+ error: "Async task timeout"
1747
+ });
1748
+ return;
1749
+ }
1750
+ if (error instanceof Error && error.message.includes("not found")) {
1751
+ res.status(HTTP_STATUS.NOT_FOUND).json({
1752
+ html: null,
1753
+ error: error.message
1754
+ });
1755
+ return;
1756
+ }
1757
+ console.error("[loly] Error resolving async task:", error);
1758
+ res.status(HTTP_STATUS.INTERNAL_ERROR).json({
1759
+ html: null,
1760
+ error: error instanceof Error ? error.message : "Unknown error"
1761
+ });
1762
+ }
1763
+ } catch (error) {
1764
+ console.error("[loly] Error handling async request:", error);
1765
+ res.status(HTTP_STATUS.INTERNAL_ERROR).json({
1766
+ html: null,
1767
+ error: error instanceof Error ? error.message : "Unknown error"
1768
+ });
1769
+ }
1770
+ }
1771
+
1772
+ // src/utils/express-setup.ts
1773
+ import compression from "compression";
1774
+ function setupExpressMiddleware(app) {
1775
+ app.use(compression({
1776
+ level: 6,
1777
+ // Compression level (1-9, 6 is good balance)
1778
+ threshold: 1024,
1779
+ // Only compress responses > 1KB
1780
+ filter: (req, res) => {
1781
+ if (req.headers["x-no-compression"]) {
1782
+ return false;
1783
+ }
1784
+ return compression.filter(req, res);
1785
+ }
1786
+ }));
1787
+ app.use((req, res, next) => {
1788
+ const url = new URL(req.url, `http://${req.headers.host}`);
1789
+ req.query = Object.fromEntries(url.searchParams);
1790
+ next();
1791
+ });
1792
+ }
1793
+ function setupStaticFiles(app, clientDir, publicDir) {
1794
+ if (existsSync4(clientDir)) {
1795
+ app.use(express.static(clientDir));
1796
+ } else {
1797
+ console.warn(ERROR_MESSAGES.CLIENT_BUILD_DIR_NOT_FOUND(clientDir));
1798
+ }
1799
+ if (existsSync4(publicDir)) {
1800
+ app.use(express.static(publicDir));
1801
+ }
1802
+ }
1803
+ function setupRSCEndpoint(app, handler) {
1804
+ app.get(SERVER.RSC_ENDPOINT, async (req, res) => {
1805
+ try {
1806
+ const pathname = req.query.path || "/";
1807
+ const search = req.query.search || "";
1808
+ const searchParams = search ? Object.fromEntries(new URLSearchParams(search)) : {};
1809
+ const result = await handler(pathname, searchParams);
1810
+ res.setHeader("Content-Type", HTTP_HEADERS.CONTENT_TYPE_JSON);
1811
+ res.json({
1812
+ pathname,
1813
+ params: result.params,
1814
+ searchParams,
1815
+ html: result.html,
1816
+ scripts: result.scripts,
1817
+ styles: result.styles,
1818
+ preloads: result.preloads,
1819
+ islandData: result.islandData
1820
+ });
1821
+ } catch (error) {
1822
+ console.error(ERROR_MESSAGES.RSC_ENDPOINT_ERROR, error);
1823
+ if (error.message.includes("Route not found")) {
1824
+ res.status(HTTP_STATUS.NOT_FOUND).json({ error: "Route not found" });
1825
+ } else {
1826
+ res.status(HTTP_STATUS.INTERNAL_ERROR).json({
1827
+ error: "Internal Server Error"
1828
+ });
1829
+ }
1830
+ }
1831
+ });
1832
+ }
1833
+ function setupImageEndpoint(app, config) {
1834
+ app.get(SERVER.IMAGE_ENDPOINT, async (req, res) => {
1835
+ await handleImageRequest({ req, res, config });
1836
+ });
1837
+ }
1838
+ function setupAsyncEndpoint(app) {
1839
+ app.get(`${SERVER.ASYNC_ENDPOINT}/:id`, async (req, res) => {
1840
+ await handleAsyncRequest(req, res);
1841
+ });
1842
+ }
1843
+ function setupApiRoutes(app, getConfig) {
1844
+ app.use(express.json({ limit: "10mb" }));
1845
+ app.use(express.urlencoded({ extended: true, limit: "10mb" }));
1846
+ const apiHandler = createApiRouteHandler();
1847
+ app.use(async (req, res, next) => {
1848
+ const config = getConfig();
1849
+ if (!config) {
1850
+ return next();
1851
+ }
1852
+ const url = new URL(req.url, `http://${req.headers.host}`);
1853
+ const pathname = url.pathname;
1854
+ const method = req.method;
1855
+ if (config.apiRouteMap && config.apiRouteMap.size > 0) {
1856
+ try {
1857
+ const handled = await apiHandler(pathname, method, req, res, config);
1858
+ if (handled) {
1859
+ return;
1860
+ }
1861
+ } catch (error) {
1862
+ if (!res.headersSent) {
1863
+ return next();
1864
+ }
1865
+ return;
1866
+ }
1867
+ }
1868
+ next();
1869
+ });
1870
+ }
1871
+ function setupSSREndpoint(app, handler) {
1872
+ app.get("*", async (req, res) => {
1873
+ try {
1874
+ const url = new URL(req.url, `http://${req.headers.host}`);
1875
+ const result = await handler(
1876
+ url.pathname,
1877
+ Object.fromEntries(url.searchParams)
1878
+ );
1879
+ res.status(result.status);
1880
+ res.setHeader("Content-Type", HTTP_HEADERS.CONTENT_TYPE_HTML);
1881
+ const nodeStream = Readable.fromWeb(result.html);
1882
+ nodeStream.pipe(res);
1883
+ nodeStream.on("error", (err) => {
1884
+ if (!res.headersSent) {
1885
+ console.error(ERROR_MESSAGES.ERROR_HANDLING_REQUEST, err);
1886
+ res.status(HTTP_STATUS.INTERNAL_ERROR).send("Internal Server Error");
1887
+ }
1888
+ });
1889
+ } catch (error) {
1890
+ console.error(ERROR_MESSAGES.ERROR_HANDLING_REQUEST, error);
1891
+ res.status(HTTP_STATUS.INTERNAL_ERROR).send("Internal Server Error");
1892
+ }
1893
+ });
1894
+ }
1895
+
1896
+ // src/server/base-server.ts
1897
+ var BaseServer = class {
1898
+ constructor(port = SERVER.DEFAULT_PORT) {
1899
+ this.app = express2();
1900
+ this.port = port;
1901
+ }
1902
+ /**
1903
+ * Template method - defines the algorithm structure
1904
+ */
1905
+ async setup() {
1906
+ this.setupMiddleware();
1907
+ this.setupStaticFiles();
1908
+ this.setupImageEndpoint();
1909
+ this.setupAsyncEndpoint();
1910
+ this.setupApiRoutes();
1911
+ this.setupRSCEndpoint();
1912
+ this.setupSSREndpoint();
1913
+ }
1914
+ /**
1915
+ * Setup Express middleware
1916
+ */
1917
+ setupMiddleware() {
1918
+ setupExpressMiddleware(this.app);
1919
+ }
1920
+ /**
1921
+ * Setup static file serving
1922
+ */
1923
+ setupStaticFiles() {
1924
+ setupStaticFiles(this.app, getClientDir(), getPublicDir());
1925
+ }
1926
+ /**
1927
+ * Setup image optimization endpoint
1928
+ */
1929
+ setupImageEndpoint() {
1930
+ setupImageEndpoint(this.app);
1931
+ }
1932
+ /**
1933
+ * Setup async component endpoint
1934
+ */
1935
+ setupAsyncEndpoint() {
1936
+ setupAsyncEndpoint(this.app);
1937
+ }
1938
+ /**
1939
+ * Setup API routes
1940
+ */
1941
+ setupApiRoutes() {
1942
+ setupApiRoutes(this.app, () => {
1943
+ try {
1944
+ return getRouteConfig();
1945
+ } catch {
1946
+ return null;
1947
+ }
1948
+ });
1949
+ }
1950
+ /**
1951
+ * Setup RSC endpoint
1952
+ */
1953
+ setupRSCEndpoint() {
1954
+ setupRSCEndpoint(this.app, this.createRSCHandler());
1955
+ }
1956
+ /**
1957
+ * Setup SSR endpoint
1958
+ */
1959
+ setupSSREndpoint() {
1960
+ setupSSREndpoint(this.app, this.createSSRHandler());
1961
+ }
1962
+ /**
1963
+ * Start the server
1964
+ */
1965
+ async start() {
1966
+ await this.setup();
1967
+ this.app.listen(this.port, () => {
1968
+ console.log(
1969
+ `[loly-core] Server running at http://localhost:${this.port}`
1970
+ );
1971
+ });
1972
+ }
1973
+ /**
1974
+ * Get the Express app instance
1975
+ */
1976
+ getApp() {
1977
+ return this.app;
1978
+ }
1979
+ };
1980
+
1981
+ // src/server/dev-server.ts
1982
+ var routeConfig = null;
1983
+ var routeConfigPromise = null;
1984
+ async function reloadRoutes(appDir) {
1985
+ const config = await scanRoutes(appDir);
1986
+ routeConfig = config;
1987
+ return config;
1988
+ }
1989
+ async function getRouteConfig2(appDir) {
1990
+ if (routeConfig) {
1991
+ return routeConfig;
1992
+ }
1993
+ if (routeConfigPromise) {
1994
+ return routeConfigPromise;
1995
+ }
1996
+ routeConfigPromise = reloadRoutes(appDir);
1997
+ return routeConfigPromise;
1998
+ }
1999
+ var DevServer = class extends BaseServer {
2000
+ constructor(options) {
2001
+ super(options.port);
2002
+ this.watcher = null;
2003
+ this.projectDir = options.projectDir;
2004
+ }
2005
+ /**
2006
+ * Initialize server context and load routes
2007
+ */
2008
+ async initialize() {
2009
+ const appDir = getAppDirPath(this.projectDir);
2010
+ const srcDir = join3(this.projectDir, DIRECTORIES.SRC);
2011
+ const outDir = join3(this.projectDir, DIRECTORIES.LOLY, DIRECTORIES.DIST);
2012
+ if (!existsSync5(appDir)) {
2013
+ throw new Error(ERROR_MESSAGES.APP_DIR_NOT_FOUND(this.projectDir));
2014
+ }
2015
+ setContext({
2016
+ projectDir: this.projectDir,
2017
+ appDir,
2018
+ srcDir,
2019
+ outDir,
2020
+ buildMode: "development",
2021
+ isDev: true,
2022
+ serverPort: this.port
2023
+ });
2024
+ const initialConfig = await reloadRoutes(getAppDir());
2025
+ setRouteConfig(initialConfig);
2026
+ }
2027
+ /**
2028
+ * Create RSC handler
2029
+ */
2030
+ createRSCHandler() {
2031
+ return async (pathname, searchParams) => {
2032
+ const config = await getRouteConfig2(getAppDir());
2033
+ const context = {
2034
+ pathname,
2035
+ searchParams,
2036
+ params: {}
2037
+ };
2038
+ const result = await renderPageContent(context, config, getAppDir());
2039
+ return {
2040
+ ...result,
2041
+ params: context.params
2042
+ };
2043
+ };
2044
+ }
2045
+ /**
2046
+ * Create SSR handler
2047
+ */
2048
+ createSSRHandler() {
2049
+ return async (pathname, searchParams) => {
2050
+ const config = await getRouteConfig2(getAppDir());
2051
+ const context = {
2052
+ pathname,
2053
+ searchParams,
2054
+ params: {}
2055
+ };
2056
+ return await renderPageStream(context, config, getAppDir());
2057
+ };
2058
+ }
2059
+ /**
2060
+ * Load route configuration
2061
+ */
2062
+ async loadRouteConfig() {
2063
+ return await getRouteConfig2(getAppDir());
2064
+ }
2065
+ /**
2066
+ * Setup hot reload watcher
2067
+ */
2068
+ setupHotReload() {
2069
+ this.watcher = chokidar.watch(getAppDir(), {
2070
+ ignored: /node_modules/,
2071
+ persistent: true
2072
+ });
2073
+ const reloadHandler = async () => {
2074
+ routeConfig = null;
2075
+ routeConfigPromise = null;
2076
+ await reloadRoutes(getAppDir());
2077
+ };
2078
+ this.watcher.on("change", reloadHandler);
2079
+ this.watcher.on("add", reloadHandler);
2080
+ this.watcher.on("unlink", reloadHandler);
2081
+ }
2082
+ /**
2083
+ * Start the development server with hot reload
2084
+ */
2085
+ async start() {
2086
+ await this.initialize();
2087
+ await this.setup();
2088
+ this.setupHotReload();
2089
+ this.getApp().listen(this.port, () => {
2090
+ console.log(
2091
+ `[loly-core] Dev server running at http://localhost:${this.port}`
2092
+ );
2093
+ });
2094
+ }
2095
+ /**
2096
+ * Stop the server and cleanup
2097
+ */
2098
+ async stop() {
2099
+ if (this.watcher) {
2100
+ await this.watcher.close();
2101
+ this.watcher = null;
2102
+ }
2103
+ }
2104
+ };
2105
+ async function startDevServer(options) {
2106
+ const server = new DevServer(options);
2107
+ await server.start();
2108
+ }
2109
+
2110
+ // src/server/prod-server.ts
2111
+ import { join as join4 } from "path";
2112
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
2113
+ function loadRouteConfigFromManifest() {
2114
+ const manifestPath = getRoutesManifestPath();
2115
+ if (!existsSync6(manifestPath)) {
2116
+ throw new Error(ERROR_MESSAGES.ROUTES_MANIFEST_NOT_FOUND(manifestPath));
2117
+ }
2118
+ const manifestContent = readFileSync2(manifestPath, "utf-8");
2119
+ const manifest = JSON.parse(manifestContent);
2120
+ const routes = manifest.routes.map((route) => ({
2121
+ filePath: route.serverPath || route.sourcePath,
2122
+ // Prefer compiled path
2123
+ segments: route.segments,
2124
+ type: route.type,
2125
+ isRootLayout: route.isRootLayout,
2126
+ urlPath: route.urlPath,
2127
+ isApiRoute: route.isApiRoute || void 0,
2128
+ httpMethods: route.httpMethods || void 0
2129
+ }));
2130
+ const rootLayout = manifest.rootLayout ? {
2131
+ filePath: manifest.rootLayout.serverPath || manifest.rootLayout.sourcePath,
2132
+ segments: manifest.rootLayout.segments,
2133
+ type: manifest.rootLayout.type,
2134
+ isRootLayout: manifest.rootLayout.isRootLayout,
2135
+ urlPath: manifest.rootLayout.urlPath
2136
+ } : void 0;
2137
+ const routeMap = /* @__PURE__ */ new Map();
2138
+ const apiRouteMap = /* @__PURE__ */ new Map();
2139
+ for (const routeEntry of manifest.routeMap || []) {
2140
+ const route = {
2141
+ filePath: routeEntry.serverPath || routeEntry.sourcePath,
2142
+ segments: routeEntry.segments,
2143
+ type: routeEntry.type,
2144
+ isRootLayout: routeEntry.isRootLayout,
2145
+ urlPath: routeEntry.urlPath,
2146
+ isApiRoute: routeEntry.isApiRoute || void 0,
2147
+ httpMethods: routeEntry.httpMethods || void 0
2148
+ };
2149
+ if (routeEntry.type === "page") {
2150
+ routeMap.set(routeEntry.path, route);
2151
+ } else if (routeEntry.isApiRoute && routeEntry.urlPath) {
2152
+ apiRouteMap.set(routeEntry.urlPath, route);
2153
+ }
2154
+ }
2155
+ const routeMapManifest = /* @__PURE__ */ new Map();
2156
+ for (const routeEntry of manifest.routeMap || []) {
2157
+ routeMapManifest.set(routeEntry.path, routeEntry);
2158
+ }
2159
+ return {
2160
+ routes,
2161
+ rootLayout,
2162
+ routeMap,
2163
+ apiRouteMap,
2164
+ routeMapManifest,
2165
+ // Include manifest data for accessing compiled paths and chunks
2166
+ rootLayoutManifest: manifest.rootLayout,
2167
+ // Include root layout manifest
2168
+ routesManifest: manifest.routes
2169
+ // Include all routes from manifest for layout lookup
2170
+ };
2171
+ }
2172
+ var ProdServer = class extends BaseServer {
2173
+ constructor(options) {
2174
+ super(options.port);
2175
+ this.config = null;
2176
+ this.projectDir = options.projectDir;
2177
+ }
2178
+ /**
2179
+ * Initialize server context and load routes from manifest
2180
+ */
2181
+ async initialize() {
2182
+ const appDir = getAppDirPath(this.projectDir);
2183
+ const srcDir = join4(this.projectDir, DIRECTORIES.SRC);
2184
+ const outDir = join4(this.projectDir, DIRECTORIES.LOLY, DIRECTORIES.DIST);
2185
+ if (!existsSync6(appDir)) {
2186
+ throw new Error(ERROR_MESSAGES.APP_DIR_NOT_FOUND(this.projectDir));
2187
+ }
2188
+ setContext({
2189
+ projectDir: this.projectDir,
2190
+ appDir,
2191
+ srcDir,
2192
+ outDir,
2193
+ buildMode: "production",
2194
+ isDev: false,
2195
+ serverPort: this.port
2196
+ });
2197
+ this.config = loadRouteConfigFromManifest();
2198
+ setRouteConfig(this.config);
2199
+ }
2200
+ /**
2201
+ * Create RSC handler
2202
+ */
2203
+ createRSCHandler() {
2204
+ return async (pathname, searchParams) => {
2205
+ if (!this.config) {
2206
+ this.config = await this.loadRouteConfig();
2207
+ }
2208
+ const context = {
2209
+ pathname,
2210
+ searchParams,
2211
+ params: {}
2212
+ };
2213
+ const result = await renderPageContent(context, this.config, getAppDir());
2214
+ return {
2215
+ ...result,
2216
+ params: context.params
2217
+ };
2218
+ };
2219
+ }
2220
+ /**
2221
+ * Create SSR handler
2222
+ */
2223
+ createSSRHandler() {
2224
+ return async (pathname, searchParams) => {
2225
+ if (!this.config) {
2226
+ this.config = await this.loadRouteConfig();
2227
+ }
2228
+ const context = {
2229
+ pathname,
2230
+ searchParams,
2231
+ params: {}
2232
+ };
2233
+ return await renderPageStream(context, this.config, getAppDir());
2234
+ };
2235
+ }
2236
+ /**
2237
+ * Load route configuration from manifest
2238
+ */
2239
+ async loadRouteConfig() {
2240
+ return loadRouteConfigFromManifest();
2241
+ }
2242
+ /**
2243
+ * Start the production server
2244
+ */
2245
+ async start() {
2246
+ await this.initialize();
2247
+ await this.setup();
2248
+ this.getApp().listen(this.port, () => {
2249
+ console.log(
2250
+ `[loly-core] Production server running at http://localhost:${this.port}`
2251
+ );
2252
+ });
2253
+ }
2254
+ };
2255
+ async function startProdServer(options) {
2256
+ const server = new ProdServer(options);
2257
+ await server.start();
2258
+ }
2259
+
2260
+ // src/client/router.ts
2261
+ import { bootIslands, mount, activateAsyncComponents } from "loly-jsx";
2262
+
2263
+ // src/utils/route-matcher.ts
2264
+ function matchRoutePattern3(pattern, pathname) {
2265
+ const normalizedPattern = normalizeUrlPath(pattern);
2266
+ const normalizedPathname = normalizeUrlPath(pathname);
2267
+ if (normalizedPattern === normalizedPathname) {
2268
+ return { params: {} };
2269
+ }
2270
+ const patternParts = normalizedPattern.split("/").filter(Boolean);
2271
+ const pathParts = normalizedPathname.split("/").filter(Boolean);
2272
+ if (patternParts.length === 0 && pathParts.length === 0) {
2273
+ return { params: {} };
2274
+ }
2275
+ const params = {};
2276
+ let patternIdx = 0;
2277
+ let pathIdx = 0;
2278
+ while (patternIdx < patternParts.length && pathIdx < pathParts.length) {
2279
+ const patternPart = patternParts[patternIdx];
2280
+ const pathPart = pathParts[pathIdx];
2281
+ if (patternPart.startsWith("*")) {
2282
+ const paramName = patternPart.slice(1);
2283
+ const remaining = pathParts.slice(pathIdx);
2284
+ if (paramName) {
2285
+ params[paramName] = remaining.join("/");
2286
+ }
2287
+ return { params };
2288
+ }
2289
+ if (patternPart.startsWith(":")) {
2290
+ const paramName = patternPart.slice(1);
2291
+ params[paramName] = decodeURIComponent(pathPart);
2292
+ patternIdx++;
2293
+ pathIdx++;
2294
+ continue;
2295
+ }
2296
+ if (patternPart === pathPart) {
2297
+ patternIdx++;
2298
+ pathIdx++;
2299
+ continue;
2300
+ }
2301
+ return null;
2302
+ }
2303
+ if (patternIdx === patternParts.length && pathIdx === pathParts.length) {
2304
+ return { params };
2305
+ }
2306
+ return null;
2307
+ }
2308
+ function extractRouteParams(pattern, pathname) {
2309
+ const match = matchRoutePattern3(pattern, pathname);
2310
+ return match?.params || {};
2311
+ }
2312
+ function matchesRoute(pattern, pathname) {
2313
+ if (pattern === pathname) return true;
2314
+ const normalizedPattern = normalizeUrlPath(pattern);
2315
+ const normalizedPathname = normalizeUrlPath(pathname);
2316
+ if (normalizedPattern === normalizedPathname) return true;
2317
+ const escaped = normalizedPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
2318
+ const regex = new RegExp(
2319
+ "^" + escaped.replace(/:[^/]+/g, "([^/]+)") + "/?$"
2320
+ );
2321
+ return regex.test(normalizedPathname);
2322
+ }
2323
+
2324
+ // src/client/router.ts
2325
+ var LRUCache = class {
2326
+ constructor(maxSize = 10) {
2327
+ this.maxSize = maxSize;
2328
+ this.cache = /* @__PURE__ */ new Map();
2329
+ }
2330
+ get(key) {
2331
+ if (!this.cache.has(key)) {
2332
+ return void 0;
2333
+ }
2334
+ const value = this.cache.get(key);
2335
+ this.cache.delete(key);
2336
+ this.cache.set(key, value);
2337
+ return value;
2338
+ }
2339
+ set(key, value) {
2340
+ if (this.cache.has(key)) {
2341
+ this.cache.delete(key);
2342
+ } else if (this.cache.size >= this.maxSize) {
2343
+ const firstKey = this.cache.keys().next().value;
2344
+ if (firstKey !== void 0) {
2345
+ this.cache.delete(firstKey);
2346
+ }
2347
+ }
2348
+ this.cache.set(key, value);
2349
+ }
2350
+ has(key) {
2351
+ return this.cache.has(key);
2352
+ }
2353
+ delete(key) {
2354
+ return this.cache.delete(key);
2355
+ }
2356
+ clear() {
2357
+ this.cache.clear();
2358
+ }
2359
+ get size() {
2360
+ return this.cache.size;
2361
+ }
2362
+ };
2363
+ function getInitialLocation() {
2364
+ const { pathname, search, hash } = window.location;
2365
+ return { pathname, search, hash };
2366
+ }
2367
+ function normalizePath2(path4) {
2368
+ const url = new URL(path4, "http://localhost");
2369
+ return {
2370
+ pathname: normalizeUrlPath(url.pathname || "/"),
2371
+ search: url.search,
2372
+ hash: url.hash
2373
+ };
2374
+ }
2375
+ async function fetchRSCPayload(pathname, search = "", options) {
2376
+ const url = `${SERVER.RSC_ENDPOINT}?path=${encodeURIComponent(pathname)}${search ? `&search=${encodeURIComponent(search)}` : ""}`;
2377
+ try {
2378
+ const fetchOptions = {};
2379
+ if (options?.priority) {
2380
+ fetchOptions.priority = options.priority;
2381
+ }
2382
+ const response = await fetch(url, fetchOptions);
2383
+ if (!response.ok) {
2384
+ if (response.status === 404) return null;
2385
+ throw new Error(`Failed to fetch RSC: ${response.statusText}`);
2386
+ }
2387
+ const rscPayload = await response.json();
2388
+ return {
2389
+ html: rscPayload.html,
2390
+ scripts: rscPayload.scripts || "",
2391
+ styles: rscPayload.styles || "",
2392
+ islandData: rscPayload.islandData || {}
2393
+ };
2394
+ } catch (error) {
2395
+ return null;
2396
+ }
2397
+ }
2398
+ async function loadComponent(route) {
2399
+ try {
2400
+ const module = await route.component();
2401
+ const Component = module.default || module;
2402
+ if (!Component) {
2403
+ throw new Error(ERROR_MESSAGES.COMPONENT_NOT_FOUND(route.path));
2404
+ }
2405
+ return (props) => {
2406
+ const safeProps = props && typeof props === "object" && !Array.isArray(props) ? props : { params: {}, searchParams: {} };
2407
+ if (typeof Component === "function") {
2408
+ return Component(safeProps);
2409
+ }
2410
+ return Component;
2411
+ };
2412
+ } catch (error) {
2413
+ throw error;
2414
+ }
2415
+ }
2416
+ function bootIslandsNow() {
2417
+ bootIslands();
2418
+ }
2419
+ var globalRouter = null;
2420
+ function setGlobalRouter(router) {
2421
+ globalRouter = router;
2422
+ }
2423
+ var FrameworkRouter = class {
2424
+ // Track links that have been pre-fetched
2425
+ constructor(routes, appContainer) {
2426
+ this.componentCache = /* @__PURE__ */ new Map();
2427
+ this.loadedScripts = /* @__PURE__ */ new Set();
2428
+ // Track loaded scripts
2429
+ this.routeStyles = /* @__PURE__ */ new Map();
2430
+ this.prefetchPromises = /* @__PURE__ */ new Map();
2431
+ this.prefetchObserver = null;
2432
+ this.MAX_CACHE_SIZE = 10;
2433
+ this.invalidatedPaths = /* @__PURE__ */ new Set();
2434
+ // Prepared for revalidatePath
2435
+ this.prefetchedLinks = /* @__PURE__ */ new Set();
2436
+ this.routes = routes;
2437
+ this.appContainer = appContainer;
2438
+ this.currentLocation = getInitialLocation();
2439
+ this.rscPayloadCache = new LRUCache(this.MAX_CACHE_SIZE);
2440
+ setGlobalRouter(this);
2441
+ this.setupNavigation();
2442
+ this.setupPopState();
2443
+ this.setupPrefetch();
2444
+ }
2445
+ /**
2446
+ * Check if pre-fetch should be performed based on connection
2447
+ */
2448
+ shouldPrefetch() {
2449
+ const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
2450
+ if (!connection) {
2451
+ return true;
2452
+ }
2453
+ if (connection.effectiveType === "slow-2g" || connection.effectiveType === "2g") {
2454
+ return false;
2455
+ }
2456
+ if (connection.saveData === true) {
2457
+ return false;
2458
+ }
2459
+ return true;
2460
+ }
2461
+ /**
2462
+ * Generate cache key from pathname and search
2463
+ */
2464
+ getCacheKey(pathname, search = "") {
2465
+ return `${normalizeUrlPath(pathname)}${search || ""}`;
2466
+ }
2467
+ /**
2468
+ * Check if href is an internal link
2469
+ */
2470
+ isInternalLink(href) {
2471
+ if (!href) return false;
2472
+ if (href.startsWith("http") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:")) {
2473
+ return false;
2474
+ }
2475
+ return true;
2476
+ }
2477
+ /**
2478
+ * Pre-fetch route data (RSC payload and JS component)
2479
+ */
2480
+ async prefetchRoute(pathname, search = "") {
2481
+ if (!this.shouldPrefetch()) {
2482
+ return null;
2483
+ }
2484
+ const key = this.getCacheKey(pathname, search);
2485
+ if (this.invalidatedPaths.has(key)) {
2486
+ return null;
2487
+ }
2488
+ const cached = this.rscPayloadCache.get(key);
2489
+ if (cached) {
2490
+ this.prefetchedLinks.add(key);
2491
+ return cached;
2492
+ }
2493
+ if (this.prefetchPromises.has(key)) {
2494
+ return this.prefetchPromises.get(key);
2495
+ }
2496
+ const promise = fetchRSCPayload(pathname, search, { priority: "low" }).then((payload) => {
2497
+ if (payload) {
2498
+ this.rscPayloadCache.set(key, payload);
2499
+ this.prefetchedLinks.add(key);
2500
+ const route = this.findRoute(pathname);
2501
+ if (route) {
2502
+ this.loadComponentCached(route).catch(() => {
2503
+ });
2504
+ }
2505
+ }
2506
+ return payload;
2507
+ }).catch((error) => {
2508
+ if (typeof console !== "undefined" && console.debug) {
2509
+ console.debug("[loly-core] Pre-fetch failed:", error);
2510
+ }
2511
+ return null;
2512
+ }).finally(() => {
2513
+ this.prefetchPromises.delete(key);
2514
+ });
2515
+ this.prefetchPromises.set(key, promise);
2516
+ return promise;
2517
+ }
2518
+ /**
2519
+ * Navigate to a new path
2520
+ */
2521
+ navigate(to, options = {}) {
2522
+ const next = normalizePath2(to);
2523
+ if (options.replace) {
2524
+ window.history.replaceState({}, "", to);
2525
+ } else {
2526
+ window.history.pushState({}, "", to);
2527
+ }
2528
+ this.currentLocation = next;
2529
+ this.updateContent();
2530
+ if (!next.hash) {
2531
+ window.scrollTo({ top: 0 });
2532
+ } else {
2533
+ const el = document.getElementById(next.hash.slice(1));
2534
+ if (el) {
2535
+ el.scrollIntoView();
2536
+ }
2537
+ }
2538
+ }
2539
+ /**
2540
+ * Update app content based on current location
2541
+ */
2542
+ async updateContent() {
2543
+ const { pathname, search } = this.currentLocation;
2544
+ const key = this.getCacheKey(pathname, search);
2545
+ let rscPayload = this.rscPayloadCache.get(key) || null;
2546
+ if (!rscPayload) {
2547
+ rscPayload = await fetchRSCPayload(pathname, search);
2548
+ if (rscPayload) {
2549
+ this.rscPayloadCache.set(key, rscPayload);
2550
+ }
2551
+ }
2552
+ this.prefetchPromises.delete(key);
2553
+ if (rscPayload) {
2554
+ const hasIslands = rscPayload.html.includes("data-loly-island");
2555
+ if (hasIslands) {
2556
+ const route2 = this.findRoute(pathname);
2557
+ if (route2) {
2558
+ try {
2559
+ await this.loadComponentCached(route2);
2560
+ } catch (err) {
2561
+ console.warn(
2562
+ "[loly-core] Failed to load route component for island registration:",
2563
+ err
2564
+ );
2565
+ }
2566
+ }
2567
+ }
2568
+ if (rscPayload.islandData && Object.keys(rscPayload.islandData).length > 0) {
2569
+ if (!window.__LOLY_ISLAND_DATA__) {
2570
+ window.__LOLY_ISLAND_DATA__ = {};
2571
+ }
2572
+ Object.assign(
2573
+ window.__LOLY_ISLAND_DATA__,
2574
+ rscPayload.islandData
2575
+ );
2576
+ }
2577
+ const fragment = document.createDocumentFragment();
2578
+ const tempDiv = document.createElement("div");
2579
+ tempDiv.innerHTML = rscPayload.html;
2580
+ while (tempDiv.firstChild) {
2581
+ fragment.appendChild(tempDiv.firstChild);
2582
+ }
2583
+ this.appContainer.innerHTML = "";
2584
+ this.appContainer.appendChild(fragment);
2585
+ if (rscPayload.styles) {
2586
+ this.injectStyles(pathname, rscPayload.styles);
2587
+ }
2588
+ if (rscPayload.scripts) {
2589
+ await this.injectScripts(rscPayload.scripts);
2590
+ }
2591
+ requestAnimationFrame(() => {
2592
+ requestAnimationFrame(() => {
2593
+ bootIslandsNow();
2594
+ this.observeNewLinks(this.appContainer);
2595
+ });
2596
+ });
2597
+ return;
2598
+ }
2599
+ const route = this.findRoute(pathname);
2600
+ if (route) {
2601
+ try {
2602
+ const Component = await this.loadComponentCached(route);
2603
+ const searchParams = Object.fromEntries(
2604
+ new URLSearchParams(search || "")
2605
+ );
2606
+ const params = extractRouteParams(route.path, pathname);
2607
+ const result = Component({ params, searchParams });
2608
+ this.appContainer.innerHTML = "";
2609
+ mount(result, this.appContainer);
2610
+ requestAnimationFrame(() => {
2611
+ requestAnimationFrame(() => {
2612
+ bootIslandsNow();
2613
+ activateAsyncComponents(this.appContainer);
2614
+ this.observeNewLinks(this.appContainer);
2615
+ });
2616
+ });
2617
+ } catch (err) {
2618
+ this.appContainer.innerHTML = "<div>Error loading route</div>";
2619
+ }
2620
+ } else {
2621
+ this.appContainer.innerHTML = "<div>404 - Not Found</div>";
2622
+ }
2623
+ }
2624
+ /**
2625
+ * Inject styles for a route with deduplication
2626
+ */
2627
+ injectStyles(routePath, styles) {
2628
+ const existing = this.routeStyles.get(routePath);
2629
+ if (existing) {
2630
+ existing.forEach((link) => {
2631
+ if (!link.href.includes("globals.css")) {
2632
+ link.remove();
2633
+ }
2634
+ });
2635
+ }
2636
+ if (!styles.trim()) return;
2637
+ const tempDiv = document.createElement("div");
2638
+ tempDiv.innerHTML = styles.trim();
2639
+ const linkElements = [];
2640
+ const links = tempDiv.querySelectorAll('link[rel="stylesheet"]');
2641
+ links.forEach((link) => {
2642
+ const href = link.getAttribute("href") || "";
2643
+ if (href.includes("globals.css")) {
2644
+ return;
2645
+ }
2646
+ const linkEl = document.createElement("link");
2647
+ linkEl.rel = "stylesheet";
2648
+ linkEl.href = href;
2649
+ linkEl.setAttribute("data-route-styles", routePath);
2650
+ const existingLink = document.querySelector(
2651
+ `link[rel="stylesheet"][href="${href}"]`
2652
+ );
2653
+ if (!existingLink) {
2654
+ document.head.appendChild(linkEl);
2655
+ linkElements.push(linkEl);
2656
+ }
2657
+ });
2658
+ if (linkElements.length > 0) {
2659
+ this.routeStyles.set(routePath, linkElements);
2660
+ }
2661
+ }
2662
+ /**
2663
+ * Inject scripts with deduplication
2664
+ */
2665
+ async injectScripts(scripts) {
2666
+ if (!scripts.trim()) return;
2667
+ const tempDiv = document.createElement("div");
2668
+ tempDiv.innerHTML = scripts;
2669
+ const scriptTags = tempDiv.querySelectorAll("script");
2670
+ const loadPromises = [];
2671
+ scriptTags.forEach((script) => {
2672
+ const src = script.getAttribute("src");
2673
+ if (src) {
2674
+ if (this.loadedScripts.has(src)) {
2675
+ return;
2676
+ }
2677
+ this.loadedScripts.add(src);
2678
+ const promise = new Promise((resolve5, reject) => {
2679
+ const newScript = document.createElement("script");
2680
+ newScript.type = "module";
2681
+ newScript.src = src;
2682
+ newScript.onload = () => resolve5();
2683
+ newScript.onerror = () => {
2684
+ this.loadedScripts.delete(src);
2685
+ reject(new Error(`Failed to load script: ${src}`));
2686
+ };
2687
+ document.head.appendChild(newScript);
2688
+ });
2689
+ loadPromises.push(promise);
2690
+ } else {
2691
+ const inlineScript = document.createElement("script");
2692
+ inlineScript.type = "module";
2693
+ inlineScript.textContent = script.textContent || "";
2694
+ document.head.appendChild(inlineScript);
2695
+ setTimeout(() => inlineScript.remove(), 0);
2696
+ }
2697
+ });
2698
+ await Promise.all(loadPromises);
2699
+ }
2700
+ /**
2701
+ * Find matching route for pathname
2702
+ */
2703
+ findRoute(pathname) {
2704
+ const exact = this.routes.find((r) => r.path === pathname);
2705
+ if (exact) return exact;
2706
+ for (const route of this.routes) {
2707
+ if (matchesRoute(route.path, pathname)) {
2708
+ return route;
2709
+ }
2710
+ }
2711
+ return null;
2712
+ }
2713
+ /**
2714
+ * Load component with caching
2715
+ */
2716
+ async loadComponentCached(route) {
2717
+ if (this.componentCache.has(route.path)) {
2718
+ return this.componentCache.get(route.path);
2719
+ }
2720
+ const Component = await loadComponent(route);
2721
+ this.componentCache.set(route.path, Component);
2722
+ return Component;
2723
+ }
2724
+ /**
2725
+ * Setup navigation interception
2726
+ */
2727
+ setupNavigation() {
2728
+ document.addEventListener("click", (e) => {
2729
+ const target = e.target;
2730
+ const link = target.closest("a");
2731
+ if (!link) return;
2732
+ const href = link.getAttribute("href");
2733
+ if (!href) return;
2734
+ if (href.startsWith("http") || href.startsWith("//") || href.startsWith("mailto:") || href.startsWith("tel:")) {
2735
+ return;
2736
+ }
2737
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey || e.button !== 0) {
2738
+ return;
2739
+ }
2740
+ if (link.target && link.target !== "_self") {
2741
+ return;
2742
+ }
2743
+ if (e.defaultPrevented) {
2744
+ return;
2745
+ }
2746
+ e.preventDefault();
2747
+ this.navigate(href);
2748
+ });
2749
+ }
2750
+ /**
2751
+ * Setup popstate handler for browser back/forward
2752
+ */
2753
+ setupPopState() {
2754
+ window.addEventListener("popstate", () => {
2755
+ this.currentLocation = getInitialLocation();
2756
+ this.updateContent();
2757
+ });
2758
+ }
2759
+ /**
2760
+ * Observe new links in container (called after DOM updates)
2761
+ */
2762
+ observeNewLinks(container) {
2763
+ if (!this.prefetchObserver) return;
2764
+ const links = container.querySelectorAll("a[href]");
2765
+ links.forEach((link) => {
2766
+ const href = link.getAttribute("href");
2767
+ if (href && this.isInternalLink(href)) {
2768
+ const { pathname, search } = normalizePath2(href);
2769
+ const key = this.getCacheKey(pathname, search);
2770
+ if (!this.rscPayloadCache.has(key) && !this.prefetchedLinks.has(key)) {
2771
+ this.prefetchObserver.observe(link);
2772
+ }
2773
+ }
2774
+ });
2775
+ }
2776
+ /**
2777
+ * Setup pre-fetch with Intersection Observer and hover
2778
+ */
2779
+ setupPrefetch() {
2780
+ this.prefetchObserver = new IntersectionObserver(
2781
+ (entries) => {
2782
+ entries.forEach((entry) => {
2783
+ if (entry.isIntersecting) {
2784
+ const link = entry.target;
2785
+ const href = link.getAttribute("href");
2786
+ if (href && this.isInternalLink(href)) {
2787
+ const { pathname, search } = normalizePath2(href);
2788
+ const key = this.getCacheKey(pathname, search);
2789
+ if (this.rscPayloadCache.has(key) || this.prefetchPromises.has(key)) {
2790
+ this.prefetchObserver.unobserve(link);
2791
+ this.prefetchedLinks.add(key);
2792
+ return;
2793
+ }
2794
+ if (this.prefetchedLinks.has(key)) {
2795
+ this.prefetchObserver.unobserve(link);
2796
+ return;
2797
+ }
2798
+ this.prefetchedLinks.add(key);
2799
+ this.prefetchRoute(pathname, search).then(() => {
2800
+ this.prefetchObserver.unobserve(link);
2801
+ });
2802
+ }
2803
+ }
2804
+ });
2805
+ },
2806
+ { rootMargin: "200px" }
2807
+ );
2808
+ const allLinks = document.querySelectorAll("a[href]");
2809
+ allLinks.forEach((link) => {
2810
+ const href = link.getAttribute("href");
2811
+ if (href && this.isInternalLink(href)) {
2812
+ const { pathname, search } = normalizePath2(href);
2813
+ const key = this.getCacheKey(pathname, search);
2814
+ if (!this.rscPayloadCache.has(key) && !this.prefetchedLinks.has(key)) {
2815
+ this.prefetchObserver.observe(link);
2816
+ }
2817
+ }
2818
+ });
2819
+ let hoverTimeout = null;
2820
+ document.addEventListener(
2821
+ "mouseenter",
2822
+ (e) => {
2823
+ const target = e.target;
2824
+ if (!target || !(target instanceof Element)) {
2825
+ return;
2826
+ }
2827
+ const link = target.closest("a");
2828
+ if (link) {
2829
+ const href = link.getAttribute("href");
2830
+ if (href && this.isInternalLink(href)) {
2831
+ const { pathname, search } = normalizePath2(href);
2832
+ const key = this.getCacheKey(pathname, search);
2833
+ if (this.rscPayloadCache.has(key) || this.prefetchedLinks.has(key)) {
2834
+ return;
2835
+ }
2836
+ if (hoverTimeout) {
2837
+ clearTimeout(hoverTimeout);
2838
+ }
2839
+ hoverTimeout = setTimeout(() => {
2840
+ if (!this.rscPayloadCache.has(key) && !this.prefetchedLinks.has(key)) {
2841
+ this.prefetchedLinks.add(key);
2842
+ this.prefetchRoute(pathname, search);
2843
+ }
2844
+ }, 100);
2845
+ }
2846
+ }
2847
+ },
2848
+ true
2849
+ // Capture phase
2850
+ );
2851
+ }
2852
+ /**
2853
+ * Initialize router (call after initial page load)
2854
+ */
2855
+ init() {
2856
+ const self = this;
2857
+ const initIslands = () => {
2858
+ setTimeout(() => {
2859
+ requestAnimationFrame(() => {
2860
+ requestAnimationFrame(() => {
2861
+ bootIslandsNow();
2862
+ activateAsyncComponents(self.appContainer);
2863
+ self.observeNewLinks(self.appContainer);
2864
+ });
2865
+ });
2866
+ }, 0);
2867
+ };
2868
+ if (document.readyState === "loading") {
2869
+ document.addEventListener("DOMContentLoaded", initIslands, { once: true });
2870
+ } else {
2871
+ initIslands();
2872
+ }
2873
+ }
2874
+ /**
2875
+ * Invalidate cache for a specific path (prepared for future revalidatePath implementation)
2876
+ * TODO: Implementar invalidación completa cuando se implemente revalidatePath
2877
+ */
2878
+ revalidatePath(pathname, search = "") {
2879
+ const key = this.getCacheKey(pathname, search);
2880
+ this.invalidatedPaths.add(key);
2881
+ this.rscPayloadCache.delete(key);
2882
+ this.prefetchPromises.delete(key);
2883
+ this.prefetchedLinks.delete(key);
2884
+ }
2885
+ };
2886
+
2887
+ // src/client/bootstrap.tsx
2888
+ async function loadComponent2(route) {
2889
+ try {
2890
+ const module = await route.component();
2891
+ const Component = module.default || module;
2892
+ if (!Component) {
2893
+ throw new Error(ERROR_MESSAGES.COMPONENT_NOT_FOUND(route.path));
2894
+ }
2895
+ return Component;
2896
+ } catch (error) {
2897
+ console.error(ERROR_MESSAGES.FAILED_TO_LOAD_ROUTE_COMPONENT(route.path), error);
2898
+ throw error;
2899
+ }
2900
+ }
2901
+ function bootstrapClient(options) {
2902
+ const { routes } = options;
2903
+ const appContainer = document.getElementById(SERVER.APP_CONTAINER_ID);
2904
+ if (!appContainer) {
2905
+ console.error(ERROR_MESSAGES.APP_CONTAINER_NOT_FOUND);
2906
+ return;
2907
+ }
2908
+ const currentPath = window.location.pathname;
2909
+ const currentRoute = routes.find((route) => matchesRoute(route.path, currentPath));
2910
+ if (currentRoute) {
2911
+ loadComponent2(currentRoute).then(() => {
2912
+ }).catch(() => {
2913
+ });
2914
+ }
2915
+ const router = new FrameworkRouter(routes, appContainer);
2916
+ router.init();
2917
+ }
2918
+
2919
+ // src/build/index.ts
2920
+ import rspackBuild from "@rspack/core";
2921
+ import { join as join9 } from "path";
2922
+ import { existsSync as existsSync10 } from "fs";
2923
+
2924
+ // src/build/manifest.ts
2925
+ import { writeFileSync, mkdirSync, existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
2926
+ import { join as join5, dirname as dirname2, relative as relative2 } from "path";
2927
+ function getServerCompiledPath(sourcePath, appDir) {
2928
+ const relativePath = relative2(appDir, sourcePath);
2929
+ const relativeWithoutExt = relativePath.replace(/\.(tsx?|jsx?)$/, ".js");
2930
+ const serverDistDir = getServerOutDir();
2931
+ return join5(serverDistDir, relativeWithoutExt).replace(/\\/g, "/");
2932
+ }
2933
+ function getClientChunksForRoute(routeIndex, clientManifestPath) {
2934
+ if (!existsSync7(clientManifestPath)) {
2935
+ return null;
2936
+ }
2937
+ try {
2938
+ const manifestContent = readFileSync3(clientManifestPath, "utf-8");
2939
+ const manifest = JSON.parse(manifestContent);
2940
+ const chunkName = `route-${routeIndex}`;
2941
+ const chunk = manifest[chunkName];
2942
+ if (!chunk) {
2943
+ return null;
2944
+ }
2945
+ const js = [];
2946
+ const css = [];
2947
+ if (chunk.file) {
2948
+ js.push(chunk.file);
2949
+ }
2950
+ if (chunk.css && chunk.css.length > 0) {
2951
+ css.push(...chunk.css);
2952
+ }
2953
+ if (chunk.imports && chunk.imports.length > 0) {
2954
+ for (const importName of chunk.imports) {
2955
+ const importChunk = manifest[importName];
2956
+ if (importChunk?.file) {
2957
+ js.push(importChunk.file);
2958
+ }
2959
+ if (importChunk?.css && importChunk.css.length > 0) {
2960
+ css.push(...importChunk.css);
2961
+ }
2962
+ }
2963
+ }
2964
+ return { js, css };
2965
+ } catch (error) {
2966
+ return null;
2967
+ }
2968
+ }
2969
+ function generateRoutesManifest(config, projectDir, appDir) {
2970
+ const manifestDir = join5(getProjectDir(), DIRECTORIES.LOLY);
2971
+ if (!existsSync7(manifestDir)) {
2972
+ mkdirSync(manifestDir, { recursive: true });
2973
+ }
2974
+ const clientManifestPath = getManifestPath();
2975
+ const hasClientManifest = existsSync7(clientManifestPath);
2976
+ const pageRoutes = config.routes.filter((r) => r.type === "page");
2977
+ let routeIndex = 0;
2978
+ const routesData = config.routes.map((route) => {
2979
+ const sourcePath = route.filePath;
2980
+ const serverPath = getServerCompiledPath(sourcePath, appDir);
2981
+ let clientChunks = null;
2982
+ if (route.type === "page" && hasClientManifest) {
2983
+ clientChunks = getClientChunksForRoute(routeIndex, clientManifestPath);
2984
+ routeIndex++;
2985
+ }
2986
+ return {
2987
+ sourcePath,
2988
+ // Original TSX path (for development)
2989
+ serverPath,
2990
+ // Compiled server path (for production)
2991
+ segments: route.segments,
2992
+ type: route.type,
2993
+ isRootLayout: route.isRootLayout,
2994
+ urlPath: route.urlPath,
2995
+ isApiRoute: route.isApiRoute || void 0,
2996
+ httpMethods: route.httpMethods || void 0,
2997
+ clientChunks: clientChunks || void 0
2998
+ // Client chunks for this route
2999
+ };
3000
+ });
3001
+ const rootLayoutData = config.rootLayout ? {
3002
+ sourcePath: config.rootLayout.filePath,
3003
+ serverPath: getServerCompiledPath(
3004
+ config.rootLayout.filePath,
3005
+ appDir
3006
+ ),
3007
+ segments: config.rootLayout.segments,
3008
+ type: config.rootLayout.type,
3009
+ isRootLayout: config.rootLayout.isRootLayout,
3010
+ urlPath: config.rootLayout.urlPath
3011
+ } : void 0;
3012
+ routeIndex = 0;
3013
+ const routeMapData = Array.from(config.routeMap.entries()).map(
3014
+ ([path4, route]) => {
3015
+ const sourcePath = route.filePath;
3016
+ const serverPath = getServerCompiledPath(sourcePath, appDir);
3017
+ let clientChunks = null;
3018
+ if (hasClientManifest) {
3019
+ clientChunks = getClientChunksForRoute(routeIndex, clientManifestPath);
3020
+ routeIndex++;
3021
+ }
3022
+ return {
3023
+ path: path4,
3024
+ sourcePath,
3025
+ serverPath,
3026
+ segments: route.segments,
3027
+ type: route.type,
3028
+ isRootLayout: route.isRootLayout,
3029
+ urlPath: route.urlPath,
3030
+ isApiRoute: route.isApiRoute || void 0,
3031
+ httpMethods: route.httpMethods || void 0,
3032
+ clientChunks: clientChunks || void 0
3033
+ };
3034
+ }
3035
+ );
3036
+ if (config.apiRouteMap) {
3037
+ for (const [path4, route] of config.apiRouteMap.entries()) {
3038
+ const sourcePath = route.filePath;
3039
+ const serverPath = getServerCompiledPath(sourcePath, appDir);
3040
+ routeMapData.push({
3041
+ path: path4,
3042
+ sourcePath,
3043
+ serverPath,
3044
+ segments: route.segments,
3045
+ type: route.type,
3046
+ isRootLayout: route.isRootLayout,
3047
+ urlPath: route.urlPath,
3048
+ isApiRoute: route.isApiRoute || void 0,
3049
+ httpMethods: route.httpMethods || void 0,
3050
+ clientChunks: void 0
3051
+ // API routes don't have client chunks
3052
+ });
3053
+ }
3054
+ }
3055
+ const manifest = {
3056
+ routes: routesData,
3057
+ rootLayout: rootLayoutData,
3058
+ routeMap: routeMapData
3059
+ };
3060
+ const manifestPath = getRoutesManifestPath();
3061
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
3062
+ }
3063
+ function generateBootstrap(projectDir, routes) {
3064
+ const bootstrapDir = join5(getProjectDir(), DIRECTORIES.LOLY);
3065
+ if (!existsSync7(bootstrapDir)) {
3066
+ mkdirSync(bootstrapDir, { recursive: true });
3067
+ }
3068
+ const routeImports = routes.map((route, index) => {
3069
+ const routePath = route.filePath.replace(/\\/g, "/");
3070
+ const projectPath = getProjectDir().replace(/\\/g, "/");
3071
+ let relativePath = routePath.replace(projectPath, "").replace(/^\//, "");
3072
+ relativePath = `../${relativePath}`;
3073
+ return ` {
3074
+ path: "${route.urlPath}",
3075
+ component: async () => {
3076
+ const mod = await import(/* webpackChunkName: "route-${index}" */ "${relativePath}");
3077
+ return mod.default || mod;
3078
+ },
3079
+ }`;
3080
+ }).join(",\n");
3081
+ const bootstrapContent = `import { bootstrapClient } from "loly/client";
3082
+
3083
+ const routes = [
3084
+ ${routeImports}
3085
+ ];
3086
+
3087
+ const notFoundRoute = null;
3088
+ const errorRoute = null;
3089
+
3090
+ try {
3091
+ bootstrapClient({
3092
+ routes,
3093
+ notFoundRoute,
3094
+ errorRoute,
3095
+ });
3096
+ } catch (error) {
3097
+ console.error("[bootstrap] Fatal error during bootstrap:", error);
3098
+ throw error;
3099
+ }
3100
+ `;
3101
+ const bootstrapPath = getBootstrapPath();
3102
+ writeFileSync(bootstrapPath, bootstrapContent);
3103
+ }
3104
+ function readRspackManifest(manifestPath) {
3105
+ const { readFileSync: readFileSync6 } = __require("fs");
3106
+ const content = readFileSync6(manifestPath, "utf-8");
3107
+ return JSON.parse(content);
3108
+ }
3109
+ function writeManifest(manifestPath, manifest) {
3110
+ const { writeFileSync: writeFileSync3 } = __require("fs");
3111
+ const dir = dirname2(manifestPath);
3112
+ if (!existsSync7(dir)) {
3113
+ mkdirSync(dir, { recursive: true });
3114
+ }
3115
+ writeFileSync3(manifestPath, JSON.stringify(manifest, null, 2));
3116
+ }
3117
+ function generateAssetTags2(manifest, entryName, publicPath) {
3118
+ const entry = manifest[entryName];
3119
+ if (!entry) {
3120
+ return { scripts: "", styles: "", preloads: "" };
3121
+ }
3122
+ let scripts = "";
3123
+ let styles = "";
3124
+ let preloads = "";
3125
+ if (entry.file) {
3126
+ const scriptPath = entry.file.startsWith("/") ? entry.file : `${publicPath}${entry.file}`;
3127
+ preloads = `<link rel="modulepreload" href="${scriptPath}" crossorigin>`;
3128
+ scripts = `<script type="module" src="${scriptPath}"></script>`;
3129
+ }
3130
+ if (entry.css && entry.css.length > 0) {
3131
+ styles = entry.css.map((css) => {
3132
+ const cssPath = css.startsWith("/") ? css : `${publicPath}${css}`;
3133
+ return `<link rel="stylesheet" href="${cssPath}">`;
3134
+ }).join("");
3135
+ }
3136
+ if (entry.imports && entry.imports.length > 0) {
3137
+ for (const importName of entry.imports) {
3138
+ const importEntry = manifest[importName];
3139
+ if (importEntry?.file) {
3140
+ const scriptPath = importEntry.file.startsWith("/") ? importEntry.file : `${publicPath}${importEntry.file}`;
3141
+ scripts += `
3142
+ <script type="module" src="${scriptPath}"></script>`;
3143
+ }
3144
+ if (importEntry?.css && importEntry.css.length > 0) {
3145
+ for (const css of importEntry.css) {
3146
+ const cssPath = css.startsWith("/") ? css : `${publicPath}${css}`;
3147
+ styles += `
3148
+ <link rel="stylesheet" href="${cssPath}">`;
3149
+ }
3150
+ }
3151
+ }
3152
+ }
3153
+ return { scripts, styles, preloads };
3154
+ }
3155
+
3156
+ // src/build/client.ts
3157
+ import { join as join6, normalize as normalize2, relative as relative3 } from "path";
3158
+
3159
+ // src/build/server-only.ts
3160
+ import { readFileSync as readFileSync4 } from "fs";
3161
+ import { glob as glob2 } from "glob";
3162
+ import { normalize } from "path";
3163
+ function hasUseServerDirective(filePath) {
3164
+ try {
3165
+ const content = readFileSync4(filePath, "utf-8");
3166
+ const trimmed = content.trim();
3167
+ return trimmed.startsWith('"use server"') || trimmed.startsWith("'use server'") || trimmed.startsWith('"use server";') || trimmed.startsWith("'use server';");
3168
+ } catch {
3169
+ return false;
3170
+ }
3171
+ }
3172
+ async function scanServerOnlyFiles(srcDir) {
3173
+ const serverOnlyFiles = /* @__PURE__ */ new Set();
3174
+ if (!srcDir) {
3175
+ return serverOnlyFiles;
3176
+ }
3177
+ try {
3178
+ const patterns = ["**/*.{ts,tsx,js,jsx}"];
3179
+ const files = await glob2(patterns, {
3180
+ cwd: srcDir,
3181
+ absolute: true,
3182
+ ignore: ["**/node_modules/**", "**/.loly/**", "**/dist/**", "**/.next/**"]
3183
+ });
3184
+ for (const file of files) {
3185
+ if (hasUseServerDirective(file)) {
3186
+ const normalizedPath = normalize(file);
3187
+ serverOnlyFiles.add(normalizedPath);
3188
+ }
3189
+ }
3190
+ } catch (error) {
3191
+ console.warn("[loly-core] Failed to scan for server-only files:", error);
3192
+ }
3193
+ return serverOnlyFiles;
3194
+ }
3195
+
3196
+ // src/build/client.ts
3197
+ function generateServerOnlyStubCode() {
3198
+ return `
3199
+ const SERVER_ONLY_ERROR = "This module is marked with 'use server' and can only be imported in server components.";
3200
+
3201
+ function createErrorFunction(propName) {
3202
+ const errorFn = function (...args) {
3203
+ throw new Error(SERVER_ONLY_ERROR + " Attempted to call: " + propName);
3204
+ };
3205
+ errorFn.toString = () => "[ServerOnlyStub:" + propName + "]";
3206
+ errorFn[Symbol.toStringTag] = "Function";
3207
+ errorFn.then = () => Promise.reject(new Error(SERVER_ONLY_ERROR + " Attempted to await: " + propName));
3208
+ errorFn.catch = () => Promise.reject(new Error(SERVER_ONLY_ERROR + " Attempted to catch: " + propName));
3209
+ return errorFn;
3210
+ }
3211
+
3212
+ function createServerOnlyProxy() {
3213
+ return new Proxy({}, {
3214
+ get(_target, prop) {
3215
+ return createErrorFunction(String(prop));
3216
+ },
3217
+ set() {
3218
+ throw new Error(SERVER_ONLY_ERROR);
3219
+ },
3220
+ has() {
3221
+ return true;
3222
+ },
3223
+ ownKeys() {
3224
+ return [];
3225
+ },
3226
+ getOwnPropertyDescriptor(_target, prop) {
3227
+ return {
3228
+ enumerable: true,
3229
+ configurable: true,
3230
+ get: () => createErrorFunction(String(prop)),
3231
+ };
3232
+ },
3233
+ });
3234
+ }
3235
+
3236
+ const serverOnlyStub = createServerOnlyProxy();
3237
+ export default serverOnlyStub;
3238
+ `;
3239
+ }
3240
+ function matchServerOnlyFile(request, serverOnlyFiles, srcDir) {
3241
+ const normalizedSrcDir = normalize2(srcDir).replace(/\\/g, "/");
3242
+ const normalizedRequest = request.replace(/\\/g, "/");
3243
+ for (const serverOnlyPath of serverOnlyFiles) {
3244
+ const normalizedPath = normalize2(serverOnlyPath).replace(/\\/g, "/");
3245
+ if (normalizedRequest === normalizedPath) {
3246
+ return serverOnlyPath;
3247
+ }
3248
+ const relativeFromSrc = normalizedPath.replace(normalizedSrcDir + "/", "").replace(/^\/+/, "");
3249
+ const relativeWithoutExt = relativeFromSrc.replace(/\.(ts|tsx|js|jsx)$/, "");
3250
+ if (normalizedRequest === relativeFromSrc || normalizedRequest === relativeWithoutExt) {
3251
+ return serverOnlyPath;
3252
+ }
3253
+ if (normalizedRequest.endsWith("/" + relativeFromSrc) || normalizedRequest.endsWith("/" + relativeWithoutExt)) {
3254
+ return serverOnlyPath;
3255
+ }
3256
+ try {
3257
+ const relativePath = relative3(srcDir, serverOnlyPath).replace(/\\/g, "/");
3258
+ const relativePathWithoutExt = relativePath.replace(/\.(ts|tsx|js|jsx)$/, "");
3259
+ if (normalizedRequest === relativePath || normalizedRequest === relativePathWithoutExt) {
3260
+ return serverOnlyPath;
3261
+ }
3262
+ if (normalizedRequest.endsWith("/" + relativePath) || normalizedRequest.endsWith("/" + relativePathWithoutExt)) {
3263
+ return serverOnlyPath;
3264
+ }
3265
+ const requestParts = normalizedRequest.split("/").filter((p) => p && p !== ".");
3266
+ const relativeParts = relativePathWithoutExt.split("/").filter((p) => p && p !== ".");
3267
+ if (relativeParts.length > 0 && requestParts.length >= relativeParts.length) {
3268
+ const requestEnd = requestParts.slice(-relativeParts.length).join("/");
3269
+ const relativeEnd = relativeParts.join("/");
3270
+ if (requestEnd === relativeEnd) {
3271
+ return serverOnlyPath;
3272
+ }
3273
+ }
3274
+ } catch {
3275
+ }
3276
+ const filename = normalizedPath.split("/").pop();
3277
+ if (filename) {
3278
+ const filenameWithoutExt = filename.replace(/\.(ts|tsx|js|jsx)$/, "");
3279
+ if (normalizedRequest === filename || normalizedRequest === filenameWithoutExt) {
3280
+ return serverOnlyPath;
3281
+ }
3282
+ if (normalizedRequest.endsWith("/" + filename) || normalizedRequest.endsWith("/" + filenameWithoutExt)) {
3283
+ return serverOnlyPath;
3284
+ }
3285
+ }
3286
+ try {
3287
+ if (!normalizedRequest.startsWith("/") && !normalizedRequest.startsWith(".")) {
3288
+ if (normalizedRequest === relativeWithoutExt || normalizedRequest === relativeFromSrc) {
3289
+ return serverOnlyPath;
3290
+ }
3291
+ }
3292
+ } catch {
3293
+ }
3294
+ }
3295
+ return null;
3296
+ }
3297
+ async function createClientConfig(options) {
3298
+ const { mode = "production" } = options;
3299
+ const bootstrapEntry = getBootstrapPath();
3300
+ const srcDir = getSrcDir();
3301
+ const serverOnlyFiles = await scanServerOnlyFiles(srcDir);
3302
+ const stubCode = generateServerOnlyStubCode();
3303
+ const entries = {
3304
+ main: bootstrapEntry
3305
+ };
3306
+ return {
3307
+ mode,
3308
+ entry: entries,
3309
+ output: {
3310
+ path: getClientDir(),
3311
+ filename: "[name].[contenthash].js",
3312
+ chunkFilename: "[name].[contenthash].js",
3313
+ clean: true,
3314
+ publicPath: "/"
3315
+ },
3316
+ resolve: {
3317
+ extensions: [".ts", ".tsx", ".js", ".jsx"],
3318
+ alias: {
3319
+ "loly": join6(
3320
+ getProjectDir(),
3321
+ "node_modules",
3322
+ "loly",
3323
+ "dist",
3324
+ "client.js"
3325
+ )
3326
+ }
3327
+ },
3328
+ externals: [
3329
+ "express",
3330
+ "chokidar",
3331
+ "glob",
3332
+ "@rspack/core",
3333
+ "@rspack/cli",
3334
+ "tsx",
3335
+ ({ request, contextInfo }) => {
3336
+ if (!request) return false;
3337
+ if (request.startsWith("node:")) return request;
3338
+ if (NODE_BUILTINS.includes(request)) return `node:${request}`;
3339
+ return false;
3340
+ }
3341
+ ],
3342
+ externalsType: "module",
3343
+ module: {
3344
+ rules: [
3345
+ {
3346
+ test: /\.tsx?$/,
3347
+ use: [
3348
+ {
3349
+ loader: "builtin:swc-loader",
3350
+ options: {
3351
+ jsc: {
3352
+ parser: {
3353
+ syntax: "typescript",
3354
+ tsx: true,
3355
+ decorators: false,
3356
+ dynamicImport: true
3357
+ },
3358
+ transform: {
3359
+ react: {
3360
+ runtime: "automatic",
3361
+ importSource: "loly-jsx",
3362
+ throwIfNamespace: false
3363
+ }
3364
+ },
3365
+ target: "es2020"
3366
+ },
3367
+ module: {
3368
+ type: "es6"
3369
+ }
3370
+ }
3371
+ }
3372
+ ]
3373
+ },
3374
+ {
3375
+ test: /\.css$/,
3376
+ type: "css"
3377
+ }
3378
+ ]
3379
+ },
3380
+ optimization: {
3381
+ minimize: mode === "production",
3382
+ splitChunks: {
3383
+ chunks: "all"
3384
+ },
3385
+ usedExports: true,
3386
+ sideEffects: false,
3387
+ // More aggressive tree shaking - assume no side effects
3388
+ providedExports: true,
3389
+ concatenateModules: true,
3390
+ // Better for tree shaking
3391
+ removeEmptyChunks: true,
3392
+ mergeDuplicateChunks: true
3393
+ },
3394
+ plugins: [
3395
+ {
3396
+ name: "server-only-plugin",
3397
+ apply(compiler) {
3398
+ compiler.hooks.normalModuleFactory.tap("server-only-plugin", (factory) => {
3399
+ factory.hooks.beforeResolve.tap("server-only-plugin", (data) => {
3400
+ if (!data.request) return;
3401
+ const request = data.request;
3402
+ if (request.startsWith("data:")) {
3403
+ return;
3404
+ }
3405
+ const matchedFile = matchServerOnlyFile(request, serverOnlyFiles, srcDir);
3406
+ if (matchedFile) {
3407
+ const encodedStub = Buffer.from(stubCode.trim()).toString("base64");
3408
+ data.request = `data:text/javascript;base64,${encodedStub}`;
3409
+ }
3410
+ });
3411
+ });
3412
+ }
3413
+ },
3414
+ {
3415
+ name: "manifest-plugin",
3416
+ apply(compiler) {
3417
+ compiler.hooks.emit.tap("manifest-plugin", (compilation) => {
3418
+ const manifest = {};
3419
+ const entrypoints = compilation.entrypoints || /* @__PURE__ */ new Map();
3420
+ const entryChunkNames = /* @__PURE__ */ new Set();
3421
+ for (const [name, entrypoint] of entrypoints) {
3422
+ entryChunkNames.add(name);
3423
+ const chunks2 = entrypoint.chunks || [];
3424
+ for (const chunk of chunks2) {
3425
+ const chunkName = chunk.name || chunk.id || "unknown";
3426
+ entryChunkNames.add(chunkName);
3427
+ }
3428
+ }
3429
+ const chunks = compilation.chunks || [];
3430
+ for (const chunk of chunks) {
3431
+ const name = chunk.name || chunk.id || "unknown";
3432
+ const files = [];
3433
+ const cssFiles = [];
3434
+ const chunkFiles = chunk.files || [];
3435
+ for (const file of chunkFiles) {
3436
+ if (file.endsWith(".css")) {
3437
+ cssFiles.push(file);
3438
+ } else {
3439
+ files.push(file);
3440
+ }
3441
+ }
3442
+ const isEntry = entryChunkNames.has(name) || chunk.hasEntryModule && chunk.hasEntryModule() || name === "main";
3443
+ manifest[name] = {
3444
+ file: files[0] || "",
3445
+ isEntry,
3446
+ imports: [],
3447
+ css: cssFiles.length > 0 ? cssFiles : void 0
3448
+ };
3449
+ }
3450
+ const manifestJson = JSON.stringify(manifest, null, 2);
3451
+ compilation.emitAsset("manifest.json", {
3452
+ source: () => manifestJson,
3453
+ size: () => Buffer.byteLength(manifestJson, "utf8")
3454
+ });
3455
+ });
3456
+ }
3457
+ }
3458
+ ],
3459
+ devtool: mode === "production" ? false : "source-map"
3460
+ };
3461
+ }
3462
+
3463
+ // src/build/server.ts
3464
+ import { join as join7 } from "path";
3465
+ import { existsSync as existsSync8, readdirSync } from "fs";
3466
+ function collectAppSources(appDir) {
3467
+ const entries = [];
3468
+ function walk(dir) {
3469
+ if (!existsSync8(dir)) return;
3470
+ const items = readdirSync(dir, { withFileTypes: true });
3471
+ for (const item of items) {
3472
+ const full = join7(dir, item.name);
3473
+ if (item.isDirectory()) {
3474
+ if (item.name === "node_modules" || item.name.startsWith(".")) {
3475
+ continue;
3476
+ }
3477
+ walk(full);
3478
+ continue;
3479
+ }
3480
+ if (item.isFile()) {
3481
+ const isRouteFile = ROUTE_FILE_NAMES.some(
3482
+ (routeFile) => item.name === routeFile
3483
+ );
3484
+ if (isRouteFile && (full.endsWith(".ts") || full.endsWith(".tsx"))) {
3485
+ if (full.endsWith(".d.ts")) continue;
3486
+ entries.push(full);
3487
+ }
3488
+ }
3489
+ }
3490
+ }
3491
+ walk(appDir);
3492
+ return entries;
3493
+ }
3494
+ async function buildServerFiles(options) {
3495
+ const { mode = "production" } = options;
3496
+ const appDir = getAppDir();
3497
+ const serverOutDir = getServerOutDir();
3498
+ if (existsSync8(serverOutDir)) {
3499
+ const { rmSync } = await import("fs");
3500
+ rmSync(serverOutDir, { recursive: true, force: true });
3501
+ }
3502
+ const entryPoints = collectAppSources(appDir);
3503
+ if (entryPoints.length === 0) {
3504
+ return;
3505
+ }
3506
+ try {
3507
+ const { build } = await import("esbuild");
3508
+ await build({
3509
+ entryPoints,
3510
+ outdir: serverOutDir,
3511
+ outbase: appDir,
3512
+ // Preserve directory structure
3513
+ format: "esm",
3514
+ platform: "node",
3515
+ target: "es2020",
3516
+ bundle: true,
3517
+ // Bundle to resolve relative imports
3518
+ packages: "external",
3519
+ // Externalize node_modules packages
3520
+ external: [
3521
+ // Explicitly externalize loly-jsx to avoid bundling it
3522
+ "loly-jsx",
3523
+ "loly-jsx/*",
3524
+ "loly-jsx/jsx-runtime",
3525
+ "loly-jsx/render/*",
3526
+ "loly-jsx/router"
3527
+ ],
3528
+ sourcemap: true,
3529
+ jsx: "automatic",
3530
+ jsxImportSource: "loly-jsx",
3531
+ logLevel: mode === "production" ? "warning" : "info"
3532
+ });
3533
+ } catch (error) {
3534
+ console.error(`[loly-core] Failed to build server files:`, error);
3535
+ throw error;
3536
+ }
3537
+ }
3538
+
3539
+ // src/build/css-processor.ts
3540
+ import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, copyFileSync } from "fs";
3541
+ import { join as join8, dirname as dirname3 } from "path";
3542
+ import { pathToFileURL } from "url";
3543
+ import { createRequire } from "module";
3544
+ async function processCssWithPostCSS(options) {
3545
+ const { inputPath, outputPath, projectDir } = options;
3546
+ let postcss;
3547
+ try {
3548
+ const postcssModule = await import("postcss");
3549
+ postcss = postcssModule.default || postcssModule;
3550
+ } catch {
3551
+ mkdirSync2(dirname3(outputPath), { recursive: true });
3552
+ copyFileSync(inputPath, outputPath);
3553
+ return;
3554
+ }
3555
+ const configPaths = [
3556
+ join8(projectDir, "postcss.config.js"),
3557
+ join8(projectDir, "postcss.config.mjs"),
3558
+ join8(projectDir, "postcss.config.cjs")
3559
+ ];
3560
+ const configPath = configPaths.find((p) => existsSync9(p));
3561
+ if (!configPath) {
3562
+ mkdirSync2(dirname3(outputPath), { recursive: true });
3563
+ copyFileSync(inputPath, outputPath);
3564
+ return;
3565
+ }
3566
+ const configUrl = pathToFileURL(configPath).href;
3567
+ const configModule = await import(configUrl);
3568
+ const config = configModule.default || configModule;
3569
+ const css = readFileSync5(inputPath, "utf-8");
3570
+ let plugins = config.plugins || [];
3571
+ const projectRequire = createRequire(join8(projectDir, "package.json"));
3572
+ if (plugins && typeof plugins === "object" && !Array.isArray(plugins)) {
3573
+ const pluginArray = [];
3574
+ for (const [name, options2] of Object.entries(plugins)) {
3575
+ try {
3576
+ const pluginPath = projectRequire.resolve(name);
3577
+ const pluginUrl = pathToFileURL(pluginPath).href;
3578
+ const pluginModule = await import(pluginUrl);
3579
+ const plugin = pluginModule.default || pluginModule;
3580
+ const pluginOptions = options2 && typeof options2 === "object" && Object.keys(options2).length > 0 ? options2 : void 0;
3581
+ pluginArray.push(pluginOptions ? plugin(pluginOptions) : plugin());
3582
+ } catch (err) {
3583
+ console.warn(`[loly-core] Failed to load PostCSS plugin "${name}":`, err);
3584
+ throw err;
3585
+ }
3586
+ }
3587
+ plugins = pluginArray;
3588
+ }
3589
+ const result = await postcss(plugins).process(css, {
3590
+ from: inputPath,
3591
+ to: outputPath
3592
+ });
3593
+ mkdirSync2(dirname3(outputPath), { recursive: true });
3594
+ writeFileSync2(outputPath, result.css);
3595
+ if (result.map) {
3596
+ writeFileSync2(outputPath + ".map", result.map.toString());
3597
+ }
3598
+ }
3599
+
3600
+ // src/build/index.ts
3601
+ async function buildProject(options) {
3602
+ const {
3603
+ projectDir,
3604
+ outDir = join9(projectDir, DIRECTORIES.LOLY, DIRECTORIES.DIST)
3605
+ } = options;
3606
+ const appDir = getAppDirPath(projectDir);
3607
+ const srcDir = join9(projectDir, DIRECTORIES.SRC);
3608
+ if (!existsSync10(appDir)) {
3609
+ throw new Error(ERROR_MESSAGES.APP_DIR_NOT_FOUND(projectDir));
3610
+ }
3611
+ setContext({
3612
+ projectDir,
3613
+ appDir,
3614
+ srcDir,
3615
+ outDir,
3616
+ buildMode: options.mode || "production",
3617
+ isDev: options.mode === "development"
3618
+ });
3619
+ const routeConfig2 = await scanRoutes(getAppDir());
3620
+ setRouteConfig(routeConfig2);
3621
+ const pageRoutes = routeConfig2.routes.filter((r) => r.type === "page");
3622
+ generateBootstrap(getProjectDir(), pageRoutes);
3623
+ const clientConfig = await createClientConfig(options);
3624
+ try {
3625
+ await new Promise((resolve5, reject) => {
3626
+ const compiler = rspackBuild(clientConfig);
3627
+ compiler.run((err, stats) => {
3628
+ if (err) {
3629
+ console.error("[loly-core] Client build error:", err);
3630
+ reject(err);
3631
+ return;
3632
+ }
3633
+ if (stats && stats.hasErrors()) {
3634
+ console.error("[loly-core] Client build has errors");
3635
+ console.error(stats.compilation.errors);
3636
+ reject(new Error(ERROR_MESSAGES.CLIENT_BUILD_FAILED));
3637
+ return;
3638
+ }
3639
+ resolve5();
3640
+ });
3641
+ });
3642
+ } catch (error) {
3643
+ console.error(ERROR_MESSAGES.CLIENT_BUILD_FAILED, error);
3644
+ throw error;
3645
+ }
3646
+ 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;
3647
+ const clientOutDir = getClientDir();
3648
+ if (globalsCssPath && existsSync10(globalsCssPath)) {
3649
+ const destCssPath = join9(clientOutDir, FILE_NAMES.GLOBALS_CSS);
3650
+ await processCssWithPostCSS({
3651
+ inputPath: globalsCssPath,
3652
+ outputPath: destCssPath,
3653
+ projectDir: getProjectDir()
3654
+ });
3655
+ }
3656
+ try {
3657
+ await buildServerFiles(options);
3658
+ } catch (error) {
3659
+ console.error(ERROR_MESSAGES.SERVER_BUILD_FAILED, error);
3660
+ throw error;
3661
+ }
3662
+ generateRoutesManifest(routeConfig2, getProjectDir(), getAppDir());
3663
+ }
3664
+
3665
+ // src/client/components/Image/index.tsx
3666
+ import { jsx, jsxs } from "loly-jsx/jsx-runtime";
3667
+ function getOptimizedImageUrl(src, width, height, quality, format) {
3668
+ const params = new URLSearchParams();
3669
+ params.set("src", src);
3670
+ if (width) params.set("w", width.toString());
3671
+ if (height) params.set("h", height.toString());
3672
+ if (quality) params.set("q", quality.toString());
3673
+ if (format && format !== "auto") params.set("format", format);
3674
+ return `${SERVER.IMAGE_ENDPOINT}?${params.toString()}`;
3675
+ }
3676
+ function generateSrcSet(src, sizes, height, quality, format) {
3677
+ return sizes.map((size) => {
3678
+ const url = getOptimizedImageUrl(src, size, height, quality, format);
3679
+ return `${url} ${size}w`;
3680
+ }).join(", ");
3681
+ }
3682
+ function Image({
3683
+ src,
3684
+ alt,
3685
+ width,
3686
+ height,
3687
+ priority = false,
3688
+ fill = false,
3689
+ sizes,
3690
+ placeholder,
3691
+ blurDataURL,
3692
+ quality,
3693
+ format,
3694
+ className,
3695
+ deviceSizes,
3696
+ imageSizes,
3697
+ style,
3698
+ ...rest
3699
+ }) {
3700
+ const defaultDeviceSizes = deviceSizes || [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
3701
+ const defaultImageSizes = imageSizes || [16, 32, 48, 64, 96, 128, 256, 384];
3702
+ if (!fill && (!width || !height)) {
3703
+ if (typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
3704
+ console.warn(
3705
+ "[Image] width and height are required when fill is false. This helps prevent layout shift (CLS)."
3706
+ );
3707
+ }
3708
+ }
3709
+ const optimizedSrc = getOptimizedImageUrl(src, width, height, quality, format);
3710
+ const srcSetSizes = width ? defaultDeviceSizes.filter((s) => s <= width * 2) : defaultImageSizes;
3711
+ const srcSet = srcSetSizes.length > 0 ? generateSrcSet(src, srcSetSizes, height, quality, format) : void 0;
3712
+ const imageStyle = typeof style === "string" ? {} : { ...style || {} };
3713
+ if (fill) {
3714
+ imageStyle.position = "absolute";
3715
+ imageStyle.height = "100%";
3716
+ imageStyle.width = "100%";
3717
+ imageStyle.objectFit = "cover";
3718
+ imageStyle.objectPosition = "center";
3719
+ imageStyle.top = 0;
3720
+ imageStyle.left = 0;
3721
+ } else {
3722
+ if (width) imageStyle.width = width;
3723
+ if (height) imageStyle.height = height;
3724
+ }
3725
+ if (!fill && width && height) {
3726
+ const aspectRatio = height / width * 100;
3727
+ const containerStyle = {
3728
+ display: "block",
3729
+ position: "relative",
3730
+ width,
3731
+ maxWidth: "100%"
3732
+ };
3733
+ const spacerStyle = {
3734
+ display: "block",
3735
+ paddingBottom: `${aspectRatio}%`
3736
+ };
3737
+ return /* @__PURE__ */ jsxs("span", { style: containerStyle, className, children: [
3738
+ /* @__PURE__ */ jsx("span", { style: spacerStyle }),
3739
+ placeholder === "blur" && blurDataURL && /* @__PURE__ */ jsx(
3740
+ "img",
3741
+ {
3742
+ src: blurDataURL,
3743
+ alt: "",
3744
+ "aria-hidden": "true",
3745
+ style: {
3746
+ position: "absolute",
3747
+ top: 0,
3748
+ left: 0,
3749
+ width: "100%",
3750
+ height: "100%",
3751
+ objectFit: "cover",
3752
+ filter: "blur(20px)",
3753
+ transform: "scale(1.1)"
3754
+ }
3755
+ }
3756
+ ),
3757
+ /* @__PURE__ */ jsx(
3758
+ "img",
3759
+ {
3760
+ src: optimizedSrc,
3761
+ alt,
3762
+ width,
3763
+ height,
3764
+ srcSet,
3765
+ sizes,
3766
+ loading: priority ? "eager" : "lazy",
3767
+ decoding: "async",
3768
+ style: {
3769
+ ...imageStyle,
3770
+ position: "absolute",
3771
+ top: 0,
3772
+ left: 0,
3773
+ height: "100%",
3774
+ width: "100%"
3775
+ },
3776
+ ...rest
3777
+ }
3778
+ )
3779
+ ] });
3780
+ }
3781
+ const finalStyle = typeof style === "string" ? style : imageStyle;
3782
+ return /* @__PURE__ */ jsx(
3783
+ "img",
3784
+ {
3785
+ src: optimizedSrc,
3786
+ alt,
3787
+ width,
3788
+ height,
3789
+ srcSet,
3790
+ sizes,
3791
+ loading: priority ? "eager" : "lazy",
3792
+ decoding: "async",
3793
+ className,
3794
+ style: finalStyle,
3795
+ ...rest
3796
+ }
3797
+ );
3798
+ }
3799
+ export {
3800
+ BaseServer,
3801
+ DevServer,
3802
+ Image,
3803
+ ProdServer,
3804
+ RSCStrategy,
3805
+ RenderingStrategyFactory,
3806
+ RouteComponentFactory,
3807
+ SSRStrategy,
3808
+ bootstrapClient,
3809
+ buildProject,
3810
+ extractParams,
3811
+ generateAssetTags2 as generateAssetTags,
3812
+ generateBootstrap,
3813
+ generatePublicEnvScript,
3814
+ generateRoutesManifest,
3815
+ getEnv,
3816
+ getPublicEnv,
3817
+ matchRoute,
3818
+ readRspackManifest,
3819
+ renderPageContent,
3820
+ renderPageStream,
3821
+ scanRoutes,
3822
+ startDevServer,
3823
+ startProdServer,
3824
+ writeManifest
3825
+ };
3826
+ //# sourceMappingURL=index.js.map