@warlock.js/sitemap 5.19.0 → 5.20.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/CHANGELOG.md +15 -0
- package/cjs/index.cjs +194 -10
- package/cjs/index.cjs.map +1 -1
- package/esm/generation-manifest.d.mts +40 -0
- package/esm/generation-manifest.mjs +114 -0
- package/esm/generation-manifest.mjs.map +1 -0
- package/esm/index.d.mts +3 -2
- package/esm/index.mjs +2 -1
- package/esm/normalize-entry.mjs +19 -1
- package/esm/normalize-entry.mjs.map +1 -1
- package/esm/sitemap-index-options.mjs +10 -1
- package/esm/sitemap-index-options.mjs.map +1 -1
- package/esm/sitemap-index-types.d.mts +5 -3
- package/esm/sitemap-index-xml.mjs +2 -2
- package/esm/sitemap-index-xml.mjs.map +1 -1
- package/esm/sitemap-index.d.mts +2 -0
- package/esm/sitemap-index.mjs +13 -2
- package/esm/sitemap-index.mjs.map +1 -1
- package/esm/sitemap-shard-writer.mjs +9 -3
- package/esm/sitemap-shard-writer.mjs.map +1 -1
- package/esm/sitemap.d.mts +3 -0
- package/esm/sitemap.mjs +12 -1
- package/esm/sitemap.mjs.map +1 -1
- package/esm/types.d.mts +13 -3
- package/esm/xml.mjs +12 -2
- package/esm/xml.mjs.map +1 -1
- package/llms-full.txt +190 -161
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/skills/sitemap-overview/SKILL.md +190 -161
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to `@warlock.js/sitemap` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
+
## 5.20.0 - 2026-09-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `SitemapEntry.images` emits the Google image sitemap extension, validates
|
|
12
|
+
absolute HTTP(S) image URLs, and limits each URL to 1,000 images with a
|
|
13
|
+
caller-visible diagnostic.
|
|
14
|
+
- `SitemapIndexOptions.shardPathPrefix` controls public shard links in an
|
|
15
|
+
index without changing the files written by `saveTo()`.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Refined grouped skill discovery guidance and regenerated package llms
|
|
20
|
+
projections.
|
|
21
|
+
|
|
7
22
|
## 5.19.0 - 2026-09-23
|
|
8
23
|
|
|
9
24
|
### Changed
|
package/cjs/index.cjs
CHANGED
|
@@ -206,6 +206,7 @@ const CHANGE_FREQS = [
|
|
|
206
206
|
"yearly",
|
|
207
207
|
"never"
|
|
208
208
|
];
|
|
209
|
+
const MAX_IMAGES_PER_URL = 1e3;
|
|
209
210
|
function assertChangeFreq(value) {
|
|
210
211
|
if (!CHANGE_FREQS.includes(value)) throw new InvalidSitemapEntryError(`changefreq ${JSON.stringify(value)} is not one of ${CHANGE_FREQS.join(", ")}`);
|
|
211
212
|
}
|
|
@@ -238,6 +239,22 @@ function normalizeEntry(entry, defaults) {
|
|
|
238
239
|
path: normalizePath(alternate.path)
|
|
239
240
|
};
|
|
240
241
|
});
|
|
242
|
+
if (entry.images !== void 0 && !Array.isArray(entry.images)) throw new InvalidSitemapEntryError("images must be an array");
|
|
243
|
+
const images = entry.images?.slice(0, MAX_IMAGES_PER_URL).map((image) => {
|
|
244
|
+
if (typeof image?.loc !== "string" || image.loc.trim() === "") throw new InvalidSitemapEntryError("image loc is required");
|
|
245
|
+
let url;
|
|
246
|
+
try {
|
|
247
|
+
url = new URL(image.loc);
|
|
248
|
+
} catch {
|
|
249
|
+
throw new InvalidSitemapEntryError("image loc must be an absolute HTTP(S) URL");
|
|
250
|
+
}
|
|
251
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new InvalidSitemapEntryError("image loc must be an absolute HTTP(S) URL");
|
|
252
|
+
return { loc: image.loc };
|
|
253
|
+
});
|
|
254
|
+
if ((entry.images?.length ?? 0) > MAX_IMAGES_PER_URL) defaults.onImageLimitExceeded?.({
|
|
255
|
+
...entry.route === void 0 ? {} : { route: entry.route },
|
|
256
|
+
dropped: entry.images.length - MAX_IMAGES_PER_URL
|
|
257
|
+
});
|
|
241
258
|
return {
|
|
242
259
|
path,
|
|
243
260
|
...entry.name !== void 0 ? { name: entry.name } : {},
|
|
@@ -245,7 +262,8 @@ function normalizeEntry(entry, defaults) {
|
|
|
245
262
|
...lastmod !== void 0 ? { lastmod: formatLastmod(lastmod) } : {},
|
|
246
263
|
...changefreq !== void 0 ? { changefreq } : {},
|
|
247
264
|
...priority !== void 0 ? { priority } : {},
|
|
248
|
-
...alternates !== void 0 ? { alternates } : {}
|
|
265
|
+
...alternates !== void 0 ? { alternates } : {},
|
|
266
|
+
...images !== void 0 ? { images } : {}
|
|
249
267
|
};
|
|
250
268
|
}
|
|
251
269
|
|
|
@@ -301,12 +319,15 @@ function escapeXml(value) {
|
|
|
301
319
|
return value.replace(/[&<>"']/g, (char) => XML_ESCAPES[char] ?? char);
|
|
302
320
|
}
|
|
303
321
|
const XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
322
|
+
const IMAGE_NAMESPACE = "http://www.google.com/schemas/sitemap-image/1.1";
|
|
304
323
|
/**
|
|
305
324
|
* The `xmlns:xhtml` attribute exactly as it appears on `<urlset>`, including
|
|
306
325
|
* its leading space. Exported so the shard writer can charge its byte length
|
|
307
326
|
* against the ceiling without duplicating the string it measures.
|
|
308
327
|
*/
|
|
309
328
|
const XHTML_NAMESPACE_ATTR = ` xmlns:xhtml="${XHTML_NAMESPACE}"`;
|
|
329
|
+
/** See {@link XHTML_NAMESPACE_ATTR}; measured by the shard writer before roll-over. */
|
|
330
|
+
const IMAGE_NAMESPACE_ATTR = ` xmlns:image="${IMAGE_NAMESPACE}"`;
|
|
310
331
|
/**
|
|
311
332
|
* Renders one `<url>` block. Exported so the streaming writer can measure the
|
|
312
333
|
* exact bytes it is about to append before deciding whether the byte ceiling
|
|
@@ -322,6 +343,11 @@ function renderUrlBlock(entry, baseUrl) {
|
|
|
322
343
|
const href = escapeXml(resolveAgainstBase(baseUrl, alternate.path));
|
|
323
344
|
lines.push(` <xhtml:link rel="alternate" hreflang="${escapeXml(alternate.hreflang)}" href="${href}"/>`);
|
|
324
345
|
}
|
|
346
|
+
for (const image of entry.images ?? []) {
|
|
347
|
+
lines.push(` <image:image>`);
|
|
348
|
+
lines.push(` <image:loc>${escapeXml(resolveAgainstBase(baseUrl, image.loc))}</image:loc>`);
|
|
349
|
+
lines.push(` </image:image>`);
|
|
350
|
+
}
|
|
325
351
|
lines.push(` </url>`);
|
|
326
352
|
return lines.join("\n");
|
|
327
353
|
}
|
|
@@ -335,7 +361,9 @@ function renderUrlBlock(entry, baseUrl) {
|
|
|
335
361
|
* alternate — an unused namespace on every single-language sitemap is noise.
|
|
336
362
|
*/
|
|
337
363
|
function buildSitemapXml(entries, baseUrl) {
|
|
338
|
-
const
|
|
364
|
+
const hasAlternates = entries.some((entry) => (entry.alternates?.length ?? 0) > 0);
|
|
365
|
+
const hasImages = entries.some((entry) => (entry.images?.length ?? 0) > 0);
|
|
366
|
+
const namespaces = `${hasAlternates ? XHTML_NAMESPACE_ATTR : ""}${hasImages ? IMAGE_NAMESPACE_ATTR : ""}`;
|
|
339
367
|
const body = entries.map((entry) => renderUrlBlock(entry, baseUrl)).join("\n");
|
|
340
368
|
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"${namespaces}>\n` + (body.length > 0 ? `${body}\n` : "") + `</urlset>\n`;
|
|
341
369
|
}
|
|
@@ -355,6 +383,8 @@ function buildSitemapXml(entries, baseUrl) {
|
|
|
355
383
|
var Sitemap = class {
|
|
356
384
|
baseUrl;
|
|
357
385
|
defaults;
|
|
386
|
+
warnedImageRoutes = /* @__PURE__ */ new Set();
|
|
387
|
+
optionsImageReporter;
|
|
358
388
|
/** Keyed by path: a duplicate `<loc>` makes the document invalid, so the later add wins. */
|
|
359
389
|
entriesByPath = /* @__PURE__ */ new Map();
|
|
360
390
|
/** Insertion order per path, so `duplicates()` can name every colliding source. */
|
|
@@ -363,12 +393,21 @@ var Sitemap = class {
|
|
|
363
393
|
declaredRoutes = /* @__PURE__ */ new Set();
|
|
364
394
|
constructor(options) {
|
|
365
395
|
this.baseUrl = normalizeBaseUrl(options?.baseUrl);
|
|
396
|
+
this.optionsImageReporter = options.onImageLimitExceeded;
|
|
366
397
|
this.defaults = {
|
|
367
398
|
changefreq: options.changefreq,
|
|
368
399
|
priority: options.priority,
|
|
369
|
-
lastmod: options.lastmod
|
|
400
|
+
lastmod: options.lastmod,
|
|
401
|
+
onImageLimitExceeded: (event) => this.reportImageLimit(event)
|
|
370
402
|
};
|
|
371
403
|
}
|
|
404
|
+
reportImageLimit(event) {
|
|
405
|
+
const key = event.route ?? "unattributed";
|
|
406
|
+
if (this.warnedImageRoutes.has(key)) return;
|
|
407
|
+
this.warnedImageRoutes.add(key);
|
|
408
|
+
if (this.optionsImageReporter) this.optionsImageReporter(event);
|
|
409
|
+
else console.warn(`[warlock:sitemap] ${key} exceeded the 1,000-image limit; dropped ${event.dropped} image(s).`);
|
|
410
|
+
}
|
|
372
411
|
add(entry) {
|
|
373
412
|
const resolved = normalizeEntry(entry, this.defaults);
|
|
374
413
|
this.entriesByPath.set(resolved.path, resolved);
|
|
@@ -467,6 +506,7 @@ const PROTOCOL_MAX_BYTES_PER_FILE = 50 * 1024 * 1024;
|
|
|
467
506
|
/** Validates and folds in defaults, once, at the constructor — the same discipline as `Sitemap`. */
|
|
468
507
|
function normalizeSitemapIndexOptions(options) {
|
|
469
508
|
const baseUrl = normalizeBaseUrl(options?.baseUrl);
|
|
509
|
+
const shardPathPrefix = normalizeShardPathPrefix(options.shardPathPrefix);
|
|
470
510
|
const maxUrlsPerFile = options.maxUrlsPerFile ?? 5e4;
|
|
471
511
|
const maxBytesPerFile = options.maxBytesPerFile ?? 52428800;
|
|
472
512
|
if (!Number.isInteger(maxUrlsPerFile) || maxUrlsPerFile < 1 || maxUrlsPerFile > 5e4) throw new RangeError(`maxUrlsPerFile must be an integer between 1 and the sitemaps.org ceiling of ${PROTOCOL_MAX_URLS_PER_FILE}, got ${JSON.stringify(maxUrlsPerFile)}.`);
|
|
@@ -475,16 +515,24 @@ function normalizeSitemapIndexOptions(options) {
|
|
|
475
515
|
baseUrl,
|
|
476
516
|
filePrefix: options.filePrefix ?? "sitemap",
|
|
477
517
|
indexFileName: options.indexFileName ?? "sitemap_index.xml",
|
|
518
|
+
shardPathPrefix,
|
|
478
519
|
gzip: options.gzip ?? false,
|
|
479
520
|
maxUrlsPerFile,
|
|
480
521
|
maxBytesPerFile,
|
|
481
522
|
defaults: {
|
|
482
523
|
changefreq: options.changefreq,
|
|
483
524
|
priority: options.priority,
|
|
484
|
-
lastmod: options.lastmod
|
|
525
|
+
lastmod: options.lastmod,
|
|
526
|
+
onImageLimitExceeded: options.onImageLimitExceeded
|
|
485
527
|
}
|
|
486
528
|
};
|
|
487
529
|
}
|
|
530
|
+
function normalizeShardPathPrefix(value) {
|
|
531
|
+
if (value === void 0 || value === "" || value === "/") return "";
|
|
532
|
+
const prefix = value.replace(/^\//, "").replace(/\/$/, "");
|
|
533
|
+
if (prefix.length === 0 || prefix.split("/").some((segment) => !/^[A-Za-z0-9_-]+$/.test(segment))) throw new TypeError("shardPathPrefix must contain only safe URL path segments.");
|
|
534
|
+
return `${prefix}/`;
|
|
535
|
+
}
|
|
488
536
|
|
|
489
537
|
//#endregion
|
|
490
538
|
//#region ../sitemap/src/shard-name.ts
|
|
@@ -525,6 +573,7 @@ function envelopeBytes(baseUrl) {
|
|
|
525
573
|
* shard's real bytes on disk exceed the ceiling it was rolled against.
|
|
526
574
|
*/
|
|
527
575
|
const NAMESPACE_BYTES = Buffer.byteLength(XHTML_NAMESPACE_ATTR, "utf8");
|
|
576
|
+
const IMAGE_NAMESPACE_BYTES = Buffer.byteLength(IMAGE_NAMESPACE_ATTR, "utf8");
|
|
528
577
|
async function writeShardFile(ctx, fileName, entries) {
|
|
529
578
|
const xml = buildSitemapXml(entries, ctx.baseUrl);
|
|
530
579
|
const filePath = (0, node_path.join)(ctx.tempDir, fileName);
|
|
@@ -546,6 +595,7 @@ async function writeShardGroup(group, ctx) {
|
|
|
546
595
|
let buffer = [];
|
|
547
596
|
let bufferBytes = envelopeBytes(ctx.baseUrl);
|
|
548
597
|
let bufferHasAlternates = false;
|
|
598
|
+
let bufferHasImages = false;
|
|
549
599
|
let ordinal = 1;
|
|
550
600
|
const flush = async () => {
|
|
551
601
|
if (buffer.length === 0) return;
|
|
@@ -562,6 +612,7 @@ async function writeShardGroup(group, ctx) {
|
|
|
562
612
|
buffer = [];
|
|
563
613
|
bufferBytes = envelopeBytes(ctx.baseUrl);
|
|
564
614
|
bufferHasAlternates = false;
|
|
615
|
+
bufferHasImages = false;
|
|
565
616
|
};
|
|
566
617
|
for (const factory of group.factories) {
|
|
567
618
|
const source = await factory();
|
|
@@ -569,15 +620,18 @@ async function writeShardGroup(group, ctx) {
|
|
|
569
620
|
const resolved = normalizeEntry(rawEntry, ctx.defaults);
|
|
570
621
|
if (!ctx.duplicates.attempt(resolved.path, resolved.route)) continue;
|
|
571
622
|
const entryHasAlternates = (resolved.alternates?.length ?? 0) > 0;
|
|
623
|
+
const entryHasImages = (resolved.images?.length ?? 0) > 0;
|
|
572
624
|
const blockBytes = Buffer.byteLength(renderUrlBlock(resolved, ctx.baseUrl), "utf8") + 1;
|
|
573
|
-
const addedBytes = blockBytes + (entryHasAlternates && !bufferHasAlternates ? NAMESPACE_BYTES : 0);
|
|
625
|
+
const addedBytes = blockBytes + (entryHasAlternates && !bufferHasAlternates ? NAMESPACE_BYTES : 0) + (entryHasImages && !bufferHasImages ? IMAGE_NAMESPACE_BYTES : 0);
|
|
574
626
|
const hitsUrlCeiling = buffer.length >= ctx.maxUrlsPerFile;
|
|
575
627
|
const hitsByteCeiling = buffer.length > 0 && bufferBytes + addedBytes > ctx.maxBytesPerFile;
|
|
576
628
|
if (hitsUrlCeiling || hitsByteCeiling) await flush();
|
|
577
629
|
const addsNamespaceNow = entryHasAlternates && !bufferHasAlternates;
|
|
630
|
+
const addsImageNamespaceNow = entryHasImages && !bufferHasImages;
|
|
578
631
|
buffer.push(resolved);
|
|
579
|
-
bufferBytes += blockBytes + (addsNamespaceNow ? NAMESPACE_BYTES : 0);
|
|
632
|
+
bufferBytes += blockBytes + (addsNamespaceNow ? NAMESPACE_BYTES : 0) + (addsImageNamespaceNow ? IMAGE_NAMESPACE_BYTES : 0);
|
|
580
633
|
if (addsNamespaceNow) bufferHasAlternates = true;
|
|
634
|
+
if (addsImageNamespaceNow) bufferHasImages = true;
|
|
581
635
|
ctx.routes.record(resolved.route);
|
|
582
636
|
}
|
|
583
637
|
}
|
|
@@ -600,9 +654,9 @@ async function writeShardGroup(group, ctx) {
|
|
|
600
654
|
* nested per-group indexes, and never a zero-url row (that is a diagnostic
|
|
601
655
|
* for `files`, not something a crawler should be told to fetch).
|
|
602
656
|
*/
|
|
603
|
-
function buildSitemapIndexXml(files, baseUrl) {
|
|
657
|
+
function buildSitemapIndexXml(files, baseUrl, shardPathPrefix = "") {
|
|
604
658
|
const body = files.filter((file) => file.urls > 0).map((file) => {
|
|
605
|
-
return ` <sitemap>\n <loc>${escapeXml(joinOrigin(baseUrl, file.fileName))}</loc>\n </sitemap>`;
|
|
659
|
+
return ` <sitemap>\n <loc>${escapeXml(joinOrigin(baseUrl, `${shardPathPrefix}${file.fileName}`))}</loc>\n </sitemap>`;
|
|
606
660
|
}).join("\n");
|
|
607
661
|
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n" + (body.length > 0 ? `${body}\n` : "") + `</sitemapindex>\n`;
|
|
608
662
|
}
|
|
@@ -674,6 +728,7 @@ var SitemapIndex = class {
|
|
|
674
728
|
namedGroups = [];
|
|
675
729
|
/** Canonical (lowercased) keys already registered, so a case collision is caught at `addSource()`. */
|
|
676
730
|
registeredKeys = /* @__PURE__ */ new Set();
|
|
731
|
+
warnedImageRoutes = /* @__PURE__ */ new Set();
|
|
677
732
|
constructor(options) {
|
|
678
733
|
this.options = normalizeSitemapIndexOptions(options);
|
|
679
734
|
}
|
|
@@ -713,13 +768,16 @@ var SitemapIndex = class {
|
|
|
713
768
|
gzip: this.options.gzip,
|
|
714
769
|
maxUrlsPerFile: this.options.maxUrlsPerFile,
|
|
715
770
|
maxBytesPerFile: this.options.maxBytesPerFile,
|
|
716
|
-
defaults:
|
|
771
|
+
defaults: {
|
|
772
|
+
...this.options.defaults,
|
|
773
|
+
onImageLimitExceeded: (event) => this.reportImageLimit(event)
|
|
774
|
+
},
|
|
717
775
|
duplicates,
|
|
718
776
|
routes
|
|
719
777
|
});
|
|
720
778
|
shardFiles.push(...files);
|
|
721
779
|
}
|
|
722
|
-
const indexXml = buildSitemapIndexXml(shardFiles, this.options.baseUrl);
|
|
780
|
+
const indexXml = buildSitemapIndexXml(shardFiles, this.options.baseUrl, this.options.shardPathPrefix);
|
|
723
781
|
await (0, node_fs_promises.writeFile)((0, node_path.join)(tempDir, this.options.indexFileName), indexXml, "utf8");
|
|
724
782
|
return [...shardFiles.filter((file) => file.urls > 0).map((file) => file.fileName), this.options.indexFileName];
|
|
725
783
|
};
|
|
@@ -739,17 +797,143 @@ var SitemapIndex = class {
|
|
|
739
797
|
routes: routes.summary()
|
|
740
798
|
};
|
|
741
799
|
}
|
|
800
|
+
reportImageLimit(event) {
|
|
801
|
+
const key = event.route ?? "unattributed";
|
|
802
|
+
if (this.warnedImageRoutes.has(key)) return;
|
|
803
|
+
this.warnedImageRoutes.add(key);
|
|
804
|
+
if (this.options.defaults.onImageLimitExceeded) this.options.defaults.onImageLimitExceeded(event);
|
|
805
|
+
else console.warn(`[warlock:sitemap] ${key} exceeded the 1,000-image limit; dropped ${event.dropped} image(s).`);
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region ../sitemap/src/generation-manifest.ts
|
|
811
|
+
/** Storage-independent schema and key helpers for immutable sitemap generations. */
|
|
812
|
+
const GENERATION_MANIFEST_VERSION = 1;
|
|
813
|
+
const MANIFEST_FENCE_WIDTH = 16;
|
|
814
|
+
var InvalidSitemapGenerationManifestError = class extends Error {
|
|
815
|
+
constructor(message) {
|
|
816
|
+
super(`Invalid sitemap generation manifest: ${message}`);
|
|
817
|
+
this.name = "InvalidSitemapGenerationManifestError";
|
|
818
|
+
}
|
|
742
819
|
};
|
|
820
|
+
const SAFE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
821
|
+
const SAFE_FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;
|
|
822
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
823
|
+
const ISO_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
824
|
+
const MANIFEST_KEY = /^manifests\/(\d{16})-([A-Za-z0-9][A-Za-z0-9_-]{0,127})\.json$/;
|
|
825
|
+
function fail(message) {
|
|
826
|
+
throw new InvalidSitemapGenerationManifestError(message);
|
|
827
|
+
}
|
|
828
|
+
function object(value, label) {
|
|
829
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return fail(`${label} must be an object.`);
|
|
830
|
+
return value;
|
|
831
|
+
}
|
|
832
|
+
function nonNegativeInteger(value, label) {
|
|
833
|
+
if (!Number.isSafeInteger(value) || value < 0) return fail(`${label} must be a safe non-negative integer.`);
|
|
834
|
+
return value;
|
|
835
|
+
}
|
|
836
|
+
function generationIdentifier(value, label) {
|
|
837
|
+
if (typeof value !== "string" || !SAFE_IDENTIFIER.test(value)) return fail(`${label} must be a safe generation identifier.`);
|
|
838
|
+
return value;
|
|
839
|
+
}
|
|
840
|
+
function generatedAt(value) {
|
|
841
|
+
if (typeof value !== "string" || !ISO_DATE_TIME.test(value) || Number.isNaN(Date.parse(value))) return fail("generatedAt must be a valid ISO-8601 date-time.");
|
|
842
|
+
return value;
|
|
843
|
+
}
|
|
844
|
+
function artifactPath(value, generationId, label) {
|
|
845
|
+
if (typeof value !== "string") return fail(`${label} must be a string.`);
|
|
846
|
+
const prefix = `generations/${generationId}/`;
|
|
847
|
+
if (!value.startsWith(prefix)) return fail(`${label} must be contained in ${prefix}.`);
|
|
848
|
+
const fileName = value.slice(prefix.length);
|
|
849
|
+
if (!SAFE_FILE_NAME.test(fileName) || fileName.includes("/") || fileName === "." || fileName === "..") return fail(`${label} must name one safe, flat artifact file.`);
|
|
850
|
+
return value;
|
|
851
|
+
}
|
|
852
|
+
function manifestFile(value, generationId, index) {
|
|
853
|
+
const record = object(value, `files[${index}]`);
|
|
854
|
+
const path = artifactPath(record.path, generationId, `files[${index}].path`);
|
|
855
|
+
const bytes = nonNegativeInteger(record.bytes, `files[${index}].bytes`);
|
|
856
|
+
if (typeof record.sha256 !== "string" || !SHA256.test(record.sha256)) return fail(`files[${index}].sha256 must be a lowercase SHA-256 hex digest.`);
|
|
857
|
+
return {
|
|
858
|
+
path,
|
|
859
|
+
bytes,
|
|
860
|
+
sha256: record.sha256
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
/** Parse untrusted JSON into the v1 manifest contract. It does not touch storage. */
|
|
864
|
+
function parseSitemapGenerationManifest(value) {
|
|
865
|
+
const record = object(value, "manifest");
|
|
866
|
+
if (record.version !== 1) return fail(`version must be ${1}.`);
|
|
867
|
+
const fence = nonNegativeInteger(record.fence, "fence");
|
|
868
|
+
const generationId = generationIdentifier(record.generationId, "generationId");
|
|
869
|
+
const coversRev = nonNegativeInteger(record.coversRev, "coversRev");
|
|
870
|
+
if (record.kind !== "single" && record.kind !== "index") return fail("kind must be \"single\" or \"index\".");
|
|
871
|
+
if (!Array.isArray(record.files) || record.files.length === 0) return fail("files must be a non-empty array.");
|
|
872
|
+
const files = record.files.map((file, index) => manifestFile(file, generationId, index));
|
|
873
|
+
const paths = /* @__PURE__ */ new Set();
|
|
874
|
+
const fileNames = /* @__PURE__ */ new Set();
|
|
875
|
+
for (const file of files) {
|
|
876
|
+
const fileName = file.path.slice(`generations/${generationId}/`.length);
|
|
877
|
+
if (paths.has(file.path)) return fail(`files contains duplicate path ${JSON.stringify(file.path)}.`);
|
|
878
|
+
if (fileNames.has(fileName)) return fail(`files contains duplicate public file name ${JSON.stringify(fileName)}.`);
|
|
879
|
+
paths.add(file.path);
|
|
880
|
+
fileNames.add(fileName);
|
|
881
|
+
}
|
|
882
|
+
const mainFile = artifactPath(record.mainFile, generationId, "mainFile");
|
|
883
|
+
if (!paths.has(mainFile)) return fail("mainFile must match one files[].path.");
|
|
884
|
+
return {
|
|
885
|
+
version: 1,
|
|
886
|
+
fence,
|
|
887
|
+
generationId,
|
|
888
|
+
coversRev,
|
|
889
|
+
kind: record.kind,
|
|
890
|
+
mainFile,
|
|
891
|
+
files,
|
|
892
|
+
entries: nonNegativeInteger(record.entries, "entries"),
|
|
893
|
+
generatedAt: generatedAt(record.generatedAt)
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
/** Build the immutable v1 manifest object key for a generation fence. */
|
|
897
|
+
function sitemapManifestKey(fence, generationId) {
|
|
898
|
+
const safeFence = nonNegativeInteger(fence, "fence");
|
|
899
|
+
if (safeFence >= 10 ** 16) return fail(`fence must fit within ${16} decimal digits.`);
|
|
900
|
+
return `manifests/${String(safeFence).padStart(16, "0")}-${generationIdentifier(generationId, "generationId")}.json`;
|
|
901
|
+
}
|
|
902
|
+
/** Parse only the shape of a manifest object key; artifact validation belongs to the storage adapter. */
|
|
903
|
+
function parseSitemapManifestKey(key) {
|
|
904
|
+
if (typeof key !== "string") return void 0;
|
|
905
|
+
const match = MANIFEST_KEY.exec(key);
|
|
906
|
+
if (!match) return void 0;
|
|
907
|
+
const fence = Number(match[1]);
|
|
908
|
+
const generationId = match[2];
|
|
909
|
+
if (!Number.isSafeInteger(fence) || generationId === void 0) return void 0;
|
|
910
|
+
return {
|
|
911
|
+
key,
|
|
912
|
+
fence,
|
|
913
|
+
generationId
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
/** Numeric-fence-descending candidates; callers validate storage contents before adoption. */
|
|
917
|
+
function sortSitemapManifestCandidates(keys) {
|
|
918
|
+
return keys.map(parseSitemapManifestKey).filter((candidate) => candidate !== void 0).sort((left, right) => right.fence - left.fence || right.generationId.localeCompare(left.generationId));
|
|
919
|
+
}
|
|
743
920
|
|
|
744
921
|
//#endregion
|
|
745
922
|
exports.DuplicateSourceKeyError = DuplicateSourceKeyError;
|
|
923
|
+
exports.GENERATION_MANIFEST_VERSION = GENERATION_MANIFEST_VERSION;
|
|
746
924
|
exports.InvalidBaseUrlError = InvalidBaseUrlError;
|
|
747
925
|
exports.InvalidSitemapEntryError = InvalidSitemapEntryError;
|
|
926
|
+
exports.InvalidSitemapGenerationManifestError = InvalidSitemapGenerationManifestError;
|
|
927
|
+
exports.MANIFEST_FENCE_WIDTH = MANIFEST_FENCE_WIDTH;
|
|
748
928
|
exports.Sitemap = Sitemap;
|
|
749
929
|
exports.SitemapIndex = SitemapIndex;
|
|
750
930
|
exports.UnownedOutputDirectoryError = UnownedOutputDirectoryError;
|
|
751
931
|
exports.buildSitemapXml = buildSitemapXml;
|
|
752
932
|
exports.escapeXml = escapeXml;
|
|
753
933
|
exports.joinOrigin = joinOrigin;
|
|
934
|
+
exports.parseSitemapGenerationManifest = parseSitemapGenerationManifest;
|
|
935
|
+
exports.parseSitemapManifestKey = parseSitemapManifestKey;
|
|
754
936
|
exports.renderUrlBlock = renderUrlBlock;
|
|
937
|
+
exports.sitemapManifestKey = sitemapManifestKey;
|
|
938
|
+
exports.sortSitemapManifestCandidates = sortSitemapManifestCandidates;
|
|
755
939
|
//# sourceMappingURL=index.cjs.map
|