@eqtylab/docs 0.3.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +77 -12
  2. package/dist/{chunk-VJN3HXKM.js → chunk-LNQV7TVH.js} +18 -6
  3. package/dist/chunk-LNQV7TVH.js.map +1 -0
  4. package/dist/chunk-OM7745ZQ.js +77 -0
  5. package/dist/chunk-OM7745ZQ.js.map +1 -0
  6. package/dist/{chunk-IWKZQ4CW.js → chunk-VNQZJMDO.js} +4 -2
  7. package/dist/chunk-VNQZJMDO.js.map +1 -0
  8. package/dist/{chunk-K7PJRTK3.js → chunk-X4LT7ASQ.js} +31 -4
  9. package/dist/chunk-X4LT7ASQ.js.map +1 -0
  10. package/dist/config.d.ts +10 -1
  11. package/dist/config.js +1 -1
  12. package/dist/index.js +366 -29
  13. package/dist/index.js.map +1 -1
  14. package/dist/internal/extract-versions.d.ts +33 -0
  15. package/dist/internal/rehype-base-url.d.ts +9 -2
  16. package/dist/internal/rehype-base-url.js +1 -1
  17. package/dist/internal/remark-archive-document.d.ts +6 -0
  18. package/dist/internal/run-plugins.d.ts +22 -0
  19. package/dist/internal/virtual-config.d.ts +2 -0
  20. package/dist/loaders.d.ts +5 -5
  21. package/dist/loaders.js +18 -7
  22. package/dist/loaders.js.map +1 -1
  23. package/dist/nav.d.ts +4 -0
  24. package/dist/nav.js +21 -7
  25. package/dist/nav.js.map +1 -1
  26. package/dist/paths.d.ts +25 -3
  27. package/dist/paths.js +1 -1
  28. package/dist/runtime/chrome/GlobalSearch.tsx +7 -2
  29. package/dist/runtime/chrome/Header.astro +8 -3
  30. package/dist/runtime/chrome/NavDrawer.astro +44 -1
  31. package/dist/runtime/chrome/NavTree.astro +1 -11
  32. package/dist/runtime/chrome/Prose.astro +17 -3
  33. package/dist/runtime/chrome/VersionBanner.astro +19 -0
  34. package/dist/runtime/chrome/VersionSwitcher.tsx +105 -0
  35. package/dist/runtime/layouts/DocsPage.astro +36 -4
  36. package/dist/runtime/layouts/DocsShell.astro +4 -2
  37. package/dist/runtime/layouts/RedirectPage.astro +19 -0
  38. package/dist/runtime/lib/mdx-components.ts +4 -1
  39. package/dist/runtime/lib/nav-data.ts +44 -15
  40. package/dist/runtime/lib/versions-ui.ts +12 -0
  41. package/dist/runtime/routes/docs-md.ts +50 -11
  42. package/dist/runtime/routes/docs.astro +182 -73
  43. package/dist/runtime/routes/llms-txt.ts +2 -1
  44. package/dist/runtime/routes/not-found.astro +27 -0
  45. package/dist/schema.d.ts +1 -0
  46. package/dist/schema.js +1 -1
  47. package/dist/types.d.ts +14 -5
  48. package/dist/versions.d.ts +37 -0
  49. package/package.json +16 -16
  50. package/dist/chunk-GSSSQTEV.js +0 -51
  51. package/dist/chunk-GSSSQTEV.js.map +0 -1
  52. package/dist/chunk-IWKZQ4CW.js.map +0 -1
  53. package/dist/chunk-K7PJRTK3.js.map +0 -1
  54. package/dist/chunk-VJN3HXKM.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import './chunk-55J6XMHW.js';
2
- import { rehypeBaseUrl } from './chunk-GSSSQTEV.js';
3
- import { resolveConfig } from './chunk-K7PJRTK3.js';
4
- export { resolveConfig } from './chunk-K7PJRTK3.js';
2
+ import { rehypeBaseUrl, versionFromPath } from './chunk-OM7745ZQ.js';
3
+ import { resolveConfig } from './chunk-X4LT7ASQ.js';
4
+ export { resolveConfig } from './chunk-X4LT7ASQ.js';
5
5
  import { resolveDocsEnv } from './chunk-ZZ45WAVR.js';
6
6
  export { resolveDocsEnv } from './chunk-ZZ45WAVR.js';
7
- import { docsHref, isExternalHref } from './chunk-VJN3HXKM.js';
8
- export { badgeSchema, docsSchema, groupSchema } from './chunk-IWKZQ4CW.js';
9
- import path4 from 'path';
7
+ import { docsHref, isExternalHref } from './chunk-LNQV7TVH.js';
8
+ export { badgeSchema, docsSchema, groupSchema } from './chunk-VNQZJMDO.js';
9
+ import path5 from 'path';
10
10
  import { fileURLToPath } from 'url';
11
11
  import { globSync } from 'tinyglobby';
12
+ import { execFileSync } from 'child_process';
12
13
  import fs, { readdirSync, readFileSync } from 'fs';
13
14
  import { createRequire } from 'module';
14
15
  import { visit, SKIP } from 'unist-util-visit';
@@ -24,7 +25,7 @@ function assertMdxOnly(contentDir) {
24
25
  if (strays.length === 0) return;
25
26
  const list = strays.sort().map((file) => ` - ${file} -> ${file.replace(/\.md$/i, ".mdx")}`).join("\n");
26
27
  throw new Error(
27
- `[@eqtylab/docs] Found ${strays.length} Markdown file${strays.length > 1 ? "s" : ""} in ${path4.relative(process.cwd(), dir) || dir}:
28
+ `[@eqtylab/docs] Found ${strays.length} Markdown file${strays.length > 1 ? "s" : ""} in ${path5.relative(process.cwd(), dir) || dir}:
28
29
  ${list}
29
30
 
30
31
  Docs content must be .mdx. Only MDX supports the component overrides that render
@@ -33,12 +34,242 @@ different from every other page. Renaming is usually the only change needed --
33
34
  MDX is a superset of Markdown.`
34
35
  );
35
36
  }
37
+
38
+ // src/versions.ts
39
+ var SEMVER = /(?:^|[^0-9.])(\d+)\.(\d+)\.(\d+)$/;
40
+ var LEVELS = ["patch", "minor", "major"];
41
+ function parseTag(tag) {
42
+ const m = SEMVER.exec(tag.trim());
43
+ if (!m) return null;
44
+ const major = Number(m[1]);
45
+ const minor = Number(m[2]);
46
+ const patch = Number(m[3]);
47
+ return { tag, major, minor, patch, version: `${major}.${minor}.${patch}` };
48
+ }
49
+ function compareTags(a, b) {
50
+ return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
51
+ }
52
+ function groupOf(v, granularity) {
53
+ if (granularity === "major") return `${v.major}`;
54
+ if (granularity === "minor") return `${v.major}.${v.minor}`;
55
+ return v.version;
56
+ }
57
+ function idOf(group) {
58
+ return `v${group}`;
59
+ }
60
+ function suffixOf(id) {
61
+ return id.replace(/\./g, "_");
62
+ }
63
+ function highestTag(tags) {
64
+ const parsed = tags.map(parseTag).filter((t) => t !== null);
65
+ if (!parsed.length) return null;
66
+ return parsed.sort(compareTags)[parsed.length - 1];
67
+ }
68
+ function selectVersions(tags, current, granularity, skippedGroups = []) {
69
+ const cur = parseTag(current);
70
+ if (!cur) {
71
+ throw new Error(`[@eqtylab/docs] versions.current "${current}" is not MAJOR.MINOR.PATCH`);
72
+ }
73
+ const skipped = [];
74
+ const above = [];
75
+ const parsed = [];
76
+ for (const tag of tags) {
77
+ const p = parseTag(tag);
78
+ if (p) parsed.push(p);
79
+ else skipped.push(tag);
80
+ }
81
+ parsed.sort(compareTags);
82
+ const currentGroup = groupOf(cur, granularity);
83
+ const skip = new Set(skippedGroups);
84
+ const byGroup = /* @__PURE__ */ new Map();
85
+ const inCurrent = [];
86
+ const covered = [];
87
+ for (const p of parsed) {
88
+ if (compareTags(p, cur) > 0) {
89
+ above.push(p.tag);
90
+ continue;
91
+ }
92
+ covered.push(p);
93
+ const g = groupOf(p, granularity);
94
+ if (g === currentGroup) {
95
+ inCurrent.push(p);
96
+ continue;
97
+ }
98
+ if (!byGroup.has(g)) byGroup.set(g, []);
99
+ byGroup.get(g).push(p);
100
+ }
101
+ const copies = [];
102
+ const picks = [];
103
+ const coverOf = /* @__PURE__ */ new Map();
104
+ let lastCovered = null;
105
+ for (const [g, members] of byGroup) {
106
+ if (skip.has(g)) {
107
+ coverOf.set(g, lastCovered);
108
+ continue;
109
+ }
110
+ const pick = members[members.length - 1];
111
+ const id = idOf(g);
112
+ copies.push({ id, suffix: suffixOf(id), group: g, tag: pick.tag });
113
+ picks.push(pick);
114
+ coverOf.set(g, id);
115
+ lastCovered = id;
116
+ }
117
+ const redirects = [];
118
+ for (const [g, members] of byGroup) {
119
+ const to = coverOf.get(g) ?? null;
120
+ for (const p of members) {
121
+ const id = idOf(p.version);
122
+ if (id === to) continue;
123
+ redirects.push({ id, to });
124
+ }
125
+ }
126
+ for (const p of inCurrent) redirects.push({ id: idOf(p.version), to: null });
127
+ copies.reverse();
128
+ picks.reverse();
129
+ const emittedIds = new Set(redirects.map((r) => r.id));
130
+ const copyIds = new Set(copies.map((c) => c.id));
131
+ const stubSources = copies.length ? covered.some((p) => p.version === cur.version) ? covered : [...covered, cur] : [];
132
+ for (const level of LEVELS.slice(LEVELS.indexOf(granularity))) {
133
+ const seenGroups = /* @__PURE__ */ new Set();
134
+ for (let i = stubSources.length - 1; i >= 0; i--) {
135
+ const group = groupOf(stubSources[i], level);
136
+ if (seenGroups.has(group)) continue;
137
+ seenGroups.add(group);
138
+ const id = idOf(group);
139
+ if (copyIds.has(id) || emittedIds.has(id)) continue;
140
+ const holdsCurrent = groupOf(cur, level) === group;
141
+ let cover = null;
142
+ for (let c = 0; !holdsCurrent && c < copies.length; c++) {
143
+ if (groupOf(picks[c], level) === group) {
144
+ cover = copies[c].id;
145
+ break;
146
+ }
147
+ }
148
+ if (!holdsCurrent && cover === null) continue;
149
+ redirects.push({ id, to: cover });
150
+ emittedIds.add(id);
151
+ }
152
+ }
153
+ return { copies, redirects, skipped, above };
154
+ }
155
+
156
+ // src/internal/extract-versions.ts
157
+ var EMPTY_VERSIONS = {
158
+ currentVersion: null,
159
+ versionManifest: [],
160
+ versionRedirects: []
161
+ };
162
+ var TAG = "[@eqtylab/docs] versions:";
163
+ function git(cwd, ...args) {
164
+ return execFileSync("git", args, {
165
+ cwd,
166
+ encoding: "utf8",
167
+ stdio: ["ignore", "pipe", "pipe"]
168
+ }).trim();
169
+ }
170
+ function realpath(p) {
171
+ try {
172
+ return fs.realpathSync(p);
173
+ } catch {
174
+ return p;
175
+ }
176
+ }
177
+ function hasPathAtTag(repoRoot, tag, rel) {
178
+ try {
179
+ execFileSync("git", ["cat-file", "-e", `${tag}:${rel}`], { cwd: repoRoot, stdio: "ignore" });
180
+ return true;
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+ function extractVersions(o) {
186
+ let repoRoot;
187
+ try {
188
+ repoRoot = git(o.root, "rev-parse", "--show-toplevel");
189
+ } catch {
190
+ o.logger.warn(
191
+ `${TAG} not a git repository, or git is missing. Versioning is off for this build.`
192
+ );
193
+ return EMPTY_VERSIONS;
194
+ }
195
+ if (git(o.root, "rev-parse", "--is-shallow-repository") === "true") {
196
+ o.logger.warn(`${TAG} shallow clone; tags may be incomplete. Check out with fetch-depth: 0.`);
197
+ }
198
+ const tags = git(o.root, "tag", "--list", o.tags).split("\n").filter(Boolean);
199
+ const currentVersion = o.current ?? highestTag(tags)?.version;
200
+ if (!currentVersion) {
201
+ o.logger.info(
202
+ `${TAG} on and dormant. No tag matches "${o.tags}"; versioning activates at the first release tag.`
203
+ );
204
+ return EMPTY_VERSIONS;
205
+ }
206
+ const contentRel = path5.relative(realpath(repoRoot), realpath(o.contentDirAbs)).split(path5.sep).join("/");
207
+ let selection = selectVersions(tags, currentVersion, o.granularity);
208
+ for (const tag of selection.skipped) {
209
+ o.logger.warn(`${TAG} tag "${tag}" is not MAJOR.MINOR.PATCH; skipped.`);
210
+ }
211
+ if (selection.above.length) {
212
+ o.logger.warn(
213
+ `${TAG} tags above current ${currentVersion} ignored: ${selection.above.join(", ")}.`
214
+ );
215
+ }
216
+ const missing = selection.copies.filter((c) => !hasPathAtTag(repoRoot, c.tag, contentRel));
217
+ if (missing.length) {
218
+ for (const c of missing) {
219
+ o.logger.warn(
220
+ `${TAG} ${c.tag} has no ${contentRel}; skipping ${c.id}. Its releases redirect to the copy below.`
221
+ );
222
+ }
223
+ selection = selectVersions(
224
+ tags,
225
+ currentVersion,
226
+ o.granularity,
227
+ missing.map((c) => c.group)
228
+ );
229
+ }
230
+ const versionsDir = path5.join(o.cacheDir, "versions");
231
+ fs.rmSync(versionsDir, { recursive: true, force: true });
232
+ const versionManifest = selection.copies.map((c) => {
233
+ const dir = path5.join(versionsDir, c.id);
234
+ fs.mkdirSync(dir, { recursive: true });
235
+ try {
236
+ const archive = execFileSync("git", ["archive", `${c.tag}:${contentRel}`], {
237
+ cwd: repoRoot,
238
+ maxBuffer: 512 * 1024 * 1024,
239
+ stdio: ["ignore", "pipe", "inherit"]
240
+ });
241
+ execFileSync("tar", ["-x", "-C", dir], {
242
+ input: archive,
243
+ stdio: ["pipe", "ignore", "inherit"]
244
+ });
245
+ } catch (cause) {
246
+ throw new Error(`${TAG} ${c.tag} failed to extract ${contentRel} into ${dir}`, { cause });
247
+ }
248
+ if (!fs.readdirSync(dir).length) {
249
+ throw new Error(`${TAG} ${c.tag} extracted no files into ${dir}`);
250
+ }
251
+ return { ...c, dir };
252
+ });
253
+ const cur = parseTag(currentVersion);
254
+ const group = groupOf(cur, o.granularity);
255
+ const result = {
256
+ currentVersion: { id: idOf(group), group, version: cur.version },
257
+ versionManifest,
258
+ versionRedirects: selection.redirects
259
+ };
260
+ fs.mkdirSync(o.cacheDir, { recursive: true });
261
+ fs.writeFileSync(path5.join(o.cacheDir, "versions.json"), JSON.stringify(result, null, 2));
262
+ o.logger.info(
263
+ `${TAG} current ${result.currentVersion.id}; ${versionManifest.length} older ${versionManifest.length === 1 ? "copy" : "copies"}, ${selection.redirects.length} redirects.`
264
+ );
265
+ return result;
266
+ }
36
267
  var GRAMMAR_IMPORT = "grammars/${";
37
268
  function findGrammarDir(projectRoot) {
38
269
  const requireFromProject = createRequire(new URL("package.json", projectRoot));
39
270
  const equalityEntry = requireFromProject.resolve("@eqtylab/equality");
40
271
  const requireFromEquality = createRequire(equalityEntry);
41
- return path4.resolve(path4.dirname(requireFromEquality.resolve("microlighter")), "grammars");
272
+ return path5.resolve(path5.dirname(requireFromEquality.resolve("microlighter")), "grammars");
42
273
  }
43
274
  function microlighterGrammarsPlugin(projectRoot, logger) {
44
275
  return {
@@ -46,7 +277,7 @@ function microlighterGrammarsPlugin(projectRoot, logger) {
46
277
  apply: "build",
47
278
  generateBundle(_options, bundle) {
48
279
  const directories = new Set(
49
- Object.values(bundle).filter((output) => output.type === "chunk" && output.code.includes(GRAMMAR_IMPORT)).map((output) => path4.posix.dirname(output.fileName))
280
+ Object.values(bundle).filter((output) => output.type === "chunk" && output.code.includes(GRAMMAR_IMPORT)).map((output) => path5.posix.dirname(output.fileName))
50
281
  );
51
282
  if (directories.size === 0) return;
52
283
  let grammarDir;
@@ -66,7 +297,7 @@ function microlighterGrammarsPlugin(projectRoot, logger) {
66
297
  this.emitFile({
67
298
  type: "asset",
68
299
  fileName: directory === "." ? `grammars/${grammar}` : `${directory}/grammars/${grammar}`,
69
- source: readFileSync(path4.resolve(grammarDir, grammar), "utf8")
300
+ source: readFileSync(path5.resolve(grammarDir, grammar), "utf8")
70
301
  });
71
302
  }
72
303
  }
@@ -97,7 +328,7 @@ function pagefindIntegration() {
97
328
  name: "@eqtylab/docs/pagefind",
98
329
  hooks: {
99
330
  "astro:config:setup"({ config, logger }) {
100
- indexDir = path4.join(fileURLToPath(config.outDir), INDEX_DIR);
331
+ indexDir = path5.join(fileURLToPath(config.outDir), INDEX_DIR);
101
332
  if (config.output === "server") {
102
333
  logger.warn(
103
334
  'Output "server" emits no static HTML, so there is nothing for Pagefind to index.'
@@ -113,9 +344,9 @@ function pagefindIntegration() {
113
344
  server.middlewares.use((req, res, next) => {
114
345
  const requested = indexRequest(req.url, base);
115
346
  if (requested === null) return next();
116
- const file = path4.join(indexDir, requested);
117
- if (!file.startsWith(indexDir + path4.sep) || !fs.existsSync(file)) return next();
118
- res.setHeader("Content-Type", MIME[path4.extname(file)] ?? "application/octet-stream");
347
+ const file = path5.join(indexDir, requested);
348
+ if (!file.startsWith(indexDir + path5.sep) || !fs.existsSync(file)) return next();
349
+ res.setHeader("Content-Type", MIME[path5.extname(file)] ?? "application/octet-stream");
119
350
  res.setHeader("Cache-Control", "no-store");
120
351
  fs.createReadStream(file).pipe(res);
121
352
  });
@@ -130,7 +361,7 @@ function pagefindIntegration() {
130
361
  const { page_count, errors: addErrors } = await index.addDirectory({ path: outDir });
131
362
  failOn(addErrors, logger, "index the built output");
132
363
  const { outputPath, errors: writeErrors } = await index.writeFiles({
133
- outputPath: path4.join(outDir, INDEX_DIR)
364
+ outputPath: path5.join(outDir, INDEX_DIR)
134
365
  });
135
366
  failOn(writeErrors, logger, "write the index");
136
367
  logger.info(`indexed ${page_count} pages to ${outputPath}`);
@@ -202,9 +433,9 @@ function rehypeProseScope() {
202
433
  }
203
434
  var CONTENT_EXTENSIONS = [".mdx", ".md"];
204
435
  function hasEntry(contentRoot, id) {
205
- const stem = path4.join(contentRoot, id);
436
+ const stem = path5.join(contentRoot, id);
206
437
  return CONTENT_EXTENSIONS.some(
207
- (ext) => fs.existsSync(stem + ext) || fs.existsSync(path4.join(stem, `index${ext}`))
438
+ (ext) => fs.existsSync(stem + ext) || fs.existsSync(path5.join(stem, `index${ext}`))
208
439
  );
209
440
  }
210
441
  function splitTarget(href) {
@@ -220,18 +451,18 @@ function rehypeRelativeLinks(options) {
220
451
  return function transformer(tree, file) {
221
452
  const filePath = file?.path;
222
453
  if (!filePath) return;
223
- const fromRoot = path4.relative(contentRoot, filePath);
224
- if (!fromRoot || fromRoot.startsWith("..") || path4.isAbsolute(fromRoot)) return;
454
+ const fromRoot = path5.relative(contentRoot, filePath);
455
+ if (!fromRoot || fromRoot.startsWith("..") || path5.isAbsolute(fromRoot)) return;
225
456
  visit(tree, "element", (node) => {
226
457
  const props = node.properties;
227
458
  const href = props?.href;
228
459
  if (!props || typeof href !== "string") return;
229
460
  const split = splitTarget(href);
230
461
  if (!split) return;
231
- const absolute = path4.resolve(path4.dirname(filePath), split.target);
232
- const relative = path4.relative(contentRoot, absolute);
233
- if (!relative || relative.startsWith("..") || path4.isAbsolute(relative)) return;
234
- const id = relative.split(path4.sep).join("/").replace(/\.mdx?$/i, "").replace(/\/index$/, "");
462
+ const absolute = path5.resolve(path5.dirname(filePath), split.target);
463
+ const relative = path5.relative(contentRoot, absolute);
464
+ if (!relative || relative.startsWith("..") || path5.isAbsolute(relative)) return;
465
+ const id = relative.split(path5.sep).join("/").replace(/\.mdx?$/i, "").replace(/\/index$/, "");
235
466
  if (!hasEntry(contentRoot, id)) onMissing?.(href, filePath);
236
467
  props.href = docsHref(id, ctx) + split.suffix;
237
468
  });
@@ -268,10 +499,76 @@ function rehypeTableColumns() {
268
499
  });
269
500
  };
270
501
  }
502
+
503
+ // src/internal/remark-archive-document.ts
504
+ var MDX_NODES = /* @__PURE__ */ new Set([
505
+ "mdxjsEsm",
506
+ "mdxJsxFlowElement",
507
+ "mdxJsxTextElement",
508
+ "mdxFlowExpression",
509
+ "mdxTextExpression"
510
+ ]);
511
+ function prune(node) {
512
+ if (!Array.isArray(node.children)) return;
513
+ node.children = node.children.filter((child) => !MDX_NODES.has(child.type));
514
+ for (const child of node.children) prune(child);
515
+ }
516
+ function remarkArchiveDocument(options) {
517
+ return function transformer(tree, file) {
518
+ const version = versionFromPath(file?.path ?? file?.history?.[0], options.versionsDir);
519
+ if (!version) return;
520
+ prune(tree);
521
+ };
522
+ }
523
+
524
+ // src/internal/run-plugins.ts
525
+ function assertPlugins(value) {
526
+ if (!Array.isArray(value)) throw new Error("[@eqtylab/docs] `plugins` must be an array.");
527
+ return value.map((entry, index) => {
528
+ const plugin = entry;
529
+ if (!plugin || typeof plugin.name !== "string" || typeof plugin.setup !== "function") {
530
+ throw new Error(`[@eqtylab/docs] plugins[${index}] must be { name: string, setup(ctx) }.`);
531
+ }
532
+ return plugin;
533
+ });
534
+ }
535
+ async function runPlugins(plugins, astro, logger) {
536
+ const navGroups = [];
537
+ const componentModules = {};
538
+ for (const plugin of plugins) {
539
+ const ctx = {
540
+ addNavGroup(group) {
541
+ navGroups.push(group);
542
+ },
543
+ addMdxComponents(map) {
544
+ for (const [name, specifier] of Object.entries(map)) {
545
+ if (typeof specifier !== "string") {
546
+ throw new Error(
547
+ `[@eqtylab/docs] plugin "${plugin.name}": component "${name}" must be a module specifier string; the runtime imports it, so a component object cannot be passed.`
548
+ );
549
+ }
550
+ componentModules[name] = specifier;
551
+ }
552
+ },
553
+ astro
554
+ };
555
+ await plugin.setup(ctx);
556
+ logger?.info(`plugin ${plugin.name} registered`);
557
+ }
558
+ return { navGroups, componentModules };
559
+ }
560
+ function pluginComponentsSource(componentModules) {
561
+ const entries = Object.entries(componentModules);
562
+ const imports = entries.map(
563
+ ([, specifier], i) => `import C${i} from ${JSON.stringify(specifier)};`
564
+ );
565
+ const map = entries.map(([name], i) => ` ${JSON.stringify(name)}: C${i},`);
566
+ return [...imports, "export default {", ...map, "};", ""].join("\n");
567
+ }
271
568
  var PAGE_EXTENSIONS = ["astro", "md", "mdx", "html", "ts", "js"];
272
569
  function toRoutePath(relative) {
273
570
  const withoutExt = relative.replace(/\.[^./]+$/, "");
274
- const segments = withoutExt.split(path4.sep).join("/").split("/").filter(Boolean);
571
+ const segments = withoutExt.split(path5.sep).join("/").split("/").filter(Boolean);
275
572
  if (segments.at(-1) === "index") segments.pop();
276
573
  return "/" + segments.join("/");
277
574
  }
@@ -289,7 +586,7 @@ function scanConsumerPages(srcDir) {
289
586
  const ownedPaths = [];
290
587
  const claimedPatterns = /* @__PURE__ */ new Set();
291
588
  for (const file of files) {
292
- if (file.split(path4.sep).some((segment) => segment.startsWith("_"))) continue;
589
+ if (file.split(path5.sep).some((segment) => segment.startsWith("_"))) continue;
293
590
  const routePath = toRoutePath(file);
294
591
  claimedPatterns.add(routePath === "" ? "/" : routePath);
295
592
  if (!/[[\]]/.test(file)) {
@@ -314,6 +611,20 @@ function virtualConfigPlugin(getPayload) {
314
611
  }
315
612
  };
316
613
  }
614
+ var COMPONENTS_ID = "virtual:eqty-docs/plugin-components";
615
+ var COMPONENTS_RESOLVED_ID = "\0" + COMPONENTS_ID;
616
+ function virtualPluginComponentsPlugin(getSource) {
617
+ return {
618
+ name: "eqty-docs:virtual-plugin-components",
619
+ resolveId(id) {
620
+ return id === COMPONENTS_ID ? COMPONENTS_RESOLVED_ID : null;
621
+ },
622
+ load(id) {
623
+ if (id !== COMPONENTS_RESOLVED_ID) return null;
624
+ return getSource();
625
+ }
626
+ };
627
+ }
317
628
 
318
629
  // src/index.ts
319
630
  function plannedRoutes(cfg) {
@@ -353,6 +664,15 @@ function docs(options = {}) {
353
664
  async "astro:config:setup"(params) {
354
665
  const { config, injectRoute, injectScript, updateConfig, addWatchFile, logger, command } = params;
355
666
  assertMdxOnly(new URL(`./${cfg.contentDir}/`, config.srcDir));
667
+ const versionData = cfg.versions === false ? EMPTY_VERSIONS : extractVersions({
668
+ root: fileURLToPath(config.root),
669
+ contentDirAbs: fileURLToPath(new URL(`./${cfg.contentDir}/`, config.srcDir)),
670
+ cacheDir: fileURLToPath(new URL("./.astro/eqty-docs/", config.root)),
671
+ current: cfg.versions.current,
672
+ tags: cfg.versions.tags,
673
+ granularity: cfg.versions.granularity,
674
+ logger
675
+ });
356
676
  const consumer = scanConsumerPages(config.srcDir);
357
677
  for (const route of plannedRoutes(cfg)) {
358
678
  if (consumer.claimedPatterns.has(route.pattern)) {
@@ -363,11 +683,14 @@ function docs(options = {}) {
363
683
  }
364
684
  injectRoute({ pattern: route.pattern, entrypoint: route.entrypoint, prerender: true });
365
685
  }
686
+ const contributions = await runPlugins(assertPlugins(cfg.plugins), params, logger);
366
687
  payload = {
367
688
  ...cfg,
689
+ sidebar: { ...cfg.sidebar, extra: [...cfg.sidebar.extra, ...contributions.navGroups] },
368
690
  env,
369
691
  // Read by the catch-all's getStaticPaths so no path is emitted twice.
370
- ownedByConsumer: consumer.ownedPaths
692
+ ownedByConsumer: consumer.ownedPaths,
693
+ ...versionData
371
694
  };
372
695
  if (command === "dev") {
373
696
  addWatchFile(new URL(`./${cfg.contentDir}/`, config.srcDir));
@@ -387,7 +710,12 @@ function docs(options = {}) {
387
710
  added.push(pagefindIntegration());
388
711
  }
389
712
  }
390
- const vitePlugins = [virtualConfigPlugin(() => payload)];
713
+ const vitePlugins = [
714
+ virtualConfigPlugin(() => payload),
715
+ virtualPluginComponentsPlugin(
716
+ () => pluginComponentsSource(contributions.componentModules)
717
+ )
718
+ ];
391
719
  if (cfg.code.highlighter === "codeblock") {
392
720
  vitePlugins.push(microlighterGrammarsPlugin(config.root, logger));
393
721
  }
@@ -414,8 +742,10 @@ function docs(options = {}) {
414
742
  };
415
743
  markdown.gfm = true;
416
744
  markdown.smartypants = true;
745
+ const versionsDir = fileURLToPath(new URL("./.astro/eqty-docs/versions/", config.root));
746
+ markdown.remarkPlugins = [[remarkArchiveDocument, { versionsDir }]];
417
747
  markdown.rehypePlugins = [
418
- [rehypeBaseUrl, { base: config.base }],
748
+ [rehypeBaseUrl, { base: config.base, pathPrefix: cfg.pathPrefix, versionsDir }],
419
749
  // Must follow rehypeBaseUrl: the hrefs this emits already carry `base`, and
420
750
  // rehypeBaseUrl would prefix them a second time.
421
751
  [
@@ -425,7 +755,7 @@ function docs(options = {}) {
425
755
  base: config.base,
426
756
  pathPrefix: cfg.pathPrefix,
427
757
  onMissing: (href, filePath) => logger.warn(
428
- `${path4.relative(fileURLToPath(config.root), filePath)} links to ${href}, which is not a page in ${cfg.contentDir}`
758
+ `${path5.relative(fileURLToPath(config.root), filePath)} links to ${href}, which is not a page in ${cfg.contentDir}`
429
759
  )
430
760
  }
431
761
  ],
@@ -454,9 +784,16 @@ function docs(options = {}) {
454
784
  ` const config: import('@eqtylab/docs').DocsConfig & {`,
455
785
  ` env: import('@eqtylab/docs').DocsEnv;`,
456
786
  ` ownedByConsumer: string[];`,
787
+ ` currentVersion: { id: string; group: string; version: string } | null;`,
788
+ ` versionManifest: Array<{ id: string; suffix: string; group: string; tag: string; dir: string }>;`,
789
+ ` versionRedirects: Array<{ id: string; to: string | null }>;`,
457
790
  ` };`,
458
791
  ` export default config;`,
459
792
  `}`,
793
+ `declare module 'virtual:eqty-docs/plugin-components' {`,
794
+ ` const components: Record<string, unknown>;`,
795
+ ` export default components;`,
796
+ `}`,
460
797
  ""
461
798
  ].join("\n")
462
799
  });