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.
- package/README.md +47 -14
- package/dist/buildtime/markdown-source.d.ts +11 -0
- package/dist/buildtime/markdown-source.js +48 -1
- package/dist/buildtime/paths.d.ts +5 -0
- package/dist/buildtime/paths.js +7 -0
- package/dist/buildtime/props/extract.d.ts +25 -0
- package/dist/buildtime/props/extract.js +169 -0
- package/dist/buildtime/props/parse.d.ts +17 -0
- package/dist/buildtime/props/parse.js +108 -0
- package/dist/buildtime/props/typescript.d.ts +54 -0
- package/dist/buildtime/props/typescript.js +345 -0
- package/dist/buildtime/vite/agent-markdown.d.ts +43 -0
- package/dist/buildtime/vite/agent-markdown.js +204 -0
- package/dist/buildtime/vite/collect.d.ts +4 -1
- package/dist/buildtime/vite/collect.js +27 -44
- package/dist/buildtime/vite/extract.js +8 -3
- package/dist/buildtime/vite/fields.d.ts +49 -0
- package/dist/buildtime/vite/fields.js +72 -0
- package/dist/buildtime/vite/frontmatter-schema.d.ts +38 -0
- package/dist/buildtime/vite/frontmatter-schema.js +96 -0
- package/dist/buildtime/vite/index.d.ts +16 -0
- package/dist/buildtime/vite/index.js +119 -7
- package/dist/buildtime/vite/pages.d.ts +5 -0
- package/dist/buildtime/vite/pages.js +10 -5
- package/dist/client.d.ts +24 -0
- package/dist/components/chrome/copy-button.svelte +4 -1
- package/dist/components/chrome/copy-button.svelte.d.ts +2 -0
- package/dist/components/docs/accordion-item.svelte +2 -1
- package/dist/components/docs/accordion-item.svelte.d.ts +2 -1
- package/dist/components/docs/accordion.svelte +2 -1
- package/dist/components/docs/accordion.svelte.d.ts +2 -1
- package/dist/components/docs/badge.svelte +4 -3
- package/dist/components/docs/badge.svelte.d.ts +4 -3
- package/dist/components/docs/callout.svelte +6 -2
- package/dist/components/docs/callout.svelte.d.ts +6 -2
- package/dist/components/docs/card-grid.svelte +6 -1
- package/dist/components/docs/card-grid.svelte.d.ts +1 -0
- package/dist/components/docs/card.svelte +4 -2
- package/dist/components/docs/card.svelte.d.ts +4 -2
- package/dist/components/docs/file-tree-item.svelte +5 -2
- package/dist/components/docs/file-tree-item.svelte.d.ts +5 -2
- package/dist/components/docs/file-tree.svelte +6 -1
- package/dist/components/docs/file-tree.svelte.d.ts +1 -0
- package/dist/components/docs/kbd.svelte +6 -1
- package/dist/components/docs/kbd.svelte.d.ts +1 -0
- package/dist/components/docs/live-example.svelte +5 -2
- package/dist/components/docs/live-example.svelte.d.ts +5 -2
- package/dist/components/docs/prop.svelte +43 -12
- package/dist/components/docs/prop.svelte.d.ts +4 -0
- package/dist/components/docs/props-table.svelte +73 -6
- package/dist/components/docs/props-table.svelte.d.ts +9 -2
- package/dist/components/docs/step.svelte +1 -0
- package/dist/components/docs/step.svelte.d.ts +1 -0
- package/dist/components/docs/steps.svelte +6 -1
- package/dist/components/docs/steps.svelte.d.ts +1 -0
- package/dist/components/docs/tab-item.svelte +6 -2
- package/dist/components/docs/tab-item.svelte.d.ts +6 -2
- package/dist/components/docs/tabs.svelte +8 -4
- package/dist/components/docs/tabs.svelte.d.ts +8 -4
- package/dist/components/landing/action.svelte +5 -2
- package/dist/components/landing/action.svelte.d.ts +5 -2
- package/dist/components/landing/cta.svelte +4 -3
- package/dist/components/landing/cta.svelte.d.ts +4 -3
- package/dist/components/landing/feature-grid.svelte +7 -4
- package/dist/components/landing/feature-grid.svelte.d.ts +7 -4
- package/dist/components/landing/feature.svelte +1 -0
- package/dist/components/landing/feature.svelte.d.ts +1 -0
- package/dist/components/landing/hero.svelte +5 -5
- package/dist/components/landing/hero.svelte.d.ts +5 -5
- package/dist/components/layouts/copy-page-menu.svelte +110 -17
- package/dist/components/layouts/copy-page-menu.svelte.d.ts +4 -0
- package/dist/components/layouts/docs-shell.svelte +7 -1
- package/dist/components/layouts/docs-shell.svelte.d.ts +1 -0
- package/dist/core/config.d.ts +9 -0
- package/dist/core/config.js +2 -1
- package/dist/core/content.d.ts +27 -0
- package/dist/core/extracted-props.d.ts +44 -0
- package/dist/core/extracted-props.js +8 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/version.d.ts +4 -2
- package/dist/core/version.js +1 -1
- package/dist/generate/llms.d.ts +15 -0
- package/dist/generate/llms.js +6 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -0
- package/dist/mcp/connect.d.ts +17 -0
- package/dist/mcp/connect.js +40 -0
- package/dist/mcp/handler.d.ts +57 -0
- package/dist/mcp/handler.js +391 -0
- package/dist/mcp/tools.d.ts +81 -0
- package/dist/mcp/tools.js +196 -0
- package/dist/utils/type-text.d.ts +13 -0
- package/dist/utils/type-text.js +49 -0
- 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.
|
|
28
|
-
>
|
|
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`
|
|
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
|
|
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/
|
|
228
|
-
| `svelte-docsmith/
|
|
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;
|
package/dist/buildtime/paths.js
CHANGED
|
@@ -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 {};
|