nimbus-docs 0.1.3 → 0.1.5
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/dist/cli/index.js +611 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/content.d.ts +5 -74
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +2 -2
- package/dist/content.js.map +1 -1
- package/dist/diagnostic-DZf0z79l.d.ts +123 -0
- package/dist/diagnostic-DZf0z79l.d.ts.map +1 -0
- package/dist/index.d.ts +1006 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +779 -174
- package/dist/index.js.map +1 -1
- package/dist/rules-DnAP-j89.js +5836 -0
- package/dist/rules-DnAP-j89.js.map +1 -0
- package/dist/schemas.d.ts +84 -105
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +114 -31
- package/dist/schemas.js.map +1 -1
- package/dist/strict-keys-BiXiT3pq.js +35 -0
- package/dist/strict-keys-BiXiT3pq.js.map +1 -0
- package/dist/types.d.ts +62 -21
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -4
- package/src/components/NimbusHead.astro +0 -4
package/dist/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { o as validateLintOptions, r as suggest, t as IMPLEMENTED_CODES } from "./rules-DnAP-j89.js";
|
|
2
|
+
import { t as withStrictKeys } from "./strict-keys-BiXiT3pq.js";
|
|
1
3
|
import { execFile } from "node:child_process";
|
|
2
4
|
import { promisify } from "node:util";
|
|
5
|
+
import fs from "node:fs";
|
|
3
6
|
import path from "node:path";
|
|
4
7
|
import { fileURLToPath } from "node:url";
|
|
5
8
|
import mdx from "@astrojs/mdx";
|
|
6
9
|
import { satteri } from "@astrojs/markdown-satteri";
|
|
7
10
|
import sitemap from "@astrojs/sitemap";
|
|
8
|
-
import fs from "node:fs/promises";
|
|
9
|
-
import { transformerMetaHighlight, transformerMetaWordHighlight, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight } from "@shikijs/transformers";
|
|
11
|
+
import fs$1 from "node:fs/promises";
|
|
10
12
|
import { z } from "astro/zod";
|
|
13
|
+
import { transformerMetaHighlight, transformerMetaWordHighlight, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight } from "@shikijs/transformers";
|
|
11
14
|
|
|
12
15
|
//#region src/_internal/runtime-config.ts
|
|
13
16
|
let _cached = null;
|
|
@@ -15,8 +18,9 @@ let _cachedCollections = null;
|
|
|
15
18
|
let _cachedAlternates = null;
|
|
16
19
|
async function loadNimbusConfig() {
|
|
17
20
|
if (_cached) return _cached;
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
const value = (await import("virtual:nimbus/config")).config;
|
|
22
|
+
_cached = value;
|
|
23
|
+
return value;
|
|
20
24
|
}
|
|
21
25
|
/**
|
|
22
26
|
* Build-time-resolved list of collections the agent-facing routes
|
|
@@ -25,8 +29,9 @@ async function loadNimbusConfig() {
|
|
|
25
29
|
*/
|
|
26
30
|
async function loadIndexedCollections() {
|
|
27
31
|
if (_cachedCollections) return _cachedCollections;
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
const value = (await import("virtual:nimbus/config")).indexedCollections;
|
|
33
|
+
_cachedCollections = value;
|
|
34
|
+
return value;
|
|
30
35
|
}
|
|
31
36
|
/**
|
|
32
37
|
* Build-time-resolved alternates table for cross-version SEO links.
|
|
@@ -35,14 +40,15 @@ async function loadIndexedCollections() {
|
|
|
35
40
|
*/
|
|
36
41
|
async function loadVersionAlternates() {
|
|
37
42
|
if (_cachedAlternates) return _cachedAlternates;
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
const value = (await import("virtual:nimbus/config")).versionAlternates ?? {};
|
|
44
|
+
_cachedAlternates = value;
|
|
45
|
+
return value;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
//#endregion
|
|
43
49
|
//#region src/_internal/content.ts
|
|
44
50
|
/** Primary collection name. Hard-coded — see also `getDocsStaticPaths`. */
|
|
45
|
-
const PRIMARY_COLLECTION$
|
|
51
|
+
const PRIMARY_COLLECTION$2 = "docs";
|
|
46
52
|
/**
|
|
47
53
|
* Return visible entries from one or more collections. Drafts are
|
|
48
54
|
* filtered out in production builds (matching the existing
|
|
@@ -57,7 +63,7 @@ const PRIMARY_COLLECTION$3 = "docs";
|
|
|
57
63
|
* time. Callers that need per-collection type safety should call
|
|
58
64
|
* `getCollection("api")` directly.
|
|
59
65
|
*/
|
|
60
|
-
async function getVisibleEntries(collections = [PRIMARY_COLLECTION$
|
|
66
|
+
async function getVisibleEntries(collections = [PRIMARY_COLLECTION$2]) {
|
|
61
67
|
const { getCollection } = await import("astro:content");
|
|
62
68
|
const all = (await Promise.all(collections.map((name) => getCollection(name).catch(() => [])))).flat();
|
|
63
69
|
return import.meta.env.PROD ? all.filter((entry) => !entry.data.draft) : all;
|
|
@@ -77,6 +83,191 @@ async function getVisibleEntriesByCollection(collections) {
|
|
|
77
83
|
return out;
|
|
78
84
|
}
|
|
79
85
|
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region ../../node_modules/.pnpm/github-slugger@2.0.0/node_modules/github-slugger/regex.js
|
|
88
|
+
const regex = /[\0-\x1F!-,\.\/:-@\[-\^`\{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482\u0530\u0557\u0558\u055A-\u055F\u0589-\u0590\u05BE\u05C0\u05C3\u05C6\u05C8-\u05CF\u05EB-\u05EE\u05F3-\u060F\u061B-\u061F\u066A-\u066D\u06D4\u06DD\u06DE\u06E9\u06FD\u06FE\u0700-\u070F\u074B\u074C\u07B2-\u07BF\u07F6-\u07F9\u07FB\u07FC\u07FE\u07FF\u082E-\u083F\u085C-\u085F\u086B-\u089F\u08B5\u08C8-\u08D2\u08E2\u0964\u0965\u0970\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09F2-\u09FB\u09FD\u09FF\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF0-\u0AF8\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B54\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B70\u0B72-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BF0-\u0BFF\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5B-\u0C5F\u0C64\u0C65\u0C70-\u0C7F\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0CFF\u0D0D\u0D11\u0D45\u0D49\u0D4F-\u0D53\u0D58-\u0D5E\u0D64\u0D65\u0D70-\u0D79\u0D80\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DE5\u0DF0\u0DF1\u0DF4-\u0E00\u0E3B-\u0E3F\u0E4F\u0E5A-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F01-\u0F17\u0F1A-\u0F1F\u0F2A-\u0F34\u0F36\u0F38\u0F3A-\u0F3D\u0F48\u0F6D-\u0F70\u0F85\u0F98\u0FBD-\u0FC5\u0FC7-\u0FFF\u104A-\u104F\u109E\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u1360-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16ED\u16F9-\u16FF\u170D\u1715-\u171F\u1735-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17D4-\u17D6\u17D8-\u17DB\u17DE\u17DF\u17EA-\u180A\u180E\u180F\u181A-\u181F\u1879-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191F\u192C-\u192F\u193C-\u1945\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DA-\u19FF\u1A1C-\u1A1F\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1AA6\u1AA8-\u1AAF\u1AC1-\u1AFF\u1B4C-\u1B4F\u1B5A-\u1B6A\u1B74-\u1B7F\u1BF4-\u1BFF\u1C38-\u1C3F\u1C4A-\u1C4C\u1C7E\u1C7F\u1C89-\u1C8F\u1CBB\u1CBC\u1CC0-\u1CCF\u1CD3\u1CFB-\u1CFF\u1DFA\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u203E\u2041-\u2053\u2055-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u20CF\u20F1-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u215F\u2189-\u24B5\u24EA-\u2BFF\u2C2F\u2C5F\u2CE5-\u2CEA\u2CF4-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E00-\u2E2E\u2E30-\u3004\u3008-\u3020\u3030\u3036\u3037\u303D-\u3040\u3097\u3098\u309B\u309C\u30A0\u30FB\u3100-\u3104\u3130\u318F-\u319F\u31C0-\u31EF\u3200-\u33FF\u4DC0-\u4DFF\u9FFD-\u9FFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA62C-\uA63F\uA673\uA67E\uA6F2-\uA716\uA720\uA721\uA789\uA78A\uA7C0\uA7C1\uA7CB-\uA7F4\uA828-\uA82B\uA82D-\uA83F\uA874-\uA87F\uA8C6-\uA8CF\uA8DA-\uA8DF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA954-\uA95F\uA97D-\uA97F\uA9C1-\uA9CE\uA9DA-\uA9DF\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A-\uAA5F\uAA77-\uAA79\uAAC3-\uAADA\uAADE\uAADF\uAAF0\uAAF1\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB6A-\uAB6F\uABEB\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFDFF\uFE10-\uFE1F\uFE30-\uFE32\uFE35-\uFE4C\uFE50-\uFE6F\uFE75\uFEFD-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3E\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDD3F\uDD75-\uDDFC\uDDFE-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEDF\uDEE1-\uDEFF\uDF20-\uDF2C\uDF4B-\uDF4F\uDF7B-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0\uDFD6-\uDFFF]|\uD801[\uDC9E\uDC9F\uDCAA-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE04\uDE07-\uDE0B\uDE14\uDE18\uDE36\uDE37\uDE3B-\uDE3E\uDE40-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE7-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCFF\uDD28-\uDD2F\uDD3A-\uDE7F\uDEAA\uDEAD-\uDEAF\uDEB2-\uDEFF\uDF1D-\uDF26\uDF28-\uDF2F\uDF51-\uDFAF\uDFC5-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC47-\uDC65\uDC70-\uDC7E\uDCBB-\uDCCF\uDCE9-\uDCEF\uDCFA-\uDCFF\uDD35\uDD40-\uDD43\uDD48-\uDD4F\uDD74\uDD75\uDD77-\uDD7F\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDFF\uDE12\uDE38-\uDE3D\uDE3F-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEEB-\uDEEF\uDEFA-\uDEFF\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A\uDF45\uDF46\uDF49\uDF4A\uDF4E\uDF4F\uDF51-\uDF56\uDF58-\uDF5C\uDF64\uDF65\uDF6D-\uDF6F\uDF75-\uDFFF]|\uD805[\uDC4B-\uDC4F\uDC5A-\uDC5D\uDC62-\uDC7F\uDCC6\uDCC8-\uDCCF\uDCDA-\uDD7F\uDDB6\uDDB7\uDDC1-\uDDD7\uDDDE-\uDDFF\uDE41-\uDE43\uDE45-\uDE4F\uDE5A-\uDE7F\uDEB9-\uDEBF\uDECA-\uDEFF\uDF1B\uDF1C\uDF2C-\uDF2F\uDF3A-\uDFFF]|\uD806[\uDC3B-\uDC9F\uDCEA-\uDCFE\uDD07\uDD08\uDD0A\uDD0B\uDD14\uDD17\uDD36\uDD39\uDD3A\uDD44-\uDD4F\uDD5A-\uDD9F\uDDA8\uDDA9\uDDD8\uDDD9\uDDE2\uDDE5-\uDDFF\uDE3F-\uDE46\uDE48-\uDE4F\uDE9A-\uDE9C\uDE9E-\uDEBF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC37\uDC41-\uDC4F\uDC5A-\uDC71\uDC90\uDC91\uDCA8\uDCB7-\uDCFF\uDD07\uDD0A\uDD37-\uDD39\uDD3B\uDD3E\uDD48-\uDD4F\uDD5A-\uDD5F\uDD66\uDD69\uDD8F\uDD92\uDD99-\uDD9F\uDDAA-\uDEDF\uDEF7-\uDFAF\uDFB1-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC6F-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80B\uD80E-\uD810\uD812-\uD819\uD824-\uD82B\uD82D\uD82E\uD830-\uD833\uD837\uD839\uD83D\uD83F\uD87B-\uD87D\uD87F\uD885-\uDB3F\uDB41-\uDBFF][\uDC00-\uDFFF]|\uD80D[\uDC2F-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F\uDE6A-\uDECF\uDEEE\uDEEF\uDEF5-\uDEFF\uDF37-\uDF3F\uDF44-\uDF4F\uDF5A-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE80-\uDEFF\uDF4B-\uDF4E\uDF88-\uDF8E\uDFA0-\uDFDF\uDFE2\uDFE5-\uDFEF\uDFF2-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD823[\uDCD6-\uDCFF\uDD09-\uDFFF]|\uD82C[\uDD1F-\uDD4F\uDD53-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDC9C\uDC9F-\uDFFF]|\uD834[\uDC00-\uDD64\uDD6A-\uDD6C\uDD73-\uDD7A\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDE41\uDE45-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC\uDFCD]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85-\uDE9A\uDEA0\uDEB0-\uDFFF]|\uD838[\uDC07\uDC19\uDC1A\uDC22\uDC25\uDC2B-\uDCFF\uDD2D-\uDD2F\uDD3E\uDD3F\uDD4A-\uDD4D\uDD4F-\uDEBF\uDEFA-\uDFFF]|\uD83A[\uDCC5-\uDCCF\uDCD7-\uDCFF\uDD4C-\uDD4F\uDD5A-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD83C[\uDC00-\uDD2F\uDD4A-\uDD4F\uDD6A-\uDD6F\uDD8A-\uDFFF]|\uD83E[\uDC00-\uDFEF\uDFFA-\uDFFF]|\uD869[\uDEDE-\uDEFF]|\uD86D[\uDF35-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uD884[\uDF4B-\uDFFF]|\uDB40[\uDC00-\uDCFF\uDDF0-\uDFFF]/g;
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region ../../node_modules/.pnpm/github-slugger@2.0.0/node_modules/github-slugger/index.js
|
|
92
|
+
const own = Object.hasOwnProperty;
|
|
93
|
+
/**
|
|
94
|
+
* Generate a slug.
|
|
95
|
+
*
|
|
96
|
+
* Does not track previously generated slugs: repeated calls with the same value
|
|
97
|
+
* will result in the exact same slug.
|
|
98
|
+
* Use the `GithubSlugger` class to get unique slugs.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} value
|
|
101
|
+
* String of text to slugify
|
|
102
|
+
* @param {boolean} [maintainCase=false]
|
|
103
|
+
* Keep the current case, otherwise make all lowercase
|
|
104
|
+
* @return {string}
|
|
105
|
+
* A unique slug string
|
|
106
|
+
*/
|
|
107
|
+
function slug(value, maintainCase) {
|
|
108
|
+
if (typeof value !== "string") return "";
|
|
109
|
+
if (!maintainCase) value = value.toLowerCase();
|
|
110
|
+
return value.replace(regex, "").replace(/ /g, "-");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/_internal/astro-slug.ts
|
|
115
|
+
/**
|
|
116
|
+
* Mirror of Astro's content-layer slug normalization, used by every
|
|
117
|
+
* framework URL builder that derives a URL from an `entry.id`.
|
|
118
|
+
*
|
|
119
|
+
* Astro's `glob` content loader (used by Nimbus's `docsCollection` /
|
|
120
|
+
* `partialsCollection` factories) runs each path segment through
|
|
121
|
+
* `github-slugger.slug()` and strips a trailing `/index`. That output is
|
|
122
|
+
* what `entry.id` becomes inside `getCollection`, and it's what `params.slug`
|
|
123
|
+
* substitutes into `[...slug].astro` routes. Anything that constructs a
|
|
124
|
+
* URL from the raw filesystem path *must* apply the same normalization or
|
|
125
|
+
* the URL won't match what Astro actually serves.
|
|
126
|
+
*
|
|
127
|
+
* Why mirror instead of asking Astro: framework URL builders run at page
|
|
128
|
+
* render time (sidebar hrefs) and during the integration's
|
|
129
|
+
* `astro:config:setup` (sitemap, llms.txt). At neither point do we have a
|
|
130
|
+
* clean way to read Astro's resolved routes. The honest architectural fix
|
|
131
|
+
* is to refactor those builders to consume the route manifest at
|
|
132
|
+
* `astro:routes:resolved` (sitemap/llms) or via a build-emitted lookup
|
|
133
|
+
* table (sidebar). Until that work lands, this helper keeps the URLs
|
|
134
|
+
* correct.
|
|
135
|
+
*
|
|
136
|
+
* Mirror caveat: this matches `github-slugger.slug()` and the trailing-
|
|
137
|
+
* /index strip — the documented public-library behaviors Astro inherits
|
|
138
|
+
* — not Astro's private routing internals. If a user supplies a custom
|
|
139
|
+
* `generateId` to the content loader, or a `data.slug` override in
|
|
140
|
+
* frontmatter, this helper doesn't see it. Both are uncommon.
|
|
141
|
+
*/
|
|
142
|
+
/** Canonicalize one entry id the way Astro's content layer does. */
|
|
143
|
+
function canonicalSlug(entryId) {
|
|
144
|
+
const slugged = entryId.split("/").map((segment) => slug(segment)).join("/");
|
|
145
|
+
if (slugged === "index") return "";
|
|
146
|
+
return slugged.endsWith("/index") ? slugged.slice(0, -6) : slugged;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Compose the URL Astro serves for an entry at a given collection prefix.
|
|
150
|
+
*
|
|
151
|
+
* canonicalEntryUrl("", "WIP/index") → "/wip"
|
|
152
|
+
* canonicalEntryUrl("/blog", "first") → "/blog/first"
|
|
153
|
+
* canonicalEntryUrl("/v1", "index") → "/v1"
|
|
154
|
+
*
|
|
155
|
+
* `prefix` is the collection mount path (`""` for the primary `docs`
|
|
156
|
+
* collection, `/blog` for a `blog` collection, `/v1` for a version
|
|
157
|
+
* collection). Pass exactly what your framework prefix resolver returns.
|
|
158
|
+
*/
|
|
159
|
+
function canonicalEntryUrl(prefix, entryId) {
|
|
160
|
+
const slug = canonicalSlug(entryId);
|
|
161
|
+
if (slug === "") return prefix === "" ? "/" : prefix;
|
|
162
|
+
return `${prefix}/${slug}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/_internal/url.ts
|
|
167
|
+
/**
|
|
168
|
+
* Internal URL helpers — one shape for matching, one shape for rendering.
|
|
169
|
+
*
|
|
170
|
+
* Static hosts that serve `page/index.html` (Astro's default `build.format:
|
|
171
|
+
* "directory"`) canonicalize to a trailing-slash URL. If framework helpers
|
|
172
|
+
* emit slashless hrefs, every sidebar click costs a 307 redirect before
|
|
173
|
+
* Astro's client router can pick up the page. The fix splits href shape
|
|
174
|
+
* into two forms:
|
|
175
|
+
*
|
|
176
|
+
* - `toRouteKey(href)` — slashless canonical form. Used wherever the
|
|
177
|
+
* framework compares paths for identity (active sidebar state,
|
|
178
|
+
* prev/next lookup, validation against the indexed route set).
|
|
179
|
+
*
|
|
180
|
+
* - `toBrowserHref(href)` — what we emit into `<a href>` / `<link>` for
|
|
181
|
+
* HTML document routes. Adds a trailing slash so the URL matches the
|
|
182
|
+
* directory-index page the host serves directly.
|
|
183
|
+
*
|
|
184
|
+
* Asset URLs (`.md`, `.png`, `.txt`, …), external URLs, and anchor-only
|
|
185
|
+
* hrefs are returned unchanged by `toBrowserHref` — they aren't HTML
|
|
186
|
+
* document routes and adding a slash would break them.
|
|
187
|
+
*
|
|
188
|
+
* Keep these out of the public API: starter components consume hrefs the
|
|
189
|
+
* framework already shaped. Authors don't (and shouldn't) call these
|
|
190
|
+
* directly.
|
|
191
|
+
*/
|
|
192
|
+
/**
|
|
193
|
+
* True for hrefs that point off-site — anything with a URI scheme
|
|
194
|
+
* (`https:`, `mailto:`, `data:`, …) or a protocol-relative `//cdn.…`
|
|
195
|
+
* prefix. Bare relative paths like `"cli"` and `"./foo"` are NOT external
|
|
196
|
+
* — they resolve against the current page and the framework shouldn't
|
|
197
|
+
* second-guess them.
|
|
198
|
+
*/
|
|
199
|
+
function isAbsoluteUrl(href) {
|
|
200
|
+
return /^([a-z][a-z0-9+\-.]*:|\/\/)/i.test(href);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Detect whether the final path segment looks like a file (has an
|
|
204
|
+
* extension). HTML document routes don't carry an extension under
|
|
205
|
+
* `build.format: "directory"`; assets like `/og/card.png`,
|
|
206
|
+
* `/llms.txt`, and `/cli/index.md` do.
|
|
207
|
+
*
|
|
208
|
+
* Conservative: only treats short, ASCII-letter-only extensions as files,
|
|
209
|
+
* so paths with dots inside a segment (`/v1.2/foo`, version slugs) still
|
|
210
|
+
* count as document routes.
|
|
211
|
+
*/
|
|
212
|
+
function hasFileExtension(pathname) {
|
|
213
|
+
const lastSegment = pathname.slice(pathname.lastIndexOf("/") + 1);
|
|
214
|
+
const dot = lastSegment.lastIndexOf(".");
|
|
215
|
+
if (dot <= 0) return false;
|
|
216
|
+
const ext = lastSegment.slice(dot + 1);
|
|
217
|
+
return ext.length > 0 && ext.length <= 6 && /^[a-zA-Z0-9]+$/.test(ext);
|
|
218
|
+
}
|
|
219
|
+
/** Split an href into `[pathname, suffix]` where `suffix` is the `?…#…` tail. */
|
|
220
|
+
function splitSuffix(href) {
|
|
221
|
+
const queryAt = href.indexOf("?");
|
|
222
|
+
const hashAt = href.indexOf("#");
|
|
223
|
+
const cutAt = queryAt === -1 ? hashAt : hashAt === -1 ? queryAt : Math.min(queryAt, hashAt);
|
|
224
|
+
if (cutAt === -1) return [href, ""];
|
|
225
|
+
return [href.slice(0, cutAt), href.slice(cutAt)];
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Slashless canonical form for path comparisons.
|
|
229
|
+
*
|
|
230
|
+
* /cli → /cli
|
|
231
|
+
* /cli/ → /cli
|
|
232
|
+
* /cli/?x=1#y → /cli
|
|
233
|
+
* / → /
|
|
234
|
+
* /guides/setup/ → /guides/setup
|
|
235
|
+
*
|
|
236
|
+
* Strips query and hash so callers can compare two hrefs that differ only
|
|
237
|
+
* in their tail. Root stays `"/"` — that's identity, not a trailing-slash
|
|
238
|
+
* artifact.
|
|
239
|
+
*/
|
|
240
|
+
function toRouteKey(href) {
|
|
241
|
+
const [pathname] = splitSuffix(href);
|
|
242
|
+
if (pathname.length <= 1) return pathname || "/";
|
|
243
|
+
return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Trailing-slash form for browser-facing hrefs to HTML document routes.
|
|
247
|
+
* Preserves query and hash; root, external URLs, anchor-only hrefs, and
|
|
248
|
+
* asset URLs (paths with a file extension) are returned unchanged.
|
|
249
|
+
*
|
|
250
|
+
* /cli → /cli/
|
|
251
|
+
* /cli/ → /cli/
|
|
252
|
+
* /cli#install → /cli/#install
|
|
253
|
+
* /cli?v=1 → /cli/?v=1
|
|
254
|
+
* / → /
|
|
255
|
+
* /og/card.png → /og/card.png (asset, unchanged)
|
|
256
|
+
* /cli/index.md → /cli/index.md (asset, unchanged)
|
|
257
|
+
* https://x.com/a → https://x.com/a (external, unchanged)
|
|
258
|
+
* #anchor → #anchor (anchor-only, unchanged)
|
|
259
|
+
*/
|
|
260
|
+
function toBrowserHref(href) {
|
|
261
|
+
if (isAbsoluteUrl(href)) return href;
|
|
262
|
+
if (href.startsWith("#") || href.startsWith("?")) return href;
|
|
263
|
+
if (!href.startsWith("/")) return href;
|
|
264
|
+
const [pathname, suffix] = splitSuffix(href);
|
|
265
|
+
if (pathname === "/") return href;
|
|
266
|
+
if (hasFileExtension(pathname)) return href;
|
|
267
|
+
if (pathname.endsWith("/")) return href;
|
|
268
|
+
return `${pathname}/${suffix}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
80
271
|
//#endregion
|
|
81
272
|
//#region src/_internal/sidebar.ts
|
|
82
273
|
const sortKeyByItem = /* @__PURE__ */ new WeakMap();
|
|
@@ -89,17 +280,6 @@ function sortSidebarItems(a, b) {
|
|
|
89
280
|
if (keyDiff !== 0) return keyDiff;
|
|
90
281
|
return a.type.localeCompare(b.type);
|
|
91
282
|
}
|
|
92
|
-
/** Ensure internal href has leading /, no trailing slash (except root) */
|
|
93
|
-
function normalizeInternalHref(href) {
|
|
94
|
-
let h = href.split("?")[0].split("#")[0];
|
|
95
|
-
if (!h.startsWith("/")) h = `/${h}`;
|
|
96
|
-
if (h.length > 1 && h.endsWith("/")) h = h.slice(0, -1);
|
|
97
|
-
return h;
|
|
98
|
-
}
|
|
99
|
-
/** Strip query and hash for active-state matching */
|
|
100
|
-
function stripQueryHash(href) {
|
|
101
|
-
return href.split("?")[0].split("#")[0];
|
|
102
|
-
}
|
|
103
283
|
function buildEntryIndex(entries) {
|
|
104
284
|
const visible = entries.filter((e) => !e.data.sidebar?.hidden);
|
|
105
285
|
const byId = /* @__PURE__ */ new Map();
|
|
@@ -115,9 +295,16 @@ function buildEntryIndex(entries) {
|
|
|
115
295
|
hasChildren
|
|
116
296
|
};
|
|
117
297
|
}
|
|
118
|
-
/**
|
|
298
|
+
/**
|
|
299
|
+
* Compose a final href for an entry. `hrefPrefix` is the collection mount
|
|
300
|
+
* path (e.g. `/api`). Runs through `canonicalEntryUrl` so the href
|
|
301
|
+
* matches what Astro serves (lowercase + folder-index strip — see
|
|
302
|
+
* `_internal/astro-slug.ts` for the why and known caveats), then through
|
|
303
|
+
* `toBrowserHref` so the rendered link is the trailing-slash form static
|
|
304
|
+
* hosts serve directly (no 307 round-trip on click).
|
|
305
|
+
*/
|
|
119
306
|
function joinHref(hrefPrefix, entryId) {
|
|
120
|
-
return
|
|
307
|
+
return toBrowserHref(canonicalEntryUrl(hrefPrefix.replace(/\/$/, ""), entryId));
|
|
121
308
|
}
|
|
122
309
|
function createLink(entry, currentPath, hrefPrefix = "") {
|
|
123
310
|
const href = joinHref(hrefPrefix, entry.id);
|
|
@@ -129,7 +316,7 @@ function createLink(entry, currentPath, hrefPrefix = "") {
|
|
|
129
316
|
type: "link",
|
|
130
317
|
label: entry.data.sidebar?.label ?? entry.data.title,
|
|
131
318
|
href,
|
|
132
|
-
isCurrent: currentPath === href,
|
|
319
|
+
isCurrent: toRouteKey(currentPath) === toRouteKey(href),
|
|
133
320
|
badge,
|
|
134
321
|
order: entry.data.sidebar?.order ?? Number.MAX_VALUE
|
|
135
322
|
};
|
|
@@ -221,6 +408,7 @@ function resolveConfigItems(configItems, entriesByCollection, primaryCollection,
|
|
|
221
408
|
const result = [];
|
|
222
409
|
for (let i = 0; i < configItems.length; i++) {
|
|
223
410
|
const item = configItems[i];
|
|
411
|
+
if (!item) continue;
|
|
224
412
|
const order = orderStart + i;
|
|
225
413
|
if (typeof item === "string") {
|
|
226
414
|
const entry = byId.get(item);
|
|
@@ -229,7 +417,7 @@ function resolveConfigItems(configItems, entriesByCollection, primaryCollection,
|
|
|
229
417
|
link.order = order;
|
|
230
418
|
result.push(link);
|
|
231
419
|
} else console.warn(`[sidebar] Page "${item}" referenced in config but not found in primary collection "${primaryCollection}"`);
|
|
232
|
-
} else if ("link" in item) if (!item.link.startsWith("/")) {
|
|
420
|
+
} else if ("link" in item) if (isAbsoluteUrl(item.link) || !item.link.startsWith("/")) {
|
|
233
421
|
const extLink = {
|
|
234
422
|
type: "external",
|
|
235
423
|
label: item.label,
|
|
@@ -239,15 +427,15 @@ function resolveConfigItems(configItems, entriesByCollection, primaryCollection,
|
|
|
239
427
|
};
|
|
240
428
|
result.push(extLink);
|
|
241
429
|
} else {
|
|
242
|
-
const href =
|
|
243
|
-
const
|
|
244
|
-
const lookup =
|
|
245
|
-
if (!lookup.includes("/") &&
|
|
430
|
+
const href = toBrowserHref(item.link);
|
|
431
|
+
const routeKey = toRouteKey(item.link);
|
|
432
|
+
const lookup = routeKey.slice(1);
|
|
433
|
+
if (lookup !== "" && !lookup.includes("/") && !byId.has(lookup)) console.warn(`[sidebar] Internal link "${item.link}" (label: "${item.label}") does not match any entry in primary collection "${primaryCollection}"`);
|
|
246
434
|
const link = {
|
|
247
435
|
type: "link",
|
|
248
436
|
label: item.label,
|
|
249
437
|
href,
|
|
250
|
-
isCurrent: currentPath ===
|
|
438
|
+
isCurrent: toRouteKey(currentPath) === routeKey,
|
|
251
439
|
badge: item.badge,
|
|
252
440
|
order
|
|
253
441
|
};
|
|
@@ -333,7 +521,7 @@ function deriveSidebarSections(items) {
|
|
|
333
521
|
if (links.length === 0) return [];
|
|
334
522
|
return [{
|
|
335
523
|
label: item.label,
|
|
336
|
-
href: item._prefix ?? links[0].href,
|
|
524
|
+
href: toBrowserHref(item._prefix ?? links[0].href),
|
|
337
525
|
isActive: links.some((link) => link.isCurrent === true)
|
|
338
526
|
}];
|
|
339
527
|
});
|
|
@@ -387,8 +575,8 @@ function processHideChildren(items, entries) {
|
|
|
387
575
|
}
|
|
388
576
|
if (item._indexId) {
|
|
389
577
|
if (entryById.get(item._indexId)?.data.sidebar?.hideChildren) {
|
|
390
|
-
const
|
|
391
|
-
const indexLink = item.children.find((c) => c.type === "link" && c.href ===
|
|
578
|
+
const indexKey = toRouteKey(canonicalEntryUrl("", item._indexId));
|
|
579
|
+
const indexLink = item.children.find((c) => c.type === "link" && toRouteKey(c.href) === indexKey);
|
|
392
580
|
if (indexLink) {
|
|
393
581
|
const link = {
|
|
394
582
|
...indexLink,
|
|
@@ -451,6 +639,54 @@ function sidebarHash(items) {
|
|
|
451
639
|
return (hash >>> 0).toString(36).padStart(7, "0");
|
|
452
640
|
}
|
|
453
641
|
|
|
642
|
+
//#endregion
|
|
643
|
+
//#region src/_internal/collection-mount.ts
|
|
644
|
+
/**
|
|
645
|
+
* Collection-mount conventions — one source of truth for "what URL prefix
|
|
646
|
+
* does collection X serve at?".
|
|
647
|
+
*
|
|
648
|
+
* Shared between `index.ts` (which uses it for `getIndexedEntries`,
|
|
649
|
+
* `getDocsPageProps`, etc.) and `lint/site-model.ts` (where
|
|
650
|
+
* `findDuplicateRoutes` needs it to detect cross-collection URL
|
|
651
|
+
* collisions). Keeping the function here prevents the duplicate-slug
|
|
652
|
+
* validator from drifting out of sync with the actual routing.
|
|
653
|
+
*/
|
|
654
|
+
/** Primary collection name — mounted at the site root with no prefix. */
|
|
655
|
+
const PRIMARY_COLLECTION$1 = "docs";
|
|
656
|
+
/**
|
|
657
|
+
* Resolve the URL-prefix segment for a given collection name.
|
|
658
|
+
*
|
|
659
|
+
* 1. Primary `docs` collection mounts at root → returns `""`.
|
|
660
|
+
* 2. With `versions` configured, a `docs-<slug>` collection whose slug
|
|
661
|
+
* appears in `versions.others` mounts under `/<slug>` (the version
|
|
662
|
+
* label, not the collection id).
|
|
663
|
+
* 3. Any other collection (`api`, `blog`, …) mounts at `/<collection>`.
|
|
664
|
+
*
|
|
665
|
+
* Returned shape: empty string OR `/<segment>` with leading slash, no
|
|
666
|
+
* trailing slash. Callers append `/<entryId>` or `/index.md`.
|
|
667
|
+
*/
|
|
668
|
+
function collectionMountPrefix(collection, versions) {
|
|
669
|
+
if (collection === PRIMARY_COLLECTION$1) return "";
|
|
670
|
+
if (versions && collection.startsWith("docs-")) {
|
|
671
|
+
const slug = collection.slice(5);
|
|
672
|
+
if (versions.others.includes(slug)) return `/${slug}`;
|
|
673
|
+
}
|
|
674
|
+
return `/${collection}`;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Resolve the URL-safe label a collection is referenced by — the segment
|
|
678
|
+
* that appears in URLs and section headers. For version collections this
|
|
679
|
+
* is the manifest's short slug; for everything else it's the collection id.
|
|
680
|
+
*/
|
|
681
|
+
function collectionLabel(collection, versions) {
|
|
682
|
+
if (collection === PRIMARY_COLLECTION$1) return collection;
|
|
683
|
+
if (versions && collection.startsWith("docs-")) {
|
|
684
|
+
const slug = collection.slice(5);
|
|
685
|
+
if (versions.others.includes(slug)) return slug;
|
|
686
|
+
}
|
|
687
|
+
return collection;
|
|
688
|
+
}
|
|
689
|
+
|
|
454
690
|
//#endregion
|
|
455
691
|
//#region src/_internal/transform.ts
|
|
456
692
|
function protectCode(markdown) {
|
|
@@ -610,16 +846,11 @@ function getBreadcrumbs$1(slug, homeLabel = "Home") {
|
|
|
610
846
|
path += `/${part}`;
|
|
611
847
|
crumbs.push({
|
|
612
848
|
label: part.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
613
|
-
href: path
|
|
849
|
+
href: toBrowserHref(path)
|
|
614
850
|
});
|
|
615
851
|
}
|
|
616
852
|
return crumbs;
|
|
617
853
|
}
|
|
618
|
-
function normalizeInternalPath(path) {
|
|
619
|
-
const [withoutHash] = path.split("#", 1);
|
|
620
|
-
const [pathname] = withoutHash.split("?", 1);
|
|
621
|
-
return pathname || "/";
|
|
622
|
-
}
|
|
623
854
|
function resolveOverride(override, fallback, validInternalLinks) {
|
|
624
855
|
if (override === false) return void 0;
|
|
625
856
|
if (override === void 0) return fallback;
|
|
@@ -632,11 +863,11 @@ function resolveOverride(override, fallback, validInternalLinks) {
|
|
|
632
863
|
}
|
|
633
864
|
if (override.link && !override.link.startsWith("/") && !override.link.startsWith("http")) throw new Error(`prev/next override link "${override.link}" must be an absolute path (starting with /) or a full URL`);
|
|
634
865
|
if (override.link?.startsWith("/") && validInternalLinks) {
|
|
635
|
-
const targetPath =
|
|
866
|
+
const targetPath = toRouteKey(override.link);
|
|
636
867
|
if (!validInternalLinks.has(targetPath)) throw new Error(`prev/next override link "${override.link}" does not match any existing internal docs route`);
|
|
637
868
|
}
|
|
638
869
|
const label = override.label ?? fallback?.label;
|
|
639
|
-
const href = override.link
|
|
870
|
+
const href = override.link ? toBrowserHref(override.link) : fallback?.href;
|
|
640
871
|
if (!fallback && (label === void 0 || href === void 0)) throw new Error("prev/next object override requires both `label` and `link` when no sidebar neighbor exists");
|
|
641
872
|
if (!href) return void 0;
|
|
642
873
|
return {
|
|
@@ -646,7 +877,8 @@ function resolveOverride(override, fallback, validInternalLinks) {
|
|
|
646
877
|
}
|
|
647
878
|
function getPrevNext$1(currentPath, sidebarTree, overrides, validInternalLinks) {
|
|
648
879
|
const flat = flattenSidebar(sidebarTree);
|
|
649
|
-
const
|
|
880
|
+
const currentKey = toRouteKey(currentPath);
|
|
881
|
+
const index = flat.findIndex((item) => toRouteKey(item.href) === currentKey);
|
|
650
882
|
const sidebarPrev = index > 0 ? {
|
|
651
883
|
label: flat[index - 1].label,
|
|
652
884
|
href: flat[index - 1].href
|
|
@@ -768,7 +1000,7 @@ const EXPORT_PATTERN = /export\s+const\s+components\s*(?::\s*[^=]+)?=\s*\{([\s\S
|
|
|
768
1000
|
async function parseComponentsRegistry(filePath) {
|
|
769
1001
|
let source;
|
|
770
1002
|
try {
|
|
771
|
-
source = await fs.readFile(filePath, "utf8");
|
|
1003
|
+
source = await fs$1.readFile(filePath, "utf8");
|
|
772
1004
|
} catch (err) {
|
|
773
1005
|
if (err.code === "ENOENT") return null;
|
|
774
1006
|
throw err;
|
|
@@ -820,6 +1052,178 @@ function splitTopLevelCommas$1(input) {
|
|
|
820
1052
|
return result;
|
|
821
1053
|
}
|
|
822
1054
|
|
|
1055
|
+
//#endregion
|
|
1056
|
+
//#region src/lint/site-model.ts
|
|
1057
|
+
/**
|
|
1058
|
+
* Lint-side data shapes + the `nimbus/duplicate-slug` build validator.
|
|
1059
|
+
*
|
|
1060
|
+
* Route truth for `nimbus/internal-link` comes from Astro itself
|
|
1061
|
+
* (`astro:build:done` hands us the emitted `pages` array — the single
|
|
1062
|
+
* source of truth for served URLs). The integration writes that to
|
|
1063
|
+
* `.nimbus/routes.json`; the type lives here only so the rule and the
|
|
1064
|
+
* writer agree on the shape.
|
|
1065
|
+
*
|
|
1066
|
+
* The duplicate-slug validator runs *before* the build because Astro
|
|
1067
|
+
* silently dedupes colliding routes — by the time `astro:build:done`
|
|
1068
|
+
* fires, one entry has already shadowed the other. We catch collisions
|
|
1069
|
+
* pre-build by computing each entry's canonical slug with the same
|
|
1070
|
+
* library Astro's content layer uses (`github-slugger`), then grouping
|
|
1071
|
+
* entries by collection + slug. This is mirror-behavior, but mirroring
|
|
1072
|
+
* a documented public library rather than Astro's private internals —
|
|
1073
|
+
* a much smaller maintenance surface.
|
|
1074
|
+
*/
|
|
1075
|
+
/**
|
|
1076
|
+
* Walk `src/content/` using a `(collection key → folder name)` map so
|
|
1077
|
+
* entries are tagged with the registered key, not the on-disk folder.
|
|
1078
|
+
* Skips folders that aren't in the map (they're loose content, not a
|
|
1079
|
+
* routed collection).
|
|
1080
|
+
*/
|
|
1081
|
+
function enumerateEntriesByBase(contentRoot, bases) {
|
|
1082
|
+
const folderToKey = /* @__PURE__ */ new Map();
|
|
1083
|
+
for (const [key, base] of bases) folderToKey.set(base, key);
|
|
1084
|
+
const out = [];
|
|
1085
|
+
for (const [folder, key] of folderToKey) {
|
|
1086
|
+
const baseDir = path.join(contentRoot, folder);
|
|
1087
|
+
walkFolder(baseDir, baseDir, (relInBase) => {
|
|
1088
|
+
out.push({
|
|
1089
|
+
collection: key,
|
|
1090
|
+
id: relInBase.replace(/\.mdx$/, ""),
|
|
1091
|
+
relPath: `${folder}/${relInBase}`
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
return out;
|
|
1096
|
+
}
|
|
1097
|
+
function walkFolder(dir, root, visit) {
|
|
1098
|
+
let dirents;
|
|
1099
|
+
try {
|
|
1100
|
+
dirents = fs.readdirSync(dir, { withFileTypes: true });
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
if (err.code === "ENOENT") return;
|
|
1103
|
+
throw err;
|
|
1104
|
+
}
|
|
1105
|
+
for (const entry of dirents) {
|
|
1106
|
+
const full = path.join(dir, entry.name);
|
|
1107
|
+
if (entry.isDirectory()) {
|
|
1108
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
1109
|
+
walkFolder(full, root, visit);
|
|
1110
|
+
} else if (entry.isFile() && entry.name.endsWith(".mdx")) visit(path.relative(root, full).replace(/\\/g, "/"));
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Group route owners by URL. A group with more than one member is a real
|
|
1115
|
+
* collision Astro will silently shadow at build time — that's what this
|
|
1116
|
+
* helper exists to surface before the build wastes a cycle.
|
|
1117
|
+
*
|
|
1118
|
+
* Owners come from two sources, both fed in by the integration:
|
|
1119
|
+
*
|
|
1120
|
+
* - **Content entries**, with URLs computed via
|
|
1121
|
+
* `collectionMountPrefix(entry.collection, versions) +
|
|
1122
|
+
* canonicalEntryUrl(prefix, entry.id)`. Catches cross-collection
|
|
1123
|
+
* (`docs/blog/post.mdx` vs `blog/post.mdx`), version-collection
|
|
1124
|
+
* (`docs/v1/x.mdx` vs `docs-v1/x.mdx`), case-only, and
|
|
1125
|
+
* leaf-vs-folder-index collisions Astro's content layer dedupes.
|
|
1126
|
+
* - **Static `src/pages/**` files** (via `enumerateStaticPageRoutes`).
|
|
1127
|
+
* Catches the page-vs-content collision (`pages/search.astro`
|
|
1128
|
+
* shadowing `content/docs/search.mdx` at `/search`).
|
|
1129
|
+
*
|
|
1130
|
+
* Doesn't honor `data.slug` frontmatter overrides — entries that use those
|
|
1131
|
+
* may produce false negatives. Reading frontmatter from every entry
|
|
1132
|
+
* pre-build would add noticeable I/O for a v1 feature; tracked as a
|
|
1133
|
+
* follow-up.
|
|
1134
|
+
*/
|
|
1135
|
+
function findDuplicateRoutes(owners) {
|
|
1136
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
1137
|
+
for (const { url, source } of owners) {
|
|
1138
|
+
const bucket = byUrl.get(url);
|
|
1139
|
+
if (bucket) bucket.push(source);
|
|
1140
|
+
else byUrl.set(url, [source]);
|
|
1141
|
+
}
|
|
1142
|
+
const dups = [];
|
|
1143
|
+
for (const [url, sources] of byUrl) if (sources.length > 1) dups.push({
|
|
1144
|
+
url,
|
|
1145
|
+
sources
|
|
1146
|
+
});
|
|
1147
|
+
return dups;
|
|
1148
|
+
}
|
|
1149
|
+
/** Format duplicate groups into a build-error message. */
|
|
1150
|
+
function formatDuplicateRoutes(dups) {
|
|
1151
|
+
const lines = dups.map((d) => ` ${d.url} ← ${d.sources.join(", ")}`);
|
|
1152
|
+
return `[nimbus-docs] Duplicate ${dups.length === 1 ? "route is" : "routes are"} claimed by more than one source (nimbus/duplicate-slug):\n` + lines.join("\n") + "\n\nTwo or more sources resolve to the same URL — one would shadow the other on the deployed site (Astro silently dedupes colliding routes). Rename or move one source in each pair.";
|
|
1153
|
+
}
|
|
1154
|
+
const PAGE_EXTS = new Set([
|
|
1155
|
+
".astro",
|
|
1156
|
+
".ts",
|
|
1157
|
+
".js",
|
|
1158
|
+
".md",
|
|
1159
|
+
".mdx"
|
|
1160
|
+
]);
|
|
1161
|
+
function enumerateStaticPageRoutes(pagesRoot, projectRoot) {
|
|
1162
|
+
const out = [];
|
|
1163
|
+
walkPages(pagesRoot, pagesRoot, (relPath) => {
|
|
1164
|
+
const ext = path.extname(relPath);
|
|
1165
|
+
if (!PAGE_EXTS.has(ext)) return;
|
|
1166
|
+
if (path.basename(relPath).startsWith("_")) return;
|
|
1167
|
+
const parts = relPath.replace(/\\/g, "/").split("/");
|
|
1168
|
+
const bareLeaf = parts[parts.length - 1].replace(/\.[^.]+$/, "");
|
|
1169
|
+
if (parts.slice(0, -1).some(isDynamicSegment) || isDynamicSegment(bareLeaf)) return;
|
|
1170
|
+
const url = fileToRoute(parts, ext);
|
|
1171
|
+
const sourceAbs = path.join(pagesRoot, relPath);
|
|
1172
|
+
const source = path.relative(projectRoot, sourceAbs).replace(/\\/g, "/");
|
|
1173
|
+
out.push({
|
|
1174
|
+
url,
|
|
1175
|
+
source
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
1178
|
+
return out;
|
|
1179
|
+
}
|
|
1180
|
+
function walkPages(dir, root, visit) {
|
|
1181
|
+
let dirents;
|
|
1182
|
+
try {
|
|
1183
|
+
dirents = fs.readdirSync(dir, { withFileTypes: true });
|
|
1184
|
+
} catch (err) {
|
|
1185
|
+
if (err.code === "ENOENT") return;
|
|
1186
|
+
throw err;
|
|
1187
|
+
}
|
|
1188
|
+
for (const entry of dirents) {
|
|
1189
|
+
const full = path.join(dir, entry.name);
|
|
1190
|
+
if (entry.isDirectory()) {
|
|
1191
|
+
if (entry.name.startsWith("_") || entry.name === "node_modules") continue;
|
|
1192
|
+
walkPages(full, root, visit);
|
|
1193
|
+
} else if (entry.isFile()) visit(path.relative(root, full));
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
function isDynamicSegment(seg) {
|
|
1197
|
+
return seg.startsWith("[") && seg.endsWith("]") && seg.length >= 3;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Translate a static page file path (no dynamic segments) to its URL.
|
|
1201
|
+
*
|
|
1202
|
+
* pages/index.astro → /
|
|
1203
|
+
* pages/search.astro → /search
|
|
1204
|
+
* pages/Search.astro → /search (Astro lowercases via joinSegments)
|
|
1205
|
+
* pages/llms.txt.ts → /llms.txt (endpoint: strip .ts, keep .txt)
|
|
1206
|
+
* pages/blog/index.astro → /blog
|
|
1207
|
+
* pages/blog/post.md → /blog/post
|
|
1208
|
+
*/
|
|
1209
|
+
function fileToRoute(parts, ext) {
|
|
1210
|
+
const cloned = [...parts];
|
|
1211
|
+
const last = cloned[cloned.length - 1];
|
|
1212
|
+
if ((ext === ".ts" || ext === ".js") && /\.[^.]+\.[tj]s$/.test(last)) cloned[cloned.length - 1] = last.replace(/\.[tj]s$/, "");
|
|
1213
|
+
else cloned[cloned.length - 1] = last.replace(/\.[^.]+$/, "");
|
|
1214
|
+
if (cloned[cloned.length - 1] === "index") cloned.pop();
|
|
1215
|
+
const joined = cloned.map((s) => s.toLowerCase()).join("/");
|
|
1216
|
+
return joined === "" ? "/" : `/${joined}`;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Compute the mounted URL a content entry resolves to. Exposed so callers
|
|
1220
|
+
* (the integration's pre-build dup-check) can build `RouteOwner` records
|
|
1221
|
+
* with the same logic the framework uses everywhere else.
|
|
1222
|
+
*/
|
|
1223
|
+
function contentEntryUrl(entry, versions) {
|
|
1224
|
+
return canonicalEntryUrl(collectionMountPrefix(entry.collection, versions), entry.id);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
823
1227
|
//#endregion
|
|
824
1228
|
//#region src/_internal/parse-content-collections.ts
|
|
825
1229
|
/**
|
|
@@ -859,7 +1263,7 @@ const EXPORT_PREFIX_PATTERN = /export\s+const\s+collections\s*(?::\s*[^=]+)?=\s*
|
|
|
859
1263
|
async function parseContentCollections(filePath) {
|
|
860
1264
|
let source;
|
|
861
1265
|
try {
|
|
862
|
-
source = await fs.readFile(filePath, "utf8");
|
|
1266
|
+
source = await fs$1.readFile(filePath, "utf8");
|
|
863
1267
|
} catch (err) {
|
|
864
1268
|
if (err.code === "ENOENT") return null;
|
|
865
1269
|
throw err;
|
|
@@ -884,6 +1288,181 @@ async function parseContentCollections(filePath) {
|
|
|
884
1288
|
return names;
|
|
885
1289
|
}
|
|
886
1290
|
/**
|
|
1291
|
+
* Sibling of `parseContentCollections` that also extracts each entry's
|
|
1292
|
+
* `base` option — the folder name under `src/content/` the collection
|
|
1293
|
+
* loads from. Returns a `Map<key, folderName>` where the folder defaults
|
|
1294
|
+
* to the collection key when no `base:` override is present.
|
|
1295
|
+
*
|
|
1296
|
+
* Used by the `nimbus/duplicate-slug` validator and any other framework
|
|
1297
|
+
* code that needs to walk a collection's actual on-disk location rather
|
|
1298
|
+
* than assuming the folder name matches the collection key. Astro's
|
|
1299
|
+
* content layer respects the `base` option — `docsCollection({ base:
|
|
1300
|
+
* "documentation" })` loads from `src/content/documentation/` while still
|
|
1301
|
+
* registering as collection `docs`. Without this map, filesystem-walking
|
|
1302
|
+
* code would mis-tag those entries (`collection: "documentation"`) and
|
|
1303
|
+
* either flag bogus collisions or silently skip them via the indexable-
|
|
1304
|
+
* collections filter.
|
|
1305
|
+
*
|
|
1306
|
+
* Extraction is regex-based — for each entry's value text, looks for
|
|
1307
|
+
* `\bbase:\s*["']([^"']+)["']`. That covers the documented Nimbus pattern
|
|
1308
|
+
* (`docsCollection({ base: "..." })`, `partialsCollection({ base: "..." })`,
|
|
1309
|
+
* `componentsCollection({ base: "..." })`). Limitations:
|
|
1310
|
+
*
|
|
1311
|
+
* - Computed/dynamic bases (`base: someVar`) fall back to the collection
|
|
1312
|
+
* key. A future regression would silently miscount; for now,
|
|
1313
|
+
* accepted as a known v1 limitation.
|
|
1314
|
+
* - Hand-rolled `defineCollection({ loader: glob({ base: "./src/content/x" }) })`
|
|
1315
|
+
* puts a *path* in `base`, not a folder name. The extracted value
|
|
1316
|
+
* starts with `./src/content/` and won't match a folder under
|
|
1317
|
+
* `src/content/` directly. Users who write the loader by hand can
|
|
1318
|
+
* keep folder names matching collection keys, or accept that
|
|
1319
|
+
* `duplicate-slug` won't see their non-conforming collection until
|
|
1320
|
+
* they migrate to the Nimbus helpers.
|
|
1321
|
+
*
|
|
1322
|
+
* Returns `null` when the file is missing or unparseable, matching
|
|
1323
|
+
* `parseContentCollections`'s contract.
|
|
1324
|
+
*/
|
|
1325
|
+
async function parseCollectionBases(filePath) {
|
|
1326
|
+
let source;
|
|
1327
|
+
try {
|
|
1328
|
+
source = await fs$1.readFile(filePath, "utf8");
|
|
1329
|
+
} catch (err) {
|
|
1330
|
+
if (err.code === "ENOENT") return null;
|
|
1331
|
+
throw err;
|
|
1332
|
+
}
|
|
1333
|
+
const stripped = source.replace(/\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
|
|
1334
|
+
const prefixMatch = stripped.match(EXPORT_PREFIX_PATTERN);
|
|
1335
|
+
if (!prefixMatch || prefixMatch.index === void 0) return null;
|
|
1336
|
+
const objectStart = prefixMatch.index + prefixMatch[0].length;
|
|
1337
|
+
const objectEnd = findMatchingBrace(stripped, objectStart - 1);
|
|
1338
|
+
if (objectEnd === -1) return null;
|
|
1339
|
+
const body = stripped.slice(objectStart, objectEnd);
|
|
1340
|
+
const out = /* @__PURE__ */ new Map();
|
|
1341
|
+
for (const raw of splitTopLevelCommas(body)) {
|
|
1342
|
+
const entry = raw.trim();
|
|
1343
|
+
if (!entry) continue;
|
|
1344
|
+
if (entry.startsWith("...")) continue;
|
|
1345
|
+
if (entry.startsWith("[")) continue;
|
|
1346
|
+
const colonIdx = entry.indexOf(":");
|
|
1347
|
+
const key = (colonIdx === -1 ? entry : entry.slice(0, colonIdx)).trim().replace(/^['"`]|['"`]$/g, "");
|
|
1348
|
+
if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(key)) continue;
|
|
1349
|
+
const baseMatch = (colonIdx === -1 ? findLocalDeclarationValue(stripped, key) : entry.slice(colonIdx + 1)).match(/\bbase\s*:\s*["']([^"']+)["']/);
|
|
1350
|
+
out.set(key, baseMatch ? baseMatch[1] : key);
|
|
1351
|
+
}
|
|
1352
|
+
return out;
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Locate `const|let|var <identifier> = <value>` at any depth in the source
|
|
1356
|
+
* and return the captured `<value>` text. Used to resolve shorthand
|
|
1357
|
+
* collection entries (`{ docs }` in the registration object refers to a
|
|
1358
|
+
* `const docs = defineCollection(...)` declared somewhere above).
|
|
1359
|
+
*
|
|
1360
|
+
* The walker is brace/bracket/paren-aware and string-literal-aware so the
|
|
1361
|
+
* captured value spans nested object/function-call expressions without
|
|
1362
|
+
* losing depth. It stops at the next top-level `;` or end-of-input.
|
|
1363
|
+
*
|
|
1364
|
+
* Limitations (false negatives — never false positives):
|
|
1365
|
+
* - Identifiers imported from other modules can't be resolved (we don't
|
|
1366
|
+
* follow imports).
|
|
1367
|
+
* - Type-only annotations using `=>` (`const docs: () => X = …`) would
|
|
1368
|
+
* fool the `=` detection. Realistic collection declarations don't use
|
|
1369
|
+
* this shape; if a user does, the check falls back to the key.
|
|
1370
|
+
*/
|
|
1371
|
+
function findLocalDeclarationValue(source, identifier) {
|
|
1372
|
+
const safeId = identifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1373
|
+
const declRe = new RegExp(`\\b(?:const|let|var)\\s+${safeId}\\b`, "g");
|
|
1374
|
+
let match;
|
|
1375
|
+
while ((match = declRe.exec(source)) !== null) {
|
|
1376
|
+
const eqIdx = findAssignmentEquals(source, match.index + match[0].length);
|
|
1377
|
+
if (eqIdx === -1) continue;
|
|
1378
|
+
const endIdx = findStatementEnd(source, eqIdx + 1);
|
|
1379
|
+
return source.slice(eqIdx + 1, endIdx);
|
|
1380
|
+
}
|
|
1381
|
+
return "";
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* Find the first `=` after `from` that's an assignment operator —
|
|
1385
|
+
* skipping `==`, `===`, and `=>`. Used to locate where the assignment
|
|
1386
|
+
* value begins in `const id [: Type] = value;`.
|
|
1387
|
+
*/
|
|
1388
|
+
function findAssignmentEquals(source, from) {
|
|
1389
|
+
for (let i = from; i < source.length; i++) {
|
|
1390
|
+
if (source[i] !== "=") continue;
|
|
1391
|
+
if (source[i + 1] === "=") {
|
|
1392
|
+
i++;
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
if (source[i + 1] === ">") {
|
|
1396
|
+
i++;
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
if (source[i - 1] === "!" || source[i - 1] === "<" || source[i - 1] === ">") continue;
|
|
1400
|
+
return i;
|
|
1401
|
+
}
|
|
1402
|
+
return -1;
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Walk forward from `from` tracking brace/bracket/paren depth and string
|
|
1406
|
+
* literals; return the index of the statement terminator.
|
|
1407
|
+
*
|
|
1408
|
+
* Terminates at the first of:
|
|
1409
|
+
* 1. A top-level `;`.
|
|
1410
|
+
* 2. A top-level newline that occurs *after* the value expression has
|
|
1411
|
+
* produced any non-whitespace content. This is the ASI rule the
|
|
1412
|
+
* walker has to honor for semicolonless code:
|
|
1413
|
+
*
|
|
1414
|
+
* ```
|
|
1415
|
+
* const docs = defineCollection(docsCollection()) // ← stop here
|
|
1416
|
+
* export const collections = { … } // ← not part of `docs`
|
|
1417
|
+
* ```
|
|
1418
|
+
*
|
|
1419
|
+
* Without the ASI rule the walker would swallow the next statement
|
|
1420
|
+
* and the `base:` regex could read an unrelated value.
|
|
1421
|
+
* 3. End-of-input.
|
|
1422
|
+
*
|
|
1423
|
+
* The "after non-whitespace" gate handles the leading-newline case:
|
|
1424
|
+
*
|
|
1425
|
+
* ```
|
|
1426
|
+
* const docs =
|
|
1427
|
+
* defineCollection(docsCollection({ base: "x" }))
|
|
1428
|
+
* ```
|
|
1429
|
+
*
|
|
1430
|
+
* Here the newline right after `=` doesn't terminate; the walker
|
|
1431
|
+
* keeps scanning until the value starts. Once content has been seen,
|
|
1432
|
+
* the next top-level newline ends the statement.
|
|
1433
|
+
*
|
|
1434
|
+
* Limitations: method-chain continuations like `defineCollection(…)\n .extend(…)`
|
|
1435
|
+
* stop at the first `)`. That captures the inner call's options (where any
|
|
1436
|
+
* `base:` would live), so the result is still correct in practice.
|
|
1437
|
+
*/
|
|
1438
|
+
function findStatementEnd(source, from) {
|
|
1439
|
+
let depth = 0;
|
|
1440
|
+
let inString = null;
|
|
1441
|
+
let sawContent = false;
|
|
1442
|
+
for (let i = from; i < source.length; i++) {
|
|
1443
|
+
const ch = source[i];
|
|
1444
|
+
if (inString) {
|
|
1445
|
+
if (ch === "\\") {
|
|
1446
|
+
i++;
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
if (ch === inString) inString = null;
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
if (ch === "\"" || ch === "'" || ch === "`") {
|
|
1453
|
+
inString = ch;
|
|
1454
|
+
sawContent = true;
|
|
1455
|
+
} else if (ch === "{" || ch === "[" || ch === "(") {
|
|
1456
|
+
depth++;
|
|
1457
|
+
sawContent = true;
|
|
1458
|
+
} else if (ch === "}" || ch === "]" || ch === ")") depth--;
|
|
1459
|
+
else if (ch === ";" && depth === 0) return i;
|
|
1460
|
+
else if (ch === "\n" && depth === 0 && sawContent) return i;
|
|
1461
|
+
else if (ch !== " " && ch !== " " && ch !== "\r" && ch !== "\n") sawContent = true;
|
|
1462
|
+
}
|
|
1463
|
+
return source.length;
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
887
1466
|
* Starting from an opening brace at `openIdx`, walk forward tracking brace
|
|
888
1467
|
* depth (and skipping string literals + nested brackets) and return the
|
|
889
1468
|
* index of the matching close brace. Returns `-1` if no match is found —
|
|
@@ -1038,52 +1617,6 @@ function titleAndLangTransformer() {
|
|
|
1038
1617
|
};
|
|
1039
1618
|
}
|
|
1040
1619
|
|
|
1041
|
-
//#endregion
|
|
1042
|
-
//#region src/_internal/levenshtein.ts
|
|
1043
|
-
/**
|
|
1044
|
-
* Tiny Levenshtein distance + "did you mean" suggester.
|
|
1045
|
-
*
|
|
1046
|
-
* Used by the MDX PascalCase validator and any framework diagnostic that
|
|
1047
|
-
* wants to suggest a near-match on a misspelled name. Kept internal — user
|
|
1048
|
-
* code that wants the same hint duplicates ~10 lines rather than depending
|
|
1049
|
-
* on a framework wrapper. See the north-star guardrail on thin wrappers.
|
|
1050
|
-
*/
|
|
1051
|
-
function levenshtein(a, b) {
|
|
1052
|
-
if (a === b) return 0;
|
|
1053
|
-
if (a.length === 0) return b.length;
|
|
1054
|
-
if (b.length === 0) return a.length;
|
|
1055
|
-
const v0 = new Array(b.length + 1);
|
|
1056
|
-
const v1 = new Array(b.length + 1);
|
|
1057
|
-
for (let i = 0; i <= b.length; i++) v0[i] = i;
|
|
1058
|
-
for (let i = 0; i < a.length; i++) {
|
|
1059
|
-
v1[0] = i + 1;
|
|
1060
|
-
for (let j = 0; j < b.length; j++) {
|
|
1061
|
-
const cost = a[i] === b[j] ? 0 : 1;
|
|
1062
|
-
v1[j + 1] = Math.min(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost);
|
|
1063
|
-
}
|
|
1064
|
-
for (let j = 0; j <= b.length; j++) v0[j] = v1[j];
|
|
1065
|
-
}
|
|
1066
|
-
return v1[b.length];
|
|
1067
|
-
}
|
|
1068
|
-
/**
|
|
1069
|
-
* Return the closest candidate within `maxDist`, or null.
|
|
1070
|
-
*
|
|
1071
|
-
* Comparison is case-insensitive (so "tabs" suggests "Tabs"), but the
|
|
1072
|
-
* returned name keeps its original casing.
|
|
1073
|
-
*/
|
|
1074
|
-
function suggest(target, candidates, maxDist = 3) {
|
|
1075
|
-
const targetLower = target.toLowerCase();
|
|
1076
|
-
let best = null;
|
|
1077
|
-
for (const c of candidates) {
|
|
1078
|
-
const dist = levenshtein(targetLower, c.toLowerCase());
|
|
1079
|
-
if (dist <= maxDist && (!best || dist < best.dist)) best = {
|
|
1080
|
-
name: c,
|
|
1081
|
-
dist
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
return best?.name ?? null;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
1620
|
//#endregion
|
|
1088
1621
|
//#region src/_internal/validate-mdx-content.ts
|
|
1089
1622
|
/**
|
|
@@ -1125,7 +1658,7 @@ async function validateMdxContent(options) {
|
|
|
1125
1658
|
const files = await walkMdx(dir);
|
|
1126
1659
|
for (const file of files) {
|
|
1127
1660
|
if (options.skip?.(file)) continue;
|
|
1128
|
-
const fileFailures = scanFile(await fs.readFile(file, "utf8"), globalsSet);
|
|
1661
|
+
const fileFailures = scanFile(await fs$1.readFile(file, "utf8"), globalsSet);
|
|
1129
1662
|
for (const f of fileFailures) {
|
|
1130
1663
|
const knownNames = [...globalsSet, ...f.imports];
|
|
1131
1664
|
failures.push({
|
|
@@ -1156,7 +1689,7 @@ async function walkMdx(dir) {
|
|
|
1156
1689
|
async function visit(current) {
|
|
1157
1690
|
let entries;
|
|
1158
1691
|
try {
|
|
1159
|
-
entries = await fs.readdir(current, { withFileTypes: true });
|
|
1692
|
+
entries = await fs$1.readdir(current, { withFileTypes: true });
|
|
1160
1693
|
} catch (err) {
|
|
1161
1694
|
if (err.code === "ENOENT") return;
|
|
1162
1695
|
throw err;
|
|
@@ -1283,16 +1816,20 @@ const headElementSchema = z.object({
|
|
|
1283
1816
|
attrs: z.record(z.string(), z.string()).default({}),
|
|
1284
1817
|
content: z.string().optional()
|
|
1285
1818
|
});
|
|
1286
|
-
const featuresSchema = z.object({
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1819
|
+
const featuresSchema = withStrictKeys(z.object({
|
|
1820
|
+
sidebar: z.boolean().default(true),
|
|
1821
|
+
tableOfContents: z.boolean().default(true)
|
|
1822
|
+
}), {
|
|
1823
|
+
removedKeys: {
|
|
1824
|
+
toc: "was renamed to \"tableOfContents\". Replace `features: { toc: false }` with `features: { tableOfContents: false }`.",
|
|
1825
|
+
pagination: "was removed. To hide pagination site-wide, remove `<Pagination />` from `src/layouts/DocsLayout.astro` (it is user-owned).",
|
|
1826
|
+
editLinks: "was removed. To hide edit links site-wide, omit `editPattern` from the config — the default is null, which produces no edit URLs. Setting `github` alone does not enable edit links.",
|
|
1827
|
+
search: "moved to the top-level `search` field on the config. Replace `features: { search: false }` with `search: false`."
|
|
1828
|
+
},
|
|
1829
|
+
contextLabel: "features sub-key"
|
|
1291
1830
|
}).default({
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
pagination: true,
|
|
1295
|
-
toc: true
|
|
1831
|
+
sidebar: true,
|
|
1832
|
+
tableOfContents: true
|
|
1296
1833
|
});
|
|
1297
1834
|
const searchSchema = z.union([z.literal(false), z.object({ provider: z.enum(["pagefind", "custom"]).default("pagefind") })]).optional();
|
|
1298
1835
|
const sidebarSchema = z.object({
|
|
@@ -1331,16 +1868,14 @@ const versionsSchema = z.object({
|
|
|
1331
1868
|
path: ["hidden", i]
|
|
1332
1869
|
});
|
|
1333
1870
|
}).optional();
|
|
1334
|
-
const nimbusConfigSchema = z.object({
|
|
1871
|
+
const nimbusConfigSchema = withStrictKeys(z.object({
|
|
1335
1872
|
site: z.string().url({ message: "\"site\" must be a valid URL" }),
|
|
1336
1873
|
title: z.string(),
|
|
1337
1874
|
description: z.string().optional(),
|
|
1338
|
-
logo: z.string().max(2),
|
|
1339
1875
|
locale: z.string().default("en"),
|
|
1340
1876
|
homeLabel: z.string().default("Home"),
|
|
1341
1877
|
github: z.string().url().nullable().default(null),
|
|
1342
1878
|
editPattern: z.string().nullable().default(null).refine((v) => v === null || v.includes("{path}"), { message: "\"editPattern\" must contain the \"{path}\" placeholder, which is replaced with the entry source path. Example: \"https://github.com/my-org/my-repo/edit/main/{path}\"" }),
|
|
1343
|
-
footer: z.string().default("Built with Nimbus"),
|
|
1344
1879
|
socialImage: z.string({ error: "\"socialImage\" must be a string (path or URL)" }).optional(),
|
|
1345
1880
|
socialImageAlt: z.string({ error: "\"socialImageAlt\" must be a string" }).optional(),
|
|
1346
1881
|
head: z.array(headElementSchema).default([]),
|
|
@@ -1348,6 +1883,12 @@ const nimbusConfigSchema = z.object({
|
|
|
1348
1883
|
features: featuresSchema,
|
|
1349
1884
|
search: searchSchema,
|
|
1350
1885
|
versions: versionsSchema
|
|
1886
|
+
}), {
|
|
1887
|
+
removedKeys: {
|
|
1888
|
+
logo: "was removed. The header now renders `config.title` as text. To use a logo image, edit `src/components/Header.astro` and drop in an <img> or <svg>.",
|
|
1889
|
+
footer: "was removed. The starter no longer ships a default `Footer.astro`. To add one, create your own component and render it in `src/layouts/DocsLayout.astro`."
|
|
1890
|
+
},
|
|
1891
|
+
contextLabel: "Config field"
|
|
1351
1892
|
});
|
|
1352
1893
|
function validateNimbusConfig(input) {
|
|
1353
1894
|
const result = nimbusConfigSchema.safeParse(input);
|
|
@@ -1416,13 +1957,13 @@ function virtualConfigPlugin(config, extras) {
|
|
|
1416
1957
|
* versioned-docs collections. Drafts (frontmatter `draft: true`) are
|
|
1417
1958
|
* filtered. Consumers feed this into `buildVersionAlternates()`.
|
|
1418
1959
|
*/
|
|
1419
|
-
const PRIMARY_COLLECTION
|
|
1960
|
+
const PRIMARY_COLLECTION = "docs";
|
|
1420
1961
|
const EXTENSIONS = new Set([".mdx", ".md"]);
|
|
1421
1962
|
async function scanVersionFrontmatter(options) {
|
|
1422
1963
|
const { projectRoot, versions } = options;
|
|
1423
1964
|
const out = [];
|
|
1424
1965
|
const collectionsToScan = [{
|
|
1425
|
-
collection: PRIMARY_COLLECTION
|
|
1966
|
+
collection: PRIMARY_COLLECTION,
|
|
1426
1967
|
dir: path.join(projectRoot, "src/content/docs")
|
|
1427
1968
|
}, ...versions.others.map((slug) => ({
|
|
1428
1969
|
collection: `docs-${slug}`,
|
|
@@ -1433,7 +1974,7 @@ async function scanVersionFrontmatter(options) {
|
|
|
1433
1974
|
for (const file of files) {
|
|
1434
1975
|
let source;
|
|
1435
1976
|
try {
|
|
1436
|
-
source = await fs.readFile(file, "utf8");
|
|
1977
|
+
source = await fs$1.readFile(file, "utf8");
|
|
1437
1978
|
} catch {
|
|
1438
1979
|
continue;
|
|
1439
1980
|
}
|
|
@@ -1456,7 +1997,7 @@ async function walk(dir) {
|
|
|
1456
1997
|
async function visit(current) {
|
|
1457
1998
|
let entries;
|
|
1458
1999
|
try {
|
|
1459
|
-
entries = await fs.readdir(current, { withFileTypes: true });
|
|
2000
|
+
entries = await fs$1.readdir(current, { withFileTypes: true });
|
|
1460
2001
|
} catch (err) {
|
|
1461
2002
|
if (err.code === "ENOENT") return;
|
|
1462
2003
|
throw err;
|
|
@@ -1585,7 +2126,6 @@ function idFromPath(collectionDir, filePath) {
|
|
|
1585
2126
|
|
|
1586
2127
|
//#endregion
|
|
1587
2128
|
//#region src/_internal/version-alternates.ts
|
|
1588
|
-
const PRIMARY_COLLECTION$1 = "docs";
|
|
1589
2129
|
/**
|
|
1590
2130
|
* Build the alternates table for one site.
|
|
1591
2131
|
*
|
|
@@ -1694,8 +2234,10 @@ function buildVersionAlternates(versions, entries) {
|
|
|
1694
2234
|
*
|
|
1695
2235
|
* Returns a list of `{ from, to }` redirect pairs ready for Astro's
|
|
1696
2236
|
* `redirects` config. `from` is the URL the reader hit; `to` is the
|
|
1697
|
-
* current-version sibling. Both are absolute paths
|
|
1698
|
-
*
|
|
2237
|
+
* current-version sibling. Both are absolute paths in the trailing-slash
|
|
2238
|
+
* browser-href form Astro serves under `build.format: "directory"`.
|
|
2239
|
+
* Astro's default `trailingSlash: "ignore"` matches incoming requests in
|
|
2240
|
+
* either form, so a reader landing on `/v1/foo` still resolves.
|
|
1699
2241
|
*/
|
|
1700
2242
|
function computeMissingPageRedirects(versions, table, entries) {
|
|
1701
2243
|
if (!versions || versions.all.length < 2) return [];
|
|
@@ -1714,9 +2256,7 @@ function computeMissingPageRedirects(versions, table, entries) {
|
|
|
1714
2256
|
if (existing.has(`${oldVersion}:${entry.id}`)) continue;
|
|
1715
2257
|
if (table[refKey({
|
|
1716
2258
|
collection: entry.collection,
|
|
1717
|
-
|
|
1718
|
-
slug: entry.id,
|
|
1719
|
-
url: currentUrl
|
|
2259
|
+
slug: entry.id
|
|
1720
2260
|
})]?.alternates.some((a) => a.version === oldVersion)) continue;
|
|
1721
2261
|
redirects.push({
|
|
1722
2262
|
from: pageUrl(versions, oldVersion, entry.id),
|
|
@@ -1744,10 +2284,14 @@ function collectionToVersion(versions, collection) {
|
|
|
1744
2284
|
* `index.ts::resolveCollectionPrefix`:
|
|
1745
2285
|
* - current version → root (`/foo`)
|
|
1746
2286
|
* - others → `/<version>/<slug>`
|
|
2287
|
+
*
|
|
2288
|
+
* Runs through `canonicalEntryUrl` so the result matches what Astro
|
|
2289
|
+
* serves (lowercase + folder-index strip — see `_internal/astro-slug.ts`
|
|
2290
|
+
* for the why and known caveats). The `<link rel="alternate">` tags and
|
|
2291
|
+
* auto-redirect machinery both consume these URLs.
|
|
1747
2292
|
*/
|
|
1748
2293
|
function pageUrl(versions, version, slug) {
|
|
1749
|
-
|
|
1750
|
-
return `/${version}/${slug}`;
|
|
2294
|
+
return toBrowserHref(canonicalEntryUrl(version === versions.current ? "" : `/${version}`, slug));
|
|
1751
2295
|
}
|
|
1752
2296
|
|
|
1753
2297
|
//#endregion
|
|
@@ -1784,12 +2328,19 @@ function pageUrl(versions, version, slug) {
|
|
|
1784
2328
|
*/
|
|
1785
2329
|
function nimbus(rawConfig, options = {}) {
|
|
1786
2330
|
const config = validateNimbusConfig(rawConfig);
|
|
2331
|
+
const lintOptions = validateLintOptions({
|
|
2332
|
+
rules: options.rules,
|
|
2333
|
+
collections: options.collections
|
|
2334
|
+
}, IMPLEMENTED_CODES);
|
|
2335
|
+
let projectRootForBuild = "";
|
|
2336
|
+
let astroBaseForBuild = "";
|
|
1787
2337
|
return {
|
|
1788
2338
|
name: "nimbus-docs",
|
|
1789
2339
|
hooks: {
|
|
1790
2340
|
"astro:config:setup": async (params) => {
|
|
1791
2341
|
const { updateConfig, config: astroConfig, logger } = params;
|
|
1792
2342
|
const integrationsToAdd = [];
|
|
2343
|
+
materializeLintConfig(fileURLToPath(astroConfig.root), lintOptions.rules, lintOptions.collections, config.site);
|
|
1793
2344
|
if (options.validateMdx !== false) {
|
|
1794
2345
|
const validateOpts = typeof options.validateMdx === "object" ? options.validateMdx : {};
|
|
1795
2346
|
const projectRoot = fileURLToPath(astroConfig.root);
|
|
@@ -1809,9 +2360,26 @@ function nimbus(rawConfig, options = {}) {
|
|
|
1809
2360
|
}
|
|
1810
2361
|
}
|
|
1811
2362
|
const projectRoot = fileURLToPath(astroConfig.root);
|
|
1812
|
-
|
|
2363
|
+
projectRootForBuild = projectRoot;
|
|
2364
|
+
astroBaseForBuild = astroConfig.base ?? "";
|
|
2365
|
+
const contentConfigPath = path.join(projectRoot, "src/content.config.ts");
|
|
2366
|
+
const rawCollections = await parseContentCollections(contentConfigPath);
|
|
2367
|
+
const collectionBases = await parseCollectionBases(contentConfigPath);
|
|
1813
2368
|
const indexedCollections = rawCollections === null ? ["docs"] : filterIndexableCollections(rawCollections);
|
|
1814
2369
|
if (rawCollections === null) logger.warn("nimbus-docs: `src/content.config.ts` is missing or doesn't expose a parseable `export const collections = { ... }`. Falling back to indexing the `docs` collection only.");
|
|
2370
|
+
const indexedSet = new Set(indexedCollections);
|
|
2371
|
+
const versionInfo = config.versions ? { others: config.versions.others ?? [] } : null;
|
|
2372
|
+
const indexedBases = /* @__PURE__ */ new Map();
|
|
2373
|
+
if (collectionBases !== null) {
|
|
2374
|
+
for (const [key, base] of collectionBases) if (indexedSet.has(key)) indexedBases.set(key, base);
|
|
2375
|
+
} else for (const key of indexedCollections) indexedBases.set(key, key);
|
|
2376
|
+
const contentOwners = enumerateEntriesByBase(path.join(projectRoot, "src/content"), indexedBases).map((entry) => ({
|
|
2377
|
+
url: contentEntryUrl(entry, versionInfo),
|
|
2378
|
+
source: `src/content/${entry.relPath}`
|
|
2379
|
+
}));
|
|
2380
|
+
const pageOwners = enumerateStaticPageRoutes(path.join(projectRoot, "src/pages"), projectRoot);
|
|
2381
|
+
const duplicateRoutes = findDuplicateRoutes([...contentOwners, ...pageOwners]);
|
|
2382
|
+
if (duplicateRoutes.length > 0) throw new Error(formatDuplicateRoutes(duplicateRoutes));
|
|
1815
2383
|
if (config.versions && rawCollections !== null) {
|
|
1816
2384
|
const registered = new Set(rawCollections);
|
|
1817
2385
|
const missing = config.versions.others.filter((slug) => !registered.has(`docs-${slug}`));
|
|
@@ -1878,13 +2446,74 @@ function nimbus(rawConfig, options = {}) {
|
|
|
1878
2446
|
].join("\n")
|
|
1879
2447
|
});
|
|
1880
2448
|
},
|
|
1881
|
-
"astro:build:done": async ({ dir }) => {
|
|
2449
|
+
"astro:build:done": async ({ dir, pages, logger }) => {
|
|
2450
|
+
materializeRouteTruthFromPages(projectRootForBuild, astroBaseForBuild, pages, logger);
|
|
1882
2451
|
if (config.search === false || config.search?.provider === "custom") return;
|
|
1883
2452
|
await runPagefind(fileURLToPath(dir));
|
|
1884
2453
|
}
|
|
1885
2454
|
}
|
|
1886
2455
|
};
|
|
1887
2456
|
}
|
|
2457
|
+
/**
|
|
2458
|
+
* Write the resolved authoring-lint config to `<root>/.nimbus/lint.json`
|
|
2459
|
+
* for the standalone CLI. Best-effort: any filesystem error is swallowed
|
|
2460
|
+
* so it can't fail an `astro build`. `.nimbus/` is a gitignored scratch
|
|
2461
|
+
* dir (same home the Vale recipe uses).
|
|
2462
|
+
*
|
|
2463
|
+
* `site` is materialized alongside the rules so site-aware rules
|
|
2464
|
+
* (`no-self-host-url`) get the project's deploy host without making the
|
|
2465
|
+
* user duplicate it in their lint config.
|
|
2466
|
+
*/
|
|
2467
|
+
function materializeLintConfig(projectRoot, rules, collections, site) {
|
|
2468
|
+
try {
|
|
2469
|
+
const dir = path.join(projectRoot, ".nimbus");
|
|
2470
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2471
|
+
fs.writeFileSync(path.join(dir, "lint.json"), JSON.stringify({
|
|
2472
|
+
version: 1,
|
|
2473
|
+
rules,
|
|
2474
|
+
collections,
|
|
2475
|
+
site
|
|
2476
|
+
}, null, 2) + "\n", "utf8");
|
|
2477
|
+
} catch {}
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Write the site's route truth to `<root>/.nimbus/routes.json` from the
|
|
2481
|
+
* `pages` array Astro hands us at `astro:build:done`. Each entry in `pages`
|
|
2482
|
+
* is a real emitted URL — no reconstruction, no slug mirroring.
|
|
2483
|
+
*
|
|
2484
|
+
* Best-effort write, same as `materializeLintConfig`. When the file is
|
|
2485
|
+
* missing (e.g. lint ran before any `astro build`), `internal-link` skips
|
|
2486
|
+
* silently rather than false-positive.
|
|
2487
|
+
*
|
|
2488
|
+
* Duplicate-slug detection lives in `astro:config:setup` (above), not
|
|
2489
|
+
* here. Astro silently dedupes colliding routes before this hook fires,
|
|
2490
|
+
* so a post-build collision check on `pages` would never see the
|
|
2491
|
+
* collisions it claims to catch.
|
|
2492
|
+
*/
|
|
2493
|
+
function materializeRouteTruthFromPages(projectRoot, base, pages, logger) {
|
|
2494
|
+
const canonical = /* @__PURE__ */ new Set();
|
|
2495
|
+
for (const { pathname } of pages) canonical.add(canonicalizePathname(pathname));
|
|
2496
|
+
const truth = {
|
|
2497
|
+
version: 1,
|
|
2498
|
+
base,
|
|
2499
|
+
knownRoutes: [...canonical].sort(),
|
|
2500
|
+
opaqueNamespaces: []
|
|
2501
|
+
};
|
|
2502
|
+
try {
|
|
2503
|
+
const dir = path.join(projectRoot, ".nimbus");
|
|
2504
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2505
|
+
fs.writeFileSync(path.join(dir, "routes.json"), JSON.stringify(truth, null, 2) + "\n", "utf8");
|
|
2506
|
+
} catch (err) {
|
|
2507
|
+
logger.debug?.(`failed to write .nimbus/routes.json — internal-link will skip: ${err.message}`);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
function canonicalizePathname(pathname) {
|
|
2511
|
+
let s = pathname;
|
|
2512
|
+
if (s === "") return "/";
|
|
2513
|
+
if (!s.startsWith("/")) s = `/${s}`;
|
|
2514
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
2515
|
+
return s;
|
|
2516
|
+
}
|
|
1888
2517
|
function runPagefind(siteDir) {
|
|
1889
2518
|
const bin = process.platform === "win32" ? "pagefind.cmd" : "pagefind";
|
|
1890
2519
|
return new Promise((resolve) => {
|
|
@@ -1910,8 +2539,6 @@ function runPagefind(siteDir) {
|
|
|
1910
2539
|
* by our Vite plugin) and content entries from `astro:content`. Both
|
|
1911
2540
|
* are external in tsdown and resolved at the consumer's build time.
|
|
1912
2541
|
*/
|
|
1913
|
-
/** Primary collection name — kept in sync with `_internal/content.ts`. */
|
|
1914
|
-
const PRIMARY_COLLECTION = "docs";
|
|
1915
2542
|
/**
|
|
1916
2543
|
* Define a typed Nimbus config. Returns the config unchanged but inferred.
|
|
1917
2544
|
*/
|
|
@@ -1930,55 +2557,20 @@ function defineConfig(config) {
|
|
|
1930
2557
|
* - **Schema-tolerant.** Reads `title` and `description` if present;
|
|
1931
2558
|
* falls back to the entry id for the title and omits the
|
|
1932
2559
|
* description otherwise.
|
|
1933
|
-
* - **Per-page filters baked in.** Drops entries with `
|
|
1934
|
-
*
|
|
1935
|
-
*
|
|
2560
|
+
* - **Per-page filters baked in.** Drops entries with `draft: true`;
|
|
2561
|
+
* absent fields read as the docs-schema default (`draft: false`).
|
|
2562
|
+
* All published pages are indexed — there is no per-page opt-out.
|
|
2563
|
+
* A page that genuinely shouldn't be agent-readable should be kept
|
|
2564
|
+
* out of the content collection entirely.
|
|
1936
2565
|
*
|
|
1937
2566
|
* The returned shape is identical regardless of which factory created
|
|
1938
2567
|
* the collection: hand-rolled `defineCollection({ loader, schema })`
|
|
1939
2568
|
* collections work without modification.
|
|
1940
2569
|
*/
|
|
1941
|
-
/**
|
|
1942
|
-
* Resolve the URL-prefix segment for a given collection ID.
|
|
1943
|
-
*
|
|
1944
|
-
* Rules, in order:
|
|
1945
|
-
* 1. Primary `docs` collection mounts at root → returns `""`.
|
|
1946
|
-
* 2. When `versions` is configured, a `docs-<slug>` collection whose
|
|
1947
|
-
* slug appears in `versions.others` mounts under `/<slug>/` (not
|
|
1948
|
-
* `/docs-<slug>/`). This is the versioning URL convention — a
|
|
1949
|
-
* version is reached by its short label, not its collection ID.
|
|
1950
|
-
* 3. Any other collection (`api`, `blog`, …) mounts at `/<collection>/`
|
|
1951
|
-
* — the default multi-collection convention.
|
|
1952
|
-
*
|
|
1953
|
-
* Returned shape: empty string OR `/<segment>` with leading slash, no
|
|
1954
|
-
* trailing slash. Callers append `/<entryId>` or `/index.md`.
|
|
1955
|
-
*/
|
|
1956
|
-
function resolveCollectionPrefix(versions, collection) {
|
|
1957
|
-
if (collection === PRIMARY_COLLECTION) return "";
|
|
1958
|
-
if (versions && collection.startsWith("docs-")) {
|
|
1959
|
-
const slug = collection.slice(5);
|
|
1960
|
-
if (versions.others.includes(slug)) return `/${slug}`;
|
|
1961
|
-
}
|
|
1962
|
-
return `/${collection}`;
|
|
1963
|
-
}
|
|
1964
|
-
/**
|
|
1965
|
-
* Resolve the URL-safe slug a collection should be referenced by — the
|
|
1966
|
-
* label that appears in URLs and section headers. For version
|
|
1967
|
-
* collections this is the manifest's short slug; for everything else
|
|
1968
|
-
* it's the collection ID.
|
|
1969
|
-
*/
|
|
1970
|
-
function resolveCollectionSlug(versions, collection) {
|
|
1971
|
-
if (collection === PRIMARY_COLLECTION) return collection;
|
|
1972
|
-
if (versions && collection.startsWith("docs-")) {
|
|
1973
|
-
const slug = collection.slice(5);
|
|
1974
|
-
if (versions.others.includes(slug)) return slug;
|
|
1975
|
-
}
|
|
1976
|
-
return collection;
|
|
1977
|
-
}
|
|
1978
2570
|
async function getIndexedEntries() {
|
|
1979
2571
|
const { getCollection } = await import("astro:content");
|
|
1980
2572
|
const collectionNames = await loadIndexedCollections();
|
|
1981
|
-
const names = collectionNames.length > 0 ? collectionNames : [PRIMARY_COLLECTION];
|
|
2573
|
+
const names = collectionNames.length > 0 ? collectionNames : [PRIMARY_COLLECTION$1];
|
|
1982
2574
|
const versions = await getVersions();
|
|
1983
2575
|
const indexed = [];
|
|
1984
2576
|
for (const name of names) {
|
|
@@ -1988,20 +2580,22 @@ async function getIndexedEntries() {
|
|
|
1988
2580
|
} catch {
|
|
1989
2581
|
continue;
|
|
1990
2582
|
}
|
|
1991
|
-
const prefix =
|
|
2583
|
+
const prefix = collectionMountPrefix(name, versions);
|
|
1992
2584
|
for (const entry of entries) {
|
|
1993
2585
|
const data = entry.data ?? {};
|
|
1994
|
-
if (data.llms === false) continue;
|
|
1995
2586
|
if (data.draft === true) continue;
|
|
1996
2587
|
const title = typeof data.title === "string" && data.title.length > 0 ? data.title : entry.id;
|
|
1997
2588
|
const rawDescription = data.description;
|
|
1998
2589
|
const description = typeof rawDescription === "string" && rawDescription.length > 0 ? rawDescription : void 0;
|
|
2590
|
+
const canonicalUrl = canonicalEntryUrl(prefix, entry.id);
|
|
2591
|
+
const markdownUrl = canonicalUrl === "/" ? "/index.md" : `${canonicalUrl}/index.md`;
|
|
1999
2592
|
indexed.push({
|
|
2000
2593
|
entry,
|
|
2001
2594
|
collection: name,
|
|
2002
2595
|
title,
|
|
2003
2596
|
description,
|
|
2004
|
-
url:
|
|
2597
|
+
url: toBrowserHref(canonicalUrl),
|
|
2598
|
+
markdownUrl
|
|
2005
2599
|
});
|
|
2006
2600
|
}
|
|
2007
2601
|
}
|
|
@@ -2025,13 +2619,13 @@ async function getIndexedTopLevel() {
|
|
|
2025
2619
|
const secondaryBuckets = /* @__PURE__ */ new Map();
|
|
2026
2620
|
const versionSlugs = new Set(versions?.others ?? []);
|
|
2027
2621
|
const hiddenSlugs = new Set(versions?.hidden ?? []);
|
|
2028
|
-
for (const item of items) if (item.collection === PRIMARY_COLLECTION) {
|
|
2622
|
+
for (const item of items) if (item.collection === PRIMARY_COLLECTION$1) {
|
|
2029
2623
|
const top = item.entry.id.split("/")[0];
|
|
2030
2624
|
const bucket = primaryBuckets.get(top);
|
|
2031
2625
|
if (bucket) bucket.push(item);
|
|
2032
2626
|
else primaryBuckets.set(top, [item]);
|
|
2033
2627
|
} else {
|
|
2034
|
-
const slug =
|
|
2628
|
+
const slug = collectionLabel(item.collection, versions);
|
|
2035
2629
|
const bucket = secondaryBuckets.get(slug);
|
|
2036
2630
|
if (bucket) bucket.push(item);
|
|
2037
2631
|
else secondaryBuckets.set(slug, [item]);
|
|
@@ -2126,13 +2720,13 @@ async function getSidebarSections(currentSlug, options) {
|
|
|
2126
2720
|
async function buildFullSidebarTree(currentSlug, pageCollection) {
|
|
2127
2721
|
const runtimeConfig = await loadNimbusConfig();
|
|
2128
2722
|
const versions = await getVersions();
|
|
2129
|
-
let effectivePrimary = PRIMARY_COLLECTION;
|
|
2723
|
+
let effectivePrimary = PRIMARY_COLLECTION$1;
|
|
2130
2724
|
let primaryPrefix = "";
|
|
2131
2725
|
if (versions && pageCollection && pageCollection.startsWith("docs-") && versions.others.includes(pageCollection.slice(5))) {
|
|
2132
2726
|
effectivePrimary = pageCollection;
|
|
2133
|
-
primaryPrefix =
|
|
2727
|
+
primaryPrefix = collectionMountPrefix(pageCollection, versions);
|
|
2134
2728
|
}
|
|
2135
|
-
const rewrittenItems = effectivePrimary !== PRIMARY_COLLECTION ? rewriteSidebarItemsForVersion(runtimeConfig.sidebar?.items, effectivePrimary) : runtimeConfig.sidebar?.items;
|
|
2729
|
+
const rewrittenItems = effectivePrimary !== PRIMARY_COLLECTION$1 ? rewriteSidebarItemsForVersion(runtimeConfig.sidebar?.items, effectivePrimary) : runtimeConfig.sidebar?.items;
|
|
2136
2730
|
const referenced = collectSidebarCollectionRefs(rewrittenItems);
|
|
2137
2731
|
return buildSidebarTree(await getVisibleEntriesByCollection([effectivePrimary, ...referenced.filter((c) => c !== effectivePrimary)]), effectivePrimary, currentSlug, runtimeConfig.sidebar ? {
|
|
2138
2732
|
...runtimeConfig.sidebar,
|
|
@@ -2151,7 +2745,7 @@ function rewriteSidebarItemsForVersion(items, effectivePrimary) {
|
|
|
2151
2745
|
if (!item || typeof item !== "object") return item;
|
|
2152
2746
|
const o = item;
|
|
2153
2747
|
const autogen = o.autogenerate;
|
|
2154
|
-
if (autogen && autogen.collection === PRIMARY_COLLECTION) return {
|
|
2748
|
+
if (autogen && autogen.collection === PRIMARY_COLLECTION$1) return {
|
|
2155
2749
|
...o,
|
|
2156
2750
|
autogenerate: {
|
|
2157
2751
|
...autogen,
|
|
@@ -2170,9 +2764,20 @@ function rewriteSidebarItemsForVersion(items, effectivePrimary) {
|
|
|
2170
2764
|
*
|
|
2171
2765
|
* Walks the flattened sidebar; returns the surrounding entries. Honors
|
|
2172
2766
|
* `prev`/`next` frontmatter overrides if provided.
|
|
2767
|
+
*
|
|
2768
|
+
* When an override uses the object form with an internal `link`
|
|
2769
|
+
* (e.g. `prev: { link: "/getting-started" }`), the link is validated
|
|
2770
|
+
* against every visible content entry's URL at build time. A pointer
|
|
2771
|
+
* to a missing page fails the build with a clear error — the same
|
|
2772
|
+
* staleness-detection mechanism used for `previousSlug` in versioning.
|
|
2773
|
+
* The string form (`prev: "Custom label"`) is a label-only override
|
|
2774
|
+
* and doesn't go through link validation.
|
|
2173
2775
|
*/
|
|
2174
2776
|
async function getPrevNext(currentSlug, options) {
|
|
2175
|
-
|
|
2777
|
+
const tree = options?.sidebarTree ?? await getSidebar(currentSlug);
|
|
2778
|
+
const indexed = await getIndexedEntries();
|
|
2779
|
+
const validInternalLinks = new Set(indexed.map((e) => toRouteKey(e.url)));
|
|
2780
|
+
return getPrevNext$1(currentSlug, tree, options?.overrides, validInternalLinks);
|
|
2176
2781
|
}
|
|
2177
2782
|
/**
|
|
2178
2783
|
* Build breadcrumb trail from "/" to the current page.
|
|
@@ -2383,7 +2988,7 @@ async function getVersions() {
|
|
|
2383
2988
|
async function getCurrentVersion(collectionId) {
|
|
2384
2989
|
const versions = await getVersions();
|
|
2385
2990
|
if (!versions) return null;
|
|
2386
|
-
if (collectionId === PRIMARY_COLLECTION) return versions.current;
|
|
2991
|
+
if (collectionId === PRIMARY_COLLECTION$1) return versions.current;
|
|
2387
2992
|
if (!collectionId.startsWith("docs-")) return null;
|
|
2388
2993
|
const suffix = collectionId.slice(5);
|
|
2389
2994
|
return versions.all.includes(suffix) ? suffix : null;
|
|
@@ -2451,7 +3056,7 @@ async function getCanonicalUrl(collectionId, entryId) {
|
|
|
2451
3056
|
* the wrong section.
|
|
2452
3057
|
*/
|
|
2453
3058
|
async function getCollectionLlmsUrl(collectionId) {
|
|
2454
|
-
if (collectionId === PRIMARY_COLLECTION) return "/llms.txt";
|
|
3059
|
+
if (collectionId === PRIMARY_COLLECTION$1) return "/llms.txt";
|
|
2455
3060
|
const versions = await getVersions();
|
|
2456
3061
|
if (versions && collectionId.startsWith("docs-")) {
|
|
2457
3062
|
const slug = collectionId.slice(5);
|
|
@@ -2510,7 +3115,7 @@ async function getVersionLandingUrl(version) {
|
|
|
2510
3115
|
const versions = await getVersions();
|
|
2511
3116
|
if (!versions) return null;
|
|
2512
3117
|
if (!versions.all.includes(version)) return null;
|
|
2513
|
-
const targetCollection = version === versions.current ? PRIMARY_COLLECTION : `docs-${version}`;
|
|
3118
|
+
const targetCollection = version === versions.current ? PRIMARY_COLLECTION$1 : `docs-${version}`;
|
|
2514
3119
|
const inVersion = (await getIndexedEntries()).filter((i) => i.collection === targetCollection);
|
|
2515
3120
|
if (inVersion.length === 0) return null;
|
|
2516
3121
|
const byId = new Map(inVersion.map((i) => [i.entry.id, i]));
|