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.
Files changed (62) hide show
  1. package/bin/svelte-docsmith.mjs +13 -158
  2. package/dist/buildtime/archive.js +5 -3
  3. package/dist/buildtime/archives.d.ts +18 -0
  4. package/dist/buildtime/archives.js +43 -0
  5. package/dist/buildtime/cli/archive-version.d.ts +18 -0
  6. package/dist/buildtime/cli/archive-version.js +104 -0
  7. package/dist/buildtime/cli/error.d.ts +9 -0
  8. package/dist/buildtime/cli/error.js +9 -0
  9. package/dist/buildtime/cli/run.d.ts +13 -0
  10. package/dist/buildtime/cli/run.js +62 -0
  11. package/dist/buildtime/{vite/git.d.ts → git.d.ts} +1 -1
  12. package/dist/buildtime/{vite/git.js → git.js} +15 -3
  13. package/dist/buildtime/paths.d.ts +12 -0
  14. package/dist/buildtime/paths.js +23 -0
  15. package/dist/buildtime/vite/collect.d.ts +25 -16
  16. package/dist/buildtime/vite/collect.js +100 -106
  17. package/dist/buildtime/vite/extract.d.ts +0 -2
  18. package/dist/buildtime/vite/extract.js +1 -1
  19. package/dist/buildtime/vite/index.d.ts +0 -2
  20. package/dist/buildtime/vite/index.js +46 -22
  21. package/dist/buildtime/vite/pages.d.ts +51 -0
  22. package/dist/buildtime/vite/pages.js +58 -0
  23. package/dist/buildtime/vite/releases.d.ts +0 -2
  24. package/dist/buildtime/vite/releases.js +1 -6
  25. package/dist/components/changelog/changelog-entry.svelte +6 -1
  26. package/dist/components/chrome/search.svelte +8 -5
  27. package/dist/components/chrome/version-banner.svelte +5 -20
  28. package/dist/components/chrome/version-banner.svelte.d.ts +3 -3
  29. package/dist/components/chrome/version-switcher.svelte +39 -49
  30. package/dist/components/chrome/version-switcher.svelte.d.ts +3 -9
  31. package/dist/components/docs-page-context.d.ts +20 -0
  32. package/dist/components/docs-page-context.js +41 -0
  33. package/dist/components/layouts/copy-page-menu.svelte +4 -2
  34. package/dist/components/layouts/docs-header.svelte +18 -28
  35. package/dist/components/layouts/docs-header.svelte.d.ts +1 -9
  36. package/dist/components/layouts/docs-mobile-header.svelte +6 -21
  37. package/dist/components/layouts/docs-mobile-header.svelte.d.ts +1 -9
  38. package/dist/components/layouts/docs-shell.svelte +29 -28
  39. package/dist/components/layouts/docs-sidebar.svelte +1 -1
  40. package/dist/components/layouts/seo-head.svelte +7 -5
  41. package/dist/core/changelog.d.ts +7 -1
  42. package/dist/core/content.d.ts +6 -1
  43. package/dist/core/docs-page.d.ts +15 -0
  44. package/dist/core/docs-page.js +22 -7
  45. package/dist/core/index.d.ts +1 -1
  46. package/dist/core/version.d.ts +36 -0
  47. package/dist/core/version.js +105 -8
  48. package/dist/generate/feed.d.ts +4 -1
  49. package/dist/generate/feed.js +7 -19
  50. package/dist/generate/llms.d.ts +4 -1
  51. package/dist/generate/llms.js +3 -2
  52. package/dist/generate/sitemap.d.ts +4 -1
  53. package/dist/generate/sitemap.js +3 -16
  54. package/dist/generate/xml.d.ts +12 -0
  55. package/dist/generate/xml.js +27 -0
  56. package/dist/search/context.svelte.d.ts +0 -2
  57. package/dist/search/context.svelte.js +0 -2
  58. package/dist/utils/url.d.ts +54 -0
  59. package/dist/utils/url.js +84 -0
  60. package/package.json +1 -1
  61. package/dist/utils/normalize-path.d.ts +0 -6
  62. package/dist/utils/normalize-path.js +0 -8
@@ -1,161 +1,16 @@
1
1
  #!/usr/bin/env node
2
- // The svelte-docsmith maintenance CLI. Currently one command: `archive-version`,
3
- // which freezes today's docs into an archived version folder so they stay live
4
- // after a breaking release.
5
- //
6
- // The current version is served unprefixed from the docs root and is the folder
7
- // you keep editing; archives are copies under their own prefix. See
8
- // docs/adr/0001-unprefixed-current-docs.md. The text transforms live in
9
- // `src/lib/buildtime/archive.ts` so they are typechecked and unit-tested; this
10
- // file is the filesystem wrapper around them.
11
- import fs from 'node:fs';
12
- import path from 'node:path';
13
- import { execFileSync } from 'node:child_process';
14
- import {
15
- rewriteDocsLinks,
16
- freezeLastUpdated,
17
- isInheritedRouteFile
18
- } from '../dist/buildtime/archive.js';
19
-
20
- const HELP = `svelte-docsmith — docs maintenance CLI
21
-
22
- Usage:
23
- svelte-docsmith archive-version <id> [options]
24
-
25
- Freeze the current docs into an archived version folder. The archive keeps
26
- serving the release it documents while you go on editing the docs root.
27
-
28
- Options:
29
- --label <label> switcher label for the archive (default: the id)
30
- --content <dir> docs content directory (default: src/routes/docs)
31
- --routes <dir> SvelteKit routes directory (default: src/routes)
32
- `;
33
-
34
- // Written into every archive this command creates, so later runs know which
35
- // directories are already archives and must not be copied into the new one.
36
- const MARKER = '.docsmith-archive';
37
-
38
- const [command, ...rest] = process.argv.slice(2);
39
-
40
- if (command !== 'archive-version') {
41
- console.log(HELP);
42
- process.exit(command ? 1 : 0);
43
- }
44
-
45
- const opts = { content: 'src/routes/docs', routes: 'src/routes', label: undefined };
46
- let id;
47
- for (let i = 0; i < rest.length; i++) {
48
- const arg = rest[i];
49
- if (arg === '--label') opts.label = rest[++i];
50
- else if (arg === '--content') opts.content = rest[++i];
51
- else if (arg === '--routes') opts.routes = rest[++i];
52
- else if (!arg.startsWith('--') && !id) id = arg;
53
- }
54
-
55
- const fail = (message) => {
56
- console.error(`svelte-docsmith archive-version: ${message}`);
57
- process.exit(1);
58
- };
59
-
60
- if (!id) fail('missing <id>. e.g. `svelte-docsmith archive-version v1`');
61
- // Must start alphanumeric: the id is both a directory name and a URL segment, so
62
- // `.`, `..` and dotfiles would escape the docs root or make a hidden, dead route.
63
- if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) {
64
- fail(`invalid id: ${id}. Start with a letter or digit; it is used as a URL segment.`);
65
- }
66
-
67
- const contentDir = path.resolve(opts.content);
68
- const routesDir = path.resolve(opts.routes);
69
- const toDir = path.join(contentDir, id);
70
- const rel = (p) => path.relative(process.cwd(), p);
71
-
72
- if (!fs.existsSync(contentDir)) fail(`content directory not found: ${rel(contentDir)}`);
73
- if (fs.existsSync(toDir)) fail(`target already exists: ${rel(toDir)}`);
74
-
75
- // The docs URL base (e.g. `/docs`), derived exactly as the vite plugin does.
76
- const docsBase = '/' + path.relative(routesDir, contentDir).split(path.sep).join('/');
77
-
78
- /** Directories directly under the docs root that are already archives. */
79
- const archivedIds = new Set(
80
- fs
81
- .readdirSync(contentDir, { withFileTypes: true })
82
- .filter((e) => e.isDirectory() && fs.existsSync(path.join(contentDir, e.name, MARKER)))
83
- .map((e) => e.name)
84
- );
85
-
86
- const isPage = (name) => name.endsWith('+page.md') || name.endsWith('+page.svx');
87
-
88
- /** Copy the docs root into the archive, skipping what the archive shouldn't hold. */
89
- function copyCurrent(from, to) {
90
- fs.mkdirSync(to, { recursive: true });
91
- for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
92
- const src = path.join(from, entry.name);
93
- const dest = path.join(to, entry.name);
94
- if (entry.isDirectory()) {
95
- // Never descend into the archive being written (it lives inside the docs
96
- // root) or into an archive written by an earlier run.
97
- if (src === toDir) continue;
98
- if (from === contentDir && archivedIds.has(entry.name)) continue;
99
- copyCurrent(src, dest);
100
- } else {
101
- // The root layout and error page already apply to the archive, which is
102
- // nested inside them; copying them would nest a second DocsShell.
103
- if (from === contentDir && isInheritedRouteFile(entry.name)) continue;
104
- fs.copyFileSync(src, dest);
105
- }
106
- }
107
- }
108
-
109
- /** Every file in the archive, paired with the source file it was copied from. */
110
- function* eachCopiedFile(dir = toDir) {
111
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
112
- const file = path.join(dir, entry.name);
113
- if (entry.isDirectory()) yield* eachCopiedFile(file);
114
- else yield { file, source: path.join(contentDir, path.relative(toDir, file)) };
2
+ // Process boundary only. The command itself lives in
3
+ // `src/lib/buildtime/cli/`, where it is typechecked and unit-tested; this file
4
+ // exists so `svelte-docsmith` is a runnable bin entry.
5
+ import { run, CliError } from '../dist/buildtime/cli/run.js';
6
+
7
+ try {
8
+ run(process.argv.slice(2));
9
+ } catch (error) {
10
+ if (error instanceof CliError) {
11
+ // A mistake the user can fix. The message says how; a stack would not help.
12
+ console.error(`svelte-docsmith: ${error.message}`);
13
+ process.exit(1);
115
14
  }
15
+ throw error;
116
16
  }
117
-
118
- /** The date a page last really changed, so the archive doesn't claim today. */
119
- function lastCommitDate(file) {
120
- try {
121
- const out = execFileSync('git', ['log', '-1', '--format=%cs', '--', file], {
122
- cwd: path.dirname(file),
123
- encoding: 'utf-8',
124
- stdio: ['ignore', 'pipe', 'ignore']
125
- }).trim();
126
- return out || undefined;
127
- } catch {
128
- return undefined;
129
- }
130
- }
131
-
132
- copyCurrent(contentDir, toDir);
133
- fs.writeFileSync(
134
- path.join(toDir, MARKER),
135
- `Archived docs for ${id}, created by \`svelte-docsmith archive-version\`.\n` +
136
- `This folder is frozen: edit the docs root instead.\n`
137
- );
138
-
139
- let pages = 0;
140
- for (const { file, source } of eachCopiedFile()) {
141
- if (!isPage(path.basename(file))) continue;
142
- pages++;
143
- const text = fs.readFileSync(file, 'utf-8');
144
- fs.writeFileSync(
145
- file,
146
- freezeLastUpdated(
147
- rewriteDocsLinks(text, { docsBase, versionId: id, archivedIds }),
148
- lastCommitDate(source)
149
- )
150
- );
151
- }
152
-
153
- const label = opts.label ?? id;
154
- console.log(`\n✓ Archived the current docs into ${rel(toDir)} (${pages} pages)\n`);
155
- console.log('Links were rewritten to stay inside the archive, and each page kept');
156
- console.log('its real last-updated date. Review the diff, then update');
157
- console.log('docsmith({ versions }) so the archive is served:\n');
158
- console.log(' versions: {');
159
- console.log(" current: { id: '<new release>', label: '<new release>' },");
160
- console.log(` archived: [{ id: '${id}', label: '${label}' }${archivedIds.size ? ', …' : ''}]`);
161
- console.log(' }\n');
@@ -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}`;
@@ -0,0 +1,18 @@
1
+ import type { ArchivesOnDisk } from '../core/version.js';
2
+ /**
3
+ * Written into every archive `archive-version` creates. Not a convenience for
4
+ * the command: this file is what makes a directory an archived version rather
5
+ * than an ordinary section of the current docs, and the build reads it. The two
6
+ * are otherwise indistinguishable, since a page is assigned to a version by its
7
+ * first directory segment. See
8
+ * `docs/adr/0003-the-archive-marker-defines-an-archive.md`.
9
+ */
10
+ export declare const ARCHIVE_MARKER = ".docsmith-archive";
11
+ /** The text written into a new archive's marker file. */
12
+ export declare function markerContents(id: string): string;
13
+ /**
14
+ * The directories directly under `contentDir`, and which of them are archives.
15
+ * An absent docs root reads as empty rather than throwing: the collectors
16
+ * already report that case, and the command has its own message for it.
17
+ */
18
+ export declare function discoverArchives(contentDir: string): ArchivesOnDisk;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * What the docs root says about itself: which directories under it are archived
3
+ * versions. The scan is deliberately thin, so the reconciliation it feeds
4
+ * (`checkVersions`) stays pure and testable without a filesystem.
5
+ *
6
+ * Both callers cross this seam. The vite plugin scans before resolving versions;
7
+ * `archive-version` scans so it knows which directories not to copy into the
8
+ * archive it is writing.
9
+ */
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ /**
13
+ * Written into every archive `archive-version` creates. Not a convenience for
14
+ * the command: this file is what makes a directory an archived version rather
15
+ * than an ordinary section of the current docs, and the build reads it. The two
16
+ * are otherwise indistinguishable, since a page is assigned to a version by its
17
+ * first directory segment. See
18
+ * `docs/adr/0003-the-archive-marker-defines-an-archive.md`.
19
+ */
20
+ export const ARCHIVE_MARKER = '.docsmith-archive';
21
+ /** The text written into a new archive's marker file. */
22
+ export function markerContents(id) {
23
+ return (`Archived docs for ${id}, created by \`svelte-docsmith archive-version\`.\n` +
24
+ `This folder is frozen: edit the docs root instead.\n` +
25
+ `Removing this file makes the build treat it as current-version content.\n`);
26
+ }
27
+ /**
28
+ * The directories directly under `contentDir`, and which of them are archives.
29
+ * An absent docs root reads as empty rather than throwing: the collectors
30
+ * already report that case, and the command has its own message for it.
31
+ */
32
+ export function discoverArchives(contentDir) {
33
+ if (!fs.existsSync(contentDir))
34
+ return { marked: [], directories: [] };
35
+ const directories = fs
36
+ .readdirSync(contentDir, { withFileTypes: true })
37
+ .filter((entry) => entry.isDirectory())
38
+ .map((entry) => entry.name);
39
+ return {
40
+ directories,
41
+ marked: directories.filter((name) => fs.existsSync(path.join(contentDir, name, ARCHIVE_MARKER)))
42
+ };
43
+ }
@@ -0,0 +1,18 @@
1
+ export type ArchiveVersionOptions = {
2
+ /** Id of the archive to create. Becomes its directory name and URL segment. */
3
+ id: string;
4
+ /** Switcher label for the archive. Defaults to the id. */
5
+ label?: string;
6
+ /** Docs content directory, resolved against `cwd`. */
7
+ content?: string;
8
+ /** SvelteKit routes directory, resolved against `cwd`. */
9
+ routes?: string;
10
+ /** Working directory the relative options resolve against. */
11
+ cwd?: string;
12
+ };
13
+ /**
14
+ * Copy the docs root into `<content>/<id>/`, mark it as an archive, and rewrite
15
+ * each copied page so it stays inside the archive and keeps its real
16
+ * last-updated date. Returns the number of pages archived.
17
+ */
18
+ export declare function archiveVersion(options: ArchiveVersionOptions, log: (line: string) => void): number;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * `svelte-docsmith archive-version <id>`: freeze the current docs into an
3
+ * archived version folder so they keep serving the release they document while
4
+ * the docs root goes on being edited.
5
+ *
6
+ * The pure text transforms live in `../archive.js`; this is the filesystem work
7
+ * around them. It takes its working directory and its output sink rather than
8
+ * reading `process.cwd()` and calling `console.log`, so the whole command runs
9
+ * in-process under test. See `docs/adr/0002-archives-are-rewritten-source-copies.md`.
10
+ */
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { assertValidVersionId } from '../../core/version.js';
14
+ import { freezeLastUpdated, isInheritedRouteFile, rewriteDocsLinks } from '../archive.js';
15
+ import { ARCHIVE_MARKER, discoverArchives, markerContents } from '../archives.js';
16
+ import { lastCommitDate } from '../git.js';
17
+ import { docsBaseFrom } from '../paths.js';
18
+ import { isPageFile } from '../vite/pages.js';
19
+ import { CliError } from './error.js';
20
+ /**
21
+ * Copy the docs root into `<content>/<id>/`, mark it as an archive, and rewrite
22
+ * each copied page so it stays inside the archive and keeps its real
23
+ * last-updated date. Returns the number of pages archived.
24
+ */
25
+ export function archiveVersion(options, log) {
26
+ const cwd = options.cwd ?? process.cwd();
27
+ const { id, label = options.id } = options;
28
+ try {
29
+ assertValidVersionId(id);
30
+ }
31
+ catch (error) {
32
+ throw new CliError(error.message);
33
+ }
34
+ const contentDir = path.resolve(cwd, options.content ?? 'src/routes/docs');
35
+ const routesDir = path.resolve(cwd, options.routes ?? 'src/routes');
36
+ const toDir = path.join(contentDir, id);
37
+ const rel = (p) => path.relative(cwd, p);
38
+ if (!fs.existsSync(contentDir)) {
39
+ throw new CliError(`content directory not found: ${rel(contentDir)}`);
40
+ }
41
+ if (fs.existsSync(toDir)) {
42
+ throw new CliError(`target already exists: ${rel(toDir)}`);
43
+ }
44
+ const docsBase = docsBaseFrom(routesDir, contentDir);
45
+ // Archives already on disk, so this run neither copies them into the new
46
+ // archive nor rewrites links that already point into one of them.
47
+ const archivedIds = new Set(discoverArchives(contentDir).marked);
48
+ /** Copy the docs root into the archive, skipping what the archive shouldn't hold. */
49
+ function copyCurrent(from, to) {
50
+ fs.mkdirSync(to, { recursive: true });
51
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
52
+ const src = path.join(from, entry.name);
53
+ const dest = path.join(to, entry.name);
54
+ if (entry.isDirectory()) {
55
+ // Never descend into the archive being written (it lives inside the docs
56
+ // root) or into an archive written by an earlier run.
57
+ if (src === toDir)
58
+ continue;
59
+ if (from === contentDir && archivedIds.has(entry.name))
60
+ continue;
61
+ copyCurrent(src, dest);
62
+ }
63
+ else {
64
+ // The root layout and error page already apply to the archive, which is
65
+ // nested inside them; copying them would nest a second DocsShell.
66
+ if (from === contentDir && isInheritedRouteFile(entry.name))
67
+ continue;
68
+ fs.copyFileSync(src, dest);
69
+ }
70
+ }
71
+ }
72
+ /** Every file in the archive, paired with the source file it was copied from. */
73
+ function* eachCopiedFile(dir = toDir) {
74
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
75
+ const file = path.join(dir, entry.name);
76
+ if (entry.isDirectory())
77
+ yield* eachCopiedFile(file);
78
+ else
79
+ yield { file, source: path.join(contentDir, path.relative(toDir, file)) };
80
+ }
81
+ }
82
+ copyCurrent(contentDir, toDir);
83
+ fs.writeFileSync(path.join(toDir, ARCHIVE_MARKER), markerContents(id));
84
+ let pages = 0;
85
+ for (const { file, source } of eachCopiedFile()) {
86
+ if (!isPageFile(path.basename(file)))
87
+ continue;
88
+ pages++;
89
+ const text = fs.readFileSync(file, 'utf-8');
90
+ fs.writeFileSync(file, freezeLastUpdated(rewriteDocsLinks(text, { docsBase, versionId: id, archivedIds }), lastCommitDate(source)));
91
+ }
92
+ 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');
96
+ log(' versions: {');
97
+ log(" current: { id: '<new release>', label: '<new release>' },");
98
+ log(` archived: [{ id: '${id}', label: '${label}' }${archivedIds.size ? ', …' : ''}]`);
99
+ log(' }\n');
100
+ // Until that paste lands the build fails, by design: the archive is on disk
101
+ // and undeclared. See docs/adr/0003-the-archive-marker-defines-an-archive.md.
102
+ log('Until then the build will fail, reporting this archive as undeclared.\n');
103
+ return pages;
104
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A failure the user caused and can fix: a missing argument, a bad id, a target
3
+ * that already exists. The `bin/` shim prints its message and exits 1, without
4
+ * a stack, because a stack is noise for these. Anything else thrown out of the
5
+ * command is a bug and keeps its stack.
6
+ */
7
+ export declare class CliError extends Error {
8
+ readonly name = "CliError";
9
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A failure the user caused and can fix: a missing argument, a bad id, a target
3
+ * that already exists. The `bin/` shim prints its message and exits 1, without
4
+ * a stack, because a stack is noise for these. Anything else thrown out of the
5
+ * command is a bug and keeps its stack.
6
+ */
7
+ export class CliError extends Error {
8
+ name = 'CliError';
9
+ }
@@ -0,0 +1,13 @@
1
+ export { CliError } from './error.js';
2
+ export { archiveVersion } from './archive-version.js';
3
+ export declare const HELP = "svelte-docsmith \u2014 docs maintenance CLI\n\nUsage:\n svelte-docsmith archive-version <id> [options]\n\nFreeze the current docs into an archived version folder. The archive keeps\nserving the release it documents while you go on editing the docs root.\n\nOptions:\n --label <label> switcher label for the archive (default: the id)\n --content <dir> docs content directory (default: src/routes/docs)\n --routes <dir> SvelteKit routes directory (default: src/routes)\n";
4
+ /** Where the command writes, and what it resolves relative paths against. */
5
+ export type CliIo = {
6
+ cwd?: string;
7
+ log?: (line: string) => void;
8
+ };
9
+ /**
10
+ * Run the CLI over `argv` (everything after the node binary and script). Throws
11
+ * {@link CliError} for anything the user can fix; the caller owns the exit code.
12
+ */
13
+ export declare function run(argv: string[], io?: CliIo): void;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The `svelte-docsmith` maintenance CLI. Currently one command:
3
+ * `archive-version`.
4
+ *
5
+ * This is the whole command, not a helper for it: `bin/svelte-docsmith.mjs` is a
6
+ * shim that hands over `process.argv` and turns a thrown {@link CliError} into a
7
+ * message and an exit code. Keeping the process boundary out here means the
8
+ * command is typechecked, and testable in-process against a temporary directory
9
+ * rather than by spawning node and parsing stdout.
10
+ */
11
+ import { archiveVersion } from './archive-version.js';
12
+ import { CliError } from './error.js';
13
+ export { CliError } from './error.js';
14
+ export { archiveVersion } from './archive-version.js';
15
+ export const HELP = `svelte-docsmith — docs maintenance CLI
16
+
17
+ Usage:
18
+ svelte-docsmith archive-version <id> [options]
19
+
20
+ Freeze the current docs into an archived version folder. The archive keeps
21
+ serving the release it documents while you go on editing the docs root.
22
+
23
+ Options:
24
+ --label <label> switcher label for the archive (default: the id)
25
+ --content <dir> docs content directory (default: src/routes/docs)
26
+ --routes <dir> SvelteKit routes directory (default: src/routes)
27
+ `;
28
+ /** Parse `archive-version`'s flags and its one positional id. */
29
+ function parseArchiveArgs(rest) {
30
+ const options = {};
31
+ for (let i = 0; i < rest.length; i++) {
32
+ const arg = rest[i];
33
+ if (arg === '--label')
34
+ options.label = rest[++i];
35
+ else if (arg === '--content')
36
+ options.content = rest[++i];
37
+ else if (arg === '--routes')
38
+ options.routes = rest[++i];
39
+ else if (!arg.startsWith('--') && !options.id)
40
+ options.id = arg;
41
+ }
42
+ if (!options.id) {
43
+ throw new CliError('missing <id>. e.g. `svelte-docsmith archive-version v1`');
44
+ }
45
+ return options;
46
+ }
47
+ /**
48
+ * Run the CLI over `argv` (everything after the node binary and script). Throws
49
+ * {@link CliError} for anything the user can fix; the caller owns the exit code.
50
+ */
51
+ export function run(argv, io = {}) {
52
+ const log = io.log ?? ((line) => console.log(line));
53
+ const [command, ...rest] = argv;
54
+ if (!command) {
55
+ log(HELP);
56
+ return;
57
+ }
58
+ if (command !== 'archive-version') {
59
+ throw new CliError(`unknown command: ${command}\n\n${HELP}`);
60
+ }
61
+ archiveVersion({ ...parseArchiveArgs(rest), cwd: io.cwd }, log);
62
+ }
@@ -1,4 +1,4 @@
1
- /** Last git commit date (strict ISO) for a file, or undefined outside a repo. */
1
+ /** Commit day (`YYYY-MM-DD`) a file last changed, or undefined outside a repo. */
2
2
  export declare function lastCommitDate(file: string): string | undefined;
3
3
  /**
4
4
  * Release dates keyed by version, read from the commit that introduced each
@@ -1,8 +1,20 @@
1
+ /**
2
+ * The git queries the build makes: when a page last changed, and when each
3
+ * release landed. Used by both the vite plugin and the `archive-version`
4
+ * command, which is why it sits here rather than under `vite/`.
5
+ *
6
+ * Every date here is `%cs`, the commit's calendar day (`YYYY-MM-DD`), never a
7
+ * timestamp. Dates from this module are rendered as a day and nothing more, and
8
+ * a date-only string parses to UTC midnight, so pinning the formatter to UTC
9
+ * renders the committer's own day identically for every reader and identically
10
+ * on the server and the client. A full timestamp buys precision no consumer
11
+ * uses and reintroduces the day-boundary drift.
12
+ */
1
13
  import { spawnSync } from 'node:child_process';
2
14
  import path from 'node:path';
3
- /** Last git commit date (strict ISO) for a file, or undefined outside a repo. */
15
+ /** Commit day (`YYYY-MM-DD`) a file last changed, or undefined outside a repo. */
4
16
  export function lastCommitDate(file) {
5
- const res = spawnSync('git', ['log', '-1', '--format=%cI', '--', file], {
17
+ const res = spawnSync('git', ['log', '-1', '--format=%cs', '--', file], {
6
18
  cwd: path.dirname(file),
7
19
  encoding: 'utf-8'
8
20
  });
@@ -19,7 +31,7 @@ export function changelogDates(file) {
19
31
  const dates = new Map();
20
32
  // `-L` would be per-line; instead walk the file's commits newest-first and
21
33
  // record the first commit in which each version heading appears.
22
- const log = spawnSync('git', ['log', '--format=%H %cI', '--reverse', '--follow', '--', path.basename(file)], { cwd: path.dirname(file), encoding: 'utf-8' });
34
+ const log = spawnSync('git', ['log', '--format=%H %cs', '--reverse', '--follow', '--', path.basename(file)], { cwd: path.dirname(file), encoding: 'utf-8' });
23
35
  if (log.status !== 0)
24
36
  return dates;
25
37
  for (const line of log.stdout.trim().split('\n').filter(Boolean)) {
@@ -0,0 +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;
6
+ /**
7
+ * The docs URL base, e.g. `/docs`, from the content directory's location under
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`.
11
+ */
12
+ export declare function docsBaseFrom(routesDir: string, contentDir: string): string;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Build-time path derivations shared by the vite plugin and the maintenance CLI.
3
+ * Both have to agree on where the docs live in URL space, and each deriving it
4
+ * separately is how they drift.
5
+ */
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
+ }
15
+ /**
16
+ * The docs URL base, e.g. `/docs`, from the content directory's location under
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`.
20
+ */
21
+ export function docsBaseFrom(routesDir, contentDir) {
22
+ return urlFor(routesDir, contentDir);
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[];