@warlock.js/sitemap 5.15.0 → 5.16.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +179 -137
  3. package/cjs/index.cjs +677 -201
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/atomic-publish.mjs +85 -0
  6. package/esm/atomic-publish.mjs.map +1 -0
  7. package/esm/atomic-write-file.mjs +50 -0
  8. package/esm/atomic-write-file.mjs.map +1 -0
  9. package/esm/duplicate-path-tracker.mjs +36 -0
  10. package/esm/duplicate-path-tracker.mjs.map +1 -0
  11. package/esm/errors.d.mts +39 -0
  12. package/esm/errors.mjs +52 -0
  13. package/esm/errors.mjs.map +1 -0
  14. package/esm/index.d.mts +8 -8
  15. package/esm/index.mjs +6 -6
  16. package/esm/lastmod.mjs +23 -0
  17. package/esm/lastmod.mjs.map +1 -0
  18. package/esm/normalize-entry.mjs +59 -0
  19. package/esm/normalize-entry.mjs.map +1 -0
  20. package/esm/route-counter.mjs +19 -0
  21. package/esm/route-counter.mjs.map +1 -0
  22. package/esm/shard-name.mjs +28 -0
  23. package/esm/shard-name.mjs.map +1 -0
  24. package/esm/sitemap-index-options.mjs +31 -0
  25. package/esm/sitemap-index-options.mjs.map +1 -0
  26. package/esm/sitemap-index-types.d.mts +39 -0
  27. package/esm/sitemap-index-xml.mjs +20 -0
  28. package/esm/sitemap-index-xml.mjs.map +1 -0
  29. package/esm/sitemap-index.d.mts +31 -0
  30. package/esm/sitemap-index.mjs +96 -0
  31. package/esm/sitemap-index.mjs.map +1 -0
  32. package/esm/sitemap-shard-writer.mjs +89 -0
  33. package/esm/sitemap-shard-writer.mjs.map +1 -0
  34. package/esm/sitemap.d.mts +63 -0
  35. package/esm/sitemap.mjs +129 -0
  36. package/esm/sitemap.mjs.map +1 -0
  37. package/esm/types.d.mts +51 -20
  38. package/esm/url.d.mts +1 -20
  39. package/esm/url.mjs +26 -19
  40. package/esm/url.mjs.map +1 -1
  41. package/esm/xml.d.mts +15 -4
  42. package/esm/xml.mjs +30 -8
  43. package/esm/xml.mjs.map +1 -1
  44. package/llms-full.txt +139 -159
  45. package/llms.txt +2 -2
  46. package/package.json +2 -14
  47. package/skills/sitemap-overview/SKILL.md +139 -159
  48. package/esm/collect-entries.d.mts +0 -44
  49. package/esm/collect-entries.mjs +0 -73
  50. package/esm/collect-entries.mjs.map +0 -1
  51. package/esm/diagnostic.d.mts +0 -13
  52. package/esm/diagnostic.mjs +0 -19
  53. package/esm/diagnostic.mjs.map +0 -1
  54. package/esm/routable-page.d.mts +0 -27
  55. package/esm/sitemap-connector.d.mts +0 -60
  56. package/esm/sitemap-connector.mjs +0 -117
  57. package/esm/sitemap-connector.mjs.map +0 -1
package/cjs/index.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../../../../../sitemap/src/collect-entries.ts","../../../../../../sitemap/src/diagnostic.ts","../../../../../../sitemap/src/url.ts","../../../../../../sitemap/src/xml.ts","../../../../../../sitemap/src/sitemap-connector.ts"],"sourcesContent":["import type { RoutablePage } from \"./routable-page\";\r\nimport type { SitemapDefaults, SitemapEntry } from \"./types\";\r\n\r\nconst DYNAMIC_SEGMENT = /(^|\\/):[A-Za-z_][A-Za-z0-9_]*/;\r\n\r\n/** A route path carries a dynamic segment (`[id]` -> `:id`) it cannot enumerate on its own. */\r\nexport function isDynamicRoutePath(routePath: string): boolean {\r\n return DYNAMIC_SEGMENT.test(routePath);\r\n}\r\n\r\nfunction isNoindex(robots: string | undefined): boolean {\r\n return robots !== undefined && /noindex/i.test(robots);\r\n}\r\n\r\n/** Applies the config's `defaults` to any entry that omits `changefreq`/`priority` — shared by page-derived and app-supplied entries alike. */\r\nexport function withDefaults(entry: SitemapEntry, defaults: SitemapDefaults | undefined): SitemapEntry {\r\n return {\r\n ...entry,\r\n changefreq: entry.changefreq ?? defaults?.changefreq,\r\n priority: entry.priority ?? defaults?.priority,\r\n };\r\n}\r\n\r\n/**\r\n * Combines page-derived entries with app-supplied ones (`SitemapConnectorOptions.entries`),\r\n * deduplicating by `path`. App-supplied entries are ADDED, not substituted — an\r\n * app with both a page graph and extra URLs (e.g. rows the page graph can't\r\n * see) wants both — but where the same path appears in both, the app-supplied\r\n * entry wins, since it was written for that exact path on purpose.\r\n */\r\nexport function mergeSitemapEntries(\r\n pageEntries: readonly SitemapEntry[],\r\n appEntries: readonly SitemapEntry[],\r\n): SitemapEntry[] {\r\n const byPath = new Map<string, SitemapEntry>();\r\n\r\n for (const entry of pageEntries) byPath.set(entry.path, entry);\r\n for (const entry of appEntries) byPath.set(entry.path, entry);\r\n\r\n return Array.from(byPath.values());\r\n}\r\n\r\nexport type CollectSitemapEntriesOptions = {\r\n defaults?: SitemapDefaults;\r\n};\r\n\r\nexport type CollectSitemapEntriesResult = {\r\n entries: SitemapEntry[];\r\n /** Route names of dynamic routes with no `sitemap` export — feed to `describeUnresolvedDynamicRoutes`. */\r\n unresolvedDynamicRoutes: string[];\r\n};\r\n\r\n/**\r\n * Walks the routable pages and produces the entries + the unresolved-dynamic\r\n * diagnostic input, applying every exclusion rule:\r\n *\r\n * - `metadata.robots` says `noindex` -> excluded.\r\n * - `sitemap: false` -> excluded.\r\n * - a `sitemap` export (any route) -> its returned entries, in place of the\r\n * page's own route path.\r\n * - a dynamic route with no `sitemap` export -> omitted, name collected.\r\n * - everything else (static routes) -> one entry at the page's own route path.\r\n *\r\n * Not-found and error pages are excluded by construction: the caller is\r\n * expected to hand this only `DiscoveredRoutablePage`-derived entries, and\r\n * the not-found route is never one of those (`@warlock.js/web`'s discovery\r\n * reports it as a routable page for the client matcher, but the runtime\r\n * wiring filters it out before calling here — see the sitemap README).\r\n */\r\nexport async function collectSitemapEntries(\r\n pages: readonly RoutablePage[],\r\n options: CollectSitemapEntriesOptions = {},\r\n): Promise<CollectSitemapEntriesResult> {\r\n const entries: SitemapEntry[] = [];\r\n const unresolvedDynamicRoutes: string[] = [];\r\n\r\n for (const page of pages) {\r\n if (isNoindex(page.robots)) continue;\r\n if (page.sitemap === false) continue;\r\n\r\n if (typeof page.sitemap === \"function\") {\r\n const produced = await page.sitemap();\r\n\r\n for (const entry of produced) entries.push(withDefaults(entry, options.defaults));\r\n\r\n continue;\r\n }\r\n\r\n if (isDynamicRoutePath(page.routePath)) {\r\n unresolvedDynamicRoutes.push(page.routeName);\r\n continue;\r\n }\r\n\r\n entries.push(withDefaults({ path: page.routePath }, options.defaults));\r\n }\r\n\r\n return { entries, unresolvedDynamicRoutes };\r\n}\r\n","/**\n * The dev-mode diagnostic for dynamic routes {@link collectSitemapEntries}\n * (`collect-entries.ts`) could not enumerate — the whole reason this package\n * is written carefully. A dynamic route cannot be enumerated without\n * application data; what the framework controls is whether the developer\n * finds out. Returns `undefined` when there is nothing to report, so a caller\n * can `if (message) console.warn(message)` without an extra length check.\n */\nexport function describeUnresolvedDynamicRoutes(routeNames: readonly string[]): string | undefined {\n if (routeNames.length === 0) return undefined;\n\n const plural = routeNames.length === 1 ? \"\" : \"s\";\n const named = routeNames.map((name) => ` - ${name}`).join(\"\\n\");\n\n return (\n `[warlock:sitemap] ${routeNames.length} dynamic route${plural} ` +\n `${routeNames.length === 1 ? \"has\" : \"have\"} no \\`sitemap\\` export and ` +\n `${routeNames.length === 1 ? \"is\" : \"are\"} OMITTED from sitemap.xml:\\n${named}\\n` +\n \"A dynamic route cannot be enumerated without application data. Add \" +\n \"`export const sitemap: SitemapEntries = async () => [...]` to each page above, \" +\n \"or `export const sitemap = false` to keep it out of the sitemap deliberately.\"\n );\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 * Raised when the sitemap is enabled but no public origin is configured.\n * Refuses to boot rather than falling back to a request-derived origin: a\n * sitemap served with the wrong host is worse than one that refuses to\n * start, because nothing downstream ever tells you it was wrong.\n */\nexport class MissingPublicUrlError extends Error {\n public constructor() {\n super(\n \"Sitemap is enabled but no public origin is configured. Set `app.publicUrl` \" +\n \"in warlock.config.ts, or the PUBLIC_APP_URL environment variable.\",\n );\n this.name = \"MissingPublicUrlError\";\n }\n}\n\nexport type ResolveOriginOptions = {\n /** `app.publicUrl` from the app's config, when set. */\n publicUrl?: string;\n /** Defaults to `process.env`; overridable for tests. */\n env?: Record<string, string | undefined>;\n};\n\n/**\n * The origin the sitemap is served from: `app.publicUrl` first, then the\n * `PUBLIC_APP_URL` env fallback. Throws {@link MissingPublicUrlError} when\n * neither is set — this is the boot-time check, called once, not per-request.\n */\nexport function resolveOrigin(options: ResolveOriginOptions = {}): string {\n const origin = options.publicUrl ?? options.env?.PUBLIC_APP_URL;\n\n if (!origin) throw new MissingPublicUrlError();\n\n return origin;\n}\n","import type { SitemapEntry } from \"./types\";\nimport { joinOrigin } from \"./url\";\n\nconst XML_ESCAPES: Record<string, string> = {\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&apos;\",\n};\n\n/** Escapes the five XML-significant characters. A URL's query string routinely contains `&`. */\nexport function escapeXml(value: string): string {\n // The character class and the table are written together, so the lookup can\n // only miss if one is edited without the other; falling back to the original\n // character keeps that editing mistake from silently emitting `undefined`\n // into a URL.\n return value.replace(/[&<>\"']/g, (char) => XML_ESCAPES[char] ?? char);\n}\n\nfunction entryXml(entry: SitemapEntry, origin: string): string {\n const lines = [` <url>`, ` <loc>${escapeXml(joinOrigin(origin, 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 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 */\nexport function buildSitemapXml(entries: readonly SitemapEntry[], origin: string): string {\n const body = entries.map((entry) => entryXml(entry, origin)).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\">\\n` +\n (body.length > 0 ? `${body}\\n` : \"\") +\n `</urlset>\\n`\n );\n}\n","/**\n * `sitemapConnector()` — the ONE thing `warlock.config.ts` imports from\n * `@warlock.js/sitemap`, and the connector `warlock add sitemap` registers.\n *\n * Deliberately a plain object with TYPE-ONLY imports from core, same\n * reasoning as `queueConnector()`/`webConnector()`: the config file that\n * constructs it must not drag core's runtime graph in at config-load time.\n * `@warlock.js/core` and `@warlock.js/web` are imported lazily, inside\n * `boot()`, where the app has already loaded both.\n */\nimport type { Connector, ConnectorLifecyclePhase, HttpContext } from \"@warlock.js/core\";\nimport { collectSitemapEntries, mergeSitemapEntries, withDefaults } from \"./collect-entries\";\nimport { describeUnresolvedDynamicRoutes } from \"./diagnostic\";\nimport type { RoutablePage } from \"./routable-page\";\nimport type { SitemapConfig, SitemapEntries } from \"./types\";\nimport { resolveOrigin } from \"./url\";\nimport { buildSitemapXml } from \"./xml\";\n\n/** Default path when `src/config/sitemap.ts` does not set one. */\nexport const DEFAULT_SITEMAP_PATH = \"/sitemap.xml\";\n\n/**\n * Boots after the HTTP connector (`ConnectorPriority.HTTP` is `5`) and after\n * web (`5.5`, `web-connector-factory.ts`) — the route it registers has to\n * land on the same router web's pages already share, and `listRoutablePages`\n * only has a page graph to read once web has scanned it.\n */\nexport const SITEMAP_CONNECTOR_PRIORITY = 5.6;\n\nexport type SitemapConnectorOptions = {\n /** Supply the configuration directly instead of reading the `sitemap` config key (`src/config/sitemap.ts`). */\n config?: SitemapConfig;\n /**\n * App-supplied entries — the only source of entries in a Warlock **API-only**\n * project, which has no `@warlock.js/web` page registry for `listRoutablePages()`\n * to read. When `@warlock.js/web` IS installed, these are ADDED to the\n * page-derived entries (see {@link mergeSitemapEntries}), not substituted, so an\n * app with both pages and extra URLs (e.g. rows a database holds) gets both.\n */\n entries?: SitemapEntries;\n};\n\n/**\n * Raised at `boot()` when the sitemap is enabled but has no way to produce\n * entries: `@warlock.js/web` is not installed, so there is no page registry\n * for `listRoutablePages()` to read, AND no `entries` option was supplied.\n * Refuses to boot rather than registering a route that would silently serve\n * an empty `<urlset>` — the same reasoning as {@link MissingPublicUrlError}:\n * a sitemap that looks complete while producing nothing is worse than one\n * that never started.\n */\nexport class NoPageRegistryError extends Error {\n public constructor() {\n super(\n \"Sitemap is enabled but has no source of entries: `@warlock.js/web` is not installed, \" +\n \"so there is no page registry to read, and no `entries` option was supplied either. \" +\n \"Fix this by installing `@warlock.js/web`, or by passing \" +\n \"`sitemapConnector({ entries: async () => [...] })` with your own supplier.\",\n );\n this.name = \"NoPageRegistryError\";\n }\n}\n\n/** Adapts one `listRoutablePages()` result into the package's own minimal `RoutablePage` shape. */\nfunction toRoutablePage(page: {\n routeName: string;\n routePath: string;\n metadata?: unknown;\n sitemap?: unknown;\n}): RoutablePage {\n const metadata = page.metadata;\n const robots =\n metadata !== null && typeof metadata === \"object\" && \"robots\" in metadata\n ? (metadata as { robots?: unknown }).robots\n : undefined;\n\n return {\n routeName: page.routeName,\n routePath: page.routePath,\n robots: typeof robots === \"string\" ? robots : undefined,\n sitemap: page.sitemap as RoutablePage[\"sitemap\"],\n };\n}\n\n/**\n * Construct the sitemap connector.\n *\n * At `boot()`: reads the `sitemap` config (a no-op when `enabled` is not\n * `true`), resolves the public origin ONCE — failing loud via\n * {@link resolveOrigin}'s {@link MissingPublicUrlError} rather than falling\n * back to a request-derived host — and registers `GET <config.path>`.\n *\n * The route itself re-reads the page graph on every request via\n * `listRoutablePages()`, not once at boot: the registry can change under\n * `warlock dev`, and a sitemap that only reflects the app's shape at the\n * moment it booted is stale in exactly the way that made `2ede40cf`-class\n * defects expensive.\n *\n * @example\n * // warlock.config.ts\n * import { sitemapConnector } from \"@warlock.js/sitemap\";\n *\n * export default defineConfig({ connectors: [sitemapConnector()] });\n */\nexport function sitemapConnector(options: SitemapConnectorOptions = {}): Connector {\n let active = false;\n\n const connector: Connector = {\n name: \"sitemap\",\n priority: SITEMAP_CONNECTOR_PRIORITY,\n // Core's `ConnectorLifecyclePhase.Late`; spelled out so this module stays\n // free of a runtime import of core, same as `queueConnector()`.\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\n isActive: () => active,\n async boot() {\n const { config, router } = await import(\"@warlock.js/core\");\n\n const sitemapConfig = options.config ?? config.get<SitemapConfig | undefined>(\"sitemap\");\n\n if (!sitemapConfig?.enabled) {\n return;\n }\n\n const appConfig = config.get<{ publicUrl?: string } | undefined>(\"app\");\n const origin = resolveOrigin({ publicUrl: appConfig?.publicUrl, env: process.env });\n const path = sitemapConfig.path || DEFAULT_SITEMAP_PATH;\n\n // Checked once, at boot: `@warlock.js/web`'s presence can't change per\n // request, and failing here — before the route is even registered —\n // surfaces a misconfigured app at startup instead of on its first hit.\n let listRoutablePages: typeof import(\"@warlock.js/web/build\").listRoutablePages | undefined;\n try {\n ({ listRoutablePages } = await import(\"@warlock.js/web/build\"));\n } catch {\n listRoutablePages = undefined;\n }\n\n if (!listRoutablePages && !options.entries) {\n throw new NoPageRegistryError();\n }\n\n router.get(path, async ({ response }: HttpContext) => {\n const pages = listRoutablePages\n ? (await listRoutablePages({ appRoot: process.cwd() })).map(toRoutablePage)\n : [];\n\n const { entries: pageEntries, unresolvedDynamicRoutes } = await collectSitemapEntries(pages, {\n defaults: sitemapConfig.defaults,\n });\n\n const appEntries = options.entries\n ? (await options.entries()).map((entry) => withDefaults(entry, sitemapConfig.defaults))\n : [];\n\n const entries = mergeSitemapEntries(pageEntries, appEntries);\n\n if (process.env.NODE_ENV !== \"production\") {\n const diagnostic = describeUnresolvedDynamicRoutes(unresolvedDynamicRoutes);\n if (diagnostic) console.warn(diagnostic);\n }\n\n const xml = buildSitemapXml(entries, origin);\n\n return response.setContentType(\"application/xml\").send(xml);\n });\n\n active = true;\n },\n async start() {\n // Nothing to start: the route is registered at boot, once the HTTP\n // connector has built its server but before it listens — the same\n // window `queueConnector()`'s dashboard mount uses.\n },\n async restart() {\n await connector.shutdown();\n await connector.boot();\n },\n async shutdown() {\n active = false;\n },\n shouldRestart(changedFiles: string[]) {\n return changedFiles.some((file) => {\n const normalized = file.replace(/\\\\/g, \"/\");\n\n return normalized === \"src/config/sitemap.ts\" || normalized.endsWith(\"/src/config/sitemap.ts\");\n });\n },\n };\n\n return connector;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../../../../../sitemap/src/errors.ts","../../../../../../sitemap/src/atomic-publish.ts","../../../../../../sitemap/src/atomic-write-file.ts","../../../../../../sitemap/src/lastmod.ts","../../../../../../sitemap/src/normalize-entry.ts","../../../../../../sitemap/src/url.ts","../../../../../../sitemap/src/xml.ts","../../../../../../sitemap/src/sitemap.ts","../../../../../../sitemap/src/sitemap-index-options.ts","../../../../../../sitemap/src/shard-name.ts","../../../../../../sitemap/src/sitemap-shard-writer.ts","../../../../../../sitemap/src/sitemap-index-xml.ts","../../../../../../sitemap/src/duplicate-path-tracker.ts","../../../../../../sitemap/src/route-counter.ts","../../../../../../sitemap/src/sitemap-index.ts"],"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 { ChangeFreq, ResolvedSitemapEntry, SitemapEntry, SitemapOptions } from \"./types\";\n\nconst CHANGE_FREQS: readonly ChangeFreq[] = [\n \"always\",\n \"hourly\",\n \"daily\",\n \"weekly\",\n \"monthly\",\n \"yearly\",\n \"never\",\n];\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\">,\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\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 };\n}\n","import { InvalidBaseUrlError } from \"./errors\";\n\n/**\n * Joins a configured origin and an app-relative route path into one absolute\n * URL, with exactly one slash at the seam regardless of whether either side\n * already carries one.\n */\nexport function joinOrigin(origin: string, routePath: string): string {\n const trimmedOrigin = origin.endsWith(\"/\") ? origin.slice(0, -1) : origin;\n const normalizedPath = routePath.startsWith(\"/\") ? routePath : `/${routePath}`;\n\n return `${trimmedOrigin}${normalizedPath}`;\n}\n\n/**\n * Validates a `baseUrl` and returns it without its trailing slash.\n *\n * `new URL()` accepts `mailto:` and `file:` happily, so the protocol is\n * checked explicitly — a sitemap `<loc>` that is not http(s) is not a document\n * any crawler will fetch.\n */\nexport function normalizeBaseUrl(value: string): string {\n if (typeof value !== \"string\" || value.trim() === \"\") {\n throw new InvalidBaseUrlError(value, \"expected a non-empty string\");\n }\n\n let parsed: URL;\n\n try {\n parsed = new URL(value);\n } catch {\n throw new InvalidBaseUrlError(value, \"not an absolute URL\");\n }\n\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new InvalidBaseUrlError(value, `unsupported protocol \"${parsed.protocol}\"`);\n }\n\n const href = parsed.href;\n\n return href.endsWith(\"/\") ? href.slice(0, -1) : href;\n}\n\n/** True for a value already usable as a `<loc>` without joining an origin. */\nexport function isAbsoluteUrl(value: string): boolean {\n return /^https?:\\/\\//i.test(value);\n}\n\n/** Resolves an entry or alternate path against the base URL, unless it is already absolute. */\nexport function resolveAgainstBase(baseUrl: string, path: string): string {\n return isAbsoluteUrl(path) ? path : joinOrigin(baseUrl, path);\n}\n","import type { ResolvedSitemapEntry } from \"./types\";\nimport { resolveAgainstBase } from \"./url\";\n\nconst XML_ESCAPES: Record<string, string> = {\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&apos;\",\n};\n\n/** Escapes the five XML-significant characters. A URL's query string routinely contains `&`. */\nexport function escapeXml(value: string): string {\n // The character class and the table are written together, so the lookup can\n // only miss if one is edited without the other; falling back to the original\n // character keeps that editing mistake from silently emitting `undefined`\n // into a URL.\n return value.replace(/[&<>\"']/g, (char) => XML_ESCAPES[char] ?? char);\n}\n\nconst XHTML_NAMESPACE = \"http://www.w3.org/1999/xhtml\";\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\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 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 namespaces = hasAlternates ? XHTML_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<SitemapOptions, \"changefreq\" | \"priority\" | \"lastmod\">;\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.defaults = {\n changefreq: options.changefreq,\n priority: options.priority,\n lastmod: options.lastmod,\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 gzip: boolean;\n readonly maxUrlsPerFile: number;\n readonly maxBytesPerFile: number;\n readonly defaults: Pick<SitemapOptions, \"changefreq\" | \"priority\" | \"lastmod\">;\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\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 gzip: options.gzip ?? false,\n maxUrlsPerFile,\n maxBytesPerFile,\n defaults: {\n changefreq: options.changefreq,\n priority: options.priority,\n lastmod: options.lastmod,\n },\n };\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, 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<SitemapOptions, \"changefreq\" | \"priority\" | \"lastmod\">;\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\");\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 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 };\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 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 + (entryHasAlternates && !bufferHasAlternates ? 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\n buffer.push(resolved);\n bufferBytes += blockBytes + (addsNamespaceNow ? NAMESPACE_BYTES : 0);\n if (addsNamespaceNow) bufferHasAlternates = 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(files: readonly ShardFile[], baseUrl: string): string {\n const body = files\n .filter((file) => file.urls > 0)\n .map((file) => {\n const loc = escapeXml(joinOrigin(baseUrl, 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 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: this.options.defaults,\n duplicates,\n routes,\n });\n\n shardFiles.push(...files);\n }\n\n const indexXml = buildSitemapIndexXml(shardFiles, this.options.baseUrl);\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"],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ import { UnownedOutputDirectoryError } from "./errors.mjs";
2
+ import { mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, join } from "node:path";
4
+
5
+ //#region ../sitemap/src/atomic-publish.ts
6
+ /**
7
+ * Marks a directory as one `publishAtomically` swapped into place. Its
8
+ * presence is the ONLY thing that lets a later publish treat the directory
9
+ * as safe to swap out from under itself — see {@link assertOutDirIsOwned}.
10
+ */
11
+ const OWNERSHIP_MARKER_FILE = ".sitemap-set.json";
12
+ const OWNERSHIP_MARKER_VERSION = 1;
13
+ async function validateShards(tempDir, fileNames) {
14
+ for (const fileName of fileNames) {
15
+ const info = await stat(join(tempDir, fileName)).catch(() => void 0);
16
+ if (!info || !info.isFile() || info.size === 0) throw new Error(`sitemap publish aborted: shard "${fileName}" is missing or empty.`);
17
+ }
18
+ }
19
+ /**
20
+ * `outDir` is safe to take over when it does not exist yet, is empty, or
21
+ * already carries {@link OWNERSHIP_MARKER_FILE} from a previous publish.
22
+ * Anything else — a non-empty directory this package never wrote — is
23
+ * refused rather than swapped or deleted (`de97020e`).
24
+ */
25
+ async function assertOutDirIsOwned(outDir) {
26
+ const entries = await readdir(outDir).catch((error) => {
27
+ if (error.code === "ENOENT") return void 0;
28
+ throw error;
29
+ });
30
+ if (entries === void 0 || entries.length === 0) return;
31
+ if (!entries.includes(OWNERSHIP_MARKER_FILE)) throw new UnownedOutputDirectoryError(outDir);
32
+ }
33
+ /**
34
+ * Writes a complete set into a sibling temp directory, validates that every
35
+ * file it named actually exists and is non-empty, then swaps it into
36
+ * `outDir`. A crawler arriving mid-write sees the previous complete set or
37
+ * the new one, never a partial one, and a failed run leaves the previous set
38
+ * untouched and rejects.
39
+ *
40
+ * `write` performs the writes and returns the file names (relative to the
41
+ * temp dir) that must be present for the set to be considered valid — it is
42
+ * only known after writing, since shard count depends on what was walked.
43
+ */
44
+ async function publishAtomically(outDir, write) {
45
+ await assertOutDirIsOwned(outDir);
46
+ const parent = dirname(outDir);
47
+ await mkdir(parent, { recursive: true });
48
+ const tempDir = join(parent, `.${basename(outDir)}.tmp-${process.pid}-${Date.now()}`);
49
+ await mkdir(tempDir, { recursive: true });
50
+ try {
51
+ await validateShards(tempDir, await write(tempDir));
52
+ await writeFile(join(tempDir, OWNERSHIP_MARKER_FILE), JSON.stringify({
53
+ package: "@warlock.js/sitemap",
54
+ version: OWNERSHIP_MARKER_VERSION
55
+ }), "utf8");
56
+ const displacedDir = join(parent, `.${basename(outDir)}.previous-${Date.now()}`);
57
+ let displacedPrevious = false;
58
+ try {
59
+ await rename(outDir, displacedDir);
60
+ displacedPrevious = true;
61
+ } catch (error) {
62
+ if (error.code !== "ENOENT") throw error;
63
+ }
64
+ try {
65
+ await rename(tempDir, outDir);
66
+ } catch (error) {
67
+ if (displacedPrevious) await rename(displacedDir, outDir);
68
+ throw error;
69
+ }
70
+ if (displacedPrevious) await rm(displacedDir, {
71
+ recursive: true,
72
+ force: true
73
+ });
74
+ } catch (error) {
75
+ await rm(tempDir, {
76
+ recursive: true,
77
+ force: true
78
+ });
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ //#endregion
84
+ export { publishAtomically };
85
+ //# sourceMappingURL=atomic-publish.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic-publish.mjs","names":[],"sources":["../../../../../../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"}
@@ -0,0 +1,50 @@
1
+ import { rename, rm, writeFile } from "node:fs/promises";
2
+ import { basename, dirname, join } from "node:path";
3
+
4
+ //#region ../sitemap/src/atomic-write-file.ts
5
+ const defaultDeps = {
6
+ writeFile,
7
+ rename,
8
+ rm
9
+ };
10
+ const RENAME_RETRY_ATTEMPTS = 5;
11
+ const RENAME_RETRY_DELAY_MS = 20;
12
+ function delay(ms) {
13
+ return new Promise((resolve) => setTimeout(resolve, ms));
14
+ }
15
+ /** 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. */
16
+ function isTransientRenameError(error) {
17
+ const code = error?.code;
18
+ return code === "EPERM" || code === "EBUSY";
19
+ }
20
+ async function renameWithRetry(from, to, deps) {
21
+ for (let attempt = 1; attempt <= RENAME_RETRY_ATTEMPTS; attempt++) try {
22
+ await deps.rename(from, to);
23
+ return;
24
+ } catch (error) {
25
+ if (attempt === RENAME_RETRY_ATTEMPTS || !isTransientRenameError(error)) throw error;
26
+ await delay(RENAME_RETRY_DELAY_MS * attempt);
27
+ }
28
+ }
29
+ /**
30
+ * Writes `content` to `filePath` atomically: the content lands in a unique
31
+ * sibling temp file first — same directory, so same filesystem, so the
32
+ * rename that follows is atomic — and only then is renamed over the target.
33
+ * An interrupted or failing write never truncates or otherwise touches the
34
+ * existing target; on any failure the temp file is removed and the error is
35
+ * rethrown.
36
+ */
37
+ async function atomicWriteFile(filePath, content, deps = defaultDeps) {
38
+ const tempPath = join(dirname(filePath), `.${basename(filePath)}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
39
+ try {
40
+ await deps.writeFile(tempPath, content, "utf8");
41
+ await renameWithRetry(tempPath, filePath, deps);
42
+ } catch (error) {
43
+ await deps.rm(tempPath, { force: true }).catch(() => void 0);
44
+ throw error;
45
+ }
46
+ }
47
+
48
+ //#endregion
49
+ export { atomicWriteFile };
50
+ //# sourceMappingURL=atomic-write-file.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic-write-file.mjs","names":[],"sources":["../../../../../../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"}
@@ -0,0 +1,36 @@
1
+ //#region ../sitemap/src/duplicate-path-tracker.ts
2
+ /**
3
+ * The streaming equivalent of `Sitemap`'s `routesByPath` map. A streaming
4
+ * writer cannot compare an entry against 500K predecessors, so it keeps only
5
+ * the paths (and contributing routes) it has already seen. A duplicate is
6
+ * SKIPPED here, not last-wins — the earlier one is already on disk.
7
+ */
8
+ var DuplicatePathTracker = class {
9
+ routesByPath = /* @__PURE__ */ new Map();
10
+ /** Records an attempt to write `path`. Returns `false` when it was already seen — skip it. */
11
+ attempt(path, route) {
12
+ const seen = this.routesByPath.get(path);
13
+ if (seen) {
14
+ seen.push(route);
15
+ return false;
16
+ }
17
+ this.routesByPath.set(path, [route]);
18
+ return true;
19
+ }
20
+ report() {
21
+ const reports = [];
22
+ for (const [path, routes] of this.routesByPath) {
23
+ if (routes.length < 2) continue;
24
+ reports.push({
25
+ path,
26
+ count: routes.length,
27
+ routes: [...routes]
28
+ });
29
+ }
30
+ return reports;
31
+ }
32
+ };
33
+
34
+ //#endregion
35
+ export { DuplicatePathTracker };
36
+ //# sourceMappingURL=duplicate-path-tracker.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duplicate-path-tracker.mjs","names":[],"sources":["../../../../../../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"}
@@ -0,0 +1,39 @@
1
+ //#region ../sitemap/src/errors.d.ts
2
+ /**
3
+ * The `baseUrl` given to a `Sitemap` is not an absolute http(s) URL.
4
+ *
5
+ * Thrown from the CONSTRUCTOR: a sitemap that cannot produce a valid URL
6
+ * should not exist, and the mistake belongs at the line that wrote the value
7
+ * rather than at the first request that reads it.
8
+ */
9
+ declare class InvalidBaseUrlError extends Error {
10
+ constructor(value: unknown, reason: string);
11
+ }
12
+ /** An entry the sitemap protocol cannot represent. Thrown from `add()`. */
13
+ declare class InvalidSitemapEntryError extends Error {
14
+ constructor(reason: string);
15
+ }
16
+ /**
17
+ * Two `SitemapIndex` source keys collide once canonicalised (`en-US` and
18
+ * `en-us` would produce the same filename on a case-insensitive filesystem
19
+ * and silently overwrite one another). Thrown from `addSource()`, not at
20
+ * `saveTo()`, so the mistake is caught at the line that registered it.
21
+ */
22
+ declare class DuplicateSourceKeyError extends Error {
23
+ constructor(key: string);
24
+ }
25
+ /**
26
+ * `SitemapIndex.saveTo(outDir)` swaps the ENTIRE `outDir` for a freshly
27
+ * written set (`atomic-publish.ts`). That is safe only when `outDir` is a
28
+ * directory this package already owns — marked by its own
29
+ * `.sitemap-set.json` from a prior publish. A non-empty directory with no
30
+ * marker is presumed to belong to someone else (a caller's `public/`, most
31
+ * dangerously) and is never swapped or deleted; this is thrown instead, from
32
+ * `saveTo()` before anything is written.
33
+ */
34
+ declare class UnownedOutputDirectoryError extends Error {
35
+ constructor(outDir: string);
36
+ }
37
+ //#endregion
38
+ export { DuplicateSourceKeyError, InvalidBaseUrlError, InvalidSitemapEntryError, UnownedOutputDirectoryError };
39
+ //# sourceMappingURL=errors.d.mts.map
package/esm/errors.mjs ADDED
@@ -0,0 +1,52 @@
1
+ //#region ../sitemap/src/errors.ts
2
+ /**
3
+ * The `baseUrl` given to a `Sitemap` is not an absolute http(s) URL.
4
+ *
5
+ * Thrown from the CONSTRUCTOR: a sitemap that cannot produce a valid URL
6
+ * should not exist, and the mistake belongs at the line that wrote the value
7
+ * rather than at the first request that reads it.
8
+ */
9
+ var InvalidBaseUrlError = class extends Error {
10
+ constructor(value, reason) {
11
+ super(`Invalid sitemap baseUrl ${JSON.stringify(value)}: ${reason}.`);
12
+ this.name = "InvalidBaseUrlError";
13
+ }
14
+ };
15
+ /** An entry the sitemap protocol cannot represent. Thrown from `add()`. */
16
+ var InvalidSitemapEntryError = class extends Error {
17
+ constructor(reason) {
18
+ super(`Invalid sitemap entry: ${reason}.`);
19
+ this.name = "InvalidSitemapEntryError";
20
+ }
21
+ };
22
+ /**
23
+ * Two `SitemapIndex` source keys collide once canonicalised (`en-US` and
24
+ * `en-us` would produce the same filename on a case-insensitive filesystem
25
+ * and silently overwrite one another). Thrown from `addSource()`, not at
26
+ * `saveTo()`, so the mistake is caught at the line that registered it.
27
+ */
28
+ var DuplicateSourceKeyError = class extends Error {
29
+ constructor(key) {
30
+ super(`Duplicate sitemap source key ${JSON.stringify(key)}: keys collide case-insensitively and would overwrite one another's shard files.`);
31
+ this.name = "DuplicateSourceKeyError";
32
+ }
33
+ };
34
+ /**
35
+ * `SitemapIndex.saveTo(outDir)` swaps the ENTIRE `outDir` for a freshly
36
+ * written set (`atomic-publish.ts`). That is safe only when `outDir` is a
37
+ * directory this package already owns — marked by its own
38
+ * `.sitemap-set.json` from a prior publish. A non-empty directory with no
39
+ * marker is presumed to belong to someone else (a caller's `public/`, most
40
+ * dangerously) and is never swapped or deleted; this is thrown instead, from
41
+ * `saveTo()` before anything is written.
42
+ */
43
+ var UnownedOutputDirectoryError = class extends Error {
44
+ constructor(outDir) {
45
+ super(`Refusing to publish a sitemap set to ${JSON.stringify(outDir)}: this directory already has content but no ".sitemap-set.json" marker from a previous @warlock.js/sitemap publish, so it is not safe to swap or delete. Point saveTo() at a dedicated, sitemap-only directory instead.`);
46
+ this.name = "UnownedOutputDirectoryError";
47
+ }
48
+ };
49
+
50
+ //#endregion
51
+ export { DuplicateSourceKeyError, InvalidBaseUrlError, InvalidSitemapEntryError, UnownedOutputDirectoryError };
52
+ //# sourceMappingURL=errors.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../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"}
package/esm/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
- import { ChangeFreq, SitemapConfig, SitemapDefaults, SitemapEntries, SitemapEntry } from "./types.mjs";
2
- import { RoutablePage, SitemapPageExport } from "./routable-page.mjs";
3
- import { CollectSitemapEntriesOptions, CollectSitemapEntriesResult, collectSitemapEntries, isDynamicRoutePath, mergeSitemapEntries, withDefaults } from "./collect-entries.mjs";
4
- import { describeUnresolvedDynamicRoutes } from "./diagnostic.mjs";
5
- import { buildSitemapXml, escapeXml } from "./xml.mjs";
6
- import { MissingPublicUrlError, ResolveOriginOptions, joinOrigin, resolveOrigin } from "./url.mjs";
7
- import { DEFAULT_SITEMAP_PATH, NoPageRegistryError, SITEMAP_CONNECTOR_PRIORITY, SitemapConnectorOptions, sitemapConnector } from "./sitemap-connector.mjs";
8
- export { type ChangeFreq, type CollectSitemapEntriesOptions, type CollectSitemapEntriesResult, DEFAULT_SITEMAP_PATH, MissingPublicUrlError, NoPageRegistryError, type ResolveOriginOptions, type RoutablePage, SITEMAP_CONNECTOR_PRIORITY, type SitemapConfig, type SitemapConnectorOptions, type SitemapDefaults, type SitemapEntries, type SitemapEntry, type SitemapPageExport, buildSitemapXml, collectSitemapEntries, describeUnresolvedDynamicRoutes, escapeXml, isDynamicRoutePath, joinOrigin, mergeSitemapEntries, resolveOrigin, sitemapConnector, withDefaults };
1
+ import { ChangeFreq, DuplicateReport, ResolvedSitemapEntry, RouteSummary, SitemapAlternate, SitemapEntry, SitemapOptions } from "./types.mjs";
2
+ import { Sitemap } from "./sitemap.mjs";
3
+ import { SitemapFileResult, SitemapIndexOptions, SitemapSetResult, SitemapSource, SitemapSourceFactory } from "./sitemap-index-types.mjs";
4
+ import { SitemapIndex } from "./sitemap-index.mjs";
5
+ import { DuplicateSourceKeyError, InvalidBaseUrlError, InvalidSitemapEntryError, UnownedOutputDirectoryError } from "./errors.mjs";
6
+ import { buildSitemapXml, escapeXml, renderUrlBlock } from "./xml.mjs";
7
+ import { joinOrigin } from "./url.mjs";
8
+ export { type ChangeFreq, type DuplicateReport, DuplicateSourceKeyError, InvalidBaseUrlError, InvalidSitemapEntryError, type ResolvedSitemapEntry, type RouteSummary, Sitemap, type SitemapAlternate, type SitemapEntry, type SitemapFileResult, SitemapIndex, type SitemapIndexOptions, type SitemapOptions, type SitemapSetResult, type SitemapSource, type SitemapSourceFactory, UnownedOutputDirectoryError, buildSitemapXml, escapeXml, joinOrigin, renderUrlBlock };
package/esm/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { collectSitemapEntries, isDynamicRoutePath, mergeSitemapEntries, withDefaults } from "./collect-entries.mjs";
2
- import { describeUnresolvedDynamicRoutes } from "./diagnostic.mjs";
3
- import { MissingPublicUrlError, joinOrigin, resolveOrigin } from "./url.mjs";
4
- import { buildSitemapXml, escapeXml } from "./xml.mjs";
5
- import { DEFAULT_SITEMAP_PATH, NoPageRegistryError, SITEMAP_CONNECTOR_PRIORITY, sitemapConnector } from "./sitemap-connector.mjs";
1
+ import { DuplicateSourceKeyError, InvalidBaseUrlError, InvalidSitemapEntryError, UnownedOutputDirectoryError } from "./errors.mjs";
2
+ import { joinOrigin } from "./url.mjs";
3
+ import { buildSitemapXml, escapeXml, renderUrlBlock } from "./xml.mjs";
4
+ import { Sitemap } from "./sitemap.mjs";
5
+ import { SitemapIndex } from "./sitemap-index.mjs";
6
6
 
7
- export { DEFAULT_SITEMAP_PATH, MissingPublicUrlError, NoPageRegistryError, SITEMAP_CONNECTOR_PRIORITY, buildSitemapXml, collectSitemapEntries, describeUnresolvedDynamicRoutes, escapeXml, isDynamicRoutePath, joinOrigin, mergeSitemapEntries, resolveOrigin, sitemapConnector, withDefaults };
7
+ export { DuplicateSourceKeyError, InvalidBaseUrlError, InvalidSitemapEntryError, Sitemap, SitemapIndex, UnownedOutputDirectoryError, buildSitemapXml, escapeXml, joinOrigin, renderUrlBlock };
@@ -0,0 +1,23 @@
1
+ import { InvalidSitemapEntryError } from "./errors.mjs";
2
+
3
+ //#region ../sitemap/src/lastmod.ts
4
+ /**
5
+ * Serialises a `lastmod` value.
6
+ *
7
+ * A `Date` becomes W3C datetime, which is what the schema wants and what
8
+ * `toISOString()` already produces. A string is passed through UNTOUCHED: a
9
+ * caller who already holds an ISO string gets it back verbatim rather than
10
+ * having us re-parse it and risk shifting it across a timezone.
11
+ */
12
+ function formatLastmod(value) {
13
+ if (value instanceof Date) {
14
+ if (Number.isNaN(value.getTime())) throw new InvalidSitemapEntryError("lastmod is an invalid Date");
15
+ return value.toISOString();
16
+ }
17
+ if (typeof value !== "string" || value.trim() === "") throw new InvalidSitemapEntryError("lastmod must be a non-empty string or a Date");
18
+ return value;
19
+ }
20
+
21
+ //#endregion
22
+ export { formatLastmod };
23
+ //# sourceMappingURL=lastmod.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lastmod.mjs","names":[],"sources":["../../../../../../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"}
@@ -0,0 +1,59 @@
1
+ import { InvalidSitemapEntryError } from "./errors.mjs";
2
+ import { formatLastmod } from "./lastmod.mjs";
3
+
4
+ //#region ../sitemap/src/normalize-entry.ts
5
+ const CHANGE_FREQS = [
6
+ "always",
7
+ "hourly",
8
+ "daily",
9
+ "weekly",
10
+ "monthly",
11
+ "yearly",
12
+ "never"
13
+ ];
14
+ function assertChangeFreq(value) {
15
+ if (!CHANGE_FREQS.includes(value)) throw new InvalidSitemapEntryError(`changefreq ${JSON.stringify(value)} is not one of ${CHANGE_FREQS.join(", ")}`);
16
+ }
17
+ function assertPriority(value) {
18
+ if (typeof value !== "number" || Number.isNaN(value) || value < 0 || value > 1) throw new InvalidSitemapEntryError(`priority ${JSON.stringify(value)} is outside the protocol range 0.0–1.0`);
19
+ }
20
+ /** Every stored path carries its leading slash, so `/a` and `a` are one entry, not two. */
21
+ function normalizePath(path) {
22
+ if (typeof path !== "string" || path.trim() === "") throw new InvalidSitemapEntryError("path is required and must be a non-empty string");
23
+ if (/^https?:\/\//i.test(path)) return path;
24
+ return path.startsWith("/") ? path : `/${path}`;
25
+ }
26
+ /**
27
+ * Validates one entry and folds the builder's defaults into it. Defaults are
28
+ * resolved HERE rather than at serialisation time so that `entries()` shows
29
+ * what will actually be emitted — a diagnostic that reports something other
30
+ * than the output is worse than none.
31
+ */
32
+ function normalizeEntry(entry, defaults) {
33
+ const path = normalizePath(entry.path);
34
+ const changefreq = entry.changefreq ?? defaults.changefreq;
35
+ const priority = entry.priority ?? defaults.priority;
36
+ const lastmod = entry.lastmod ?? defaults.lastmod;
37
+ if (changefreq !== void 0) assertChangeFreq(changefreq);
38
+ if (priority !== void 0) assertPriority(priority);
39
+ const alternates = entry.alternates?.map((alternate) => {
40
+ if (typeof alternate?.hreflang !== "string" || alternate.hreflang.trim() === "") throw new InvalidSitemapEntryError("alternate hreflang is required");
41
+ return {
42
+ hreflang: alternate.hreflang,
43
+ path: normalizePath(alternate.path)
44
+ };
45
+ });
46
+ return {
47
+ path,
48
+ ...entry.name !== void 0 ? { name: entry.name } : {},
49
+ ...entry.route !== void 0 ? { route: entry.route } : {},
50
+ ...lastmod !== void 0 ? { lastmod: formatLastmod(lastmod) } : {},
51
+ ...changefreq !== void 0 ? { changefreq } : {},
52
+ ...priority !== void 0 ? { priority } : {},
53
+ ...alternates !== void 0 ? { alternates } : {}
54
+ };
55
+ }
56
+
57
+ //#endregion
58
+ export { normalizeEntry };
59
+ //# sourceMappingURL=normalize-entry.mjs.map