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
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The type half of props extraction: the TypeScript checker reads a
|
|
3
|
+
* component's props type the way svelte-check does, so a prop typed with an
|
|
4
|
+
* imported alias shows its resolved union rather than the alias name. See
|
|
5
|
+
* docs/adr/0006-own-extractor-on-typescript-checker.md.
|
|
6
|
+
*
|
|
7
|
+
* Each `.svelte` file is served to TypeScript as a virtual `X.svelte.ts`
|
|
8
|
+
* holding its svelte2tsx output. One language service lives per Vite root and
|
|
9
|
+
* is reused across props imports, so unchanged files aren't re-parsed.
|
|
10
|
+
*
|
|
11
|
+
* `typescript` and `svelte2tsx` are optional peer dependencies, loaded only when
|
|
12
|
+
* a page imports `?props`.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
export const MISSING_PEERS = 'Generated props tables need TypeScript. Install the optional peer ' +
|
|
18
|
+
'dependencies with: npm i -D typescript svelte2tsx';
|
|
19
|
+
let loaded;
|
|
20
|
+
/** Load the optional peers once per process; `undefined` when not installed. */
|
|
21
|
+
export function loadPeers() {
|
|
22
|
+
loaded ??= (async () => {
|
|
23
|
+
try {
|
|
24
|
+
const [tsModule, s2t] = await Promise.all([import('typescript'), import('svelte2tsx')]);
|
|
25
|
+
const ts = (tsModule.default ?? tsModule);
|
|
26
|
+
const dir = path.dirname(createRequire(import.meta.url).resolve('svelte2tsx'));
|
|
27
|
+
const shims = ['svelte-shims-v4.d.ts', 'svelte-jsx-v4.d.ts'].map((f) => path.join(dir, f));
|
|
28
|
+
return { ts, svelte2tsx: s2t.svelte2tsx, shims };
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
34
|
+
return loaded;
|
|
35
|
+
}
|
|
36
|
+
/** A file's modification time, or `-1` when it can't be read. */
|
|
37
|
+
export function mtimeOf(file) {
|
|
38
|
+
try {
|
|
39
|
+
return fs.statSync(file).mtimeMs;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return -1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Whether a path lies inside a `node_modules` directory, on any platform. */
|
|
46
|
+
export function inNodeModules(file) {
|
|
47
|
+
return /[\\/]node_modules[\\/]/.test(file);
|
|
48
|
+
}
|
|
49
|
+
const formatFlags = (ts) => ts.TypeFormatFlags.NoTruncation |
|
|
50
|
+
ts.TypeFormatFlags.UseSingleQuotesForStringLiteralType |
|
|
51
|
+
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope;
|
|
52
|
+
export function createPropsTypeChecker({ ts, svelte2tsx, shims }, options) {
|
|
53
|
+
const compilerOptions = readCompilerOptions(ts, options.root, options.tsconfig);
|
|
54
|
+
const roots = new Set();
|
|
55
|
+
const generated = new Map();
|
|
56
|
+
const mtime = mtimeOf;
|
|
57
|
+
// `X.svelte.ts` stands for `X.svelte` unless a real `X.svelte.ts` (a runes
|
|
58
|
+
// module) exists.
|
|
59
|
+
const isVirtual = (file) => file.endsWith('.svelte.ts') && !ts.sys.fileExists(file) && ts.sys.fileExists(file.slice(0, -3));
|
|
60
|
+
/** The file on disk a program file stands for. */
|
|
61
|
+
const onDisk = (file) => (isVirtual(file) ? file.slice(0, -3) : file);
|
|
62
|
+
const readVirtual = (file) => {
|
|
63
|
+
const svelteFile = file.slice(0, -3);
|
|
64
|
+
const time = mtime(svelteFile);
|
|
65
|
+
const cached = generated.get(svelteFile);
|
|
66
|
+
if (cached && cached.mtime === time)
|
|
67
|
+
return cached.code;
|
|
68
|
+
const { code } = svelte2tsx(fs.readFileSync(svelteFile, 'utf-8'), {
|
|
69
|
+
filename: svelteFile,
|
|
70
|
+
isTsFile: true,
|
|
71
|
+
mode: 'ts',
|
|
72
|
+
version: options.svelteVersion
|
|
73
|
+
});
|
|
74
|
+
generated.set(svelteFile, { mtime: time, code });
|
|
75
|
+
return code;
|
|
76
|
+
};
|
|
77
|
+
const fileExists = (file) => isVirtual(file) || ts.sys.fileExists(file);
|
|
78
|
+
const readFile = (file) => (isVirtual(file) ? readVirtual(file) : ts.sys.readFile(file));
|
|
79
|
+
const resolutionHost = {
|
|
80
|
+
fileExists,
|
|
81
|
+
readFile,
|
|
82
|
+
directoryExists: ts.sys.directoryExists,
|
|
83
|
+
realpath: ts.sys.realpath,
|
|
84
|
+
getCurrentDirectory: () => options.root,
|
|
85
|
+
getDirectories: ts.sys.getDirectories
|
|
86
|
+
};
|
|
87
|
+
const host = {
|
|
88
|
+
getCompilationSettings: () => compilerOptions,
|
|
89
|
+
getScriptFileNames: () => [...shims, ...roots],
|
|
90
|
+
getScriptVersion: (file) => String(mtime(onDisk(file))),
|
|
91
|
+
getScriptSnapshot: (file) => {
|
|
92
|
+
const text = readFile(file);
|
|
93
|
+
return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text);
|
|
94
|
+
},
|
|
95
|
+
getCurrentDirectory: () => options.root,
|
|
96
|
+
getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
|
|
97
|
+
fileExists,
|
|
98
|
+
readFile,
|
|
99
|
+
directoryExists: ts.sys.directoryExists,
|
|
100
|
+
getDirectories: ts.sys.getDirectories,
|
|
101
|
+
realpath: ts.sys.realpath,
|
|
102
|
+
resolveModuleNameLiterals(literals, containingFile, redirected, opts, containingSourceFile) {
|
|
103
|
+
return literals.map((literal) => resolveModule(literal.text, containingFile, opts, redirected, {
|
|
104
|
+
mode: ts.getModeForUsageLocation(containingSourceFile, literal, opts)
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
// A `.svelte` specifier resolves like its `.svelte.js` twin, which every
|
|
109
|
+
// resolution mode maps onto the virtual `.svelte.ts`. That keeps relative
|
|
110
|
+
// imports, package `imports` (`#lib/…`) and tsconfig `paths` working.
|
|
111
|
+
function resolveModule(name, containingFile, opts = compilerOptions, redirected, { mode } = {}) {
|
|
112
|
+
const resolve = (spec) => ts.resolveModuleName(spec, containingFile, opts, resolutionHost, undefined, redirected, mode);
|
|
113
|
+
const result = resolve(name);
|
|
114
|
+
if (result.resolvedModule || !name.endsWith('.svelte'))
|
|
115
|
+
return result;
|
|
116
|
+
return resolve(name + '.js');
|
|
117
|
+
}
|
|
118
|
+
const service = ts.createLanguageService(host, ts.createDocumentRegistry());
|
|
119
|
+
// The project files a component pulls in (its type imports included), so
|
|
120
|
+
// editing a shared `types.ts` re-extracts the props that use it.
|
|
121
|
+
const dependencies = (program, entry) => {
|
|
122
|
+
const seen = new Set([entry.fileName]);
|
|
123
|
+
const queue = [entry.fileName];
|
|
124
|
+
for (let file = queue.shift(); file; file = queue.shift()) {
|
|
125
|
+
const text = program.getSourceFile(file)?.text ?? '';
|
|
126
|
+
for (const { fileName } of ts.preProcessFile(text, true, true).importedFiles) {
|
|
127
|
+
const target = resolveModule(fileName, file).resolvedModule?.resolvedFileName;
|
|
128
|
+
if (!target || inNodeModules(target) || seen.has(target))
|
|
129
|
+
continue;
|
|
130
|
+
seen.add(target);
|
|
131
|
+
queue.push(target);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return [...seen].map(onDisk);
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
read(files) {
|
|
138
|
+
for (const file of files)
|
|
139
|
+
roots.add(file + '.ts');
|
|
140
|
+
const program = service.getProgram();
|
|
141
|
+
return files.map((file) => {
|
|
142
|
+
try {
|
|
143
|
+
const source = program?.getSourceFile(file + '.ts');
|
|
144
|
+
if (!program || !source)
|
|
145
|
+
return undefined;
|
|
146
|
+
const typed = readProps(ts, program, source);
|
|
147
|
+
return typed && { ...typed, files: dependencies(program, source) };
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function readCompilerOptions(ts, root, tsconfig) {
|
|
157
|
+
const configFile = tsconfig
|
|
158
|
+
? path.resolve(root, tsconfig)
|
|
159
|
+
: ts.findConfigFile(root, ts.sys.fileExists, 'tsconfig.json');
|
|
160
|
+
let base = {
|
|
161
|
+
target: ts.ScriptTarget.ESNext,
|
|
162
|
+
module: ts.ModuleKind.ESNext,
|
|
163
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
164
|
+
strict: true
|
|
165
|
+
};
|
|
166
|
+
if (configFile && ts.sys.fileExists(configFile)) {
|
|
167
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(configFile, {}, {
|
|
168
|
+
...ts.sys,
|
|
169
|
+
onUnRecoverableConfigFileDiagnostic: () => { }
|
|
170
|
+
});
|
|
171
|
+
if (parsed)
|
|
172
|
+
base = parsed.options;
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
...base,
|
|
176
|
+
allowJs: true,
|
|
177
|
+
skipLibCheck: true,
|
|
178
|
+
noEmit: true,
|
|
179
|
+
declaration: false,
|
|
180
|
+
composite: false,
|
|
181
|
+
incremental: false
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function readProps(ts, program, source) {
|
|
185
|
+
const typeNode = findPropsTypeNode(ts, source);
|
|
186
|
+
if (!typeNode)
|
|
187
|
+
return undefined;
|
|
188
|
+
const checker = program.getTypeChecker();
|
|
189
|
+
const flags = formatFlags(ts);
|
|
190
|
+
const propsType = checker.getTypeFromTypeNode(typeNode);
|
|
191
|
+
const isExternal = (sf) => program.isSourceFileFromExternalLibrary(sf) ||
|
|
192
|
+
program.isSourceFileDefaultLibrary(sf) ||
|
|
193
|
+
inNodeModules(sf.fileName);
|
|
194
|
+
const declaredOutside = (symbol) => {
|
|
195
|
+
const declarations = symbol.getDeclarations() ?? [];
|
|
196
|
+
return declarations.length > 0 && declarations.every((d) => isExternal(d.getSourceFile()));
|
|
197
|
+
};
|
|
198
|
+
// A prop typed with an alias from a library (`ClassValue`) keeps its name:
|
|
199
|
+
// expanding it would print the library's internals. Project aliases fall
|
|
200
|
+
// through to `printPropType`, which expands them.
|
|
201
|
+
const libraryAlias = (symbol) => {
|
|
202
|
+
const declaration = symbol.valueDeclaration;
|
|
203
|
+
if (!declaration || !ts.isPropertySignature(declaration) || !declaration.type)
|
|
204
|
+
return;
|
|
205
|
+
const declared = checker.getTypeFromTypeNode(declaration.type);
|
|
206
|
+
const alias = declared.aliasSymbol?.getDeclarations() ?? [];
|
|
207
|
+
if (alias.length === 0 || !alias.every((d) => isExternal(d.getSourceFile())))
|
|
208
|
+
return;
|
|
209
|
+
return checker.typeToString(declared, typeNode, flags);
|
|
210
|
+
};
|
|
211
|
+
const props = [];
|
|
212
|
+
let anyInherited = false;
|
|
213
|
+
for (const symbol of checker.getPropertiesOfType(propsType)) {
|
|
214
|
+
if (declaredOutside(symbol)) {
|
|
215
|
+
anyInherited = true;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
const optional = (symbol.flags & ts.SymbolFlags.Optional) !== 0;
|
|
219
|
+
const tags = symbol.getJsDocTags(checker);
|
|
220
|
+
const tag = (name) => {
|
|
221
|
+
const found = tags.find((t) => t.name === name);
|
|
222
|
+
return found ? ts.displayPartsToString(found.text ?? []).trim() : undefined;
|
|
223
|
+
};
|
|
224
|
+
const description = ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim();
|
|
225
|
+
props.push({
|
|
226
|
+
name: symbol.name,
|
|
227
|
+
type: libraryAlias(symbol) ??
|
|
228
|
+
printPropType(ts, checker, checker.getTypeOfSymbol(symbol), optional, typeNode, flags),
|
|
229
|
+
optional,
|
|
230
|
+
description: description || undefined,
|
|
231
|
+
deprecated: tag('deprecated'),
|
|
232
|
+
defaultTag: tag('default'),
|
|
233
|
+
internal: tag('internal') !== undefined
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const inherited = anyInherited
|
|
237
|
+
? inheritedGroups(ts, program, checker, propsType, declaredOutside, typeNode, flags)
|
|
238
|
+
: [];
|
|
239
|
+
return { props, inherited, files: [] };
|
|
240
|
+
}
|
|
241
|
+
/** The type annotation on the `$props()` declaration in the svelte2tsx output. */
|
|
242
|
+
function findPropsTypeNode(ts, source) {
|
|
243
|
+
let found;
|
|
244
|
+
const visit = (node) => {
|
|
245
|
+
if (found)
|
|
246
|
+
return;
|
|
247
|
+
if (ts.isVariableDeclaration(node) &&
|
|
248
|
+
node.initializer &&
|
|
249
|
+
ts.isCallExpression(node.initializer) &&
|
|
250
|
+
ts.isIdentifier(node.initializer.expression) &&
|
|
251
|
+
node.initializer.expression.text === '$props') {
|
|
252
|
+
found = node.type;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
ts.forEachChild(node, visit);
|
|
256
|
+
};
|
|
257
|
+
visit(source);
|
|
258
|
+
return found;
|
|
259
|
+
}
|
|
260
|
+
/** `Snippet<[]>` is how the checker spells a bare `Snippet`. */
|
|
261
|
+
const bareSnippet = (type) => type.replace(/\bSnippet<\[\]>/g, 'Snippet');
|
|
262
|
+
/**
|
|
263
|
+
* A union prints member by member: an alias of literals (`BadgeVariant`)
|
|
264
|
+
* expands to its values, and the `undefined` an optional prop picks up is
|
|
265
|
+
* dropped. Any other type prints as written (`Snippet`, `Item[]`, `T`).
|
|
266
|
+
*/
|
|
267
|
+
function printPropType(ts, checker, type, optional, enclosing, flags) {
|
|
268
|
+
if (!type.isUnion())
|
|
269
|
+
return bareSnippet(checker.typeToString(type, enclosing, flags));
|
|
270
|
+
let members = type.types.filter((t) => !(optional && t.flags & ts.TypeFlags.Undefined));
|
|
271
|
+
const booleans = members.filter((t) => t.flags & ts.TypeFlags.BooleanLiteral);
|
|
272
|
+
const parts = [];
|
|
273
|
+
let wroteBoolean = false;
|
|
274
|
+
if (booleans.length === 2) {
|
|
275
|
+
members = members.filter((t) => !booleans.includes(t));
|
|
276
|
+
wroteBoolean = true;
|
|
277
|
+
}
|
|
278
|
+
for (const member of members)
|
|
279
|
+
parts.push(bareSnippet(checker.typeToString(member, enclosing, flags)));
|
|
280
|
+
if (wroteBoolean)
|
|
281
|
+
parts.push('boolean');
|
|
282
|
+
if (parts.length === 1)
|
|
283
|
+
return parts[0];
|
|
284
|
+
return parts.map((p) => (p.includes('=>') ? `(${p})` : p)).join(' | ');
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Name what the external props came from: each part of the props intersection
|
|
288
|
+
* (or base of the props interface) whose props are all declared outside the
|
|
289
|
+
* project. An element's attributes (`HTMLAnchorAttributes`) are named by tag.
|
|
290
|
+
*/
|
|
291
|
+
function inheritedGroups(ts, program, checker, propsType, declaredOutside, enclosing, flags) {
|
|
292
|
+
const parts = [];
|
|
293
|
+
const collect = (type) => {
|
|
294
|
+
if (type.isIntersection()) {
|
|
295
|
+
type.types.forEach(collect);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const props = checker.getPropertiesOfType(type);
|
|
299
|
+
if (props.length > 0 && props.every(declaredOutside)) {
|
|
300
|
+
parts.push(type);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const target = type.target ?? type;
|
|
304
|
+
if (target.isClassOrInterface()) {
|
|
305
|
+
for (const base of checker.getBaseTypes(target))
|
|
306
|
+
collect(base);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
collect(propsType);
|
|
310
|
+
const tags = elementTags(program, checker);
|
|
311
|
+
const groups = parts.map((type) => {
|
|
312
|
+
const tag = tags.get(type);
|
|
313
|
+
return tag
|
|
314
|
+
? { kind: 'element', label: tag }
|
|
315
|
+
: { kind: 'type', label: checker.typeToString(type, enclosing, flags) };
|
|
316
|
+
});
|
|
317
|
+
return groups.length > 0 ? groups : [{ kind: 'type', label: 'more inherited attributes' }];
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Attribute types that belong to exactly one tag in Svelte's
|
|
321
|
+
* `SvelteHTMLElements` map (`HTMLAnchorAttributes` → `a`). Shared types like
|
|
322
|
+
* `HTMLAttributes<HTMLElement>` map to many tags, so they keep their name.
|
|
323
|
+
*/
|
|
324
|
+
function elementTags(program, checker) {
|
|
325
|
+
const tags = new Map();
|
|
326
|
+
const elements = program
|
|
327
|
+
.getSourceFiles()
|
|
328
|
+
.find((sf) => /[\\/]svelte[\\/]elements\.d\.ts$/.test(sf.fileName));
|
|
329
|
+
const moduleSymbol = elements && checker.getSymbolAtLocation(elements);
|
|
330
|
+
const map = moduleSymbol &&
|
|
331
|
+
checker.getExportsOfModule(moduleSymbol).find((s) => s.name === 'SvelteHTMLElements');
|
|
332
|
+
if (!map)
|
|
333
|
+
return tags;
|
|
334
|
+
const shared = new Set();
|
|
335
|
+
for (const entry of checker.getPropertiesOfType(checker.getDeclaredTypeOfSymbol(map))) {
|
|
336
|
+
const type = checker.getTypeOfSymbol(entry);
|
|
337
|
+
if (tags.has(type))
|
|
338
|
+
shared.add(type);
|
|
339
|
+
else
|
|
340
|
+
tags.set(type, entry.name);
|
|
341
|
+
}
|
|
342
|
+
for (const type of shared)
|
|
343
|
+
tags.delete(type);
|
|
344
|
+
return tags;
|
|
345
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ExtractedProps } from '../../core/extracted-props.js';
|
|
2
|
+
import type { SourcePage } from './pages.js';
|
|
3
|
+
/** Which build-time transform a page import goes through. */
|
|
4
|
+
export type DocImportKind = 'source' | 'props';
|
|
5
|
+
/** One `*.svelte?source` or `*.svelte?props` import in a page's `<script>`. */
|
|
6
|
+
export type DocImport = {
|
|
7
|
+
kind: DocImportKind;
|
|
8
|
+
/** The specifier as written, query included, for messages. */
|
|
9
|
+
specifier: string;
|
|
10
|
+
/** The component file it resolves to, or `undefined` when it did not resolve. */
|
|
11
|
+
file: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a specifier (query stripped) from a page, the way Vite resolves the
|
|
15
|
+
* page's own imports: the app's aliases (`$lib`, `#lib`) included. Returns the
|
|
16
|
+
* resolved file, or `undefined` when nothing resolves it.
|
|
17
|
+
*/
|
|
18
|
+
export type ResolveImport = (specifier: string, importer: string) => Promise<string | undefined>;
|
|
19
|
+
/** What building agent markdown needs from the plugin. */
|
|
20
|
+
export type AgentMarkdownHost = {
|
|
21
|
+
resolve: ResolveImport;
|
|
22
|
+
/** Record a file the agent markdown was read from, so editing it rebuilds the index. */
|
|
23
|
+
watch(file: string): void;
|
|
24
|
+
warn(message: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* A component's props as the `?props` transform extracts them, with
|
|
27
|
+
* descriptions as markdown. The host watches the files they were read from
|
|
28
|
+
* and passes on extraction warnings.
|
|
29
|
+
*/
|
|
30
|
+
props(file: string): Promise<ExtractedProps>;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* A page's `?source` and `?props` imports, keyed by the local name each binds,
|
|
34
|
+
* with every specifier resolved from the page. Imports inside fenced code
|
|
35
|
+
* samples are not the page's, and are skipped.
|
|
36
|
+
*/
|
|
37
|
+
export declare function docImports(page: Pick<SourcePage, 'file' | 'source'>, resolve: ResolveImport): Promise<Map<string, DocImport>>;
|
|
38
|
+
/**
|
|
39
|
+
* Build the agent markdown rewrite for one page: a function from the page's
|
|
40
|
+
* markdown, setup blocks stripped, to its agent markdown. `undefined` when the page imports nothing
|
|
41
|
+
* agent markdown expands, so most pages cost one scan of their script.
|
|
42
|
+
*/
|
|
43
|
+
export declare function agentMarkdownFor(page: SourcePage, host: AgentMarkdownHost): Promise<((content: string) => string) | undefined>;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent markdown: a page's markdown as an agent reads it (llms-full.txt, the
|
|
3
|
+
* `.md` twins, the MCP server). The page as written keeps `<LiveExample
|
|
4
|
+
* source={…}>` and `<PropsTable of={…}>` as bare tags and their imports are
|
|
5
|
+
* stripped with the `<script>` block, so an agent would never see the example
|
|
6
|
+
* code or the component's props. Here each `LiveExample` whose source is a
|
|
7
|
+
* `?source` import becomes a fenced `svelte` block of the example file's raw
|
|
8
|
+
* source, and each `PropsTable` whose `of` is a props import becomes a heading
|
|
9
|
+
* and a markdown table of the same rows the page renders.
|
|
10
|
+
*
|
|
11
|
+
* Unlike `./collect.js` this reads the filesystem and resolves imports, so it
|
|
12
|
+
* runs in the plugin, which hands it Vite's resolver and the props extractor.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import { propsCaption } from '../../core/extracted-props.js';
|
|
16
|
+
import { outsideCodeFenceRuns, scriptBlocks } from '../markdown-source.js';
|
|
17
|
+
import { relativeToCwd } from '../paths.js';
|
|
18
|
+
/** A default import of a `.svelte` file through `?source` or `?props`. */
|
|
19
|
+
const DOC_IMPORT = /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s*(['"])([^'"\n]+?\.svelte)\?(source|props)\2/g;
|
|
20
|
+
/**
|
|
21
|
+
* A page's `?source` and `?props` imports, keyed by the local name each binds,
|
|
22
|
+
* with every specifier resolved from the page. Imports inside fenced code
|
|
23
|
+
* samples are not the page's, and are skipped.
|
|
24
|
+
*/
|
|
25
|
+
export async function docImports(page, resolve) {
|
|
26
|
+
const imports = new Map();
|
|
27
|
+
for (const block of scriptBlocks(page.source)) {
|
|
28
|
+
for (const [, local, , base, kind] of block.matchAll(DOC_IMPORT)) {
|
|
29
|
+
const file = await resolve(base, page.file);
|
|
30
|
+
imports.set(local, { kind: kind, specifier: `${base}?${kind}`, file });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return imports;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A `<LiveExample …>…</LiveExample>` (or self-closing) tag starting a line.
|
|
37
|
+
* Group 1 is its indentation, group 2 its attributes; an attribute value in
|
|
38
|
+
* braces may contain `>`.
|
|
39
|
+
*/
|
|
40
|
+
const LIVE_EXAMPLE = /^([ \t]*)<LiveExample\b((?:[^>{}]|\{[^{}]*\})*?)(?:\/>|>[\s\S]*?<\/LiveExample\s*>)/gm;
|
|
41
|
+
/** `source={name}`, or the `{source}` shorthand. */
|
|
42
|
+
const SOURCE_ATTRIBUTE = /(?:\bsource\s*=\s*\{\s*([A-Za-z_$][\w$]*)\s*\}|\{\s*(source)\s*\})/;
|
|
43
|
+
/** A backtick fence one longer than the longest backtick run in `text`, and at least `min`. */
|
|
44
|
+
function fenceFor(text, min) {
|
|
45
|
+
const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map(([run]) => run.length));
|
|
46
|
+
return '`'.repeat(Math.max(min, longest + 1));
|
|
47
|
+
}
|
|
48
|
+
/** `text` with `indent` before each non-empty line. */
|
|
49
|
+
function indented(text, indent) {
|
|
50
|
+
return text
|
|
51
|
+
.split('\n')
|
|
52
|
+
.map((line) => (line ? indent + line : line))
|
|
53
|
+
.join('\n');
|
|
54
|
+
}
|
|
55
|
+
/** A fenced block of `code`, fenced longer than any backtick run inside it. */
|
|
56
|
+
function fenced(code, lang, indent) {
|
|
57
|
+
const fence = fenceFor(code, 3);
|
|
58
|
+
return indented([fence + lang, code, fence].join('\n'), indent);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build the agent markdown rewrite for one page: a function from the page's
|
|
62
|
+
* markdown, setup blocks stripped, to its agent markdown. `undefined` when the page imports nothing
|
|
63
|
+
* agent markdown expands, so most pages cost one scan of their script.
|
|
64
|
+
*/
|
|
65
|
+
export async function agentMarkdownFor(page, host) {
|
|
66
|
+
const imports = await docImports(page, host.resolve);
|
|
67
|
+
if (imports.size === 0)
|
|
68
|
+
return undefined;
|
|
69
|
+
// Read up front, so the rewrite itself is synchronous. Props extractions all
|
|
70
|
+
// start together, so the extractor batches them into one TypeScript update.
|
|
71
|
+
const sources = new Map();
|
|
72
|
+
const tables = new Map();
|
|
73
|
+
await Promise.all([...imports].map(async ([local, entry]) => {
|
|
74
|
+
// Watched even when it can't be read, so creating or fixing it rebuilds.
|
|
75
|
+
if (entry.file !== undefined)
|
|
76
|
+
host.watch(entry.file);
|
|
77
|
+
if (entry.kind === 'source') {
|
|
78
|
+
sources.set(local, entry.file === undefined ? undefined : readExample(entry.file));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
tables.set(local, entry.file === undefined ? undefined : await host.props(entry.file));
|
|
82
|
+
}
|
|
83
|
+
}));
|
|
84
|
+
const keepTag = (tag, local, attribute) => {
|
|
85
|
+
host.warn(`${relativeToCwd(page.file)}: could not read \`${imports.get(local)?.specifier}\` ` +
|
|
86
|
+
`for <${tag} ${attribute}={${local}}>, so agent markdown keeps the tag.`);
|
|
87
|
+
};
|
|
88
|
+
return (content) => {
|
|
89
|
+
// The level of the heading a table sits under, carried across code fences.
|
|
90
|
+
let level = 1;
|
|
91
|
+
return outsideCodeFenceRuns(content, (prose) => {
|
|
92
|
+
const levelAt = (offset) => lastHeadingLevel(prose.slice(0, offset)) ?? level;
|
|
93
|
+
const withTables = prose.replace(PROPS_TABLE, (tag, indent, attributes, offset) => {
|
|
94
|
+
const match = OF_ATTRIBUTE.exec(attributes);
|
|
95
|
+
const local = match?.[1] ?? match?.[2];
|
|
96
|
+
if (local === undefined || !tables.has(local))
|
|
97
|
+
return tag;
|
|
98
|
+
const props = tables.get(local);
|
|
99
|
+
if (props === undefined) {
|
|
100
|
+
keepTag('PropsTable', local, 'of');
|
|
101
|
+
return tag;
|
|
102
|
+
}
|
|
103
|
+
const title = TITLE_ATTRIBUTE.exec(attributes);
|
|
104
|
+
const caption = title ? title.slice(1).find((value) => value !== undefined) : undefined;
|
|
105
|
+
return propsTable(props, caption, Math.min(6, levelAt(offset) + 1), indent);
|
|
106
|
+
});
|
|
107
|
+
level = levelAt(prose.length);
|
|
108
|
+
return withTables.replace(LIVE_EXAMPLE, (tag, indent, attributes) => {
|
|
109
|
+
const match = SOURCE_ATTRIBUTE.exec(attributes);
|
|
110
|
+
const local = match?.[1] ?? match?.[2];
|
|
111
|
+
if (local === undefined || !sources.has(local))
|
|
112
|
+
return tag;
|
|
113
|
+
const code = sources.get(local);
|
|
114
|
+
if (code === undefined) {
|
|
115
|
+
keepTag('LiveExample', local, 'source');
|
|
116
|
+
return tag;
|
|
117
|
+
}
|
|
118
|
+
return fenced(code, 'svelte', indent);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A `<PropsTable …/>` or `<PropsTable …>…</PropsTable>` tag starting a line,
|
|
125
|
+
* grouped like {@link LIVE_EXAMPLE}.
|
|
126
|
+
*/
|
|
127
|
+
const PROPS_TABLE = /^([ \t]*)<PropsTable\b((?:[^>{}]|\{[^{}]*\})*?)(?:\/>|>[\s\S]*?<\/PropsTable\s*>)/gm;
|
|
128
|
+
/** `of={name}`, or the `{of}` shorthand. */
|
|
129
|
+
const OF_ATTRIBUTE = /(?:(?:^|\s)of\s*=\s*\{\s*([A-Za-z_$][\w$]*)\s*\}|\{\s*(of)\s*\})/;
|
|
130
|
+
/** A `title` given as a string literal, quoted or in braces. */
|
|
131
|
+
const TITLE_ATTRIBUTE = /(?:^|\s)title\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*(?:"([^"]*)"|'([^']*)')\s*\})/;
|
|
132
|
+
/** An ATX heading's marker. */
|
|
133
|
+
const HEADING = /^ {0,3}(#{1,6})(?:[ \t]|$)/gm;
|
|
134
|
+
/** The level of the last heading in `prose`, if it has one. */
|
|
135
|
+
function lastHeadingLevel(prose) {
|
|
136
|
+
return [...prose.matchAll(HEADING)].at(-1)?.[1].length;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A generated props table as agent markdown: the caption as a heading, then a
|
|
140
|
+
* `Prop | Type | Default | Description` table with the rows the page renders,
|
|
141
|
+
* then the inherited attributes summary.
|
|
142
|
+
*/
|
|
143
|
+
function propsTable(props, title, level, indent) {
|
|
144
|
+
const blocks = [`${'#'.repeat(level)} ${code(title ?? propsCaption(props))}`];
|
|
145
|
+
if (props.props.length) {
|
|
146
|
+
blocks.push([
|
|
147
|
+
'| Prop | Type | Default | Description |',
|
|
148
|
+
'| --- | --- | --- | --- |',
|
|
149
|
+
...props.props.map((prop) => `| ${propCells(prop).join(' | ')} |`)
|
|
150
|
+
].join('\n'));
|
|
151
|
+
}
|
|
152
|
+
for (const group of props.inherited) {
|
|
153
|
+
blocks.push(group.kind === 'element'
|
|
154
|
+
? `Also accepts every ${code(`<${group.label}>`)} attribute.`
|
|
155
|
+
: `Also accepts every prop of ${code(group.label)}.`);
|
|
156
|
+
}
|
|
157
|
+
if (props.props.length === 0 && props.inherited.length === 0)
|
|
158
|
+
blocks.push('No props.');
|
|
159
|
+
return indented(blocks.join('\n\n'), indent);
|
|
160
|
+
}
|
|
161
|
+
/** One row's cells, marked as the rendered row is and escaped for a table. */
|
|
162
|
+
function propCells(prop) {
|
|
163
|
+
let name = code(prop.required ? prop.name : `${prop.name}?`);
|
|
164
|
+
if (prop.deprecated !== undefined)
|
|
165
|
+
name = `~~${name}~~`;
|
|
166
|
+
const marks = [
|
|
167
|
+
prop.bindable && '*(bindable)*',
|
|
168
|
+
prop.deprecated !== undefined && '*(deprecated)*'
|
|
169
|
+
];
|
|
170
|
+
const description = [prop.deprecated && `Deprecated: ${prop.deprecated}`, prop.description]
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.map((text) => inline(text))
|
|
173
|
+
.join('<br>');
|
|
174
|
+
return [
|
|
175
|
+
[name, ...marks.filter(Boolean)].join(' '),
|
|
176
|
+
prop.type === undefined ? '' : code(prop.type),
|
|
177
|
+
prop.default === undefined ? '' : code(prop.default),
|
|
178
|
+
description
|
|
179
|
+
].map((cell) => cell.replaceAll('|', '\\|'));
|
|
180
|
+
}
|
|
181
|
+
/** `text` as a code span on one line, fenced longer than any backtick run in it. */
|
|
182
|
+
function code(text) {
|
|
183
|
+
const flat = text.replace(/\s*\n\s*/g, ' ');
|
|
184
|
+
const fence = fenceFor(flat, 1);
|
|
185
|
+
const pad = flat.startsWith('`') || flat.endsWith('`') ? ' ' : '';
|
|
186
|
+
return fence + pad + flat + pad + fence;
|
|
187
|
+
}
|
|
188
|
+
/** Markdown on one line: paragraphs joined with `<br><br>`, soft breaks as spaces. */
|
|
189
|
+
function inline(markdown) {
|
|
190
|
+
return markdown
|
|
191
|
+
.trim()
|
|
192
|
+
.split(/\n[ \t]*\n\s*/)
|
|
193
|
+
.map((paragraph) => paragraph.replace(/\s*\n\s*/g, ' '))
|
|
194
|
+
.join('<br><br>');
|
|
195
|
+
}
|
|
196
|
+
/** An example file's raw source, as the `?source` transform reads it. */
|
|
197
|
+
function readExample(file) {
|
|
198
|
+
try {
|
|
199
|
+
return fs.readFileSync(file, 'utf-8').trimEnd();
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -29,5 +29,8 @@ export declare function searchIndex(pages: SourcePage[], options: IndexOptions):
|
|
|
29
29
|
* The LLM index: title, section, description, and the full markdown content.
|
|
30
30
|
* Served as the `svelte-docsmith/llms` virtual module and consumed server-side
|
|
31
31
|
* by `llms.txt` / `llms-full.txt` routes, so it never ships to the client.
|
|
32
|
+
*
|
|
33
|
+
* `rewrite` turns a page's markdown into agent markdown (see
|
|
34
|
+
* `./agent-markdown.js`); without it, the content is the page as written.
|
|
32
35
|
*/
|
|
33
|
-
export declare function llmsIndex(pages: SourcePage[], options: IndexOptions): LlmsDoc[];
|
|
36
|
+
export declare function llmsIndex(pages: SourcePage[], options: IndexOptions, rewrite?: (content: string, page: SourcePage) => string): LlmsDoc[];
|