svelte-docsmith 0.14.0 → 0.16.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 (94) hide show
  1. package/README.md +47 -14
  2. package/dist/buildtime/markdown-source.d.ts +11 -0
  3. package/dist/buildtime/markdown-source.js +48 -1
  4. package/dist/buildtime/paths.d.ts +5 -0
  5. package/dist/buildtime/paths.js +7 -0
  6. package/dist/buildtime/props/extract.d.ts +25 -0
  7. package/dist/buildtime/props/extract.js +169 -0
  8. package/dist/buildtime/props/parse.d.ts +17 -0
  9. package/dist/buildtime/props/parse.js +108 -0
  10. package/dist/buildtime/props/typescript.d.ts +54 -0
  11. package/dist/buildtime/props/typescript.js +345 -0
  12. package/dist/buildtime/vite/agent-markdown.d.ts +43 -0
  13. package/dist/buildtime/vite/agent-markdown.js +204 -0
  14. package/dist/buildtime/vite/collect.d.ts +4 -1
  15. package/dist/buildtime/vite/collect.js +27 -44
  16. package/dist/buildtime/vite/extract.js +8 -3
  17. package/dist/buildtime/vite/fields.d.ts +49 -0
  18. package/dist/buildtime/vite/fields.js +72 -0
  19. package/dist/buildtime/vite/frontmatter-schema.d.ts +38 -0
  20. package/dist/buildtime/vite/frontmatter-schema.js +96 -0
  21. package/dist/buildtime/vite/index.d.ts +16 -0
  22. package/dist/buildtime/vite/index.js +119 -7
  23. package/dist/buildtime/vite/pages.d.ts +5 -0
  24. package/dist/buildtime/vite/pages.js +10 -5
  25. package/dist/client.d.ts +24 -0
  26. package/dist/components/chrome/copy-button.svelte +4 -1
  27. package/dist/components/chrome/copy-button.svelte.d.ts +2 -0
  28. package/dist/components/docs/accordion-item.svelte +2 -1
  29. package/dist/components/docs/accordion-item.svelte.d.ts +2 -1
  30. package/dist/components/docs/accordion.svelte +2 -1
  31. package/dist/components/docs/accordion.svelte.d.ts +2 -1
  32. package/dist/components/docs/badge.svelte +4 -3
  33. package/dist/components/docs/badge.svelte.d.ts +4 -3
  34. package/dist/components/docs/callout.svelte +6 -2
  35. package/dist/components/docs/callout.svelte.d.ts +6 -2
  36. package/dist/components/docs/card-grid.svelte +6 -1
  37. package/dist/components/docs/card-grid.svelte.d.ts +1 -0
  38. package/dist/components/docs/card.svelte +4 -2
  39. package/dist/components/docs/card.svelte.d.ts +4 -2
  40. package/dist/components/docs/file-tree-item.svelte +5 -2
  41. package/dist/components/docs/file-tree-item.svelte.d.ts +5 -2
  42. package/dist/components/docs/file-tree.svelte +6 -1
  43. package/dist/components/docs/file-tree.svelte.d.ts +1 -0
  44. package/dist/components/docs/kbd.svelte +6 -1
  45. package/dist/components/docs/kbd.svelte.d.ts +1 -0
  46. package/dist/components/docs/live-example.svelte +5 -2
  47. package/dist/components/docs/live-example.svelte.d.ts +5 -2
  48. package/dist/components/docs/prop.svelte +43 -12
  49. package/dist/components/docs/prop.svelte.d.ts +4 -0
  50. package/dist/components/docs/props-table.svelte +73 -6
  51. package/dist/components/docs/props-table.svelte.d.ts +9 -2
  52. package/dist/components/docs/step.svelte +1 -0
  53. package/dist/components/docs/step.svelte.d.ts +1 -0
  54. package/dist/components/docs/steps.svelte +6 -1
  55. package/dist/components/docs/steps.svelte.d.ts +1 -0
  56. package/dist/components/docs/tab-item.svelte +6 -2
  57. package/dist/components/docs/tab-item.svelte.d.ts +6 -2
  58. package/dist/components/docs/tabs.svelte +8 -4
  59. package/dist/components/docs/tabs.svelte.d.ts +8 -4
  60. package/dist/components/landing/action.svelte +5 -2
  61. package/dist/components/landing/action.svelte.d.ts +5 -2
  62. package/dist/components/landing/cta.svelte +4 -3
  63. package/dist/components/landing/cta.svelte.d.ts +4 -3
  64. package/dist/components/landing/feature-grid.svelte +7 -4
  65. package/dist/components/landing/feature-grid.svelte.d.ts +7 -4
  66. package/dist/components/landing/feature.svelte +1 -0
  67. package/dist/components/landing/feature.svelte.d.ts +1 -0
  68. package/dist/components/landing/hero.svelte +5 -5
  69. package/dist/components/landing/hero.svelte.d.ts +5 -5
  70. package/dist/components/layouts/copy-page-menu.svelte +110 -17
  71. package/dist/components/layouts/copy-page-menu.svelte.d.ts +4 -0
  72. package/dist/components/layouts/docs-shell.svelte +7 -1
  73. package/dist/components/layouts/docs-shell.svelte.d.ts +1 -0
  74. package/dist/core/config.d.ts +9 -0
  75. package/dist/core/config.js +2 -1
  76. package/dist/core/content.d.ts +27 -0
  77. package/dist/core/extracted-props.d.ts +44 -0
  78. package/dist/core/extracted-props.js +8 -0
  79. package/dist/core/index.d.ts +1 -1
  80. package/dist/core/version.d.ts +4 -2
  81. package/dist/core/version.js +1 -1
  82. package/dist/generate/llms.d.ts +15 -0
  83. package/dist/generate/llms.js +6 -2
  84. package/dist/index.d.ts +3 -1
  85. package/dist/index.js +5 -0
  86. package/dist/mcp/connect.d.ts +17 -0
  87. package/dist/mcp/connect.js +40 -0
  88. package/dist/mcp/handler.d.ts +57 -0
  89. package/dist/mcp/handler.js +391 -0
  90. package/dist/mcp/tools.d.ts +81 -0
  91. package/dist/mcp/tools.js +196 -0
  92. package/dist/utils/type-text.d.ts +13 -0
  93. package/dist/utils/type-text.js +49 -0
  94. package/package.json +14 -2
package/README.md CHANGED
@@ -24,8 +24,11 @@ live table of contents. No per-page wiring, no content collection to configure.
24
24
  > breaking changes ride a major.
25
25
  >
26
26
  > **What stays experimental.** Versioning: `DocsVersions`, `currentOnly`, the
27
- > archive marker, and the `archive-version` command. They may still change in a
28
- > minor until authors have used them for real.
27
+ > archive marker, and the `archive-version` command. Generated props tables:
28
+ > `?props` imports and `PropsTable`'s `of`. The MCP server: `mcpHandler` and the
29
+ > `mcp` config field. Agent markdown: the props tables and example code written
30
+ > into `llms-full.txt` and `.md` pages. They may still change in a minor until
31
+ > authors have used them for real.
29
32
 
30
33
  ## Install
31
34
 
@@ -170,6 +173,35 @@ plugin you already added:
170
173
  </LiveExample>
171
174
  ```
172
175
 
176
+ To type the `?source` import, reference the package's client types once in
177
+ `src/app.d.ts`:
178
+
179
+ ```ts
180
+ /// <reference types="svelte-docsmith/client" />
181
+ ```
182
+
183
+ ## Props tables from your types
184
+
185
+ `PropsTable` can read a component's API straight from its typed `$props()`, so
186
+ the reference can't drift from the code either. Import the component with
187
+ `?props` and pass it to `of`:
188
+
189
+ ```svelte
190
+ <script>
191
+ import { PropsTable } from 'svelte-docsmith';
192
+ import buttonProps from '#lib/button.svelte?props';
193
+ </script>
194
+
195
+ <PropsTable of={buttonProps} />
196
+ ```
197
+
198
+ Types come from the TypeScript checker (an imported `ButtonVariant` alias shows
199
+ its values), descriptions from JSDoc, defaults from the destructuring. Snippets,
200
+ callbacks and `$bindable()` props all get rows, and inherited HTML attributes
201
+ collapse to one line. It needs the optional peers `typescript` and `svelte2tsx`
202
+ (`npm i -D typescript svelte2tsx`). Generated props tables are **experimental**:
203
+ outside the 1.0 promise for now.
204
+
173
205
  ## Batteries included
174
206
 
175
207
  - **Search.** A ⌘K / Ctrl-K command palette over a build-time full-text index, no
@@ -214,18 +246,19 @@ override the tokens yourself.
214
246
 
215
247
  ## What's exported
216
248
 
217
- | Entry point | What it is |
218
- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
219
- | `svelte-docsmith` | Components (`DocsShell`, `Changelog`, landing sections, `LiveExample`, `Callout`, …), `defineConfig`, `createSearchEngine`, `generateSitemap` / `generateLlmsTxt` / `generateLlmsFullTxt` / `generateFeed`, `currentOnly`, and the types |
220
- | `svelte-docsmith/preprocess` | `docsmith()`, the mdsvex/Shiki pipeline (Node, config time) |
221
- | `svelte-docsmith/vite` | `docsmith()`, the content / search / llms / changelog indexes and `?source` transform (Node, build) |
222
- | `svelte-docsmith/content` | `docs` and `versions`, the generated sidebar content index and version manifest |
223
- | `svelte-docsmith/search` | `docs`, the generated full-text search index (lazy-load it) |
224
- | `svelte-docsmith/llms` | `docs`, the generated LLM content index (for `llms.txt` / `llms-full.txt` routes) |
225
- | `svelte-docsmith/changelog` | `releases`, the generated changelog index (parsed from `CHANGELOG.md`) |
226
- | `svelte-docsmith/mermaid` | the Mermaid diagram component (lazy-loaded by the preprocessor when a mermaid fence is present) |
227
- | `svelte-docsmith/theme.css` | the base style contract |
228
- | `svelte-docsmith/themes/*.css` | the eleven theme presets |
249
+ | Entry point | What it is |
250
+ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
251
+ | `svelte-docsmith` | Components (`DocsShell`, `Changelog`, landing sections, `LiveExample`, `Callout`, …), `defineConfig`, `createSearchEngine`, `generateSitemap` / `generateLlmsTxt` / `generateLlmsFullTxt` / `generateFeed`, `mcpHandler`, `currentOnly`, and the types |
252
+ | `svelte-docsmith/preprocess` | `docsmith()`, the mdsvex/Shiki pipeline (Node, config time) |
253
+ | `svelte-docsmith/vite` | `docsmith()`, the content / search / llms / changelog indexes and `?source` / `?props` transforms (Node, build) |
254
+ | `svelte-docsmith/content` | `docs` and `versions`, the generated sidebar content index and version manifest |
255
+ | `svelte-docsmith/search` | `docs`, the generated full-text search index (lazy-load it) |
256
+ | `svelte-docsmith/llms` | `docs`, the generated agent markdown index (for `llms.txt` / `llms-full.txt` routes) |
257
+ | `svelte-docsmith/changelog` | `releases`, the generated changelog index (parsed from `CHANGELOG.md`) |
258
+ | `svelte-docsmith/mermaid` | the Mermaid diagram component (lazy-loaded by the preprocessor when a mermaid fence is present) |
259
+ | `svelte-docsmith/client` | ambient types for `?source` and `?props` imports (reference it from `src/app.d.ts`) |
260
+ | `svelte-docsmith/theme.css` | the base style contract |
261
+ | `svelte-docsmith/themes/*.css` | the eleven theme presets |
229
262
 
230
263
  The package also ships a CLI binary: `npx svelte-docsmith archive-version <id>`
231
264
  freezes the current docs into a versioned archive.
@@ -27,6 +27,17 @@ export declare function splitFrontmatter(source: string): MarkdownSource;
27
27
  export declare function withFrontmatter(source: string, transform: (frontmatter: string) => string): string;
28
28
  /** Apply `transform` to a page's prose lines, leaving fenced code untouched. */
29
29
  export declare function outsideCodeFences(text: string, transform: (line: string) => string): string;
30
+ /**
31
+ * Apply `transform` to each run of consecutive prose lines, leaving fenced code
32
+ * and its markers untouched. For rewrites that span lines, such as a component
33
+ * tag with children, which {@link outsideCodeFences} would see a line at a time.
34
+ *
35
+ * With `setupBlocks`, each top-level `<script>`/`<style>` block stays whole
36
+ * inside one run, even when its code has a fence-like line.
37
+ */
38
+ export declare function outsideCodeFenceRuns(text: string, transform: (prose: string) => string, options?: {
39
+ setupBlocks?: boolean;
40
+ }): string;
30
41
  /** A page's prose lines, in order, with fenced code and its markers dropped. */
31
42
  export declare function proseLines(text: string): Generator<string>;
32
43
  /**
@@ -23,6 +23,8 @@
23
23
  const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
24
24
  /** An opening or closing code fence, indented anywhere on its line. */
25
25
  const FENCE = /^\s*(`{3,}|~{3,})/;
26
+ /** The open tag of a `<script>` or `<style>` block, starting a line as HTML may. */
27
+ const SETUP_BLOCK_OPEN = /^ {0,3}<(script|style)(?=[\s>]|$)/i;
26
28
  /** Split a page into its frontmatter and its body. */
27
29
  export function splitFrontmatter(source) {
28
30
  const match = FRONTMATTER.exec(source);
@@ -51,10 +53,20 @@ export function withFrontmatter(source, transform) {
51
53
  *
52
54
  * Returns a predicate that consumes the page's lines in order and answers
53
55
  * whether each one is prose. Fence markers themselves are not.
56
+ *
57
+ * With `setupBlocks`, a `<script>` or `<style>` block starting a prose line is
58
+ * prose through its closing tag, as CommonMark reads such an HTML block, so a
59
+ * fence-like line in its code (a ``` in a template literal) opens nothing.
54
60
  */
55
- function fenceScanner() {
61
+ function fenceScanner({ setupBlocks = false } = {}) {
56
62
  let open;
63
+ let setupClose;
57
64
  return (line) => {
65
+ if (setupClose) {
66
+ if (setupClose.test(line))
67
+ setupClose = undefined;
68
+ return true;
69
+ }
58
70
  const match = FENCE.exec(line);
59
71
  if (open) {
60
72
  const closes = match &&
@@ -69,6 +81,12 @@ function fenceScanner() {
69
81
  open = match[1];
70
82
  return false;
71
83
  }
84
+ const setup = setupBlocks ? SETUP_BLOCK_OPEN.exec(line) : null;
85
+ if (setup) {
86
+ const close = new RegExp(`</${setup[1]}>`, 'i');
87
+ if (!close.test(line.slice(setup[0].length)))
88
+ setupClose = close;
89
+ }
72
90
  return true;
73
91
  };
74
92
  }
@@ -80,6 +98,35 @@ export function outsideCodeFences(text, transform) {
80
98
  .map((line) => (isProse(line) ? transform(line) : line))
81
99
  .join('\n');
82
100
  }
101
+ /**
102
+ * Apply `transform` to each run of consecutive prose lines, leaving fenced code
103
+ * and its markers untouched. For rewrites that span lines, such as a component
104
+ * tag with children, which {@link outsideCodeFences} would see a line at a time.
105
+ *
106
+ * With `setupBlocks`, each top-level `<script>`/`<style>` block stays whole
107
+ * inside one run, even when its code has a fence-like line.
108
+ */
109
+ export function outsideCodeFenceRuns(text, transform, options = {}) {
110
+ const isProse = fenceScanner(options);
111
+ const out = [];
112
+ let run = [];
113
+ const flush = () => {
114
+ if (run.length)
115
+ out.push(transform(run.join('\n')));
116
+ run = [];
117
+ };
118
+ for (const line of text.split('\n')) {
119
+ if (isProse(line)) {
120
+ run.push(line);
121
+ }
122
+ else {
123
+ flush();
124
+ out.push(line);
125
+ }
126
+ }
127
+ flush();
128
+ return out.join('\n');
129
+ }
83
130
  /** A page's prose lines, in order, with fenced code and its markers dropped. */
84
131
  export function* proseLines(text) {
85
132
  const isProse = fenceScanner();
@@ -10,3 +10,8 @@ export declare function urlFor(routesDir: string, dir: string): string;
10
10
  * base of `/docs`.
11
11
  */
12
12
  export declare function docsBaseFrom(routesDir: string, contentDir: string): string;
13
+ /**
14
+ * A file's path relative to the app's working directory, with `/` separators:
15
+ * the path an author would type, for "Edit this page" links and error messages.
16
+ */
17
+ export declare function relativeToCwd(file: string): string;
@@ -21,3 +21,10 @@ export function urlFor(routesDir, dir) {
21
21
  export function docsBaseFrom(routesDir, contentDir) {
22
22
  return urlFor(routesDir, contentDir);
23
23
  }
24
+ /**
25
+ * A file's path relative to the app's working directory, with `/` separators:
26
+ * the path an author would type, for "Edit this page" links and error messages.
27
+ */
28
+ export function relativeToCwd(file) {
29
+ return path.relative(process.cwd(), file).split(path.sep).join('/');
30
+ }
@@ -0,0 +1,25 @@
1
+ import type { ExtractedProps } from '../../core/extracted-props.js';
2
+ export interface ExtractResult {
3
+ /** What `<PropsTable of={…}>` renders: descriptions as inline HTML. */
4
+ props: ExtractedProps;
5
+ /**
6
+ * The same props with descriptions and deprecation notes as the JSDoc's own
7
+ * markdown, for agent markdown.
8
+ */
9
+ markdown: ExtractedProps;
10
+ warnings: string[];
11
+ /** Files whose changes should re-extract this component. */
12
+ watchFiles: string[];
13
+ }
14
+ export interface PropsExtractorOptions {
15
+ /** Project root: where `tsconfig.json` is looked up. */
16
+ root: string;
17
+ /** tsconfig used to read prop types. Default: the root's `tsconfig.json`. */
18
+ tsconfig?: string;
19
+ }
20
+ export interface PropsExtractor {
21
+ extract(file: string): Promise<ExtractResult>;
22
+ }
23
+ export declare function createPropsExtractor(options: PropsExtractorOptions): PropsExtractor;
24
+ /** `file-tree-item.svelte` → `FileTreeItem`. */
25
+ export declare function componentName(file: string): string;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Props extraction for props imports (`Component.svelte?props`): merges what
3
+ * the source says (./parse.ts: destructured names, defaults, `$bindable()`)
4
+ * with what TypeScript says (./typescript.ts: types, optionality, JSDoc) into
5
+ * the `ExtractedProps` that `<PropsTable of={…}>` renders.
6
+ *
7
+ * Extraction never throws over one component. Anything it can't read becomes a
8
+ * warning naming the file, and the table shows what was found: untyped props
9
+ * by name only, a missing TypeScript install as names and defaults.
10
+ */
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { marked } from 'marked';
14
+ import { VERSION } from 'svelte/compiler';
15
+ import { parseComponent } from './parse.js';
16
+ import { createPropsTypeChecker, inNodeModules, loadPeers, mtimeOf, MISSING_PEERS } from './typescript.js';
17
+ export function createPropsExtractor(options) {
18
+ let checker;
19
+ const getChecker = () => (checker ??= loadPeers().then((peers) => peers &&
20
+ createPropsTypeChecker(peers, {
21
+ ...options,
22
+ svelteVersion: VERSION
23
+ })));
24
+ // Props imports load concurrently (a page with five tables asks for five at
25
+ // once). Collect the requests made in the same tick and read them against
26
+ // one TypeScript program instead of rebuilding it per component.
27
+ let batch = [];
28
+ const flush = async () => {
29
+ const items = batch;
30
+ batch = [];
31
+ let results;
32
+ try {
33
+ const tsChecker = await getChecker();
34
+ results = tsChecker
35
+ ? tsChecker.read(items.map((item) => item.file))
36
+ : items.map(() => 'missing-peers');
37
+ }
38
+ catch (error) {
39
+ // Setting TypeScript up failed (a broken tsconfig, say). Report it on
40
+ // every waiting import rather than leaving them pending, and try again
41
+ // from scratch next time.
42
+ checker = undefined;
43
+ const failure = error instanceof Error ? error : new Error(String(error));
44
+ results = items.map(() => failure);
45
+ }
46
+ items.forEach((item, i) => item.done(results[i]));
47
+ };
48
+ const readTyped = (file) => new Promise((done) => {
49
+ if (batch.push({ file, done }) === 1)
50
+ setTimeout(flush, 0);
51
+ });
52
+ // Results stay valid until one of the files they were read from changes.
53
+ // The client and server builds both import every props module, so the
54
+ // second build is served from here.
55
+ const cache = new Map();
56
+ const stamp = mtimeOf;
57
+ return {
58
+ async extract(file) {
59
+ const cached = cache.get(file);
60
+ if (cached && cached.stamps.every(([f, time]) => stamp(f) === time))
61
+ return cached.result;
62
+ const result = await extractFresh(file);
63
+ cache.set(file, { stamps: result.watchFiles.map((f) => [f, stamp(f)]), result });
64
+ return result;
65
+ }
66
+ };
67
+ async function extractFresh(file) {
68
+ const warnings = [];
69
+ const rel = path.relative(options.root, file) || file;
70
+ const warn = (message) => warnings.push(`${rel}: ${message}`);
71
+ const extracted = {
72
+ name: componentName(file),
73
+ typeParameters: [],
74
+ props: [],
75
+ inherited: []
76
+ };
77
+ if (inNodeModules(file)) {
78
+ warn('reading a published package file. Published components usually have their ' +
79
+ 'types stripped, so point the props import at the source instead.');
80
+ }
81
+ // Rows are built once, with JSDoc as markdown; the rendered table's copy
82
+ // differs only in turning that markdown into HTML.
83
+ const done = (watchFiles) => ({
84
+ props: {
85
+ ...extracted,
86
+ props: extracted.props.map((prop) => clean({
87
+ ...prop,
88
+ deprecated: prop.deprecated === undefined ? undefined : renderMarkdown(prop.deprecated),
89
+ description: prop.description ? renderMarkdown(prop.description) : undefined
90
+ }))
91
+ },
92
+ markdown: extracted,
93
+ warnings,
94
+ watchFiles
95
+ });
96
+ let parsed;
97
+ try {
98
+ parsed = parseComponent(fs.readFileSync(file, 'utf-8'), file);
99
+ }
100
+ catch (error) {
101
+ warn(`could not parse the component (${message(error)}).`);
102
+ return done([file]);
103
+ }
104
+ extracted.typeParameters = parsed.typeParameters;
105
+ if (!parsed.hasProps) {
106
+ warn('no `$props()` found, so the props table is empty.');
107
+ return done([file]);
108
+ }
109
+ let typed;
110
+ if (parsed.typed) {
111
+ const result = await readTyped(file);
112
+ if (result === 'missing-peers')
113
+ warn(MISSING_PEERS);
114
+ else if (result instanceof Error) {
115
+ warn(`could not read prop types (${result.message}). Showing names only.`);
116
+ }
117
+ else
118
+ typed = result;
119
+ }
120
+ else {
121
+ warn('`$props()` has no type annotation, so props show without types.');
122
+ }
123
+ const defaults = new Map(parsed.destructured.map((d) => [d.name, d]));
124
+ if (typed) {
125
+ for (const prop of typed.props) {
126
+ if (prop.internal)
127
+ continue;
128
+ const destructured = defaults.get(prop.name);
129
+ const fallback = prop.defaultTag ?? destructured?.default;
130
+ extracted.props.push(clean({
131
+ name: prop.name,
132
+ type: prop.type,
133
+ default: fallback,
134
+ required: !prop.optional && fallback === undefined,
135
+ bindable: destructured?.bindable ?? false,
136
+ deprecated: prop.deprecated,
137
+ description: prop.description
138
+ }));
139
+ }
140
+ extracted.inherited = typed.inherited;
141
+ }
142
+ else {
143
+ for (const d of parsed.destructured) {
144
+ extracted.props.push(clean({ name: d.name, default: d.default, required: false, bindable: d.bindable }));
145
+ }
146
+ }
147
+ return done([...new Set([file, ...(typed?.files ?? [])])]);
148
+ }
149
+ }
150
+ /** `file-tree-item.svelte` → `FileTreeItem`. */
151
+ export function componentName(file) {
152
+ return path
153
+ .basename(file, '.svelte')
154
+ .split(/[^A-Za-z0-9]+/)
155
+ .filter(Boolean)
156
+ .map((word) => word[0].toUpperCase() + word.slice(1))
157
+ .join('');
158
+ }
159
+ function renderMarkdown(text) {
160
+ return marked.parse(text, { async: false, gfm: true })
161
+ .trim()
162
+ .replace(/^<p>([\s\S]*)<\/p>$/, (match, inner) => inner.includes('<p>') ? match : inner);
163
+ }
164
+ function clean(prop) {
165
+ return Object.fromEntries(Object.entries(prop).filter(([, value]) => value !== undefined));
166
+ }
167
+ function message(error) {
168
+ return error instanceof Error ? error.message : String(error);
169
+ }
@@ -0,0 +1,17 @@
1
+ export interface DestructuredProp {
2
+ name: string;
3
+ /** Source text of the default value. */
4
+ default?: string;
5
+ bindable: boolean;
6
+ }
7
+ export interface ParsedComponent {
8
+ /** Whether the instance script calls `$props()` at all. */
9
+ hasProps: boolean;
10
+ /** Whether the `$props()` declaration carries a type annotation. */
11
+ typed: boolean;
12
+ /** Destructured props, in source order. Empty for `let props = $props()`. */
13
+ destructured: DestructuredProp[];
14
+ /** Type parameter names from `generics="…"`. */
15
+ typeParameters: string[];
16
+ }
17
+ export declare function parseComponent(code: string, filename: string): ParsedComponent;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The syntax half of props extraction: what can be read from a component's
3
+ * source without TypeScript. That is the `$props()` destructuring (names,
4
+ * default values, `$bindable()`) and the `generics`
5
+ * attribute. Types, optionality and descriptions come from ./typescript.ts.
6
+ */
7
+ import { parse } from 'svelte/compiler';
8
+ import { splitTopLevel } from '../../utils/type-text.js';
9
+ export function parseComponent(code, filename) {
10
+ const ast = parse(code, { modern: true, filename });
11
+ const typeParameters = genericsOf(ast.instance?.attributes ?? [], code);
12
+ const empty = {
13
+ hasProps: false,
14
+ typed: false,
15
+ destructured: [],
16
+ typeParameters
17
+ };
18
+ if (!ast.instance)
19
+ return empty;
20
+ const declarator = findPropsDeclarator(ast.instance.content);
21
+ if (!declarator)
22
+ return empty;
23
+ const id = declarator.id;
24
+ const typed = Boolean(id.typeAnnotation);
25
+ if (id.type !== 'ObjectPattern')
26
+ return { ...empty, hasProps: true, typed };
27
+ const destructured = [];
28
+ for (const property of id.properties) {
29
+ // `...rest` is read from the type instead (inherited attributes).
30
+ if (property.type === 'RestElement')
31
+ continue;
32
+ const name = keyName(property.key);
33
+ if (name === undefined)
34
+ continue;
35
+ destructured.push({ name, ...defaultOf(property.value, code) });
36
+ }
37
+ return { hasProps: true, typed, destructured, typeParameters };
38
+ }
39
+ function findPropsDeclarator(node) {
40
+ let found;
41
+ walk(node, (n) => {
42
+ if (found)
43
+ return false;
44
+ if (n.type === 'VariableDeclarator' && isCall(n.init, '$props')) {
45
+ found = n;
46
+ return false;
47
+ }
48
+ return true;
49
+ });
50
+ return found;
51
+ }
52
+ function walk(node, visit) {
53
+ if (!visit(node))
54
+ return;
55
+ for (const value of Object.values(node)) {
56
+ if (Array.isArray(value)) {
57
+ for (const child of value)
58
+ if (isNode(child))
59
+ walk(child, visit);
60
+ }
61
+ else if (isNode(value)) {
62
+ walk(value, visit);
63
+ }
64
+ }
65
+ }
66
+ function isNode(value) {
67
+ return typeof value === 'object' && value !== null && typeof value.type === 'string';
68
+ }
69
+ function isCall(node, callee) {
70
+ if (!node || node.type !== 'CallExpression')
71
+ return false;
72
+ const fn = node.callee;
73
+ return fn.type === 'Identifier' && fn.name === callee;
74
+ }
75
+ function keyName(key) {
76
+ if (key.type === 'Identifier')
77
+ return key.name;
78
+ if (key.type === 'Literal' && typeof key.value === 'string')
79
+ return key.value;
80
+ return undefined;
81
+ }
82
+ function defaultOf(value, code) {
83
+ if (value.type !== 'AssignmentPattern')
84
+ return { bindable: false };
85
+ const right = value.right;
86
+ if (isCall(right, '$bindable')) {
87
+ const [fallback] = right.arguments;
88
+ return { bindable: true, default: fallback ? source(fallback, code) : undefined };
89
+ }
90
+ return { bindable: false, default: source(right, code) };
91
+ }
92
+ function source(node, code) {
93
+ return code.slice(node.start, node.end);
94
+ }
95
+ /** `generics="T extends string, U = number"` → `['T', 'U']`. */
96
+ function genericsOf(attributes, code) {
97
+ for (const attr of attributes) {
98
+ if (attr.type !== 'Attribute' || attr.name !== 'generics')
99
+ continue;
100
+ const [text] = attr.value;
101
+ if (!text || text.type !== 'Text')
102
+ return [];
103
+ return splitTopLevel(String(text.data ?? source(text, code)), ',')
104
+ .map((param) => /^\s*([A-Za-z_$][\w$]*)/.exec(param)?.[1])
105
+ .filter((name) => Boolean(name));
106
+ }
107
+ return [];
108
+ }
@@ -0,0 +1,54 @@
1
+ import type TS from 'typescript';
2
+ type Ts = typeof TS;
3
+ type Svelte2tsx = typeof import('svelte2tsx').svelte2tsx;
4
+ export interface TypedProp {
5
+ name: string;
6
+ type: string;
7
+ optional: boolean;
8
+ /** JSDoc description, as markdown. */
9
+ description?: string;
10
+ /** `@deprecated` text (`''` when the tag is bare). */
11
+ deprecated?: string;
12
+ /** `@default` text, which wins over the destructured default. */
13
+ defaultTag?: string;
14
+ /** Tagged `@internal`: left out of the table. */
15
+ internal: boolean;
16
+ }
17
+ export interface TypedInheritedAttributes {
18
+ kind: 'element' | 'type';
19
+ label: string;
20
+ }
21
+ export interface TypedComponent {
22
+ props: TypedProp[];
23
+ inherited: TypedInheritedAttributes[];
24
+ /** Project files the component imports, directly or not, for HMR. */
25
+ files: string[];
26
+ }
27
+ export interface PropsTypeChecker {
28
+ /**
29
+ * Read several components against one program: building the program is the
30
+ * expensive part, so concurrent props imports share it. Each entry is
31
+ * `undefined` when that component has no typed `$props()`, or the error it
32
+ * threw.
33
+ */
34
+ read(files: string[]): (TypedComponent | undefined | Error)[];
35
+ }
36
+ export declare const MISSING_PEERS: string;
37
+ /** The optional peer dependencies, plus svelte2tsx's ambient shims. */
38
+ export interface Peers {
39
+ ts: Ts;
40
+ svelte2tsx: Svelte2tsx;
41
+ shims: string[];
42
+ }
43
+ /** Load the optional peers once per process; `undefined` when not installed. */
44
+ export declare function loadPeers(): Promise<Peers | undefined>;
45
+ /** A file's modification time, or `-1` when it can't be read. */
46
+ export declare function mtimeOf(file: string): number;
47
+ /** Whether a path lies inside a `node_modules` directory, on any platform. */
48
+ export declare function inNodeModules(file: string): boolean;
49
+ export declare function createPropsTypeChecker({ ts, svelte2tsx, shims }: Peers, options: {
50
+ root: string;
51
+ tsconfig?: string;
52
+ svelteVersion: string;
53
+ }): PropsTypeChecker;
54
+ export {};