TSerの遊び場:TS playground plugin

UIT meetup vol.8 online「We Are TypeScripters!」
Leko

Leko

JS/TSオタクです

🎉 V2 🎉

2020/03/18時点では/v2/つけるとアクセスできそう

🎉 日本語 🎉

— Japanese Translation Coordination Issue · Issue #220 · microsoft/TypeScript-Website
— Website Internationalization Roadmap · Issue #100 · microsoft/TypeScript-Website

きっかけ

— gqlquramy { name } on Twitter: "#typescript My first playground plugin works :) https://t.co/8OHq0i07BX" / Twitter

Playground plugin? 🤔

TypeScript playground plugin

https://www.typescriptlang.org/v2/dev/playground-plugins/

  • TypeScript v2 docs is under development
  • New playground has plugin system 🙄
  • It can load local plugin through localhost 🤯

TL;DR

TS playground pluginという遊び場がきでた。TSのメタを遊んで覚えて楽しもう

例えばこういうことができる

  • .d.tsを解析して破壊的変更を検知する
  • OpenAPI定義からexpressのルーティング型を生成
  • こちらCompilerAPI派出所any警察です👮‍♂️ - Qiita
  • TypeScript CompilerAPI によるVuexの参照型生成
  • Monaco Editor をハックする - Qiita
  • 型と命名(isXXX, hasXXX etc)の整合性をlintする
  • Docコメントに動くテストを書く

試してみる

  • V2 playground開く
  • Optionsタブ開く
  • Custom npm Modulesに、
    ts-playground-plugin-stackoverflowと入力
  • リロード

今回作ったもの

playgroundのエラーと類似のエラーをStackoverflowから探してサジェストするplaygroundプラグイン。
精度はご愛嬌... 🙏

— Leko/ts-playground-plugin-stackoverflow

やってること

  • コードが変更されたら型チェックを実施
  • エラーメッセージをStackExchange APIで検索
  • 整形、出力

必要知識

  • TypeScript playground plugin

    • コア部分
  • TypeScript Compiler API/LanguageService

    • 型チェックとかエラーの整形とか
  • Monaco editor

    • エディタとpluginのやりとり
  • Stackexchange API

    • Stackoverflowからエラーを検索する

話すこと、話さないこと

  • TypeScript playground plugin (サブ)

    • プラグイン機構や使えるAPI、フックなど
  • TypeScript Compiler API/LanguageService (メイン)

    • 型チェック、エラーの整形とか触りだけ
  • Monaco editor (割愛)

    • 外部ライブラリもインストール・型解釈できるTypeScript playgroundを作った
    • Monaco Editor をハックする - Qiita
  • Stackexchange API (割愛)

    • APIドキュメント
    • API叩いてるコード

Playground plugin

プラグイン機構や使えるAPI、フックなど

おおまかな開発の流れ

  1. テンプレからプロジェクト生成
  2. ローカル環境のセットアップ
  3. プラグイン書く
  4. npm publish
  5. playgroundに読み込ませて使う

テンプレからプロジェクト生成

https://www.typescriptlang.org/v2/dev/playground-plugins/

yarn create typescript-playground-plugin xxx
cd xxx
yarn start

このリポジトリもテンプレから作ってる:
Leko/ts-playground-plugin-stackoverflow

ローカル環境のセットアップ

  • 本番playgroundからlocalhsotのプラグインを読める
  • ローカルにplayground cloneしなくていい

Overview

import type { PlaygroundPlugin, PluginUtils } from "./vendor/playground"

export default (utils: PluginUtils): PlaygroundPlugin => {
  return {
    id: 'xxx',
    displayName: 'タブの名前',

    didMount(sandbox, container) {
      // ...
    },

    modelChangedDebounce(sandbox, model) {
      // ...
    },
  }
}

pluginのフック

  • didMount

    • Event: タブが選択され画面に表示された
    • セットアップ、初期描画など
  • modelChanged(Debounce)

    • Event: エディタのコードが変更された
    • コードに対して何かして画面に表示する

これ以外のフックは生成されたPlaygroundPluginの型定義を参照

Compiler API / Language Service

コードに対して何か(型チェック、エラーの整形)して画面に表示する

型チェック

modelChangedDebounce(sandbox, model) {
sandbox.getWorkerProcess().then((worker) =>
Promise.all([
worker.getSemanticDiagnostics(model.uri.toString()),
worker.getSyntacticDiagnostics(model.uri.toString()),
])
)
.then(([semanticDiagnostics, syntacticDiagnostics]) =>
semanticDiagnostics.concat(syntacticDiagnostics)
)
.then(diagnostics => {
// ハンドリングする
})
// ...
}

コードが変更されたとき

Diagnostic

    export interface DiagnosticRelatedInformation {
        category: DiagnosticCategory;
        code: number;
        file: SourceFile | undefined;

        // コード上のエラー開始位置
        start: number | undefined;

        // エラーが起こっているコードの長さ
        length: number | undefined;

        // エラーメッセージ OR ネストしたエラー
        messageText: string | DiagnosticMessageChain;
    }
    export interface Diagnostic extends DiagnosticRelatedInformation {
        /** ... */
        reportsUnnecessary?: {};
        source?: string;
        relatedInformation?: DiagnosticRelatedInformation[];
    }

エラー内容の整形、表示

エラーの整形を分解

1: const port = process.env.PORT;
                ^^^^^^^

↑start, lengthから該当行、エラー箇所を抽出(コード)

error TS2580: Cannot find name 'process'. Do you need to install type definitions for node? Try
`npm i @types/node`.

↑Compiler APIの一部を使用

messageTextといいつつstringではない

        messageText: string | DiagnosticMessageChain;

ts.formatDiagnositc

import type { FormatDiagnosticsHost } from 'typescript'

const formatDiagnosticHost: FormatDiagnosticsHost = { /* ... */ }
const errorMessage = ts.formatDiagnostic(d, formatDiagnosticHost)

FormatDiagnosticsHost... 🤔

FormatDiagnosticsHost

  • ~HostはTSをプラットフォーム非依存するためのDI
  • ブラウザやDenoなど任意の(仮想)FSに対し使える
  • 他にはCompilerHost, LanguageServiceHostなど
declare namespace ts {
    // ...
    export interface FormatDiagnosticsHost {
        getCurrentDirectory(): string;
        getCanonicalFileName(fileName: string): string;
        getNewLine(): string;
    }

雑な実装例

const formatDiagnosticHost: FormatDiagnosticsHost = {
  getCurrentDirectory() {
    // playgroundにディレクトリの概念はないので型だけ合わせる
    return ''
  },
  getCanonicalFileName(fileName: string) {
    // ファイルパスのエスケープもないのでそのまま返す
    return fileName
  },
  getNewLine() {
    return '\n'
  }
}

HTMLに整えて完成

アイデア次第、あなた次第。

— TypeScript Playground Plugin ideas · Issue #221 · microsoft/TypeScript-Website

Playground pluginのすごいところ

  • TS本体に触れなくていい、3rd-partyプラグインとして作れる
  • アイデア次第で非常に強力
  • 他人に使ってもらいやすい
  • 開発しやすい

FAQ

  • Q. 何をどうやって勉強したらいいですか

    • A. 公式のwiki, TypeScript Compiler Internals読みつつひたすらデバッグ
    • 先人が作ったもののコードリーディング
  • Q. 画面への反映ってどうしてますか

    • A. 生DOM API叩いてます
  • Q. 生DOM触るのつらくないですか

    • A. React等で書けるテンプレートも用意されてます
    • React: gojutin/typescript-playground-plugin-react
    • Svelte: gojutin/typescript-playground-plugin-svelte
    • Angular, Vue版はまだない(貢献チャンス)

終わり

  • References

    • Playground plugin ドキュメント
    • 今回作ったプラグインのリポジトリ
  • Related talks

    • TypeScript v2 playground plugin
    • "型パズル"との付き合い方
    • 外部ライブラリもインストール・型解釈できるTypeScript playgroundを作った
    • .d.tsを解析して破壊的変更を検知する