nimbus-docs 0.1.4 → 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 +610 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/client.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 +85 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +743 -161
- 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 +74 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +8 -3
- package/dist/schemas.js.map +1 -1
- package/dist/{strict-keys-D06tc9YZ.js → strict-keys-BiXiT3pq.js} +1 -1
- package/dist/{strict-keys-D06tc9YZ.js.map → strict-keys-BiXiT3pq.js.map} +1 -1
- package/dist/types.d.ts +9 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -4
package/dist/index.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { t as
|
|
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";
|
|
2
3
|
import { execFile } from "node:child_process";
|
|
3
4
|
import { promisify } from "node:util";
|
|
5
|
+
import fs from "node:fs";
|
|
4
6
|
import path from "node:path";
|
|
5
7
|
import { fileURLToPath } from "node:url";
|
|
6
8
|
import mdx from "@astrojs/mdx";
|
|
7
9
|
import { satteri } from "@astrojs/markdown-satteri";
|
|
8
10
|
import sitemap from "@astrojs/sitemap";
|
|
9
|
-
import fs from "node:fs/promises";
|
|
10
|
-
import { transformerMetaHighlight, transformerMetaWordHighlight, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight } from "@shikijs/transformers";
|
|
11
|
+
import fs$1 from "node:fs/promises";
|
|
11
12
|
import { z } from "astro/zod";
|
|
13
|
+
import { transformerMetaHighlight, transformerMetaWordHighlight, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight, transformerNotationWordHighlight } from "@shikijs/transformers";
|
|
12
14
|
|
|
13
15
|
//#region src/_internal/runtime-config.ts
|
|
14
16
|
let _cached = null;
|
|
@@ -16,8 +18,9 @@ let _cachedCollections = null;
|
|
|
16
18
|
let _cachedAlternates = null;
|
|
17
19
|
async function loadNimbusConfig() {
|
|
18
20
|
if (_cached) return _cached;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const value = (await import("virtual:nimbus/config")).config;
|
|
22
|
+
_cached = value;
|
|
23
|
+
return value;
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
23
26
|
* Build-time-resolved list of collections the agent-facing routes
|
|
@@ -26,8 +29,9 @@ async function loadNimbusConfig() {
|
|
|
26
29
|
*/
|
|
27
30
|
async function loadIndexedCollections() {
|
|
28
31
|
if (_cachedCollections) return _cachedCollections;
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
const value = (await import("virtual:nimbus/config")).indexedCollections;
|
|
33
|
+
_cachedCollections = value;
|
|
34
|
+
return value;
|
|
31
35
|
}
|
|
32
36
|
/**
|
|
33
37
|
* Build-time-resolved alternates table for cross-version SEO links.
|
|
@@ -36,14 +40,15 @@ async function loadIndexedCollections() {
|
|
|
36
40
|
*/
|
|
37
41
|
async function loadVersionAlternates() {
|
|
38
42
|
if (_cachedAlternates) return _cachedAlternates;
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
const value = (await import("virtual:nimbus/config")).versionAlternates ?? {};
|
|
44
|
+
_cachedAlternates = value;
|
|
45
|
+
return value;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
//#endregion
|
|
44
49
|
//#region src/_internal/content.ts
|
|
45
50
|
/** Primary collection name. Hard-coded — see also `getDocsStaticPaths`. */
|
|
46
|
-
const PRIMARY_COLLECTION$
|
|
51
|
+
const PRIMARY_COLLECTION$2 = "docs";
|
|
47
52
|
/**
|
|
48
53
|
* Return visible entries from one or more collections. Drafts are
|
|
49
54
|
* filtered out in production builds (matching the existing
|
|
@@ -58,7 +63,7 @@ const PRIMARY_COLLECTION$3 = "docs";
|
|
|
58
63
|
* time. Callers that need per-collection type safety should call
|
|
59
64
|
* `getCollection("api")` directly.
|
|
60
65
|
*/
|
|
61
|
-
async function getVisibleEntries(collections = [PRIMARY_COLLECTION$
|
|
66
|
+
async function getVisibleEntries(collections = [PRIMARY_COLLECTION$2]) {
|
|
62
67
|
const { getCollection } = await import("astro:content");
|
|
63
68
|
const all = (await Promise.all(collections.map((name) => getCollection(name).catch(() => [])))).flat();
|
|
64
69
|
return import.meta.env.PROD ? all.filter((entry) => !entry.data.draft) : all;
|
|
@@ -78,6 +83,191 @@ async function getVisibleEntriesByCollection(collections) {
|
|
|
78
83
|
return out;
|
|
79
84
|
}
|
|
80
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
|
+
|
|
81
271
|
//#endregion
|
|
82
272
|
//#region src/_internal/sidebar.ts
|
|
83
273
|
const sortKeyByItem = /* @__PURE__ */ new WeakMap();
|
|
@@ -90,17 +280,6 @@ function sortSidebarItems(a, b) {
|
|
|
90
280
|
if (keyDiff !== 0) return keyDiff;
|
|
91
281
|
return a.type.localeCompare(b.type);
|
|
92
282
|
}
|
|
93
|
-
/** Ensure internal href has leading /, no trailing slash (except root) */
|
|
94
|
-
function normalizeInternalHref(href) {
|
|
95
|
-
let h = href.split("?")[0].split("#")[0];
|
|
96
|
-
if (!h.startsWith("/")) h = `/${h}`;
|
|
97
|
-
if (h.length > 1 && h.endsWith("/")) h = h.slice(0, -1);
|
|
98
|
-
return h;
|
|
99
|
-
}
|
|
100
|
-
/** Strip query and hash for active-state matching */
|
|
101
|
-
function stripQueryHash(href) {
|
|
102
|
-
return href.split("?")[0].split("#")[0];
|
|
103
|
-
}
|
|
104
283
|
function buildEntryIndex(entries) {
|
|
105
284
|
const visible = entries.filter((e) => !e.data.sidebar?.hidden);
|
|
106
285
|
const byId = /* @__PURE__ */ new Map();
|
|
@@ -116,9 +295,16 @@ function buildEntryIndex(entries) {
|
|
|
116
295
|
hasChildren
|
|
117
296
|
};
|
|
118
297
|
}
|
|
119
|
-
/**
|
|
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
|
+
*/
|
|
120
306
|
function joinHref(hrefPrefix, entryId) {
|
|
121
|
-
return
|
|
307
|
+
return toBrowserHref(canonicalEntryUrl(hrefPrefix.replace(/\/$/, ""), entryId));
|
|
122
308
|
}
|
|
123
309
|
function createLink(entry, currentPath, hrefPrefix = "") {
|
|
124
310
|
const href = joinHref(hrefPrefix, entry.id);
|
|
@@ -130,7 +316,7 @@ function createLink(entry, currentPath, hrefPrefix = "") {
|
|
|
130
316
|
type: "link",
|
|
131
317
|
label: entry.data.sidebar?.label ?? entry.data.title,
|
|
132
318
|
href,
|
|
133
|
-
isCurrent: currentPath === href,
|
|
319
|
+
isCurrent: toRouteKey(currentPath) === toRouteKey(href),
|
|
134
320
|
badge,
|
|
135
321
|
order: entry.data.sidebar?.order ?? Number.MAX_VALUE
|
|
136
322
|
};
|
|
@@ -222,6 +408,7 @@ function resolveConfigItems(configItems, entriesByCollection, primaryCollection,
|
|
|
222
408
|
const result = [];
|
|
223
409
|
for (let i = 0; i < configItems.length; i++) {
|
|
224
410
|
const item = configItems[i];
|
|
411
|
+
if (!item) continue;
|
|
225
412
|
const order = orderStart + i;
|
|
226
413
|
if (typeof item === "string") {
|
|
227
414
|
const entry = byId.get(item);
|
|
@@ -230,7 +417,7 @@ function resolveConfigItems(configItems, entriesByCollection, primaryCollection,
|
|
|
230
417
|
link.order = order;
|
|
231
418
|
result.push(link);
|
|
232
419
|
} else console.warn(`[sidebar] Page "${item}" referenced in config but not found in primary collection "${primaryCollection}"`);
|
|
233
|
-
} else if ("link" in item) if (!item.link.startsWith("/")) {
|
|
420
|
+
} else if ("link" in item) if (isAbsoluteUrl(item.link) || !item.link.startsWith("/")) {
|
|
234
421
|
const extLink = {
|
|
235
422
|
type: "external",
|
|
236
423
|
label: item.label,
|
|
@@ -240,15 +427,15 @@ function resolveConfigItems(configItems, entriesByCollection, primaryCollection,
|
|
|
240
427
|
};
|
|
241
428
|
result.push(extLink);
|
|
242
429
|
} else {
|
|
243
|
-
const href =
|
|
244
|
-
const
|
|
245
|
-
const lookup =
|
|
246
|
-
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}"`);
|
|
247
434
|
const link = {
|
|
248
435
|
type: "link",
|
|
249
436
|
label: item.label,
|
|
250
437
|
href,
|
|
251
|
-
isCurrent: currentPath ===
|
|
438
|
+
isCurrent: toRouteKey(currentPath) === routeKey,
|
|
252
439
|
badge: item.badge,
|
|
253
440
|
order
|
|
254
441
|
};
|
|
@@ -334,7 +521,7 @@ function deriveSidebarSections(items) {
|
|
|
334
521
|
if (links.length === 0) return [];
|
|
335
522
|
return [{
|
|
336
523
|
label: item.label,
|
|
337
|
-
href: item._prefix ?? links[0].href,
|
|
524
|
+
href: toBrowserHref(item._prefix ?? links[0].href),
|
|
338
525
|
isActive: links.some((link) => link.isCurrent === true)
|
|
339
526
|
}];
|
|
340
527
|
});
|
|
@@ -388,8 +575,8 @@ function processHideChildren(items, entries) {
|
|
|
388
575
|
}
|
|
389
576
|
if (item._indexId) {
|
|
390
577
|
if (entryById.get(item._indexId)?.data.sidebar?.hideChildren) {
|
|
391
|
-
const
|
|
392
|
-
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);
|
|
393
580
|
if (indexLink) {
|
|
394
581
|
const link = {
|
|
395
582
|
...indexLink,
|
|
@@ -452,6 +639,54 @@ function sidebarHash(items) {
|
|
|
452
639
|
return (hash >>> 0).toString(36).padStart(7, "0");
|
|
453
640
|
}
|
|
454
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
|
+
|
|
455
690
|
//#endregion
|
|
456
691
|
//#region src/_internal/transform.ts
|
|
457
692
|
function protectCode(markdown) {
|
|
@@ -611,18 +846,11 @@ function getBreadcrumbs$1(slug, homeLabel = "Home") {
|
|
|
611
846
|
path += `/${part}`;
|
|
612
847
|
crumbs.push({
|
|
613
848
|
label: part.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
614
|
-
href: path
|
|
849
|
+
href: toBrowserHref(path)
|
|
615
850
|
});
|
|
616
851
|
}
|
|
617
852
|
return crumbs;
|
|
618
853
|
}
|
|
619
|
-
function normalizeInternalPath(path) {
|
|
620
|
-
const [withoutHash] = path.split("#", 1);
|
|
621
|
-
const [pathname] = withoutHash.split("?", 1);
|
|
622
|
-
if (!pathname) return "/";
|
|
623
|
-
if (pathname.length > 1 && pathname.endsWith("/")) return pathname.slice(0, -1);
|
|
624
|
-
return pathname;
|
|
625
|
-
}
|
|
626
854
|
function resolveOverride(override, fallback, validInternalLinks) {
|
|
627
855
|
if (override === false) return void 0;
|
|
628
856
|
if (override === void 0) return fallback;
|
|
@@ -635,11 +863,11 @@ function resolveOverride(override, fallback, validInternalLinks) {
|
|
|
635
863
|
}
|
|
636
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`);
|
|
637
865
|
if (override.link?.startsWith("/") && validInternalLinks) {
|
|
638
|
-
const targetPath =
|
|
866
|
+
const targetPath = toRouteKey(override.link);
|
|
639
867
|
if (!validInternalLinks.has(targetPath)) throw new Error(`prev/next override link "${override.link}" does not match any existing internal docs route`);
|
|
640
868
|
}
|
|
641
869
|
const label = override.label ?? fallback?.label;
|
|
642
|
-
const href = override.link
|
|
870
|
+
const href = override.link ? toBrowserHref(override.link) : fallback?.href;
|
|
643
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");
|
|
644
872
|
if (!href) return void 0;
|
|
645
873
|
return {
|
|
@@ -649,7 +877,8 @@ function resolveOverride(override, fallback, validInternalLinks) {
|
|
|
649
877
|
}
|
|
650
878
|
function getPrevNext$1(currentPath, sidebarTree, overrides, validInternalLinks) {
|
|
651
879
|
const flat = flattenSidebar(sidebarTree);
|
|
652
|
-
const
|
|
880
|
+
const currentKey = toRouteKey(currentPath);
|
|
881
|
+
const index = flat.findIndex((item) => toRouteKey(item.href) === currentKey);
|
|
653
882
|
const sidebarPrev = index > 0 ? {
|
|
654
883
|
label: flat[index - 1].label,
|
|
655
884
|
href: flat[index - 1].href
|
|
@@ -771,7 +1000,7 @@ const EXPORT_PATTERN = /export\s+const\s+components\s*(?::\s*[^=]+)?=\s*\{([\s\S
|
|
|
771
1000
|
async function parseComponentsRegistry(filePath) {
|
|
772
1001
|
let source;
|
|
773
1002
|
try {
|
|
774
|
-
source = await fs.readFile(filePath, "utf8");
|
|
1003
|
+
source = await fs$1.readFile(filePath, "utf8");
|
|
775
1004
|
} catch (err) {
|
|
776
1005
|
if (err.code === "ENOENT") return null;
|
|
777
1006
|
throw err;
|
|
@@ -823,6 +1052,178 @@ function splitTopLevelCommas$1(input) {
|
|
|
823
1052
|
return result;
|
|
824
1053
|
}
|
|
825
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
|
+
|
|
826
1227
|
//#endregion
|
|
827
1228
|
//#region src/_internal/parse-content-collections.ts
|
|
828
1229
|
/**
|
|
@@ -862,7 +1263,7 @@ const EXPORT_PREFIX_PATTERN = /export\s+const\s+collections\s*(?::\s*[^=]+)?=\s*
|
|
|
862
1263
|
async function parseContentCollections(filePath) {
|
|
863
1264
|
let source;
|
|
864
1265
|
try {
|
|
865
|
-
source = await fs.readFile(filePath, "utf8");
|
|
1266
|
+
source = await fs$1.readFile(filePath, "utf8");
|
|
866
1267
|
} catch (err) {
|
|
867
1268
|
if (err.code === "ENOENT") return null;
|
|
868
1269
|
throw err;
|
|
@@ -887,6 +1288,181 @@ async function parseContentCollections(filePath) {
|
|
|
887
1288
|
return names;
|
|
888
1289
|
}
|
|
889
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
|
+
/**
|
|
890
1466
|
* Starting from an opening brace at `openIdx`, walk forward tracking brace
|
|
891
1467
|
* depth (and skipping string literals + nested brackets) and return the
|
|
892
1468
|
* index of the matching close brace. Returns `-1` if no match is found —
|
|
@@ -1041,52 +1617,6 @@ function titleAndLangTransformer() {
|
|
|
1041
1617
|
};
|
|
1042
1618
|
}
|
|
1043
1619
|
|
|
1044
|
-
//#endregion
|
|
1045
|
-
//#region src/_internal/levenshtein.ts
|
|
1046
|
-
/**
|
|
1047
|
-
* Tiny Levenshtein distance + "did you mean" suggester.
|
|
1048
|
-
*
|
|
1049
|
-
* Used by the MDX PascalCase validator and any framework diagnostic that
|
|
1050
|
-
* wants to suggest a near-match on a misspelled name. Kept internal — user
|
|
1051
|
-
* code that wants the same hint duplicates ~10 lines rather than depending
|
|
1052
|
-
* on a framework wrapper. See the north-star guardrail on thin wrappers.
|
|
1053
|
-
*/
|
|
1054
|
-
function levenshtein(a, b) {
|
|
1055
|
-
if (a === b) return 0;
|
|
1056
|
-
if (a.length === 0) return b.length;
|
|
1057
|
-
if (b.length === 0) return a.length;
|
|
1058
|
-
const v0 = new Array(b.length + 1);
|
|
1059
|
-
const v1 = new Array(b.length + 1);
|
|
1060
|
-
for (let i = 0; i <= b.length; i++) v0[i] = i;
|
|
1061
|
-
for (let i = 0; i < a.length; i++) {
|
|
1062
|
-
v1[0] = i + 1;
|
|
1063
|
-
for (let j = 0; j < b.length; j++) {
|
|
1064
|
-
const cost = a[i] === b[j] ? 0 : 1;
|
|
1065
|
-
v1[j + 1] = Math.min(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost);
|
|
1066
|
-
}
|
|
1067
|
-
for (let j = 0; j <= b.length; j++) v0[j] = v1[j];
|
|
1068
|
-
}
|
|
1069
|
-
return v1[b.length];
|
|
1070
|
-
}
|
|
1071
|
-
/**
|
|
1072
|
-
* Return the closest candidate within `maxDist`, or null.
|
|
1073
|
-
*
|
|
1074
|
-
* Comparison is case-insensitive (so "tabs" suggests "Tabs"), but the
|
|
1075
|
-
* returned name keeps its original casing.
|
|
1076
|
-
*/
|
|
1077
|
-
function suggest(target, candidates, maxDist = 3) {
|
|
1078
|
-
const targetLower = target.toLowerCase();
|
|
1079
|
-
let best = null;
|
|
1080
|
-
for (const c of candidates) {
|
|
1081
|
-
const dist = levenshtein(targetLower, c.toLowerCase());
|
|
1082
|
-
if (dist <= maxDist && (!best || dist < best.dist)) best = {
|
|
1083
|
-
name: c,
|
|
1084
|
-
dist
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
|
-
return best?.name ?? null;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
1620
|
//#endregion
|
|
1091
1621
|
//#region src/_internal/validate-mdx-content.ts
|
|
1092
1622
|
/**
|
|
@@ -1128,7 +1658,7 @@ async function validateMdxContent(options) {
|
|
|
1128
1658
|
const files = await walkMdx(dir);
|
|
1129
1659
|
for (const file of files) {
|
|
1130
1660
|
if (options.skip?.(file)) continue;
|
|
1131
|
-
const fileFailures = scanFile(await fs.readFile(file, "utf8"), globalsSet);
|
|
1661
|
+
const fileFailures = scanFile(await fs$1.readFile(file, "utf8"), globalsSet);
|
|
1132
1662
|
for (const f of fileFailures) {
|
|
1133
1663
|
const knownNames = [...globalsSet, ...f.imports];
|
|
1134
1664
|
failures.push({
|
|
@@ -1159,7 +1689,7 @@ async function walkMdx(dir) {
|
|
|
1159
1689
|
async function visit(current) {
|
|
1160
1690
|
let entries;
|
|
1161
1691
|
try {
|
|
1162
|
-
entries = await fs.readdir(current, { withFileTypes: true });
|
|
1692
|
+
entries = await fs$1.readdir(current, { withFileTypes: true });
|
|
1163
1693
|
} catch (err) {
|
|
1164
1694
|
if (err.code === "ENOENT") return;
|
|
1165
1695
|
throw err;
|
|
@@ -1427,13 +1957,13 @@ function virtualConfigPlugin(config, extras) {
|
|
|
1427
1957
|
* versioned-docs collections. Drafts (frontmatter `draft: true`) are
|
|
1428
1958
|
* filtered. Consumers feed this into `buildVersionAlternates()`.
|
|
1429
1959
|
*/
|
|
1430
|
-
const PRIMARY_COLLECTION
|
|
1960
|
+
const PRIMARY_COLLECTION = "docs";
|
|
1431
1961
|
const EXTENSIONS = new Set([".mdx", ".md"]);
|
|
1432
1962
|
async function scanVersionFrontmatter(options) {
|
|
1433
1963
|
const { projectRoot, versions } = options;
|
|
1434
1964
|
const out = [];
|
|
1435
1965
|
const collectionsToScan = [{
|
|
1436
|
-
collection: PRIMARY_COLLECTION
|
|
1966
|
+
collection: PRIMARY_COLLECTION,
|
|
1437
1967
|
dir: path.join(projectRoot, "src/content/docs")
|
|
1438
1968
|
}, ...versions.others.map((slug) => ({
|
|
1439
1969
|
collection: `docs-${slug}`,
|
|
@@ -1444,7 +1974,7 @@ async function scanVersionFrontmatter(options) {
|
|
|
1444
1974
|
for (const file of files) {
|
|
1445
1975
|
let source;
|
|
1446
1976
|
try {
|
|
1447
|
-
source = await fs.readFile(file, "utf8");
|
|
1977
|
+
source = await fs$1.readFile(file, "utf8");
|
|
1448
1978
|
} catch {
|
|
1449
1979
|
continue;
|
|
1450
1980
|
}
|
|
@@ -1467,7 +1997,7 @@ async function walk(dir) {
|
|
|
1467
1997
|
async function visit(current) {
|
|
1468
1998
|
let entries;
|
|
1469
1999
|
try {
|
|
1470
|
-
entries = await fs.readdir(current, { withFileTypes: true });
|
|
2000
|
+
entries = await fs$1.readdir(current, { withFileTypes: true });
|
|
1471
2001
|
} catch (err) {
|
|
1472
2002
|
if (err.code === "ENOENT") return;
|
|
1473
2003
|
throw err;
|
|
@@ -1596,7 +2126,6 @@ function idFromPath(collectionDir, filePath) {
|
|
|
1596
2126
|
|
|
1597
2127
|
//#endregion
|
|
1598
2128
|
//#region src/_internal/version-alternates.ts
|
|
1599
|
-
const PRIMARY_COLLECTION$1 = "docs";
|
|
1600
2129
|
/**
|
|
1601
2130
|
* Build the alternates table for one site.
|
|
1602
2131
|
*
|
|
@@ -1705,8 +2234,10 @@ function buildVersionAlternates(versions, entries) {
|
|
|
1705
2234
|
*
|
|
1706
2235
|
* Returns a list of `{ from, to }` redirect pairs ready for Astro's
|
|
1707
2236
|
* `redirects` config. `from` is the URL the reader hit; `to` is the
|
|
1708
|
-
* current-version sibling. Both are absolute paths
|
|
1709
|
-
*
|
|
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.
|
|
1710
2241
|
*/
|
|
1711
2242
|
function computeMissingPageRedirects(versions, table, entries) {
|
|
1712
2243
|
if (!versions || versions.all.length < 2) return [];
|
|
@@ -1725,9 +2256,7 @@ function computeMissingPageRedirects(versions, table, entries) {
|
|
|
1725
2256
|
if (existing.has(`${oldVersion}:${entry.id}`)) continue;
|
|
1726
2257
|
if (table[refKey({
|
|
1727
2258
|
collection: entry.collection,
|
|
1728
|
-
|
|
1729
|
-
slug: entry.id,
|
|
1730
|
-
url: currentUrl
|
|
2259
|
+
slug: entry.id
|
|
1731
2260
|
})]?.alternates.some((a) => a.version === oldVersion)) continue;
|
|
1732
2261
|
redirects.push({
|
|
1733
2262
|
from: pageUrl(versions, oldVersion, entry.id),
|
|
@@ -1755,10 +2284,14 @@ function collectionToVersion(versions, collection) {
|
|
|
1755
2284
|
* `index.ts::resolveCollectionPrefix`:
|
|
1756
2285
|
* - current version → root (`/foo`)
|
|
1757
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.
|
|
1758
2292
|
*/
|
|
1759
2293
|
function pageUrl(versions, version, slug) {
|
|
1760
|
-
|
|
1761
|
-
return `/${version}/${slug}`;
|
|
2294
|
+
return toBrowserHref(canonicalEntryUrl(version === versions.current ? "" : `/${version}`, slug));
|
|
1762
2295
|
}
|
|
1763
2296
|
|
|
1764
2297
|
//#endregion
|
|
@@ -1795,12 +2328,19 @@ function pageUrl(versions, version, slug) {
|
|
|
1795
2328
|
*/
|
|
1796
2329
|
function nimbus(rawConfig, options = {}) {
|
|
1797
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 = "";
|
|
1798
2337
|
return {
|
|
1799
2338
|
name: "nimbus-docs",
|
|
1800
2339
|
hooks: {
|
|
1801
2340
|
"astro:config:setup": async (params) => {
|
|
1802
2341
|
const { updateConfig, config: astroConfig, logger } = params;
|
|
1803
2342
|
const integrationsToAdd = [];
|
|
2343
|
+
materializeLintConfig(fileURLToPath(astroConfig.root), lintOptions.rules, lintOptions.collections, config.site);
|
|
1804
2344
|
if (options.validateMdx !== false) {
|
|
1805
2345
|
const validateOpts = typeof options.validateMdx === "object" ? options.validateMdx : {};
|
|
1806
2346
|
const projectRoot = fileURLToPath(astroConfig.root);
|
|
@@ -1820,9 +2360,26 @@ function nimbus(rawConfig, options = {}) {
|
|
|
1820
2360
|
}
|
|
1821
2361
|
}
|
|
1822
2362
|
const projectRoot = fileURLToPath(astroConfig.root);
|
|
1823
|
-
|
|
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);
|
|
1824
2368
|
const indexedCollections = rawCollections === null ? ["docs"] : filterIndexableCollections(rawCollections);
|
|
1825
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));
|
|
1826
2383
|
if (config.versions && rawCollections !== null) {
|
|
1827
2384
|
const registered = new Set(rawCollections);
|
|
1828
2385
|
const missing = config.versions.others.filter((slug) => !registered.has(`docs-${slug}`));
|
|
@@ -1889,13 +2446,74 @@ function nimbus(rawConfig, options = {}) {
|
|
|
1889
2446
|
].join("\n")
|
|
1890
2447
|
});
|
|
1891
2448
|
},
|
|
1892
|
-
"astro:build:done": async ({ dir }) => {
|
|
2449
|
+
"astro:build:done": async ({ dir, pages, logger }) => {
|
|
2450
|
+
materializeRouteTruthFromPages(projectRootForBuild, astroBaseForBuild, pages, logger);
|
|
1893
2451
|
if (config.search === false || config.search?.provider === "custom") return;
|
|
1894
2452
|
await runPagefind(fileURLToPath(dir));
|
|
1895
2453
|
}
|
|
1896
2454
|
}
|
|
1897
2455
|
};
|
|
1898
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
|
+
}
|
|
1899
2517
|
function runPagefind(siteDir) {
|
|
1900
2518
|
const bin = process.platform === "win32" ? "pagefind.cmd" : "pagefind";
|
|
1901
2519
|
return new Promise((resolve) => {
|
|
@@ -1921,8 +2539,6 @@ function runPagefind(siteDir) {
|
|
|
1921
2539
|
* by our Vite plugin) and content entries from `astro:content`. Both
|
|
1922
2540
|
* are external in tsdown and resolved at the consumer's build time.
|
|
1923
2541
|
*/
|
|
1924
|
-
/** Primary collection name — kept in sync with `_internal/content.ts`. */
|
|
1925
|
-
const PRIMARY_COLLECTION = "docs";
|
|
1926
2542
|
/**
|
|
1927
2543
|
* Define a typed Nimbus config. Returns the config unchanged but inferred.
|
|
1928
2544
|
*/
|
|
@@ -1951,47 +2567,10 @@ function defineConfig(config) {
|
|
|
1951
2567
|
* the collection: hand-rolled `defineCollection({ loader, schema })`
|
|
1952
2568
|
* collections work without modification.
|
|
1953
2569
|
*/
|
|
1954
|
-
/**
|
|
1955
|
-
* Resolve the URL-prefix segment for a given collection ID.
|
|
1956
|
-
*
|
|
1957
|
-
* Rules, in order:
|
|
1958
|
-
* 1. Primary `docs` collection mounts at root → returns `""`.
|
|
1959
|
-
* 2. When `versions` is configured, a `docs-<slug>` collection whose
|
|
1960
|
-
* slug appears in `versions.others` mounts under `/<slug>/` (not
|
|
1961
|
-
* `/docs-<slug>/`). This is the versioning URL convention — a
|
|
1962
|
-
* version is reached by its short label, not its collection ID.
|
|
1963
|
-
* 3. Any other collection (`api`, `blog`, …) mounts at `/<collection>/`
|
|
1964
|
-
* — the default multi-collection convention.
|
|
1965
|
-
*
|
|
1966
|
-
* Returned shape: empty string OR `/<segment>` with leading slash, no
|
|
1967
|
-
* trailing slash. Callers append `/<entryId>` or `/index.md`.
|
|
1968
|
-
*/
|
|
1969
|
-
function resolveCollectionPrefix(versions, collection) {
|
|
1970
|
-
if (collection === PRIMARY_COLLECTION) return "";
|
|
1971
|
-
if (versions && collection.startsWith("docs-")) {
|
|
1972
|
-
const slug = collection.slice(5);
|
|
1973
|
-
if (versions.others.includes(slug)) return `/${slug}`;
|
|
1974
|
-
}
|
|
1975
|
-
return `/${collection}`;
|
|
1976
|
-
}
|
|
1977
|
-
/**
|
|
1978
|
-
* Resolve the URL-safe slug a collection should be referenced by — the
|
|
1979
|
-
* label that appears in URLs and section headers. For version
|
|
1980
|
-
* collections this is the manifest's short slug; for everything else
|
|
1981
|
-
* it's the collection ID.
|
|
1982
|
-
*/
|
|
1983
|
-
function resolveCollectionSlug(versions, collection) {
|
|
1984
|
-
if (collection === PRIMARY_COLLECTION) return collection;
|
|
1985
|
-
if (versions && collection.startsWith("docs-")) {
|
|
1986
|
-
const slug = collection.slice(5);
|
|
1987
|
-
if (versions.others.includes(slug)) return slug;
|
|
1988
|
-
}
|
|
1989
|
-
return collection;
|
|
1990
|
-
}
|
|
1991
2570
|
async function getIndexedEntries() {
|
|
1992
2571
|
const { getCollection } = await import("astro:content");
|
|
1993
2572
|
const collectionNames = await loadIndexedCollections();
|
|
1994
|
-
const names = collectionNames.length > 0 ? collectionNames : [PRIMARY_COLLECTION];
|
|
2573
|
+
const names = collectionNames.length > 0 ? collectionNames : [PRIMARY_COLLECTION$1];
|
|
1995
2574
|
const versions = await getVersions();
|
|
1996
2575
|
const indexed = [];
|
|
1997
2576
|
for (const name of names) {
|
|
@@ -2001,19 +2580,22 @@ async function getIndexedEntries() {
|
|
|
2001
2580
|
} catch {
|
|
2002
2581
|
continue;
|
|
2003
2582
|
}
|
|
2004
|
-
const prefix =
|
|
2583
|
+
const prefix = collectionMountPrefix(name, versions);
|
|
2005
2584
|
for (const entry of entries) {
|
|
2006
2585
|
const data = entry.data ?? {};
|
|
2007
2586
|
if (data.draft === true) continue;
|
|
2008
2587
|
const title = typeof data.title === "string" && data.title.length > 0 ? data.title : entry.id;
|
|
2009
2588
|
const rawDescription = data.description;
|
|
2010
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`;
|
|
2011
2592
|
indexed.push({
|
|
2012
2593
|
entry,
|
|
2013
2594
|
collection: name,
|
|
2014
2595
|
title,
|
|
2015
2596
|
description,
|
|
2016
|
-
url:
|
|
2597
|
+
url: toBrowserHref(canonicalUrl),
|
|
2598
|
+
markdownUrl
|
|
2017
2599
|
});
|
|
2018
2600
|
}
|
|
2019
2601
|
}
|
|
@@ -2037,13 +2619,13 @@ async function getIndexedTopLevel() {
|
|
|
2037
2619
|
const secondaryBuckets = /* @__PURE__ */ new Map();
|
|
2038
2620
|
const versionSlugs = new Set(versions?.others ?? []);
|
|
2039
2621
|
const hiddenSlugs = new Set(versions?.hidden ?? []);
|
|
2040
|
-
for (const item of items) if (item.collection === PRIMARY_COLLECTION) {
|
|
2622
|
+
for (const item of items) if (item.collection === PRIMARY_COLLECTION$1) {
|
|
2041
2623
|
const top = item.entry.id.split("/")[0];
|
|
2042
2624
|
const bucket = primaryBuckets.get(top);
|
|
2043
2625
|
if (bucket) bucket.push(item);
|
|
2044
2626
|
else primaryBuckets.set(top, [item]);
|
|
2045
2627
|
} else {
|
|
2046
|
-
const slug =
|
|
2628
|
+
const slug = collectionLabel(item.collection, versions);
|
|
2047
2629
|
const bucket = secondaryBuckets.get(slug);
|
|
2048
2630
|
if (bucket) bucket.push(item);
|
|
2049
2631
|
else secondaryBuckets.set(slug, [item]);
|
|
@@ -2138,13 +2720,13 @@ async function getSidebarSections(currentSlug, options) {
|
|
|
2138
2720
|
async function buildFullSidebarTree(currentSlug, pageCollection) {
|
|
2139
2721
|
const runtimeConfig = await loadNimbusConfig();
|
|
2140
2722
|
const versions = await getVersions();
|
|
2141
|
-
let effectivePrimary = PRIMARY_COLLECTION;
|
|
2723
|
+
let effectivePrimary = PRIMARY_COLLECTION$1;
|
|
2142
2724
|
let primaryPrefix = "";
|
|
2143
2725
|
if (versions && pageCollection && pageCollection.startsWith("docs-") && versions.others.includes(pageCollection.slice(5))) {
|
|
2144
2726
|
effectivePrimary = pageCollection;
|
|
2145
|
-
primaryPrefix =
|
|
2727
|
+
primaryPrefix = collectionMountPrefix(pageCollection, versions);
|
|
2146
2728
|
}
|
|
2147
|
-
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;
|
|
2148
2730
|
const referenced = collectSidebarCollectionRefs(rewrittenItems);
|
|
2149
2731
|
return buildSidebarTree(await getVisibleEntriesByCollection([effectivePrimary, ...referenced.filter((c) => c !== effectivePrimary)]), effectivePrimary, currentSlug, runtimeConfig.sidebar ? {
|
|
2150
2732
|
...runtimeConfig.sidebar,
|
|
@@ -2163,7 +2745,7 @@ function rewriteSidebarItemsForVersion(items, effectivePrimary) {
|
|
|
2163
2745
|
if (!item || typeof item !== "object") return item;
|
|
2164
2746
|
const o = item;
|
|
2165
2747
|
const autogen = o.autogenerate;
|
|
2166
|
-
if (autogen && autogen.collection === PRIMARY_COLLECTION) return {
|
|
2748
|
+
if (autogen && autogen.collection === PRIMARY_COLLECTION$1) return {
|
|
2167
2749
|
...o,
|
|
2168
2750
|
autogenerate: {
|
|
2169
2751
|
...autogen,
|
|
@@ -2194,7 +2776,7 @@ function rewriteSidebarItemsForVersion(items, effectivePrimary) {
|
|
|
2194
2776
|
async function getPrevNext(currentSlug, options) {
|
|
2195
2777
|
const tree = options?.sidebarTree ?? await getSidebar(currentSlug);
|
|
2196
2778
|
const indexed = await getIndexedEntries();
|
|
2197
|
-
const validInternalLinks = new Set(indexed.map((e) => e.url));
|
|
2779
|
+
const validInternalLinks = new Set(indexed.map((e) => toRouteKey(e.url)));
|
|
2198
2780
|
return getPrevNext$1(currentSlug, tree, options?.overrides, validInternalLinks);
|
|
2199
2781
|
}
|
|
2200
2782
|
/**
|
|
@@ -2406,7 +2988,7 @@ async function getVersions() {
|
|
|
2406
2988
|
async function getCurrentVersion(collectionId) {
|
|
2407
2989
|
const versions = await getVersions();
|
|
2408
2990
|
if (!versions) return null;
|
|
2409
|
-
if (collectionId === PRIMARY_COLLECTION) return versions.current;
|
|
2991
|
+
if (collectionId === PRIMARY_COLLECTION$1) return versions.current;
|
|
2410
2992
|
if (!collectionId.startsWith("docs-")) return null;
|
|
2411
2993
|
const suffix = collectionId.slice(5);
|
|
2412
2994
|
return versions.all.includes(suffix) ? suffix : null;
|
|
@@ -2474,7 +3056,7 @@ async function getCanonicalUrl(collectionId, entryId) {
|
|
|
2474
3056
|
* the wrong section.
|
|
2475
3057
|
*/
|
|
2476
3058
|
async function getCollectionLlmsUrl(collectionId) {
|
|
2477
|
-
if (collectionId === PRIMARY_COLLECTION) return "/llms.txt";
|
|
3059
|
+
if (collectionId === PRIMARY_COLLECTION$1) return "/llms.txt";
|
|
2478
3060
|
const versions = await getVersions();
|
|
2479
3061
|
if (versions && collectionId.startsWith("docs-")) {
|
|
2480
3062
|
const slug = collectionId.slice(5);
|
|
@@ -2533,7 +3115,7 @@ async function getVersionLandingUrl(version) {
|
|
|
2533
3115
|
const versions = await getVersions();
|
|
2534
3116
|
if (!versions) return null;
|
|
2535
3117
|
if (!versions.all.includes(version)) return null;
|
|
2536
|
-
const targetCollection = version === versions.current ? PRIMARY_COLLECTION : `docs-${version}`;
|
|
3118
|
+
const targetCollection = version === versions.current ? PRIMARY_COLLECTION$1 : `docs-${version}`;
|
|
2537
3119
|
const inVersion = (await getIndexedEntries()).filter((i) => i.collection === targetCollection);
|
|
2538
3120
|
if (inVersion.length === 0) return null;
|
|
2539
3121
|
const byId = new Map(inVersion.map((i) => [i.entry.id, i]));
|