@jxsuite/compiler 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/compiler",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "Jx static HTML compiler, island detector, and site builder",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -43,10 +43,10 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@apidevtools/json-schema-ref-parser": "^15.4.0",
46
- "@jxsuite/parser": "^0.32.0",
47
- "@jxsuite/runtime": "^0.32.0",
48
- "@jxsuite/schema": "^0.32.0",
49
- "sharp": "^0.35.2"
46
+ "@jxsuite/parser": "^0.33.0",
47
+ "@jxsuite/runtime": "^0.33.0",
48
+ "@jxsuite/schema": "^0.33.0",
49
+ "sharp": "^0.35.3"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@happy-dom/global-registrator": "^20.10.6",
@@ -13,8 +13,10 @@ import {
13
13
  cpSync,
14
14
  existsSync,
15
15
  mkdirSync,
16
+ readFileSync,
16
17
  readdirSync,
17
18
  rmSync,
19
+ statSync,
18
20
  writeFileSync,
19
21
  } from "node:fs";
20
22
  import { basename, dirname, join, resolve } from "node:path";
@@ -220,6 +222,12 @@ export async function buildSite(
220
222
  );
221
223
  }
222
224
 
225
+ // Sitemap is generated from the route table when a production `url` is configured
226
+ // (absolute <loc> URLs require it) and not explicitly disabled via build.sitemap: false.
227
+ const siteUrl = projectConfig.url;
228
+ const sitemapEnabled = Boolean(siteUrl) && projectConfig.build.sitemap !== false;
229
+ const sitemapEntries: { loc: string; lastmod: Date }[] = [];
230
+
223
231
  for (const route of routes) {
224
232
  try {
225
233
  log(` Compiling ${route.urlPattern} ...`);
@@ -258,6 +266,17 @@ export async function buildSite(
258
266
  writeFileSync(outPath, result.html, "utf8");
259
267
  fileCount += 1;
260
268
 
269
+ // Record a sitemap entry for this concrete page (skip unexpanded dynamic routes and
270
+ // Pages that opted out via $sitemap: false). <loc> is built like the canonical URL so
271
+ // The two always agree.
272
+ const isConcrete = !route.urlPattern.includes(":") && !route.urlPattern.includes("*");
273
+ if (sitemapEnabled && !result.excludeFromSitemap && isConcrete) {
274
+ sitemapEntries.push({
275
+ lastmod: statSync(route.sourcePath).mtime,
276
+ loc: new URL(route.urlPattern, siteUrl).href,
277
+ });
278
+ }
279
+
261
280
  // Write serialized export sidecars alongside HTML (formats with exportTarget: true)
262
281
  for (const fmt of formatRegistry.withCapability("serialize")) {
263
282
  if (!fmt.exportTarget) {
@@ -378,12 +397,26 @@ export async function buildSite(
378
397
  fileCount += redirectFiles;
379
398
  }
380
399
 
381
- // ── 7. Copy public/ assets ──────────────────────────────────────────────
400
+ // ── 7b. Generate sitemap.xml ────────────────────────────────────────────
401
+ if (sitemapEnabled) {
402
+ log(`Generating sitemap (${sitemapEntries.length} URL(s))...`);
403
+ fileCount += generateSitemap(sitemapEntries, outDir);
404
+ } else if (!siteUrl && projectConfig.build.sitemap !== false) {
405
+ console.warn("sitemap.xml skipped — set `url` in project.json to enable sitemap generation.");
406
+ }
407
+
408
+ // ── 7c. Copy public/ assets ─────────────────────────────────────────────
382
409
  if (existsSync(publicDir)) {
383
410
  log("Copying public/ assets...");
384
411
  cpSync(publicDir, outDir, { recursive: true });
385
412
  }
386
413
 
414
+ // ── 7d. Reference the sitemap from robots.txt ───────────────────────────
415
+ // Runs after the public/ copy so it edits the deployed dist/robots.txt.
416
+ if (sitemapEnabled) {
417
+ fileCount += ensureRobotsSitemap(outDir, siteUrl);
418
+ }
419
+
387
420
  // ── 8. Copy declarative file mappings ──────────────────────────────────
388
421
  if (projectConfig.copy) {
389
422
  log("Copying mapped files...");
@@ -575,6 +608,9 @@ async function compilePage(
575
608
 
576
609
  return {
577
610
  doc: layoutDoc,
611
+ // A page opts out of the sitemap by setting `$sitemap: false` (interim escape hatch
612
+ // Until draft filtering lands in the build pipeline).
613
+ excludeFromSitemap: pageDoc.$sitemap === false,
578
614
  files: result.files,
579
615
  html: result.html,
580
616
  serverHandler,
@@ -1185,3 +1221,69 @@ function escapeHtml(str: string) {
1185
1221
  function escapeAttr(str: string) {
1186
1222
  return String(str).replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;");
1187
1223
  }
1224
+
1225
+ /**
1226
+ * Escape a value for inclusion in XML text (e.g. a sitemap `<loc>`). Handles the five predefined
1227
+ * XML entities — `&` must be first so the others aren't double-escaped.
1228
+ *
1229
+ * @param {string} str
1230
+ * @returns {string}
1231
+ */
1232
+ function escapeXml(str: string) {
1233
+ return String(str)
1234
+ .replaceAll("&", "&amp;")
1235
+ .replaceAll("<", "&lt;")
1236
+ .replaceAll(">", "&gt;")
1237
+ .replaceAll('"', "&quot;")
1238
+ .replaceAll("'", "&apos;");
1239
+ }
1240
+
1241
+ /**
1242
+ * Write a sitemap.xml from the collected page entries (sitemaps.org urlset 0.9). Each entry emits a
1243
+ * `<loc>` plus a `<lastmod>` date (W3C YYYY-MM-DD).
1244
+ *
1245
+ * @param {{ loc: string; lastmod: Date }[]} entries
1246
+ * @param {string} outDir
1247
+ * @returns {number} Number of files written
1248
+ */
1249
+ function generateSitemap(entries: { loc: string; lastmod: Date }[], outDir: string) {
1250
+ if (entries.length === 0) {
1251
+ return 0;
1252
+ }
1253
+ const urls = entries
1254
+ .map(
1255
+ (e) =>
1256
+ ` <url>\n <loc>${escapeXml(e.loc)}</loc>\n` +
1257
+ ` <lastmod>${e.lastmod.toISOString().slice(0, 10)}</lastmod>\n </url>`,
1258
+ )
1259
+ .join("\n");
1260
+ const xml =
1261
+ `<?xml version="1.0" encoding="UTF-8"?>\n` +
1262
+ `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls}\n</urlset>\n`;
1263
+ writeFileSync(join(outDir, "sitemap.xml"), xml, "utf8");
1264
+ return 1;
1265
+ }
1266
+
1267
+ /**
1268
+ * Ensure dist/robots.txt references the sitemap. Appends a `Sitemap:` line to an existing
1269
+ * robots.txt (creating a permissive default if none was copied from public/), unless one is already
1270
+ * present.
1271
+ *
1272
+ * @param {string} outDir
1273
+ * @param {string} siteUrl
1274
+ * @returns {number} Number of files newly created (0 if robots.txt already existed)
1275
+ */
1276
+ function ensureRobotsSitemap(outDir: string, siteUrl: string) {
1277
+ const robotsPath = join(outDir, "robots.txt");
1278
+ const existed = existsSync(robotsPath);
1279
+ let content = existed ? readFileSync(robotsPath, "utf8") : "User-agent: *\nAllow: /\n";
1280
+ if (/^Sitemap:/im.test(content)) {
1281
+ return 0;
1282
+ }
1283
+ if (!content.endsWith("\n")) {
1284
+ content += "\n";
1285
+ }
1286
+ content += `\nSitemap: ${new URL("/sitemap.xml", siteUrl).href}\n`;
1287
+ writeFileSync(robotsPath, content, "utf8");
1288
+ return existed ? 0 : 1;
1289
+ }