Skip to content

Commit

Permalink
Improve TsInfo support
Browse files Browse the repository at this point in the history
Now expands type aliases and interfaces with literal only types.
Also allows to expand inherited props.

Example:

```mdx
<TsInfo src='./Button.tsx' name='ButtonProps'` />
<TsInfo src='./Button.tsx' name='ButtonProps:*'` />
<TsInfo src='./Button.tsx' name='ButtonProps:AsProp'` />
<TsInfo src='./Button.tsx' name='ButtonProps:AsProp:BaseProps'` />
```

Fixes vitejs#33
  • Loading branch information
cristatus committed Jun 23, 2021
1 parent 8018ab0 commit a2c79ae
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ demo1:

ButtonProps:
<TsInfo src="./types.ts" name="ButtonProps" />

ButtonProps with all inherited props:
<TsInfo src="./types.ts" name="ButtonProps:*" />

ButtonProps with only specific inherited props:
<TsInfo src="./types.ts" name="ButtonProps:AsProp" />
25 changes: 23 additions & 2 deletions packages/playground/use-theme-doc/pages/components/button/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
type Variant = 'primary' | 'default' | 'text'

type Dimension = {
w?: number
h?: number
}

interface Location {
x?: number
y?: number
}

interface AsProp {
as?: string
}

interface BaseProps {
xy?: Location
wh?: Dimension
}

/**
* This is the description of the Button component's props
*/
export interface ButtonProps {
export interface ButtonProps extends BaseProps, AsProp {
/**
* the type of button
* @defaultValue 'default'
*/
type?: 'primary' | 'default' | 'text'
type?: Variant | 'link'
/**
* the size of button
* @defaultValue 'middle'
Expand Down
210 changes: 152 additions & 58 deletions packages/react-pages/src/node/ts-info-module/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,41 @@ export function collectInterfaceInfo(

const sourceFile = program.getSourceFile(fileName)!

const [interfaceName, ...heritageNames] = exportName.split(':')

// inspired by
// https://github.com/microsoft/rushstack/blob/6ca0cba723ad8428e6e099f12715ce799f29a73f/apps/api-extractor/src/analyzer/ExportAnalyzer.ts#L702
// and https://stackoverflow.com/a/58885450
const fileSymbol = checker.getSymbolAtLocation(sourceFile)
if (!fileSymbol || !fileSymbol.exports) {
throw new Error(`unexpected fileSymbol`)
}
const escapedExportName = ts.escapeLeadingUnderscores(exportName)
const escapedExportName = ts.escapeLeadingUnderscores(interfaceName)
const exportSymbol = fileSymbol.exports.get(escapedExportName)
if (!exportSymbol) {
throw new Error(`Named export ${exportName} is not found in file`)
throw new Error(`Named export ${interfaceName} is not found in file`)
}
const sourceDeclareSymbol = getAliasedSymbolIfNecessary(exportSymbol)
const sourceDeclare = sourceDeclareSymbol.declarations?.[0]
if (!sourceDeclare) {
throw new Error(`Can't find sourceDeclare for ${exportName}`)
if (!sourceDeclare || !ts.isInterfaceDeclaration(sourceDeclare)) {
throw new Error(`Can't find sourceDeclare for ${interfaceName}`)
}

const interfaceInfo = collectInterfaceInfo(sourceDeclare, sourceDeclareSymbol)

if (heritageNames && heritageNames.length > 0) {
const heritage = [...findHeritage(sourceDeclare)]
const heritageToInclude =
heritageNames[0] === '*'
? heritage
: heritage.filter((x) => heritageNames.includes(x.name.text))

heritageToInclude.forEach((decl) => {
const { properties } = collectInterfaceInfo(decl)
interfaceInfo.properties = [...interfaceInfo.properties, ...properties]
})
}

return interfaceInfo

function getAliasedSymbolIfNecessary(symbol: ts.Symbol) {
Expand All @@ -44,74 +61,151 @@ export function collectInterfaceInfo(
return symbol
}

function collectInterfaceInfo(node: ts.Declaration, symbol: ts.Symbol) {
if (!ts.isInterfaceDeclaration(node))
throw new Error(`target is not an InterfaceDeclaration`)
function collectInterfaceInfo(
node: ts.InterfaceDeclaration,
symbol?: ts.Symbol
): TsInterfaceInfo {
const propertiesInfo: TsInterfacePropertyInfo[] = []

const type = checker.getTypeAtLocation(node)
if (!symbol) throw new Error(`can't find symbol`)
for (const member of node.members) {
if (ts.isPropertySignature(member) || ts.isMethodSignature(member)) {
const name = member.name.getText()
const type = member.type ? typeInfo(member.type) : ''
const symbol = checker.getSymbolAtLocation(member.name)

const name = node.name.getText()
const commentText =
getComment(node, node.getSourceFile().getFullText()) ?? ''
const description = ts.displayPartsToString(
symbol.getDocumentationComment(checker)
)
if (symbol) {
const commentText =
getComment(member, member.getSourceFile().getFullText()) ?? ''
const description = ts.displayPartsToString(
symbol.getDocumentationComment(checker)
)
const optional = !!(symbol.getFlags() & ts.SymbolFlags.Optional)

const propertiesInfo: TsInterfacePropertyInfo[] = []
// get defaultValue from jsDocTags
const jsDocTags = symbol.getJsDocTags()
const defaultValueTag = jsDocTags.find(
(t) => t.name === 'defaultValue' || 'default'
)
const defaultValue = defaultValueTag?.text?.[0].text

// extract property info
symbol.members?.forEach((symbol) => {
const name = symbol.name
const declaration = symbol.valueDeclaration
if (
!(
declaration &&
(ts.isPropertySignature(declaration) ||
ts.isMethodSignature(declaration))
)
) {
throw new Error(
`unexpected declaration type in interface. name: ${name}, kind: ${
ts.SyntaxKind[declaration?.kind as any]
}`
)
propertiesInfo.push({
name: name,
// commentText,
type,
description,
defaultValue,
optional,
// fullText: member.getFullText(),
})
}
}
const commentText =
getComment(declaration, declaration.getSourceFile().getFullText()) ?? ''
const typeText = declaration.type?.getFullText() ?? ''
const description = ts.displayPartsToString(
symbol.getDocumentationComment(checker)
)
}

const isOptional = !!(symbol.getFlags() & ts.SymbolFlags.Optional)
// get defaultValue from jsDocTags
const jsDocTags = symbol.getJsDocTags()
const defaultValueTag = jsDocTags.find(
(t) => t.name === 'defaultValue' || 'default'
)
const defaultValue = defaultValueTag?.text?.[0].text

propertiesInfo.push({
name,
// commentText,
type: typeText,
description,
defaultValue,
optional: isOptional,
// fullText: declaration.getFullText(),
})
})
const name = node.name.getText()
const commentText =
getComment(node, node.getSourceFile().getFullText()) ?? ''
const description = symbol
? ts.displayPartsToString(symbol.getDocumentationComment(checker))
: ''

const interfaceInfo: TsInterfaceInfo = {
return {
name,
// commentText,
description,
properties: propertiesInfo,
// fullText: node.getFullText(),
}
}

function findHeritage(
node: ts.InterfaceDeclaration
): Set<ts.InterfaceDeclaration> {
const heritage = new Set<ts.InterfaceDeclaration>()
const heritageTypes = node.heritageClauses?.[0]?.types ?? []

for (const p of heritageTypes) {
if (ts.isExpressionWithTypeArguments(p)) {
var e = p.expression
if (ts.isIdentifier(e)) {
const t = checker.getTypeAtLocation(e)
const d = t.symbol.declarations?.[0]
if (d && ts.isInterfaceDeclaration(d)) {
heritage.add(d)
findHeritage(d).forEach((x) => heritage.add(d))
}
}
}
}

return heritage
}

function typeInfo(node: ts.TypeNode): string {
if (isLiteralsOnly(node)) {
if (ts.isLiteralTypeNode(node)) return node.getText()
if (ts.isTypeLiteralNode(node)) return objectString(node.members)
if (ts.isUnionTypeNode(node)) return node.types.map(typeInfo).join(' | ')
if (ts.isIntersectionTypeNode(node)) {
return node.types.map(typeInfo).join(' & ')
}
if (ts.isTypeReferenceNode(node)) {
var t = checker.getTypeAtLocation(node)
var s = t.aliasSymbol || t.symbol
var d = s?.declarations?.[0]
if (d) {
if (ts.isTypeAliasDeclaration(d)) return typeInfo(d.type)
if (ts.isInterfaceDeclaration(d)) return objectString(d.members)
}
}
}
return node.getFullText()
}

function isKeyword(kind: ts.SyntaxKind): boolean {
return (
ts.SyntaxKind.FirstKeyword <= kind && kind <= ts.SyntaxKind.LastKeyword
)
}

function isLiteralsOnly(node: ts.TypeNode): boolean {
if (isKeyword(node.kind)) return true
if (ts.isLiteralTypeNode(node)) return true
if (ts.isTypeLiteralNode(node)) {
return node.members.every(
(m) => ts.isPropertySignature(m) && m.type && isLiteralsOnly(m.type)
)
}
if (ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node)) {
return node.types.every(isLiteralsOnly)
}
if (ts.isTypeReferenceNode(node)) {
var t = checker.getTypeAtLocation(node)
var s = t.aliasSymbol || t.symbol
var d = s?.declarations?.[0]
if (d && ts.isTypeAliasDeclaration(d)) {
return isLiteralsOnly(d.type)
}
if (d && ts.isInterfaceDeclaration(d)) {
return d.members.every(
(m) => ts.isPropertySignature(m) && m.type && isLiteralsOnly(m.type)
)
}
}
return false
}

function objectString(members: ts.NodeArray<ts.TypeElement>): string {
const body = members
.filter((m) => ts.isPropertySignature(m))
.map((m) => {
const p = m as ts.PropertySignature
const n = p.name.getText()
const t = p.type && typeInfo(p.type)
return `${n}: ${t}`
})
.join(', ')

return interfaceInfo
return `{ ${body} }`
}

/** True if this is visible outside this file, false otherwise */
Expand Down

0 comments on commit a2c79ae

Please sign in to comment.