@warlock.js/sitemap 5.24.0 → 5.26.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 CHANGED
@@ -4,6 +4,18 @@ 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.26.0 - 2026-09-30
8
+
9
+ ### Changed
10
+
11
+ - Lockstep release; package APIs are unchanged.
12
+
13
+ ## 5.25.0 - 2026-09-28
14
+
15
+ ### Changed
16
+
17
+ - Lockstep release; package APIs are unchanged.
18
+
7
19
  ## 5.24.0 - 2026-09-27
8
20
 
9
21
  ### Changed
package/cjs/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../../../../../sitemap/src/errors.ts","../../../../../../sitemap/src/atomic-publish.ts","../../../../../../sitemap/src/atomic-write-file.ts","../../../../../../sitemap/src/lastmod.ts","../../../../../../sitemap/src/normalize-entry.ts","../../../../../../sitemap/src/url.ts","../../../../../../sitemap/src/xml.ts","../../../../../../sitemap/src/sitemap.ts","../../../../../../sitemap/src/sitemap-index-options.ts","../../../../../../sitemap/src/shard-name.ts","../../../../../../sitemap/src/sitemap-shard-writer.ts","../../../../../../sitemap/src/sitemap-index-xml.ts","../../../../../../sitemap/src/duplicate-path-tracker.ts","../../../../../../sitemap/src/route-counter.ts","../../../../../../sitemap/src/sitemap-index.ts","../../../../../../sitemap/src/generation-manifest.ts"],"sourcesContent":["/**\n * The `baseUrl` given to a `Sitemap` is not an absolute http(s) URL.\n *\n * Thrown from the CONSTRUCTOR: a sitemap that cannot produce a valid URL\n * should not exist, and the mistake belongs at the line that wrote the value\n * rather than at the first request that reads it.\n */\nexport class InvalidBaseUrlError extends Error {\n public constructor(value: unknown, reason: string) {\n super(`Invalid sitemap baseUrl ${JSON.stringify(value)}: ${reason}.`);\n this.name = \"InvalidBaseUrlError\";\n }\n}\n\n/** An entry the sitemap protocol cannot represent. Thrown from `add()`. */\nexport class InvalidSitemapEntryError extends Error {\n public constructor(reason: string) {\n super(`Invalid sitemap entry: ${reason}.`);\n this.name = \"InvalidSitemapEntryError\";\n }\n}\n\n/**\n * Two `SitemapIndex` source keys collide once canonicalised (`en-US` and\n * `en-us` would produce the same filename on a case-insensitive filesystem\n * and silently overwrite one another). Thrown from `addSource()`, not at\n * `saveTo()`, so the mistake is caught at the line that registered it.\n */\nexport class DuplicateSourceKeyError extends Error {\n public constructor(key: string) {\n super(\n `Duplicate sitemap source key ${JSON.stringify(key)}: keys collide case-insensitively ` +\n `and would overwrite one another's shard files.`,\n );\n this.name = \"DuplicateSourceKeyError\";\n }\n}\n\n/**\n * `SitemapIndex.saveTo(outDir)` swaps the ENTIRE `outDir` for a freshly\n * written set (`atomic-publish.ts`). That is safe only when `outDir` is a\n * directory this package already owns — marked by its own\n * `.sitemap-set.json` from a prior publish. A non-empty directory with no\n * marker is presumed to belong to someone else (a caller's `public/`, most\n * dangerously) and is never swapped or deleted; this is thrown instead, from\n * `saveTo()` before anything is written.\n */\nexport class UnownedOutputDirectoryError extends Error {\n public constructor(outDir: string) {\n super(\n `Refusing to publish a sitemap set to ${JSON.stringify(outDir)}: this directory already ` +\n `has content but no \".sitemap-set.json\" marker from a previous @warlock.js/sitemap ` +\n `publish, so it is not safe to swap or delete. Point saveTo() at a dedicated, ` +\n `sitemap-only directory instead.`,\n );\n this.name = \"UnownedOutputDirectoryError\";\n }\n}\n","import { mkdir, readdir, rename, rm, stat, writeFile } from \"node:fs/promises\";\nimport { basename, dirname, join } from \"node:path\";\nimport { UnownedOutputDirectoryError } from \"./errors\";\n\n/**\n * Marks a directory as one `publishAtomically` swapped into place. Its\n * presence is the ONLY thing that lets a later publish treat the directory\n * as safe to swap out from under itself — see {@link assertOutDirIsOwned}.\n */\nconst OWNERSHIP_MARKER_FILE = \".sitemap-set.json\";\nconst OWNERSHIP_MARKER_VERSION = 1;\n\nasync function validateShards(tempDir: string, fileNames: readonly string[]): Promise<void> {\n for (const fileName of fileNames) {\n const info = await stat(join(tempDir, fileName)).catch(() => undefined);\n\n if (!info || !info.isFile() || info.size === 0) {\n throw new Error(`sitemap publish aborted: shard \"${fileName}\" is missing or empty.`);\n }\n }\n}\n\n/**\n * `outDir` is safe to take over when it does not exist yet, is empty, or\n * already carries {@link OWNERSHIP_MARKER_FILE} from a previous publish.\n * Anything else — a non-empty directory this package never wrote — is\n * refused rather than swapped or deleted (`de97020e`).\n */\nasync function assertOutDirIsOwned(outDir: string): Promise<void> {\n const entries = await readdir(outDir).catch((error) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return undefined;\n throw error;\n });\n\n if (entries === undefined || entries.length === 0) return;\n\n if (!entries.includes(OWNERSHIP_MARKER_FILE)) {\n throw new UnownedOutputDirectoryError(outDir);\n }\n}\n\n/**\n * Writes a complete set into a sibling temp directory, validates that every\n * file it named actually exists and is non-empty, then swaps it into\n * `outDir`. A crawler arriving mid-write sees the previous complete set or\n * the new one, never a partial one, and a failed run leaves the previous set\n * untouched and rejects.\n *\n * `write` performs the writes and returns the file names (relative to the\n * temp dir) that must be present for the set to be considered valid — it is\n * only known after writing, since shard count depends on what was walked.\n */\nexport async function publishAtomically(\n outDir: string,\n write: (tempDir: string) => Promise<readonly string[]>,\n): Promise<void> {\n await assertOutDirIsOwned(outDir);\n\n const parent = dirname(outDir);\n\n await mkdir(parent, { recursive: true });\n\n const tempDir = join(parent, `.${basename(outDir)}.tmp-${process.pid}-${Date.now()}`);\n\n await mkdir(tempDir, { recursive: true });\n\n try {\n const fileNames = await write(tempDir);\n\n await validateShards(tempDir, fileNames);\n\n await writeFile(\n join(tempDir, OWNERSHIP_MARKER_FILE),\n JSON.stringify({ package: \"@warlock.js/sitemap\", version: OWNERSHIP_MARKER_VERSION }),\n \"utf8\",\n );\n\n const displacedDir = join(parent, `.${basename(outDir)}.previous-${Date.now()}`);\n let displacedPrevious = false;\n\n try {\n await rename(outDir, displacedDir);\n displacedPrevious = true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n }\n\n try {\n await rename(tempDir, outDir);\n } catch (error) {\n if (displacedPrevious) await rename(displacedDir, outDir);\n throw error;\n }\n\n if (displacedPrevious) await rm(displacedDir, { recursive: true, force: true });\n } catch (error) {\n await rm(tempDir, { recursive: true, force: true });\n throw error;\n }\n}\n","import { rename, rm, writeFile } from \"node:fs/promises\";\nimport { basename, dirname, join } from \"node:path\";\n\n/** The subset of `node:fs/promises` this helper needs — swappable so a test can inject a failing write or rename without mocking the global module. */\nexport type AtomicWriteFileDeps = {\n writeFile: typeof writeFile;\n rename: typeof rename;\n rm: typeof rm;\n};\n\nconst defaultDeps: AtomicWriteFileDeps = { writeFile, rename, rm };\n\nconst RENAME_RETRY_ATTEMPTS = 5;\nconst RENAME_RETRY_DELAY_MS = 20;\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Windows can report a rename over an existing file as EPERM/EBUSY while something briefly holds the target (an AV scan, a reader) — retrying is correct there, not a masked bug. */\nfunction isTransientRenameError(error: unknown): boolean {\n const code = (error as NodeJS.ErrnoException)?.code;\n\n return code === \"EPERM\" || code === \"EBUSY\";\n}\n\nasync function renameWithRetry(from: string, to: string, deps: AtomicWriteFileDeps): Promise<void> {\n for (let attempt = 1; attempt <= RENAME_RETRY_ATTEMPTS; attempt++) {\n try {\n await deps.rename(from, to);\n\n return;\n } catch (error) {\n if (attempt === RENAME_RETRY_ATTEMPTS || !isTransientRenameError(error)) throw error;\n\n await delay(RENAME_RETRY_DELAY_MS * attempt);\n }\n }\n}\n\n/**\n * Writes `content` to `filePath` atomically: the content lands in a unique\n * sibling temp file first — same directory, so same filesystem, so the\n * rename that follows is atomic — and only then is renamed over the target.\n * An interrupted or failing write never truncates or otherwise touches the\n * existing target; on any failure the temp file is removed and the error is\n * rethrown.\n */\nexport async function atomicWriteFile(\n filePath: string,\n content: string,\n deps: AtomicWriteFileDeps = defaultDeps,\n): Promise<void> {\n const tempPath = join(\n dirname(filePath),\n `.${basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n );\n\n try {\n await deps.writeFile(tempPath, content, \"utf8\");\n await renameWithRetry(tempPath, filePath, deps);\n } catch (error) {\n await deps.rm(tempPath, { force: true }).catch(() => undefined);\n\n throw error;\n }\n}\n","import { InvalidSitemapEntryError } from \"./errors\";\n\n/**\n * Serialises a `lastmod` value.\n *\n * A `Date` becomes W3C datetime, which is what the schema wants and what\n * `toISOString()` already produces. A string is passed through UNTOUCHED: a\n * caller who already holds an ISO string gets it back verbatim rather than\n * having us re-parse it and risk shifting it across a timezone.\n */\nexport function formatLastmod(value: string | Date): string {\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) {\n throw new InvalidSitemapEntryError(\"lastmod is an invalid Date\");\n }\n\n return value.toISOString();\n }\n\n if (typeof value !== \"string\" || value.trim() === \"\") {\n throw new InvalidSitemapEntryError(\"lastmod must be a non-empty string or a Date\");\n }\n\n return value;\n}\n","import { InvalidSitemapEntryError } from \"./errors\";\nimport { formatLastmod } from \"./lastmod\";\nimport type {\n ChangeFreq,\n ResolvedSitemapEntry,\n SitemapEntry,\n SitemapImage,\n SitemapOptions,\n} from \"./types\";\n\nconst CHANGE_FREQS: readonly ChangeFreq[] = [\n \"always\",\n \"hourly\",\n \"daily\",\n \"weekly\",\n \"monthly\",\n \"yearly\",\n \"never\",\n];\nconst MAX_IMAGES_PER_URL = 1_000;\n\nfunction assertChangeFreq(value: unknown): asserts value is ChangeFreq {\n if (!CHANGE_FREQS.includes(value as ChangeFreq)) {\n throw new InvalidSitemapEntryError(\n `changefreq ${JSON.stringify(value)} is not one of ${CHANGE_FREQS.join(\", \")}`,\n );\n }\n}\n\nfunction assertPriority(value: unknown): asserts value is number {\n if (typeof value !== \"number\" || Number.isNaN(value) || value < 0 || value > 1) {\n throw new InvalidSitemapEntryError(\n `priority ${JSON.stringify(value)} is outside the protocol range 0.0–1.0`,\n );\n }\n}\n\n/** Every stored path carries its leading slash, so `/a` and `a` are one entry, not two. */\nexport function normalizePath(path: unknown): string {\n if (typeof path !== \"string\" || path.trim() === \"\") {\n throw new InvalidSitemapEntryError(\"path is required and must be a non-empty string\");\n }\n\n // An absolute URL is left alone: the caller is overriding the base origin\n // deliberately, which a cross-origin alternate legitimately needs.\n if (/^https?:\\/\\//i.test(path)) return path;\n\n return path.startsWith(\"/\") ? path : `/${path}`;\n}\n\n/**\n * Validates one entry and folds the builder's defaults into it. Defaults are\n * resolved HERE rather than at serialisation time so that `entries()` shows\n * what will actually be emitted — a diagnostic that reports something other\n * than the output is worse than none.\n */\nexport function normalizeEntry(\n entry: SitemapEntry,\n defaults: Pick<SitemapOptions, \"changefreq\" | \"priority\" | \"lastmod\" | \"onImageLimitExceeded\">,\n): ResolvedSitemapEntry {\n const path = normalizePath(entry.path);\n const changefreq = entry.changefreq ?? defaults.changefreq;\n const priority = entry.priority ?? defaults.priority;\n const lastmod = entry.lastmod ?? defaults.lastmod;\n\n if (changefreq !== undefined) assertChangeFreq(changefreq);\n if (priority !== undefined) assertPriority(priority);\n\n const alternates = entry.alternates?.map((alternate) => {\n if (typeof alternate?.hreflang !== \"string\" || alternate.hreflang.trim() === \"\") {\n throw new InvalidSitemapEntryError(\"alternate hreflang is required\");\n }\n\n return { hreflang: alternate.hreflang, path: normalizePath(alternate.path) };\n });\n if (entry.images !== undefined && !Array.isArray(entry.images)) {\n throw new InvalidSitemapEntryError(\"images must be an array\");\n }\n const images = entry.images?.slice(0, MAX_IMAGES_PER_URL).map((image): SitemapImage => {\n if (typeof image?.loc !== \"string\" || image.loc.trim() === \"\") {\n throw new InvalidSitemapEntryError(\"image loc is required\");\n }\n\n let url: URL;\n try {\n url = new URL(image.loc);\n } catch {\n throw new InvalidSitemapEntryError(\"image loc must be an absolute HTTP(S) URL\");\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new InvalidSitemapEntryError(\"image loc must be an absolute HTTP(S) URL\");\n }\n\n return { loc: image.loc };\n });\n if ((entry.images?.length ?? 0) > MAX_IMAGES_PER_URL) {\n defaults.onImageLimitExceeded?.({\n ...(entry.route === undefined ? {} : { route: entry.route }),\n dropped: entry.images!.length - MAX_IMAGES_PER_URL,\n });\n }\n\n return {\n path,\n ...(entry.name !== undefined ? { name: entry.name } : {}),\n ...(entry.route !== undefined ? { route: entry.route } : {}),\n ...(lastmod !== undefined ? { lastmod: formatLastmod(lastmod) } : {}),\n ...(changefreq !== undefined ? { changefreq } : {}),\n ...(priority !== undefined ? { priority } : {}),\n ...(alternates !== undefined ? { alternates } : {}),\n ...(images !== undefined ? { images } : {}),\n };\n}\n","import { InvalidBaseUrlError } from \"./errors\";\n\n/**\n * Joins a configured origin and an app-relative route path into one absolute\n * URL, with exactly one slash at the seam regardless of whether either side\n * already carries one.\n */\nexport function joinOrigin(origin: string, routePath: string): string {\n const trimmedOrigin = origin.endsWith(\"/\") ? origin.slice(0, -1) : origin;\n const normalizedPath = routePath.startsWith(\"/\") ? routePath : `/${routePath}`;\n\n return `${trimmedOrigin}${normalizedPath}`;\n}\n\n/**\n * Validates a `baseUrl` and returns it without its trailing slash.\n *\n * `new URL()` accepts `mailto:` and `file:` happily, so the protocol is\n * checked explicitly — a sitemap `<loc>` that is not http(s) is not a document\n * any crawler will fetch.\n */\nexport function normalizeBaseUrl(value: string): string {\n if (typeof value !== \"string\" || value.trim() === \"\") {\n throw new InvalidBaseUrlError(value, \"expected a non-empty string\");\n }\n\n let parsed: URL;\n\n try {\n parsed = new URL(value);\n } catch {\n throw new InvalidBaseUrlError(value, \"not an absolute URL\");\n }\n\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new InvalidBaseUrlError(value, `unsupported protocol \"${parsed.protocol}\"`);\n }\n\n const href = parsed.href;\n\n return href.endsWith(\"/\") ? href.slice(0, -1) : href;\n}\n\n/** True for a value already usable as a `<loc>` without joining an origin. */\nexport function isAbsoluteUrl(value: string): boolean {\n return /^https?:\\/\\//i.test(value);\n}\n\n/** Resolves an entry or alternate path against the base URL, unless it is already absolute. */\nexport function resolveAgainstBase(baseUrl: string, path: string): string {\n return isAbsoluteUrl(path) ? path : joinOrigin(baseUrl, path);\n}\n","import type { ResolvedSitemapEntry } from \"./types\";\nimport { resolveAgainstBase } from \"./url\";\n\nconst XML_ESCAPES: Record<string, string> = {\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&apos;\",\n};\n\n/** Escapes the five XML-significant characters. A URL's query string routinely contains `&`. */\nexport function escapeXml(value: string): string {\n // The character class and the table are written together, so the lookup can\n // only miss if one is edited without the other; falling back to the original\n // character keeps that editing mistake from silently emitting `undefined`\n // into a URL.\n return value.replace(/[&<>\"']/g, (char) => XML_ESCAPES[char] ?? char);\n}\n\nconst XHTML_NAMESPACE = \"http://www.w3.org/1999/xhtml\";\nconst IMAGE_NAMESPACE = \"http://www.google.com/schemas/sitemap-image/1.1\";\n\n/**\n * The `xmlns:xhtml` attribute exactly as it appears on `<urlset>`, including\n * its leading space. Exported so the shard writer can charge its byte length\n * against the ceiling without duplicating the string it measures.\n */\nexport const XHTML_NAMESPACE_ATTR = ` xmlns:xhtml=\"${XHTML_NAMESPACE}\"`;\n/** See {@link XHTML_NAMESPACE_ATTR}; measured by the shard writer before roll-over. */\nexport const IMAGE_NAMESPACE_ATTR = ` xmlns:image=\"${IMAGE_NAMESPACE}\"`;\n\n/**\n * Renders one `<url>` block. Exported so the streaming writer can measure the\n * exact bytes it is about to append before deciding whether the byte ceiling\n * forces a new shard — a separate approximation could disagree with what is\n * actually written.\n */\nexport function renderUrlBlock(entry: ResolvedSitemapEntry, baseUrl: string): string {\n const lines = [` <url>`, ` <loc>${escapeXml(resolveAgainstBase(baseUrl, entry.path))}</loc>`];\n\n if (entry.lastmod !== undefined) {\n lines.push(` <lastmod>${escapeXml(entry.lastmod)}</lastmod>`);\n }\n\n if (entry.changefreq !== undefined) {\n lines.push(` <changefreq>${entry.changefreq}</changefreq>`);\n }\n\n if (entry.priority !== undefined) {\n lines.push(` <priority>${entry.priority}</priority>`);\n }\n\n for (const alternate of entry.alternates ?? []) {\n const href = escapeXml(resolveAgainstBase(baseUrl, alternate.path));\n\n lines.push(\n ` <xhtml:link rel=\"alternate\" hreflang=\"${escapeXml(alternate.hreflang)}\" href=\"${href}\"/>`,\n );\n }\n\n for (const image of entry.images ?? []) {\n lines.push(` <image:image>`);\n lines.push(` <image:loc>${escapeXml(resolveAgainstBase(baseUrl, image.loc))}</image:loc>`);\n lines.push(` </image:image>`);\n }\n\n lines.push(` </url>`);\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Serialises entries into a `urlset` sitemap document — the sitemaps.org\n * namespace, `<url>` per entry, element order `loc` / `lastmod` / `changefreq`\n * / `priority` (schema order; a validator that checks order rejects any other),\n * then any `xhtml:link` alternates.\n *\n * The xhtml namespace is declared only when some entry actually carries an\n * alternate — an unused namespace on every single-language sitemap is noise.\n */\nexport function buildSitemapXml(entries: readonly ResolvedSitemapEntry[], baseUrl: string): string {\n const hasAlternates = entries.some((entry) => (entry.alternates?.length ?? 0) > 0);\n const hasImages = entries.some((entry) => (entry.images?.length ?? 0) > 0);\n const namespaces = `${hasAlternates ? XHTML_NAMESPACE_ATTR : \"\"}${hasImages ? IMAGE_NAMESPACE_ATTR : \"\"}`;\n const body = entries.map((entry) => renderUrlBlock(entry, baseUrl)).join(\"\\n\");\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"${namespaces}>\\n` +\n (body.length > 0 ? `${body}\\n` : \"\") +\n `</urlset>\\n`\n );\n}\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { publishAtomically } from \"./atomic-publish\";\nimport { atomicWriteFile } from \"./atomic-write-file\";\nimport { normalizeEntry } from \"./normalize-entry\";\nimport type {\n DuplicateReport,\n ResolvedSitemapEntry,\n RouteSummary,\n SitemapEntry,\n SitemapOptions,\n} from \"./types\";\nimport { normalizeBaseUrl } from \"./url\";\nimport { buildSitemapXml } from \"./xml\";\n\n/**\n * A bounded sitemap builder: it RETAINS every entry, which is what makes\n * `entries()` and a repeatable `toXML()` possible, and is exactly right up to\n * the sitemaps.org ceiling of 50,000 URLs / 50MB.\n *\n * Above that ceiling this is the wrong tool — the streaming writer retains\n * nothing and emits shards plus an index instead. The two modes are separate\n * on purpose: a class that promised not to retain entries and still offered\n * `entries()` would be lying about one of them.\n */\nexport class Sitemap {\n private readonly baseUrl: string;\n\n private readonly defaults: Pick<\n SitemapOptions,\n \"changefreq\" | \"priority\" | \"lastmod\" | \"onImageLimitExceeded\"\n >;\n\n private readonly warnedImageRoutes = new Set<string>();\n\n private readonly optionsImageReporter: SitemapOptions[\"onImageLimitExceeded\"];\n\n /** Keyed by path: a duplicate `<loc>` makes the document invalid, so the later add wins. */\n private readonly entriesByPath = new Map<string, ResolvedSitemapEntry>();\n\n /** Insertion order per path, so `duplicates()` can name every colliding source. */\n private readonly routesByPath = new Map<string, (string | undefined)[]>();\n\n /** Declared patterns, including ones that never contributed a URL. */\n private readonly declaredRoutes = new Set<string>();\n\n public constructor(options: SitemapOptions) {\n this.baseUrl = normalizeBaseUrl(options?.baseUrl);\n this.optionsImageReporter = options.onImageLimitExceeded;\n this.defaults = {\n changefreq: options.changefreq,\n priority: options.priority,\n lastmod: options.lastmod,\n onImageLimitExceeded: (event) => this.reportImageLimit(event),\n };\n }\n\n private reportImageLimit(event: { readonly route?: string; readonly dropped: number }): void {\n const key = event.route ?? \"unattributed\";\n if (this.warnedImageRoutes.has(key)) return;\n\n this.warnedImageRoutes.add(key);\n if (this.optionsImageReporter) this.optionsImageReporter(event);\n else\n console.warn(\n `[warlock:sitemap] ${key} exceeded the 1,000-image limit; dropped ${event.dropped} image(s).`,\n );\n }\n\n public add(entry: SitemapEntry): this {\n const resolved = normalizeEntry(entry, this.defaults);\n\n this.entriesByPath.set(resolved.path, resolved);\n\n const seen = this.routesByPath.get(resolved.path);\n\n if (seen) {\n seen.push(resolved.route);\n } else {\n this.routesByPath.set(resolved.path, [resolved.route]);\n }\n\n if (resolved.route !== undefined) this.declaredRoutes.add(resolved.route);\n\n return this;\n }\n\n public addMany(entries: Iterable<SitemapEntry>): this {\n for (const entry of entries) this.add(entry);\n\n return this;\n }\n\n /**\n * Names a pattern the caller EXPECTS to contribute URLs, so that one which\n * contributes none shows up in `routes()` as a `count: 0` row instead of as\n * silence. A dynamic route whose supplier returned nothing is the failure\n * this package exists to make visible.\n */\n public declareRoute(route: string): this {\n this.declaredRoutes.add(route);\n\n return this;\n }\n\n public get size(): number {\n return this.entriesByPath.size;\n }\n\n public entries(): readonly ResolvedSitemapEntry[] {\n return [...this.entriesByPath.values()];\n }\n\n public routes(): readonly RouteSummary[] {\n const counts = new Map<string, number>();\n\n for (const route of this.declaredRoutes) counts.set(route, 0);\n\n for (const entry of this.entriesByPath.values()) {\n if (entry.route === undefined) continue;\n\n counts.set(entry.route, (counts.get(entry.route) ?? 0) + 1);\n }\n\n return [...counts].map(([route, count]) => ({ route, count }));\n }\n\n /**\n * Every path added more than once. The override is silent — it is not\n * hidden: a collision between static discovery and a dynamic supplier is a\n * real defect, and the CALLER decides whether it fails their build. This\n * package reports; it never prints and never throws over a duplicate.\n */\n public duplicates(): readonly DuplicateReport[] {\n const reports: DuplicateReport[] = [];\n\n for (const [path, routes] of this.routesByPath) {\n if (routes.length < 2) continue;\n\n reports.push({ path, count: routes.length, routes: [...routes] });\n }\n\n return reports;\n }\n\n /** Pure and repeatable: calling it twice returns the same string and mutates nothing. */\n public toXML(): string {\n return buildSitemapXml(this.entries(), this.baseUrl);\n }\n\n /**\n * Writes the document, creating parent directories so a clean checkout\n * works. The write is atomic: a failed or interrupted publish leaves\n * whatever was already at `filePath` untouched instead of truncating it.\n */\n public async saveTo(filePath: string): Promise<void> {\n await mkdir(dirname(filePath), { recursive: true });\n await atomicWriteFile(filePath, this.toXML());\n }\n\n /**\n * Publishes the document as the whole content of `outDir`, exactly the way\n * `SitemapIndex.saveTo` publishes a set: swapped in atomically, and marked\n * as owned. So a site that later outgrows one file can publish an index\n * into the same directory, and a later single file removes stale shards.\n * Refuses a non-empty directory this package did not write\n * (`UnownedOutputDirectoryError`). Returns the published file's path.\n */\n public async publishTo(outDir: string, fileName = \"sitemap.xml\"): Promise<string> {\n const xml = this.toXML();\n\n await publishAtomically(outDir, async (tempDir) => {\n await writeFile(join(tempDir, fileName), xml, \"utf8\");\n\n return [fileName];\n });\n\n return join(outDir, fileName);\n }\n}\n","import type { SitemapOptions } from \"./types\";\nimport { normalizeBaseUrl } from \"./url\";\nimport type { SitemapIndexOptions } from \"./sitemap-index-types\";\n\n/** The sitemaps.org limits. Never clamped to — a silently clamped option is a lie about what was written. */\nexport const PROTOCOL_MAX_URLS_PER_FILE = 50_000;\nexport const PROTOCOL_MAX_BYTES_PER_FILE = 50 * 1024 * 1024;\n\nexport type ResolvedSitemapIndexOptions = {\n readonly baseUrl: string;\n readonly filePrefix: string;\n readonly indexFileName: string;\n readonly shardPathPrefix: string;\n readonly gzip: boolean;\n readonly maxUrlsPerFile: number;\n readonly maxBytesPerFile: number;\n readonly defaults: Pick<\n SitemapOptions,\n \"changefreq\" | \"priority\" | \"lastmod\" | \"onImageLimitExceeded\"\n >;\n};\n\n/** Validates and folds in defaults, once, at the constructor — the same discipline as `Sitemap`. */\nexport function normalizeSitemapIndexOptions(\n options: SitemapIndexOptions,\n): ResolvedSitemapIndexOptions {\n const baseUrl = normalizeBaseUrl(options?.baseUrl);\n const shardPathPrefix = normalizeShardPathPrefix(options.shardPathPrefix);\n\n const maxUrlsPerFile = options.maxUrlsPerFile ?? PROTOCOL_MAX_URLS_PER_FILE;\n const maxBytesPerFile = options.maxBytesPerFile ?? PROTOCOL_MAX_BYTES_PER_FILE;\n\n if (\n !Number.isInteger(maxUrlsPerFile) ||\n maxUrlsPerFile < 1 ||\n maxUrlsPerFile > PROTOCOL_MAX_URLS_PER_FILE\n ) {\n throw new RangeError(\n `maxUrlsPerFile must be an integer between 1 and the sitemaps.org ceiling of ` +\n `${PROTOCOL_MAX_URLS_PER_FILE}, got ${JSON.stringify(maxUrlsPerFile)}.`,\n );\n }\n\n if (\n !Number.isFinite(maxBytesPerFile) ||\n maxBytesPerFile < 1 ||\n maxBytesPerFile > PROTOCOL_MAX_BYTES_PER_FILE\n ) {\n throw new RangeError(\n `maxBytesPerFile must be between 1 and the sitemaps.org ceiling of ` +\n `${PROTOCOL_MAX_BYTES_PER_FILE} bytes, got ${JSON.stringify(maxBytesPerFile)}.`,\n );\n }\n\n return {\n baseUrl,\n filePrefix: options.filePrefix ?? \"sitemap\",\n indexFileName: options.indexFileName ?? \"sitemap_index.xml\",\n shardPathPrefix,\n gzip: options.gzip ?? false,\n maxUrlsPerFile,\n maxBytesPerFile,\n defaults: {\n changefreq: options.changefreq,\n priority: options.priority,\n lastmod: options.lastmod,\n onImageLimitExceeded: options.onImageLimitExceeded,\n },\n };\n}\n\nfunction normalizeShardPathPrefix(value: string | undefined): string {\n if (value === undefined || value === \"\" || value === \"/\") return \"\";\n\n const prefix = value.replace(/^\\//, \"\").replace(/\\/$/, \"\");\n if (\n prefix.length === 0 ||\n prefix.split(\"/\").some((segment) => !/^[A-Za-z0-9_-]+$/.test(segment))\n ) {\n throw new TypeError(\"shardPathPrefix must contain only safe URL path segments.\");\n }\n\n return `${prefix}/`;\n}\n","const SAFE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;\n\n/**\n * Canonical form used to detect a case collision before it reaches the\n * filesystem: `en-US` and `en-us` would produce the same file on a\n * case-insensitive filesystem and silently overwrite one another.\n *\n * The key reaches the filename and nothing else — it is validated as a\n * filename fragment, not interpreted as a locale or anything else.\n */\nexport function canonicalizeSourceKey(key: string): string {\n if (typeof key !== \"string\" || !SAFE_KEY_PATTERN.test(key)) {\n throw new RangeError(\n `sitemap source key ${JSON.stringify(key)} must be a non-empty filename fragment ` +\n `(letters, digits, \"-\", \"_\").`,\n );\n }\n\n return key.toLowerCase();\n}\n\n/**\n * Stable, zero-padded, ordinal from shard one. A group that later crosses a\n * ceiling GAINS a file; it never renames the first one, so a crawler that has\n * already indexed `sitemap-en-0001.xml` never loses it because the site grew.\n */\nexport function shardFileName(\n filePrefix: string,\n key: string | undefined,\n ordinal: number,\n gzip: boolean,\n): string {\n const paddedOrdinal = String(ordinal).padStart(4, \"0\");\n const base =\n key !== undefined ? `${filePrefix}-${key}-${paddedOrdinal}` : `${filePrefix}-${paddedOrdinal}`;\n\n return gzip ? `${base}.xml.gz` : `${base}.xml`;\n}\n","import { writeFile } from \"node:fs/promises\";\nimport { gzipSync } from \"node:zlib\";\nimport { join } from \"node:path\";\nimport { normalizeEntry } from \"./normalize-entry\";\nimport { shardFileName } from \"./shard-name\";\nimport type { ResolvedSitemapEntry, SitemapOptions } from \"./types\";\nimport type { DuplicatePathTracker } from \"./duplicate-path-tracker\";\nimport type { RouteCounter } from \"./route-counter\";\nimport type { SitemapSourceFactory } from \"./sitemap-index-types\";\nimport { buildSitemapXml, IMAGE_NAMESPACE_ATTR, renderUrlBlock, XHTML_NAMESPACE_ATTR } from \"./xml\";\n\n/** One `addSource` group: the unnamed group merges every unkeyed call; a keyed group holds exactly one factory. */\nexport type ShardGroup = {\n readonly key?: string;\n readonly factories: readonly SitemapSourceFactory[];\n};\n\nexport type ShardWriterContext = {\n readonly tempDir: string;\n readonly baseUrl: string;\n readonly filePrefix: string;\n readonly gzip: boolean;\n readonly maxUrlsPerFile: number;\n readonly maxBytesPerFile: number;\n readonly defaults: Pick<\n SitemapOptions,\n \"changefreq\" | \"priority\" | \"lastmod\" | \"onImageLimitExceeded\"\n >;\n readonly duplicates: DuplicatePathTracker;\n readonly routes: RouteCounter;\n};\n\n/** A shard actually written, or the zero-url row reported for a group that produced nothing. */\nexport type ShardFile = {\n readonly fileName: string;\n readonly key?: string;\n readonly urls: number;\n readonly bytes: number;\n readonly gzipped: boolean;\n};\n\nfunction envelopeBytes(baseUrl: string): number {\n return Buffer.byteLength(buildSitemapXml([], baseUrl), \"utf8\");\n}\n\n/**\n * The extra bytes `<urlset>` gains for `xmlns:xhtml=\"…\"` once a shard holds\n * an entry with alternates. Charged separately from `envelopeBytes()` so the\n * ceiling check can add it exactly once — the moment the first alternate\n * enters the buffer — rather than missing it entirely, which would let a\n * shard's real bytes on disk exceed the ceiling it was rolled against.\n */\nconst NAMESPACE_BYTES = Buffer.byteLength(XHTML_NAMESPACE_ATTR, \"utf8\");\nconst IMAGE_NAMESPACE_BYTES = Buffer.byteLength(IMAGE_NAMESPACE_ATTR, \"utf8\");\n\nasync function writeShardFile(\n ctx: ShardWriterContext,\n fileName: string,\n entries: readonly ResolvedSitemapEntry[],\n): Promise<{ bytes: number }> {\n const xml = buildSitemapXml(entries, ctx.baseUrl);\n const filePath = join(ctx.tempDir, fileName);\n\n if (ctx.gzip) {\n const compressed = gzipSync(Buffer.from(xml, \"utf8\"));\n\n await writeFile(filePath, compressed);\n\n return { bytes: compressed.byteLength };\n }\n\n await writeFile(filePath, xml, \"utf8\");\n\n return { bytes: Buffer.byteLength(xml, \"utf8\") };\n}\n\n/**\n * Walks one group's factories, one at a time, rolling to a new shard on\n * whichever ceiling — URL count or serialised bytes — is hit first. Holds\n * only the current shard's buffer, never the whole group.\n */\nexport async function writeShardGroup(\n group: ShardGroup,\n ctx: ShardWriterContext,\n): Promise<ShardFile[]> {\n const results: ShardFile[] = [];\n\n let buffer: ResolvedSitemapEntry[] = [];\n let bufferBytes = envelopeBytes(ctx.baseUrl);\n // Charged once, the moment the buffer's first alternate-bearing entry is added — mirrors\n // buildSitemapXml()'s own \"at least one entry has alternates\" rule for the same shard.\n let bufferHasAlternates = false;\n let bufferHasImages = false;\n let ordinal = 1;\n\n const flush = async () => {\n if (buffer.length === 0) return;\n\n const fileName = shardFileName(ctx.filePrefix, group.key, ordinal, ctx.gzip);\n const { bytes } = await writeShardFile(ctx, fileName, buffer);\n\n results.push({ fileName, key: group.key, urls: buffer.length, bytes, gzipped: ctx.gzip });\n\n ordinal += 1;\n buffer = [];\n bufferBytes = envelopeBytes(ctx.baseUrl);\n bufferHasAlternates = false;\n bufferHasImages = false;\n };\n\n for (const factory of group.factories) {\n const source = await factory();\n\n for await (const rawEntry of source) {\n const resolved = normalizeEntry(rawEntry, ctx.defaults);\n\n // Duplicate paths are SKIPPED, not overridden: the earlier one is already on disk.\n if (!ctx.duplicates.attempt(resolved.path, resolved.route)) continue;\n\n const entryHasAlternates = (resolved.alternates?.length ?? 0) > 0;\n const entryHasImages = (resolved.images?.length ?? 0) > 0;\n const blockBytes = Buffer.byteLength(renderUrlBlock(resolved, ctx.baseUrl), \"utf8\") + 1;\n // What this entry would add to the CURRENT shard: its own block, plus the namespace\n // attribute if this is the shard's first alternate and the buffer doesn't carry it yet.\n const addedBytes =\n blockBytes +\n (entryHasAlternates && !bufferHasAlternates ? NAMESPACE_BYTES : 0) +\n (entryHasImages && !bufferHasImages ? IMAGE_NAMESPACE_BYTES : 0);\n\n const hitsUrlCeiling = buffer.length >= ctx.maxUrlsPerFile;\n // A single entry can never be split, so the byte ceiling only rolls an already-nonempty shard.\n const hitsByteCeiling = buffer.length > 0 && bufferBytes + addedBytes > ctx.maxBytesPerFile;\n\n if (hitsUrlCeiling || hitsByteCeiling) await flush();\n\n const addsNamespaceNow = entryHasAlternates && !bufferHasAlternates;\n const addsImageNamespaceNow = entryHasImages && !bufferHasImages;\n\n buffer.push(resolved);\n bufferBytes +=\n blockBytes +\n (addsNamespaceNow ? NAMESPACE_BYTES : 0) +\n (addsImageNamespaceNow ? IMAGE_NAMESPACE_BYTES : 0);\n if (addsNamespaceNow) bufferHasAlternates = true;\n if (addsImageNamespaceNow) bufferHasImages = true;\n ctx.routes.record(resolved.route);\n }\n }\n\n await flush();\n\n if (results.length === 0) {\n // Reported, never written: an empty shard in an index is a section someone lost.\n results.push({\n fileName: shardFileName(ctx.filePrefix, group.key, 1, ctx.gzip),\n key: group.key,\n urls: 0,\n bytes: 0,\n gzipped: false,\n });\n }\n\n return results;\n}\n","import { escapeXml } from \"./xml\";\nimport { joinOrigin } from \"./url\";\nimport type { ShardFile } from \"./sitemap-shard-writer\";\n\n/**\n * Serialises the flat master `sitemapindex` document: every shard of every\n * group, directly, in the order the groups and shards were produced — no\n * nested per-group indexes, and never a zero-url row (that is a diagnostic\n * for `files`, not something a crawler should be told to fetch).\n */\nexport function buildSitemapIndexXml(\n files: readonly ShardFile[],\n baseUrl: string,\n shardPathPrefix = \"\",\n): string {\n const body = files\n .filter((file) => file.urls > 0)\n .map((file) => {\n const loc = escapeXml(joinOrigin(baseUrl, `${shardPathPrefix}${file.fileName}`));\n\n return ` <sitemap>\\n <loc>${loc}</loc>\\n </sitemap>`;\n })\n .join(\"\\n\");\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n` +\n (body.length > 0 ? `${body}\\n` : \"\") +\n `</sitemapindex>\\n`\n );\n}\n","import type { DuplicateReport } from \"./types\";\n\n/**\n * The streaming equivalent of `Sitemap`'s `routesByPath` map. A streaming\n * writer cannot compare an entry against 500K predecessors, so it keeps only\n * the paths (and contributing routes) it has already seen. A duplicate is\n * SKIPPED here, not last-wins — the earlier one is already on disk.\n */\nexport class DuplicatePathTracker {\n private readonly routesByPath = new Map<string, (string | undefined)[]>();\n\n /** Records an attempt to write `path`. Returns `false` when it was already seen — skip it. */\n public attempt(path: string, route: string | undefined): boolean {\n const seen = this.routesByPath.get(path);\n\n if (seen) {\n seen.push(route);\n\n return false;\n }\n\n this.routesByPath.set(path, [route]);\n\n return true;\n }\n\n public report(): DuplicateReport[] {\n const reports: DuplicateReport[] = [];\n\n for (const [path, routes] of this.routesByPath) {\n if (routes.length < 2) continue;\n\n reports.push({ path, count: routes.length, routes: [...routes] });\n }\n\n return reports;\n }\n}\n","import type { RouteSummary } from \"./types\";\n\n/** Counts URLs actually written per declared route, across every shard of every group. */\nexport class RouteCounter {\n private readonly counts = new Map<string, number>();\n\n public record(route: string | undefined): void {\n if (route === undefined) return;\n\n this.counts.set(route, (this.counts.get(route) ?? 0) + 1);\n }\n\n public summary(): RouteSummary[] {\n return [...this.counts].map(([route, count]) => ({ route, count }));\n }\n}\n","import { join } from \"node:path\";\nimport { writeFile } from \"node:fs/promises\";\nimport { DuplicateSourceKeyError } from \"./errors\";\nimport {\n normalizeSitemapIndexOptions,\n type ResolvedSitemapIndexOptions,\n} from \"./sitemap-index-options\";\nimport { canonicalizeSourceKey } from \"./shard-name\";\nimport { publishAtomically } from \"./atomic-publish\";\nimport { writeShardGroup, type ShardFile, type ShardGroup } from \"./sitemap-shard-writer\";\nimport { buildSitemapIndexXml } from \"./sitemap-index-xml\";\nimport { DuplicatePathTracker } from \"./duplicate-path-tracker\";\nimport { RouteCounter } from \"./route-counter\";\nimport type {\n SitemapFileResult,\n SitemapIndexOptions,\n SitemapSetResult,\n SitemapSourceFactory,\n} from \"./sitemap-index-types\";\n\n/**\n * The streaming path for 300–500K URLs: shards, one flat master index, gzip,\n * and an atomic publish. It is a SECOND mode, not a bigger `Sitemap` — it\n * retains only the current shard's buffer and the set of paths it has seen.\n * There is no `entries()`, `toXML()` or `size`; anyone who can afford those\n * is in `Sitemap` and should be there instead.\n */\nexport class SitemapIndex {\n private readonly options: ResolvedSitemapIndexOptions;\n\n /** Every unnamed `addSource(factory)` call merges into this one group, sharing one shard counter. */\n private readonly unnamedFactories: SitemapSourceFactory[] = [];\n\n private readonly namedGroups: ShardGroup[] = [];\n\n /** Canonical (lowercased) keys already registered, so a case collision is caught at `addSource()`. */\n private readonly registeredKeys = new Set<string>();\n\n private readonly warnedImageRoutes = new Set<string>();\n\n public constructor(options: SitemapIndexOptions) {\n this.options = normalizeSitemapIndexOptions(options);\n }\n\n public addSource(source: SitemapSourceFactory): this;\n public addSource(key: string, source: SitemapSourceFactory): this;\n public addSource(\n keyOrSource: string | SitemapSourceFactory,\n maybeSource?: SitemapSourceFactory,\n ): this {\n if (typeof keyOrSource === \"string\") {\n const canonicalKey = canonicalizeSourceKey(keyOrSource);\n\n if (this.registeredKeys.has(canonicalKey)) {\n throw new DuplicateSourceKeyError(keyOrSource);\n }\n\n this.registeredKeys.add(canonicalKey);\n this.namedGroups.push({ key: keyOrSource, factories: [maybeSource as SitemapSourceFactory] });\n } else {\n this.unnamedFactories.push(keyOrSource);\n }\n\n return this;\n }\n\n /**\n * Walks every group one at a time — never all at once — into a sibling\n * temp directory, then publishes the whole set atomically. `files` is\n * reported in registration order: the unnamed group first (if any), then\n * named groups key-then-ordinal.\n */\n public async saveTo(outDir: string): Promise<SitemapSetResult> {\n const groups: ShardGroup[] = [\n ...(this.unnamedFactories.length > 0\n ? [{ key: undefined, factories: this.unnamedFactories }]\n : []),\n ...this.namedGroups,\n ];\n\n const duplicates = new DuplicatePathTracker();\n const routes = new RouteCounter();\n let shardFiles: ShardFile[] = [];\n\n const write = async (tempDir: string): Promise<readonly string[]> => {\n shardFiles = [];\n\n for (const group of groups) {\n const files = await writeShardGroup(group, {\n tempDir,\n baseUrl: this.options.baseUrl,\n filePrefix: this.options.filePrefix,\n gzip: this.options.gzip,\n maxUrlsPerFile: this.options.maxUrlsPerFile,\n maxBytesPerFile: this.options.maxBytesPerFile,\n defaults: {\n ...this.options.defaults,\n onImageLimitExceeded: (event) => this.reportImageLimit(event),\n },\n duplicates,\n routes,\n });\n\n shardFiles.push(...files);\n }\n\n const indexXml = buildSitemapIndexXml(\n shardFiles,\n this.options.baseUrl,\n this.options.shardPathPrefix,\n );\n\n await writeFile(join(tempDir, this.options.indexFileName), indexXml, \"utf8\");\n\n const writtenShardNames = shardFiles\n .filter((file) => file.urls > 0)\n .map((file) => file.fileName);\n\n return [...writtenShardNames, this.options.indexFileName];\n };\n\n await publishAtomically(outDir, write);\n\n const files: SitemapFileResult[] = shardFiles.map((file) => ({\n path: join(outDir, file.fileName),\n ...(file.key !== undefined ? { key: file.key } : {}),\n urls: file.urls,\n bytes: file.bytes,\n gzipped: file.gzipped,\n }));\n\n return {\n indexPath: join(outDir, this.options.indexFileName),\n files,\n totalUrls: files.reduce((total, file) => total + file.urls, 0),\n duplicates: duplicates.report(),\n routes: routes.summary(),\n };\n }\n\n private reportImageLimit(event: { readonly route?: string; readonly dropped: number }): void {\n const key = event.route ?? \"unattributed\";\n if (this.warnedImageRoutes.has(key)) return;\n\n this.warnedImageRoutes.add(key);\n if (this.options.defaults.onImageLimitExceeded)\n this.options.defaults.onImageLimitExceeded(event);\n else\n console.warn(\n `[warlock:sitemap] ${key} exceeded the 1,000-image limit; dropped ${event.dropped} image(s).`,\n );\n }\n}\n","/** Storage-independent schema and key helpers for immutable sitemap generations. */\r\nexport const GENERATION_MANIFEST_VERSION = 1 as const;\r\nexport const MANIFEST_FENCE_WIDTH = 16;\r\n\r\nexport type SitemapGenerationKind = \"single\" | \"index\";\r\n\r\nexport type SitemapGenerationFile = {\r\n readonly path: string;\r\n readonly bytes: number;\r\n readonly sha256: string;\r\n};\r\n\r\nexport type SitemapGenerationManifest = {\r\n readonly version: typeof GENERATION_MANIFEST_VERSION;\r\n readonly fence: number;\r\n readonly generationId: string;\r\n readonly coversRev: number;\r\n readonly kind: SitemapGenerationKind;\r\n readonly mainFile: string;\r\n readonly files: readonly SitemapGenerationFile[];\r\n readonly entries: number;\r\n readonly generatedAt: string;\r\n};\r\n\r\nexport type SitemapManifestKey = {\r\n readonly key: string;\r\n readonly fence: number;\r\n readonly generationId: string;\r\n};\r\n\r\nexport class InvalidSitemapGenerationManifestError extends Error {\r\n public constructor(message: string) {\r\n super(`Invalid sitemap generation manifest: ${message}`);\r\n this.name = \"InvalidSitemapGenerationManifestError\";\r\n }\r\n}\r\n\r\nconst SAFE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;\r\nconst SAFE_FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;\r\nconst SHA256 = /^[a-f0-9]{64}$/;\r\nconst ISO_DATE_TIME = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$/;\r\nconst MANIFEST_KEY = /^manifests\\/(\\d{16})-([A-Za-z0-9][A-Za-z0-9_-]{0,127})\\.json$/;\r\n\r\nfunction fail(message: string): never {\r\n throw new InvalidSitemapGenerationManifestError(message);\r\n}\r\n\r\nfunction object(value: unknown, label: string): Record<string, unknown> {\r\n if (typeof value !== \"object\" || value === null || Array.isArray(value))\r\n return fail(`${label} must be an object.`);\r\n return value as Record<string, unknown>;\r\n}\r\n\r\nfunction nonNegativeInteger(value: unknown, label: string): number {\r\n if (!Number.isSafeInteger(value) || (value as number) < 0)\r\n return fail(`${label} must be a safe non-negative integer.`);\r\n return value as number;\r\n}\r\n\r\nfunction generationIdentifier(value: unknown, label: string): string {\r\n if (typeof value !== \"string\" || !SAFE_IDENTIFIER.test(value))\r\n return fail(`${label} must be a safe generation identifier.`);\r\n return value;\r\n}\r\n\r\nfunction generatedAt(value: unknown): string {\r\n if (typeof value !== \"string\" || !ISO_DATE_TIME.test(value) || Number.isNaN(Date.parse(value))) {\r\n return fail(\"generatedAt must be a valid ISO-8601 date-time.\");\r\n }\r\n return value;\r\n}\r\n\r\nfunction artifactPath(value: unknown, generationId: string, label: string): string {\r\n if (typeof value !== \"string\") return fail(`${label} must be a string.`);\r\n const prefix = `generations/${generationId}/`;\r\n if (!value.startsWith(prefix)) return fail(`${label} must be contained in ${prefix}.`);\r\n const fileName = value.slice(prefix.length);\r\n if (\r\n !SAFE_FILE_NAME.test(fileName) ||\r\n fileName.includes(\"/\") ||\r\n fileName === \".\" ||\r\n fileName === \"..\"\r\n ) {\r\n return fail(`${label} must name one safe, flat artifact file.`);\r\n }\r\n return value;\r\n}\r\n\r\nfunction manifestFile(value: unknown, generationId: string, index: number): SitemapGenerationFile {\r\n const record = object(value, `files[${index}]`);\r\n const path = artifactPath(record.path, generationId, `files[${index}].path`);\r\n const bytes = nonNegativeInteger(record.bytes, `files[${index}].bytes`);\r\n if (typeof record.sha256 !== \"string\" || !SHA256.test(record.sha256)) {\r\n return fail(`files[${index}].sha256 must be a lowercase SHA-256 hex digest.`);\r\n }\r\n return { path, bytes, sha256: record.sha256 };\r\n}\r\n\r\n/** Parse untrusted JSON into the v1 manifest contract. It does not touch storage. */\r\nexport function parseSitemapGenerationManifest(value: unknown): SitemapGenerationManifest {\r\n const record = object(value, \"manifest\");\r\n if (record.version !== GENERATION_MANIFEST_VERSION)\r\n return fail(`version must be ${GENERATION_MANIFEST_VERSION}.`);\r\n\r\n const fence = nonNegativeInteger(record.fence, \"fence\");\r\n const generationId = generationIdentifier(record.generationId, \"generationId\");\r\n const coversRev = nonNegativeInteger(record.coversRev, \"coversRev\");\r\n if (record.kind !== \"single\" && record.kind !== \"index\")\r\n return fail('kind must be \"single\" or \"index\".');\r\n if (!Array.isArray(record.files) || record.files.length === 0)\r\n return fail(\"files must be a non-empty array.\");\r\n\r\n const files = record.files.map((file, index) => manifestFile(file, generationId, index));\r\n const paths = new Set<string>();\r\n const fileNames = new Set<string>();\r\n for (const file of files) {\r\n const fileName = file.path.slice(`generations/${generationId}/`.length);\r\n if (paths.has(file.path))\r\n return fail(`files contains duplicate path ${JSON.stringify(file.path)}.`);\r\n if (fileNames.has(fileName))\r\n return fail(`files contains duplicate public file name ${JSON.stringify(fileName)}.`);\r\n paths.add(file.path);\r\n fileNames.add(fileName);\r\n }\r\n\r\n const mainFile = artifactPath(record.mainFile, generationId, \"mainFile\");\r\n if (!paths.has(mainFile)) return fail(\"mainFile must match one files[].path.\");\r\n\r\n return {\r\n version: GENERATION_MANIFEST_VERSION,\r\n fence,\r\n generationId,\r\n coversRev,\r\n kind: record.kind,\r\n mainFile,\r\n files,\r\n entries: nonNegativeInteger(record.entries, \"entries\"),\r\n generatedAt: generatedAt(record.generatedAt),\r\n };\r\n}\r\n\r\n/** Build the immutable v1 manifest object key for a generation fence. */\r\nexport function sitemapManifestKey(fence: number, generationId: string): string {\r\n const safeFence = nonNegativeInteger(fence, \"fence\");\r\n if (safeFence >= 10 ** MANIFEST_FENCE_WIDTH)\r\n return fail(`fence must fit within ${MANIFEST_FENCE_WIDTH} decimal digits.`);\r\n return `manifests/${String(safeFence).padStart(MANIFEST_FENCE_WIDTH, \"0\")}-${generationIdentifier(generationId, \"generationId\")}.json`;\r\n}\r\n/** Parse only the shape of a manifest object key; artifact validation belongs to the storage adapter. */\r\nexport function parseSitemapManifestKey(key: unknown): SitemapManifestKey | undefined {\r\n if (typeof key !== \"string\") return undefined;\r\n const match = MANIFEST_KEY.exec(key);\r\n if (!match) return undefined;\r\n const fence = Number(match[1]);\r\n const generationId = match[2];\r\n if (!Number.isSafeInteger(fence) || generationId === undefined) return undefined;\r\n return { key, fence, generationId };\r\n}\r\n\r\n/** Numeric-fence-descending candidates; callers validate storage contents before adoption. */\r\nexport function sortSitemapManifestCandidates(keys: readonly string[]): SitemapManifestKey[] {\r\n return keys\r\n .map(parseSitemapManifestKey)\r\n .filter((candidate): candidate is SitemapManifestKey => candidate !== undefined)\r\n .sort(\r\n (left, right) =>\r\n right.fence - left.fence || right.generationId.localeCompare(left.generationId),\r\n );\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../../../../../sitemap/src/errors.ts","../../../../../../sitemap/src/atomic-publish.ts","../../../../../../sitemap/src/atomic-write-file.ts","../../../../../../sitemap/src/lastmod.ts","../../../../../../sitemap/src/normalize-entry.ts","../../../../../../sitemap/src/url.ts","../../../../../../sitemap/src/xml.ts","../../../../../../sitemap/src/sitemap.ts","../../../../../../sitemap/src/sitemap-index-options.ts","../../../../../../sitemap/src/shard-name.ts","../../../../../../sitemap/src/sitemap-shard-writer.ts","../../../../../../sitemap/src/sitemap-index-xml.ts","../../../../../../sitemap/src/duplicate-path-tracker.ts","../../../../../../sitemap/src/route-counter.ts","../../../../../../sitemap/src/sitemap-index.ts","../../../../../../sitemap/src/generation-manifest.ts"],"sourcesContent":["/**\n * The `baseUrl` given to a `Sitemap` is not an absolute http(s) URL.\n *\n * Thrown from the CONSTRUCTOR: a sitemap that cannot produce a valid URL\n * should not exist, and the mistake belongs at the line that wrote the value\n * rather than at the first request that reads it.\n */\nexport class InvalidBaseUrlError extends Error {\n public constructor(value: unknown, reason: string) {\n super(`Invalid sitemap baseUrl ${JSON.stringify(value)}: ${reason}.`);\n this.name = \"InvalidBaseUrlError\";\n }\n}\n\n/** An entry the sitemap protocol cannot represent. Thrown from `add()`. */\nexport class InvalidSitemapEntryError extends Error {\n public constructor(reason: string) {\n super(`Invalid sitemap entry: ${reason}.`);\n this.name = \"InvalidSitemapEntryError\";\n }\n}\n\n/**\n * Two `SitemapIndex` source keys collide once canonicalised (`en-US` and\n * `en-us` would produce the same filename on a case-insensitive filesystem\n * and silently overwrite one another). Thrown from `addSource()`, not at\n * `saveTo()`, so the mistake is caught at the line that registered it.\n */\nexport class DuplicateSourceKeyError extends Error {\n public constructor(key: string) {\n super(\n `Duplicate sitemap source key ${JSON.stringify(key)}: keys collide case-insensitively ` +\n `and would overwrite one another's shard files.`,\n );\n this.name = \"DuplicateSourceKeyError\";\n }\n}\n\n/**\n * `SitemapIndex.saveTo(outDir)` swaps the ENTIRE `outDir` for a freshly\n * written set (`atomic-publish.ts`). That is safe only when `outDir` is a\n * directory this package already owns — marked by its own\n * `.sitemap-set.json` from a prior publish. A non-empty directory with no\n * marker is presumed to belong to someone else (a caller's `public/`, most\n * dangerously) and is never swapped or deleted; this is thrown instead, from\n * `saveTo()` before anything is written.\n */\nexport class UnownedOutputDirectoryError extends Error {\n public constructor(outDir: string) {\n super(\n `Refusing to publish a sitemap set to ${JSON.stringify(outDir)}: this directory already ` +\n `has content but no \".sitemap-set.json\" marker from a previous @warlock.js/sitemap ` +\n `publish, so it is not safe to swap or delete. Point saveTo() at a dedicated, ` +\n `sitemap-only directory instead.`,\n );\n this.name = \"UnownedOutputDirectoryError\";\n }\n}\n","import { mkdir, readdir, rename, rm, stat, writeFile } from \"node:fs/promises\";\nimport { basename, dirname, join } from \"node:path\";\nimport { UnownedOutputDirectoryError } from \"./errors\";\n\n/**\n * Marks a directory as one `publishAtomically` swapped into place. Its\n * presence is the ONLY thing that lets a later publish treat the directory\n * as safe to swap out from under itself — see {@link assertOutDirIsOwned}.\n */\nconst OWNERSHIP_MARKER_FILE = \".sitemap-set.json\";\nconst OWNERSHIP_MARKER_VERSION = 1;\n\nasync function validateShards(tempDir: string, fileNames: readonly string[]): Promise<void> {\n for (const fileName of fileNames) {\n const info = await stat(join(tempDir, fileName)).catch(() => undefined);\n\n if (!info || !info.isFile() || info.size === 0) {\n throw new Error(`sitemap publish aborted: shard \"${fileName}\" is missing or empty.`);\n }\n }\n}\n\n/**\n * `outDir` is safe to take over when it does not exist yet, is empty, or\n * already carries {@link OWNERSHIP_MARKER_FILE} from a previous publish.\n * Anything else — a non-empty directory this package never wrote — is\n * refused rather than swapped or deleted (`de97020e`).\n */\nasync function assertOutDirIsOwned(outDir: string): Promise<void> {\n const entries = await readdir(outDir).catch((error) => {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return undefined;\n throw error;\n });\n\n if (entries === undefined || entries.length === 0) return;\n\n if (!entries.includes(OWNERSHIP_MARKER_FILE)) {\n throw new UnownedOutputDirectoryError(outDir);\n }\n}\n\n/**\n * Writes a complete set into a sibling temp directory, validates that every\n * file it named actually exists and is non-empty, then swaps it into\n * `outDir`. A crawler arriving mid-write sees the previous complete set or\n * the new one, never a partial one, and a failed run leaves the previous set\n * untouched and rejects.\n *\n * `write` performs the writes and returns the file names (relative to the\n * temp dir) that must be present for the set to be considered valid — it is\n * only known after writing, since shard count depends on what was walked.\n */\nexport async function publishAtomically(\n outDir: string,\n write: (tempDir: string) => Promise<readonly string[]>,\n): Promise<void> {\n await assertOutDirIsOwned(outDir);\n\n const parent = dirname(outDir);\n\n await mkdir(parent, { recursive: true });\n\n const tempDir = join(parent, `.${basename(outDir)}.tmp-${process.pid}-${Date.now()}`);\n\n await mkdir(tempDir, { recursive: true });\n\n try {\n const fileNames = await write(tempDir);\n\n await validateShards(tempDir, fileNames);\n\n await writeFile(\n join(tempDir, OWNERSHIP_MARKER_FILE),\n JSON.stringify({ package: \"@warlock.js/sitemap\", version: OWNERSHIP_MARKER_VERSION }),\n \"utf8\",\n );\n\n const displacedDir = join(parent, `.${basename(outDir)}.previous-${Date.now()}`);\n let displacedPrevious = false;\n\n try {\n await rename(outDir, displacedDir);\n displacedPrevious = true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n }\n\n try {\n await rename(tempDir, outDir);\n } catch (error) {\n if (displacedPrevious) await rename(displacedDir, outDir);\n throw error;\n }\n\n if (displacedPrevious) await rm(displacedDir, { recursive: true, force: true });\n } catch (error) {\n await rm(tempDir, { recursive: true, force: true });\n throw error;\n }\n}\n","import { rename, rm, writeFile } from \"node:fs/promises\";\nimport { basename, dirname, join } from \"node:path\";\n\n/** The subset of `node:fs/promises` this helper needs — swappable so a test can inject a failing write or rename without mocking the global module. */\nexport type AtomicWriteFileDeps = {\n writeFile: typeof writeFile;\n rename: typeof rename;\n rm: typeof rm;\n};\n\nconst defaultDeps: AtomicWriteFileDeps = { writeFile, rename, rm };\n\nconst RENAME_RETRY_ATTEMPTS = 5;\nconst RENAME_RETRY_DELAY_MS = 20;\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Windows can report a rename over an existing file as EPERM/EBUSY while something briefly holds the target (an AV scan, a reader) — retrying is correct there, not a masked bug. */\nfunction isTransientRenameError(error: unknown): boolean {\n const code = (error as NodeJS.ErrnoException)?.code;\n\n return code === \"EPERM\" || code === \"EBUSY\";\n}\n\nasync function renameWithRetry(from: string, to: string, deps: AtomicWriteFileDeps): Promise<void> {\n for (let attempt = 1; attempt <= RENAME_RETRY_ATTEMPTS; attempt++) {\n try {\n await deps.rename(from, to);\n\n return;\n } catch (error) {\n if (attempt === RENAME_RETRY_ATTEMPTS || !isTransientRenameError(error)) throw error;\n\n await delay(RENAME_RETRY_DELAY_MS * attempt);\n }\n }\n}\n\n/**\n * Writes `content` to `filePath` atomically: the content lands in a unique\n * sibling temp file first — same directory, so same filesystem, so the\n * rename that follows is atomic — and only then is renamed over the target.\n * An interrupted or failing write never truncates or otherwise touches the\n * existing target; on any failure the temp file is removed and the error is\n * rethrown.\n */\nexport async function atomicWriteFile(\n filePath: string,\n content: string,\n deps: AtomicWriteFileDeps = defaultDeps,\n): Promise<void> {\n const tempPath = join(\n dirname(filePath),\n `.${basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`,\n );\n\n try {\n await deps.writeFile(tempPath, content, \"utf8\");\n await renameWithRetry(tempPath, filePath, deps);\n } catch (error) {\n await deps.rm(tempPath, { force: true }).catch(() => undefined);\n\n throw error;\n }\n}\n","import { InvalidSitemapEntryError } from \"./errors\";\n\n/**\n * Serialises a `lastmod` value.\n *\n * A `Date` becomes W3C datetime, which is what the schema wants and what\n * `toISOString()` already produces. A string is passed through UNTOUCHED: a\n * caller who already holds an ISO string gets it back verbatim rather than\n * having us re-parse it and risk shifting it across a timezone.\n */\nexport function formatLastmod(value: string | Date): string {\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) {\n throw new InvalidSitemapEntryError(\"lastmod is an invalid Date\");\n }\n\n return value.toISOString();\n }\n\n if (typeof value !== \"string\" || value.trim() === \"\") {\n throw new InvalidSitemapEntryError(\"lastmod must be a non-empty string or a Date\");\n }\n\n return value;\n}\n","import { InvalidSitemapEntryError } from \"./errors\";\nimport { formatLastmod } from \"./lastmod\";\nimport type {\n ChangeFreq,\n ResolvedSitemapEntry,\n SitemapEntry,\n SitemapImage,\n SitemapOptions,\n} from \"./types\";\n\nconst CHANGE_FREQS: readonly ChangeFreq[] = [\n \"always\",\n \"hourly\",\n \"daily\",\n \"weekly\",\n \"monthly\",\n \"yearly\",\n \"never\",\n];\nconst MAX_IMAGES_PER_URL = 1_000;\n\nfunction assertChangeFreq(value: unknown): asserts value is ChangeFreq {\n if (!CHANGE_FREQS.includes(value as ChangeFreq)) {\n throw new InvalidSitemapEntryError(\n `changefreq ${JSON.stringify(value)} is not one of ${CHANGE_FREQS.join(\", \")}`,\n );\n }\n}\n\nfunction assertPriority(value: unknown): asserts value is number {\n if (typeof value !== \"number\" || Number.isNaN(value) || value < 0 || value > 1) {\n throw new InvalidSitemapEntryError(\n `priority ${JSON.stringify(value)} is outside the protocol range 0.0–1.0`,\n );\n }\n}\n\n/** Every stored path carries its leading slash, so `/a` and `a` are one entry, not two. */\nexport function normalizePath(path: unknown): string {\n if (typeof path !== \"string\" || path.trim() === \"\") {\n throw new InvalidSitemapEntryError(\"path is required and must be a non-empty string\");\n }\n\n // An absolute URL is left alone: the caller is overriding the base origin\n // deliberately, which a cross-origin alternate legitimately needs.\n if (/^https?:\\/\\//i.test(path)) return path;\n\n return path.startsWith(\"/\") ? path : `/${path}`;\n}\n\n/**\n * Validates one entry and folds the builder's defaults into it. Defaults are\n * resolved HERE rather than at serialisation time so that `entries()` shows\n * what will actually be emitted — a diagnostic that reports something other\n * than the output is worse than none.\n */\nexport function normalizeEntry(\n entry: SitemapEntry,\n defaults: Pick<SitemapOptions, \"changefreq\" | \"priority\" | \"lastmod\" | \"onImageLimitExceeded\">,\n): ResolvedSitemapEntry {\n const path = normalizePath(entry.path);\n const changefreq = entry.changefreq ?? defaults.changefreq;\n const priority = entry.priority ?? defaults.priority;\n const lastmod = entry.lastmod ?? defaults.lastmod;\n\n if (changefreq !== undefined) assertChangeFreq(changefreq);\n if (priority !== undefined) assertPriority(priority);\n\n const alternates = entry.alternates?.map((alternate) => {\n if (typeof alternate?.hreflang !== \"string\" || alternate.hreflang.trim() === \"\") {\n throw new InvalidSitemapEntryError(\"alternate hreflang is required\");\n }\n\n return { hreflang: alternate.hreflang, path: normalizePath(alternate.path) };\n });\n if (entry.images !== undefined && !Array.isArray(entry.images)) {\n throw new InvalidSitemapEntryError(\"images must be an array\");\n }\n const images = entry.images?.slice(0, MAX_IMAGES_PER_URL).map((image): SitemapImage => {\n if (typeof image?.loc !== \"string\" || image.loc.trim() === \"\") {\n throw new InvalidSitemapEntryError(\"image loc is required\");\n }\n\n let url: URL;\n try {\n url = new URL(image.loc);\n } catch {\n throw new InvalidSitemapEntryError(\"image loc must be an absolute HTTP(S) URL\");\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new InvalidSitemapEntryError(\"image loc must be an absolute HTTP(S) URL\");\n }\n\n return { loc: image.loc };\n });\n if ((entry.images?.length ?? 0) > MAX_IMAGES_PER_URL) {\n defaults.onImageLimitExceeded?.({\n ...(entry.route === undefined ? {} : { route: entry.route }),\n dropped: entry.images!.length - MAX_IMAGES_PER_URL,\n });\n }\n\n return {\n path,\n ...(entry.name !== undefined ? { name: entry.name } : {}),\n ...(entry.route !== undefined ? { route: entry.route } : {}),\n ...(lastmod !== undefined ? { lastmod: formatLastmod(lastmod) } : {}),\n ...(changefreq !== undefined ? { changefreq } : {}),\n ...(priority !== undefined ? { priority } : {}),\n ...(alternates !== undefined ? { alternates } : {}),\n ...(images !== undefined ? { images } : {}),\n };\n}\n","import { InvalidBaseUrlError } from \"./errors\";\n\n/**\n * Joins a configured origin and an app-relative route path into one absolute\n * URL, with exactly one slash at the seam regardless of whether either side\n * already carries one.\n */\nexport function joinOrigin(origin: string, routePath: string): string {\n const trimmedOrigin = origin.endsWith(\"/\") ? origin.slice(0, -1) : origin;\n const normalizedPath = routePath.startsWith(\"/\") ? routePath : `/${routePath}`;\n\n return `${trimmedOrigin}${normalizedPath}`;\n}\n\n/**\n * Validates a `baseUrl` and returns it without its trailing slash.\n *\n * `new URL()` accepts `mailto:` and `file:` happily, so the protocol is\n * checked explicitly — a sitemap `<loc>` that is not http(s) is not a document\n * any crawler will fetch.\n */\nexport function normalizeBaseUrl(value: string): string {\n if (typeof value !== \"string\" || value.trim() === \"\") {\n throw new InvalidBaseUrlError(value, \"expected a non-empty string\");\n }\n\n let parsed: URL;\n\n try {\n parsed = new URL(value);\n } catch {\n throw new InvalidBaseUrlError(value, \"not an absolute URL\");\n }\n\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new InvalidBaseUrlError(value, `unsupported protocol \"${parsed.protocol}\"`);\n }\n\n const href = parsed.href;\n\n return href.endsWith(\"/\") ? href.slice(0, -1) : href;\n}\n\n/** True for a value already usable as a `<loc>` without joining an origin. */\nexport function isAbsoluteUrl(value: string): boolean {\n return /^https?:\\/\\//i.test(value);\n}\n\n/** Resolves an entry or alternate path against the base URL, unless it is already absolute. */\nexport function resolveAgainstBase(baseUrl: string, path: string): string {\n return isAbsoluteUrl(path) ? path : joinOrigin(baseUrl, path);\n}\n","import type { ResolvedSitemapEntry } from \"./types\";\nimport { resolveAgainstBase } from \"./url\";\n\nconst XML_ESCAPES: Record<string, string> = {\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&apos;\",\n};\n\n/** Escapes the five XML-significant characters. A URL's query string routinely contains `&`. */\nexport function escapeXml(value: string): string {\n // The character class and the table are written together, so the lookup can\n // only miss if one is edited without the other; falling back to the original\n // character keeps that editing mistake from silently emitting `undefined`\n // into a URL.\n return value.replace(/[&<>\"']/g, (char) => XML_ESCAPES[char] ?? char);\n}\n\nconst XHTML_NAMESPACE = \"http://www.w3.org/1999/xhtml\";\nconst IMAGE_NAMESPACE = \"http://www.google.com/schemas/sitemap-image/1.1\";\n\n/**\n * The `xmlns:xhtml` attribute exactly as it appears on `<urlset>`, including\n * its leading space. Exported so the shard writer can charge its byte length\n * against the ceiling without duplicating the string it measures.\n */\nexport const XHTML_NAMESPACE_ATTR = ` xmlns:xhtml=\"${XHTML_NAMESPACE}\"`;\n/** See {@link XHTML_NAMESPACE_ATTR}; measured by the shard writer before roll-over. */\nexport const IMAGE_NAMESPACE_ATTR = ` xmlns:image=\"${IMAGE_NAMESPACE}\"`;\n\n/**\n * Renders one `<url>` block. Exported so the streaming writer can measure the\n * exact bytes it is about to append before deciding whether the byte ceiling\n * forces a new shard — a separate approximation could disagree with what is\n * actually written.\n */\nexport function renderUrlBlock(entry: ResolvedSitemapEntry, baseUrl: string): string {\n const lines = [` <url>`, ` <loc>${escapeXml(resolveAgainstBase(baseUrl, entry.path))}</loc>`];\n\n if (entry.lastmod !== undefined) {\n lines.push(` <lastmod>${escapeXml(entry.lastmod)}</lastmod>`);\n }\n\n if (entry.changefreq !== undefined) {\n lines.push(` <changefreq>${entry.changefreq}</changefreq>`);\n }\n\n if (entry.priority !== undefined) {\n lines.push(` <priority>${entry.priority}</priority>`);\n }\n\n for (const alternate of entry.alternates ?? []) {\n const href = escapeXml(resolveAgainstBase(baseUrl, alternate.path));\n\n lines.push(\n ` <xhtml:link rel=\"alternate\" hreflang=\"${escapeXml(alternate.hreflang)}\" href=\"${href}\"/>`,\n );\n }\n\n for (const image of entry.images ?? []) {\n lines.push(` <image:image>`);\n lines.push(` <image:loc>${escapeXml(resolveAgainstBase(baseUrl, image.loc))}</image:loc>`);\n lines.push(` </image:image>`);\n }\n\n lines.push(` </url>`);\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Serialises entries into a `urlset` sitemap document — the sitemaps.org\n * namespace, `<url>` per entry, element order `loc` / `lastmod` / `changefreq`\n * / `priority` (schema order; a validator that checks order rejects any other),\n * then any `xhtml:link` alternates.\n *\n * The xhtml namespace is declared only when some entry actually carries an\n * alternate — an unused namespace on every single-language sitemap is noise.\n */\nexport function buildSitemapXml(entries: readonly ResolvedSitemapEntry[], baseUrl: string): string {\n const hasAlternates = entries.some((entry) => (entry.alternates?.length ?? 0) > 0);\n const hasImages = entries.some((entry) => (entry.images?.length ?? 0) > 0);\n const namespaces = `${hasAlternates ? XHTML_NAMESPACE_ATTR : \"\"}${hasImages ? IMAGE_NAMESPACE_ATTR : \"\"}`;\n const body = entries.map((entry) => renderUrlBlock(entry, baseUrl)).join(\"\\n\");\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"${namespaces}>\\n` +\n (body.length > 0 ? `${body}\\n` : \"\") +\n `</urlset>\\n`\n );\n}\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { publishAtomically } from \"./atomic-publish\";\nimport { atomicWriteFile } from \"./atomic-write-file\";\nimport { normalizeEntry } from \"./normalize-entry\";\nimport type {\n DuplicateReport,\n ResolvedSitemapEntry,\n RouteSummary,\n SitemapEntry,\n SitemapOptions,\n} from \"./types\";\nimport { normalizeBaseUrl } from \"./url\";\nimport { buildSitemapXml } from \"./xml\";\n\n/**\n * A bounded sitemap builder: it RETAINS every entry, which is what makes\n * `entries()` and a repeatable `toXML()` possible, and is exactly right up to\n * the sitemaps.org ceiling of 50,000 URLs / 50MB.\n *\n * Above that ceiling this is the wrong tool — the streaming writer retains\n * nothing and emits shards plus an index instead. The two modes are separate\n * on purpose: a class that promised not to retain entries and still offered\n * `entries()` would be lying about one of them.\n */\nexport class Sitemap {\n private readonly baseUrl: string;\n\n private readonly defaults: Pick<\n SitemapOptions,\n \"changefreq\" | \"priority\" | \"lastmod\" | \"onImageLimitExceeded\"\n >;\n\n private readonly warnedImageRoutes = new Set<string>();\n\n private readonly optionsImageReporter: SitemapOptions[\"onImageLimitExceeded\"];\n\n /** Keyed by path: a duplicate `<loc>` makes the document invalid, so the later add wins. */\n private readonly entriesByPath = new Map<string, ResolvedSitemapEntry>();\n\n /** Insertion order per path, so `duplicates()` can name every colliding source. */\n private readonly routesByPath = new Map<string, (string | undefined)[]>();\n\n /** Declared patterns, including ones that never contributed a URL. */\n private readonly declaredRoutes = new Set<string>();\n\n public constructor(options: SitemapOptions) {\n this.baseUrl = normalizeBaseUrl(options?.baseUrl);\n this.optionsImageReporter = options.onImageLimitExceeded;\n this.defaults = {\n changefreq: options.changefreq,\n priority: options.priority,\n lastmod: options.lastmod,\n onImageLimitExceeded: (event) => this.reportImageLimit(event),\n };\n }\n\n private reportImageLimit(event: { readonly route?: string; readonly dropped: number }): void {\n const key = event.route ?? \"unattributed\";\n if (this.warnedImageRoutes.has(key)) return;\n\n this.warnedImageRoutes.add(key);\n if (this.optionsImageReporter) this.optionsImageReporter(event);\n else\n console.warn(\n `[warlock:sitemap] ${key} exceeded the 1,000-image limit; dropped ${event.dropped} image(s).`,\n );\n }\n\n public add(entry: SitemapEntry): this {\n const resolved = normalizeEntry(entry, this.defaults);\n\n this.entriesByPath.set(resolved.path, resolved);\n\n const seen = this.routesByPath.get(resolved.path);\n\n if (seen) {\n seen.push(resolved.route);\n } else {\n this.routesByPath.set(resolved.path, [resolved.route]);\n }\n\n if (resolved.route !== undefined) this.declaredRoutes.add(resolved.route);\n\n return this;\n }\n\n public addMany(entries: Iterable<SitemapEntry>): this {\n for (const entry of entries) this.add(entry);\n\n return this;\n }\n\n /**\n * Names a pattern the caller EXPECTS to contribute URLs, so that one which\n * contributes none shows up in `routes()` as a `count: 0` row instead of as\n * silence. A dynamic route whose supplier returned nothing is the failure\n * this package exists to make visible.\n */\n public declareRoute(route: string): this {\n this.declaredRoutes.add(route);\n\n return this;\n }\n\n public get size(): number {\n return this.entriesByPath.size;\n }\n\n public entries(): readonly ResolvedSitemapEntry[] {\n return [...this.entriesByPath.values()];\n }\n\n public routes(): readonly RouteSummary[] {\n const counts = new Map<string, number>();\n\n for (const route of this.declaredRoutes) counts.set(route, 0);\n\n for (const entry of this.entriesByPath.values()) {\n if (entry.route === undefined) continue;\n\n counts.set(entry.route, (counts.get(entry.route) ?? 0) + 1);\n }\n\n return [...counts].map(([route, count]) => ({ route, count }));\n }\n\n /**\n * Every path added more than once. The override is silent — it is not\n * hidden: a collision between static discovery and a dynamic supplier is a\n * real defect, and the CALLER decides whether it fails their build. This\n * package reports; it never prints and never throws over a duplicate.\n */\n public duplicates(): readonly DuplicateReport[] {\n const reports: DuplicateReport[] = [];\n\n for (const [path, routes] of this.routesByPath) {\n if (routes.length < 2) continue;\n\n reports.push({ path, count: routes.length, routes: [...routes] });\n }\n\n return reports;\n }\n\n /** Pure and repeatable: calling it twice returns the same string and mutates nothing. */\n public toXML(): string {\n return buildSitemapXml(this.entries(), this.baseUrl);\n }\n\n /**\n * Writes the document, creating parent directories so a clean checkout\n * works. The write is atomic: a failed or interrupted publish leaves\n * whatever was already at `filePath` untouched instead of truncating it.\n */\n public async saveTo(filePath: string): Promise<void> {\n await mkdir(dirname(filePath), { recursive: true });\n await atomicWriteFile(filePath, this.toXML());\n }\n\n /**\n * Publishes the document as the whole content of `outDir`, exactly the way\n * `SitemapIndex.saveTo` publishes a set: swapped in atomically, and marked\n * as owned. So a site that later outgrows one file can publish an index\n * into the same directory, and a later single file removes stale shards.\n * Refuses a non-empty directory this package did not write\n * (`UnownedOutputDirectoryError`). Returns the published file's path.\n */\n public async publishTo(outDir: string, fileName = \"sitemap.xml\"): Promise<string> {\n const xml = this.toXML();\n\n await publishAtomically(outDir, async (tempDir) => {\n await writeFile(join(tempDir, fileName), xml, \"utf8\");\n\n return [fileName];\n });\n\n return join(outDir, fileName);\n }\n}\n","import type { SitemapOptions } from \"./types\";\nimport { normalizeBaseUrl } from \"./url\";\nimport type { SitemapIndexOptions } from \"./sitemap-index-types\";\n\n/** The sitemaps.org limits. Never clamped to — a silently clamped option is a lie about what was written. */\nexport const PROTOCOL_MAX_URLS_PER_FILE = 50_000;\nexport const PROTOCOL_MAX_BYTES_PER_FILE = 50 * 1024 * 1024;\n\nexport type ResolvedSitemapIndexOptions = {\n readonly baseUrl: string;\n readonly filePrefix: string;\n readonly indexFileName: string;\n readonly shardPathPrefix: string;\n readonly gzip: boolean;\n readonly maxUrlsPerFile: number;\n readonly maxBytesPerFile: number;\n readonly defaults: Pick<\n SitemapOptions,\n \"changefreq\" | \"priority\" | \"lastmod\" | \"onImageLimitExceeded\"\n >;\n};\n\n/** Validates and folds in defaults, once, at the constructor — the same discipline as `Sitemap`. */\nexport function normalizeSitemapIndexOptions(\n options: SitemapIndexOptions,\n): ResolvedSitemapIndexOptions {\n const baseUrl = normalizeBaseUrl(options?.baseUrl);\n const shardPathPrefix = normalizeShardPathPrefix(options.shardPathPrefix);\n\n const maxUrlsPerFile = options.maxUrlsPerFile ?? PROTOCOL_MAX_URLS_PER_FILE;\n const maxBytesPerFile = options.maxBytesPerFile ?? PROTOCOL_MAX_BYTES_PER_FILE;\n\n if (\n !Number.isInteger(maxUrlsPerFile) ||\n maxUrlsPerFile < 1 ||\n maxUrlsPerFile > PROTOCOL_MAX_URLS_PER_FILE\n ) {\n throw new RangeError(\n `maxUrlsPerFile must be an integer between 1 and the sitemaps.org ceiling of ` +\n `${PROTOCOL_MAX_URLS_PER_FILE}, got ${JSON.stringify(maxUrlsPerFile)}.`,\n );\n }\n\n if (\n !Number.isFinite(maxBytesPerFile) ||\n maxBytesPerFile < 1 ||\n maxBytesPerFile > PROTOCOL_MAX_BYTES_PER_FILE\n ) {\n throw new RangeError(\n `maxBytesPerFile must be between 1 and the sitemaps.org ceiling of ` +\n `${PROTOCOL_MAX_BYTES_PER_FILE} bytes, got ${JSON.stringify(maxBytesPerFile)}.`,\n );\n }\n\n return {\n baseUrl,\n filePrefix: options.filePrefix ?? \"sitemap\",\n indexFileName: options.indexFileName ?? \"sitemap_index.xml\",\n shardPathPrefix,\n gzip: options.gzip ?? false,\n maxUrlsPerFile,\n maxBytesPerFile,\n defaults: {\n changefreq: options.changefreq,\n priority: options.priority,\n lastmod: options.lastmod,\n onImageLimitExceeded: options.onImageLimitExceeded,\n },\n };\n}\n\nfunction normalizeShardPathPrefix(value: string | undefined): string {\n if (value === undefined || value === \"\" || value === \"/\") return \"\";\n\n const prefix = value.replace(/^\\//, \"\").replace(/\\/$/, \"\");\n if (\n prefix.length === 0 ||\n prefix.split(\"/\").some((segment) => !/^[A-Za-z0-9_-]+$/.test(segment))\n ) {\n throw new TypeError(\"shardPathPrefix must contain only safe URL path segments.\");\n }\n\n return `${prefix}/`;\n}\n","const SAFE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;\n\n/**\n * Canonical form used to detect a case collision before it reaches the\n * filesystem: `en-US` and `en-us` would produce the same file on a\n * case-insensitive filesystem and silently overwrite one another.\n *\n * The key reaches the filename and nothing else — it is validated as a\n * filename fragment, not interpreted as a locale or anything else.\n */\nexport function canonicalizeSourceKey(key: string): string {\n if (typeof key !== \"string\" || !SAFE_KEY_PATTERN.test(key)) {\n throw new RangeError(\n `sitemap source key ${JSON.stringify(key)} must be a non-empty filename fragment ` +\n `(letters, digits, \"-\", \"_\").`,\n );\n }\n\n return key.toLowerCase();\n}\n\n/**\n * Stable, zero-padded, ordinal from shard one. A group that later crosses a\n * ceiling GAINS a file; it never renames the first one, so a crawler that has\n * already indexed `sitemap-en-0001.xml` never loses it because the site grew.\n */\nexport function shardFileName(\n filePrefix: string,\n key: string | undefined,\n ordinal: number,\n gzip: boolean,\n): string {\n const paddedOrdinal = String(ordinal).padStart(4, \"0\");\n const base =\n key !== undefined ? `${filePrefix}-${key}-${paddedOrdinal}` : `${filePrefix}-${paddedOrdinal}`;\n\n return gzip ? `${base}.xml.gz` : `${base}.xml`;\n}\n","import { writeFile } from \"node:fs/promises\";\nimport { gzipSync } from \"node:zlib\";\nimport { join } from \"node:path\";\nimport { normalizeEntry } from \"./normalize-entry\";\nimport { shardFileName } from \"./shard-name\";\nimport type { ResolvedSitemapEntry, SitemapOptions } from \"./types\";\nimport type { DuplicatePathTracker } from \"./duplicate-path-tracker\";\nimport type { RouteCounter } from \"./route-counter\";\nimport type { SitemapSourceFactory } from \"./sitemap-index-types\";\nimport { buildSitemapXml, IMAGE_NAMESPACE_ATTR, renderUrlBlock, XHTML_NAMESPACE_ATTR } from \"./xml\";\n\n/** One `addSource` group: the unnamed group merges every unkeyed call; a keyed group holds exactly one factory. */\nexport type ShardGroup = {\n readonly key?: string;\n readonly factories: readonly SitemapSourceFactory[];\n};\n\nexport type ShardWriterContext = {\n readonly tempDir: string;\n readonly baseUrl: string;\n readonly filePrefix: string;\n readonly gzip: boolean;\n readonly maxUrlsPerFile: number;\n readonly maxBytesPerFile: number;\n readonly defaults: Pick<\n SitemapOptions,\n \"changefreq\" | \"priority\" | \"lastmod\" | \"onImageLimitExceeded\"\n >;\n readonly duplicates: DuplicatePathTracker;\n readonly routes: RouteCounter;\n};\n\n/** A shard actually written, or the zero-url row reported for a group that produced nothing. */\nexport type ShardFile = {\n readonly fileName: string;\n readonly key?: string;\n readonly urls: number;\n readonly bytes: number;\n readonly gzipped: boolean;\n};\n\nfunction envelopeBytes(baseUrl: string): number {\n return Buffer.byteLength(buildSitemapXml([], baseUrl), \"utf8\");\n}\n\n/**\n * The extra bytes `<urlset>` gains for `xmlns:xhtml=\"…\"` once a shard holds\n * an entry with alternates. Charged separately from `envelopeBytes()` so the\n * ceiling check can add it exactly once — the moment the first alternate\n * enters the buffer — rather than missing it entirely, which would let a\n * shard's real bytes on disk exceed the ceiling it was rolled against.\n */\nconst NAMESPACE_BYTES = Buffer.byteLength(XHTML_NAMESPACE_ATTR, \"utf8\");\nconst IMAGE_NAMESPACE_BYTES = Buffer.byteLength(IMAGE_NAMESPACE_ATTR, \"utf8\");\n\nasync function writeShardFile(\n ctx: ShardWriterContext,\n fileName: string,\n entries: readonly ResolvedSitemapEntry[],\n): Promise<{ bytes: number }> {\n const xml = buildSitemapXml(entries, ctx.baseUrl);\n const filePath = join(ctx.tempDir, fileName);\n\n if (ctx.gzip) {\n const compressed = gzipSync(Buffer.from(xml, \"utf8\"));\n\n await writeFile(filePath, compressed);\n\n return { bytes: compressed.byteLength };\n }\n\n await writeFile(filePath, xml, \"utf8\");\n\n return { bytes: Buffer.byteLength(xml, \"utf8\") };\n}\n\n/**\n * Walks one group's factories, one at a time, rolling to a new shard on\n * whichever ceiling — URL count or serialised bytes — is hit first. Holds\n * only the current shard's buffer, never the whole group.\n */\nexport async function writeShardGroup(\n group: ShardGroup,\n ctx: ShardWriterContext,\n): Promise<ShardFile[]> {\n const results: ShardFile[] = [];\n\n let buffer: ResolvedSitemapEntry[] = [];\n let bufferBytes = envelopeBytes(ctx.baseUrl);\n // Charged once, the moment the buffer's first alternate-bearing entry is added — mirrors\n // buildSitemapXml()'s own \"at least one entry has alternates\" rule for the same shard.\n let bufferHasAlternates = false;\n let bufferHasImages = false;\n let ordinal = 1;\n\n const flush = async () => {\n if (buffer.length === 0) return;\n\n const fileName = shardFileName(ctx.filePrefix, group.key, ordinal, ctx.gzip);\n const { bytes } = await writeShardFile(ctx, fileName, buffer);\n\n results.push({ fileName, key: group.key, urls: buffer.length, bytes, gzipped: ctx.gzip });\n\n ordinal += 1;\n buffer = [];\n bufferBytes = envelopeBytes(ctx.baseUrl);\n bufferHasAlternates = false;\n bufferHasImages = false;\n };\n\n for (const factory of group.factories) {\n const source = await factory();\n\n for await (const rawEntry of source) {\n const resolved = normalizeEntry(rawEntry, ctx.defaults);\n\n // Duplicate paths are SKIPPED, not overridden: the earlier one is already on disk.\n if (!ctx.duplicates.attempt(resolved.path, resolved.route)) continue;\n\n const entryHasAlternates = (resolved.alternates?.length ?? 0) > 0;\n const entryHasImages = (resolved.images?.length ?? 0) > 0;\n const blockBytes = Buffer.byteLength(renderUrlBlock(resolved, ctx.baseUrl), \"utf8\") + 1;\n // What this entry would add to the CURRENT shard: its own block, plus the namespace\n // attribute if this is the shard's first alternate and the buffer doesn't carry it yet.\n const addedBytes =\n blockBytes +\n (entryHasAlternates && !bufferHasAlternates ? NAMESPACE_BYTES : 0) +\n (entryHasImages && !bufferHasImages ? IMAGE_NAMESPACE_BYTES : 0);\n\n const hitsUrlCeiling = buffer.length >= ctx.maxUrlsPerFile;\n // A single entry can never be split, so the byte ceiling only rolls an already-nonempty shard.\n const hitsByteCeiling = buffer.length > 0 && bufferBytes + addedBytes > ctx.maxBytesPerFile;\n\n if (hitsUrlCeiling || hitsByteCeiling) await flush();\n\n const addsNamespaceNow = entryHasAlternates && !bufferHasAlternates;\n const addsImageNamespaceNow = entryHasImages && !bufferHasImages;\n\n buffer.push(resolved);\n bufferBytes +=\n blockBytes +\n (addsNamespaceNow ? NAMESPACE_BYTES : 0) +\n (addsImageNamespaceNow ? IMAGE_NAMESPACE_BYTES : 0);\n if (addsNamespaceNow) bufferHasAlternates = true;\n if (addsImageNamespaceNow) bufferHasImages = true;\n ctx.routes.record(resolved.route);\n }\n }\n\n await flush();\n\n if (results.length === 0) {\n // Reported, never written: an empty shard in an index is a section someone lost.\n results.push({\n fileName: shardFileName(ctx.filePrefix, group.key, 1, ctx.gzip),\n key: group.key,\n urls: 0,\n bytes: 0,\n gzipped: false,\n });\n }\n\n return results;\n}\n","import { escapeXml } from \"./xml\";\nimport { joinOrigin } from \"./url\";\nimport type { ShardFile } from \"./sitemap-shard-writer\";\n\n/**\n * Serialises the flat master `sitemapindex` document: every shard of every\n * group, directly, in the order the groups and shards were produced — no\n * nested per-group indexes, and never a zero-url row (that is a diagnostic\n * for `files`, not something a crawler should be told to fetch).\n */\nexport function buildSitemapIndexXml(\n files: readonly ShardFile[],\n baseUrl: string,\n shardPathPrefix = \"\",\n): string {\n const body = files\n .filter((file) => file.urls > 0)\n .map((file) => {\n const loc = escapeXml(joinOrigin(baseUrl, `${shardPathPrefix}${file.fileName}`));\n\n return ` <sitemap>\\n <loc>${loc}</loc>\\n </sitemap>`;\n })\n .join(\"\\n\");\n\n return (\n `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n` +\n `<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n` +\n (body.length > 0 ? `${body}\\n` : \"\") +\n `</sitemapindex>\\n`\n );\n}\n","import type { DuplicateReport } from \"./types\";\n\n/**\n * The streaming equivalent of `Sitemap`'s `routesByPath` map. A streaming\n * writer cannot compare an entry against 500K predecessors, so it keeps only\n * the paths (and contributing routes) it has already seen. A duplicate is\n * SKIPPED here, not last-wins — the earlier one is already on disk.\n */\nexport class DuplicatePathTracker {\n private readonly routesByPath = new Map<string, (string | undefined)[]>();\n\n /** Records an attempt to write `path`. Returns `false` when it was already seen — skip it. */\n public attempt(path: string, route: string | undefined): boolean {\n const seen = this.routesByPath.get(path);\n\n if (seen) {\n seen.push(route);\n\n return false;\n }\n\n this.routesByPath.set(path, [route]);\n\n return true;\n }\n\n public report(): DuplicateReport[] {\n const reports: DuplicateReport[] = [];\n\n for (const [path, routes] of this.routesByPath) {\n if (routes.length < 2) continue;\n\n reports.push({ path, count: routes.length, routes: [...routes] });\n }\n\n return reports;\n }\n}\n","import type { RouteSummary } from \"./types\";\n\n/** Counts URLs actually written per declared route, across every shard of every group. */\nexport class RouteCounter {\n private readonly counts = new Map<string, number>();\n\n public record(route: string | undefined): void {\n if (route === undefined) return;\n\n this.counts.set(route, (this.counts.get(route) ?? 0) + 1);\n }\n\n public summary(): RouteSummary[] {\n return [...this.counts].map(([route, count]) => ({ route, count }));\n }\n}\n","import { join } from \"node:path\";\nimport { writeFile } from \"node:fs/promises\";\nimport { DuplicateSourceKeyError } from \"./errors\";\nimport {\n normalizeSitemapIndexOptions,\n type ResolvedSitemapIndexOptions,\n} from \"./sitemap-index-options\";\nimport { canonicalizeSourceKey } from \"./shard-name\";\nimport { publishAtomically } from \"./atomic-publish\";\nimport { writeShardGroup, type ShardFile, type ShardGroup } from \"./sitemap-shard-writer\";\nimport { buildSitemapIndexXml } from \"./sitemap-index-xml\";\nimport { DuplicatePathTracker } from \"./duplicate-path-tracker\";\nimport { RouteCounter } from \"./route-counter\";\nimport type {\n SitemapFileResult,\n SitemapIndexOptions,\n SitemapSetResult,\n SitemapSourceFactory,\n} from \"./sitemap-index-types\";\n\n/**\n * The streaming path for 300–500K URLs: shards, one flat master index, gzip,\n * and an atomic publish. It is a SECOND mode, not a bigger `Sitemap` — it\n * retains only the current shard's buffer and the set of paths it has seen.\n * There is no `entries()`, `toXML()` or `size`; anyone who can afford those\n * is in `Sitemap` and should be there instead.\n */\nexport class SitemapIndex {\n private readonly options: ResolvedSitemapIndexOptions;\n\n /** Every unnamed `addSource(factory)` call merges into this one group, sharing one shard counter. */\n private readonly unnamedFactories: SitemapSourceFactory[] = [];\n\n private readonly namedGroups: ShardGroup[] = [];\n\n /** Canonical (lowercased) keys already registered, so a case collision is caught at `addSource()`. */\n private readonly registeredKeys = new Set<string>();\n\n private readonly warnedImageRoutes = new Set<string>();\n\n public constructor(options: SitemapIndexOptions) {\n this.options = normalizeSitemapIndexOptions(options);\n }\n\n public addSource(source: SitemapSourceFactory): this;\n public addSource(key: string, source: SitemapSourceFactory): this;\n public addSource(\n keyOrSource: string | SitemapSourceFactory,\n maybeSource?: SitemapSourceFactory,\n ): this {\n if (typeof keyOrSource === \"string\") {\n const canonicalKey = canonicalizeSourceKey(keyOrSource);\n\n if (this.registeredKeys.has(canonicalKey)) {\n throw new DuplicateSourceKeyError(keyOrSource);\n }\n\n this.registeredKeys.add(canonicalKey);\n this.namedGroups.push({ key: keyOrSource, factories: [maybeSource as SitemapSourceFactory] });\n } else {\n this.unnamedFactories.push(keyOrSource);\n }\n\n return this;\n }\n\n /**\n * Walks every group one at a time — never all at once — into a sibling\n * temp directory, then publishes the whole set atomically. `files` is\n * reported in registration order: the unnamed group first (if any), then\n * named groups key-then-ordinal.\n */\n public async saveTo(outDir: string): Promise<SitemapSetResult> {\n const groups: ShardGroup[] = [\n ...(this.unnamedFactories.length > 0\n ? [{ key: undefined, factories: this.unnamedFactories }]\n : []),\n ...this.namedGroups,\n ];\n\n const duplicates = new DuplicatePathTracker();\n const routes = new RouteCounter();\n let shardFiles: ShardFile[] = [];\n\n const write = async (tempDir: string): Promise<readonly string[]> => {\n shardFiles = [];\n\n for (const group of groups) {\n const files = await writeShardGroup(group, {\n tempDir,\n baseUrl: this.options.baseUrl,\n filePrefix: this.options.filePrefix,\n gzip: this.options.gzip,\n maxUrlsPerFile: this.options.maxUrlsPerFile,\n maxBytesPerFile: this.options.maxBytesPerFile,\n defaults: {\n ...this.options.defaults,\n onImageLimitExceeded: (event) => this.reportImageLimit(event),\n },\n duplicates,\n routes,\n });\n\n shardFiles.push(...files);\n }\n\n const indexXml = buildSitemapIndexXml(\n shardFiles,\n this.options.baseUrl,\n this.options.shardPathPrefix,\n );\n\n await writeFile(join(tempDir, this.options.indexFileName), indexXml, \"utf8\");\n\n const writtenShardNames = shardFiles\n .filter((file) => file.urls > 0)\n .map((file) => file.fileName);\n\n return [...writtenShardNames, this.options.indexFileName];\n };\n\n await publishAtomically(outDir, write);\n\n const files: SitemapFileResult[] = shardFiles.map((file) => ({\n path: join(outDir, file.fileName),\n ...(file.key !== undefined ? { key: file.key } : {}),\n urls: file.urls,\n bytes: file.bytes,\n gzipped: file.gzipped,\n }));\n\n return {\n indexPath: join(outDir, this.options.indexFileName),\n files,\n totalUrls: files.reduce((total, file) => total + file.urls, 0),\n duplicates: duplicates.report(),\n routes: routes.summary(),\n };\n }\n\n private reportImageLimit(event: { readonly route?: string; readonly dropped: number }): void {\n const key = event.route ?? \"unattributed\";\n if (this.warnedImageRoutes.has(key)) return;\n\n this.warnedImageRoutes.add(key);\n if (this.options.defaults.onImageLimitExceeded)\n this.options.defaults.onImageLimitExceeded(event);\n else\n console.warn(\n `[warlock:sitemap] ${key} exceeded the 1,000-image limit; dropped ${event.dropped} image(s).`,\n );\n }\n}\n","/** Storage-independent schema and key helpers for immutable sitemap generations. */\nexport const GENERATION_MANIFEST_VERSION = 1 as const;\nexport const MANIFEST_FENCE_WIDTH = 16;\n\nexport type SitemapGenerationKind = \"single\" | \"index\";\n\nexport type SitemapGenerationFile = {\n readonly path: string;\n readonly bytes: number;\n readonly sha256: string;\n};\n\nexport type SitemapGenerationManifest = {\n readonly version: typeof GENERATION_MANIFEST_VERSION;\n readonly fence: number;\n readonly generationId: string;\n readonly coversRev: number;\n readonly kind: SitemapGenerationKind;\n readonly mainFile: string;\n readonly files: readonly SitemapGenerationFile[];\n readonly entries: number;\n readonly generatedAt: string;\n};\n\nexport type SitemapManifestKey = {\n readonly key: string;\n readonly fence: number;\n readonly generationId: string;\n};\n\nexport class InvalidSitemapGenerationManifestError extends Error {\n public constructor(message: string) {\n super(`Invalid sitemap generation manifest: ${message}`);\n this.name = \"InvalidSitemapGenerationManifestError\";\n }\n}\n\nconst SAFE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;\nconst SAFE_FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;\nconst SHA256 = /^[a-f0-9]{64}$/;\nconst ISO_DATE_TIME = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$/;\nconst MANIFEST_KEY = /^manifests\\/(\\d{16})-([A-Za-z0-9][A-Za-z0-9_-]{0,127})\\.json$/;\n\nfunction fail(message: string): never {\n throw new InvalidSitemapGenerationManifestError(message);\n}\n\nfunction object(value: unknown, label: string): Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value))\n return fail(`${label} must be an object.`);\n return value as Record<string, unknown>;\n}\n\nfunction nonNegativeInteger(value: unknown, label: string): number {\n if (!Number.isSafeInteger(value) || (value as number) < 0)\n return fail(`${label} must be a safe non-negative integer.`);\n return value as number;\n}\n\nfunction generationIdentifier(value: unknown, label: string): string {\n if (typeof value !== \"string\" || !SAFE_IDENTIFIER.test(value))\n return fail(`${label} must be a safe generation identifier.`);\n return value;\n}\n\nfunction generatedAt(value: unknown): string {\n if (typeof value !== \"string\" || !ISO_DATE_TIME.test(value) || Number.isNaN(Date.parse(value))) {\n return fail(\"generatedAt must be a valid ISO-8601 date-time.\");\n }\n return value;\n}\n\nfunction artifactPath(value: unknown, generationId: string, label: string): string {\n if (typeof value !== \"string\") return fail(`${label} must be a string.`);\n const prefix = `generations/${generationId}/`;\n if (!value.startsWith(prefix)) return fail(`${label} must be contained in ${prefix}.`);\n const fileName = value.slice(prefix.length);\n if (\n !SAFE_FILE_NAME.test(fileName) ||\n fileName.includes(\"/\") ||\n fileName === \".\" ||\n fileName === \"..\"\n ) {\n return fail(`${label} must name one safe, flat artifact file.`);\n }\n return value;\n}\n\nfunction manifestFile(value: unknown, generationId: string, index: number): SitemapGenerationFile {\n const record = object(value, `files[${index}]`);\n const path = artifactPath(record.path, generationId, `files[${index}].path`);\n const bytes = nonNegativeInteger(record.bytes, `files[${index}].bytes`);\n if (typeof record.sha256 !== \"string\" || !SHA256.test(record.sha256)) {\n return fail(`files[${index}].sha256 must be a lowercase SHA-256 hex digest.`);\n }\n return { path, bytes, sha256: record.sha256 };\n}\n\n/** Parse untrusted JSON into the v1 manifest contract. It does not touch storage. */\nexport function parseSitemapGenerationManifest(value: unknown): SitemapGenerationManifest {\n const record = object(value, \"manifest\");\n if (record.version !== GENERATION_MANIFEST_VERSION)\n return fail(`version must be ${GENERATION_MANIFEST_VERSION}.`);\n\n const fence = nonNegativeInteger(record.fence, \"fence\");\n const generationId = generationIdentifier(record.generationId, \"generationId\");\n const coversRev = nonNegativeInteger(record.coversRev, \"coversRev\");\n if (record.kind !== \"single\" && record.kind !== \"index\")\n return fail('kind must be \"single\" or \"index\".');\n if (!Array.isArray(record.files) || record.files.length === 0)\n return fail(\"files must be a non-empty array.\");\n\n const files = record.files.map((file, index) => manifestFile(file, generationId, index));\n const paths = new Set<string>();\n const fileNames = new Set<string>();\n for (const file of files) {\n const fileName = file.path.slice(`generations/${generationId}/`.length);\n if (paths.has(file.path))\n return fail(`files contains duplicate path ${JSON.stringify(file.path)}.`);\n if (fileNames.has(fileName))\n return fail(`files contains duplicate public file name ${JSON.stringify(fileName)}.`);\n paths.add(file.path);\n fileNames.add(fileName);\n }\n\n const mainFile = artifactPath(record.mainFile, generationId, \"mainFile\");\n if (!paths.has(mainFile)) return fail(\"mainFile must match one files[].path.\");\n\n return {\n version: GENERATION_MANIFEST_VERSION,\n fence,\n generationId,\n coversRev,\n kind: record.kind,\n mainFile,\n files,\n entries: nonNegativeInteger(record.entries, \"entries\"),\n generatedAt: generatedAt(record.generatedAt),\n };\n}\n\n/** Build the immutable v1 manifest object key for a generation fence. */\nexport function sitemapManifestKey(fence: number, generationId: string): string {\n const safeFence = nonNegativeInteger(fence, \"fence\");\n if (safeFence >= 10 ** MANIFEST_FENCE_WIDTH)\n return fail(`fence must fit within ${MANIFEST_FENCE_WIDTH} decimal digits.`);\n return `manifests/${String(safeFence).padStart(MANIFEST_FENCE_WIDTH, \"0\")}-${generationIdentifier(generationId, \"generationId\")}.json`;\n}\n/** Parse only the shape of a manifest object key; artifact validation belongs to the storage adapter. */\nexport function parseSitemapManifestKey(key: unknown): SitemapManifestKey | undefined {\n if (typeof key !== \"string\") return undefined;\n const match = MANIFEST_KEY.exec(key);\n if (!match) return undefined;\n const fence = Number(match[1]);\n const generationId = match[2];\n if (!Number.isSafeInteger(fence) || generationId === undefined) return undefined;\n return { key, fence, generationId };\n}\n\n/** Numeric-fence-descending candidates; callers validate storage contents before adoption. */\nexport function sortSitemapManifestCandidates(keys: readonly string[]): SitemapManifestKey[] {\n return keys\n .map(parseSitemapManifestKey)\n .filter((candidate): candidate is SitemapManifestKey => candidate !== undefined)\n .sort(\n (left, right) =>\n right.fence - left.fence || right.generationId.localeCompare(left.generationId),\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"generation-manifest.mjs","names":[],"sources":["../../../../../../sitemap/src/generation-manifest.ts"],"sourcesContent":["/** Storage-independent schema and key helpers for immutable sitemap generations. */\r\nexport const GENERATION_MANIFEST_VERSION = 1 as const;\r\nexport const MANIFEST_FENCE_WIDTH = 16;\r\n\r\nexport type SitemapGenerationKind = \"single\" | \"index\";\r\n\r\nexport type SitemapGenerationFile = {\r\n readonly path: string;\r\n readonly bytes: number;\r\n readonly sha256: string;\r\n};\r\n\r\nexport type SitemapGenerationManifest = {\r\n readonly version: typeof GENERATION_MANIFEST_VERSION;\r\n readonly fence: number;\r\n readonly generationId: string;\r\n readonly coversRev: number;\r\n readonly kind: SitemapGenerationKind;\r\n readonly mainFile: string;\r\n readonly files: readonly SitemapGenerationFile[];\r\n readonly entries: number;\r\n readonly generatedAt: string;\r\n};\r\n\r\nexport type SitemapManifestKey = {\r\n readonly key: string;\r\n readonly fence: number;\r\n readonly generationId: string;\r\n};\r\n\r\nexport class InvalidSitemapGenerationManifestError extends Error {\r\n public constructor(message: string) {\r\n super(`Invalid sitemap generation manifest: ${message}`);\r\n this.name = \"InvalidSitemapGenerationManifestError\";\r\n }\r\n}\r\n\r\nconst SAFE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;\r\nconst SAFE_FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;\r\nconst SHA256 = /^[a-f0-9]{64}$/;\r\nconst ISO_DATE_TIME = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$/;\r\nconst MANIFEST_KEY = /^manifests\\/(\\d{16})-([A-Za-z0-9][A-Za-z0-9_-]{0,127})\\.json$/;\r\n\r\nfunction fail(message: string): never {\r\n throw new InvalidSitemapGenerationManifestError(message);\r\n}\r\n\r\nfunction object(value: unknown, label: string): Record<string, unknown> {\r\n if (typeof value !== \"object\" || value === null || Array.isArray(value))\r\n return fail(`${label} must be an object.`);\r\n return value as Record<string, unknown>;\r\n}\r\n\r\nfunction nonNegativeInteger(value: unknown, label: string): number {\r\n if (!Number.isSafeInteger(value) || (value as number) < 0)\r\n return fail(`${label} must be a safe non-negative integer.`);\r\n return value as number;\r\n}\r\n\r\nfunction generationIdentifier(value: unknown, label: string): string {\r\n if (typeof value !== \"string\" || !SAFE_IDENTIFIER.test(value))\r\n return fail(`${label} must be a safe generation identifier.`);\r\n return value;\r\n}\r\n\r\nfunction generatedAt(value: unknown): string {\r\n if (typeof value !== \"string\" || !ISO_DATE_TIME.test(value) || Number.isNaN(Date.parse(value))) {\r\n return fail(\"generatedAt must be a valid ISO-8601 date-time.\");\r\n }\r\n return value;\r\n}\r\n\r\nfunction artifactPath(value: unknown, generationId: string, label: string): string {\r\n if (typeof value !== \"string\") return fail(`${label} must be a string.`);\r\n const prefix = `generations/${generationId}/`;\r\n if (!value.startsWith(prefix)) return fail(`${label} must be contained in ${prefix}.`);\r\n const fileName = value.slice(prefix.length);\r\n if (\r\n !SAFE_FILE_NAME.test(fileName) ||\r\n fileName.includes(\"/\") ||\r\n fileName === \".\" ||\r\n fileName === \"..\"\r\n ) {\r\n return fail(`${label} must name one safe, flat artifact file.`);\r\n }\r\n return value;\r\n}\r\n\r\nfunction manifestFile(value: unknown, generationId: string, index: number): SitemapGenerationFile {\r\n const record = object(value, `files[${index}]`);\r\n const path = artifactPath(record.path, generationId, `files[${index}].path`);\r\n const bytes = nonNegativeInteger(record.bytes, `files[${index}].bytes`);\r\n if (typeof record.sha256 !== \"string\" || !SHA256.test(record.sha256)) {\r\n return fail(`files[${index}].sha256 must be a lowercase SHA-256 hex digest.`);\r\n }\r\n return { path, bytes, sha256: record.sha256 };\r\n}\r\n\r\n/** Parse untrusted JSON into the v1 manifest contract. It does not touch storage. */\r\nexport function parseSitemapGenerationManifest(value: unknown): SitemapGenerationManifest {\r\n const record = object(value, \"manifest\");\r\n if (record.version !== GENERATION_MANIFEST_VERSION)\r\n return fail(`version must be ${GENERATION_MANIFEST_VERSION}.`);\r\n\r\n const fence = nonNegativeInteger(record.fence, \"fence\");\r\n const generationId = generationIdentifier(record.generationId, \"generationId\");\r\n const coversRev = nonNegativeInteger(record.coversRev, \"coversRev\");\r\n if (record.kind !== \"single\" && record.kind !== \"index\")\r\n return fail('kind must be \"single\" or \"index\".');\r\n if (!Array.isArray(record.files) || record.files.length === 0)\r\n return fail(\"files must be a non-empty array.\");\r\n\r\n const files = record.files.map((file, index) => manifestFile(file, generationId, index));\r\n const paths = new Set<string>();\r\n const fileNames = new Set<string>();\r\n for (const file of files) {\r\n const fileName = file.path.slice(`generations/${generationId}/`.length);\r\n if (paths.has(file.path))\r\n return fail(`files contains duplicate path ${JSON.stringify(file.path)}.`);\r\n if (fileNames.has(fileName))\r\n return fail(`files contains duplicate public file name ${JSON.stringify(fileName)}.`);\r\n paths.add(file.path);\r\n fileNames.add(fileName);\r\n }\r\n\r\n const mainFile = artifactPath(record.mainFile, generationId, \"mainFile\");\r\n if (!paths.has(mainFile)) return fail(\"mainFile must match one files[].path.\");\r\n\r\n return {\r\n version: GENERATION_MANIFEST_VERSION,\r\n fence,\r\n generationId,\r\n coversRev,\r\n kind: record.kind,\r\n mainFile,\r\n files,\r\n entries: nonNegativeInteger(record.entries, \"entries\"),\r\n generatedAt: generatedAt(record.generatedAt),\r\n };\r\n}\r\n\r\n/** Build the immutable v1 manifest object key for a generation fence. */\r\nexport function sitemapManifestKey(fence: number, generationId: string): string {\r\n const safeFence = nonNegativeInteger(fence, \"fence\");\r\n if (safeFence >= 10 ** MANIFEST_FENCE_WIDTH)\r\n return fail(`fence must fit within ${MANIFEST_FENCE_WIDTH} decimal digits.`);\r\n return `manifests/${String(safeFence).padStart(MANIFEST_FENCE_WIDTH, \"0\")}-${generationIdentifier(generationId, \"generationId\")}.json`;\r\n}\r\n/** Parse only the shape of a manifest object key; artifact validation belongs to the storage adapter. */\r\nexport function parseSitemapManifestKey(key: unknown): SitemapManifestKey | undefined {\r\n if (typeof key !== \"string\") return undefined;\r\n const match = MANIFEST_KEY.exec(key);\r\n if (!match) return undefined;\r\n const fence = Number(match[1]);\r\n const generationId = match[2];\r\n if (!Number.isSafeInteger(fence) || generationId === undefined) return undefined;\r\n return { key, fence, generationId };\r\n}\r\n\r\n/** Numeric-fence-descending candidates; callers validate storage contents before adoption. */\r\nexport function sortSitemapManifestCandidates(keys: readonly string[]): SitemapManifestKey[] {\r\n return keys\r\n .map(parseSitemapManifestKey)\r\n .filter((candidate): candidate is SitemapManifestKey => candidate !== undefined)\r\n .sort(\r\n (left, right) =>\r\n right.fence - left.fence || right.generationId.localeCompare(left.generationId),\r\n );\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"generation-manifest.mjs","names":[],"sources":["../../../../../../sitemap/src/generation-manifest.ts"],"sourcesContent":["/** Storage-independent schema and key helpers for immutable sitemap generations. */\nexport const GENERATION_MANIFEST_VERSION = 1 as const;\nexport const MANIFEST_FENCE_WIDTH = 16;\n\nexport type SitemapGenerationKind = \"single\" | \"index\";\n\nexport type SitemapGenerationFile = {\n readonly path: string;\n readonly bytes: number;\n readonly sha256: string;\n};\n\nexport type SitemapGenerationManifest = {\n readonly version: typeof GENERATION_MANIFEST_VERSION;\n readonly fence: number;\n readonly generationId: string;\n readonly coversRev: number;\n readonly kind: SitemapGenerationKind;\n readonly mainFile: string;\n readonly files: readonly SitemapGenerationFile[];\n readonly entries: number;\n readonly generatedAt: string;\n};\n\nexport type SitemapManifestKey = {\n readonly key: string;\n readonly fence: number;\n readonly generationId: string;\n};\n\nexport class InvalidSitemapGenerationManifestError extends Error {\n public constructor(message: string) {\n super(`Invalid sitemap generation manifest: ${message}`);\n this.name = \"InvalidSitemapGenerationManifestError\";\n }\n}\n\nconst SAFE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;\nconst SAFE_FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;\nconst SHA256 = /^[a-f0-9]{64}$/;\nconst ISO_DATE_TIME = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$/;\nconst MANIFEST_KEY = /^manifests\\/(\\d{16})-([A-Za-z0-9][A-Za-z0-9_-]{0,127})\\.json$/;\n\nfunction fail(message: string): never {\n throw new InvalidSitemapGenerationManifestError(message);\n}\n\nfunction object(value: unknown, label: string): Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value))\n return fail(`${label} must be an object.`);\n return value as Record<string, unknown>;\n}\n\nfunction nonNegativeInteger(value: unknown, label: string): number {\n if (!Number.isSafeInteger(value) || (value as number) < 0)\n return fail(`${label} must be a safe non-negative integer.`);\n return value as number;\n}\n\nfunction generationIdentifier(value: unknown, label: string): string {\n if (typeof value !== \"string\" || !SAFE_IDENTIFIER.test(value))\n return fail(`${label} must be a safe generation identifier.`);\n return value;\n}\n\nfunction generatedAt(value: unknown): string {\n if (typeof value !== \"string\" || !ISO_DATE_TIME.test(value) || Number.isNaN(Date.parse(value))) {\n return fail(\"generatedAt must be a valid ISO-8601 date-time.\");\n }\n return value;\n}\n\nfunction artifactPath(value: unknown, generationId: string, label: string): string {\n if (typeof value !== \"string\") return fail(`${label} must be a string.`);\n const prefix = `generations/${generationId}/`;\n if (!value.startsWith(prefix)) return fail(`${label} must be contained in ${prefix}.`);\n const fileName = value.slice(prefix.length);\n if (\n !SAFE_FILE_NAME.test(fileName) ||\n fileName.includes(\"/\") ||\n fileName === \".\" ||\n fileName === \"..\"\n ) {\n return fail(`${label} must name one safe, flat artifact file.`);\n }\n return value;\n}\n\nfunction manifestFile(value: unknown, generationId: string, index: number): SitemapGenerationFile {\n const record = object(value, `files[${index}]`);\n const path = artifactPath(record.path, generationId, `files[${index}].path`);\n const bytes = nonNegativeInteger(record.bytes, `files[${index}].bytes`);\n if (typeof record.sha256 !== \"string\" || !SHA256.test(record.sha256)) {\n return fail(`files[${index}].sha256 must be a lowercase SHA-256 hex digest.`);\n }\n return { path, bytes, sha256: record.sha256 };\n}\n\n/** Parse untrusted JSON into the v1 manifest contract. It does not touch storage. */\nexport function parseSitemapGenerationManifest(value: unknown): SitemapGenerationManifest {\n const record = object(value, \"manifest\");\n if (record.version !== GENERATION_MANIFEST_VERSION)\n return fail(`version must be ${GENERATION_MANIFEST_VERSION}.`);\n\n const fence = nonNegativeInteger(record.fence, \"fence\");\n const generationId = generationIdentifier(record.generationId, \"generationId\");\n const coversRev = nonNegativeInteger(record.coversRev, \"coversRev\");\n if (record.kind !== \"single\" && record.kind !== \"index\")\n return fail('kind must be \"single\" or \"index\".');\n if (!Array.isArray(record.files) || record.files.length === 0)\n return fail(\"files must be a non-empty array.\");\n\n const files = record.files.map((file, index) => manifestFile(file, generationId, index));\n const paths = new Set<string>();\n const fileNames = new Set<string>();\n for (const file of files) {\n const fileName = file.path.slice(`generations/${generationId}/`.length);\n if (paths.has(file.path))\n return fail(`files contains duplicate path ${JSON.stringify(file.path)}.`);\n if (fileNames.has(fileName))\n return fail(`files contains duplicate public file name ${JSON.stringify(fileName)}.`);\n paths.add(file.path);\n fileNames.add(fileName);\n }\n\n const mainFile = artifactPath(record.mainFile, generationId, \"mainFile\");\n if (!paths.has(mainFile)) return fail(\"mainFile must match one files[].path.\");\n\n return {\n version: GENERATION_MANIFEST_VERSION,\n fence,\n generationId,\n coversRev,\n kind: record.kind,\n mainFile,\n files,\n entries: nonNegativeInteger(record.entries, \"entries\"),\n generatedAt: generatedAt(record.generatedAt),\n };\n}\n\n/** Build the immutable v1 manifest object key for a generation fence. */\nexport function sitemapManifestKey(fence: number, generationId: string): string {\n const safeFence = nonNegativeInteger(fence, \"fence\");\n if (safeFence >= 10 ** MANIFEST_FENCE_WIDTH)\n return fail(`fence must fit within ${MANIFEST_FENCE_WIDTH} decimal digits.`);\n return `manifests/${String(safeFence).padStart(MANIFEST_FENCE_WIDTH, \"0\")}-${generationIdentifier(generationId, \"generationId\")}.json`;\n}\n/** Parse only the shape of a manifest object key; artifact validation belongs to the storage adapter. */\nexport function parseSitemapManifestKey(key: unknown): SitemapManifestKey | undefined {\n if (typeof key !== \"string\") return undefined;\n const match = MANIFEST_KEY.exec(key);\n if (!match) return undefined;\n const fence = Number(match[1]);\n const generationId = match[2];\n if (!Number.isSafeInteger(fence) || generationId === undefined) return undefined;\n return { key, fence, generationId };\n}\n\n/** Numeric-fence-descending candidates; callers validate storage contents before adoption. */\nexport function sortSitemapManifestCandidates(keys: readonly string[]): SitemapManifestKey[] {\n return keys\n .map(parseSitemapManifestKey)\n .filter((candidate): candidate is SitemapManifestKey => candidate !== undefined)\n .sort(\n (left, right) =>\n right.fence - left.fence || right.generationId.localeCompare(left.generationId),\n );\n}\n"],"mappings":"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"}
package/llms-full.txt CHANGED
@@ -6,141 +6,141 @@
6
6
 
7
7
  ## sitemap-overview `@warlock.js/sitemap/sitemap-overview/SKILL.md`
8
8
 
9
- ---
10
- name: sitemap-overview
11
- description: 'Front-door orientation for `@warlock.js/sitemap`: framework-blind `Sitemap` and streaming `SitemapIndex` builders, URL metadata, hreflang, Google image entries, sharding, diagnostics, and atomic publication. Use when adding or debugging sitemap output; Warlock page discovery and `/sitemap.xml` routing belong to `@warlock.js/web`.'
12
- ---
13
-
14
- # `@warlock.js/sitemap` — overview
15
-
16
- A sitemap builder that knows nothing about any framework. Entries in, a valid
17
- sitemaps.org document out. **Zero runtime dependencies.** Generation only; no
18
- remote sitemap parser.
19
-
20
- ## It does not import Warlock
21
-
22
- There is no connector, no config module, no lifecycle and no page registry in
23
- this package. It owns the sitemap PROTOCOL and every byte it emits, and
24
- nothing else. `@warlock.js/web` is a CALLER of it, exactly like an Express app
25
- or a cron script is — which is what makes the same three lines work everywhere.
26
-
27
- If you are looking for page discovery, `sitemap: false` exclusion, locale
28
- expansion, the `/sitemap.xml` route or `robots.txt`, those belong to
29
- `@warlock.js/web`, not here.
30
-
31
- ## The whole common case
32
-
33
- ```ts
34
- import { Sitemap } from "@warlock.js/sitemap";
35
-
36
- const sitemap = new Sitemap({
37
- baseUrl: "https://example.com",
38
- changefreq: "weekly",
39
- priority: 0.5,
40
- });
41
-
42
- sitemap.add({ path: "/" });
43
-
44
- declare const posts: { slug: string; updatedAt: Date }[];
45
-
46
- for (const post of posts) {
47
- sitemap.add({
48
- name: "post-details",
49
- route: "/posts/:id",
50
- path: `/posts/${post.slug}`,
51
- lastmod: post.updatedAt,
52
- priority: 0.8,
53
- });
54
- }
55
-
56
- const xml = sitemap.toXML();
57
-
58
- await sitemap.saveTo("public/sitemap.xml");
59
- ```
60
-
61
- ## `baseUrl` is validated in the CONSTRUCTOR
62
-
63
- A `Sitemap` that cannot produce a valid URL should not exist. Anything that is
64
- not an absolute `http(s)` URL throws `InvalidBaseUrlError` at construction, so
65
- a typo surfaces at the line that wrote it rather than at the first request.
66
- `new URL()` accepts `mailto:` and `file:` happily, so the protocol is checked
67
- explicitly.
68
-
69
- This replaces the 5.15.0 config key, its `PUBLIC_APP_URL` env fallback and its
70
- boot refusal entirely: the origin is an argument now, so it cannot be missing.
71
-
72
- ## The entry
73
-
74
- Only `path` is required, and it must be a **concrete path** — `/posts/123`,
75
- never `/posts/:id`. A pattern is not a URL.
76
-
77
- | Field | Meaning |
78
- | --- | --- |
79
- | `path` | Required. Resolved against `baseUrl`; an absolute URL is used as given. |
80
- | `name` | Optional route label, for your own diagnostics. Never serialised. |
81
- | `route` | Optional pattern this URL came from. Feeds `routes()` and `duplicates()`. |
82
- | `lastmod` | `Date` → W3C datetime. A string is passed through UNTOUCHED, never re-parsed. |
83
- | `changefreq` | One of the seven protocol values; anything else throws. |
84
- | `priority` | `0.0`–`1.0`; anything else throws. |
9
+ ---
10
+ name: sitemap-overview
11
+ description: 'Front-door orientation for `@warlock.js/sitemap`: framework-blind `Sitemap` and streaming `SitemapIndex` builders, URL metadata, hreflang, Google image entries, sharding, diagnostics, and atomic publication. Use when adding or debugging sitemap output; Warlock page discovery and `/sitemap.xml` routing belong to `@warlock.js/web`.'
12
+ ---
13
+
14
+ # `@warlock.js/sitemap` — overview
15
+
16
+ A sitemap builder that knows nothing about any framework. Entries in, a valid
17
+ sitemaps.org document out. **Zero runtime dependencies.** Generation only; no
18
+ remote sitemap parser.
19
+
20
+ ## It does not import Warlock
21
+
22
+ There is no connector, no config module, no lifecycle and no page registry in
23
+ this package. It owns the sitemap PROTOCOL and every byte it emits, and
24
+ nothing else. `@warlock.js/web` is a CALLER of it, exactly like an Express app
25
+ or a cron script is — which is what makes the same three lines work everywhere.
26
+
27
+ If you are looking for page discovery, `sitemap: false` exclusion, locale
28
+ expansion, the `/sitemap.xml` route or `robots.txt`, those belong to
29
+ `@warlock.js/web`, not here.
30
+
31
+ ## The whole common case
32
+
33
+ ```ts
34
+ import { Sitemap } from "@warlock.js/sitemap";
35
+
36
+ const sitemap = new Sitemap({
37
+ baseUrl: "https://example.com",
38
+ changefreq: "weekly",
39
+ priority: 0.5,
40
+ });
41
+
42
+ sitemap.add({ path: "/" });
43
+
44
+ declare const posts: { slug: string; updatedAt: Date }[];
45
+
46
+ for (const post of posts) {
47
+ sitemap.add({
48
+ name: "post-details",
49
+ route: "/posts/:id",
50
+ path: `/posts/${post.slug}`,
51
+ lastmod: post.updatedAt,
52
+ priority: 0.8,
53
+ });
54
+ }
55
+
56
+ const xml = sitemap.toXML();
57
+
58
+ await sitemap.saveTo("public/sitemap.xml");
59
+ ```
60
+
61
+ ## `baseUrl` is validated in the CONSTRUCTOR
62
+
63
+ A `Sitemap` that cannot produce a valid URL should not exist. Anything that is
64
+ not an absolute `http(s)` URL throws `InvalidBaseUrlError` at construction, so
65
+ a typo surfaces at the line that wrote it rather than at the first request.
66
+ `new URL()` accepts `mailto:` and `file:` happily, so the protocol is checked
67
+ explicitly.
68
+
69
+ This replaces the 5.15.0 config key, its `PUBLIC_APP_URL` env fallback and its
70
+ boot refusal entirely: the origin is an argument now, so it cannot be missing.
71
+
72
+ ## The entry
73
+
74
+ Only `path` is required, and it must be a **concrete path** — `/posts/123`,
75
+ never `/posts/:id`. A pattern is not a URL.
76
+
77
+ | Field | Meaning |
78
+ | --- | --- |
79
+ | `path` | Required. Resolved against `baseUrl`; an absolute URL is used as given. |
80
+ | `name` | Optional route label, for your own diagnostics. Never serialised. |
81
+ | `route` | Optional pattern this URL came from. Feeds `routes()` and `duplicates()`. |
82
+ | `lastmod` | `Date` → W3C datetime. A string is passed through UNTOUCHED, never re-parsed. |
83
+ | `changefreq` | One of the seven protocol values; anything else throws. |
84
+ | `priority` | `0.0`–`1.0`; anything else throws. |
85
85
  | `alternates` | `{ hreflang, path }[]` — language versions of this page. |
86
86
  | `images` | `{ loc }[]` — absolute HTTP(S) image URLs, including verified external CDNs. |
87
-
88
- Invalid entries throw `InvalidSitemapEntryError` from `add()`. Paths are
89
- normalised to carry a leading slash, so `a` and `/a` are ONE entry.
90
-
91
- ## Duplicates: silent, but not hidden
92
-
93
- Entries are stored keyed by `path`. Adding the same path twice keeps the later
94
- one, silently — a duplicate `<loc>` makes the document invalid, and two loops
95
- legitimately covering an overlapping set is the normal cause.
96
-
97
- `duplicates()` reports every collision with its count and the `route` of each
98
- contributing add, so a caller who cares can fail their own build:
99
-
100
- ```ts
101
- sitemap.duplicates();
102
- // [ { path: "/posts/1", count: 2, routes: ["/posts/:id", "/:slug"] } ]
103
- ```
104
-
105
- ## `routes()` and why `declareRoute()` exists
106
-
107
- ```ts
108
- sitemap.declareRoute("/products/:slug");
109
-
110
- sitemap.routes();
111
- // [ { route: "/posts/:id", count: 400 }, { route: "/products/:slug", count: 0 } ]
112
- ```
113
-
114
- **The zero is the interesting row.** It means a route you expected to
115
- contribute URLs contributed none, and a whole section of the site is missing
116
- from a document that otherwise looks perfect.
117
-
118
- Without `declareRoute()` a route only becomes known by appearing on an entry,
119
- so every route has at least one URL, `count: 0` can never occur and the
120
- diagnostic is decorative. Declaring the pattern is what turns a silent loss
121
- into a visible one.
122
-
123
- **This package reports; it never prints.** Whether a zero is a warning or a
124
- build failure is the caller's decision.
125
-
126
- ## Alternates
127
-
128
- `alternates` emits `<xhtml:link rel="alternate" hreflang="…">` inside each
129
- `<url>`. Two rules:
130
-
131
- 1. **Every language version is also its own `<url>`, carrying the complete
132
- alternate set including itself.** Listing alternates on only one of them is
133
- the usual way this ships broken.
134
- 2. **The package does not know what a locale is.** `hreflang` is any string —
135
- `en`, `en-GB`, `x-default` — and it never derives `/{locale}/…` for you.
136
- Each path is supplied explicitly, which is the only thing that works when
137
- slugs diverge between languages.
138
-
139
- The `xhtml` namespace is declared only when something uses it. `SitemapIndex`
140
- applies the same rule PER SHARD — a shard declares `xmlns:xhtml` only when its
141
- own entries carry alternates — and an alternate's `<xhtml:link>` bytes count
142
- toward that shard's byte ceiling exactly like the rest of its `<url>` block.
143
-
87
+
88
+ Invalid entries throw `InvalidSitemapEntryError` from `add()`. Paths are
89
+ normalised to carry a leading slash, so `a` and `/a` are ONE entry.
90
+
91
+ ## Duplicates: silent, but not hidden
92
+
93
+ Entries are stored keyed by `path`. Adding the same path twice keeps the later
94
+ one, silently — a duplicate `<loc>` makes the document invalid, and two loops
95
+ legitimately covering an overlapping set is the normal cause.
96
+
97
+ `duplicates()` reports every collision with its count and the `route` of each
98
+ contributing add, so a caller who cares can fail their own build:
99
+
100
+ ```ts
101
+ sitemap.duplicates();
102
+ // [ { path: "/posts/1", count: 2, routes: ["/posts/:id", "/:slug"] } ]
103
+ ```
104
+
105
+ ## `routes()` and why `declareRoute()` exists
106
+
107
+ ```ts
108
+ sitemap.declareRoute("/products/:slug");
109
+
110
+ sitemap.routes();
111
+ // [ { route: "/posts/:id", count: 400 }, { route: "/products/:slug", count: 0 } ]
112
+ ```
113
+
114
+ **The zero is the interesting row.** It means a route you expected to
115
+ contribute URLs contributed none, and a whole section of the site is missing
116
+ from a document that otherwise looks perfect.
117
+
118
+ Without `declareRoute()` a route only becomes known by appearing on an entry,
119
+ so every route has at least one URL, `count: 0` can never occur and the
120
+ diagnostic is decorative. Declaring the pattern is what turns a silent loss
121
+ into a visible one.
122
+
123
+ **This package reports; it never prints.** Whether a zero is a warning or a
124
+ build failure is the caller's decision.
125
+
126
+ ## Alternates
127
+
128
+ `alternates` emits `<xhtml:link rel="alternate" hreflang="…">` inside each
129
+ `<url>`. Two rules:
130
+
131
+ 1. **Every language version is also its own `<url>`, carrying the complete
132
+ alternate set including itself.** Listing alternates on only one of them is
133
+ the usual way this ships broken.
134
+ 2. **The package does not know what a locale is.** `hreflang` is any string —
135
+ `en`, `en-GB`, `x-default` — and it never derives `/{locale}/…` for you.
136
+ Each path is supplied explicitly, which is the only thing that works when
137
+ slugs diverge between languages.
138
+
139
+ The `xhtml` namespace is declared only when something uses it. `SitemapIndex`
140
+ applies the same rule PER SHARD — a shard declares `xmlns:xhtml` only when its
141
+ own entries carry alternates — and an alternate's `<xhtml:link>` bytes count
142
+ toward that shard's byte ceiling exactly like the rest of its `<url>` block.
143
+
144
144
  ## Images
145
145
 
146
146
  Use `images` when a page has image assets to expose:
@@ -165,18 +165,18 @@ the one diagnostic for that route; without it, the builder issues one
165
165
  ceiling.
166
166
 
167
167
  ## `toXML()` is pure, repeatable and synchronous
168
-
169
- Calling it twice returns the same string; it mutates nothing and drains
170
- nothing. It has no I/O and stays synchronous.
171
-
172
- ## The ceiling
173
-
174
- The protocol caps one file at **50,000 URLs or 50MB uncompressed**. `Sitemap`
175
- retains every entry — that is what makes `entries()` and a repeatable
176
- `toXML()` possible — so it is right up to that ceiling and wrong above it. A
177
- site past it needs the streaming writer, which retains nothing and emits shards
178
- plus an index.
179
-
168
+
169
+ Calling it twice returns the same string; it mutates nothing and drains
170
+ nothing. It has no I/O and stays synchronous.
171
+
172
+ ## The ceiling
173
+
174
+ The protocol caps one file at **50,000 URLs or 50MB uncompressed**. `Sitemap`
175
+ retains every entry — that is what makes `entries()` and a repeatable
176
+ `toXML()` possible — so it is right up to that ceiling and wrong above it. A
177
+ site past it needs the streaming writer, which retains nothing and emits shards
178
+ plus an index.
179
+
180
180
  `maxBytesPerFile` is enforced at shard boundaries, not within an entry: a
181
181
  single entry too large for the ceiling is written alone in its own shard
182
182
  rather than split — an entry can't be split.
@@ -184,27 +184,27 @@ rather than split — an entry can't be split.
184
184
  For `SitemapIndex`, `shardPathPrefix` changes the URL directory used for shard
185
185
  links in the index without changing where `saveTo(outDir)` writes those files.
186
186
  Use it when a proxy serves generated shards from a different public path.
187
-
188
- ## `SitemapIndex.saveTo(outDir)` owns the whole directory
189
-
190
- It publishes atomically by swapping `outDir` for a freshly written temp
191
- directory in one `rename` — which means `outDir` must be a directory
192
- dedicated to this sitemap set, never an app's `public/` or anything else
193
- something else writes to. Every publish marks `outDir` with
194
- `.sitemap-set.json`; a later call only swaps a directory that is absent,
195
- empty, or already carries that marker. A non-empty, unmarked `outDir` gets
196
- `UnownedOutputDirectoryError` instead, and is left completely untouched.
197
-
198
- ## Errors
199
-
200
- | Error | When |
201
- | --- | --- |
202
- | `InvalidBaseUrlError` | `baseUrl` missing, relative, or not `http(s)`. From the constructor. |
203
- | `InvalidSitemapEntryError` | No path, priority outside `0.0`–`1.0`, unknown `changefreq`, invalid `Date`, alternate with no `hreflang`. From `add()`. |
204
- | `UnownedOutputDirectoryError` | `SitemapIndex.saveTo(outDir)`: `outDir` is non-empty with no `.sitemap-set.json` marker. From `saveTo()`, before anything is written. |
205
-
206
- ## Also exported
207
-
187
+
188
+ ## `SitemapIndex.saveTo(outDir)` owns the whole directory
189
+
190
+ It publishes atomically by swapping `outDir` for a freshly written temp
191
+ directory in one `rename` — which means `outDir` must be a directory
192
+ dedicated to this sitemap set, never an app's `public/` or anything else
193
+ something else writes to. Every publish marks `outDir` with
194
+ `.sitemap-set.json`; a later call only swaps a directory that is absent,
195
+ empty, or already carries that marker. A non-empty, unmarked `outDir` gets
196
+ `UnownedOutputDirectoryError` instead, and is left completely untouched.
197
+
198
+ ## Errors
199
+
200
+ | Error | When |
201
+ | --- | --- |
202
+ | `InvalidBaseUrlError` | `baseUrl` missing, relative, or not `http(s)`. From the constructor. |
203
+ | `InvalidSitemapEntryError` | No path, priority outside `0.0`–`1.0`, unknown `changefreq`, invalid `Date`, alternate with no `hreflang`. From `add()`. |
204
+ | `UnownedOutputDirectoryError` | `SitemapIndex.saveTo(outDir)`: `outDir` is non-empty with no `.sitemap-set.json` marker. From `saveTo()`, before anything is written. |
205
+
206
+ ## Also exported
207
+
208
208
  `buildSitemapXml(entries, baseUrl)`, `escapeXml(value)`, `joinOrigin(origin,
209
209
  path)`, `SitemapImage`, and `SitemapImageLimitExceeded` — the pieces the class
210
210
  is built from, for a caller who wants the serialiser or image diagnostics
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/warlockjs/sitemap"
10
10
  },
11
- "version": "5.24.0",
11
+ "version": "5.26.0",
12
12
  "main": "./cjs/index.cjs",
13
13
  "module": "./esm/index.mjs",
14
14
  "types": "./esm/index.d.mts",
@@ -1,138 +1,138 @@
1
- ---
2
- name: sitemap-overview
3
- description: 'Front-door orientation for `@warlock.js/sitemap`: framework-blind `Sitemap` and streaming `SitemapIndex` builders, URL metadata, hreflang, Google image entries, sharding, diagnostics, and atomic publication. Use when adding or debugging sitemap output; Warlock page discovery and `/sitemap.xml` routing belong to `@warlock.js/web`.'
4
- ---
5
-
6
- # `@warlock.js/sitemap` — overview
7
-
8
- A sitemap builder that knows nothing about any framework. Entries in, a valid
9
- sitemaps.org document out. **Zero runtime dependencies.** Generation only; no
10
- remote sitemap parser.
11
-
12
- ## It does not import Warlock
13
-
14
- There is no connector, no config module, no lifecycle and no page registry in
15
- this package. It owns the sitemap PROTOCOL and every byte it emits, and
16
- nothing else. `@warlock.js/web` is a CALLER of it, exactly like an Express app
17
- or a cron script is — which is what makes the same three lines work everywhere.
18
-
19
- If you are looking for page discovery, `sitemap: false` exclusion, locale
20
- expansion, the `/sitemap.xml` route or `robots.txt`, those belong to
21
- `@warlock.js/web`, not here.
22
-
23
- ## The whole common case
24
-
25
- ```ts
26
- import { Sitemap } from "@warlock.js/sitemap";
27
-
28
- const sitemap = new Sitemap({
29
- baseUrl: "https://example.com",
30
- changefreq: "weekly",
31
- priority: 0.5,
32
- });
33
-
34
- sitemap.add({ path: "/" });
35
-
36
- declare const posts: { slug: string; updatedAt: Date }[];
37
-
38
- for (const post of posts) {
39
- sitemap.add({
40
- name: "post-details",
41
- route: "/posts/:id",
42
- path: `/posts/${post.slug}`,
43
- lastmod: post.updatedAt,
44
- priority: 0.8,
45
- });
46
- }
47
-
48
- const xml = sitemap.toXML();
49
-
50
- await sitemap.saveTo("public/sitemap.xml");
51
- ```
52
-
53
- ## `baseUrl` is validated in the CONSTRUCTOR
54
-
55
- A `Sitemap` that cannot produce a valid URL should not exist. Anything that is
56
- not an absolute `http(s)` URL throws `InvalidBaseUrlError` at construction, so
57
- a typo surfaces at the line that wrote it rather than at the first request.
58
- `new URL()` accepts `mailto:` and `file:` happily, so the protocol is checked
59
- explicitly.
60
-
61
- This replaces the 5.15.0 config key, its `PUBLIC_APP_URL` env fallback and its
62
- boot refusal entirely: the origin is an argument now, so it cannot be missing.
63
-
64
- ## The entry
65
-
66
- Only `path` is required, and it must be a **concrete path** — `/posts/123`,
67
- never `/posts/:id`. A pattern is not a URL.
68
-
69
- | Field | Meaning |
70
- | --- | --- |
71
- | `path` | Required. Resolved against `baseUrl`; an absolute URL is used as given. |
72
- | `name` | Optional route label, for your own diagnostics. Never serialised. |
73
- | `route` | Optional pattern this URL came from. Feeds `routes()` and `duplicates()`. |
74
- | `lastmod` | `Date` → W3C datetime. A string is passed through UNTOUCHED, never re-parsed. |
75
- | `changefreq` | One of the seven protocol values; anything else throws. |
76
- | `priority` | `0.0`–`1.0`; anything else throws. |
1
+ ---
2
+ name: sitemap-overview
3
+ description: 'Front-door orientation for `@warlock.js/sitemap`: framework-blind `Sitemap` and streaming `SitemapIndex` builders, URL metadata, hreflang, Google image entries, sharding, diagnostics, and atomic publication. Use when adding or debugging sitemap output; Warlock page discovery and `/sitemap.xml` routing belong to `@warlock.js/web`.'
4
+ ---
5
+
6
+ # `@warlock.js/sitemap` — overview
7
+
8
+ A sitemap builder that knows nothing about any framework. Entries in, a valid
9
+ sitemaps.org document out. **Zero runtime dependencies.** Generation only; no
10
+ remote sitemap parser.
11
+
12
+ ## It does not import Warlock
13
+
14
+ There is no connector, no config module, no lifecycle and no page registry in
15
+ this package. It owns the sitemap PROTOCOL and every byte it emits, and
16
+ nothing else. `@warlock.js/web` is a CALLER of it, exactly like an Express app
17
+ or a cron script is — which is what makes the same three lines work everywhere.
18
+
19
+ If you are looking for page discovery, `sitemap: false` exclusion, locale
20
+ expansion, the `/sitemap.xml` route or `robots.txt`, those belong to
21
+ `@warlock.js/web`, not here.
22
+
23
+ ## The whole common case
24
+
25
+ ```ts
26
+ import { Sitemap } from "@warlock.js/sitemap";
27
+
28
+ const sitemap = new Sitemap({
29
+ baseUrl: "https://example.com",
30
+ changefreq: "weekly",
31
+ priority: 0.5,
32
+ });
33
+
34
+ sitemap.add({ path: "/" });
35
+
36
+ declare const posts: { slug: string; updatedAt: Date }[];
37
+
38
+ for (const post of posts) {
39
+ sitemap.add({
40
+ name: "post-details",
41
+ route: "/posts/:id",
42
+ path: `/posts/${post.slug}`,
43
+ lastmod: post.updatedAt,
44
+ priority: 0.8,
45
+ });
46
+ }
47
+
48
+ const xml = sitemap.toXML();
49
+
50
+ await sitemap.saveTo("public/sitemap.xml");
51
+ ```
52
+
53
+ ## `baseUrl` is validated in the CONSTRUCTOR
54
+
55
+ A `Sitemap` that cannot produce a valid URL should not exist. Anything that is
56
+ not an absolute `http(s)` URL throws `InvalidBaseUrlError` at construction, so
57
+ a typo surfaces at the line that wrote it rather than at the first request.
58
+ `new URL()` accepts `mailto:` and `file:` happily, so the protocol is checked
59
+ explicitly.
60
+
61
+ This replaces the 5.15.0 config key, its `PUBLIC_APP_URL` env fallback and its
62
+ boot refusal entirely: the origin is an argument now, so it cannot be missing.
63
+
64
+ ## The entry
65
+
66
+ Only `path` is required, and it must be a **concrete path** — `/posts/123`,
67
+ never `/posts/:id`. A pattern is not a URL.
68
+
69
+ | Field | Meaning |
70
+ | --- | --- |
71
+ | `path` | Required. Resolved against `baseUrl`; an absolute URL is used as given. |
72
+ | `name` | Optional route label, for your own diagnostics. Never serialised. |
73
+ | `route` | Optional pattern this URL came from. Feeds `routes()` and `duplicates()`. |
74
+ | `lastmod` | `Date` → W3C datetime. A string is passed through UNTOUCHED, never re-parsed. |
75
+ | `changefreq` | One of the seven protocol values; anything else throws. |
76
+ | `priority` | `0.0`–`1.0`; anything else throws. |
77
77
  | `alternates` | `{ hreflang, path }[]` — language versions of this page. |
78
78
  | `images` | `{ loc }[]` — absolute HTTP(S) image URLs, including verified external CDNs. |
79
-
80
- Invalid entries throw `InvalidSitemapEntryError` from `add()`. Paths are
81
- normalised to carry a leading slash, so `a` and `/a` are ONE entry.
82
-
83
- ## Duplicates: silent, but not hidden
84
-
85
- Entries are stored keyed by `path`. Adding the same path twice keeps the later
86
- one, silently — a duplicate `<loc>` makes the document invalid, and two loops
87
- legitimately covering an overlapping set is the normal cause.
88
-
89
- `duplicates()` reports every collision with its count and the `route` of each
90
- contributing add, so a caller who cares can fail their own build:
91
-
92
- ```ts
93
- sitemap.duplicates();
94
- // [ { path: "/posts/1", count: 2, routes: ["/posts/:id", "/:slug"] } ]
95
- ```
96
-
97
- ## `routes()` and why `declareRoute()` exists
98
-
99
- ```ts
100
- sitemap.declareRoute("/products/:slug");
101
-
102
- sitemap.routes();
103
- // [ { route: "/posts/:id", count: 400 }, { route: "/products/:slug", count: 0 } ]
104
- ```
105
-
106
- **The zero is the interesting row.** It means a route you expected to
107
- contribute URLs contributed none, and a whole section of the site is missing
108
- from a document that otherwise looks perfect.
109
-
110
- Without `declareRoute()` a route only becomes known by appearing on an entry,
111
- so every route has at least one URL, `count: 0` can never occur and the
112
- diagnostic is decorative. Declaring the pattern is what turns a silent loss
113
- into a visible one.
114
-
115
- **This package reports; it never prints.** Whether a zero is a warning or a
116
- build failure is the caller's decision.
117
-
118
- ## Alternates
119
-
120
- `alternates` emits `<xhtml:link rel="alternate" hreflang="…">` inside each
121
- `<url>`. Two rules:
122
-
123
- 1. **Every language version is also its own `<url>`, carrying the complete
124
- alternate set including itself.** Listing alternates on only one of them is
125
- the usual way this ships broken.
126
- 2. **The package does not know what a locale is.** `hreflang` is any string —
127
- `en`, `en-GB`, `x-default` — and it never derives `/{locale}/…` for you.
128
- Each path is supplied explicitly, which is the only thing that works when
129
- slugs diverge between languages.
130
-
131
- The `xhtml` namespace is declared only when something uses it. `SitemapIndex`
132
- applies the same rule PER SHARD — a shard declares `xmlns:xhtml` only when its
133
- own entries carry alternates — and an alternate's `<xhtml:link>` bytes count
134
- toward that shard's byte ceiling exactly like the rest of its `<url>` block.
135
-
79
+
80
+ Invalid entries throw `InvalidSitemapEntryError` from `add()`. Paths are
81
+ normalised to carry a leading slash, so `a` and `/a` are ONE entry.
82
+
83
+ ## Duplicates: silent, but not hidden
84
+
85
+ Entries are stored keyed by `path`. Adding the same path twice keeps the later
86
+ one, silently — a duplicate `<loc>` makes the document invalid, and two loops
87
+ legitimately covering an overlapping set is the normal cause.
88
+
89
+ `duplicates()` reports every collision with its count and the `route` of each
90
+ contributing add, so a caller who cares can fail their own build:
91
+
92
+ ```ts
93
+ sitemap.duplicates();
94
+ // [ { path: "/posts/1", count: 2, routes: ["/posts/:id", "/:slug"] } ]
95
+ ```
96
+
97
+ ## `routes()` and why `declareRoute()` exists
98
+
99
+ ```ts
100
+ sitemap.declareRoute("/products/:slug");
101
+
102
+ sitemap.routes();
103
+ // [ { route: "/posts/:id", count: 400 }, { route: "/products/:slug", count: 0 } ]
104
+ ```
105
+
106
+ **The zero is the interesting row.** It means a route you expected to
107
+ contribute URLs contributed none, and a whole section of the site is missing
108
+ from a document that otherwise looks perfect.
109
+
110
+ Without `declareRoute()` a route only becomes known by appearing on an entry,
111
+ so every route has at least one URL, `count: 0` can never occur and the
112
+ diagnostic is decorative. Declaring the pattern is what turns a silent loss
113
+ into a visible one.
114
+
115
+ **This package reports; it never prints.** Whether a zero is a warning or a
116
+ build failure is the caller's decision.
117
+
118
+ ## Alternates
119
+
120
+ `alternates` emits `<xhtml:link rel="alternate" hreflang="…">` inside each
121
+ `<url>`. Two rules:
122
+
123
+ 1. **Every language version is also its own `<url>`, carrying the complete
124
+ alternate set including itself.** Listing alternates on only one of them is
125
+ the usual way this ships broken.
126
+ 2. **The package does not know what a locale is.** `hreflang` is any string —
127
+ `en`, `en-GB`, `x-default` — and it never derives `/{locale}/…` for you.
128
+ Each path is supplied explicitly, which is the only thing that works when
129
+ slugs diverge between languages.
130
+
131
+ The `xhtml` namespace is declared only when something uses it. `SitemapIndex`
132
+ applies the same rule PER SHARD — a shard declares `xmlns:xhtml` only when its
133
+ own entries carry alternates — and an alternate's `<xhtml:link>` bytes count
134
+ toward that shard's byte ceiling exactly like the rest of its `<url>` block.
135
+
136
136
  ## Images
137
137
 
138
138
  Use `images` when a page has image assets to expose:
@@ -157,18 +157,18 @@ the one diagnostic for that route; without it, the builder issues one
157
157
  ceiling.
158
158
 
159
159
  ## `toXML()` is pure, repeatable and synchronous
160
-
161
- Calling it twice returns the same string; it mutates nothing and drains
162
- nothing. It has no I/O and stays synchronous.
163
-
164
- ## The ceiling
165
-
166
- The protocol caps one file at **50,000 URLs or 50MB uncompressed**. `Sitemap`
167
- retains every entry — that is what makes `entries()` and a repeatable
168
- `toXML()` possible — so it is right up to that ceiling and wrong above it. A
169
- site past it needs the streaming writer, which retains nothing and emits shards
170
- plus an index.
171
-
160
+
161
+ Calling it twice returns the same string; it mutates nothing and drains
162
+ nothing. It has no I/O and stays synchronous.
163
+
164
+ ## The ceiling
165
+
166
+ The protocol caps one file at **50,000 URLs or 50MB uncompressed**. `Sitemap`
167
+ retains every entry — that is what makes `entries()` and a repeatable
168
+ `toXML()` possible — so it is right up to that ceiling and wrong above it. A
169
+ site past it needs the streaming writer, which retains nothing and emits shards
170
+ plus an index.
171
+
172
172
  `maxBytesPerFile` is enforced at shard boundaries, not within an entry: a
173
173
  single entry too large for the ceiling is written alone in its own shard
174
174
  rather than split — an entry can't be split.
@@ -176,27 +176,27 @@ rather than split — an entry can't be split.
176
176
  For `SitemapIndex`, `shardPathPrefix` changes the URL directory used for shard
177
177
  links in the index without changing where `saveTo(outDir)` writes those files.
178
178
  Use it when a proxy serves generated shards from a different public path.
179
-
180
- ## `SitemapIndex.saveTo(outDir)` owns the whole directory
181
-
182
- It publishes atomically by swapping `outDir` for a freshly written temp
183
- directory in one `rename` — which means `outDir` must be a directory
184
- dedicated to this sitemap set, never an app's `public/` or anything else
185
- something else writes to. Every publish marks `outDir` with
186
- `.sitemap-set.json`; a later call only swaps a directory that is absent,
187
- empty, or already carries that marker. A non-empty, unmarked `outDir` gets
188
- `UnownedOutputDirectoryError` instead, and is left completely untouched.
189
-
190
- ## Errors
191
-
192
- | Error | When |
193
- | --- | --- |
194
- | `InvalidBaseUrlError` | `baseUrl` missing, relative, or not `http(s)`. From the constructor. |
195
- | `InvalidSitemapEntryError` | No path, priority outside `0.0`–`1.0`, unknown `changefreq`, invalid `Date`, alternate with no `hreflang`. From `add()`. |
196
- | `UnownedOutputDirectoryError` | `SitemapIndex.saveTo(outDir)`: `outDir` is non-empty with no `.sitemap-set.json` marker. From `saveTo()`, before anything is written. |
197
-
198
- ## Also exported
199
-
179
+
180
+ ## `SitemapIndex.saveTo(outDir)` owns the whole directory
181
+
182
+ It publishes atomically by swapping `outDir` for a freshly written temp
183
+ directory in one `rename` — which means `outDir` must be a directory
184
+ dedicated to this sitemap set, never an app's `public/` or anything else
185
+ something else writes to. Every publish marks `outDir` with
186
+ `.sitemap-set.json`; a later call only swaps a directory that is absent,
187
+ empty, or already carries that marker. A non-empty, unmarked `outDir` gets
188
+ `UnownedOutputDirectoryError` instead, and is left completely untouched.
189
+
190
+ ## Errors
191
+
192
+ | Error | When |
193
+ | --- | --- |
194
+ | `InvalidBaseUrlError` | `baseUrl` missing, relative, or not `http(s)`. From the constructor. |
195
+ | `InvalidSitemapEntryError` | No path, priority outside `0.0`–`1.0`, unknown `changefreq`, invalid `Date`, alternate with no `hreflang`. From `add()`. |
196
+ | `UnownedOutputDirectoryError` | `SitemapIndex.saveTo(outDir)`: `outDir` is non-empty with no `.sitemap-set.json` marker. From `saveTo()`, before anything is written. |
197
+
198
+ ## Also exported
199
+
200
200
  `buildSitemapXml(entries, baseUrl)`, `escapeXml(value)`, `joinOrigin(origin,
201
201
  path)`, `SitemapImage`, and `SitemapImageLimitExceeded` — the pieces the class
202
202
  is built from, for a caller who wants the serialiser or image diagnostics