Madogiwa Blog

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

Ruby: Haml v6アップデート時のカスタム属性の振る舞いの互換性を維持するメモ📝

Haml v6からHamlの内部実装がHamlitに置き換わりパフォーマンス向上等のメリットがありますが、

github.com

以下のissueでコメントされている通り、

github.com

Vue.js等を利用している場合に以下のようなfalsyの値がv5系では<cutsom-element />となっていたのが、

%cutsom-element{ ":costom-attributes": nil  }

Haml v6では<cutsom-element :costom-attributes='' />となってしまいます。

上記の場合Vue.jsを利用しているHaml v6の挙動ではpropsのデフォルト値が利用されなくなり、明示的にundefinedを渡すような実装に修正する必要があり非常に影響が大きいです😢

これに対応する仕組みが以下のHaml v6.2.2でリリースされたHaml::BOOLEAN_ATTRIBUTESに任意の属性名の文字列・正規表現を追加することでHaml v5相当の振る舞いにすることができます。

github.com

全部やるなら以下とすれば良さそうですが、

Haml::BOOLEAN_ATTRIBUTES.push(/.*/)

この変更自体がパフォーマンス向上のトレードオフで発生しているもののようなので、パフォーマンスに問題がないか等は計測して対応を入れる必要がありそうです。

It varies. No impact, as slow as Haml 5, or slower than Haml 5, depending on the benchmark.

https://github.com/haml/haml/issues/1148#issuecomment-1754421295

📝以下のようなスクリプトで既存のhamlファイルを読み込んで利用されているカスタムタグの属性のリストを抽出し、それがだけ許可するような感じでもいいのかもしれない🤔

require 'bundler/inline'

gemfile do
  source 'https://rubygems.org'
  gem 'haml'
end

require 'haml/parser'

haml = <<~HAML
  %h1 Hello!
  %custom-element{ ":custom-attributes1": false, "string-attributes1": "hello" }
  %custom-element{ ":custom-attributes2": nil, "string-attributes2": "" }
  %p
    %span World!
HAML

parser = Haml::Parser.new({})
parsedHaml = parser.call(haml)

def extract_tags_and_attributes(node)
  result = []

  node.children.each do |child|
    tag_name = child.value[:name]
    attribute_names = child.value[:attributes].keys
    dynamic_attributes = child.value[:dynamic_attributes]
    dynamic_attribute_hash = dynamic_attributes.old ? eval(dynamic_attributes.old) : {}
    result << { name: tag_name, attributes: attribute_names + dynamic_attribute_hash.keys }
    result.concat(extract_tags_and_attributes(child))
  end

  result
end

tags_and_attributes = extract_tags_and_attributes(parsedHaml)
is_custom_tag = ->(tag) { tag[:name].include?("-") }

puts tags_and_attributes.select(&is_custom_tag)
                        .flat_map { |tag| tag[:attributes] }
                        .uniq
# =>
# :custom-attributes1
# string-attributes1
# :custom-attributes2
# string-attributes2

Vue.js: 静的解析でVue3から非推奨なディープセレクタ`::v-deep`をエラーにするメモ📝

Vue3から以下のドキュメント記載の通りディープセレクタを利用する際に::v-deepから:deepに変更になりました。

<style scoped>
/* deep selectors */
::v-deep(.foo) {}
/* shorthand */
:deep(.foo) {}

/* targeting slot content */
::v-slotted(.foo) {}
/* shorthand */
:slotted(.foo) {}

/* one-off global rule */
::v-global(.foo) {}
/* shorthand */
:global(.foo) {}
</style>

rfcs/active-rfcs/0023-scoped-styles-changes.md at master · vuejs/rfcs · GitHub

利用するとビルドした際に以下のような警告が出るものの気づかずに利用してしまうことも多いので静的解析でエラーにできないかやってみたのでメモ📝

::v-deep usage as a combinator has been deprecated. Use ::v-deep(<inner-selector>) instead

やり方

結論から言うとstylelintのselector-disallowed-listを使って、

stylelint.io

以下のように指定してあげると::v-deep系のセレクターを静的解析でエラーにすることができた。

{
  "extends": ["stylelint-config-standard-scss"],
  "overrides": [
    {
      "files": ["**/*.vue"],
      "customSyntax": "postcss-html"
    }
  ],
  "rules": {
    "selector-disallowed-list": ["/::v-deep/", "/::v-slotted/", "/::v-global/"],

利用していると以下のようにエラーになります。

app/javascript/components/pages/EditBoardContainer.vue
 178:3  ✖  Unexpected selector "::v-deep(.main-with-sidemenu__sidemenu)"  selector-disallowed-list

Stylelint便利ですね🤵✨

`vscode-standard-ruby`でプリインストールのrubyが利用されてしまうのを直した時のメモ📝

vscode-standard-rubyでプリインストールのrubyで実行されてしまいパッケージマネージャーで関しているバージョンで実行されずLSPが落ちてしまいハマったので対応したことをメモ📝

github.com

事象

以下のようにターミナルで確認するとパッケージマネージャで管理している最新のRubyが利用されているが、

$ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-darwin22]

vscode-standard-rubyのLSPの起動時に以下の通り謎にプリインストールのrubyが利用されてしまっており失敗していた。

[client] stderr:
/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/rubygems.rb:283:in `find_spec_for_exe': Could not find 'bundler' (2.4.10) required by your /Users/morita.jun/Documents/repo/dogfeeds/Gemfile.lock. (Gem::GemNotFoundException)

解決方法

以下の設定が入っていたので削除し、ターミナルで利用しているもの(zsh)が利用されるようにした。

- "terminal.integrated.defaultProfile.osx": "bash"

そしてVSCodeを単純に再起動するのではなく、ターミナルからcodeコマンドでVSCodeを再起動するとパッケージマネージャで管理しているRubyが利用されLSPの起動に成功した🤔

[server] Standard Ruby v1.32.0 LSP server initialized, pid 81066

"terminal.integrated.defaultProfile.osx": "bash"の設定によりbashで最初起動してしまったので、zshのターミナルで再起動するまではパッケージマネージャーの設定が無いbashが利用されてしまいプリインストールのRubyが使われてしまっていたっぽい?(VSCodeは起動時のShellの設定を引き継ぐ?)

参考

qiita.com

rbenv+nodebrewからasdfに移行したので作業メモ📝

rbenv+nodebrewからasdfに移行してみたのでやったことをメモしておきます📝

github.com

asdfインストール

公式はgit cloneの方法のようだが、homebrewからinstallできるようなのでinstall

https://asdf-vm.com/guide/getting-started.html#_2-download-asdf

$ brew install asdf

以下に従ってbrew + zshのinstallコマンドを実行 https://asdf-vm.com/guide/getting-started.html#_3-install-asdf

$ echo -e "\n. $(brew --prefix asdf)/libexec/asdf.sh" >> ${ZDOTDIR:-~}/.zshrc

出来た🆗

$ asdf --version
v0.13.1

asdfrubyを管理

asdfruby pluginを追加

$ asdf plugin add ruby 

install可能なrubyのバージョンを確認

$ asdf list-all ruby

任意のバージョンのrubyのinstall

$ asdf install ruby 3.2.2

任意のバージョンのrubyのグローバル利用

$ asdf global ruby 3.2.2

出来た🆗

$ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-darwin22]

rbenv uninstall

rbenvは不要なので削除しておく

$ brew uninstall rbenv
$ rm -rf ~/.rbenv 

.zshrcから以下も削除

# for rbenv
eval "$(rbenv init - zsh)"

asdfでnodeを管理

asdfのnodeプラグインを追加

$ asdf plugin add nodejs 

インストール可能なnodeの一覧を表示

$ asdf list-all nodejs

任意のバージョンのnodeをinstall

$ asdf install nodejs 18.18.2

任意のバージョンのnodeをグローバル利用

$ asdf global nodejs 18.18.2

出来た🆗

$ node -v
v18.18.2

nodebrew uninstall

$ brew uninstall nodebrew
$ rm -rf ~/.nodebrew 

.zshrcから以下も削除

# for nodebrew
export PATH=$HOME/.nodebrew/current/bin:$PATH

参考

scrapbox.io

https://asdf-vm.com/guide/getting-started.html#_4-install-a-plugin

Ruby on Rails: テスト用にrouteを動的に追加するメモ📝

テスト用にRoutingを動的にいじってテストしたいことがたまにあるのでやり方をメモ📝

まず以下のようなテスト用のhelperを用意します。 中でやっていることは動的にrouteを追加するdraw_test_routesとそれをリセットするreload_routes!を実装しています。

module RoutesHelper
  def draw_test_routes(&block)
    Rails.application.routes.disable_clear_and_finalize = true

    Rails.application.routes.draw do
      instance_exec(&block)
    end
  end

  def reload_routes!
    Rails.application.reload_routes!
  end
end

Rails.application.routes.disable_clear_and_finalize = trueをすることで以下のroutesのclearやfinalizeを無効化してroutesを固定化させず、動的に追加したものを初期化させないようにします。

https://github.com/rails/rails/blob/v7.1.2/actionpack/lib/action_dispatch/routing/route_set.rb#L428-L433

またRails.application.reload_routes!を実行することで、clear!し再読み込みされfinalize!することで動的に追加したroutesを削除するとともにRails.application.routes.disable_clear_and_finalizeがfalseに再設定されるので、元に戻ります。

https://github.com/rails/rails/blob/v7.1.2/railties/lib/rails/application/routes_reloader.rb#L22-L29

以下の通り、任意のcontrollerを使って動的にrouteを追加することができました🎉

require "spec_helper"
require_relative "../../test/support/routes_helper"

class TestsController < ActionController::Base
  def index
    head :ok
  end
end

describe "GET /test", type: :request do
  include RoutesHelper

  before(:example) do
    draw_test_routes do
      get "/tests", to: "tests#index"
    end
  end

  after(:example) do
    reload_routes!
  end

  it "success" do
    get "/tests"
    expect(response).to have_http_status :ok
  end
end

参考

techracho.bpsinc.jp

型定義のimport時に`import type`をESLintで強制・自動修正する方法メモ📝

TypeScript v5から導入されたverbatimModuleSyntaxをtrueにすると型定義を普通にimportするとエラーになります。

TypeScript 5.0 introduces a new option called --verbatimModuleSyntax to simplify the situation. The rules are much simpler - any imports or exports without a type modifier are left around. Anything that uses the type modifier is dropped entirely. https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax

ただいずれは導入したい場合に新規のコードの発生を防いだり、既存コードの利用箇所を特定するために漸進的に乗り換えるために静的解析で検知してエラーに出来ないかなぁと思ったら、まさになルールがあったのでメモ📝

typescript-eslint.io

以下のようなコードでOptionsTypeは型定義でしか使ってない場合にエラーにしてくれます。

import { zxcvbn, zxcvbnOptions, OptionsType } from "@zxcvbn-ts/core";
// -> Import "OptionsType" is only used as types.eslint@typescript-eslint/consistent-type-imports

Auto fixにも対応していて

- import { zxcvbn, zxcvbnOptions, OptionsType } from "@zxcvbn-ts/core";
+ import type { OptionsType } from "@zxcvbn-ts/core";
+ import { zxcvbn, zxcvbnOptions } from "@zxcvbn-ts/core";

fixStyleを指定するとinlineでtypeを付与する事もできる🙌

// "@typescript-eslint/consistent-type-imports": ["error", { fixStyle: "inline-type-imports" }],
- import { zxcvbn, zxcvbnOptions, OptionsType } from "@zxcvbn-ts/core";
+ import { zxcvbn, zxcvbnOptions, type OptionsType } from "@zxcvbn-ts/core";

verbatimModuleSyntaxを有効化したい場合にAuto Fixしてからやるとスムーズに出来そうですね 👍

参考

zenn.dev

`zxcvbn`のモダンな代替ライブラリ`zxcvbn-ts`を使ってパスワード強度を測定するメモ📝

ユーザーに安全なパスワードの設定を促すために強度を測定しフィードバックしたいといった時にDropbox製のzxcvbnがよく使われていると思います。

github.com

ただ、このライブラリのサイズが大きかったりメンテナンスに不安があったりと、

So, I came across this library and was very excited to put it to use. I pulled it down via npm and was shocked to find the minified dist script is 823kb. That's insane. Dist size (JavaScript) makes this lib unusable in a client · Issue #169 · dropbox/zxcvbn · GitHub

代替ライブラリを探していたところzxcvbn-tsというライブラリを使うとLazy Loading等が使えていい感じに使えそうだったので使い方をメモ📝

github.com

zxcvbn-tsとは?

以下の通りzxcvbnを再実装したライブラリとのこと。

This is a complete rewrite of zxcvbn in TypeScript which is licensed under the MIT license.
Introduction | zxcvbn-ts

完全に再現しているわけではなく微妙にスコアが違うみたいなのでzxcvbnからの乗り換えを行う場合には参照しておくと良さそうです。

zxcvbn-ts.github.io

zxcvbn-tsのinstall

以下のページの通りですが、

zxcvbn-ts.github.io

以下のコマンドでインストールできます。

npm install @zxcvbn-ts/core @zxcvbn-ts/language-common --save

各国のよくある名前とかの辞書を取り込みたい場合には以下から任意の国の辞書情報を取り込むこともできます。

github.com

zxcvbn-tsを利用する

以下がzxcvbn-tsを利用してパスワードの文字列を受け取ってスコアの数値を返却するサンプル実装です。 zxcvbnOptionsを使ってoptionを設定する感じ以外はzxcvbnとほとんど同じように使うことができます。

import { zxcvbn, zxcvbnOptions, type OptionsType } from "@zxcvbn-ts/core";
import { adjacencyGraphs, dictionary } from "@zxcvbn-ts/language-common";

const defaultOption: () => OptionsType = () => {
  return { graphs: adjacencyGraphs, dictionary };
};

export const calcPasswordStrengthScore = (password: string): number => {
  const options = defaultOption();
  zxcvbnOptions.setOptions(options);
  return zxcvbn(password).score;
};

zxcvbn-tsの辞書情報等を遅延読み込みさせる

辞書情報等はサイズが大きいのでパフォーマンス等の問題で実際に利用するまで読み込ませたくないときもあるかもですが、zxcvbn-tsは遅延読み込みにも対応しているようです。

zxcvbn-ts.github.io

以下が@zxcvbn-ts/language-commonを遅延importして利用時に読み込むようにしたサンプルです。 ※私はviteを使っているので、別のmodule bundlerを利用している場合には微妙にコードを修正する必要があるかもです。

import { zxcvbn, zxcvbnOptions, type OptionsType } from "@zxcvbn-ts/core";

const defaultOption: () => Promise<OptionsType> = async () => {
  const common = await import("@zxcvbn-ts/language-common");
  return { graphs: common.adjacencyGraphs, dictionary: common.dictionary };
};

export const calcPasswordStrengthScore = async (password: string): Promise<number> => {
  const options = await defaultOption();
  zxcvbnOptions.setOptions(options);
  return zxcvbn(password).score;
};

おわりに

zxcvbn-ts、遅延ローディングも対応してるのとアクティブにメンテナンスされているようて良さそうですね✨