@sigx/ssg 0.1.6

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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +358 -0
  3. package/dist/build.d.ts +17 -0
  4. package/dist/build.d.ts.map +1 -0
  5. package/dist/build.js +309 -0
  6. package/dist/build.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +62 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/client.d.ts +28 -0
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +38 -0
  14. package/dist/client.js.map +1 -0
  15. package/dist/config.d.ts +33 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/dev.d.ts +43 -0
  18. package/dist/dev.d.ts.map +1 -0
  19. package/dist/dev.js +71 -0
  20. package/dist/dev.js.map +1 -0
  21. package/dist/errors.d.ts +58 -0
  22. package/dist/errors.d.ts.map +1 -0
  23. package/dist/index.d.ts +14 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +58 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/layouts/index.d.ts +7 -0
  28. package/dist/layouts/index.d.ts.map +1 -0
  29. package/dist/layouts/resolver.d.ts +49 -0
  30. package/dist/layouts/resolver.d.ts.map +1 -0
  31. package/dist/layouts/virtual.d.ts +34 -0
  32. package/dist/layouts/virtual.d.ts.map +1 -0
  33. package/dist/mdx/frontmatter.d.ts +42 -0
  34. package/dist/mdx/frontmatter.d.ts.map +1 -0
  35. package/dist/mdx/index.d.ts +9 -0
  36. package/dist/mdx/index.d.ts.map +1 -0
  37. package/dist/mdx/plugin.d.ts +32 -0
  38. package/dist/mdx/plugin.d.ts.map +1 -0
  39. package/dist/mdx/rehype-headings.d.ts +39 -0
  40. package/dist/mdx/rehype-headings.d.ts.map +1 -0
  41. package/dist/mdx/shiki.d.ts +32 -0
  42. package/dist/mdx/shiki.d.ts.map +1 -0
  43. package/dist/plugin-DxH1VUGC.js +547 -0
  44. package/dist/plugin-DxH1VUGC.js.map +1 -0
  45. package/dist/routing/index.d.ts +12 -0
  46. package/dist/routing/index.d.ts.map +1 -0
  47. package/dist/routing/navigation.d.ts +44 -0
  48. package/dist/routing/navigation.d.ts.map +1 -0
  49. package/dist/routing/resolver.d.ts +66 -0
  50. package/dist/routing/resolver.d.ts.map +1 -0
  51. package/dist/routing/scanner.d.ts +60 -0
  52. package/dist/routing/scanner.d.ts.map +1 -0
  53. package/dist/routing/virtual-navigation.d.ts +39 -0
  54. package/dist/routing/virtual-navigation.d.ts.map +1 -0
  55. package/dist/routing/virtual.d.ts +28 -0
  56. package/dist/routing/virtual.d.ts.map +1 -0
  57. package/dist/sitemap.d.ts +55 -0
  58. package/dist/sitemap.d.ts.map +1 -0
  59. package/dist/types.d.ts +562 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/virtual-entries-Bz97SKQ0.js +1053 -0
  62. package/dist/virtual-entries-Bz97SKQ0.js.map +1 -0
  63. package/dist/vite/index.d.ts +6 -0
  64. package/dist/vite/index.d.ts.map +1 -0
  65. package/dist/vite/plugin.d.ts +35 -0
  66. package/dist/vite/plugin.d.ts.map +1 -0
  67. package/dist/vite/plugin.js +3 -0
  68. package/dist/vite/virtual-entries.d.ts +59 -0
  69. package/dist/vite/virtual-entries.d.ts.map +1 -0
  70. package/package.json +92 -0
@@ -0,0 +1,1053 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import fg from "fast-glob";
4
+ import matter from "gray-matter";
5
+ function defineSSGConfig(config) {
6
+ return {
7
+ pages: "src/pages",
8
+ layouts: "src/layouts",
9
+ content: "src/content",
10
+ defaultLayout: "default",
11
+ outDir: "dist",
12
+ base: "/",
13
+ autoEntries: true,
14
+ prefetch: true,
15
+ ...config,
16
+ site: {
17
+ lang: "en",
18
+ ...config.site
19
+ },
20
+ markdown: {
21
+ shiki: true,
22
+ ...config.markdown
23
+ },
24
+ toc: {
25
+ minLevel: 2,
26
+ maxLevel: 3,
27
+ ...config.toc
28
+ }
29
+ };
30
+ }
31
+ async function loadConfig(configPath) {
32
+ const fsPath = await import("node:path");
33
+ const fs = await import("node:fs");
34
+ const { pathToFileURL } = await import("node:url");
35
+ const os = await import("node:os");
36
+ const cwd = process.cwd();
37
+ const possiblePaths = configPath ? [fsPath.resolve(cwd, configPath)] : [
38
+ fsPath.resolve(cwd, "ssg.config.ts"),
39
+ fsPath.resolve(cwd, "ssg.config.js"),
40
+ fsPath.resolve(cwd, "ssg.config.mjs")
41
+ ];
42
+ let foundPath = null;
43
+ for (const p of possiblePaths) if (fs.existsSync(p)) {
44
+ foundPath = p;
45
+ break;
46
+ }
47
+ if (!foundPath) {
48
+ console.warn("No ssg.config found, using defaults");
49
+ return defineSSGConfig({});
50
+ }
51
+ try {
52
+ if (foundPath.endsWith(".ts")) {
53
+ const esbuild = await import("esbuild");
54
+ const tempDir = os.tmpdir();
55
+ fsPath.join(tempDir, `ssg-config-${Date.now()}.mjs`);
56
+ const source = fs.readFileSync(foundPath, "utf-8");
57
+ const result = await esbuild.transform(source, {
58
+ loader: "ts",
59
+ format: "esm"
60
+ });
61
+ const configDir = fsPath.dirname(foundPath);
62
+ const localTempFile = fsPath.join(configDir, `.ssg-config-temp-${Date.now()}.mjs`);
63
+ fs.writeFileSync(localTempFile, result.code);
64
+ try {
65
+ const configModule = await import(pathToFileURL(localTempFile).href);
66
+ return defineSSGConfig(configModule.default || configModule);
67
+ } finally {
68
+ try {
69
+ fs.unlinkSync(localTempFile);
70
+ } catch {}
71
+ }
72
+ }
73
+ const configModule = await import(pathToFileURL(foundPath).href);
74
+ return defineSSGConfig(configModule.default || configModule);
75
+ } catch (err) {
76
+ console.error(`Failed to load config from ${foundPath}:`, err);
77
+ return defineSSGConfig({});
78
+ }
79
+ }
80
+ function resolveConfigPaths(config, root) {
81
+ return {
82
+ ...config,
83
+ pages: path.resolve(root, config.pages || "src/pages"),
84
+ layouts: path.resolve(root, config.layouts || "src/layouts"),
85
+ content: path.resolve(root, config.content || "src/content"),
86
+ outDir: path.resolve(root, config.outDir || "dist")
87
+ };
88
+ }
89
+ function parseFrontmatter(source) {
90
+ const { data, content, matter: raw } = matter(source);
91
+ return {
92
+ data: normalizeFrontmatter(data),
93
+ content,
94
+ raw: raw || "",
95
+ hasFrontmatter: !!raw
96
+ };
97
+ }
98
+ function normalizeFrontmatter(data) {
99
+ const meta = {};
100
+ if (typeof data.title === "string") meta.title = data.title;
101
+ if (typeof data.description === "string") meta.description = data.description;
102
+ if (typeof data.layout === "string") meta.layout = data.layout;
103
+ if (typeof data.draft === "boolean") meta.draft = data.draft;
104
+ if (data.date) {
105
+ if (data.date instanceof Date) meta.date = data.date;
106
+ else if (typeof data.date === "string") meta.date = new Date(data.date);
107
+ }
108
+ if (Array.isArray(data.tags)) meta.tags = data.tags.filter((t) => typeof t === "string");
109
+ if (typeof data.ssr === "boolean") meta.ssr = data.ssr;
110
+ for (const [key, value] of Object.entries(data)) if (!(key in meta)) meta[key] = value;
111
+ return meta;
112
+ }
113
+ function extractTitleFromContent(content) {
114
+ const h1Match = content.match(/^#\s+(.+)$/m);
115
+ return h1Match ? h1Match[1].trim() : null;
116
+ }
117
+ var PAGE_EXTENSIONS = [
118
+ ".tsx",
119
+ ".jsx",
120
+ ".mdx",
121
+ ".md"
122
+ ];
123
+ var EXCLUDED_PATTERNS = [
124
+ "components/**",
125
+ "hooks/**",
126
+ "utils/**",
127
+ "lib/**",
128
+ "**/_*",
129
+ "**/*.test.*",
130
+ "**/*.spec.*"
131
+ ];
132
+ async function scanPages(config, root) {
133
+ const pagesDir = path.resolve(root, config.pages || "src/pages");
134
+ const files = await fg(PAGE_EXTENSIONS.map((ext) => `**/*${ext}`), {
135
+ cwd: pagesDir,
136
+ ignore: EXCLUDED_PATTERNS,
137
+ onlyFiles: true,
138
+ absolute: false
139
+ });
140
+ const routes = [];
141
+ for (const file of files) {
142
+ const route = await fileToRouteWithMeta(file, pagesDir);
143
+ if (route) routes.push(route);
144
+ }
145
+ return sortRoutes(routes);
146
+ }
147
+ async function fileToRouteWithMeta(filePath, pagesDir) {
148
+ const route = fileToRoute(filePath, pagesDir);
149
+ if (!route) return null;
150
+ const ext = path.extname(filePath).toLowerCase();
151
+ if (ext === ".mdx" || ext === ".md") try {
152
+ const { data } = parseFrontmatter(fs.readFileSync(route.file, "utf-8"));
153
+ route.meta = data;
154
+ } catch (err) {}
155
+ return route;
156
+ }
157
+ function fileToRoute(filePath, pagesDir) {
158
+ const ext = path.extname(filePath);
159
+ let routePath = filePath.slice(0, -ext.length);
160
+ if (routePath.endsWith("/index") || routePath === "index") routePath = routePath.replace(/\/?index$/, "") || "/";
161
+ routePath = filePathToRoutePath(routePath);
162
+ const name = pathToRouteName(routePath);
163
+ return {
164
+ path: routePath.startsWith("/") ? routePath : `/${routePath}`,
165
+ file: path.join(pagesDir, filePath),
166
+ name
167
+ };
168
+ }
169
+ function filePathToRoutePath(filePath) {
170
+ return filePath.split("/").map((segment) => {
171
+ if (segment.startsWith("[...") && segment.endsWith("]")) return `*${segment.slice(4, -1)}`;
172
+ if (segment.startsWith("[[...") && segment.endsWith("]]")) return `*${segment.slice(5, -2)}`;
173
+ if (segment.startsWith("[") && segment.endsWith("]")) return `:${segment.slice(1, -1)}`;
174
+ if (segment.startsWith("[[") && segment.endsWith("]]")) return `:${segment.slice(2, -2)}?`;
175
+ return segment;
176
+ }).join("/");
177
+ }
178
+ function pathToRouteName(routePath) {
179
+ if (routePath === "/" || routePath === "") return "index";
180
+ return routePath.replace(/^\//, "").replace(/\//g, "-").replace(/:/g, "").replace(/\*/g, "").replace(/\?/g, "").replace(/-+/g, "-").replace(/-$/, "");
181
+ }
182
+ function sortRoutes(routes) {
183
+ return routes.sort((a, b) => {
184
+ const scoreA = getRouteScore(a.path);
185
+ const scoreB = getRouteScore(b.path);
186
+ if (scoreA !== scoreB) return scoreB - scoreA;
187
+ return b.path.length - a.path.length;
188
+ });
189
+ }
190
+ function getRouteScore(routePath) {
191
+ const segments = routePath.split("/").filter(Boolean);
192
+ let score = 0;
193
+ for (const segment of segments) if (segment.startsWith("*")) score += 1;
194
+ else if (segment.startsWith(":")) score += 10;
195
+ else score += 100;
196
+ return score;
197
+ }
198
+ function isDynamicRoute(route) {
199
+ return route.path.includes(":") || route.path.includes("*");
200
+ }
201
+ function extractParams(routePath) {
202
+ const params = [];
203
+ const segments = routePath.split("/");
204
+ for (const segment of segments) if (segment.startsWith(":")) params.push(segment.slice(1).replace("?", ""));
205
+ else if (segment.startsWith("*")) params.push(segment.slice(1));
206
+ return params;
207
+ }
208
+ function expandDynamicRoute(route, staticPaths) {
209
+ const paths = [];
210
+ for (const { params } of staticPaths) {
211
+ let expandedPath = route.path;
212
+ for (const [key, value] of Object.entries(params)) {
213
+ expandedPath = expandedPath.replace(`:${key}`, value);
214
+ expandedPath = expandedPath.replace(`*${key}`, value);
215
+ }
216
+ paths.push(expandedPath);
217
+ }
218
+ return paths;
219
+ }
220
+ const VIRTUAL_ROUTES_ID = "virtual:ssg-routes";
221
+ const RESOLVED_VIRTUAL_ROUTES_ID = "\0" + VIRTUAL_ROUTES_ID;
222
+ function normalizePath$1(filePath) {
223
+ return filePath.replace(/\\/g, "/");
224
+ }
225
+ function generateRoutesModule(routes, config) {
226
+ const imports = [];
227
+ const routeDefinitions = [];
228
+ for (let i = 0; i < routes.length; i++) {
229
+ const route = routes[i];
230
+ const componentName = `Page${i}`;
231
+ const metaName = `meta${i}`;
232
+ const normalizedFile = normalizePath$1(route.file);
233
+ imports.push(`import * as ${componentName}Module from '${normalizedFile}';`);
234
+ imports.push(`const ${metaName} = { ...('meta' in ${componentName}Module ? ${componentName}Module.meta : ${componentName}Module.default?.frontmatter || ${JSON.stringify(route.meta || {})}), headings: 'headings' in ${componentName}Module ? ${componentName}Module.headings : [] };`);
235
+ imports.push(`const ${componentName} = ${componentName}Module.default || ${componentName}Module;`);
236
+ routeDefinitions.push(`
237
+ {
238
+ path: '${route.path}',
239
+ name: '${route.name}',
240
+ file: '${normalizedFile}',
241
+ component: ${componentName},
242
+ meta: ${metaName},
243
+ layout: ${metaName}.layout || '${config.defaultLayout || "default"}',
244
+ }`);
245
+ }
246
+ return `
247
+ ${imports.join("\n")}
248
+
249
+ const routes = [${routeDefinitions.join(",")}
250
+ ];
251
+
252
+ export default routes;
253
+ `;
254
+ }
255
+ function generateLazyRoutesModule(routes, config) {
256
+ const imports = [];
257
+ const routeDefinitions = [];
258
+ for (let i = 0; i < routes.length; i++) {
259
+ const route = routes[i];
260
+ const componentName = `Page${i}`;
261
+ const metaName = `meta${i}`;
262
+ const normalizedFile = normalizePath$1(route.file);
263
+ imports.push(`import * as ${componentName}Module from '${normalizedFile}';`);
264
+ imports.push(`const ${metaName} = { ...('meta' in ${componentName}Module ? ${componentName}Module.meta : ${componentName}Module.default?.frontmatter || ${JSON.stringify(route.meta || {})}), headings: 'headings' in ${componentName}Module ? ${componentName}Module.headings : [] };`);
265
+ routeDefinitions.push(`
266
+ {
267
+ path: '${route.path}',
268
+ name: '${route.name}',
269
+ file: '${normalizedFile}',
270
+ component: () => import('${normalizedFile}'),
271
+ meta: ${metaName},
272
+ layout: ${metaName}.layout || '${config.defaultLayout || "default"}',
273
+ }`);
274
+ }
275
+ return `
276
+ ${imports.join("\n")}
277
+
278
+ const routes = [${routeDefinitions.join(",")}
279
+ ];
280
+
281
+ export default routes;
282
+ `;
283
+ }
284
+ function generateNavigation(routes, collectionPath, showDrafts, isDev) {
285
+ const navRoutes = routes.filter((route) => {
286
+ if (!route.path.startsWith(collectionPath)) return false;
287
+ const meta = route.meta || {};
288
+ if (meta.sidebar === false) return false;
289
+ if (meta.draft) {
290
+ if (showDrafts === "never") return false;
291
+ if (showDrafts === "dev" && !isDev) return false;
292
+ }
293
+ return true;
294
+ });
295
+ const context = {
296
+ categories: /* @__PURE__ */ new Map(),
297
+ uncategorized: []
298
+ };
299
+ for (const route of navRoutes) {
300
+ const meta = route.meta || {};
301
+ const title = meta.title || routeToTitle(route.path);
302
+ const order = typeof meta.order === "number" ? meta.order : 999;
303
+ const category = meta.category;
304
+ const item = {
305
+ title,
306
+ href: route.path,
307
+ order
308
+ };
309
+ if (!category) context.uncategorized.push(item);
310
+ else if (typeof category === "string") addToCategory(context.categories, [category], item);
311
+ else if (Array.isArray(category)) addToCategory(context.categories, category, item);
312
+ }
313
+ return { sidebar: buildSections(context) };
314
+ }
315
+ function addToCategory(categories, path, item) {
316
+ if (path.length === 0) return;
317
+ const [first, ...rest] = path;
318
+ let category = categories.get(first);
319
+ if (!category) {
320
+ category = {
321
+ title: first,
322
+ order: void 0,
323
+ items: [],
324
+ children: /* @__PURE__ */ new Map()
325
+ };
326
+ categories.set(first, category);
327
+ }
328
+ if (rest.length === 0) category.items.push(item);
329
+ else addToCategory(category.children, rest, item);
330
+ }
331
+ var SECTION_ORDER = {
332
+ "Getting Started": 10,
333
+ "Core Concepts": 20,
334
+ "Core": 20,
335
+ "Built-in Components": 30,
336
+ "Components": 30,
337
+ "Guides": 40,
338
+ "Advanced": 50,
339
+ "Ecosystem": 60,
340
+ "API Reference": 70,
341
+ "API": 70,
342
+ "Reference": 80,
343
+ "Actions": 100,
344
+ "Data Display": 110,
345
+ "Navigation": 120,
346
+ "Feedback": 130,
347
+ "Data Input": 140,
348
+ "Layout": 150,
349
+ "Mockup": 160,
350
+ "Other": 999
351
+ };
352
+ function getSectionOrder(title, explicitOrder) {
353
+ if (explicitOrder !== void 0) return explicitOrder;
354
+ return SECTION_ORDER[title] ?? 50;
355
+ }
356
+ function buildSections(context) {
357
+ const sections = [];
358
+ for (const [, category] of context.categories) sections.push(buildSection(category));
359
+ if (context.uncategorized.length > 0) sections.push({
360
+ title: "Other",
361
+ items: context.uncategorized.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title)).map((item) => ({
362
+ title: item.title,
363
+ href: item.href,
364
+ order: item.order
365
+ })),
366
+ order: 999
367
+ });
368
+ sections.sort((a, b) => {
369
+ return getSectionOrder(a.title, a.order) - getSectionOrder(b.title, b.order);
370
+ });
371
+ return sections;
372
+ }
373
+ function buildSection(category) {
374
+ const sortedItems = category.items.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title)).map((item) => ({
375
+ title: item.title,
376
+ href: item.href,
377
+ order: item.order
378
+ }));
379
+ const childSections = [];
380
+ for (const [, child] of category.children) childSections.push(buildNestedSection(child));
381
+ const items = [...sortedItems];
382
+ for (const nested of childSections.sort((a, b) => {
383
+ return getSectionOrder(a.title, a.order) - getSectionOrder(b.title, b.order);
384
+ })) items.push(nested);
385
+ return {
386
+ title: category.title,
387
+ items,
388
+ order: category.order
389
+ };
390
+ }
391
+ function buildNestedSection(category) {
392
+ const sortedItems = category.items.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title)).map((item) => ({
393
+ title: item.title,
394
+ href: item.href,
395
+ order: item.order
396
+ }));
397
+ const childItems = [];
398
+ for (const [, child] of category.children) childItems.push(buildNestedSection(child));
399
+ return {
400
+ title: category.title,
401
+ items: [...sortedItems, ...childItems.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))],
402
+ order: category.order
403
+ };
404
+ }
405
+ function routeToTitle(routePath) {
406
+ const segments = routePath.split("/").filter(Boolean);
407
+ return (segments[segments.length - 1] || "Home").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
408
+ }
409
+ function generateAllCollections(routes, config, isDev) {
410
+ const collections = config.collections || {};
411
+ const result = {};
412
+ for (const [name, collectionConfig] of Object.entries(collections)) {
413
+ const showDrafts = collectionConfig.showDrafts ?? config.navigation?.showDrafts ?? "dev";
414
+ result[name] = generateNavigation(routes, collectionConfig.path, showDrafts, isDev);
415
+ }
416
+ return result;
417
+ }
418
+ const VIRTUAL_NAVIGATION_ID = "virtual:ssg-navigation";
419
+ const RESOLVED_VIRTUAL_NAVIGATION_ID = "\0" + VIRTUAL_NAVIGATION_ID;
420
+ function generateNavigationModule(routes, config, isDev) {
421
+ const navigation = generateAllCollections(routes, config, isDev);
422
+ const collections = config.collections || {};
423
+ return `
424
+ /**
425
+ * Auto-generated navigation module
426
+ *
427
+ * @generated by @sigx/ssg
428
+ */
429
+
430
+ /**
431
+ * Navigation for all collections, keyed by collection name
432
+ */
433
+ export const navigation = ${JSON.stringify(navigation, null, 2)};
434
+
435
+ /**
436
+ * Collections configuration
437
+ */
438
+ const collections = ${JSON.stringify(collections, null, 2)};
439
+
440
+ /**
441
+ * Get navigation for a specific collection
442
+ * @param name - Collection name (e.g., 'docs', 'examples')
443
+ * @returns The collection's navigation or undefined if not found
444
+ */
445
+ export function getCollectionNav(name) {
446
+ return navigation[name];
447
+ }
448
+
449
+ /**
450
+ * Detect which collection a route path belongs to
451
+ * @param path - Current route path
452
+ * @returns The collection name if found, undefined otherwise
453
+ */
454
+ export function detectCollection(path) {
455
+ // Sort by path length descending to match most specific path first
456
+ const sorted = Object.entries(collections).sort(
457
+ ([, a], [, b]) => b.path.length - a.path.length
458
+ );
459
+
460
+ for (const [name, config] of sorted) {
461
+ if (path.startsWith(config.path)) {
462
+ return name;
463
+ }
464
+ }
465
+
466
+ return undefined;
467
+ }
468
+
469
+ /**
470
+ * Get sidebar for a specific collection
471
+ * @param name - Collection name
472
+ * @returns The sidebar array or empty array if not found
473
+ */
474
+ export function getSidebar(name) {
475
+ return navigation[name]?.sidebar || [];
476
+ }
477
+
478
+ export default { navigation, collections, getCollectionNav, detectCollection, getSidebar };
479
+ `;
480
+ }
481
+ var LAYOUT_EXTENSIONS = [".tsx", ".jsx"];
482
+ async function scanLocalLayouts(config, root) {
483
+ const layoutsDir = path.resolve(root, config.layouts || "src/layouts");
484
+ if (!fs.existsSync(layoutsDir)) return [];
485
+ return (await fg(LAYOUT_EXTENSIONS.map((ext) => `*${ext}`), {
486
+ cwd: layoutsDir,
487
+ onlyFiles: true,
488
+ absolute: false
489
+ })).map((file) => {
490
+ const ext = path.extname(file);
491
+ return {
492
+ name: file.slice(0, -ext.length),
493
+ file: path.join(layoutsDir, file),
494
+ source: "local"
495
+ };
496
+ });
497
+ }
498
+ async function loadThemeLayouts(themeName, root) {
499
+ try {
500
+ const { createRequire } = await import("node:module");
501
+ const { pathToFileURL } = await import("node:url");
502
+ const themePackageJson = createRequire(path.join(root, "package.json")).resolve(`${themeName}/package.json`);
503
+ const themeDir = path.dirname(themePackageJson);
504
+ const packageJson = JSON.parse(fs.readFileSync(themePackageJson, "utf-8"));
505
+ const mainFile = packageJson.exports?.["."]?.import || packageJson.main || "./dist/index.js";
506
+ const themeModule = await import(pathToFileURL(path.resolve(themeDir, mainFile)).href);
507
+ if (!themeModule.layouts) return [];
508
+ return Object.keys(themeModule.layouts).map((name) => ({
509
+ name,
510
+ file: `${themeName}/layouts/${name}`,
511
+ source: themeName
512
+ }));
513
+ } catch (err) {
514
+ console.warn(`Failed to load theme ${themeName}:`, err);
515
+ return [];
516
+ }
517
+ }
518
+ async function discoverLayouts(config, root) {
519
+ const layouts = /* @__PURE__ */ new Map();
520
+ if (config.theme) {
521
+ const themeLayouts = await loadThemeLayouts(config.theme, root);
522
+ for (const layout of themeLayouts) layouts.set(layout.name, layout);
523
+ }
524
+ const localLayouts = await scanLocalLayouts(config, root);
525
+ for (const layout of localLayouts) layouts.set(layout.name, layout);
526
+ return Array.from(layouts.values());
527
+ }
528
+ const VIRTUAL_LAYOUTS_ID = "virtual:generated-layouts";
529
+ const RESOLVED_VIRTUAL_LAYOUTS_ID = "\0virtual:generated-layouts";
530
+ function normalizePath(filePath) {
531
+ return filePath.replace(/\\/g, "/");
532
+ }
533
+ function generateLayoutsModule(layouts, config) {
534
+ const imports = [];
535
+ const layoutEntries = [];
536
+ for (let i = 0; i < layouts.length; i++) {
537
+ const layout = layouts[i];
538
+ const importName = `Layout${i}`;
539
+ if (layout.source === "local") {
540
+ const normalizedFile = normalizePath(layout.file);
541
+ imports.push(`import ${importName} from '${normalizedFile}';`);
542
+ } else {
543
+ imports.push(`import { layouts as themeLayouts${i} } from '${layout.source}';`);
544
+ imports.push(`const ${importName} = themeLayouts${i}['${layout.name}'];`);
545
+ }
546
+ layoutEntries.push(` '${layout.name}': ${importName}.default || ${importName}`);
547
+ }
548
+ return `
549
+ import { component, signal, jsx } from 'sigx';
550
+ import { useRoute } from '@sigx/router';
551
+ ${imports.join("\n")}
552
+
553
+ /**
554
+ * All available layouts
555
+ */
556
+ export const layouts = {
557
+ ${layoutEntries.join(",\n")}
558
+ };
559
+
560
+ /**
561
+ * Default layout name
562
+ */
563
+ export const defaultLayout = '${config.defaultLayout || "default"}';
564
+
565
+ /**
566
+ * Check if a component is marked as lazy (created by lazy())
567
+ */
568
+ function isMarkedLazy(component) {
569
+ return component && component.__lazy === true;
570
+ }
571
+
572
+ /**
573
+ * Check if a value is a Promise/thenable
574
+ */
575
+ function isPromise(value) {
576
+ return value && typeof value.then === 'function';
577
+ }
578
+
579
+ /**
580
+ * Track hydration state - we're hydrating if the app container has SSR content
581
+ */
582
+ let isHydrating = typeof window !== 'undefined' &&
583
+ document.getElementById('app')?.innerHTML?.trim().length > 0;
584
+
585
+ // After first render, we're no longer hydrating
586
+ if (typeof window !== 'undefined') {
587
+ const markHydrationComplete = () => { isHydrating = false; };
588
+ if ('requestIdleCallback' in window) {
589
+ window.requestIdleCallback(markHydrationComplete);
590
+ } else {
591
+ setTimeout(markHydrationComplete, 0);
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Placeholder component that preserves existing DOM during hydration.
597
+ */
598
+ const HydrationPlaceholder = component(() => {
599
+ return () => null;
600
+ }, { name: 'HydrationPlaceholder' });
601
+
602
+ /**
603
+ * Layout router component that preserves layouts across navigation.
604
+ *
605
+ * This component renders the current route's layout and page content reactively.
606
+ * When navigating between routes with the same layout, only the page content
607
+ * updates - the layout (Navbar, Sidebar, Footer) persists without re-rendering.
608
+ */
609
+ export const LayoutRouter = component((ctx) => {
610
+ const route = useRoute();
611
+
612
+ // Track loaded lazy components to avoid reloading
613
+ const loadedComponents = {};
614
+
615
+ // Track if this is the initial hydration render
616
+ let initialRender = true;
617
+
618
+ // HMR support: Listen for MDX updates and clear the component cache
619
+ if (typeof window !== 'undefined' && import.meta.hot) {
620
+ // Listen for sigx:mdx-hmr events dispatched by MDX files after they accept HMR
621
+ const handleMdxHmr = (event) => {
622
+ const { moduleId, newModule } = event.detail || {};
623
+
624
+ // Clear all cached components to force reload with new module
625
+ for (const key in loadedComponents) {
626
+ delete loadedComponents[key];
627
+ }
628
+
629
+ // If we have the new module directly from HMR, cache it for the matching route
630
+ if (newModule?.default) {
631
+ const currentPath = route.path;
632
+ loadedComponents[currentPath] = newModule.default;
633
+ }
634
+
635
+ // Force re-render
636
+ ctx.update();
637
+ };
638
+
639
+ window.addEventListener('sigx:mdx-hmr', handleMdxHmr);
640
+
641
+ ctx.onUnmounted(() => {
642
+ window.removeEventListener('sigx:mdx-hmr', handleMdxHmr);
643
+ });
644
+ }
645
+
646
+ return () => {
647
+ const matched = route.matched;
648
+ if (!matched || matched.length === 0) return null;
649
+
650
+ const match = matched[0];
651
+ if (!match) return null;
652
+
653
+ // Get layout name from route meta or use default
654
+ const layoutName = match.layout || match.meta?.layout || defaultLayout;
655
+ const Layout = layouts[layoutName];
656
+
657
+ if (!Layout) {
658
+ console.warn(\`Layout "\${layoutName}" not found for route \${route.path}\`);
659
+ return null;
660
+ }
661
+
662
+ // Get the original (unwrapped) component
663
+ const rawComponent = match.originalComponent || match.component;
664
+ const routePath = route.path;
665
+
666
+ // Handle lazy/dynamic import components
667
+ if (isMarkedLazy(rawComponent) || (typeof rawComponent === 'function' && !rawComponent.__setup)) {
668
+ // Check if already loaded
669
+ if (loadedComponents[routePath]) {
670
+ const PageComponent = loadedComponents[routePath];
671
+ initialRender = false;
672
+ return jsx(Layout, {
673
+ meta: match.meta,
674
+ path: routePath,
675
+ key: layoutName, // Key by layout to preserve layout across pages
676
+ children: PageComponent({})
677
+ });
678
+ }
679
+
680
+ // Load the component
681
+ const loadState = signal({ value: null, loading: true, error: null });
682
+
683
+ try {
684
+ const result = rawComponent();
685
+ if (isPromise(result)) {
686
+ result.then(module => {
687
+ const Component = module.default || module;
688
+ loadedComponents[routePath] = Component;
689
+ loadState.value = Component;
690
+ loadState.loading = false;
691
+ }).catch(err => {
692
+ console.error('Failed to load component for route:', routePath, err);
693
+ loadState.error = err;
694
+ loadState.loading = false;
695
+ });
696
+ } else {
697
+ // Not a promise, use directly
698
+ loadedComponents[routePath] = rawComponent;
699
+ loadState.value = rawComponent;
700
+ loadState.loading = false;
701
+ }
702
+ } catch (err) {
703
+ loadState.error = err;
704
+ loadState.loading = false;
705
+ }
706
+
707
+ // During hydration, preserve existing SSR content instead of showing loading state
708
+ if (loadState.loading) {
709
+ if (isHydrating && initialRender) {
710
+ return jsx(Layout, {
711
+ meta: match.meta,
712
+ path: routePath,
713
+ key: layoutName,
714
+ children: jsx(HydrationPlaceholder, {})
715
+ });
716
+ }
717
+ return jsx(Layout, {
718
+ meta: match.meta,
719
+ path: routePath,
720
+ key: layoutName,
721
+ children: null
722
+ });
723
+ }
724
+
725
+ if (loadState.error || !loadState.value) {
726
+ return jsx(Layout, {
727
+ meta: match.meta,
728
+ path: routePath,
729
+ key: layoutName,
730
+ children: null
731
+ });
732
+ }
733
+
734
+ const PageComponent = loadState.value;
735
+ initialRender = false;
736
+ return jsx(Layout, {
737
+ meta: match.meta,
738
+ path: routePath,
739
+ key: layoutName,
740
+ children: PageComponent({})
741
+ });
742
+ }
743
+
744
+ // Eager component (has __setup)
745
+ // Check cache first for HMR-updated components
746
+ if (loadedComponents[routePath]) {
747
+ const PageComponent = loadedComponents[routePath];
748
+ initialRender = false;
749
+ return jsx(Layout, {
750
+ meta: match.meta,
751
+ path: routePath,
752
+ key: layoutName,
753
+ children: PageComponent({})
754
+ });
755
+ }
756
+
757
+ initialRender = false;
758
+ return jsx(Layout, {
759
+ meta: match.meta,
760
+ path: routePath,
761
+ key: layoutName,
762
+ children: rawComponent({})
763
+ });
764
+ };
765
+ }, { name: 'LayoutRouter' });
766
+
767
+ /**
768
+ * Setup layouts for routes
769
+ *
770
+ * This function now simply annotates routes with their layout information.
771
+ * The actual layout wrapping is handled by LayoutRouter, which preserves
772
+ * layouts across navigation.
773
+ */
774
+ export function setupLayouts(routes) {
775
+ const availableLayoutNames = Object.keys(layouts);
776
+
777
+ return routes.map(route => {
778
+ const layoutName = route.layout || route.meta?.layout || defaultLayout;
779
+ const Layout = layouts[layoutName];
780
+
781
+ if (!Layout) {
782
+ console.error(
783
+ \`❌ SSG103: Layout "\${layoutName}" not found for route \${route.path}\\n\` +
784
+ \` 📁 \${route.file || 'unknown'}\\n\` +
785
+ \` 💡 Available layouts: \${availableLayoutNames.join(', ') || 'none'}\\n\` +
786
+ \` Create src/layouts/\${layoutName}.tsx or set a valid layout in frontmatter.\`
787
+ );
788
+ return route;
789
+ }
790
+
791
+ // Store layout info on the route, but don't wrap the component
792
+ // LayoutRouter will handle layout rendering
793
+ return {
794
+ ...route,
795
+ layout: layoutName,
796
+ originalComponent: route.component,
797
+ meta: {
798
+ ...route.meta,
799
+ layout: layoutName,
800
+ },
801
+ };
802
+ });
803
+ }
804
+
805
+ export default layouts;
806
+ `;
807
+ }
808
+ const VIRTUAL_CLIENT_ID = "virtual:ssg-client";
809
+ const RESOLVED_VIRTUAL_CLIENT_ID = "\0" + VIRTUAL_CLIENT_ID + ".tsx";
810
+ const SSG_CLIENT_ENTRY_PATH = "/@ssg/client.tsx";
811
+ const VIRTUAL_SERVER_ID = "virtual:ssg-server";
812
+ const RESOLVED_VIRTUAL_SERVER_ID = "\0" + VIRTUAL_SERVER_ID + ".tsx";
813
+ function detectCustomEntries(root, config) {
814
+ const clientPaths = [
815
+ "src/main.tsx",
816
+ "src/main.ts",
817
+ "src/entry-client.tsx",
818
+ "src/entry-client.ts",
819
+ "src/entry.tsx",
820
+ "src/entry.ts"
821
+ ];
822
+ const serverPaths = ["src/entry-server.tsx", "src/entry-server.ts"];
823
+ const htmlPaths = ["index.html"];
824
+ const cssPaths = [
825
+ "src/styles/global.css",
826
+ "src/styles/main.css",
827
+ "src/styles/index.css",
828
+ "src/style.css",
829
+ "src/global.css",
830
+ "src/index.css"
831
+ ];
832
+ let customClientPath;
833
+ let customServerPath;
834
+ let customHtmlPath;
835
+ let globalCssPath;
836
+ for (const p of clientPaths) {
837
+ const fullPath = path.join(root, p);
838
+ if (fs.existsSync(fullPath)) {
839
+ customClientPath = fullPath;
840
+ break;
841
+ }
842
+ }
843
+ for (const p of serverPaths) {
844
+ const fullPath = path.join(root, p);
845
+ if (fs.existsSync(fullPath)) {
846
+ customServerPath = fullPath;
847
+ break;
848
+ }
849
+ }
850
+ for (const p of htmlPaths) {
851
+ const fullPath = path.join(root, p);
852
+ if (fs.existsSync(fullPath)) {
853
+ customHtmlPath = fullPath;
854
+ break;
855
+ }
856
+ }
857
+ for (const p of cssPaths) {
858
+ const fullPath = path.join(root, p);
859
+ if (fs.existsSync(fullPath)) {
860
+ globalCssPath = "/" + p.replace(/\\/g, "/");
861
+ break;
862
+ }
863
+ }
864
+ return {
865
+ useVirtualClient: !customClientPath,
866
+ useVirtualServer: !customServerPath,
867
+ useVirtualHtml: !customHtmlPath,
868
+ customClientPath,
869
+ customServerPath,
870
+ customHtmlPath,
871
+ globalCssPath
872
+ };
873
+ }
874
+ function generateClientEntry(config, detection) {
875
+ const cssImport = detection.globalCssPath ? `import '${detection.globalCssPath}';\n` : "";
876
+ const additionalImports = (config.clientImports ?? []).map((imp) => `import '${imp}';`).join("\n");
877
+ const additionalImportsBlock = additionalImports ? `\n${additionalImports}\n` : "";
878
+ const prefetchDelay = typeof config.prefetch === "object" ? config.prefetch.delay ?? 100 : 100;
879
+ const prefetchEnabled = config.prefetch !== false;
880
+ return `/**
881
+ * Auto-generated client entry point
882
+ * This file is generated by @sigx/ssg when no custom entry is detected.
883
+ * To customize, create src/main.tsx or src/entry-client.tsx
884
+ */
885
+ ${cssImport}${additionalImportsBlock}
886
+ import { defineApp, component } from 'sigx';
887
+ import { createRouter, createWebHistory } from '@sigx/router';
888
+ import { ssrClientPlugin } from '@sigx/ssg/client';
889
+ import routes from 'virtual:ssg-routes';
890
+ import { setupLayouts, LayoutRouter } from 'virtual:generated-layouts';
891
+ ${prefetchEnabled ? `import { setupPrefetch } from '@sigx/ssg/client';` : ""}
892
+
893
+ // Apply layouts to routes (annotates routes with layout info)
894
+ const layoutRoutes = setupLayouts(routes);
895
+
896
+ // Create router with browser history
897
+ const router = createRouter({
898
+ history: createWebHistory('${config.base || "/"}'),
899
+ routes: layoutRoutes,
900
+ });
901
+
902
+ // Root app component - uses LayoutRouter which preserves layouts across navigation
903
+ const App = component(() => {
904
+ return () => <LayoutRouter />;
905
+ });
906
+
907
+ // Hydrate the server-rendered HTML
908
+ defineApp(<App />)
909
+ .use(router)
910
+ .use(ssrClientPlugin)
911
+ .hydrate('#app');
912
+
913
+ ${prefetchEnabled ? `// Enable link prefetching for faster navigation
914
+ setupPrefetch({ delay: ${prefetchDelay} });` : ""}
915
+ `;
916
+ }
917
+ function generateServerEntry(config) {
918
+ return `/**
919
+ * Auto-generated server entry point
920
+ * This file is generated by @sigx/ssg when no custom entry is detected.
921
+ * To customize, create src/entry-server.tsx
922
+ */
923
+ import { renderToString } from '@sigx/server-renderer/server';
924
+ import { defineApp } from 'sigx';
925
+ import { createRouter, createMemoryHistory } from '@sigx/router';
926
+ import routes from 'virtual:ssg-routes';
927
+ import { setupLayouts, LayoutRouter } from 'virtual:generated-layouts';
928
+
929
+ // Pre-process routes with layouts once at module load time (not per-render)
930
+ const routesWithLayouts = setupLayouts(routes);
931
+
932
+ /**
933
+ * Render the app to HTML string for a given URL
934
+ */
935
+ export async function render(url, context) {
936
+ // Create router with memory history for SSR
937
+ // Note: We must create a new router per render because history is URL-specific
938
+ const router = createRouter({
939
+ routes: routesWithLayouts,
940
+ history: createMemoryHistory({ initialLocation: url || '/' }),
941
+ });
942
+
943
+ // Create app with router - router's install() sets up DI via app.defineProvide()
944
+ const app = defineApp(<LayoutRouter />).use(router);
945
+
946
+ const html = await renderToString(app);
947
+ return html;
948
+ }
949
+ `;
950
+ }
951
+ function generateHtmlTemplate(config) {
952
+ const site = config.site || {};
953
+ const lang = site.lang || "en";
954
+ const title = site.title || "SignalX App";
955
+ const description = site.description || "";
956
+ const favicon = site.favicon || "/favicon.ico";
957
+ const themeColor = site.themeColor || "#000000";
958
+ const ogImage = site.ogImage || "";
959
+ const url = site.url || "";
960
+ const twitter = site.twitter || "";
961
+ const fonts = site.fonts || [];
962
+ let fontLinks = "";
963
+ if (fonts.length > 0) fontLinks = `
964
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
965
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
966
+ <link href="https://fonts.googleapis.com/css2?family=${fonts.join("&family=")}&display=swap" rel="stylesheet" />`;
967
+ let ogTags = "";
968
+ if (url || ogImage) ogTags = `
969
+ <!-- Open Graph -->
970
+ <meta property="og:type" content="website" />
971
+ <meta property="og:title" content="${title}" />
972
+ <meta property="og:description" content="${description}" />${url ? `
973
+ <meta property="og:url" content="${url}" />` : ""}${ogImage ? `
974
+ <meta property="og:image" content="${ogImage}" />` : ""}`;
975
+ let twitterTags = "";
976
+ if (twitter || ogImage) twitterTags = `
977
+ <!-- Twitter Card -->
978
+ <meta name="twitter:card" content="${ogImage ? "summary_large_image" : "summary"}" />${twitter ? `
979
+ <meta name="twitter:site" content="@${twitter}" />` : ""}
980
+ <meta name="twitter:title" content="${title}" />
981
+ <meta name="twitter:description" content="${description}" />${ogImage ? `
982
+ <meta name="twitter:image" content="${ogImage}" />` : ""}`;
983
+ return `<!DOCTYPE html>
984
+ <html lang="${lang}">
985
+ <head>
986
+ <meta charset="UTF-8" />
987
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
988
+ <meta name="description" content="${description}" />
989
+ <meta name="theme-color" content="${themeColor}" />
990
+ <link rel="icon" type="image/x-icon" href="${favicon}" />${fontLinks}${ogTags}${twitterTags}
991
+ <title>${title}</title>
992
+ <!--head-tags-->
993
+ </head>
994
+ <body>
995
+ <div id="app"><!--app-html--></div>
996
+ <script type="module" src="/@ssg/client.tsx"><\/script>
997
+ </body>
998
+ </html>
999
+ `;
1000
+ }
1001
+ function generateProductionHtmlTemplate(config, clientEntryPath) {
1002
+ const site = config.site || {};
1003
+ const lang = site.lang || "en";
1004
+ const title = site.title || "SignalX App";
1005
+ const description = site.description || "";
1006
+ const favicon = site.favicon || "/favicon.ico";
1007
+ const themeColor = site.themeColor || "#000000";
1008
+ const ogImage = site.ogImage || "";
1009
+ const url = site.url || "";
1010
+ const twitter = site.twitter || "";
1011
+ const fonts = site.fonts || [];
1012
+ let fontLinks = "";
1013
+ if (fonts.length > 0) fontLinks = `
1014
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
1015
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
1016
+ <link href="https://fonts.googleapis.com/css2?family=${fonts.join("&family=")}&display=swap" rel="stylesheet" />`;
1017
+ let ogTags = "";
1018
+ if (url || ogImage) ogTags = `
1019
+ <!-- Open Graph -->
1020
+ <meta property="og:type" content="website" />
1021
+ <meta property="og:title" content="${title}" />
1022
+ <meta property="og:description" content="${description}" />${url ? `
1023
+ <meta property="og:url" content="${url}" />` : ""}${ogImage ? `
1024
+ <meta property="og:image" content="${ogImage}" />` : ""}`;
1025
+ let twitterTags = "";
1026
+ if (twitter || ogImage) twitterTags = `
1027
+ <!-- Twitter Card -->
1028
+ <meta name="twitter:card" content="${ogImage ? "summary_large_image" : "summary"}" />${twitter ? `
1029
+ <meta name="twitter:site" content="@${twitter}" />` : ""}
1030
+ <meta name="twitter:title" content="${title}" />
1031
+ <meta name="twitter:description" content="${description}" />${ogImage ? `
1032
+ <meta name="twitter:image" content="${ogImage}" />` : ""}`;
1033
+ return `<!DOCTYPE html>
1034
+ <html lang="${lang}">
1035
+ <head>
1036
+ <meta charset="UTF-8" />
1037
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1038
+ <meta name="description" content="${description}" />
1039
+ <meta name="theme-color" content="${themeColor}" />
1040
+ <link rel="icon" type="image/x-icon" href="${favicon}" />${fontLinks}${ogTags}${twitterTags}
1041
+ <title>${title}</title>
1042
+ <!--head-tags-->
1043
+ </head>
1044
+ <body>
1045
+ <div id="app"><!--app-html--></div>
1046
+ <script type="module" src="${clientEntryPath}"><\/script>
1047
+ </body>
1048
+ </html>
1049
+ `;
1050
+ }
1051
+ export { resolveConfigPaths as A, extractParams as C, parseFrontmatter as D, extractTitleFromContent as E, defineSSGConfig as O, expandDynamicRoute as S, scanPages as T, generateNavigationModule as _, VIRTUAL_SERVER_ID as a, generateLazyRoutesModule as b, generateHtmlTemplate as c, RESOLVED_VIRTUAL_LAYOUTS_ID as d, VIRTUAL_LAYOUTS_ID as f, VIRTUAL_NAVIGATION_ID as g, RESOLVED_VIRTUAL_NAVIGATION_ID as h, VIRTUAL_CLIENT_ID as i, loadConfig as k, generateProductionHtmlTemplate as l, discoverLayouts as m, RESOLVED_VIRTUAL_SERVER_ID as n, detectCustomEntries as o, generateLayoutsModule as p, SSG_CLIENT_ENTRY_PATH as r, generateClientEntry as s, RESOLVED_VIRTUAL_CLIENT_ID as t, generateServerEntry as u, RESOLVED_VIRTUAL_ROUTES_ID as v, isDynamicRoute as w, generateRoutesModule as x, VIRTUAL_ROUTES_ID as y };
1052
+
1053
+ //# sourceMappingURL=virtual-entries-Bz97SKQ0.js.map