vite-plugin-react-pages
Version:
<p> <a href="https://www.npmjs.com/package/vite-plugin-react-pages" target="_blank" rel="noopener"><img src="https://img.shields.io/npm/v/vite-plugin-react-pages.svg" alt="npm package" /></a> </p>
272 lines (249 loc) • 7.66 kB
text/typescript
import {
Project,
TypeElementMemberedNode,
Node,
TypeChecker,
ts,
} from 'ts-morph'
import type {
TsInfo,
TsPropertyOrMethodInfo,
CallSignatureInfo,
} from '../../../../clientTypes'
const defaultTsConfig: ts.CompilerOptions = {
target: ts.ScriptTarget.ESNext,
lib: ['lib.esnext.full.d.ts'],
moduleResolution: ts.ModuleResolutionKind.NodeJs,
}
export function collectInterfaceInfo(
fileName: string,
exportName: string,
options: ts.CompilerOptions = {}
): TsInfo {
const project = new Project({
compilerOptions: {
...defaultTsConfig,
...options,
},
})
const sourceFile = project.addSourceFileAtPath(fileName)
const typeChecker = project.getTypeChecker()
const exportedDeclarations = sourceFile
.getExportedDeclarations()
.get(exportName)
if (!exportedDeclarations) {
throw new Error(
`Can not find export. ${JSON.stringify({ exportName, fileName })}`
)
}
if (exportedDeclarations.length !== 1) {
throw new Error(
`Unexpected exportedDeclaration.length. ${JSON.stringify({
exportName,
fileName,
})}`
)
}
const node = exportedDeclarations[0]
if (Node.isTypeAliasDeclaration(node)) {
// type A = { k: v } (type literal)
// or type A = 'asd' | 123 (complex type)
const name = node.getName()
const description = node
.getJsDocs()
.map((jsDoc) => {
return jsDoc.getDescription().trim()
})
.join('\n\n')
const typeNode = node.getTypeNode()
if (Node.isTypeLiteral(typeNode)) {
// example: type A = { k: v }
const { members, callSignatures, constructSignatures } =
handleTypeElementMembered(typeNode, typeChecker)
return {
type: 'object-literal',
name,
description,
properties: members,
callSignatures,
constructSignatures,
}
} else {
// example: type A = 'asd' | 123
return {
type: 'other',
name,
description,
text:
typeNode?.getText({
includeJsDocComments: false,
trimLeadingIndentation: true,
}) || '',
}
}
}
if (Node.isInterfaceDeclaration(node)) {
const name = node.getName()
const description = node
.getJsDocs()
.map((jsDoc) => {
return jsDoc.getDescription().trim()
})
.join('\n\n')
const { members, callSignatures, constructSignatures } =
handleTypeElementMembered(node, typeChecker)
return {
type: 'interface',
name,
description,
properties: members,
callSignatures,
constructSignatures,
}
}
throw new Error('unexpected node type: ' + node.getKindName())
}
// handle Interface or TypeLiteral
// iterate members at type level
// which is higher than ast level, so that we can get inherited membered from a Interface
// https://github.com/dsherret/ts-morph/issues/457#issuecomment-427688926
function handleTypeElementMembered(
node: TypeElementMemberedNode & Node,
typeChecker: TypeChecker
): {
members: TsPropertyOrMethodInfo[]
callSignatures: CallSignatureInfo[]
constructSignatures: CallSignatureInfo[]
} {
const members: TsPropertyOrMethodInfo[] = []
// or use node.getSymbol()?.getMembers() ?
const nodeType = node.getType()
// https://stackoverflow.com/a/68623960
for (const prop of nodeType.getProperties()) {
const name = prop.getName()
const description = ts.displayPartsToString(
prop.compilerSymbol.getDocumentationComment(typeChecker.compilerObject)
)
const type = prop
.getTypeAtLocation(node)
// drop the `import('/path/to/file').` before the type text
// https://github.com/dsherret/ts-morph/issues/453#issuecomment-667578386
.getText(node, ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope)
const defaultValue = (() => {
let res = ''
prop.getJsDocTags().find((tag) => {
const match = ['defaultvalue', 'default'].includes(
tag.getName().toLowerCase()
)
if (match) {
res = ts.displayPartsToString(tag.getText())
return true
}
})
return res
})()
const optional = prop.isOptional()
members.push({
name,
description,
type,
defaultValue,
optional,
})
}
const callSignatures: CallSignatureInfo[] = []
for (const sig of nodeType.getCallSignatures()) {
const description = ts.displayPartsToString(
sig.compilerSignature.getDocumentationComment(typeChecker.compilerObject)
)
const type = sig.getDeclaration().getText()
callSignatures.push({
description,
type,
})
}
const constructSignatures: CallSignatureInfo[] = []
for (const sig of nodeType.getConstructSignatures()) {
const description = ts.displayPartsToString(
sig.compilerSignature.getDocumentationComment(typeChecker.compilerObject)
)
const type = sig.getDeclaration().getText()
constructSignatures.push({
description,
type,
})
}
return { members, callSignatures, constructSignatures }
}
// an alternative way to implement handleTypeElementMembered
// iterate members at ast level
// not used. just for backup...
function handleTypeElementMembered2(
node: TypeElementMemberedNode & Node,
typeChecker: TypeChecker
): TsPropertyOrMethodInfo[] {
const result: TsPropertyOrMethodInfo[] = []
node.getMembers().forEach((member) => {
if (Node.isPropertySignature(member) || Node.isMethodSignature(member)) {
const memberSymbol = member.getSymbolOrThrow()
const name = member.getName()
// or use this to get description?
// const description = ts.displayPartsToString(
// memberSymbol.compilerSymbol.getDocumentationComment(
// typeChecker.compilerObject
// )
// )
// My consideration: getJsDocs is newer than getDocumentationComment
// and ts-morph docs recommend using getJsDocs:
// https://github.com/dsherret/ts-morph/blob/cea07aa7759ecf5a1e9f90b628334b8bd617c624/docs/details/documentation.md#L59
const description = member
.getJsDocs()
.map((jsDoc) => {
return jsDoc.getDescription().trim()
})
.join('\n\n')
const type = member.getType().getText()
const defaultValue = (() => {
let res = ''
member.getJsDocs().find((jsDoc) =>
jsDoc.getTags().find((tag) => {
const match = ['defaultvalue', 'default'].includes(
tag.getTagName().toLowerCase()
)
if (match) {
res = tag.getCommentText() || ''
return true
}
})
)
return res
})()
// or use member.hasQuestionToken()
const optional = memberSymbol.isOptional()
result.push({
name,
description,
type,
defaultValue,
optional,
})
}
})
return result
}
/**
* ref:
*
* https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API
*
* https://stackoverflow.com/questions/59838013/how-can-i-use-the-ts-compiler-api-to-find-where-a-variable-was-defined-in-anothe
*
* https://stackoverflow.com/questions/60249275/typescript-compiler-api-generate-the-full-properties-arborescence-of-a-type-ide
*
* https://stackoverflow.com/questions/47429792/is-it-possible-to-get-comments-as-nodes-in-the-ast-using-the-typescript-compiler
*
* Instructions of learning ts compiler:
* https://stackoverflow.com/a/58885450
*
* https://learning-notes.mistermicheels.com/javascript/typescript/compiler-api/
*/