@constela/start 1.1.0 → 1.2.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.
@@ -0,0 +1,2553 @@
1
+ import {
2
+ generateHydrationScript,
3
+ renderPage,
4
+ wrapHtml
5
+ } from "./chunk-PUTC5BCP.js";
6
+
7
+ // src/router/file-router.ts
8
+ import fg from "fast-glob";
9
+ import { existsSync, statSync } from "fs";
10
+ import { join } from "path";
11
+ function filePathToPattern(filePath, _routesDir) {
12
+ let normalized = filePath.replace(/\\/g, "/");
13
+ normalized = normalized.replace(/\.(ts|tsx|js|jsx|json)$/, "");
14
+ const segments = normalized.split("/");
15
+ const processedSegments = segments.map((segment) => {
16
+ if (segment.startsWith("[...") && segment.endsWith("]")) {
17
+ return "*";
18
+ }
19
+ if (segment.startsWith("[") && segment.endsWith("]")) {
20
+ const paramName = segment.slice(1, -1);
21
+ return `:${paramName}`;
22
+ }
23
+ return segment;
24
+ });
25
+ if (processedSegments.at(-1) === "index") {
26
+ processedSegments.pop();
27
+ }
28
+ const path = "/" + processedSegments.join("/");
29
+ if (path === "/") {
30
+ return "/";
31
+ }
32
+ return path.endsWith("/") ? path.slice(0, -1) : path;
33
+ }
34
+ function extractParams(filePath) {
35
+ const params = [];
36
+ const normalized = filePath.replace(/\\/g, "/");
37
+ const regex = /\[(?:\.\.\.)?([^\]]+)\]/g;
38
+ let match;
39
+ while ((match = regex.exec(normalized)) !== null) {
40
+ const paramName = match[1];
41
+ if (paramName !== void 0) {
42
+ params.push(paramName);
43
+ }
44
+ }
45
+ return params;
46
+ }
47
+ function determineRouteType(filePath) {
48
+ const normalized = filePath.replace(/\\/g, "/");
49
+ const fileName = normalized.split("/").pop() ?? "";
50
+ if (fileName.startsWith("_middleware.")) {
51
+ return "middleware";
52
+ }
53
+ if (normalized.startsWith("api/") || normalized.includes("/api/")) {
54
+ return "api";
55
+ }
56
+ return "page";
57
+ }
58
+ function shouldIncludeRoute(filePath) {
59
+ const normalized = filePath.replace(/\\/g, "/");
60
+ const fileName = normalized.split("/").pop() ?? "";
61
+ if (fileName.endsWith(".d.ts")) {
62
+ return false;
63
+ }
64
+ if (fileName.startsWith("_") && !fileName.startsWith("_middleware.")) {
65
+ return false;
66
+ }
67
+ return true;
68
+ }
69
+ async function scanRoutes(routesDir) {
70
+ if (!existsSync(routesDir)) {
71
+ throw new Error(`Routes directory does not exist: ${routesDir}`);
72
+ }
73
+ const stats = statSync(routesDir);
74
+ if (!stats.isDirectory()) {
75
+ throw new Error(`Routes path is not a directory: ${routesDir}`);
76
+ }
77
+ const files = await fg("**/*.{ts,tsx,js,jsx,json}", {
78
+ cwd: routesDir,
79
+ ignore: ["node_modules/**", "**/*.d.ts"],
80
+ onlyFiles: true,
81
+ followSymbolicLinks: false
82
+ });
83
+ const routes = [];
84
+ for (const filePath of files) {
85
+ if (!shouldIncludeRoute(filePath)) {
86
+ continue;
87
+ }
88
+ const route = {
89
+ file: join(routesDir, filePath),
90
+ pattern: filePathToPattern(filePath, routesDir),
91
+ type: determineRouteType(filePath),
92
+ params: extractParams(filePath)
93
+ };
94
+ routes.push(route);
95
+ }
96
+ routes.sort((a, b) => {
97
+ return compareRoutes(a, b);
98
+ });
99
+ return routes;
100
+ }
101
+ function compareRoutes(a, b) {
102
+ if (a.type === "middleware" && b.type !== "middleware") return -1;
103
+ if (b.type === "middleware" && a.type !== "middleware") return 1;
104
+ const segmentsA = a.pattern.split("/").filter(Boolean);
105
+ const segmentsB = b.pattern.split("/").filter(Boolean);
106
+ const minLen = Math.min(segmentsA.length, segmentsB.length);
107
+ for (let i = 0; i < minLen; i++) {
108
+ const segA = segmentsA[i] ?? "";
109
+ const segB = segmentsB[i] ?? "";
110
+ const typeA = getSegmentType(segA);
111
+ const typeB = getSegmentType(segB);
112
+ if (typeA !== typeB) {
113
+ return typeA - typeB;
114
+ }
115
+ if (typeA === 0 && segA !== segB) {
116
+ return segA.localeCompare(segB);
117
+ }
118
+ }
119
+ if (segmentsA.length !== segmentsB.length) {
120
+ return segmentsA.length - segmentsB.length;
121
+ }
122
+ return a.pattern.localeCompare(b.pattern);
123
+ }
124
+ function getSegmentType(segment) {
125
+ if (segment === "*") return 2;
126
+ if (segment.startsWith(":")) return 1;
127
+ return 0;
128
+ }
129
+
130
+ // src/static/index.ts
131
+ import { extname, join as join2, normalize, resolve } from "path";
132
+ import { existsSync as existsSync2, statSync as statSync2 } from "fs";
133
+ var MIME_TYPES = {
134
+ // Images
135
+ ".ico": "image/x-icon",
136
+ ".png": "image/png",
137
+ ".jpg": "image/jpeg",
138
+ ".jpeg": "image/jpeg",
139
+ ".gif": "image/gif",
140
+ ".svg": "image/svg+xml",
141
+ ".webp": "image/webp",
142
+ // Fonts
143
+ ".woff": "font/woff",
144
+ ".woff2": "font/woff2",
145
+ ".ttf": "font/ttf",
146
+ ".otf": "font/otf",
147
+ ".eot": "application/vnd.ms-fontobject",
148
+ // Web assets
149
+ ".css": "text/css",
150
+ ".js": "text/javascript",
151
+ ".json": "application/json",
152
+ ".txt": "text/plain",
153
+ ".html": "text/html",
154
+ ".xml": "application/xml",
155
+ // Other
156
+ ".pdf": "application/pdf",
157
+ ".mp3": "audio/mpeg",
158
+ ".mp4": "video/mp4",
159
+ ".webm": "video/webm",
160
+ ".map": "application/json"
161
+ };
162
+ var DEFAULT_MIME_TYPE = "application/octet-stream";
163
+ function isPathSafe(pathname) {
164
+ if (!pathname) {
165
+ return false;
166
+ }
167
+ if (!pathname.startsWith("/")) {
168
+ return false;
169
+ }
170
+ if (pathname.includes("\\")) {
171
+ return false;
172
+ }
173
+ if (pathname.includes("//")) {
174
+ return false;
175
+ }
176
+ if (pathname.includes("\0") || pathname.includes("%00")) {
177
+ return false;
178
+ }
179
+ let decoded;
180
+ try {
181
+ decoded = pathname;
182
+ let prevDecoded = "";
183
+ while (decoded !== prevDecoded) {
184
+ prevDecoded = decoded;
185
+ decoded = decodeURIComponent(decoded);
186
+ }
187
+ } catch {
188
+ return false;
189
+ }
190
+ if (decoded.includes("\0")) {
191
+ return false;
192
+ }
193
+ if (decoded.includes("..")) {
194
+ return false;
195
+ }
196
+ const segments = decoded.split("/").filter(Boolean);
197
+ for (const segment of segments) {
198
+ if (segment.startsWith(".")) {
199
+ return false;
200
+ }
201
+ }
202
+ return true;
203
+ }
204
+ function getMimeType(filePath) {
205
+ const ext = extname(filePath).toLowerCase();
206
+ return MIME_TYPES[ext] ?? DEFAULT_MIME_TYPE;
207
+ }
208
+ function hasObviousAttackPattern(pathname) {
209
+ if (!pathname) {
210
+ return true;
211
+ }
212
+ if (!pathname.startsWith("/")) {
213
+ return true;
214
+ }
215
+ if (pathname.includes("\\")) {
216
+ return true;
217
+ }
218
+ if (pathname.includes("//")) {
219
+ return true;
220
+ }
221
+ if (pathname.includes("\0") || pathname.includes("%00")) {
222
+ return true;
223
+ }
224
+ let decoded;
225
+ try {
226
+ decoded = pathname;
227
+ let prevDecoded = "";
228
+ while (decoded !== prevDecoded) {
229
+ prevDecoded = decoded;
230
+ decoded = decodeURIComponent(decoded);
231
+ }
232
+ } catch {
233
+ return true;
234
+ }
235
+ if (decoded.includes("\0")) {
236
+ return true;
237
+ }
238
+ const segments = decoded.split("/").filter(Boolean);
239
+ for (const segment of segments) {
240
+ if (segment.startsWith(".") && segment !== "..") {
241
+ return true;
242
+ }
243
+ }
244
+ if (decoded.startsWith("/..")) {
245
+ return true;
246
+ }
247
+ return false;
248
+ }
249
+ function resolveStaticFile(pathname, publicDir) {
250
+ if (hasObviousAttackPattern(pathname)) {
251
+ return {
252
+ exists: false,
253
+ filePath: null,
254
+ mimeType: null,
255
+ error: "path_traversal"
256
+ };
257
+ }
258
+ let decodedPathname;
259
+ try {
260
+ decodedPathname = decodeURIComponent(pathname);
261
+ } catch {
262
+ return {
263
+ exists: false,
264
+ filePath: null,
265
+ mimeType: null,
266
+ error: "path_traversal"
267
+ };
268
+ }
269
+ const relativePath = decodedPathname.slice(1);
270
+ const resolvedPath = normalize(join2(publicDir, relativePath));
271
+ const absolutePublicDir = resolve(publicDir);
272
+ const publicDirWithSep = absolutePublicDir.endsWith("/") ? absolutePublicDir : absolutePublicDir + "/";
273
+ if (!resolvedPath.startsWith(publicDirWithSep) && resolvedPath !== absolutePublicDir) {
274
+ return {
275
+ exists: false,
276
+ filePath: null,
277
+ mimeType: null,
278
+ error: "outside_public"
279
+ };
280
+ }
281
+ const mimeType = getMimeType(resolvedPath);
282
+ let exists = false;
283
+ if (existsSync2(resolvedPath)) {
284
+ try {
285
+ const stats = statSync2(resolvedPath);
286
+ exists = stats.isFile();
287
+ } catch {
288
+ exists = false;
289
+ }
290
+ }
291
+ return {
292
+ exists,
293
+ filePath: resolvedPath,
294
+ mimeType
295
+ };
296
+ }
297
+
298
+ // src/build/mdx.ts
299
+ import { unified } from "unified";
300
+ import remarkParse from "remark-parse";
301
+ import remarkMdx from "remark-mdx";
302
+ import remarkGfm from "remark-gfm";
303
+ import matter from "gray-matter";
304
+ function lit(value) {
305
+ return { expr: "lit", value };
306
+ }
307
+ function textNode(value) {
308
+ return { kind: "text", value: lit(value) };
309
+ }
310
+ function elementNode(tag, props, children) {
311
+ const node = { kind: "element", tag };
312
+ if (props && Object.keys(props).length > 0) {
313
+ node.props = props;
314
+ }
315
+ if (children && children.length > 0) {
316
+ node.children = children;
317
+ }
318
+ return node;
319
+ }
320
+ function codeNode(language, content) {
321
+ return {
322
+ kind: "code",
323
+ language: lit(language),
324
+ content: lit(content)
325
+ };
326
+ }
327
+ function wrapNodes(nodes) {
328
+ if (nodes.length === 0) {
329
+ return elementNode("div");
330
+ }
331
+ if (nodes.length === 1 && nodes[0]) {
332
+ return nodes[0];
333
+ }
334
+ return elementNode("div", void 0, nodes);
335
+ }
336
+ function isCustomComponent(name) {
337
+ if (!name) return false;
338
+ return /^[A-Z]/.test(name);
339
+ }
340
+ var DISALLOWED_PATTERNS = [
341
+ /\bfunction\b/i,
342
+ /\b(eval|Function|setTimeout|setInterval)\b/,
343
+ /\bimport\b/,
344
+ /\brequire\b/,
345
+ /\bfetch\b/,
346
+ /\bwindow\b/,
347
+ /\bdocument\b/,
348
+ /\bglobal\b/,
349
+ /\bprocess\b/,
350
+ /\b__proto__\b/,
351
+ /\bconstructor\b/,
352
+ /\bprototype\b/
353
+ ];
354
+ function isSafeLiteral(value) {
355
+ return !DISALLOWED_PATTERNS.some((pattern) => pattern.test(value));
356
+ }
357
+ function safeEvalLiteral(value) {
358
+ try {
359
+ return JSON.parse(value);
360
+ } catch {
361
+ }
362
+ if (!isSafeLiteral(value)) {
363
+ return null;
364
+ }
365
+ try {
366
+ const fn = new Function(`return (${value});`);
367
+ return fn();
368
+ } catch {
369
+ return null;
370
+ }
371
+ }
372
+ function parseAttributeValue(attr) {
373
+ if (attr.value === null) {
374
+ return lit(true);
375
+ }
376
+ if (typeof attr.value === "string") {
377
+ return lit(attr.value);
378
+ }
379
+ if (attr.value.type === "mdxJsxAttributeValueExpression") {
380
+ const exprValue = attr.value.value.trim();
381
+ if (exprValue === "true") return lit(true);
382
+ if (exprValue === "false") return lit(false);
383
+ if (exprValue === "null") return lit(null);
384
+ const num = Number(exprValue);
385
+ if (!Number.isNaN(num)) return lit(num);
386
+ if (exprValue.startsWith("[") || exprValue.startsWith("{")) {
387
+ const parsed = safeEvalLiteral(exprValue);
388
+ if (parsed !== null && parsed !== void 0) {
389
+ return lit(parsed);
390
+ }
391
+ }
392
+ return lit(exprValue);
393
+ }
394
+ return lit(null);
395
+ }
396
+ function transformNode(node, ctx) {
397
+ switch (node.type) {
398
+ case "heading":
399
+ return elementNode(
400
+ `h${node.depth}`,
401
+ void 0,
402
+ transformChildren(node.children, ctx)
403
+ );
404
+ case "paragraph":
405
+ return elementNode("p", void 0, transformChildren(node.children, ctx));
406
+ case "text":
407
+ return textNode(node.value);
408
+ case "emphasis":
409
+ return elementNode("em", void 0, transformChildren(node.children, ctx));
410
+ case "strong":
411
+ return elementNode("strong", void 0, transformChildren(node.children, ctx));
412
+ case "link": {
413
+ const props = {
414
+ href: lit(node.url)
415
+ };
416
+ if (node["title"]) {
417
+ props["title"] = lit(node["title"]);
418
+ }
419
+ return elementNode("a", props, transformChildren(node.children, ctx));
420
+ }
421
+ case "inlineCode":
422
+ return elementNode("code", void 0, [textNode(node.value)]);
423
+ case "code": {
424
+ const lang = node.lang || "text";
425
+ return codeNode(lang, node.value);
426
+ }
427
+ case "blockquote":
428
+ return elementNode("blockquote", void 0, transformChildren(node.children, ctx));
429
+ case "list": {
430
+ const tag = node.ordered ? "ol" : "ul";
431
+ const props = node.ordered && node.start != null && node.start !== 1 ? { start: lit(node.start) } : void 0;
432
+ return elementNode(tag, props, transformChildren(node.children, ctx));
433
+ }
434
+ case "listItem": {
435
+ const children = [];
436
+ for (const child of node.children) {
437
+ if (child.type === "paragraph") {
438
+ children.push(...transformChildren(child.children, ctx));
439
+ } else {
440
+ const transformed = transformNode(child, ctx);
441
+ if (transformed) {
442
+ if (Array.isArray(transformed)) {
443
+ children.push(...transformed);
444
+ } else {
445
+ children.push(transformed);
446
+ }
447
+ }
448
+ }
449
+ }
450
+ return elementNode("li", void 0, children);
451
+ }
452
+ case "thematicBreak":
453
+ return elementNode("hr");
454
+ case "break":
455
+ return elementNode("br");
456
+ case "image": {
457
+ const props = {
458
+ src: lit(node.url)
459
+ };
460
+ if (node["alt"]) {
461
+ props["alt"] = lit(node["alt"]);
462
+ }
463
+ if (node["title"]) {
464
+ props["title"] = lit(node["title"]);
465
+ }
466
+ return elementNode("img", props);
467
+ }
468
+ case "html":
469
+ return textNode(node.value);
470
+ // MDX JSX elements
471
+ case "mdxJsxFlowElement":
472
+ case "mdxJsxTextElement":
473
+ return transformJsxElement(node, ctx);
474
+ // MDX expressions
475
+ case "mdxFlowExpression":
476
+ case "mdxTextExpression": {
477
+ const exprNode = node;
478
+ const value = exprNode.value.trim();
479
+ if (value === "") return null;
480
+ if (value === "true") return textNode("true");
481
+ if (value === "false") return textNode("false");
482
+ if (value === "null") return textNode("null");
483
+ const num = Number(value);
484
+ if (!Number.isNaN(num)) return textNode(String(num));
485
+ return textNode(value);
486
+ }
487
+ // GFM extensions
488
+ case "table": {
489
+ const rows = node.children;
490
+ if (!rows || rows.length === 0) {
491
+ return elementNode("table", void 0, []);
492
+ }
493
+ const headerRow = rows[0];
494
+ const headerCells = headerRow.children.map(
495
+ (cell) => elementNode("th", void 0, transformChildren(cell.children, ctx))
496
+ );
497
+ const thead = elementNode("thead", void 0, [
498
+ elementNode("tr", void 0, headerCells)
499
+ ]);
500
+ const bodyRows = rows.slice(1).map((row) => {
501
+ const cells = row.children.map(
502
+ (cell) => elementNode("td", void 0, transformChildren(cell.children, ctx))
503
+ );
504
+ return elementNode("tr", void 0, cells);
505
+ });
506
+ const tbody = bodyRows.length > 0 ? elementNode("tbody", void 0, bodyRows) : null;
507
+ const tableChildren = [thead];
508
+ if (tbody) {
509
+ tableChildren.push(tbody);
510
+ }
511
+ return elementNode("table", void 0, tableChildren);
512
+ }
513
+ // tableRow and tableCell are handled within the 'table' case above
514
+ case "delete":
515
+ return elementNode("del", void 0, transformChildren(node.children, ctx));
516
+ default:
517
+ return null;
518
+ }
519
+ }
520
+ function transformJsxElement(node, ctx) {
521
+ const name = node.name;
522
+ if (!name) {
523
+ const children2 = transformChildren(node.children, ctx);
524
+ return wrapNodes(children2);
525
+ }
526
+ if (isCustomComponent(name)) {
527
+ const def = ctx.components[name];
528
+ if (!def) {
529
+ throw new Error(`Undefined component: ${name}`);
530
+ }
531
+ const props2 = {};
532
+ for (const attr of node.attributes) {
533
+ if (attr.type === "mdxJsxAttribute") {
534
+ props2[attr.name] = parseAttributeValue(attr);
535
+ }
536
+ }
537
+ const children2 = transformChildren(node.children, ctx);
538
+ return applyComponentView(def.view, props2, children2);
539
+ }
540
+ const props = {};
541
+ for (const attr of node.attributes) {
542
+ if (attr.type === "mdxJsxAttribute") {
543
+ props[attr.name] = parseAttributeValue(attr);
544
+ }
545
+ }
546
+ const children = transformChildren(node.children, ctx);
547
+ return elementNode(name, props, children);
548
+ }
549
+ function substituteExpression(expr, props) {
550
+ const exprAny = expr;
551
+ if (exprAny.expr === "param") {
552
+ const paramExpr = expr;
553
+ const propValue = props[paramExpr.name];
554
+ if (!propValue) {
555
+ return { expr: "lit", value: null };
556
+ }
557
+ if (paramExpr.path && propValue.expr === "var") {
558
+ const varExpr = propValue;
559
+ return {
560
+ expr: "var",
561
+ name: varExpr.name,
562
+ path: paramExpr.path
563
+ };
564
+ }
565
+ return propValue;
566
+ }
567
+ if (expr.expr === "bin") {
568
+ const binExpr = expr;
569
+ return {
570
+ expr: "bin",
571
+ op: binExpr.op,
572
+ left: substituteExpression(binExpr.left, props),
573
+ right: substituteExpression(binExpr.right, props)
574
+ };
575
+ }
576
+ if (expr.expr === "not") {
577
+ const notExpr = expr;
578
+ return {
579
+ expr: "not",
580
+ operand: substituteExpression(notExpr.operand, props)
581
+ };
582
+ }
583
+ if (expr.expr === "cond") {
584
+ const condExpr = expr;
585
+ return {
586
+ expr: "cond",
587
+ if: substituteExpression(condExpr.if, props),
588
+ then: substituteExpression(condExpr.then, props),
589
+ else: substituteExpression(condExpr.else, props)
590
+ };
591
+ }
592
+ if (expr.expr === "get") {
593
+ const getExpr = expr;
594
+ return {
595
+ expr: "get",
596
+ base: substituteExpression(getExpr.base, props),
597
+ path: getExpr.path
598
+ };
599
+ }
600
+ return expr;
601
+ }
602
+ function applyComponentView(view, props, children) {
603
+ return substituteInNode(view, props, children);
604
+ }
605
+ function substituteInNode(node, props, children) {
606
+ switch (node.kind) {
607
+ case "each": {
608
+ const eachNode = node;
609
+ const result = {
610
+ kind: "each",
611
+ items: substituteExpression(eachNode.items, props),
612
+ as: eachNode.as,
613
+ body: substituteInNode(eachNode.body, props, children)
614
+ };
615
+ if (eachNode.index) {
616
+ result.index = eachNode.index;
617
+ }
618
+ if (eachNode.key) {
619
+ result.key = substituteExpression(eachNode.key, props);
620
+ }
621
+ return result;
622
+ }
623
+ case "text": {
624
+ const textNode2 = node;
625
+ return {
626
+ kind: "text",
627
+ value: substituteExpression(textNode2.value, props)
628
+ };
629
+ }
630
+ case "if": {
631
+ const ifNode = node;
632
+ const result = {
633
+ kind: "if",
634
+ condition: substituteExpression(ifNode.condition, props),
635
+ then: substituteInNode(ifNode.then, props, children)
636
+ };
637
+ if (ifNode.else) {
638
+ result.else = substituteInNode(ifNode.else, props, children);
639
+ }
640
+ return result;
641
+ }
642
+ case "element": {
643
+ const elem = node;
644
+ const newProps = {};
645
+ if (elem.props) {
646
+ for (const [key, value] of Object.entries(elem.props)) {
647
+ newProps[key] = substituteExpression(value, props);
648
+ }
649
+ }
650
+ let newChildren;
651
+ if (elem.children) {
652
+ newChildren = [];
653
+ for (const child of elem.children) {
654
+ if (child.kind === "slot") {
655
+ newChildren.push(...children);
656
+ } else {
657
+ newChildren.push(substituteInNode(child, props, children));
658
+ }
659
+ }
660
+ }
661
+ return elementNode(
662
+ elem.tag,
663
+ Object.keys(newProps).length > 0 ? newProps : void 0,
664
+ newChildren && newChildren.length > 0 ? newChildren : void 0
665
+ );
666
+ }
667
+ case "markdown":
668
+ case "code":
669
+ return node;
670
+ default:
671
+ return node;
672
+ }
673
+ }
674
+ function transformChildren(children, ctx) {
675
+ const result = [];
676
+ for (const child of children) {
677
+ const transformed = transformNode(child, ctx);
678
+ if (transformed) {
679
+ if (Array.isArray(transformed)) {
680
+ result.push(...transformed);
681
+ } else {
682
+ result.push(transformed);
683
+ }
684
+ }
685
+ }
686
+ return result;
687
+ }
688
+ function transformRoot(root, ctx) {
689
+ const nodes = transformChildren(root.children, ctx);
690
+ return wrapNodes(nodes);
691
+ }
692
+ async function mdxToConstela(source, options) {
693
+ const { content, data: _frontmatter } = matter(source);
694
+ const processor = unified().use(remarkParse).use(remarkGfm).use(remarkMdx);
695
+ const tree = processor.parse(content);
696
+ const ctx = {
697
+ components: options?.components ?? {}
698
+ };
699
+ const view = transformRoot(tree, ctx);
700
+ return {
701
+ version: "1.0",
702
+ state: {},
703
+ actions: {},
704
+ view
705
+ };
706
+ }
707
+ async function mdxContentToNode(content, options) {
708
+ const processor = unified().use(remarkParse).use(remarkGfm).use(remarkMdx);
709
+ const tree = processor.parse(content);
710
+ const ctx = {
711
+ components: options?.components ?? {}
712
+ };
713
+ return transformRoot(tree, ctx);
714
+ }
715
+
716
+ // src/data/loader.ts
717
+ import { existsSync as existsSync3, readFileSync } from "fs";
718
+ import { basename, extname as extname2, join as join3 } from "path";
719
+ import fg2 from "fast-glob";
720
+ var mdxContentToNode2 = mdxContentToNode;
721
+ function resolveJsonRefs(json) {
722
+ const cloned = JSON.parse(JSON.stringify(json));
723
+ const resolvingPaths = /* @__PURE__ */ new Set();
724
+ return resolveRefsRecursive(cloned, cloned, resolvingPaths);
725
+ }
726
+ function resolveRefsRecursive(current, root, resolvingPaths) {
727
+ if (current === null || typeof current !== "object") {
728
+ return current;
729
+ }
730
+ if (Array.isArray(current)) {
731
+ return current.map((item) => resolveRefsRecursive(item, root, resolvingPaths));
732
+ }
733
+ const obj = current;
734
+ if ("$ref" in obj && Object.keys(obj).length === 1) {
735
+ const refPath = obj["$ref"];
736
+ if (typeof refPath !== "string") {
737
+ throw new Error(`Invalid $ref: value must be a string`);
738
+ }
739
+ if (!refPath.startsWith("#")) {
740
+ throw new Error(`Invalid $ref: path must start with # (got "${refPath}")`);
741
+ }
742
+ if (resolvingPaths.has(refPath)) {
743
+ throw new Error(`Circular $ref detected: "${refPath}"`);
744
+ }
745
+ resolvingPaths.add(refPath);
746
+ try {
747
+ const resolved = resolveJsonPointer(root, refPath);
748
+ return resolveRefsRecursive(resolved, root, resolvingPaths);
749
+ } finally {
750
+ resolvingPaths.delete(refPath);
751
+ }
752
+ }
753
+ const result = {};
754
+ for (const [key, value] of Object.entries(obj)) {
755
+ result[key] = resolveRefsRecursive(value, root, resolvingPaths);
756
+ }
757
+ return result;
758
+ }
759
+ function resolveJsonPointer(root, pointer) {
760
+ const path = pointer.slice(1);
761
+ if (path === "" || path === "/") {
762
+ return root;
763
+ }
764
+ if (!path.startsWith("/")) {
765
+ throw new Error(`Invalid $ref: path after # must be empty or start with / (got "${pointer}")`);
766
+ }
767
+ const segments = path.slice(1).split("/").map(decodeJsonPointerSegment);
768
+ let current = root;
769
+ for (const segment of segments) {
770
+ if (current === null || current === void 0) {
771
+ throw new Error(`Invalid $ref: path "${pointer}" references null/undefined`);
772
+ }
773
+ if (Array.isArray(current)) {
774
+ const index = parseInt(segment, 10);
775
+ if (isNaN(index) || index < 0 || index >= current.length) {
776
+ throw new Error(`Invalid $ref: array index "${segment}" out of bounds in "${pointer}"`);
777
+ }
778
+ current = current[index];
779
+ } else if (typeof current === "object") {
780
+ const obj = current;
781
+ if (!(segment in obj)) {
782
+ throw new Error(`Invalid $ref: property "${segment}" not found in "${pointer}"`);
783
+ }
784
+ current = obj[segment];
785
+ } else {
786
+ throw new Error(`Invalid $ref: cannot access "${segment}" on primitive value in "${pointer}"`);
787
+ }
788
+ }
789
+ return current;
790
+ }
791
+ function decodeJsonPointerSegment(segment) {
792
+ return segment.replace(/~1/g, "/").replace(/~0/g, "~");
793
+ }
794
+ function parseYaml(content) {
795
+ const result = {};
796
+ const lines = content.split("\n");
797
+ const stack = [{ indent: -2, obj: result }];
798
+ for (let i = 0; i < lines.length; i++) {
799
+ const line = lines[i];
800
+ if (!line || line.trim() === "" || line.trim().startsWith("#")) continue;
801
+ const arrayMatch = line.match(/^(\s*)-\s*(.*)$/);
802
+ if (arrayMatch) {
803
+ const [, indentStr2, rest] = arrayMatch;
804
+ const indent2 = indentStr2?.length ?? 0;
805
+ while (stack.length > 1 && indent2 <= stack[stack.length - 1].indent) {
806
+ stack.pop();
807
+ }
808
+ const parent = stack[stack.length - 1];
809
+ const key2 = parent.key;
810
+ if (key2) {
811
+ if (!Array.isArray(parent.obj[key2])) {
812
+ parent.obj[key2] = [];
813
+ }
814
+ const arr = parent.obj[key2];
815
+ const objMatch = rest?.match(/^([\w-]+):\s*(.*)$/);
816
+ if (objMatch) {
817
+ const [, k, v] = objMatch;
818
+ const newObj = {};
819
+ if (v?.trim()) {
820
+ newObj[k] = parseValue(v);
821
+ }
822
+ arr.push(newObj);
823
+ stack.push({ indent: indent2, obj: newObj, key: k, isArray: true });
824
+ } else if (rest?.trim()) {
825
+ arr.push(parseValue(rest.trim()));
826
+ }
827
+ }
828
+ continue;
829
+ }
830
+ const match = line.match(/^(\s*)([\w-]+):\s*(.*)$/);
831
+ if (!match) continue;
832
+ const [, indentStr, key, value] = match;
833
+ const indent = indentStr?.length ?? 0;
834
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
835
+ stack.pop();
836
+ }
837
+ let targetObj;
838
+ const currentTop = stack[stack.length - 1];
839
+ if (currentTop.isArray) {
840
+ targetObj = currentTop.obj;
841
+ } else if (currentTop.key) {
842
+ if (!currentTop.obj[currentTop.key]) {
843
+ currentTop.obj[currentTop.key] = {};
844
+ }
845
+ targetObj = currentTop.obj[currentTop.key];
846
+ } else {
847
+ targetObj = currentTop.obj;
848
+ }
849
+ if (value?.trim() === "" || value === void 0) {
850
+ const newObj = {};
851
+ targetObj[key] = newObj;
852
+ stack.push({ indent, obj: targetObj, key });
853
+ } else {
854
+ targetObj[key] = parseValue(value);
855
+ }
856
+ }
857
+ return result;
858
+ }
859
+ function parseValue(value) {
860
+ const trimmed = value.trim();
861
+ if (trimmed === "true") return true;
862
+ if (trimmed === "false") return false;
863
+ if (trimmed === "null" || trimmed === "~") return null;
864
+ if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10);
865
+ if (/^-?\d+\.\d+$/.test(trimmed)) return parseFloat(trimmed);
866
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
867
+ return trimmed.slice(1, -1);
868
+ }
869
+ return trimmed;
870
+ }
871
+ function loadComponentDefinitions(baseDir, componentsPath) {
872
+ const fullPath = join3(baseDir, componentsPath);
873
+ const resolvedBase = join3(baseDir, "");
874
+ const resolvedPath = join3(fullPath, "");
875
+ if (!resolvedPath.startsWith(resolvedBase)) {
876
+ throw new Error(`Invalid component path: path traversal detected`);
877
+ }
878
+ if (!existsSync3(fullPath)) {
879
+ throw new Error(`MDX components file not found: ${fullPath}`);
880
+ }
881
+ const content = readFileSync(fullPath, "utf-8");
882
+ try {
883
+ return JSON.parse(content);
884
+ } catch {
885
+ throw new Error(`Invalid JSON in MDX components file: ${fullPath}`);
886
+ }
887
+ }
888
+ async function transformMdx(content, file, options) {
889
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
890
+ let frontmatter = {};
891
+ let mdxContent;
892
+ if (match) {
893
+ frontmatter = parseYaml(match[1]);
894
+ mdxContent = match[2].trim();
895
+ } else {
896
+ mdxContent = content.trim();
897
+ }
898
+ const compiledContent = await mdxContentToNode(
899
+ mdxContent,
900
+ options?.components ? { components: options.components } : void 0
901
+ );
902
+ const fmSlug = frontmatter["slug"];
903
+ const slug = typeof fmSlug === "string" ? fmSlug : basename(file, extname2(file));
904
+ return {
905
+ file,
906
+ raw: content,
907
+ frontmatter,
908
+ content: compiledContent,
909
+ slug
910
+ };
911
+ }
912
+ function transformYaml(content) {
913
+ return parseYaml(content);
914
+ }
915
+ function transformCsv(content) {
916
+ const lines = content.trim().split("\n");
917
+ if (lines.length === 0) return [];
918
+ const headerLine = lines[0];
919
+ const headers = parseCSVLine(headerLine);
920
+ const result = [];
921
+ for (let i = 1; i < lines.length; i++) {
922
+ const line = lines[i];
923
+ if (line.trim() === "") continue;
924
+ const values = parseCSVLine(line);
925
+ const row = {};
926
+ for (let j = 0; j < headers.length; j++) {
927
+ row[headers[j].trim()] = (values[j] ?? "").trim();
928
+ }
929
+ result.push(row);
930
+ }
931
+ return result;
932
+ }
933
+ function parseCSVLine(line) {
934
+ const result = [];
935
+ let current = "";
936
+ let inQuotes = false;
937
+ for (let i = 0; i < line.length; i++) {
938
+ const char = line[i];
939
+ if (char === '"') {
940
+ inQuotes = !inQuotes;
941
+ } else if (char === "," && !inQuotes) {
942
+ result.push(current);
943
+ current = "";
944
+ } else {
945
+ current += char;
946
+ }
947
+ }
948
+ result.push(current);
949
+ return result;
950
+ }
951
+ function applyTransform(content, transform, filename) {
952
+ if (!transform) {
953
+ if (filename.endsWith(".json")) {
954
+ const parsed = JSON.parse(content);
955
+ return resolveJsonRefs(parsed);
956
+ }
957
+ return content;
958
+ }
959
+ switch (transform) {
960
+ case "mdx":
961
+ throw new Error("MDX transform for single files is not supported via loadFile. Use loadGlob instead.");
962
+ case "yaml":
963
+ return transformYaml(content);
964
+ case "csv":
965
+ return transformCsv(content);
966
+ default:
967
+ return content;
968
+ }
969
+ }
970
+ async function loadGlob(baseDir, pattern, transform, options) {
971
+ const files = await fg2(pattern, { cwd: baseDir });
972
+ if (transform === "mdx") {
973
+ const results2 = [];
974
+ for (const file of files) {
975
+ const fullPath = join3(baseDir, file);
976
+ const content = readFileSync(fullPath, "utf-8");
977
+ const transformed = await transformMdx(content, file, options);
978
+ results2.push(transformed);
979
+ }
980
+ return results2;
981
+ }
982
+ const results = [];
983
+ for (const file of files) {
984
+ const fullPath = join3(baseDir, file);
985
+ const content = readFileSync(fullPath, "utf-8");
986
+ results.push({
987
+ file,
988
+ raw: content
989
+ });
990
+ }
991
+ return results;
992
+ }
993
+ async function loadFile(baseDir, filePath, transform) {
994
+ const fullPath = join3(baseDir, filePath);
995
+ if (!existsSync3(fullPath)) {
996
+ throw new Error(`File not found: ${fullPath}`);
997
+ }
998
+ const content = readFileSync(fullPath, "utf-8");
999
+ return applyTransform(content, transform, filePath);
1000
+ }
1001
+ async function loadApi(url, transform) {
1002
+ try {
1003
+ const response = await fetch(url);
1004
+ if (!response.ok) {
1005
+ throw new Error(`api request failed: ${response.status} ${response.statusText}`);
1006
+ }
1007
+ if (transform === "csv") {
1008
+ const text = await response.text();
1009
+ return transformCsv(text);
1010
+ }
1011
+ return await response.json();
1012
+ } catch (error) {
1013
+ if (error instanceof Error && error.message.includes("api request failed")) {
1014
+ throw error;
1015
+ }
1016
+ throw new Error(`Network error: ${error.message}`);
1017
+ }
1018
+ }
1019
+ function evaluateParamExpression(expr, item) {
1020
+ switch (expr.expr) {
1021
+ case "lit":
1022
+ return String(expr.value);
1023
+ case "var":
1024
+ if (expr.name === "item") {
1025
+ if (expr.path) {
1026
+ return getNestedValue(item, expr.path);
1027
+ }
1028
+ return String(item);
1029
+ }
1030
+ return "";
1031
+ case "get":
1032
+ if (expr.base.expr === "var" && expr.base.name === "item") {
1033
+ return getNestedValue(item, expr.path);
1034
+ }
1035
+ return "";
1036
+ default:
1037
+ return "";
1038
+ }
1039
+ }
1040
+ function getNestedValue(obj, path) {
1041
+ const parts = path.split(".");
1042
+ let current = obj;
1043
+ for (const part of parts) {
1044
+ if (current === null || current === void 0) return "";
1045
+ if (typeof current !== "object") return "";
1046
+ current = current[part];
1047
+ }
1048
+ return current !== void 0 && current !== null ? String(current) : "";
1049
+ }
1050
+ async function generateStaticPaths(data, staticPathsDef) {
1051
+ const paths = [];
1052
+ for (const item of data) {
1053
+ const params = {};
1054
+ for (const [paramName, paramExpr] of Object.entries(staticPathsDef.params)) {
1055
+ params[paramName] = evaluateParamExpression(paramExpr, item);
1056
+ }
1057
+ paths.push({ params, data: item });
1058
+ }
1059
+ return paths;
1060
+ }
1061
+ var DataLoader = class {
1062
+ cache = /* @__PURE__ */ new Map();
1063
+ componentCache = /* @__PURE__ */ new Map();
1064
+ projectRoot;
1065
+ constructor(projectRoot) {
1066
+ this.projectRoot = projectRoot;
1067
+ }
1068
+ /**
1069
+ * Resolve components from string path or import reference
1070
+ */
1071
+ resolveComponents(ref, imports) {
1072
+ if (typeof ref === "string") {
1073
+ if (this.componentCache.has(ref)) {
1074
+ return this.componentCache.get(ref);
1075
+ }
1076
+ const defs = loadComponentDefinitions(this.projectRoot, ref);
1077
+ this.componentCache.set(ref, defs);
1078
+ return defs;
1079
+ }
1080
+ if (ref.expr === "import") {
1081
+ if (!imports) {
1082
+ throw new Error(`Import context required for component reference "${ref.name}"`);
1083
+ }
1084
+ const imported = imports[ref.name];
1085
+ if (!imported || typeof imported !== "object") {
1086
+ throw new Error(`Component import "${ref.name}" not found or invalid`);
1087
+ }
1088
+ return imported;
1089
+ }
1090
+ return {};
1091
+ }
1092
+ /**
1093
+ * Load a single data source
1094
+ */
1095
+ async loadDataSource(name, dataSource, context) {
1096
+ if (this.cache.has(name)) {
1097
+ return this.cache.get(name);
1098
+ }
1099
+ let componentDefs;
1100
+ if (dataSource.transform === "mdx" && dataSource.components) {
1101
+ componentDefs = this.resolveComponents(
1102
+ dataSource.components,
1103
+ context?.imports
1104
+ );
1105
+ }
1106
+ let data;
1107
+ switch (dataSource.type) {
1108
+ case "glob":
1109
+ if (!dataSource.pattern) {
1110
+ throw new Error(`Glob data source '${name}' requires pattern`);
1111
+ }
1112
+ data = await loadGlob(
1113
+ this.projectRoot,
1114
+ dataSource.pattern,
1115
+ dataSource.transform,
1116
+ componentDefs ? { components: componentDefs } : void 0
1117
+ );
1118
+ break;
1119
+ case "file":
1120
+ if (!dataSource.path) {
1121
+ throw new Error(`File data source '${name}' requires path`);
1122
+ }
1123
+ data = await loadFile(this.projectRoot, dataSource.path, dataSource.transform);
1124
+ break;
1125
+ case "api":
1126
+ if (!dataSource.url) {
1127
+ throw new Error(`API data source '${name}' requires url`);
1128
+ }
1129
+ data = await loadApi(dataSource.url, dataSource.transform);
1130
+ break;
1131
+ default:
1132
+ throw new Error(`Unknown data source type: ${dataSource.type}`);
1133
+ }
1134
+ this.cache.set(name, data);
1135
+ return data;
1136
+ }
1137
+ /**
1138
+ * Load all data sources
1139
+ */
1140
+ async loadAllDataSources(dataSources) {
1141
+ const result = {};
1142
+ for (const [name, source] of Object.entries(dataSources)) {
1143
+ result[name] = await this.loadDataSource(name, source);
1144
+ }
1145
+ return result;
1146
+ }
1147
+ /**
1148
+ * Clear cache for a specific data source or all caches
1149
+ */
1150
+ clearCache(name) {
1151
+ if (name) {
1152
+ this.cache.delete(name);
1153
+ } else {
1154
+ this.cache.clear();
1155
+ }
1156
+ }
1157
+ /**
1158
+ * Clear all cache entries
1159
+ */
1160
+ clearAllCache() {
1161
+ this.cache.clear();
1162
+ }
1163
+ /**
1164
+ * Get the current cache size
1165
+ */
1166
+ getCacheSize() {
1167
+ return this.cache.size;
1168
+ }
1169
+ };
1170
+
1171
+ // src/layout/resolver.ts
1172
+ import { existsSync as existsSync5, statSync as statSync3, readFileSync as readFileSync3 } from "fs";
1173
+ import { join as join5, basename as basename2, dirname } from "path";
1174
+ import fg3 from "fast-glob";
1175
+ import { isLayoutProgram } from "@constela/core";
1176
+
1177
+ // src/utils/import-resolver.ts
1178
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
1179
+ import { join as join4, resolve as resolve2 } from "path";
1180
+ async function resolveImports(pageDir, imports, projectRoot) {
1181
+ if (!imports || Object.keys(imports).length === 0) {
1182
+ return {};
1183
+ }
1184
+ const resolved = {};
1185
+ const resolvedRoot = projectRoot ? resolve2(projectRoot) : null;
1186
+ for (const [name, importPath] of Object.entries(imports)) {
1187
+ const fullPath = join4(pageDir, importPath);
1188
+ if (resolvedRoot) {
1189
+ const resolvedPath = resolve2(fullPath);
1190
+ if (!resolvedPath.startsWith(resolvedRoot + "/") && resolvedPath !== resolvedRoot) {
1191
+ throw new Error(`Invalid import path "${name}": path traversal detected`);
1192
+ }
1193
+ }
1194
+ if (!existsSync4(fullPath)) {
1195
+ throw new Error(`Import "${name}" not found: ${fullPath}`);
1196
+ }
1197
+ let content;
1198
+ try {
1199
+ content = readFileSync2(fullPath, "utf-8");
1200
+ } catch (error) {
1201
+ throw new Error(`Failed to read import "${name}": ${fullPath}`);
1202
+ }
1203
+ try {
1204
+ const parsed = JSON.parse(content);
1205
+ resolved[name] = resolveJsonRefs(parsed);
1206
+ } catch (error) {
1207
+ if (error instanceof SyntaxError) {
1208
+ throw new Error(`Invalid JSON in import "${name}": ${fullPath}`);
1209
+ }
1210
+ throw error;
1211
+ }
1212
+ }
1213
+ return resolved;
1214
+ }
1215
+
1216
+ // src/layout/resolver.ts
1217
+ async function scanLayouts(layoutsDir) {
1218
+ if (!existsSync5(layoutsDir)) {
1219
+ throw new Error(`Layouts directory does not exist: ${layoutsDir}`);
1220
+ }
1221
+ const stat2 = statSync3(layoutsDir);
1222
+ if (!stat2.isDirectory()) {
1223
+ throw new Error(`Path is not a directory: ${layoutsDir}`);
1224
+ }
1225
+ const files = await fg3(["**/*.ts", "**/*.tsx", "**/*.json"], {
1226
+ cwd: layoutsDir,
1227
+ ignore: ["**/_*", "**/*.d.ts"]
1228
+ });
1229
+ const layouts = files.filter((file) => {
1230
+ const name = basename2(file);
1231
+ if (name.startsWith("_")) return false;
1232
+ if (name.endsWith(".d.ts")) return false;
1233
+ if (!name.endsWith(".ts") && !name.endsWith(".tsx") && !name.endsWith(".json")) return false;
1234
+ return true;
1235
+ }).map((file) => {
1236
+ const name = basename2(file).replace(/\.(tsx?|json)$/, "");
1237
+ return {
1238
+ name,
1239
+ file: join5(layoutsDir, file)
1240
+ };
1241
+ });
1242
+ return layouts;
1243
+ }
1244
+ function resolveLayout(layoutName, layouts) {
1245
+ return layouts.find((l) => l.name === layoutName);
1246
+ }
1247
+ async function loadLayout(layoutFile) {
1248
+ try {
1249
+ let exported;
1250
+ if (layoutFile.endsWith(".json")) {
1251
+ const content = readFileSync3(layoutFile, "utf-8");
1252
+ const parsed = JSON.parse(content);
1253
+ const importsValue = parsed["imports"];
1254
+ if (importsValue && typeof importsValue === "object" && Object.keys(importsValue).length > 0) {
1255
+ const layoutDir = dirname(layoutFile);
1256
+ const resolvedImports = await resolveImports(
1257
+ layoutDir,
1258
+ importsValue
1259
+ );
1260
+ exported = { ...parsed, importData: resolvedImports };
1261
+ } else {
1262
+ exported = parsed;
1263
+ }
1264
+ } else {
1265
+ const module = await import(layoutFile);
1266
+ exported = module.default || module;
1267
+ }
1268
+ if (!isLayoutProgram(exported)) {
1269
+ throw new Error(`File is not a valid layout: ${layoutFile}`);
1270
+ }
1271
+ return exported;
1272
+ } catch (error) {
1273
+ if (error instanceof Error && error.message.includes("not a valid layout")) {
1274
+ throw error;
1275
+ }
1276
+ if (error instanceof Error && error.message.includes("not found")) {
1277
+ throw error;
1278
+ }
1279
+ if (error instanceof Error && error.message.includes("Invalid JSON")) {
1280
+ throw error;
1281
+ }
1282
+ throw new Error(`Failed to load layout: ${layoutFile}`);
1283
+ }
1284
+ }
1285
+ var LayoutResolver = class {
1286
+ layoutsDir;
1287
+ layouts = [];
1288
+ loadedLayouts = /* @__PURE__ */ new Map();
1289
+ initialized = false;
1290
+ constructor(layoutsDir) {
1291
+ this.layoutsDir = layoutsDir;
1292
+ }
1293
+ /**
1294
+ * Initialize the resolver by scanning the layouts directory
1295
+ */
1296
+ async initialize() {
1297
+ try {
1298
+ this.layouts = await scanLayouts(this.layoutsDir);
1299
+ this.initialized = true;
1300
+ } catch {
1301
+ this.layouts = [];
1302
+ this.initialized = true;
1303
+ }
1304
+ }
1305
+ /**
1306
+ * Check if a layout exists
1307
+ */
1308
+ hasLayout(name) {
1309
+ return this.layouts.some((l) => l.name === name);
1310
+ }
1311
+ /**
1312
+ * Get a layout by name
1313
+ *
1314
+ * @param name - Layout name
1315
+ * @returns The layout program or undefined if not found
1316
+ */
1317
+ async getLayout(name) {
1318
+ const cached = this.loadedLayouts.get(name);
1319
+ if (cached) {
1320
+ return cached;
1321
+ }
1322
+ const scanned = resolveLayout(name, this.layouts);
1323
+ if (!scanned) {
1324
+ return void 0;
1325
+ }
1326
+ const layout = await loadLayout(scanned.file);
1327
+ this.loadedLayouts.set(name, layout);
1328
+ return layout;
1329
+ }
1330
+ /**
1331
+ * Compose a page with its layout
1332
+ *
1333
+ * @param page - Page program to compose
1334
+ * @returns Composed program (or original if no layout)
1335
+ * @throws Error if specified layout is not found
1336
+ */
1337
+ async composeWithLayout(page) {
1338
+ const layoutName = page.route?.layout;
1339
+ if (!layoutName) {
1340
+ return page;
1341
+ }
1342
+ if (!this.hasLayout(layoutName)) {
1343
+ const available = this.layouts.map((l) => l.name).join(", ");
1344
+ throw new Error(
1345
+ `Layout '${layoutName}' not found. Available layouts: ${available || "none"}`
1346
+ );
1347
+ }
1348
+ const layout = await this.getLayout(layoutName);
1349
+ if (!layout) {
1350
+ throw new Error(`Layout '${layoutName}' not found`);
1351
+ }
1352
+ return page;
1353
+ }
1354
+ /**
1355
+ * Get all scanned layouts
1356
+ */
1357
+ getAll() {
1358
+ return [...this.layouts];
1359
+ }
1360
+ };
1361
+
1362
+ // src/dev/server.ts
1363
+ import { createServer } from "http";
1364
+ import { createReadStream } from "fs";
1365
+ import { join as join7, isAbsolute } from "path";
1366
+ import { createServer as createViteServer } from "vite";
1367
+
1368
+ // src/json-page-loader.ts
1369
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
1370
+ import { dirname as dirname2, join as join6, relative, resolve as resolve3 } from "path";
1371
+ function validateVersion(version) {
1372
+ if (version !== "1.0") {
1373
+ throw new Error(`Unsupported version: ${version}. Only version "1.0" is supported.`);
1374
+ }
1375
+ }
1376
+ function extractRouteParams(path) {
1377
+ const params = [];
1378
+ const segments = path.split("/");
1379
+ for (const segment of segments) {
1380
+ if (segment.startsWith(":")) {
1381
+ const paramName = segment.slice(1).replace("*", "");
1382
+ params.push(paramName);
1383
+ }
1384
+ }
1385
+ return params;
1386
+ }
1387
+ function getNestedValue2(obj, path) {
1388
+ const parts = path.split(".");
1389
+ let current = obj;
1390
+ for (const part of parts) {
1391
+ if (current === null || current === void 0) return "";
1392
+ if (typeof current !== "object") return "";
1393
+ current = current[part];
1394
+ }
1395
+ return current !== void 0 && current !== null ? String(current) : "";
1396
+ }
1397
+ function evaluateParamExpression2(expr, item) {
1398
+ switch (expr.expr) {
1399
+ case "lit":
1400
+ return String(expr.value);
1401
+ case "var":
1402
+ if (expr.name === "item") {
1403
+ if (expr.path) {
1404
+ return getNestedValue2(item, expr.path);
1405
+ }
1406
+ return String(item);
1407
+ }
1408
+ return "";
1409
+ case "get":
1410
+ if (expr.base.expr === "var" && expr.base.name === "item") {
1411
+ return getNestedValue2(item, expr.path);
1412
+ }
1413
+ return "";
1414
+ default:
1415
+ return "";
1416
+ }
1417
+ }
1418
+ function compileWidget(widgetJson) {
1419
+ return {
1420
+ version: widgetJson.version || "1.0",
1421
+ state: convertState(widgetJson.state),
1422
+ actions: convertActions(widgetJson.actions),
1423
+ view: convertViewNode(widgetJson.view)
1424
+ };
1425
+ }
1426
+ async function loadWidgets(pageDir, widgets, baseDir) {
1427
+ if (!widgets || widgets.length === 0) {
1428
+ return [];
1429
+ }
1430
+ const resolvedBase = resolve3(baseDir);
1431
+ const compiledWidgets = [];
1432
+ for (const widget of widgets) {
1433
+ const widgetPath = join6(pageDir, widget.src);
1434
+ const resolvedWidgetPath = resolve3(widgetPath);
1435
+ if (!resolvedWidgetPath.startsWith(resolvedBase + "/") && resolvedWidgetPath !== resolvedBase) {
1436
+ throw new Error(`Invalid widget path "${widget.id}": path traversal detected`);
1437
+ }
1438
+ if (!existsSync6(widgetPath)) {
1439
+ throw new Error(`Widget file not found: ${resolvedWidgetPath}`);
1440
+ }
1441
+ let widgetContent;
1442
+ try {
1443
+ widgetContent = readFileSync4(widgetPath, "utf-8");
1444
+ } catch {
1445
+ throw new Error(`Failed to read widget file "${widget.id}": ${resolvedWidgetPath}`);
1446
+ }
1447
+ let widgetJson;
1448
+ try {
1449
+ widgetJson = JSON.parse(widgetContent);
1450
+ } catch {
1451
+ throw new Error(`Invalid JSON in widget file "${widget.id}": ${resolvedWidgetPath}`);
1452
+ }
1453
+ const program = compileWidget(widgetJson);
1454
+ compiledWidgets.push({
1455
+ id: widget.id,
1456
+ program
1457
+ });
1458
+ }
1459
+ return compiledWidgets;
1460
+ }
1461
+ function normalizeDataSourcePatterns(projectRoot, pageDir, dataSources) {
1462
+ if (!dataSources || Object.keys(dataSources).length === 0) {
1463
+ return {};
1464
+ }
1465
+ const resolvedProjectRoot = resolve3(projectRoot);
1466
+ const result = {};
1467
+ for (const [name, source] of Object.entries(dataSources)) {
1468
+ const normalizedSource = { ...source };
1469
+ if (source.type === "glob" && source.pattern) {
1470
+ if (source.pattern.startsWith("./") || source.pattern.startsWith("../")) {
1471
+ const absolutePath = resolve3(pageDir, source.pattern);
1472
+ if (!absolutePath.startsWith(resolvedProjectRoot + "/") && absolutePath !== resolvedProjectRoot) {
1473
+ throw new Error(
1474
+ `Invalid pattern "${source.pattern}": path traversal outside project root detected`
1475
+ );
1476
+ }
1477
+ normalizedSource.pattern = relative(resolvedProjectRoot, absolutePath);
1478
+ }
1479
+ }
1480
+ if (source.type === "file" && source.path) {
1481
+ if (source.path.startsWith("./") || source.path.startsWith("../")) {
1482
+ const absolutePath = resolve3(pageDir, source.path);
1483
+ if (!absolutePath.startsWith(resolvedProjectRoot + "/") && absolutePath !== resolvedProjectRoot) {
1484
+ throw new Error(
1485
+ `Invalid path "${source.path}": path traversal outside project root detected`
1486
+ );
1487
+ }
1488
+ normalizedSource.path = relative(resolvedProjectRoot, absolutePath);
1489
+ }
1490
+ }
1491
+ result[name] = normalizedSource;
1492
+ }
1493
+ return result;
1494
+ }
1495
+ async function loadJsonPage(baseDir, pagePath) {
1496
+ const filePath = join6(baseDir, pagePath);
1497
+ const resolvedBase = resolve3(baseDir);
1498
+ const resolvedPath = resolve3(filePath);
1499
+ if (!resolvedPath.startsWith(resolvedBase + "/") && resolvedPath !== resolvedBase) {
1500
+ throw new Error(`Invalid path: path traversal detected`);
1501
+ }
1502
+ if (!existsSync6(filePath)) {
1503
+ throw new Error(`Page file not found: ${filePath}`);
1504
+ }
1505
+ let content;
1506
+ try {
1507
+ content = readFileSync4(filePath, "utf-8");
1508
+ } catch (error) {
1509
+ throw new Error(`Failed to read page file: ${filePath}`);
1510
+ }
1511
+ let page;
1512
+ try {
1513
+ page = JSON.parse(content);
1514
+ } catch {
1515
+ throw new Error(`Invalid JSON in page file: ${filePath}`);
1516
+ }
1517
+ if (!("version" in page) || page.version === void 0) {
1518
+ throw new Error(`Missing required field "version" in page: ${filePath}`);
1519
+ }
1520
+ if (!("view" in page) || page.view === void 0) {
1521
+ throw new Error(`Missing required field "view" in page: ${filePath}`);
1522
+ }
1523
+ validateVersion(page.version);
1524
+ const pageDir = dirname2(filePath);
1525
+ const resolvedImports = await resolveImports(pageDir, page.imports, baseDir);
1526
+ const normalizedData = normalizeDataSourcePatterns(baseDir, pageDir, page.data);
1527
+ const loadedData = await loadPageData(baseDir, normalizedData, { imports: resolvedImports });
1528
+ const widgets = await loadWidgets(pageDir, page.widgets, baseDir);
1529
+ return {
1530
+ filePath,
1531
+ page,
1532
+ resolvedImports,
1533
+ loadedData,
1534
+ widgets
1535
+ };
1536
+ }
1537
+ async function loadPageData(baseDir, dataSources, context) {
1538
+ if (!dataSources || Object.keys(dataSources).length === 0) {
1539
+ return {};
1540
+ }
1541
+ const loader = new DataLoader(baseDir);
1542
+ const result = {};
1543
+ for (const [name, source] of Object.entries(dataSources)) {
1544
+ result[name] = await loader.loadDataSource(name, source, context);
1545
+ }
1546
+ return result;
1547
+ }
1548
+ async function generateStaticPathsFromPage(pageConfig, loadedData) {
1549
+ if (!pageConfig.getStaticPaths) {
1550
+ return [];
1551
+ }
1552
+ const { source, params } = pageConfig.getStaticPaths;
1553
+ const sourceData = loadedData[source];
1554
+ if (sourceData === void 0) {
1555
+ throw new Error(`Data source "${source}" not found for getStaticPaths`);
1556
+ }
1557
+ if (!Array.isArray(sourceData)) {
1558
+ throw new Error(`Data source "${source}" must be an array for getStaticPaths`);
1559
+ }
1560
+ const paths = [];
1561
+ for (const item of sourceData) {
1562
+ const extractedParams = {};
1563
+ for (const [paramName, paramExpr] of Object.entries(params)) {
1564
+ extractedParams[paramName] = evaluateParamExpression2(paramExpr, item);
1565
+ }
1566
+ paths.push({
1567
+ params: extractedParams,
1568
+ data: item
1569
+ });
1570
+ }
1571
+ return paths;
1572
+ }
1573
+ function substituteParamExpr(expr, props) {
1574
+ if (expr.expr === "param") {
1575
+ const propValue = props[expr.name];
1576
+ if (propValue) {
1577
+ if (expr.path) {
1578
+ return {
1579
+ expr: "get",
1580
+ base: propValue,
1581
+ path: expr.path
1582
+ };
1583
+ }
1584
+ return propValue;
1585
+ }
1586
+ return expr;
1587
+ }
1588
+ if (expr.expr === "bin") {
1589
+ return {
1590
+ ...expr,
1591
+ left: substituteParamExpr(expr.left, props),
1592
+ right: substituteParamExpr(expr.right, props)
1593
+ };
1594
+ }
1595
+ if (expr.expr === "not") {
1596
+ return {
1597
+ ...expr,
1598
+ operand: substituteParamExpr(expr.operand, props)
1599
+ };
1600
+ }
1601
+ if (expr.expr === "cond") {
1602
+ return {
1603
+ ...expr,
1604
+ if: substituteParamExpr(expr.if, props),
1605
+ then: substituteParamExpr(expr.then, props),
1606
+ else: substituteParamExpr(expr.else, props)
1607
+ };
1608
+ }
1609
+ if (expr.expr === "get") {
1610
+ return {
1611
+ ...expr,
1612
+ base: substituteParamExpr(expr.base, props)
1613
+ };
1614
+ }
1615
+ if (expr.expr === "index") {
1616
+ return {
1617
+ ...expr,
1618
+ base: substituteParamExpr(expr.base, props),
1619
+ key: substituteParamExpr(expr.key, props)
1620
+ };
1621
+ }
1622
+ return expr;
1623
+ }
1624
+ function substituteParamsInNode(node, props, components) {
1625
+ switch (node.kind) {
1626
+ case "text":
1627
+ return {
1628
+ ...node,
1629
+ value: substituteParamExpr(node.value, props)
1630
+ };
1631
+ case "element": {
1632
+ const elementNode2 = node;
1633
+ const newProps = elementNode2.props ? Object.fromEntries(
1634
+ Object.entries(elementNode2.props).map(([key, value]) => {
1635
+ if (value && typeof value === "object" && "event" in value) {
1636
+ return [key, value];
1637
+ }
1638
+ return [key, substituteParamExpr(value, props)];
1639
+ })
1640
+ ) : void 0;
1641
+ const newChildren = elementNode2.children ? elementNode2.children.map((child) => substituteParamsInNode(child, props, components)) : void 0;
1642
+ return {
1643
+ ...elementNode2,
1644
+ props: newProps,
1645
+ children: newChildren
1646
+ };
1647
+ }
1648
+ case "if": {
1649
+ const ifNode = node;
1650
+ const result = {
1651
+ kind: "if",
1652
+ condition: substituteParamExpr(ifNode.condition, props),
1653
+ then: substituteParamsInNode(ifNode.then, props, components)
1654
+ };
1655
+ if (ifNode.else) {
1656
+ result.else = substituteParamsInNode(ifNode.else, props, components);
1657
+ }
1658
+ return result;
1659
+ }
1660
+ case "each": {
1661
+ const eachNode = node;
1662
+ const result = {
1663
+ kind: "each",
1664
+ items: substituteParamExpr(eachNode.items, props),
1665
+ as: eachNode.as,
1666
+ body: substituteParamsInNode(eachNode.body, props, components)
1667
+ };
1668
+ if (eachNode.index) {
1669
+ result.index = eachNode.index;
1670
+ }
1671
+ if (eachNode.key) {
1672
+ result.key = substituteParamExpr(eachNode.key, props);
1673
+ }
1674
+ return result;
1675
+ }
1676
+ case "component": {
1677
+ const componentNode = node;
1678
+ const substitutedProps = componentNode.props ? Object.fromEntries(
1679
+ Object.entries(componentNode.props).map(([key, value]) => [
1680
+ key,
1681
+ substituteParamExpr(value, props)
1682
+ ])
1683
+ ) : {};
1684
+ return expandComponent(
1685
+ { ...componentNode, props: substitutedProps },
1686
+ components
1687
+ );
1688
+ }
1689
+ case "markdown":
1690
+ return {
1691
+ ...node,
1692
+ content: substituteParamExpr(node.content, props)
1693
+ };
1694
+ case "code":
1695
+ return {
1696
+ ...node,
1697
+ language: substituteParamExpr(node.language, props),
1698
+ content: substituteParamExpr(node.content, props)
1699
+ };
1700
+ case "slot":
1701
+ return node;
1702
+ default:
1703
+ return node;
1704
+ }
1705
+ }
1706
+ function expandComponent(node, components) {
1707
+ const componentDef = components[node.name];
1708
+ if (!componentDef) {
1709
+ throw new Error(`Component "${node.name}" not found in component definitions`);
1710
+ }
1711
+ const props = node.props || {};
1712
+ return substituteParamsInNode(componentDef.view, props, components);
1713
+ }
1714
+ function convertViewNode(node, components = {}) {
1715
+ switch (node.kind) {
1716
+ case "component": {
1717
+ const expanded = expandComponent(node, components);
1718
+ return convertViewNode(expanded, components);
1719
+ }
1720
+ case "element": {
1721
+ const elementNode2 = node;
1722
+ const convertedChildren = elementNode2.children ? elementNode2.children.map((child) => convertViewNode(child, components)) : void 0;
1723
+ return {
1724
+ ...elementNode2,
1725
+ children: convertedChildren
1726
+ };
1727
+ }
1728
+ case "if": {
1729
+ const ifNode = node;
1730
+ return {
1731
+ ...ifNode,
1732
+ then: convertViewNode(ifNode.then, components),
1733
+ else: ifNode.else ? convertViewNode(ifNode.else, components) : void 0
1734
+ };
1735
+ }
1736
+ case "each": {
1737
+ const eachNode = node;
1738
+ return {
1739
+ ...eachNode,
1740
+ body: convertViewNode(eachNode.body, components)
1741
+ };
1742
+ }
1743
+ default:
1744
+ return node;
1745
+ }
1746
+ }
1747
+ function convertActions(actions) {
1748
+ if (!actions) {
1749
+ return {};
1750
+ }
1751
+ if (typeof actions === "object" && !Array.isArray(actions) && Object.keys(actions).length === 0) {
1752
+ return {};
1753
+ }
1754
+ if (Array.isArray(actions)) {
1755
+ if (actions.length === 0) {
1756
+ return {};
1757
+ }
1758
+ const result2 = {};
1759
+ for (const action of actions) {
1760
+ const actionDef = action;
1761
+ result2[actionDef.name] = {
1762
+ name: actionDef.name,
1763
+ steps: actionDef.steps
1764
+ };
1765
+ }
1766
+ return result2;
1767
+ }
1768
+ const result = {};
1769
+ for (const [name, action] of Object.entries(actions)) {
1770
+ const actionDef = action;
1771
+ result[name] = {
1772
+ name,
1773
+ steps: actionDef.steps ?? []
1774
+ };
1775
+ }
1776
+ return result;
1777
+ }
1778
+ function convertState(state) {
1779
+ if (!state || Object.keys(state).length === 0) {
1780
+ return {};
1781
+ }
1782
+ const result = {};
1783
+ for (const [name, field] of Object.entries(state)) {
1784
+ const stateField = field;
1785
+ result[name] = {
1786
+ type: stateField.type,
1787
+ initial: stateField.initial
1788
+ };
1789
+ }
1790
+ return result;
1791
+ }
1792
+ async function convertToCompiledProgram(pageInfo) {
1793
+ const { page, resolvedImports, loadedData } = pageInfo;
1794
+ const components = page.components || {};
1795
+ const program = {
1796
+ version: "1.0",
1797
+ state: convertState(page.state),
1798
+ actions: convertActions(page.actions),
1799
+ view: convertViewNode(page.view, components)
1800
+ };
1801
+ if (page.route) {
1802
+ program.route = {
1803
+ path: page.route.path,
1804
+ params: extractRouteParams(page.route.path)
1805
+ };
1806
+ if (page.route.layout) {
1807
+ program.route.layout = page.route.layout;
1808
+ }
1809
+ if (page.route.layoutParams) {
1810
+ program.route.layoutParams = page.route.layoutParams;
1811
+ }
1812
+ if (page.route.meta) {
1813
+ program.route.meta = {};
1814
+ for (const [key, value] of Object.entries(page.route.meta)) {
1815
+ program.route.meta[key] = { expr: "lit", value };
1816
+ }
1817
+ }
1818
+ }
1819
+ const hasImports = Object.keys(resolvedImports).length > 0;
1820
+ const hasData = Object.keys(loadedData).length > 0;
1821
+ if (hasImports || hasData) {
1822
+ program.importData = {
1823
+ ...resolvedImports,
1824
+ ...loadedData
1825
+ };
1826
+ }
1827
+ if (page.lifecycle) {
1828
+ program.lifecycle = page.lifecycle;
1829
+ }
1830
+ return program;
1831
+ }
1832
+ var JsonPageLoader = class {
1833
+ projectRoot;
1834
+ cache = /* @__PURE__ */ new Map();
1835
+ constructor(projectRoot) {
1836
+ this.projectRoot = projectRoot;
1837
+ }
1838
+ /**
1839
+ * Load a JSON page with full resolution
1840
+ */
1841
+ async loadPage(pagePath) {
1842
+ if (this.cache.has(pagePath)) {
1843
+ return this.cache.get(pagePath);
1844
+ }
1845
+ const pageInfo = await loadJsonPage(this.projectRoot, pagePath);
1846
+ this.cache.set(pagePath, pageInfo);
1847
+ return pageInfo;
1848
+ }
1849
+ /**
1850
+ * Get static paths for a page
1851
+ */
1852
+ async getStaticPaths(pagePath) {
1853
+ const pageInfo = await this.loadPage(pagePath);
1854
+ return generateStaticPathsFromPage(pageInfo.page, pageInfo.loadedData);
1855
+ }
1856
+ /**
1857
+ * Compile a page to CompiledProgram
1858
+ */
1859
+ async compile(pagePath, options) {
1860
+ const pageInfo = await this.loadPage(pagePath);
1861
+ const program = await convertToCompiledProgram(pageInfo);
1862
+ if (options?.params) {
1863
+ }
1864
+ return program;
1865
+ }
1866
+ /**
1867
+ * Clear cache for a specific page or all pages
1868
+ */
1869
+ clearCache(pagePath) {
1870
+ if (pagePath) {
1871
+ this.cache.delete(pagePath);
1872
+ } else {
1873
+ this.cache.clear();
1874
+ }
1875
+ }
1876
+ };
1877
+
1878
+ // src/dev/server.ts
1879
+ import { analyzeLayoutPass, transformLayoutPass, composeLayoutWithPage } from "@constela/compiler";
1880
+ var DEFAULT_PORT = 3e3;
1881
+ var DEFAULT_HOST = "localhost";
1882
+ var DEFAULT_PUBLIC_DIR = "public";
1883
+ var DEFAULT_ROUTES_DIR = "src/pages";
1884
+ function matchRoute(url, routes) {
1885
+ const normalizedUrl = url === "/" ? "/" : url.replace(/\/$/, "");
1886
+ const urlSegments = normalizedUrl.split("/").filter(Boolean);
1887
+ for (const route of routes) {
1888
+ if (route.type !== "page") {
1889
+ continue;
1890
+ }
1891
+ const patternSegments = route.pattern.split("/").filter(Boolean);
1892
+ const params = {};
1893
+ const hasCatchAll = patternSegments.includes("*");
1894
+ if (hasCatchAll) {
1895
+ const catchAllIndex = patternSegments.indexOf("*");
1896
+ let matched = true;
1897
+ for (let i = 0; i < catchAllIndex; i++) {
1898
+ const patternSeg = patternSegments[i];
1899
+ const urlSeg = urlSegments[i];
1900
+ if (patternSeg === void 0 || urlSeg === void 0) {
1901
+ matched = false;
1902
+ break;
1903
+ }
1904
+ if (patternSeg.startsWith(":")) {
1905
+ const paramName = patternSeg.slice(1);
1906
+ params[paramName] = urlSeg;
1907
+ } else if (patternSeg !== urlSeg) {
1908
+ matched = false;
1909
+ break;
1910
+ }
1911
+ }
1912
+ if (matched && urlSegments.length >= catchAllIndex) {
1913
+ const catchAllParamName = route.params.find((p) => {
1914
+ return true;
1915
+ });
1916
+ if (catchAllParamName) {
1917
+ const remainingSegments = urlSegments.slice(catchAllIndex);
1918
+ params[catchAllParamName] = remainingSegments.join("/");
1919
+ }
1920
+ return { route, params };
1921
+ }
1922
+ } else {
1923
+ if (patternSegments.length !== urlSegments.length) {
1924
+ continue;
1925
+ }
1926
+ let matched = true;
1927
+ for (let i = 0; i < patternSegments.length; i++) {
1928
+ const patternSeg = patternSegments[i];
1929
+ const urlSeg = urlSegments[i];
1930
+ if (patternSeg === void 0 || urlSeg === void 0) {
1931
+ matched = false;
1932
+ break;
1933
+ }
1934
+ if (patternSeg.startsWith(":")) {
1935
+ const paramName = patternSeg.slice(1);
1936
+ params[paramName] = urlSeg;
1937
+ } else if (patternSeg !== urlSeg) {
1938
+ matched = false;
1939
+ break;
1940
+ }
1941
+ }
1942
+ if (matched) {
1943
+ return { route, params };
1944
+ }
1945
+ }
1946
+ }
1947
+ return null;
1948
+ }
1949
+ function escapeHtml(str) {
1950
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1951
+ }
1952
+ function extractMdxContentSlot(loadedData, dataSourceName, routeParams) {
1953
+ const dataSource = loadedData[dataSourceName];
1954
+ if (!Array.isArray(dataSource)) {
1955
+ return void 0;
1956
+ }
1957
+ const slug = routeParams["slug"] || "index";
1958
+ const item = dataSource.find(
1959
+ (entry) => typeof entry === "object" && entry !== null && "slug" in entry && entry.slug === slug
1960
+ );
1961
+ if (!item || typeof item !== "object" || !("content" in item)) {
1962
+ return void 0;
1963
+ }
1964
+ return { "mdx-content": item.content };
1965
+ }
1966
+ async function createDevServer(options = {}) {
1967
+ const {
1968
+ port = DEFAULT_PORT,
1969
+ host = DEFAULT_HOST,
1970
+ routesDir = DEFAULT_ROUTES_DIR,
1971
+ publicDir = join7(process.cwd(), DEFAULT_PUBLIC_DIR),
1972
+ layoutsDir,
1973
+ css
1974
+ } = options;
1975
+ let httpServer = null;
1976
+ let actualPort = port;
1977
+ let viteServer = null;
1978
+ if (css) {
1979
+ viteServer = await createViteServer({
1980
+ root: process.cwd(),
1981
+ server: { middlewareMode: true },
1982
+ appType: "custom",
1983
+ logLevel: "silent"
1984
+ });
1985
+ }
1986
+ const absoluteRoutesDir = isAbsolute(routesDir) ? routesDir : join7(process.cwd(), routesDir);
1987
+ let routes = [];
1988
+ try {
1989
+ routes = await scanRoutes(absoluteRoutesDir);
1990
+ } catch {
1991
+ routes = [];
1992
+ }
1993
+ let layoutResolver = null;
1994
+ if (layoutsDir) {
1995
+ const absoluteLayoutsDir = isAbsolute(layoutsDir) ? layoutsDir : join7(process.cwd(), layoutsDir);
1996
+ layoutResolver = new LayoutResolver(absoluteLayoutsDir);
1997
+ await layoutResolver.initialize();
1998
+ }
1999
+ const devServer = {
2000
+ get port() {
2001
+ return actualPort;
2002
+ },
2003
+ async listen() {
2004
+ return new Promise((resolve4, reject) => {
2005
+ httpServer = createServer(async (req, res) => {
2006
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
2007
+ const pathname = url.pathname;
2008
+ if (viteServer) {
2009
+ await new Promise((resolveMiddleware) => {
2010
+ viteServer.middlewares(req, res, () => {
2011
+ resolveMiddleware();
2012
+ });
2013
+ });
2014
+ if (res.writableEnded) {
2015
+ return;
2016
+ }
2017
+ }
2018
+ const staticResult = resolveStaticFile(pathname, publicDir);
2019
+ if (staticResult.error === "path_traversal" || staticResult.error === "outside_public") {
2020
+ res.writeHead(403, { "Content-Type": "text/plain" });
2021
+ res.end("Forbidden");
2022
+ return;
2023
+ }
2024
+ if (staticResult.exists && staticResult.filePath && staticResult.mimeType) {
2025
+ res.writeHead(200, { "Content-Type": staticResult.mimeType });
2026
+ const stream = createReadStream(staticResult.filePath);
2027
+ stream.pipe(res);
2028
+ stream.on("error", () => {
2029
+ if (!res.headersSent) {
2030
+ res.writeHead(500, { "Content-Type": "text/plain" });
2031
+ }
2032
+ res.end("Internal Server Error");
2033
+ });
2034
+ return;
2035
+ }
2036
+ const match = matchRoute(pathname, routes);
2037
+ if (match) {
2038
+ try {
2039
+ const projectRoot = process.cwd();
2040
+ const pageLoader = new JsonPageLoader(projectRoot);
2041
+ const relativePath = match.route.file.startsWith(projectRoot) ? match.route.file.slice(projectRoot.length + 1) : match.route.file;
2042
+ const pageInfo = await pageLoader.loadPage(relativePath);
2043
+ const program = await convertToCompiledProgram(pageInfo);
2044
+ let composedProgram = program;
2045
+ const widgets = pageInfo.widgets.map((w) => ({ id: w.id, program: w.program }));
2046
+ if (program.route?.layout && layoutResolver) {
2047
+ const layoutProgram = await layoutResolver.getLayout(program.route.layout);
2048
+ if (layoutProgram) {
2049
+ const analysis = analyzeLayoutPass(layoutProgram);
2050
+ if (analysis.ok) {
2051
+ const compiledLayout = transformLayoutPass(layoutProgram, analysis.context);
2052
+ const layoutParams = program.route?.layoutParams;
2053
+ const slots = extractMdxContentSlot(pageInfo.loadedData, "docs", match.params);
2054
+ composedProgram = composeLayoutWithPage(
2055
+ compiledLayout,
2056
+ program,
2057
+ layoutParams,
2058
+ slots
2059
+ );
2060
+ }
2061
+ }
2062
+ }
2063
+ const ssrContext = {
2064
+ url: pathname,
2065
+ params: match.params,
2066
+ query: url.searchParams
2067
+ };
2068
+ const content = await renderPage(composedProgram, ssrContext);
2069
+ const routeContext = {
2070
+ params: match.params,
2071
+ query: Object.fromEntries(url.searchParams.entries()),
2072
+ path: pathname
2073
+ };
2074
+ const hydrationScript = generateHydrationScript(composedProgram, widgets, routeContext);
2075
+ const cssHead = css ? (Array.isArray(css) ? css : [css]).map((p) => `<link rel="stylesheet" href="/${p}">`).join("\n") : "";
2076
+ const themeState = composedProgram.state?.["theme"];
2077
+ const initialTheme = themeState?.initial;
2078
+ const importMap = {
2079
+ "@constela/runtime": "/node_modules/@constela/runtime/dist/index.js",
2080
+ "@constela/core": "/node_modules/@constela/core/dist/index.js",
2081
+ "@constela/compiler": "/node_modules/@constela/compiler/dist/index.js",
2082
+ "marked": "/node_modules/marked/lib/marked.esm.js",
2083
+ "monaco-editor": "/node_modules/monaco-editor/esm/vs/editor/editor.api.js"
2084
+ };
2085
+ const html = wrapHtml(content, hydrationScript, cssHead, {
2086
+ ...initialTheme ? { theme: initialTheme } : {},
2087
+ importMap
2088
+ });
2089
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2090
+ res.end(html);
2091
+ return;
2092
+ } catch (error) {
2093
+ const errorMessage = error instanceof Error ? error.message : String(error);
2094
+ const errorStack = error instanceof Error ? error.stack : "";
2095
+ res.writeHead(500, { "Content-Type": "text/html; charset=utf-8" });
2096
+ res.end(`<!DOCTYPE html>
2097
+ <html>
2098
+ <head>
2099
+ <meta charset="utf-8">
2100
+ <title>Server Error</title>
2101
+ <style>
2102
+ body { font-family: system-ui, sans-serif; padding: 2rem; background: #1a1a1a; color: #fff; }
2103
+ h1 { color: #ff6b6b; }
2104
+ pre { background: #2d2d2d; padding: 1rem; border-radius: 4px; overflow-x: auto; }
2105
+ </style>
2106
+ </head>
2107
+ <body>
2108
+ <h1>Server Error</h1>
2109
+ <p>${escapeHtml(errorMessage)}</p>
2110
+ <pre>${escapeHtml(errorStack ?? "")}</pre>
2111
+ </body>
2112
+ </html>`);
2113
+ return;
2114
+ }
2115
+ }
2116
+ res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
2117
+ res.end(`<!DOCTYPE html>
2118
+ <html>
2119
+ <head>
2120
+ <meta charset="utf-8">
2121
+ <title>404 Not Found</title>
2122
+ <style>
2123
+ body { font-family: system-ui, sans-serif; padding: 2rem; text-align: center; }
2124
+ h1 { color: #666; }
2125
+ </style>
2126
+ </head>
2127
+ <body>
2128
+ <h1>404 Not Found</h1>
2129
+ <p>The page <code>${escapeHtml(pathname)}</code> could not be found.</p>
2130
+ </body>
2131
+ </html>`);
2132
+ });
2133
+ httpServer.on("error", (err) => {
2134
+ reject(err);
2135
+ });
2136
+ httpServer.listen(port, host, () => {
2137
+ const address = httpServer?.address();
2138
+ if (address) {
2139
+ actualPort = address.port;
2140
+ }
2141
+ resolve4();
2142
+ });
2143
+ });
2144
+ },
2145
+ async close() {
2146
+ if (viteServer) {
2147
+ await viteServer.close();
2148
+ viteServer = null;
2149
+ }
2150
+ return new Promise((resolve4, reject) => {
2151
+ if (!httpServer) {
2152
+ resolve4();
2153
+ return;
2154
+ }
2155
+ httpServer.closeAllConnections();
2156
+ httpServer.close((err) => {
2157
+ if (err) {
2158
+ reject(err);
2159
+ } else {
2160
+ httpServer = null;
2161
+ resolve4();
2162
+ }
2163
+ });
2164
+ });
2165
+ }
2166
+ };
2167
+ return devServer;
2168
+ }
2169
+
2170
+ // src/build/index.ts
2171
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
2172
+ import { mkdir as mkdir2, writeFile, cp, readdir } from "fs/promises";
2173
+ import { join as join9, dirname as dirname5, relative as relative2, basename as basename4 } from "path";
2174
+
2175
+ // src/build/bundler.ts
2176
+ import * as esbuild from "esbuild";
2177
+ import { mkdir } from "fs/promises";
2178
+ import { join as join8, dirname as dirname4 } from "path";
2179
+ import { fileURLToPath } from "url";
2180
+ var __dirname = dirname4(fileURLToPath(import.meta.url));
2181
+ async function bundleRuntime(options) {
2182
+ const entryContent = `
2183
+ export { hydrateApp, createApp } from '@constela/runtime';
2184
+ `;
2185
+ const outFile = join8(options.outDir, "_constela", "runtime.js");
2186
+ await mkdir(dirname4(outFile), { recursive: true });
2187
+ try {
2188
+ await esbuild.build({
2189
+ stdin: {
2190
+ contents: entryContent,
2191
+ resolveDir: __dirname,
2192
+ loader: "ts"
2193
+ },
2194
+ bundle: true,
2195
+ format: "esm",
2196
+ target: "es2020",
2197
+ platform: "browser",
2198
+ outfile: outFile,
2199
+ minify: options.minify ?? true,
2200
+ treeShaking: true
2201
+ });
2202
+ } catch (error) {
2203
+ const message = error instanceof Error ? error.message : String(error);
2204
+ throw new Error(`Failed to bundle runtime to ${outFile}: ${message}`);
2205
+ }
2206
+ return "/_constela/runtime.js";
2207
+ }
2208
+
2209
+ // src/build/index.ts
2210
+ function isDynamicRoute(pattern) {
2211
+ return pattern.includes(":") || pattern.includes("*");
2212
+ }
2213
+ function getOutputPath(filePath, outDir) {
2214
+ const withoutExt = filePath.replace(/\.(json|ts|tsx|js|jsx)$/, "");
2215
+ if (withoutExt === "index" || withoutExt.endsWith("/index")) {
2216
+ return join9(outDir, withoutExt + ".html");
2217
+ }
2218
+ return join9(outDir, withoutExt, "index.html");
2219
+ }
2220
+ function paramsToOutputPath(basePattern, params, outDir) {
2221
+ let path = basePattern;
2222
+ for (const [key, value] of Object.entries(params)) {
2223
+ path = path.replace(`:${key}`, value);
2224
+ path = path.replace("*", value);
2225
+ }
2226
+ const relativePath = path.startsWith("/") ? path.slice(1) : path;
2227
+ if (relativePath === "") {
2228
+ return join9(outDir, "index.html");
2229
+ }
2230
+ return join9(outDir, relativePath, "index.html");
2231
+ }
2232
+ async function loadGetStaticPaths(pageFile) {
2233
+ const dir = dirname5(pageFile);
2234
+ const baseName = basename4(pageFile, ".json");
2235
+ const pathsFile = join9(dir, `${baseName}.paths.ts`);
2236
+ if (!existsSync7(pathsFile)) {
2237
+ return null;
2238
+ }
2239
+ try {
2240
+ const content = readFileSync5(pathsFile, "utf-8");
2241
+ const pathsMatch = content.match(/paths:\s*\[([\s\S]*?)\]/);
2242
+ if (!pathsMatch) {
2243
+ throw new Error(`Invalid getStaticPaths format in ${pathsFile}`);
2244
+ }
2245
+ const pathsContent = pathsMatch[1];
2246
+ const paramsRegex = /\{\s*params:\s*\{([^}]+)\}\s*\}/g;
2247
+ const paths = [];
2248
+ let match;
2249
+ while ((match = paramsRegex.exec(pathsContent ?? "")) !== null) {
2250
+ const paramsStr = match[1];
2251
+ const params = {};
2252
+ const paramMatches = paramsStr?.matchAll(/(\w+):\s*['"]([^'"]+)['"]/g);
2253
+ if (paramMatches) {
2254
+ for (const paramMatch of paramMatches) {
2255
+ const key = paramMatch[1];
2256
+ const value = paramMatch[2];
2257
+ if (key !== void 0 && value !== void 0) {
2258
+ params[key] = value;
2259
+ }
2260
+ }
2261
+ }
2262
+ paths.push({ params });
2263
+ }
2264
+ if (!Array.isArray(paths)) {
2265
+ throw new Error(`Invalid getStaticPaths format in ${pathsFile}: must return { paths: [] }`);
2266
+ }
2267
+ return { paths };
2268
+ } catch (error) {
2269
+ if (error instanceof Error && error.message.includes("Invalid getStaticPaths")) {
2270
+ throw error;
2271
+ }
2272
+ throw new Error(`Invalid getStaticPaths format in ${pathsFile}`);
2273
+ }
2274
+ }
2275
+ async function loadLayout2(layoutName, layoutsDir) {
2276
+ const layoutPath = join9(layoutsDir, `${layoutName}.json`);
2277
+ if (!existsSync7(layoutPath)) {
2278
+ throw new Error(`Layout "${layoutName}" not found at ${layoutPath}`);
2279
+ }
2280
+ try {
2281
+ const content = readFileSync5(layoutPath, "utf-8");
2282
+ return JSON.parse(content);
2283
+ } catch {
2284
+ throw new Error(`Invalid JSON in layout file: ${layoutPath}`);
2285
+ }
2286
+ }
2287
+ function applyLayout(pageView, layoutView) {
2288
+ return replaceSlot(layoutView, pageView);
2289
+ }
2290
+ function normalizeProps(props) {
2291
+ if (!props || typeof props !== "object") {
2292
+ return {};
2293
+ }
2294
+ const normalized = {};
2295
+ for (const [key, value] of Object.entries(props)) {
2296
+ if (value && typeof value === "object" && "expr" in value) {
2297
+ normalized[key] = value;
2298
+ } else if (value !== void 0 && value !== null) {
2299
+ normalized[key] = { expr: "lit", value };
2300
+ }
2301
+ }
2302
+ return normalized;
2303
+ }
2304
+ function normalizeExpression(expr) {
2305
+ if (!expr || typeof expr !== "object") {
2306
+ return expr;
2307
+ }
2308
+ const exprObj = expr;
2309
+ if (exprObj["expr"] === "param" && typeof exprObj["name"] === "string") {
2310
+ return {
2311
+ expr: "route",
2312
+ source: "param",
2313
+ name: exprObj["name"]
2314
+ };
2315
+ }
2316
+ return expr;
2317
+ }
2318
+ function normalizeViewNode(node) {
2319
+ if (!node || typeof node !== "object") {
2320
+ return node;
2321
+ }
2322
+ const nodeObj = node;
2323
+ if (nodeObj["props"]) {
2324
+ nodeObj["props"] = normalizeProps(nodeObj["props"]);
2325
+ }
2326
+ if (nodeObj["kind"] === "text" && nodeObj["value"]) {
2327
+ nodeObj["value"] = normalizeExpression(nodeObj["value"]);
2328
+ }
2329
+ if (Array.isArray(nodeObj["children"])) {
2330
+ nodeObj["children"] = nodeObj["children"].map((child) => normalizeViewNode(child));
2331
+ }
2332
+ return nodeObj;
2333
+ }
2334
+ function replaceSlot(node, content) {
2335
+ if (!node || typeof node !== "object") {
2336
+ return node;
2337
+ }
2338
+ const nodeObj = node;
2339
+ if (nodeObj["kind"] === "slot") {
2340
+ return content;
2341
+ }
2342
+ if (Array.isArray(nodeObj["children"])) {
2343
+ return {
2344
+ ...nodeObj,
2345
+ children: nodeObj["children"].map((child) => replaceSlot(child, content))
2346
+ };
2347
+ }
2348
+ return node;
2349
+ }
2350
+ async function processLayouts(pageInfo, layoutsDir) {
2351
+ const layoutName = pageInfo.page.route?.layout;
2352
+ if (!layoutName || !layoutsDir) {
2353
+ return pageInfo;
2354
+ }
2355
+ const layout = await loadLayout2(layoutName, layoutsDir);
2356
+ const normalizedLayoutView = normalizeViewNode(structuredClone(layout.view));
2357
+ const wrappedView = applyLayout(pageInfo.page.view, normalizedLayoutView);
2358
+ let updatedRoute;
2359
+ if (pageInfo.page.route) {
2360
+ const { layout: _layout, ...routeWithoutLayout } = pageInfo.page.route;
2361
+ updatedRoute = routeWithoutLayout;
2362
+ }
2363
+ let updatedPageInfo = {
2364
+ ...pageInfo,
2365
+ page: {
2366
+ ...pageInfo.page,
2367
+ view: wrappedView,
2368
+ route: updatedRoute
2369
+ }
2370
+ };
2371
+ if (layout.layout) {
2372
+ const parentLayout = await loadLayout2(layout.layout, layoutsDir);
2373
+ const normalizedParentLayoutView = normalizeViewNode(structuredClone(parentLayout.view));
2374
+ const doubleWrappedView = applyLayout(updatedPageInfo.page.view, normalizedParentLayoutView);
2375
+ updatedPageInfo = {
2376
+ ...updatedPageInfo,
2377
+ page: {
2378
+ ...updatedPageInfo.page,
2379
+ view: doubleWrappedView
2380
+ }
2381
+ };
2382
+ }
2383
+ return updatedPageInfo;
2384
+ }
2385
+ async function renderPageToHtml(program, params, runtimePath) {
2386
+ const normalizedProgram = {
2387
+ ...program,
2388
+ view: normalizeViewNode(structuredClone(program.view))
2389
+ };
2390
+ const ctx = {
2391
+ url: "/",
2392
+ params,
2393
+ query: new URLSearchParams()
2394
+ };
2395
+ const content = await renderPage(normalizedProgram, ctx);
2396
+ const routeContext = {
2397
+ params,
2398
+ query: {},
2399
+ path: "/"
2400
+ };
2401
+ const hydrationScript = generateHydrationScript(normalizedProgram, void 0, routeContext);
2402
+ return wrapHtml(content, hydrationScript, void 0, runtimePath ? { runtimePath } : void 0);
2403
+ }
2404
+ async function copyPublicDir(publicDir, outDir, generatedFiles) {
2405
+ if (!existsSync7(publicDir)) {
2406
+ return;
2407
+ }
2408
+ await copyDirRecursive(publicDir, outDir, generatedFiles);
2409
+ }
2410
+ async function copyDirRecursive(srcDir, destDir, skipFiles) {
2411
+ const entries = await readdir(srcDir, { withFileTypes: true });
2412
+ for (const entry of entries) {
2413
+ const srcPath = join9(srcDir, entry.name);
2414
+ const destPath = join9(destDir, entry.name);
2415
+ if (entry.isDirectory()) {
2416
+ await mkdir2(destPath, { recursive: true });
2417
+ await copyDirRecursive(srcPath, destPath, skipFiles);
2418
+ } else {
2419
+ if (skipFiles.has(destPath)) {
2420
+ continue;
2421
+ }
2422
+ await mkdir2(dirname5(destPath), { recursive: true });
2423
+ await cp(srcPath, destPath);
2424
+ }
2425
+ }
2426
+ }
2427
+ function validateJsonPage(content, filePath) {
2428
+ let parsed;
2429
+ try {
2430
+ parsed = JSON.parse(content);
2431
+ } catch {
2432
+ throw new Error(`Invalid JSON in ${filePath}`);
2433
+ }
2434
+ const page = parsed;
2435
+ if (!page["view"]) {
2436
+ throw new Error(`Missing required field "view" in ${filePath}`);
2437
+ }
2438
+ return page;
2439
+ }
2440
+ async function build2(options) {
2441
+ const outDir = options?.outDir ?? "dist";
2442
+ const routesDir = options?.routesDir ?? "src/routes";
2443
+ const publicDir = options?.publicDir;
2444
+ const layoutsDir = options?.layoutsDir;
2445
+ const generatedFiles = [];
2446
+ await mkdir2(outDir, { recursive: true });
2447
+ let scannedRoutes = [];
2448
+ try {
2449
+ scannedRoutes = await scanRoutes(routesDir);
2450
+ } catch {
2451
+ return {
2452
+ outDir,
2453
+ routes: [],
2454
+ generatedFiles: []
2455
+ };
2456
+ }
2457
+ const routes = scannedRoutes.map((r) => r.pattern);
2458
+ const jsonPages = scannedRoutes.filter(
2459
+ (route) => route.type === "page" && route.file.endsWith(".json")
2460
+ );
2461
+ if (jsonPages.length === 0) {
2462
+ if (publicDir) {
2463
+ const generatedSet = new Set(generatedFiles);
2464
+ await copyPublicDir(publicDir, outDir, generatedSet);
2465
+ }
2466
+ return {
2467
+ outDir,
2468
+ routes,
2469
+ generatedFiles: []
2470
+ };
2471
+ }
2472
+ const runtimePath = await bundleRuntime({ outDir });
2473
+ for (const route of jsonPages) {
2474
+ const relPath = relative2(routesDir, route.file);
2475
+ const content = readFileSync5(route.file, "utf-8");
2476
+ const page = validateJsonPage(content, route.file);
2477
+ if (isDynamicRoute(route.pattern)) {
2478
+ const staticPaths = await loadGetStaticPaths(route.file);
2479
+ if (!staticPaths) {
2480
+ continue;
2481
+ }
2482
+ if (!staticPaths.paths || !Array.isArray(staticPaths.paths)) {
2483
+ throw new Error(`Invalid getStaticPaths format in ${route.file}`);
2484
+ }
2485
+ for (const pathEntry of staticPaths.paths) {
2486
+ const params = pathEntry.params;
2487
+ const outputPath = paramsToOutputPath(route.pattern, params, outDir);
2488
+ const loader = new JsonPageLoader(routesDir);
2489
+ let pageInfo = await loader.loadPage(relPath);
2490
+ if (layoutsDir) {
2491
+ pageInfo = await processLayouts(pageInfo, layoutsDir);
2492
+ }
2493
+ const program = await convertToCompiledProgram(pageInfo);
2494
+ const html = await renderPageToHtml(program, params, runtimePath);
2495
+ await mkdir2(dirname5(outputPath), { recursive: true });
2496
+ await writeFile(outputPath, html, "utf-8");
2497
+ generatedFiles.push(outputPath);
2498
+ let routePath = route.pattern;
2499
+ for (const [key, value] of Object.entries(params)) {
2500
+ routePath = routePath.replace(`:${key}`, value);
2501
+ routePath = routePath.replace("*", value);
2502
+ }
2503
+ routes.push(routePath);
2504
+ }
2505
+ } else {
2506
+ const outputPath = getOutputPath(relPath, outDir);
2507
+ const loader = new JsonPageLoader(routesDir);
2508
+ let pageInfo = await loader.loadPage(relPath);
2509
+ if (layoutsDir) {
2510
+ pageInfo = await processLayouts(pageInfo, layoutsDir);
2511
+ }
2512
+ const program = await convertToCompiledProgram(pageInfo);
2513
+ const html = await renderPageToHtml(program, {}, runtimePath);
2514
+ await mkdir2(dirname5(outputPath), { recursive: true });
2515
+ await writeFile(outputPath, html, "utf-8");
2516
+ generatedFiles.push(outputPath);
2517
+ }
2518
+ }
2519
+ if (publicDir) {
2520
+ const generatedSet = new Set(generatedFiles);
2521
+ await copyPublicDir(publicDir, outDir, generatedSet);
2522
+ }
2523
+ return {
2524
+ outDir,
2525
+ routes,
2526
+ generatedFiles
2527
+ };
2528
+ }
2529
+
2530
+ export {
2531
+ filePathToPattern,
2532
+ scanRoutes,
2533
+ isPathSafe,
2534
+ getMimeType,
2535
+ resolveStaticFile,
2536
+ mdxToConstela,
2537
+ mdxContentToNode2 as mdxContentToNode,
2538
+ loadComponentDefinitions,
2539
+ transformMdx,
2540
+ transformYaml,
2541
+ transformCsv,
2542
+ loadGlob,
2543
+ loadFile,
2544
+ loadApi,
2545
+ generateStaticPaths,
2546
+ DataLoader,
2547
+ scanLayouts,
2548
+ resolveLayout,
2549
+ loadLayout,
2550
+ LayoutResolver,
2551
+ createDevServer,
2552
+ build2 as build
2553
+ };