🤕

Propshaft で node_modules 内のファイルをアセットとして使う

2024/02/11に公開

結論

これから作るものについては、 node_modules を assets パスに含めようとするのはやめよう

動機

importmap-rails で node_modules 以下のファイルを指定したい。
config/importmap.rbto:asset_path で解決できるものを指定できるので、アセットパスに追加すれば使える。

https://github.com/rails/importmap-rails/blob/ddf9be434e0aca1103eabafe6d34b0e8a5064057/lib/importmap/map.rb#L109

https://api.rubyonrails.org/classes/ActionView/Helpers/AssetUrlHelper.html#method-i-asset_path

経過

paths に node_modules を追加すれば使えはするが

次のように node_modules を追加すれば参照できるが、Propshaftは paths 以下にあるすべてのファイルをプリコンパイルの際に public/assets にコピーするため、不要なものを含む大量のファイルをコピーしてしまい、よくない。

Rails.configuration.assets.paths << Rails.root.join("node_modules")

package.json の dependencies 記載のもののみに…も問題あり

必要なものは dependencies に追加することとして、そのモジュールのみ追加すれば、いくらかましになる。

config/initializers/assets.rb
node_modules = Rails.root.join("node_modules")
Rails.application.config.assets.paths << node_modules

package_json = Rails.root.join("package.json")
Rails.configuration.watchable_files << package_json
Rails.configuration.to_prepare do
  node_config = JSON.parse(package_json.read)

  Rails.configuration.assets.excluded_paths.reject! { |p| p.to_s.start_with?(node_modules.to_s) }
  node_config["dependencies"].keys.each do |node_module|
    Rails.application.config.assets.paths << node_modules.join(node_module)
  end
end

が、各モジュールの下に同じディレクトリが存在する場合、最初にマッチしたものが優先されるので、うまくいかない

config/importmap.rb
pin "jquery", to: "dist/jquery.min.js"
pin "jquery-ui", to: "dist/jquery-ui.min.js" # Importmap skipped missing path: dist/jquery-ui.min.js

excluded_paths は使えないか?使えない

excluded_paths で追加したディレクトリは除外できます。とあるので、これをつかって依存関係を明記していないモジュールのディレクトリを除外設定すればどうか?

例えばこうだ。

config/initializers/assets.rb
node_modules = Rails.root.join("node_modules")
Rails.application.config.assets.paths << node_modules

package_json = Rails.root.join("package.json")
Rails.configuration.watchable_files << package_json
Rails.configuration.to_prepare do
  node_config = JSON.parse(package_json.read)

  # 依存関係にないパッケージは除外できる?
  Rails.configuration.assets.excluded_paths.reject! { |p| p.to_s.start_with?(node_modules.to_s) }
  (Dir.children(node_modules) - node_config["dependencies"].keys).each do |package|
    # ディレクトリを指定するならこう?
    Rails.configuration.assets.excluded_paths << node_modules.join(package)
    # フルパスで指定するならこう?
    Dir.glob(node_modules.join(package, "**", "*")).each do |exclude_path|
      Rails.configuration.assets.excluded_paths << exclude_path
    end
  end
end

効果なし。プリコンパイルですべてのファイルがコピーされてしまった。
https://github.com/rails/propshaft/blob/47f14e09438cd3d295d8821c0098513804e91678/lib/propshaft/railtie.rb#L25-L31
コードを確認すると、Engineでパスが追加された際に、追加されたパスの中から除外するものであり、パスを参照するときのフィルタではないことがわかった。なるほどね…

先人の知恵…もやはりうまくいかない

https://gist.github.com/tobiashm/c1b0b80b2f3616637a64

package.jsonmain で指定されているもののパスを追加するので、フォルダ名被り問題は発生しない。
が、 main が必ずしも参照したいファイルのディレクトリとは限らず、うまくいかない。

node_modules/jquery/package.json
main": "dist/jquery.js",
node_modules/jquery-ui-dist/package.json
"main": "ui/widget.js",
config/importmap.rb
pin "jquery", to: "jquery.min.js" # node_modules/jquery/dist が paths に含まれているのでok
pin "jquery-ui-dist", to: "jquery-ui.min.js" # node_modules/jquery-ui-dist/ui が paths に含まれているので jquery-ui.min.js は見つからなくてng

まとめ

Propshaft はそういうのやってない。考え方を変えよう。
必要なファイルがあるときはCDNで参照するか、 vendor/javascript にダウンロードするのが正しい方法だと思う。

調べごとの過程で、 Propshaft や importmap-rails のコードを読むことになり、勉強になったので良かった。

タケユー・ウェブ株式会社

Discussion