mizdra's blog

ぽよぐらみんぐ

「ページの編集は大胆に」、転じて「ドキュメントの整理は大胆に」

「ページの編集は大胆に」という考え方

Wikipedia のページの執筆にあたってのガイドラインの1つに「ページの編集は大胆に」というものがあります。簡単に言えば、ちょっとした編集に留まらず、必要であれば大きな編集もしよう、という指針です。

最初のうちはちょっとした編集を積み重ねていくだけで上手くいっていたはずが、次第に文章がつぎはぎになり、やがて行き詰まる…。このような経験をしたことのある人は多いと思います。文章の元の体裁を尊重して少しずつ手を加えていくのも大事なことです。しかし、どんなに優れたページであっても、編集が積み重なっていくと、思うような体裁にならない時がやってきます。そうした時は大胆な変更をしていくべきだということです。

非常に素朴な指針ですが、文章を変更する上で大胆にやっていくというこの心がけはとても大事なことだと id:mizdra は思っています。当該ガイドラインのページには、このガイドラインにまつわる丁寧な説明が書いてあるので、ぜひ読んでみて下さい。

転じて「ドキュメントの整理は大胆に」

ところでこの「大胆にやる」という考えは、Wikipedia のページの編集に限らず、様々なものについても広げられると id:mizdra は考えています。例えば社内ドキュメントの整理などです。3 年、5年と業務を継続していけば、事業の変化に合わせて自然とドキュメントがつぎはぎになっていくことが多いです。そうして誤読を誘発したり、目の滑るドキュメントができてしまいます。

同様に、定例会のアジェンダについても適用できます。定例会で扱う話題が増えたり、会自体の目的に変化があると、定期的に取り上げる必要がなくなる議題が出てきます。そうした議題をアジェンダに残してしまうと、他の重要な議題に割く時間が減り、定例会の生産性が落ちてしまいます。

こうしたことが起きないよう、id:mizdra はドキュメントの整理を大胆にやることが大事だと思っています。アジェンダで扱うコンテンツの順序を入れ替えたり、思い切って一部のコンテンツを落としてみたり、追加してみたり、格好良い標語を差し込んでみたり…。ついつい何か変化を起こす時は見えざる声に恐れをなしたり、面倒に感じて、変化を控えめにしがちですが、時には大きな変化も必要です。こうすれば絶対に良くなるに違いないと思ったら、その直感を信じて勇気を出して整理していく、ということをよく心がけています。たまに大胆な編集を実践しては、良い体裁に修正することができて、ああやって良かったなとなっています。

大きな変更を加えるのは勇気のいることですが、勇気を振り絞ってやってみると何もかもが良くなるかもしれません。良くならなかったら変更履歴からすぐに戻せば良いだけです。思い切ってやってみましょう。

全部ひっくり返して別物にする、ではない

実践するにあたって 1 つ注意しておきたいのは、「大胆に変更する」が意味するのは「今まで作り上げたものを全部ひっくり返して別物にする」ではなく「大きく変更を加えることを恐れない」だということです。全部を変える必要は当然ないです。良いものは良いものとして引き継いでいくことを心がけると良いと思います。

個人的 Web フロントエンドスキルの獲得方法

ここ2年くらいの話なのですが、仕事で「フロントエンド会」というチーム内委員会のようなものを立ち上げて運営しています。元々1人の Web フロントエンド職人がプロダクトの Web フロントエンドの面倒を見ていたのですが、その方が異動されることになったので、残った人で面倒を見ていける体制を作りましょう、というモチベーションで発足した会でした。この話については以前イベントで発表したので、詳しくはこのスライドをご覧下さい。

speakerdeck.com

Web フロントエンド職人の異動とともに入社した id:mizdra が Web フロントエンドが得意だったので、ペアプロやペアオペ、定例会などを通じてどんどんスキルや知見を配っていく、という戦略で運営していました。実際に 2 年経過してみてメンバーも徐々にキャッチアップしていって、ちょっとしたパフォーマンス改善をやってみたり、最近 Gulp や jQuery からの脱出を推し進めています。すごすぎる!

一方で識者がスキルや知見を配っていく、という体制だったがゆえに、そのスキルや知識を配る人に依存してしまっているという問題も出てきました。元々1人の Web フロントエンド職人が見る体制から複数人で見る体制になったとはいえ、やはり識者が支えている部分がまだまだ大きいです。

ではどうするかという話なのですが、まあ色々やりようがありますね。本質的な解決ではないですが、採用や異動で工夫して継続的に識者をチームに配備できるようにするのも 1 つの手ですし、レガシーなグッズ (Gulp, jQuery) をどんどん捨てて開発の難易度を下げる/現代的なベストプラクティクスを取り入れやすくするのも手ですし、Next.js のようなフレームワークを導入してレールの上に乗れるようにするのも手です。

一方で、それぞれのメンバーが自発的に Web フロントエンドスキルを獲得していける状態を目指す、その状態に向かえるようサポートする、という手もあります。スキルを伝授するのではなく、スキルを習得する方法を伝授する、ということですね。

そういう経緯で社内向けに id:mizdra の個人的な Web フロントエンドスキルの獲得方法について共有してみました。筋肉で解決みたいな内容で、かつ Web フロントエンド要素もあまりないメタ的な内容が多いので、どういう反応になるか不安だったのですが、思いの外良い反応を頂けました。ああやって良かったなと今になって振り返っています。

で、折角書いたのに社内に置いておくのは勿体ない!と思ったので、折角なのでこの記事で公開してみます。やっぱり公開するのは不安なのですが、誰かの参考になれば良いかなと思います。


まず作りたいプロダクトを決める

いきなりメタ的な話なのですが、作るものがあるのとないのとでは、スキルのキャッチアップの速度が大きく変わってくるので、何を作るかは最初に決めておくのがオススメです。ゴールがあればそこへ至るまでの道筋 (最初にどこから学んでいって、次に何を学べばよいのか) もイメージしやすいし、作りたいものならやる気もあって挫折することなく続けることができます。

作りたいプロダクトの大きさは何でも良いのですが、半年以内で作れるもののような大きすぎないものが良いです。プロダクトによって学べることは変わってくるので、小さいプロダクトを沢山こなすほうが色々学べてお得です。とはいえ作りたいものを作ったほうがやる気の維持には良いので、作りたいものを作ると良いと思います。id:mizdra は小さいものを作っていって、だんだんできることを増やしていく、という戦略が好みです。以前ブログにも書きました。

作りたいものがない場合は、ポートフォリオとかオススメです。プログラミング初心者の方がよく作られているイメージが強いですが、Next.js 使えるし、SSR も活かせるし、完成したものも自分のプロフィールとして今後も使えるし、題材としてはピッタリだと思います。

とにかく記事を読みまくる

フロントエンドは必要な技術スタックが沢山ある上に、歴史的経緯も盛りだくさんという感じで、学ぶことが沢山あります。効率よく学んでいくには、情報を取捨選択して必要なものだけを読んでいく、ということになるのですが、学びはじめの頃はそういう勘もないので、愚直に沢山の記事を読んでいくのがオススメです。

id:mizdra は 2015 年の頃に Web フロントエンドを本格的に学び始めたのですが、その頃は ES2015 が登場してフロントエンドのツールが充実しつつあるタイミングでした。Bower/gulp/Browserify/io.js とか色々あって、次は TypeScript/VSCode/Webpack が来るぞとか盛り上がっていて、Web フロントエンド戦国時代という感じ。とにかく色々なツールの情報があって、人によってオススメしているものが違って情報の取捨選択が大変でした。仕方がないので Qiita などを眺めて、真面目に気になった記事を片っ端から読む暮らしをしていました。

片っ端から集めたものは質の良くないものも多いですが、色々な情報を網羅的に収集したい入門時に参照するのには十分価値のあるものなので、どんどん読んでいくと良いです。記事を読んでいると、審美眼が研ぎ澄まされてどの技術スタックが優れているかとか分かってきて、情報の取捨選択ができるようになってきます。

何が言いたいかというと、がむしゃらにインプットしていくことは全くもって遠回りなんかではない!ということです。がむしゃらにインプットしていくのオススメです。

信頼できる情報が目に入る環境を作る

毎回自分から情報を探しに行くのはコストが掛かり、長続きしません。対策として、id:mizdra は何もせずとも情報が振ってくる環境を整えたりしてました。

例えば RSS を購読したりとか、フロントエンドの情報を発信している人を Twitter でフォローするとか。何でも良いです。ただ、自分が普段目を通す場所にその情報が流れてくるようにしておきましょう。せっかく RSS を購読しても目に入らなければ意味ないので、ちゃんと普段見るところに情報を持ってきます。自分の近くに情報を寄せ、息をしているだけで情報が手に入る、というのが理想です。

id:mizdra の場合は Twitter のタイムラインがそれです。フロントエンドの情報を発信している人をフォローしています。暇さえあれば Twitter を開いているので、息をしているだけで情報が手に入るという環境になってます。フォローすると通知が飛ぶのが嫌で、以前リストを使おうとしたこともあったのですが、リストを開くという行為自体がコストそのものだったのでやめました。とにかく低体力で情報が手に入るようにしています。

フォローする人にも気を配っていて、とにかくこの人は信頼できるという人を積極的にフォローするようにしています。その人が書いた記事を読んでみて、面白いことを書いている!とか、着眼点が良い!とか、そういう基準でどんどんフォローしていきます。折角普段目を通すところに置く情報を選ぶのだから、質の良いものを選ぶと良い、という考えが根底にあります。これでタイムラインには信頼できる情報筋からの質の良い情報が集まるようになります。

ところで自分のスキルが高まるにつれ、情報を見る目も変わってきます。その結果、どの情報が自分にとって信頼できるか、質が良いかは変わってきます。id:mizdra もその時々によって、自分のスキルに応じてフォローする基準を柔軟に変えていっています。

完全に余談ですが、こうやってタイムラインを研ぎ澄ましていくと、「filter:follows react」でフロントエンドのエキスパートのツイートを対象に検索することができて便利です。

学習資料や書籍を読む

さっき何でも片っ端から記事を読んでいけば良い、と書いたのですが、体系的に何かを学ぶ場合は学習資料や書籍を読むと良いです。これはフロントエンドが得意な人に聞いたりすると大体教えてくれます。

作りたいプロダクトのアイデアをメモしておく

継続的に学んでいくためには作りたいプロダクトを絶やさないことが重要で、とにかく手持ち無沙汰にならないようにしておく、というのを意識しています。「そんなこと言われても作りたいものなんか思いつかないよ!」という人にオススメしたいのが、技術的な挑戦のアイデアをメモしておくことです。

id:mizdra は個人 Slack の #idea に日々思いついたネタをメモしていって、面白そうなものをやっていく、という暮らしをしています。アイデアを記録し始めの段階では慣れないかもしれませんが、段々書いていくうちに、これもやってみようだとか、あれ面白そうだとか、意外と自然とアイデアが出てくるようになります。あとタイムラインの話と同じですが、このメモする場も出来るだけ毎日目を通す場の近くにあったほうが良いです。アイデアを書きたいと思ったらすぐ書けるように、メモ帳を開くのが億劫にならないように、ということを意識しています。


という訳で以上が個人的 Web フロントエンドスキルの獲得方法になります。全部が参考にならなくても、これは良いねと思ってもらえるものが1つくらいあれば嬉しいです。

チームでもこうしたスキルの獲得などを日々行っております。もし弊チームの活動に興味があれば、ここから応募できますのでぜひぜひ。

jobs.qiita.com

TypeScript 4.5 Beta で実装された Node.js ESM 対応を試してみた

ご存じの方もいるかもしれませんが、TypeScript 4.5 Beta で遂に Node.js ESM がサポートされました。まだ Stable に来ていない実験的な機能なのですが、どういうものなのか気になったので、先日趣味で作っているプロダクトに導入してみました。で、この記事はその備忘録です。実験的な機能で、これから状況もどんどん変わっていくので、数カ月後にはこの記事の内容も古くなっているかもしれません。未来から来た人がこの記事を読んでいる場合は、注意して読んで下さい。

今回 TypeScript の Node.js ESM 対応を導入してみたのはこちらの eslint-interactive というプロダクトです。以前このブログでも紹介した ESLint の API を使った CLI ツールです。

github.com

www.mizdra.net

www.mizdra.net

どうせ物好きな人しか読まないと思うので、雑にサクサク書いていきます。

最初に結論

どういう手順で進めて、どんな障害があって、それをどうやって解決していったかは全部 PR に詳しく書いたので、これ見てもらえれば大体分かります。

github.com

という訳で、以下はその PR の補足です。PR で書ききれなかったこととか、面白かった部分について書いていきます。

まずガイドを読む

JavaScript エコシステムを取り巻くモジュールシステムの事情にもそれなりに知っておかないと、かなり進めるのが難しいです (というか知っていてもムズいです)。手を加えなければならない場所も多岐にわたり、作業量自体も大きいです。そこで、まずは手を動かす前に参考になる資料を読んで、どういったことをしていくことになりそうか、目処をつけることにしました。

ありがたいことに @teppeis さんが TypeScript の Node.js ESM 対応についてまとめた非常に素晴らしい記事を書かれています。まずはこれを読みましょう。

zenn.dev

もし ESM 化するプロジェクトがライブラリの場合、sindresorhus 氏 *1 が書かれている記事も読むと良いです。ライブラリを ESM 化するとユーザ側にどんな影響があるかがまとまっています。

toolchain のサポート状況を確認する

Node.js と TypeScript が ESM をサポートしていても、Jest などの toolchain がサポートしていなくて ESM 化できない、という可能性もあるので、 toolchain の対応状況は調べておきます。大体 toolchain ごとに公式ドキュメントがあったり、 ESM 用のトラッキング Issue があるので、それを読んだり、トラッキング Issue にリンクされている Issue や PR のタイトルを見て気になるものがあったらクリックして、我々の作業のブロッカーになりそうな話題がないか確認していきます。

Jest のサポートは絶賛進行中で、モジュールの mock 周りのサポートが結構欠けているので気をつけて下さい。jest.mockjest.requireActual はまだ機能しません。 結構使われている機能なので、ここがブロッカーになることが多いと思います。eslint-interactive でも使っていましたが、別に mock 使わなくても良い箇所だったので、mock を使わない形に書き換えて乗り切ることにしました。

モジュールファイルを動的に import する機構を持つライブラリに気をつける

これはプロダクトによるのですが、「モジュールファイルを動的に import するような機構を持つライブラリを使っている場合、そのモジュールファイルを ESM で書けるようライブラリがサポートしているか」がブロッカーになることがあります。ライブラリが CJS なモジュールファイルしかサポートしていない場合、モジュールファイルを ESM で書くことができません。

例えば、eslint-interactive では ESLint が開発者向けに提供している API を使い、ESLint に自作のカスタムルールを動的に import させていました。ESLint のドキュメントを読む限りはカスタムルールは CJS でしか書けなさそうだったので、仕方なく当初はカスタムルールは CJS、それ以外は ESM で書く、という戦略で ESM 化を進めようと考えていました。dynamic import を使うと CJS から ESM を import できるので、これで部分的に CJS を使い、コードベースの他では ESM を使う、というハイブリットな構成を実現できます。

// src/rules/custom-rule.cts
import { type Rule } from 'eslint';

const rule: Rule.RuleModule = {
  create(context: Rule.RuleContext) {
    // ESM なモジュールを dynamic import で import する
    // 本当は import statement (`import ... from '...';`) で import したいけど、CJS => ESM だと
    // できなくて、dynamic import が唯一の import 方法。
    const { foo } = await import('./util.mts');

    // foo など ESM で書かれたモジュールの utility を使ってルールを実装
  },
}

普通はこれで問題ないのですが、ESLint のカスタムルールでは、ESLint 側の実装上の都合により、カスタムルールを非同期で記述できないことが分かりました。つまり dynamic import は使えません。CJS から ESM を読み込むには、dynamic import が唯一の方法なので、カスタムルールから事実上 ESM を読み込めないということを意味します。詰んだ…

仕方がないので eslint/eslint に issue を書いて、ESM のカスタムルールサポートして欲しいと Feature Request を送りました。

github.com

実装の許可が貰えれば実装するつもりでいたのですが、実は (.eslintrc.js とか一般ユーザからはまだ利用できないけど) ESLint が開発者向けに提供している API ならもう ESM なカスタムプラグイン/ルール読み込めるよ (ref1, ref2, ref3)、と教えてもらえたので、それを使う形で乗り切ることにしました。聞いてみてよかった。

pkg.bin で参照している実行可能ファイルには拡張子が必須

eslint-interactive は CLI ツールです。npm i @mizdra/eslint-interactive すると eslint-interactive というコマンドがインストールされ、このコマンドを介してツールを利用できるようになります。このコマンドのインストールには package.jsonbin というフィールド (以下 pkg.bin) を使っています。

numb86-tech.hatenablog.com

当初、eslint-interactive の package.json では、以下のように pkg.bin を記述し、bin/eslint-interactive にリンクさせていました。

  "bin": {
    "eslint-interactive": "bin/eslint-interactive"
  },

bin/eslint-interactive は shebang を書いて node コマンドで実行するようにシステムに指示していました。拡張子がないのですが、中に書いてあるのは JavaScript (厳密に言うと shebang はまだ ECMAScript 的に valid ではないですが…) なので、これで問題なく実行できます。

#!/usr/bin/env -S node -r source-map-support/register

import { run } from '../dist/index.js';

run({
  argv: process.argv,
}).catch((error) => {
  process.exitCode = 1;
  console.error(error);
});

で、本題なのですが、ESM 化するとこれ壊れます。厳密に言うと、package.json"type": "module" が書かれた状態で、bin/eslint-interactive を実行すると Unknown file extension "" という実行時エラーが出ます。

$ bin/eslint-interactive
TypeError: Unknown file extension "" for /Users/mizdra/src/github.com/mizdra/eslint-interactive/bin/eslint-interactive
    at new NodeError (node:internal/errors:371:5)
    at Object.file: (node:internal/modules/esm/get_format:72:15)
    at defaultGetFormat (node:internal/modules/esm/get_format:85:38)
    at defaultLoad (node:internal/modules/esm/load:22:14)
    at ESMLoader.load (node:internal/modules/esm/loader:353:26)
    at ESMLoader.moduleProvider (node:internal/modules/esm/loader:274:58)
    at new ModuleJob (node:internal/modules/esm/module_job:66:26)
    at ESMLoader.#createModuleJob (node:internal/modules/esm/loader:291:17)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:255:34)
    at async Promise.all (index 0)

多分 Node.js が拡張子を元に ESM として扱うか CJS として扱うかを決定しているので、拡張子がないとそこがうまく動かないんでしょうね。node --eval "..." で文字列で JS コードを渡したときも、拡張子がないので似たような問題が発生します。

何か良い回避策無いかなあと Node.js のドキュメントを探してみたところ、--input-type=module というオプションがあることに気づきました。どうやらこれを使うと node --eval "..." で渡した文字列を ESM として処理するか CJS として処理するか、Node.js ランタイムに指示できるらしいです。

// from: https://nodejs.org/api/packages.html#--input-type-flag
node --input-type=module --eval "import { sep } from 'path'; console.log(sep);"

これ pkg.bin にも使えないかな? と思って試してみたんですが、駄目でした。--eval オプション OR --print オプションとの併用が必須らしいです。

github.com

打つ手がなく途方に暮れていたところ、id:rinsuki さんに拡張子を付けてみてはどうか、と教えていただきました。試しに .jsを つけてみたところ、なんと無事回避できました。

github.com

勘の良い人ならコマンドのインストール時に拡張子が外れた状態で node_modules/.bin に symlink が置かれるので、動かないのでは? と思うかもしれませんが、意外と何事もなく動きました。気づけば簡単だったけど、気づくまで結構時間掛かった… 何はともあれ id:rinsuki さんありがとうございました!

特定のディレクトリ配下のファイルを全部 CJS にする

eslint-interactive ではテストで使う fixtures があって、fixtures/ ディレクトリに配置しています。この中に .js を含む静的リソースを入れているのですが、プロジェクトのルートの package.json"type": "module" を加えたせいで、fixtures/ 配下の *.js も ESM として扱われるようになってしまいました。eslin-interactive のテストの都合上、fixtures/ 配下は CJS である必要があったので、これによってテストが壊れてしまいました。

今回は { "type": "commonjs" } とだけ書かれた package.jsonfixtures/package.json に配置して回避しました。これで fixtures/ 配下の *.js が CJS として扱われます。ESM 化する上で便利なテクなので覚えておくと良いです。

github.com

Jest が pkg.exports を解決できるようにする

Jest はまだ pkg.exports を解決してくれません (pkg.exports って何? という方はこちらを参照)。

github.com

一部のパッケージは pkg.exports を使って ESM モジュールを export しているので、これを解決できないと、パッケージを ESM で利用することができません。sindresorhus 氏のパッケージもこれを使っています。

Jest は割とモジュールの resolution をユーザ側で制御できるので、色々な workaround があります。例えば以下は moduleNameMapper を使って手動で解決する workaround です。こことかこことかここ とか見ながら合わせていきます。職人技です。

github.com

とはいえ #ansi-styles とかが chalk 以外で使われていると普通に壊れるので、オススメしません。オススメはカスタムリゾルバによる workaround です。どれくらいちゃんと動くものなのか分かりませんが、id:mizdra が確認した限りでは期待通りに動いてそうです。eslint-interactive の ESM 化でも最終的にこれを使いました。Jest の公式のサポートが来るまでのつなぎなので、一旦雑に回避しました。

github.com

github.com

package/path/to/module.mjs 形式の import がうまく動かない

3rd-party パッケージの中には、パッケージ名だけでなくパスまで指定させて import してもらうタイプのモジュールを提供しているものがあります。例えば comlink とかです。

import { Worker } from 'worker_threads';
import { wrap } from 'comlink';
// `package/path/to/module.mjs` 形式で import
import nodeEndpoint from 'comlink/esm/node-adapter.mjs';

const worker = new Worker('./worker.js');
const remoteFn = wrap(nodeEndpoint(worker));
console.log(remoteFn());

tsconfig.json"module": "node12" にすると、この形式で import するとモジュールに対応する型定義が見つからないとコンパイルエラーになることがあります。

$ tsc -p tsconfig.src.json --noEmit
src/index.ts:5:26 - error TS7016: Could not find a declaration file for module 'comlink/dist/esm/node-adapter.mjs'. '/Users/mizdra/src/github.com/mizdra/eslint-interactive/node_modules/comlink/dist/esm/node-adapter.mjs' implicitly has an 'any' type.
  If the 'comlink' package actually exposes this module, try adding a new declaration (.d.ts) file containing `declare module 'comlink/dist/esm/node-adapter.mjs';`

5 import nodeEndpoint from 'comlink/dist/esm/node-adapter.mjs';
                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

comlink には node_modules/comlink/dist/esm/node-adapter.d.ts にちゃんと型定義があるのですが、tsc がこれをうまく見てくれません。これは node-adapter.mjs に対応する型定義が node-adapter.d.mts であるためです。*.d.ts ではなく *.d.mts です。試しに *.d.mts を用意してみると型検査に pass します。

$ cp node_modules/comlink/dist/esm/node-adapter.d.{ts,mts}
$ tsc -p tsconfig.src.json --noEmit && echo passed
passed

仕方がないので eslint-interactive では tsconfig.jsonpaths オプションを使い、TypeScript の世界でのみ comlink/dist/esm/node-adapter.mjs を無理やり comlink/dist/esm/node-adapter.js にマップさせ、comlink/dist/esm/node-adapter.d.ts の型定義を使わせる、ということをしています。

github.com

けど comlink/dist/esm/node-adapter.d.ts は CJS な型定義ファイルなので、default の挙動がおかしくなります。TypeScript の型上では nodeEndpoint.default に default export されたものが詰め込まれているように見えるのですが、ランタイム上では nodeEndpoint に詰め込まれています。どうしようもないので as any してます。難しい。

github.com

所感

各ツールチェインで ESM のサポートが進みつつあるとはいえ、まだまだ workaround を駆使しないと難しい、というのが所感です。comlink のように 3rd-party の型定義が合わない問題も結構悩ましいです。こういう形式でモジュールを export しているパッケージは沢山あるので、至るところで同様の問題に遭遇しそうです。TypeScript の Node.js ESM が Stable に来たタイミングで、皆で協力して型定義を直していく、みたいな感じになるのかな?

Pure ESM なパッケージを自然に import できるようになったりといったメリットがあるので、業務のプロダクトでも入れられると良いかなと思っていたのですが、実際に ESM 対応をしてみて、業務のプロダクトへの導入はまだ先になるかなと思いました。eslint-interactive は最小限のツールチェインしか使っていなかったので、何とか ESM 化できましたが、業務のプロダクトのような大規模のものになると、ここで紹介していない問題も色々出てきそうな予感がします。webpack とか babel の ESM のサポート状況とかも関わってきそうです。とはいえまだ実験的な機能ですからね。導入できる日が来るのを楽しみに待ちましょう。

あと今回の作業で気づいたこととかあれば、適時ツールチェイン側に issue を立てたりしようと思います。ちゃんとコミュニティにもフィードバックしていきたい。それに同じことをしようとした人がいた時に、参考にしてもらえますしね。

*1:sindresorhus 氏ってどんな人? という方はこちら: https://github.com/sponsors/sindresorhus

vscode-jest を導入してテストの開発体験を向上させる

この記事は JavaScript Advent Calendar 2021 の 12日目です。11日目は なかがわはじめ さんの 「Array.prototype.reduce()とかのprototypeってなに?」 でした。


Jest は JavaScript のテスティングフレームワークです。Node.js で動くアプリケーションのテストや、React を使った Web アプリケーションのテストなど、様々なプロジェクトのテストをサポートしています。また、エディタ向けに拡張機能やプラグインも公開されています。

(各拡張機能のサポート状況によりますが) これらの拡張機能を使うと、エディタ上から Jest によるテストの実行やエディタへのテスト結果の表示、breakpoint を使ったテストの実行などができるようになります。こうした便利な機能を活用することで、テストの開発体験が大きく向上します。

この記事では vscode-jest を対象に、実際にどのように導入して、どういった機能が利用できるのかについて紹介します。

前提

まず vscode-jest を導入する前に、Jest が導入済みのプロジェクトを用意して下さい。この記事では以下のようなjest.config.jsで構成されるプロジェクトを例に解説していきます。

// @ts-check

/** @typedef {import('ts-jest/dist/types')} */
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
  preset: 'ts-jest',
  // test/ 配下にテストファイルがある想定
  testMatch: ['<rootDir>/test/**/*.test.ts?(x)'],
  globals: {
    'ts-jest': {
      tsconfig: 'tsconfig.test.json',
    },
  },
};

また、 Jest の テストを実行する npm-scripts も用意しておいて下さい。あとで使います。script 名はtestでもjestでも何でも良いです。

{
  "scripts": {
    // `npm run test` or `yarn test` で実行できる npm-scripts
    "test": "jest"
  },
}

vscode-jest の導入

準備ができたので、早速導入してみます。まずは以下のリンクから拡張機能をインストールしましょう。

marketplace.visualstudio.com

続けて jest.jestCommandLineというオプションを.vscode/settings.jsonに追加して下さい。これは Jest の起動方法を vscode-jest に教えてやるためのものです。事前に準備した Jest を起動する npm-script を書きましょう。yarn を使っていて、Jest を起動する npm-script の名前がtestなら、以下のように書きます。

// .vscode/settings.json
{
    "jest.jestCommandLine": "yarn run test",
}

vscode-jest はこのコマンドを使い、--watchオプションなどを付けてテストの自動実行を実現しています。そのため、npm を利用している場合は--を末尾に付けたコマンドを記入しなければなりません。ハマりやすいので注意して下さい。

// .vscode/settings.json
{
    // vscode-jest に jest を実行するコマンドを教えてやる
    // NOTE: `vscode-jest` はこのコマンドに `--watch` などのオプションを渡して実行するので、
    // `--` を明示してオプションが続けて渡せるようにしておく必要がある。
    "jest.jestCommandLine": "npm run test --",
}

これで完了です! ファイルの変更を保存して、VSCode を再起動してみて下さい。

各種機能の説明

インストールが完了すると、テストファイルの行番号の左にチェックマークが表示されたり、サイドバーに三角フラスコのメニューが追加されているはずです。1つずつ説明していきます。

エディタパネル

まず右側のエディタパネルから。行番号横の左にあるチェックマークは、テストの実行結果を示しています。passed なら緑のマークが、failed なら赤色のマークが表示されます。VSCode 起動時に自動的にテストが実行されるようになっており、起動した直後かつすべて pass していれば、すべてのテストケースに緑のマークが表示されているはずです。failed の場合は、assertion に失敗した行に赤の下線が引かれます。その行にマウスでホバーすると、失敗した原因のレポートを確認できます。

vscode-jest はテストの自動実行をサポートしています。そのためファイルを変更すると、関連するテストが自動的に実行されます。試してみましょう。

youtu.be

ちなみに全てのテストが自動実行される訳ではなく、モジュール同士の依存関係を理解して必要なテストファイルのみ実行してくれるようになっています。内部的には Jest の--watchオプションを用いているため、基本的にはjest --watchと同じ挙動をします。

Test Explorer

サイドバーに追加された三角フラスコアイコンのボタンを押すと、Test Explorer という機能が利用できます。これを使うと Jest のテストケースを一覧できます。

各テストケースの名前にホバーすると、「テストのデバッグ」ボタンと、「テストに移動」ボタンが表示されます。「テストに移動」ボタンを押すと、エディタパネルのほうで当該テストケースのあるファイルを開いて、その行にフォーカスを当ててくれます。これが地味に便利です。「テストのデバッグ」ボタンについては後で説明します。

上のほうにあるコンソールのようなアイコンのボタンを押すと、Jest のテストレポートをコンソール上で見ることができます。jest--watchで表示されるようなログがそのまま流れてきます。先程紹介した赤線にホバーした時に表示されるレポートはただのプレーンなテキストによるものでしたが、こちらは色付されているので、ちょっと見やすいです。かゆいところに手が届く感じがして良いですね。

デバッガーを利用する

vscode-jest は VSCode の組み込みデバッガーとの連携をサポートしています。これを使うと、breakpoint を使ったテストの実行などができます。

vscode-jest からデバッガーを利用するには、VSCode のデバッガー向けの設定ファイル (.vscode/launch.json) が必要です。vscode-jest にはこのファイルを自動生成する機能が実装されており、F1 > Jest Setup Extension (Beta) > Setup Jest Debug Config > Generateとするだけで簡単に用意できます。

www.youtube.com

これで以下のように、Test Explorer にある「テストのデバッグ」ボタンから breakpoint などを使ったデバッグが利用できるようになります。エディタパネルから変数をホバーして格納されている値を参照したり、ローカル変数一覧を参照したり、デバッグコンソールでローカル変数を使ってちょっとしたコードの評価をしたりできます。

www.youtube.com

以上が vscode-jest の各種機能の紹介でした。ちょっとした設定を書くだけで、これだけの機能が使えるのは驚きですね。エディタを離れることなくテスト結果が即参照できるというというのは思いの外体験が良くて、これだけでも十分導入する価値があります。またデバッガーを使いこなせれば、非常に効率よくテストをデバッグできます。こちらも是非導入してみてください。

本当はデバッガーの使い方などについてもっと丁寧に解説したかったのですが、非常に機能が豊富で説明しきれないので、都度他の資料を当たって下さい。

Tips: VSCode の起動時にテストを自動実行しないようにする

VSCode を起動すると、vscode-jest は自動的にテストを実行して、結果を表示してくれます。これは大変便利なのですが、VSCode の起動時に行われる VSCode 本体の初期化や、他の拡張機能の初期化のためのリソースを奪ってしまいます。個人的にはエディタが立ち上がったら素早くファイルを編集できるようになってほしいという思いがあり、起動時のテストの自動実行はおせっかいに感じています。

そこで id:mizdra は以下のような設定を.vscode/settings.jsonに書き、起動時にテストが自動実行されないようにしています。VSCode 上でファイルが変更されたらテストが自動実行がされる点は変わらないので、普段の開発体験への影響もほとんどありません。オススメです。

// .vscode/settings.json
{
  // VSCode の起動時に自動で jest のテストを走らせないように。
  // VSCode 起動時は色々な拡張機能の初期化処理が走っているので、
  // あまりカジュアルに起動時に処理を実行したくない、という狙いがある。
  // *.ts ファイルが変更されたら jest のテストが自動実行がされる点は変わらないので、
  // これで開発体験が損なわれることはないはず。
  "jest.autoRun": { "watch": true }
}

追記 (2022-11-17)

場合によっては Jest の起動がマシンの負荷になって、開発体験が悪くなることがあります (ファイルを保存する度に CPU が 100% に張り付くなど / Developer: Open Process Explorer を見よう!)。そこで id:mizdra は最近はテストファイルを保存した時のみテストを走らせるようにしています。

// .vscode/settings.json
{
  // VSCode の起動時に自動で jest のテストを走らせないように。
  // VSCode 起動時は色々な拡張機能の初期化処理が走っているので、
  // あまりカジュアルに起動時に処理を実行したくない、という狙いがある。
  // *.ts ファイルが変更されたら jest のテストが自動実行がされる点は変わらないので、
  // これで開発体験が損なわれることはないはず。
  "jest.autoRun": {
      "watch": false,
      // 加えて、普段のコーディングの時にいちいちテストが走っていては開発体験が悪いので、
      // テストファイルを保存したときにのみテストを走らせるように。
      "onSave": "test-file"
  },
}

逆にテストを先に書いて、後から実装を書く開発方法 (テスト駆動開発など) とは相性悪いので、そういう場合はこのオプションは不要かもしれません。もしくはテスト駆動開発したければ npm run test -- --watch するとかでも良いかもです。

あと VSCode 起動時に jest のログを表示するターミナルが自動に開いてわずらわしいので、設定で off にしています。

// .vscode/settings.json
{
  // vscode 起動時に jest の実行結果を表示するターミナルを自動で開かないように
  "jest.showTerminalOnLaunch": false
}

まとめ

この記事では vscode-jest を使ったテストの開発体験の改善について紹介しました。ちょっとした設定を書くだけで非常に強力な機能が使えるので、皆さんも是非導入してみてください。実際に紹介した手法で vscode-jest を導入してみた PR も貼っておきますので、参考にしてみてください。

github.com

JavaScript Advent Calendar 2021 の明日の担当は @youtoy さんです!

Babel をリファクタリングツールとして使う

この記事は はてなエンジニア Advent Calendar 2021 の 5 日目の記事です。

4 日目は id:anatofuz さんの 「入社してから書いていた分報の行数を眺めてみる」 でした。日報に書き込んだ行数を可視化するというアイデアが面白い! 僕も日報書いているので今度可視化してみようと思います。

anatofuz.hatenablog.com

本題

さて今回はタイトルにもある通り、Babel の話をします。Babel というのは JavaScript のトランスパイラです。 JavaScript のソースコードを入力として受け取り、適切な変換を施し、JavaScript のソースコードを出力する (トランスパイルする) ツールです。主に新しい構文で書かれた JavaScript を、古いブラウザなどでも動くよう、古い構文で書かれた JavaScript に変換するために使われています。

実はあまり知られていないのですが、 Babel は旧構文への変換以外にも、ソースコードを機械的に書き換えるためのリファクタリングツールとしても利用できます。通常 Babel を利用する際は、src/配下にある JavaScript ファイルを、Babel で変換してdist/に出力しますが、Babel をリファクタリングツールとして利用する場合は、src/配下にある JavaScript ファイルを、Babel で変換してsrc/に出力することになります。

f:id:mizdra:20211205173833p:plain
旧構文への変換ツールとしての利用した時と、リファクタリングツールとしての利用した時の違い

JavaScript におけるリファクタリングツールとなると、codemods や JSCodeshift が有名で、Babel の名前は聞き慣れないと思います。しかし、公式ドキュメントにも Babel がリファクタリングツールとして利用できることが明記されており、きちんとユースケースの一部としてカバーされていることが分かります。

Here are the main things Babel can do for you:

  • Transform syntax
  • Polyfill features that are missing in your target environment (through a third-party polyfill such as core-js)
  • Source code transformations (codemods)
  • And more! (check out these videos for inspiration)

https://babeljs.io/docs/en/

リファクタリングツールとして利用する方法

Babel をリファクタリングツールとして利用する方法については、Babel のメンテナーである Nicolò Ribaudo 氏が発表されているので、その資料を参考にするのが良いです。

発表では React の Class Component を Function Component を書き換える例が紹介されています。合わせてリファクタリングのデモに使ったコード郡も以下のリポジトリで公開されており、ここから簡単に Babel を使ったリファクタリングを試せます。

github.com

デモを動かしてみる

まずはデモを動かしてみましょう。README に起動方法が書いてあるので、その手順通りにやれば良いです。

$ # リポジトリを clone
$ git clone https://github.com/nicolo-ribaudo/conf-holyjs-moscow-2020
$ cd conf-holyjs-moscow-2020

$ # ディレクトリ構成を確認
$ ls -1F
Babel_ A refactoring tool.pdf
README.md
codemod/ ... Babel を使ったリファクタリングツールのコード
todomvc/ ... Class Component を含むリファクタリング対象のアプリケーションのコード

$ # codemod/ 配下に package.json があるので、`npm ci` で依存パッケージをインストールしておく
$ cd codemod
$ npm ci

$ # プロジェクトルートに戻る
$ cd ..

$ # リファクタリングツールを起動し、アプリケーションコードをリファクタリング
$ node codemod/run.js todomvc/js/*.{ts,tsx}

リファクタリングツールの実行が終わると、以下のように Class Component が Function Component に書き変わっているはずです。

$ git diff
diff --git a/todomvc/js/app.tsx b/todomvc/js/app.tsx
index 6fc2842..ded2b9c 100644
--- a/todomvc/js/app.tsx
+++ b/todomvc/js/app.tsx
@@ -7,167 +7,172 @@
 /// <reference path="./interfaces.d.ts"/>

 declare var Router;
-import React from "react";
+import React, { useState, useEffect } from "react";
 import ReactDOM from "react-dom";
 import { TodoModel } from "./todoModel";
 import { TodoFooter } from "./footer";
 import { TodoItem } from "./todoItem";
 import { ALL_TODOS, ACTIVE_TODOS, COMPLETED_TODOS, ENTER_KEY } from "./constants";

-class TodoApp extends React.Component<IAppProps, IAppState> {
+const TodoApp = props => {
+  const [nowShowing, setNowShowing] = useState(ALL_TODOS);
+  const [editing, setEditing] = useState(null);
...

リファクタリングツールの仕組み

codemod/配下のコードを見てみると分かるのですが、リファクタリングツールの実装はcodemod/codemod.jscodemod/run.jsのたった 2 つの JS ファイルから構成されています。codemod/codemod.jsは実行したい変換処理を表す Babel Plugin です。ここに Class Component を Function Component に変換する処理が書かれています。一方codemod/run.jsはコマンドライン引数で渡されたファイルを1ファイルずつ AST に parse し (ステップ1)、前述のBabel Plugin を使って AST を変換して (ステップ2)、AST を文字列化して元のファイルに上書きする (ステップ3) コードになっています。

// https://github.com/nicolo-ribaudo/conf-holyjs-moscow-2020/blob/7c2b597f284f0fbafe3f823aba0e6bc2f0e46154/codemod/run.js#L20 より一部引用
function transform(source, filename, codemod) {
  // ステップ1
  const ast = recast.parse(source, {
    parser: {
      parse(source) {
        return parser.parse(source, {
          filename,
          tokens: true,
          sourceType: "module",
          // typescript や jsx が扱えるように
          plugins: ["typescript", "classProperties", "jsx"],
        });
      },
    },
  });

  // ステップ2
  babel.transformFromAstSync(ast, source, {
    code: false,
    cloneInputAst: false,
    configFile: false,
    // 注: ここで plugin を差し込んでいる
    plugins: [codemod],
  });

  // ステップ3
  return recast.print(ast).code;
}

任意のリファクタリングができるように処理を差し替える

何となく気づいていると思いますが、codemod/codemod.jsの実装を変更すれば、任意のリファクタリングを実行できるようになります。先程も触れましたが、codemod/codemod.jsの実態はただの Babel Plugin なので、Babel Plugin のルールに沿ってコードを書いていけば良いです。

// @ts-check
// ts-check を使って書くと、型検査が効いてお得
module.exports = function plugin(babel) {
  /** @type {import('@babel/core').types} */
  const t = babel.types;
  /** @type {import('@babel/core').template} */
  const template = babel.template;

  return {
    visitor: {
      StringLiteral(path) {
        // シングルクオートで囲んでいるstring literal をダブルクオートで囲むように
        path.node.extra.raw = path.node.extra.raw.replaceAll("'", '"');
      },
    },
  };
};

使い所

正直なところ、単にリファクタリングしたいだけであれば JSCodeshift を使えば事足りるので、Babel をリファクタリングツールとして使う場面はそうないと思います。JSCodeshift はリファクタリングに特化したツール故に、リファクタリングに特化した utility が用意されていて、公式 example やも豊富で、使い方を解説した資料も沢山あります。一方で、Babel を使った手法では、Babel Plugin 向けに提供されている API をそのままリファクタリングに利用できたり、Babel Plugin のノウハウをそのまま利用できるという利点があります。これは Nicolò Ribaudo 氏の発表スライドでも触れられています。

f:id:mizdra:20211205164558p:plain
Babel: A refactoring tool 63 ページより引用

特に Babel Plugin のノウハウをそのまま活かせる、というのは大きなメリットだと考えています。ちょっと Babel Plugin を書いたことのある人なら、簡単に扱えますし、codemod.js を実装するときも既存の Babel Plugin のコードを参考にしたり、そのまま流用できます。

実際に id:mizdra は社内でとあるリファクタリングを行う際に、既存の Babel Plugin の一部コードをそのまま流用してリファクタリングをする、ということをやっていました。Babel Plugin 向けに書かれたコードを一切変更せずそのまま使えるので、(JSCodeshift 向けの codemod を新規に書くよりは) 簡単にリファクタリングを実施できました。

まとめ

今回は Babel を使ったリファクタリング手法について紹介しました。JSCodeshift という良い代替ツールがあるので、出番はそうないと思いますが、覚えておくとどこかで役に立つかもしれません。

明日は id:yutailang0119 さんです!

1Password のカスタムフィールドを autofill に利用する

1Password にはカスタムフィールドという機能があります。これを使うと、ログインのためのちょっとしたメモや、秘密の質問の答えなど、好きな情報を id/pass とともに記録できます。

f:id:mizdra:20211005001726p:plain
秘密の質問の答えを記録している様子

ところでこのカスタムフィールドは、実はログインフォームなどの autofill に活用することもできます。

具体例

例えば以下のような HTML Form があると仮定します *1

See the Pen by mizdra (@mizdra) on CodePen.

こうしたフォームがある時に以下のように 1Password を設定しておくと、所々の入力欄を autofill してくれます。

f:id:mizdra:20211005002513p:plain

f:id:mizdra:20211005003109g:plain

id:mizdra が調べた限りでは、以下の規則で「ラベル」や「新規フィールド」を設定しておくと、autofill してくれるようです。undocumented な機能だったので、正確な仕様は分かりません *2

  • ラベル: 以下のいずれかにマッチするもの
    • autofill したい入力欄のname属性の値に部分一致する文字列
    • autofill したい入力欄のid属性の値に部分一致する文字列
    • autofill したい入力欄に対応するlabel要素のテキストに部分一致する文字列
  • 新規フィールド
    • autofill したい値 (value属性の値)

別解

実はカスタムフィールドとは別に、form の autofill 用の設定を記入する欄が 1Password に用意されています。非常に分かりにくいのですが、Mac 版の 1Password 7 では「保存済みのフォームの詳細を表示」 から、その入力欄を表示できます。

f:id:mizdra:20211005003643p:plain

f:id:mizdra:20211005003850p:plain

専用の入力欄があるならそっちを使えば良さそうと思いつつ、入力欄が下の方にあって使い勝手が悪そうな感じもして難しいですね。皆さんはどうしてますか?

*1:説明のために誕生年を要求するフォームを挙げていますが、勿論こういったログインフォームを作るべきではありません。

*2:1Password のブラウザ拡張機能のソースコードを見ればなにか分かるかも

Sentry で IP アドレスの収集をやめる

@sentry/browser を使うと、ブラウザでエラーが発生した時にそのエラーを Sentry の集計サーバに送信して記録してくれます。送信されたエラーはエラーの種類ごとに Issue という単位にグルーピングされ、Issue ごとに何件発生しているのか、何人のユーザで発生しているのか、過去2週間にどれぐらいのエラー数の増減があったのか、などと簡潔に表示してくれます。便利ですね。セットアップも非常に簡単で、十数行程度のセットアップコードを書くだけで使い始めることができます。

エラーが Issue ごとにグルーピングされている様子。画像は https://docs.sentry.io/product/issues/ から引用。

IP アドレス の収集をやめる

ところでこのエラーが発生したユーザ数 (画像の USERS のカラムの部分) なのですが、デフォルトではエラーの送信元の IP アドレスを元に割り出しています。ブラウザから Sentry の集計サーバにエラーが送信されると、集計サーバ側で送信元の IP アドレスを取得し、それをキーにユーザを識別しています *1。何もしなくても勝手にユーザ識別してくれて便利ですね。

一方でプロダクトによっては諸事情で IP アドレスの収集をやめたいということもあると思います。実は管理画面から IP アドレスの収集をやめるオプションがちゃんと用意されています。

  • プロジェクトごとに収集停止する場合
    • Settings -> オーガニゼーション -> プロジェクト -> セキュリティとプライバシー > Prevent Storing of IP Addresses を ON に
  • オーガニゼーション全体で収集停止する場合
    • Settings -> オーガニゼーション -> セキュリティとプライバシー > Prevent Storing of IP Addresses を ON に
    • オーガニゼーション全体で収集停止すると、オーガニゼーション全体で強制的に収集停止になりプロジェクトごとに ON/OFF の切り替えさえできなくなってしまうので注意してください

IP アドレスの収集停止による弊害

IP アドレスの収集をやめると、Sentry からユーザを識別できなくなってしまいます。その結果として当然といえば当然なのですが、Issue ごとのユーザ数を見ることができなくなってしまいます。

ユーザが識別できなくて、ユーザ数が 0 埋めされてしまっている様子

ユーザ数はその Issue の深刻度を判断する上で非常に重要な情報ですから、これが見れないとなると Issue のトリアージが困難になってしまいます。IP アドレスの収集をやめたいとはいえ、流石にこれだと困りますね。

手動でユーザ情報を設定する

実は @sentry/browser では任意のユーザ情報を設定する Sentry.setUser という API が用意されています。これを使うと IP アドレス以外の任意のデータを、ユーザを区別するための情報として設定できるようになります。

例えば、以下のようにページアクセスの度にランダムで生成した文字列を id として設定しておけば、とりあえずユーザ数は表示されるようになります。もしセッションを跨いでも同じユーザであると判定したければ、localStorageid を保存しておけば良いです。

/** 0 以上 max 未満の整数をランダムで返す */
function getRandomInt(max: number): number {
  return Math.floor(Math.random() * max);
}

// Sentry SDK の初期化
Sentry.init({
  // ...
});

// ユーザ情報をセット
Sentry.setUser({ id: getRandomInt(0xffff_ffff).toString() });

もし IP アドレスを収集していないプロジェクトをやっていて、同様の問題でお困りの方が居ましたら、ぜひお試しください。

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.