@jxsuite/compiler 0.32.0 → 0.34.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/dist/cli.js +133 -119
- package/dist/compiler.js +29 -29
- package/package.json +5 -5
- package/src/site/site-build.ts +103 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.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.
|
|
47
|
-
"@jxsuite/runtime": "^0.
|
|
48
|
-
"@jxsuite/schema": "^0.
|
|
49
|
-
"sharp": "^0.35.
|
|
46
|
+
"@jxsuite/parser": "^0.34.0",
|
|
47
|
+
"@jxsuite/runtime": "^0.34.0",
|
|
48
|
+
"@jxsuite/schema": "^0.34.0",
|
|
49
|
+
"sharp": "^0.35.3"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@happy-dom/global-registrator": "^20.10.6",
|
package/src/site/site-build.ts
CHANGED
|
@@ -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
|
-
// ──
|
|
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("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
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("&", "&")
|
|
1235
|
+
.replaceAll("<", "<")
|
|
1236
|
+
.replaceAll(">", ">")
|
|
1237
|
+
.replaceAll('"', """)
|
|
1238
|
+
.replaceAll("'", "'");
|
|
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
|
+
}
|