@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.
@@ -1,385 +0,0 @@
1
- import { A as resolveConfigPaths, C as extractParams, S as expandDynamicRoute, T as scanPages, k as loadConfig, l as generateProductionHtmlTemplate, m as discoverLayouts, o as detectCustomEntries, s as generateClientEntry, u as generateServerEntry, w as isDynamicRoute } from "./virtual-entries-TuNN2It1.js";
2
- import path from "node:path";
3
- import fs from "node:fs/promises";
4
- import fsSync from "node:fs";
5
- import { pathToFileURL } from "node:url";
6
- //#region src/sitemap.ts
7
- /**
8
- * Sitemap Generation
9
- *
10
- * Generates XML sitemaps for SSG sites following the sitemap protocol.
11
- * https://www.sitemaps.org/protocol.html
12
- */
13
- /**
14
- * Generate sitemap XML content
15
- */
16
- function generateSitemap(entries, config) {
17
- const siteUrl = config.site?.url?.replace(/\/$/, "") || "";
18
- const base = config.base?.replace(/\/$/, "") || "";
19
- return `<?xml version="1.0" encoding="UTF-8"?>
20
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
21
- ${entries.map((entry) => {
22
- const loc = `${siteUrl}${base}${entry.path}`;
23
- const lastmod = entry.lastmod ? typeof entry.lastmod === "string" ? entry.lastmod : entry.lastmod.toISOString().split("T")[0] : void 0;
24
- return ` <url>
25
- <loc>${escapeXml(loc)}</loc>${lastmod ? `
26
- <lastmod>${lastmod}</lastmod>` : ""}${entry.changefreq ? `
27
- <changefreq>${entry.changefreq}</changefreq>` : ""}${entry.priority !== void 0 ? `
28
- <priority>${entry.priority.toFixed(1)}</priority>` : ""}
29
- </url>`;
30
- }).join("\n")}
31
- </urlset>`;
32
- }
33
- /**
34
- * Generate robots.txt content
35
- */
36
- function generateRobotsTxt(config, sitemapPath = "/sitemap.xml") {
37
- return `User-agent: *
38
- Allow: /
39
-
40
- Sitemap: ${config.site?.url?.replace(/\/$/, "") || ""}${config.base?.replace(/\/$/, "") || ""}${sitemapPath}
41
- `;
42
- }
43
- /**
44
- * Convert page build results to sitemap entries
45
- */
46
- function pagesToSitemapEntries(pages, options = {}) {
47
- const { exclude = [], defaultChangefreq = "weekly", defaultPriority = .5 } = options;
48
- return pages.filter((page) => {
49
- for (const pattern of exclude) if (pattern.includes("*")) {
50
- if (new RegExp("^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$").test(page.path)) return false;
51
- } else if (page.path === pattern) return false;
52
- return true;
53
- }).map((page) => {
54
- const depth = page.path.split("/").filter(Boolean).length;
55
- let priority = defaultPriority;
56
- if (page.path === "/") priority = 1;
57
- else if (depth === 1) priority = .8;
58
- else if (depth === 2) priority = .6;
59
- return {
60
- path: page.path,
61
- changefreq: defaultChangefreq,
62
- priority
63
- };
64
- });
65
- }
66
- /**
67
- * Write sitemap and robots.txt to output directory
68
- */
69
- async function writeSitemap(pages, config, outDir, options = {}) {
70
- const entries = pagesToSitemapEntries(pages, options);
71
- if (options.additionalUrls) entries.push(...options.additionalUrls);
72
- const sitemapContent = generateSitemap(entries, config);
73
- const sitemapPath = path.join(outDir, "sitemap.xml");
74
- await fs.writeFile(sitemapPath, sitemapContent, "utf-8");
75
- const robotsContent = generateRobotsTxt(config);
76
- const robotsPath = path.join(outDir, "robots.txt");
77
- await fs.writeFile(robotsPath, robotsContent, "utf-8");
78
- return {
79
- sitemapPath,
80
- robotsPath
81
- };
82
- }
83
- /**
84
- * Escape special XML characters
85
- */
86
- function escapeXml(str) {
87
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
88
- }
89
- //#endregion
90
- //#region src/build.ts
91
- /**
92
- * SSG Build CLI
93
- *
94
- * Static site generation build process:
95
- * 1. Load configuration
96
- * 2. Scan routes and expand dynamic paths
97
- * 3. Build with Vite for production
98
- * 4. Render each route to static HTML
99
- * 5. Write output files
100
- * 6. Generate sitemap and robots.txt
101
- */
102
- /**
103
- * Build static site
104
- */
105
- async function build(options = {}) {
106
- const startTime = Date.now();
107
- const root = process.cwd();
108
- const warnings = [];
109
- const pages = [];
110
- console.log("\n🚀 @sigx/ssg - Building static site...\n");
111
- console.log("📦 Loading configuration...");
112
- const resolvedConfig = resolveConfigPaths(await loadConfig(options.configPath), root);
113
- console.log("🔍 Scanning pages...");
114
- const routes = await scanPages(resolvedConfig, root);
115
- console.log(` Found ${routes.length} page(s)`);
116
- console.log("📐 Discovering layouts...");
117
- const layouts = await discoverLayouts(resolvedConfig, root);
118
- console.log(` Found ${layouts.length} layout(s)`);
119
- const entryDetection = detectCustomEntries(root, resolvedConfig);
120
- if (entryDetection.useVirtualClient || entryDetection.useVirtualServer) {
121
- console.log("📦 Using zero-config mode");
122
- if (entryDetection.useVirtualClient) console.log(" → Virtual client entry");
123
- if (entryDetection.useVirtualServer) console.log(" → Virtual server entry");
124
- if (entryDetection.useVirtualHtml) console.log(" → Virtual HTML template");
125
- }
126
- const clientEntry = await getClientEntryPoint(resolvedConfig, root);
127
- const ssrEntry = await getSSREntryPoint(resolvedConfig, root);
128
- const htmlTemplatePath = path.join(root, "index.html");
129
- let cleanupHtml = false;
130
- let originalHtmlContent = null;
131
- if (!entryDetection.useVirtualHtml && fsSync.existsSync(htmlTemplatePath)) originalHtmlContent = fsSync.readFileSync(htmlTemplatePath, "utf-8");
132
- const htmlContent = await getHtmlTemplate(resolvedConfig, root, clientEntry);
133
- fsSync.writeFileSync(htmlTemplatePath, htmlContent, "utf-8");
134
- cleanupHtml = entryDetection.useVirtualHtml;
135
- console.log("🔨 Building with Vite...");
136
- const vite = await import("vite");
137
- try {
138
- const clientInput = htmlTemplatePath;
139
- await vite.build({
140
- root,
141
- mode: "production",
142
- build: {
143
- outDir: resolvedConfig.outDir,
144
- emptyOutDir: false,
145
- ssrManifest: true,
146
- rollupOptions: { input: clientInput }
147
- },
148
- logLevel: options.verbose ? "info" : "warn"
149
- });
150
- const ssrOutDir = path.join(resolvedConfig.outDir, ".ssg");
151
- await vite.build({
152
- root,
153
- mode: "production",
154
- build: {
155
- outDir: ssrOutDir,
156
- ssr: true,
157
- rollupOptions: { input: ssrEntry }
158
- },
159
- logLevel: options.verbose ? "info" : "warn"
160
- });
161
- console.log("📝 Collecting paths to render...");
162
- const pathsToRender = await collectPaths(routes, root, warnings);
163
- console.log(` ${pathsToRender.length} path(s) to render`);
164
- const outputDirs = /* @__PURE__ */ new Set();
165
- for (const pathInfo of pathsToRender) {
166
- const outputPath = getOutputPath(pathInfo.path, resolvedConfig.outDir);
167
- outputDirs.add(path.dirname(outputPath));
168
- }
169
- await Promise.all(Array.from(outputDirs).map((dir) => fs.mkdir(dir, { recursive: true })));
170
- console.log("🎨 Rendering pages...");
171
- const ssrEntryName = path.basename(ssrEntry, path.extname(ssrEntry)) + ".js";
172
- const entryModule = await import(pathToFileURL(path.join(ssrOutDir, ssrEntryName)).href);
173
- const templatePath = path.join(resolvedConfig.outDir, "index.html");
174
- const template = await fs.readFile(templatePath, "utf-8");
175
- const CONCURRENCY = options.concurrency ?? 20;
176
- const verbose = options.verbose ?? false;
177
- async function renderPage(pathInfo) {
178
- const renderStart = Date.now();
179
- try {
180
- const appHtml = await entryModule.render(pathInfo.path, {
181
- params: pathInfo.params,
182
- props: pathInfo.props
183
- });
184
- let html = template.replace("<!--app-html-->", appHtml);
185
- const headTags = generateHeadTags(pathInfo, resolvedConfig);
186
- html = html.replace("<!--head-tags-->", headTags);
187
- const outputPath = getOutputPath(pathInfo.path, resolvedConfig.outDir);
188
- const renderTime = Date.now() - renderStart;
189
- return {
190
- pathInfo,
191
- html,
192
- outputPath,
193
- renderTime
194
- };
195
- } catch (err) {
196
- const errorMessage = err instanceof Error ? err.message : String(err);
197
- console.error(` ❌ ${pathInfo.path}: ${errorMessage}`);
198
- warnings.push(`Failed to render ${pathInfo.path}: ${errorMessage}`);
199
- return null;
200
- }
201
- }
202
- console.log(" Phase 1: Rendering...");
203
- const renderPhaseStart = Date.now();
204
- const renderResults = [];
205
- for (let i = 0; i < pathsToRender.length; i += CONCURRENCY) {
206
- const batch = pathsToRender.slice(i, i + CONCURRENCY);
207
- const results = await Promise.all(batch.map(renderPage));
208
- for (const result of results) if (result) renderResults.push(result);
209
- }
210
- const renderPhaseDuration = Date.now() - renderPhaseStart;
211
- console.log(` Phase 1 complete: ${renderResults.length} pages in ${renderPhaseDuration}ms (${Math.round(renderPhaseDuration / renderResults.length)}ms avg)`);
212
- console.log(" Phase 2: Writing files...");
213
- const writePhaseStart = Date.now();
214
- const WRITE_CONCURRENCY = 10;
215
- for (let i = 0; i < renderResults.length; i += WRITE_CONCURRENCY) {
216
- const batch = renderResults.slice(i, i + WRITE_CONCURRENCY);
217
- await Promise.all(batch.map(async (result) => {
218
- await fs.writeFile(result.outputPath, result.html, "utf-8");
219
- const size = Buffer.byteLength(result.html, "utf-8");
220
- pages.push({
221
- path: result.pathInfo.path,
222
- file: result.outputPath,
223
- time: result.renderTime,
224
- size
225
- });
226
- if (verbose) console.log(` ✓ ${result.pathInfo.path} (${result.renderTime}ms, ${formatBytes(size)})`);
227
- }));
228
- }
229
- const writePhaseDuration = Date.now() - writePhaseStart;
230
- console.log(` Phase 2 complete: ${renderResults.length} files in ${writePhaseDuration}ms`);
231
- if (!verbose) console.log(` ✓ Rendered ${renderResults.length} pages`);
232
- await fs.rm(ssrOutDir, {
233
- recursive: true,
234
- force: true
235
- });
236
- if (pages.length > 0) {
237
- console.log("🗺️ Generating sitemap...");
238
- await writeSitemap(pages, resolvedConfig, resolvedConfig.outDir);
239
- console.log(" ✓ sitemap.xml");
240
- console.log(" ✓ robots.txt");
241
- }
242
- } finally {
243
- await cleanupTempEntries(root);
244
- if (cleanupHtml) try {
245
- await fs.unlink(htmlTemplatePath);
246
- } catch {}
247
- else if (originalHtmlContent !== null) try {
248
- await fs.writeFile(htmlTemplatePath, originalHtmlContent, "utf-8");
249
- } catch {}
250
- }
251
- const totalTime = Date.now() - startTime;
252
- console.log(`\n✅ Built ${pages.length} page(s) in ${totalTime}ms`);
253
- if (warnings.length > 0) {
254
- console.log(`\n⚠️ ${warnings.length} warning(s):`);
255
- for (const warning of warnings) console.log(` - ${warning}`);
256
- }
257
- console.log(`\n📁 Output: ${resolvedConfig.outDir}\n`);
258
- return {
259
- pages,
260
- totalTime,
261
- warnings
262
- };
263
- }
264
- /**
265
- * Collect all paths to render, expanding dynamic routes
266
- */
267
- async function collectPaths(routes, root, warnings) {
268
- const paths = [];
269
- for (const route of routes) if (isDynamicRoute(route)) try {
270
- const pageModule = await import(pathToFileURL(route.file).href);
271
- if (!pageModule.getStaticPaths) {
272
- const params = extractParams(route.path).join(", ");
273
- console.warn(`\n⚠️ SSG102: Dynamic route missing getStaticPaths()\n 📁 ${route.file}\n Route: ${route.path} (params: ${params})\n 💡 Export getStaticPaths() to generate static pages:\n\n export async function getStaticPaths() {\n return [{ params: { ${params.split(", ")[0]}: 'value' } }];\n }\n`);
274
- warnings.push(`Route ${route.path} has dynamic segments [${params}] but no getStaticPaths() export. Skipping.`);
275
- continue;
276
- }
277
- const staticPaths = await pageModule.getStaticPaths();
278
- for (const staticPath of staticPaths) {
279
- const expandedPaths = expandDynamicRoute(route, [staticPath]);
280
- for (const expandedPath of expandedPaths) paths.push({
281
- path: expandedPath,
282
- route,
283
- params: staticPath.params,
284
- props: staticPath.props
285
- });
286
- }
287
- } catch (err) {
288
- warnings.push(`Failed to load ${route.file}: ${err}`);
289
- }
290
- else paths.push({
291
- path: route.path,
292
- route,
293
- params: {}
294
- });
295
- return paths;
296
- }
297
- /**
298
- * Generate head tags for a page
299
- */
300
- function generateHeadTags(pathInfo, config) {
301
- const tags = [];
302
- const meta = pathInfo.route.meta || {};
303
- const title = meta.title || config.site?.title;
304
- if (title) tags.push(`<title>${escapeHtml(title)}</title>`);
305
- const description = meta.description || config.site?.description;
306
- if (description) tags.push(`<meta name="description" content="${escapeHtml(description)}">`);
307
- if (config.site?.url) {
308
- const canonical = new URL(pathInfo.path, config.site.url).href;
309
- tags.push(`<link rel="canonical" href="${canonical}">`);
310
- }
311
- return tags.join("\n ");
312
- }
313
- /**
314
- * Get output file path for a URL path
315
- */
316
- function getOutputPath(urlPath, outDir) {
317
- let normalized = urlPath.replace(/^\//, "").replace(/\/$/, "");
318
- if (!normalized) normalized = "index";
319
- if (!normalized.endsWith(".html")) normalized = path.join(normalized, "index.html");
320
- return path.join(outDir, normalized);
321
- }
322
- /**
323
- * Get SSR entry point
324
- * Returns virtual module ID if no custom entry exists
325
- */
326
- async function getSSREntryPoint(config, root) {
327
- const detection = detectCustomEntries(root, config);
328
- if (!detection.useVirtualServer && detection.customServerPath) return detection.customServerPath;
329
- const virtualServerCode = generateServerEntry(config);
330
- const tempServerPath = path.join(root, ".ssg-temp-entry-server.tsx");
331
- fsSync.writeFileSync(tempServerPath, virtualServerCode, "utf-8");
332
- return tempServerPath;
333
- }
334
- /**
335
- * Get client entry point
336
- * Returns virtual module ID if no custom entry exists
337
- */
338
- async function getClientEntryPoint(config, root) {
339
- const detection = detectCustomEntries(root, config);
340
- if (!detection.useVirtualClient && detection.customClientPath) return detection.customClientPath;
341
- const virtualClientCode = generateClientEntry(config, detection);
342
- const tempClientPath = path.join(root, ".ssg-temp-entry-client.tsx");
343
- fsSync.writeFileSync(tempClientPath, virtualClientCode, "utf-8");
344
- return tempClientPath;
345
- }
346
- /**
347
- * Clean up temporary entry files
348
- */
349
- async function cleanupTempEntries(root) {
350
- const tempFiles = [path.join(root, ".ssg-temp-entry-server.tsx"), path.join(root, ".ssg-temp-entry-client.tsx")];
351
- for (const file of tempFiles) try {
352
- await fs.unlink(file);
353
- } catch {}
354
- }
355
- /**
356
- * Get or generate HTML template
357
- */
358
- async function getHtmlTemplate(config, root, clientEntryPath) {
359
- const detection = detectCustomEntries(root, config);
360
- if (!detection.useVirtualHtml && detection.customHtmlPath) {
361
- let html = await fs.readFile(detection.customHtmlPath, "utf-8");
362
- const relativePath = "./" + path.relative(root, clientEntryPath).replace(/\\/g, "/");
363
- html = html.replace(/<script([^>]*)\s+src=["']?\/@ssg\/client\.tsx["']?/g, `<script$1 src="${relativePath}"`);
364
- return html;
365
- }
366
- return generateProductionHtmlTemplate(config, clientEntryPath);
367
- }
368
- /**
369
- * Format bytes to human-readable string
370
- */
371
- function formatBytes(bytes) {
372
- if (bytes < 1024) return `${bytes}B`;
373
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
374
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
375
- }
376
- /**
377
- * Escape HTML special characters
378
- */
379
- function escapeHtml(str) {
380
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
381
- }
382
- //#endregion
383
- export { writeSitemap as a, pagesToSitemapEntries as i, generateRobotsTxt as n, generateSitemap as r, build as t };
384
-
385
- //# sourceMappingURL=build-qmtK32gt.js.map