- for ginza.js#7
- @mizchi
- このスライドツールの作者
- 今はワケあってこの会場の会社にいる(Plaid)
お品書き
- VSCode をハックしようとして敗北した
- MonacoEditor をハックしてる
フロントエンドの仕事、ブラウザだけで完結するはず、と思ったことないですか?
フロントエンドのおしごと
- コンポーネントを作る
- ビルドする
- テストする
- ブラウザで確認する
フロントエンドのおしごと
- コンポーネントを作る(ブラウザでできる?)
- ビルドする(ブラウザでできる?)
- テストする(ブラウザでできる)
- ブラウザで確認する(ブラウザでできる)
経緯: コンパイラを作った
フロントエンドでフロントエンドをビルドする で、ブラウザ上で動くコンパイラ作った
今日は、このコンパイラを動かすためのエディタを作ってる話
@mizchi/web-compiler (仮)ができること
サンプルコード
// ブラウザで実行
import { compile } from "@mizchi/web-compiler";
// package.json
const pkg = {
dependencies: {
"lodash.flatten": "*"
}
};
// tsconfig.json
const tsConfig = `{
compilerOptions: {
target: "es5"
}
}`
// コード
const index = `
import foo from "./foo";
import flatten from "lodash.flatten";
console.log(flatten([[1], 2]), foo);
`;
const foo = `export default 1;`
const files = {
"/index.js": index,
"/foo.js": foo
},
// /index.js でビルド
const compiledCode = await compile({
entry: "/index.js",
pkg, tsConfig, files
});
- インメモリの仮想 FS で相対パスを解決
- npm のモジュールを解決
- 任意の
tsconfig.json
の設定で typescript をコンパイル - terser で minify
モダンなエディタに要求されることは?
答え: TypeScript の補完ができる
エディタをどう作るか
// このインメモリなファイルを TypeScript で補完して快適に編集したい
const files: {[filepath: string]: string} = {...}
=> とりあえず VSCode ベースでやる
エディタの設計: 3 つの方針
- VSCode Online 上の拡張を作る
- VSCode をハックする
- MonacoEditor をハックする
VSCode Online 使ってみた
Visual Studio Online - Web ベースの IDE と共同コード エディター
- MS Account 登録
- Environment(ホストとなる仮想マシン) の登録・起動
- エディタ起動
VSCode Online 感想
- Environment を自由に作れるけど、セットアップに手間がかかる
- クラウドなのでキビキビ感が足りない
- 要課金だしお手軽感がない
MS Account はちょっと… 電話してくるのもやめてください
手間の関係で NG
もうどうせだから vscode コード読もうぜ!
VSCode がハックできるかの調査
仮説: 「ブラウザで動く VSCode Online があるなら、ブラウザ用ビルドがあるはず…」
- https://github.com/microsoft/vscode
- web ビルド用のコマンドがあったので、 (
yarn web
) ので、それを調査する - どうやらリロードごとに FS が揮発してるので、そこを任意の処理(永続化)を入れられるか?という視点で調べる
コードリーディング: VSCode の基本的な設計
- 専用の AMD Loader で起動する (github.com/microsoft/vscode-loader)
- 最初に複数の
~Service
を WebWorker のワーカーで起動する - サービスに任意の実装の Provider を注入することで、役割・環境ごとに実際の処理を切り分けられる
- Service の起動が終わると、各 Extension を起動する (言語ごとのプリインの拡張機能など)
- Service と Extension が起動し終わったら、 Workbench (VSCode の UI 部分) を起動
(UI は何のフレームワークも使ってない独自のものだが、 state の書き換えがトランザクション前提で Reducer っぽい)
ここまでのメモ: https://gist.github.com/mizchi/9dbd9dbbc97378c48ced47964dc97055
VSCode web ビルドの調査
-
yarn watch-client
してからyarn web
すると、ブラウザで動くビルドが起動する -
vscode/src/vs/code/browser/workbench/workbench-dev.html
が読み込まれている。 - FileService に注入された
RemoteFileSystemProvider
はどことも通信しておらず、new Map()
のオンメモリキャッシュに乗ってるので、永続化されない - Extension を全部消したりしてみたが、
vscode.vscode-api-tests
がないと立ち上がらない。どうやら ここもテスト用の Mock っぽいので、色々死にそう。
Web ビルド: わかったこと
- 色々とパッチ当てたら動くが大変
- もうちょっとハックしやすければ VSCode の独自ビルド路線もアリだが…
- 今回は monaco-editor をハックする路線で頑張る
monaco-editor をハックする
monaco-editor
- https://github.com/Microsoft/monaco-editor
- VSCode のエディタ部分
- TypeScript や各言語の補完機能なども monaco で行われている
- Electron 非依存
monaco-editor の実装
- 言語ごとに WebWorker プロセスを立てて通信する
- そのための webpack の設定が必要
- https://github.com/microsoft/monaco-editor-webpack-plugin
- クライアント上の monaco に変更を加えると、ワーカーの mirror に同期される
- ワーカー上で読み書きしたことを UI スレッドに書き戻すことで、双方向通信を実現
monaco-typescript
- https://github.com/Microsoft/monaco-typescript
- VSCode の TypeScript の補完などを行っているモジュール
- worker 側で typescript の LanguageService を抱えて起動する
Using the Language Service API · microsoft/TypeScript Wiki
monaco-typescript 読んでわかったこと
- monaco-typescript/tsWorker はファイルスキーマで登録された monaco.Model を解決する
- monaco の Model オブジェクトをファイルスキーマで登録すれば補完が動きそう
=> 実現できる API を調べる
monaco で仮想 FS を構築して補完の準備
monaco 上でのファイルオブジェクトの生成
import * as monaco from "monaco-editor";
const model = monaco.editor.createModel(
"export default { a: 1 }",
"typescript",
monaco.Uri.from({
scheme: "file",
path: "/foo.ts"
})
);
(同名のファイルを生成するとエラーになる)
エディタへの Model 読み込み
const editor = monaco.editor.create(...);
editor.setModel(model);
今エディタで開いている TypeScript の型エラーを取得
const uri = editor.getModel().uri;
const worker = await monaco.languages.typescript.getTypeScriptWorker();
const proxy = await worker(uri);
const diagnostics = await proxy.getSemanticDiagnostics(uri.toString());
console.log(diagnostics);
モデルの破棄(削除)
model.dispose();
つまり…
こういう FS を構築して
const model = monaco.editor.createModel(
"export default { a: 1 }",
"typescript",
monaco.Uri.from({
scheme: "file",
path: "/foo.ts"
})
);
const model = monaco.editor.createModel(
'import * as foo from "./foo";console.log(foo)',
"typescript",
monaco.Uri.from({
scheme: "file",
path: "/index.ts"
})
);
/*
/index.ts
/foo.ts
*/
index.ts から ./foo
を引くと補完できる!!
デモ
TypeScript のコンパイラオプションを変更する
import { parseConfigFileTextToJson } from "typescript";
const conf = parseConfigFileTextToJson("/tsconfig.json", '{ "compilerOptions": {} }');
monaco.languages.typescript.typescriptDefaults.setCompilerOptions(
conf.config.compilerOptions
);
Thx @L_e_k_o
外部の型定義ファイルを読み込む
monaco.languages.typescript.typescriptDefaults.addExtraLib(
"declare module '*';",
"decls.d.ts"
);
まだできてないこと
- npm の
@types
からの型定義の読み込み -
*.d.ts
の型定義ファイルのダウンロード(その中からさらに相対パスがあり難しい
ツラミ
- ブラウザにろくな永続層がない
- NativeFileSystem API に必要な仕様が足りない
-
./foo
が./foo.js
か/foo.json
か./foo/index.js
かfoo/package.json#main
なのか一意に決まらないものの、ネットワーク経由で解決するのが非効率 - Filer みたいな基本的なもので、作るものが多い!
おまけ: react-unite
- https://github.com/mizchi/react-unite
- unity3d みたいなレイアウトシステムがほしくて作った
- CSS Grid Layout のパラメタを入力値にできる。便利
今後
- VSCode もう一回読む
- コンパイラのパターン増やしてちゃんと作る
- TSの型解決を頑張る