LoginSignup
23
20

More than 5 years have passed since last update.

TypeScriptのコード(d.ts)からAPIドキュメントを自動生成する

Last updated at Posted at 2017-03-24

TypeScriptで書いたライブラリのAPIリファレンスをコードから生成したいときに使えるツールがなかったので、簡単な仕組みで作ってみました。

Almin.jsというJavaScriptのライブラリを書いているのですが、コードベースをTypeScriptに変更してからAPIリファレンスが自動生成できなくて困っていました。

TypeScriptでJSDocESDocdocumentationjsのようなツールとしては、typedocがあります。

しかし、typedocdocumentationjsのようにMarkdownを出力にすることができなさそうでした。

以前は、documentationjs + JSDocで、次のような感じのMarkdownをドキュメントを吐いて使っていました。

探していて、TypeScriptの.d.tsファイルがそもそもそこまで読みにくいものではないなーと思って、.d.tsファイル自体からリファレンスを作ればよさそうと思ってやってみました。

例) https://github.com/almin/almin/blob/0.10.0/src/Dispatcher.ts

/// <reference types="node" />
import { EventEmitter } from "events";
import { DispatcherPayloadMeta } from "./DispatcherPayloadMeta";
import { Payload } from "./payload/Payload";
import { ErrorPayload } from "./payload/ErrorPayload";
import { CompletedPayload } from "./payload/CompletedPayload";
import { DidExecutedPayload } from "./payload/DidExecutedPayload";
import { WillExecutedPayload } from "./payload/WillExecutedPayload";
export declare const ON_DISPATCH = "__ON_DISPATCH__";
/**
 *  Actual payload object types
 */
export declare type DispatchedPayload = Payload | ErrorPayload | CompletedPayload | DidExecutedPayload | WillExecutedPayload;
/**
 * Dispatcher is the **central** event bus system.
 *
 * also have these method.
 *
 * - `onDispatch(function(payload){  });`
 * - `dispatch(payload);`
 *
 * Almost event pass the (on)dispatch.
 *
 * ### FAQ
 *
 * Q. Why use `Payload` object instead emit(key, ...args).
 *
 * A. It is for optimization and limitation.
 * If apply emit style, we cast ...args for passing other dispatcher at every time.
 */
export declare class Dispatcher extends EventEmitter {
    /**
     * if {@link v} is instance of Dispatcher, return true
     */
    static isDispatcher(v: any): v is Dispatcher;
    constructor();
    /**
     * add onAction handler and return unbind function
     * @param   handler
     * @returns call the function and release handler
     */
    onDispatch(handler: (payload: DispatchedPayload, meta: DispatcherPayloadMeta) => void): () => void;
    /**
     * dispatch action object.
     * StoreGroups receive this action and reduce state.
     * @param payload
     * @param [meta]    meta is internal arguments
     */
    dispatch(payload: DispatchedPayload, meta?: DispatcherPayloadMeta): void;
    /**
     * delegate payload object to other dispatcher.
     * @param   toDispatcher
     * @returns call the function and release handler
     */
    pipe(toDispatcher: Dispatcher): () => void;
}

この.d.tsファイルからMarkdownを吐いて、最終的にそのMarkdownをGitBookで取り込んでドキュメントにするという形です。

.d.ts -> Markdown

.d.tsファイルから次の事をすれば大体読めるMarkdownになるんじゃないかなと思いました

  • importやprivateなどリファレンスとしていらない部分を削る
  • コメントを普通のMarkdownとして表示する

これをやるベストなツールがなかったのですが、doccoとテンプレートを組み合わせればできそうでした。
doccoはLiterate Programming的なドキュメントを作るツールです

複数行コメントに対応してなかったのでforkしたものを作りました(本体の更新停止してるし)

npm install -g @azu/docco

.jstという拡張子でdoccoのテンプレートファイルを作ることができます。
.d.tsから不要なものを取り除くテンプレートを書いていきます。

# <%= title.replace(".d.ts", "") %>
<!-- THIS DOCUMENT IS AUTOMATICALLY GENERATED FROM src/*.ts -->
<!-- Please edit src/*.ts and `npm run build:docs:api` -->
<%
function shouldIgnoreSection(section) {
   if (/^\s*$/.test(section.docsText)) { return true; }
   if (/^\s*$/.test(section.codeText)) { return true; }
   // private method
   if (/private /.test(section.codeText)) { return true; }
   // @private
   if (/<b>private<\/b>/.test(section.docsText)) { return true; }
   // **internal** flag
   if (/\*\*internal\*\*/.test(section.docsText)) { return true; }
   if (/<reference type/.test(section.docsText)) { return true; }
   return false;
}
function stripIndent(str) {
   const match = str.match(/^[ \t]*(?=\S)/gm);

   if (!match) {
       return str;
   }

   const indent = Math.min(...match.map(x => x.length));
   const re = new RegExp(`^[ \\t]{${indent}}`, 'gm');

   return indent > 0 ? str.replace(re, "") : str;
};
// Multiple line or Single Header
function renderAPIHeader(section) {
   // remove } block
   const header = stripIndent(section.codeText.replace(/}$/m, "")).trim();
   if(header.split("\n").length > 1){
     return "### Interface of \n"
      + "```typescript\n"
      + header + "\n"
      + "```";
   }
   return "### `" + header + "`";
}
%>

## Interface

```typescript
<%= sections.filter(section => !shouldIgnoreSection(section)).map(section => section.codeText).join("\n").trim() %>
```

----
<% sections.filter(section => !shouldIgnoreSection(section)).forEach(section => { %>
<%= renderAPIHeader(section) %>

<%= section.docsText %>

----
<% }) %>

doccoはhtmlの拡張子でしかファイルを吐けないので、

  1. .d.ts.htmlのドキュメントに変換(中身はMarkdown)
  2. 拡張子を.htmlから.mdに変更

というようなスクリプトを書いて回しています。

#!/bin/bash
declare projectDir=$(git rev-parse --show-toplevel)
declare srcDir="${projectDir}/lib"
declare docDir="${projectDir}/docs"
# add build result
function addDoc(){
    declare filePath=$1;
    declare fileName=$(basename ${filePath} "d.ts")
    $(npm bin)/docco --blocks -t ${projectDir}/tools/d.ts-markdown.jst ${filePath} --output ${projectDir}/__obj/docs
    mv ${projectDir}/__obj/docs/**${fileName}*.html ${projectDir}/docs/api/${fileName}md
    echo "Create: ${docDir}/api/${fileName}md"
}
npm run build
# update
addDoc "${srcDir}/Context.d.ts"
addDoc "${srcDir}/Dispatcher.d.ts"
addDoc "${srcDir}/DispatcherPayloadMeta.d.ts"
addDoc "${srcDir}/Store.d.ts"
addDoc "${srcDir}/UILayer/StoreGroup.d.ts"
addDoc "${srcDir}/UseCase.d.ts"
addDoc "${srcDir}/UseCaseContext.d.ts"

完成例

実際にできたドキュメントは次のサイトでみることができます。

Dispatcher · Almin.js 2017-03-24 11-03-33.png

変換処理はalmin/tools at master · almin/alminあたりを見るとわかります。

APIリファレンスの目的としては、そのAPIはundocumentであることを明示したいというのが主だったのでコスパを考えるとこれぐらいでも十分かなという感じでした。

イメージとしてはrustdocのような、コメントにMarkdownを書いてそれがドキュメントとなるものが欲しい感じでした。

この辺もっといい感じのツールがあったら知りたいです。

23
20
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
20