Core: Support generating docs for Typescript file

This commit is contained in:
Fuma Nama
2024-01-30 20:43:34 +08:00
parent 3a92635a2b
commit 054ec60842
6 changed files with 223 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': minor
---
Support generating docs for Typescript file
+7
View File
@@ -44,6 +44,10 @@
"import": "./dist/link.js",
"types": "./dist/link.d.ts"
},
"./typescript": {
"import": "./dist/typescript.js",
"types": "./dist/typescript.d.ts"
},
"./middleware": {
"import": "./dist/middleware.js",
"types": "./dist/middleware.d.ts"
@@ -72,6 +76,9 @@
"toc": [
"./dist/toc.d.ts"
],
"typescript": [
"./dist/typescript.d.ts"
],
"search/client": [
"./dist/search/client.d.ts"
],
+145
View File
@@ -0,0 +1,145 @@
import * as ts from 'typescript';
export interface DocEntry {
name: string;
description: string;
type: string;
default?: string;
}
interface Context {
program: ts.Program;
checker: ts.TypeChecker;
options: Options;
}
interface EntryContext extends Context {
type: ts.Type;
symbol: ts.Symbol;
}
export interface Options {
file: string;
name: string;
/**
* Modify output property entry
*/
transform?: (
this: EntryContext,
entry: DocEntry,
propertyType: ts.Type,
propertySymbol: ts.Symbol,
) => void;
options?: Partial<{
files: string[];
tsconfigPath: string;
/** A root directory to resolve relative path entries in the config file to. e.g. outDir */
basePath: string;
}>;
}
const cache = new Map<string, ts.Program>();
function getProgram(options: Options['options'] = {}): ts.Program {
const key = JSON.stringify(options);
const cached = cache.get(key);
if (cached) return cached;
const configFile = ts.readJsonConfigFile(
options.tsconfigPath ?? './tsconfig.json',
ts.sys.readFile.bind(ts.sys),
);
const parsed = ts.parseJsonSourceFileConfigFileContent(
configFile,
ts.sys,
options.basePath ?? './',
);
const program = ts.createProgram({
rootNames: options.files ?? parsed.fileNames,
options: {
...parsed.options,
incremental: false,
},
});
cache.set(key, program);
return program;
}
function getExportedSymbol({
options: { file, name },
checker,
program,
}: Context): ts.Symbol | undefined {
const sourceFile = program.getSourceFile(file);
if (!sourceFile) return;
const fileSymbol = checker.getSymbolAtLocation(sourceFile);
if (!fileSymbol) return;
const exports = checker.getExportsOfModule(fileSymbol);
return exports.find((e) => e.getEscapedName().toString() === name);
}
/**
* Generate documentation for properties in an exported type/interface
*/
export function generateDocumentation(options: Options): DocEntry[] {
const program = getProgram(options.options);
const checker = program.getTypeChecker();
const ctx: Context = {
checker,
program,
options,
};
const symbol = getExportedSymbol(ctx);
if (!symbol) return [];
const type = checker.getDeclaredTypeOfSymbol(symbol);
const entryContext: EntryContext = {
...ctx,
type,
symbol,
};
return type.getProperties().map(getDocEntry.bind(entryContext));
}
function getDocEntry(this: EntryContext, prop: ts.Symbol): DocEntry {
const subType = this.checker.getTypeOfSymbol(prop);
const defaultJsDocTag = prop
.getJsDocTags()
.find((info) => ['default', 'defaultValue'].includes(info.name));
let typeName = this.checker.typeToString(
subType.getNonNullableType(),
undefined,
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope,
);
if (subType.aliasSymbol && !subType.aliasTypeArguments) {
typeName = subType.aliasSymbol.escapedName.toString();
}
const entry: DocEntry = {
name: prop.getName(),
description: ts.displayPartsToString(
prop.getDocumentationComment(this.checker),
),
default: defaultJsDocTag?.text
? ts.displayPartsToString(defaultJsDocTag.text)
: undefined,
type: typeName,
};
this.options.transform?.call(this, entry, subType, prop);
return entry;
}
+45
View File
@@ -0,0 +1,45 @@
[
[
{
"name": "name",
"description": "",
"type": "string"
},
{
"name": "age",
"description": "",
"default": "4",
"type": "number"
}
],
[
{
"name": "name",
"description": "",
"type": "string"
},
{
"name": "age",
"description": "",
"default": "4",
"type": "number"
},
{
"name": "generic",
"description": "",
"type": "GenericType<string, string, string>"
}
],
[
{
"name": "name",
"description": "",
"type": "string"
},
{
"name": "url",
"description": "",
"type": "string"
}
]
]
+19
View File
@@ -0,0 +1,19 @@
export interface Test1 {
name: string;
/**
* @defaultValue 4
*/
age?: number;
}
export type Test2 = Test1 & {
generic: GenericType<string, string, string>;
};
export type { BreadcrumbItem as Test3 } from '@/breadcrumb';
interface GenericType<A, B, C> {
A: A;
B: B;
C: C;
}
+2 -1
View File
@@ -6,6 +6,7 @@ const sharedConfig = {
'algoliasearch',
'contentlayer',
'unified',
'typescript',
],
};
@@ -20,6 +21,6 @@ export default defineConfig({
'src/{search,search-algolia}/client.ts',
'src/search/shared.ts',
'src/{search,search-algolia}/server.ts',
'src/middleware.ts',
'src/{middleware,typescript}.ts',
],
});