@rarusoft/dendrite-wiki 0.1.0-alpha.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 +79 -0
- package/dist/api-extractor/extract.js +269 -0
- package/dist/api-extractor/language-extractor.js +15 -0
- package/dist/api-extractor/python-extractor.js +358 -0
- package/dist/api-extractor/render.js +195 -0
- package/dist/api-extractor/tree-sitter-extractor.js +1079 -0
- package/dist/api-extractor/types.js +11 -0
- package/dist/api-extractor/typescript-extractor.js +50 -0
- package/dist/api-extractor/walk.js +178 -0
- package/dist/api-reference.js +438 -0
- package/dist/benchmark-events.js +129 -0
- package/dist/benchmark.js +270 -0
- package/dist/binder-export.js +381 -0
- package/dist/canonical-target.js +168 -0
- package/dist/chart-insert.js +377 -0
- package/dist/chart-prompts.js +414 -0
- package/dist/context-cache.js +98 -0
- package/dist/contradicts-shipped-memory.js +232 -0
- package/dist/diff-context.js +142 -0
- package/dist/doctor.js +220 -0
- package/dist/generated-docs.js +219 -0
- package/dist/i18n.js +71 -0
- package/dist/index.js +49 -0
- package/dist/librarian.js +255 -0
- package/dist/maintenance-actions.js +244 -0
- package/dist/maintenance-inbox.js +842 -0
- package/dist/maintenance-runner.js +62 -0
- package/dist/page-drift.js +225 -0
- package/dist/page-inbox.js +168 -0
- package/dist/report-export.js +339 -0
- package/dist/review-bridge.js +1386 -0
- package/dist/search-index.js +199 -0
- package/dist/store.js +1617 -0
- package/dist/telemetry-defaults.js +44 -0
- package/dist/telemetry-report.js +263 -0
- package/dist/telemetry.js +544 -0
- package/dist/wiki-synthesis.js +901 -0
- package/package.json +35 -0
- package/src/api-extractor/extract.ts +333 -0
- package/src/api-extractor/language-extractor.ts +37 -0
- package/src/api-extractor/python-extractor.ts +380 -0
- package/src/api-extractor/render.ts +267 -0
- package/src/api-extractor/tree-sitter-extractor.ts +1210 -0
- package/src/api-extractor/types.ts +41 -0
- package/src/api-extractor/typescript-extractor.ts +56 -0
- package/src/api-extractor/walk.ts +209 -0
- package/src/api-reference.ts +552 -0
- package/src/benchmark-events.ts +216 -0
- package/src/benchmark.ts +376 -0
- package/src/binder-export.ts +437 -0
- package/src/canonical-target.ts +192 -0
- package/src/chart-insert.ts +478 -0
- package/src/chart-prompts.ts +417 -0
- package/src/context-cache.ts +129 -0
- package/src/contradicts-shipped-memory.ts +311 -0
- package/src/diff-context.ts +187 -0
- package/src/doctor.ts +260 -0
- package/src/generated-docs.ts +316 -0
- package/src/i18n.ts +106 -0
- package/src/index.ts +59 -0
- package/src/librarian.ts +331 -0
- package/src/maintenance-actions.ts +314 -0
- package/src/maintenance-inbox.ts +1132 -0
- package/src/maintenance-runner.ts +85 -0
- package/src/page-drift.ts +292 -0
- package/src/page-inbox.ts +254 -0
- package/src/report-export.ts +392 -0
- package/src/review-bridge.ts +1729 -0
- package/src/search-index.ts +266 -0
- package/src/store.ts +2171 -0
- package/src/telemetry-defaults.ts +50 -0
- package/src/telemetry-report.ts +365 -0
- package/src/telemetry.ts +757 -0
- package/src/wiki-synthesis.ts +1307 -0
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rarusoft/dendrite-wiki",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "The markdown-wiki adapter for @rarusoft/dendrite-memory: implements the CanonicalTarget interface against VitePress-rendered docs/wiki/, owns the wiki page store + lint + search + synthesis + maintenance review surface + browser-side review bridge. Pair with @rarusoft/dendrite-memory for the full Dendrite living-wiki experience.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.ts",
|
|
12
|
+
"source": "./src/index.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@rarusoft/dendrite-memory": "0.1.0-alpha.0"
|
|
29
|
+
},
|
|
30
|
+
"license": "Apache-2.0",
|
|
31
|
+
"author": {
|
|
32
|
+
"name": "Michael Fillalan",
|
|
33
|
+
"url": "https://x.com/MichaelFillalan"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts an `ApiFileReference` from a single TypeScript source file.
|
|
3
|
+
*
|
|
4
|
+
* Uses the TypeScript Compiler API directly (no typedoc, no extra dependencies — the
|
|
5
|
+
* `typescript` package is already a devDep). Parses the file with `ts.createSourceFile`
|
|
6
|
+
* and `setParentNodes: true` so `Node.getText()`, the printer, and parent-walking helpers
|
|
7
|
+
* all work without a full Program. JSDoc is read off `node.jsDoc[last]` directly because
|
|
8
|
+
* Program-loaded source files don't set parent pointers in a way `getJSDocCommentsAndTags`
|
|
9
|
+
* can use; the last entry of the array is the immediately-preceding doc block.
|
|
10
|
+
*
|
|
11
|
+
* Each top-level exported declaration becomes one `ApiSymbol`. Function default values
|
|
12
|
+
* are stripped from rendered signatures (implementation detail, not type contract).
|
|
13
|
+
* Interfaces render with their full member body via the TS printer. `@internal`-tagged
|
|
14
|
+
* exports are filtered. The renderer in `./render.ts` formats the result as markdown.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import ts from 'typescript';
|
|
20
|
+
import type { ApiDocTag, ApiFileReference, ApiSymbol, ApiSymbolKind } from './types.js';
|
|
21
|
+
|
|
22
|
+
export interface ExtractOptions {
|
|
23
|
+
// Project root used to derive `sourcePath` (project-relative, forward slashes).
|
|
24
|
+
rootDir?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function extractApiFileReference(
|
|
28
|
+
sourcePath: string,
|
|
29
|
+
options: ExtractOptions = {}
|
|
30
|
+
): ApiFileReference {
|
|
31
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
32
|
+
const absolute = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(rootDir, sourcePath);
|
|
33
|
+
const relative = toForwardSlash(path.relative(rootDir, absolute));
|
|
34
|
+
|
|
35
|
+
// Parse the file directly with parent pointers enabled. We don't need a Program for A1 —
|
|
36
|
+
// there's no type-checker work, just AST traversal of declarations and JSDoc. Setting parent
|
|
37
|
+
// pointers makes ts.Node.getText() and printNode work without a separate setup step.
|
|
38
|
+
const text = readFileSync(absolute, 'utf8');
|
|
39
|
+
const sourceFile = ts.createSourceFile(absolute, text, ts.ScriptTarget.ES2022, /*setParentNodes*/ true, ts.ScriptKind.TS);
|
|
40
|
+
|
|
41
|
+
const symbols: ApiSymbol[] = [];
|
|
42
|
+
for (const statement of sourceFile.statements) {
|
|
43
|
+
const extracted = extractSymbolsFromStatement(statement, sourceFile);
|
|
44
|
+
for (const symbol of extracted) {
|
|
45
|
+
if (isInternal(symbol)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
symbols.push(symbol);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
symbols.sort((left, right) => left.sourceLine - right.sourceLine);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
sourcePath: relative,
|
|
56
|
+
moduleSlug: deriveModuleSlug(relative),
|
|
57
|
+
symbols,
|
|
58
|
+
fileDocComment: extractFileDocComment(sourceFile)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function extractSymbolsFromStatement(
|
|
63
|
+
statement: ts.Statement,
|
|
64
|
+
sourceFile: ts.SourceFile
|
|
65
|
+
): ApiSymbol[] {
|
|
66
|
+
if (!hasExportModifier(statement)) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (ts.isFunctionDeclaration(statement) && statement.name) {
|
|
71
|
+
return [buildSymbol(statement, statement.name.text, 'function', renderFunctionSignature(statement), sourceFile)];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (ts.isClassDeclaration(statement) && statement.name) {
|
|
75
|
+
return [buildSymbol(statement, statement.name.text, 'class', renderClassSignature(statement), sourceFile)];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (ts.isInterfaceDeclaration(statement)) {
|
|
79
|
+
return [buildSymbol(statement, statement.name.text, 'interface', renderInterfaceSignature(statement), sourceFile)];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (ts.isTypeAliasDeclaration(statement)) {
|
|
83
|
+
return [buildSymbol(statement, statement.name.text, 'type-alias', renderTypeAliasSignature(statement), sourceFile)];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (ts.isEnumDeclaration(statement)) {
|
|
87
|
+
return [buildSymbol(statement, statement.name.text, 'enum', renderEnumSignature(statement), sourceFile)];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (ts.isVariableStatement(statement)) {
|
|
91
|
+
const out: ApiSymbol[] = [];
|
|
92
|
+
for (const decl of statement.declarationList.declarations) {
|
|
93
|
+
if (!ts.isIdentifier(decl.name)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
out.push(
|
|
97
|
+
buildSymbol(statement, decl.name.text, 'variable', renderVariableSignature(statement, decl), sourceFile)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildSymbol(
|
|
107
|
+
jsDocCarrier: ts.Node,
|
|
108
|
+
name: string,
|
|
109
|
+
kind: ApiSymbolKind,
|
|
110
|
+
signature: string,
|
|
111
|
+
sourceFile: ts.SourceFile
|
|
112
|
+
): ApiSymbol {
|
|
113
|
+
const { docComment, tags } = parseJSDoc(jsDocCarrier);
|
|
114
|
+
const sourceLine = sourceFile.getLineAndCharacterOfPosition(jsDocCarrier.getStart(sourceFile)).line + 1;
|
|
115
|
+
const isDeprecated = tags.some((tag) => tag.name === 'deprecated');
|
|
116
|
+
return {
|
|
117
|
+
name,
|
|
118
|
+
kind,
|
|
119
|
+
signature,
|
|
120
|
+
docComment,
|
|
121
|
+
tags,
|
|
122
|
+
sourceLine,
|
|
123
|
+
isDeprecated
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hasExportModifier(node: ts.Node): boolean {
|
|
128
|
+
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
|
129
|
+
if (!modifiers) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseJSDoc(node: ts.Node): { docComment: string | null; tags: ApiDocTag[] } {
|
|
136
|
+
// ts.getJSDocCommentsAndTags walks the parent chain, but Program-created source files do
|
|
137
|
+
// not have parent pointers set, so it returns nothing. Read .jsDoc directly instead. The
|
|
138
|
+
// parser attaches one JSDoc node per JSDoc block preceding the declaration; the LAST entry
|
|
139
|
+
// is the immediately-preceding one, which is the "owning" doc for the declaration.
|
|
140
|
+
const jsDocList = (node as { jsDoc?: ts.JSDoc[] }).jsDoc;
|
|
141
|
+
if (!jsDocList || jsDocList.length === 0) {
|
|
142
|
+
return { docComment: null, tags: [] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const owning = jsDocList[jsDocList.length - 1];
|
|
146
|
+
const body = renderJSDocComment(owning.comment).trim();
|
|
147
|
+
const tags: ApiDocTag[] = [];
|
|
148
|
+
if (owning.tags) {
|
|
149
|
+
for (const tag of owning.tags) {
|
|
150
|
+
tags.push(parseTag(tag));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { docComment: body.length > 0 ? body : null, tags };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseTag(tag: ts.JSDocTag): ApiDocTag {
|
|
157
|
+
const name = tag.tagName.text;
|
|
158
|
+
const text = renderJSDocComment(tag.comment).trim();
|
|
159
|
+
|
|
160
|
+
if (ts.isJSDocParameterTag(tag)) {
|
|
161
|
+
return {
|
|
162
|
+
name,
|
|
163
|
+
text,
|
|
164
|
+
paramName: ts.isIdentifier(tag.name) ? tag.name.text : tag.name.getText()
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { name, text };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function renderJSDocComment(comment: string | ts.NodeArray<ts.JSDocComment> | undefined): string {
|
|
172
|
+
if (!comment) {
|
|
173
|
+
return '';
|
|
174
|
+
}
|
|
175
|
+
if (typeof comment === 'string') {
|
|
176
|
+
return normalizeLineEndings(comment);
|
|
177
|
+
}
|
|
178
|
+
const joined = comment
|
|
179
|
+
.map((part) => {
|
|
180
|
+
if (part.kind === ts.SyntaxKind.JSDocText) {
|
|
181
|
+
return (part as ts.JSDocText).text;
|
|
182
|
+
}
|
|
183
|
+
if (part.kind === ts.SyntaxKind.JSDocLink || part.kind === ts.SyntaxKind.JSDocLinkCode || part.kind === ts.SyntaxKind.JSDocLinkPlain) {
|
|
184
|
+
const link = part as ts.JSDocLink | ts.JSDocLinkCode | ts.JSDocLinkPlain;
|
|
185
|
+
const targetName = link.name ? link.name.getText() : '';
|
|
186
|
+
const linkText = link.text ?? '';
|
|
187
|
+
const label = linkText.trim().length > 0 ? `${targetName} ${linkText.trim()}`.trim() : targetName;
|
|
188
|
+
return `{@link ${label}}`;
|
|
189
|
+
}
|
|
190
|
+
return '';
|
|
191
|
+
})
|
|
192
|
+
.join('');
|
|
193
|
+
return normalizeLineEndings(joined);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Normalize CRLF / lone CR to LF in extracted text. Source files checked out on Windows
|
|
198
|
+
* with `core.autocrlf=true` come back from disk with CRLF, which would otherwise leak
|
|
199
|
+
* into the extracted JSDoc/TSDoc text fields and produce platform-dependent fixtures
|
|
200
|
+
* and pages. Normalizing at the extraction boundary keeps every downstream artifact
|
|
201
|
+
* (`ApiFileReference`, rendered markdown, manifest content hash) byte-identical across
|
|
202
|
+
* Windows / macOS / Linux checkouts.
|
|
203
|
+
*/
|
|
204
|
+
function normalizeLineEndings(text: string): string {
|
|
205
|
+
return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isInternal(symbol: ApiSymbol): boolean {
|
|
209
|
+
return symbol.tags.some((tag) => tag.name === 'internal');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function extractFileDocComment(sourceFile: ts.SourceFile): string | null {
|
|
213
|
+
const fullText = sourceFile.getFullText();
|
|
214
|
+
const ranges = ts.getLeadingCommentRanges(fullText, 0);
|
|
215
|
+
if (!ranges) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const range of ranges) {
|
|
220
|
+
if (range.kind !== ts.SyntaxKind.MultiLineCommentTrivia) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const raw = fullText.slice(range.pos, range.end);
|
|
224
|
+
if (!raw.startsWith('/**')) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
return cleanBlockComment(raw);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function cleanBlockComment(raw: string): string {
|
|
234
|
+
const inner = raw.replace(/^\/\*\*/, '').replace(/\*\/$/, '');
|
|
235
|
+
const lines = inner.split(/\r?\n/).map((line) => line.replace(/^\s*\*\s?/, '').trimEnd());
|
|
236
|
+
while (lines.length > 0 && lines[0].trim() === '') {
|
|
237
|
+
lines.shift();
|
|
238
|
+
}
|
|
239
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '') {
|
|
240
|
+
lines.pop();
|
|
241
|
+
}
|
|
242
|
+
return lines.join('\n').trim();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// --- signature renderers -----------------------------------------------------
|
|
246
|
+
|
|
247
|
+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, removeComments: true, omitTrailingSemicolon: true });
|
|
248
|
+
|
|
249
|
+
function renderFunctionSignature(node: ts.FunctionDeclaration): string {
|
|
250
|
+
const typeParams = renderTypeParameters(node.typeParameters);
|
|
251
|
+
const params = (node.parameters ?? []).map((p) => renderParameter(p)).join(', ');
|
|
252
|
+
const returnType = node.type ? `: ${printNode(node.type)}` : '';
|
|
253
|
+
return `function ${node.name?.text ?? ''}${typeParams}(${params})${returnType}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function renderParameter(node: ts.ParameterDeclaration): string {
|
|
257
|
+
// Strip default values from the rendered signature — they're implementation detail, not
|
|
258
|
+
// part of the public type contract, and they add visual noise to the API page. Preserve
|
|
259
|
+
// rest tokens, optionals, and the type annotation.
|
|
260
|
+
const dotDotDot = node.dotDotDotToken ? '...' : '';
|
|
261
|
+
const name = node.name.getText();
|
|
262
|
+
const question = node.questionToken ? '?' : '';
|
|
263
|
+
const typeAnnotation = node.type ? `: ${printNode(node.type)}` : '';
|
|
264
|
+
return `${dotDotDot}${name}${question}${typeAnnotation}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function renderClassSignature(node: ts.ClassDeclaration): string {
|
|
268
|
+
const typeParams = renderTypeParameters(node.typeParameters);
|
|
269
|
+
const heritage = (node.heritageClauses ?? [])
|
|
270
|
+
.map((clause) => printNode(clause).trim())
|
|
271
|
+
.join(' ');
|
|
272
|
+
const heritageSuffix = heritage.length > 0 ? ` ${heritage}` : '';
|
|
273
|
+
return `class ${node.name?.text ?? ''}${typeParams}${heritageSuffix}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function renderInterfaceSignature(node: ts.InterfaceDeclaration): string {
|
|
277
|
+
// Print the full interface including its members. An empty `interface Foo` line on its
|
|
278
|
+
// own is useless on the API page; readers care about the shape of the type, which is the
|
|
279
|
+
// member list. The TS printer handles formatting + indentation cleanly. Strip the leading
|
|
280
|
+
// `export` modifier — every symbol on an API reference page is exported by definition.
|
|
281
|
+
return printNode(node).replace(/^export\s+/, '');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function renderTypeAliasSignature(node: ts.TypeAliasDeclaration): string {
|
|
285
|
+
const typeParams = renderTypeParameters(node.typeParameters);
|
|
286
|
+
return `type ${node.name.text}${typeParams} = ${printNode(node.type)}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function renderEnumSignature(node: ts.EnumDeclaration): string {
|
|
290
|
+
const members = node.members.map((member) => ` ${printNode(member)}`).join(',\n');
|
|
291
|
+
return `enum ${node.name.text} {\n${members}\n}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function renderVariableSignature(
|
|
295
|
+
statement: ts.VariableStatement,
|
|
296
|
+
decl: ts.VariableDeclaration
|
|
297
|
+
): string {
|
|
298
|
+
const flags = statement.declarationList.flags;
|
|
299
|
+
const keyword = flags & ts.NodeFlags.Const ? 'const' : flags & ts.NodeFlags.Let ? 'let' : 'var';
|
|
300
|
+
const name = ts.isIdentifier(decl.name) ? decl.name.text : decl.name.getText();
|
|
301
|
+
const typeAnnotation = decl.type ? `: ${printNode(decl.type)}` : '';
|
|
302
|
+
return `${keyword} ${name}${typeAnnotation}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function renderTypeParameters(params: ts.NodeArray<ts.TypeParameterDeclaration> | undefined): string {
|
|
306
|
+
if (!params || params.length === 0) {
|
|
307
|
+
return '';
|
|
308
|
+
}
|
|
309
|
+
return `<${params.map((p) => printNode(p)).join(', ')}>`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function printNode(node: ts.Node): string {
|
|
313
|
+
const sourceFile = node.getSourceFile();
|
|
314
|
+
if (sourceFile) {
|
|
315
|
+
return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
|
|
316
|
+
}
|
|
317
|
+
// Fallback for synthesized nodes: build a transient SourceFile.
|
|
318
|
+
const transientFile = ts.createSourceFile('__transient__.ts', '', ts.ScriptTarget.ES2022, false, ts.ScriptKind.TS);
|
|
319
|
+
return printer.printNode(ts.EmitHint.Unspecified, node, transientFile);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function deriveModuleSlug(relativeSourcePath: string): string {
|
|
323
|
+
const trimmed = relativeSourcePath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
324
|
+
const withoutExt = trimmed.replace(/\.[cm]?tsx?$/i, '');
|
|
325
|
+
const stripped = withoutExt
|
|
326
|
+
.replace(/^src\//, '')
|
|
327
|
+
.replace(/^packages\/([^/]+)\/src\//, '$1/');
|
|
328
|
+
return `api/${stripped}`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function toForwardSlash(value: string): string {
|
|
332
|
+
return value.replace(/\\/g, '/');
|
|
333
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language pluggability surface for the API reference generator.
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator (`refreshApiReference`) walks a registered list of `LanguageExtractor`
|
|
5
|
+
* implementations and dispatches to the first one whose `detect(rootDir)` returns true.
|
|
6
|
+
* TypeScript is the only built-in today (`./typescript-extractor.ts`); future Python/Rust/Go
|
|
7
|
+
* support is a drop-in module implementing this same interface — no orchestrator changes.
|
|
8
|
+
*
|
|
9
|
+
* The interface is deliberately small and async-friendly so a Python extractor that shells
|
|
10
|
+
* out to `pdoc --output json` or a Rust extractor wrapping `rustdoc --output-format json`
|
|
11
|
+
* can implement it without contortion. It is also free of TypeScript-specific shapes;
|
|
12
|
+
* everything the orchestrator needs is the language-agnostic `ApiFileReference` from
|
|
13
|
+
* `./types.ts`. Phase A7 of the API reference roadmap establishes this layering.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ApiFileReference } from './types.js';
|
|
17
|
+
import type { WalkOptions } from './walk.js';
|
|
18
|
+
|
|
19
|
+
export interface LanguageExtractor {
|
|
20
|
+
// Stable identifier — 'typescript' | 'python' | 'rust' | etc. Used in diagnostics and
|
|
21
|
+
// (eventually) in per-language manifest entries when more than one extractor is active.
|
|
22
|
+
id: string;
|
|
23
|
+
|
|
24
|
+
// Returns true iff this extractor can meaningfully handle the project rooted at
|
|
25
|
+
// `rootDir`. Should be cheap (file-existence checks) and never throw — return false on
|
|
26
|
+
// any error.
|
|
27
|
+
detect(rootDir: string): Promise<boolean>;
|
|
28
|
+
|
|
29
|
+
// Returns the list of source files this extractor will operate on, project-relative,
|
|
30
|
+
// forward slashes, sorted.
|
|
31
|
+
walk(rootDir: string, options?: WalkOptions): Promise<string[]>;
|
|
32
|
+
|
|
33
|
+
// Parses one source file and returns its API surface. The orchestrator never calls
|
|
34
|
+
// `extract` for a path that did not come back from the same extractor's `walk`, so each
|
|
35
|
+
// extractor controls its own parser semantics end-to-end.
|
|
36
|
+
extract(sourcePath: string, options?: { rootDir?: string }): Promise<ApiFileReference>;
|
|
37
|
+
}
|