@warlock.js/sitemap 5.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/cjs/index.cjs +279 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/collect-entries.d.mts +44 -0
- package/esm/collect-entries.mjs +73 -0
- package/esm/collect-entries.mjs.map +1 -0
- package/esm/diagnostic.d.mts +13 -0
- package/esm/diagnostic.mjs +19 -0
- package/esm/diagnostic.mjs.map +1 -0
- package/esm/index.d.mts +8 -0
- package/esm/index.mjs +7 -0
- package/esm/routable-page.d.mts +27 -0
- package/esm/sitemap-connector.d.mts +60 -0
- package/esm/sitemap-connector.mjs +117 -0
- package/esm/sitemap-connector.mjs.map +1 -0
- package/esm/types.d.mts +33 -0
- package/esm/url.d.mts +29 -0
- package/esm/url.mjs +35 -0
- package/esm/url.mjs.map +1 -0
- package/esm/xml.d.mts +14 -0
- package/esm/xml.mjs +35 -0
- package/esm/xml.mjs.map +1 -0
- package/llms-full.txt +204 -0
- package/llms.txt +9 -0
- package/package.json +39 -0
- package/skills/sitemap-overview/SKILL.md +194 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collect-entries.mjs","names":[],"sources":["../../../../../../sitemap/src/collect-entries.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"],"mappings":";AAGA,MAAM,kBAAkB;;AAGxB,SAAgB,mBAAmB,WAA4B;CAC7D,OAAO,gBAAgB,KAAK,SAAS;AACvC;AAEA,SAAS,UAAU,QAAqC;CACtD,OAAO,WAAW,UAAa,WAAW,KAAK,MAAM;AACvD;;AAGA,SAAgB,aAAa,OAAqB,UAAqD;CACrG,OAAO;EACL,GAAG;EACH,YAAY,MAAM,cAAc,UAAU;EAC1C,UAAU,MAAM,YAAY,UAAU;CACxC;AACF;;;;;;;;AASA,SAAgB,oBACd,aACA,YACgB;CAChB,MAAM,yBAAS,IAAI,IAA0B;CAE7C,KAAK,MAAM,SAAS,aAAa,OAAO,IAAI,MAAM,MAAM,KAAK;CAC7D,KAAK,MAAM,SAAS,YAAY,OAAO,IAAI,MAAM,MAAM,KAAK;CAE5D,OAAO,MAAM,KAAK,OAAO,OAAO,CAAC;AACnC;;;;;;;;;;;;;;;;;;AA6BA,eAAsB,sBACpB,OACA,UAAwC,CAAC,GACH;CACtC,MAAM,UAA0B,CAAC;CACjC,MAAM,0BAAoC,CAAC;CAE3C,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,UAAU,KAAK,MAAM,GAAG;EAC5B,IAAI,KAAK,YAAY,OAAO;EAE5B,IAAI,OAAO,KAAK,YAAY,YAAY;GACtC,MAAM,WAAW,MAAM,KAAK,QAAQ;GAEpC,KAAK,MAAM,SAAS,UAAU,QAAQ,KAAK,aAAa,OAAO,QAAQ,QAAQ,CAAC;GAEhF;EACF;EAEA,IAAI,mBAAmB,KAAK,SAAS,GAAG;GACtC,wBAAwB,KAAK,KAAK,SAAS;GAC3C;EACF;EAEA,QAAQ,KAAK,aAAa,EAAE,MAAM,KAAK,UAAU,GAAG,QAAQ,QAAQ,CAAC;CACvE;CAEA,OAAO;EAAE;EAAS;CAAwB;AAC5C"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region ../sitemap/src/diagnostic.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The dev-mode diagnostic for dynamic routes {@link collectSitemapEntries}
|
|
4
|
+
* (`collect-entries.ts`) could not enumerate — the whole reason this package
|
|
5
|
+
* is written carefully. A dynamic route cannot be enumerated without
|
|
6
|
+
* application data; what the framework controls is whether the developer
|
|
7
|
+
* finds out. Returns `undefined` when there is nothing to report, so a caller
|
|
8
|
+
* can `if (message) console.warn(message)` without an extra length check.
|
|
9
|
+
*/
|
|
10
|
+
declare function describeUnresolvedDynamicRoutes(routeNames: readonly string[]): string | undefined;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { describeUnresolvedDynamicRoutes };
|
|
13
|
+
//# sourceMappingURL=diagnostic.d.mts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region ../sitemap/src/diagnostic.ts
|
|
2
|
+
/**
|
|
3
|
+
* The dev-mode diagnostic for dynamic routes {@link collectSitemapEntries}
|
|
4
|
+
* (`collect-entries.ts`) could not enumerate — the whole reason this package
|
|
5
|
+
* is written carefully. A dynamic route cannot be enumerated without
|
|
6
|
+
* application data; what the framework controls is whether the developer
|
|
7
|
+
* finds out. Returns `undefined` when there is nothing to report, so a caller
|
|
8
|
+
* can `if (message) console.warn(message)` without an extra length check.
|
|
9
|
+
*/
|
|
10
|
+
function describeUnresolvedDynamicRoutes(routeNames) {
|
|
11
|
+
if (routeNames.length === 0) return void 0;
|
|
12
|
+
const plural = routeNames.length === 1 ? "" : "s";
|
|
13
|
+
const named = routeNames.map((name) => ` - ${name}`).join("\n");
|
|
14
|
+
return `[warlock:sitemap] ${routeNames.length} dynamic route${plural} ${routeNames.length === 1 ? "has" : "have"} no \`sitemap\` export and ${routeNames.length === 1 ? "is" : "are"} OMITTED from sitemap.xml:\n${named}\nA dynamic route cannot be enumerated without application data. Add \`export const sitemap: SitemapEntries = async () => [...]\` to each page above, or \`export const sitemap = false\` to keep it out of the sitemap deliberately.`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { describeUnresolvedDynamicRoutes };
|
|
19
|
+
//# sourceMappingURL=diagnostic.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostic.mjs","names":[],"sources":["../../../../../../sitemap/src/diagnostic.ts"],"sourcesContent":["/**\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"],"mappings":";;;;;;;;;AAQA,SAAgB,gCAAgC,YAAmD;CACjG,IAAI,WAAW,WAAW,GAAG,OAAO;CAEpC,MAAM,SAAS,WAAW,WAAW,IAAI,KAAK;CAC9C,MAAM,QAAQ,WAAW,KAAK,SAAS,OAAO,MAAM,CAAC,CAAC,KAAK,IAAI;CAE/D,OACE,qBAAqB,WAAW,OAAO,gBAAgB,OAAO,GAC3D,WAAW,WAAW,IAAI,QAAQ,OAAO,6BACzC,WAAW,WAAW,IAAI,OAAO,MAAM,8BAA8B,MAAM;AAKlF"}
|
package/esm/index.d.mts
ADDED
|
@@ -0,0 +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 };
|
package/esm/index.mjs
ADDED
|
@@ -0,0 +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";
|
|
6
|
+
|
|
7
|
+
export { DEFAULT_SITEMAP_PATH, MissingPublicUrlError, NoPageRegistryError, SITEMAP_CONNECTOR_PRIORITY, buildSitemapXml, collectSitemapEntries, describeUnresolvedDynamicRoutes, escapeXml, isDynamicRoutePath, joinOrigin, mergeSitemapEntries, resolveOrigin, sitemapConnector, withDefaults };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SitemapEntries } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../sitemap/src/routable-page.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A page's `sitemap` export: the entries function, an explicit opt-out
|
|
6
|
+
* (`export const sitemap = false`), or absent entirely.
|
|
7
|
+
*/
|
|
8
|
+
type SitemapPageExport = SitemapEntries | false | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* The minimal facts {@link collectSitemapEntries} (`collect-entries.ts`) needs
|
|
11
|
+
* about one routable page.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately NOT `DiscoveredRoutablePage` from `@warlock.js/web` — that type
|
|
14
|
+
* carries filesystem paths, layout chains and middleware wiring this package
|
|
15
|
+
* has no business reading. The runtime wiring (a separate package, adapting
|
|
16
|
+
* `discoverPages()` and each page module's exports) builds this shape; this
|
|
17
|
+
* package only ever consumes it.
|
|
18
|
+
*/
|
|
19
|
+
type RoutablePage = {
|
|
20
|
+
/** Unique route identity — named in the dynamic-route diagnostic when unresolved. */routeName: string; /** The effective route path, `:param` for a dynamic segment (e.g. `/posts/:id`). */
|
|
21
|
+
routePath: string; /** `metadata.robots`, when the page's `metadata` export is a static object. */
|
|
22
|
+
robots?: string;
|
|
23
|
+
sitemap?: SitemapPageExport;
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { RoutablePage, SitemapPageExport };
|
|
27
|
+
//# sourceMappingURL=routable-page.d.mts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { SitemapConfig, SitemapEntries } from "./types.mjs";
|
|
2
|
+
import { Connector } from "@warlock.js/core";
|
|
3
|
+
|
|
4
|
+
//#region ../sitemap/src/sitemap-connector.d.ts
|
|
5
|
+
/** Default path when `src/config/sitemap.ts` does not set one. */
|
|
6
|
+
declare const DEFAULT_SITEMAP_PATH = "/sitemap.xml";
|
|
7
|
+
/**
|
|
8
|
+
* Boots after the HTTP connector (`ConnectorPriority.HTTP` is `5`) and after
|
|
9
|
+
* web (`5.5`, `web-connector-factory.ts`) — the route it registers has to
|
|
10
|
+
* land on the same router web's pages already share, and `listRoutablePages`
|
|
11
|
+
* only has a page graph to read once web has scanned it.
|
|
12
|
+
*/
|
|
13
|
+
declare const SITEMAP_CONNECTOR_PRIORITY = 5.6;
|
|
14
|
+
type SitemapConnectorOptions = {
|
|
15
|
+
/** Supply the configuration directly instead of reading the `sitemap` config key (`src/config/sitemap.ts`). */config?: SitemapConfig;
|
|
16
|
+
/**
|
|
17
|
+
* App-supplied entries — the only source of entries in a Warlock **API-only**
|
|
18
|
+
* project, which has no `@warlock.js/web` page registry for `listRoutablePages()`
|
|
19
|
+
* to read. When `@warlock.js/web` IS installed, these are ADDED to the
|
|
20
|
+
* page-derived entries (see {@link mergeSitemapEntries}), not substituted, so an
|
|
21
|
+
* app with both pages and extra URLs (e.g. rows a database holds) gets both.
|
|
22
|
+
*/
|
|
23
|
+
entries?: SitemapEntries;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Raised at `boot()` when the sitemap is enabled but has no way to produce
|
|
27
|
+
* entries: `@warlock.js/web` is not installed, so there is no page registry
|
|
28
|
+
* for `listRoutablePages()` to read, AND no `entries` option was supplied.
|
|
29
|
+
* Refuses to boot rather than registering a route that would silently serve
|
|
30
|
+
* an empty `<urlset>` — the same reasoning as {@link MissingPublicUrlError}:
|
|
31
|
+
* a sitemap that looks complete while producing nothing is worse than one
|
|
32
|
+
* that never started.
|
|
33
|
+
*/
|
|
34
|
+
declare class NoPageRegistryError extends Error {
|
|
35
|
+
constructor();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Construct the sitemap connector.
|
|
39
|
+
*
|
|
40
|
+
* At `boot()`: reads the `sitemap` config (a no-op when `enabled` is not
|
|
41
|
+
* `true`), resolves the public origin ONCE — failing loud via
|
|
42
|
+
* {@link resolveOrigin}'s {@link MissingPublicUrlError} rather than falling
|
|
43
|
+
* back to a request-derived host — and registers `GET <config.path>`.
|
|
44
|
+
*
|
|
45
|
+
* The route itself re-reads the page graph on every request via
|
|
46
|
+
* `listRoutablePages()`, not once at boot: the registry can change under
|
|
47
|
+
* `warlock dev`, and a sitemap that only reflects the app's shape at the
|
|
48
|
+
* moment it booted is stale in exactly the way that made `2ede40cf`-class
|
|
49
|
+
* defects expensive.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // warlock.config.ts
|
|
53
|
+
* import { sitemapConnector } from "@warlock.js/sitemap";
|
|
54
|
+
*
|
|
55
|
+
* export default defineConfig({ connectors: [sitemapConnector()] });
|
|
56
|
+
*/
|
|
57
|
+
declare function sitemapConnector(options?: SitemapConnectorOptions): Connector;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { DEFAULT_SITEMAP_PATH, NoPageRegistryError, SITEMAP_CONNECTOR_PRIORITY, SitemapConnectorOptions, sitemapConnector };
|
|
60
|
+
//# sourceMappingURL=sitemap-connector.d.mts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { collectSitemapEntries, mergeSitemapEntries, withDefaults } from "./collect-entries.mjs";
|
|
2
|
+
import { describeUnresolvedDynamicRoutes } from "./diagnostic.mjs";
|
|
3
|
+
import { resolveOrigin } from "./url.mjs";
|
|
4
|
+
import { buildSitemapXml } from "./xml.mjs";
|
|
5
|
+
|
|
6
|
+
//#region ../sitemap/src/sitemap-connector.ts
|
|
7
|
+
/** Default path when `src/config/sitemap.ts` does not set one. */
|
|
8
|
+
const DEFAULT_SITEMAP_PATH = "/sitemap.xml";
|
|
9
|
+
/**
|
|
10
|
+
* Boots after the HTTP connector (`ConnectorPriority.HTTP` is `5`) and after
|
|
11
|
+
* web (`5.5`, `web-connector-factory.ts`) — the route it registers has to
|
|
12
|
+
* land on the same router web's pages already share, and `listRoutablePages`
|
|
13
|
+
* only has a page graph to read once web has scanned it.
|
|
14
|
+
*/
|
|
15
|
+
const SITEMAP_CONNECTOR_PRIORITY = 5.6;
|
|
16
|
+
/**
|
|
17
|
+
* Raised at `boot()` when the sitemap is enabled but has no way to produce
|
|
18
|
+
* entries: `@warlock.js/web` is not installed, so there is no page registry
|
|
19
|
+
* for `listRoutablePages()` to read, AND no `entries` option was supplied.
|
|
20
|
+
* Refuses to boot rather than registering a route that would silently serve
|
|
21
|
+
* an empty `<urlset>` — the same reasoning as {@link MissingPublicUrlError}:
|
|
22
|
+
* a sitemap that looks complete while producing nothing is worse than one
|
|
23
|
+
* that never started.
|
|
24
|
+
*/
|
|
25
|
+
var NoPageRegistryError = class extends Error {
|
|
26
|
+
constructor() {
|
|
27
|
+
super("Sitemap is enabled but has no source of entries: `@warlock.js/web` is not installed, so there is no page registry to read, and no `entries` option was supplied either. Fix this by installing `@warlock.js/web`, or by passing `sitemapConnector({ entries: async () => [...] })` with your own supplier.");
|
|
28
|
+
this.name = "NoPageRegistryError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/** Adapts one `listRoutablePages()` result into the package's own minimal `RoutablePage` shape. */
|
|
32
|
+
function toRoutablePage(page) {
|
|
33
|
+
const metadata = page.metadata;
|
|
34
|
+
const robots = metadata !== null && typeof metadata === "object" && "robots" in metadata ? metadata.robots : void 0;
|
|
35
|
+
return {
|
|
36
|
+
routeName: page.routeName,
|
|
37
|
+
routePath: page.routePath,
|
|
38
|
+
robots: typeof robots === "string" ? robots : void 0,
|
|
39
|
+
sitemap: page.sitemap
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Construct the sitemap connector.
|
|
44
|
+
*
|
|
45
|
+
* At `boot()`: reads the `sitemap` config (a no-op when `enabled` is not
|
|
46
|
+
* `true`), resolves the public origin ONCE — failing loud via
|
|
47
|
+
* {@link resolveOrigin}'s {@link MissingPublicUrlError} rather than falling
|
|
48
|
+
* back to a request-derived host — and registers `GET <config.path>`.
|
|
49
|
+
*
|
|
50
|
+
* The route itself re-reads the page graph on every request via
|
|
51
|
+
* `listRoutablePages()`, not once at boot: the registry can change under
|
|
52
|
+
* `warlock dev`, and a sitemap that only reflects the app's shape at the
|
|
53
|
+
* moment it booted is stale in exactly the way that made `2ede40cf`-class
|
|
54
|
+
* defects expensive.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // warlock.config.ts
|
|
58
|
+
* import { sitemapConnector } from "@warlock.js/sitemap";
|
|
59
|
+
*
|
|
60
|
+
* export default defineConfig({ connectors: [sitemapConnector()] });
|
|
61
|
+
*/
|
|
62
|
+
function sitemapConnector(options = {}) {
|
|
63
|
+
let active = false;
|
|
64
|
+
const connector = {
|
|
65
|
+
name: "sitemap",
|
|
66
|
+
priority: SITEMAP_CONNECTOR_PRIORITY,
|
|
67
|
+
lifecyclePhase: "late",
|
|
68
|
+
isActive: () => active,
|
|
69
|
+
async boot() {
|
|
70
|
+
const { config, router } = await import("@warlock.js/core");
|
|
71
|
+
const sitemapConfig = options.config ?? config.get("sitemap");
|
|
72
|
+
if (!sitemapConfig?.enabled) return;
|
|
73
|
+
const origin = resolveOrigin({
|
|
74
|
+
publicUrl: config.get("app")?.publicUrl,
|
|
75
|
+
env: process.env
|
|
76
|
+
});
|
|
77
|
+
const path = sitemapConfig.path || "/sitemap.xml";
|
|
78
|
+
let listRoutablePages;
|
|
79
|
+
try {
|
|
80
|
+
({listRoutablePages} = await import("@warlock.js/web/build"));
|
|
81
|
+
} catch {
|
|
82
|
+
listRoutablePages = void 0;
|
|
83
|
+
}
|
|
84
|
+
if (!listRoutablePages && !options.entries) throw new NoPageRegistryError();
|
|
85
|
+
router.get(path, async ({ response }) => {
|
|
86
|
+
const { entries: pageEntries, unresolvedDynamicRoutes } = await collectSitemapEntries(listRoutablePages ? (await listRoutablePages({ appRoot: process.cwd() })).map(toRoutablePage) : [], { defaults: sitemapConfig.defaults });
|
|
87
|
+
const entries = mergeSitemapEntries(pageEntries, options.entries ? (await options.entries()).map((entry) => withDefaults(entry, sitemapConfig.defaults)) : []);
|
|
88
|
+
if (process.env.NODE_ENV !== "production") {
|
|
89
|
+
const diagnostic = describeUnresolvedDynamicRoutes(unresolvedDynamicRoutes);
|
|
90
|
+
if (diagnostic) console.warn(diagnostic);
|
|
91
|
+
}
|
|
92
|
+
const xml = buildSitemapXml(entries, origin);
|
|
93
|
+
return response.setContentType("application/xml").send(xml);
|
|
94
|
+
});
|
|
95
|
+
active = true;
|
|
96
|
+
},
|
|
97
|
+
async start() {},
|
|
98
|
+
async restart() {
|
|
99
|
+
await connector.shutdown();
|
|
100
|
+
await connector.boot();
|
|
101
|
+
},
|
|
102
|
+
async shutdown() {
|
|
103
|
+
active = false;
|
|
104
|
+
},
|
|
105
|
+
shouldRestart(changedFiles) {
|
|
106
|
+
return changedFiles.some((file) => {
|
|
107
|
+
const normalized = file.replace(/\\/g, "/");
|
|
108
|
+
return normalized === "src/config/sitemap.ts" || normalized.endsWith("/src/config/sitemap.ts");
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
return connector;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
export { DEFAULT_SITEMAP_PATH, NoPageRegistryError, SITEMAP_CONNECTOR_PRIORITY, sitemapConnector };
|
|
117
|
+
//# sourceMappingURL=sitemap-connector.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap-connector.mjs","names":[],"sources":["../../../../../../sitemap/src/sitemap-connector.ts"],"sourcesContent":["/**\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":";;;;;;;AAmBA,MAAa,uBAAuB;;;;;;;AAQpC,MAAa,6BAA6B;;;;;;;;;;AAwB1C,IAAa,sBAAb,cAAyC,MAAM;CAC7C,AAAO,cAAc;EACnB,MACE,4SAIF;EACA,KAAK,OAAO;CACd;AACF;;AAGA,SAAS,eAAe,MAKP;CACf,MAAM,WAAW,KAAK;CACtB,MAAM,SACJ,aAAa,QAAQ,OAAO,aAAa,YAAY,YAAY,WAC5D,SAAkC,SACnC;CAEN,OAAO;EACL,WAAW,KAAK;EAChB,WAAW,KAAK;EAChB,QAAQ,OAAO,WAAW,WAAW,SAAS;EAC9C,SAAS,KAAK;CAChB;AACF;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,iBAAiB,UAAmC,CAAC,GAAc;CACjF,IAAI,SAAS;CAEb,MAAM,YAAuB;EAC3B,MAAM;EACN,UAAU;EAGV,gBAAgB;EAChB,gBAAgB;EAChB,MAAM,OAAO;GACX,MAAM,EAAE,QAAQ,WAAW,MAAM,OAAO;GAExC,MAAM,gBAAgB,QAAQ,UAAU,OAAO,IAA+B,SAAS;GAEvF,IAAI,CAAC,eAAe,SAClB;GAIF,MAAM,SAAS,cAAc;IAAE,WADb,OAAO,IAAwC,KACf,CAAC,EAAE;IAAW,KAAK,QAAQ;GAAI,CAAC;GAClF,MAAM,OAAO,cAAc;GAK3B,IAAI;GACJ,IAAI;IACF,CAAC,CAAE,qBAAsB,MAAM,OAAO;GACxC,QAAQ;IACN,oBAAoB;GACtB;GAEA,IAAI,CAAC,qBAAqB,CAAC,QAAQ,SACjC,MAAM,IAAI,oBAAoB;GAGhC,OAAO,IAAI,MAAM,OAAO,EAAE,eAA4B;IAKpD,MAAM,EAAE,SAAS,aAAa,4BAA4B,MAAM,sBAJlD,qBACT,MAAM,kBAAkB,EAAE,SAAS,QAAQ,IAAI,EAAE,CAAC,EAAC,CAAE,IAAI,cAAc,IACxE,CAAC,GAEwF,EAC3F,UAAU,cAAc,SAC1B,CAAC;IAMD,MAAM,UAAU,oBAAoB,aAJjB,QAAQ,WACtB,MAAM,QAAQ,QAAQ,EAAC,CAAE,KAAK,UAAU,aAAa,OAAO,cAAc,QAAQ,CAAC,IACpF,CAAC,CAEsD;IAE3D,IAAI,QAAQ,IAAI,aAAa,cAAc;KACzC,MAAM,aAAa,gCAAgC,uBAAuB;KAC1E,IAAI,YAAY,QAAQ,KAAK,UAAU;IACzC;IAEA,MAAM,MAAM,gBAAgB,SAAS,MAAM;IAE3C,OAAO,SAAS,eAAe,iBAAiB,CAAC,CAAC,KAAK,GAAG;GAC5D,CAAC;GAED,SAAS;EACX;EACA,MAAM,QAAQ,CAId;EACA,MAAM,UAAU;GACd,MAAM,UAAU,SAAS;GACzB,MAAM,UAAU,KAAK;EACvB;EACA,MAAM,WAAW;GACf,SAAS;EACX;EACA,cAAc,cAAwB;GACpC,OAAO,aAAa,MAAM,SAAS;IACjC,MAAM,aAAa,KAAK,QAAQ,OAAO,GAAG;IAE1C,OAAO,eAAe,2BAA2B,WAAW,SAAS,wBAAwB;GAC/F,CAAC;EACH;CACF;CAEA,OAAO;AACT"}
|
package/esm/types.d.mts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region ../sitemap/src/types.d.ts
|
|
2
|
+
/** The `<changefreq>` values the sitemap protocol defines. */
|
|
3
|
+
type ChangeFreq = "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
4
|
+
/**
|
|
5
|
+
* One `<url>` block. `path` is app-relative (`/posts/hello-world`) — this
|
|
6
|
+
* package joins it against the configured origin; see `url.ts`.
|
|
7
|
+
*/
|
|
8
|
+
type SitemapEntry = {
|
|
9
|
+
path: string;
|
|
10
|
+
lastmod?: string;
|
|
11
|
+
changefreq?: ChangeFreq;
|
|
12
|
+
priority?: number;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
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.
|
|
18
|
+
*/
|
|
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;
|
|
24
|
+
};
|
|
25
|
+
/** `src/config/sitemap.ts` — written by `warlock add sitemap`. */
|
|
26
|
+
type SitemapConfig = {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
path: string;
|
|
29
|
+
defaults?: SitemapDefaults;
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { ChangeFreq, SitemapConfig, SitemapDefaults, SitemapEntries, SitemapEntry };
|
|
33
|
+
//# sourceMappingURL=types.d.mts.map
|
package/esm/url.d.mts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region ../sitemap/src/url.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Joins a configured origin and an app-relative route path into one absolute
|
|
4
|
+
* URL, with exactly one slash at the seam regardless of whether either side
|
|
5
|
+
* already carries one.
|
|
6
|
+
*/
|
|
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
|
+
//#endregion
|
|
28
|
+
export { MissingPublicUrlError, ResolveOriginOptions, joinOrigin, resolveOrigin };
|
|
29
|
+
//# sourceMappingURL=url.d.mts.map
|
package/esm/url.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//#region ../sitemap/src/url.ts
|
|
2
|
+
/**
|
|
3
|
+
* Joins a configured origin and an app-relative route path into one absolute
|
|
4
|
+
* URL, with exactly one slash at the seam regardless of whether either side
|
|
5
|
+
* already carries one.
|
|
6
|
+
*/
|
|
7
|
+
function joinOrigin(origin, routePath) {
|
|
8
|
+
return `${origin.endsWith("/") ? origin.slice(0, -1) : origin}${routePath.startsWith("/") ? routePath : `/${routePath}`}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
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.
|
|
15
|
+
*/
|
|
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";
|
|
20
|
+
}
|
|
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;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { MissingPublicUrlError, joinOrigin, resolveOrigin };
|
|
35
|
+
//# sourceMappingURL=url.mjs.map
|
package/esm/url.mjs.map
ADDED
|
@@ -0,0 +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"}
|
package/esm/xml.d.mts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SitemapEntry } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../sitemap/src/xml.d.ts
|
|
4
|
+
/** Escapes the five XML-significant characters. A URL's query string routinely contains `&`. */
|
|
5
|
+
declare function escapeXml(value: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Serialises entries into a `urlset` sitemap document — the sitemaps.org
|
|
8
|
+
* namespace, `<url>` per entry, element order `loc` / `lastmod` / `changefreq`
|
|
9
|
+
* / `priority` (schema order; a validator that checks order rejects any other).
|
|
10
|
+
*/
|
|
11
|
+
declare function buildSitemapXml(entries: readonly SitemapEntry[], origin: string): string;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { buildSitemapXml, escapeXml };
|
|
14
|
+
//# sourceMappingURL=xml.d.mts.map
|
package/esm/xml.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { joinOrigin } from "./url.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../sitemap/src/xml.ts
|
|
4
|
+
const XML_ESCAPES = {
|
|
5
|
+
"&": "&",
|
|
6
|
+
"<": "<",
|
|
7
|
+
">": ">",
|
|
8
|
+
"\"": """,
|
|
9
|
+
"'": "'"
|
|
10
|
+
};
|
|
11
|
+
/** Escapes the five XML-significant characters. A URL's query string routinely contains `&`. */
|
|
12
|
+
function escapeXml(value) {
|
|
13
|
+
return value.replace(/[&<>"']/g, (char) => XML_ESCAPES[char] ?? char);
|
|
14
|
+
}
|
|
15
|
+
function entryXml(entry, origin) {
|
|
16
|
+
const lines = [` <url>`, ` <loc>${escapeXml(joinOrigin(origin, entry.path))}</loc>`];
|
|
17
|
+
if (entry.lastmod !== void 0) lines.push(` <lastmod>${escapeXml(entry.lastmod)}</lastmod>`);
|
|
18
|
+
if (entry.changefreq !== void 0) lines.push(` <changefreq>${entry.changefreq}</changefreq>`);
|
|
19
|
+
if (entry.priority !== void 0) lines.push(` <priority>${entry.priority}</priority>`);
|
|
20
|
+
lines.push(` </url>`);
|
|
21
|
+
return lines.join("\n");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Serialises entries into a `urlset` sitemap document — the sitemaps.org
|
|
25
|
+
* namespace, `<url>` per entry, element order `loc` / `lastmod` / `changefreq`
|
|
26
|
+
* / `priority` (schema order; a validator that checks order rejects any other).
|
|
27
|
+
*/
|
|
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`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { buildSitemapXml, escapeXml };
|
|
35
|
+
//# sourceMappingURL=xml.mjs.map
|
package/esm/xml.mjs.map
ADDED
|
@@ -0,0 +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 \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\n};\n\n/** Escapes the five XML-significant characters. A URL's query string routinely contains `&`. */\nexport function escapeXml(value: string): string {\n // The character class and the table are written together, so the lookup can\n // only miss if one is edited without the other; falling back to the original\n // character keeps that editing mistake from silently emitting `undefined`\n // into a URL.\n return value.replace(/[&<>\"']/g, (char) => XML_ESCAPES[char] ?? char);\n}\n\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"}
|