@warlock.js/sitemap 5.29.0 → 5.30.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 +6 -0
- package/cjs/index.cjs +16 -16
- package/cjs/index.cjs.map +1 -1
- package/esm/atomic-publish.mjs +1 -1
- package/esm/atomic-publish.mjs.map +1 -1
- package/esm/atomic-write-file.mjs +1 -1
- package/esm/atomic-write-file.mjs.map +1 -1
- package/esm/duplicate-path-tracker.mjs +1 -1
- package/esm/duplicate-path-tracker.mjs.map +1 -1
- package/esm/errors.d.mts +1 -1
- package/esm/errors.mjs +1 -1
- package/esm/errors.mjs.map +1 -1
- package/esm/generation-manifest.d.mts +1 -1
- package/esm/generation-manifest.mjs +1 -1
- package/esm/generation-manifest.mjs.map +1 -1
- package/esm/lastmod.mjs +1 -1
- package/esm/lastmod.mjs.map +1 -1
- package/esm/normalize-entry.mjs +1 -1
- package/esm/normalize-entry.mjs.map +1 -1
- package/esm/route-counter.mjs +1 -1
- package/esm/route-counter.mjs.map +1 -1
- package/esm/shard-name.mjs +1 -1
- package/esm/shard-name.mjs.map +1 -1
- package/esm/sitemap-index-options.mjs +1 -1
- package/esm/sitemap-index-options.mjs.map +1 -1
- package/esm/sitemap-index-types.d.mts +1 -1
- package/esm/sitemap-index-xml.mjs +1 -1
- package/esm/sitemap-index-xml.mjs.map +1 -1
- package/esm/sitemap-index.d.mts +1 -1
- package/esm/sitemap-index.mjs +1 -1
- package/esm/sitemap-index.mjs.map +1 -1
- package/esm/sitemap-shard-writer.mjs +1 -1
- package/esm/sitemap-shard-writer.mjs.map +1 -1
- package/esm/sitemap.d.mts +1 -1
- package/esm/sitemap.mjs +1 -1
- package/esm/sitemap.mjs.map +1 -1
- package/esm/types.d.mts +1 -1
- package/esm/url.d.mts +1 -1
- package/esm/url.mjs +1 -1
- package/esm/url.mjs.map +1 -1
- package/esm/xml.d.mts +1 -1
- package/esm/xml.mjs +1 -1
- package/esm/xml.mjs.map +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ 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.30.0 - 2026-10-16
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Lockstep release; package APIs are unchanged.
|
|
12
|
+
|
|
7
13
|
## 5.29.0 - 2026-10-01
|
|
8
14
|
|
|
9
15
|
### Changed
|
package/cjs/index.cjs
CHANGED
|
@@ -3,7 +3,7 @@ let node_fs_promises = require("node:fs/promises");
|
|
|
3
3
|
let node_path = require("node:path");
|
|
4
4
|
let node_zlib = require("node:zlib");
|
|
5
5
|
|
|
6
|
-
//#region
|
|
6
|
+
//#region ../../packages/sitemap/src/errors.ts
|
|
7
7
|
/**
|
|
8
8
|
* The `baseUrl` given to a `Sitemap` is not an absolute http(s) URL.
|
|
9
9
|
*
|
|
@@ -53,7 +53,7 @@ var UnownedOutputDirectoryError = class extends Error {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
//#endregion
|
|
56
|
-
//#region
|
|
56
|
+
//#region ../../packages/sitemap/src/atomic-publish.ts
|
|
57
57
|
/**
|
|
58
58
|
* Marks a directory as one `publishAtomically` swapped into place. Its
|
|
59
59
|
* presence is the ONLY thing that lets a later publish treat the directory
|
|
@@ -132,7 +132,7 @@ async function publishAtomically(outDir, write) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
//#endregion
|
|
135
|
-
//#region
|
|
135
|
+
//#region ../../packages/sitemap/src/atomic-write-file.ts
|
|
136
136
|
const defaultDeps = {
|
|
137
137
|
writeFile: node_fs_promises.writeFile,
|
|
138
138
|
rename: node_fs_promises.rename,
|
|
@@ -177,7 +177,7 @@ async function atomicWriteFile(filePath, content, deps = defaultDeps) {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
//#endregion
|
|
180
|
-
//#region
|
|
180
|
+
//#region ../../packages/sitemap/src/lastmod.ts
|
|
181
181
|
/**
|
|
182
182
|
* Serialises a `lastmod` value.
|
|
183
183
|
*
|
|
@@ -196,7 +196,7 @@ function formatLastmod(value) {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
//#endregion
|
|
199
|
-
//#region
|
|
199
|
+
//#region ../../packages/sitemap/src/normalize-entry.ts
|
|
200
200
|
const CHANGE_FREQS = [
|
|
201
201
|
"always",
|
|
202
202
|
"hourly",
|
|
@@ -268,7 +268,7 @@ function normalizeEntry(entry, defaults) {
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
//#endregion
|
|
271
|
-
//#region
|
|
271
|
+
//#region ../../packages/sitemap/src/url.ts
|
|
272
272
|
/**
|
|
273
273
|
* Joins a configured origin and an app-relative route path into one absolute
|
|
274
274
|
* URL, with exactly one slash at the seam regardless of whether either side
|
|
@@ -306,7 +306,7 @@ function resolveAgainstBase(baseUrl, path) {
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
//#endregion
|
|
309
|
-
//#region
|
|
309
|
+
//#region ../../packages/sitemap/src/xml.ts
|
|
310
310
|
const XML_ESCAPES = {
|
|
311
311
|
"&": "&",
|
|
312
312
|
"<": "<",
|
|
@@ -369,7 +369,7 @@ function buildSitemapXml(entries, baseUrl) {
|
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
//#endregion
|
|
372
|
-
//#region
|
|
372
|
+
//#region ../../packages/sitemap/src/sitemap.ts
|
|
373
373
|
/**
|
|
374
374
|
* A bounded sitemap builder: it RETAINS every entry, which is what makes
|
|
375
375
|
* `entries()` and a repeatable `toXML()` possible, and is exactly right up to
|
|
@@ -499,7 +499,7 @@ var Sitemap = class {
|
|
|
499
499
|
};
|
|
500
500
|
|
|
501
501
|
//#endregion
|
|
502
|
-
//#region
|
|
502
|
+
//#region ../../packages/sitemap/src/sitemap-index-options.ts
|
|
503
503
|
/** The sitemaps.org limits. Never clamped to — a silently clamped option is a lie about what was written. */
|
|
504
504
|
const PROTOCOL_MAX_URLS_PER_FILE = 5e4;
|
|
505
505
|
const PROTOCOL_MAX_BYTES_PER_FILE = 50 * 1024 * 1024;
|
|
@@ -535,7 +535,7 @@ function normalizeShardPathPrefix(value) {
|
|
|
535
535
|
}
|
|
536
536
|
|
|
537
537
|
//#endregion
|
|
538
|
-
//#region
|
|
538
|
+
//#region ../../packages/sitemap/src/shard-name.ts
|
|
539
539
|
const SAFE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
540
540
|
/**
|
|
541
541
|
* Canonical form used to detect a case collision before it reaches the
|
|
@@ -561,7 +561,7 @@ function shardFileName(filePrefix, key, ordinal, gzip) {
|
|
|
561
561
|
}
|
|
562
562
|
|
|
563
563
|
//#endregion
|
|
564
|
-
//#region
|
|
564
|
+
//#region ../../packages/sitemap/src/sitemap-shard-writer.ts
|
|
565
565
|
function envelopeBytes(baseUrl) {
|
|
566
566
|
return Buffer.byteLength(buildSitemapXml([], baseUrl), "utf8");
|
|
567
567
|
}
|
|
@@ -647,7 +647,7 @@ async function writeShardGroup(group, ctx) {
|
|
|
647
647
|
}
|
|
648
648
|
|
|
649
649
|
//#endregion
|
|
650
|
-
//#region
|
|
650
|
+
//#region ../../packages/sitemap/src/sitemap-index-xml.ts
|
|
651
651
|
/**
|
|
652
652
|
* Serialises the flat master `sitemapindex` document: every shard of every
|
|
653
653
|
* group, directly, in the order the groups and shards were produced — no
|
|
@@ -662,7 +662,7 @@ function buildSitemapIndexXml(files, baseUrl, shardPathPrefix = "") {
|
|
|
662
662
|
}
|
|
663
663
|
|
|
664
664
|
//#endregion
|
|
665
|
-
//#region
|
|
665
|
+
//#region ../../packages/sitemap/src/duplicate-path-tracker.ts
|
|
666
666
|
/**
|
|
667
667
|
* The streaming equivalent of `Sitemap`'s `routesByPath` map. A streaming
|
|
668
668
|
* writer cannot compare an entry against 500K predecessors, so it keeps only
|
|
@@ -696,7 +696,7 @@ var DuplicatePathTracker = class {
|
|
|
696
696
|
};
|
|
697
697
|
|
|
698
698
|
//#endregion
|
|
699
|
-
//#region
|
|
699
|
+
//#region ../../packages/sitemap/src/route-counter.ts
|
|
700
700
|
/** Counts URLs actually written per declared route, across every shard of every group. */
|
|
701
701
|
var RouteCounter = class {
|
|
702
702
|
counts = /* @__PURE__ */ new Map();
|
|
@@ -713,7 +713,7 @@ var RouteCounter = class {
|
|
|
713
713
|
};
|
|
714
714
|
|
|
715
715
|
//#endregion
|
|
716
|
-
//#region
|
|
716
|
+
//#region ../../packages/sitemap/src/sitemap-index.ts
|
|
717
717
|
/**
|
|
718
718
|
* The streaming path for 300–500K URLs: shards, one flat master index, gzip,
|
|
719
719
|
* and an atomic publish. It is a SECOND mode, not a bigger `Sitemap` — it
|
|
@@ -807,7 +807,7 @@ var SitemapIndex = class {
|
|
|
807
807
|
};
|
|
808
808
|
|
|
809
809
|
//#endregion
|
|
810
|
-
//#region
|
|
810
|
+
//#region ../../packages/sitemap/src/generation-manifest.ts
|
|
811
811
|
/** Storage-independent schema and key helpers for immutable sitemap generations. */
|
|
812
812
|
const GENERATION_MANIFEST_VERSION = 1;
|
|
813
813
|
const MANIFEST_FENCE_WIDTH = 16;
|
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 \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\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
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../../../../../packages/sitemap/src/errors.ts","../../../../../../../packages/sitemap/src/atomic-publish.ts","../../../../../../../packages/sitemap/src/atomic-write-file.ts","../../../../../../../packages/sitemap/src/lastmod.ts","../../../../../../../packages/sitemap/src/normalize-entry.ts","../../../../../../../packages/sitemap/src/url.ts","../../../../../../../packages/sitemap/src/xml.ts","../../../../../../../packages/sitemap/src/sitemap.ts","../../../../../../../packages/sitemap/src/sitemap-index-options.ts","../../../../../../../packages/sitemap/src/shard-name.ts","../../../../../../../packages/sitemap/src/sitemap-shard-writer.ts","../../../../../../../packages/sitemap/src/sitemap-index-xml.ts","../../../../../../../packages/sitemap/src/duplicate-path-tracker.ts","../../../../../../../packages/sitemap/src/route-counter.ts","../../../../../../../packages/sitemap/src/sitemap-index.ts","../../../../../../../packages/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 \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\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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"}
|
package/esm/atomic-publish.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { UnownedOutputDirectoryError } from "./errors.mjs";
|
|
|
2
2
|
import { mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
|
|
5
|
-
//#region
|
|
5
|
+
//#region ../../packages/sitemap/src/atomic-publish.ts
|
|
6
6
|
/**
|
|
7
7
|
* Marks a directory as one `publishAtomically` swapped into place. Its
|
|
8
8
|
* presence is the ONLY thing that lets a later publish treat the directory
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"atomic-publish.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"atomic-publish.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/atomic-publish.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;AASA,MAAM,wBAAwB;AAC9B,MAAM,2BAA2B;AAEjC,eAAe,eAAe,SAAiB,WAA6C;CAC1F,KAAK,MAAM,YAAY,WAAW;EAChC,MAAM,OAAO,MAAM,KAAK,KAAK,SAAS,QAAQ,CAAC,CAAC,CAAC,YAAY,MAAS;EAEtE,IAAI,CAAC,QAAQ,CAAC,KAAK,OAAO,KAAK,KAAK,SAAS,GAC3C,MAAM,IAAI,MAAM,mCAAmC,SAAS,uBAAuB;CAEvF;AACF;;;;;;;AAQA,eAAe,oBAAoB,QAA+B;CAChE,MAAM,UAAU,MAAM,QAAQ,MAAM,CAAC,CAAC,OAAO,UAAU;EACrD,IAAK,MAAgC,SAAS,UAAU,OAAO;EAC/D,MAAM;CACR,CAAC;CAED,IAAI,YAAY,UAAa,QAAQ,WAAW,GAAG;CAEnD,IAAI,CAAC,QAAQ,SAAS,qBAAqB,GACzC,MAAM,IAAI,4BAA4B,MAAM;AAEhD;;;;;;;;;;;;AAaA,eAAsB,kBACpB,QACA,OACe;CACf,MAAM,oBAAoB,MAAM;CAEhC,MAAM,SAAS,QAAQ,MAAM;CAE7B,MAAM,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;CAEvC,MAAM,UAAU,KAAK,QAAQ,IAAI,SAAS,MAAM,EAAE,OAAO,QAAQ,IAAI,GAAG,KAAK,IAAI,GAAG;CAEpF,MAAM,MAAM,SAAS,EAAE,WAAW,KAAK,CAAC;CAExC,IAAI;EAGF,MAAM,eAAe,SAAS,MAFN,MAAM,OAAO,CAEE;EAEvC,MAAM,UACJ,KAAK,SAAS,qBAAqB,GACnC,KAAK,UAAU;GAAE,SAAS;GAAuB,SAAS;EAAyB,CAAC,GACpF,MACF;EAEA,MAAM,eAAe,KAAK,QAAQ,IAAI,SAAS,MAAM,EAAE,YAAY,KAAK,IAAI,GAAG;EAC/E,IAAI,oBAAoB;EAExB,IAAI;GACF,MAAM,OAAO,QAAQ,YAAY;GACjC,oBAAoB;EACtB,SAAS,OAAO;GACd,IAAK,MAAgC,SAAS,UAAU,MAAM;EAChE;EAEA,IAAI;GACF,MAAM,OAAO,SAAS,MAAM;EAC9B,SAAS,OAAO;GACd,IAAI,mBAAmB,MAAM,OAAO,cAAc,MAAM;GACxD,MAAM;EACR;EAEA,IAAI,mBAAmB,MAAM,GAAG,cAAc;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;CAChF,SAAS,OAAO;EACd,MAAM,GAAG,SAAS;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;EAClD,MAAM;CACR;AACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { rename, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../../packages/sitemap/src/atomic-write-file.ts
|
|
5
5
|
const defaultDeps = {
|
|
6
6
|
writeFile,
|
|
7
7
|
rename,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"atomic-write-file.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"atomic-write-file.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/atomic-write-file.ts"],"sourcesContent":["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"],"mappings":";;;;AAUA,MAAM,cAAmC;CAAE;CAAW;CAAQ;AAAG;AAEjE,MAAM,wBAAwB;AAC9B,MAAM,wBAAwB;AAE9B,SAAS,MAAM,IAA2B;CACxC,OAAO,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;;AAGA,SAAS,uBAAuB,OAAyB;CACvD,MAAM,OAAQ,OAAiC;CAE/C,OAAO,SAAS,WAAW,SAAS;AACtC;AAEA,eAAe,gBAAgB,MAAc,IAAY,MAA0C;CACjG,KAAK,IAAI,UAAU,GAAG,WAAW,uBAAuB,WACtD,IAAI;EACF,MAAM,KAAK,OAAO,MAAM,EAAE;EAE1B;CACF,SAAS,OAAO;EACd,IAAI,YAAY,yBAAyB,CAAC,uBAAuB,KAAK,GAAG,MAAM;EAE/E,MAAM,MAAM,wBAAwB,OAAO;CAC7C;AAEJ;;;;;;;;;AAUA,eAAsB,gBACpB,UACA,SACA,OAA4B,aACb;CACf,MAAM,WAAW,KACf,QAAQ,QAAQ,GAChB,IAAI,SAAS,QAAQ,EAAE,OAAO,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,GAC/F;CAEA,IAAI;EACF,MAAM,KAAK,UAAU,UAAU,SAAS,MAAM;EAC9C,MAAM,gBAAgB,UAAU,UAAU,IAAI;CAChD,SAAS,OAAO;EACd,MAAM,KAAK,GAAG,UAAU,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,YAAY,MAAS;EAE9D,MAAM;CACR;AACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region
|
|
1
|
+
//#region ../../packages/sitemap/src/duplicate-path-tracker.ts
|
|
2
2
|
/**
|
|
3
3
|
* The streaming equivalent of `Sitemap`'s `routesByPath` map. A streaming
|
|
4
4
|
* writer cannot compare an entry against 500K predecessors, so it keeps only
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"duplicate-path-tracker.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"duplicate-path-tracker.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/duplicate-path-tracker.ts"],"sourcesContent":["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"],"mappings":";;;;;;;AAQA,IAAa,uBAAb,MAAkC;CAChC,AAAiB,+BAAe,IAAI,IAAoC;;CAGxE,AAAO,QAAQ,MAAc,OAAoC;EAC/D,MAAM,OAAO,KAAK,aAAa,IAAI,IAAI;EAEvC,IAAI,MAAM;GACR,KAAK,KAAK,KAAK;GAEf,OAAO;EACT;EAEA,KAAK,aAAa,IAAI,MAAM,CAAC,KAAK,CAAC;EAEnC,OAAO;CACT;CAEA,AAAO,SAA4B;EACjC,MAAM,UAA6B,CAAC;EAEpC,KAAK,MAAM,CAAC,MAAM,WAAW,KAAK,cAAc;GAC9C,IAAI,OAAO,SAAS,GAAG;GAEvB,QAAQ,KAAK;IAAE;IAAM,OAAO,OAAO;IAAQ,QAAQ,CAAC,GAAG,MAAM;GAAE,CAAC;EAClE;EAEA,OAAO;CACT;AACF"}
|
package/esm/errors.d.mts
CHANGED
package/esm/errors.mjs
CHANGED
package/esm/errors.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/errors.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"],"mappings":";;;;;;;;AAOA,IAAa,sBAAb,cAAyC,MAAM;CAC7C,AAAO,YAAY,OAAgB,QAAgB;EACjD,MAAM,2BAA2B,KAAK,UAAU,KAAK,EAAE,IAAI,OAAO,EAAE;EACpE,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,2BAAb,cAA8C,MAAM;CAClD,AAAO,YAAY,QAAgB;EACjC,MAAM,0BAA0B,OAAO,EAAE;EACzC,KAAK,OAAO;CACd;AACF;;;;;;;AAQA,IAAa,0BAAb,cAA6C,MAAM;CACjD,AAAO,YAAY,KAAa;EAC9B,MACE,gCAAgC,KAAK,UAAU,GAAG,EAAE,iFAEtD;EACA,KAAK,OAAO;CACd;AACF;;;;;;;;;;AAWA,IAAa,8BAAb,cAAiD,MAAM;CACrD,AAAO,YAAY,QAAgB;EACjC,MACE,wCAAwC,KAAK,UAAU,MAAM,EAAE,wNAIjE;EACA,KAAK,OAAO;CACd;AACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region
|
|
1
|
+
//#region ../../packages/sitemap/src/generation-manifest.d.ts
|
|
2
2
|
/** Storage-independent schema and key helpers for immutable sitemap generations. */
|
|
3
3
|
declare const GENERATION_MANIFEST_VERSION: 1;
|
|
4
4
|
declare const MANIFEST_FENCE_WIDTH = 16;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region
|
|
1
|
+
//#region ../../packages/sitemap/src/generation-manifest.ts
|
|
2
2
|
/** Storage-independent schema and key helpers for immutable sitemap generations. */
|
|
3
3
|
const GENERATION_MANIFEST_VERSION = 1;
|
|
4
4
|
const MANIFEST_FENCE_WIDTH = 16;
|
|
@@ -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. */\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
|
+
{"version":3,"file":"generation-manifest.mjs","names":[],"sources":["../../../../../../../packages/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/esm/lastmod.mjs
CHANGED
package/esm/lastmod.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lastmod.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"lastmod.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/lastmod.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;AAUA,SAAgB,cAAc,OAA8B;CAC1D,IAAI,iBAAiB,MAAM;EACzB,IAAI,OAAO,MAAM,MAAM,QAAQ,CAAC,GAC9B,MAAM,IAAI,yBAAyB,4BAA4B;EAGjE,OAAO,MAAM,YAAY;CAC3B;CAEA,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,IAChD,MAAM,IAAI,yBAAyB,8CAA8C;CAGnF,OAAO;AACT"}
|
package/esm/normalize-entry.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalize-entry.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"normalize-entry.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/normalize-entry.ts"],"sourcesContent":["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"],"mappings":";;;;AAUA,MAAM,eAAsC;CAC1C;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AACA,MAAM,qBAAqB;AAE3B,SAAS,iBAAiB,OAA6C;CACrE,IAAI,CAAC,aAAa,SAAS,KAAmB,GAC5C,MAAM,IAAI,yBACR,cAAc,KAAK,UAAU,KAAK,EAAE,iBAAiB,aAAa,KAAK,IAAI,GAC7E;AAEJ;AAEA,SAAS,eAAe,OAAyC;CAC/D,IAAI,OAAO,UAAU,YAAY,OAAO,MAAM,KAAK,KAAK,QAAQ,KAAK,QAAQ,GAC3E,MAAM,IAAI,yBACR,YAAY,KAAK,UAAU,KAAK,EAAE,uCACpC;AAEJ;;AAGA,SAAgB,cAAc,MAAuB;CACnD,IAAI,OAAO,SAAS,YAAY,KAAK,KAAK,MAAM,IAC9C,MAAM,IAAI,yBAAyB,iDAAiD;CAKtF,IAAI,gBAAgB,KAAK,IAAI,GAAG,OAAO;CAEvC,OAAO,KAAK,WAAW,GAAG,IAAI,OAAO,IAAI;AAC3C;;;;;;;AAQA,SAAgB,eACd,OACA,UACsB;CACtB,MAAM,OAAO,cAAc,MAAM,IAAI;CACrC,MAAM,aAAa,MAAM,cAAc,SAAS;CAChD,MAAM,WAAW,MAAM,YAAY,SAAS;CAC5C,MAAM,UAAU,MAAM,WAAW,SAAS;CAE1C,IAAI,eAAe,QAAW,iBAAiB,UAAU;CACzD,IAAI,aAAa,QAAW,eAAe,QAAQ;CAEnD,MAAM,aAAa,MAAM,YAAY,KAAK,cAAc;EACtD,IAAI,OAAO,WAAW,aAAa,YAAY,UAAU,SAAS,KAAK,MAAM,IAC3E,MAAM,IAAI,yBAAyB,gCAAgC;EAGrE,OAAO;GAAE,UAAU,UAAU;GAAU,MAAM,cAAc,UAAU,IAAI;EAAE;CAC7E,CAAC;CACD,IAAI,MAAM,WAAW,UAAa,CAAC,MAAM,QAAQ,MAAM,MAAM,GAC3D,MAAM,IAAI,yBAAyB,yBAAyB;CAE9D,MAAM,SAAS,MAAM,QAAQ,MAAM,GAAG,kBAAkB,CAAC,CAAC,KAAK,UAAwB;EACrF,IAAI,OAAO,OAAO,QAAQ,YAAY,MAAM,IAAI,KAAK,MAAM,IACzD,MAAM,IAAI,yBAAyB,uBAAuB;EAG5D,IAAI;EACJ,IAAI;GACF,MAAM,IAAI,IAAI,MAAM,GAAG;EACzB,QAAQ;GACN,MAAM,IAAI,yBAAyB,2CAA2C;EAChF;EACA,IAAI,IAAI,aAAa,WAAW,IAAI,aAAa,UAC/C,MAAM,IAAI,yBAAyB,2CAA2C;EAGhF,OAAO,EAAE,KAAK,MAAM,IAAI;CAC1B,CAAC;CACD,KAAK,MAAM,QAAQ,UAAU,KAAK,oBAChC,SAAS,uBAAuB;EAC9B,GAAI,MAAM,UAAU,SAAY,CAAC,IAAI,EAAE,OAAO,MAAM,MAAM;EAC1D,SAAS,MAAM,OAAQ,SAAS;CAClC,CAAC;CAGH,OAAO;EACL;EACA,GAAI,MAAM,SAAS,SAAY,EAAE,MAAM,MAAM,KAAK,IAAI,CAAC;EACvD,GAAI,MAAM,UAAU,SAAY,EAAE,OAAO,MAAM,MAAM,IAAI,CAAC;EAC1D,GAAI,YAAY,SAAY,EAAE,SAAS,cAAc,OAAO,EAAE,IAAI,CAAC;EACnE,GAAI,eAAe,SAAY,EAAE,WAAW,IAAI,CAAC;EACjD,GAAI,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC;EAC7C,GAAI,eAAe,SAAY,EAAE,WAAW,IAAI,CAAC;EACjD,GAAI,WAAW,SAAY,EAAE,OAAO,IAAI,CAAC;CAC3C;AACF"}
|
package/esm/route-counter.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-counter.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"route-counter.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/route-counter.ts"],"sourcesContent":["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"],"mappings":";;AAGA,IAAa,eAAb,MAA0B;CACxB,AAAiB,yBAAS,IAAI,IAAoB;CAElD,AAAO,OAAO,OAAiC;EAC7C,IAAI,UAAU,QAAW;EAEzB,KAAK,OAAO,IAAI,QAAQ,KAAK,OAAO,IAAI,KAAK,KAAK,KAAK,CAAC;CAC1D;CAEA,AAAO,UAA0B;EAC/B,OAAO,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY;GAAE;GAAO;EAAM,EAAE;CACpE;AACF"}
|
package/esm/shard-name.mjs
CHANGED
package/esm/shard-name.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shard-name.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"shard-name.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/shard-name.ts"],"sourcesContent":["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"],"mappings":";AAAA,MAAM,mBAAmB;;;;;;;;;AAUzB,SAAgB,sBAAsB,KAAqB;CACzD,IAAI,OAAO,QAAQ,YAAY,CAAC,iBAAiB,KAAK,GAAG,GACvD,MAAM,IAAI,WACR,sBAAsB,KAAK,UAAU,GAAG,EAAE,oEAE5C;CAGF,OAAO,IAAI,YAAY;AACzB;;;;;;AAOA,SAAgB,cACd,YACA,KACA,SACA,MACQ;CACR,MAAM,gBAAgB,OAAO,OAAO,CAAC,CAAC,SAAS,GAAG,GAAG;CACrD,MAAM,OACJ,QAAQ,SAAY,GAAG,WAAW,GAAG,IAAI,GAAG,kBAAkB,GAAG,WAAW,GAAG;CAEjF,OAAO,OAAO,GAAG,KAAK,WAAW,GAAG,KAAK;AAC3C"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeBaseUrl } from "./url.mjs";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../../packages/sitemap/src/sitemap-index-options.ts
|
|
4
4
|
/** The sitemaps.org limits. Never clamped to — a silently clamped option is a lie about what was written. */
|
|
5
5
|
const PROTOCOL_MAX_URLS_PER_FILE = 5e4;
|
|
6
6
|
const PROTOCOL_MAX_BYTES_PER_FILE = 50 * 1024 * 1024;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sitemap-index-options.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"sitemap-index-options.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/sitemap-index-options.ts"],"sourcesContent":["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"],"mappings":";;;;AAKA,MAAa,6BAA6B;AAC1C,MAAa,8BAA8B,KAAK,OAAO;;AAiBvD,SAAgB,6BACd,SAC6B;CAC7B,MAAM,UAAU,iBAAiB,SAAS,OAAO;CACjD,MAAM,kBAAkB,yBAAyB,QAAQ,eAAe;CAExE,MAAM,iBAAiB,QAAQ;CAC/B,MAAM,kBAAkB,QAAQ;CAEhC,IACE,CAAC,OAAO,UAAU,cAAc,KAChC,iBAAiB,KACjB,sBAEA,MAAM,IAAI,WACR,+EACK,2BAA2B,QAAQ,KAAK,UAAU,cAAc,EAAE,EACzE;CAGF,IACE,CAAC,OAAO,SAAS,eAAe,KAChC,kBAAkB,KAClB,4BAEA,MAAM,IAAI,WACR,qEACK,4BAA4B,cAAc,KAAK,UAAU,eAAe,EAAE,EACjF;CAGF,OAAO;EACL;EACA,YAAY,QAAQ,cAAc;EAClC,eAAe,QAAQ,iBAAiB;EACxC;EACA,MAAM,QAAQ,QAAQ;EACtB;EACA;EACA,UAAU;GACR,YAAY,QAAQ;GACpB,UAAU,QAAQ;GAClB,SAAS,QAAQ;GACjB,sBAAsB,QAAQ;EAChC;CACF;AACF;AAEA,SAAS,yBAAyB,OAAmC;CACnE,IAAI,UAAU,UAAa,UAAU,MAAM,UAAU,KAAK,OAAO;CAEjE,MAAM,SAAS,MAAM,QAAQ,OAAO,EAAE,CAAC,CAAC,QAAQ,OAAO,EAAE;CACzD,IACE,OAAO,WAAW,KAClB,OAAO,MAAM,GAAG,CAAC,CAAC,MAAM,YAAY,CAAC,mBAAmB,KAAK,OAAO,CAAC,GAErE,MAAM,IAAI,UAAU,2DAA2D;CAGjF,OAAO,GAAG,OAAO;AACnB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ChangeFreq, DuplicateReport, RouteSummary, SitemapEntry, SitemapImageLimitExceeded } from "./types.mjs";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../../packages/sitemap/src/sitemap-index-types.d.ts
|
|
4
4
|
/** What a source factory produces: one walk over a group's entries. */
|
|
5
5
|
type SitemapSource = Iterable<SitemapEntry> | AsyncIterable<SitemapEntry>;
|
|
6
6
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { joinOrigin } from "./url.mjs";
|
|
2
2
|
import { escapeXml } from "./xml.mjs";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../../packages/sitemap/src/sitemap-index-xml.ts
|
|
5
5
|
/**
|
|
6
6
|
* Serialises the flat master `sitemapindex` document: every shard of every
|
|
7
7
|
* group, directly, in the order the groups and shards were produced — no
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sitemap-index-xml.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"sitemap-index-xml.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/sitemap-index-xml.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;AAUA,SAAgB,qBACd,OACA,SACA,kBAAkB,IACV;CACR,MAAM,OAAO,MACV,QAAQ,SAAS,KAAK,OAAO,CAAC,CAAC,CAC/B,KAAK,SAAS;EAGb,OAAO,yBAFK,UAAU,WAAW,SAAS,GAAG,kBAAkB,KAAK,UAAU,CAE5C,EAAE;CACtC,CAAC,CAAC,CACD,KAAK,IAAI;CAEZ,OACE,wHAEC,KAAK,SAAS,IAAI,GAAG,KAAK,MAAM,MACjC;AAEJ"}
|
package/esm/sitemap-index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SitemapIndexOptions, SitemapSetResult, SitemapSourceFactory } from "./sitemap-index-types.mjs";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../../packages/sitemap/src/sitemap-index.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* The streaming path for 300–500K URLs: shards, one flat master index, gzip,
|
|
6
6
|
* and an atomic publish. It is a SECOND mode, not a bigger `Sitemap` — it
|
package/esm/sitemap-index.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { RouteCounter } from "./route-counter.mjs";
|
|
|
9
9
|
import { writeFile } from "node:fs/promises";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
12
|
-
//#region
|
|
12
|
+
//#region ../../packages/sitemap/src/sitemap-index.ts
|
|
13
13
|
/**
|
|
14
14
|
* The streaming path for 300–500K URLs: shards, one flat master index, gzip,
|
|
15
15
|
* and an atomic publish. It is a SECOND mode, not a bigger `Sitemap` — it
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sitemap-index.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"sitemap-index.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/sitemap-index.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;;;;;;;;;AA2BA,IAAa,eAAb,MAA0B;CACxB,AAAiB;;CAGjB,AAAiB,mBAA2C,CAAC;CAE7D,AAAiB,cAA4B,CAAC;;CAG9C,AAAiB,iCAAiB,IAAI,IAAY;CAElD,AAAiB,oCAAoB,IAAI,IAAY;CAErD,AAAO,YAAY,SAA8B;EAC/C,KAAK,UAAU,6BAA6B,OAAO;CACrD;CAIA,AAAO,UACL,aACA,aACM;EACN,IAAI,OAAO,gBAAgB,UAAU;GACnC,MAAM,eAAe,sBAAsB,WAAW;GAEtD,IAAI,KAAK,eAAe,IAAI,YAAY,GACtC,MAAM,IAAI,wBAAwB,WAAW;GAG/C,KAAK,eAAe,IAAI,YAAY;GACpC,KAAK,YAAY,KAAK;IAAE,KAAK;IAAa,WAAW,CAAC,WAAmC;GAAE,CAAC;EAC9F,OACE,KAAK,iBAAiB,KAAK,WAAW;EAGxC,OAAO;CACT;;;;;;;CAQA,MAAa,OAAO,QAA2C;EAC7D,MAAM,SAAuB,CAC3B,GAAI,KAAK,iBAAiB,SAAS,IAC/B,CAAC;GAAE,KAAK;GAAW,WAAW,KAAK;EAAiB,CAAC,IACrD,CAAC,GACL,GAAG,KAAK,WACV;EAEA,MAAM,aAAa,IAAI,qBAAqB;EAC5C,MAAM,SAAS,IAAI,aAAa;EAChC,IAAI,aAA0B,CAAC;EAE/B,MAAM,QAAQ,OAAO,YAAgD;GACnE,aAAa,CAAC;GAEd,KAAK,MAAM,SAAS,QAAQ;IAC1B,MAAM,QAAQ,MAAM,gBAAgB,OAAO;KACzC;KACA,SAAS,KAAK,QAAQ;KACtB,YAAY,KAAK,QAAQ;KACzB,MAAM,KAAK,QAAQ;KACnB,gBAAgB,KAAK,QAAQ;KAC7B,iBAAiB,KAAK,QAAQ;KAC9B,UAAU;MACR,GAAG,KAAK,QAAQ;MAChB,uBAAuB,UAAU,KAAK,iBAAiB,KAAK;KAC9D;KACA;KACA;IACF,CAAC;IAED,WAAW,KAAK,GAAG,KAAK;GAC1B;GAEA,MAAM,WAAW,qBACf,YACA,KAAK,QAAQ,SACb,KAAK,QAAQ,eACf;GAEA,MAAM,UAAU,KAAK,SAAS,KAAK,QAAQ,aAAa,GAAG,UAAU,MAAM;GAM3E,OAAO,CAAC,GAJkB,WACvB,QAAQ,SAAS,KAAK,OAAO,CAAC,CAAC,CAC/B,KAAK,SAAS,KAAK,QAEK,GAAG,KAAK,QAAQ,aAAa;EAC1D;EAEA,MAAM,kBAAkB,QAAQ,KAAK;EAErC,MAAM,QAA6B,WAAW,KAAK,UAAU;GAC3D,MAAM,KAAK,QAAQ,KAAK,QAAQ;GAChC,GAAI,KAAK,QAAQ,SAAY,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;GAClD,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,SAAS,KAAK;EAChB,EAAE;EAEF,OAAO;GACL,WAAW,KAAK,QAAQ,KAAK,QAAQ,aAAa;GAClD;GACA,WAAW,MAAM,QAAQ,OAAO,SAAS,QAAQ,KAAK,MAAM,CAAC;GAC7D,YAAY,WAAW,OAAO;GAC9B,QAAQ,OAAO,QAAQ;EACzB;CACF;CAEA,AAAQ,iBAAiB,OAAoE;EAC3F,MAAM,MAAM,MAAM,SAAS;EAC3B,IAAI,KAAK,kBAAkB,IAAI,GAAG,GAAG;EAErC,KAAK,kBAAkB,IAAI,GAAG;EAC9B,IAAI,KAAK,QAAQ,SAAS,sBACxB,KAAK,QAAQ,SAAS,qBAAqB,KAAK;OAEhD,QAAQ,KACN,qBAAqB,IAAI,2CAA2C,MAAM,QAAQ,WACpF;CACJ;AACF"}
|
|
@@ -5,7 +5,7 @@ import { writeFile } from "node:fs/promises";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { gzipSync } from "node:zlib";
|
|
7
7
|
|
|
8
|
-
//#region
|
|
8
|
+
//#region ../../packages/sitemap/src/sitemap-shard-writer.ts
|
|
9
9
|
function envelopeBytes(baseUrl) {
|
|
10
10
|
return Buffer.byteLength(buildSitemapXml([], baseUrl), "utf8");
|
|
11
11
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sitemap-shard-writer.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"sitemap-shard-writer.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/sitemap-shard-writer.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;AAyCA,SAAS,cAAc,SAAyB;CAC9C,OAAO,OAAO,WAAW,gBAAgB,CAAC,GAAG,OAAO,GAAG,MAAM;AAC/D;;;;;;;;AASA,MAAM,kBAAkB,OAAO,WAAW,sBAAsB,MAAM;AACtE,MAAM,wBAAwB,OAAO,WAAW,sBAAsB,MAAM;AAE5E,eAAe,eACb,KACA,UACA,SAC4B;CAC5B,MAAM,MAAM,gBAAgB,SAAS,IAAI,OAAO;CAChD,MAAM,WAAW,KAAK,IAAI,SAAS,QAAQ;CAE3C,IAAI,IAAI,MAAM;EACZ,MAAM,aAAa,SAAS,OAAO,KAAK,KAAK,MAAM,CAAC;EAEpD,MAAM,UAAU,UAAU,UAAU;EAEpC,OAAO,EAAE,OAAO,WAAW,WAAW;CACxC;CAEA,MAAM,UAAU,UAAU,KAAK,MAAM;CAErC,OAAO,EAAE,OAAO,OAAO,WAAW,KAAK,MAAM,EAAE;AACjD;;;;;;AAOA,eAAsB,gBACpB,OACA,KACsB;CACtB,MAAM,UAAuB,CAAC;CAE9B,IAAI,SAAiC,CAAC;CACtC,IAAI,cAAc,cAAc,IAAI,OAAO;CAG3C,IAAI,sBAAsB;CAC1B,IAAI,kBAAkB;CACtB,IAAI,UAAU;CAEd,MAAM,QAAQ,YAAY;EACxB,IAAI,OAAO,WAAW,GAAG;EAEzB,MAAM,WAAW,cAAc,IAAI,YAAY,MAAM,KAAK,SAAS,IAAI,IAAI;EAC3E,MAAM,EAAE,UAAU,MAAM,eAAe,KAAK,UAAU,MAAM;EAE5D,QAAQ,KAAK;GAAE;GAAU,KAAK,MAAM;GAAK,MAAM,OAAO;GAAQ;GAAO,SAAS,IAAI;EAAK,CAAC;EAExF,WAAW;EACX,SAAS,CAAC;EACV,cAAc,cAAc,IAAI,OAAO;EACvC,sBAAsB;EACtB,kBAAkB;CACpB;CAEA,KAAK,MAAM,WAAW,MAAM,WAAW;EACrC,MAAM,SAAS,MAAM,QAAQ;EAE7B,WAAW,MAAM,YAAY,QAAQ;GACnC,MAAM,WAAW,eAAe,UAAU,IAAI,QAAQ;GAGtD,IAAI,CAAC,IAAI,WAAW,QAAQ,SAAS,MAAM,SAAS,KAAK,GAAG;GAE5D,MAAM,sBAAsB,SAAS,YAAY,UAAU,KAAK;GAChE,MAAM,kBAAkB,SAAS,QAAQ,UAAU,KAAK;GACxD,MAAM,aAAa,OAAO,WAAW,eAAe,UAAU,IAAI,OAAO,GAAG,MAAM,IAAI;GAGtF,MAAM,aACJ,cACC,sBAAsB,CAAC,sBAAsB,kBAAkB,MAC/D,kBAAkB,CAAC,kBAAkB,wBAAwB;GAEhE,MAAM,iBAAiB,OAAO,UAAU,IAAI;GAE5C,MAAM,kBAAkB,OAAO,SAAS,KAAK,cAAc,aAAa,IAAI;GAE5E,IAAI,kBAAkB,iBAAiB,MAAM,MAAM;GAEnD,MAAM,mBAAmB,sBAAsB,CAAC;GAChD,MAAM,wBAAwB,kBAAkB,CAAC;GAEjD,OAAO,KAAK,QAAQ;GACpB,eACE,cACC,mBAAmB,kBAAkB,MACrC,wBAAwB,wBAAwB;GACnD,IAAI,kBAAkB,sBAAsB;GAC5C,IAAI,uBAAuB,kBAAkB;GAC7C,IAAI,OAAO,OAAO,SAAS,KAAK;EAClC;CACF;CAEA,MAAM,MAAM;CAEZ,IAAI,QAAQ,WAAW,GAErB,QAAQ,KAAK;EACX,UAAU,cAAc,IAAI,YAAY,MAAM,KAAK,GAAG,IAAI,IAAI;EAC9D,KAAK,MAAM;EACX,MAAM;EACN,OAAO;EACP,SAAS;CACX,CAAC;CAGH,OAAO;AACT"}
|
package/esm/sitemap.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DuplicateReport, ResolvedSitemapEntry, RouteSummary, SitemapEntry, SitemapOptions } from "./types.mjs";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../../packages/sitemap/src/sitemap.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* A bounded sitemap builder: it RETAINS every entry, which is what makes
|
|
6
6
|
* `entries()` and a repeatable `toXML()` possible, and is exactly right up to
|
package/esm/sitemap.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { buildSitemapXml } from "./xml.mjs";
|
|
|
6
6
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
|
|
9
|
-
//#region
|
|
9
|
+
//#region ../../packages/sitemap/src/sitemap.ts
|
|
10
10
|
/**
|
|
11
11
|
* A bounded sitemap builder: it RETAINS every entry, which is what makes
|
|
12
12
|
* `entries()` and a repeatable `toXML()` possible, and is exactly right up to
|
package/esm/sitemap.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sitemap.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"sitemap.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/sitemap.ts"],"sourcesContent":["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"],"mappings":"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"}
|
package/esm/types.d.mts
CHANGED
package/esm/url.d.mts
CHANGED
package/esm/url.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { InvalidBaseUrlError } from "./errors.mjs";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../../packages/sitemap/src/url.ts
|
|
4
4
|
/**
|
|
5
5
|
* Joins a configured origin and an app-relative route path into one absolute
|
|
6
6
|
* URL, with exactly one slash at the seam regardless of whether either side
|
package/esm/url.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"url.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/url.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;AAOA,SAAgB,WAAW,QAAgB,WAA2B;CAIpE,OAAO,GAHe,OAAO,SAAS,GAAG,IAAI,OAAO,MAAM,GAAG,EAAE,IAAI,SAC5C,UAAU,WAAW,GAAG,IAAI,YAAY,IAAI;AAGrE;;;;;;;;AASA,SAAgB,iBAAiB,OAAuB;CACtD,IAAI,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,IAChD,MAAM,IAAI,oBAAoB,OAAO,6BAA6B;CAGpE,IAAI;CAEJ,IAAI;EACF,SAAS,IAAI,IAAI,KAAK;CACxB,QAAQ;EACN,MAAM,IAAI,oBAAoB,OAAO,qBAAqB;CAC5D;CAEA,IAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UACrD,MAAM,IAAI,oBAAoB,OAAO,yBAAyB,OAAO,SAAS,EAAE;CAGlF,MAAM,OAAO,OAAO;CAEpB,OAAO,KAAK,SAAS,GAAG,IAAI,KAAK,MAAM,GAAG,EAAE,IAAI;AAClD;;AAGA,SAAgB,cAAc,OAAwB;CACpD,OAAO,gBAAgB,KAAK,KAAK;AACnC;;AAGA,SAAgB,mBAAmB,SAAiB,MAAsB;CACxE,OAAO,cAAc,IAAI,IAAI,OAAO,WAAW,SAAS,IAAI;AAC9D"}
|
package/esm/xml.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ResolvedSitemapEntry } from "./types.mjs";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../../packages/sitemap/src/xml.d.ts
|
|
4
4
|
/** Escapes the five XML-significant characters. A URL's query string routinely contains `&`. */
|
|
5
5
|
declare function escapeXml(value: string): string;
|
|
6
6
|
/**
|
package/esm/xml.mjs
CHANGED
package/esm/xml.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xml.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"xml.mjs","names":[],"sources":["../../../../../../../packages/sitemap/src/xml.ts"],"sourcesContent":["import type { ResolvedSitemapEntry } from \"./types\";\nimport { resolveAgainstBase } from \"./url\";\n\nconst XML_ESCAPES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\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"],"mappings":";;;AAGA,MAAM,cAAsC;CAC1C,KAAK;CACL,KAAK;CACL,KAAK;CACL,MAAK;CACL,KAAK;AACP;;AAGA,SAAgB,UAAU,OAAuB;CAK/C,OAAO,MAAM,QAAQ,aAAa,SAAS,YAAY,SAAS,IAAI;AACtE;AAEA,MAAM,kBAAkB;AACxB,MAAM,kBAAkB;;;;;;AAOxB,MAAa,uBAAuB,iBAAiB,gBAAgB;;AAErE,MAAa,uBAAuB,iBAAiB,gBAAgB;;;;;;;AAQrE,SAAgB,eAAe,OAA6B,SAAyB;CACnF,MAAM,QAAQ,CAAC,WAAW,YAAY,UAAU,mBAAmB,SAAS,MAAM,IAAI,CAAC,EAAE,OAAO;CAEhG,IAAI,MAAM,YAAY,QACpB,MAAM,KAAK,gBAAgB,UAAU,MAAM,OAAO,EAAE,WAAW;CAGjE,IAAI,MAAM,eAAe,QACvB,MAAM,KAAK,mBAAmB,MAAM,WAAW,cAAc;CAG/D,IAAI,MAAM,aAAa,QACrB,MAAM,KAAK,iBAAiB,MAAM,SAAS,YAAY;CAGzD,KAAK,MAAM,aAAa,MAAM,cAAc,CAAC,GAAG;EAC9C,MAAM,OAAO,UAAU,mBAAmB,SAAS,UAAU,IAAI,CAAC;EAElE,MAAM,KACJ,6CAA6C,UAAU,UAAU,QAAQ,EAAE,UAAU,KAAK,IAC5F;CACF;CAEA,KAAK,MAAM,SAAS,MAAM,UAAU,CAAC,GAAG;EACtC,MAAM,KAAK,mBAAmB;EAC9B,MAAM,KAAK,oBAAoB,UAAU,mBAAmB,SAAS,MAAM,GAAG,CAAC,EAAE,aAAa;EAC9F,MAAM,KAAK,oBAAoB;CACjC;CAEA,MAAM,KAAK,UAAU;CAErB,OAAO,MAAM,KAAK,IAAI;AACxB;;;;;;;;;;AAWA,SAAgB,gBAAgB,SAA0C,SAAyB;CACjG,MAAM,gBAAgB,QAAQ,MAAM,WAAW,MAAM,YAAY,UAAU,KAAK,CAAC;CACjF,MAAM,YAAY,QAAQ,MAAM,WAAW,MAAM,QAAQ,UAAU,KAAK,CAAC;CACzE,MAAM,aAAa,GAAG,gBAAgB,uBAAuB,KAAK,YAAY,uBAAuB;CACrG,MAAM,OAAO,QAAQ,KAAK,UAAU,eAAe,OAAO,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI;CAE7E,OACE,sGAC8D,WAAW,QACxE,KAAK,SAAS,IAAI,GAAG,KAAK,MAAM,MACjC;AAEJ"}
|
package/package.json
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
},
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/warlockjs/
|
|
9
|
+
"url": "git+https://github.com/warlockjs/framework.git",
|
|
10
|
+
"directory": "packages/sitemap"
|
|
10
11
|
},
|
|
11
|
-
"version": "5.
|
|
12
|
+
"version": "5.30.0",
|
|
12
13
|
"main": "./cjs/index.cjs",
|
|
13
14
|
"module": "./esm/index.mjs",
|
|
14
15
|
"types": "./esm/index.d.mts",
|