├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── assets └── templates │ └── default.txt ├── bin ├── console ├── setup └── test │ ├── server-puma │ ├── todos-api │ ├── todos-hotwire │ └── todos-scaffold ├── examples ├── hello-world.rb ├── server-falcon-app.rb ├── server-puma-app.rb ├── stimulus-app.rb ├── todos-api.rb ├── todos-hotwire.rb └── todos-scaffold.rb ├── exe └── unirails ├── features ├── server-puma │ ├── Dockerfile │ ├── Gemfile │ ├── app.rb │ ├── app_test.rb │ └── docker-compose.yml ├── todos-api │ ├── .DS_Store │ ├── Dockerfile │ ├── Gemfile │ ├── app.rb │ ├── app_test.rb │ └── docker-compose.yml ├── todos-hotwire │ ├── Dockerfile │ ├── Gemfile │ ├── app.rb │ ├── app_test.rb │ └── docker-compose.yml └── todos-scaffold │ ├── .DS_Store │ ├── Dockerfile │ ├── Gemfile │ ├── app.rb │ ├── app_test.rb │ └── docker-compose.yml ├── lib ├── uni_rails.rb └── uni_rails │ ├── app.rb │ ├── app │ ├── css.rb │ ├── javascript.rb │ └── views.rb │ ├── helpers.rb │ ├── helpers │ ├── css_helper.rb │ └── javascript_helper.rb │ └── version.rb ├── sig └── uni_rails.rbs ├── test ├── test_helper.rb └── test_uni_rails.rb └── uni_rails.gemspec /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Ruby 9 | 10 | on: 11 | push: 12 | branches: [ main ] 13 | pull_request: 14 | branches: [ main ] 15 | 16 | jobs: 17 | unit-tests: 18 | name: Unit tests 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Set up Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: 3.3 26 | bundler-cache: true 27 | - run: bundle install 28 | - run: bundle exec rake 29 | 30 | app-tests: 31 | name: ${{ matrix.repo }} feature specs 32 | runs-on: ubuntu-latest 33 | strategy: 34 | matrix: 35 | repo: 36 | - todos-scaffold 37 | - todos-hotwire 38 | - todos-api 39 | - server-puma 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: Set up Ruby 43 | uses: ruby/setup-ruby@v1 44 | with: 45 | ruby-version: 3.3 46 | bundler-cache: true 47 | - run: bin/test/${{ matrix.repo }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | examples/*.sqlite* 10 | **/*.sqlite* 11 | log/* 12 | **/uni_rails.gem 13 | .byebug_history 14 | .ruby-version -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at barret.alx@gmail.com. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in uni_rails.gemspec 6 | gemspec 7 | 8 | gem "rake", "~> 13.0" 9 | gem "minitest", "~> 5.0" 10 | gem "rack-test" 11 | gem "debug" 12 | gem "sqlite3" 13 | gem "puma" -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | uni_rails (0.5.0) 5 | rackup (~> 2.1) 6 | rails (~> 7.2.0) 7 | turbo-rails (~> 2.0) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | actioncable (7.2.0) 13 | actionpack (= 7.2.0) 14 | activesupport (= 7.2.0) 15 | nio4r (~> 2.0) 16 | websocket-driver (>= 0.6.1) 17 | zeitwerk (~> 2.6) 18 | actionmailbox (7.2.0) 19 | actionpack (= 7.2.0) 20 | activejob (= 7.2.0) 21 | activerecord (= 7.2.0) 22 | activestorage (= 7.2.0) 23 | activesupport (= 7.2.0) 24 | mail (>= 2.8.0) 25 | actionmailer (7.2.0) 26 | actionpack (= 7.2.0) 27 | actionview (= 7.2.0) 28 | activejob (= 7.2.0) 29 | activesupport (= 7.2.0) 30 | mail (>= 2.8.0) 31 | rails-dom-testing (~> 2.2) 32 | actionpack (7.2.0) 33 | actionview (= 7.2.0) 34 | activesupport (= 7.2.0) 35 | nokogiri (>= 1.8.5) 36 | racc 37 | rack (>= 2.2.4, < 3.2) 38 | rack-session (>= 1.0.1) 39 | rack-test (>= 0.6.3) 40 | rails-dom-testing (~> 2.2) 41 | rails-html-sanitizer (~> 1.6) 42 | useragent (~> 0.16) 43 | actiontext (7.2.0) 44 | actionpack (= 7.2.0) 45 | activerecord (= 7.2.0) 46 | activestorage (= 7.2.0) 47 | activesupport (= 7.2.0) 48 | globalid (>= 0.6.0) 49 | nokogiri (>= 1.8.5) 50 | actionview (7.2.0) 51 | activesupport (= 7.2.0) 52 | builder (~> 3.1) 53 | erubi (~> 1.11) 54 | rails-dom-testing (~> 2.2) 55 | rails-html-sanitizer (~> 1.6) 56 | activejob (7.2.0) 57 | activesupport (= 7.2.0) 58 | globalid (>= 0.3.6) 59 | activemodel (7.2.0) 60 | activesupport (= 7.2.0) 61 | activerecord (7.2.0) 62 | activemodel (= 7.2.0) 63 | activesupport (= 7.2.0) 64 | timeout (>= 0.4.0) 65 | activestorage (7.2.0) 66 | actionpack (= 7.2.0) 67 | activejob (= 7.2.0) 68 | activerecord (= 7.2.0) 69 | activesupport (= 7.2.0) 70 | marcel (~> 1.0) 71 | activesupport (7.2.0) 72 | base64 73 | bigdecimal 74 | concurrent-ruby (~> 1.0, >= 1.3.1) 75 | connection_pool (>= 2.2.5) 76 | drb 77 | i18n (>= 1.6, < 2) 78 | logger (>= 1.4.2) 79 | minitest (>= 5.1) 80 | securerandom (>= 0.3) 81 | tzinfo (~> 2.0, >= 2.0.5) 82 | base64 (0.2.0) 83 | bigdecimal (3.1.7) 84 | builder (3.2.4) 85 | concurrent-ruby (1.3.4) 86 | connection_pool (2.4.1) 87 | crass (1.0.6) 88 | date (3.3.4) 89 | debug (1.9.2) 90 | irb (~> 1.10) 91 | reline (>= 0.3.8) 92 | drb (2.2.1) 93 | erubi (1.12.0) 94 | globalid (1.2.1) 95 | activesupport (>= 6.1) 96 | i18n (1.14.4) 97 | concurrent-ruby (~> 1.0) 98 | io-console (0.7.2) 99 | irb (1.14.0) 100 | rdoc (>= 4.0.0) 101 | reline (>= 0.4.2) 102 | logger (1.6.0) 103 | loofah (2.22.0) 104 | crass (~> 1.0.2) 105 | nokogiri (>= 1.12.0) 106 | mail (2.8.1) 107 | mini_mime (>= 0.1.1) 108 | net-imap 109 | net-pop 110 | net-smtp 111 | marcel (1.0.4) 112 | mini_mime (1.1.5) 113 | minitest (5.22.3) 114 | net-imap (0.4.10) 115 | date 116 | net-protocol 117 | net-pop (0.1.2) 118 | net-protocol 119 | net-protocol (0.2.2) 120 | timeout 121 | net-smtp (0.5.0) 122 | net-protocol 123 | nio4r (2.7.1) 124 | nokogiri (1.16.3-arm64-darwin) 125 | racc (~> 1.4) 126 | nokogiri (1.16.3-x86_64-darwin) 127 | racc (~> 1.4) 128 | nokogiri (1.16.3-x86_64-linux) 129 | racc (~> 1.4) 130 | psych (5.1.2) 131 | stringio 132 | puma (6.4.2) 133 | nio4r (~> 2.0) 134 | racc (1.7.3) 135 | rack (3.0.10) 136 | rack-session (2.0.0) 137 | rack (>= 3.0.0) 138 | rack-test (2.1.0) 139 | rack (>= 1.3) 140 | rackup (2.1.0) 141 | rack (>= 3) 142 | webrick (~> 1.8) 143 | rails (7.2.0) 144 | actioncable (= 7.2.0) 145 | actionmailbox (= 7.2.0) 146 | actionmailer (= 7.2.0) 147 | actionpack (= 7.2.0) 148 | actiontext (= 7.2.0) 149 | actionview (= 7.2.0) 150 | activejob (= 7.2.0) 151 | activemodel (= 7.2.0) 152 | activerecord (= 7.2.0) 153 | activestorage (= 7.2.0) 154 | activesupport (= 7.2.0) 155 | bundler (>= 1.15.0) 156 | railties (= 7.2.0) 157 | rails-dom-testing (2.2.0) 158 | activesupport (>= 5.0.0) 159 | minitest 160 | nokogiri (>= 1.6) 161 | rails-html-sanitizer (1.6.0) 162 | loofah (~> 2.21) 163 | nokogiri (~> 1.14) 164 | railties (7.2.0) 165 | actionpack (= 7.2.0) 166 | activesupport (= 7.2.0) 167 | irb (~> 1.13) 168 | rackup (>= 1.0.0) 169 | rake (>= 12.2) 170 | thor (~> 1.0, >= 1.2.2) 171 | zeitwerk (~> 2.6) 172 | rake (13.1.0) 173 | rdoc (6.6.3.1) 174 | psych (>= 4.0.0) 175 | reline (0.5.0) 176 | io-console (~> 0.5) 177 | securerandom (0.3.1) 178 | sqlite3 (1.7.3-arm64-darwin) 179 | sqlite3 (1.7.3-x86_64-darwin) 180 | sqlite3 (1.7.3-x86_64-linux) 181 | stringio (3.1.0) 182 | thor (1.3.1) 183 | timeout (0.4.1) 184 | turbo-rails (2.0.5) 185 | actionpack (>= 6.0.0) 186 | activejob (>= 6.0.0) 187 | railties (>= 6.0.0) 188 | tzinfo (2.0.6) 189 | concurrent-ruby (~> 1.0) 190 | useragent (0.16.10) 191 | webrick (1.8.1) 192 | websocket-driver (0.7.6) 193 | websocket-extensions (>= 0.1.0) 194 | websocket-extensions (0.1.5) 195 | zeitwerk (2.6.13) 196 | 197 | PLATFORMS 198 | arm64-darwin-23 199 | x86_64-darwin-21 200 | x86_64-darwin-23 201 | x86_64-linux 202 | 203 | DEPENDENCIES 204 | debug 205 | minitest (~> 5.0) 206 | puma 207 | rack-test 208 | rake (~> 13.0) 209 | sqlite3 210 | uni_rails! 211 | 212 | BUNDLED WITH 213 | 2.4.22 214 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Alexandre Barret 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 | # UniRails 2 | 3 | UniRails makes it easy to build Ruby on Rails apps all within one Ruby file. It's perfect for small personal projects or teaching, as everything is one scroll away. 4 | 5 | We aim to help educators and writers create clear examples for articles or books. Rails requires a full folder structure to show the basics, which can be a hassle. UniRails cuts through that by letting you spin up a complete Rails app from just one Ruby file — everything you need in one place. 6 | 7 | Check out our [examples](/examples) to understand how the library creates Rails apps from a single file. 8 | 9 | ## Installation & Usage 10 | 11 | See some examples of how the UniRails library can be used. Running an example is a easy as `ruby filename.rb` 12 | 13 | - [Hello world](/examples/hello-world.rb) 14 | - [Todos app (JSON API)](/examples/todos-api.rb) 15 | - [Todos app (Rails scaffold)](/examples/todos-scaffold.rb) based off `bin/rails g scaffold todo name completed_at:datetime` 16 | - [Todos app (Hotwire)](/examples/todos-hotwire.rb) based off online article: [turbo-rails-101-todo-list](https://www.colby.so/posts/turbo-rails-101-todo-list) by David Colby 17 | - [App using StimulusJS](/examples/stimulus-app.rb) 18 | - [App using Puma server](/examples/server-puma-app.rb) 19 | - [App using Falcon server](/examples/server-falcon-app.rb) 20 | 21 | ## Development 22 | 23 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 24 | 25 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). 26 | 27 | ## Contributing 28 | 29 | Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/uni_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/[USERNAME]/uni_rails/blob/main/CODE_OF_CONDUCT.md). 30 | 31 | ### Roadmap 32 | 33 | We would like to support all railties and engines. Please help us support more of them 34 | 35 | - [X] action_controller 36 | - [X] active_record 37 | - [X] active_model/railtie 38 | - [ ] active_job/railtie 39 | - [ ] active_storage/engine 40 | - [ ] action_mailer/railtie 41 | - [ ] action_mailbox/engine 42 | - [ ] action_text/engine 43 | - [ ] action_view/railtie 44 | - [ ] action_cable/engine 45 | 46 | 47 | ## License 48 | 49 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 50 | 51 | ## Code of Conduct 52 | 53 | Everyone interacting in the UniRails project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/uni_rails/blob/main/CODE_OF_CONDUCT.md). 54 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/testtask" 5 | 6 | Rake::TestTask.new(:test) do |t| 7 | t.libs << "test" 8 | t.libs << "lib" 9 | t.test_files = FileList["test/**/test_*.rb"] 10 | end 11 | 12 | task default: :test 13 | -------------------------------------------------------------------------------- /assets/templates/default.txt: -------------------------------------------------------------------------------- 1 | ENV["SECRET_KEY_BASE"] = "1212312313" 2 | ENV["DATABASE_URL"] = "sqlite3:///#{__dir__}/database.sqlite" 3 | 4 | require "bundler/inline" 5 | 6 | gemfile do 7 | source "https://www.rubygems.org" 8 | gem "uni_rails", "~> 0.4.0" 9 | gem "sqlite3", "~> 1.7" 10 | gem "byebug" 11 | end 12 | 13 | require "uni_rails" 14 | require "sqlite3" 15 | require "byebug" 16 | 17 | # ==== ROUTES ==== 18 | UniRails.routes do 19 | root "resource_names#new" 20 | resources :resource_names 21 | end 22 | 23 | # ==== DB SCHEMA ==== 24 | ActiveRecord::Base.establish_connection 25 | ActiveRecord::Schema.define do 26 | create_table :resources, force: :cascade do |t| 27 | t.string :title 28 | end 29 | end 30 | 31 | # ==== MODELS ==== 32 | class Resource < ActiveRecord::Base 33 | end 34 | 35 | # ==== SEEDS ==== 36 | # Create your existing records here 37 | 38 | # ==== CONTROLLERS ==== 39 | class ResourceNamesController < ActionController::Base 40 | layout 'application' 41 | 42 | def new 43 | end 44 | 45 | def create 46 | end 47 | end 48 | 49 | # ==== IMPORT MAPS ==== 50 | UniRails.import_maps( 51 | 'stimulus' => 'https://unpkg.com/@hotwired/stimulus/dist/stimulus.js' 52 | ) 53 | 54 | 55 | # ==== JAVASCRTIP ==== 56 | UniRails.javascript <<~JAVASCRIPT 57 | import { Application, Controller } from "stimulus" 58 | window.Stimulus = Application.start() 59 | 60 | Stimulus.register("hello", class extends Controller { 61 | connect() { 62 | console.log("hello world") 63 | } 64 | }) 65 | 66 | JAVASCRIPT 67 | 68 | # ==== CSS ==== 69 | UniRails.css <<~CSS 70 | html { background-color:#EEE; } 71 | body { width:500px; height:700px; margin:auto; 72 | background-color:white; padding:1rem; 73 | } 74 | form { 75 | label { display: block; } 76 | input[type="submit"] { display: block; margin-top:1rem; } 77 | .field_with_errors { color:red; display:inline; } 78 | } 79 | CSS 80 | 81 | # ==== VIEWS ==== 82 | UniRails.register_view "resource_names/new.html.erb", <<~HTML 83 | HTML 84 | 85 | UniRails.register_view "resource_names/show.html.erb", <<~HTML 86 | HTML 87 | 88 | UniRails.run(Port: 3000) 89 | # UniRails.run(Host: '0.0.0.0', Port: 3000) # when run in a docker container -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "uni_rails" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | require "irb" 11 | IRB.start(__FILE__) 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /bin/test/server-puma: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bundle install 4 | bundle exec rake build 5 | ls -t pkg | head -n1 | xargs -I {} mv pkg/{} features/server-puma/uni_rails.gem 6 | docker compose -f features/server-puma/docker-compose.yml up --build --exit-code-from test -------------------------------------------------------------------------------- /bin/test/todos-api: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bundle install 4 | bundle exec rake build 5 | ls -t pkg | head -n1 | xargs -I {} mv pkg/{} features/todos-api/uni_rails.gem 6 | docker compose -f features/todos-api/docker-compose.yml up --build --exit-code-from test -------------------------------------------------------------------------------- /bin/test/todos-hotwire: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bundle install 4 | bundle exec rake build 5 | ls -t pkg | head -n1 | xargs -I {} mv pkg/{} features/todos-hotwire/uni_rails.gem 6 | docker compose -f features/todos-hotwire/docker-compose.yml up --build --exit-code-from test -------------------------------------------------------------------------------- /bin/test/todos-scaffold: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bundle install 4 | bundle exec rake build 5 | ls -t pkg | head -n1 | xargs -I {} mv pkg/{} features/todos-scaffold/uni_rails.gem 6 | docker compose -f features/todos-scaffold/docker-compose.yml up --build --exit-code-from test -------------------------------------------------------------------------------- /examples/hello-world.rb: -------------------------------------------------------------------------------- 1 | 2 | ENV['SECRET_KEY_BASE'] = 'my_secret_key_base' 3 | 4 | require "bundler/inline" 5 | 6 | gemfile(true) do 7 | source "https://rubygems.org" 8 | gem 'uni_rails', '~> 0.5.0' 9 | end 10 | 11 | require 'uni_rails' 12 | 13 | UniRails.routes do 14 | root 'pages#index' 15 | end 16 | 17 | class PagesController < ActionController::Base 18 | def index 19 | render plain: 'Hello world' 20 | end 21 | end 22 | 23 | UniRails.run(Port: 3000) 24 | -------------------------------------------------------------------------------- /examples/server-falcon-app.rb: -------------------------------------------------------------------------------- 1 | 2 | ENV['SECRET_KEY_BASE'] = 'my_secret_key_base' 3 | 4 | require "bundler/inline" 5 | 6 | gemfile(true) do 7 | source "https://rubygems.org" 8 | 9 | gem 'uni_rails', '~> 0.5.0' 10 | gem 'falcon' 11 | end 12 | 13 | require 'uni_rails' 14 | require 'rackup/handler/falcon' 15 | 16 | UniRails.rackup_handler = Rackup::Handler::Falcon 17 | 18 | UniRails.routes do 19 | root 'pages#index' 20 | end 21 | 22 | class PagesController < ActionController::Base 23 | def index 24 | render plain: 'Application served by Falcon' 25 | end 26 | end 27 | 28 | UniRails.run(Port: 3000) 29 | -------------------------------------------------------------------------------- /examples/server-puma-app.rb: -------------------------------------------------------------------------------- 1 | 2 | ENV['SECRET_KEY_BASE'] = 'my_secret_key_base' 3 | 4 | require "bundler/inline" 5 | 6 | gemfile(true) do 7 | source "https://rubygems.org" 8 | 9 | gem 'uni_rails', '~> 0.5.0' 10 | gem 'puma' 11 | end 12 | 13 | require 'uni_rails' 14 | require 'rack/handler/puma' 15 | 16 | UniRails.rackup_handler = Rack::Handler::Puma 17 | 18 | UniRails.routes do 19 | root 'pages#index' 20 | end 21 | 22 | class PagesController < ActionController::Base 23 | def index 24 | render plain: 'Application served by Puma' 25 | end 26 | end 27 | 28 | UniRails.run(Port: 3000) 29 | -------------------------------------------------------------------------------- /examples/stimulus-app.rb: -------------------------------------------------------------------------------- 1 | 2 | ENV['SECRET_KEY_BASE'] = 'my_secret_key_base' 3 | 4 | require "bundler/inline" 5 | 6 | gemfile(true) do 7 | source "https://rubygems.org" 8 | gem 'uni_rails', '~> 0.5.0' 9 | end 10 | 11 | require 'uni_rails' 12 | 13 | UniRails.routes do 14 | root 'pages#index' 15 | end 16 | 17 | UniRails.import_maps( 18 | 'stimulus' => 'https://unpkg.com/@hotwired/stimulus/dist/stimulus.js' 19 | ) 20 | 21 | UniRails.javascript <<~JAVASCRIPT 22 | import { Application, Controller } from "stimulus" 23 | window.Stimulus = Application.start() 24 | 25 | Stimulus.register("hello", class extends Controller { 26 | connect() { 27 | console.log("hello world") 28 | } 29 | }) 30 | JAVASCRIPT 31 | 32 | class PagesController < ActionController::Base 33 | layout 'application' 34 | def index;end 35 | end 36 | 37 | UniRails.register_view "pages/index.html.erb", <<~HTML 38 |
Check out the dev console to see "hello world"
39 | HTML 40 | 41 | UniRails.run(Port: 3000) 42 | -------------------------------------------------------------------------------- /examples/todos-api.rb: -------------------------------------------------------------------------------- 1 | 2 | ENV['SECRET_KEY_BASE'] = 'my_secret_key_base' 3 | ENV['DATABASE_URL'] = "sqlite3:///#{Dir.pwd}/todos-api.sqlite" 4 | 5 | require "bundler/inline" 6 | 7 | gemfile(true) do 8 | source "https://rubygems.org" 9 | 10 | gem 'uni_rails', '~> 0.5.0' 11 | gem 'sqlite3', '~> 1.7' 12 | end 13 | 14 | require 'uni_rails' 15 | require 'sqlite3' 16 | 17 | ActiveRecord::Base.establish_connection 18 | ActiveRecord::Schema.define do 19 | create_table :todos, force: :cascade do |t| 20 | t.string :name 21 | t.datetime :completed_at 22 | t.timestamps 23 | end 24 | end 25 | 26 | UniRails.routes do 27 | resources :todos do 28 | put :complete, on: :member 29 | end 30 | end 31 | 32 | # MODELS 33 | 34 | class Todo < ActiveRecord::Base 35 | validates :name, presence: true 36 | 37 | def complete(at: Time.zone.now) 38 | update(completed_at: at) 39 | end 40 | end 41 | 42 | # CONTROLLERS 43 | 44 | class ApplicationController < ActionController::Base 45 | protect_from_forgery with: :null_session 46 | end 47 | 48 | class TodosController < ApplicationController 49 | before_action :set_todo, only: [:show, :update, :destroy, :complete] 50 | 51 | def index 52 | @todos = Todo.all 53 | render json: @todos 54 | end 55 | 56 | def show 57 | render json: @todo 58 | end 59 | 60 | def create 61 | @todo = Todo.new(todo_params) 62 | if @todo.save 63 | render json: @todo, status: :created, location: @todo 64 | else 65 | render json: @todo.errors, status: :unprocessable_entity 66 | end 67 | end 68 | 69 | def update 70 | if @todo.update(todo_params) 71 | render json: @todo 72 | else 73 | render json: @todo.errors, status: :unprocessable_entity 74 | end 75 | end 76 | 77 | def complete 78 | if @todo.complete 79 | render json: @todo 80 | else 81 | render json: @todo.errors, status: :unprocessable_entity 82 | end 83 | end 84 | 85 | def destroy 86 | @todo.destroy 87 | head :no_content 88 | end 89 | 90 | private 91 | 92 | def set_todo 93 | @todo = Todo.find(params[:id]) 94 | end 95 | 96 | def todo_params 97 | params.require(:todo).permit(:name, :status) 98 | end 99 | end 100 | 101 | UniRails.run(Port: 3000) 102 | -------------------------------------------------------------------------------- /examples/todos-hotwire.rb: -------------------------------------------------------------------------------- 1 | # Based on the Hotwire tutorial: https://www.colby.so/posts/turbo-rails-101-todo-list 2 | 3 | ENV['SECRET_KEY_BASE'] = 'my_secret_key_base' 4 | ENV['DATABASE_URL'] = "sqlite3:///#{Dir.pwd}/todos-hotwire.sqlite" 5 | 6 | require "bundler/inline" 7 | 8 | gemfile(true) do 9 | source "https://rubygems.org" 10 | 11 | gem 'uni_rails', '~> 0.5.0' 12 | gem 'sqlite3', '~> 1.7' 13 | gem 'debug' 14 | end 15 | 16 | require 'uni_rails' 17 | require 'sqlite3' 18 | 19 | ActiveRecord::Base.establish_connection 20 | ActiveRecord::Schema.define do 21 | create_table :todos, force: :cascade do |t| 22 | t.string :name 23 | t.integer :status, default: 0 24 | t.timestamps 25 | end 26 | end 27 | 28 | UniRails.enable_turbo_rails! 29 | 30 | UniRails.routes do 31 | root 'todos#index' 32 | resources :todos do 33 | patch :change_status, on: :member 34 | end 35 | end 36 | 37 | # MODELS 38 | 39 | class Todo < ActiveRecord::Base 40 | enum status: { 41 | incomplete: 0, 42 | complete: 1 43 | } 44 | 45 | validates :name, presence: true 46 | end 47 | 48 | 49 | # CONTROLLERS 50 | 51 | class TodosController < ActionController::Base 52 | layout 'application' 53 | 54 | before_action :set_todo, only: [:edit, :update, :destroy, :change_status] 55 | 56 | def index 57 | @todos = Todo.where(status: params[:status].presence || 'incomplete') 58 | end 59 | 60 | def new 61 | @todo = Todo.new 62 | end 63 | 64 | def edit 65 | @todo = Todo.find(params[:id]) 66 | end 67 | 68 | def create 69 | @todo = Todo.new(todo_params) 70 | 71 | respond_to do |format| 72 | if @todo.save 73 | format.turbo_stream 74 | format.html { redirect_to todo_url(@todo), notice: "Todo was successfully created." } 75 | else 76 | format.turbo_stream { render turbo_stream: turbo_stream.replace("#{helpers.dom_id(@todo)}_form", partial: "form", locals: { todo: @todo }) } 77 | format.html { render :new, status: :unprocessable_entity } 78 | end 79 | end 80 | end 81 | 82 | def update 83 | respond_to do |format| 84 | if @todo.update(todo_params) 85 | format.turbo_stream 86 | format.html { redirect_to todo_url(@todo), notice: "Todo was successfully updated." } 87 | format.json { render :show, status: :ok, location: @todo } 88 | else 89 | format.turbo_stream { render turbo_stream: turbo_stream.replace("#{helpers.dom_id(@todo)}_form", partial: "form", locals: { todo: @todo }) } 90 | format.html { render :edit, status: :unprocessable_entity } 91 | format.json { render json: @todo.errors, status: :unprocessable_entity } 92 | end 93 | end 94 | end 95 | 96 | def destroy 97 | @todo.destroy 98 | 99 | respond_to do |format| 100 | format.turbo_stream { render turbo_stream: turbo_stream.remove("#{helpers.dom_id(@todo)}_container") } 101 | format.html { redirect_to todos_url, notice: "Todo was successfully destroyed." } 102 | format.json { head :no_content } 103 | end 104 | end 105 | 106 | def change_status 107 | @todo.update(status: todo_params[:status]) 108 | respond_to do |format| 109 | format.turbo_stream { render turbo_stream: turbo_stream.remove("#{helpers.dom_id(@todo)}_container") } 110 | format.html { redirect_to todos_path, notice: "Updated todo status." } 111 | end 112 | end 113 | 114 | private 115 | 116 | def set_todo 117 | @todo = Todo.find(params[:id]) 118 | end 119 | 120 | def todo_params 121 | params.require(:todo).permit(:name, :status) 122 | end 123 | end 124 | 125 | # VIEWS 126 | 127 | UniRails.javascript <<~JAVASCRIPT 128 | import * as Turbo from "turbo" 129 | JAVASCRIPT 130 | 131 | UniRails.register_view "layouts/application.html.erb", <<~'HTML' 132 | 133 | 134 | 135 | Template 136 | 137 | <%= csrf_meta_tags %> 138 | <%= csp_meta_tag %> 139 | 140 | <%= uni_rails_css_stylesheet %> 141 | <%= uni_rails_import_map_tag %> 142 | <%= uni_rails_javascript_script %> 143 | 144 | 145 | 146 | <%= yield %> 147 | 148 | 149 | HTML 150 | 151 | UniRails.register_view "todos/index.html.erb", <<~'HTML' 152 |
153 |

154 | Your todos 155 |

156 | <%= turbo_frame_tag "todos-container", class: "block max-w-2xl w-full bg-gray-100 py-8 px-4 border border-gray-200 rounded shadow-sm" do %> 157 |
158 | 171 |
172 | <% unless params[:status] == "complete" %> 173 |
174 | <%= render "form", todo: Todo.new %> 175 |
176 | <% end %> 177 | 180 | <% end %> 181 |
182 | HTML 183 | 184 | UniRails.register_view "todos/_form.html.erb", <<~'HTML' 185 | <%= form_with(model: todo, id: "#{dom_id(todo)}_form") do |form| %> 186 | <% if todo.errors.any? %> 187 |
188 |

<%= pluralize(todo.errors.count, "error") %> prohibited this todo from being saved:

189 | 190 | 195 |
196 | <% end %> 197 |
198 | <%= form.label :name, class: "sr-only" %> 199 | <%= form.text_field :name, class: "block w-full rounded-none rounded-l-md sm:text-sm border-gray-300", placeholder: "Add a new todo..." %> 200 | <%= form.submit class: "-ml-px relative px-4 py-2 border border-blue-600 text-sm font-medium rounded-r-md text-white bg-blue-600 hover:bg-blue-700" %> 201 |
202 | <% end %> 203 | HTML 204 | 205 | UniRails.register_view "todos/_todo.html.erb", <<~'HTML' 206 |
  • " class="py-2 px-4 border-b border-gray-300"> 207 | <%= turbo_frame_tag dom_id(todo) do %> 208 |
    209 | <%= link_to todo.name, edit_todo_path(todo), class: todo.complete? ? 'line-through' : '' %> 210 | 211 |
    212 | <% if todo.complete? %> 213 | <%= button_to change_status_todo_path(todo, todo: { status: 'incomplete' }), class: "bg-green-600 px-4 py-2 rounded hover:bg-green-700", method: :patch do %> 214 | Mark as incomplete 215 | 216 | 217 | 218 | <% end %> 219 | <% else %> 220 | <%= button_to change_status_todo_path(todo, todo: { status: 'complete' }), class: "bg-gray-400 px-4 py-2 rounded hover:bg-green-700", method: :patch do %> 221 | Mark as complete 222 | 223 | 224 | 225 | <% end %> 226 | <% end %> 227 | 228 | <%= button_to todo_path(todo), class: "bg-red-600 px-4 py-2 rounded hover:bg-red-700", method: :delete do %> 229 | Delete 230 | 231 | 232 | 233 | <% end %> 234 |
    235 |
    236 | <% end %> 237 |
  • 238 | HTML 239 | 240 | UniRails.register_view "todos/edit.html.erb", <<~'HTML' 241 | <%= turbo_frame_tag dom_id(@todo) do %> 242 | <%= render "form", todo: @todo %> 243 | <% end %> 244 | HTML 245 | 246 | UniRails.register_view "todos/create.turbo_stream.erb", <<~'HTML' 247 | <%= turbo_stream.prepend "todos" do %> 248 | <%= render "todo", todo: @todo %> 249 | <% end %> 250 | <%= turbo_stream.replace "#{dom_id(Todo.new)}_form" do %> 251 | <%= render "form", todo: Todo.new %> 252 | <% end %> 253 | HTML 254 | 255 | UniRails.register_view "todos/update.turbo_stream.erb", <<~'HTML' 256 | <%= turbo_stream.replace "#{dom_id(@todo)}_container" do %> 257 | <%= render "todo", todo: @todo %> 258 | <% end %> 259 | HTML 260 | 261 | UniRails.run(Port: 3000) 262 | -------------------------------------------------------------------------------- /examples/todos-scaffold.rb: -------------------------------------------------------------------------------- 1 | 2 | ENV['SECRET_KEY_BASE'] = 'my_secret_key_base' 3 | ENV['DATABASE_URL'] = "sqlite3:///#{Dir.pwd}/todos-scaffold.sqlite" 4 | 5 | require "bundler/inline" 6 | 7 | gemfile(true) do 8 | source "https://rubygems.org" 9 | 10 | gem 'uni_rails', '~> 0.5.0' 11 | gem 'sqlite3', '~> 1.7' 12 | gem 'jbuilder' # jbuilder allows the .jbuilder extension for views 13 | end 14 | 15 | require 'uni_rails' 16 | require 'sqlite3' 17 | 18 | ActiveRecord::Base.establish_connection 19 | ActiveRecord::Schema.define do 20 | create_table :todos, force: :cascade do |t| 21 | t.string :name 22 | t.datetime :completed_at 23 | t.timestamps 24 | end 25 | end 26 | 27 | UniRails.routes do 28 | root 'todos#index' 29 | resources :todos 30 | end 31 | 32 | # MODELS 33 | 34 | class Todo < ActiveRecord::Base 35 | validates :name, presence: true 36 | 37 | def complete(at: Time.zone.now) 38 | update(completed_at: at) 39 | end 40 | end 41 | 42 | 43 | # CONTROLLERS 44 | class ApplicationController < ActionController::Base 45 | end 46 | 47 | class TodosController < ApplicationController 48 | before_action :set_todo, only: %i[ show edit update destroy ] 49 | 50 | # GET /todos or /todos.json 51 | def index 52 | @todos = Todo.all 53 | end 54 | 55 | # GET /todos/1 or /todos/1.json 56 | def show 57 | end 58 | 59 | # GET /todos/new 60 | def new 61 | @todo = Todo.new 62 | end 63 | 64 | # GET /todos/1/edit 65 | def edit 66 | end 67 | 68 | # POST /todos or /todos.json 69 | def create 70 | @todo = Todo.new(todo_params) 71 | 72 | respond_to do |format| 73 | if @todo.save 74 | format.html { redirect_to todo_url(@todo), notice: "Todo was successfully created." } 75 | format.json { render :show, status: :created, location: @todo } 76 | else 77 | format.html { render :new, status: :unprocessable_entity } 78 | format.json { render json: @todo.errors, status: :unprocessable_entity } 79 | end 80 | end 81 | end 82 | 83 | # PATCH/PUT /todos/1 or /todos/1.json 84 | def update 85 | respond_to do |format| 86 | if @todo.update(todo_params) 87 | format.html { redirect_to todo_url(@todo), notice: "Todo was successfully updated." } 88 | format.json { render :show, status: :ok, location: @todo } 89 | else 90 | format.html { render :edit, status: :unprocessable_entity } 91 | format.json { render json: @todo.errors, status: :unprocessable_entity } 92 | end 93 | end 94 | end 95 | 96 | # DELETE /todos/1 or /todos/1.json 97 | def destroy 98 | @todo.destroy! 99 | 100 | respond_to do |format| 101 | format.html { redirect_to todos_url, notice: "Todo was successfully destroyed." } 102 | format.json { head :no_content } 103 | end 104 | end 105 | 106 | private 107 | # Use callbacks to share common setup or constraints between actions. 108 | def set_todo 109 | @todo = Todo.find(params[:id]) 110 | end 111 | 112 | # Only allow a list of trusted parameters through. 113 | def todo_params 114 | params.require(:todo).permit(:name, :completed_at) 115 | end 116 | end 117 | 118 | # VIEWS - HTML 119 | 120 | UniRails.register_view "todos/_form.html.erb", <<~HTML 121 | <%= form_with(model: todo) do |form| %> 122 | <% if todo.errors.any? %> 123 |
    124 |

    <%= pluralize(todo.errors.count, "error") %> prohibited this todo from being saved:

    125 | 126 | 131 |
    132 | <% end %> 133 | 134 |
    135 | <%= form.label :name, style: "display: block" %> 136 | <%= form.text_field :name %> 137 |
    138 | 139 |
    140 | <%= form.label :completed_at, style: "display: block" %> 141 | <%= form.datetime_field :completed_at %> 142 |
    143 | 144 |
    145 | <%= form.submit %> 146 |
    147 | <% end %> 148 | HTML 149 | 150 | UniRails.register_view "todos/_todo.html.erb", <<~HTML 151 |
    152 |

    153 | Name: 154 | <%= todo.name %> 155 |

    156 | 157 |

    158 | Completed at: 159 | <%= todo.completed_at %> 160 |

    161 | 162 |
    163 | HTML 164 | 165 | UniRails.register_view "todos/edit.html.erb", <<~HTML 166 |

    Editing todo

    167 | 168 | <%= render "form", todo: @todo %> 169 | 170 |
    171 | 172 |
    173 | <%= link_to "Show this todo", @todo %> | 174 | <%= link_to "Back to todos", todos_path %> 175 |
    176 | HTML 177 | 178 | UniRails.register_view "todos/index.html.erb", <<~HTML 179 |

    <%= notice %>

    180 | 181 |

    Todos

    182 | 183 |
    184 | <% @todos.each do |todo| %> 185 | <%= render todo %> 186 |

    187 | <%= link_to "Show this todo", todo %> 188 |

    189 | <% end %> 190 |
    191 | 192 | <%= link_to "New todo", new_todo_path %> 193 | HTML 194 | 195 | UniRails.register_view "todos/new.html.erb", <<~HTML 196 |

    New todo

    197 | 198 | <%= render "form", todo: @todo %> 199 | 200 |
    201 | 202 |
    203 | <%= link_to "Back to todos", todos_path %> 204 |
    205 | HTML 206 | 207 | UniRails.register_view "todos/show.html.erb", <<~HTML 208 |

    <%= notice %>

    209 | 210 | <%= render @todo %> 211 | 212 |
    213 | <%= link_to "Edit this todo", edit_todo_path(@todo) %> | 214 | <%= link_to "Back to todos", todos_path %> 215 | 216 | <%= button_to "Destroy this todo", @todo, method: :delete %> 217 |
    218 | HTML 219 | 220 | # VIEWS - JSON 221 | 222 | UniRails.register_view "todos/_todo.json.jbuilder", <<~RUBY 223 | json.extract! todo, :id, :name, :completed_at, :created_at, :updated_at 224 | json.url todo_url(todo, format: :json) 225 | RUBY 226 | 227 | UniRails.register_view "todos/index.json.jbuilder", <<~RUBY 228 | json.array! @todos, partial: "todos/todo", as: :todo 229 | RUBY 230 | 231 | UniRails.register_view "todos/show.json.jbuilder", <<~RUBY 232 | json.partial! "todos/todo", todo: @todo 233 | RUBY 234 | 235 | UniRails.run(Port: 3000) 236 | -------------------------------------------------------------------------------- /exe/unirails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # require "byebug" 4 | # byebug 5 | 6 | assets_path = File.expand_path('../assets', __dir__) 7 | 8 | options = {} 9 | case ARGV[0] 10 | when "new" 11 | filename = ARGV[1] 12 | File.open(filename, 'wb+') do |f| 13 | f.write File.read File.join(assets_path, 'templates', 'default.txt') 14 | end 15 | else 16 | puts "unknown command" 17 | puts "Usage: unirails new " 18 | end 19 | -------------------------------------------------------------------------------- /features/server-puma/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.3.0-slim-bullseye 2 | 3 | ARG BUILD_PACKAGES="build-essential git curl libvips pkg-config sqlite3 libsqlite3-dev" 4 | 5 | # Install packages needed to build gems 6 | RUN apt-get update -qq && \ 7 | apt-get install --no-install-recommends -y $BUILD_PACKAGES 8 | 9 | WORKDIR /usr/src/app 10 | 11 | ENV LANG C.UTF-8 12 | ENV BUNDLER_VERSION 2.1 13 | ENV GEM_HOME="/usr/local/bundle" 14 | ENV PATH $GEM_HOME/bin:$GEM_HOME/gems/bin:$PATH 15 | 16 | COPY Gemfile uni_rails.gem ./ 17 | RUN gem install uni_rails.gem 18 | RUN bundle install 19 | 20 | COPY . /usr/src/app 21 | 22 | CMD ["ruby", "app.rb"] -------------------------------------------------------------------------------- /features/server-puma/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem 'uni_rails', path: '/usr/local/bundle' 6 | gem 'puma' 7 | 8 | # Test 9 | gem "capybara", "~> 3.40" 10 | gem "selenium-webdriver", "~> 4.19" 11 | gem "minitest" 12 | gem "debug" 13 | -------------------------------------------------------------------------------- /features/server-puma/app.rb: -------------------------------------------------------------------------------- 1 | require 'uni_rails' 2 | require 'rack/handler/puma' 3 | 4 | UniRails.rackup_handler = Rack::Handler::Puma 5 | 6 | UniRails.routes do 7 | root 'pages#index' 8 | end 9 | 10 | class PagesController < ActionController::Base 11 | def index 12 | render plain: 'Application served by Puma' 13 | end 14 | end 15 | 16 | UniRails::App.configure do 17 | config.log_level = :error 18 | config.hosts << ENV['APP_HOST'] 19 | end 20 | 21 | UniRails.run(Port: 3000, BindAddress: '0.0.0.0') 22 | -------------------------------------------------------------------------------- /features/server-puma/app_test.rb: -------------------------------------------------------------------------------- 1 | require "capybara" 2 | require "capybara/dsl" 3 | require 'capybara/minitest' 4 | require "selenium-webdriver" 5 | require "minitest/autorun" 6 | require "debug" 7 | 8 | APP_HOST = ENV.fetch('APP_HOST') 9 | 10 | Capybara.register_driver :selenium_remote_chrome do |app| 11 | options = Selenium::WebDriver::Chrome::Options.new 12 | # Add any additional options you need 13 | options.add_argument('--headless') # Uncomment if you want to run headless mode 14 | 15 | Capybara::Selenium::Driver.new(app, 16 | browser: :remote, 17 | url: "http://selenium:4444/wd/hub", 18 | options: options 19 | ) 20 | end 21 | 22 | Capybara.default_driver = :selenium_remote_chrome 23 | Capybara.app_host = APP_HOST 24 | 25 | class TestHTMLTodos < Minitest::Test 26 | include Capybara::DSL 27 | include Capybara::Minitest::Assertions 28 | 29 | def teardown 30 | Capybara.reset_sessions! 31 | Capybara.use_default_driver 32 | end 33 | 34 | def test_create_a_new_todo 35 | visit "/" 36 | assert_text 'Application served by Puma' 37 | end 38 | end 39 | 40 | -------------------------------------------------------------------------------- /features/server-puma/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | uni_rails: 3 | build: . 4 | ports: 5 | - 127.0.0.1:3000:3000 6 | volumes: 7 | - .:/usr/src/app 8 | environment: 9 | - APP_HOST=uni_rails:3000 10 | - SECRET_KEY_BASE=whatever 11 | healthcheck: 12 | test: ["CMD-SHELL", "curl -f http://localhost:3000 || exit 1"] 13 | interval: 3s 14 | timeout: 5s 15 | retries: 3 16 | start_period: 5s 17 | command: ruby app.rb 18 | test: 19 | build: . 20 | volumes: 21 | - .:/usr/src/app 22 | environment: 23 | - APP_HOST=http://uni_rails:3000 24 | depends_on: 25 | uni_rails: 26 | condition: service_healthy 27 | selenium: 28 | condition: service_healthy 29 | command: ruby app_test.rb 30 | selenium: 31 | image: selenium/standalone-chrome:latest 32 | ports: 33 | - "4444:4444" 34 | - "7900:7900" 35 | healthcheck: 36 | test: ["CMD-SHELL", "curl -f http://localhost:4444/status || exit 1"] 37 | interval: 3s 38 | timeout: 5s 39 | retries: 3 40 | start_period: 5s 41 | -------------------------------------------------------------------------------- /features/todos-api/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexB52/uni_rails/2fa0563f39433d7236edfdeefa02d301fb793425/features/todos-api/.DS_Store -------------------------------------------------------------------------------- /features/todos-api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.3.0-slim-bullseye 2 | 3 | ARG BUILD_PACKAGES="build-essential git curl libvips pkg-config sqlite3 libsqlite3-dev" 4 | 5 | # Install packages needed to build gems 6 | RUN apt-get update -qq && \ 7 | apt-get install --no-install-recommends -y $BUILD_PACKAGES 8 | 9 | WORKDIR /usr/src/app 10 | 11 | ENV LANG C.UTF-8 12 | ENV BUNDLER_VERSION 2.1 13 | ENV GEM_HOME="/usr/local/bundle" 14 | ENV PATH $GEM_HOME/bin:$GEM_HOME/gems/bin:$PATH 15 | 16 | COPY Gemfile uni_rails.gem ./ 17 | RUN gem install uni_rails.gem 18 | RUN bundle install 19 | 20 | COPY . /usr/src/app 21 | 22 | CMD ["ruby", "app.rb"] -------------------------------------------------------------------------------- /features/todos-api/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem 'uni_rails', path: '/usr/local/bundle' 6 | gem "sqlite3", "~> 1.7" 7 | 8 | # Test 9 | gem "capybara", "~> 3.40" 10 | gem "selenium-webdriver", "~> 4.19" 11 | gem "minitest" 12 | gem "debug" 13 | -------------------------------------------------------------------------------- /features/todos-api/app.rb: -------------------------------------------------------------------------------- 1 | require 'uni_rails' 2 | require 'debug' 3 | require 'sqlite3' 4 | 5 | ActiveRecord::Base.establish_connection 6 | ActiveRecord::Schema.define do 7 | create_table :todos, force: :cascade do |t| 8 | t.string :name 9 | t.datetime :completed_at 10 | t.timestamps 11 | end 12 | end 13 | 14 | UniRails.routes do 15 | resources :todos do 16 | put :complete, on: :member 17 | end 18 | end 19 | 20 | # MODELS 21 | 22 | class Todo < ActiveRecord::Base 23 | validates :name, presence: true 24 | 25 | def complete(at: Time.zone.now) 26 | update(completed_at: at) 27 | end 28 | end 29 | 30 | # CONTROLLERS 31 | 32 | class ApplicationController < ActionController::Base 33 | protect_from_forgery with: :null_session 34 | end 35 | 36 | class TodosController < ApplicationController 37 | before_action :set_todo, only: [:show, :update, :destroy, :complete] 38 | 39 | def index 40 | @todos = Todo.all 41 | render json: @todos 42 | end 43 | 44 | def show 45 | render json: @todo 46 | end 47 | 48 | def create 49 | @todo = Todo.new(todo_params) 50 | if @todo.save 51 | render json: @todo, status: :created, location: @todo 52 | else 53 | render json: @todo.errors, status: :unprocessable_entity 54 | end 55 | end 56 | 57 | def update 58 | if @todo.update(todo_params) 59 | render json: @todo 60 | else 61 | render json: @todo.errors, status: :unprocessable_entity 62 | end 63 | end 64 | 65 | def complete 66 | if @todo.complete 67 | render json: @todo 68 | else 69 | render json: @todo.errors, status: :unprocessable_entity 70 | end 71 | end 72 | 73 | def destroy 74 | @todo.destroy 75 | head :no_content 76 | end 77 | 78 | private 79 | 80 | def set_todo 81 | @todo = Todo.find(params[:id]) 82 | end 83 | 84 | def todo_params 85 | params.require(:todo).permit(:name, :status) 86 | end 87 | end 88 | 89 | UniRails::App.configure do 90 | config.log_level = :error 91 | config.hosts << ENV['APP_HOST'] 92 | end 93 | 94 | UniRails.run(Port: 3000, BindAddress: '0.0.0.0') 95 | -------------------------------------------------------------------------------- /features/todos-api/app_test.rb: -------------------------------------------------------------------------------- 1 | require "capybara" 2 | require "capybara/dsl" 3 | require 'capybara/minitest' 4 | require "selenium-webdriver" 5 | require "minitest/autorun" 6 | require "debug" 7 | 8 | APP_HOST = ENV.fetch('APP_HOST') 9 | 10 | def truncate_tables 11 | require "sqlite3" 12 | SQLite3::Database.new( "database.sqlite" ) do |db| 13 | db.execute("DELETE FROM todos") 14 | end 15 | end 16 | 17 | class Client 18 | def initialize(url) 19 | @uri = URI(url) 20 | end 21 | 22 | def get(url) 23 | start do |http| 24 | http.get(url) 25 | end 26 | end 27 | 28 | def post(url, params) 29 | start do |http| 30 | http.post(url, params.to_json, "Content-Type" => "application/json") 31 | end 32 | end 33 | 34 | def delete(url) 35 | start do |http| 36 | http.delete(url) 37 | end 38 | end 39 | 40 | def start 41 | Net::HTTP.start(@uri.hostname, @uri.port) do |http| 42 | yield http 43 | end 44 | end 45 | end 46 | 47 | class TestJSONTodos < Minitest::Test 48 | def setup 49 | @client = Client.new(APP_HOST) 50 | truncate_tables 51 | end 52 | 53 | def test_create_a_new_todo 54 | response = @client.get('/todos.json') 55 | todos = JSON.parse(response.body) 56 | assert_equal 0, todos.length 57 | 58 | response = @client.post '/todos.json', { todo: { name: 'Buy milk' } } 59 | todo = JSON.parse(response.body) 60 | 61 | response = @client.get('/todos.json') 62 | todos = JSON.parse(response.body) 63 | assert_equal 1, todos.length 64 | 65 | @client.delete("/todos/#{todo['id']}.json") 66 | 67 | response = @client.get('/todos.json') 68 | todos = JSON.parse(response.body) 69 | assert_equal 0, todos.length 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /features/todos-api/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | uni_rails: 3 | build: . 4 | ports: 5 | - 127.0.0.1:3000:3000 6 | volumes: 7 | - .:/usr/src/app 8 | environment: 9 | - APP_HOST=uni_rails:3000 10 | - DATABASE_URL=sqlite3:////usr/src/app/database.sqlite 11 | - SECRET_KEY_BASE=whatever 12 | healthcheck: 13 | test: ["CMD-SHELL", "curl -f http://localhost:3000 || exit 1"] 14 | interval: 3s 15 | timeout: 5s 16 | retries: 3 17 | start_period: 5s 18 | command: ruby app.rb 19 | test: 20 | build: . 21 | volumes: 22 | - .:/usr/src/app 23 | environment: 24 | - APP_HOST=http://uni_rails:3000 25 | depends_on: 26 | uni_rails: 27 | condition: service_healthy 28 | command: ruby app_test.rb 29 | -------------------------------------------------------------------------------- /features/todos-hotwire/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.3.0-slim-bullseye 2 | 3 | ARG BUILD_PACKAGES="build-essential git curl libvips pkg-config sqlite3 libsqlite3-dev" 4 | 5 | # Install packages needed to build gems 6 | RUN apt-get update -qq && \ 7 | apt-get install --no-install-recommends -y $BUILD_PACKAGES 8 | 9 | WORKDIR /usr/src/app 10 | 11 | ENV LANG C.UTF-8 12 | ENV BUNDLER_VERSION 2.1 13 | ENV GEM_HOME="/usr/local/bundle" 14 | ENV PATH $GEM_HOME/bin:$GEM_HOME/gems/bin:$PATH 15 | 16 | COPY Gemfile uni_rails.gem ./ 17 | RUN gem install uni_rails.gem 18 | RUN bundle install 19 | 20 | COPY . /usr/src/app 21 | 22 | CMD ["ruby", "app.rb"] -------------------------------------------------------------------------------- /features/todos-hotwire/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem 'uni_rails', path: '/usr/local/bundle' 6 | gem "sqlite3", "~> 1.7" 7 | 8 | # Test 9 | gem "capybara", "~> 3.40" 10 | gem "selenium-webdriver", "~> 4.19" 11 | gem "minitest" 12 | gem "debug" 13 | -------------------------------------------------------------------------------- /features/todos-hotwire/app.rb: -------------------------------------------------------------------------------- 1 | require 'uni_rails' 2 | require 'debug' 3 | require 'sqlite3' 4 | 5 | ActiveRecord::Base.establish_connection 6 | ActiveRecord::Schema.define do 7 | create_table :todos, force: :cascade do |t| 8 | t.string :name 9 | t.integer :status, default: 0 10 | t.timestamps 11 | end 12 | end 13 | 14 | UniRails.enable_turbo_rails! 15 | 16 | UniRails.routes do 17 | root 'todos#index' 18 | resources :todos do 19 | patch :change_status, on: :member 20 | end 21 | end 22 | 23 | # MODELS 24 | 25 | class Todo < ActiveRecord::Base 26 | enum status: { 27 | incomplete: 0, 28 | complete: 1 29 | } 30 | 31 | validates :name, presence: true 32 | end 33 | 34 | 35 | # CONTROLLERS 36 | 37 | class TodosController < ActionController::Base 38 | layout 'application' 39 | 40 | before_action :set_todo, only: [:edit, :update, :destroy, :change_status] 41 | 42 | def index 43 | @todos = Todo.where(status: params[:status].presence || 'incomplete') 44 | end 45 | 46 | def new 47 | @todo = Todo.new 48 | end 49 | 50 | def edit 51 | @todo = Todo.find(params[:id]) 52 | end 53 | 54 | def create 55 | @todo = Todo.new(todo_params) 56 | 57 | respond_to do |format| 58 | if @todo.save 59 | format.turbo_stream 60 | format.html { redirect_to todo_url(@todo), notice: "Todo was successfully created." } 61 | else 62 | format.turbo_stream { render turbo_stream: turbo_stream.replace("#{helpers.dom_id(@todo)}_form", partial: "form", locals: { todo: @todo }) } 63 | format.html { render :new, status: :unprocessable_entity } 64 | end 65 | end 66 | end 67 | 68 | def update 69 | respond_to do |format| 70 | if @todo.update(todo_params) 71 | format.turbo_stream 72 | format.html { redirect_to todo_url(@todo), notice: "Todo was successfully updated." } 73 | format.json { render :show, status: :ok, location: @todo } 74 | else 75 | format.turbo_stream { render turbo_stream: turbo_stream.replace("#{helpers.dom_id(@todo)}_form", partial: "form", locals: { todo: @todo }) } 76 | format.html { render :edit, status: :unprocessable_entity } 77 | format.json { render json: @todo.errors, status: :unprocessable_entity } 78 | end 79 | end 80 | end 81 | 82 | def destroy 83 | @todo.destroy 84 | 85 | respond_to do |format| 86 | format.turbo_stream { render turbo_stream: turbo_stream.remove("#{helpers.dom_id(@todo)}_container") } 87 | format.html { redirect_to todos_url, notice: "Todo was successfully destroyed." } 88 | format.json { head :no_content } 89 | end 90 | end 91 | 92 | def change_status 93 | @todo.update(status: todo_params[:status]) 94 | respond_to do |format| 95 | format.turbo_stream { render turbo_stream: turbo_stream.remove("#{helpers.dom_id(@todo)}_container") } 96 | format.html { redirect_to todos_path, notice: "Updated todo status." } 97 | end 98 | end 99 | 100 | private 101 | 102 | def set_todo 103 | @todo = Todo.find(params[:id]) 104 | end 105 | 106 | def todo_params 107 | params.require(:todo).permit(:name, :status) 108 | end 109 | end 110 | 111 | # VIEWS 112 | 113 | UniRails.javascript <<~JAVASCRIPT 114 | import * as Turbo from "turbo" 115 | JAVASCRIPT 116 | 117 | UniRails.register_view "layouts/application.html.erb", <<~'HTML' 118 | 119 | 120 | 121 | Template 122 | 123 | <%= csrf_meta_tags %> 124 | <%= csp_meta_tag %> 125 | 126 | <%= uni_rails_css_stylesheet %> 127 | <%= uni_rails_import_map_tag %> 128 | <%= uni_rails_javascript_script %> 129 | 130 | 131 | 132 | <%= yield %> 133 | 134 | 135 | HTML 136 | 137 | UniRails.register_view "todos/index.html.erb", <<~'HTML' 138 |
    139 |

    140 | Your todos 141 |

    142 | <%= turbo_frame_tag "todos-container", class: "block max-w-2xl w-full bg-gray-100 py-8 px-4 border border-gray-200 rounded shadow-sm" do %> 143 | 160 | <% unless params[:status] == "complete" %> 161 |
    162 | <%= render "form", todo: Todo.new %> 163 |
    164 | <% end %> 165 | 168 | <% end %> 169 |
    170 | HTML 171 | 172 | UniRails.register_view "todos/_form.html.erb", <<~'HTML' 173 | <%= form_with(model: todo, id: "#{dom_id(todo)}_form") do |form| %> 174 | <% if todo.errors.any? %> 175 |
    176 |

    <%= pluralize(todo.errors.count, "error") %> prohibited this todo from being saved:

    177 | 178 | 183 |
    184 | <% end %> 185 |
    186 | <%= form.label :name, class: "sr-only" %> 187 | <%= form.text_field :name, class: "block w-full rounded-none rounded-l-md sm:text-sm border-gray-300", placeholder: "Add a new todo..." %> 188 | <%= form.submit class: "-ml-px relative px-4 py-2 border border-blue-600 text-sm font-medium rounded-r-md text-white bg-blue-600 hover:bg-blue-700" %> 189 |
    190 | <% end %> 191 | HTML 192 | 193 | UniRails.register_view "todos/_todo.html.erb", <<~'HTML' 194 |
  • " class="py-2 px-4 border-b border-gray-300"> 195 | <%= turbo_frame_tag dom_id(todo) do %> 196 |
    197 | <%= link_to todo.name, edit_todo_path(todo), class: todo.complete? ? 'line-through' : '' %> 198 | 199 |
    200 | <% if todo.complete? %> 201 | <%= button_to change_status_todo_path(todo, todo: { status: 'incomplete' }), class: "bg-green-600 px-4 py-2 rounded hover:bg-green-700", method: :patch do %> 202 | Mark as incomplete 203 | 204 | 205 | 206 | <% end %> 207 | <% else %> 208 | <%= button_to change_status_todo_path(todo, todo: { status: 'complete' }), class: "bg-gray-400 px-4 py-2 rounded hover:bg-green-700", method: :patch do %> 209 | Mark as complete 210 | 211 | 212 | 213 | <% end %> 214 | <% end %> 215 | 216 | <%= button_to todo_path(todo), class: "bg-red-600 px-4 py-2 rounded hover:bg-red-700", method: :delete do %> 217 | Delete 218 | 219 | 220 | 221 | <% end %> 222 |
    223 |
    224 | <% end %> 225 |
  • 226 | HTML 227 | 228 | UniRails.register_view "todos/edit.html.erb", <<~'HTML' 229 | <%= turbo_frame_tag dom_id(@todo) do %> 230 | <%= render "form", todo: @todo %> 231 | <% end %> 232 | HTML 233 | 234 | UniRails.register_view "todos/create.turbo_stream.erb", <<~'HTML' 235 | <%= turbo_stream.prepend "todos" do %> 236 | <%= render "todo", todo: @todo %> 237 | <% end %> 238 | <%= turbo_stream.replace "#{dom_id(Todo.new)}_form" do %> 239 | <%= render "form", todo: Todo.new %> 240 | <% end %> 241 | HTML 242 | 243 | UniRails.register_view "todos/update.turbo_stream.erb", <<~'HTML' 244 | <%= turbo_stream.replace "#{dom_id(@todo)}_container" do %> 245 | <%= render "todo", todo: @todo %> 246 | <% end %> 247 | HTML 248 | 249 | UniRails::App.configure do 250 | config.log_level = :error 251 | config.hosts << ENV['APP_HOST'] 252 | end 253 | 254 | UniRails.run(Port: 3000, BindAddress: '0.0.0.0') 255 | -------------------------------------------------------------------------------- /features/todos-hotwire/app_test.rb: -------------------------------------------------------------------------------- 1 | require "capybara" 2 | require "capybara/dsl" 3 | require 'capybara/minitest' 4 | require "selenium-webdriver" 5 | require "minitest/autorun" 6 | require "debug" 7 | 8 | APP_HOST = ENV.fetch('APP_HOST') 9 | 10 | Capybara.register_driver :selenium_remote_chrome do |app| 11 | options = Selenium::WebDriver::Chrome::Options.new 12 | # Add any additional options you need 13 | options.add_argument('--headless') # Uncomment if you want to run headless mode 14 | 15 | Capybara::Selenium::Driver.new(app, 16 | browser: :remote, 17 | url: "http://selenium:4444/wd/hub", 18 | options: options 19 | ) 20 | end 21 | 22 | Capybara.default_driver = :selenium_remote_chrome 23 | Capybara.app_host = APP_HOST 24 | Capybara.default_max_wait_time = 5 25 | 26 | def truncate_tables 27 | require "sqlite3" 28 | SQLite3::Database.new( "database.sqlite" ) do |db| 29 | db.execute("DELETE FROM todos") 30 | end 31 | end 32 | 33 | class TestNewTodos < Minitest::Test 34 | include Capybara::DSL 35 | include Capybara::Minitest::Assertions 36 | 37 | def setup 38 | truncate_tables 39 | end 40 | 41 | def teardown 42 | Capybara.reset_sessions! 43 | Capybara.use_default_driver 44 | end 45 | 46 | def test_create_a_new_todo 47 | visit "/" 48 | 49 | fill_in 'Name', with: 'Buy milk' 50 | click_on 'Create Todo' 51 | assert_text 'Buy milk' 52 | 53 | within first('ul#todos li') do 54 | click_on 'Buy milk' 55 | fill_in 'Name', with: 'Buy milk (updated)' 56 | click_on 'Update Todo' 57 | end 58 | 59 | assert_text 'Buy milk (updated)' 60 | click_on 'Mark as complete' 61 | 62 | assert_incomplete(0) 63 | assert_complete(1) 64 | 65 | click_on 'Complete' 66 | assert_text 'Buy milk (updated)' 67 | 68 | within first('ul#todos li') do 69 | click_on 'Mark as incomplete' 70 | end 71 | 72 | assert_incomplete(1) 73 | assert_complete(0) 74 | 75 | click_on 'Incomplete' 76 | assert_text 'Buy milk (updated)' 77 | 78 | within first('ul#todos li') do 79 | click_on 'Delete' 80 | end 81 | 82 | assert_complete(0) 83 | assert_incomplete(0) 84 | end 85 | 86 | private 87 | 88 | def assert_complete(n) 89 | find('#nav-bar-action-complete').click 90 | assert_equal n, all('ul#todos li').length 91 | end 92 | 93 | def assert_incomplete(n) 94 | find('#nav-bar-action-incomplete').click 95 | assert_equal n, all('ul#todos li').length 96 | end 97 | end 98 | 99 | -------------------------------------------------------------------------------- /features/todos-hotwire/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | uni_rails: 3 | build: . 4 | ports: 5 | - 127.0.0.1:3000:3000 6 | volumes: 7 | - .:/usr/src/app 8 | environment: 9 | - APP_HOST=uni_rails:3000 10 | - DATABASE_URL=sqlite3:////usr/src/app/database.sqlite 11 | - SECRET_KEY_BASE=whatever 12 | healthcheck: 13 | test: ["CMD-SHELL", "curl -f http://localhost:3000 || exit 1"] 14 | interval: 3s 15 | timeout: 5s 16 | retries: 3 17 | start_period: 5s 18 | command: ruby app.rb 19 | test: 20 | build: . 21 | volumes: 22 | - .:/usr/src/app 23 | environment: 24 | - APP_HOST=http://uni_rails:3000 25 | depends_on: 26 | uni_rails: 27 | condition: service_healthy 28 | selenium: 29 | condition: service_healthy 30 | command: ruby app_test.rb 31 | selenium: 32 | image: selenium/standalone-chrome:latest 33 | ports: 34 | - "4444:4444" 35 | - "7900:7900" 36 | healthcheck: 37 | test: ["CMD-SHELL", "curl -f http://localhost:4444/status || exit 1"] 38 | interval: 3s 39 | timeout: 5s 40 | retries: 3 41 | start_period: 5s 42 | -------------------------------------------------------------------------------- /features/todos-scaffold/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexB52/uni_rails/2fa0563f39433d7236edfdeefa02d301fb793425/features/todos-scaffold/.DS_Store -------------------------------------------------------------------------------- /features/todos-scaffold/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.3.0-slim-bullseye 2 | 3 | ARG BUILD_PACKAGES="build-essential git curl libvips pkg-config sqlite3 libsqlite3-dev" 4 | 5 | # Install packages needed to build gems 6 | RUN apt-get update -qq && \ 7 | apt-get install --no-install-recommends -y $BUILD_PACKAGES 8 | 9 | WORKDIR /usr/src/app 10 | 11 | ENV LANG C.UTF-8 12 | ENV BUNDLER_VERSION 2.1 13 | ENV GEM_HOME="/usr/local/bundle" 14 | ENV PATH $GEM_HOME/bin:$GEM_HOME/gems/bin:$PATH 15 | 16 | COPY Gemfile uni_rails.gem ./ 17 | RUN gem install uni_rails.gem 18 | RUN bundle install 19 | 20 | COPY . /usr/src/app 21 | 22 | CMD ["ruby", "app.rb"] -------------------------------------------------------------------------------- /features/todos-scaffold/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem 'uni_rails', path: '/usr/local/bundle' 6 | gem "sqlite3", "~> 1.7" 7 | gem "jbuilder" 8 | 9 | # Test 10 | gem "capybara", "~> 3.40" 11 | gem "selenium-webdriver", "~> 4.19" 12 | gem "minitest" 13 | gem "debug" 14 | -------------------------------------------------------------------------------- /features/todos-scaffold/app.rb: -------------------------------------------------------------------------------- 1 | require "uni_rails" 2 | require "jbuilder" 3 | require "sqlite3" 4 | require "debug" 5 | 6 | ActiveRecord::Base.establish_connection 7 | ActiveRecord::Schema.define do 8 | create_table :todos, force: :cascade do |t| 9 | t.string :name 10 | t.datetime :completed_at 11 | t.timestamps 12 | end 13 | end 14 | 15 | UniRails.routes do 16 | root 'todos#index' 17 | resources :todos 18 | end 19 | 20 | # MODELS 21 | 22 | class Todo < ActiveRecord::Base 23 | validates :name, presence: true 24 | 25 | def complete(at: Time.zone.now) 26 | update(completed_at: at) 27 | end 28 | end 29 | 30 | 31 | # CONTROLLERS 32 | class ApplicationController < ActionController::Base 33 | protect_from_forgery with: :null_session, unless: :csrf_required? 34 | 35 | private 36 | 37 | def csrf_required? 38 | !request.format.json? 39 | end 40 | end 41 | 42 | class TodosController < ApplicationController 43 | before_action :set_todo, only: %i[ show edit update destroy ] 44 | 45 | # GET /todos or /todos.json 46 | def index 47 | @todos = Todo.all 48 | end 49 | 50 | # GET /todos/1 or /todos/1.json 51 | def show 52 | end 53 | 54 | # GET /todos/new 55 | def new 56 | @todo = Todo.new 57 | end 58 | 59 | # GET /todos/1/edit 60 | def edit 61 | end 62 | 63 | # POST /todos or /todos.json 64 | def create 65 | @todo = Todo.new(todo_params) 66 | 67 | respond_to do |format| 68 | if @todo.save 69 | format.html { redirect_to todo_url(@todo), notice: "Todo was successfully created." } 70 | format.json { render :show, status: :created, location: @todo } 71 | else 72 | format.html { render :new, status: :unprocessable_entity } 73 | format.json { render json: @todo.errors, status: :unprocessable_entity } 74 | end 75 | end 76 | end 77 | 78 | # PATCH/PUT /todos/1 or /todos/1.json 79 | def update 80 | respond_to do |format| 81 | if @todo.update(todo_params) 82 | format.html { redirect_to todo_url(@todo), notice: "Todo was successfully updated." } 83 | format.json { render :show, status: :ok, location: @todo } 84 | else 85 | format.html { render :edit, status: :unprocessable_entity } 86 | format.json { render json: @todo.errors, status: :unprocessable_entity } 87 | end 88 | end 89 | end 90 | 91 | # DELETE /todos/1 or /todos/1.json 92 | def destroy 93 | @todo.destroy! 94 | 95 | respond_to do |format| 96 | format.html { redirect_to todos_url, notice: "Todo was successfully destroyed." } 97 | format.json { head :no_content } 98 | end 99 | end 100 | 101 | private 102 | # Use callbacks to share common setup or constraints between actions. 103 | def set_todo 104 | @todo = Todo.find(params[:id]) 105 | end 106 | 107 | # Only allow a list of trusted parameters through. 108 | def todo_params 109 | params.require(:todo).permit(:name, :completed_at) 110 | end 111 | end 112 | 113 | # VIEWS - HTML 114 | 115 | UniRails.register_view "todos/_form.html.erb", <<~HTML 116 | <%= form_with(model: todo) do |form| %> 117 | <% if todo.errors.any? %> 118 |
    119 |

    <%= pluralize(todo.errors.count, "error") %> prohibited this todo from being saved:

    120 | 121 | 126 |
    127 | <% end %> 128 | 129 |
    130 | <%= form.label :name, style: "display: block" %> 131 | <%= form.text_field :name %> 132 |
    133 | 134 |
    135 | <%= form.label :completed_at, style: "display: block" %> 136 | <%= form.datetime_field :completed_at %> 137 |
    138 | 139 |
    140 | <%= form.submit %> 141 |
    142 | <% end %> 143 | HTML 144 | 145 | UniRails.register_view "todos/_todo.html.erb", <<~HTML 146 |
    147 |

    148 | Name: 149 | <%= todo.name %> 150 |

    151 | 152 |

    153 | Completed at: 154 | <%= todo.completed_at %> 155 |

    156 | 157 |
    158 | HTML 159 | 160 | UniRails.register_view "todos/edit.html.erb", <<~HTML 161 |

    Editing todo

    162 | 163 | <%= render "form", todo: @todo %> 164 | 165 |
    166 | 167 |
    168 | <%= link_to "Show this todo", @todo %> | 169 | <%= link_to "Back to todos", todos_path %> 170 |
    171 | HTML 172 | 173 | UniRails.register_view "todos/index.html.erb", <<~HTML 174 |

    <%= notice %>

    175 | 176 |

    Todos

    177 | 178 |
    179 | <% @todos.each do |todo| %> 180 | <%= render todo %> 181 |

    182 | <%= link_to "Show this todo", todo %> 183 |

    184 | <% end %> 185 |
    186 | 187 | <%= link_to "New todo", new_todo_path %> 188 | HTML 189 | 190 | UniRails.register_view "todos/new.html.erb", <<~HTML 191 |

    New todo

    192 | 193 | <%= render "form", todo: @todo %> 194 | 195 |
    196 | 197 |
    198 | <%= link_to "Back to todos", todos_path %> 199 |
    200 | HTML 201 | 202 | UniRails.register_view "todos/show.html.erb", <<~HTML 203 |

    <%= notice %>

    204 | 205 | <%= render @todo %> 206 | 207 |
    208 | <%= link_to "Edit this todo", edit_todo_path(@todo) %> | 209 | <%= link_to "Back to todos", todos_path %> 210 | 211 | <%= button_to "Destroy this todo", @todo, method: :delete %> 212 |
    213 | HTML 214 | 215 | # VIEWS - JSON 216 | 217 | UniRails.register_view "todos/_todo.json.jbuilder", <<~RUBY 218 | json.extract! todo, :id, :name, :completed_at, :created_at, :updated_at 219 | json.url todo_url(todo, format: :json) 220 | RUBY 221 | 222 | UniRails.register_view "todos/index.json.jbuilder", <<~RUBY 223 | json.array! @todos, partial: "todos/todo", as: :todo 224 | RUBY 225 | 226 | UniRails.register_view "todos/show.json.jbuilder", <<~RUBY 227 | json.partial! "todos/todo", todo: @todo 228 | RUBY 229 | 230 | UniRails::App.configure do 231 | config.log_level = :error 232 | config.hosts << ENV['APP_HOST'] 233 | end 234 | 235 | UniRails.run(Port: 3000, BindAddress: '0.0.0.0') 236 | -------------------------------------------------------------------------------- /features/todos-scaffold/app_test.rb: -------------------------------------------------------------------------------- 1 | require "capybara" 2 | require "capybara/dsl" 3 | require 'capybara/minitest' 4 | require "selenium-webdriver" 5 | require "minitest/autorun" 6 | require "debug" 7 | 8 | APP_HOST = ENV.fetch('APP_HOST') 9 | 10 | Capybara.register_driver :selenium_remote_chrome do |app| 11 | options = Selenium::WebDriver::Chrome::Options.new 12 | # Add any additional options you need 13 | options.add_argument('--headless') # Uncomment if you want to run headless mode 14 | 15 | Capybara::Selenium::Driver.new(app, 16 | browser: :remote, 17 | url: "http://selenium:4444/wd/hub", 18 | options: options 19 | ) 20 | end 21 | 22 | Capybara.default_driver = :selenium_remote_chrome 23 | Capybara.app_host = APP_HOST 24 | 25 | def truncate_tables 26 | require "sqlite3" 27 | SQLite3::Database.new( "database.sqlite" ) do |db| 28 | db.execute("DELETE FROM todos") 29 | end 30 | end 31 | 32 | class Client 33 | def initialize(url) 34 | @uri = URI(url) 35 | end 36 | 37 | def get(url) 38 | start do |http| 39 | http.get(url) 40 | end 41 | end 42 | 43 | def post(url, params) 44 | start do |http| 45 | http.post(url, params.to_json, "Content-Type" => "application/json") 46 | end 47 | end 48 | 49 | def delete(url) 50 | start do |http| 51 | http.delete(url) 52 | end 53 | end 54 | 55 | def start 56 | Net::HTTP.start(@uri.hostname, @uri.port) do |http| 57 | yield http 58 | end 59 | end 60 | end 61 | 62 | class TestJSONTodos < Minitest::Test 63 | def setup 64 | @client = Client.new(APP_HOST) 65 | truncate_tables 66 | end 67 | 68 | def test_create_a_new_todo 69 | response = @client.get('/todos.json') 70 | todos = JSON.parse(response.body) 71 | assert_equal 0, todos.length 72 | 73 | response = @client.post '/todos.json', { todo: { name: 'Buy milk' } } 74 | todo = JSON.parse(response.body) 75 | 76 | response = @client.get('/todos.json') 77 | todos = JSON.parse(response.body) 78 | assert_equal 1, todos.length 79 | 80 | @client.delete("/todos/#{todo['id']}.json") 81 | 82 | response = @client.get('/todos.json') 83 | todos = JSON.parse(response.body) 84 | assert_equal 0, todos.length 85 | end 86 | end 87 | 88 | class TestHTMLTodos < Minitest::Test 89 | include Capybara::DSL 90 | include Capybara::Minitest::Assertions 91 | 92 | def setup 93 | truncate_tables 94 | end 95 | 96 | def test_create_a_new_todo 97 | visit "/" 98 | 99 | click_on 'New todo' 100 | assert_text 'New todo' 101 | 102 | fill_in 'Name', with: 'Buy milk' 103 | click_on 'Create Todo' 104 | assert_text 'Todo was successfully created.' 105 | 106 | click_on 'Edit this todo' 107 | fill_in 'Name', with: 'Buy milk (updated)' 108 | click_on 'Update Todo' 109 | assert_text 'Buy milk (updated)' 110 | assert_text 'Todo was successfully updated.' 111 | 112 | click_on 'Destroy this todo' 113 | assert_text 'Todo was successfully destroyed.' 114 | 115 | assert_equal '/todos', page.current_path 116 | end 117 | 118 | def test_multiple_todos 119 | skip 120 | create_todo(name: 'Buy Shampoo') 121 | create_todo(name: 'Buy Timtam') 122 | end 123 | 124 | def teardown 125 | Capybara.reset_sessions! 126 | Capybara.use_default_driver 127 | end 128 | 129 | private 130 | 131 | def cleanup_todos 132 | visit '/' 133 | until (links = all('a', text: 'Show this todo')).empty? do 134 | links.first.click 135 | click_on 'Destroy this todo' 136 | assert_text 'Todo was successfully destroyed.' 137 | end 138 | end 139 | 140 | def create_todo(name:) 141 | visit '/todos/new' 142 | fill_in 'Name', with: name 143 | click_on 'Create' 144 | assert_text 'Todo was successfully created.' 145 | end 146 | end 147 | 148 | -------------------------------------------------------------------------------- /features/todos-scaffold/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | uni_rails: 3 | build: . 4 | ports: 5 | - 127.0.0.1:3000:3000 6 | volumes: 7 | - .:/usr/src/app 8 | environment: 9 | - APP_HOST=uni_rails:3000 10 | - DATABASE_URL=sqlite3:////usr/src/app/database.sqlite 11 | - SECRET_KEY_BASE=whatever 12 | healthcheck: 13 | test: ["CMD-SHELL", "curl -f http://localhost:3000 || exit 1"] 14 | interval: 3s 15 | timeout: 5s 16 | retries: 3 17 | start_period: 5s 18 | command: ruby app.rb 19 | test: 20 | build: . 21 | volumes: 22 | - .:/usr/src/app 23 | environment: 24 | - APP_HOST=http://uni_rails:3000 25 | depends_on: 26 | uni_rails: 27 | condition: service_healthy 28 | selenium: 29 | condition: service_healthy 30 | command: ruby app_test.rb 31 | selenium: 32 | image: selenium/standalone-chrome:latest 33 | ports: 34 | - "4444:4444" 35 | - "7900:7900" 36 | healthcheck: 37 | test: ["CMD-SHELL", "curl -f http://localhost:4444/status || exit 1"] 38 | interval: 3s 39 | timeout: 5s 40 | retries: 3 41 | start_period: 5s 42 | -------------------------------------------------------------------------------- /lib/uni_rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if ENV['SECRET_KEY_BASE'].nil? 4 | raise StandardError, <<~ERROR 5 | 6 | SECRET_KEY_BASE environment variable is required 7 | Provide ENV['SECRET_KEY_BASE'] in your file or export the variable to your profile 8 | ERROR 9 | end 10 | 11 | require "rails" 12 | require_relative "uni_rails/version" 13 | require_relative "uni_rails/helpers" 14 | require_relative "uni_rails/helpers/css_helper" 15 | require_relative "uni_rails/helpers/javascript_helper" 16 | require_relative "uni_rails/app" 17 | require_relative "uni_rails/app/css" 18 | require_relative "uni_rails/app/javascript" 19 | require_relative "uni_rails/app/views" 20 | 21 | module UniRails 22 | class Error < StandardError; end 23 | 24 | def self.enable_turbo_rails! 25 | # # We currently do not support ActionCable and ActiveJob 26 | require "turbo-rails" 27 | 28 | App.configure do 29 | initializer "turbo.no_action_cable", before: :set_eager_load_paths do 30 | unless defined?(ActionCable) 31 | Rails.autoloaders.once.do_not_eager_load("#{Turbo::Engine.root}/app/channels") 32 | end 33 | 34 | unless defined?(ActiveJob) 35 | Rails.autoloaders.once.do_not_eager_load("#{Turbo::Engine.root}/app/jobs") 36 | end 37 | end 38 | end 39 | 40 | App::Javascript.dependencies.merge!( 41 | "turbo" => "https://unpkg.com/@hotwired/turbo@8.0.4/dist/turbo.es2017-umd.js" 42 | ) 43 | end 44 | 45 | def self.routes(&block) 46 | App.initializer :add_uni_routes, before: :add_internal_routes do |app| 47 | app.routes.prepend do 48 | get "/rails/info/routes" => "rails/info#routes", internal: true 49 | end 50 | app.routes.prepend(&block) 51 | end 52 | end 53 | 54 | def self.rackup_handler=(handler) 55 | @@rackup_handler = handler 56 | end 57 | 58 | def self.rackup_handler 59 | @@rackup_handler ||= begin 60 | require 'rackup' 61 | Rackup::Handler::WEBrick 62 | end 63 | end 64 | 65 | def self.register_view(action, view) 66 | UniRails::App::Views.instance.views[action] = view 67 | end 68 | 69 | def self.run(**webrick_options) 70 | App.initialize! 71 | rackup_handler.run App, **webrick_options 72 | # Rackup::Handler::Falcon.run App, **webrick_options 73 | # Rack::Handler::Puma.run App, **webrick_options 74 | # Rackup::Handler::WEBrick.run App, **webrick_options 75 | end 76 | 77 | def self.import_maps(dependencies) 78 | UniRails::App::Javascript.dependencies.merge! dependencies 79 | end 80 | 81 | def self.javascript(content) 82 | UniRails::App::Javascript.javascript = content 83 | end 84 | 85 | def self.css(content) 86 | UniRails::App::CSS.css = content 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/uni_rails/app.rb: -------------------------------------------------------------------------------- 1 | # Pick the frameworks you want: 2 | 3 | require "active_model/railtie" 4 | # require "active_job/railtie" 5 | require "active_record/railtie" unless ENV['DATABASE_URL'].nil? 6 | # require "active_storage/engine" 7 | require "action_controller/railtie" 8 | # require "action_mailer/railtie" 9 | # require "action_mailbox/engine" 10 | # require "action_text/engine" 11 | require "action_view/railtie" 12 | # require "action_cable/engine" 13 | # require "rails/test_unit/railtie" 14 | require "action_view/testing/resolvers" 15 | 16 | module UniRails 17 | class App < Rails::Application 18 | config.load_defaults 7.2 19 | 20 | # config.eager_load = true 21 | config.logger = Logger.new(STDOUT) 22 | config.log_level = :debug 23 | config.secret_key_base = ENV.fetch('SECRET_KEY_BASE') 24 | 25 | routes.draw do 26 | resources :users 27 | end 28 | 29 | config.after_initialize do 30 | ActionController::Base.view_paths = Views.view_paths 31 | ActionController::Base.include App.routes.url_helpers 32 | ActionController::Base.helper Helpers::JavascriptHelper 33 | ActionController::Base.helper Helpers::CSSHelper 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/uni_rails/app/css.rb: -------------------------------------------------------------------------------- 1 | module UniRails 2 | class App 3 | class CSS 4 | include Singleton 5 | 6 | class << self 7 | delegate :css, :css=, to: :instance 8 | end 9 | 10 | attr_accessor :css 11 | def initialize 12 | @css = "" 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/uni_rails/app/javascript.rb: -------------------------------------------------------------------------------- 1 | module UniRails 2 | class App 3 | class Javascript 4 | include Singleton 5 | 6 | class << self 7 | delegate :imports, 8 | :dependencies, :dependencies=, 9 | :javascript, :javascript=, 10 | to: :instance 11 | end 12 | 13 | attr_accessor :dependencies, :javascript 14 | def initialize 15 | @dependencies = {} 16 | @javascript = "" 17 | end 18 | 19 | def imports 20 | { imports: dependencies } 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/uni_rails/app/views.rb: -------------------------------------------------------------------------------- 1 | module UniRails 2 | class App 3 | class Views 4 | include Singleton 5 | 6 | DEFAULT_LAYOUT = <<~HTML 7 | 8 | 9 | 10 | Template 11 | 12 | <%= csrf_meta_tags %> 13 | <%= csp_meta_tag %> 14 | 15 | <%= uni_rails_css_stylesheet %> 16 | <%= uni_rails_import_map_tag %> 17 | <%= uni_rails_javascript_script %> 18 | 19 | 20 | 21 | <%= yield %> 22 | 23 | 24 | HTML 25 | 26 | attr_accessor :views 27 | def initialize 28 | @views = { 'layouts/application.html.erb' => DEFAULT_LAYOUT } 29 | end 30 | 31 | def self.view_paths 32 | [ActionView::FixtureResolver.new(instance.views)] 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/uni_rails/helpers.rb: -------------------------------------------------------------------------------- 1 | module UniRails 2 | module Helpers 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/uni_rails/helpers/css_helper.rb: -------------------------------------------------------------------------------- 1 | module UniRails 2 | module Helpers 3 | module CSSHelper 4 | def uni_rails_css_stylesheet 5 | content_tag(:style) do 6 | raw App::CSS.css 7 | end 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/uni_rails/helpers/javascript_helper.rb: -------------------------------------------------------------------------------- 1 | module UniRails 2 | module Helpers 3 | module JavascriptHelper 4 | def uni_rails_import_map_tag 5 | content_tag(:script, type: 'importmap') do 6 | raw App::Javascript.imports.to_json 7 | end 8 | end 9 | 10 | def uni_rails_javascript_script 11 | content_tag(:script, type: 'module') do 12 | raw App::Javascript.javascript 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/uni_rails/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module UniRails 4 | VERSION = "0.5.0" 5 | end 6 | -------------------------------------------------------------------------------- /sig/uni_rails.rbs: -------------------------------------------------------------------------------- 1 | module UniRails 2 | VERSION: String 3 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides 4 | end 5 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['SECRET_KEY_BASE'] = 'something' 4 | 5 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 6 | require "uni_rails" 7 | 8 | require "minitest/autorun" 9 | require "debug" 10 | 11 | -------------------------------------------------------------------------------- /test/test_uni_rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class TestUniRails < Minitest::Test 6 | def test_that_it_has_a_version_number 7 | refute_nil ::UniRails::VERSION 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /uni_rails.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/uni_rails/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "uni_rails" 7 | spec.version = UniRails::VERSION 8 | spec.authors = ["Alexandre Barret"] 9 | spec.email = ["barret.alx@gmail.com"] 10 | 11 | spec.summary = "A Ruby library to create single-file Rails application" 12 | spec.homepage = "https://github.com/AlexB52/uni_rails" 13 | spec.license = "MIT" 14 | spec.required_ruby_version = ">= 3.2.0" 15 | 16 | # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'" 17 | 18 | spec.metadata["homepage_uri"] = spec.homepage 19 | spec.metadata["source_code_uri"] = "https://github.com/AlexB52/uni_rails" 20 | 21 | # Specify which files should be added to the gem when it is released. 22 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 23 | spec.files = Dir.chdir(__dir__) do 24 | `git ls-files -z`.split("\x0").reject do |f| 25 | (File.expand_path(f) == __FILE__) || 26 | f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile]) 27 | end 28 | end 29 | spec.bindir = "exe" 30 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 31 | spec.require_paths = ["lib"] 32 | 33 | # Uncomment to register a new dependency of your gem 34 | spec.add_dependency "rails", "~> 7.2.0" 35 | spec.add_dependency "rackup", "~> 2.1" 36 | spec.add_dependency "turbo-rails", "~> 2.0" 37 | 38 | # For more information and examples about making a new gem, check out our 39 | # guide at: https://bundler.io/guides/creating_gem.html 40 | end 41 | --------------------------------------------------------------------------------