Madogiwa Blog

主に技術系の学習メモに使っていきます。

Vue.js: `vue-tsc`でSFC内のtemplateも含めてTypeScriptの型チェックを行うMEMO

vue-tscなるものを使うとSFC内のtemplate部分も含めて型チェックを行えて便利っぽいようなので使い方とかをメモ

www.npmjs.com

vue-tscとは?

vue-tscとはVeturの後継であるVolar内で管理されているライブラリです。

github.com

vue-tsc Type-check and dts build command line tool

上記の通りSFC内のテンプレートを含めた型チェックを行うことができます。

vue-tscの使い方

インストール

以下でinstallします📦

# yarn
yarn add -D vue-tsc

# npm
npm i vue-tsc -D

tsconfig.jsonの設定

以下のVue.jsの公式ドキュメントに記載されている推奨設定をもとにtsconfig.jsonを修正します。

v3.ja.vuejs.org

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "strict": true,
    "jsx": "preserve",
    "moduleResolution": "node"
  }
}

package.jsonのコマンド

元々TypeScriptでの型チェックを行なっていたコマンドにvue-tsc --noEmitを追記します。

"lint:type-check": "tsc -p . --noEmit && vue-tsc --noEmit",

型チェックの実行

実行すると以下のような形でtemplate部分の型チェック結果が表示されます🙆✨

$ yarn lint:type-check

app/javascript/components/entry/EntryCardCollection.vue:3:32 - error TS2339: Property 'entries' does not exist on type 'never'.

3     <div v-for="entry in props.entries" :key="entry.id" :class="`column is-${clumnSize}`">

VS Code上で警告出す

以下の拡張機能を使うとVS Code上で警告を出せるのでより便利でした。

marketplace.visualstudio.com

※Veturと競合するようなので、Veturは無効化しておくのが良いようです。

おわりに

template部分も型チェックできるの、大分良いですね!!

参考

tech.visasq.com

zenn.dev

TypeScript: ESLintで`_`を変数に含めた時に`no-unused-vars`のルールを無効化する方法MEMO

Vue.jsのsetupのprops等、引数を使わないけど型定義だけ設定しときたい場合等、no-unused-varsを無効化したいケースがあります。

github.com

毎回eslint-disable-lineを使うのも手間なので、他の静的解析のツールの慣習に従って_始まりの場合は許容したかったのですが意外と簡単にできたのでメモ

やり方

やり方は簡単で以下のような設定を入れるだけです。

"rules": {
    "@typescript-eslint/no-unused-vars": [
      "warn",
      {
        "argsIgnorePattern": "^_",
        "varsIgnorePattern": "^_",
        "caughtErrorsIgnorePattern": "^_",
        "destructuredArrayIgnorePattern": "^_"
      }
    ]
  },

argsIgnorePatternが引数で、varsIgnorePatternが変数で、caughtErrorsIgnorePatternがerrorハンドリングで、destructuredArrayIgnorePatternが配列内の変数参照です。

詳細はこちら

eslint.org

参考

stackoverflow.com

フロントエンドのテストフレームワークをJestからVitestに移行するメモ

テストフレームワークをJestからVitestに移行するのを試してみたので手順とか躓いたところとかをメモ🗒

Vitestとは?

Vitestは、「A blazing fast unit-test framework powered by Vite ⚡️」とあるようにViteを使った単体テストフレームワークです。

vitest.dev

Viteに関してはこちら

madogiwa0124.hatenablog.com

JestからVitestに移行する

基本的には以下の公式のマイグレーション手順に従っていけば大丈夫です。

Migration Guide | Vitest

Vitestのインストール

以下の手順に従ってVitestをインストールします。

Getting Started | Vitest

$ npm install -D vitest

Vitest requires Vite >=v2.7.10 and Node >=v14

※上記の通り、VitestはViteに依存していますがpackage.jsonにdependenciesに記載されてるので明示的なinstallは不要

Vitestの構成ファイルを作成する

以下のリポジトリ内のサンプルを元に自身の環境に合わせたVitestの構成ファイルを作成します。

github.com

私はVue3を使っているのでvueを参考にして以下のような感じで作成しました。

/// <reference types="vitest" />

import { defineConfig } from "vite";
import Vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [Vue()],
  test: {
    globals: true,
    environment: "jsdom",
  },
  resolve: {
    alias: {
      vue: "vue/dist/vue.esm-bundler.js",
      "@": `${__dirname}/src`,
      "@js": `${__dirname}/src/javascripts`,
      "@css": `${__dirname}/src/styles`,
    },
  },
});

型エラーが出る場合は以下のようなtypesを設定をtsconfig.jsonでしてあげると解決すると思います。

{
    "types": ["node", "vitest/globals"],

Vitestはデフォルトでrootのvite.config.tsを読み込んでくれるので、以下のようにvite.config.tsにVitest用の構成を追記して管理することもできます。

/// <reference types="vitest" />

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import legacy from "@vitejs/plugin-legacy";

import getEntries from "./config/vite/util/getEntries";
const JAVASCRIPT_ENTRY_PATH = "./src/javascripts/entries/";
const HTML_ENTRY_PATH = "./src/";

export default defineConfig({
  build: {
    manifest: true,
    rollupOptions: {
      input: { ...getEntries(JAVASCRIPT_ENTRY_PATH), ...getEntries(HTML_ENTRY_PATH) },
    },
    outDir: "public",
    assetsDir: "packs",
  },
  test: {
    globals: true,
    environment: "jsdom",
  },
  plugins: [vue(), legacy({ targets: ["defaults", "not IE 11"] })],
  resolve: {
    alias: {
      vue: "vue/dist/vue.esm-bundler.js",
      "@": `${__dirname}/src`,
      "@js": `${__dirname}/src/javascripts`,
      "@css": `${__dirname}/src/styles`,
    },
  },
});

テストコードの修正

VitestはJestとテスト用のメソッド名が同じため普通にテストをしている分には特に修正の必要はありませんでした。

私は使ってなかったのですがCallbackMock周りをJestで使っている場合は以下を参考に修正が必要そうです。

Auto-Mocking Behaviour
Unlike Jest, mocked modules in /mocks are not loaded unless vi.mock() is called. If you need them to be mocked in every test, like in Jest, you can mock them inside setupFiles.

Done Callback
From Vitest v0.10.0, the callback style of declaring tests is deprecated. You can rewrite them to use async/await functions, or use Promise to mimic the callback style.

https://vitest.dev/guide/migration.html#migrating-from-jest

テストを実行する

後は以下のようにテスト実行するだけです ✨ ※カバレッジを出したい時はvitest run --coverageを使います。

$ npm run test

> vite_study@1.0.0 test
> vitest


 DEV  v0.10.0 /Users/jun.morita/Documents/repo/vite_study

 √ spec/components/TopImage.spec.ts (1)
 √ spec/components/JsCounter.spec.ts (1)
 √ spec/components/TsCounter.spec.ts (1)

 Snapshots  2 obsolete
            ↳ spec/components/JsCounter.spec.ts
              · components/jsCounter.vue 初期表示 snapshot 1
            ↳ spec/components/TsCounter.spec.ts
              · components/tsCounter.vue 初期表示 snapshot 1

Test Files  3 passed (3)
     Tests  3 passed (3)
      Time  881ms (in thread 38ms, 2319.36%)


 PASS  Waiting for file changes...
       press h to show help, press q to quit

おわりに

個人の規模の小さいサンプルだったからかもですが、思ったよりもスッとJestからVitestに乗り換えることができました。 Viteでbuildしていると構成ファイルを統一できるのも管理が楽で良いですね⚡️

Vite/Vitest/Vue3/TypeScriptな構成のサンプルリポジトリも以下で公開してみました。

github.com

viteを軽く触ってみたのでメモ

Viteを軽く触ってみて分かった使い方とかメモ

ja.vitejs.dev

色々試してるリポジトリ

github.com

Viteのinstall/設定ファイルの作成

ドキュメント通りにnpm create vite@latestで自分の作りたい環境からテンプレートを作って、そこからカスタマイズしてくのが楽そうだった。

vitejs.dev

ちなみに以下はTypeScript + Vue3のViteのbuildのconfigファイル、webpackに比べると非常にシンプル。

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()]
})

Viteの開発build

開発用のbuildはnpm run viteで行う。実行するとwebpack-dev-server的なものが立ち上がりファイル修正時の自動ビルド、ホットリロードが実行される。 体感的にはすごくサクサクだった。

Viteではプロジェクトのrootにindex.htmlを配置して、そこがデフォルトのエントリになる。

お気づきかもしれませんが、Vite プロジェクトでは index.html は public 内に隠れているのではなく、最も目立つ場所にあります。これは意図的なものです。開発中、Vite はサーバで、index.html はアプリケーションのエントリポイントです。 はじめに | Vite

jsのエントリの読み込みはscript type="module"で、読み込みたいjsを読み込めばよい。 ※読み込むと自動的にエントリーになる。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    hello My index
    <div id="vue-root">
      <ts-counter></ts-counter>
      <js-counter></js-counter>
    </div>
    <script type="module" src="/src/javascripts/entries/index.js"></script>
  </body>
</html>

Viteの本番build

本番用のbuildはnpm run vite buildで実行される。この時に裏でRollupによって実行されるためbuild.rollupOptionsでこの時のオプションを色々修正できる。

ja.vitejs.dev

npm run vite previewを実行するとビルド結果をローカルで確認できる。

vite preview コマンドは、ローカルで静的なウェブサーバを起動し、dist のファイルを http://localhost:4173 で配信します。これは、プロダクションビルドが問題ないかどうかを自分のローカル環境で確認する簡単な方法です。 ja.vitejs.dev

その他Tips

build時にpublic/packsに出力したい

build.outDirbuild.assetsDirをいじれば任意のパスにbuildできる。 manifesttrueにすればmanifestも吐き出される。

  build: {
    manifest: true,
    outDir: "public",
    assetsDir: "packs",
  },

zenn.dev

レガシーブラウザへの対応

ViteはNative ESMをサポートしているブラウザ向けにビルドするので、サポートしてないブラウザでは動かない。 レガシーブラウザへの対応は、@vitejs/plugin-legacyでできるっぽい。

babelとかcorejsに依存しているから、pluginを入れて設定いれるだけで自動的やってくれるっぽい。

Vite's default browser support baseline is Native ESM. This plugin provides support for legacy browsers that do not support native ESM when building for production. https://github.com/vitejs/vite/blob/main/packages/plugin-legacy/package.json

mpaにしたい(明示的にエントリーを指定したい)

rollupOptions.inputで任意のhtmlをentryに含めるようにすればいいっぽい。

vitejs.dev

mpaのoutputパスを任意のパスにしたい

Viteはwebpackみたいにentry単位にoutputを設定することは簡単にはできないっぽい。

You can't because vite build runs a single Rollup build. If you want multiple builds, have multiple vite config files and run vite build -c different.config.js https://github.com/vitejs/vite/issues/2039

jsとかcssとかだったらいいけど、root直下にhtmlをエントリーのhtmlをたくさん置きたくないので、 src/pages配下に置きたいけどoutputを修正できない。

またassetsDirとかでもいじれないから以下のようになってしまい/src/pagesみたいなパスでアクセスすることになる。。。

public
├── manifest.json
├── packs
│   ├── home.449f531d.css
│   ├── home.fe9aefcd.js
│   ├── index.9506a175.js
│   ├── index.95a8fec0.css
│   ├── jsCounter.434c301e.css
│   └── jsCounter.ad43c1b1.js
└── src
    └── pages
        ├── home.html
        ├── index.html
        └── sub
            └── index.html

vite-plugin-mpaを使うといい感じのpathにビルド後にbuildパスを書き換えてくれるっぽいが、 jsとhtmlを一緒のディレクトリに配置しないといけないっぽい。

github.com

このへんのコード

github.com

CSSの分割方法を調整したい

CSSの分割方法とかは、自動適用されるので無効化するか、提供されるものを使うかしかないっぽい?

ビルドの最適化 以下にリストされている機能は、ビルドプロセスの一部として自動的に適用され、無効にする場合を除いて、明示的に構成する必要はありません。 特徴 | Vite

参考

ics.media

lockファイル内のライブラリがどのライブラリによってinstallされているか調べる方法MEMO

間接的に依存しているライブラリに脆弱性がある場合等、自身のpackgae.jsonのどのpackageを更新すれば解消できるか等を調べたい場合、lockファイルを頑張って確認するのは大変です。

package manager(yarn, npm)の機能を使うと割と簡単に調べられたのでメモ🗒

yarn

yarnだとwhyという機能が提供されていています。

yarn why | Yarn

使い方は簡単でyarn why package-nameで実行すると以下のように依存関係を出力してくれます。

$ yarn why minimist
yarn why v1.22.17
[1/4] 🤔  Why do we have the module "minimist"...?
[2/4] 🚚  Initialising dependency graph...
[3/4] 🔍  Finding dependency...
[4/4] 🚡  Calculating file sizes...
=> Found "minimist@1.2.6"
info Reasons this module exists
   - "@lhci#cli#chrome-launcher#mkdirp" depends on it
   - Hoisted from "@lhci#cli#chrome-launcher#mkdirp#minimist"
   - Hoisted from "@lhci#cli#update-notifier#latest-version#package-json#registry-auth-token#rc#minimist"
info Disk size without dependencies: "104KB"
info Disk size with unique dependencies: "104KB"
info Disk size with transitive dependencies: "104KB"
info Number of shared dependencies: 0
✨  Done in 0.20s.

info Reasons this module existsで最上位に記載されているpackageが自身が直接依存している(package.jsonに記載している)packageということですね。便利!

npm

npmではnpm lsという機能が提供されています。

docs.npmjs.com

こちらも使い方は簡単でnpm ls package-nameで依存関係のツリーを表示してくれます。

$ npm ls kind-of
my_package@1.0.0 /foo/bar
└─┬ webpack-cli@4.4.0
  └─┬ webpack-merge@5.7.3
    └─┬ clone-deep@4.0.1
      ├── kind-of@6.0.3
      └─┬ shallow-clone@3.0.1
        └── kind-of@6.0.3 deduped

my_packageを除いた最上位に記載されているpackageが自身が直接依存している(package.jsonに記載している)packageということですね。便利!

おまけ: RubyGems

bundle exec gem dependency -Rで調べられるっぽい。リストが間接的に依存しているもので、Used byにinstall済みで直接 nokogiriに依存しているライブラリっぽい(?)、引数にはgem名を表す正規表現を指定する。完全一致にしたい場合には^foo$で完全一致に該当する正規表現を設定してあげれば良さそう。

bundle exec gem dependency -R "^nokogiri$"
Gem nokogiri-1.13.3-arm64-darwin
  bundler (~> 2.2, development)
  hoe-markdown (~> 1.4, development)
  minitest (~> 5.15, development)
  minitest-reporters (~> 1.4, development)
  racc (~> 1.4)
  rake (~> 13.0, development)
  rake-compiler (= 1.1.7, development)
  rake-compiler-dock (~> 1.2, development)
  rdoc (~> 6.3, development)
  rexical (~> 1.0.7, development)
  rubocop (~> 1.23, development)
  rubocop-minitest (~> 0.17, development)
  rubocop-performance (~> 1.12, development)
  rubocop-rake (~> 0.6, development)
  rubocop-shopify (~> 2.3, development)
  ruby_memcheck (~> 1.0, development)
  simplecov (~> 0.21, development)
  Used by
    rails-dom-testing-2.0.3 (nokogiri (>= 1.6))
    loofah-2.15.0 (nokogiri (>= 1.5.9))
    actiontext-7.0.2.3 (nokogiri (>= 1.8.5))
    xpath-3.2.0 (nokogiri (~> 1.8))
    capybara-3.36.0 (nokogiri (~> 1.8))
    reverse_markdown-2.1.1 (nokogiri (>= 0))

参考

qiita.com

Ruby on Rails: `strict_loading!`をしたPolymorphicなModelからassociationを辿ると`ArgumentError`が発生する

タイトル通り、かなりハマったので事象をメモしておく。

事象

タイトル通りですが、strict_loading!をしてeager_loadingを強制しているPolymorphic関連を持つモデルから、 関連を辿ると本来であればActiveRecord::StrictLoadingViolationErrorが発生すると思うのですが、 以下の通りArgumentErrorが発生します。

ArgumentError: Polymorphic associations do not support computing the class.
  activerecord-7.0.2.3/lib/active_record/reflection.rb:417:in `compute_class'
  activerecord-7.0.2.3/lib/active_record/reflection.rb:376:in `klass'
  activerecord-7.0.2.3/lib/active_record/core.rb:241:in `strict_loading_violation!'
  activerecord-7.0.2.3/lib/active_record/associations/association.rb:220:in `find_target'
  activerecord-7.0.2.3/lib/active_record/associations/singular_association.rb:44:in `find_target'
  activerecord-7.0.2.3/lib/active_record/associations/association.rb:173:in `load_target'
  activerecord-7.0.2.3/lib/active_record/associations/association.rb:67:in `reload'
  activerecord-7.0.2.3/lib/active_record/associations/singular_association.rb:11:in `reader'
  activerecord-7.0.2.3/lib/active_record/associations/builder/association.rb:104:in `target'
  active_record_gem.rb:46:in `test_strict_loading_to_Polymorphic_model'

再現コードはこちら

github.com

解決策

とりあえずstrict_loading!(false)して、strict_loadingを無効化してあげれば例外発生自体は防げるのですが、ArgumentErrorになるのは謎ですね。。。 ※ strict_loadingってPolymorphic associationに対応していない?

おわりに

strict_loadingとPolymorphic associationの掛け合わせなので、あんまりハマることも少ないかもですが。。。Rails難しい。。。 strict_loading使ってたけど、まだ本番利用してるところは少ないのかも?

2022/04/17追記

issueあげてみました

github.com

2022/05/07追記

対応版のPRがマージされて7.0 stableにバックポートされそうなので次回リリースで修正されそうです🙏✨

github.com

参考

madogiwa0124.hatenablog.com

exporse-loaderを使ってwebpackでimportしたライブラリをGlobal Objectに展開する。

jQueryなど一定Global Objectに展開したいライブラリをwebpackで扱う場合に単純に以下のように書いてしまうと、Global Objectに展開されずReferenceErrorが発生してしまいます。

import "jquery";

// Uncaught ReferenceError: $ is not defined
$(document).ready(function () {
  console.log("ready!");
  console.log(jQuery().jquery);
});

これはドキュメントにも記載の通りwebpackの思想によるものですが、Global Objectに展開したいケースもあるかと思います。

Warning We don't recommend using globals! The whole concept behind webpack is to allow more modular front-end development. This means writing isolated modules that are well contained and do not rely on hidden dependencies (e.g. globals). Please use these features only when necessary. Shimming | webpack

そんな時にexpose-loaderを使うといい感じに展開できたのでメモ🗒

webpack.js.org

準備

今回はjQueryをGlobal Objectに展開することを想定するため以下のようにjQueryとexpose-loaderをインストールします。

$ yarn add jquery
$ yarn add -D expose-loader 

expose-loaderを使ってimportしたライブラリをGlobal Objectに展開する。

後は簡単で任意のファイルでjQueryのimportをimport "expose-loader?exposes=展開したい変数名!package名";で記載すればOKです。jQuery$jQueryとしてGlobal Objectに展開する場合には以下のような形で記載します。

import "expose-loader?exposes=$,jQuery!jquery";

$(document).ready(function () {
  console.log("ready!");
  console.log(jQuery().jquery);
});

以下のドキュメントに記載の通りConfigでも設定できるようです。

https://webpack.js.org/loaders/expose-loader/#using-configuration

仕組みとしては、global objectを取得して、設定した名前のプロパティにmoduleを展開するような仕組みとなっているようです👀

github.com

おわりに

webpackはこういうサポートが豊富なところ便利ですね✨(他のライブラリにもあるかもですが)