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