svelte-docsmith 0.11.0 → 0.12.1

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 (36) hide show
  1. package/dist/buildtime/archive.js +5 -3
  2. package/dist/buildtime/cli/archive-version.js +16 -5
  3. package/dist/buildtime/git.d.ts +30 -2
  4. package/dist/buildtime/git.js +58 -8
  5. package/dist/buildtime/paths.d.ts +8 -3
  6. package/dist/buildtime/paths.js +12 -4
  7. package/dist/buildtime/vite/collect.d.ts +25 -16
  8. package/dist/buildtime/vite/collect.js +100 -106
  9. package/dist/buildtime/vite/extract.d.ts +0 -2
  10. package/dist/buildtime/vite/extract.js +1 -1
  11. package/dist/buildtime/vite/index.d.ts +0 -2
  12. package/dist/buildtime/vite/index.js +53 -22
  13. package/dist/buildtime/vite/pages.d.ts +51 -0
  14. package/dist/buildtime/vite/pages.js +57 -0
  15. package/dist/buildtime/vite/releases.d.ts +0 -2
  16. package/dist/buildtime/vite/releases.js +0 -5
  17. package/dist/components/layouts/copy-page-menu.svelte +4 -2
  18. package/dist/components/layouts/docs-header.svelte +9 -5
  19. package/dist/components/layouts/docs-sidebar.svelte +1 -1
  20. package/dist/components/layouts/seo-head.svelte +7 -5
  21. package/dist/core/changelog.d.ts +3 -0
  22. package/dist/core/docs-page.js +2 -4
  23. package/dist/core/version.js +5 -9
  24. package/dist/generate/feed.d.ts +4 -1
  25. package/dist/generate/feed.js +7 -19
  26. package/dist/generate/llms.d.ts +4 -1
  27. package/dist/generate/llms.js +3 -2
  28. package/dist/generate/sitemap.d.ts +4 -1
  29. package/dist/generate/sitemap.js +3 -16
  30. package/dist/generate/xml.d.ts +12 -0
  31. package/dist/generate/xml.js +27 -0
  32. package/dist/utils/url.d.ts +54 -0
  33. package/dist/utils/url.js +84 -0
  34. package/package.json +1 -1
  35. package/dist/utils/normalize-path.d.ts +0 -6
  36. package/dist/utils/normalize-path.js +0 -8
@@ -7,6 +7,7 @@
7
7
  * Kept here rather than in `bin/` so they are typechecked and unit-tested; the
8
8
  * CLI is a thin wrapper over them. See `docs/adr/0002-archives-are-rewritten-source-copies.md`.
9
9
  */
10
+ import { atBoundary, firstSegmentUnder } from '../utils/url.js';
10
11
  import { outsideCodeFences, withFrontmatter } from './markdown-source.js';
11
12
  /** Route files at the docs root that an archive nested inside it already inherits. */
12
13
  export function isInheritedRouteFile(name) {
@@ -28,10 +29,11 @@ export function rewriteDocsLinks(text, options) {
28
29
  const skip = new Set([...(options.archivedIds ?? []), versionId]);
29
30
  const pattern = new RegExp(`(\\]\\(|href="|href='|\\]:[ \\t]+)(${escapeRe(docsBase)})([^)"'\\s]*)`, 'g');
30
31
  return outsideCodeFences(text, (chunk) => chunk.replace(pattern, (match, prefix, base, rest) => {
31
- // Guard the segment boundary: `/docsmith` must not become `/docs/v1mith`.
32
- if (rest && !/^[/#?]/.test(rest))
32
+ // The regex has already matched the base, so guard the boundary on what
33
+ // follows it: `/docsmith` must not become `/docs/v1mith`.
34
+ if (!atBoundary(rest))
33
35
  return match;
34
- const firstSegment = rest.startsWith('/') ? rest.slice(1).split(/[/#?]/)[0] : '';
36
+ const firstSegment = firstSegmentUnder(rest, '');
35
37
  if (firstSegment && skip.has(firstSegment))
36
38
  return match;
37
39
  return `${prefix}${base}/${versionId}${rest}`;
@@ -13,7 +13,7 @@ import path from 'node:path';
13
13
  import { assertValidVersionId } from '../../core/version.js';
14
14
  import { freezeLastUpdated, isInheritedRouteFile, rewriteDocsLinks } from '../archive.js';
15
15
  import { ARCHIVE_MARKER, discoverArchives, markerContents } from '../archives.js';
16
- import { lastCommitDate } from '../git.js';
16
+ import { commitDates } from '../git.js';
17
17
  import { docsBaseFrom } from '../paths.js';
18
18
  import { isPageFile } from '../vite/pages.js';
19
19
  import { CliError } from './error.js';
@@ -79,6 +79,14 @@ export function archiveVersion(options, log) {
79
79
  yield { file, source: path.join(contentDir, path.relative(toDir, file)) };
80
80
  }
81
81
  }
82
+ // Read the dates before copying, so the walk sees the docs root as it was
83
+ // rather than a tree half full of untracked copies. Say so if they could not
84
+ // be read: an archive is written once and never edited again, so a page that
85
+ // misses its date here keeps the wrong one for good.
86
+ const dates = commitDates(contentDir, (reason) => {
87
+ log(`\n! Could not read commit dates: ${reason}`);
88
+ log(' Archived pages will keep no last-updated date.\n');
89
+ });
82
90
  copyCurrent(contentDir, toDir);
83
91
  fs.writeFileSync(path.join(toDir, ARCHIVE_MARKER), markerContents(id));
84
92
  let pages = 0;
@@ -87,12 +95,15 @@ export function archiveVersion(options, log) {
87
95
  continue;
88
96
  pages++;
89
97
  const text = fs.readFileSync(file, 'utf-8');
90
- fs.writeFileSync(file, freezeLastUpdated(rewriteDocsLinks(text, { docsBase, versionId: id, archivedIds }), lastCommitDate(source)));
98
+ fs.writeFileSync(file, freezeLastUpdated(rewriteDocsLinks(text, { docsBase, versionId: id, archivedIds }), dates.get(source)));
91
99
  }
92
100
  log(`\n✓ Archived the current docs into ${rel(toDir)} (${pages} pages)\n`);
93
- log('Links were rewritten to stay inside the archive, and each page kept');
94
- log('its real last-updated date. Review the diff, then update');
95
- log('docsmith({ versions }) so the archive is served:\n');
101
+ log('Links were rewritten to stay inside the archive.');
102
+ // Only claim the dates were kept when the walk actually produced some.
103
+ if (dates.size)
104
+ log('Each page kept its real last-updated date.');
105
+ log('Review the diff, then update docsmith({ versions }) so the archive');
106
+ log('is served:\n');
96
107
  log(' versions: {');
97
108
  log(" current: { id: '<new release>', label: '<new release>' },");
98
109
  log(` archived: [{ id: '${id}', label: '${label}' }${archivedIds.size ? ', …' : ''}]`);
@@ -1,5 +1,33 @@
1
- /** Commit day (`YYYY-MM-DD`) a file last changed, or undefined outside a repo. */
2
- export declare function lastCommitDate(file: string): string | undefined;
1
+ /**
2
+ * Commit day (`YYYY-MM-DD`) every tracked file under `dir` last changed, keyed by
3
+ * absolute path. One history walk rather than a `git log` per file: the per-file
4
+ * form costs a process each, which is the build's largest single expense once a
5
+ * site has more than a handful of pages.
6
+ *
7
+ * `--relative` makes git print paths relative to `dir`, so they join straight
8
+ * back onto it with no second call to find the repository root. `--no-renames`
9
+ * makes a rename list both of its paths, which keeps the answer identical to
10
+ * asking about one file at a time.
11
+ *
12
+ * An empty map for a directory outside a repository, or one git could not read.
13
+ * That is the same "no date" every caller already handles, and it is deliberately
14
+ * all-or-nothing: a truncated read would date some pages and not others, which
15
+ * looks like real data and is not.
16
+ *
17
+ * Only a truncated read calls `onFail`, because it is the one failure that would
18
+ * otherwise date some pages and not others. A directory outside a repository, a
19
+ * missing directory, or a machine without git all yield no dates, which every
20
+ * caller already handles and which the plugin already reports where it matters.
21
+ * Reporting is the caller's to do, which is why this takes a callback rather
22
+ * than writing to the console itself.
23
+ */
24
+ export declare function commitDates(dir: string, onFail?: (reason: string) => void,
25
+ /**
26
+ * Cap on the walk's output. The default is far above any real docs root; it is
27
+ * a parameter so the truncation path can be exercised without a repository
28
+ * large enough to produce 256 MB of history.
29
+ */
30
+ maxBuffer?: number): Map<string, string>;
3
31
  /**
4
32
  * Release dates keyed by version, read from the commit that introduced each
5
33
  * `## <version>` heading in a changelog. Changesets does not write dates, and a
@@ -12,14 +12,64 @@
12
12
  */
13
13
  import { spawnSync } from 'node:child_process';
14
14
  import path from 'node:path';
15
- /** Commit day (`YYYY-MM-DD`) a file last changed, or undefined outside a repo. */
16
- export function lastCommitDate(file) {
17
- const res = spawnSync('git', ['log', '-1', '--format=%cs', '--', file], {
18
- cwd: path.dirname(file),
19
- encoding: 'utf-8'
20
- });
21
- const date = res.status === 0 ? res.stdout.trim() : '';
22
- return date || undefined;
15
+ /**
16
+ * Commit day (`YYYY-MM-DD`) every tracked file under `dir` last changed, keyed by
17
+ * absolute path. One history walk rather than a `git log` per file: the per-file
18
+ * form costs a process each, which is the build's largest single expense once a
19
+ * site has more than a handful of pages.
20
+ *
21
+ * `--relative` makes git print paths relative to `dir`, so they join straight
22
+ * back onto it with no second call to find the repository root. `--no-renames`
23
+ * makes a rename list both of its paths, which keeps the answer identical to
24
+ * asking about one file at a time.
25
+ *
26
+ * An empty map for a directory outside a repository, or one git could not read.
27
+ * That is the same "no date" every caller already handles, and it is deliberately
28
+ * all-or-nothing: a truncated read would date some pages and not others, which
29
+ * looks like real data and is not.
30
+ *
31
+ * Only a truncated read calls `onFail`, because it is the one failure that would
32
+ * otherwise date some pages and not others. A directory outside a repository, a
33
+ * missing directory, or a machine without git all yield no dates, which every
34
+ * caller already handles and which the plugin already reports where it matters.
35
+ * Reporting is the caller's to do, which is why this takes a callback rather
36
+ * than writing to the console itself.
37
+ */
38
+ export function commitDates(dir, onFail,
39
+ /**
40
+ * Cap on the walk's output. The default is far above any real docs root; it is
41
+ * a parameter so the truncation path can be exercised without a repository
42
+ * large enough to produce 256 MB of history.
43
+ */
44
+ maxBuffer = 256 * 1024 * 1024) {
45
+ const dates = new Map();
46
+ const res = spawnSync('git', ['log', '--format=%cs', '--name-only', '--relative', '--no-renames', '--', '.'],
47
+ // The 1 MB default would truncate rather than throw on a large history,
48
+ // which is why the cap is raised and the result checked below.
49
+ { cwd: dir, encoding: 'utf-8', maxBuffer });
50
+ // `ENOBUFS` is the output outgrowing maxBuffer, which truncates rather than
51
+ // throws and is the only failure that could leave a half-filled map. `ENOENT`
52
+ // (no such directory, or no git installed) and a non-zero status ("not a
53
+ // repository") both mean no dates at all, which is a state callers expect.
54
+ if (res.error?.code === 'ENOBUFS') {
55
+ onFail?.(res.error.message);
56
+ }
57
+ if (res.error || res.status !== 0)
58
+ return dates;
59
+ let date;
60
+ for (const line of res.stdout.split('\n')) {
61
+ if (!line)
62
+ continue;
63
+ if (/^\d{4}-\d{2}-\d{2}$/.test(line)) {
64
+ date = line;
65
+ continue;
66
+ }
67
+ // Newest commit first, so the first date a file appears under is its latest.
68
+ const file = path.resolve(dir, line);
69
+ if (date && !dates.has(file))
70
+ dates.set(file, date);
71
+ }
72
+ return dates;
23
73
  }
24
74
  /**
25
75
  * Release dates keyed by version, read from the commit that introduced each
@@ -1,7 +1,12 @@
1
+ /**
2
+ * The URL a directory under the routes root is served at: `<routes>/docs/intro`
3
+ * becomes `/docs/intro`. The routes root itself becomes `/`.
4
+ */
5
+ export declare function urlFor(routesDir: string, dir: string): string;
1
6
  /**
2
7
  * The docs URL base, e.g. `/docs`, from the content directory's location under
3
- * the routes directory. This is the same mapping the collectors use to turn a
4
- * page's directory into its URL, so a page at `<routes>/docs/intro/+page.md` is
5
- * served at `/docs/intro` under a base of `/docs`.
8
+ * the routes directory. The same mapping a page's own directory goes through, so
9
+ * a page at `<routes>/docs/intro/+page.md` is served at `/docs/intro` under a
10
+ * base of `/docs`.
6
11
  */
7
12
  export declare function docsBaseFrom(routesDir: string, contentDir: string): string;
@@ -4,12 +4,20 @@
4
4
  * separately is how they drift.
5
5
  */
6
6
  import path from 'node:path';
7
+ /**
8
+ * The URL a directory under the routes root is served at: `<routes>/docs/intro`
9
+ * becomes `/docs/intro`. The routes root itself becomes `/`.
10
+ */
11
+ export function urlFor(routesDir, dir) {
12
+ const rel = path.relative(routesDir, dir).split(path.sep).join('/');
13
+ return '/' + rel;
14
+ }
7
15
  /**
8
16
  * The docs URL base, e.g. `/docs`, from the content directory's location under
9
- * the routes directory. This is the same mapping the collectors use to turn a
10
- * page's directory into its URL, so a page at `<routes>/docs/intro/+page.md` is
11
- * served at `/docs/intro` under a base of `/docs`.
17
+ * the routes directory. The same mapping a page's own directory goes through, so
18
+ * a page at `<routes>/docs/intro/+page.md` is served at `/docs/intro` under a
19
+ * base of `/docs`.
12
20
  */
13
21
  export function docsBaseFrom(routesDir, contentDir) {
14
- return '/' + path.relative(routesDir, contentDir).split(path.sep).join('/');
22
+ return urlFor(routesDir, contentDir);
15
23
  }
@@ -1,24 +1,33 @@
1
1
  import type { DocsContentItem, LlmsDoc, SearchDoc } from '../../core/content.js';
2
2
  import type { DocsVersions } from '../../core/version.js';
3
+ import { type SourcePage } from './pages.js';
4
+ /** Where the pages sit, which is what turns a page's file into a URL and a version. */
5
+ export type IndexOptions = {
6
+ /** The docs root the pages were read from. */
7
+ contentDir: string;
8
+ /** Routes root, so `<routes>/docs/intro/+page.md` becomes `/docs/intro`. */
9
+ routesDir: string;
10
+ /** Declared versions, when the site has them. */
11
+ versions?: DocsVersions;
12
+ };
3
13
  /**
4
- * Scan `contentDir` for `+page.md`/`+page.svx` files and read the frontmatter
5
- * fields the sidebar needs (plus the heading list for a server-rendered TOC and
6
- * an estimated reading time), deriving each page's URL from its directory
7
- * relative to `routesDir`. Pure and synchronous so it can be unit-tested.
14
+ * The sidebar's index: the frontmatter fields navigation needs, plus the heading
15
+ * list for a server-rendered TOC and an estimated reading time. Served as the
16
+ * eagerly-imported `svelte-docsmith/content` virtual module.
17
+ *
18
+ * `lastUpdated` is read off the page rather than looked up here, so pass pages
19
+ * that have been through `withCommitDates` or the column comes out blank.
8
20
  */
9
- export declare function collectDocs(contentDir: string, routesDir: string, versions?: DocsVersions): DocsContentItem[];
21
+ export declare function contentIndex(pages: SourcePage[], options: IndexOptions): DocsContentItem[];
10
22
  /**
11
- * Build the search records for every page under `contentDir`: title, section,
12
- * description, heading list, and plain-text body. Served as the lazy-loaded
13
- * `svelte-docsmith/search` virtual module so search can index bodies without
14
- * bloating the eagerly-imported nav index. The missing-directory case is
15
- * already reported by {@link collectDocs}, so this stays quiet.
23
+ * The search index: title, section, description, heading list, and plain-text
24
+ * body. Served as the lazy-loaded `svelte-docsmith/search` virtual module so
25
+ * search can index bodies without bloating the eagerly-imported content index.
16
26
  */
17
- export declare function collectSearchDocs(contentDir: string, routesDir: string, versions?: DocsVersions): SearchDoc[];
27
+ export declare function searchIndex(pages: SourcePage[], options: IndexOptions): SearchDoc[];
18
28
  /**
19
- * Build the LLM records for every page: title, section, description, and the
20
- * full markdown content. Served as the `svelte-docsmith/llms` virtual module and
21
- * consumed server-side by `llms.txt` / `llms-full.txt` routes, so it never ships
22
- * to the client.
29
+ * The LLM index: title, section, description, and the full markdown content.
30
+ * Served as the `svelte-docsmith/llms` virtual module and consumed server-side
31
+ * by `llms.txt` / `llms-full.txt` routes, so it never ships to the client.
23
32
  */
24
- export declare function collectLlmsDocs(contentDir: string, routesDir: string, versions?: DocsVersions): LlmsDoc[];
33
+ export declare function llmsIndex(pages: SourcePage[], options: IndexOptions): LlmsDoc[];
@@ -1,9 +1,51 @@
1
- import fs from 'node:fs';
1
+ /**
2
+ * The three generated indexes, each a projection of the same page list. Nothing
3
+ * here reads the filesystem or spawns a process: a build hands these functions
4
+ * pages read by `./pages.js`, and a test hands them literals. Both go down the
5
+ * same path, so what the sidebar, search and llms.txt show is decided in one
6
+ * place and can be checked without a docs root on disk.
7
+ */
2
8
  import path from 'node:path';
3
- import { listPageFiles } from './pages.js';
4
- import { parseFrontmatter } from './frontmatter.js';
9
+ import { firstSegmentUnder } from '../../utils/url.js';
10
+ import { docsBaseFrom, urlFor } from '../paths.js';
11
+ import { titleOf } from './pages.js';
5
12
  import { extractLlmsContent, extractSearchText, extractToc, readingMinutes } from './extract.js';
6
- import { lastCommitDate } from '../git.js';
13
+ /**
14
+ * Resolve every titled page against the site's layout. Shared by all three
15
+ * indexes so they can never disagree about which pages exist or where they live.
16
+ */
17
+ function indexedPages(pages, options) {
18
+ const resolved = [];
19
+ const docsBase = docsBaseFrom(options.routesDir, options.contentDir);
20
+ for (const page of pages) {
21
+ const title = titleOf(page);
22
+ if (title === undefined)
23
+ continue;
24
+ const url = urlFor(options.routesDir, path.dirname(page.file));
25
+ resolved.push({ ...page, url, title, version: versionOf(url) });
26
+ }
27
+ // Stable output keeps the generated modules diff-friendly across rebuilds.
28
+ return resolved.sort((a, b) => a.url.localeCompare(b.url));
29
+ /**
30
+ * A page belongs to an archived version when the first segment of its URL below
31
+ * the docs base is that archive's id; everything else is the current version,
32
+ * which lives unprefixed at the docs base.
33
+ *
34
+ * Decided in URL space, by the same rule `activeVersion` applies to the reader's
35
+ * pathname at runtime. Deciding it from the directory instead would be a second
36
+ * rule for the same question, in a second vocabulary, free to drift from the one
37
+ * the reader is actually served by. No versions ⇒ undefined, which keeps an
38
+ * unversioned site's index byte-for-byte what it was.
39
+ */
40
+ function versionOf(url) {
41
+ const { versions } = options;
42
+ if (!versions)
43
+ return undefined;
44
+ const segment = firstSegmentUnder(url, docsBase);
45
+ const archived = versions.archived?.find((v) => v.id === segment);
46
+ return archived ? archived.id : versions.current.id;
47
+ }
48
+ }
7
49
  /**
8
50
  * Read a page's `section` frontmatter: a string is one level, an array is a
9
51
  * nested group path. Non-string array members are dropped; anything else is
@@ -33,116 +75,68 @@ function sectionKey(value) {
33
75
  return Array.isArray(section) ? section.join(' / ') : section;
34
76
  }
35
77
  /**
36
- * Walk every nav-worthy page under `contentDir` once: a page is nav-worthy when
37
- * its frontmatter has a string `title`. Yields the raw source, parsed
38
- * frontmatter, derived URL, and title so every index (nav, search, llms) can be
39
- * built from a single read of each file.
78
+ * A frontmatter field, when it holds the type the index expects. Frontmatter is
79
+ * whatever the author typed, so a field of the wrong type is dropped rather than
80
+ * carried into the index as one.
40
81
  */
41
- function* eachTitledPage(contentDir, routesDir, versions) {
42
- for (const file of listPageFiles(contentDir)) {
43
- const source = fs.readFileSync(file, 'utf-8');
44
- const front = parseFrontmatter(source, file);
45
- if (typeof front.title !== 'string')
46
- continue;
47
- const dir = path.dirname(file);
48
- const url = '/' + path.relative(routesDir, dir).split(path.sep).join('/');
49
- yield { source, front, url, title: front.title, file, version: versionOf(dir) };
50
- }
51
- /**
52
- * A page belongs to an archived version when its first directory segment under
53
- * the content dir is that archive's id; everything else is the current
54
- * version, which lives unprefixed at the docs root. No versions ⇒ undefined,
55
- * which keeps an unversioned site's index byte-for-byte what it was.
56
- */
57
- function versionOf(dir) {
58
- if (!versions)
59
- return undefined;
60
- const firstSegment = path.relative(contentDir, dir).split(path.sep)[0];
61
- const archived = versions.archived?.find((v) => v.id === firstSegment);
62
- return archived ? archived.id : versions.current.id;
63
- }
82
+ function stringField(front, key) {
83
+ return typeof front[key] === 'string' ? front[key] : undefined;
84
+ }
85
+ /** As {@link stringField}, for the numeric fields. */
86
+ function numberField(front, key) {
87
+ return typeof front[key] === 'number' ? front[key] : undefined;
64
88
  }
65
89
  /**
66
- * Scan `contentDir` for `+page.md`/`+page.svx` files and read the frontmatter
67
- * fields the sidebar needs (plus the heading list for a server-rendered TOC and
68
- * an estimated reading time), deriving each page's URL from its directory
69
- * relative to `routesDir`. Pure and synchronous so it can be unit-tested.
90
+ * The sidebar's index: the frontmatter fields navigation needs, plus the heading
91
+ * list for a server-rendered TOC and an estimated reading time. Served as the
92
+ * eagerly-imported `svelte-docsmith/content` virtual module.
93
+ *
94
+ * `lastUpdated` is read off the page rather than looked up here, so pass pages
95
+ * that have been through `withCommitDates` or the column comes out blank.
70
96
  */
71
- export function collectDocs(contentDir, routesDir, versions) {
72
- if (!fs.existsSync(contentDir)) {
73
- console.warn(`[svelte-docsmith] content directory not found: ${contentDir}\n` +
74
- ` The sidebar will be empty. Create your doc pages there, or point docsmith() at the right place with \`content\`.`);
75
- return [];
76
- }
77
- const items = [];
78
- for (const { source, front, url, title, file, version } of eachTitledPage(contentDir, routesDir, versions)) {
79
- items.push({
80
- title,
81
- path: url,
82
- description: typeof front.description === 'string' ? front.description : undefined,
83
- section: readSection(front.section),
84
- order: typeof front.order === 'number' ? front.order : undefined,
85
- sourcePath: path.relative(process.cwd(), file).split(path.sep).join('/'),
86
- // Frontmatter wins over git so an archived page keeps the date it last
87
- // really changed: archiving copies every page in one commit, which would
88
- // otherwise stamp the whole archive with the day it was created.
89
- lastUpdated: typeof front.lastUpdated === 'string' ? front.lastUpdated : lastCommitDate(file),
90
- readingTime: readingMinutes(extractSearchText(source)),
91
- toc: extractToc(source),
92
- version
93
- });
94
- }
95
- if (items.length === 0) {
96
- console.warn(`[svelte-docsmith] no doc pages found under ${contentDir}\n` +
97
- ` Add \`+page.md\` files with at least a \`title:\` in their frontmatter to populate the sidebar.`);
98
- }
99
- // Stable output keeps the generated module diff-friendly across rebuilds.
100
- return items.sort((a, b) => a.path.localeCompare(b.path));
97
+ export function contentIndex(pages, options) {
98
+ return indexedPages(pages, options).map((page) => ({
99
+ title: page.title,
100
+ path: page.url,
101
+ description: stringField(page.frontmatter, 'description'),
102
+ section: readSection(page.frontmatter.section),
103
+ order: numberField(page.frontmatter, 'order'),
104
+ sourcePath: path.relative(process.cwd(), page.file).split(path.sep).join('/'),
105
+ lastUpdated: page.lastUpdated,
106
+ readingTime: readingMinutes(extractSearchText(page.source)),
107
+ toc: extractToc(page.source),
108
+ version: page.version
109
+ }));
101
110
  }
102
111
  /**
103
- * Build the search records for every page under `contentDir`: title, section,
104
- * description, heading list, and plain-text body. Served as the lazy-loaded
105
- * `svelte-docsmith/search` virtual module so search can index bodies without
106
- * bloating the eagerly-imported nav index. The missing-directory case is
107
- * already reported by {@link collectDocs}, so this stays quiet.
112
+ * The search index: title, section, description, heading list, and plain-text
113
+ * body. Served as the lazy-loaded `svelte-docsmith/search` virtual module so
114
+ * search can index bodies without bloating the eagerly-imported content index.
108
115
  */
109
- export function collectSearchDocs(contentDir, routesDir, versions) {
110
- if (!fs.existsSync(contentDir))
111
- return [];
112
- const docs = [];
113
- for (const { source, front, url, title, version } of eachTitledPage(contentDir, routesDir, versions)) {
114
- docs.push({
115
- path: url,
116
- title,
117
- section: sectionLabel(front.section),
118
- description: typeof front.description === 'string' ? front.description : undefined,
119
- headings: extractToc(source).map((entry) => entry.title),
120
- text: extractSearchText(source),
121
- version
122
- });
123
- }
124
- return docs.sort((a, b) => a.path.localeCompare(b.path));
116
+ export function searchIndex(pages, options) {
117
+ return indexedPages(pages, options).map((page) => ({
118
+ path: page.url,
119
+ title: page.title,
120
+ section: sectionLabel(page.frontmatter.section),
121
+ description: stringField(page.frontmatter, 'description'),
122
+ headings: extractToc(page.source).map((entry) => entry.title),
123
+ text: extractSearchText(page.source),
124
+ version: page.version
125
+ }));
125
126
  }
126
127
  /**
127
- * Build the LLM records for every page: title, section, description, and the
128
- * full markdown content. Served as the `svelte-docsmith/llms` virtual module and
129
- * consumed server-side by `llms.txt` / `llms-full.txt` routes, so it never ships
130
- * to the client.
128
+ * The LLM index: title, section, description, and the full markdown content.
129
+ * Served as the `svelte-docsmith/llms` virtual module and consumed server-side
130
+ * by `llms.txt` / `llms-full.txt` routes, so it never ships to the client.
131
131
  */
132
- export function collectLlmsDocs(contentDir, routesDir, versions) {
133
- if (!fs.existsSync(contentDir))
134
- return [];
135
- const docs = [];
136
- for (const { source, front, url, title, version } of eachTitledPage(contentDir, routesDir, versions)) {
137
- docs.push({
138
- path: url,
139
- title,
140
- section: sectionKey(front.section),
141
- order: typeof front.order === 'number' ? front.order : undefined,
142
- description: typeof front.description === 'string' ? front.description : undefined,
143
- content: extractLlmsContent(source, title),
144
- version
145
- });
146
- }
147
- return docs.sort((a, b) => a.path.localeCompare(b.path));
132
+ export function llmsIndex(pages, options) {
133
+ return indexedPages(pages, options).map((page) => ({
134
+ path: page.url,
135
+ title: page.title,
136
+ section: sectionKey(page.frontmatter.section),
137
+ order: numberField(page.frontmatter, 'order'),
138
+ description: stringField(page.frontmatter, 'description'),
139
+ content: extractLlmsContent(page.source, page.title),
140
+ version: page.version
141
+ }));
148
142
  }
@@ -3,8 +3,6 @@ export type TocEntry = {
3
3
  title: string;
4
4
  depth: 2 | 3;
5
5
  };
6
- /** Strip inline markdown so a heading's TOC label is plain text. */
7
- export declare function stripInlineMarkdown(text: string): string;
8
6
  /**
9
7
  * Reduce a markdown page to plain, searchable body text: prose and heading text
10
8
  * with frontmatter, `<script>`/`<style>` blocks, fenced code, HTML/Svelte tags,
@@ -5,7 +5,7 @@ function withoutSetupBlocks(body) {
5
5
  return body.replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '');
6
6
  }
7
7
  /** Strip inline markdown so a heading's TOC label is plain text. */
8
- export function stripInlineMarkdown(text) {
8
+ function stripInlineMarkdown(text) {
9
9
  return text
10
10
  .replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
11
11
  .replace(/`([^`]+)`/g, '$1')
@@ -1,7 +1,5 @@
1
1
  import type { Plugin } from 'vite';
2
2
  import { type DocsVersions } from '../../core/version.js';
3
- export { collectDocs, collectLlmsDocs, collectSearchDocs } from './collect.js';
4
- export { collectReleases } from './releases.js';
5
3
  export interface DocsmithViteOptions {
6
4
  /** Directory scanned for doc pages. Default: `'src/routes/docs'`. */
7
5
  content?: string;