ts-doc-gen-md

Documentation generation library in markdown, for typescript projects.

Usage no npm install needed!

<script type="module">
  import tsDocGenMd from 'https://cdn.skypack.dev/ts-doc-gen-md';
</script>

README

ts-doc-gen-md

Table of contents

Installation

npm install --save-dev ts-doc-gen-md

Description

Library that generates documentation in markdown for TypeScript projects. The created documentation is just the type signatures together with their respective raw JSDoc comments. For the documentation to be generated you have to provide the typings entry point of your project. Every type that is exported from there, together with the types it references, is resolved and documented. Imports and exports from node modules are documented as import and export statements, without further deeper resolution into the node module itself.

You can inject the markdown in the README.md file (take a look at md-in-place for that purpose) or create a separate markdown file. The markdown is made in such a way so that it is displayed properly on github. For the best results follow the best practices. The library will not work for the non covered cases.

Coverage

Code coverage is around 90%.

Best practices

  1. The internal API should depend on the public API, not the other way around.

    examples

    The following code snippet is an example of a typings entry point file that has its public API depend on the private API:

    export declare type iAmPublic = {
        prop1: boolean;
        prop2: Omit<iAmBad, "a">;
    };
    declare type iAmBad = {
        a: "a";
        b: "b";
    };
    

    The type iAmBad gets fully documented (both of its properties a and b are documented), while only the property b is needed for the public API. That is neither a fault or a lack of features by the documentation generation library. The public api being dependent on the private api, is to be blamed here. Here is how to inverse the dependency:

    export declare type iAmPublic = {
        prop1: boolean;
        prop2: iAmPublicAndNotPrivateDependent;
    };
    declare type iAmPublicAndNotPrivateDependent = { b: "b" };
    declare type iAmPrivateAndPublicDependent = {
        a: string;
    } & iAmPublicAndNotPrivateDependent;
    

    Now the generated documentation will not document property a.

    Similarly we can apply the same idea for other built in type functions (like : Pick, Parameters and ReturnType) that are usually used in way that makes the public API depend on the private API.

    Another similar example:

    export declare type iAmPublic = iAmBad["b"];
    declare type iAmBad = {
        a: "a";
        b: "b";
    };
    

    and here is how to achieve dependency inversion:

    export type iAmPublic = iAmPublicAndNotPrivateDependent;
    declare type iAmPublicAndNotPrivateDependent = "b";
    declare type iAmPrivateAndPublicDependent = {
        a: "a";
        b: iAmPublicAndNotPrivateDependent;
    };
    
  2. For projects that do not export many values from their entry point, it is advised to create a file called publicApi.ts, that will contain all the public api types. This file should be placed in the root of your source folder so that it is easily accessible.

  3. Keep the JSDoc comments concise.

  4. A part of the documentation has to be written in the README file and the JSDoc comments of your source code should have it as a context.

  5. Add a ruler in your code editor at a fixed column for all typescript files. This will aid you in creating JSDoc comments that do not overflow beyond that column.

  6. Functions with parameters that have initial values, do not get their initial values documented. Consider gathering all the parameters in an object and then using JSDoc comments with @default JSDoc tag to defined the initial value.

    example
    • bad:
      export function foo(a: number = 1) {
          //some code
      }
      
    • good:
      export function foo(parameters: {
          /**
           * @default 1
           */
          a: number;
      }) {
          //initialize parameter `a` here
          //some code
      }
      
  7. It would be a good idea to avoid using classes, if possible. The sole reason is that the JSDoc comments of the implemented interfaces do not appear in the IDE intellisense. This forces you to write the JSDoc comments in the class, which makes your public API documentation be scattered throughout your project.

  8. If your public api exposes regular expressions values, they will appear as type RegExp in the generated documentation. For that reason make sure that you write descriptive enough JSDoc comments that will appear in the documentation and explain what the regular expression does.

Non covered cases

  1. Function overloading

  2. TypeScript namespaces

  3. TypeScript decorators

  4. TypeScript triple slash directives

  5. CommonJS import/exports

  6. imports and exports from node modules are documented as import and export statements, without further deeper resolution into the node module itself

  7. chain export statements or type references that need to be resolved for documentation and on resolution stumble upon delegation export statements from node modules

    Example

    In the current example since the library does not get into node modules when resolving exports, it is not possible to know whether there is an exported type from node-module with identifier A.

    // index.ts
    export { A } from "aFile.ts";
    
    // aFile.ts
    export * from "node-module";
    
    export declare const A = 1;
    

Documentation

The function that generates documentation has been converted to CLI using fn-to-cli. That means that you can generate documentation using the CLI or by importing the function from the entry point of this node module.

Use in terminal the following command:

npx ts-doc-gen-md --help

to get the CLI documentation:

CLI syntax:

  ts-doc-gen-md tsDocGenMd? [[--<option> | -<flag>] <value>]#

Description:

  Generates documentation in markdown, given the typings entry point file of a
  typescript project.

Non required options:

  -i --input                 : string                  = "./dist/index.d.ts"  Path to the typings entry point file.
  -o --output                : string                  = undefined            Path to save the generated documentation. If no path is provided then no output file is created.
     --sort                  : boolean                 = false                Display in alphabetic order the documentation.
     --prefixHref            : string                  = ""                   Prefix for all the hyperlinks of the generated documentation.
     --headingStartingNumber : 2 | 3 | 4 | 5 | 6       = 3                    Number that specifies the starting heading number of the generated documentation.
     --format                : (src: string) => string = (src) => src         Used to format the code.

Motivation

I was looking to automate the documentation generation process for my TypeScript projects, and I realized that I had to create my own documentation generation library. I made that decision after I checked the available choices: typedoc, api-extractor, which back then (December of 2020), I found:

  1. difficult to be used
  2. opinionated
  3. slow at adopting features
  4. missing basic features
  5. producing documentation that is unfamiliar looking to the developer

Acknowledgements

You might find interesting

  • typedoc, documentation generation library for TypeScript projects
  • api-extractor, documentation generation library for TypeScript projects

FAQs

Why there is no option for generating documentation that has by default collapsed the details tags?

That is because, it will not make it possible to properly hyperlink to type references, since they will point to a collapsed details tag.

Will there ever be an option to generate documentation in html?

No. I consider that over-engineering. Markdown is enough for generating documentation even for complex libraries.

Why do I have to provide the typings entry point file instead of the entry point of my typescript project?

That is likely because I am not experienced enough with the typescript node module abstract syntax tree parser.

The d.ts files simplify complicated cases that appear in the .ts files. For example there is no need for me to do type inference and resolve destructured exports.

Contributing

I am open to suggestions/pull request to improve this program.

You will find the following commands useful:

  • Clones the github repository of this project:

    git clone https://github.com/lillallol/ts-doc-gen-md
    
  • Installs the node modules (nothing will work without them):

    npm install
    
  • Tests the source code:

    npm run test
    
  • Lints the source folder using typescript and eslint:

    npm run lint
    
  • Builds the typescript code from the ./src folder to javascript code in ./dist:

    npm run build-ts
    
  • Creates the CLI executable of this program in ./bin/bin.js:

    npm run build-bin
    

    Make sure that the ./dist exists when you execute this command, otherwise it will not work.

  • Injects in place the generated toc and imported files to README.md:

    npm run build-md
    
  • Checks the project for spelling mistakes:

    npm run spell-check
    

    Take a look at the related configuration ./cspell.json.

  • Checks ./src for dead typescript files:

    npm run dead-files
    

    Take a look at the related configuration ./unimportedrc.json.

Terminology

Here I define the terminology used in the source code, and give simple examples when necessary.

chain export statement

export * from "./some/where";
export * as x from "./some/where";
export { A, b as B, default as C, default } from "./some/where";
export { A, b as B, c as default };
export default foo;

delegation export statement

export * from "./some/where";

namespace export statement

export * as x from "./some/where";

named export path full

export { A, b as B, default as C, default } from "./some/where";

named export path less

export { A, b as B, c as default };

default export chain

export default foo;

export statement non chain

export function foo() {}
export class A {
    constructor() {}
}
export const a = 1,
    b = 2;
export interface I {}
export type obj = { a: number; b: string };
export enum Country {
    Germany,
    Sweden,
    USA,
}

Even without the export keyword, the previous statements are still referred as export statement non chain.

export default function foo() {}
export default class A {
    constructor() {}
}
export default const a = 1;
export default interface I {

}

export chain leaf statement

These are the statements that were located while resolving the public api but can not be resolved more deeply, i.e. : non chain export statements, chain exports and imports from node modules.

export statement

Non chain, and export export statements.

resolution index

Given the following statement :

export const a: number = 1, b: string = "1";
//           0,             1

or :

import D, { A, b as B, default as C } from "./some/where";
//     0,   1,      2,            3

the resolution index defines at which one of the values we are interested in.

statement index

Given a file, the statement index corresponds to the index of the statement we are interested in. For example :

"hello world"; //statement index 0
export * from "./some/where"; //statement index 1

function foo() {} //statement index 2

entry point file

The entry point .d.ts file of the project to be documented.

resolve export statement

A chain export statement corresponds to a set of non chain export statements. Resolving an export statement means, finding this set.

documentation node

For each non chain export statements that is made public from the entry point file, a documentation node is created with all the necessary information needed to create a human readable documentation.

documentation reference node

Some statements that correspond to documentation nodes, reference other statements in their type signature. For each referenced statement, a documentation reference node is created.

How it works

The typescript abstract syntax tree (ast) parser from the typescript node module is used to parse the typings entry point file. Export statements are located. Those that are chain exports get resolved to non chain exports. After that the abstract syntax tree of each statement is iterated. The iteration serves two purposes:

  • create the documentation string of the statement
  • locate the referenced types, resolve them to their non chain export statements and recursively apply to them the same steps

Help needed

The chain export to non chain export resolution was coded from scratch and it is also used to resolve the statements of type references. It was really tedious to code that. If someone knows any similar built in functionality in the typescript node module, then feel free to inform me. Take into account that this functionality also resolves the namespaces and the exposed name to the public api.

Changelog

0.0.0

First publish.

License

MIT