vite-plugin-react18-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>
170 lines (150 loc) • 5.66 kB
text/typescript
import * as ts from 'typescript'
import {
TsInterfaceInfo,
TsInterfacePropertyInfo,
} from '../../../../clientTypes'
const defaultTsConfig: ts.CompilerOptions = {
moduleResolution: ts.ModuleResolutionKind.NodeJs,
}
export function collectInterfaceInfo(
fileName: string,
exportName: string,
options: ts.CompilerOptions = defaultTsConfig
): TsInterfaceInfo {
// Build a program using the set of root file names in fileNames
let program = ts.createProgram([fileName], options)
// Get the checker, we will use it to find more about classes
let checker = program.getTypeChecker()
const sourceFile = program.getSourceFile(fileName)!
// 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 exportSymbol = fileSymbol.exports.get(escapedExportName)
if (!exportSymbol) {
throw new Error(`Named export ${exportName} 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}`)
}
const interfaceInfo = collectInterfaceInfo(sourceDeclare, sourceDeclareSymbol)
return interfaceInfo
function getAliasedSymbolIfNecessary(symbol: ts.Symbol) {
if ((symbol.flags & ts.SymbolFlags.Alias) !== 0)
return checker.getAliasedSymbol(symbol)
return symbol
}
function collectInterfaceInfo(node: ts.Declaration, symbol: ts.Symbol) {
if (!ts.isInterfaceDeclaration(node))
throw new Error(`target is not an InterfaceDeclaration`)
const type = checker.getTypeAtLocation(node)
if (!symbol) throw new Error(`can't find symbol`)
const name = node.name.getText()
const commentText =
getComment(node, node.getSourceFile().getFullText()) ?? ''
const description = ts.displayPartsToString(
symbol.getDocumentationComment(checker)
)
const propertiesInfo: TsInterfacePropertyInfo[] = []
// extract property info
symbol.members?.forEach((symbol) => {
const name = symbol.name
const declaration = symbol.valueDeclaration
if (
!(
declaration &&
(ts.isPropertySignature(declaration) ||
ts.isMethodSignature(declaration))
)
) {
// known member in interface symbol
// that we don't handle
if (symbol.getFlags() & ts.SymbolFlags.TypeParameter) return
console.warn(
`unexpected declaration type in interface. name: ${name}, kind: ${
ts.SyntaxKind[declaration?.kind as any]
}`
)
return
}
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 interfaceInfo: TsInterfaceInfo = {
name,
// commentText,
description,
properties: propertiesInfo,
// fullText: node.getFullText(),
}
return interfaceInfo
}
/** True if this is visible outside this file, false otherwise */
function isNodeExported(node: ts.Node): boolean {
return (
(ts.getCombinedModifierFlags(node as ts.Declaration) &
ts.ModifierFlags.Export) !==
0 &&
!!node.parent &&
node.parent.kind === ts.SyntaxKind.SourceFile
)
}
}
function getJSDocCommentRanges(
node: ts.Node,
text: string
): ts.CommentRange[] | undefined {
// Compiler internal:
// https://github.com/microsoft/TypeScript/blob/66ecfcbd04b8234855a673adb85e5cff3f8458d4/src/compiler/utilities.ts#L1202
return (ts as any).getJSDocCommentRanges.apply(ts, arguments)
}
function getComment(declaration: ts.Declaration, sourceFileFullText: string) {
const ranges = getJSDocCommentRanges(declaration, sourceFileFullText)
if (!ranges || !ranges.length) return
const range = ranges[ranges.length - 1]
if (!range) return
const text = sourceFileFullText.slice(range.pos, range.end)
return text
}
/**
* 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/
*/