├── .editorconfig ├── .github └── workflows │ └── test-new-rails-app.yml ├── .gitignore ├── .nvmrc ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── doc └── release.md ├── lib └── svelte │ ├── rails.rb │ ├── rails │ ├── controller_renderer.rb │ ├── install_generator.rb │ ├── install_task.rake │ ├── railtie.rb │ ├── templates │ │ ├── app │ │ │ └── javascript │ │ │ │ ├── components │ │ │ │ └── Hello.svelte │ │ │ │ └── packs │ │ │ │ ├── application.js │ │ │ │ └── server_rendering.js │ │ └── config │ │ │ └── webpack │ │ │ ├── development.js │ │ │ ├── environment.js │ │ │ ├── loaders │ │ │ ├── svelte-ssr.js │ │ │ └── svelte.js │ │ │ ├── production.js │ │ │ └── test.js │ ├── version.rb │ └── view_helper.rb │ └── renderer.rb ├── package.json ├── spec ├── spec_helper.rb └── svelte │ └── rails_spec.rb ├── svelte-rails.gemspec ├── svelte_ujs ├── WaitPlugin.js ├── getSvelteEnvironments.js └── index.js ├── test.sh └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/workflows/test-new-rails-app.yml: -------------------------------------------------------------------------------- 1 | name: Test new Rails app 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Ruby 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: 2.7 19 | - name: Install dependencies 20 | run: bundle install 21 | - name: Use Node.js 14.x 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: 14.x 25 | - name: Create new Rails app and run system tests 26 | run: ./test.sh 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /pkg/ 6 | /spec/reports/ 7 | /tmp/ 8 | /node_modules/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | cache: bundler 4 | rvm: 5 | - 2.7.1 6 | before_install: gem install bundler -v 2.1.4 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nning/svelte-rails/209df99682ea576b642e1ce88c4aa2746ed78a7c/CHANGELOG.md -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at henning.mueller@hmmh.de. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in svelte-rails.gemspec 4 | gemspec 5 | 6 | gem "rake", "~> 12.0" 7 | gem "rspec", "~> 3.0" 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | svelte-rails (0.3.4) 5 | execjs 6 | railties (>= 5.2) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (6.0.3.4) 12 | actionpack (= 6.0.3.4) 13 | nio4r (~> 2.0) 14 | websocket-driver (>= 0.6.1) 15 | actionmailbox (6.0.3.4) 16 | actionpack (= 6.0.3.4) 17 | activejob (= 6.0.3.4) 18 | activerecord (= 6.0.3.4) 19 | activestorage (= 6.0.3.4) 20 | activesupport (= 6.0.3.4) 21 | mail (>= 2.7.1) 22 | actionmailer (6.0.3.4) 23 | actionpack (= 6.0.3.4) 24 | actionview (= 6.0.3.4) 25 | activejob (= 6.0.3.4) 26 | mail (~> 2.5, >= 2.5.4) 27 | rails-dom-testing (~> 2.0) 28 | actionpack (6.0.3.4) 29 | actionview (= 6.0.3.4) 30 | activesupport (= 6.0.3.4) 31 | rack (~> 2.0, >= 2.0.8) 32 | rack-test (>= 0.6.3) 33 | rails-dom-testing (~> 2.0) 34 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 35 | actiontext (6.0.3.4) 36 | actionpack (= 6.0.3.4) 37 | activerecord (= 6.0.3.4) 38 | activestorage (= 6.0.3.4) 39 | activesupport (= 6.0.3.4) 40 | nokogiri (>= 1.8.5) 41 | actionview (6.0.3.4) 42 | activesupport (= 6.0.3.4) 43 | builder (~> 3.1) 44 | erubi (~> 1.4) 45 | rails-dom-testing (~> 2.0) 46 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 47 | activejob (6.0.3.4) 48 | activesupport (= 6.0.3.4) 49 | globalid (>= 0.3.6) 50 | activemodel (6.0.3.4) 51 | activesupport (= 6.0.3.4) 52 | activerecord (6.0.3.4) 53 | activemodel (= 6.0.3.4) 54 | activesupport (= 6.0.3.4) 55 | activestorage (6.0.3.4) 56 | actionpack (= 6.0.3.4) 57 | activejob (= 6.0.3.4) 58 | activerecord (= 6.0.3.4) 59 | marcel (~> 0.3.1) 60 | activesupport (6.0.3.4) 61 | concurrent-ruby (~> 1.0, >= 1.0.2) 62 | i18n (>= 0.7, < 2) 63 | minitest (~> 5.1) 64 | tzinfo (~> 1.1) 65 | zeitwerk (~> 2.2, >= 2.2.2) 66 | builder (3.2.4) 67 | concurrent-ruby (1.1.7) 68 | crass (1.0.6) 69 | diff-lcs (1.4.4) 70 | erubi (1.10.0) 71 | execjs (2.7.0) 72 | globalid (0.4.2) 73 | activesupport (>= 4.2.0) 74 | i18n (1.8.5) 75 | concurrent-ruby (~> 1.0) 76 | loofah (2.8.0) 77 | crass (~> 1.0.2) 78 | nokogiri (>= 1.5.9) 79 | mail (2.7.1) 80 | mini_mime (>= 0.1.1) 81 | marcel (0.3.3) 82 | mimemagic (~> 0.3.2) 83 | method_source (1.0.0) 84 | mimemagic (0.3.5) 85 | mini_mime (1.0.2) 86 | mini_portile2 (2.4.0) 87 | minitest (5.14.2) 88 | nio4r (2.5.4) 89 | nokogiri (1.10.10) 90 | mini_portile2 (~> 2.4.0) 91 | rack (2.2.3) 92 | rack-test (1.1.0) 93 | rack (>= 1.0, < 3) 94 | rails (6.0.3.4) 95 | actioncable (= 6.0.3.4) 96 | actionmailbox (= 6.0.3.4) 97 | actionmailer (= 6.0.3.4) 98 | actionpack (= 6.0.3.4) 99 | actiontext (= 6.0.3.4) 100 | actionview (= 6.0.3.4) 101 | activejob (= 6.0.3.4) 102 | activemodel (= 6.0.3.4) 103 | activerecord (= 6.0.3.4) 104 | activestorage (= 6.0.3.4) 105 | activesupport (= 6.0.3.4) 106 | bundler (>= 1.3.0) 107 | railties (= 6.0.3.4) 108 | sprockets-rails (>= 2.0.0) 109 | rails-dom-testing (2.0.3) 110 | activesupport (>= 4.2.0) 111 | nokogiri (>= 1.6) 112 | rails-html-sanitizer (1.3.0) 113 | loofah (~> 2.3) 114 | railties (6.0.3.4) 115 | actionpack (= 6.0.3.4) 116 | activesupport (= 6.0.3.4) 117 | method_source 118 | rake (>= 0.8.7) 119 | thor (>= 0.20.3, < 2.0) 120 | rake (12.3.3) 121 | rspec (3.10.0) 122 | rspec-core (~> 3.10.0) 123 | rspec-expectations (~> 3.10.0) 124 | rspec-mocks (~> 3.10.0) 125 | rspec-core (3.10.0) 126 | rspec-support (~> 3.10.0) 127 | rspec-expectations (3.10.0) 128 | diff-lcs (>= 1.2.0, < 2.0) 129 | rspec-support (~> 3.10.0) 130 | rspec-mocks (3.10.0) 131 | diff-lcs (>= 1.2.0, < 2.0) 132 | rspec-support (~> 3.10.0) 133 | rspec-support (3.10.0) 134 | sprockets (4.0.2) 135 | concurrent-ruby (~> 1.0) 136 | rack (> 1, < 3) 137 | sprockets-rails (3.2.2) 138 | actionpack (>= 4.0) 139 | activesupport (>= 4.0) 140 | sprockets (>= 3.0.0) 141 | thor (1.0.1) 142 | thread_safe (0.3.6) 143 | tzinfo (1.2.8) 144 | thread_safe (~> 0.1) 145 | websocket-driver (0.7.3) 146 | websocket-extensions (>= 0.1.0) 147 | websocket-extensions (0.1.5) 148 | zeitwerk (2.4.2) 149 | 150 | PLATFORMS 151 | ruby 152 | 153 | DEPENDENCIES 154 | rails (>= 5.2) 155 | rake (~> 12.0) 156 | rspec (~> 3.0) 157 | svelte-rails! 158 | 159 | BUNDLED WITH 160 | 2.1.4 161 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 henning mueller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Svelte-Rails 2 | 3 | [![Gem](https://img.shields.io/gem/v/svelte-rails.svg?style=flat-square)](https://rubygems.org/gems/svelte-rails) 4 | [![npm](https://img.shields.io/npm/v/svelte_ujs_ng.svg?style=flat-square)](https://www.npmjs.com/package/svelte_ujs_ng) 5 | ![Build Status](https://img.shields.io/github/workflow/status/nning/svelte-rails/Test%20new%20Rails%20app) 6 | 7 | Svelte-Rails integrates [Svelte](https://svelte.dev/) with Ruby on Rails. It has the following features: 8 | 9 | * Automatically renders Svelte server-side and client-side 10 | * Supports Webpacker >= 4.2 and Ruby on Rails >= 6 11 | 12 | ## Usage 13 | 14 | Make sure, you have [set-up Webpacker](https://github.com/rails/webpacker#installation) and it's [Svelte integration](https://github.com/rails/webpacker/blob/master/docs/integrations.md#svelte). 15 | 16 | For a quick start with a new app, simply run: 17 | 18 | rails new demo --webpack=svelte 19 | 20 | Add this line to your application's Gemfile: 21 | 22 | ```ruby 23 | gem 'svelte-rails' 24 | ``` 25 | 26 | And then execute: 27 | 28 | $ bundle 29 | $ rails svelte:install 30 | 31 | You can overwrite conflicting files if you have started with a fresh Rails app or did not change the webpack config of your existing one. 32 | 33 | An example Rails app demonstrating the integration of svelte-rails can be found here: 34 | https://github.com/nning/svelte-rails-demo/commits/master 35 | 36 | ## View Helper 37 | 38 | ```erb 39 | <%= svelte_component :Hello, name: 'Svelte' %> 40 | <%= svelte_component :Hello, {name: 'Svelte'}, {prerender: true} %> 41 | ``` 42 | 43 | ## Controller Renderer 44 | 45 | ```ruby 46 | class TodoController < ApplicationController 47 | def index 48 | @todos = Todo.all 49 | render component: 'TodoList', props: { todos: @todos } 50 | end 51 | end 52 | ``` 53 | 54 | `prerender` is activated by default, can be disabled with `prerender: false`. 55 | 56 | ## Missing Features 57 | 58 | * HMR and Bundle consistency (server-rendered HTML is cached and client-side updates on changes to the sources) 59 | * Generator for components 60 | * Render pools 61 | * Better documentation for setup 62 | 63 | ## Configuration Options 64 | 65 | Configuration can be changed in `config/application.rb`, for example. 66 | 67 | # Prerender (SSR) by default (i.e. without passing `prerender:true` to the view helper) 68 | config.svelte.prerender_default = false 69 | 70 | ## Contributing 71 | 72 | Bug reports and pull requests are welcome on GitHub at https://github.com/nning/svelte-rails. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/nning/svelte-rails/blob/master/CODE_OF_CONDUCT.md). 73 | 74 | ## License 75 | 76 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 77 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "svelte/rails" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /doc/release.md: -------------------------------------------------------------------------------- 1 | * Update versions in `package.json` and `lib/svelte/rails/version.rb` 2 | * Run `bundle` 3 | * `git commit -m 'v0.3.4'` 4 | * `git tag -s v0.3.4` 5 | * `npm publish` 6 | * `rake release` 7 | * Create GitHub release 8 | * `git push` 9 | * `git push origin v0.3.4` 10 | -------------------------------------------------------------------------------- /lib/svelte/rails.rb: -------------------------------------------------------------------------------- 1 | require 'svelte/rails/version' 2 | 3 | module Svelte 4 | module Rails 5 | # class Error < StandardError; end 6 | end 7 | end 8 | 9 | require 'svelte/rails/view_helper' 10 | require 'svelte/rails/controller_renderer' 11 | 12 | require 'svelte/rails/railtie' if defined?(Rails) 13 | -------------------------------------------------------------------------------- /lib/svelte/rails/controller_renderer.rb: -------------------------------------------------------------------------------- 1 | require 'svelte/rails/view_helper' 2 | 3 | module Svelte 4 | module Rails 5 | class ControllerRenderer 6 | include Svelte::Rails::ViewHelper 7 | # include ActionView::Helpers::TagHelper 8 | # include ActionView::Helpers::TextHelper 9 | 10 | attr_accessor :output_buffer 11 | 12 | # @return [String] HTML for `component_name` with `options[:props]` 13 | def call(component_name, options, &block) 14 | props = options.fetch(:props, {}) 15 | options = default_options.merge(options.slice(:data, :aria, :tag, :class, :id, :prerender, :camelize_props)) 16 | svelte_component(component_name, props, options, &block) 17 | end 18 | 19 | private 20 | 21 | def default_options 22 | { prerender: true } 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/svelte/rails/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'rails/generators' 2 | 3 | 4 | module Svelte 5 | class InstallGenerator < ::Rails::Generators::Base 6 | TEMPLATE_DIR = File.expand_path('../templates', __FILE__) 7 | source_root TEMPLATE_DIR 8 | 9 | desc 'Install Svelte support' 10 | 11 | def create_directories 12 | empty_directory components_dir 13 | create_file File.join(components_dir, '.keep') 14 | end 15 | 16 | def copy_templates 17 | copy_template(webpack_dir, 'environment.js') 18 | copy_template(webpack_dir, 'development.js') 19 | copy_template(webpack_dir, 'production.js') 20 | copy_template(webpack_dir, 'test.js') 21 | 22 | copy_template(webpack_dir, 'loaders', 'svelte.js') 23 | copy_template(webpack_dir, 'loaders', 'svelte-ssr.js') 24 | 25 | copy_template(packs_dir, 'server_rendering.js') 26 | copy_template(components_dir, 'Hello.svelte') 27 | end 28 | 29 | def update_application_entry 30 | path = Pathname.new('app/javascript/packs/application.js') 31 | content = File.read(File.join(TEMPLATE_DIR, path)) 32 | 33 | if path.exist? 34 | append_file(path, content) 35 | else 36 | create_file(path, content) 37 | end 38 | end 39 | 40 | def install_svelte_ujs 41 | `yarn add svelte_ujs_ng svelte-preprocess` 42 | end 43 | 44 | private 45 | 46 | def rails_dir 47 | Pathname.new(destination_root).relative_path_from(::Rails.root) 48 | end 49 | 50 | def packs_dir 51 | Webpacker.config.source_entry_path.relative_path_from(::Rails.root) 52 | end 53 | 54 | def js_dir 55 | packs_dir.parent 56 | end 57 | 58 | def components_dir 59 | File.join(js_dir, 'components') 60 | end 61 | 62 | def webpack_dir 63 | File.join(rails_dir, 'config', 'webpack') 64 | end 65 | 66 | def copy_template(*path_segments) 67 | source = File.join(*path_segments) 68 | template(source, ::Rails.root.join(source)) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/svelte/rails/install_task.rake: -------------------------------------------------------------------------------- 1 | require 'svelte/rails/install_generator' 2 | 3 | namespace :svelte do 4 | desc 'Install Svelte support' 5 | task :install do 6 | Svelte::InstallGenerator.start 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/svelte/rails/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'rails/railtie' 2 | 3 | module Svelte::Rails 4 | class Railtie < ::Rails::Railtie 5 | config.svelte = ActiveSupport::OrderedOptions.new 6 | 7 | # Prerender (SSR) by default (i.e. without passing `prerender:true` to the view helper) 8 | config.svelte.prerender_default = false 9 | 10 | initializer 'svelte_rails.setup_view_helpers', after: :load_config_initializers, group: :all do |app| 11 | ActiveSupport.on_load(:action_view) do 12 | include ::Svelte::Rails::ViewHelper 13 | end 14 | end 15 | 16 | initializer 'svelte_rails.add_component_renderers', group: :all do |app| 17 | render_component = lambda do |component_name, options| 18 | renderer = ::Svelte::Rails::ControllerRenderer.new 19 | html = renderer.call(component_name, options) 20 | render_options = options.merge(inline: html) 21 | render(render_options) 22 | end 23 | 24 | %i[component svelte svelte_component].each do |renderer_name| 25 | ActionController::Renderers.add renderer_name, &render_component 26 | end 27 | end 28 | 29 | rake_tasks do 30 | load 'svelte/rails/install_task.rake' 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/svelte/rails/templates/app/javascript/components/Hello.svelte: -------------------------------------------------------------------------------- 1 | 2 | Hello {name}! ❤ 3 | 4 | 5 | 21 | 22 | 31 | 32 |

Hello {name}! ❤

33 | 34 | {#if time} 35 | {time} 36 | {/if} 37 | -------------------------------------------------------------------------------- /lib/svelte/rails/templates/app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | import SvelteRailsUJS from 'svelte_ujs_ng'; 2 | self.SvelteRailsUJS = SvelteRailsUJS; 3 | SvelteRailsUJS.start(); 4 | -------------------------------------------------------------------------------- /lib/svelte/rails/templates/app/javascript/packs/server_rendering.js: -------------------------------------------------------------------------------- 1 | import SvelteRailsUJS from 'svelte_ujs_ng'; 2 | self.SvelteRailsUJS = SvelteRailsUJS; 3 | -------------------------------------------------------------------------------- /lib/svelte/rails/templates/config/webpack/development.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.map(x => x.toWebpackConfig()) 6 | -------------------------------------------------------------------------------- /lib/svelte/rails/templates/config/webpack/environment.js: -------------------------------------------------------------------------------- 1 | const clientLoader = require('./loaders/svelte') 2 | const serverLoader = require('./loaders/svelte-ssr') 3 | 4 | const getSvelteEnvironments = require('svelte_ujs_ng/getSvelteEnvironments') 5 | 6 | module.exports = getSvelteEnvironments(clientLoader, serverLoader) 7 | -------------------------------------------------------------------------------- /lib/svelte/rails/templates/config/webpack/loaders/svelte-ssr.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test: /\.svelte$/, 3 | use: [{ 4 | loader: 'svelte-loader', 5 | options: { 6 | generate: 'ssr', 7 | emitCss: false, 8 | css: false 9 | } 10 | }], 11 | } 12 | -------------------------------------------------------------------------------- /lib/svelte/rails/templates/config/webpack/loaders/svelte.js: -------------------------------------------------------------------------------- 1 | const sveltePreprocess = require('svelte-preprocess') 2 | 3 | const dev = process.env.RAILS_ENV !== 'production' 4 | 5 | module.exports = { 6 | test: /\.svelte$/, 7 | use: [ 8 | { 9 | loader: 'babel-loader', 10 | options: { 11 | presets: ['@babel/preset-env'], 12 | } 13 | }, 14 | { 15 | loader: 'svelte-loader', 16 | options: { 17 | dev, 18 | hotReload: true, 19 | hydratable: true, 20 | emitCss: true, 21 | preprocess: sveltePreprocess() 22 | } 23 | } 24 | ], 25 | } 26 | -------------------------------------------------------------------------------- /lib/svelte/rails/templates/config/webpack/production.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'production' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.map(x => x.toWebpackConfig()) 6 | -------------------------------------------------------------------------------- /lib/svelte/rails/templates/config/webpack/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development' 2 | 3 | const environment = require('./environment') 4 | 5 | module.exports = environment.map(x => x.toWebpackConfig()) 6 | -------------------------------------------------------------------------------- /lib/svelte/rails/version.rb: -------------------------------------------------------------------------------- 1 | module Svelte 2 | module Rails 3 | VERSION = '0.3.4' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/svelte/rails/view_helper.rb: -------------------------------------------------------------------------------- 1 | require 'svelte/renderer' 2 | 3 | module Svelte::Rails::ViewHelper 4 | def svelte_component(*args, &block) 5 | Svelte::Renderer.new.render(*args) { capture &block if block_given? } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/svelte/renderer.rb: -------------------------------------------------------------------------------- 1 | require 'execjs' 2 | require 'action_view' 3 | 4 | module Svelte 5 | class Renderer 6 | include ActionView::Helpers::TagHelper 7 | include ActionView::Helpers::TextHelper 8 | 9 | attr_accessor :output_buffer 10 | 11 | # This pattern matches the code that initializes the dev-server client. 12 | CLIENT_REQUIRE = %r{__webpack_require__\(.*webpack-dev-server\/client\/index\.js.*\n} 13 | 14 | def render(name, props = {}, options = {}, &block) 15 | prerender_options = options[:prerender] 16 | 17 | if prerender_options.nil? 18 | prerender_options = ::Rails.application.config.svelte.prerender_default 19 | end 20 | 21 | if prerender_options 22 | block = Proc.new { concat(prerender_component(name, props, prerender_options)) } 23 | end 24 | 25 | html_options = options.reverse_merge(:data => {}) 26 | unless prerender_options == :static 27 | html_options[:data].tap do |data| 28 | data[:svelte_class] = name 29 | data[:svelte_props] = props.is_a?(String) ? props : props.to_json 30 | data[:hydrate] = 't' if prerender_options 31 | end 32 | end 33 | 34 | html_options.except!(:tag, :prerender, :camelize_props) 35 | 36 | content_tag(options[:tag] || :div, '', html_options, &block) 37 | end 38 | 39 | private 40 | 41 | def prerender_component(component_name, props, prerender_options) 42 | initial_code = <<-JS 43 | var global = global || this; 44 | var self = self || this; 45 | 46 | #{find_asset('server_rendering.js')} 47 | JS 48 | 49 | # File.write(::Rails.root.join('debug.js'), initial_code) 50 | @context = ExecJS.compile(initial_code) 51 | 52 | js_code = <<-JS 53 | (function(){ 54 | return SvelteRailsUJS.serverRender('#{component_name}', #{props.to_json}); 55 | })() 56 | JS 57 | 58 | @context.eval(js_code).html_safe 59 | end 60 | 61 | def find_asset(logical_path) 62 | if Webpacker.dev_server.running? 63 | ds = Webpacker.dev_server 64 | 65 | asset_path = Webpacker.manifest.lookup(logical_path).to_s 66 | # Remove the protocol and host from the asset path. Sometimes webpacker includes this, sometimes it does not 67 | asset_path.slice!("#{ds.protocol}://#{ds.host_with_port}") 68 | 69 | require 'open-uri' 70 | 71 | dev_server_asset = URI.send(:open, "#{ds.protocol}://#{ds.host_with_port}#{asset_path}").read 72 | dev_server_asset.sub!(CLIENT_REQUIRE, '//\0') 73 | dev_server_asset 74 | else 75 | File.read(::Rails.root.join('public', Webpacker.manifest.lookup(logical_path)[1..-1])) 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte_ujs_ng", 3 | "version": "0.3.4", 4 | "description": "Rails UJS for turbolinks and SSR of the svelte-rails gem", 5 | "license": "MIT", 6 | "author": "henning mueller ", 7 | "bugs": { 8 | "url": "https://github.com/nning/svelte-rails/issues" 9 | }, 10 | "homepage": "https://github.com/nning/svelte-rails#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/nning/svelte-rails.git" 14 | }, 15 | "main": "svelte_ujs/index.js", 16 | "exports": { 17 | "./": "./svelte_ujs/" 18 | }, 19 | "dependencies": { 20 | "@rails/webpacker": "4.2.2", 21 | "before-build-webpack": "^0.2.9", 22 | "webpack-assets-manifest": "https://github.com/pustovalov/webpack-assets-manifest#build" 23 | }, 24 | "resolutions": { 25 | "webpack-assets-manifest": "https://github.com/pustovalov/webpack-assets-manifest#build" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "svelte/rails" 3 | 4 | RSpec.configure do |config| 5 | # Enable flags like --only-failures and --next-failure 6 | config.example_status_persistence_file_path = ".rspec_status" 7 | 8 | # Disable RSpec exposing methods globally on `Module` and `main` 9 | config.disable_monkey_patching! 10 | 11 | config.expect_with :rspec do |c| 12 | c.syntax = :expect 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/svelte/rails_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Svelte::Rails do 2 | it "has a version number" do 3 | expect(Svelte::Rails::VERSION).not_to be nil 4 | end 5 | 6 | it "does something useful" do 7 | expect(false).to eq(true) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /svelte-rails.gemspec: -------------------------------------------------------------------------------- 1 | require_relative 'lib/svelte/rails/version' 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = 'svelte-rails' 5 | spec.version = Svelte::Rails::VERSION 6 | spec.authors = ['henning mueller'] 7 | spec.email = ['mail@nning.io'] 8 | 9 | spec.summary = 'Svelte integration for Ruby on Rails' 10 | spec.description = 'Render Svelte components in Rails views. Supports server-side rendering with ExecJS.' 11 | spec.homepage = 'https://github.com/nning/svelte-rails' 12 | spec.license = 'MIT' 13 | spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0') 14 | 15 | spec.metadata['homepage_uri'] = spec.homepage 16 | spec.metadata['source_code_uri'] = spec.homepage 17 | spec.metadata['changelog_uri'] = spec.homepage + '/blob/master/CHANGELOG.md' 18 | 19 | spec.add_development_dependency 'rails', '>= 5.2' 20 | 21 | # spec.add_dependency 'connection_pool' 22 | spec.add_dependency 'execjs' 23 | spec.add_dependency 'railties', '>= 5.2' 24 | 25 | # Specify which files should be added to the gem when it is released. 26 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 27 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 28 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 29 | end 30 | 31 | spec.bindir = 'exe' 32 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 33 | spec.require_paths = ['lib'] 34 | end 35 | -------------------------------------------------------------------------------- /svelte_ujs/WaitPlugin.js: -------------------------------------------------------------------------------- 1 | const config = require('@rails/webpacker/package/config') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const WebpackBeforeBuildPlugin = require('before-build-webpack') 5 | 6 | // https://www.viget.com/articles/run-multiple-webpack-configs-sequentially/ 7 | class WaitPlugin extends WebpackBeforeBuildPlugin { 8 | constructor(file, interval = 200, timeout = 30000) { 9 | const filePath = path.join(config.outputPath, file) 10 | 11 | try { 12 | fs.unlinkSync(filePath) 13 | } catch(e) { 14 | // Ignore if file does not exist 15 | } 16 | 17 | super((stats, callback) => { 18 | let start = Date.now() 19 | 20 | function poll() { 21 | if (fs.existsSync(filePath)) { 22 | callback() 23 | } else if (Date.now() - start > timeout) { 24 | throw Error(`Waited too long for "${file}" to exist.`) 25 | } else { 26 | setTimeout(poll, interval) 27 | } 28 | } 29 | 30 | poll() 31 | }) 32 | } 33 | } 34 | 35 | module.exports = WaitPlugin 36 | -------------------------------------------------------------------------------- /svelte_ujs/getSvelteEnvironments.js: -------------------------------------------------------------------------------- 1 | const config = require('@rails/webpacker/package/config') 2 | const ConfigList = require('@rails/webpacker/package/config_types/config_list') 3 | const WaitPlugin = require('./WaitPlugin') 4 | 5 | // Use version of WebpackAssetsManifest that supports merging entrypoints in manifest.json 6 | // https://github.com/webdeveric/webpack-assets-manifest/pull/59 7 | const WebpackAssetsManifest = require('webpack-assets-manifest') 8 | 9 | const env = process.env.RAILS_ENV 10 | const Environment = require('@rails/webpacker/package/environments/' + env) 11 | 12 | function getSvelteEnvironments(clientLoader, serverLoader) { 13 | const clientEnvironment = new Environment() 14 | clientEnvironment.entry.delete('server_rendering') 15 | clientEnvironment.loaders.insert('svelte', clientLoader, {before: 'babel'}) 16 | 17 | // We need to set merge:true for generating the manifest.json 18 | clientEnvironment.plugins.delete('Manifest') 19 | clientEnvironment.plugins.append( 20 | 'Manifest', 21 | new WebpackAssetsManifest({ 22 | entrypoints: true, 23 | writeToDisk: true, 24 | publicPath: config.publicPathWithoutCDN, 25 | merge: true 26 | }) 27 | ) 28 | 29 | const serverEnvironment = new Environment() 30 | // TODO Actually delete everything but server_rendering 31 | serverEnvironment.entry.delete('application') 32 | serverEnvironment.loaders.prepend('svelte', serverLoader) 33 | 34 | // We need to set merge:true for generating the manifest.json 35 | serverEnvironment.plugins.delete('Manifest') 36 | serverEnvironment.plugins.append( 37 | 'Manifest', 38 | new WebpackAssetsManifest({ 39 | entrypoints: true, 40 | writeToDisk: true, 41 | publicPath: config.publicPathWithoutCDN, 42 | merge: true 43 | }) 44 | ) 45 | 46 | // Wait for the manifest.json created by clientEnvironment 47 | serverEnvironment.plugins.prepend( 48 | 'WaitPlugin', 49 | new WaitPlugin('manifest.json') 50 | ) 51 | 52 | return [ 53 | clientEnvironment, 54 | serverEnvironment 55 | ] 56 | } 57 | 58 | module.exports = getSvelteEnvironments 59 | -------------------------------------------------------------------------------- /svelte_ujs/index.js: -------------------------------------------------------------------------------- 1 | class SvelteRailsUJS { 2 | static serverRender(component_name, props) { 3 | const requireComponent = require.context('components', true) 4 | const bundle = requireComponent('./' + component_name).default 5 | const {html} = bundle.render(props) 6 | 7 | return html 8 | } 9 | 10 | static start() { 11 | SvelteRailsUJS.mountComponents() 12 | 13 | document.addEventListener('DOMContentLoaded', 14 | SvelteRailsUJS.mountComponents) 15 | 16 | document.addEventListener('turbolinks:load', 17 | SvelteRailsUJS.mountComponents) 18 | } 19 | 20 | static mountComponents() { 21 | document.querySelectorAll('[data-svelte-class]:not([data-svelte-initialized])') 22 | .forEach(SvelteRailsUJS.mountComponent) 23 | } 24 | 25 | static mountComponent(target) { 26 | const name = target.dataset.svelteClass 27 | const hydrate = !!target.dataset.hydrate 28 | 29 | let props = {} 30 | 31 | if (target.dataset.svelteProps) { 32 | props = JSON.parse(target.dataset.svelteProps) 33 | } 34 | 35 | const requireComponent = require.context('components', true) 36 | const Component = requireComponent('./' + name).default 37 | 38 | console.debug(Component, {target, props, hydrate}); 39 | 40 | const component = new Component({target, props, hydrate}) 41 | 42 | target.dataset.svelteInitialized = true 43 | } 44 | } 45 | 46 | // self.SvelteRailsUJS = SvelteRailsUJS 47 | 48 | export default SvelteRailsUJS 49 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -xe 4 | export DISABLE_SPRING=true 5 | 6 | # nvm use --delete-prefix v14.5.0 7 | 8 | rails new svelte-rails-test --webpack=svelte 9 | cd svelte-rails-test 10 | 11 | echo "gem 'svelte-rails', path: '..'" >> Gemfile 12 | bundle 13 | 14 | yes | rails svelte:install 15 | 16 | sed -i 's/svelte_ujs_ng.*$/svelte_ujs_ng": "..",/g' package.json 17 | yarn 18 | 19 | # TODO This does not work in shell context, yet 20 | rails g controller greetings show 21 | 22 | cat < app/views/greetings/show.html.erb 23 | <%= svelte_component :Hello, {name: 'Test'}, {prerender: true} %> 24 | EOF 25 | 26 | # rails s -d 27 | # xdg-open http://localhost:3000/greetings/show 28 | 29 | # kill `cat tmp/pids/server.pid` 30 | 31 | rails db:migrate 32 | rails assets:precompile 33 | 34 | cat < test/system/features_test.rb 35 | require 'application_system_test_case' 36 | 37 | class FeaturesTest < ApplicationSystemTestCase 38 | test 'SSR from view and update by hydration' do 39 | visit greetings_show_url 40 | 41 | assert_selector 'h1', text: 'Hello Test! ❤' 42 | assert !find('code').text.nil? 43 | end 44 | end 45 | EOF 46 | 47 | sed -i 's/driven_by :selenium.*$/driven_by :selenium, using: :headless_chrome/g' test/application_system_test_case.rb 48 | 49 | rails test:system 50 | --------------------------------------------------------------------------------