├── src ├── dependencies.ts ├── type │ ├── context.ts │ ├── members.ts │ ├── declaration.ts │ ├── title.ts │ ├── utils.ts │ └── index.ts ├── description.ts ├── context.ts ├── examples.ts ├── module.ts ├── variable.ts ├── markdown.ts ├── additionalLinks.ts ├── enumeration.ts ├── typeDeclaration.ts ├── index.ts ├── class.ts ├── cli.ts ├── function.ts ├── symbol.ts └── utils.ts ├── .gitbook.yaml ├── .prettierrc ├── .husky └── pre-commit ├── .vscode ├── settings.json └── launch.json ├── tsconfig.build.json ├── .gitignore ├── .babelrc ├── tsconfig.json ├── live ├── tsconfig.json ├── index.html ├── package.json └── src │ └── index.ts ├── .editorconfig ├── .nycrc.json ├── .eslintrc ├── .github └── workflows │ ├── build.yml │ └── update-dependencies.yml ├── LICENSE ├── test ├── core.test.ts ├── utils.ts ├── variable.test.ts ├── enumeration.test.ts ├── typeDeclaration.test.ts ├── symbol.test.ts ├── cli.test.ts ├── function.test.ts ├── class.test.ts └── type.test.ts ├── package.json └── README.md /src/dependencies.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./example-output/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | npx pretty-quick --staged 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["test"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | .nyc_output/ 4 | coverage/ 5 | .test-temp/ 6 | output.md 7 | .cache/ 8 | dist/ 9 | -------------------------------------------------------------------------------- /src/type/context.ts: -------------------------------------------------------------------------------- 1 | export type TypeContext = { 2 | isArray?: boolean; 3 | name?: string; 4 | description?: string; 5 | nestingLevel?: number; 6 | }; 7 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "10" 8 | } 9 | } 10 | ], 11 | "@babel/preset-typescript" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["esnext"], 4 | "esModuleInterop": true, 5 | "strict": true, 6 | "skipLibCheck": true, 7 | "module": "commonjs" 8 | }, 9 | "include": ["src", "test"] 10 | } 11 | -------------------------------------------------------------------------------- /live/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "sourceMap": true, 6 | "lib": ["esnext", "dom"], 7 | "rootDir": "src", 8 | "strict": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | # http://editorconfig.org 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | max_line_length = 80 12 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /live/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | typescript-documentation 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/description.ts: -------------------------------------------------------------------------------- 1 | import { SymbolDisplayPart } from 'typescript'; 2 | import { joinLines } from './markdown'; 3 | 4 | export function renderDescription(comments: SymbolDisplayPart[]): string { 5 | return joinLines(comments.map(comment => comment.text)); 6 | } 7 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": ["lcov", "text", "text-summary"], 3 | "cache": false, 4 | "extension": [".ts"], 5 | "check-coverage": true, 6 | "include": ["src/**"], 7 | "exclude": ["src/utils.ts"], 8 | "statements": 100, 9 | "branches": 100, 10 | "functions": 100, 11 | "lines": 100 12 | } 13 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import { Symbol, TypeChecker } from 'typescript'; 2 | 3 | export type RenderContext = { 4 | typeChecker: TypeChecker; 5 | exportedSymbols: Symbol[]; 6 | section: string; 7 | getSectionLocation: (section: string) => string; 8 | }; 9 | 10 | export type DependencyContext = { 11 | typeChecker: TypeChecker; 12 | exportedSymbols: Symbol[]; 13 | resolutionPath: Symbol[]; 14 | }; 15 | -------------------------------------------------------------------------------- /live/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel-sandbox", 3 | "version": "1.0.0", 4 | "description": "Simple Parcel Sandbox", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "parcel index.html --open", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "marked": "^0.7.0", 12 | "typescript": "3.7.2" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.7.4", 16 | "@types/marked": "^0.7.2", 17 | "parcel-bundler": "^1.6.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /live/src/index.ts: -------------------------------------------------------------------------------- 1 | import marked from 'marked'; 2 | 3 | const entryFile = 'variables.ts'; 4 | const sourceCode: { [name: string]: string } = { 5 | 'variables.ts': ` 6 | import { B } from "./b"; 7 | 8 | export const a = 1; 9 | export const b: B = 1; 10 | `, 11 | 'b.ts': ` 12 | export type B = 1; 13 | ` 14 | }; 15 | 16 | const contentNode = document.getElementById('content'); 17 | 18 | if (contentNode) { 19 | contentNode.innerHTML = marked('# Marked in the browser\n\nRendered by **marked**.'); 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 11 | "prettier", 12 | "plugin:mocha/recommended" 13 | ], 14 | "plugins": ["@typescript-eslint", "mocha"], 15 | "rules": { 16 | "mocha/no-mocha-arrows": "off", 17 | "@typescript-eslint/ban-types": "off" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/examples.ts: -------------------------------------------------------------------------------- 1 | import { JSDocTagInfo } from 'typescript'; 2 | import { subSection, code, joinSections } from './markdown'; 3 | import { getSymbolDisplayText } from './utils'; 4 | 5 | export function renderExamples(tags: JSDocTagInfo[]): string { 6 | const examples = tags.filter((tag) => tag.name === 'example'); 7 | 8 | if (!examples.length) { 9 | return ''; 10 | } 11 | 12 | return joinSections([ 13 | subSection('Examples'), 14 | joinSections( 15 | examples.map((example: JSDocTagInfo) => 16 | code(getSymbolDisplayText(example).trim()) 17 | ) 18 | ), 19 | ]); 20 | } 21 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { Symbol } from 'typescript'; 2 | import { DependencyContext } from './context'; 3 | import { getSymbolDependencies } from './symbol'; 4 | 5 | export function getModuleDependencies( 6 | symbol: Symbol, 7 | context: DependencyContext 8 | ): Symbol[] { 9 | const moduleExports: Symbol[] = []; 10 | 11 | /* istanbul ignore else */ 12 | if (symbol.exports) { 13 | symbol.exports.forEach(exportedSymbol => { 14 | if (context.exportedSymbols.includes(exportedSymbol)) { 15 | moduleExports.push(exportedSymbol); 16 | } 17 | }); 18 | } 19 | 20 | return moduleExports.reduce( 21 | (dependencies, child) => [ 22 | ...dependencies, 23 | ...getSymbolDependencies(child, context) 24 | ], 25 | moduleExports 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - uses: actions/setup-node@v1 17 | with: 18 | node-version: '14.x' 19 | - name: Install dependencies 20 | run: npm ci 21 | - name: Check code style 22 | run: npm run lint 23 | - name: Check types 24 | run: npm run typecheck 25 | - name: Run tests 26 | run: npm run test:coverage 27 | - name: Coveralls 28 | uses: coverallsapp/github-action@master 29 | with: 30 | github-token: ${{ secrets.GITHUB_TOKEN }} 31 | env: 32 | CI: true 33 | -------------------------------------------------------------------------------- /src/variable.ts: -------------------------------------------------------------------------------- 1 | import { Symbol, Type } from 'typescript'; 2 | import { renderAdditionalLinks } from './additionalLinks'; 3 | import { RenderContext } from './context'; 4 | import { renderDescription } from './description'; 5 | import { renderExamples } from './examples'; 6 | import { heading, joinSections, subSection } from './markdown'; 7 | import { renderType } from './type'; 8 | 9 | export function renderVariable( 10 | symbol: Symbol, 11 | aliasedSymbol: Symbol, 12 | type: Type, 13 | context: RenderContext 14 | ): string { 15 | return joinSections([ 16 | heading(symbol.getName(), 2), 17 | renderDescription( 18 | aliasedSymbol.getDocumentationComment(context.typeChecker) 19 | ), 20 | subSection('Type'), 21 | renderType(type, context), 22 | renderExamples(aliasedSymbol.getJsDocTags()), 23 | renderAdditionalLinks(aliasedSymbol.getJsDocTags()), 24 | ]); 25 | } 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "test", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/ts-mocha", 9 | "runtimeArgs": ["test/**/*.test.ts"], 10 | "cwd": "${workspaceRoot}", 11 | "protocol": "inspector" 12 | }, 13 | { 14 | "name": "cli", 15 | "type": "node", 16 | "request": "launch", 17 | "args": [ 18 | "${workspaceRoot}/src/cli.ts", 19 | "--project", 20 | "C:/Users/mucsi/w3c-webdriver/packages/w3c-webdriver/tsconfig.json", 21 | "--entry", 22 | "C:/Users/mucsi/w3c-webdriver/packages/w3c-webdriver/src/index.ts", 23 | "--output", 24 | "example-output/README.md" 25 | ], 26 | "runtimeArgs": ["-r", "ts-node/register"], 27 | "cwd": "${workspaceRoot}", 28 | "protocol": "inspector", 29 | "internalConsoleOptions": "openOnSessionStart", 30 | "env": { 31 | "TS_NODE_IGNORE": "false" 32 | } 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/markdown.ts: -------------------------------------------------------------------------------- 1 | function wrap(content: string, prefix: string, suffix = prefix): string { 2 | return content && [prefix, content, suffix].join(''); 3 | } 4 | 5 | export function joinLines(lines: string[]): string { 6 | return lines.filter(Boolean).join('\n'); 7 | } 8 | 9 | export function joinSections(sections: string[]): string { 10 | return sections.filter(Boolean).join('\n\n'); 11 | } 12 | 13 | export function code(content: string): string { 14 | return content && joinLines(['```typescript', content, '```']); 15 | } 16 | 17 | export function inlineCode(content: string): string { 18 | return wrap(content, '`', '`'); 19 | } 20 | 21 | export function subSection(name: string): string { 22 | return wrap(name.toUpperCase(), '**'); 23 | } 24 | 25 | export function heading(name: string, level: number): string { 26 | return ['#'.repeat(level), name].join(' '); 27 | } 28 | 29 | export function listItem(text: string, level = 1): string { 30 | return [' '.repeat(level - 1), '- ', text].join(''); 31 | } 32 | 33 | export function link(text: string, link: string): string { 34 | return `[${text}](${link})`; 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Igor Muchychka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/additionalLinks.ts: -------------------------------------------------------------------------------- 1 | import { JSDocTagInfo } from 'typescript'; 2 | import { 3 | subSection, 4 | listItem, 5 | link, 6 | joinLines, 7 | joinSections, 8 | } from './markdown'; 9 | import { getSymbolDisplayText } from './utils'; 10 | 11 | function isLink( 12 | value?: RegExpExecArray | null | undefined 13 | ): value is RegExpExecArray { 14 | return !!value; 15 | } 16 | 17 | function getAdditionalLinks( 18 | tags: JSDocTagInfo[] 19 | ): { href: string; text: string }[] { 20 | return tags 21 | .filter((tag) => tag.name === 'see') 22 | .map((tag) => /{@link (.*?)\|(.*?)}/.exec(getSymbolDisplayText(tag))) 23 | .filter(isLink) 24 | .map(([, href, text]) => ({ href, text })); 25 | } 26 | 27 | export function renderAdditionalLinks(tags: JSDocTagInfo[]): string { 28 | const additionalLinks = getAdditionalLinks(tags); 29 | 30 | if (!additionalLinks.length) { 31 | return ''; 32 | } 33 | 34 | return joinSections([ 35 | subSection('See also'), 36 | joinLines( 37 | additionalLinks.map(({ href, text }) => 38 | listItem(link(text, href.replace(/ :\/\//g, '://'))) 39 | ) 40 | ), 41 | ]); 42 | } 43 | -------------------------------------------------------------------------------- /src/enumeration.ts: -------------------------------------------------------------------------------- 1 | import { Symbol, UnionType } from 'typescript'; 2 | import { renderAdditionalLinks } from './additionalLinks'; 3 | import { RenderContext } from './context'; 4 | import { renderDescription } from './description'; 5 | import { renderExamples } from './examples'; 6 | import { 7 | heading, 8 | joinLines, 9 | joinSections, 10 | listItem, 11 | subSection, 12 | } from './markdown'; 13 | import { renderType } from './type'; 14 | 15 | function renderEnumerationItems( 16 | type: UnionType, 17 | context: RenderContext 18 | ): string { 19 | return joinSections([ 20 | subSection('Possible values'), 21 | joinLines(type.types.map((type) => listItem(renderType(type, context)))), 22 | ]); 23 | } 24 | 25 | export function renderEnumeration( 26 | symbol: Symbol, 27 | aliasedSymbol: Symbol, 28 | type: UnionType, 29 | context: RenderContext 30 | ): string { 31 | return joinSections([ 32 | heading(symbol.getName(), 2), 33 | renderDescription( 34 | aliasedSymbol.getDocumentationComment(context.typeChecker) 35 | ), 36 | renderEnumerationItems(type, context), 37 | renderExamples(aliasedSymbol.getJsDocTags()), 38 | renderAdditionalLinks(aliasedSymbol.getJsDocTags()), 39 | ]); 40 | } 41 | -------------------------------------------------------------------------------- /test/core.test.ts: -------------------------------------------------------------------------------- 1 | import { testDocumentation } from './utils'; 2 | 3 | describe('core', () => { 4 | it('supports named re-exports', () => { 5 | testDocumentation({ 6 | 'a.ts': ` 7 | /** 8 | * Simple variable description 9 | */ 10 | export const simpleVariable: number = 1; 11 | `, 12 | 'index.ts': ` 13 | export { simpleVariable } from './a'; 14 | `, 15 | markdown: ` 16 | ## simpleVariable 17 | 18 | Simple variable description 19 | 20 | **TYPE** 21 | 22 | number 23 | `, 24 | }); 25 | }); 26 | 27 | it('supports re-named re-exports', () => { 28 | testDocumentation({ 29 | 'a.ts': ` 30 | /** 31 | * Simple variable description 32 | */ 33 | export const simpleVariable: number = 1; 34 | `, 35 | 'index.ts': ` 36 | export { simpleVariable as simpleVariable2 } from './a'; 37 | `, 38 | markdown: ` 39 | ## simpleVariable2 40 | 41 | Simple variable description 42 | 43 | **TYPE** 44 | 45 | number 46 | `, 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/typeDeclaration.ts: -------------------------------------------------------------------------------- 1 | import { Symbol, Type, TypeFlags } from 'typescript'; 2 | import { renderAdditionalLinks } from './additionalLinks'; 3 | import { RenderContext } from './context'; 4 | import { renderDescription } from './description'; 5 | import { renderExamples } from './examples'; 6 | import { heading, joinSections, subSection } from './markdown'; 7 | import { renderTypeMembers } from './type/members'; 8 | 9 | function renderContentTitle(type: Type): string { 10 | const flags = type.getFlags(); 11 | 12 | if (flags & TypeFlags.Object && type.getProperties().length) { 13 | return subSection('Properties'); 14 | } 15 | 16 | if (type.isUnion()) { 17 | return subSection('Possible values'); 18 | } 19 | 20 | return ''; 21 | } 22 | 23 | export function renderTypeDeclaration( 24 | symbol: Symbol, 25 | aliasedSymbol: Symbol, 26 | type: Type, 27 | context: RenderContext 28 | ): string { 29 | return joinSections([ 30 | heading(symbol.getName(), 2), 31 | renderDescription( 32 | aliasedSymbol.getDocumentationComment(context.typeChecker) 33 | ), 34 | renderContentTitle(type), 35 | renderTypeMembers(type, context), 36 | renderExamples(aliasedSymbol.getJsDocTags()), 37 | renderAdditionalLinks(aliasedSymbol.getJsDocTags()), 38 | ]); 39 | } 40 | -------------------------------------------------------------------------------- /src/type/members.ts: -------------------------------------------------------------------------------- 1 | import { Symbol, Type, TypeChecker, __String } from 'typescript'; 2 | import { renderType } from '.'; 3 | import { RenderContext } from '../context'; 4 | import { renderDescription } from '../description'; 5 | import { joinLines, listItem } from '../markdown'; 6 | import { getSymbolsType } from './utils'; 7 | 8 | export function getTypeMembers( 9 | type: Type, 10 | typeChecker: TypeChecker 11 | ): { name?: string; description?: string; type: Type }[] { 12 | if (type.symbol && type.symbol.members) { 13 | const membersList: { 14 | name: string; 15 | description?: string; 16 | type: Type; 17 | }[] = []; 18 | 19 | type.symbol.members.forEach((value: Symbol, key: __String) => { 20 | membersList.push({ 21 | name: key.toString(), 22 | description: renderDescription( 23 | value.getDocumentationComment(typeChecker) 24 | ), 25 | type: getSymbolsType(value, typeChecker) 26 | }); 27 | }); 28 | 29 | return membersList; 30 | } 31 | 32 | /* istanbul ignore else */ 33 | if (type.isUnion()) { 34 | return type.types.map(type => ({ type })); 35 | } 36 | 37 | /* istanbul ignore next */ 38 | return []; 39 | } 40 | 41 | export function renderTypeMembers( 42 | type: Type, 43 | context: RenderContext, 44 | nestingLevel = 1 45 | ): string { 46 | return joinLines( 47 | getTypeMembers(type, context.typeChecker).map( 48 | ({ name, description, type }) => 49 | listItem( 50 | renderType(type, context, { 51 | name, 52 | description, 53 | nestingLevel: nestingLevel + 1 54 | }), 55 | nestingLevel 56 | ) 57 | ) 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import rewiremock from 'rewiremock'; 3 | import { createDocumentation, Documentation } from '../src'; 4 | 5 | rewiremock.overrideEntryPoint(module); 6 | 7 | export function createTestDocumentation(sourceCode: { 8 | [fileName: string]: string; 9 | }): Documentation { 10 | return createDocumentation({ 11 | entry: 'index.ts', 12 | sourceCode: { 13 | ...sourceCode, 14 | 'lib.d.ts': ` 15 | interface Array {} 16 | interface Promise {} 17 | interface Buffer { 18 | write(string: string, encoding?: BufferEncoding): number; 19 | write(string: string, offset: number, encoding?: BufferEncoding): number; 20 | write(string: string, offset: number, length: number, encoding?: BufferEncoding): number; 21 | } 22 | ` 23 | }, 24 | compilerOptions: { 25 | strict: true, 26 | esModuleInterop: true 27 | }, 28 | getSectionLocation: (section: string): string => `${section}.md` 29 | }); 30 | } 31 | 32 | export function removePadding(text: string): string { 33 | const output = text.split('\n'); 34 | const padding = output.length 35 | ? output.reduce((min, line) => { 36 | const match = /^(\s*)(\S)/.exec(line); 37 | if (!match || match[1].length > min) { 38 | return min; 39 | } 40 | 41 | return match[1].length; 42 | }, 9999) 43 | : 0; 44 | return output 45 | .map(line => (line.length > padding ? line.substr(padding) : line)) 46 | .join('\n') 47 | .trim(); 48 | } 49 | 50 | export function testDocumentation({ 51 | markdown, 52 | ...sourceCode 53 | }: { 54 | [fileName: string]: string; 55 | }): void { 56 | const defaultSection = 57 | createTestDocumentation({ markdown, ...sourceCode }).get('default') || ''; 58 | 59 | expect(defaultSection.trim()).toEqual(removePadding(markdown)); 60 | } 61 | 62 | export { rewiremock }; 63 | -------------------------------------------------------------------------------- /test/variable.test.ts: -------------------------------------------------------------------------------- 1 | import { testDocumentation } from './utils'; 2 | 3 | describe('variables', () => { 4 | it('documents exported variables', () => { 5 | testDocumentation({ 6 | 'index.ts': ` 7 | /** 8 | * Simple variable description 9 | * line 2 10 | * @see {@link https://test.url.1|Example url 1} 11 | * @see {@link https://test.url.2|Example url 2} 12 | * @example 13 | * example 1 line 1 14 | * example 1 line 2 15 | * @example 16 | * example 2 line 1 17 | * example 2 line 2 18 | */ 19 | export const simpleVariable: number = 1; 20 | `, 21 | markdown: ` 22 | ## simpleVariable 23 | 24 | Simple variable description 25 | line 2 26 | 27 | **TYPE** 28 | 29 | number 30 | 31 | **EXAMPLES** 32 | 33 | \`\`\`typescript 34 | example 1 line 1 35 | example 1 line 2 36 | \`\`\` 37 | 38 | \`\`\`typescript 39 | example 2 line 1 40 | example 2 line 2 41 | \`\`\` 42 | 43 | **SEE ALSO** 44 | 45 | - [Example url 1](https://test.url.1) 46 | - [Example url 2](https://test.url.2) 47 | ` 48 | }); 49 | }); 50 | 51 | it('documents minimal information', () => { 52 | testDocumentation({ 53 | 'index.ts': ` 54 | export const simpleVariable: number = 1; 55 | `, 56 | markdown: ` 57 | ## simpleVariable 58 | 59 | **TYPE** 60 | 61 | number 62 | ` 63 | }); 64 | }); 65 | 66 | it(`doesn't document not exported variables`, () => { 67 | testDocumentation({ 68 | 'index.ts': ` 69 | const simpleVariable = 1; 70 | `, 71 | markdown: `` 72 | }); 73 | }); 74 | 75 | it(`doesn't document internal variables`, () => { 76 | testDocumentation({ 77 | 'index.ts': ` 78 | /** 79 | * @internal 80 | */ 81 | export const simpleVariable = 1; 82 | `, 83 | markdown: `` 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /src/type/declaration.ts: -------------------------------------------------------------------------------- 1 | import { Type, TypeReference } from 'typescript'; 2 | import { renderType } from '.'; 3 | import { RenderContext } from '../context'; 4 | import { inlineCode, link } from '../markdown'; 5 | import { getSymbolSection } from '../utils'; 6 | import { TypeContext } from './context'; 7 | import { getTypeTitle } from './title'; 8 | import { 9 | getArrayType, 10 | getExportedSymbolByType, 11 | isArrayType, 12 | isOptionalType 13 | } from './utils'; 14 | 15 | function getReferenceUrl( 16 | type: Type, 17 | context: RenderContext 18 | ): string | undefined { 19 | const exportedSymbol = getExportedSymbolByType(type, context); 20 | 21 | if (!exportedSymbol) { 22 | return; 23 | } 24 | 25 | const section = getSymbolSection(exportedSymbol); 26 | const location = 27 | section !== context.section ? context.getSectionLocation(section) : ''; 28 | const hash = exportedSymbol 29 | .getName() 30 | .toLowerCase() 31 | .replace(/[^a-z\d]+/g, ''); 32 | 33 | return [location, hash].join('#'); 34 | } 35 | 36 | export function renderTypeDeclaration( 37 | type: Type, 38 | context: RenderContext, 39 | typeContext: TypeContext = {} 40 | ): string { 41 | const typeReference = type as TypeReference; 42 | const arrayType = isArrayType(type) && getArrayType(type); 43 | 44 | if (arrayType) { 45 | return renderType(arrayType, context, { 46 | isArray: true, 47 | name: typeContext.name, 48 | description: typeContext.description 49 | }); 50 | } 51 | 52 | const title = getTypeTitle(type, context); 53 | const typeArguments = (typeReference.typeArguments || []) 54 | .map(typeArgument => renderType(typeArgument, context)) 55 | .join(', '); 56 | const url = getReferenceUrl(type, context); 57 | 58 | const typeDeclaration = [ 59 | url ? link(title, url) : title, 60 | ...(typeArguments ? [`<${typeArguments}>`] : []), 61 | ...(typeContext.isArray ? ['[]'] : []) 62 | ].join(''); 63 | 64 | const nameAndDeclaration = [ 65 | typeContext.name && 66 | `${inlineCode( 67 | `${typeContext.name}${isOptionalType(type) ? '?' : ''}` 68 | )}: `, 69 | typeDeclaration 70 | ].join(''); 71 | 72 | return [nameAndDeclaration, typeContext.description] 73 | .filter(Boolean) 74 | .join(' - '); 75 | } 76 | -------------------------------------------------------------------------------- /src/type/title.ts: -------------------------------------------------------------------------------- 1 | import { ObjectFlags, Type, TypeFlags, TypeReference } from 'typescript'; 2 | import { RenderContext } from '../context'; 3 | import { inlineCode } from '../markdown'; 4 | import { 5 | findExactMatchingTypeFlag, 6 | inspectObject, 7 | SupportError 8 | } from '../utils'; 9 | import { renderTypeDeclaration } from './declaration'; 10 | import { isOptionalBoolean } from './utils'; 11 | 12 | export function getTypeTitle(type: Type, context: RenderContext): string { 13 | const flags = type.getFlags(); 14 | const objectFlags = (type as TypeReference).objectFlags; 15 | 16 | if (type.aliasSymbol) { 17 | return type.aliasSymbol.getName(); 18 | } 19 | 20 | if (flags & TypeFlags.Number) { 21 | return 'number'; 22 | } 23 | 24 | if (flags & TypeFlags.String) { 25 | return 'string'; 26 | } 27 | 28 | if (flags & TypeFlags.Boolean || isOptionalBoolean(type)) { 29 | return 'boolean'; 30 | } 31 | 32 | if (flags & TypeFlags.Void) { 33 | return 'void'; 34 | } 35 | 36 | if (flags & TypeFlags.Any) { 37 | return 'any'; 38 | } 39 | 40 | if (flags & TypeFlags.Unknown) { 41 | return 'unknown'; 42 | } 43 | 44 | if (flags & TypeFlags.Null) { 45 | return 'null'; 46 | } 47 | 48 | if (type.isUnion()) { 49 | return ( 50 | type.types 51 | .filter(type => !(type.getFlags() & TypeFlags.Undefined)) 52 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 53 | .map(type => renderTypeDeclaration(type, context)) 54 | .join(' | ') 55 | ); 56 | } 57 | 58 | if ( 59 | flags & TypeFlags.TypeParameter || 60 | objectFlags & ObjectFlags.Interface || 61 | objectFlags & ObjectFlags.Reference 62 | ) { 63 | return type.symbol && type.symbol.getName(); 64 | } 65 | 66 | if (flags & TypeFlags.EnumLiteral) { 67 | return inlineCode(type.symbol && type.symbol.getName()); 68 | } 69 | 70 | if (type.isStringLiteral()) { 71 | return inlineCode(`'${type.value}'`); 72 | } 73 | 74 | /* istanbul ignore else */ 75 | if (objectFlags & ObjectFlags.Anonymous || flags & TypeFlags.NonPrimitive) { 76 | return 'object'; 77 | } 78 | 79 | /* istanbul ignore next */ 80 | throw new SupportError( 81 | `Not supported type ${inspectObject( 82 | type 83 | )} with flags "${findExactMatchingTypeFlag(flags)}"` 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-documentation", 3 | "version": "3.0.2", 4 | "description": "Generate markdown API documentation directly from TypeScript source code", 5 | "license": "MIT", 6 | "main": "./lib/index.js", 7 | "types": "./lib/index.d.ts", 8 | "bin": "./lib/cli.js", 9 | "files": [ 10 | "lib" 11 | ], 12 | "engines": { 13 | "node": ">= 14.0.0" 14 | }, 15 | "scripts": { 16 | "start": "ts-node src/cli.ts", 17 | "lint": "eslint src/**/*.ts test/**/*.ts", 18 | "typecheck": "tsc --noEmit --project tsconfig.json", 19 | "test": "ts-mocha test/**/*.test.ts", 20 | "test:debug": "npm run test -- --inspect", 21 | "test:coverage": "nyc npm test", 22 | "clean": "del-cli lib", 23 | "build:declarations": "tsc --declaration --emitDeclarationOnly --outDir lib --project tsconfig.build.json", 24 | "build:compile": "babel src --out-dir lib --extensions \".ts\" --ignore \"**/*.test.ts\"", 25 | "build": "npm run clean && npm run build:declarations && npm run build:compile", 26 | "prepack": "npm run build", 27 | "update:dependencies": "npx npm-check-updates -u" 28 | }, 29 | "keywords": [ 30 | "typescript", 31 | "documentation", 32 | "generator", 33 | "generate", 34 | "markdown" 35 | ], 36 | "repository": "git@github.com:mucsi96/typescript-documentation.git", 37 | "author": "Igor Mucsicska ", 38 | "dependencies": { 39 | "commander": "^8.2.0" 40 | }, 41 | "peerDependencies": { 42 | "typescript": "^4.4.4" 43 | }, 44 | "devDependencies": { 45 | "@babel/cli": "^7.15.7", 46 | "@babel/core": "^7.15.8", 47 | "@babel/preset-env": "^7.15.8", 48 | "@babel/preset-typescript": "^7.15.0", 49 | "@types/expect": "^24.3.0", 50 | "@types/mocha": "^9.0.0", 51 | "@types/node": "^16.11.1", 52 | "@typescript-eslint/eslint-plugin": "^5.1.0", 53 | "@typescript-eslint/parser": "^5.1.0", 54 | "cpy-cli": "^3.1.1", 55 | "del-cli": "^4.0.1", 56 | "eslint": "^8.0.1", 57 | "eslint-config-prettier": "^8.3.0", 58 | "eslint-plugin-mocha": "^9.0.0", 59 | "expect": "^27.3.1", 60 | "husky": "^7.0.2", 61 | "mocha": "^9.1.3", 62 | "nyc": "^15.1.0", 63 | "prettier": "^2.4.1", 64 | "pretty-quick": "^3.1.1", 65 | "rewiremock": "^3.14.3", 66 | "rimraf": "^3.0.2", 67 | "ts-mocha": "^8.0.0", 68 | "ts-node": "^10.3.0", 69 | "typescript": "^4.4.4" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/type/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ObjectFlags, 3 | Symbol, 4 | Type, 5 | TypeChecker, 6 | TypeFlags, 7 | TypeReference 8 | } from 'typescript'; 9 | import { RenderContext } from '../context'; 10 | import { SupportError } from '../utils'; 11 | 12 | export function getSymbolsType(symbol: Symbol, typeChecker: TypeChecker): Type { 13 | const declarations = symbol.getDeclarations(); 14 | 15 | /* istanbul ignore if */ 16 | if (!declarations) { 17 | throw new SupportError( 18 | `No declaration found for symbol ${symbol.getName()}` 19 | ); 20 | } 21 | 22 | return typeChecker.getTypeOfSymbolAtLocation(symbol, declarations[0]); 23 | } 24 | 25 | export function getNonOptionalType(type: Type): Type { 26 | return ( 27 | (type.isUnion() && 28 | type.types.length === 2 && 29 | type.types.some(type => type.getFlags() & TypeFlags.Undefined) && 30 | type.types.find(type => !(type.getFlags() & TypeFlags.Undefined))) || 31 | type 32 | ); 33 | } 34 | 35 | export function isOptionalBoolean(type: Type): boolean { 36 | return ( 37 | type.isUnion() && 38 | type.types.length === 3 && 39 | type.types.every(type => { 40 | const flags = type.getFlags(); 41 | return flags & TypeFlags.Undefined || flags & TypeFlags.BooleanLiteral; 42 | }) 43 | ); 44 | } 45 | export function isOptionalType(type: Type): boolean { 46 | return isOptionalBoolean(type) || getNonOptionalType(type) !== type; 47 | } 48 | 49 | export function isArrayType(type: Type): boolean { 50 | const name = type.symbol && type.symbol.getName(); 51 | 52 | return ( 53 | !!(type.getFlags() & TypeFlags.Object) && 54 | !!((type as TypeReference).objectFlags & ObjectFlags.Reference) && 55 | name === 'Array' 56 | ); 57 | } 58 | 59 | export function getArrayType(type: Type): Type | undefined { 60 | const typeArguments = (type as TypeReference).typeArguments; 61 | 62 | return typeArguments && typeArguments[0]; 63 | } 64 | 65 | export function getExportedSymbolByType( 66 | type: Type, 67 | context: RenderContext 68 | ): Symbol | undefined { 69 | const isExportedTypeAlias = 70 | type.aliasSymbol && context.exportedSymbols.includes(type.aliasSymbol); 71 | const isExportedObject = 72 | !!(type.getFlags() & TypeFlags.Object) && 73 | context.exportedSymbols.includes(type.symbol); 74 | 75 | if (isExportedTypeAlias) { 76 | return type.aliasSymbol; 77 | } 78 | 79 | if (isExportedObject) { 80 | return type.symbol; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { CompilerOptions, createProgram } from 'typescript'; 2 | import { spreadClassProperties } from './class'; 3 | import { joinSections } from './markdown'; 4 | import { getModuleDependencies } from './module'; 5 | import { renderSymbol } from './symbol'; 6 | import { 7 | createCompilerHost, 8 | getSymbolSection, 9 | isInternalSymbol 10 | } from './utils'; 11 | 12 | export type Options = { 13 | compilerOptions: CompilerOptions; 14 | entry: string; 15 | sourceCode?: { [name: string]: string }; 16 | getSectionLocation: (section: string) => string; 17 | }; 18 | 19 | export type Documentation = Map; 20 | 21 | export function createDocumentation(options: Options): Documentation { 22 | const { compilerOptions, entry: entryFileName, sourceCode } = options; 23 | const program = createProgram({ 24 | rootNames: [entryFileName], 25 | options: compilerOptions, 26 | ...(sourceCode && { 27 | host: createCompilerHost(sourceCode) 28 | }) 29 | }); 30 | 31 | const typeChecker = program.getTypeChecker(); 32 | const entrySourceFile = program.getSourceFile(entryFileName); 33 | 34 | /* istanbul ignore next */ 35 | if (!entrySourceFile) { 36 | throw new Error(`Cannot find entry ${entryFileName}`); 37 | } 38 | 39 | const entryModuleSymbol = typeChecker.getSymbolAtLocation(entrySourceFile); 40 | 41 | if (!entryModuleSymbol) { 42 | return new Map(); 43 | } 44 | const exportedSymbols = typeChecker 45 | .getExportsOfModule(entryModuleSymbol) 46 | .filter(symbol => !isInternalSymbol(symbol)); 47 | 48 | let symbolsInTopologicalOrder = getModuleDependencies(entryModuleSymbol, { 49 | typeChecker, 50 | exportedSymbols, 51 | resolutionPath: [] 52 | }).filter((child, index, all) => all.indexOf(child) === index); 53 | 54 | symbolsInTopologicalOrder = symbolsInTopologicalOrder.concat( 55 | exportedSymbols.filter( 56 | symbol => !symbolsInTopologicalOrder.includes(symbol) 57 | ) 58 | ); 59 | 60 | return spreadClassProperties( 61 | symbolsInTopologicalOrder, 62 | options.getSectionLocation 63 | ).reduce((acc, symbol) => { 64 | const section = getSymbolSection(symbol); 65 | const output = renderSymbol(symbol, { 66 | typeChecker, 67 | exportedSymbols: symbolsInTopologicalOrder, 68 | section, 69 | getSectionLocation: options.getSectionLocation 70 | }); 71 | 72 | if (acc.has(section)) { 73 | acc.set(section, joinSections([acc.get(section) as string, output])); 74 | } else { 75 | acc.set(section, output); 76 | } 77 | 78 | return acc; 79 | }, new Map()); 80 | } 81 | -------------------------------------------------------------------------------- /.github/workflows/update-dependencies.yml: -------------------------------------------------------------------------------- 1 | name: Update dependencies 2 | 3 | on: 4 | schedule: 5 | - cron: '0 7 * * 1' # At 07:00 GMT on Monday. https://crontab.guru/#0_7_*_*_1 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | ref: 'master' 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: '12.x' 17 | - name: Update dependencies 18 | run: npx npm-check-updates -u 19 | - name: Install dependencies 20 | run: npm install 21 | - name: Raise pull request 22 | uses: actions/github-script@0.4.0 23 | with: 24 | github-token: ${{secrets.UPDATE_DEPENDENCIES_TOKEN}} 25 | script: | 26 | const { readFileSync } = require('fs'); 27 | const { resolve } = require('path'); 28 | const { execSync } = require('child_process'); 29 | 30 | 31 | 32 | const changedFiles = execSync('git diff --name-only') 33 | .toString() 34 | .split('\n') 35 | .filter(Boolean); 36 | 37 | if (!changedFiles.includes('package.json')) { 38 | return; 39 | } 40 | 41 | const time = new Date().toISOString().split('.')[0]; 42 | const title = `Automated dependency update (${time.replace(/[T]/g, ' ')})`; 43 | 44 | const baseBranch = execSync('git branch') 45 | .toString() 46 | .split('\n') 47 | .map(branch => { 48 | const match = /^\* (.*)$/.exec(branch); 49 | return match && match[1]; 50 | }) 51 | .find(Boolean); 52 | 53 | const parentSha = execSync('git rev-parse HEAD') 54 | .toString() 55 | .trim(); 56 | 57 | const branch = `update-dependencies-${time.replace(/[T:]/g, '-')}`; 58 | 59 | const tree = await github.git.createTree({ 60 | ...context.repo, 61 | base_tree: parentSha, 62 | tree: changedFiles.map(path => ({ 63 | path, 64 | mode: '100644', 65 | content: readFileSync(resolve(process.cwd(), path), 'utf8') 66 | })) 67 | }); 68 | 69 | const commit = await github.git.createCommit({ 70 | ...context.repo, 71 | message: title, 72 | tree: tree.data.sha, 73 | parents: [parentSha] 74 | }); 75 | 76 | await github.git.createRef({ 77 | ...context.repo, 78 | ref: `refs/heads/${branch}`, 79 | sha: commit.data.sha 80 | }); 81 | 82 | await github.pulls.create({ 83 | ...context.repo, 84 | title, 85 | head: branch, 86 | base: baseBranch 87 | }); 88 | -------------------------------------------------------------------------------- /test/enumeration.test.ts: -------------------------------------------------------------------------------- 1 | import { testDocumentation } from './utils'; 2 | 3 | describe('enumerations', () => { 4 | it('documents exported enumerations', () => { 5 | testDocumentation({ 6 | 'index.ts': ` 7 | /** 8 | * Simple enumeration description 9 | * line 2 10 | * @see {@link https://test.url.1|Example url 1} 11 | * @see {@link https://test.url.2|Example url 2} 12 | * @example 13 | * example 1 line 1 14 | * example 1 line 2 15 | * @example 16 | * example 2 line 1 17 | * example 2 line 2 18 | */ 19 | export enum SimpleEnum { ONE = '\uE000', TWO = 1 }; 20 | `, 21 | markdown: ` 22 | ## SimpleEnum 23 | 24 | Simple enumeration description 25 | line 2 26 | 27 | **POSSIBLE VALUES** 28 | 29 | - \`ONE\` 30 | - \`TWO\` 31 | 32 | **EXAMPLES** 33 | 34 | \`\`\`typescript 35 | example 1 line 1 36 | example 1 line 2 37 | \`\`\` 38 | 39 | \`\`\`typescript 40 | example 2 line 1 41 | example 2 line 2 42 | \`\`\` 43 | 44 | **SEE ALSO** 45 | 46 | - [Example url 1](https://test.url.1) 47 | - [Example url 2](https://test.url.2) 48 | ` 49 | }); 50 | }); 51 | 52 | it('documents minimal information', () => { 53 | testDocumentation({ 54 | 'index.ts': ` 55 | export enum SimpleEnum { ONE, TWO }; 56 | `, 57 | markdown: ` 58 | ## SimpleEnum 59 | 60 | **POSSIBLE VALUES** 61 | 62 | - \`ONE\` 63 | - \`TWO\` 64 | ` 65 | }); 66 | }); 67 | 68 | it('documents as dependency', () => { 69 | testDocumentation({ 70 | 'dependency.ts': ` 71 | export enum SimpleEnum { ONE, TWO }; 72 | `, 73 | 'index.ts': ` 74 | import { SimpleEnum } from './dependency'; 75 | 76 | export let testVariable: SimpleEnum; 77 | export * from './dependency'; 78 | `, 79 | markdown: ` 80 | ## testVariable 81 | 82 | **TYPE** 83 | 84 | [SimpleEnum](#simpleenum) 85 | 86 | ## SimpleEnum 87 | 88 | **POSSIBLE VALUES** 89 | 90 | - \`ONE\` 91 | - \`TWO\` 92 | ` 93 | }); 94 | }); 95 | 96 | it(`doesn't documents as dependency if not exported`, () => { 97 | testDocumentation({ 98 | 'dependency.ts': ` 99 | export enum SimpleEnum { ONE, TWO }; 100 | `, 101 | 'index.ts': ` 102 | import { SimpleEnum } from './dependency'; 103 | 104 | export let testVariable: SimpleEnum; 105 | `, 106 | markdown: ` 107 | ## testVariable 108 | 109 | **TYPE** 110 | 111 | SimpleEnum 112 | ` 113 | }); 114 | }); 115 | 116 | it(`doesn't document not exported enumerations`, () => { 117 | testDocumentation({ 118 | 'index.ts': ` 119 | enum SimpleEnum { ONE, TWO }; 120 | `, 121 | markdown: `` 122 | }); 123 | }); 124 | 125 | it(`doesn't document internal enumerations`, () => { 126 | testDocumentation({ 127 | 'index.ts': ` 128 | /** 129 | * @internal 130 | */ 131 | export enum SimpleEnum { ONE, TWO }; 132 | `, 133 | markdown: `` 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /src/type/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ObjectFlags, 3 | Symbol, 4 | SymbolFlags, 5 | Type, 6 | TypeReference, 7 | } from 'typescript'; 8 | import { DependencyContext, RenderContext } from '../context'; 9 | import { joinLines } from '../markdown'; 10 | import { getSymbolDependencies } from '../symbol'; 11 | import { TypeContext } from './context'; 12 | import { renderTypeDeclaration } from './declaration'; 13 | import { renderTypeMembers } from './members'; 14 | import { getNonOptionalType } from './utils'; 15 | 16 | function hasMembers(type: Type): boolean { 17 | const objectFlags = (type as TypeReference).objectFlags; 18 | 19 | return ( 20 | !type.aliasSymbol && 21 | !!( 22 | objectFlags & ObjectFlags.Anonymous 23 | ) 24 | ); 25 | } 26 | 27 | export function getTypeLiteralDependencies( 28 | symbol: Symbol, 29 | context: DependencyContext 30 | ): Symbol[] { 31 | const members: Symbol[] = []; 32 | 33 | /* istanbul ignore else */ 34 | if (symbol.members) { 35 | symbol.members.forEach((member) => { 36 | members.push(member); 37 | }); 38 | } 39 | 40 | return members.reduce( 41 | (dependencies, member) => [ 42 | ...dependencies, 43 | ...getSymbolDependencies(member, context), 44 | ], 45 | [] 46 | ); 47 | } 48 | 49 | export function getTypeDependencies( 50 | symbol: Symbol | undefined, 51 | type: Type, 52 | context: DependencyContext 53 | ): Symbol[] { 54 | const symbolFlags = symbol ? symbol.getFlags() : 0; 55 | const typeSymbol = type.getSymbol(); 56 | 57 | if (typeSymbol && typeSymbol.getFlags() & SymbolFlags.Enum) { 58 | return []; 59 | } 60 | 61 | if (!(symbolFlags & SymbolFlags.TypeAlias) && type.aliasSymbol) { 62 | return [ 63 | ...(context.exportedSymbols.includes(type.aliasSymbol) 64 | ? [type.aliasSymbol] 65 | : []), 66 | ...getSymbolDependencies(type.aliasSymbol, context), 67 | ]; 68 | } 69 | 70 | if (type.isUnion()) { 71 | return type.types.reduce( 72 | (dependencies, type) => [ 73 | ...dependencies, 74 | ...getTypeDependencies(undefined, type, context), 75 | ], 76 | [] 77 | ); 78 | } 79 | 80 | if (!typeSymbol) { 81 | return []; 82 | } 83 | 84 | const typeReference = type as TypeReference; 85 | const typeArguments = typeReference.typeArguments || []; 86 | const typeArgumentDependencies = typeArguments.reduce( 87 | (dependencies, typeArgument) => { 88 | const symbol = typeArgument.getSymbol(); 89 | 90 | return [ 91 | ...dependencies, 92 | ...getTypeDependencies(symbol, typeArgument, context), 93 | ]; 94 | }, 95 | [] 96 | ); 97 | 98 | return [ 99 | ...(context.exportedSymbols.includes(typeSymbol) ? [typeSymbol] : []), 100 | ...getSymbolDependencies(typeSymbol, context), 101 | ...typeArgumentDependencies, 102 | ]; 103 | } 104 | 105 | export function renderType( 106 | type: Type, 107 | context: RenderContext, 108 | typeContext: TypeContext = {} 109 | ): string { 110 | const nonOptionalType = getNonOptionalType(type); 111 | 112 | return joinLines([ 113 | renderTypeDeclaration(type, context, typeContext), 114 | hasMembers(nonOptionalType) 115 | ? renderTypeMembers(nonOptionalType, context, typeContext.nestingLevel) 116 | : '', 117 | ]); 118 | } 119 | -------------------------------------------------------------------------------- /src/class.ts: -------------------------------------------------------------------------------- 1 | import { JSDocTagInfo, Symbol, SymbolFlags } from 'typescript'; 2 | import { renderAdditionalLinks } from './additionalLinks'; 3 | import { DependencyContext, RenderContext } from './context'; 4 | import { renderDescription } from './description'; 5 | import { renderExamples } from './examples'; 6 | import { heading, joinSections } from './markdown'; 7 | import { getSymbolDependencies } from './symbol'; 8 | import { getSymbolSection, isInternalSymbol } from './utils'; 9 | 10 | export function getClassDependencies( 11 | symbol: Symbol, 12 | context: DependencyContext 13 | ): Symbol[] { 14 | const members: Symbol[] = []; 15 | 16 | /* istanbul ignore else */ 17 | if (symbol.members) { 18 | symbol.members.forEach((member) => { 19 | if (!isInternalSymbol(member)) { 20 | members.push(member); 21 | } 22 | }); 23 | } 24 | 25 | return members.reduce( 26 | (dependencies, member) => [ 27 | ...dependencies, 28 | ...getSymbolDependencies(member, context), 29 | ], 30 | [] 31 | ); 32 | } 33 | 34 | export function spreadClassProperties( 35 | symbols: Symbol[], 36 | getSectionLocation: (section: string) => string 37 | ): Symbol[] { 38 | return symbols.reduce((acc, symbol) => { 39 | if (!(symbol.getFlags() & SymbolFlags.Class) || !symbol.members) { 40 | return [...acc, symbol]; 41 | } 42 | 43 | const classInstanceName = [ 44 | symbol.name.charAt(0).toLowerCase(), 45 | symbol.name.slice(1), 46 | ].join(''); 47 | const section = getSymbolSection(symbol); 48 | const members: Symbol[] = []; 49 | const memberSections = new Set(); 50 | symbol.members.forEach((member) => { 51 | if (!isInternalSymbol(member)) { 52 | // eslint-disable-next-line @typescript-eslint/unbound-method 53 | member.getName = (): string => `${classInstanceName}.${member.name}`; 54 | members.push(member); 55 | const memberSection = getSymbolSection(member); 56 | if (memberSection !== 'default' && memberSection !== section) { 57 | memberSections.add(getSymbolSection(member)); 58 | } 59 | } 60 | }); 61 | 62 | const originalTags = symbol.getJsDocTags(); 63 | const memberSectionsArray = Array.from(memberSections.values()); 64 | memberSectionsArray.sort(); 65 | const additionalMemberReferences = memberSectionsArray.map( 66 | (section: string) => ({ 67 | name: 'see', 68 | text: [ 69 | { 70 | text: `{@link ${getSectionLocation(section)}|${section}}`, 71 | kind: 'text', 72 | }, 73 | ], 74 | }) 75 | ); 76 | // eslint-disable-next-line @typescript-eslint/unbound-method 77 | symbol.getJsDocTags = (): JSDocTagInfo[] => { 78 | return [...originalTags, ...additionalMemberReferences]; 79 | }; 80 | 81 | return [...acc, symbol, ...members]; 82 | }, []); 83 | } 84 | 85 | export function renderClass( 86 | symbol: Symbol, 87 | aliasedSymbol: Symbol, 88 | context: RenderContext 89 | ): string { 90 | const name = symbol.getName(); 91 | 92 | return joinSections([ 93 | heading(name, 2), 94 | renderDescription( 95 | aliasedSymbol.getDocumentationComment(context.typeChecker) 96 | ), 97 | renderExamples(aliasedSymbol.getJsDocTags()), 98 | renderAdditionalLinks(aliasedSymbol.getJsDocTags()), 99 | ]); 100 | } 101 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { program } from 'commander'; 4 | import { mkdirSync, writeFileSync } from 'fs'; 5 | import { basename, dirname, isAbsolute, resolve } from 'path'; 6 | import { 7 | CompilerOptions, 8 | getParsedCommandLineOfConfigFile, 9 | sys, 10 | } from 'typescript'; 11 | import { createDocumentation, Options } from '.'; 12 | import { heading, joinSections } from './markdown'; 13 | import { formatDiagnosticError } from './utils'; 14 | 15 | type CLIOptions = { 16 | project: string; 17 | entry: string; 18 | output: string; 19 | }; 20 | 21 | program 22 | .name('typescript-documentation') 23 | .description( 24 | 'Generate markdown API documentation directly from TypeScript source code' 25 | ) 26 | .option( 27 | '-p, --project ', 28 | 'relative or absolute path to a tsconfig.json file', 29 | './tsconfig.json' 30 | ) 31 | .option( 32 | '-e, --entry
', 33 | 'entry/main file of project', 34 | './src/index.ts' 35 | ) 36 | .option( 37 | '-o, --output ', 38 | 'markdown documentation output file location', 39 | './docs/README.md' 40 | ); 41 | 42 | function getCompilerOptions(cliOptions: CLIOptions): CompilerOptions { 43 | const tsConfigPath = isAbsolute(cliOptions.project) 44 | ? cliOptions.project 45 | : resolve(process.cwd(), cliOptions.project); 46 | 47 | const config = getParsedCommandLineOfConfigFile( 48 | tsConfigPath, 49 | {}, 50 | { 51 | ...sys, 52 | onUnRecoverableConfigFileDiagnostic: /* istanbul ignore next */ ( 53 | diagnostic 54 | ) => { 55 | /* istanbul ignore next */ 56 | throw new Error(formatDiagnosticError(diagnostic)); 57 | }, 58 | } 59 | ); 60 | 61 | /* istanbul ignore next */ 62 | if (!config) { 63 | throw new Error(`Unable to parse ${tsConfigPath}`); 64 | } 65 | return config.options; 66 | } 67 | 68 | function getOutput(cliOptions: CLIOptions): string { 69 | return isAbsolute(cliOptions.output) 70 | ? cliOptions.output 71 | : resolve(process.cwd(), cliOptions.output); 72 | } 73 | 74 | function getOptions(cliOptions: CLIOptions): Options { 75 | try { 76 | mkdirSync(dirname(getOutput(cliOptions))); 77 | } catch { 78 | /* istanbul ignore next */ 79 | 1; 80 | } 81 | 82 | return { 83 | compilerOptions: getCompilerOptions(cliOptions), 84 | entry: isAbsolute(cliOptions.entry) 85 | ? cliOptions.entry 86 | : resolve(process.cwd(), cliOptions.entry), 87 | getSectionLocation: (section: string): string => 88 | section === 'default' 89 | ? basename(getOutput(cliOptions)) 90 | : `${section.toLowerCase().replace(/ /g, '-')}.md`, 91 | }; 92 | } 93 | 94 | program.parse(process.argv); 95 | const cliOptions: CLIOptions = program.opts(); 96 | const options = getOptions(cliOptions); 97 | try { 98 | createDocumentation(options).forEach((text: string, section: string) => { 99 | const content = 100 | section === 'default' ? text : joinSections([heading(section, 1), text]); 101 | 102 | /* istanbul ignore next */ 103 | if (!content) { 104 | return; 105 | } 106 | 107 | writeFileSync( 108 | resolve( 109 | dirname(getOutput(cliOptions)), 110 | options.getSectionLocation(section) 111 | ), 112 | content, 113 | 'utf8' 114 | ); 115 | }); 116 | } catch (e) { 117 | /* istanbul ignore next */ 118 | if (e instanceof Error) { 119 | console.log(e.stack); 120 | } 121 | /* istanbul ignore next */ 122 | process.exit(1); 123 | } 124 | -------------------------------------------------------------------------------- /test/typeDeclaration.test.ts: -------------------------------------------------------------------------------- 1 | import { testDocumentation } from './utils'; 2 | 3 | describe('type declarations', () => { 4 | it('documents exported types', () => { 5 | testDocumentation({ 6 | 'index.ts': ` 7 | /** 8 | * Simple type description 9 | * line 2 10 | * @see {@link https://test.url.1|Example url 1} 11 | * @see {@link https://test.url.2|Example url 2} 12 | * @example 13 | * example 1 line 1 14 | * example 1 line 2 15 | * @example 16 | * example 2 line 1 17 | * example 2 line 2 18 | */ 19 | export type SimpleType = { 20 | a: string; 21 | b?: number; 22 | }; 23 | `, 24 | markdown: ` 25 | ## SimpleType 26 | 27 | Simple type description 28 | line 2 29 | 30 | **PROPERTIES** 31 | 32 | - \`a\`: string 33 | - \`b?\`: number 34 | 35 | **EXAMPLES** 36 | 37 | \`\`\`typescript 38 | example 1 line 1 39 | example 1 line 2 40 | \`\`\` 41 | 42 | \`\`\`typescript 43 | example 2 line 1 44 | example 2 line 2 45 | \`\`\` 46 | 47 | **SEE ALSO** 48 | 49 | - [Example url 1](https://test.url.1) 50 | - [Example url 2](https://test.url.2) 51 | 52 | ` 53 | }); 54 | }); 55 | 56 | it('documents minimal information', () => { 57 | testDocumentation({ 58 | 'index.ts': ` 59 | export type SimpleType = {}; 60 | `, 61 | markdown: ` 62 | ## SimpleType 63 | ` 64 | }); 65 | }); 66 | 67 | it(`doesn't document not exported types`, () => { 68 | testDocumentation({ 69 | 'index.ts': ` 70 | type SimpleType = { 71 | a: string; 72 | b: number; 73 | }; 74 | `, 75 | markdown: `` 76 | }); 77 | }); 78 | 79 | it(`doesn't document internal types`, () => { 80 | testDocumentation({ 81 | 'index.ts': ` 82 | /** 83 | * @internal 84 | */ 85 | export type SimpleType = { 86 | a: string; 87 | b: number; 88 | }; 89 | `, 90 | markdown: `` 91 | }); 92 | }); 93 | 94 | it('documents unions', () => { 95 | testDocumentation({ 96 | 'index.ts': ` 97 | export type UnionType = string | number; 98 | `, 99 | markdown: ` 100 | ## UnionType 101 | 102 | **POSSIBLE VALUES** 103 | 104 | - string 105 | - number 106 | ` 107 | }); 108 | }); 109 | 110 | it('documents types with optional boolean', () => { 111 | testDocumentation({ 112 | 'index.ts': ` 113 | export type TypeWithOptionalBoolean = { 114 | a?: boolean; 115 | }; 116 | `, 117 | markdown: ` 118 | ## TypeWithOptionalBoolean 119 | 120 | **PROPERTIES** 121 | 122 | - \`a?\`: boolean 123 | ` 124 | }); 125 | }); 126 | 127 | it('documents anonymous types', () => { 128 | testDocumentation({ 129 | 'index.ts': ` 130 | export type TypeWithAnonymous = { 131 | a: { 132 | b: string 133 | }; 134 | }; 135 | `, 136 | markdown: ` 137 | ## TypeWithAnonymous 138 | 139 | **PROPERTIES** 140 | 141 | - \`a\`: object 142 | - \`b\`: string 143 | ` 144 | }); 145 | }); 146 | 147 | it('documents circular anonymous types', () => { 148 | testDocumentation({ 149 | 'index.ts': ` 150 | export type TypeWithAnonymous = { 151 | a: TypeWithAnonymous; 152 | }; 153 | `, 154 | markdown: ` 155 | ## TypeWithAnonymous 156 | 157 | **PROPERTIES** 158 | 159 | - \`a\`: [TypeWithAnonymous](#typewithanonymous) 160 | ` 161 | }); 162 | }); 163 | 164 | it('documents arrays', () => { 165 | testDocumentation({ 166 | 'index.ts': ` 167 | export type TypeWithArray = { 168 | a: string[]; 169 | }; 170 | `, 171 | markdown: ` 172 | ## TypeWithArray 173 | 174 | **PROPERTIES** 175 | 176 | - \`a\`: string[] 177 | ` 178 | }); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /src/function.ts: -------------------------------------------------------------------------------- 1 | import { Signature, Symbol, Type } from 'typescript'; 2 | import { renderAdditionalLinks } from './additionalLinks'; 3 | import { DependencyContext, RenderContext } from './context'; 4 | import { renderDescription } from './description'; 5 | import { renderExamples } from './examples'; 6 | import { 7 | heading, 8 | joinLines, 9 | joinSections, 10 | listItem, 11 | subSection, 12 | } from './markdown'; 13 | import { getSymbolDependencies } from './symbol'; 14 | import { getTypeDependencies, renderType } from './type'; 15 | import { getSymbolsType } from './type/utils'; 16 | import { getSymbolDisplayText } from './utils'; 17 | 18 | export function getFunctionDependencies( 19 | type: Type, 20 | context: DependencyContext 21 | ): Symbol[] { 22 | return type 23 | .getCallSignatures() 24 | .reduce((dependencies, signature) => { 25 | const parameterDependencies = signature 26 | .getParameters() 27 | .reduce( 28 | (dependencies, parameter) => [ 29 | ...dependencies, 30 | ...getSymbolDependencies(parameter, context), 31 | ], 32 | [] 33 | ); 34 | 35 | const returnType = signature.getReturnType(); 36 | const returnTypeSymbol = returnType.getSymbol(); 37 | 38 | return [ 39 | ...dependencies, 40 | ...getTypeDependencies(returnTypeSymbol, returnType, context), 41 | ...parameterDependencies, 42 | ]; 43 | }, []); 44 | } 45 | 46 | function getParameterDescription( 47 | name: string, 48 | signature: Signature 49 | ): string | undefined | null { 50 | const paramDescriptionRegex = new RegExp(`${name} (.*)`); 51 | return signature 52 | .getJsDocTags() 53 | .filter((tag) => tag.name === 'param') 54 | .map((tag) => { 55 | /* istanbul ignore next */ 56 | if (!tag.text) { 57 | return null; 58 | } 59 | 60 | const match = paramDescriptionRegex.exec(getSymbolDisplayText(tag)); 61 | 62 | return match && match[1]; 63 | }) 64 | .find((description) => description); 65 | } 66 | 67 | function renderFunctionParameter( 68 | parameter: Symbol, 69 | signature: Signature, 70 | context: RenderContext 71 | ): string { 72 | const name = parameter.getName(); 73 | const description = getParameterDescription(name, signature); 74 | const type = getSymbolsType(parameter, context.typeChecker); 75 | return listItem( 76 | renderType(type, context, { 77 | name, 78 | nestingLevel: 2, 79 | ...(description && { description }), 80 | }) 81 | ); 82 | } 83 | 84 | function renderFunctionParameters( 85 | parameters: Symbol[], 86 | signature: Signature, 87 | context: RenderContext 88 | ): string { 89 | if (!parameters.length) { 90 | return ''; 91 | } 92 | 93 | return joinSections([ 94 | subSection('Parameters'), 95 | joinLines( 96 | parameters.map((parameter) => 97 | renderFunctionParameter(parameter, signature, context) 98 | ) 99 | ), 100 | ]); 101 | } 102 | 103 | export function renderFunctionSignature( 104 | name: string, 105 | signature: Signature, 106 | context: RenderContext 107 | ): string { 108 | const parameters = signature.getParameters(); 109 | const typeParameters = (signature.getTypeParameters() || []) 110 | .map((typeParameter) => typeParameter.symbol.name) 111 | .join(', '); 112 | 113 | return joinSections([ 114 | heading( 115 | `${name}${ 116 | typeParameters ? `<${typeParameters}>` : '' 117 | }(${parameters.map(({ name }) => name).join(', ')})`, 118 | 2 119 | ), 120 | renderDescription(signature.getDocumentationComment(context.typeChecker)), 121 | renderFunctionParameters(parameters, signature, context), 122 | subSection('Returns'), 123 | renderType(signature.getReturnType(), context), 124 | renderExamples(signature.getJsDocTags()), 125 | renderAdditionalLinks(signature.getJsDocTags()), 126 | ]); 127 | } 128 | 129 | export function renderFunction( 130 | symbol: Symbol, 131 | aliasedSymbol: Symbol, 132 | type: Type, 133 | context: RenderContext 134 | ): string { 135 | const name = symbol.getName(); 136 | return joinSections( 137 | type 138 | .getCallSignatures() 139 | .map((signature) => 140 | renderFunctionSignature(name, signature, context) 141 | ) 142 | ); 143 | } 144 | -------------------------------------------------------------------------------- /test/symbol.test.ts: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { 3 | createTestDocumentation, 4 | removePadding, 5 | testDocumentation 6 | } from './utils'; 7 | 8 | describe('symbol', () => { 9 | it('documents in same order as in source', () => { 10 | testDocumentation({ 11 | 'index.ts': ` 12 | export let b: string; 13 | export let a: number; 14 | `, 15 | markdown: ` 16 | ## b 17 | 18 | **TYPE** 19 | 20 | string 21 | 22 | ## a 23 | 24 | **TYPE** 25 | 26 | number 27 | ` 28 | }); 29 | }); 30 | 31 | it('documents dependencies in topological order', () => { 32 | testDocumentation({ 33 | 'dependency.ts': ` 34 | export type SimpleTypeA = {}; 35 | export type SimpleTypeB = {}; 36 | export type SimpleTypeC = { c: SimpleTypeB }; 37 | `, 38 | 'index.ts': ` 39 | import { SimpleTypeC } from './dependency'; 40 | export let testVariable: SimpleTypeC; 41 | export * from './dependency'; 42 | `, 43 | markdown: ` 44 | ## testVariable 45 | 46 | **TYPE** 47 | 48 | [SimpleTypeC](#simpletypec) 49 | 50 | ## SimpleTypeC 51 | 52 | **PROPERTIES** 53 | 54 | - \`c\`: [SimpleTypeB](#simpletypeb) 55 | 56 | ## SimpleTypeB 57 | 58 | ## SimpleTypeA 59 | ` 60 | }); 61 | }); 62 | 63 | it(`doesn't document internal dependencies`, () => { 64 | testDocumentation({ 65 | 'dependency.ts': ` 66 | export type SimpleTypeB = {}; 67 | /** 68 | * @internal 69 | */ 70 | export type SimpleTypeC = { c: SimpleTypeB }; 71 | `, 72 | 'index.ts': ` 73 | import { SimpleTypeC } from './dependency'; 74 | export let testVariable: SimpleTypeC; 75 | export * from './dependency'; 76 | `, 77 | markdown: ` 78 | ## testVariable 79 | 80 | **TYPE** 81 | 82 | SimpleTypeC 83 | 84 | ## SimpleTypeB 85 | ` 86 | }); 87 | }); 88 | 89 | it('documents sections', () => { 90 | const docs = createTestDocumentation({ 91 | 'index.ts': ` 92 | /** 93 | * @section one 94 | */ 95 | export let b: string; 96 | export let a: number; 97 | ` 98 | }); 99 | expect(docs.get('one')).toEqual( 100 | removePadding(` 101 | ## b 102 | 103 | **TYPE** 104 | 105 | string 106 | `) 107 | ); 108 | 109 | expect(docs.get('default')).toEqual( 110 | removePadding(` 111 | ## a 112 | 113 | **TYPE** 114 | 115 | number 116 | `) 117 | ); 118 | }); 119 | 120 | it('documents class methods in separate sections', () => { 121 | const docs = createTestDocumentation({ 122 | 'index.ts': ` 123 | /** 124 | * @section one 125 | */ 126 | export class SimpleClass { 127 | public simpleMethod1(): void {} 128 | 129 | /** 130 | * @section two 131 | */ 132 | public simpleMethod2(): void {} 133 | 134 | /** 135 | * @section two 136 | */ 137 | public simpleMethod3(): void {} 138 | } 139 | ` 140 | }); 141 | expect(docs.get('one')).toEqual( 142 | removePadding(` 143 | ## SimpleClass 144 | 145 | **SEE ALSO** 146 | 147 | - [two](two.md) 148 | `) 149 | ); 150 | 151 | expect(docs.get('default')).toEqual( 152 | removePadding(` 153 | ## simpleClass.simpleMethod1() 154 | 155 | **RETURNS** 156 | 157 | void 158 | `) 159 | ); 160 | 161 | expect(docs.get('two')).toEqual( 162 | removePadding(` 163 | ## simpleClass.simpleMethod2() 164 | 165 | **RETURNS** 166 | 167 | void 168 | 169 | ## simpleClass.simpleMethod3() 170 | 171 | **RETURNS** 172 | 173 | void 174 | `) 175 | ); 176 | }); 177 | 178 | it('creates cross section links', () => { 179 | const docs = createTestDocumentation({ 180 | 'index.ts': ` 181 | /** 182 | * @section one 183 | */ 184 | export type TypeInOtherSection = {}; 185 | export let a: TypeInOtherSection; 186 | ` 187 | }); 188 | expect(docs.get('default')).toEqual( 189 | removePadding(` 190 | ## a 191 | 192 | **TYPE** 193 | 194 | [TypeInOtherSection](one.md#typeinothersection) 195 | `) 196 | ); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /test/cli.test.ts: -------------------------------------------------------------------------------- 1 | import expect from 'expect'; 2 | import { resolve } from 'path'; 3 | import * as ts from 'typescript'; 4 | import { Documentation, Options } from '../src'; 5 | import { rewiremock } from './utils'; 6 | 7 | type CLIResult = { 8 | options: Options; 9 | output: { [file: string]: string }; 10 | }; 11 | 12 | function runCLI(): CLIResult { 13 | const result: CLIResult = { 14 | options: {} as Options, 15 | output: {} 16 | }; 17 | 18 | rewiremock.proxy('../src/cli', { 19 | '.': { 20 | createDocumentation: (options: Options): Documentation => { 21 | result.options = options; 22 | const docs = new Map(); 23 | docs.set('default', 'test docs'); 24 | docs.set('section1', 'test section'); 25 | return docs; 26 | } 27 | }, 28 | fs: { 29 | writeFileSync: (file: string, content: string): void => { 30 | result.output[file] = content; 31 | }, 32 | //eslint-disable-next-line @typescript-eslint/no-empty-function 33 | mkdirSync: (): void => {} 34 | }, 35 | typescript: { 36 | ...ts, 37 | sys: { 38 | ...ts.sys, 39 | readFile: (file: string): string => { 40 | if (file === resolve(process.cwd(), 'test.tsconfig.json')) { 41 | return '{"compilerOptions": {"strict": true}}'; 42 | } 43 | if (file === resolve(process.cwd(), 'tsconfig.json')) { 44 | return '{"compilerOptions": {"strict": false}}'; 45 | } 46 | return ''; 47 | }, 48 | fileExists: (): boolean => true 49 | } 50 | } 51 | }); 52 | 53 | return result; 54 | } 55 | 56 | describe('CLI', () => { 57 | it('reads compiler options from default config file location', () => { 58 | process.argv = ['node', 'typescript-documentation']; 59 | expect(runCLI().options.compilerOptions.strict).toBe(false); 60 | }); 61 | 62 | it('reads compiler options from provided config file (long)', () => { 63 | process.argv = [ 64 | 'node', 65 | 'typescript-documentation', 66 | '--project', 67 | './test.tsconfig.json' 68 | ]; 69 | expect(runCLI().options.compilerOptions.strict).toBe(true); 70 | }); 71 | 72 | it('reads compiler options from provided config file (short)', () => { 73 | process.argv = [ 74 | 'node', 75 | 'typescript-documentation', 76 | '-p', 77 | './test.tsconfig.json' 78 | ]; 79 | expect(runCLI().options.compilerOptions.strict).toBe(true); 80 | }); 81 | 82 | it('reads compiler options from provided config file (absolute)', () => { 83 | const path = resolve(process.cwd(), 'test.tsconfig.json'); 84 | process.argv = ['node', 'typescript-documentation', '--project', path]; 85 | expect(runCLI().options.compilerOptions.strict).toBe(true); 86 | }); 87 | 88 | it('reads from default entry file', () => { 89 | process.argv = ['node', 'typescript-documentation']; 90 | expect(runCLI().options.entry).toEqual( 91 | resolve(process.cwd(), 'src/index.ts') 92 | ); 93 | }); 94 | 95 | it('reads entry file path from command line options (long)', () => { 96 | process.argv = [ 97 | 'node', 98 | 'typescript-documentation', 99 | '--entry', 100 | './src/main.ts' 101 | ]; 102 | expect(runCLI().options.entry).toEqual( 103 | resolve(process.cwd(), 'src/main.ts') 104 | ); 105 | }); 106 | 107 | it('reads entry file path from command line options (short)', () => { 108 | process.argv = ['node', 'typescript-documentation', '-e', './src/main.ts']; 109 | expect(runCLI().options.entry).toEqual( 110 | resolve(process.cwd(), 'src/main.ts') 111 | ); 112 | }); 113 | 114 | it('reads entry file path from command line options (absolute)', () => { 115 | const path = resolve(process.cwd(), 'src/main.ts'); 116 | process.argv = ['node', 'typescript-documentation', '--entry', path]; 117 | expect(runCLI().options.entry).toEqual(path); 118 | }); 119 | 120 | it('reads output file from command line options (long)', () => { 121 | process.argv = ['node', 'typescript-documentation', '--output', 'test.md']; 122 | expect(runCLI().output[resolve(process.cwd(), 'test.md')]).toEqual( 123 | 'test docs' 124 | ); 125 | }); 126 | 127 | it('reads output file from command line options (short)', () => { 128 | process.argv = ['node', 'typescript-documentation', '-o', 'test.md']; 129 | expect(runCLI().output[resolve(process.cwd(), 'test.md')]).toEqual( 130 | 'test docs' 131 | ); 132 | }); 133 | 134 | it('reads output file from command line options (absolute)', () => { 135 | const path = resolve(process.cwd(), 'test.md'); 136 | process.argv = ['node', 'typescript-documentation', '--output', path]; 137 | expect(runCLI().output[path]).toEqual('test docs'); 138 | }); 139 | 140 | it('writes markdown to provided output file', () => { 141 | const path = resolve(process.cwd(), 'test.md'); 142 | process.argv = ['node', 'typescript-documentation', '--output', path]; 143 | expect(runCLI().output[path]).toEqual('test docs'); 144 | }); 145 | 146 | it('writes sections to separate output files', () => { 147 | process.argv = ['node', 'typescript-documentation', '--output', 'test.md']; 148 | expect(runCLI().output[resolve(process.cwd(), 'section1.md')]).toEqual( 149 | '# section1\n\ntest section' 150 | ); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /test/function.test.ts: -------------------------------------------------------------------------------- 1 | import { testDocumentation } from './utils'; 2 | 3 | describe('functions', () => { 4 | it('documents exported functions', () => { 5 | testDocumentation({ 6 | 'index.ts': ` 7 | /** 8 | * Simple function description 9 | * line 2 10 | * @see {@link https://test.url.1|Example url 1} 11 | * @see {@link https://test.url.2|Example url 2} 12 | * @example 13 | * example 1 line 1 14 | * example 1 line 2 15 | * @example 16 | * example 2 line 1 17 | * example 2 line 2 18 | * @param a first parameter description 19 | * @param b second parameter description 20 | */ 21 | export function simpleFunction(a: string, b?: number): string { 22 | return a; 23 | } 24 | `, 25 | markdown: ` 26 | ## simpleFunction(a, b) 27 | 28 | Simple function description 29 | line 2 30 | 31 | **PARAMETERS** 32 | 33 | - \`a\`: string - first parameter description 34 | - \`b?\`: number - second parameter description 35 | 36 | **RETURNS** 37 | 38 | string 39 | 40 | **EXAMPLES** 41 | 42 | \`\`\`typescript 43 | example 1 line 1 44 | example 1 line 2 45 | \`\`\` 46 | 47 | \`\`\`typescript 48 | example 2 line 1 49 | example 2 line 2 50 | \`\`\` 51 | 52 | **SEE ALSO** 53 | 54 | - [Example url 1](https://test.url.1) 55 | - [Example url 2](https://test.url.2) 56 | ` 57 | }); 58 | }); 59 | 60 | it('documents minimal information', () => { 61 | testDocumentation({ 62 | 'index.ts': ` 63 | export function simpleFunction(a: string, b: number): string { 64 | return a + b; 65 | } 66 | `, 67 | markdown: ` 68 | ## simpleFunction(a, b) 69 | 70 | **PARAMETERS** 71 | 72 | - \`a\`: string 73 | - \`b\`: number 74 | 75 | **RETURNS** 76 | 77 | string 78 | ` 79 | }); 80 | }); 81 | 82 | it('documents as dependency', () => { 83 | testDocumentation({ 84 | 'dependency.ts': ` 85 | export function simpleFunction(): void {} 86 | `, 87 | 'index.ts': ` 88 | export * from './dependency'; 89 | `, 90 | markdown: ` 91 | ## simpleFunction() 92 | 93 | **RETURNS** 94 | 95 | void 96 | ` 97 | }); 98 | }); 99 | 100 | it(`doesn't documents as dependency if not exported`, () => { 101 | testDocumentation({ 102 | 'dependency.ts': ` 103 | export function simpleFunction(): void {} 104 | `, 105 | 'index.ts': ``, 106 | markdown: `` 107 | }); 108 | }); 109 | 110 | it(`doesn't document not exported functions`, () => { 111 | testDocumentation({ 112 | 'index.ts': ` 113 | function simpleFunction(a: string, b: number): string { 114 | return a + b; 115 | } 116 | `, 117 | markdown: `` 118 | }); 119 | }); 120 | 121 | it(`doesn't document internal functions`, () => { 122 | testDocumentation({ 123 | 'index.ts': ` 124 | /** 125 | * @internal 126 | */ 127 | export function simpleFunction(a: string, b: number): string { 128 | return a + b; 129 | } 130 | `, 131 | markdown: `` 132 | }); 133 | }); 134 | 135 | it('documents functions with nested object parameters', () => { 136 | testDocumentation({ 137 | 'index.ts': ` 138 | export function simpleFunction(a: { 139 | b: string 140 | }): void {} 141 | `, 142 | markdown: ` 143 | ## simpleFunction(a) 144 | 145 | **PARAMETERS** 146 | 147 | - \`a\`: object 148 | - \`b\`: string 149 | 150 | **RETURNS** 151 | 152 | void 153 | ` 154 | }); 155 | }); 156 | 157 | it('documents function return type as dependency', () => { 158 | testDocumentation({ 159 | 'dependency.ts': ` 160 | export let a: boolean; 161 | export type SimpleType = {}; 162 | `, 163 | 'index.ts': ` 164 | import { SimpleType } from './dependency'; 165 | export function simpleFunction(): SimpleType {} 166 | export * from './dependency'; 167 | `, 168 | markdown: ` 169 | ## simpleFunction() 170 | 171 | **RETURNS** 172 | 173 | [SimpleType](#simpletype) 174 | 175 | ## SimpleType 176 | 177 | ## a 178 | 179 | **TYPE** 180 | 181 | boolean 182 | ` 183 | }); 184 | }); 185 | 186 | it('documents function parameter type as dependency', () => { 187 | testDocumentation({ 188 | 'dependency.ts': ` 189 | export let a: boolean; 190 | export type SimpleType = {}; 191 | `, 192 | 'index.ts': ` 193 | import { SimpleType } from './dependency'; 194 | export function simpleFunction(a: SimpleType): void {} 195 | export * from './dependency'; 196 | `, 197 | markdown: ` 198 | ## simpleFunction(a) 199 | 200 | **PARAMETERS** 201 | 202 | - \`a\`: [SimpleType](#simpletype) 203 | 204 | **RETURNS** 205 | 206 | void 207 | 208 | ## SimpleType 209 | 210 | ## a 211 | 212 | **TYPE** 213 | 214 | boolean 215 | ` 216 | }); 217 | }); 218 | }); 219 | -------------------------------------------------------------------------------- /src/symbol.ts: -------------------------------------------------------------------------------- 1 | import { Symbol, SymbolFlags, Type, TypeFlags } from 'typescript'; 2 | import { getClassDependencies, renderClass } from './class'; 3 | import { DependencyContext, RenderContext } from './context'; 4 | import { renderEnumeration } from './enumeration'; 5 | import { getFunctionDependencies, renderFunction } from './function'; 6 | import { joinSections } from './markdown'; 7 | import { getTypeDependencies, getTypeLiteralDependencies } from './type'; 8 | import { renderTypeDeclaration } from './typeDeclaration'; 9 | import { 10 | findExactMatchingSymbolFlags, 11 | getDeclarationSourceLocation, 12 | inspectObject, 13 | SupportError, 14 | } from './utils'; 15 | import { renderVariable } from './variable'; 16 | 17 | function renderDeclaration( 18 | symbol: Symbol, 19 | aliasedSymbol: Symbol, 20 | type: Type, 21 | context: RenderContext 22 | ): string { 23 | const flags = aliasedSymbol.getFlags(); 24 | 25 | if (flags & SymbolFlags.BlockScopedVariable) { 26 | return renderVariable(symbol, aliasedSymbol, type, context); 27 | } 28 | 29 | if (flags & SymbolFlags.Function || flags & SymbolFlags.Method) { 30 | return renderFunction(symbol, aliasedSymbol, type, context); 31 | } 32 | 33 | if (flags & SymbolFlags.Class) { 34 | return renderClass(symbol, aliasedSymbol, context); 35 | } 36 | 37 | /* istanbul ignore next */ 38 | if (flags & SymbolFlags.Property || flags & SymbolFlags.Constructor) { 39 | return ''; 40 | } 41 | 42 | if (flags & SymbolFlags.TypeAlias || flags & SymbolFlags.Interface) { 43 | return renderTypeDeclaration(symbol, aliasedSymbol, type, context); 44 | } 45 | 46 | /* istanbul ignore else */ 47 | if (flags & SymbolFlags.RegularEnum && type.isUnion()) { 48 | return renderEnumeration(symbol, aliasedSymbol, type, context); 49 | } 50 | 51 | /* istanbul ignore next */ 52 | throw new SupportError( 53 | `Unsupported symbol ${inspectObject( 54 | symbol 55 | )} with flags "${findExactMatchingSymbolFlags(flags)}"` 56 | ); 57 | } 58 | 59 | export function getSymbolDependencies( 60 | symbol: Symbol, 61 | context: DependencyContext 62 | ): Symbol[] { 63 | if (context.resolutionPath.find((p) => p === symbol)) { 64 | return []; 65 | } 66 | 67 | let flags = symbol.getFlags(); 68 | 69 | if (flags & SymbolFlags.Alias) { 70 | symbol = context.typeChecker.getAliasedSymbol(symbol); 71 | flags = symbol.getFlags(); 72 | } 73 | 74 | const newContext = { 75 | ...context, 76 | resolutionPath: [...context.resolutionPath, symbol], 77 | }; 78 | 79 | const declarations = 80 | symbol.getDeclarations() || /* istanbul ignore next */ []; 81 | 82 | return declarations.reduce((dependencies, declaration) => { 83 | const type = context.typeChecker.getTypeAtLocation(declaration); 84 | 85 | if ( 86 | type.getFlags() & TypeFlags.Any || 87 | flags & SymbolFlags.TypeParameter || 88 | flags & SymbolFlags.RegularEnum 89 | ) { 90 | return dependencies; 91 | } 92 | 93 | if ( 94 | flags & SymbolFlags.Function || 95 | flags & SymbolFlags.Method || 96 | flags & SymbolFlags.Constructor 97 | ) { 98 | return [...dependencies, ...getFunctionDependencies(type, newContext)]; 99 | } 100 | 101 | if (flags & SymbolFlags.TypeLiteral || flags & SymbolFlags.Interface) { 102 | /* istanbul ignore next */ 103 | if (flags & SymbolFlags.Transient) { 104 | return []; 105 | } 106 | 107 | return [ 108 | ...dependencies, 109 | ...getTypeLiteralDependencies(symbol, newContext), 110 | ]; 111 | } 112 | 113 | if ( 114 | flags & SymbolFlags.FunctionScopedVariable || 115 | flags & SymbolFlags.BlockScopedVariable || 116 | flags & SymbolFlags.TypeAlias || 117 | flags & SymbolFlags.Property 118 | ) { 119 | return [ 120 | ...dependencies, 121 | ...getTypeDependencies(symbol, type, newContext), 122 | ]; 123 | } 124 | 125 | /* istanbul ignore else */ 126 | if (flags & SymbolFlags.Class) { 127 | if (!context.exportedSymbols.includes(symbol)) { 128 | return dependencies; 129 | } 130 | 131 | return [...dependencies, ...getClassDependencies(symbol, newContext)]; 132 | } 133 | 134 | /* istanbul ignore next */ 135 | throw new SupportError( 136 | `Unsupported symbol ${inspectObject( 137 | symbol 138 | )} with flags "${findExactMatchingSymbolFlags(flags)}"\n${getDeclarationSourceLocation(declaration)}` 139 | ); 140 | }, []); 141 | } 142 | 143 | export function renderSymbol(symbol: Symbol, context: RenderContext): string { 144 | const flags = symbol.getFlags(); 145 | const declarations = symbol.getDeclarations(); 146 | const aliasedSymbol = 147 | flags & SymbolFlags.Alias 148 | ? context.typeChecker.getAliasedSymbol(symbol) 149 | : symbol; 150 | 151 | /* istanbul ignore else */ 152 | if (declarations) { 153 | return joinSections( 154 | declarations.map((declaration) => { 155 | try { 156 | return renderDeclaration( 157 | symbol, 158 | aliasedSymbol, 159 | context.typeChecker.getTypeAtLocation(declaration), 160 | context 161 | ); 162 | } catch (error) { 163 | /* istanbul ignore next */ 164 | if (error instanceof SupportError) { 165 | /* istanbul ignore next */ 166 | throw new Error( 167 | [error.message, getDeclarationSourceLocation(declaration)].join( 168 | '\n' 169 | ) 170 | ); 171 | } else { 172 | /* istanbul ignore next */ 173 | throw error; 174 | } 175 | } 176 | }) 177 | ); 178 | } else { 179 | return ''; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /test/class.test.ts: -------------------------------------------------------------------------------- 1 | import { testDocumentation } from './utils'; 2 | 3 | describe('classes', () => { 4 | it('documents exported classes', () => { 5 | testDocumentation({ 6 | 'index.ts': ` 7 | /** 8 | * Simple class description 9 | * line 2 10 | * @see {@link https://test.url.1|Example url 1} 11 | * @see {@link https://test.url.2|Example url 2} 12 | * @example 13 | * example 1 line 1 14 | * example 1 line 2 15 | * @example 16 | * example 2 line 1 17 | * example 2 line 2 18 | */ 19 | export class SimpleClass { 20 | /** 21 | * simpleMethod1 description 22 | * line 2 23 | * @see {@link https://test.url.3|Example url 3} 24 | * @see {@link https://test.url.4|Example url 4} 25 | * @example 26 | * example 3 line 1 27 | * example 3 line 2 28 | * @example 29 | * example 4 line 1 30 | * example 4 line 2 31 | */ 32 | public simpleMethod1(): void { 33 | return; 34 | } 35 | 36 | /** 37 | * simpleMethod2 description 38 | * line 2 39 | */ 40 | public simpleMethod2(a: string, b: number): string { 41 | return a + b; 42 | } 43 | } 44 | `, 45 | markdown: ` 46 | ## SimpleClass 47 | 48 | Simple class description 49 | line 2 50 | 51 | **EXAMPLES** 52 | 53 | \`\`\`typescript 54 | example 1 line 1 55 | example 1 line 2 56 | \`\`\` 57 | 58 | \`\`\`typescript 59 | example 2 line 1 60 | example 2 line 2 61 | \`\`\` 62 | 63 | **SEE ALSO** 64 | 65 | - [Example url 1](https://test.url.1) 66 | - [Example url 2](https://test.url.2) 67 | 68 | ## simpleClass.simpleMethod1() 69 | 70 | simpleMethod1 description 71 | line 2 72 | 73 | **RETURNS** 74 | 75 | void 76 | 77 | **EXAMPLES** 78 | 79 | \`\`\`typescript 80 | example 3 line 1 81 | example 3 line 2 82 | \`\`\` 83 | 84 | \`\`\`typescript 85 | example 4 line 1 86 | example 4 line 2 87 | \`\`\` 88 | 89 | **SEE ALSO** 90 | 91 | - [Example url 3](https://test.url.3) 92 | - [Example url 4](https://test.url.4) 93 | 94 | ## simpleClass.simpleMethod2(a, b) 95 | 96 | simpleMethod2 description 97 | line 2 98 | 99 | **PARAMETERS** 100 | 101 | - \`a\`: string 102 | - \`b\`: number 103 | 104 | **RETURNS** 105 | 106 | string 107 | ` 108 | }); 109 | }); 110 | 111 | it('documents minimal information', () => { 112 | testDocumentation({ 113 | 'index.ts': ` 114 | export class SimpleClass { 115 | public simpleMethod1(): void { 116 | return; 117 | } 118 | 119 | public simpleMethod2(a: string, b: number): string { 120 | return a + b; 121 | } 122 | } 123 | `, 124 | markdown: ` 125 | ## SimpleClass 126 | 127 | ## simpleClass.simpleMethod1() 128 | 129 | **RETURNS** 130 | 131 | void 132 | 133 | ## simpleClass.simpleMethod2(a, b) 134 | 135 | **PARAMETERS** 136 | 137 | - \`a\`: string 138 | - \`b\`: number 139 | 140 | **RETURNS** 141 | 142 | string 143 | ` 144 | }); 145 | }); 146 | 147 | it('documents as dependency', () => { 148 | testDocumentation({ 149 | 'dependency.ts': ` 150 | export class SimpleClass { 151 | public simpleMethod1(): void { 152 | return; 153 | } 154 | } 155 | `, 156 | 'index.ts': ` 157 | import { SimpleClass } from './dependency'; 158 | 159 | export let testVariable: SimpleClass; 160 | export * from './dependency'; 161 | `, 162 | markdown: ` 163 | ## testVariable 164 | 165 | **TYPE** 166 | 167 | [SimpleClass](#simpleclass) 168 | 169 | ## SimpleClass 170 | 171 | ## simpleClass.simpleMethod1() 172 | 173 | **RETURNS** 174 | 175 | void 176 | ` 177 | }); 178 | }); 179 | 180 | it(`doesn't documents as dependency if not exported`, () => { 181 | testDocumentation({ 182 | 'dependency.ts': ` 183 | export class SimpleClass { 184 | public simpleMethod1(): void { 185 | return; 186 | } 187 | } 188 | `, 189 | 'index.ts': ` 190 | import { SimpleClass } from './dependency'; 191 | 192 | export let testVariable: SimpleClass; 193 | `, 194 | markdown: ` 195 | ## testVariable 196 | 197 | **TYPE** 198 | 199 | SimpleClass 200 | ` 201 | }); 202 | }); 203 | 204 | it(`doesn't document not exported classes`, () => { 205 | testDocumentation({ 206 | 'index.ts': ` 207 | class SimpleClass {} 208 | `, 209 | markdown: `` 210 | }); 211 | }); 212 | 213 | it(`doesn't document internal classes`, () => { 214 | testDocumentation({ 215 | 'index.ts': ` 216 | /** 217 | * @internal 218 | */ 219 | export class SimpleClass {} 220 | `, 221 | markdown: `` 222 | }); 223 | }); 224 | 225 | it(`doesn't document internal methods`, () => { 226 | testDocumentation({ 227 | 'index.ts': ` 228 | export class SimpleClass { 229 | /** 230 | * @internal 231 | */ 232 | public simpleMethod1(): void { 233 | return; 234 | } 235 | } 236 | `, 237 | markdown: ` 238 | ## SimpleClass 239 | ` 240 | }); 241 | }); 242 | 243 | it('documents empty class', () => { 244 | testDocumentation({ 245 | 'index.ts': ` 246 | export class SimpleClass {} 247 | `, 248 | markdown: ` 249 | ## SimpleClass 250 | ` 251 | }); 252 | }); 253 | }); 254 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompilerHost, 3 | createSourceFile, 4 | Declaration, 5 | Diagnostic, 6 | formatDiagnostic, 7 | JSDocTagInfo, 8 | ObjectFlags, 9 | ScriptTarget, 10 | SourceFile, 11 | Symbol, 12 | SymbolDisplayPart, 13 | SymbolFlags, 14 | Type, 15 | TypeFlags, 16 | TypeReference, 17 | } from 'typescript'; 18 | import { inspect } from 'util'; 19 | 20 | export function isInternalSymbol(symbol: Symbol): boolean { 21 | return symbol.getJsDocTags().some((tag) => tag.name === 'internal'); 22 | } 23 | 24 | export function getSymbolSection(symbol: Symbol): string { 25 | const sectionTag = symbol 26 | .getJsDocTags() 27 | .find((tag) => tag.name === 'section'); 28 | 29 | return (sectionTag && getSymbolDisplayText(sectionTag)) || 'default'; 30 | } 31 | 32 | export function createCompilerHost(sourceCode: { 33 | [name: string]: string; 34 | }): CompilerHost { 35 | return { 36 | getSourceFile: (name: string): SourceFile => 37 | createSourceFile( 38 | name, 39 | (sourceCode && sourceCode[name]) || '', 40 | ScriptTarget.Latest 41 | ), 42 | // eslint-disable-next-line @typescript-eslint/no-empty-function 43 | writeFile: (): void => {}, 44 | getDefaultLibFileName: (): string => 'lib.d.ts', 45 | useCaseSensitiveFileNames: (): boolean => false, 46 | getCanonicalFileName: (filename: string): string => filename, 47 | getCurrentDirectory: (): string => '', 48 | getNewLine: (): string => '\n', 49 | getDirectories: (): string[] => [], 50 | fileExists: (): boolean => true, 51 | readFile: (): string => '', 52 | }; 53 | } 54 | 55 | function isNumeric( 56 | value: [string, string | number] 57 | ): value is [string, number] { 58 | return typeof value[1] === 'number'; 59 | } 60 | 61 | export function getSymbolDisplayText(tag: JSDocTagInfo): string { 62 | return tag.text?.map(({ text }: SymbolDisplayPart) => text).join('') || ''; 63 | } 64 | 65 | export function findExactMatchingTypeFlag(flags: TypeFlags): string { 66 | const match = Object.keys(TypeFlags) 67 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 68 | .map<[string, string | number]>((key) => [key, TypeFlags[key as any]]) 69 | .filter(isNumeric) 70 | .find(([, value]) => Math.log2(value) % 1 === 0 && value & flags); 71 | 72 | if (!match) { 73 | throw new Error(`No exact matching flag for ${flags}`); 74 | } 75 | 76 | return match[0]; 77 | } 78 | 79 | export function findMatchingTypeFlags(type: Type): string[] { 80 | const flags = type.getFlags(); 81 | 82 | return ( 83 | Object.keys(TypeFlags) 84 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 85 | .map<[string, string | number]>((key) => [key, TypeFlags[key as any]]) 86 | .filter(isNumeric) 87 | .filter(([, value]) => value & flags) 88 | .map(([key]) => key) 89 | ); 90 | } 91 | 92 | export function findMatchingObjectsFlags(type: Type): string[] { 93 | const flags = (type as TypeReference).objectFlags; 94 | 95 | return ( 96 | Object.keys(ObjectFlags) 97 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 98 | .map<[string, string | number]>((key) => [key, ObjectFlags[key as any]]) 99 | .filter(isNumeric) 100 | .filter(([, value]) => value & flags) 101 | .map(([key]) => key) 102 | ); 103 | } 104 | 105 | export function findMatchingSymbolFlags(symbol: Symbol): string[] { 106 | const flags = symbol.getFlags(); 107 | 108 | return ( 109 | Object.keys(SymbolFlags) 110 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 111 | .map<[string, string | number]>((key) => [key, SymbolFlags[key as any]]) 112 | .filter(isNumeric) 113 | .filter(([, value]) => value & flags) 114 | .map(([key]) => key) 115 | ); 116 | } 117 | 118 | export function findExactMatchingSymbolFlags(flags: SymbolFlags): string { 119 | const match = Object.keys(SymbolFlags) 120 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 121 | .map<[string, string | number]>((key) => [key, SymbolFlags[key as any]]) 122 | .filter(isNumeric) 123 | .find(([, value]) => Math.log2(value) % 1 === 0 && value & flags); 124 | 125 | if (!match) { 126 | throw new Error(`No exact matching flag for ${flags}`); 127 | } 128 | 129 | return match[0]; 130 | } 131 | 132 | export function formatDiagnosticError(diagnostic: Diagnostic): string { 133 | return formatDiagnostic(diagnostic, { 134 | getCurrentDirectory: (): string => process.cwd(), 135 | getCanonicalFileName: (fileName: string): string => fileName, 136 | getNewLine: (): string => '\n', 137 | }); 138 | } 139 | 140 | export function getDeclarationSourceLocation(declaration: Declaration): string { 141 | const sourceFile = declaration.getSourceFile(); 142 | const pos = sourceFile.getLineAndCharacterOfPosition(declaration.getStart()); 143 | const fileNameWithPosition = [ 144 | sourceFile.fileName, 145 | pos.line, 146 | pos.character, 147 | ].join(':'); 148 | const line = sourceFile.getFullText().split('\n')[pos.line]; 149 | const indentationMatch = /^([ \t]*)(?=\S)/.exec(line); 150 | const indentation = indentationMatch ? indentationMatch[1].length : 0; 151 | const lineWithoutIndentation = indentationMatch 152 | ? line.substr(indentation) 153 | : line; 154 | const posMarker = `${' '.repeat(pos.character - indentation)}^`; 155 | return [`at ${fileNameWithPosition}`, lineWithoutIndentation, posMarker].join( 156 | '\n' 157 | ); 158 | } 159 | 160 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 161 | export function inspectObject(type: any): string { 162 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 163 | const obj = Object.keys(type) 164 | .filter((key) => ['checker'].includes(key)) 165 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access 166 | .reduce((newObj, key) => Object.assign(newObj, { [key]: type[key] }), {}); 167 | 168 | return inspect(obj, false, 1, true); 169 | } 170 | 171 | export class SupportError extends Error {} 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typescript-documentation 2 | 3 | [![npm version](https://badge.fury.io/js/typescript-documentation.svg)](https://www.npmjs.com/package/typescript-documentation) 4 | [![Build Status](https://github.com/mucsi96/typescript-documentation/workflows/Build/badge.svg)](https://github.com/mucsi96/typescript-documentation/actions?query=workflow%3ABuild+branch%3Amaster) 5 | [![Coverage Status](https://coveralls.io/repos/github/mucsi96/typescript-documentation/badge.svg?branch=master)](https://coveralls.io/github/mucsi96/typescript-documentation?branch=master) 6 | [![npm](https://img.shields.io/npm/dw/typescript-documentation)](https://www.npmjs.com/package/typescript-documentation) 7 | [![github](https://img.shields.io/badge/PRs-welcome-blue.svg)](https://github.com/mucsi96/typescript-documentation) 8 | 9 | Generate markdown API documentation directly from TypeScript source code. 10 | 11 | # Usage 12 | 13 | ``` 14 | npm i typescript-documentation 15 | ``` 16 | 17 | ``` 18 | > typescript-documentation [options] 19 | 20 | Options: 21 | -p, --project relative or absolute path to a tsconfig.json file (default: "./tsconfig.json") 22 | -e, --entry
entry/main file of project (default: "./src/index.ts") 23 | -o, --output markdown documentation output file location (default: "./docs/README.md") 24 | -h, --help output usage information 25 | ``` 26 | 27 | [Live example output](https://mucsi96.gitbook.io/w3c-webdriver/) 28 | 29 | # Documenting variables 30 | 31 | _Example input:_ 32 | 33 | ```typescript 34 | /** 35 | * Simple variable description 36 | * line 2 37 | * @see {@link https://test.url.1|Example url 1} 38 | * @see {@link https://test.url.2|Example url 2} 39 | * @example 40 | * example 1 line 1 41 | * example 1 line 2 42 | * @example 43 | * example 2 line 1 44 | * example 2 line 2 45 | */ 46 | export const simpleVariable: number = 1; 47 | ``` 48 | 49 | _Example output:_ 50 | 51 | ## simpleVariable 52 | 53 | Simple variable description 54 | line 2 55 | 56 | **TYPE** 57 | 58 | number 59 | 60 | **EXAMPLES** 61 | 62 | ```typescript 63 | example 1 line 1 64 | example 1 line 2 65 | ``` 66 | 67 | ```typescript 68 | example 2 line 1 69 | example 2 line 2 70 | ``` 71 | 72 | **SEE ALSO** 73 | 74 | - [Example url 1](https://test.url.1) 75 | - [Example url 2](https://test.url.2) 76 | 77 | # Documenting functions 78 | 79 | _Example input:_ 80 | 81 | ```typescript 82 | /** 83 | * Simple function description 84 | * line 2 85 | * @see {@link https://test.url.1|Example url 1} 86 | * @see {@link https://test.url.2|Example url 2} 87 | * @example 88 | * example 1 line 1 89 | * example 1 line 2 90 | * @example 91 | * example 2 line 1 92 | * example 2 line 2 93 | * @param a first parameter description 94 | * @param b second parameter description 95 | */ 96 | export function simpleFunction(a: string, b?: number): string { 97 | return a; 98 | } 99 | ``` 100 | 101 | _Example output:_ 102 | 103 | ## simpleFunction(a, b) 104 | 105 | Simple function description 106 | line 2 107 | 108 | **PARAMETERS** 109 | 110 | - `a`: string - first parameter description 111 | - `b?`: number - second parameter description 112 | 113 | **RETURNS** 114 | 115 | string 116 | 117 | **EXAMPLES** 118 | 119 | ```typescript 120 | example 1 line 1 121 | example 1 line 2 122 | ``` 123 | 124 | ```typescript 125 | example 2 line 1 126 | example 2 line 2 127 | ``` 128 | 129 | **SEE ALSO** 130 | 131 | - [Example url 1](https://test.url.1) 132 | - [Example url 2](https://test.url.2) 133 | 134 | # Documenting classes 135 | 136 | _Example input:_ 137 | 138 | ```typescript 139 | /** 140 | * Simple class description 141 | * line 2 142 | * @see {@link https://test.url.1|Example url 1} 143 | * @see {@link https://test.url.2|Example url 2} 144 | * @example 145 | * example 1 line 1 146 | * example 1 line 2 147 | * @example 148 | * example 2 line 1 149 | * example 2 line 2 150 | */ 151 | export class SimpleClass { 152 | /** 153 | * simpleMethod1 description 154 | * line 2 155 | * @see {@link https://test.url.3|Example url 3} 156 | * @see {@link https://test.url.4|Example url 4} 157 | * @example 158 | * example 3 line 1 159 | * example 3 line 2 160 | * @example 161 | * example 4 line 1 162 | * example 4 line 2 163 | */ 164 | public simpleMethod1(): void { 165 | return; 166 | } 167 | 168 | /** 169 | * simpleMethod2 description 170 | * line 2 171 | * @param a first parameter description 172 | * @param b second parameter description 173 | */ 174 | public simpleMethod2(a: string, b: number): string { 175 | return a + b; 176 | } 177 | } 178 | ``` 179 | 180 | _Example output:_ 181 | 182 | ## SimpleClass 183 | 184 | Simple class description 185 | line 2 186 | 187 | **EXAMPLES** 188 | 189 | ```typescript 190 | example 1 line 1 191 | example 1 line 2 192 | ``` 193 | 194 | ```typescript 195 | example 2 line 1 196 | example 2 line 2 197 | ``` 198 | 199 | **SEE ALSO** 200 | 201 | - [Example url 1](https://test.url.1) 202 | - [Example url 2](https://test.url.2) 203 | 204 | ## simpleClass.simpleMethod1() 205 | 206 | simpleMethod1 description 207 | line 2 208 | 209 | **RETURNS** 210 | 211 | void 212 | 213 | **EXAMPLES** 214 | 215 | ```typescript 216 | example 3 line 1 217 | example 3 line 2 218 | ``` 219 | 220 | ```typescript 221 | example 4 line 1 222 | example 4 line 2 223 | ``` 224 | 225 | **SEE ALSO** 226 | 227 | - [Example url 3](https://test.url.3) 228 | - [Example url 4](https://test.url.4) 229 | 230 | ## simpleClass.simpleMethod2(a, b) 231 | 232 | simpleMethod2 description 233 | line 2 234 | 235 | **PARAMETERS** 236 | 237 | - `a`: string - first parameter description 238 | - `b`: number - second parameter description 239 | 240 | **RETURNS** 241 | 242 | string 243 | 244 | # Documenting types 245 | 246 | _Example input:_ 247 | 248 | ```typescript 249 | /** 250 | * Simple type description 251 | * line 2 252 | * @see {@link https://test.url.1|Example url 1} 253 | * @see {@link https://test.url.2|Example url 2} 254 | * @example 255 | * example 1 line 1 256 | * example 1 line 2 257 | * @example 258 | * example 2 line 1 259 | * example 2 line 2 260 | */ 261 | export type SimpleType = { 262 | /** 263 | * first property description 264 | */ 265 | a: string; 266 | 267 | /** 268 | * second property description 269 | */ 270 | b?: number; 271 | }; 272 | ``` 273 | 274 | _Example output:_ 275 | 276 | ## SimpleType 277 | 278 | Simple type description 279 | line 2 280 | 281 | **PROPERTIES** 282 | 283 | - `a`: string - first property description 284 | - `b?`: number - second property description 285 | 286 | **EXAMPLES** 287 | 288 | ```typescript 289 | example 1 line 1 290 | example 1 line 2 291 | ``` 292 | 293 | ```typescript 294 | example 2 line 1 295 | example 2 line 2 296 | ``` 297 | 298 | **SEE ALSO** 299 | 300 | - [Example url 1](https://test.url.1) 301 | - [Example url 2](https://test.url.2) 302 | 303 | # Documenting enumerations 304 | 305 | _Example input:_ 306 | 307 | ```typescript 308 | /** 309 | * Simple enumeration description 310 | * line 2 311 | * @see {@link https://test.url.1|Example url 1} 312 | * @see {@link https://test.url.2|Example url 2} 313 | * @example 314 | * example 1 line 1 315 | * example 1 line 2 316 | * @example 317 | * example 2 line 1 318 | * example 2 line 2 319 | */ 320 | export enum SimpleEnum { 321 | ONE, 322 | TWO 323 | } 324 | ``` 325 | 326 | _Example output:_ 327 | 328 | ## SimpleEnum 329 | 330 | Simple enumeration description 331 | line 2 332 | 333 | **POSSIBLE VALUES** 334 | 335 | - `ONE` 336 | - `TWO` 337 | 338 | **EXAMPLES** 339 | 340 | ```typescript 341 | example 1 line 1 342 | example 1 line 2 343 | ``` 344 | 345 | ```typescript 346 | example 2 line 1 347 | example 2 line 2 348 | ``` 349 | 350 | **SEE ALSO** 351 | 352 | - [Example url 1](https://test.url.1) 353 | - [Example url 2](https://test.url.2) 354 | -------------------------------------------------------------------------------- /test/type.test.ts: -------------------------------------------------------------------------------- 1 | import { testDocumentation } from './utils'; 2 | 3 | describe('type', () => { 4 | it('documents unions', () => { 5 | testDocumentation({ 6 | 'index.ts': ` 7 | export let testVariable: string | number; 8 | `, 9 | markdown: ` 10 | ## testVariable 11 | 12 | **TYPE** 13 | 14 | string | number 15 | ` 16 | }); 17 | }); 18 | 19 | it('documents objects', () => { 20 | testDocumentation({ 21 | 'index.ts': ` 22 | type SimpleObjectType = {} 23 | export let testVariable: SimpleObjectType; 24 | `, 25 | markdown: ` 26 | ## testVariable 27 | 28 | **TYPE** 29 | 30 | SimpleObjectType 31 | ` 32 | }); 33 | }); 34 | 35 | it('documents general objects', () => { 36 | testDocumentation({ 37 | 'index.ts': ` 38 | export let testVariable: object; 39 | `, 40 | markdown: ` 41 | ## testVariable 42 | 43 | **TYPE** 44 | 45 | object 46 | ` 47 | }); 48 | }); 49 | 50 | it('documents exported objects', () => { 51 | testDocumentation({ 52 | 'index.ts': ` 53 | export type SimpleObjectType = {}; 54 | export let testVariable: SimpleObjectType; 55 | `, 56 | markdown: ` 57 | ## SimpleObjectType 58 | 59 | ## testVariable 60 | 61 | **TYPE** 62 | 63 | [SimpleObjectType](#simpleobjecttype) 64 | ` 65 | }); 66 | }); 67 | 68 | it(`doesn't documents internal objects`, () => { 69 | testDocumentation({ 70 | 'index.ts': ` 71 | /** 72 | * @internal 73 | */ 74 | export type SimpleObjectType = {}; 75 | export let testVariable: SimpleObjectType; 76 | `, 77 | markdown: ` 78 | ## testVariable 79 | 80 | **TYPE** 81 | 82 | SimpleObjectType 83 | ` 84 | }); 85 | }); 86 | 87 | it('references to types', () => { 88 | testDocumentation({ 89 | 'index.ts': ` 90 | export type UnionType = string | number; 91 | export let testVariable: UnionType; 92 | `, 93 | markdown: ` 94 | ## UnionType 95 | 96 | **POSSIBLE VALUES** 97 | 98 | - string 99 | - number 100 | 101 | ## testVariable 102 | 103 | **TYPE** 104 | 105 | [UnionType](#uniontype) 106 | 107 | ` 108 | }); 109 | }); 110 | 111 | it('documents any', () => { 112 | testDocumentation({ 113 | 'index.ts': ` 114 | export let testVariable: any; 115 | `, 116 | markdown: ` 117 | ## testVariable 118 | 119 | **TYPE** 120 | 121 | any 122 | ` 123 | }); 124 | }); 125 | 126 | it('documents unknown', () => { 127 | testDocumentation({ 128 | 'index.ts': ` 129 | export let testVariable: unknown; 130 | `, 131 | markdown: ` 132 | ## testVariable 133 | 134 | **TYPE** 135 | 136 | unknown 137 | ` 138 | }); 139 | }); 140 | 141 | it('documents interfaces', () => { 142 | testDocumentation({ 143 | 'index.ts': ` 144 | interface InterfaceType {}; 145 | export let testVariable: InterfaceType; 146 | `, 147 | markdown: ` 148 | ## testVariable 149 | 150 | **TYPE** 151 | 152 | InterfaceType 153 | ` 154 | }); 155 | }); 156 | 157 | it('documents exported interfaces', () => { 158 | testDocumentation({ 159 | 'index.ts': ` 160 | export interface InterfaceType {}; 161 | export let testVariable: InterfaceType; 162 | `, 163 | markdown: ` 164 | ## InterfaceType 165 | 166 | ## testVariable 167 | 168 | **TYPE** 169 | 170 | [InterfaceType](#interfacetype) 171 | ` 172 | }); 173 | }); 174 | 175 | it(`doesn't documents internal interfaces`, () => { 176 | testDocumentation({ 177 | 'index.ts': ` 178 | /** 179 | * @internal 180 | */ 181 | export interface InterfaceType {}; 182 | export let testVariable: InterfaceType; 183 | `, 184 | markdown: ` 185 | ## testVariable 186 | 187 | **TYPE** 188 | 189 | InterfaceType 190 | ` 191 | }); 192 | }); 193 | 194 | it('documents string literals', () => { 195 | testDocumentation({ 196 | 'index.ts': ` 197 | export let testVariable: 'test string literal'; 198 | `, 199 | markdown: ` 200 | ## testVariable 201 | 202 | **TYPE** 203 | 204 | \`'test string literal'\` 205 | ` 206 | }); 207 | }); 208 | 209 | it('documents nulls', () => { 210 | testDocumentation({ 211 | 'index.ts': ` 212 | export let testVariable: null; 213 | `, 214 | markdown: ` 215 | ## testVariable 216 | 217 | **TYPE** 218 | 219 | null 220 | ` 221 | }); 222 | }); 223 | 224 | it('documents booleans', () => { 225 | testDocumentation({ 226 | 'index.ts': ` 227 | export let testVariable: boolean; 228 | `, 229 | markdown: ` 230 | ## testVariable 231 | 232 | **TYPE** 233 | 234 | boolean 235 | ` 236 | }); 237 | }); 238 | 239 | it('documents typed arrays', () => { 240 | testDocumentation({ 241 | 'index.ts': ` 242 | export let testVariable: string[]; 243 | `, 244 | markdown: ` 245 | ## testVariable 246 | 247 | **TYPE** 248 | 249 | string[] 250 | ` 251 | }); 252 | }); 253 | 254 | it('documents type arguments', () => { 255 | testDocumentation({ 256 | 'index.ts': ` 257 | export let testVariable: Promise; 258 | `, 259 | markdown: ` 260 | ## testVariable 261 | 262 | **TYPE** 263 | 264 | Promise<string> 265 | ` 266 | }); 267 | }); 268 | 269 | it('documents functions with type parameter', () => { 270 | testDocumentation({ 271 | 'index.ts': ` 272 | export function simpleFunction(): Promise {} 273 | `, 274 | markdown: ` 275 | ## simpleFunction<T>() 276 | 277 | **RETURNS** 278 | 279 | Promise<T> 280 | ` 281 | }); 282 | }); 283 | 284 | it('documents exported object arrays', () => { 285 | testDocumentation({ 286 | 'index.ts': ` 287 | export type SimpleObjectType = {}; 288 | export let testVariable: SimpleObjectType[]; 289 | `, 290 | markdown: ` 291 | ## SimpleObjectType 292 | 293 | ## testVariable 294 | 295 | **TYPE** 296 | 297 | [SimpleObjectType](#simpleobjecttype)[] 298 | ` 299 | }); 300 | }); 301 | 302 | it('documents anonymous types', () => { 303 | testDocumentation({ 304 | 'index.ts': ` 305 | export let testVariable: { a: string }; 306 | `, 307 | markdown: ` 308 | ## testVariable 309 | 310 | **TYPE** 311 | 312 | object 313 | - \`a\`: string 314 | ` 315 | }); 316 | }); 317 | 318 | it('doesn`t documents members of non anonymous type', () => { 319 | testDocumentation({ 320 | 'index.ts': ` 321 | export let testVariable: Buffer; 322 | `, 323 | markdown: ` 324 | ## testVariable 325 | 326 | **TYPE** 327 | 328 | Buffer 329 | ` 330 | }); 331 | }); 332 | 333 | it('documents nested anonymous types', () => { 334 | testDocumentation({ 335 | 'index.ts': ` 336 | export let testVariable: { a: { b: string } }; 337 | `, 338 | markdown: ` 339 | ## testVariable 340 | 341 | **TYPE** 342 | 343 | object 344 | - \`a\`: object 345 | - \`b\`: string 346 | ` 347 | }); 348 | }); 349 | 350 | it('documents nested anonymous types with optional properties', () => { 351 | testDocumentation({ 352 | 'index.ts': ` 353 | export let testVariable: { a?: { b: string } }; 354 | `, 355 | markdown: ` 356 | ## testVariable 357 | 358 | **TYPE** 359 | 360 | object 361 | - \`a?\`: object 362 | - \`b\`: string 363 | ` 364 | }); 365 | }); 366 | 367 | it('documents nested anonymous types with optional properties with descriptions', () => { 368 | testDocumentation({ 369 | 'index.ts': ` 370 | export let testVariable: { 371 | /** 372 | * first property description 373 | */ 374 | a?: { 375 | /** 376 | * second property description 377 | */ 378 | b: string 379 | } 380 | }; 381 | `, 382 | markdown: ` 383 | ## testVariable 384 | 385 | **TYPE** 386 | 387 | object 388 | - \`a?\`: object - first property description 389 | - \`b\`: string - second property description 390 | ` 391 | }); 392 | }); 393 | 394 | it('documents array type as dependency', () => { 395 | testDocumentation({ 396 | 'dependency.ts': ` 397 | export let a: boolean; 398 | export type SimpleType = {}; 399 | `, 400 | 'index.ts': ` 401 | import { SimpleType } from './dependency'; 402 | export let testVariable: SimpleType[]; 403 | export * from './dependency'; 404 | `, 405 | markdown: ` 406 | ## testVariable 407 | 408 | **TYPE** 409 | 410 | [SimpleType](#simpletype)[] 411 | 412 | ## SimpleType 413 | 414 | ## a 415 | 416 | **TYPE** 417 | 418 | boolean 419 | ` 420 | }); 421 | }); 422 | 423 | it('documents type parameter as dependency', () => { 424 | testDocumentation({ 425 | 'dependency.ts': ` 426 | export let a: boolean; 427 | export type SimpleType = {}; 428 | `, 429 | 'index.ts': ` 430 | import { SimpleType } from './dependency'; 431 | export let testVariable: Promise; 432 | export * from './dependency'; 433 | `, 434 | markdown: ` 435 | ## testVariable 436 | 437 | **TYPE** 438 | 439 | Promise<[SimpleType](#simpletype)> 440 | 441 | ## SimpleType 442 | 443 | ## a 444 | 445 | **TYPE** 446 | 447 | boolean 448 | ` 449 | }); 450 | }); 451 | 452 | it('documents union type dependencies', () => { 453 | testDocumentation({ 454 | 'dependency.ts': ` 455 | export let a: boolean; 456 | export type SimpleTypeA = {}; 457 | export type SimpleTypeB = {}; 458 | `, 459 | 'index.ts': ` 460 | import { SimpleTypeA, SimpleTypeB } from './dependency'; 461 | export let testVariable: SimpleTypeA | SimpleTypeB; 462 | export * from './dependency'; 463 | `, 464 | markdown: ` 465 | ## testVariable 466 | 467 | **TYPE** 468 | 469 | [SimpleTypeA](#simpletypea) | [SimpleTypeB](#simpletypeb) 470 | 471 | ## SimpleTypeA 472 | 473 | ## SimpleTypeB 474 | 475 | ## a 476 | 477 | **TYPE** 478 | 479 | boolean 480 | ` 481 | }); 482 | }); 483 | }); 484 | --------------------------------------------------------------------------------