├── 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 | [](https://www.npmjs.com/package/typescript-documentation)
4 | [](https://github.com/mucsi96/typescript-documentation/actions?query=workflow%3ABuild+branch%3Amaster)
5 | [](https://coveralls.io/github/mucsi96/typescript-documentation?branch=master)
6 | [](https://www.npmjs.com/package/typescript-documentation)
7 | [](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 |
--------------------------------------------------------------------------------