@warlock.js/sitemap 5.15.0 → 5.17.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/esm/types.d.mts CHANGED
@@ -2,32 +2,63 @@
2
2
  /** The `<changefreq>` values the sitemap protocol defines. */
3
3
  type ChangeFreq = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
4
4
  /**
5
- * One `<url>` block. `path` is app-relative (`/posts/hello-world`) — this
6
- * package joins it against the configured origin; see `url.ts`.
5
+ * One language version of a page, serialised as
6
+ * `<xhtml:link rel="alternate" hreflang="…" href="…"/>`.
7
+ *
8
+ * `hreflang` is not interpreted: `en`, `en-GB` and `x-default` are all just
9
+ * values. This package understands the PROTOCOL concept of an alternate; it
10
+ * does not understand anybody's locale system, and it never derives a
11
+ * locale-prefixed path for you.
12
+ */
13
+ type SitemapAlternate = {
14
+ readonly hreflang: string; /** A path resolved against `baseUrl`, or an absolute URL used as given. */
15
+ readonly path: string;
16
+ };
17
+ /**
18
+ * One `<url>` block. Only `path` is required; `name` and `route` are carried
19
+ * for the diagnostics in `routes()` and `duplicates()` and never serialised.
7
20
  */
8
21
  type SitemapEntry = {
9
- path: string;
10
- lastmod?: string;
11
- changefreq?: ChangeFreq;
12
- priority?: number;
22
+ /** The CONCRETE path: `/posts/123`. A pattern is not a URL. */readonly path: string; /** Optional label for the route this came from: `post-details`. */
23
+ readonly name?: string; /** Optional pattern this came from: `/posts/:id`. */
24
+ readonly route?: string;
25
+ readonly lastmod?: string | Date;
26
+ readonly changefreq?: ChangeFreq;
27
+ readonly priority?: number; /** Language versions of THIS page, conventionally including itself. */
28
+ readonly alternates?: readonly SitemapAlternate[];
13
29
  };
14
30
  /**
15
- * A page's `sitemap` export, when it is a function: produces the entries a
16
- * dynamic route stands for. Runs server-side, at sitemap-build time not
17
- * per-request.
31
+ * An entry after normalisation: the path carries its leading slash, the
32
+ * builder's defaults have been folded in, and `lastmod` is always the
33
+ * serialised string — a `Date` is resolved once, at `add()`, so `toXML()`
34
+ * stays pure and repeatable.
18
35
  */
19
- type SitemapEntries = () => SitemapEntry[] | Promise<SitemapEntry[]>;
20
- /** Fallback `changefreq`/`priority` applied to any entry that omits them. */
21
- type SitemapDefaults = {
22
- changefreq?: ChangeFreq;
23
- priority?: number;
36
+ type ResolvedSitemapEntry = Omit<SitemapEntry, "lastmod"> & {
37
+ readonly lastmod?: string;
24
38
  };
25
- /** `src/config/sitemap.ts` written by `warlock add sitemap`. */
26
- type SitemapConfig = {
27
- enabled: boolean;
28
- path: string;
29
- defaults?: SitemapDefaults;
39
+ type SitemapOptions = {
40
+ /** Absolute origin: `https://example.com` or `https://example.com/base`. */readonly baseUrl: string; /** Applied to every entry that does not set its own. */
41
+ readonly changefreq?: ChangeFreq;
42
+ readonly priority?: number;
43
+ readonly lastmod?: string | Date;
44
+ };
45
+ /**
46
+ * How many URLs a route contributed. A `count: 0` row is the interesting one:
47
+ * the route was declared and produced nothing, so a whole section is missing.
48
+ */
49
+ type RouteSummary = {
50
+ readonly route: string;
51
+ readonly count: number;
52
+ };
53
+ /**
54
+ * A path that was added more than once. The later add wins silently, so this
55
+ * is the only way a caller can see it happened.
56
+ */
57
+ type DuplicateReport = {
58
+ readonly path: string; /** Always >= 2. */
59
+ readonly count: number; /** The `route` of each add, in order, so colliding sources are nameable. */
60
+ readonly routes: readonly (string | undefined)[];
30
61
  };
31
62
  //#endregion
32
- export { ChangeFreq, SitemapConfig, SitemapDefaults, SitemapEntries, SitemapEntry };
63
+ export { ChangeFreq, DuplicateReport, ResolvedSitemapEntry, RouteSummary, SitemapAlternate, SitemapEntry, SitemapOptions };
33
64
  //# sourceMappingURL=types.d.mts.map
package/esm/url.d.mts CHANGED
@@ -5,25 +5,6 @@
5
5
  * already carries one.
6
6
  */
7
7
  declare function joinOrigin(origin: string, routePath: string): string;
8
- /**
9
- * Raised when the sitemap is enabled but no public origin is configured.
10
- * Refuses to boot rather than falling back to a request-derived origin: a
11
- * sitemap served with the wrong host is worse than one that refuses to
12
- * start, because nothing downstream ever tells you it was wrong.
13
- */
14
- declare class MissingPublicUrlError extends Error {
15
- constructor();
16
- }
17
- type ResolveOriginOptions = {
18
- /** `app.publicUrl` from the app's config, when set. */publicUrl?: string; /** Defaults to `process.env`; overridable for tests. */
19
- env?: Record<string, string | undefined>;
20
- };
21
- /**
22
- * The origin the sitemap is served from: `app.publicUrl` first, then the
23
- * `PUBLIC_APP_URL` env fallback. Throws {@link MissingPublicUrlError} when
24
- * neither is set — this is the boot-time check, called once, not per-request.
25
- */
26
- declare function resolveOrigin(options?: ResolveOriginOptions): string;
27
8
  //#endregion
28
- export { MissingPublicUrlError, ResolveOriginOptions, joinOrigin, resolveOrigin };
9
+ export { joinOrigin };
29
10
  //# sourceMappingURL=url.d.mts.map
package/esm/url.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { InvalidBaseUrlError } from "./errors.mjs";
2
+
1
3
  //#region ../sitemap/src/url.ts
2
4
  /**
3
5
  * Joins a configured origin and an app-relative route path into one absolute
@@ -8,28 +10,33 @@ function joinOrigin(origin, routePath) {
8
10
  return `${origin.endsWith("/") ? origin.slice(0, -1) : origin}${routePath.startsWith("/") ? routePath : `/${routePath}`}`;
9
11
  }
10
12
  /**
11
- * Raised when the sitemap is enabled but no public origin is configured.
12
- * Refuses to boot rather than falling back to a request-derived origin: a
13
- * sitemap served with the wrong host is worse than one that refuses to
14
- * start, because nothing downstream ever tells you it was wrong.
13
+ * Validates a `baseUrl` and returns it without its trailing slash.
14
+ *
15
+ * `new URL()` accepts `mailto:` and `file:` happily, so the protocol is
16
+ * checked explicitly a sitemap `<loc>` that is not http(s) is not a document
17
+ * any crawler will fetch.
15
18
  */
16
- var MissingPublicUrlError = class extends Error {
17
- constructor() {
18
- super("Sitemap is enabled but no public origin is configured. Set `app.publicUrl` in warlock.config.ts, or the PUBLIC_APP_URL environment variable.");
19
- this.name = "MissingPublicUrlError";
19
+ function normalizeBaseUrl(value) {
20
+ if (typeof value !== "string" || value.trim() === "") throw new InvalidBaseUrlError(value, "expected a non-empty string");
21
+ let parsed;
22
+ try {
23
+ parsed = new URL(value);
24
+ } catch {
25
+ throw new InvalidBaseUrlError(value, "not an absolute URL");
20
26
  }
21
- };
22
- /**
23
- * The origin the sitemap is served from: `app.publicUrl` first, then the
24
- * `PUBLIC_APP_URL` env fallback. Throws {@link MissingPublicUrlError} when
25
- * neither is set this is the boot-time check, called once, not per-request.
26
- */
27
- function resolveOrigin(options = {}) {
28
- const origin = options.publicUrl ?? options.env?.PUBLIC_APP_URL;
29
- if (!origin) throw new MissingPublicUrlError();
30
- return origin;
27
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new InvalidBaseUrlError(value, `unsupported protocol "${parsed.protocol}"`);
28
+ const href = parsed.href;
29
+ return href.endsWith("/") ? href.slice(0, -1) : href;
30
+ }
31
+ /** True for a value already usable as a `<loc>` without joining an origin. */
32
+ function isAbsoluteUrl(value) {
33
+ return /^https?:\/\//i.test(value);
34
+ }
35
+ /** Resolves an entry or alternate path against the base URL, unless it is already absolute. */
36
+ function resolveAgainstBase(baseUrl, path) {
37
+ return isAbsoluteUrl(path) ? path : joinOrigin(baseUrl, path);
31
38
  }
32
39
 
33
40
  //#endregion
34
- export { MissingPublicUrlError, joinOrigin, resolveOrigin };
41
+ export { joinOrigin, normalizeBaseUrl, resolveAgainstBase };
35
42
  //# sourceMappingURL=url.mjs.map
package/esm/url.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"url.mjs","names":[],"sources":["../../../../../../sitemap/src/url.ts"],"sourcesContent":["/**\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"],"mappings":";;;;;;AAKA,SAAgB,WAAW,QAAgB,WAA2B;CAIpE,OAAO,GAHe,OAAO,SAAS,GAAG,IAAI,OAAO,MAAM,GAAG,EAAE,IAAI,SAC5C,UAAU,WAAW,GAAG,IAAI,YAAY,IAAI;AAGrE;;;;;;;AAQA,IAAa,wBAAb,cAA2C,MAAM;CAC/C,AAAO,cAAc;EACnB,MACE,8IAEF;EACA,KAAK,OAAO;CACd;AACF;;;;;;AAcA,SAAgB,cAAc,UAAgC,CAAC,GAAW;CACxE,MAAM,SAAS,QAAQ,aAAa,QAAQ,KAAK;CAEjD,IAAI,CAAC,QAAQ,MAAM,IAAI,sBAAsB;CAE7C,OAAO;AACT"}
1
+ {"version":3,"file":"url.mjs","names":[],"sources":["../../../../../../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,14 +1,25 @@
1
- import { SitemapEntry } from "./types.mjs";
1
+ import { ResolvedSitemapEntry } from "./types.mjs";
2
2
 
3
3
  //#region ../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
+ /**
7
+ * Renders one `<url>` block. Exported so the streaming writer can measure the
8
+ * exact bytes it is about to append before deciding whether the byte ceiling
9
+ * forces a new shard — a separate approximation could disagree with what is
10
+ * actually written.
11
+ */
12
+ declare function renderUrlBlock(entry: ResolvedSitemapEntry, baseUrl: string): string;
6
13
  /**
7
14
  * Serialises entries into a `urlset` sitemap document — the sitemaps.org
8
15
  * namespace, `<url>` per entry, element order `loc` / `lastmod` / `changefreq`
9
- * / `priority` (schema order; a validator that checks order rejects any other).
16
+ * / `priority` (schema order; a validator that checks order rejects any other),
17
+ * then any `xhtml:link` alternates.
18
+ *
19
+ * The xhtml namespace is declared only when some entry actually carries an
20
+ * alternate — an unused namespace on every single-language sitemap is noise.
10
21
  */
11
- declare function buildSitemapXml(entries: readonly SitemapEntry[], origin: string): string;
22
+ declare function buildSitemapXml(entries: readonly ResolvedSitemapEntry[], baseUrl: string): string;
12
23
  //#endregion
13
- export { buildSitemapXml, escapeXml };
24
+ export { buildSitemapXml, escapeXml, renderUrlBlock };
14
25
  //# sourceMappingURL=xml.d.mts.map
package/esm/xml.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { joinOrigin } from "./url.mjs";
1
+ import { resolveAgainstBase } from "./url.mjs";
2
2
 
3
3
  //#region ../sitemap/src/xml.ts
4
4
  const XML_ESCAPES = {
@@ -12,24 +12,46 @@ const XML_ESCAPES = {
12
12
  function escapeXml(value) {
13
13
  return value.replace(/[&<>"']/g, (char) => XML_ESCAPES[char] ?? char);
14
14
  }
15
- function entryXml(entry, origin) {
16
- const lines = [` <url>`, ` <loc>${escapeXml(joinOrigin(origin, entry.path))}</loc>`];
15
+ const XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
16
+ /**
17
+ * The `xmlns:xhtml` attribute exactly as it appears on `<urlset>`, including
18
+ * its leading space. Exported so the shard writer can charge its byte length
19
+ * against the ceiling without duplicating the string it measures.
20
+ */
21
+ const XHTML_NAMESPACE_ATTR = ` xmlns:xhtml="${XHTML_NAMESPACE}"`;
22
+ /**
23
+ * Renders one `<url>` block. Exported so the streaming writer can measure the
24
+ * exact bytes it is about to append before deciding whether the byte ceiling
25
+ * forces a new shard — a separate approximation could disagree with what is
26
+ * actually written.
27
+ */
28
+ function renderUrlBlock(entry, baseUrl) {
29
+ const lines = [` <url>`, ` <loc>${escapeXml(resolveAgainstBase(baseUrl, entry.path))}</loc>`];
17
30
  if (entry.lastmod !== void 0) lines.push(` <lastmod>${escapeXml(entry.lastmod)}</lastmod>`);
18
31
  if (entry.changefreq !== void 0) lines.push(` <changefreq>${entry.changefreq}</changefreq>`);
19
32
  if (entry.priority !== void 0) lines.push(` <priority>${entry.priority}</priority>`);
33
+ for (const alternate of entry.alternates ?? []) {
34
+ const href = escapeXml(resolveAgainstBase(baseUrl, alternate.path));
35
+ lines.push(` <xhtml:link rel="alternate" hreflang="${escapeXml(alternate.hreflang)}" href="${href}"/>`);
36
+ }
20
37
  lines.push(` </url>`);
21
38
  return lines.join("\n");
22
39
  }
23
40
  /**
24
41
  * Serialises entries into a `urlset` sitemap document — the sitemaps.org
25
42
  * namespace, `<url>` per entry, element order `loc` / `lastmod` / `changefreq`
26
- * / `priority` (schema order; a validator that checks order rejects any other).
43
+ * / `priority` (schema order; a validator that checks order rejects any other),
44
+ * then any `xhtml:link` alternates.
45
+ *
46
+ * The xhtml namespace is declared only when some entry actually carries an
47
+ * alternate — an unused namespace on every single-language sitemap is noise.
27
48
  */
28
- function buildSitemapXml(entries, origin) {
29
- const body = entries.map((entry) => entryXml(entry, origin)).join("\n");
30
- return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n" + (body.length > 0 ? `${body}\n` : "") + `</urlset>\n`;
49
+ function buildSitemapXml(entries, baseUrl) {
50
+ const namespaces = entries.some((entry) => (entry.alternates?.length ?? 0) > 0) ? XHTML_NAMESPACE_ATTR : "";
51
+ const body = entries.map((entry) => renderUrlBlock(entry, baseUrl)).join("\n");
52
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"${namespaces}>\n` + (body.length > 0 ? `${body}\n` : "") + `</urlset>\n`;
31
53
  }
32
54
 
33
55
  //#endregion
34
- export { buildSitemapXml, escapeXml };
56
+ export { XHTML_NAMESPACE_ATTR, buildSitemapXml, escapeXml, renderUrlBlock };
35
57
  //# sourceMappingURL=xml.mjs.map
package/esm/xml.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"xml.mjs","names":[],"sources":["../../../../../../sitemap/src/xml.ts"],"sourcesContent":["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"],"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,SAAS,SAAS,OAAqB,QAAwB;CAC7D,MAAM,QAAQ,CAAC,WAAW,YAAY,UAAU,WAAW,QAAQ,MAAM,IAAI,CAAC,EAAE,OAAO;CAEvF,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,MAAM,KAAK,UAAU;CAErB,OAAO,MAAM,KAAK,IAAI;AACxB;;;;;;AAOA,SAAgB,gBAAgB,SAAkC,QAAwB;CACxF,MAAM,OAAO,QAAQ,KAAK,UAAU,SAAS,OAAO,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI;CAEtE,OACE,kHAEC,KAAK,SAAS,IAAI,GAAG,KAAK,MAAM,MACjC;AAEJ"}
1
+ {"version":3,"file":"xml.mjs","names":[],"sources":["../../../../../../sitemap/src/xml.ts"],"sourcesContent":["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"],"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;;;;;;AAOxB,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,MAAM,KAAK,UAAU;CAErB,OAAO,MAAM,KAAK,IAAI;AACxB;;;;;;;;;;AAWA,SAAgB,gBAAgB,SAA0C,SAAyB;CAEjG,MAAM,aADgB,QAAQ,MAAM,WAAW,MAAM,YAAY,UAAU,KAAK,CACjD,IAAI,uBAAuB;CAC1D,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"}