mcpField
P
propshaft

propshaft

github.com/thinkoodle開発7188 回インストール8 週間前に更新
リポジトリ

propshaft について

Rails 8 以降のアプリを保守する開発者向けの Propshaft アセット管理スキルです。アセット構成、フィンガープリント、CSS レイヤーに加え、Sprockets からの移行時の違いを説明します。

主な機能

  1. 01Rails アセットディレクトリの整理
  2. 02アセットのフィンガープリントとパスの解説
  3. 03@layer による CSS カスケード制御
  4. 04スタイルシート読み込みと事前コンパイルの設定
  5. 05Sprockets 移行の案内

活用シーン

  1. 01Rails 8 アプリのアセット設定
  2. 02アセットパスやフィンガープリントの問題調査
  3. 03CDN 利用や本番用事前コンパイルの設定
  4. 04Sprockets から Propshaft への移行

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/thinkoodle/rails-skills --skill propshaft

インストール後、Claude Code で/propshaftを入力して読み込まれたか確認します。

同梱ファイルreference.md

詳細

Rails Propshaft アセットパイプラインのエキスパート

Propshaft を使用して Rails 8 以降のアプリケーションのアセットを管理します。Propshaft は、コンパイルやバンドルを行わずにファイルを直接配信する、モダンで最小限のアセットパイプラインです。

このスキルを使用する場面

  • Rails 8 以降のアプリでアセットを設定または整理するとき
  • Sprockets から Propshaft に移行するとき
  • アセットパスの破損、スタイルシートの欠落、フィンガープリントの問題を修正するとき
  • CDN、インポートマップ、アセットのプリコンパイルを設定するとき
  • Propshaft の規約に沿って CSS/JS のファイル構成を整理するとき

重要な基本概念

Propshaft は Sprockets ではありません。 すぐに Sprockets のパターンで考えるのをやめてください。

Sprockets(旧式 — 使用禁止)Propshaft(Rails 8 のデフォルト)
//= require ディレクティブ不要 — すべてのファイルが自動的に配信されます
//= require_tree .不要 — ディレクトリが自動的に含まれます
CSS 内の asset_path("image.png")CSS 内の url("/image.png")
Sass ヘルパーの image-url("bg.png")プレーン CSS の url("/bg.png")
manifest.js ファイル不要 — マニフェストはありません
Sass/SCSS のコンパイルプレーン CSS(または cssbundling-rails を使用)
アセットのコンパイル手順コンパイルなし — ファイルはそのまま配信されます
config.assets.compile = true該当しません

エージェントが最もよく犯す間違い: //= require や Sprockets 時代のヘルパーを使うことです。Propshaft はこれらを完全に無視するため、CSS/JS 内に文字列としてそのまま表示されます。

基本方針

  1. ファイルは直接配信されます — コンパイル、バンドル、変換はありません
  2. 主な役割はフィンガープリントです — Propshaft はキャッシュを更新できるよう、ファイルのダイジェストを生成します
  3. カスケードは CSS の @layer で制御します — ファイルの読み込み順やマニフェストの宣言ではありません
  4. ブラウザーで利用できるアセットのみを使用します — Propshaft が想定するのは、ブラウザーが処理できる CSS/JS です
  5. 高機能さよりシンプルさを優先します — バンドルやトランスパイルが必要な場合は、jsbundling-rails または cssbundling-rails を追加します

ファイル構成

標準的なディレクトリ構成
app/assets/
├── stylesheets/
│   ├── application.css        # Main entry — may just declare @layer order
│   ├── _global.css            # Design tokens (CSS custom properties)
│   ├── reset.css              # CSS reset
│   ├── base.css               # Base element styles
│   ├── utilities.css          # Utility classes
│   └── components/
│       ├── buttons.css
│       ├── cards.css
│       ├── forms.css
│       ├── alerts.css
│       └── navigation.css
├── images/
│   ├── logo.svg
│   └── icons/
│       ├── arrow.svg
│       └── check.svg
└── fonts/
    ├── inter-regular.woff2
    └── inter-bold.woff2
主なルール
  • app/assets/stylesheets/ 内のすべての .css ファイルが配信可能です
  • アンダースコアのプレフィックス(_global.css)は単なる慣例です — Propshaft は特別扱いしません
  • マニフェストファイルはありません — Sprockets とは異なり、manifest.js も //= require を記述した application.css も必要ありません
  • サブディレクトリも使えます — components/cards.css は components/cards.css として配信されます

@layer を使った CSS の整理

レイヤーの順序を宣言する(重要)

application.css または _global.css で、カスケードの順序を宣言します。

/* app/assets/stylesheets/application.css */
@layer reset, base, components, utilities;

この 1 行で、カスケードの優先順位がすべて決まります。後に記述されたレイヤーほど優先されます。

各ファイルをレイヤーで囲む
/* app/assets/stylesheets/reset.css */
@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
  body { margin: 0; }
}
/* app/assets/stylesheets/components/cards.css */
@layer components {
  .card {
    padding: var(--space-4);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
  }
}
/* app/assets/stylesheets/utilities.css */
@layer utilities {
  .hidden { display: none !important; }
  .text-center { text-align: center; }
}
@layer が重要な理由

@layer がない場合、CSS の詳細度はファイルの読み込み順に左右されますが、Propshaft はその順序を保証しません。@layer を使えば、どのファイルが先に読み込まれても、宣言した順序が常に優先されます。

  1. reset — 最も低い優先順位(CSS リセット、ボックスサイズ)
  2. base — 要素のデフォルト(body、見出し、リンク)
  3. components — UI コンポーネント(カード、ボタン、フォーム)
  4. utilities — 最も高い優先順位(あらゆるスタイルを上書き)

いずれの @layer にも属さないスタイルは、最も高い詳細度を持ちます — すべてのレイヤーより優先されます。意図的に使用してください。

CSS カスタムプロパティを使ったデザイントークン

/* app/assets/stylesheets/_global.css */
:root {
  /* Colors */
  --color-primary: #2563eb;
  --color-surface: #ffffff;
  --color-border: #e5e7eb;
  --color-text: #111827;
  --color-text-muted: #6b7280;

  /* Spacing */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;

  /* Typography */
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.125rem;
  --font-medium: 500;
  --font-bold: 700;

  /* Borders */
  --radius-sm: 0.25rem;
  --radius-md: 0.375rem;
  --radius-lg: 0.5rem;
}

light-dark() を使ったダークモード:

:root {
  color-scheme: light dark;
  --color-surface: light-dark(#ffffff, #1f2937);
  --color-text: light-dark(#111827, #f9fafb);
}

レイアウトでのスタイルシートの読み込み

:app シンボル(推奨)
<%# Loads ALL stylesheets from app/assets/stylesheets/ %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
特定のファイルを読み込む
<%# Load third-party CSS first, then app CSS %>
<%= stylesheet_link_tag "actiontext", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
:all は絶対に使わない
<%# BAD — loads engine CSS too (e.g., Bulma from mission_control-jobs) %>
<%= stylesheet_link_tag :all %>

<%# GOOD — only your app's stylesheets %>
<%= stylesheet_link_tag :app %>
明示的にファイル順を指定する(必要な場合)
<%= stylesheet_link_tag "reset", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "base", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag "main", "data-turbo-track": "reload" %>

アセットの参照

ビュー内(ERB)
<%# Images %>
<%= image_tag "logo.svg", alt: "Company Logo" %>
<%= image_tag "icons/arrow.svg", alt: "Arrow", class: "icon" %>

<%# Stylesheets %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>

<%# Favicon %>
<%= favicon_link_tag "favicon.ico" %>

<%# Generic asset path %>
<%= asset_path("document.pdf") %>
CSS 内
/* Use root-relative paths — Propshaft rewrites these to digested URLs */
.hero {
  background-image: url("/bg/pattern.svg");
}

.icon-check {
  background-image: url("/icons/check.svg");
}

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-regular.woff2") format("woff2");
  font-weight: 400;
}

Propshaft はプリコンパイル時に、url("/bg/pattern.svg") を url("/assets/bg/pattern-abc123.svg") に自動的に書き換えます。

JavaScript 内

RAILS_ASSET_URL マクロを使用します。

// Propshaft transforms this during precompilation
const trashIcon = RAILS_ASSET_URL("/icons/trash.svg");
// Becomes: "/assets/icons/trash-54g9cbef.svg"

インポートマップを使った JavaScript

Propshaft は CSS/画像/フォントを処理します。JavaScript は importmap-rails(Rails 8 のデフォルト)で管理します。

app/javascript/
├── application.js           # Entry point
└── controllers/             # Stimulus controllers
    ├── hello_controller.js
    └── modal_controller.js
<%# In layout — this is importmap-rails, not Propshaft %>
<%= javascript_importmap_tags %>
# config/importmap.rb
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin_all_from "app/javascript/controllers", under: "controllers"

重要な違い: Propshaft は JS ファイルにフィンガープリントを付与します。インポートマップはモジュール名を URL に解決します。両者は互いを補完します — Propshaft は JS をバンドルも変換もしません。

画像とフォント

画像

app/assets/images/ に配置します。ヘルパーで参照します。

<%= image_tag "logo.svg", alt: "Logo", width: 200 %>
<%= image_tag "icons/edit.svg", class: "icon", alt: "" %>

CSS 内では次のようにします。

.logo { background-image: url("/logo.svg"); }
フォント

app/assets/fonts/ に配置します。@font-face で宣言します。

@font-face {
  font-family: "Inter";
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src: url("/fonts/inter-regular.woff2") format("woff2");
}

@font-face {
  font-family: "Inter";
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  src: url("/fonts/inter-bold.woff2") format("woff2");
}

body {
  font-family: "Inter", system-ui, sans-serif;
}

デプロイとプリコンパイル

プリコンパイルコマンド
RAILS_ENV=production rails assets:precompile

または、実際のシークレットを使わずに実行します。

RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 rails assets:precompile

このコマンドでは、次の処理が行われます。

  1. ロードパスからすべてのアセットを public/assets/ にコピーします
  2. ファイル名にフィンガープリントを付与します(例:application-a1b2c3.css)
  3. CSS 内の url() の参照先を、ダイジェスト付きパスに書き換えます
  4. 元のファイル名とダイジェスト付きファイル名の対応を記した .manifest.json を生成します
CDN の設定
# config/environments/production.rb
config.asset_host = ENV.fetch("CDN_HOST", nil)
# e.g., CDN_HOST=https://cdn.example.com
キャッシュヘッダー(Web サーバー)

Nginx:

location ~ ^/assets/ {
  expires 1y;
  add_header Cache-Control public;
  add_header ETag "";
}

Apache:

<Location /assets/>
  Header unset ETag
  FileETag None
  ExpiresActive On
  ExpiresDefault "access plus 1 year"
</Location>
本番環境の Cache-Control
# config/environments/production.rb
config.public_file_server.headers = {
  "Cache-Control" => "public, max-age=31536000"
}

スタイルシートの追加

  1. ファイルを作成します。

    app/assets/stylesheets/components/modal.css
    
  2. 適切な @layer で囲みます。

    @layer components {
      .modal { /* ... */ }
      .modal-overlay { /* ... */ }
    }
    
  3. これで完了です。 Propshaft が自動的に読み込みます。マニフェストの更新も、require ディレクティブも不要です。

サードパーティ/エンジンの CSS

CSS を提供する gem(例:Action Text、Trix)を使用する場合:

<%# Load third-party BEFORE :app so your styles can override %>
<%= stylesheet_link_tag "actiontext", "data-turbo-track": "reload" %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>

アセットパスの追加

# config/initializers/assets.rb
Rails.application.config.assets.paths << Rails.root.join("vendor/assets/stylesheets")
Rails.application.config.assets.paths << Emoji.images_path

ダイジェスト処理からのパスの除外

# config/initializers/assets.rb
# Useful when using cssbundling-rails with Sass source files
config.assets.excluded_paths = [Rails.root.join("app/assets/stylesheets")]

デバッグ

# List all available assets
bin/rails assets:reveal

# In Rails console
Rails.application.assets.resolver.logical_paths.to_a

# Clear precompiled assets (fix stale development assets)
bin/rails assets:clobber

開発環境でよくある問題: アセットが更新されなくなった場合は、開発環境で assets:precompile を実行した可能性があります。public/assets/ 内の .manifest.json によって、Rails はプリコンパイル済みファイルを使用します。rails assets:clobber を実行して解決してください。

よくある間違い

  1. //= require を使う — Propshaft は Sprockets ディレクティブを一切無視します
  2. image-url() や asset-url() を使う — これらは Sass/Sprockets のヘルパーです。通常の url("/path") を使用してください
  3. :app の代わりに :all を使う — :all は不要なエンジンの CSS も読み込みます
  4. @layer 宣言がない — レイヤーがないと、カスケードは予測できないファイルの読み込み順に左右されます
  5. Sass のコンパイルを想定する — Propshaft は通常の CSS を配信します。Sass を使用するには、cssbundling-rails または dartsass-rails を使用してください
  6. 開発環境でプリコンパイルする — アセットを固定する .manifest.json が作成されます。assets:clobber を実行してください
  7. CSS の URL 形式が間違っている — url("icons/check.svg") ではなく、url("/icons/check.svg") を使用してください(ルート相対パス)
  8. Propshaft と importmap-rails を混同する — Propshaft は CSS/画像/フォント用、Import Maps は JavaScript モジュール用です

Sprockets からの移行

完全な移行チェックリストについては、このスキルディレクトリの reference.md を参照してください。

概要:

  1. bundle remove sprockets sprockets-rails sass-rails
  2. config/assets.rb と assets/config/manifest.js を削除します
  3. bundle add propshaft を実行します(または Propshaft が含まれる Rails 8 にアップグレードします)
  4. //= require を削除します。ファイルは自動的に読み込まれます
  5. image-url()/asset-url() を url("/path") に置き換えます
  6. CSS 内の asset_path() を url("/path") に置き換えます
  7. カスケードを制御するために @layer 宣言を追加します
  8. Sass を通常の CSS に変換します(または dartsass-rails/cssbundling-rails を追加します)

コメント