Post

Rails Engine with Vite

Rails Engine with Vite

Create engine

1
rails plugin new my_engine --mountable

Vite configuration

1
2
spec.add_dependency 'vite_rails'
spec.add_dependency 'vite_ruby'
1
2
3
4
5
6
7
8
9
10
11
12
require "bundler/setup"

+ require 'vite_ruby'
+ ViteRuby.install_tasks
+ ViteRuby.config.root # Ensure the engine is set as the root.

APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
load "rails/tasks/engine.rake"

load "rails/tasks/statistics.rake"

require "bundler/gem_tasks"
1
bundle exec vite install
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
require "vite_ruby"

module MyEngine
  class Engine < ::Rails::Engine
    isolate_namespace MyEngine

    delegate :vite_ruby, to: :class

    def self.vite_ruby
      @vite_ruby ||= ::ViteRuby.new(root: root, mode: Rails.env)
    end

    # Serves the engine's vite-ruby when requested
    initializer "my_engine.vite_rails.static" do |app|
      if Rails.application.config.public_file_server.enabled
        # this is the right setup when the main application is already
        # using Vite for the theme assets.
        app.middleware.insert_after ActionDispatch::Static,
                                    Rack::Static,
                                    urls: [ "/#{vite_ruby.config.public_output_dir}" ],
                                    root: root.join(vite_ruby.config.public_dir),
                                    header_rules: [
                                      # rubocop:disable Style/StringHashKeys
                                      [ :all, { "Access-Control-Allow-Origin" => "*" } ]
                                      # rubocop:enable Style/StringHashKeys
                                    ]
      else
        # mostly when running the application in production behind NGINX or APACHE
        app.middleware.insert_before 0,
                                     Rack::Static,
                                     urls: [ "/#{vite_ruby.config.public_output_dir}" ],
                                     root: root.join(vite_ruby.config.public_dir),
                                     header_rules: [
                                       # rubocop:disable Style/StringHashKeys
                                       [ :all, { "Access-Control-Allow-Origin" => "*" } ]
                                       # rubocop:enable Style/StringHashKeys
                                     ]
      end
    end

    initializer "my_engine.vite_rails_engine.proxy" do |app|
      if vite_ruby.run_proxy?
        app.middleware.insert_before 0,
                                     ViteRuby::DevServerProxy,
                                     ssl_verify_none: true,
                                     vite_ruby: vite_ruby
      end
    end

    initializer "my_engine.vite_rails_engine.logger" do
      config.after_initialize do
        vite_ruby.logger = Rails.logger
      end
    end
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "all": {
    "sourceCodeDir": "app/frontend",
    "watchAdditionalPaths": [],
    "publicOutputDir": "my_engine-assets"
  },
  "development": {
    "autoBuild": true,
    "publicOutputDir": "my_engine-assets-dev",
    "port": 3036
  },
  "test": {
    "autoBuild": true,
    "publicOutputDir": "my_engine-assets-test",
    "port": 3037
  }
}
1
rails g controller home index
1
2
3
MyEngine::Engine.routes.draw do
  root "home#index"
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
  <head>
    <title>My engine</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= yield :head %>
+   <%= vite_client_tag %>
+   <%= vite_javascript_tag 'application' %>
    <%= stylesheet_link_tag    "my_engine/application", media: "all" %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
require "vite_rails/version"
require "vite_rails/tag_helpers"

module MyEngine
  module ApplicationHelper
    include ::ViteRails::TagHelpers

    def vite_manifest
      ::MyEngine::Engine.vite_ruby.manifest
    end
  end
end

Create host app

1
rails new demo
1
gem "my_engine", path: "path/to/engine"
1
mount MyEngine::Engine => "/my_engine"

TailwindCSS configuration

1
2
yarn add -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./app/views/**/*.rb",
    "./app/views/**/*.html.erb",
    "./app/views/layouts/*.html.erb",
    "./app/helpers/**/*.rb",
    "./app/assets/stylesheets/**/*.css",
    "./app/frontend/**/*.js",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
1
touch app/frontend/entrypoints/application.css
1
2
3
@tailwind base;
@tailwind components;
@tailwind utilities;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
  <head>
    <title>My engine</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= yield :head %>
    <%= vite_client_tag %>
    <%= vite_javascript_tag 'application' %>
+   <%= vite_stylesheet_tag 'application', data: { "turbo-track": "reload" } %>
    <%= stylesheet_link_tag    "my_engine/application", media: "all" %>
  </head>
  <body>
    <%= yield %>
  </body>
</html>
1
2
<h1 class="font-bold text-4xl text-indigo-500">Home#index</h1>
<p>Find me in app/views/my_engine/home/index.html.erb</p>

Vue configuration

1
yarn add -D vue @vitejs/plugin-vue
1
2
3
4
5
6
7
8
9
10
import { defineConfig } from 'vite'
import RubyPlugin from 'vite-plugin-ruby'
import vue from '@vitejs/plugin-vue' // <-------- add this

export default defineConfig({
  plugins: [
    RubyPlugin(),
    vue() // <-------- add this
  ],
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
  <div>
    <h1>Vue App!</h1>
  </div>
</template>

<script setup>

</script>

<style lang="css" scoped>
  h1 {
    color: red;
  }
</style>
1
<div id="app"></div>
1
2
3
4
5
import { createApp } from 'vue'
import App from '../components/App.vue'

const app = createApp(App)
app.mount('#app')

Pass data from Rails to Vue components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
  <div>
    <h1>{{ msg }}</h1>
  </div>
</template>

<script setup>
defineProps({
  msg: String
})
</script>

<style lang="css" scoped>
  h1 {
    color: red;
  }
</style>
1
2
3
4
5
6
7
module MyEngine
  class HomeController < ApplicationController
    def index
      @msg = "Hello Vue on Rails!"
    end
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
<%=
  content_tag(
    :div,
    id: 'appProps',
    data: {
      props: {
        msg: @msg
      }
    }.as_json
  ) {}
%>
<div id="app"></div>

it will render the div below on the page.

1
2
3
4
<div
    id="appProps"
    data-props="{&quot;msg&quot;:&quot;Hello Vue on Rails!&quot;}">
</div>
1
2
3
4
5
6
import { createApp } from 'vue'
import App from '../components/App.vue'

const appProps = document.getElementById('appProps').dataset.props
const app = createApp(App, JSON.parse(appProps))
app.mount('#app')

References

  • https://github.com/maglevhq/maglev-core
  • https://primevise.com/blog/ruby-on-rails-boilerplate-vite-tailwind-stimulus
  • https://dev.to/kevinluo201/use-viterails-to-use-vue-sfcsingle-file-component-vue-in-rails7-51bn
This post is licensed under CC BY 4.0 by the author.