Madogiwa Blog

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

Ruby: 外部からprivateなメソッドをpublicにする方法メモ📝

好ましくはないですが、通常Rubyでprivateなメソッドを呼び出したい時にはsendを使うケースが多いです。

docs.ruby-lang.org

しかしライブラリの破壊的変更等によりpublicだっだメソッドがprivateになる等、すでに大量に依存しているケースですべてをsendに直すのは大変なケースもあります。

そういうケースではModule#publicを使うと任意のprivateメソッドをpublicにできて便利だったので使い方をメモ📝

docs.ruby-lang.org

当たり前ですが、以下の実装はprivate methodprivate_foo' called for an instance of Foo (NoMethodError)`が発生し、エラーになります。

class Foo
  def public_foo
    puts "public_foo"
  end

  private

  def private_foo
    puts "private_foo"
  end
end

puts Foo.new.private_foo
#=> test.rb:17:in `<main>': private method `private_foo' called for an instance of Foo (NoMethodError)
# puts Foo.new.private_foo
#            ^^^^^^^^^^^^
# Did you mean?  private_methods

しかし、以下のような感じでpublic :private_fooを使うと外からpublicなメソッドに変更することができます。

class Foo
  def public_foo
    puts "public_foo"
  end

  private

  def private_foo
    puts "private_foo"
  end
end

Foo.class_eval { public :private_foo }

puts Foo.new.private_foo
# => private_foo

使い所は気をつける必要がありますが、実装を外部から柔軟にコントロールできるのはRuby便利💎✨

Ruby: 個人サービスをRuby v3.3.0にアップデートした💎✨

以下の問題があったのでRuby v3.3.0へのアップデートを見送っていたのですが、

madogiwa0124.hatenablog.com

Docker Hubの公式イメージに修正が入り正常に動作するようになったようなのでRuby on Rails製の個人サービスをRuby v3.3.0にアップデートしました💎✨

その後Docker Hubのdocker-library/rubyで本記事の問題が修正されました↓。これにより、Docker HubのRuby 3.3.0イメージは正常に動くようになりました。 PR: Workaround 3.3.0 crash on aarch64 by osyoyu · Pull Request #439 · docker-library/ruby Ruby 3.3.0: aarch64-linux環境でFiber.new{ }.resumeを呼ぶと落ちる問題|TechRacho by BPS株式会社

いつも通りアップデート後にRspecを実行してテストが通ることを確認できました🍏

個人サービスレベルの規模ではありますが、 特別やったことと言えばzeitwerkの警告が出ていたのでcsvを明示的にinstallするようにしたぐらいで、 互換性が担保されているのはありがたいですね🙏

ruby/gems/3.3.0/gems/zeitwerk-2.6.13/lib/zeitwerk/kernel.rb:34: warning: csv was loaded from the standard library, but will no longer be part of the default gems since Ruby 3.4.0. Add csv to your Gemfile or gemspec. Also contact author of zeitwerk-2.6.13 to add csv into its gemspec.

Ruby v3.4からcsvがdefault gemでは無くなるようですね 👀

gihyo.jp

TypeScript/Vue.js/Prettierを使った環境でESLintの新しい設定 Flat Configに移行した📝

2024/04/06 ESLint v9がリリースされました🎉

eslint.org

Flat config is now the default and has some changesとあるように、 v9からは今までとは違うFlat Configという設定方法がデフォルトになります。(ESLINT_USE_FLAT_CONFIGfalseを指定することで今まで通りの設定も使い続けることは可能)

eslint.org

今回は個人で開発しているVue.jsとTypeScriptとPrettierを利用しているサービスでFlat Configに移行してみたので、やったこととかをメモ📝

公式のmigrationガイドもあるので、こちらもご参照ください。

eslint.org

終結

元のESLintのconfigファイル .eslintrc.cjs

module.exports = {
  env: {
    node: true,
    browser: true,
  },
  extends: [
    "eslint:recommended",
    "plugin:vue/vue3-recommended",
    "plugin:vue-scoped-css/base",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-requiring-type-checking",
  ],
  root: true,
  plugins: ["@typescript-eslint", "prettier"],
  parser: "vue-eslint-parser",
  parserOptions: {
    parser: "@typescript-eslint/parser",
    ecmaVersion: 2020,
    sourceType: "module",
    extraFileExtensions: [".vue"],
    project: "./tsconfig.json",
  },
  rules: {
    "prettier/prettier": "error",
    "no-console": "warn",
    "@typescript-eslint/no-unused-vars": [
      "warn",
      {
        argsIgnorePattern: "^_",
        varsIgnorePattern: "^_",
        caughtErrorsIgnorePattern: "^_",
      },
    ],
  },
  overrides: [
    {
      files: ["*.vue"],
      rules: {
        "vue/first-attribute-linebreak": "off",
        "vue/max-attributes-per-line": "off",
        "vue/singleline-html-element-content-newline": "off",
        "vue/html-self-closing": "off",
        "vue/max-lines-per-block": [
          "error",
          {
            style: 150,
            template: 200,
            script: 150,
            skipBlankLines: true,
          },
        ],
        "vue-scoped-css/enforce-style-type": ["error", { allows: ["scoped"] }],
        "@typescript-eslint/no-redundant-type-constituents": "off",
        "@typescript-eslint/no-unsafe-assignment": "off",
      },
    },
  ],
};

Flat Configに移行後 eslint.config.js

import js from "@eslint/js";
import ts from "typescript-eslint";
import vue from "eslint-plugin-vue";
import vueParser from "vue-eslint-parser";
import vueCss from "eslint-plugin-vue-scoped-css";
import prettierConfig from "eslint-plugin-prettier/recommended";
import globals from "globals";

export default [
  { ignores: [/* ignores */] },
  js.configs.recommended,
  ...ts.configs.recommendedTypeChecked,
  ...vue.configs["flat/recommended"],
  ...vueCss.configs["flat/base"],
  prettierConfig,
  {
    languageOptions: {
      parser: vueParser,
      parserOptions: {
        ecmaVersion: 2020,
        parser: ts.parser,
        extraFileExtensions: [".vue"],
        sourceType: "module",
        project: ["./tsconfig.json"],
      },
      globals: {
        ...globals.browser,
        ...globals.node,
      },
    },
  },
  {
    rules: {
      "prettier/prettier": "error",
      "no-console": "warn",
      "@typescript-eslint/no-unused-vars": [
        "warn",
        {
          argsIgnorePattern: "^_",
          varsIgnorePattern: "^_",
          caughtErrorsIgnorePattern: "^_",
        },
      ],
    },
  },
  {
    files: ["*.vue", "**/*.vue"],
    rules: {
      "vue/first-attribute-linebreak": "off",
      "vue/max-attributes-per-line": "off",
      "vue/singleline-html-element-content-newline": "off",
      "vue/html-self-closing": "off",
      "vue/max-lines-per-block": [
        "error",
        {
          style: 150,
          template: 200,
          script: 150,
          skipBlankLines: true,
        },
      ],
      "vue-scoped-css/enforce-style-type": ["error", { allows: ["scoped"] }],
      "@typescript-eslint/no-redundant-type-constituents": "off",
      "@typescript-eslint/no-unsafe-assignment": "off",
    },
  },
];

やったこと

.eslintignoreからFlat Config内のignoresに移行する

.eslintignoreは以下の通りignoresで指定するように変更になっています。

To ignore files with flat config, you can use the ignores property in a config object. https://eslint.org/docs/latest/use/configure/migration-guide#ignoring-files

以下のようにignoresプロパティに.eslintignoreの内容を文字列の配列に変換して指定するようにしました。

export default [
  { ignores: [ /* ignores files */ ] },

各種関連ライブラリの導入方法をFlat Configに対応したものに修正する

元々はextends等を使って導入していた各種ライブラリをFlat Config対応の導入方法が別途README等に記載されていたので、それに従って利用するように修正しました。

typescript-eslint.

  • ...tseslint.configs.recommended turns on our recommended config.

https://typescript-eslint.io/getting-started#step-2-configuration

eslint-plugin-vue

import pluginVue from 'eslint-plugin-vue'
export default [
  ...pluginVue.configs['flat/recommended'],

https://eslint.vuejs.org/user-guide/#configuration-eslint-config-js

eslint-plugin-prettier

const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended');
module.exports = [
  eslintPluginPrettierRecommended,
];

https://github.com/prettier/eslint-plugin-prettier?tab=readme-ov-file#configuration-new-eslintconfigjs

またFlat Configに対応してないライブラリがある場合には、従来のextendspluginsを使って利用することもできるようです。

github.com

global、parser周りの設定をlanguageOptionsに移行する

.vueファイルに対してTypeScript関連も含めて静的解析させるためにparser周りの設定以下を参照しlanguageOptionsに移行しました。

基本的にはlanguageOptionsの中にparserOptionsがあるので、以前のparservue-eslint-parserを使いつつparserOptionsparser@typescript-eslint/parserを使い、extraFileExtensionsも指定することができました。

  {
    languageOptions: {
      parser: vueParser,
      parserOptions: {
        ecmaVersion: 2020,
        parser: ts.parser,
        extraFileExtensions: [".vue"],
        sourceType: "module",
        project: ["./tsconfig.json"],
      },
      globals: {
        ...globals.browser,
        ...globals.node,
      },
    },
  },

また、envプロパティはlanguageOptions内のプロパティとしてglobalsを利用して定義するようになっているので対応しました。

In flat config files, the globals, and parserOptions are consolidated under the languageOptions key; the env property doesn’t exist https://eslint.org/docs/latest/use/configure/migration-guide#configuring-language-options

github.com

overridesを削除する

今まではglobベースでのルールの切り替えにはoverridesを利用していましたがFlat Configではデフォルトで機能が提供されているようなので

By default, flat config files support different glob pattern-based configs in exported array. You can include the glob pattern in a config object’s files property. https://eslint.org/docs/latest/use/configure/migration-guide#configuring-language-options

以下のように、overridesを利用せずにそのまま書くことができます。

export default [
  {
    files: ["*.vue", "**/*.vue"],
    rules: { /* overrides rules */ }

実行オプションの--extは削除されたのでglobでの指定に移行する

元々は以下のような形で拡張子を絞って実行していたのですが、

$ eslint app/javascript spec/javascript --ext .vue,.js,.ts

以下の通りFlat Configではサポートされていないのでglobベースの指定に修正しました。

The following CLI flags are no longer supported with the flat config file format:

  • --ext

https://eslint.org/docs/latest/use/configure/migration-guide#cli-flag-changes

$ eslint 'app/javascript/**/*.{js,ts,vue}' 'spec/javascript/**/*.{js,ts,vue}'

おわりに

Flat Confg最初はあまり分かっておらず従来の方が読みやすいような?とか思ってたんですが、理解できるとコード量も少なくスッキリ書けそうなのと、自然なJavaScriptに近い形になっており分割とか管理もしやすそう📝✨

参考

zenn.dev

zenn.dev

Viteでnode_modules内の`.vue`ファイルをビルド対象に含める方法MEMO

ライブラリ内のVue SFCコンポーネントを直接ビルドして利用したい等、Viteではnode_modules内のvueファイルと直接importして利用すると以下のようなエラーが発生します。

<template>
  <Component />
</template>
<script lang="ts" setup>
import Component from "madogiwa-ui/src/components/Component.vue";
</script>
error during build:
Error: [vite]: Rollup failed to resolve import "node_modules/my-packages/src/components/Component.vue" from "app/javascript/components/Component.vue?vue&type=script&lang.ts".
This is most likely unintended because it can break your application at runtime.
If you do want to externalize this module explicitly add it to
`build.rollupOptions.external`
    at viteWarn (file:///node_modules/vite/dist/node/chunks/dep-Bh2jsKCM.js:67408:27)
    at onRollupWarning (file:///node_modules/vite/dist/node/chunks/dep-Bh2jsKCM.js:67436:9)
    at onwarn (file:///node_modules/vite/dist/node/chunks/dep-Bh2jsKCM.js:67140:13)
    at file:///node_modules/rollup/dist/es/shared/node-entry.js:18303:13
    at Object.logger [as onLog] (file:///node_modules/rollup/dist/es/shared/node-entry.js:19950:9)
    at ModuleLoader.handleInvalidResolvedId (file:///node_modules/rollup/dist/es/shared/node-entry.js:18893:26)
    at file:///node_modules/rollup/dist/es/shared/node-entry.js:18851:26

色々調べていたのですが、ViteというかRollupはデフォルトではnode_moduleのファイルはビルド済み扱いとなるようです。そのため強制的にビルドするためのオプションがViteにはあり、

By default, linked packages not inside node_modules are not pre-bundled. Use this option to force a linked package to be pre-bundled. https://vitejs.dev/config/dep-optimization-options.html#optimizedeps-include

以下のように利用することでライブラリ内の.vueファイルをビルドして利用することができました 🎉

export default defineConfig({
  plugins: [vue()],
    optimizeDeps: {
+     include: ["my-package"],
    },

ただ外部ライブラリを直接ビルドするのは、babelの設定等々のビルド環境が違うことから想定外の挙動をしたりといった可能性もなきにしもあらずでリスクもありますが、 alias等がライブラリ側と利用側で違ってたりしても動かないので単純にプロジェクト内のVue SFCをパッケージに切り出してメンテナンスしやすいようにするとか、 そういった目的とかでは便利そうですね📝

Ruby on Rails: Capybaraで特定の要素が無くなるのを待つ方法MEMO

Capybaraで、読み込み中を表すコンポーネントが消えるのを待ってからスクショを撮りたいみたいな時に特定の要素が消えるのを待つ方法をMEMO

結論だけ言うと以下のようにスクリーンショットを習得する前にローダーが無くなることを判定すれば良い。

page.has_no_css?(".page-loader")
page.save_screenshot("tmp/foo/bar.png")

www.rubydoc.info

RSpecのmatcherとして提供されているものはhas_link?とかhas_content?とかで大体pageから呼び出せる。

便利!

決まりきったローダーがあるならmoduleに切り出してhelperにしておくと便利そう。

module WaitLoadingComponentHelper
  def wait_loading_component(page)
    page.has_no_css?(".page-loader")
  end
end

参考

blog.willnet.in

Ruby on Rails: Capybaraでスクリーンショットを取得してreg-cliで画像比較する簡単なVRT的なのを作るMEMO

Ruby on Railsのsystem specで画面ショットを取りつつmasterとの画像比較してVRT的なことできないかなーと思っていたのですが、Capybaraのpage.save_screenshotreg-cliを使うと実現できそうだったのでメモ📝

ちなみにPlaywrightでは以下の通りtoHaveScreenshotを使うことで、今回試したような簡単にVRTを実施できます。

madogiwa0124.hatenablog.com

※playwright-ruby-clinetを直接使えば出来るかなと思ったけどtoHaveScreenshotには現時点では対応していなさそうだった📝

前提事項

  • rails (7.1.3.2)
  • capybara (3.40.0)
  • capybara-playwright-driver (0.5.1)
    • 試してないですがplaywrightじゃなくても出来ると思います

実現したいこと

Ruby on Railsのsystem specで画面ショットを取りつつmasterとの差分比較して一定の差異があったら落としたいので、以下のような感じでspec/screenshotsのmasterとcompareに、それぞれmasterのものとトピックで取得した画像ファイルを配置し同一パスのファイルを比較して一定の差異があればエラーにします。 ※masterは比較用に初回実行時だけ作成しリポジトリで管理するようにします。(トピックが正になる場合に更新してpushします。)

spec/screenshots
├── compare
│   └── foo
│       └── bar
│           └── baz.png
└── master
    └── foo
        └── bar
            └── baz.png

System Specで任意のタイミングで画面ショットを取得し差分比較用のフォルダに配置する

画面ショットを取得すること自体は、page.save_screenshot(path: Rails.root.join("spec/screenshots/compare/foo/bar/baz")を実行すればいいのですが、差分比較用のフォルダを毎回してするのは面倒なので以下のようなHeplerを用意していい感じのフォルダに配置できるようにしてみました。

module VrtScreenshotHelper
  def vrt_screenshot(page, path:)
    return if ENV["GET_SCREENSHOTS_FOR_VRT"] != "true"
    base_path = Rails.root.join("spec/screenshots/compare")
    page.save_screenshot(base_path.join(path))
  end
end

こうすると以下のように取得できます。

    it "sample example" do
      visit example_path
      expect(page).to have_content "sample title"
      expect(page).to have_content "sample description"
      vrt_screenshot(page, path: "foo/bar/baz.png")
    end

初回実行時にはmasterの画面ショットを作成する必要があるので、以下のように変更してmaster側に画面ショットを取得します。

module VrtScreenshotHelper
  def vrt_screenshot(page, path:)
    return if ENV["GET_SCREENSHOTS_FOR_VRT"] != "true"
-   base_path = Rails.root.join("spec/screenshots/compare")
+   base_path = Rails.root.join("spec/screenshots/master")
    page.save_screenshot(base_path.join(path))
  end
end

取得した画面ショットを比較して一定差分があった場合に落とす

取得した画面ショットを比較して一定差分があった場合に落とすのは以下のreg-cliを使うと簡単に実現することが出来ました。

github.com

以下のように比較するパスと差分を表す画像の出力先を指定し、レポート出力先-R、エラーにする閾値-Tを指定することで先ほど保存したスクリーンショットをmasterを比較し、一定の差分があったらレポート付きでエラーにすることが出来ます。

$ reg-cli spec/screenshots/compare spec/screenshots/master spec/screenshots/diff -R spec/screenshots/diff/report.html -T 0.01

上記はspec/screenshots/comparespec/screenshots/masterを比較し、差分を示す画像をspec/screenshots/diffに配置、1%でも差分があればエラーとし、レポートをspec/screenshots/diff/report.htmlに吐き出すような指定になります。

実際に実行して失敗すると以下のような出力が表示され差分を検知できます。

$ reg-cli spec/screenshots/compare spec/screenshots/master spec/screenshots/diff -R spec/screenshots/diff/report.html -T 0.01 -U

✘ change  spec/screenshots/compare/foo/bar/baz.png

✘ 1 file(s) changed.

差分が発生した場合にはレポートで具体的な差分をブラウザで確認できます。

https://github.com/reg-viz/reg-cli?tab=readme-ov-file#html-report

CircleCIやGitHub Actionでartifactsとして保存するようにすると差分比較がよりしやすくなって良さそうです。

おわりに

reg-cliあまり使ってことなかったのですが、お手軽に画像比較・レポート作成まで出来て非常に便利ですね ✨

Ruby on Rails: CPSを有効化後にテスト環境で`content_security_policy_nonce`で空文字が返却されエラーになる件の対応方法MEMO

個人のWebサービスconfig.content_security_policy_report_only=trueを外し、CSP違反があった場合にブラウザエラーを発生させるようにしたところ、Sytem Specが軒並み落ちるようになり対応したのでやったことをメモ📝

事象

ブラウザエラーを見たところ、以下のようなCSP違反のログが多数出ていたので、

The source list for the Content Security Policy directive 'script-src' contains an invalid source: ''nonce-''. It will be ignored.

実際に返却されるhtmlを確認してみたのですが、scriptタグに設定されるnonceが空文字になっていました。

<script nonce="">

原因

Rails側のnonceの生成処理は以下のようになっているのですが、このrequest.sesionの参照処理時にテスト環境でだけ、#<ActionDispatch::Request::Session:0xd610 not yet loaded>となっており、sessionが未ロードの状態のため空文字が返ってしまっていた模様 🤔

Rails.application.configure do
  config.content_security_policy_nonce_generator = ->(request) {
    request.session.id.to_s
  }
end

テスト環境では実際にログインせずにcurrent_userallow_any_instance_ofで返してるので、その辺の理由でcontent_security_policy_nonce_generator実行時点でsessionが未ロードの状態になってしまっているのかも(?)

解決策

以下のようにrequest.session[:init] = trueを実施し明示的にsessionを扱うことでロードさせるようにしたところ解決した。

Rails.application.configure do
  config.content_security_policy_nonce_generator = ->(request) {
    # NOTE: テスト実行時に以下となりSessionが取得できずnonceが空文字になりCSP違反が発生してしまうので強制的にロードする
    # #<ActionDispatch::Request::Session:0xd610 not yet loaded>
    # https://github.com/rails/rails/issues/10813#issuecomment-297204965
    request.session[:init] = true
    request.session.id.to_s
  }
end

参考

github.com