|
| 1 | +// tslint:disable no-console |
| 2 | +import * as tsc from 'typescript'; |
| 3 | + |
| 4 | +interface TypeInfo { |
| 5 | + typeName: string; |
| 6 | + parameterDocs: Array<{ name: string, text: string }>; |
| 7 | + returnDocs?: string; |
| 8 | + example?: string; |
| 9 | + description: string; |
| 10 | + fileName: string; |
| 11 | +} |
| 12 | + |
| 13 | +const tsConfig = { |
| 14 | + baseUrl: "src/", |
| 15 | +}; |
| 16 | + |
| 17 | +const files = [ |
| 18 | + { path: 'src/index.ts' }, |
| 19 | +]; |
| 20 | + |
| 21 | +const classIsExported = (node: tsc.Node): node is tsc.ClassDeclaration => { |
| 22 | + return !!node.parent && isNodeExported(node.parent) && tsc.isClassDeclaration(node.parent); |
| 23 | +}; |
| 24 | + |
| 25 | +const buildTypeInfoFromNode = (fileName: string, checker: tsc.TypeChecker, node: tsc.Node, parentName?: string): TypeInfo | undefined => { |
| 26 | + if (!isNodeExported(node) && !classIsExported(node)) return; // we only need to document exported types |
| 27 | + if (!(tsc.isTypeAliasDeclaration(node) || tsc.isFunctionDeclaration(node) || tsc.isClassDeclaration(node) || tsc.isMethodDeclaration(node))) return; // we only need to document types, functions, and classes |
| 28 | + |
| 29 | + const symbol = checker.getSymbolAtLocation(node.name!); |
| 30 | + if (!symbol) return; // we should never get into this state because we aren't dealing with .d.ts files |
| 31 | + |
| 32 | + // Get the JSDoc description |
| 33 | + const description = tsc.displayPartsToString(symbol.getDocumentationComment(checker)); |
| 34 | + |
| 35 | + // Don't document things with `no-doc` at start of description |
| 36 | + if (description.trim().startsWith('no-doc')) return; |
| 37 | + |
| 38 | + const typeName = parentName ? `${parentName}.${symbol.name}` : symbol.name; |
| 39 | + |
| 40 | + const jsDocTags = symbol.getJsDocTags(); |
| 41 | + |
| 42 | + const parameterDocs = jsDocTags |
| 43 | + .filter(tag => tag.name === 'param') |
| 44 | + .map(tag => tag.text) |
| 45 | + .filter(text => !!text) |
| 46 | + .map(text => { |
| 47 | + const [ name, ...docWords] = text!.split(' '); |
| 48 | + return { |
| 49 | + name, |
| 50 | + text: docWords.join(' '), |
| 51 | + }; |
| 52 | + }); |
| 53 | + |
| 54 | + const returnDocs = jsDocTags |
| 55 | + .filter(tag => tag.name === 'returns') |
| 56 | + .map(tag => tag.text) |
| 57 | + .filter(text => !!text); |
| 58 | + |
| 59 | + const example = jsDocTags |
| 60 | + .filter(tag => tag.name === 'example') |
| 61 | + .map(tag => tag.text) |
| 62 | + .filter(text => !!text)[0]; |
| 63 | + |
| 64 | + const typeInfo: TypeInfo = { |
| 65 | + typeName, |
| 66 | + parameterDocs, |
| 67 | + returnDocs: returnDocs[0], |
| 68 | + example, |
| 69 | + description, |
| 70 | + fileName, |
| 71 | + }; |
| 72 | + |
| 73 | + return typeInfo; |
| 74 | +}; |
| 75 | + |
| 76 | +const generateMarkdown = (fileName: string) => { |
| 77 | + const program = tsc.createProgram([fileName], tsConfig); |
| 78 | + const checker = program.getTypeChecker(); |
| 79 | + |
| 80 | + for (const sourceFile of program.getSourceFiles()) { |
| 81 | + if (sourceFile.fileName !== fileName) continue; // we don't care about imported source files, only the single file we are inspecting |
| 82 | + |
| 83 | + const sourceDocs = [] as TypeInfo[]; |
| 84 | + |
| 85 | + tsc.forEachChild(sourceFile, (node) => { |
| 86 | + const typeInfo = buildTypeInfoFromNode(fileName, checker, node); |
| 87 | + |
| 88 | + if (typeInfo) sourceDocs.push(typeInfo); |
| 89 | + |
| 90 | + if (tsc.isClassDeclaration(node) && typeInfo) { |
| 91 | + // iterate over class members |
| 92 | + node.members |
| 93 | + .map((node) => buildTypeInfoFromNode(fileName, checker, node, typeInfo.typeName)) |
| 94 | + .forEach(typeInfo => typeInfo && sourceDocs.push(typeInfo)); |
| 95 | + |
| 96 | + } |
| 97 | + }); |
| 98 | + |
| 99 | + // drop any repeated definitions (for overloaded functions) |
| 100 | + const filteredSourceDocs = sourceDocs |
| 101 | + .reduce((coll, item) => { |
| 102 | + const exists = coll.find(s => s.typeName === item.typeName); |
| 103 | + if (exists) return coll; |
| 104 | + |
| 105 | + return [...coll, item]; |
| 106 | + }, [] as TypeInfo[]) |
| 107 | + .sort((a, b) => a.typeName.localeCompare(b.typeName)); |
| 108 | + |
| 109 | + const renderedSections = filteredSourceDocs |
| 110 | + .map(typeInfo => { |
| 111 | + const header = `### ${typeInfo.typeName}`; |
| 112 | + const description = `${typeInfo.description}`; |
| 113 | + |
| 114 | + const parameters = typeInfo.parameterDocs.map(param => { |
| 115 | + return `| ${param.name} | ${param.text} |`; |
| 116 | + }).join('\n'); |
| 117 | + |
| 118 | + const table = typeInfo.parameterDocs.length > 0 ? |
| 119 | + `\n| Param | Description |\n| --- | --- |\n${parameters}` |
| 120 | + : ''; |
| 121 | + |
| 122 | + const example = typeInfo.example |
| 123 | + ? `#### Example\n ${typeInfo.example}` |
| 124 | + : ''; |
| 125 | + |
| 126 | + const parts = [ |
| 127 | + header, |
| 128 | + description, |
| 129 | + table, |
| 130 | + example, |
| 131 | + ].filter(part => !!part); |
| 132 | + |
| 133 | + return parts.join('\n'); |
| 134 | + }); |
| 135 | + |
| 136 | + const markdown = renderedSections.join('\n\n'); |
| 137 | + |
| 138 | + return { |
| 139 | + markdown, |
| 140 | + }; |
| 141 | + } |
| 142 | +}; |
| 143 | + |
| 144 | +const headerString = |
| 145 | +`# validtyped |
| 146 | +[](https://travis-ci.org/andnp/ValidTyped) |
| 147 | +
|
| 148 | +A runtime and compile-time type checker library. |
| 149 | +
|
| 150 | +--- |
| 151 | +
|
| 152 | +
|
| 153 | +
|
| 154 | +`; |
| 155 | + |
| 156 | +const docString = files.map(file => { |
| 157 | + const doc = generateMarkdown(file.path); |
| 158 | + |
| 159 | + if (!doc) return console.log(file); |
| 160 | + |
| 161 | + return `${doc.markdown}\n`; |
| 162 | +}).join('\n'); |
| 163 | + |
| 164 | +const markdown = headerString + docString; |
| 165 | +console.log(markdown); |
| 166 | + |
| 167 | + |
| 168 | +/** True if this is visible outside this file, false otherwise */ |
| 169 | +function isNodeExported(node: tsc.Node): boolean { |
| 170 | + return (tsc.getCombinedModifierFlags(node) & tsc.ModifierFlags.Export) !== 0 || (!!node.parent && node.parent.kind === tsc.SyntaxKind.SourceFile); // tslint:disable-line no-bitwise |
| 171 | +} |
0 commit comments