Madogiwa Blog

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

Viteのdefineを使ってGlobalな定数を定義する

Viteのdefineを使ってGlobalな定数を定義するのにちょっとハマったのでメモ📝

例えばViteの実行時にしていたmodeに合わせてNODE_ENVのような定数をグローバルに定義したい場合は以下のようにすればOKです👍

export default defineConfig(({ command, mode }) => {
    define: {
      NODE_ENV: `"${mode}"`,
    },
  };
});

文字列を渡したとしても明示的にセミコロンを付与してあげる必要があるようです。

文字列の値は純粋な式として評価されるので、文字列の定数を定義する場合は、明示的に引用符で囲う必要があります https://ja.vitejs.dev/config/shared-options.html#define

TypeScriptを使っている場合は型エラーにならないように以下のように型定義も必要です⚠

// global.d.ts
declare const NODE_ENV: "development" | "test" | "production";

おわり。

参考

blog.recruit.co.jp

Viteでpackage関連(node_modules)のファイルをvendor chunkに分割する方法

node_modules配下のpackageのchunckをvendor chunkに分割するといったWebpack時代の最適化手法をViteで再現する方法をMEMO📝

各チャンクと router で使ってる node_modules で、ライブラリがかなりの部分で重複してしまう。(react, vue など) なので、 vendor chunk という共通チャンクに追い出すテクニックがある。 Webpack チャンク最適 テクニック - Qiita

やり方は以下の通り、簡単でViteに付属しているsplitVendorChunkPluginを利用するだけです✨ (Vite v2.8以前はデフォルトでvendor chunkを分割する戦略になっていたようです)

You can continue to use the Split Vendor Chunk strategy by adding the splitVendorChunkPlugin in your config file

// vite.config.js
import { splitVendorChunkPlugin } from 'vite'
export default defineConfig({
  plugins: [splitVendorChunkPlugin()]
})

Building for Production | Vite

コードを読む限りだと、CSSでもstatic fileでもなくnode_modulesが含まれていたらvendorにchunkを分割するようになってるようですね。

github.com

Vite、こういった設定が簡単にできるようになっているの便利ですね!⚡

dependabotでterraform providerのバージョンアップを行うMEMO

dependabotでterraform providerのバージョンアップを行うようにしたのでやったことととかをメモ📝 (ほぼ以下の記事の通りですが🙏)

qiita.com

dependabot用のyamlを用意する

記事の通りに以下のようなyaml.github/dependabot.ymlに配置してあげるだけでOKでした🙆‍♂️

version: 2

updates:
  - package-ecosystem: terraform
    directory: /
    schedule:
      interval: monthly

DependabotでTerraformのproviderのバージョンを上げる - Qiita

サポートしているterraformのバージョン等は以下の公式docを参照してください📘

https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file?learn=dependency_version_updates&learnProduct=code-security#package-ecosystem

dependabot用のsecretを設定する

dependabotのPRではsecretにアクセスできないのでterraform cloudで管理していてterraform init等をciで実行している場合には以下の手順にしがってdependabot用のsecretを設定します。

https://docs.github.com/en/code-security/dependabot/working-with-dependabot/managing-encrypted-secrets-for-dependabot

おわりに

手動でupdateしなくて良いのは便利ですね!!

Ruby on Rails: webpack + SimpackerからVite + Vite Railsに移行するメモ📝

最近個人のサービスのフロントエンド周りをタイトル通りwebpack + SimpackerからVite + Vite Railsに移行したので対応したこととかをメモ

前提事項

  • Vite v.3.2系のHMRは使わない構成になります
    • Vite Rubyを使うとbin/viteで起動しないといけないっぽかったのでフロントエンドのbuildをrubyのコマンドで行うのがちょっと違和感があった。。。
  • フロントエンド系のライブラリはVue.js v3系、TypeScriptを利用しています。
  • Railsは v7.0系、Vite Railsはv3.0系を利用しています

Viteで既存をファイルをbuildしてpublic/pack配下に吐き出すようにする(webpack -> Vite)

Viteの導入方法や各種設定の詳しい説明は省きますが、私は以下のようなvite.config.tsを作成してapp/javascript/packs配下のエントリーをViteでbuildし、webpack + simpacker時代と同じようにpublic/packs or public/packs-testに出力するようにしました。(buildの設定は既存のwebpackの構成によって異なるので自身の環境に合わせて変更してください)

ja.vitejs.dev

import { defineConfig } from "vitest/config";
import vue from "@vitejs/plugin-vue";
import EnvironmentPlugin from "vite-plugin-environment";
import * as glob from "glob";
import * as path from "path";

/**
 * 指定したentryのルートディレクトリ配下のjsまたはtsファイルのファイル名とパスのobjectを取得
 * 例)
 *  - /entries/foo.ts => { foo: "/entries/foo.ts" }
 *  - /entries/bars/bar.ts => { "bars/bar": "/entries/bars/bar.ts" }
 * @param {string} entryRoot entryのルートディレクトリ
 */
const getEntries = (entryRoot) => {
  const result = [];
  const filePaths = glob.sync(`${entryRoot}/**/*.{js,ts,html}`);
  filePaths.forEach((filePath) => {
    const dirName = filePath.replace(entryRoot, "").replace(path.basename(filePath), "");
    result[`${dirName}${path.basename(filePath, path.extname(filePath))}`] = filePath;
  });
  return result;
};

const JAVASCRIPT_ENTRY_PATH = "./app/javascript/packs/";
const outDirPath = (mode: string) => (mode === "test" ? "public/packs-test" : "public/packs");

export default defineConfig(({ command, mode }) => {
  return {
    build: {
      manifest: "manifest.json", // vite_rubyはmanifest.jsonを固定で参照するので名称を固定
      rollupOptions: {
        input: { ...getEntries(JAVASCRIPT_ENTRY_PATH) },
      },
      copyPublicDir: false, // public配下の既存ファイルがpublic/packs配下にコピーされないようにした。
      outDir: outDirPath(mode), // modeでテスト環境ではpublic/packs-testにbuildするようにした
      assetsDir: "", // public/packs/assets配下にbuildされないようにassetsDirには空白を設定
    },
    css: {
      postcss: "postcss.config.js",
    },
    plugins: [
      vue(),
      EnvironmentPlugin({
        NODE_ENV: ""
      }),
    ],
    resolve: {
      alias: {
        vue: "vue/dist/vue.esm-bundler.js",
        "@js": `${__dirname}/app/javascript`,
        "@css": `${__dirname}/app/javascript/stylesheets`,
      },
    },
  };
});

webpackからViteに移行する際に発生したエラー等

requireが使えずにbuild時にエラーになる

requireを使って画像等を読み込んでいる部分がエラーなるのでimportする方針に変更し、 画像のpathの解決が明示的にしとかないのルート/で参照してしまうのでmoduleを作ってpacks配下を明示するようにしました。

多分この辺が影響してそう(?)

https://ja.vitejs.dev/guide/assets.html#new-url-url-import-meta-url

++ import noImage from "@js/assets/noimage.png";
++ import { imageUrl } from "@js/utils/common/ImageUrl";
++ return imageUrl(noImage as string);
-- return require("@js/assets/noimage.png") as string
export const imageUrl = (image: string) => {
  const nodeENV = process.env.NODE_ENV;
  const envPrefix = nodeENV == "test" ? "packs-test" : "packs";
  return `/${envPrefix}${image}`;
};

ViteでbuildしたファイルをRailsで読み込む(Simpacker -> Vite Rails)

Viteで吐き出したファイルを読み込むにはVite Railsを使うと諸々いい感じに実装できるので便利です💎✨(導入方法等は公式ガイドを参照いただければと思います)

vite-ruby.netlify.app

※当初以下の参考にSimpacker経由でViteでbuildしたファイルを読み込もうとしましたが、cssのchunk周りがViteでは調整が難しく利用が複雑になってしまいそうだったのでVite Railsを素直に使う方針としました。

text.hmsk.me

HMRを使わないのであれば設定はシンプルでconfig/simpacker.ymlconfig/vite.jsonに置き換えて、

{
  "all": {
    "sourceCodeDir": "app/javascript",
    "watchAdditionalPaths": []
  },
  "production": {
    "publicOutputDir": "packs"
  },
  "development": {
    "publicOutputDir": "packs"
  },
  "test": {
    "publicOutputDir": "packs-test"
  }
}

既存のjavascript_pack_tag等を置換するのが大変そうに思ったので以下のようなapp/helpers/vite_pack_helper.rbを用意してApplicationHelperにincludeすることでViewへの変更は行わずにVite RailsのHelperに移譲するようにしました。 Vite RailsはHMRの利用が前提のためかmanifestの読み込みが初回起動時に行われなかったので、開発環境では明示的にViteRuby.instance.manifest.refreshを呼び出しmanifestを再読込するようにしました。

# frozen_string_literal: true

module VitePackHelper
  include ViteRails::TagHelpers

  def javascript_pack_tag(*names, typescript: true, **options)
    refresh_manifest unless cache_manifest?
    paths = names.map { |name| entry_path(name) }
    return vite_typescript_tag(*paths, **options) if typescript
    vite_javascript_tag(*paths, **options)
  end

  def stylesheet_pack_tag(*names, **options)
    refresh_manifest unless cache_manifest?
    paths = names.map { |name| entry_path(name) }
    vite_stylesheet_tag(*paths, **options)
  end

  def image_pack_tag(name, **options)
    refresh_manifest unless cache_manifest?
    vite_image_tag(assets_path(name), **options)
  end

  private

  def javascript_root_path
    'app/javascript/'
  end

  def entry_path(name, entry_dir: 'packs')
    File.join(javascript_root_path, entry_dir, name)
  end

  def assets_path(name, assets_dir: 'assets')
    File.join(javascript_root_path, assets_dir, name)
  end

  def refresh_manifest
    ViteRuby.instance.manifest.refresh
  end

  def cache_manifest?
    # NOTE: vite_rubyはHRM前提からかリクエスト時にmanifestのrefreshは行なわれなので開発環境では明示的に行う。
    !Rails.env.development?
  end
end

おわりに

esbuild-loaderを使っていたこともあり、そこまでbuild速度とかは変わらなかったのですが、webpackかViteに乗り換えると依存パッケージが私の個人サービスでも10個程度減らせたのと、VitestやHistoireといった他ライブラリと設定を共有できるのが、非常に管理しやすくて良いですね⚡️

Vite Railsのおかげで、意外と既存との差分も少なくwebpack + Simpackerから乗り換えられるのも体験が良かったです💎✨

参考

zenn.dev

Vitestで時間を固定(`jest-date-mock`の`advancedTo`相当)する方法メモ📝

JestからVitestに移行する際に以下のような感じで jest-date-mockadvancedToを使って時刻の固定している箇所をvitestで再現するのに少しハマったのでメモ📝

以下のような形でadvanceToを使って引数で渡した時間に固定できる🕛

import { advanceTo } from "jest-date-mock";
advanceTo(new Date("2021-05-03T15:35:47+09:00"));

Vue✕TypeScriptなプロジェクトにJestを導入する方法MEMO👢 - Madogiwa Blog

vitestで時間を固定する方法

以下のようにadvanceToを使った時間を固定したテストの場合は、

import { mount } from "@vue/test-utils";
import Component from "@js/components/Foo.vue";
import { advanceTo } from "jest-date-mock";

describe("components/Foo.vue", () => {
  describe("default", () => {
    it("snapshot", () => {
      const props = { bar: 'bar' };
      advanceTo(new Date("2021-05-03T15:35:47+09:00"));
      const wrapper = mount(Component, { props: props });
      expect(wrapper.element).toMatchSnapshot();
    });
  });

以下のような形で書き換えると同様の挙動を再現できる✨

import { mount } from "@vue/test-utils";
import Component from "@js/components/Foo.vue";
import { advanceTo } from "jest-date-mock";

describe("components/Foo.vue", () => {
  describe("default", () => {
    it("snapshot", () => {
      const props = { bar: 'bar' };
      vi.useFakeTimers();
      vi.setSystemTime(new Date("2021-05-03T15:35:47+09:00"));
      const wrapper = mount(Component, { props: props });
      expect(wrapper.element).toMatchSnapshot();
      vi.useRealTimers();
    });
  });

参考

github.com

CircleCI: circleci tests globの結果から任意のファイルをexludeする方法MEMO

CircleCIでテストを分割して並列実行する場合に以下のドキュメントに記載されているようなcircleci tests glob "foo/**/*"のようなコマンドを実行すると思います。

circleci.com

このときに任意の複数のディレクトを対象にするのはcircleci tests glob {foo,bar}/**/*のような形式で書けば大丈夫なのですが、 任意の複数のディレクトリを除外するやり方は公式には用意されておらずハマったのでメモ📝

rspecであればcircleci tests globを使っていなければ以下のrspec--patternが使えるが、circleci glob testの場合にはファイルパスが直接引数に渡される都合上厳しい。。。

madogiwa0124.hatenablog.com

circleci tests globで任意の複数のディレクトリを除外する方法

方法としては以下のような感じでsedを使って-Eで渡した正規表現に合致した任意の行を削除するのが良さそうだった ✨

circleci tests glob | sed -E "sed -E "/(foo|bar)/d"

参考

discuss.circleci.com

tech-blog.rakus.co.jp

Ruby on Rails: TypeScriptで`@rails/ujs`利用するための最低限の型定義MEMO

個人のWebサービスでj.sファイルを全て.tsファイルに変更するときに@rails/ujsの型定義が提供されておらず、ちょっとハマったのでメモ📝

@rails/ujsとは

@rails/ujsは、ActionView内の管理されているnpm packageです。

github.com

Railsのガイドで記載されているdata-confirmdisable_withといった便利機能はこのpackageを利用して実現されており、

railsguides.jp

以下のコードをapplication.jsとかに書くと使うことが出来ます。

import Rails from "@rails/ujs";
Rails.start();

.tsファイルで@rails/ujsを利用する。

以下を単純に.jsファイルを.tsファイルに書き換えると、

import Rails from "@rails/ujs";
Rails.start();

型定義ファイルが提供されてないので以下のようなエラーが発生します。

app/javascript/packs/rails.ts:1:19 - error TS7016: Could not find a declaration file for module '@rails/ujs'. 'node_modules/@rails/ujs/lib/assets/compiled/rails-ujs.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/rails__ujs` if it exists or add a new declaration (.d.ts) file containing `declare module '@rails/ujs';`

1 import Rails from "@rails/ujs";

以下のようなrails-ujs.d.ts的なファイルを配置してあげれば最低限の利用に関しては型チェックを成功させることができます。

const Rails: {
  start(): void;
};

declare module "@rails/ujs" {
  export default Rails;
}

以下のようなcsrfTokenを取得するといったmoduleをimportとかしたい場合には

import { csrfToken } from "@rails/ujs";

型定義を追加してあげれば大丈夫です。

const Rails: {
  start(): void;
};

declare module "@rails/ujs" {
  export default Rails;
  export function csrfToken(): string;
}

参考

stackoverflow.com