svelte-docsmith 0.13.1 → 0.15.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 +30 -26
- package/dist/buildtime/archive.d.ts +4 -3
- package/dist/buildtime/archive.js +4 -3
- package/dist/buildtime/markdown-layout.svelte.d.ts +2 -2
- package/dist/buildtime/paths.d.ts +5 -0
- package/dist/buildtime/paths.js +7 -0
- package/dist/buildtime/props/extract.d.ts +19 -0
- package/dist/buildtime/props/extract.js +158 -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/collect.js +22 -42
- 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/frontmatter.js +5 -3
- package/dist/buildtime/vite/index.d.ts +16 -0
- package/dist/buildtime/vite/index.js +65 -2
- 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/search-trigger.svelte +1 -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 +5 -10
- 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 +75 -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 +2 -2
- package/dist/components/shadcn/badge/badge.svelte.d.ts +2 -20
- package/dist/components/shadcn/button/button.svelte.d.ts +2 -40
- package/dist/components/shadcn/input-group/input-group-addon.svelte.d.ts +2 -16
- package/dist/components/shadcn/input-group/input-group-button.svelte.d.ts +2 -16
- package/dist/components/shadcn/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/shadcn/tabs/tabs-list.svelte.d.ts +2 -12
- package/dist/components/shadcn/textarea/textarea.svelte +1 -2
- package/dist/core/content.d.ts +27 -0
- package/dist/core/extracted-props.d.ts +39 -0
- package/dist/core/extracted-props.js +1 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/toc/toc.svelte.js +2 -0
- package/dist/utils/type-text.d.ts +13 -0
- package/dist/utils/type-text.js +49 -0
- package/package.json +54 -39
package/README.md
CHANGED
|
@@ -42,50 +42,54 @@ To add DocSmith to an existing SvelteKit app, install the package instead:
|
|
|
42
42
|
npm install svelte-docsmith
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
Peer dependencies: Svelte 5, SvelteKit 2, and Tailwind v4, set up the
|
|
46
|
-
way in your app.
|
|
45
|
+
Peer dependencies: Svelte 5, SvelteKit 2 or 3, and Tailwind v4, set up the
|
|
46
|
+
standard way in your app.
|
|
47
47
|
|
|
48
48
|
## Setup
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
Two small pieces, once.
|
|
51
51
|
|
|
52
|
-
**1.
|
|
53
|
-
|
|
54
|
-
fallback for unknown languages), heading anchors, and the packaged page layout:
|
|
52
|
+
**1. Vite.** Everything goes in `vite.config.ts`. DocSmith has two halves, both
|
|
53
|
+
called `docsmith()`:
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
extensions: ['.svelte', '.md'],
|
|
64
|
-
preprocess: [vitePreprocess(), docsmith()],
|
|
65
|
-
kit: { adapter: adapter() }
|
|
66
|
-
};
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
**2. The Vite plugin.** In `vite.config.ts`, it scans your doc pages' frontmatter
|
|
70
|
-
into the `svelte-docsmith/content` module (so the sidebar is derived from
|
|
71
|
-
content, never hand-written), builds the search index, and powers `LiveExample`:
|
|
55
|
+
- The **Vite plugin** (`svelte-docsmith/vite`) scans your doc pages' frontmatter
|
|
56
|
+
into the `svelte-docsmith/content` module (so the sidebar is derived from
|
|
57
|
+
content, never hand-written), builds the search index, and powers
|
|
58
|
+
`LiveExample`. It goes before `sveltekit()`.
|
|
59
|
+
- The **markdown pipeline** (`svelte-docsmith/preprocess`) bundles mdsvex, Shiki
|
|
60
|
+
highlighting (dual light/dark themes, a generous language set, plain-text
|
|
61
|
+
fallback for unknown languages), heading anchors, and the packaged page layout.
|
|
72
62
|
|
|
73
63
|
```ts
|
|
74
64
|
// vite.config.ts
|
|
65
|
+
import adapter from '@sveltejs/adapter-auto';
|
|
75
66
|
import { sveltekit } from '@sveltejs/kit/vite';
|
|
67
|
+
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|
76
68
|
import tailwindcss from '@tailwindcss/vite';
|
|
77
69
|
import { docsmith } from 'svelte-docsmith/vite';
|
|
70
|
+
import { docsmith as markdown } from 'svelte-docsmith/preprocess';
|
|
78
71
|
import { defineConfig } from 'vite';
|
|
79
72
|
|
|
80
73
|
export default defineConfig({
|
|
81
|
-
plugins: [
|
|
74
|
+
plugins: [
|
|
75
|
+
docsmith(),
|
|
76
|
+
tailwindcss(),
|
|
77
|
+
sveltekit({
|
|
78
|
+
extensions: ['.svelte', '.md'],
|
|
79
|
+
preprocess: [vitePreprocess(), markdown()],
|
|
80
|
+
adapter: adapter()
|
|
81
|
+
})
|
|
82
|
+
]
|
|
82
83
|
});
|
|
83
84
|
```
|
|
84
85
|
|
|
86
|
+
On SvelteKit 2, `extensions` and `preprocess` go in `svelte.config.js` instead,
|
|
87
|
+
and `sveltekit()` takes no options.
|
|
88
|
+
|
|
85
89
|
By default it scans `src/routes/docs`; pass `docsmith({ content: 'src/routes/guide' })`
|
|
86
90
|
to point elsewhere.
|
|
87
91
|
|
|
88
|
-
**
|
|
92
|
+
**2. The stylesheet.** In your root `app.css`, once Tailwind v4 is set up the
|
|
89
93
|
standard way (`tailwindcss` + the `@tailwindcss/vite` plugin, stylesheet imported
|
|
90
94
|
in the root layout), the whole style contract is one import:
|
|
91
95
|
|
|
@@ -157,8 +161,8 @@ plugin you already added:
|
|
|
157
161
|
```svelte
|
|
158
162
|
<script>
|
|
159
163
|
import { LiveExample } from 'svelte-docsmith';
|
|
160
|
-
import Counter from '
|
|
161
|
-
import counterSource from '
|
|
164
|
+
import Counter from '#lib/examples/counter.svelte';
|
|
165
|
+
import counterSource from '#lib/examples/counter.svelte?source';
|
|
162
166
|
</script>
|
|
163
167
|
|
|
164
168
|
<LiveExample source={counterSource}>
|
|
@@ -37,9 +37,10 @@ export declare function freezeLastUpdated(text: string, date: string | undefined
|
|
|
37
37
|
* Archiving copies the docs root and nothing else, so anything reached from
|
|
38
38
|
* outside it keeps resolving to current code: a live example on an archived
|
|
39
39
|
* page demonstrates the current library rather than the version the page
|
|
40
|
-
* documents, with a green build and nothing said. Hence `$lib
|
|
41
|
-
* specifiers and relative paths that climb out all count,
|
|
42
|
-
* import landing inside the docs root is frozen along with
|
|
40
|
+
* documents, with a green build and nothing said. Hence `$lib` (Kit 2), `#lib`
|
|
41
|
+
* (Kit 3), bare npm specifiers and relative paths that climb out all count,
|
|
42
|
+
* while a relative import landing inside the docs root is frozen along with
|
|
43
|
+
* the page.
|
|
43
44
|
*
|
|
44
45
|
* `svelte-docsmith` itself is never reported. An archived page importing the
|
|
45
46
|
* authoring components is the expected shape, and those are covered by the
|
|
@@ -85,9 +85,10 @@ function escapesRoot(specifier, depth) {
|
|
|
85
85
|
* Archiving copies the docs root and nothing else, so anything reached from
|
|
86
86
|
* outside it keeps resolving to current code: a live example on an archived
|
|
87
87
|
* page demonstrates the current library rather than the version the page
|
|
88
|
-
* documents, with a green build and nothing said. Hence `$lib
|
|
89
|
-
* specifiers and relative paths that climb out all count,
|
|
90
|
-
* import landing inside the docs root is frozen along with
|
|
88
|
+
* documents, with a green build and nothing said. Hence `$lib` (Kit 2), `#lib`
|
|
89
|
+
* (Kit 3), bare npm specifiers and relative paths that climb out all count,
|
|
90
|
+
* while a relative import landing inside the docs root is frozen along with
|
|
91
|
+
* the page.
|
|
91
92
|
*
|
|
92
93
|
* `svelte-docsmith` itself is never reported. An archived page importing the
|
|
93
94
|
* authoring components is the expected shape, and those are covered by the
|
|
@@ -7,8 +7,8 @@ export declare const pre: import("svelte").Component<{
|
|
|
7
7
|
export declare const code: import("svelte").Component<{
|
|
8
8
|
children: Snippet;
|
|
9
9
|
}, {}, "">;
|
|
10
|
-
export declare const h2: import("svelte").Component<import("../components/markdown/types.
|
|
11
|
-
export declare const h3: import("svelte").Component<import("../components/markdown/types.
|
|
10
|
+
export declare const h2: import("svelte").Component<import("../components/markdown/types.ts").HeadingProps, {}, "">;
|
|
11
|
+
export declare const h3: import("svelte").Component<import("../components/markdown/types.ts").HeadingProps, {}, "">;
|
|
12
12
|
export declare const table: import("svelte").Component<{
|
|
13
13
|
children: Snippet;
|
|
14
14
|
}, {}, "">;
|
|
@@ -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,19 @@
|
|
|
1
|
+
import type { ExtractedProps } from '../../core/extracted-props.js';
|
|
2
|
+
export interface ExtractResult {
|
|
3
|
+
props: ExtractedProps;
|
|
4
|
+
warnings: string[];
|
|
5
|
+
/** Files whose changes should re-extract this component. */
|
|
6
|
+
watchFiles: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface PropsExtractorOptions {
|
|
9
|
+
/** Project root: where `tsconfig.json` is looked up. */
|
|
10
|
+
root: string;
|
|
11
|
+
/** tsconfig used to read prop types. Default: the root's `tsconfig.json`. */
|
|
12
|
+
tsconfig?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface PropsExtractor {
|
|
15
|
+
extract(file: string): Promise<ExtractResult>;
|
|
16
|
+
}
|
|
17
|
+
export declare function createPropsExtractor(options: PropsExtractorOptions): PropsExtractor;
|
|
18
|
+
/** `file-tree-item.svelte` → `FileTreeItem`. */
|
|
19
|
+
export declare function componentName(file: string): string;
|
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = parseComponent(fs.readFileSync(file, 'utf-8'), file);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
warn(`could not parse the component (${message(error)}).`);
|
|
87
|
+
return { props: extracted, warnings, watchFiles: [file] };
|
|
88
|
+
}
|
|
89
|
+
extracted.typeParameters = parsed.typeParameters;
|
|
90
|
+
if (!parsed.hasProps) {
|
|
91
|
+
warn('no `$props()` found, so the props table is empty.');
|
|
92
|
+
return { props: extracted, warnings, watchFiles: [file] };
|
|
93
|
+
}
|
|
94
|
+
let typed;
|
|
95
|
+
if (parsed.typed) {
|
|
96
|
+
const result = await readTyped(file);
|
|
97
|
+
if (result === 'missing-peers')
|
|
98
|
+
warn(MISSING_PEERS);
|
|
99
|
+
else if (result instanceof Error) {
|
|
100
|
+
warn(`could not read prop types (${result.message}). Showing names only.`);
|
|
101
|
+
}
|
|
102
|
+
else
|
|
103
|
+
typed = result;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
warn('`$props()` has no type annotation, so props show without types.');
|
|
107
|
+
}
|
|
108
|
+
const defaults = new Map(parsed.destructured.map((d) => [d.name, d]));
|
|
109
|
+
if (typed) {
|
|
110
|
+
for (const prop of typed.props) {
|
|
111
|
+
if (prop.internal)
|
|
112
|
+
continue;
|
|
113
|
+
const destructured = defaults.get(prop.name);
|
|
114
|
+
const fallback = prop.defaultTag ?? destructured?.default;
|
|
115
|
+
extracted.props.push(clean({
|
|
116
|
+
name: prop.name,
|
|
117
|
+
type: prop.type,
|
|
118
|
+
default: fallback,
|
|
119
|
+
required: !prop.optional && fallback === undefined,
|
|
120
|
+
bindable: destructured?.bindable ?? false,
|
|
121
|
+
deprecated: prop.deprecated === undefined ? undefined : renderMarkdown(prop.deprecated),
|
|
122
|
+
description: prop.description ? renderMarkdown(prop.description) : undefined
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
extracted.inherited = typed.inherited;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
for (const d of parsed.destructured) {
|
|
129
|
+
extracted.props.push(clean({ name: d.name, default: d.default, required: false, bindable: d.bindable }));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
props: extracted,
|
|
134
|
+
warnings,
|
|
135
|
+
watchFiles: [...new Set([file, ...(typed?.files ?? [])])]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** `file-tree-item.svelte` → `FileTreeItem`. */
|
|
140
|
+
export function componentName(file) {
|
|
141
|
+
return path
|
|
142
|
+
.basename(file, '.svelte')
|
|
143
|
+
.split(/[^A-Za-z0-9]+/)
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.map((word) => word[0].toUpperCase() + word.slice(1))
|
|
146
|
+
.join('');
|
|
147
|
+
}
|
|
148
|
+
function renderMarkdown(text) {
|
|
149
|
+
return marked.parse(text, { async: false, gfm: true })
|
|
150
|
+
.trim()
|
|
151
|
+
.replace(/^<p>([\s\S]*)<\/p>$/, (match, inner) => inner.includes('<p>') ? match : inner);
|
|
152
|
+
}
|
|
153
|
+
function clean(prop) {
|
|
154
|
+
return Object.fromEntries(Object.entries(prop).filter(([, value]) => value !== undefined));
|
|
155
|
+
}
|
|
156
|
+
function message(error) {
|
|
157
|
+
return error instanceof Error ? error.message : String(error);
|
|
158
|
+
}
|
|
@@ -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 {};
|