svelte-docsmith 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/svelte-docsmith.mjs +13 -158
- package/dist/buildtime/archive.js +5 -3
- package/dist/buildtime/archives.d.ts +18 -0
- package/dist/buildtime/archives.js +43 -0
- package/dist/buildtime/cli/archive-version.d.ts +18 -0
- package/dist/buildtime/cli/archive-version.js +104 -0
- package/dist/buildtime/cli/error.d.ts +9 -0
- package/dist/buildtime/cli/error.js +9 -0
- package/dist/buildtime/cli/run.d.ts +13 -0
- package/dist/buildtime/cli/run.js +62 -0
- package/dist/buildtime/{vite/git.d.ts → git.d.ts} +1 -1
- package/dist/buildtime/{vite/git.js → git.js} +15 -3
- package/dist/buildtime/paths.d.ts +12 -0
- package/dist/buildtime/paths.js +23 -0
- package/dist/buildtime/vite/collect.d.ts +25 -16
- package/dist/buildtime/vite/collect.js +100 -106
- package/dist/buildtime/vite/extract.d.ts +0 -2
- package/dist/buildtime/vite/extract.js +1 -1
- package/dist/buildtime/vite/index.d.ts +0 -2
- package/dist/buildtime/vite/index.js +46 -22
- package/dist/buildtime/vite/pages.d.ts +51 -0
- package/dist/buildtime/vite/pages.js +58 -0
- package/dist/buildtime/vite/releases.d.ts +0 -2
- package/dist/buildtime/vite/releases.js +1 -6
- package/dist/components/changelog/changelog-entry.svelte +6 -1
- package/dist/components/chrome/search.svelte +8 -5
- package/dist/components/chrome/version-banner.svelte +5 -20
- package/dist/components/chrome/version-banner.svelte.d.ts +3 -3
- package/dist/components/chrome/version-switcher.svelte +39 -49
- package/dist/components/chrome/version-switcher.svelte.d.ts +3 -9
- package/dist/components/docs-page-context.d.ts +20 -0
- package/dist/components/docs-page-context.js +41 -0
- package/dist/components/layouts/copy-page-menu.svelte +4 -2
- package/dist/components/layouts/docs-header.svelte +18 -28
- package/dist/components/layouts/docs-header.svelte.d.ts +1 -9
- package/dist/components/layouts/docs-mobile-header.svelte +6 -21
- package/dist/components/layouts/docs-mobile-header.svelte.d.ts +1 -9
- package/dist/components/layouts/docs-shell.svelte +29 -28
- package/dist/components/layouts/docs-sidebar.svelte +1 -1
- package/dist/components/layouts/seo-head.svelte +7 -5
- package/dist/core/changelog.d.ts +7 -1
- package/dist/core/content.d.ts +6 -1
- package/dist/core/docs-page.d.ts +15 -0
- package/dist/core/docs-page.js +22 -7
- package/dist/core/index.d.ts +1 -1
- package/dist/core/version.d.ts +36 -0
- package/dist/core/version.js +105 -8
- package/dist/generate/feed.d.ts +4 -1
- package/dist/generate/feed.js +7 -19
- package/dist/generate/llms.d.ts +4 -1
- package/dist/generate/llms.js +3 -2
- package/dist/generate/sitemap.d.ts +4 -1
- package/dist/generate/sitemap.js +3 -16
- package/dist/generate/xml.d.ts +12 -0
- package/dist/generate/xml.js +27 -0
- package/dist/search/context.svelte.d.ts +0 -2
- package/dist/search/context.svelte.js +0 -2
- package/dist/utils/url.d.ts +54 -0
- package/dist/utils/url.js +84 -0
- package/package.json +1 -1
- package/dist/utils/normalize-path.d.ts +0 -6
- package/dist/utils/normalize-path.js +0 -8
|
@@ -1,9 +1,51 @@
|
|
|
1
|
-
|
|
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 {
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -13,18 +13,19 @@
|
|
|
13
13
|
* 3. The **`?source` transform** powering `LiveExample`: importing
|
|
14
14
|
* `Component.svelte?source` yields that file's Shiki-highlighted source.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Reading the docs root lives in `./pages.js` and building each index from what
|
|
17
|
+
* it read lives in `./collect.js`; this file owns the two Vite plugins, and is
|
|
18
|
+
* the only layer that reports to the console.
|
|
18
19
|
*/
|
|
19
20
|
import fs from 'node:fs';
|
|
20
21
|
import path from 'node:path';
|
|
21
22
|
import { DEFAULT_THEMES, lazyHighlighter } from '../highlight.js';
|
|
22
|
-
import { resolveVersions } from '../../core/version.js';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
23
|
+
import { checkVersions, resolveVersions } from '../../core/version.js';
|
|
24
|
+
import { ARCHIVE_MARKER, discoverArchives } from '../archives.js';
|
|
25
|
+
import { docsBaseFrom } from '../paths.js';
|
|
26
|
+
import { isPageFile, readSourcePages, withCommitDates } from './pages.js';
|
|
27
|
+
import { contentIndex, llmsIndex, searchIndex } from './collect.js';
|
|
25
28
|
import { collectReleases } from './releases.js';
|
|
26
|
-
export { collectDocs, collectLlmsDocs, collectSearchDocs } from './collect.js';
|
|
27
|
-
export { collectReleases } from './releases.js';
|
|
28
29
|
/**
|
|
29
30
|
* The svelte-docsmith Vite plugin. Add it to `plugins` in `vite.config.ts`:
|
|
30
31
|
*
|
|
@@ -45,16 +46,30 @@ const LLMS_SPECIFIER = 'svelte-docsmith/llms';
|
|
|
45
46
|
const VIRTUAL_LLMS_ID = '\0svelte-docsmith:llms';
|
|
46
47
|
const CHANGELOG_SPECIFIER = 'svelte-docsmith/changelog';
|
|
47
48
|
const VIRTUAL_CHANGELOG_ID = '\0svelte-docsmith:changelog';
|
|
49
|
+
/**
|
|
50
|
+
* Tell the author when their docs root produced nothing, and why. An empty
|
|
51
|
+
* sidebar is otherwise silent: the build succeeds and the site renders, so
|
|
52
|
+
* without this the first sign of a misconfigured `content` is a missing nav.
|
|
53
|
+
*/
|
|
54
|
+
function warnAboutDocsRoot(contentDir, exists, indexed) {
|
|
55
|
+
if (!exists) {
|
|
56
|
+
console.warn(`[svelte-docsmith] content directory not found: ${contentDir}\n` +
|
|
57
|
+
` The sidebar will be empty. Create your doc pages there, or point docsmith() at the right place with \`content\`.`);
|
|
58
|
+
}
|
|
59
|
+
else if (indexed === 0) {
|
|
60
|
+
console.warn(`[svelte-docsmith] no doc pages found under ${contentDir}\n` +
|
|
61
|
+
` Add \`+page.md\` files with at least a \`title:\` in their frontmatter to populate the sidebar.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
48
64
|
function contentIndexPlugin(options) {
|
|
49
65
|
const contentDir = path.resolve(options.content ?? 'src/routes/docs');
|
|
50
66
|
const routesDir = path.resolve(options.routes ?? 'src/routes');
|
|
51
67
|
const versions = options.versions;
|
|
52
|
-
|
|
53
|
-
// under the routes dir — the same mapping `collect.ts` uses for page URLs.
|
|
54
|
-
const docsBase = '/' + path.relative(routesDir, contentDir).split(path.sep).join('/');
|
|
68
|
+
const docsBase = docsBaseFrom(routesDir, contentDir);
|
|
55
69
|
const changelogFile = options.changelog === false ? undefined : path.resolve(options.changelog ?? 'CHANGELOG.md');
|
|
56
70
|
const changelogRoute = options.changelogPath ?? '/changelog';
|
|
57
|
-
const changelogOverrides = path.join(routesDir, changelogRoute
|
|
71
|
+
const changelogOverrides = path.join(routesDir, changelogRoute);
|
|
72
|
+
const indexOptions = { contentDir, routesDir, versions };
|
|
58
73
|
return {
|
|
59
74
|
name: 'docsmith-content',
|
|
60
75
|
enforce: 'pre',
|
|
@@ -73,25 +88,34 @@ function contentIndexPlugin(options) {
|
|
|
73
88
|
// body re-runs this load. A directory here is treated as an
|
|
74
89
|
// unresolvable import by vite:import-analysis; new/removed pages are
|
|
75
90
|
// handled by the watcher in configureServer.
|
|
91
|
+
const watch = (pages) => {
|
|
92
|
+
for (const page of pages)
|
|
93
|
+
this.addWatchFile(page.file);
|
|
94
|
+
};
|
|
76
95
|
if (id === VIRTUAL_CONTENT_ID) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
96
|
+
// Before anything is read: a config that disagrees with the docs root
|
|
97
|
+
// produces an index that is wrong rather than empty, so it has to fail
|
|
98
|
+
// here rather than render. Re-runs on invalidation, so archiving during
|
|
99
|
+
// `dev` reports immediately.
|
|
100
|
+
checkVersions(versions, discoverArchives(contentDir), ARCHIVE_MARKER);
|
|
101
|
+
const { pages, exists } = readSourcePages(contentDir);
|
|
102
|
+
watch(pages);
|
|
103
|
+
// Only this index has a date field, so only this one pays for git.
|
|
104
|
+
const docs = contentIndex(withCommitDates(pages), indexOptions);
|
|
105
|
+
warnAboutDocsRoot(contentDir, exists, docs.length);
|
|
80
106
|
const resolved = resolveVersions(versions, docsBase, docs);
|
|
81
107
|
return (`export const docs = ${JSON.stringify(docs, null, 2)};\n` +
|
|
82
108
|
`export const versions = ${JSON.stringify(resolved, null, 2)};\n`);
|
|
83
109
|
}
|
|
84
110
|
if (id === VIRTUAL_SEARCH_ID) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const docs =
|
|
88
|
-
return `export const docs = ${JSON.stringify(docs)};\n`;
|
|
111
|
+
const { pages } = readSourcePages(contentDir);
|
|
112
|
+
watch(pages);
|
|
113
|
+
return `export const docs = ${JSON.stringify(searchIndex(pages, indexOptions))};\n`;
|
|
89
114
|
}
|
|
90
115
|
if (id === VIRTUAL_LLMS_ID) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const docs =
|
|
94
|
-
return `export const docs = ${JSON.stringify(docs)};\n`;
|
|
116
|
+
const { pages } = readSourcePages(contentDir);
|
|
117
|
+
watch(pages);
|
|
118
|
+
return `export const docs = ${JSON.stringify(llmsIndex(pages, indexOptions))};\n`;
|
|
95
119
|
}
|
|
96
120
|
if (id === VIRTUAL_CHANGELOG_ID) {
|
|
97
121
|
if (!changelogFile)
|
|
@@ -1,5 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One doc page as authored on disk, before any index-specific derivation. Its
|
|
3
|
+
* URL, version, headings and search text are all derived from these fields, so
|
|
4
|
+
* everything downstream of the read is pure.
|
|
5
|
+
*/
|
|
6
|
+
export type SourcePage = {
|
|
7
|
+
/** Absolute path to the page file. */
|
|
8
|
+
file: string;
|
|
9
|
+
/** The page's raw markdown, frontmatter included. */
|
|
10
|
+
source: string;
|
|
11
|
+
/** The page's parsed frontmatter, `{}` when it has none. */
|
|
12
|
+
frontmatter: Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* The day (`YYYY-MM-DD`) the page last changed. Only {@link withCommitDates}
|
|
15
|
+
* fills this in, so it is absent on a page list that never went through it.
|
|
16
|
+
*/
|
|
17
|
+
lastUpdated?: string;
|
|
18
|
+
};
|
|
1
19
|
/** Whether a filename is a doc page the plugin should index. */
|
|
2
20
|
export declare function isPageFile(file: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Turn a page's path and raw markdown into a {@link SourcePage}. Pure, and
|
|
23
|
+
* shared with tests so an in-memory page is built exactly the way a read one is.
|
|
24
|
+
*/
|
|
25
|
+
export declare function toPage(file: string, source: string): SourcePage;
|
|
26
|
+
/**
|
|
27
|
+
* Read every doc page under `contentDir`. `exists` distinguishes a missing docs
|
|
28
|
+
* root from an empty one, which the page list alone cannot: both give no pages,
|
|
29
|
+
* but only one of them is a misconfiguration worth reporting.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readSourcePages(contentDir: string): {
|
|
32
|
+
pages: SourcePage[];
|
|
33
|
+
exists: boolean;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A page's title, when it has one. A page whose frontmatter has no string
|
|
37
|
+
* `title` is a stub: no index carries it, so nothing is derived for it and
|
|
38
|
+
* nothing is looked up about it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function titleOf(page: SourcePage): string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Date each page from the commit that last touched it, skipping the lookup for
|
|
43
|
+
* pages that carry their own `lastUpdated`. Archiving copies every page in one
|
|
44
|
+
* commit, so an archive's pages are written with the date they last really
|
|
45
|
+
* changed and must keep it.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately a second pass rather than part of {@link readSourcePages}: it
|
|
48
|
+
* spawns a git process per page, and only the content index has a date field.
|
|
49
|
+
* Folding it into the read would make the search and llms indexes pay for a
|
|
50
|
+
* value neither of them has anywhere to put. For the same reason it skips
|
|
51
|
+
* untitled pages, which no index carries.
|
|
52
|
+
*/
|
|
53
|
+
export declare function withCommitDates(pages: SourcePage[]): SourcePage[];
|
|
3
54
|
/**
|
|
4
55
|
* List the absolute paths of every `+page.md`/`+page.svx` under `contentDir`.
|
|
5
56
|
*/
|
|
@@ -1,10 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The docs root, and what a page is before any index has looked at it. Reading
|
|
3
|
+
* stops at a {@link SourcePage} list, which is the only value the index builders
|
|
4
|
+
* in `./collect.js` see — so the same builders run against a real docs root in
|
|
5
|
+
* the plugin and against an array of literals in tests.
|
|
6
|
+
*/
|
|
1
7
|
import fs from 'node:fs';
|
|
2
8
|
import path from 'node:path';
|
|
9
|
+
import { parseFrontmatter } from './frontmatter.js';
|
|
10
|
+
import { lastCommitDate } from '../git.js';
|
|
3
11
|
const PAGE_NAMES = ['+page.md', '+page.svx'];
|
|
4
12
|
/** Whether a filename is a doc page the plugin should index. */
|
|
5
13
|
export function isPageFile(file) {
|
|
6
14
|
return PAGE_NAMES.some((name) => file.endsWith(name));
|
|
7
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Turn a page's path and raw markdown into a {@link SourcePage}. Pure, and
|
|
18
|
+
* shared with tests so an in-memory page is built exactly the way a read one is.
|
|
19
|
+
*/
|
|
20
|
+
export function toPage(file, source) {
|
|
21
|
+
return { file, source, frontmatter: parseFrontmatter(source, file) };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read every doc page under `contentDir`. `exists` distinguishes a missing docs
|
|
25
|
+
* root from an empty one, which the page list alone cannot: both give no pages,
|
|
26
|
+
* but only one of them is a misconfiguration worth reporting.
|
|
27
|
+
*/
|
|
28
|
+
export function readSourcePages(contentDir) {
|
|
29
|
+
if (!fs.existsSync(contentDir))
|
|
30
|
+
return { pages: [], exists: false };
|
|
31
|
+
const pages = listPageFiles(contentDir).map((file) => toPage(file, fs.readFileSync(file, 'utf-8')));
|
|
32
|
+
return { pages, exists: true };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A page's title, when it has one. A page whose frontmatter has no string
|
|
36
|
+
* `title` is a stub: no index carries it, so nothing is derived for it and
|
|
37
|
+
* nothing is looked up about it.
|
|
38
|
+
*/
|
|
39
|
+
export function titleOf(page) {
|
|
40
|
+
return typeof page.frontmatter.title === 'string' ? page.frontmatter.title : undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Date each page from the commit that last touched it, skipping the lookup for
|
|
44
|
+
* pages that carry their own `lastUpdated`. Archiving copies every page in one
|
|
45
|
+
* commit, so an archive's pages are written with the date they last really
|
|
46
|
+
* changed and must keep it.
|
|
47
|
+
*
|
|
48
|
+
* Deliberately a second pass rather than part of {@link readSourcePages}: it
|
|
49
|
+
* spawns a git process per page, and only the content index has a date field.
|
|
50
|
+
* Folding it into the read would make the search and llms indexes pay for a
|
|
51
|
+
* value neither of them has anywhere to put. For the same reason it skips
|
|
52
|
+
* untitled pages, which no index carries.
|
|
53
|
+
*/
|
|
54
|
+
export function withCommitDates(pages) {
|
|
55
|
+
return pages.map((page) => {
|
|
56
|
+
if (titleOf(page) === undefined)
|
|
57
|
+
return page;
|
|
58
|
+
return {
|
|
59
|
+
...page,
|
|
60
|
+
lastUpdated: typeof page.frontmatter.lastUpdated === 'string'
|
|
61
|
+
? page.frontmatter.lastUpdated
|
|
62
|
+
: lastCommitDate(page.file)
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
8
66
|
/**
|
|
9
67
|
* List the absolute paths of every `+page.md`/`+page.svx` under `contentDir`.
|
|
10
68
|
*/
|
|
@@ -8,5 +8,3 @@ import type { ChangelogRelease } from '../../core/changelog.js';
|
|
|
8
8
|
* have one while the rest stay generated.
|
|
9
9
|
*/
|
|
10
10
|
export declare function collectReleases(changelogFile: string, overridesDir?: string, routePath?: string): ChangelogRelease[];
|
|
11
|
-
/** Default location of a monorepo package's changelog, relative to the app. */
|
|
12
|
-
export declare function defaultChangelogPath(cwd?: string): string;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
import { marked } from 'marked';
|
|
4
3
|
import { parseChangelog } from '../changelog.js';
|
|
5
|
-
import { changelogDates } from '
|
|
4
|
+
import { changelogDates } from '../git.js';
|
|
6
5
|
/**
|
|
7
6
|
* Render an entry's markdown to HTML at build time. Changesets entries use
|
|
8
7
|
* inline code, emphasis, links and nested lists, so shipping the raw markdown
|
|
@@ -51,7 +50,3 @@ export function collectReleases(changelogFile, overridesDir, routePath = '/chang
|
|
|
51
50
|
};
|
|
52
51
|
});
|
|
53
52
|
}
|
|
54
|
-
/** Default location of a monorepo package's changelog, relative to the app. */
|
|
55
|
-
export function defaultChangelogPath(cwd = process.cwd()) {
|
|
56
|
-
return path.resolve(cwd, 'CHANGELOG.md');
|
|
57
|
-
}
|
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
const { release }: { release: ChangelogRelease } = $props();
|
|
5
5
|
|
|
6
|
+
// `timeZone: 'UTC'` is not cosmetic. `release.date` is a calendar day, which
|
|
7
|
+
// parses to UTC midnight, so formatting in the ambient zone shifts it a day
|
|
8
|
+
// west of UTC — and shifts it on the client but not on the server, so the
|
|
9
|
+
// rendered date changes under hydration.
|
|
6
10
|
const formatted = $derived(
|
|
7
11
|
release.date
|
|
8
12
|
? new Date(release.date).toLocaleDateString('en-US', {
|
|
9
13
|
year: 'numeric',
|
|
10
14
|
month: 'long',
|
|
11
|
-
day: 'numeric'
|
|
15
|
+
day: 'numeric',
|
|
16
|
+
timeZone: 'UTC'
|
|
12
17
|
})
|
|
13
18
|
: undefined
|
|
14
19
|
);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { goto } from '$app/navigation';
|
|
3
3
|
import * as Command from '../shadcn/command/index.js';
|
|
4
4
|
import { useSearch } from '../../search/context.svelte.js';
|
|
5
|
+
import { useDocsPage } from '../docs-page-context.js';
|
|
5
6
|
import type { SearchDoc } from '../../core/index.js';
|
|
6
7
|
import type { SearchEngine } from '../../search/create-search.js';
|
|
7
8
|
import CornerDownLeft from '@lucide/svelte/icons/corner-down-left';
|
|
@@ -26,6 +27,10 @@
|
|
|
26
27
|
} = $props();
|
|
27
28
|
|
|
28
29
|
const search = useSearch();
|
|
30
|
+
// The version being read, straight off the shell's resolved page view, so
|
|
31
|
+
// results and the cached engine follow the reader across a version switch.
|
|
32
|
+
const docsPage = useDocsPage();
|
|
33
|
+
const activeVersion = $derived(docsPage.view.activeVersionId);
|
|
29
34
|
|
|
30
35
|
let query = $state('');
|
|
31
36
|
let engine = $state<SearchEngine | null>(null);
|
|
@@ -36,14 +41,12 @@
|
|
|
36
41
|
let engineVersion: string | undefined;
|
|
37
42
|
|
|
38
43
|
const trimmed = $derived(query.trim());
|
|
39
|
-
// Scope to the active version
|
|
40
|
-
const results = $derived(
|
|
41
|
-
engine && trimmed ? engine.search(query, undefined, search?.version) : []
|
|
42
|
-
);
|
|
44
|
+
// Scope to the active version on a versioned site; `undefined` otherwise.
|
|
45
|
+
const results = $derived(engine && trimmed ? engine.search(query, undefined, activeVersion) : []);
|
|
43
46
|
|
|
44
47
|
// Build the index the first time the palette opens; keep it for later opens.
|
|
45
48
|
async function ensureEngine() {
|
|
46
|
-
const version =
|
|
49
|
+
const version = activeVersion;
|
|
47
50
|
if ((engine && engineVersion === version) || status === 'loading') return;
|
|
48
51
|
status = 'loading';
|
|
49
52
|
try {
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import TriangleAlert from '@lucide/svelte/icons/triangle-alert';
|
|
3
3
|
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
|
4
|
-
import {
|
|
5
|
-
mapPathToVersion,
|
|
6
|
-
scopeContent,
|
|
7
|
-
type ResolvedVersion,
|
|
8
|
-
type DocsContentItem
|
|
9
|
-
} from '../../core/index.js';
|
|
4
|
+
import type { ResolvedVersion } from '../../core/index.js';
|
|
10
5
|
|
|
11
6
|
// Rendered by DocsShell only on an archived version, to warn a reader who
|
|
12
7
|
// landed there (usually from a search engine) and give them a one-click path
|
|
@@ -15,23 +10,13 @@
|
|
|
15
10
|
const {
|
|
16
11
|
active,
|
|
17
12
|
current,
|
|
18
|
-
|
|
19
|
-
content
|
|
13
|
+
href
|
|
20
14
|
}: {
|
|
21
15
|
active: ResolvedVersion;
|
|
22
16
|
current: ResolvedVersion;
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
/** This page under the current version, from the resolved page view. */
|
|
18
|
+
href: string;
|
|
25
19
|
} = $props();
|
|
26
|
-
|
|
27
|
-
const currentHref = $derived(
|
|
28
|
-
mapPathToVersion(
|
|
29
|
-
pathname,
|
|
30
|
-
active,
|
|
31
|
-
current,
|
|
32
|
-
scopeContent(content, current.id).map((c) => c.path)
|
|
33
|
-
)
|
|
34
|
-
);
|
|
35
20
|
</script>
|
|
36
21
|
|
|
37
22
|
<div class="docsmith-version-banner" role="note">
|
|
@@ -39,7 +24,7 @@
|
|
|
39
24
|
<p>
|
|
40
25
|
You're reading the <strong>{active.label}</strong> docs. The current version is
|
|
41
26
|
<strong>{current.label}</strong>.
|
|
42
|
-
<a href
|
|
27
|
+
<a {href}>View this page in {current.label} <ArrowRight size={14} /></a>
|
|
43
28
|
</p>
|
|
44
29
|
</div>
|
|
45
30
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ResolvedVersion } from '../../core/index.js';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
active: ResolvedVersion;
|
|
4
4
|
current: ResolvedVersion;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/** This page under the current version, from the resolved page view. */
|
|
6
|
+
href: string;
|
|
7
7
|
};
|
|
8
8
|
declare const VersionBanner: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
9
9
|
type VersionBanner = ReturnType<typeof VersionBanner>;
|