Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

dependencies と devDependencies の使い分け

混乱しがちだったのでまとめました。

なぜ使い分けたいのか?

ウェブアプリケーション開発においては、本番環境で不要なパッケージをインストールせずに済むからです。npm には dependencies に書かれたパッケージのみインストールする機能があるので、これを使ってインストール時間を短くできます。

また、本番環境に不要なものを置くのはセキュリティの観点から良くありません。

ライブラリ開発の場合は異なる

ライブラリ開発ではウェブアプリケーション開発と比べて、devDependencies の意味合いがやや異なります。devDependencies には、配布パッケージに含めるべきでないパッケージを書きます。ウェブアプリケーション開発ではビルド時間が縮むくらいの違いでした。しかし、ライブラリ開発では配布パッケージに含まれるか否かに関わるので、使い分けがとても重要です。

開発環境でしか使わないものは devDependencies

大抵、本番環境ではリポジトリをクローンしてきてビルドをするだけです。そのため、ESLint や Prettier は本番環境で使わないものは devDependencies に書きます。反対に、本番環境でも使いたいものは dependencies に書きます。

  • dependencies に書くパッケージ
    • ビルドに必要なパッケージ(webpack、TypeScript、Babelなど)
    • 使用するライブラリ・フレームワーク(React や Vue)
  • devDependencies に書くパッケージ
    • Linter・Formatter(ESLint、Prettier 関連のパッケージ)
    • テストフレームワーク(Jest など)

ただし、TypeScript には注意してください。TypeScript は build でも test でも使います。そのため、TypeScript関連のパッケージは一概に dependencies、devDependencies のどちらに含まれるか断定できません。一例をあげます。

  • @types/jset: devDependencies(テストでしか使わないため)
  • @types/react: dependencies(ビルド時に使うため)
  • typescript: dependencies(ビルド時に使うため)

さらに: 開発環境でデプロイする場合

実は、わざわざ本番環境でクローン&ビルドせずとも、開発環境でビルドしたものをそのまま本番環境へデプロイできます*1。この場合、そもそも本番環境でパッケージのインストールをしないので、そこまで使い分けを意識する必要はありません。

ただし、CI/CDをしている場合は注意が必要です。この場合は結局、本番環境でクローン&ビルドしているのと変わりません。そのため、上で紹介したように dependencies と devDependencies を使い分ける必要があります。

ライブラリ開発について

ライブラリ開発の場合は多少異なりますが、考え方としてはほぼ同じです。

  • devDependencies: 開発時に必要なもの(ビルドツール含む)
  • dependencies: publish 後に必要なもの(require しているパッケージ)

ビルド関連ツールも devDependencies に含まれる点が異なっています。

まとめ

まず、ウェブアプリケーション開発とライブラリ開発に大別されます。

ウェブアプリケーション開発

  • 開発環境でデプロイする場合:
    • 特に必要なし
  • CircleCI等でデプロイする場合、本番環境でクローン&ビルドする場合:
    dependencies
    ビルド時に使うもの
    devDependencies
    テスト時に使うもの、linter、formatter

ライブラリ開発

dependencies
配布物に含めるべきもの(require するパッケージなど)
devDependencies
開発時に使うもの(ビルド、テスト、linter、formatter)

*1:というより、本番環境でビルドせずに済むので、できることなら開発環境でデプロイできるようにするべきです

Defx.nvim でプレビューウィンドウを自動的に閉じる

Defx.nvim には floating window を使ったイカしたプレビュー機能があります。しかし、何も設定をしていないと、defx を閉じたあともプレビューウィンドウは残ります。この記事は autocmd を活用してプレビューをうまく消そうという記事です。

Defx を閉じたタイミングを取得する

Defx.nvim のウィンドウを閉じると同時にプレビューウィンドウを閉じるには、ウィンドウが閉じたことを検知する必要があります。ウィンドウが閉じたときに発火するイベントはいくつかあります。

  • BufLeave
  • BufHidden
  • BufEnter(ほかのウィンドウに入った=前のウィンドウを離れたとみなせる)

Defx は独自に DefxClosed のようなイベントを発火しないので、これらを使わなければなりません。今回はBufHidden を使います。

BufHidden が発火したときに pclose (プレビューウィンドウを閉じるコマンド)を実行すればやりたいことが実現できます。

(BufLeave ではプレビューウィンドウを開いたタイミングにも発火するのでうまくいきません。また、BufEnter は defx のウィンドウが閉じられたのかを検知できません)

autocmd BufHidden \[defx\]* pclose

これで defx#do_action('quit') などで閉じればプレビューウィンドウも消えます。

ついでに: Gitのプロジェクトルートで defx を開く

あまりに記事のボリュームが少ないので、ついでに全く関係ない Tips をいくつか盛っておきます。

Defx はデフォルトでは現在のディレクトリを表示します。しかし、プロジェクトルート(.gitなどのある位置)で開いたほうが便利ですよね?というわけで、 DefxProjectFile() という関数を作りました。といってもほとんど fzf のリポジトリにある ProjectFiles を参考に書いただけですが。

nnoremap <silent> ,f :Defx `system('git rev-parse --show-toplevel 2> /dev/null')[:-2]` -search=`expand('%:p')`<CR>

ウィンドウが defx のみになったら vim を終了

quit をしてもファイラーウィンドウが残ってしまうのが嬉しいことはほとんどないので、ファイラーウィンドウのみになったら quit を実行するようにしてみました。

function! s:previewWindowOpened() abort
    for nr in range(1, winnr('$'))
        if getwinvar(nr, "&pvw") == 1
            return 1
        endif
    endfor
    return 0
endfunction

autocmd WinEnter \[defx\]* if winnr('$') == 1 || winnr('$') == 2 && <SID>previewWindowOpened() | quit | endif

WinEnter が発火したとき、 defx のウィンドウのみ、あるいは defx ウィンドウとプレビューウィンドウのみだったら終了します。

previewWindowOpened関数はこちらを参考にして、一部動かなかった箇所を修正しています。

.eslintrcを別フォルダに移す

ルートディレクトリにコンフィグファイルがちらばっていると気になりませんか?僕はとても気になります。そこで、設定ファイルを別ディレクトリへ移す方法をご紹介します。

package.jsonに追記するだけで良い

たとえば.config/eslintrc.jsへ移動させたとします。そしたら、以下をpackage.jsonへ記述します。

{
  ...
  "eslintConfig": {
    "extends": "./.config/eslintrc.js"
  },
  ...
}

基本的にはこれだけでOKです。

原理

extendsをつかってファイルを指定すると、そのファイルに書かれた設定が読み込まれます。今回でいえば.config/eslintrc.jsを使うように設定してあるので、これまでと同様に扱うことができます。

上の設定はエディタのプラグインともうまく協調します。また、すでに書いたnpm-scriptsのeslint部分をいじる必要もありません。

Vue3に書き直してみる

Vue2からVue3へ書き直す際に情報が不足していて困ったのでまとめておきます。

公式ドキュメント

まだversion3の公式ドキュメントは揃っていません。マージされたPRRFCを見るしかない状況です。

migrate from v2 to v3

※「これだけ押さえれば7割くらいは移行できるだろう」くらいで網羅的には書けておりません

  1. new Vueによるインスタンス生成はcreateApp()へと置き換え
  2. setupメソッドの引数の型推論をしたい場合はSFCのexportするオブジェクトをdefineComponentでラップ
  3. new Vueのdataやmethodsはinject()app.provide()を使ってグローバルステートで置き換え React Context APIのようなもので、書き方もだいたい同じ。
  4. watchやdata、computed、mountedなどほとんどのプロパティは消えてsetupへ集約
  5. vue-template-compilerの代わりに@vue/compiler-sfcを使う
  6. shims-vue.d.ts(SFCの型定義ファイル)の書き方が変わる
  7. render関数の引数として渡されていたh関数はグローバルインポート

インストール

バージョンは2020年6月19日現在のものです。

  • vue@3.0.0-beta.15

SFCで書きたい、webpackを使いたいのであれば以下も必要です。

  • vue-loader@16.0.0
  • @vue/compiler-sfc@3.0.0-beta.15
  • webpack, webpack-cli

書き方

大きく異なる点がいくつかあります

  • composition APIが使える
  • webpackのalias設定がいらなくなる
  • new Vueが消える
  • Vueオブジェクトに生えていたAPIがnamed exportsオンリーになる
  • TypeScriptとの親和性が高くなる

composition APIが使える

まずこれがかなり大きいです。このお陰でVueへもTypeScriptを導入しやすくなりました。this関連でひどい目に合わずに済むのは大変嬉しいです。

また、書き方もかなり楽になりました。ロジックを外へ抽出できるようになった点も嬉しいです。ReactのHooksとできることはほぼ同じですが、かなりVueらしいやり方に収まった感覚があり、とても好きです。

webpackのalias設定がいらなくなる

new Vueの代わりにcreateAppというメソッドを使ってマウントするようになりました。そのため、vue/dist/vue.esm.jsへエイリアスを張らなくて良くなります。逆に、Vue2の頃のaliasがwebpack.config.jsに残っているとビルドが失敗します。

createApp

こいつが結構やっかいです。従来のやりかたと所々異なっています。

rfcs/0009-global-api-change.md at 9f18645a700f54e7d9a4e3b53046d48791e31459 · vuejs/rfcs · GitHub

上記リンク先を見ていただけばわかりますが、ほとんどは同じです。しかし、たとえば下のようなケースでは若干変わってきます。

// v2
new Vue({
  render: (h) =>
    h(MyComponent, {
      props: { prop1, prop2 },
    })
})
// v3
const app = createApp(MyComponent, { prop1, prop2 });

僕はかなり泥沼にハマりました。

defineComponentについて

setupのpropで型推論を働かせたいときにはdefineComponentでラップする必要があります。逆に、setupでpropを参照しないのならdefineComponentで囲む必要はなく、従来のSFCの書き方で十分です。

公式にも書いてありますが、defineComponent自体は受け取ったオブジェクトをただ返すだけの関数で、型推論以外では1ミリも役に立ちません。

Note that implementation-wise defineComponent does nothing - it simply returns the object passed to it. However, in terms of typing, the returned value has a synthetic type of a constructor for manual render function, TSX and IDE tooling support. This mismatch is an intentional trade-off.

JSXについて

renderのなかでJSXを使えるようになりました(今まではh関数を使ったcreateElement的な書き方しかできなかった)。これでvueファイルの中の<template>を消してJSオンリーでかけるようになりました。

Global APIのnamed exports化

tree-shakingのための変更です。Vue以下に生えていたVue.nextTickなどはnextTick関数としてexportされるようになります。Vue.nextTickは使えなくなります。このように、Vue3には後方互換性のない変更があるので注意が必要です。

SFCの型定義が変わる

new Vueによるインスタンス生成から変わった影響で、SFCの型定義もdefineComponentを使うものへ変わりました。

declare module "*.vue" {
  import { defineComponent } from "vue";
  const component: ReturnType<typeof defineComponent>;
  export default component;
}

従来の型定義はcreateAppの引数として使えないので注意してください。

参考: vue-next(Vue.js 3.0 wip)+ TypeScript + webpackでの開発環境を構築する - Qiita

TypeScriptで配列からnullを取り除く

結論

const a = [1, 2, 3, null];
// nullとundefinedをはじく
const filtered = a.filter(<T>(n: T): n is NonNullable<T> => n != null);
// nullだけはじく
const filtered2 = a.filter(<T>(n: T): n is Exclude<T, null> => n !== null);

nullを除いた配列の型

nullを許容する配列があったとします。この配列にfilterをかけてnullをはじくコードは以下のようになります。

const a = [1, 2, 3, null];
const filtered = a.filter(n => n != null);

しかし、これだけではfilteredの型は(number | null)[]になります。filteredの型をnumber[]型にするにはasかisを使います。

const filtered = a.filter(n => n != null) as number[];
const filtered = a.filter((n): n is number => n != null);

これだけでもある程度は十分です。しかし、オブジェクトの配列であった場合、これだけでは不十分な場合があります。

type Obj = {
  hoge: string;
  fuga: number;
};
type ExObj = Obj & { piyo: boolean };
const objs: (Obj | ExObj | null)[] = [
  { hoge: "hoge", fuga: 3 },
  { hoge: "hoge", fuga: 3, piyo: true },
  null,
];
const filtered = objs.filter((n): n is Obj => n != null); // Obj[]型になって情報が失われる!

というわけで、もっときちんと型をつけましょう。

コード解説

const a = [1, 2, 3, null];
const filtered = a.filter(<T>(n: T): n is Exclude<T, null> => n != null);

ExcludeはUnion型Tを受け取り、TからUを除いたUnion型を返します。たとえばExclude<string | number | null, null>string | numberです。NonNullable<t>はTからnullとundefinedを除いた型を返します。

Exclude、NonNullableを使っているため、コードの意味もnullを除いたUnion型の手書きよりハッキリしています。

参考

参考というか、このページをもとに改良したのがこの記事です。