これはEmacs Advent Calendar 2020 10日目の記事です。
前置き
この記事は淡々と問題を解決する記事ではなく、つらつら苦悩が描写されている。
Typescript + React JSX問題は現在進行形で解決案がドシドシ提案されている。これもその1つとなるといいな。
始まり
Emacsのコーディング体験は最高だ。だけど、Typescript + React JSXではいまいちだ。
去年まで会社で利用していたwebのフロントエンド開発はこれだ。
- React
- Redux
- react-router-dom
Emacsのメージャーモードはrjsx-mode
を使っていた。
ところが、その1年後の2020年8月。
社内で採用しているフロントエンド周りが大きく変わった。
- Typescript
- Next.js
そのためEmacsの構成を大きく見直す必要が出てきてしまった。
下のコードはこの記事で利用するサンプルのtsx
ファイル
import * as React from 'react'
export interface Props {
name: string
enthusiasmLevel?: number
onIncrement?: () => void
onDecrement?: () => void
}
function Hello({ name, enthusiasmLevel = 1, onIncrement, onDecrement }: Props) {
if (enthusiasmLevel <= 0) {
throw new Error('You could be a little more enthusiastic. :D')
}
return (
<div className="hello">
<div className="greeting">Hello {name + getExclamationMarks(enthusiasmLevel)}</div>
<div>
<button onClick={onDecrement}>-</button>
<button onClick={onIncrement}>+</button>
</div>
</div>
)
}
export default Hello
// helpers
function getExclamationMarks(numChars: number) {
return Array(numChars + 1).join('!')
}
typescript-mode使えば良いんじゃないの?
typescriptのコードを書くなら、typescript-mode使えば良い。普通はそう考える。早速このような設定で、.tsx
ファイルを読み込んでみた。
M-x package-install
typescript-mode
(use-package typescript-mode
:init
(add-to-list 'auto-mode-alist '("\\.ts\\'" . typescript-mode))
(add-to-list 'auto-mode-alist '("\\.tsx\\'" . typescript-mode))
:config
(setq typescript-indent-level 2)
)
結果は明らかだ。typescript-modeはReact JSXをうまく表現できない。カラーリングとインデントがおかしくなる。
それもそのはず、typescript-modeはあくまでtypescriptのためのモードであり、React JSXはサポート外なのである。
githubのissueを除くと、実は数年前から議論されていることがわかった。だが、決め手となる一手はまだ見つかってないらしい。
web-mode使えば良いんじゃないの?
そう、おなじみの最強web系モードweb-mode
これを使えば行けそうな気がする。
Googleでtypescript react eamcs
とかでググるとweb-modeをメジャーモードにして利用できるという記事が散見される。
Emacs Typescript+JSXをなるべくweb-modeで編集するための設定
Emacs Typescript Setup for ReactJS, NextJS or NestJS
実際、私はこの数ヶ月はweb-modeベースの設定で開発を行ってきた。
M-x package-install
web-mode
(use-package web-mode
:init
(add-to-list 'auto-mode-alist '("\\.ts\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.tsx\\'" . web-mode))
:config
(setq web-mode-attr-indent-offset nil)
(setq web-mode-enable-auto-closing t)
(setq web-mode-enable-auto-pairing t)
(setq web-mode-auto-close-style 2)
(setq web-mode-tag-auto-close-style 2)
(setq web-mode-markup-indent-offset 2)
(setq web-mode-css-indent-offset 2)
(setq web-mode-code-indent-offset 2)
(setq indent-tabs-mode nil)
(setq tab-width 2)
)
しかし、JSXの中でのタイピングに遅延が発生しているような?
最初は他のemacsパッケージが悪さしているのではと思って気にしていなかった。
だけど、疑惑は時間とともに確信に変わっていった。
web-modeでのタイピングどう考えても遅いやんけ!!
特にJSX部分が膨大になっていくと、その挙動の遅さは如実になった。
Githubのissueを探索すると、幾つか同じような問題を抱えている人のissueが上がっていた。
Editing JSX is generally very slow #1126
しかし、どれも解決には至ってなかった。issueに対するweb-mode開発者の返信も「俺の環境では再現できないからわからん」という感じでイマイチ噛み合っていない。
まあそうだよな。Emacsって自由すぎて、動作している環境がMac、Linux、Winなのか、CUIまたGUIとかバージョンとか人によって環境が違うわけであって、多分、開発者さんの環境ではうまくいっているんだろうなって。
もちろん、web-modeは特別パフォーマンスがどうこうというのはないので、ちょっとしたラグが気にならない方はこれでもやっていけると思う。
js-mode使えば良いんじゃないの?
ありがたいことに、Emacs27.1ではemacsに標準搭載されているjs-mode
のマイナーモードで、js-jsx-mode
というのがある。js-modeが動いている状態で、JSXの箇所にカーソルが移動すると、自動的にjsxに対応し、インデントやカラーハイライトが機能する。
もちろん、typescript用のものではないので、js-modeでtypescriptを編集するのは色々あれだが、タイピングにラグが生まれないのならと試してみた。
(add-to-list 'auto-mode-alist '("\\.ts\\'" . js-mode))
(add-to-list 'auto-mode-alist '("\\.tsx\\'" . js-mode))
(setq indent-tabs-mode nil)
(setq js-indent-level 2)
使ってみると、web-modeの比ではないくらいにタイピング速度が壊滅的に遅い。遅い。遅すぎる。
こちらはEmacsの標準搭載機能なので、もしかすると、今後のバージョンアップで使いやすいものになる可能性はあると思う。だが、今は使い物にならない。
js2-mode使えばいいじゃないの?
そう、ならばjs-modeを拡張した、js2-modeはどうか。
githubのREADMEの記述をみると、
js-modeを起動してそのマイナーモードとしてjs2-minor-modeを利用するとReact Jsxあたりが良い感じになるとある。
M-x package-install
js2-mode
(use-package js2-mode
:init
(add-to-list 'auto-mode-alist '("\\.ts\\'" . js-mode))
(add-to-list 'auto-mode-alist '("\\.tsx\\'" . js-mode))
(add-hook 'js-mode-hook 'js2-minor-mode)
:config
(setq indent-tabs-mode nil)
(setq js-indent-level 2)
)
ただ、こちらはこちらで、typescriptと相性が悪く、多くの記述のエラーが出てしまった。
これではReact JSXどころの騒ぎではなく
mmm-modeでゴリ押すしかないじゃない!
このページのやり方を参考にした。
mmm-modeでReact Jsx以外の部分はtypescript-modeを動かして、Jsxの部分だけはweb-modeを使う。
M-x package-install
mmm-mode
(use-package typescript-mode
:config
(setq typescript-indent-level 2)
(add-hook 'typescript-mode-hook
(lambda ()
(interactive)
(mmm-mode)
)))
(use-package mmm-mode
:commands mmm-mode
:mode (("\\.tsx\\'" . typescript-mode))
:config
(setq mmm-global-mode t)
(setq mmm-submode-decoration-level 0)
(mmm-add-classes
'((mmm-jsx-mode
:submode web-mode
:face mmm-code-submode-face
:front "\\(return\s\\|n\s\\|(\n\s*\\)<"
:front-offset -1
:back ">\n?\s*)\n}\n"
:back-offset 1
)))
(mmm-add-mode-ext-class 'typescript-mode nil 'mmm-jsx-mode)
(defun mmm-reapply ()
(mmm-mode)
(mmm-mode))
(add-hook 'after-save-hook
(lambda ()
(when (string-match-p "\\.tsx?" buffer-file-name)
(mmm-reapply)
)))
)
元となった設定と違うところはまず、この3つだ。
- use-packageを使っている
- jsxの終わりを判定する正規表現が違う
- typescriptの設定もできるようになっている
2に関してはちょっと補足すると、元のサイトのコードではJsx中にif文を書くとweb-modeを抜けてしまうという問題があったので修正した。
{is_okey ? (
<p>Good</p>
) : (
<p>Bad</p>
)}
この設定のメリット
何と言っても基本はtypescript-modeでかけるということ。またここで解説されているように、tideなどのtypescript-modeならではの拡張機能も使える。
Emacs Typescript Setup for ReactJS, NextJS or NestJS
そして、JSXの中での挙動が爆速である。
この設定のデメリット
JSXのカラーリングが崩れる。これはこの設定を紹介しているサイトでも書かれているが、mmmの弊害なのか、新しくJSXにコードを書いているとどうしても色が白くなってしまう。
それをなんとか修復するために、mmm-reapply
という関数で保存時にmmm-modeをオフ、オンしてカラーリングを初期化して対応している。
(defun mmm-reapply ()
(mmm-mode)
(mmm-mode))
まとめ
カラーリングやその他諸々、満足には解決していないが、ひとまずタイピング速度が改善されればそれで良かったので今はこれで頑張っている。何か発見があったらまた随時紹介していきたい。