TypeScriptで書いたライブラリのAPIリファレンスをコードから生成したいときに使えるツールがなかったので、簡単な仕組みで作ってみました。
Almin.jsというJavaScriptのライブラリを書いているのですが、コードベースをTypeScriptに変更してからAPIリファレンスが自動生成できなくて困っていました。
- almin/almin: Almin.js provide Flux/CQRS patterns for JavaScript application
- Inroducing Almin 0.10.0: TypeScript, FlowType, Logger | Web Scratch
TypeScriptでJSDocやESDoc、documentationjsのようなツールとしては、typedocがあります。
しかし、typedocはdocumentationjsのように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の拡張子でしかファイルを吐けないので、
-
.d.ts
を.html
のドキュメントに変換(中身はMarkdown) - 拡張子を
.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"
完成例
実際にできたドキュメントは次のサイトでみることができます。
変換処理はalmin/tools at master · almin/alminあたりを見るとわかります。
APIリファレンスの目的としては、そのAPIはundocumentであることを明示したいというのが主だったのでコスパを考えるとこれぐらいでも十分かなという感じでした。
イメージとしてはrustdocのような、コメントにMarkdownを書いてそれがドキュメントとなるものが欲しい感じでした。
この辺もっといい感じのツールがあったら知りたいです。