@sigx/ssg 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js DELETED
@@ -1,2357 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/build.ts
4
- import path6 from "node:path";
5
- import fs5 from "node:fs/promises";
6
- import fsSync from "node:fs";
7
- import { pathToFileURL } from "node:url";
8
-
9
- // src/config.ts
10
- import path from "node:path";
11
- function defineSSGConfig(config) {
12
- return {
13
- // Defaults
14
- pages: "src/pages",
15
- layouts: "src/layouts",
16
- content: "src/content",
17
- defaultLayout: "default",
18
- outDir: "dist",
19
- base: "/",
20
- // Zero-config defaults
21
- autoEntries: true,
22
- prefetch: true,
23
- // User overrides
24
- ...config,
25
- // Merge nested objects
26
- site: {
27
- lang: "en",
28
- ...config.site
29
- },
30
- markdown: {
31
- shiki: true,
32
- ...config.markdown
33
- },
34
- toc: {
35
- minLevel: 2,
36
- maxLevel: 3,
37
- ...config.toc
38
- }
39
- };
40
- }
41
- async function loadConfig(configPath2) {
42
- const fsPath = await import("node:path");
43
- const fs8 = await import("node:fs");
44
- const { pathToFileURL: pathToFileURL2 } = await import("node:url");
45
- const os = await import("node:os");
46
- const cwd = process.cwd();
47
- const possiblePaths = configPath2 ? [fsPath.resolve(cwd, configPath2)] : [
48
- fsPath.resolve(cwd, "ssg.config.ts"),
49
- fsPath.resolve(cwd, "ssg.config.js"),
50
- fsPath.resolve(cwd, "ssg.config.mjs")
51
- ];
52
- let foundPath = null;
53
- for (const p of possiblePaths) {
54
- if (fs8.existsSync(p)) {
55
- foundPath = p;
56
- break;
57
- }
58
- }
59
- if (!foundPath) {
60
- console.warn("No ssg.config found, using defaults");
61
- return defineSSGConfig({});
62
- }
63
- try {
64
- if (foundPath.endsWith(".ts")) {
65
- const esbuild = await import("esbuild");
66
- const tempDir = os.tmpdir();
67
- const tempFile = fsPath.join(tempDir, `ssg-config-${Date.now()}.mjs`);
68
- const source = fs8.readFileSync(foundPath, "utf-8");
69
- const result = await esbuild.transform(source, {
70
- loader: "ts",
71
- format: "esm"
72
- });
73
- const configDir = fsPath.dirname(foundPath);
74
- const localTempFile = fsPath.join(configDir, `.ssg-config-temp-${Date.now()}.mjs`);
75
- fs8.writeFileSync(localTempFile, result.code);
76
- try {
77
- const configModule2 = await import(pathToFileURL2(localTempFile).href);
78
- return defineSSGConfig(configModule2.default || configModule2);
79
- } finally {
80
- try {
81
- fs8.unlinkSync(localTempFile);
82
- } catch {
83
- }
84
- }
85
- }
86
- const configModule = await import(pathToFileURL2(foundPath).href);
87
- return defineSSGConfig(configModule.default || configModule);
88
- } catch (err) {
89
- console.error(`Failed to load config from ${foundPath}:`, err);
90
- return defineSSGConfig({});
91
- }
92
- }
93
- function resolveConfigPaths(config, root) {
94
- return {
95
- ...config,
96
- pages: path.resolve(root, config.pages || "src/pages"),
97
- layouts: path.resolve(root, config.layouts || "src/layouts"),
98
- content: path.resolve(root, config.content || "src/content"),
99
- outDir: path.resolve(root, config.outDir || "dist")
100
- };
101
- }
102
-
103
- // src/routing/scanner.ts
104
- import fg from "fast-glob";
105
- import path2 from "node:path";
106
- import fs from "node:fs";
107
-
108
- // src/mdx/frontmatter.ts
109
- import matter from "gray-matter";
110
- function parseFrontmatter(source) {
111
- const { data, content, matter: raw } = matter(source);
112
- return {
113
- data: normalizeFrontmatter(data),
114
- content,
115
- raw: raw || "",
116
- hasFrontmatter: !!raw
117
- };
118
- }
119
- function normalizeFrontmatter(data) {
120
- const meta = {};
121
- if (typeof data.title === "string") meta.title = data.title;
122
- if (typeof data.description === "string") meta.description = data.description;
123
- if (typeof data.layout === "string") meta.layout = data.layout;
124
- if (typeof data.draft === "boolean") meta.draft = data.draft;
125
- if (data.date) {
126
- if (data.date instanceof Date) {
127
- meta.date = data.date;
128
- } else if (typeof data.date === "string") {
129
- meta.date = new Date(data.date);
130
- }
131
- }
132
- if (Array.isArray(data.tags)) {
133
- meta.tags = data.tags.filter((t) => typeof t === "string");
134
- }
135
- if (typeof data.ssr === "boolean") meta.ssr = data.ssr;
136
- for (const [key, value] of Object.entries(data)) {
137
- if (!(key in meta)) {
138
- meta[key] = value;
139
- }
140
- }
141
- return meta;
142
- }
143
- function extractTitleFromContent(content) {
144
- const h1Match = content.match(/^#\s+(.+)$/m);
145
- return h1Match ? h1Match[1].trim() : null;
146
- }
147
-
148
- // src/routing/scanner.ts
149
- var PAGE_EXTENSIONS = [".tsx", ".jsx", ".mdx", ".md"];
150
- var EXCLUDED_PATTERNS = [
151
- "components/**",
152
- // Root-level components folder only
153
- "hooks/**",
154
- // Root-level hooks folder only
155
- "utils/**",
156
- // Root-level utils folder only
157
- "lib/**",
158
- // Root-level lib folder only
159
- "**/_*",
160
- // Files/folders starting with underscore (at any level)
161
- "**/*.test.*",
162
- "**/*.spec.*"
163
- ];
164
- async function scanPages(config, root) {
165
- const pagesDir = path2.resolve(root, config.pages || "src/pages");
166
- const patterns = PAGE_EXTENSIONS.map((ext) => `**/*${ext}`);
167
- const files = await fg(patterns, {
168
- cwd: pagesDir,
169
- ignore: EXCLUDED_PATTERNS,
170
- onlyFiles: true,
171
- absolute: false
172
- });
173
- const routes = [];
174
- for (const file of files) {
175
- const route = await fileToRouteWithMeta(file, pagesDir);
176
- if (route) {
177
- routes.push(route);
178
- }
179
- }
180
- return sortRoutes(routes);
181
- }
182
- async function fileToRouteWithMeta(filePath, pagesDir) {
183
- const route = fileToRoute(filePath, pagesDir);
184
- if (!route) return null;
185
- const ext = path2.extname(filePath).toLowerCase();
186
- if (ext === ".mdx" || ext === ".md") {
187
- try {
188
- const content = fs.readFileSync(route.file, "utf-8");
189
- const { data } = parseFrontmatter(content);
190
- route.meta = data;
191
- } catch (err) {
192
- }
193
- }
194
- return route;
195
- }
196
- function fileToRoute(filePath, pagesDir) {
197
- const ext = path2.extname(filePath);
198
- let routePath = filePath.slice(0, -ext.length);
199
- if (routePath.endsWith("/index") || routePath === "index") {
200
- routePath = routePath.replace(/\/?index$/, "") || "/";
201
- }
202
- routePath = filePathToRoutePath(routePath);
203
- const name = pathToRouteName(routePath);
204
- return {
205
- path: routePath.startsWith("/") ? routePath : `/${routePath}`,
206
- file: path2.join(pagesDir, filePath),
207
- name
208
- };
209
- }
210
- function filePathToRoutePath(filePath) {
211
- return filePath.split("/").map((segment) => {
212
- if (segment.startsWith("[...") && segment.endsWith("]")) {
213
- const param = segment.slice(4, -1);
214
- return `*${param}`;
215
- }
216
- if (segment.startsWith("[[...") && segment.endsWith("]]")) {
217
- const param = segment.slice(5, -2);
218
- return `*${param}`;
219
- }
220
- if (segment.startsWith("[") && segment.endsWith("]")) {
221
- const param = segment.slice(1, -1);
222
- return `:${param}`;
223
- }
224
- if (segment.startsWith("[[") && segment.endsWith("]]")) {
225
- const param = segment.slice(2, -2);
226
- return `:${param}?`;
227
- }
228
- return segment;
229
- }).join("/");
230
- }
231
- function pathToRouteName(routePath) {
232
- if (routePath === "/" || routePath === "") {
233
- return "index";
234
- }
235
- return routePath.replace(/^\//, "").replace(/\//g, "-").replace(/:/g, "").replace(/\*/g, "").replace(/\?/g, "").replace(/-+/g, "-").replace(/-$/, "");
236
- }
237
- function sortRoutes(routes) {
238
- return routes.sort((a, b) => {
239
- const scoreA = getRouteScore(a.path);
240
- const scoreB = getRouteScore(b.path);
241
- if (scoreA !== scoreB) {
242
- return scoreB - scoreA;
243
- }
244
- return b.path.length - a.path.length;
245
- });
246
- }
247
- function getRouteScore(routePath) {
248
- const segments = routePath.split("/").filter(Boolean);
249
- let score = 0;
250
- for (const segment of segments) {
251
- if (segment.startsWith("*")) {
252
- score += 1;
253
- } else if (segment.startsWith(":")) {
254
- score += 10;
255
- } else {
256
- score += 100;
257
- }
258
- }
259
- return score;
260
- }
261
- function isDynamicRoute(route) {
262
- return route.path.includes(":") || route.path.includes("*");
263
- }
264
- function extractParams(routePath) {
265
- const params = [];
266
- const segments = routePath.split("/");
267
- for (const segment of segments) {
268
- if (segment.startsWith(":")) {
269
- params.push(segment.slice(1).replace("?", ""));
270
- } else if (segment.startsWith("*")) {
271
- params.push(segment.slice(1));
272
- }
273
- }
274
- return params;
275
- }
276
- function expandDynamicRoute(route, staticPaths) {
277
- const paths = [];
278
- for (const { params } of staticPaths) {
279
- let expandedPath = route.path;
280
- for (const [key, value] of Object.entries(params)) {
281
- expandedPath = expandedPath.replace(`:${key}`, value);
282
- expandedPath = expandedPath.replace(`*${key}`, value);
283
- }
284
- paths.push(expandedPath);
285
- }
286
- return paths;
287
- }
288
-
289
- // src/routing/virtual.ts
290
- var VIRTUAL_ROUTES_ID = "virtual:ssg-routes";
291
- var RESOLVED_VIRTUAL_ROUTES_ID = "\0" + VIRTUAL_ROUTES_ID;
292
- function normalizePath(filePath) {
293
- return filePath.replace(/\\/g, "/");
294
- }
295
- function generateRoutesModule(routes, config) {
296
- const imports = [];
297
- const routeDefinitions = [];
298
- for (let i = 0; i < routes.length; i++) {
299
- const route = routes[i];
300
- const componentName = `Page${i}`;
301
- const metaName = `meta${i}`;
302
- const normalizedFile = normalizePath(route.file);
303
- imports.push(`import * as ${componentName}Module from '${normalizedFile}';`);
304
- imports.push(
305
- `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 : [] };`
306
- );
307
- imports.push(
308
- `const ${componentName} = ${componentName}Module.default || ${componentName}Module;`
309
- );
310
- routeDefinitions.push(`
311
- {
312
- path: '${route.path}',
313
- name: '${route.name}',
314
- file: '${normalizedFile}',
315
- component: ${componentName},
316
- meta: ${metaName},
317
- layout: ${metaName}.layout || '${config.defaultLayout || "default"}',
318
- }`);
319
- }
320
- return `
321
- ${imports.join("\n")}
322
-
323
- const routes = [${routeDefinitions.join(",")}
324
- ];
325
-
326
- export default routes;
327
- `;
328
- }
329
- function generateLazyRoutesModule(routes, config) {
330
- const imports = [];
331
- const routeDefinitions = [];
332
- for (let i = 0; i < routes.length; i++) {
333
- const route = routes[i];
334
- const componentName = `Page${i}`;
335
- const metaName = `meta${i}`;
336
- const normalizedFile = normalizePath(route.file);
337
- imports.push(`import * as ${componentName}Module from '${normalizedFile}';`);
338
- imports.push(
339
- `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 : [] };`
340
- );
341
- routeDefinitions.push(`
342
- {
343
- path: '${route.path}',
344
- name: '${route.name}',
345
- file: '${normalizedFile}',
346
- component: () => import('${normalizedFile}'),
347
- meta: ${metaName},
348
- layout: ${metaName}.layout || '${config.defaultLayout || "default"}',
349
- }`);
350
- }
351
- return `
352
- ${imports.join("\n")}
353
-
354
- const routes = [${routeDefinitions.join(",")}
355
- ];
356
-
357
- export default routes;
358
- `;
359
- }
360
-
361
- // src/routing/navigation.ts
362
- function generateNavigation(routes, collectionPath, showDrafts, isDev) {
363
- const navRoutes = routes.filter((route) => {
364
- if (!route.path.startsWith(collectionPath)) {
365
- return false;
366
- }
367
- const meta = route.meta || {};
368
- if (meta.sidebar === false) {
369
- return false;
370
- }
371
- if (meta.draft) {
372
- if (showDrafts === "never") {
373
- return false;
374
- }
375
- if (showDrafts === "dev" && !isDev) {
376
- return false;
377
- }
378
- }
379
- return true;
380
- });
381
- const context = {
382
- categories: /* @__PURE__ */ new Map(),
383
- uncategorized: []
384
- };
385
- for (const route of navRoutes) {
386
- const meta = route.meta || {};
387
- const title = meta.title || routeToTitle(route.path);
388
- const order = typeof meta.order === "number" ? meta.order : 999;
389
- const category = meta.category;
390
- const item = {
391
- title,
392
- href: route.path,
393
- order
394
- };
395
- if (!category) {
396
- context.uncategorized.push(item);
397
- } else if (typeof category === "string") {
398
- addToCategory(context.categories, [category], item);
399
- } else if (Array.isArray(category)) {
400
- addToCategory(context.categories, category, item);
401
- }
402
- }
403
- const sidebar = buildSections(context);
404
- return { sidebar };
405
- }
406
- function addToCategory(categories, path9, item) {
407
- if (path9.length === 0) {
408
- return;
409
- }
410
- const [first, ...rest] = path9;
411
- let category = categories.get(first);
412
- if (!category) {
413
- category = {
414
- title: first,
415
- order: void 0,
416
- items: [],
417
- children: /* @__PURE__ */ new Map()
418
- };
419
- categories.set(first, category);
420
- }
421
- if (rest.length === 0) {
422
- category.items.push(item);
423
- } else {
424
- addToCategory(category.children, rest, item);
425
- }
426
- }
427
- var SECTION_ORDER = {
428
- "Getting Started": 10,
429
- "Core Concepts": 20,
430
- "Core": 20,
431
- "Built-in Components": 30,
432
- "Components": 30,
433
- "Guides": 40,
434
- "Advanced": 50,
435
- "Ecosystem": 60,
436
- "API Reference": 70,
437
- "API": 70,
438
- "Reference": 80,
439
- // DaisyUI component categories (matching DaisyUI website)
440
- "Actions": 100,
441
- "Data Display": 110,
442
- "Navigation": 120,
443
- "Feedback": 130,
444
- "Data Input": 140,
445
- "Layout": 150,
446
- "Mockup": 160,
447
- "Other": 999
448
- };
449
- function getSectionOrder(title, explicitOrder) {
450
- if (explicitOrder !== void 0) {
451
- return explicitOrder;
452
- }
453
- return SECTION_ORDER[title] ?? 50;
454
- }
455
- function buildSections(context) {
456
- const sections = [];
457
- for (const [, category] of context.categories) {
458
- sections.push(buildSection(category));
459
- }
460
- if (context.uncategorized.length > 0) {
461
- sections.push({
462
- title: "Other",
463
- items: context.uncategorized.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title)).map((item) => ({
464
- title: item.title,
465
- href: item.href,
466
- order: item.order
467
- })),
468
- order: 999
469
- });
470
- }
471
- sections.sort((a, b) => {
472
- const orderA = getSectionOrder(a.title, a.order);
473
- const orderB = getSectionOrder(b.title, b.order);
474
- return orderA - orderB;
475
- });
476
- return sections;
477
- }
478
- function buildSection(category) {
479
- const sortedItems = category.items.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title)).map((item) => ({
480
- title: item.title,
481
- href: item.href,
482
- order: item.order
483
- }));
484
- const childSections = [];
485
- for (const [, child] of category.children) {
486
- childSections.push(buildNestedSection(child));
487
- }
488
- const items = [...sortedItems];
489
- for (const nested of childSections.sort((a, b) => {
490
- const orderA = getSectionOrder(a.title, a.order);
491
- const orderB = getSectionOrder(b.title, b.order);
492
- return orderA - orderB;
493
- })) {
494
- items.push(nested);
495
- }
496
- return {
497
- title: category.title,
498
- items,
499
- order: category.order
500
- };
501
- }
502
- function buildNestedSection(category) {
503
- const sortedItems = category.items.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title)).map((item) => ({
504
- title: item.title,
505
- href: item.href,
506
- order: item.order
507
- }));
508
- const childItems = [];
509
- for (const [, child] of category.children) {
510
- childItems.push(buildNestedSection(child));
511
- }
512
- return {
513
- title: category.title,
514
- items: [...sortedItems, ...childItems.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))],
515
- order: category.order
516
- };
517
- }
518
- function routeToTitle(routePath) {
519
- const segments = routePath.split("/").filter(Boolean);
520
- const lastSegment = segments[segments.length - 1] || "Home";
521
- return lastSegment.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
522
- }
523
- function generateAllCollections(routes, config, isDev) {
524
- const collections = config.collections || {};
525
- const result = {};
526
- for (const [name, collectionConfig] of Object.entries(collections)) {
527
- const showDrafts = collectionConfig.showDrafts ?? config.navigation?.showDrafts ?? "dev";
528
- result[name] = generateNavigation(routes, collectionConfig.path, showDrafts, isDev);
529
- }
530
- return result;
531
- }
532
-
533
- // src/routing/virtual-navigation.ts
534
- var VIRTUAL_NAVIGATION_ID = "virtual:ssg-navigation";
535
- var RESOLVED_VIRTUAL_NAVIGATION_ID = "\0" + VIRTUAL_NAVIGATION_ID;
536
- function generateNavigationModule(routes, config, isDev) {
537
- const navigation = generateAllCollections(routes, config, isDev);
538
- const collections = config.collections || {};
539
- const navJson = JSON.stringify(navigation, null, 2);
540
- const collectionsJson = JSON.stringify(collections, null, 2);
541
- return `
542
- /**
543
- * Auto-generated navigation module
544
- *
545
- * @generated by @sigx/ssg
546
- */
547
-
548
- /**
549
- * Navigation for all collections, keyed by collection name
550
- */
551
- export const navigation = ${navJson};
552
-
553
- /**
554
- * Collections configuration
555
- */
556
- const collections = ${collectionsJson};
557
-
558
- /**
559
- * Get navigation for a specific collection
560
- * @param name - Collection name (e.g., 'docs', 'examples')
561
- * @returns The collection's navigation or undefined if not found
562
- */
563
- export function getCollectionNav(name) {
564
- return navigation[name];
565
- }
566
-
567
- /**
568
- * Detect which collection a route path belongs to
569
- * @param path - Current route path
570
- * @returns The collection name if found, undefined otherwise
571
- */
572
- export function detectCollection(path) {
573
- // Sort by path length descending to match most specific path first
574
- const sorted = Object.entries(collections).sort(
575
- ([, a], [, b]) => b.path.length - a.path.length
576
- );
577
-
578
- for (const [name, config] of sorted) {
579
- if (path.startsWith(config.path)) {
580
- return name;
581
- }
582
- }
583
-
584
- return undefined;
585
- }
586
-
587
- /**
588
- * Get sidebar for a specific collection
589
- * @param name - Collection name
590
- * @returns The sidebar array or empty array if not found
591
- */
592
- export function getSidebar(name) {
593
- return navigation[name]?.sidebar || [];
594
- }
595
-
596
- export default { navigation, collections, getCollectionNav, detectCollection, getSidebar };
597
- `;
598
- }
599
-
600
- // src/layouts/resolver.ts
601
- import fg2 from "fast-glob";
602
- import path3 from "node:path";
603
- import fs2 from "node:fs";
604
- var LAYOUT_EXTENSIONS = [".tsx", ".jsx"];
605
- async function scanLocalLayouts(config, root) {
606
- const layoutsDir = path3.resolve(root, config.layouts || "src/layouts");
607
- if (!fs2.existsSync(layoutsDir)) {
608
- return [];
609
- }
610
- const patterns = LAYOUT_EXTENSIONS.map((ext) => `*${ext}`);
611
- const files = await fg2(patterns, {
612
- cwd: layoutsDir,
613
- onlyFiles: true,
614
- absolute: false
615
- });
616
- return files.map((file) => {
617
- const ext = path3.extname(file);
618
- const name = file.slice(0, -ext.length);
619
- return {
620
- name,
621
- file: path3.join(layoutsDir, file),
622
- source: "local"
623
- };
624
- });
625
- }
626
- async function loadThemeLayouts(themeName, root) {
627
- try {
628
- const { createRequire } = await import("node:module");
629
- const { pathToFileURL: pathToFileURL2 } = await import("node:url");
630
- const require2 = createRequire(path3.join(root, "package.json"));
631
- const themePackageJson = require2.resolve(`${themeName}/package.json`);
632
- const themeDir = path3.dirname(themePackageJson);
633
- const packageJson = JSON.parse(fs2.readFileSync(themePackageJson, "utf-8"));
634
- const mainFile = packageJson.exports?.["."]?.import || packageJson.main || "./dist/index.js";
635
- const themePath = path3.resolve(themeDir, mainFile);
636
- const themeModule = await import(pathToFileURL2(themePath).href);
637
- if (!themeModule.layouts) {
638
- return [];
639
- }
640
- return Object.keys(themeModule.layouts).map((name) => ({
641
- name,
642
- file: `${themeName}/layouts/${name}`,
643
- source: themeName
644
- }));
645
- } catch (err) {
646
- console.warn(`Failed to load theme ${themeName}:`, err);
647
- return [];
648
- }
649
- }
650
- async function discoverLayouts(config, root) {
651
- const layouts = /* @__PURE__ */ new Map();
652
- if (config.theme) {
653
- const themeLayouts = await loadThemeLayouts(config.theme, root);
654
- for (const layout of themeLayouts) {
655
- layouts.set(layout.name, layout);
656
- }
657
- }
658
- const localLayouts = await scanLocalLayouts(config, root);
659
- for (const layout of localLayouts) {
660
- layouts.set(layout.name, layout);
661
- }
662
- return Array.from(layouts.values());
663
- }
664
-
665
- // src/layouts/virtual.ts
666
- var VIRTUAL_LAYOUTS_ID = "virtual:generated-layouts";
667
- var RESOLVED_VIRTUAL_LAYOUTS_ID = "\0virtual:generated-layouts";
668
- function normalizePath2(filePath) {
669
- return filePath.replace(/\\/g, "/");
670
- }
671
- function generateLayoutsModule(layouts, config) {
672
- const imports = [];
673
- const layoutEntries = [];
674
- for (let i = 0; i < layouts.length; i++) {
675
- const layout = layouts[i];
676
- const importName = `Layout${i}`;
677
- if (layout.source === "local") {
678
- const normalizedFile = normalizePath2(layout.file);
679
- imports.push(`import ${importName} from '${normalizedFile}';`);
680
- } else {
681
- imports.push(`import { layouts as themeLayouts${i} } from '${layout.source}';`);
682
- imports.push(`const ${importName} = themeLayouts${i}['${layout.name}'];`);
683
- }
684
- layoutEntries.push(` '${layout.name}': ${importName}.default || ${importName}`);
685
- }
686
- return `
687
- import { component, signal, jsx } from 'sigx';
688
- import { useRoute } from '@sigx/router';
689
- ${imports.join("\n")}
690
-
691
- /**
692
- * All available layouts
693
- */
694
- export const layouts = {
695
- ${layoutEntries.join(",\n")}
696
- };
697
-
698
- /**
699
- * Default layout name
700
- */
701
- export const defaultLayout = '${config.defaultLayout || "default"}';
702
-
703
- /**
704
- * Check if a component is marked as lazy (created by lazy())
705
- */
706
- function isMarkedLazy(component) {
707
- return component && component.__lazy === true;
708
- }
709
-
710
- /**
711
- * Check if a value is a Promise/thenable
712
- */
713
- function isPromise(value) {
714
- return value && typeof value.then === 'function';
715
- }
716
-
717
- /**
718
- * Track hydration state - we're hydrating if the app container has SSR content
719
- */
720
- let isHydrating = typeof window !== 'undefined' &&
721
- document.getElementById('app')?.innerHTML?.trim().length > 0;
722
-
723
- // After first render, we're no longer hydrating
724
- if (typeof window !== 'undefined') {
725
- const markHydrationComplete = () => { isHydrating = false; };
726
- if ('requestIdleCallback' in window) {
727
- window.requestIdleCallback(markHydrationComplete);
728
- } else {
729
- setTimeout(markHydrationComplete, 0);
730
- }
731
- }
732
-
733
- /**
734
- * Placeholder component that preserves existing DOM during hydration.
735
- */
736
- const HydrationPlaceholder = component(() => {
737
- return () => null;
738
- }, { name: 'HydrationPlaceholder' });
739
-
740
- /**
741
- * Layout router component that preserves layouts across navigation.
742
- *
743
- * This component renders the current route's layout and page content reactively.
744
- * When navigating between routes with the same layout, only the page content
745
- * updates - the layout (Navbar, Sidebar, Footer) persists without re-rendering.
746
- */
747
- export const LayoutRouter = component((ctx) => {
748
- const route = useRoute();
749
-
750
- // Track loaded lazy components to avoid reloading
751
- const loadedComponents = {};
752
-
753
- // Track if this is the initial hydration render
754
- let initialRender = true;
755
-
756
- // HMR support: Listen for MDX updates and clear the component cache
757
- if (typeof window !== 'undefined' && import.meta.hot) {
758
- // Listen for sigx:mdx-hmr events dispatched by MDX files after they accept HMR
759
- const handleMdxHmr = (event) => {
760
- const { moduleId, newModule } = event.detail || {};
761
-
762
- // Clear all cached components to force reload with new module
763
- for (const key in loadedComponents) {
764
- delete loadedComponents[key];
765
- }
766
-
767
- // If we have the new module directly from HMR, cache it for the matching route
768
- if (newModule?.default) {
769
- const currentPath = route.path;
770
- loadedComponents[currentPath] = newModule.default;
771
- }
772
-
773
- // Force re-render
774
- ctx.update();
775
- };
776
-
777
- window.addEventListener('sigx:mdx-hmr', handleMdxHmr);
778
-
779
- ctx.onUnmounted(() => {
780
- window.removeEventListener('sigx:mdx-hmr', handleMdxHmr);
781
- });
782
- }
783
-
784
- return () => {
785
- const matched = route.matched;
786
- if (!matched || matched.length === 0) return null;
787
-
788
- const match = matched[0];
789
- if (!match) return null;
790
-
791
- // Get layout name from route meta or use default
792
- const layoutName = match.layout || match.meta?.layout || defaultLayout;
793
- const Layout = layouts[layoutName];
794
-
795
- if (!Layout) {
796
- console.warn(\`Layout "\${layoutName}" not found for route \${route.path}\`);
797
- return null;
798
- }
799
-
800
- // Get the original (unwrapped) component
801
- const rawComponent = match.originalComponent || match.component;
802
- const routePath = route.path;
803
-
804
- // Handle lazy/dynamic import components
805
- if (isMarkedLazy(rawComponent) || (typeof rawComponent === 'function' && !rawComponent.__setup)) {
806
- // Check if already loaded
807
- if (loadedComponents[routePath]) {
808
- const PageComponent = loadedComponents[routePath];
809
- initialRender = false;
810
- return jsx(Layout, {
811
- meta: match.meta,
812
- path: routePath,
813
- key: layoutName, // Key by layout to preserve layout across pages
814
- children: PageComponent({})
815
- });
816
- }
817
-
818
- // Load the component
819
- const loadState = signal({ value: null, loading: true, error: null });
820
-
821
- try {
822
- const result = rawComponent();
823
- if (isPromise(result)) {
824
- result.then(module => {
825
- const Component = module.default || module;
826
- loadedComponents[routePath] = Component;
827
- loadState.value = Component;
828
- loadState.loading = false;
829
- }).catch(err => {
830
- console.error('Failed to load component for route:', routePath, err);
831
- loadState.error = err;
832
- loadState.loading = false;
833
- });
834
- } else {
835
- // Not a promise, use directly
836
- loadedComponents[routePath] = rawComponent;
837
- loadState.value = rawComponent;
838
- loadState.loading = false;
839
- }
840
- } catch (err) {
841
- loadState.error = err;
842
- loadState.loading = false;
843
- }
844
-
845
- // During hydration, preserve existing SSR content instead of showing loading state
846
- if (loadState.loading) {
847
- if (isHydrating && initialRender) {
848
- return jsx(Layout, {
849
- meta: match.meta,
850
- path: routePath,
851
- key: layoutName,
852
- children: jsx(HydrationPlaceholder, {})
853
- });
854
- }
855
- return jsx(Layout, {
856
- meta: match.meta,
857
- path: routePath,
858
- key: layoutName,
859
- children: null
860
- });
861
- }
862
-
863
- if (loadState.error || !loadState.value) {
864
- return jsx(Layout, {
865
- meta: match.meta,
866
- path: routePath,
867
- key: layoutName,
868
- children: null
869
- });
870
- }
871
-
872
- const PageComponent = loadState.value;
873
- initialRender = false;
874
- return jsx(Layout, {
875
- meta: match.meta,
876
- path: routePath,
877
- key: layoutName,
878
- children: PageComponent({})
879
- });
880
- }
881
-
882
- // Eager component (has __setup)
883
- // Check cache first for HMR-updated components
884
- if (loadedComponents[routePath]) {
885
- const PageComponent = loadedComponents[routePath];
886
- initialRender = false;
887
- return jsx(Layout, {
888
- meta: match.meta,
889
- path: routePath,
890
- key: layoutName,
891
- children: PageComponent({})
892
- });
893
- }
894
-
895
- initialRender = false;
896
- return jsx(Layout, {
897
- meta: match.meta,
898
- path: routePath,
899
- key: layoutName,
900
- children: rawComponent({})
901
- });
902
- };
903
- }, { name: 'LayoutRouter' });
904
-
905
- /**
906
- * Setup layouts for routes
907
- *
908
- * This function now simply annotates routes with their layout information.
909
- * The actual layout wrapping is handled by LayoutRouter, which preserves
910
- * layouts across navigation.
911
- */
912
- export function setupLayouts(routes) {
913
- const availableLayoutNames = Object.keys(layouts);
914
-
915
- return routes.map(route => {
916
- const layoutName = route.layout || route.meta?.layout || defaultLayout;
917
- const Layout = layouts[layoutName];
918
-
919
- if (!Layout) {
920
- console.error(
921
- \`\u274C SSG103: Layout "\${layoutName}" not found for route \${route.path}\\n\` +
922
- \` \u{1F4C1} \${route.file || 'unknown'}\\n\` +
923
- \` \u{1F4A1} Available layouts: \${availableLayoutNames.join(', ') || 'none'}\\n\` +
924
- \` Create src/layouts/\${layoutName}.tsx or set a valid layout in frontmatter.\`
925
- );
926
- return route;
927
- }
928
-
929
- // Store layout info on the route, but don't wrap the component
930
- // LayoutRouter will handle layout rendering
931
- return {
932
- ...route,
933
- layout: layoutName,
934
- originalComponent: route.component,
935
- meta: {
936
- ...route.meta,
937
- layout: layoutName,
938
- },
939
- };
940
- });
941
- }
942
-
943
- export default layouts;
944
- `;
945
- }
946
-
947
- // src/sitemap.ts
948
- import fs3 from "node:fs/promises";
949
- import path4 from "node:path";
950
- function generateSitemap(entries, config) {
951
- const siteUrl = config.site?.url?.replace(/\/$/, "") || "";
952
- const base = config.base?.replace(/\/$/, "") || "";
953
- const urlEntries = entries.map((entry) => {
954
- const loc = `${siteUrl}${base}${entry.path}`;
955
- const lastmod = entry.lastmod ? typeof entry.lastmod === "string" ? entry.lastmod : entry.lastmod.toISOString().split("T")[0] : void 0;
956
- return ` <url>
957
- <loc>${escapeXml(loc)}</loc>${lastmod ? `
958
- <lastmod>${lastmod}</lastmod>` : ""}${entry.changefreq ? `
959
- <changefreq>${entry.changefreq}</changefreq>` : ""}${entry.priority !== void 0 ? `
960
- <priority>${entry.priority.toFixed(1)}</priority>` : ""}
961
- </url>`;
962
- });
963
- return `<?xml version="1.0" encoding="UTF-8"?>
964
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
965
- ${urlEntries.join("\n")}
966
- </urlset>`;
967
- }
968
- function generateRobotsTxt(config, sitemapPath = "/sitemap.xml") {
969
- const siteUrl = config.site?.url?.replace(/\/$/, "") || "";
970
- const base = config.base?.replace(/\/$/, "") || "";
971
- return `User-agent: *
972
- Allow: /
973
-
974
- Sitemap: ${siteUrl}${base}${sitemapPath}
975
- `;
976
- }
977
- function pagesToSitemapEntries(pages, options = {}) {
978
- const {
979
- exclude = [],
980
- defaultChangefreq = "weekly",
981
- defaultPriority = 0.5
982
- } = options;
983
- return pages.filter((page) => {
984
- for (const pattern of exclude) {
985
- if (pattern.includes("*")) {
986
- const regex = new RegExp(
987
- "^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
988
- );
989
- if (regex.test(page.path)) return false;
990
- } else if (page.path === pattern) {
991
- return false;
992
- }
993
- }
994
- return true;
995
- }).map((page) => {
996
- const depth = page.path.split("/").filter(Boolean).length;
997
- let priority = defaultPriority;
998
- if (page.path === "/") {
999
- priority = 1;
1000
- } else if (depth === 1) {
1001
- priority = 0.8;
1002
- } else if (depth === 2) {
1003
- priority = 0.6;
1004
- }
1005
- return {
1006
- path: page.path,
1007
- changefreq: defaultChangefreq,
1008
- priority
1009
- };
1010
- });
1011
- }
1012
- async function writeSitemap(pages, config, outDir, options = {}) {
1013
- const entries = pagesToSitemapEntries(pages, options);
1014
- if (options.additionalUrls) {
1015
- entries.push(...options.additionalUrls);
1016
- }
1017
- const sitemapContent = generateSitemap(entries, config);
1018
- const sitemapPath = path4.join(outDir, "sitemap.xml");
1019
- await fs3.writeFile(sitemapPath, sitemapContent, "utf-8");
1020
- const robotsContent = generateRobotsTxt(config);
1021
- const robotsPath = path4.join(outDir, "robots.txt");
1022
- await fs3.writeFile(robotsPath, robotsContent, "utf-8");
1023
- return { sitemapPath, robotsPath };
1024
- }
1025
- function escapeXml(str) {
1026
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1027
- }
1028
-
1029
- // src/vite/virtual-entries.ts
1030
- import fs4 from "node:fs";
1031
- import path5 from "node:path";
1032
- var VIRTUAL_CLIENT_ID = "virtual:ssg-client";
1033
- var RESOLVED_VIRTUAL_CLIENT_ID = "\0" + VIRTUAL_CLIENT_ID + ".tsx";
1034
- var SSG_CLIENT_ENTRY_PATH = "/@ssg/client.tsx";
1035
- var VIRTUAL_SERVER_ID = "virtual:ssg-server";
1036
- var RESOLVED_VIRTUAL_SERVER_ID = "\0" + VIRTUAL_SERVER_ID + ".tsx";
1037
- var VIRTUAL_HTML_ID = "virtual:ssg-html";
1038
- var RESOLVED_VIRTUAL_HTML_ID = "\0" + VIRTUAL_HTML_ID;
1039
- function detectCustomEntries(root, config) {
1040
- const clientPaths = [
1041
- "src/main.tsx",
1042
- "src/main.ts",
1043
- "src/entry-client.tsx",
1044
- "src/entry-client.ts",
1045
- "src/entry.tsx",
1046
- "src/entry.ts"
1047
- ];
1048
- const serverPaths = [
1049
- "src/entry-server.tsx",
1050
- "src/entry-server.ts"
1051
- ];
1052
- const htmlPaths = [
1053
- "index.html"
1054
- ];
1055
- const cssPaths = [
1056
- "src/styles/global.css",
1057
- "src/styles/main.css",
1058
- "src/styles/index.css",
1059
- "src/style.css",
1060
- "src/global.css",
1061
- "src/index.css"
1062
- ];
1063
- let customClientPath;
1064
- let customServerPath;
1065
- let customHtmlPath;
1066
- let globalCssPath;
1067
- for (const p of clientPaths) {
1068
- const fullPath = path5.join(root, p);
1069
- if (fs4.existsSync(fullPath)) {
1070
- customClientPath = fullPath;
1071
- break;
1072
- }
1073
- }
1074
- for (const p of serverPaths) {
1075
- const fullPath = path5.join(root, p);
1076
- if (fs4.existsSync(fullPath)) {
1077
- customServerPath = fullPath;
1078
- break;
1079
- }
1080
- }
1081
- for (const p of htmlPaths) {
1082
- const fullPath = path5.join(root, p);
1083
- if (fs4.existsSync(fullPath)) {
1084
- customHtmlPath = fullPath;
1085
- break;
1086
- }
1087
- }
1088
- for (const p of cssPaths) {
1089
- const fullPath = path5.join(root, p);
1090
- if (fs4.existsSync(fullPath)) {
1091
- globalCssPath = "/" + p.replace(/\\/g, "/");
1092
- break;
1093
- }
1094
- }
1095
- return {
1096
- useVirtualClient: !customClientPath,
1097
- useVirtualServer: !customServerPath,
1098
- useVirtualHtml: !customHtmlPath,
1099
- customClientPath,
1100
- customServerPath,
1101
- customHtmlPath,
1102
- globalCssPath
1103
- };
1104
- }
1105
- function generateClientEntry(config, detection) {
1106
- const cssImport = detection.globalCssPath ? `import '${detection.globalCssPath}';
1107
- ` : "";
1108
- const additionalImports = (config.clientImports ?? []).map((imp) => `import '${imp}';`).join("\n");
1109
- const additionalImportsBlock = additionalImports ? `
1110
- ${additionalImports}
1111
- ` : "";
1112
- const prefetchDelay = typeof config.prefetch === "object" ? config.prefetch.delay ?? 100 : 100;
1113
- const prefetchEnabled = config.prefetch !== false;
1114
- return `/**
1115
- * Auto-generated client entry point
1116
- * This file is generated by @sigx/ssg when no custom entry is detected.
1117
- * To customize, create src/main.tsx or src/entry-client.tsx
1118
- */
1119
- ${cssImport}${additionalImportsBlock}
1120
- import { defineApp, component } from 'sigx';
1121
- import { createRouter, createWebHistory } from '@sigx/router';
1122
- import { ssrClientPlugin } from '@sigx/ssg/client';
1123
- import routes from 'virtual:ssg-routes';
1124
- import { setupLayouts, LayoutRouter } from 'virtual:generated-layouts';
1125
- ${prefetchEnabled ? `import { setupPrefetch } from '@sigx/ssg/client';` : ""}
1126
-
1127
- // Apply layouts to routes (annotates routes with layout info)
1128
- const layoutRoutes = setupLayouts(routes);
1129
-
1130
- // Create router with browser history
1131
- const router = createRouter({
1132
- history: createWebHistory('${config.base || "/"}'),
1133
- routes: layoutRoutes,
1134
- scrollBehavior(to, from, savedPosition) {
1135
- if (savedPosition) return savedPosition;
1136
- if (to.hash) return { el: to.hash };
1137
- return { top: 0 };
1138
- },
1139
- });
1140
-
1141
- // Root app component - uses LayoutRouter which preserves layouts across navigation
1142
- const App = component(() => {
1143
- return () => <LayoutRouter />;
1144
- });
1145
-
1146
- // Hydrate the server-rendered HTML
1147
- defineApp(<App />)
1148
- .use(router)
1149
- .use(ssrClientPlugin)
1150
- .hydrate('#app');
1151
-
1152
- ${prefetchEnabled ? `// Enable link prefetching for faster navigation
1153
- setupPrefetch({ delay: ${prefetchDelay} });` : ""}
1154
- `;
1155
- }
1156
- function generateServerEntry(config) {
1157
- return `/**
1158
- * Auto-generated server entry point
1159
- * This file is generated by @sigx/ssg when no custom entry is detected.
1160
- * To customize, create src/entry-server.tsx
1161
- */
1162
- import { renderToString } from '@sigx/server-renderer/server';
1163
- import { defineApp } from 'sigx';
1164
- import { createRouter, createMemoryHistory } from '@sigx/router';
1165
- import routes from 'virtual:ssg-routes';
1166
- import { setupLayouts, LayoutRouter } from 'virtual:generated-layouts';
1167
-
1168
- // Pre-process routes with layouts once at module load time (not per-render)
1169
- const routesWithLayouts = setupLayouts(routes);
1170
-
1171
- /**
1172
- * Render the app to HTML string for a given URL
1173
- */
1174
- export async function render(url, context) {
1175
- // Create router with memory history for SSR
1176
- // Note: We must create a new router per render because history is URL-specific
1177
- const router = createRouter({
1178
- routes: routesWithLayouts,
1179
- history: createMemoryHistory({ initialLocation: url || '/' }),
1180
- });
1181
-
1182
- // Create app with router - router's install() sets up DI via app.defineProvide()
1183
- const app = defineApp(<LayoutRouter />).use(router);
1184
-
1185
- const html = await renderToString(app);
1186
- return html;
1187
- }
1188
- `;
1189
- }
1190
- function generateHtmlTemplate(config) {
1191
- const site = config.site || {};
1192
- const lang = site.lang || "en";
1193
- const title = site.title || "SignalX App";
1194
- const description = site.description || "";
1195
- const favicon = site.favicon || "/favicon.ico";
1196
- const themeColor = site.themeColor || "#000000";
1197
- const ogImage = site.ogImage || "";
1198
- const url = site.url || "";
1199
- const twitter = site.twitter || "";
1200
- const fonts = site.fonts || [];
1201
- let fontLinks = "";
1202
- if (fonts.length > 0) {
1203
- fontLinks = `
1204
- <link rel="preconnect" href="https://fonts.googleapis.com" />
1205
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
1206
- <link href="https://fonts.googleapis.com/css2?family=${fonts.join("&family=")}&display=swap" rel="stylesheet" />`;
1207
- }
1208
- let ogTags = "";
1209
- if (url || ogImage) {
1210
- ogTags = `
1211
- <!-- Open Graph -->
1212
- <meta property="og:type" content="website" />
1213
- <meta property="og:title" content="${title}" />
1214
- <meta property="og:description" content="${description}" />${url ? `
1215
- <meta property="og:url" content="${url}" />` : ""}${ogImage ? `
1216
- <meta property="og:image" content="${ogImage}" />` : ""}`;
1217
- }
1218
- let twitterTags = "";
1219
- if (twitter || ogImage) {
1220
- twitterTags = `
1221
- <!-- Twitter Card -->
1222
- <meta name="twitter:card" content="${ogImage ? "summary_large_image" : "summary"}" />${twitter ? `
1223
- <meta name="twitter:site" content="@${twitter}" />` : ""}
1224
- <meta name="twitter:title" content="${title}" />
1225
- <meta name="twitter:description" content="${description}" />${ogImage ? `
1226
- <meta name="twitter:image" content="${ogImage}" />` : ""}`;
1227
- }
1228
- return `<!DOCTYPE html>
1229
- <html lang="${lang}">
1230
- <head>
1231
- <meta charset="UTF-8" />
1232
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1233
- <meta name="description" content="${description}" />
1234
- <meta name="theme-color" content="${themeColor}" />
1235
- <link rel="icon" type="image/x-icon" href="${favicon}" />${fontLinks}${ogTags}${twitterTags}
1236
- <title>${title}</title>
1237
- <!--head-tags-->
1238
- </head>
1239
- <body>
1240
- <div id="app"><!--app-html--></div>
1241
- <script type="module" src="/@ssg/client.tsx"></script>
1242
- </body>
1243
- </html>
1244
- `;
1245
- }
1246
- function generateProductionHtmlTemplate(config, clientEntryPath) {
1247
- const site = config.site || {};
1248
- const lang = site.lang || "en";
1249
- const title = site.title || "SignalX App";
1250
- const description = site.description || "";
1251
- const favicon = site.favicon || "/favicon.ico";
1252
- const themeColor = site.themeColor || "#000000";
1253
- const ogImage = site.ogImage || "";
1254
- const url = site.url || "";
1255
- const twitter = site.twitter || "";
1256
- const fonts = site.fonts || [];
1257
- let fontLinks = "";
1258
- if (fonts.length > 0) {
1259
- fontLinks = `
1260
- <link rel="preconnect" href="https://fonts.googleapis.com" />
1261
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
1262
- <link href="https://fonts.googleapis.com/css2?family=${fonts.join("&family=")}&display=swap" rel="stylesheet" />`;
1263
- }
1264
- let ogTags = "";
1265
- if (url || ogImage) {
1266
- ogTags = `
1267
- <!-- Open Graph -->
1268
- <meta property="og:type" content="website" />
1269
- <meta property="og:title" content="${title}" />
1270
- <meta property="og:description" content="${description}" />${url ? `
1271
- <meta property="og:url" content="${url}" />` : ""}${ogImage ? `
1272
- <meta property="og:image" content="${ogImage}" />` : ""}`;
1273
- }
1274
- let twitterTags = "";
1275
- if (twitter || ogImage) {
1276
- twitterTags = `
1277
- <!-- Twitter Card -->
1278
- <meta name="twitter:card" content="${ogImage ? "summary_large_image" : "summary"}" />${twitter ? `
1279
- <meta name="twitter:site" content="@${twitter}" />` : ""}
1280
- <meta name="twitter:title" content="${title}" />
1281
- <meta name="twitter:description" content="${description}" />${ogImage ? `
1282
- <meta name="twitter:image" content="${ogImage}" />` : ""}`;
1283
- }
1284
- return `<!DOCTYPE html>
1285
- <html lang="${lang}">
1286
- <head>
1287
- <meta charset="UTF-8" />
1288
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1289
- <meta name="description" content="${description}" />
1290
- <meta name="theme-color" content="${themeColor}" />
1291
- <link rel="icon" type="image/x-icon" href="${favicon}" />${fontLinks}${ogTags}${twitterTags}
1292
- <title>${title}</title>
1293
- <!--head-tags-->
1294
- </head>
1295
- <body>
1296
- <div id="app"><!--app-html--></div>
1297
- <script type="module" src="${clientEntryPath}"></script>
1298
- </body>
1299
- </html>
1300
- `;
1301
- }
1302
-
1303
- // src/build.ts
1304
- async function build(options = {}) {
1305
- const startTime = Date.now();
1306
- const root = process.cwd();
1307
- const warnings = [];
1308
- const pages = [];
1309
- console.log("\n\u{1F680} @sigx/ssg - Building static site...\n");
1310
- console.log("\u{1F4E6} Loading configuration...");
1311
- const config = await loadConfig(options.configPath);
1312
- const resolvedConfig = resolveConfigPaths(config, root);
1313
- console.log("\u{1F50D} Scanning pages...");
1314
- const routes = await scanPages(resolvedConfig, root);
1315
- console.log(` Found ${routes.length} page(s)`);
1316
- console.log("\u{1F4D0} Discovering layouts...");
1317
- const layouts = await discoverLayouts(resolvedConfig, root);
1318
- console.log(` Found ${layouts.length} layout(s)`);
1319
- const entryDetection = detectCustomEntries(root, resolvedConfig);
1320
- if (entryDetection.useVirtualClient || entryDetection.useVirtualServer) {
1321
- console.log("\u{1F4E6} Using zero-config mode");
1322
- if (entryDetection.useVirtualClient) console.log(" \u2192 Virtual client entry");
1323
- if (entryDetection.useVirtualServer) console.log(" \u2192 Virtual server entry");
1324
- if (entryDetection.useVirtualHtml) console.log(" \u2192 Virtual HTML template");
1325
- }
1326
- const clientEntry = await getClientEntryPoint(resolvedConfig, root);
1327
- const ssrEntry = await getSSREntryPoint(resolvedConfig, root);
1328
- const htmlTemplatePath = path6.join(root, "index.html");
1329
- let cleanupHtml = false;
1330
- let originalHtmlContent = null;
1331
- if (!entryDetection.useVirtualHtml && fsSync.existsSync(htmlTemplatePath)) {
1332
- originalHtmlContent = fsSync.readFileSync(htmlTemplatePath, "utf-8");
1333
- }
1334
- const htmlContent = await getHtmlTemplate(resolvedConfig, root, clientEntry);
1335
- fsSync.writeFileSync(htmlTemplatePath, htmlContent, "utf-8");
1336
- cleanupHtml = entryDetection.useVirtualHtml;
1337
- console.log("\u{1F528} Building with Vite...");
1338
- const vite = await import("vite");
1339
- try {
1340
- const clientInput = htmlTemplatePath;
1341
- await vite.build({
1342
- root,
1343
- mode: "production",
1344
- build: {
1345
- outDir: resolvedConfig.outDir,
1346
- emptyOutDir: false,
1347
- ssrManifest: true,
1348
- rollupOptions: {
1349
- input: clientInput
1350
- }
1351
- },
1352
- logLevel: options.verbose ? "info" : "warn"
1353
- });
1354
- const ssrOutDir = path6.join(resolvedConfig.outDir, ".ssg");
1355
- await vite.build({
1356
- root,
1357
- mode: "production",
1358
- build: {
1359
- outDir: ssrOutDir,
1360
- ssr: true,
1361
- rollupOptions: {
1362
- input: ssrEntry
1363
- }
1364
- },
1365
- logLevel: options.verbose ? "info" : "warn"
1366
- });
1367
- console.log("\u{1F4DD} Collecting paths to render...");
1368
- const pathsToRender = await collectPaths(routes, root, warnings);
1369
- console.log(` ${pathsToRender.length} path(s) to render`);
1370
- const outputDirs = /* @__PURE__ */ new Set();
1371
- for (const pathInfo of pathsToRender) {
1372
- const outputPath = getOutputPath(pathInfo.path, resolvedConfig.outDir);
1373
- outputDirs.add(path6.dirname(outputPath));
1374
- }
1375
- await Promise.all(
1376
- Array.from(outputDirs).map((dir) => fs5.mkdir(dir, { recursive: true }))
1377
- );
1378
- console.log("\u{1F3A8} Rendering pages...");
1379
- const ssrEntryBasename = path6.basename(ssrEntry, path6.extname(ssrEntry));
1380
- const ssrEntryName = ssrEntryBasename + ".js";
1381
- const entryPath = path6.join(ssrOutDir, ssrEntryName);
1382
- const entryModule = await import(pathToFileURL(entryPath).href);
1383
- const templatePath = path6.join(resolvedConfig.outDir, "index.html");
1384
- const template = await fs5.readFile(templatePath, "utf-8");
1385
- const CONCURRENCY = options.concurrency ?? 20;
1386
- const verbose2 = options.verbose ?? false;
1387
- async function renderPage(pathInfo) {
1388
- const renderStart = Date.now();
1389
- try {
1390
- const appHtml = await entryModule.render(pathInfo.path, {
1391
- params: pathInfo.params,
1392
- props: pathInfo.props
1393
- });
1394
- let html = template.replace("<!--app-html-->", appHtml);
1395
- const headTags = generateHeadTags(pathInfo, resolvedConfig);
1396
- html = html.replace("<!--head-tags-->", headTags);
1397
- const outputPath = getOutputPath(pathInfo.path, resolvedConfig.outDir);
1398
- const renderTime = Date.now() - renderStart;
1399
- return { pathInfo, html, outputPath, renderTime };
1400
- } catch (err) {
1401
- const errorMessage = err instanceof Error ? err.message : String(err);
1402
- console.error(` \u274C ${pathInfo.path}: ${errorMessage}`);
1403
- warnings.push(`Failed to render ${pathInfo.path}: ${errorMessage}`);
1404
- return null;
1405
- }
1406
- }
1407
- console.log(" Phase 1: Rendering...");
1408
- const renderPhaseStart = Date.now();
1409
- const renderResults = [];
1410
- for (let i = 0; i < pathsToRender.length; i += CONCURRENCY) {
1411
- const batch = pathsToRender.slice(i, i + CONCURRENCY);
1412
- const results = await Promise.all(batch.map(renderPage));
1413
- for (const result of results) {
1414
- if (result) {
1415
- renderResults.push(result);
1416
- }
1417
- }
1418
- }
1419
- const renderPhaseDuration = Date.now() - renderPhaseStart;
1420
- console.log(` Phase 1 complete: ${renderResults.length} pages in ${renderPhaseDuration}ms (${Math.round(renderPhaseDuration / renderResults.length)}ms avg)`);
1421
- console.log(" Phase 2: Writing files...");
1422
- const writePhaseStart = Date.now();
1423
- const WRITE_CONCURRENCY = 10;
1424
- for (let i = 0; i < renderResults.length; i += WRITE_CONCURRENCY) {
1425
- const batch = renderResults.slice(i, i + WRITE_CONCURRENCY);
1426
- await Promise.all(batch.map(async (result) => {
1427
- await fs5.writeFile(result.outputPath, result.html, "utf-8");
1428
- const size = Buffer.byteLength(result.html, "utf-8");
1429
- pages.push({
1430
- path: result.pathInfo.path,
1431
- file: result.outputPath,
1432
- time: result.renderTime,
1433
- size
1434
- });
1435
- if (verbose2) {
1436
- console.log(` \u2713 ${result.pathInfo.path} (${result.renderTime}ms, ${formatBytes(size)})`);
1437
- }
1438
- }));
1439
- }
1440
- const writePhaseDuration = Date.now() - writePhaseStart;
1441
- console.log(` Phase 2 complete: ${renderResults.length} files in ${writePhaseDuration}ms`);
1442
- if (!verbose2) {
1443
- console.log(` \u2713 Rendered ${renderResults.length} pages`);
1444
- }
1445
- await fs5.rm(ssrOutDir, { recursive: true, force: true });
1446
- if (pages.length > 0) {
1447
- console.log("\u{1F5FA}\uFE0F Generating sitemap...");
1448
- await writeSitemap(pages, resolvedConfig, resolvedConfig.outDir);
1449
- console.log(" \u2713 sitemap.xml");
1450
- console.log(" \u2713 robots.txt");
1451
- }
1452
- } finally {
1453
- await cleanupTempEntries(root);
1454
- if (cleanupHtml) {
1455
- try {
1456
- await fs5.unlink(htmlTemplatePath);
1457
- } catch {
1458
- }
1459
- } else if (originalHtmlContent !== null) {
1460
- try {
1461
- await fs5.writeFile(htmlTemplatePath, originalHtmlContent, "utf-8");
1462
- } catch {
1463
- }
1464
- }
1465
- }
1466
- const totalTime = Date.now() - startTime;
1467
- console.log(`
1468
- \u2705 Built ${pages.length} page(s) in ${totalTime}ms`);
1469
- if (warnings.length > 0) {
1470
- console.log(`
1471
- \u26A0\uFE0F ${warnings.length} warning(s):`);
1472
- for (const warning of warnings) {
1473
- console.log(` - ${warning}`);
1474
- }
1475
- }
1476
- console.log(`
1477
- \u{1F4C1} Output: ${resolvedConfig.outDir}
1478
- `);
1479
- return {
1480
- pages,
1481
- totalTime,
1482
- warnings
1483
- };
1484
- }
1485
- async function collectPaths(routes, root, warnings) {
1486
- const paths = [];
1487
- for (const route of routes) {
1488
- if (isDynamicRoute(route)) {
1489
- try {
1490
- const moduleUrl = pathToFileURL(route.file).href;
1491
- const pageModule = await import(moduleUrl);
1492
- if (!pageModule.getStaticPaths) {
1493
- const params = extractParams(route.path).join(", ");
1494
- console.warn(
1495
- `
1496
- \u26A0\uFE0F SSG102: Dynamic route missing getStaticPaths()
1497
- \u{1F4C1} ${route.file}
1498
- Route: ${route.path} (params: ${params})
1499
- \u{1F4A1} Export getStaticPaths() to generate static pages:
1500
-
1501
- export async function getStaticPaths() {
1502
- return [{ params: { ${params.split(", ")[0]}: 'value' } }];
1503
- }
1504
- `
1505
- );
1506
- warnings.push(
1507
- `Route ${route.path} has dynamic segments [${params}] but no getStaticPaths() export. Skipping.`
1508
- );
1509
- continue;
1510
- }
1511
- const staticPaths = await pageModule.getStaticPaths();
1512
- for (const staticPath of staticPaths) {
1513
- const expandedPaths = expandDynamicRoute(route, [staticPath]);
1514
- for (const expandedPath of expandedPaths) {
1515
- paths.push({
1516
- path: expandedPath,
1517
- route,
1518
- params: staticPath.params,
1519
- props: staticPath.props
1520
- });
1521
- }
1522
- }
1523
- } catch (err) {
1524
- warnings.push(`Failed to load ${route.file}: ${err}`);
1525
- }
1526
- } else {
1527
- paths.push({
1528
- path: route.path,
1529
- route,
1530
- params: {}
1531
- });
1532
- }
1533
- }
1534
- return paths;
1535
- }
1536
- function generateHeadTags(pathInfo, config) {
1537
- const tags = [];
1538
- const meta = pathInfo.route.meta || {};
1539
- const title = meta.title || config.site?.title;
1540
- if (title) {
1541
- tags.push(`<title>${escapeHtml(title)}</title>`);
1542
- }
1543
- const description = meta.description || config.site?.description;
1544
- if (description) {
1545
- tags.push(`<meta name="description" content="${escapeHtml(description)}">`);
1546
- }
1547
- if (config.site?.url) {
1548
- const canonical = new URL(pathInfo.path, config.site.url).href;
1549
- tags.push(`<link rel="canonical" href="${canonical}">`);
1550
- }
1551
- return tags.join("\n ");
1552
- }
1553
- function getOutputPath(urlPath, outDir) {
1554
- let normalized = urlPath.replace(/^\//, "").replace(/\/$/, "");
1555
- if (!normalized) {
1556
- normalized = "index";
1557
- }
1558
- if (!normalized.endsWith(".html")) {
1559
- normalized = path6.join(normalized, "index.html");
1560
- }
1561
- return path6.join(outDir, normalized);
1562
- }
1563
- async function getSSREntryPoint(config, root) {
1564
- const detection = detectCustomEntries(root, config);
1565
- if (!detection.useVirtualServer && detection.customServerPath) {
1566
- return detection.customServerPath;
1567
- }
1568
- const virtualServerCode = generateServerEntry(config);
1569
- const tempServerPath = path6.join(root, ".ssg-temp-entry-server.tsx");
1570
- fsSync.writeFileSync(tempServerPath, virtualServerCode, "utf-8");
1571
- return tempServerPath;
1572
- }
1573
- async function getClientEntryPoint(config, root) {
1574
- const detection = detectCustomEntries(root, config);
1575
- if (!detection.useVirtualClient && detection.customClientPath) {
1576
- return detection.customClientPath;
1577
- }
1578
- const virtualClientCode = generateClientEntry(config, detection);
1579
- const tempClientPath = path6.join(root, ".ssg-temp-entry-client.tsx");
1580
- fsSync.writeFileSync(tempClientPath, virtualClientCode, "utf-8");
1581
- return tempClientPath;
1582
- }
1583
- async function cleanupTempEntries(root) {
1584
- const tempFiles = [
1585
- path6.join(root, ".ssg-temp-entry-server.tsx"),
1586
- path6.join(root, ".ssg-temp-entry-client.tsx")
1587
- ];
1588
- for (const file of tempFiles) {
1589
- try {
1590
- await fs5.unlink(file);
1591
- } catch {
1592
- }
1593
- }
1594
- }
1595
- async function getHtmlTemplate(config, root, clientEntryPath) {
1596
- const detection = detectCustomEntries(root, config);
1597
- if (!detection.useVirtualHtml && detection.customHtmlPath) {
1598
- let html = await fs5.readFile(detection.customHtmlPath, "utf-8");
1599
- const relativePath = "./" + path6.relative(root, clientEntryPath).replace(/\\/g, "/");
1600
- html = html.replace(
1601
- /<script([^>]*)\s+src=["']?\/@ssg\/client\.tsx["']?/g,
1602
- `<script$1 src="${relativePath}"`
1603
- );
1604
- return html;
1605
- }
1606
- return generateProductionHtmlTemplate(config, clientEntryPath);
1607
- }
1608
- function formatBytes(bytes) {
1609
- if (bytes < 1024) return `${bytes}B`;
1610
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
1611
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
1612
- }
1613
- function escapeHtml(str) {
1614
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1615
- }
1616
-
1617
- // src/dev.ts
1618
- import path8 from "node:path";
1619
- import fs7 from "node:fs";
1620
-
1621
- // src/vite/plugin.ts
1622
- import path7 from "node:path";
1623
- import fs6 from "node:fs";
1624
-
1625
- // src/mdx/shiki.ts
1626
- import { createHighlighter } from "shiki";
1627
- var highlighterPromise = null;
1628
- var DEFAULT_CONFIG = {
1629
- light: "github-light",
1630
- dark: "github-dark",
1631
- langs: ["javascript", "typescript", "jsx", "tsx", "json", "css", "html", "markdown", "bash", "shell"]
1632
- };
1633
- async function getHighlighter(config) {
1634
- if (!highlighterPromise) {
1635
- const mergedConfig = { ...DEFAULT_CONFIG, ...config };
1636
- highlighterPromise = createHighlighter({
1637
- themes: [mergedConfig.light, mergedConfig.dark],
1638
- langs: mergedConfig.langs
1639
- });
1640
- }
1641
- return highlighterPromise;
1642
- }
1643
- async function highlightCode(code, lang, config, meta) {
1644
- const highlighter = await getHighlighter(config);
1645
- const mergedConfig = { ...DEFAULT_CONFIG, ...config };
1646
- const loadedLangs = highlighter.getLoadedLanguages();
1647
- const effectiveLang = loadedLangs.includes(lang) ? lang : "text";
1648
- const codeHtml = highlighter.codeToHtml(code, {
1649
- lang: effectiveLang,
1650
- themes: {
1651
- light: mergedConfig.light,
1652
- dark: mergedConfig.dark
1653
- }
1654
- });
1655
- const filename = meta?.filename ?? "";
1656
- const isLive = meta?.live ?? false;
1657
- const tabs = meta?.tabs;
1658
- const hasTabs = tabs && tabs.length > 0;
1659
- const filenameHtml = filename ? `<span class="code-window-filename">${escapeHtml2(filename)}</span>` : `<span class="code-window-lang">${getLanguageLabel(effectiveLang)}</span>`;
1660
- if (hasTabs) {
1661
- const base64Code = encodeBase64(code);
1662
- const firstTab = tabs[0];
1663
- const tabButtonsHtml = tabs.map((tab, i) => {
1664
- const label = tab.charAt(0).toUpperCase() + tab.slice(1);
1665
- const isActive = i === 0;
1666
- return `<button class="code-window-tab${isActive ? " code-window-tab-active" : ""}">${label}</button>`;
1667
- }).join("\n ");
1668
- const html2 = `<div
1669
- class="live-preview-island"
1670
- data-island="LivePreview"
1671
- data-island-strategy="visible"
1672
- data-island-props="${escapeHtml2(JSON.stringify({
1673
- code: base64Code,
1674
- highlightedCode: codeHtml,
1675
- language: effectiveLang,
1676
- filename,
1677
- tabs,
1678
- live: isLive
1679
- }))}"
1680
- >
1681
- <div class="code-window code-window-live code-window-preview">
1682
- <div class="code-window-header">
1683
- <div class="code-window-header-left">
1684
- <div class="code-window-dots">
1685
- <span class="code-window-dot dot-red"></span>
1686
- <span class="code-window-dot dot-yellow"></span>
1687
- <span class="code-window-dot dot-green"></span>
1688
- </div>
1689
- ${filenameHtml}
1690
- </div>
1691
- <div class="code-window-tabs">
1692
- ${tabButtonsHtml}
1693
- </div>
1694
- <button class="code-window-try-live" disabled>\u26A1 Try Live</button>
1695
- </div>
1696
- <div class="code-window-preview-pane"${firstTab !== "preview" ? ' style="display:none;"' : ""}>
1697
- <div class="code-window-preview-loading">
1698
- <span class="code-window-spinner"></span>
1699
- Loading preview...
1700
- </div>
1701
- </div>
1702
- <div class="code-window-console-pane"${firstTab !== "console" ? ' style="display:none;"' : ""}>
1703
- <div class="code-window-console-empty">No console output</div>
1704
- </div>
1705
- <div class="code-window-content"${firstTab !== "code" ? ' style="display:none;"' : ""}>
1706
- ${codeHtml}
1707
- </div>
1708
- </div>
1709
- </div>`;
1710
- return html2;
1711
- }
1712
- const tryLiveButton = isLive ? `<button class="code-window-try-live" data-live-code="${escapeHtml2(encodeBase64(code))}" data-lang="${effectiveLang}" data-filename="${escapeHtml2(filename)}" title="Open in Live Playground">\u26A1 Try Live</button>` : "";
1713
- const html = `<div class="code-window${isLive ? " code-window-live" : ""}">
1714
- <div class="code-window-header">
1715
- <div class="code-window-header-left">
1716
- <div class="code-window-dots">
1717
- <span class="code-window-dot dot-red"></span>
1718
- <span class="code-window-dot dot-yellow"></span>
1719
- <span class="code-window-dot dot-green"></span>
1720
- </div>
1721
- ${filenameHtml}
1722
- </div>
1723
- ${tryLiveButton}
1724
- </div>
1725
- <div class="code-window-content">
1726
- ${codeHtml}
1727
- </div>
1728
- </div>`;
1729
- return html;
1730
- }
1731
- function encodeBase64(str) {
1732
- if (typeof Buffer !== "undefined") {
1733
- return Buffer.from(str, "utf-8").toString("base64");
1734
- }
1735
- return btoa(unescape(encodeURIComponent(str)));
1736
- }
1737
- function getLanguageLabel(lang) {
1738
- const labels = {
1739
- "tsx": "TSX",
1740
- "jsx": "JSX",
1741
- "ts": "TypeScript",
1742
- "typescript": "TypeScript",
1743
- "js": "JavaScript",
1744
- "javascript": "JavaScript",
1745
- "css": "CSS",
1746
- "html": "HTML",
1747
- "json": "JSON",
1748
- "bash": "Terminal",
1749
- "shell": "Terminal",
1750
- "sh": "Terminal",
1751
- "md": "Markdown",
1752
- "markdown": "Markdown",
1753
- "python": "Python",
1754
- "py": "Python",
1755
- "rust": "Rust",
1756
- "go": "Go",
1757
- "text": ""
1758
- };
1759
- return labels[lang.toLowerCase()] ?? lang.toUpperCase();
1760
- }
1761
- function escapeHtml2(str) {
1762
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1763
- }
1764
- function rehypeShiki(config) {
1765
- return async (tree) => {
1766
- const { visit: visit2 } = await import("unist-util-visit");
1767
- const { fromHtml } = await import("hast-util-from-html");
1768
- const nodesToProcess = [];
1769
- visit2(tree, "element", (node, index, parent) => {
1770
- if (node.tagName === "pre" && node.children?.[0]?.tagName === "code") {
1771
- nodesToProcess.push({ node, parent, index: index ?? 0 });
1772
- }
1773
- });
1774
- await Promise.all(
1775
- nodesToProcess.map(async ({ node, parent, index }) => {
1776
- const codeNode = node.children[0];
1777
- const className = codeNode.properties?.className?.[0] || "";
1778
- const lang = className.replace(/^language-/, "") || "text";
1779
- const metaString = codeNode.data?.meta || codeNode.properties?.metastring || "";
1780
- const filename = extractMeta(metaString, "filename") || extractMeta(metaString, "title") || "";
1781
- const isLive = /\blive\b/i.test(metaString);
1782
- const tabKeywords = ["preview", "code", "console"];
1783
- const tabs = [];
1784
- const tabPositions = [];
1785
- for (const keyword of tabKeywords) {
1786
- const regex = new RegExp(`\\b${keyword}\\b`, "i");
1787
- const match = metaString.match(regex);
1788
- if (match && match.index !== void 0) {
1789
- tabPositions.push({ tab: keyword, index: match.index });
1790
- }
1791
- }
1792
- tabPositions.sort((a, b) => a.index - b.index);
1793
- for (const { tab } of tabPositions) {
1794
- tabs.push(tab);
1795
- }
1796
- const code = getTextContent(codeNode);
1797
- const html = await highlightCode(code.trim(), lang, config, {
1798
- filename,
1799
- live: isLive,
1800
- tabs: tabs.length > 0 ? tabs : void 0
1801
- });
1802
- const fragment = fromHtml(html, { fragment: true });
1803
- if (parent && typeof index === "number" && fragment.children.length > 0) {
1804
- parent.children[index] = fragment.children[0];
1805
- }
1806
- })
1807
- );
1808
- };
1809
- }
1810
- function extractMeta(metaString, key) {
1811
- if (!metaString) return null;
1812
- const regex = new RegExp(`${key}=["']?([^"'\\s]+)["']?`, "i");
1813
- const match = metaString.match(regex);
1814
- return match ? match[1] : null;
1815
- }
1816
- function getTextContent(node) {
1817
- if (node.type === "text") {
1818
- return node.value;
1819
- }
1820
- if (node.children) {
1821
- return node.children.map(getTextContent).join("");
1822
- }
1823
- return "";
1824
- }
1825
-
1826
- // src/mdx/rehype-headings.ts
1827
- import { visit } from "unist-util-visit";
1828
- import { toString } from "hast-util-to-string";
1829
- function rehypeExtractHeadings(options = {}) {
1830
- const { minLevel = 2, maxLevel = 3 } = options;
1831
- return (tree, file) => {
1832
- const headings = [];
1833
- visit(tree, "element", (node) => {
1834
- const match = /^h([1-6])$/.exec(node.tagName);
1835
- if (!match) return;
1836
- const level = parseInt(match[1], 10);
1837
- if (level < minLevel || level > maxLevel) return;
1838
- const id = node.properties?.id;
1839
- if (!id) return;
1840
- const text = toString(node).trim();
1841
- if (!text) return;
1842
- headings.push({ id, text, level });
1843
- });
1844
- file.data = file.data || {};
1845
- file.data.headings = headings;
1846
- };
1847
- }
1848
-
1849
- // src/mdx/plugin.ts
1850
- function mdxPlugin(options = {}) {
1851
- const { markdown = {} } = options;
1852
- let mdxRollup;
1853
- let viteConfig;
1854
- return {
1855
- name: "sigx-ssg-mdx",
1856
- enforce: "pre",
1857
- async configResolved(config) {
1858
- viteConfig = config;
1859
- const mdxModule = await import("@mdx-js/rollup");
1860
- const remarkFrontmatter = (await import("remark-frontmatter")).default;
1861
- const remarkMdxFrontmatter = (await import("remark-mdx-frontmatter")).default;
1862
- const remarkGfm = (await import("remark-gfm")).default;
1863
- const rehypeSlug = (await import("rehype-slug")).default;
1864
- const rehypeAutolinkHeadings = (await import("rehype-autolink-headings")).default;
1865
- const tocConfig = options.ssgConfig?.toc || { minLevel: 2, maxLevel: 3 };
1866
- const rehypePlugins = [];
1867
- rehypePlugins.push(rehypeSlug);
1868
- rehypePlugins.push([rehypeAutolinkHeadings, {
1869
- behavior: "append",
1870
- properties: {
1871
- class: "heading-anchor",
1872
- ariaHidden: true,
1873
- tabIndex: -1
1874
- },
1875
- content: {
1876
- type: "element",
1877
- tagName: "span",
1878
- properties: { class: "heading-anchor-icon" },
1879
- children: [{ type: "text", value: "#" }]
1880
- }
1881
- }]);
1882
- rehypePlugins.push([rehypeExtractHeadings, tocConfig]);
1883
- if (markdown.shiki !== false) {
1884
- const shikiConfig = typeof markdown.shiki === "object" ? markdown.shiki : void 0;
1885
- rehypePlugins.push([rehypeShiki, shikiConfig]);
1886
- }
1887
- if (markdown.rehypePlugins) {
1888
- rehypePlugins.push(...markdown.rehypePlugins);
1889
- }
1890
- const remarkPlugins = [
1891
- remarkFrontmatter,
1892
- [remarkMdxFrontmatter, { name: "frontmatter" }],
1893
- remarkGfm
1894
- ];
1895
- if (markdown.remarkPlugins) {
1896
- remarkPlugins.push(...markdown.remarkPlugins);
1897
- }
1898
- mdxRollup = mdxModule.default({
1899
- jsx: false,
1900
- jsxImportSource: "sigx",
1901
- remarkPlugins,
1902
- rehypePlugins,
1903
- providerImportSource: void 0
1904
- });
1905
- },
1906
- async transform(code, id) {
1907
- if (!/\.mdx?$/.test(id)) {
1908
- return null;
1909
- }
1910
- const { data: frontmatter, content } = parseFrontmatter(code);
1911
- if (!frontmatter.title) {
1912
- const extractedTitle = extractTitleFromContent(content);
1913
- if (extractedTitle) {
1914
- frontmatter.title = extractedTitle;
1915
- }
1916
- }
1917
- if (!mdxRollup?.transform) {
1918
- throw new Error("MDX plugin not initialized");
1919
- }
1920
- const result = await mdxRollup.transform(code, id);
1921
- if (!result) {
1922
- return null;
1923
- }
1924
- const headings = await extractHeadingsFromContent(content, options);
1925
- const moduleId = id.replace(/\\/g, "/");
1926
- const transformedCode = wrapMDXComponent(
1927
- result.code,
1928
- frontmatter,
1929
- headings,
1930
- moduleId,
1931
- viteConfig.command === "serve"
1932
- );
1933
- return {
1934
- code: transformedCode,
1935
- map: result.map
1936
- };
1937
- }
1938
- };
1939
- }
1940
- function wrapMDXComponent(code, frontmatter, headings, moduleId, isDev) {
1941
- if (isDev) {
1942
- const fileName = moduleId.split("/").pop()?.replace(/\.mdx?$/, "") || "MDXPage";
1943
- const componentName = fileName.charAt(0).toUpperCase() + fileName.slice(1).replace(/[^a-zA-Z0-9]/g, "") + "Page";
1944
- const modifiedCode = code.replace(/export\s+default\s+function\s+MDXContent/g, "function _MDXContent").replace(/export\s+{\s*MDXContent\s+as\s+default\s*}/g, "");
1945
- return `
1946
- import { registerHMRModule } from '@sigx/vite/hmr';
1947
- import { component as __component } from 'sigx';
1948
- registerHMRModule('${moduleId}');
1949
-
1950
- ${modifiedCode}
1951
-
1952
- // Export layout from frontmatter for SSG routing
1953
- export const layout = ${frontmatter.layout ? JSON.stringify(frontmatter.layout) : "undefined"};
1954
-
1955
- // Export headings for table of contents
1956
- export const headings = ${JSON.stringify(headings)};
1957
-
1958
- // Wrap MDXContent in a sigx component for HMR support
1959
- const MDXPage = __component(() => {
1960
- return () => _MDXContent({});
1961
- }, { name: '${componentName}' });
1962
-
1963
- export default MDXPage;
1964
-
1965
- if (import.meta.hot) {
1966
- // Accept HMR updates with a callback to trigger re-render after module is updated
1967
- import.meta.hot.accept((newModule) => {
1968
- if (newModule) {
1969
- // Notify LayoutRouter to clear its cache and re-render with new module
1970
- window.dispatchEvent(new CustomEvent('sigx:mdx-hmr', {
1971
- detail: { moduleId: '${moduleId}', newModule }
1972
- }));
1973
- }
1974
- });
1975
- }
1976
- `;
1977
- }
1978
- return `
1979
- ${code}
1980
-
1981
- // Export layout from frontmatter for SSG routing
1982
- export const layout = ${frontmatter.layout ? JSON.stringify(frontmatter.layout) : "undefined"};
1983
-
1984
- // Export headings for table of contents
1985
- export const headings = ${JSON.stringify(headings)};
1986
- `;
1987
- }
1988
- async function extractHeadingsFromContent(content, options) {
1989
- const { unified } = await import("unified");
1990
- const remarkParse = (await import("remark-parse")).default;
1991
- const remarkRehype = (await import("remark-rehype")).default;
1992
- const rehypeSlug = (await import("rehype-slug")).default;
1993
- const rehypeStringify = (await import("rehype-stringify")).default;
1994
- const tocConfig = options.ssgConfig?.toc || { minLevel: 2, maxLevel: 3 };
1995
- const processor = unified().use(remarkParse).use(remarkRehype).use(rehypeSlug).use(rehypeExtractHeadings, tocConfig).use(rehypeStringify);
1996
- const file = await processor.process(content);
1997
- return file.data.headings || [];
1998
- }
1999
-
2000
- // src/vite/plugin.ts
2001
- var VIRTUAL_CONFIG_ID = "virtual:ssg-config";
2002
- var RESOLVED_VIRTUAL_CONFIG_ID = "\0" + VIRTUAL_CONFIG_ID;
2003
- function ssgPlugin(options = {}) {
2004
- let config;
2005
- let ssgConfig;
2006
- let root;
2007
- let server;
2008
- let entryDetection;
2009
- let routesCache = null;
2010
- let layoutsCache = null;
2011
- let navigationCache = null;
2012
- const frontmatterHashCache = /* @__PURE__ */ new Map();
2013
- const mainPlugin = {
2014
- name: "sigx-ssg",
2015
- enforce: "pre",
2016
- async configResolved(resolvedConfig) {
2017
- config = resolvedConfig;
2018
- root = resolvedConfig.root;
2019
- const fileConfig = await loadConfig(options.configPath);
2020
- ssgConfig = defineSSGConfig({
2021
- ...fileConfig,
2022
- ...options
2023
- });
2024
- entryDetection = detectCustomEntries(root, ssgConfig);
2025
- if (entryDetection.useVirtualClient || entryDetection.useVirtualServer) {
2026
- console.log("\u{1F4E6} @sigx/ssg: Using zero-config mode");
2027
- if (entryDetection.useVirtualClient) {
2028
- console.log(" \u2192 Virtual client entry");
2029
- }
2030
- if (entryDetection.useVirtualServer) {
2031
- console.log(" \u2192 Virtual server entry");
2032
- }
2033
- if (entryDetection.globalCssPath) {
2034
- console.log(` \u2192 Auto-importing ${entryDetection.globalCssPath}`);
2035
- }
2036
- }
2037
- },
2038
- configureServer(devServer) {
2039
- server = devServer;
2040
- const pagesDir = path7.resolve(root, ssgConfig.pages || "src/pages");
2041
- const layoutsDir = path7.resolve(root, ssgConfig.layouts || "src/layouts");
2042
- const contentDir = path7.resolve(root, ssgConfig.content || "src/content");
2043
- devServer.watcher.on("add", (file) => {
2044
- if (file.startsWith(pagesDir)) {
2045
- routesCache = null;
2046
- navigationCache = null;
2047
- invalidateModule(RESOLVED_VIRTUAL_ROUTES_ID);
2048
- invalidateModule(RESOLVED_VIRTUAL_NAVIGATION_ID);
2049
- } else if (file.startsWith(layoutsDir)) {
2050
- layoutsCache = null;
2051
- invalidateModule(RESOLVED_VIRTUAL_LAYOUTS_ID);
2052
- }
2053
- });
2054
- devServer.watcher.on("unlink", (file) => {
2055
- if (file.startsWith(pagesDir)) {
2056
- routesCache = null;
2057
- navigationCache = null;
2058
- frontmatterHashCache.delete(file);
2059
- invalidateModule(RESOLVED_VIRTUAL_ROUTES_ID);
2060
- invalidateModule(RESOLVED_VIRTUAL_NAVIGATION_ID);
2061
- } else if (file.startsWith(layoutsDir)) {
2062
- layoutsCache = null;
2063
- invalidateModule(RESOLVED_VIRTUAL_LAYOUTS_ID);
2064
- }
2065
- });
2066
- devServer.watcher.on("change", async (file) => {
2067
- if (!file.startsWith(pagesDir)) return;
2068
- if (!/\.mdx?$/.test(file)) return;
2069
- try {
2070
- const content = await fs6.promises.readFile(file, "utf-8");
2071
- const { data: newFrontmatter } = parseFrontmatter(content);
2072
- const newHash = JSON.stringify(newFrontmatter);
2073
- const oldHash = frontmatterHashCache.get(file);
2074
- frontmatterHashCache.set(file, newHash);
2075
- if (oldHash !== void 0 && oldHash !== newHash) {
2076
- navigationCache = null;
2077
- routesCache = null;
2078
- const navMod = devServer.moduleGraph.getModuleById(RESOLVED_VIRTUAL_NAVIGATION_ID);
2079
- if (navMod) {
2080
- devServer.moduleGraph.invalidateModule(navMod);
2081
- }
2082
- const routesMod = devServer.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ROUTES_ID);
2083
- if (routesMod) {
2084
- devServer.moduleGraph.invalidateModule(routesMod);
2085
- }
2086
- devServer.ws.send({ type: "full-reload" });
2087
- }
2088
- } catch (err) {
2089
- }
2090
- });
2091
- function invalidateModule(id) {
2092
- const mod = devServer.moduleGraph.getModuleById(id);
2093
- if (mod) {
2094
- devServer.moduleGraph.invalidateModule(mod);
2095
- devServer.ws.send({ type: "full-reload" });
2096
- }
2097
- }
2098
- if (entryDetection.useVirtualHtml) {
2099
- devServer.middlewares.use((req, res, next) => {
2100
- if (req.url?.startsWith("/@") || req.url?.startsWith("/__") || req.url?.includes("virtual:") || req.url?.includes("node_modules") || req.url?.startsWith("/@vite") || req.url?.startsWith("/@fs")) {
2101
- return next();
2102
- }
2103
- if (req.url && (req.url === "/" || !req.url.includes("."))) {
2104
- const html = generateHtmlTemplate(ssgConfig);
2105
- devServer.transformIndexHtml(req.url, html).then((transformedHtml) => {
2106
- res.setHeader("Content-Type", "text/html");
2107
- res.end(transformedHtml);
2108
- }).catch(next);
2109
- return;
2110
- }
2111
- next();
2112
- });
2113
- }
2114
- },
2115
- resolveId(id) {
2116
- if (id === VIRTUAL_ROUTES_ID) {
2117
- return RESOLVED_VIRTUAL_ROUTES_ID;
2118
- }
2119
- if (id === VIRTUAL_LAYOUTS_ID) {
2120
- return RESOLVED_VIRTUAL_LAYOUTS_ID;
2121
- }
2122
- if (id === VIRTUAL_CONFIG_ID) {
2123
- return RESOLVED_VIRTUAL_CONFIG_ID;
2124
- }
2125
- if (id === VIRTUAL_NAVIGATION_ID) {
2126
- return RESOLVED_VIRTUAL_NAVIGATION_ID;
2127
- }
2128
- if (id === VIRTUAL_CLIENT_ID || id === SSG_CLIENT_ENTRY_PATH) {
2129
- return RESOLVED_VIRTUAL_CLIENT_ID;
2130
- }
2131
- if (id === VIRTUAL_SERVER_ID) {
2132
- return RESOLVED_VIRTUAL_SERVER_ID;
2133
- }
2134
- return null;
2135
- },
2136
- async load(id) {
2137
- if (id === RESOLVED_VIRTUAL_ROUTES_ID) {
2138
- if (!routesCache) {
2139
- const routes = await scanPages(ssgConfig, root);
2140
- const code = config.command === "serve" ? generateLazyRoutesModule(routes, ssgConfig) : generateRoutesModule(routes, ssgConfig);
2141
- routesCache = { routes, code };
2142
- }
2143
- return routesCache.code;
2144
- }
2145
- if (id === RESOLVED_VIRTUAL_LAYOUTS_ID) {
2146
- if (!layoutsCache) {
2147
- const layouts = await discoverLayouts(ssgConfig, root);
2148
- const code = generateLayoutsModule(layouts, ssgConfig);
2149
- layoutsCache = { layouts, code };
2150
- }
2151
- return layoutsCache.code;
2152
- }
2153
- if (id === RESOLVED_VIRTUAL_NAVIGATION_ID) {
2154
- if (!navigationCache) {
2155
- if (!routesCache) {
2156
- const routes = await scanPages(ssgConfig, root);
2157
- const routesCode = config.command === "serve" ? generateLazyRoutesModule(routes, ssgConfig) : generateRoutesModule(routes, ssgConfig);
2158
- routesCache = { routes, code: routesCode };
2159
- }
2160
- const isDev = config.command === "serve";
2161
- const code = generateNavigationModule(routesCache.routes, ssgConfig, isDev);
2162
- navigationCache = { code };
2163
- }
2164
- return navigationCache.code;
2165
- }
2166
- if (id === RESOLVED_VIRTUAL_CONFIG_ID) {
2167
- return `export default ${JSON.stringify(ssgConfig)};`;
2168
- }
2169
- if (id === RESOLVED_VIRTUAL_CLIENT_ID) {
2170
- const code = generateClientEntry(ssgConfig, entryDetection);
2171
- const esbuild = await import("esbuild");
2172
- const result = await esbuild.transform(code, {
2173
- loader: "tsx",
2174
- jsx: "automatic",
2175
- jsxImportSource: "sigx"
2176
- });
2177
- return result.code;
2178
- }
2179
- if (id === RESOLVED_VIRTUAL_SERVER_ID) {
2180
- const code = generateServerEntry(ssgConfig);
2181
- const esbuild = await import("esbuild");
2182
- const result = await esbuild.transform(code, {
2183
- loader: "tsx",
2184
- jsx: "automatic",
2185
- jsxImportSource: "sigx"
2186
- });
2187
- return result.code;
2188
- }
2189
- return null;
2190
- },
2191
- // Handle HMR for layouts and pages
2192
- async handleHotUpdate({ file, server: server2 }) {
2193
- const layoutsDir = path7.resolve(root, ssgConfig.layouts || "src/layouts");
2194
- const pagesDir = path7.resolve(root, ssgConfig.pages || "src/pages");
2195
- if (file.startsWith(layoutsDir)) {
2196
- layoutsCache = null;
2197
- const mod = server2.moduleGraph.getModuleById(RESOLVED_VIRTUAL_LAYOUTS_ID);
2198
- if (mod) {
2199
- server2.moduleGraph.invalidateModule(mod);
2200
- }
2201
- return [];
2202
- }
2203
- if (file.startsWith(pagesDir) && /\.mdx?$/.test(file)) {
2204
- return void 0;
2205
- }
2206
- return void 0;
2207
- }
2208
- };
2209
- const enableMdx = options.enableMdx !== false;
2210
- if (enableMdx) {
2211
- const mdx = mdxPlugin({
2212
- markdown: options.markdown,
2213
- ssgConfig: void 0
2214
- // Will be set by configResolved
2215
- });
2216
- return [mainPlugin, mdx];
2217
- }
2218
- return [mainPlugin];
2219
- }
2220
-
2221
- // src/dev.ts
2222
- async function dev(options = {}) {
2223
- const root = process.cwd();
2224
- console.log("\n\u{1F680} @sigx/ssg - Starting development server...\n");
2225
- const ssgConfig = await loadConfig(options.configPath);
2226
- const hasViteConfig = fs7.existsSync(path8.join(root, "vite.config.ts")) || fs7.existsSync(path8.join(root, "vite.config.js")) || fs7.existsSync(path8.join(root, "vite.config.mjs"));
2227
- const vite = await import("vite");
2228
- if (hasViteConfig) {
2229
- console.log("\u{1F4E6} Using existing vite.config\n");
2230
- const server = await vite.createServer({
2231
- root,
2232
- server: {
2233
- port: options.port,
2234
- host: options.host,
2235
- open: options.open
2236
- }
2237
- });
2238
- await server.listen();
2239
- server.printUrls();
2240
- } else {
2241
- console.log("\u{1F4E6} Zero-config mode enabled\n");
2242
- let sigxPlugin;
2243
- try {
2244
- const sigxVite = await import("@sigx/vite");
2245
- sigxPlugin = sigxVite.sigxPlugin;
2246
- } catch {
2247
- console.warn("\u26A0\uFE0F @sigx/vite not found, JSX transform may not work");
2248
- console.warn(" Install with: npm install @sigx/vite\n");
2249
- }
2250
- const plugins = [];
2251
- try {
2252
- const tailwind = await import("@tailwindcss/vite");
2253
- plugins.push(tailwind.default());
2254
- } catch {
2255
- }
2256
- if (sigxPlugin) {
2257
- plugins.push(sigxPlugin());
2258
- }
2259
- plugins.push(...ssgPlugin({ configPath: options.configPath }));
2260
- const server = await vite.createServer({
2261
- root,
2262
- plugins,
2263
- // Vite 8 uses oxc instead of esbuild for JSX transforms
2264
- oxc: {
2265
- jsx: {
2266
- runtime: "automatic",
2267
- importSource: "sigx"
2268
- }
2269
- },
2270
- server: {
2271
- port: options.port ?? 5173,
2272
- host: options.host,
2273
- open: options.open
2274
- }
2275
- });
2276
- await server.listen();
2277
- server.printUrls();
2278
- }
2279
- }
2280
- async function preview(options = {}) {
2281
- const root = process.cwd();
2282
- console.log("\n\u{1F440} @sigx/ssg - Preview server...\n");
2283
- const vite = await import("vite");
2284
- const server = await vite.preview({
2285
- root,
2286
- preview: {
2287
- port: options.port ?? 4173,
2288
- host: options.host,
2289
- open: options.open
2290
- }
2291
- });
2292
- server.printUrls();
2293
- console.log("\n");
2294
- }
2295
-
2296
- // src/cli.ts
2297
- var __cli__ = true;
2298
- var args = process.argv.slice(2);
2299
- var command = args[0];
2300
- var configPath = args.find((a) => a.startsWith("--config="))?.split("=")[1];
2301
- var port = parseInt(args.find((a) => a.startsWith("--port="))?.split("=")[1] || "");
2302
- var host = args.includes("--host") ? true : args.find((a) => a.startsWith("--host="))?.split("=")[1];
2303
- var open = args.includes("--open");
2304
- var verbose = args.includes("--verbose") || args.includes("-v");
2305
- if (command === "dev") {
2306
- dev({
2307
- configPath,
2308
- port: isNaN(port) ? void 0 : port,
2309
- host,
2310
- open,
2311
- verbose
2312
- }).catch((err) => {
2313
- console.error("Dev server failed:", err);
2314
- process.exit(1);
2315
- });
2316
- } else if (command === "build") {
2317
- build({ configPath, verbose }).catch((err) => {
2318
- console.error("Build failed:", err);
2319
- process.exit(1);
2320
- });
2321
- } else if (command === "preview") {
2322
- preview({
2323
- configPath,
2324
- port: isNaN(port) ? void 0 : port,
2325
- host,
2326
- open
2327
- }).catch((err) => {
2328
- console.error("Preview server failed:", err);
2329
- process.exit(1);
2330
- });
2331
- } else {
2332
- console.log(`
2333
- @sigx/ssg - Static Site Generator for SignalX
2334
-
2335
- Usage:
2336
- ssg dev [options] Start development server
2337
- ssg build [options] Build static site for production
2338
- ssg preview [options] Preview production build locally
2339
-
2340
- Options:
2341
- --config=path Path to ssg.config.ts (default: ./ssg.config.ts)
2342
- --port=number Port for dev/preview server (default: 5173/4173)
2343
- --host Expose to network (or --host=0.0.0.0)
2344
- --open Open browser automatically
2345
- --verbose, -v Enable verbose logging
2346
-
2347
- Examples:
2348
- ssg dev Start dev server on localhost:5173
2349
- ssg dev --port=3000 --open Start on port 3000 and open browser
2350
- ssg build Build static site to ./dist
2351
- ssg preview Preview built site on localhost:4173
2352
- `);
2353
- }
2354
- export {
2355
- __cli__
2356
- };
2357
- //# sourceMappingURL=cli.js.map