diff --git a/Gemfile.tt b/Gemfile.tt index 3c21195..e61804e 100644 --- a/Gemfile.tt +++ b/Gemfile.tt @@ -3,7 +3,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby "<%= RUBY_VERSION %>" -gem "rails", "~> <%= Rails.version %>" +gem "rails", "~> <%= Rails.version || "~> 6.0.0" %>" # gem 'rails-i18n' gem 'pg', '>= 0.18', '< 2.0'<%= gemfile_requirement("pg") %> @@ -14,8 +14,8 @@ gem 'puma', '~> 3.11'<%= gemfile_requirement("puma") %> # gem 'aws-sdk-s3' # for DigitalOcean config # Auth -<%= "#" unless use_active_admin == 'yes' %>gem 'devise' -<%= "#" unless use_active_admin == 'yes' %>gem 'devise-i18n' +<%= "#" unless use_active_admin %>gem 'devise' +<%= "#" unless use_active_admin %>gem 'devise-i18n' # Model gem 'jbuilder', '~> 2.5' @@ -23,19 +23,19 @@ gem 'jbuilder', '~> 2.5' # View gem 'sass-rails', '~> 5.0' -gem 'webpacker', '>= 4.0.0.rc.3' -gem 'turbolinks', '~> 5' +gem 'webpacker', '>= 4.0.0' +<%= "gem 'turbolinks', '~> 5'" unless use_react %> # gem 'inline_svg' if using svg files # CMS -<%= "#" unless use_active_admin == 'yes' %>gem 'activeadmin' -<%= "#" unless use_active_admin == 'yes' %>gem 'activeadmin_addons' -<%= "#" unless use_active_admin == 'yes' %>gem 'arctic_admin' -<%= "#" unless use_active_admin == 'yes' %>gem 'arbre', '>= 1.2.1' +<%= "#" unless use_active_admin %>gem 'activeadmin' +<%= "#" unless use_active_admin %>gem 'activeadmin_addons' +<%= "#" unless use_active_admin %>gem 'arctic_admin' +<%= "#" unless use_active_admin %>gem 'arbre', '>= 1.2.1' # Background gem 'sidekiq' -<%= "#" unless slack_notification == 'yes' %>gem 'slack-ruby-client' +<%= "#" unless use_slack_notification %>gem 'slack-ruby-client' # gem 'whenever' gem 'bootsnap', '>= 1.1.0', require: false diff --git a/README.md b/README.md index 5cf135b..74679bd 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ rails new project -T -d postgresql \ * Docker for production deploy * Nginx Proxy server configuration with Let's encrypt SSL Certificate -* Webpacker and Stimulus setting for client javascript +* React / Stimulus setting for client javascript * ActiveJob + Sidekiq + Redis setting for async jobs * ActiveAdmin + ArcticAdmin for application admin * Foreman setting for integrative dev setup @@ -43,7 +43,11 @@ It runs * guard * sidekiq -## Stimulus generator +## Stimulus.js + +With [stimulus.js]([https://stimulusjs.org](https://stimulusjs.org/)) you can keep your client-side code style as basic style `html + css + js` stack and still get the advantages of modern Javascript open sources through npm. + +### generator Stimulus specific generator task. @@ -53,6 +57,20 @@ It generates * `app/javascript/posts/index_controller.js` with sample html markup containing stimulus path helper. +## React.js + +With [react.js](https://reactjs.org/) you can build modern single page application in the most common way. (This template implements react.js with hooks.) + +In order to integrate react.js and rails. + +Template contains + +- react layout : `react.html.erb` +- routing for react : `/:path => 'react#index'` +- routing for rails : `/api`, `/app` +- some examples with routing over rails pages <-> react pages +- example functional component with fetching api from client to server. + ## Production deploy process After installing [docker](https://docs.docker.com/install/) & [docker-compose](https://docs.docker.com/compose/install/) in your host machine. @@ -151,4 +169,5 @@ docker rm processid ``` ## TODO +- Zero downtime deployment with docker-compose. - \ No newline at end of file diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/api/home_controller.rb b/app/controllers/api/home_controller.rb new file mode 100644 index 0000000..7d491c9 --- /dev/null +++ b/app/controllers/api/home_controller.rb @@ -0,0 +1,9 @@ +module Api + class HomeController < Api::ApiController + def index + render json: { + hello: "Hello World from Rails" + } + end + end +end \ No newline at end of file diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 0000000..95f2992 --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,4 @@ +class HomeController < ApplicationController + def index + end +end diff --git a/app/controllers/react_controller.rb b/app/controllers/react_controller.rb new file mode 100644 index 0000000..1acd36b --- /dev/null +++ b/app/controllers/react_controller.rb @@ -0,0 +1,3 @@ +class ReactController < ApplicationController + def index; render "layouts/react" end +end diff --git a/app/javascript/packs/App.jsx b/app/javascript/packs/App.jsx new file mode 100644 index 0000000..5ab8276 --- /dev/null +++ b/app/javascript/packs/App.jsx @@ -0,0 +1,20 @@ +import React from "react"; +import { useRoutes, A } from "hookrouter"; +import routes from "./routes"; + +function App() { + const routeResult = useRoutes(routes); + return ( +
+ React SPA /home +
+ React SPA /about +
+ Rails SSR / +
+ {routeResult ||

404 on React SPA

} +
+ ); +} + +export default App; \ No newline at end of file diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js new file mode 100644 index 0000000..cc1f860 --- /dev/null +++ b/app/javascript/packs/application.js @@ -0,0 +1,7 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import App from './App'; + +document.addEventListener('DOMContentLoaded', () => { + ReactDOM.render(, document.body.appendChild(document.createElement('div'))) +}); diff --git a/app/javascript/packs/pages/home/Index.jsx b/app/javascript/packs/pages/home/Index.jsx new file mode 100644 index 0000000..a249e9c --- /dev/null +++ b/app/javascript/packs/pages/home/Index.jsx @@ -0,0 +1,25 @@ +import React, { useState, useEffect } from "react"; +import api from 'utils/api'; + +function Index() { + const [ helloWorld, setHelloWorld ] = useState('loading...'); + + useEffect(() => { + console.log(window, 'mount'); + api.get('/api/home/index').then((res) => { + setHelloWorld(res.data.hello); + }); + return () => { + console.log(window, 'unmount') + } + }, []); + + return ( +
+

HomeIndex Page from React.js

+

{helloWorld}

+
+ ); +} + +export default Index; diff --git a/app/javascript/packs/routes.js b/app/javascript/packs/routes.js new file mode 100644 index 0000000..f8cf960 --- /dev/null +++ b/app/javascript/packs/routes.js @@ -0,0 +1,9 @@ +import React from "react"; +import HomeIndex from "./pages/home/Index.jsx"; + +const routes = { + "/home": () => , + "/about": () =>

About Page from React.js

+}; + +export default routes; \ No newline at end of file diff --git a/app/lib/exceptions/default_error.rb.tt b/app/lib/exceptions/default_error.rb.tt index 82bab9d..4ca63f9 100644 --- a/app/lib/exceptions/default_error.rb.tt +++ b/app/lib/exceptions/default_error.rb.tt @@ -5,7 +5,7 @@ module Exceptions def initialize(msg = "알 수 없는 에러가 발생했습니다.", notification = false) @message = msg puts "DefaultError => #{msg}" if Rails.env.development? - <%= "#" unless slack_notification == 'yes' %>SlackService.send_message("Exceptions::DefaultError", msg, :error) if notification + <%= "#" unless use_slack_notification %>SlackService.send_message("Exceptions::DefaultError", msg, :error) if notification end end end diff --git a/app/services/slack_service.rb b/app/services/slack_service.rb index 9e2942f..bbeb361 100644 --- a/app/services/slack_service.rb +++ b/app/services/slack_service.rb @@ -12,6 +12,11 @@ def send_message(title, msg, channel = :log) SlackMessageJob.perform_later("*#{title}*#{"\n"}```#{msg}```", CHANNELS[channel]) end + def send_exception(e, title = e.message, prepend = nil) + title = "🤔 " + title + SlackMessageJob.perform_later("*#{title}*#{"\n"}```#{prepend}#{"\n"}#{e.backtrace.first(20).join("\n")}```", CHANNELS[:error]) + end + def upload_file(file, channel = :log) # client.files_upload( # channels: '#general', diff --git a/app/template.rb b/app/template.rb index 8becd56..83f3399 100644 --- a/app/template.rb +++ b/app/template.rb @@ -13,13 +13,32 @@ copy_file 'app/controllers/api/api_controller.rb' +copy_file 'app/controllers/home_controller.rb' +template 'app/views/home/index.html.erb.tt' + copy_file 'app/javascript/utils/api.js' copy_file 'app/javascript/utils/helpers.js' -copy_file 'app/javascript/controllers/index.js', force: true + +if use_react + copy_file 'app/javascript/packs/application.js', force: true + copy_file 'app/javascript/packs/routes.js' + copy_file 'app/javascript/packs/App.jsx' + copy_file 'app/javascript/packs/pages/home/Index.jsx' + + copy_file 'app/controllers/react_controller.rb' + template 'app/views/layouts/react.html.erb.tt' + + copy_file 'app/controllers/api/home_controller.rb' + + copy_file 'app/assets/javascripts/application.js', force: true + template 'app/views/layouts/application.html.erb.tt', force: true +else + copy_file 'app/javascript/controllers/index.js', force: true +end copy_file 'app/jobs/http_post_job.rb' template 'app/lib/exceptions/default_error.rb.tt' copy_file 'app/lib/bot_helper.rb' -copy_file 'app/jobs/slack_message_job.rb' if slack_notification == 'yes' -copy_file 'app/services/slack_service.rb' if slack_notification == 'yes' \ No newline at end of file +copy_file 'app/jobs/slack_message_job.rb' if use_slack_notification +copy_file 'app/services/slack_service.rb' if use_slack_notification \ No newline at end of file diff --git a/app/views/home/index.html.erb.tt b/app/views/home/index.html.erb.tt new file mode 100644 index 0000000..9aa3d8a --- /dev/null +++ b/app/views/home/index.html.erb.tt @@ -0,0 +1,13 @@ +
<% if use_react %> + React SPA /home +
+ React SPA /about +
+ Rails SSR / +<% else %> + Home +<% end %>
+
+

+ Application Root +

diff --git a/app/views/layouts/application.html.erb.tt b/app/views/layouts/application.html.erb.tt new file mode 100644 index 0000000..3d280bb --- /dev/null +++ b/app/views/layouts/application.html.erb.tt @@ -0,0 +1,15 @@ + + + + <%= app_name %> + <%%= csrf_meta_tags %> + <%%= csp_meta_tag %> + + <%%= stylesheet_link_tag 'application', media: 'all' %> + <%%= javascript_include_tag 'application', media: 'all' %> + + + + <%%= yield %> + + diff --git a/app/views/layouts/react.html.erb.tt b/app/views/layouts/react.html.erb.tt new file mode 100644 index 0000000..6173ca1 --- /dev/null +++ b/app/views/layouts/react.html.erb.tt @@ -0,0 +1,11 @@ + + + + <%= app_name %> + <%%= csrf_meta_tags %> + <%%= csp_meta_tag %> + + <%%= stylesheet_link_tag 'application', media: 'all' %> + <%%= javascript_pack_tag 'application' %> + + diff --git a/config/routes.rb b/config/routes.rb index 9f101e0..3047b96 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,31 +1,53 @@ insert_into_file 'config/routes.rb', before: /^end/ do if use_active_admin <<-'RUBY' + authenticate :admin_user do require 'sidekiq/web' mount Sidekiq::Web => '/sidekiq' end - - namespace :api do - - end - - %w( 404 422 500 ).each do |code| - get code, :to => "errors#show", :code => code - end RUBY else <<-'RUBY' + require 'sidekiq/web' mount Sidekiq::Web => '/sidekiq' + RUBY + end +end +insert_into_file 'config/routes.rb', before: /^end/ do + <<-'RUBY' + namespace :api do + get '/home/index' => 'home#index' + end + RUBY +end + +insert_into_file 'config/routes.rb', before: /^end/ do + if use_react + <<-'RUBY' + + namespace :app do + get '/' => 'home#index' + end + # To render react packs for any path except app/api + scope '/:path', constraints: { path: /.+/ } do + get '/' => 'react#index', as: :react # react_path end + RUBY + end +end + +insert_into_file 'config/routes.rb', before: /^end/ do + <<-'RUBY' + + root 'home#index' %w( 404 422 500 ).each do |code| get code, :to => "errors#show", :code => code end RUBY - end -end \ No newline at end of file +end diff --git a/config/template.rb b/config/template.rb index f03a0e6..54327d2 100644 --- a/config/template.rb +++ b/config/template.rb @@ -6,4 +6,4 @@ template "config/database.yml.tt", force: true copy_file 'config/initializers/sidekiq.rb' copy_file 'config/sidekiq.yml' -copy_file 'config/initializers/slack.rb' if slack_notification == 'yes' \ No newline at end of file +copy_file 'config/initializers/slack.rb' if use_slack_notification \ No newline at end of file diff --git a/lib/template.rb b/lib/template.rb index 8aa4964..1cf8dcf 100644 --- a/lib/template.rb +++ b/lib/template.rb @@ -1,5 +1,9 @@ copy_file 'lib/tasks/deploy.rake' copy_file 'lib/tasks/hot.rake' -template 'lib/generators/stimulus/templates/controller.js.erb.tt' -copy_file 'lib/generators/stimulus/stimulus_generator.rb' -copy_file 'lib/generators/stimulus/USAGE' +if use_react + +else + template 'lib/generators/stimulus/templates/controller.js.erb.tt' + copy_file 'lib/generators/stimulus/stimulus_generator.rb' + copy_file 'lib/generators/stimulus/USAGE' +end diff --git a/template.rb b/template.rb index 27e1170..e45b676 100644 --- a/template.rb +++ b/template.rb @@ -2,12 +2,14 @@ require "shellwords" require "tmpdir" -RAILS_REQUIREMENT = "~> 6.0.0.rc1".freeze +RAILS_REQUIREMENT = "~> 6.0.0".freeze def apply_template! + assert_minimum_rails_version assert_postgresql add_template_repository_to_source_path + ask_questions # Ask if using React template "Gemfile.tt", force: true @@ -18,17 +20,31 @@ def apply_template! run "gem install bundler -v '~> 2.0.0' --no-document --conservative" run "bundle install" git_commit("Gemfile setup") + rails_command("webpacker:install") - rails_command("webpacker:install:stimulus") - git_commit("Webpacker and Stimulus installed") + if use_react + rails_command("webpacker:install:react") + git_commit("Webpacker and React installed") + npms = ["axios", "hookrouter", "eslint-plugin-react-hooks --dev"] + else + rails_command("webpacker:install:stimulus") + git_commit("Webpacker and Stimulus installed") + npms = %w(axios stimulus @stimulus/polyfills) + end + + run "yarn add #{npms.join(' ')}" + if use_react + run "yarn remove turbolinks" + end + git_commit("Yarn installed") + rails_command("generate rspec:install") run "bundle exec guard init" run "guard init livereload" git_commit("Rspec & Guard setup") - npms = %w(axios stimulus @stimulus/polyfills) - run "yarn add #{npms.join(' ')}" - git_commit("Yarn installed") + apply 'app/template.rb' + run "rm app/assets/stylesheets/application.css" git_commit("app/* setup") apply 'lib/template.rb' git_commit("lib/* setup") @@ -38,7 +54,7 @@ def apply_template! template 'docker-compose.yml' git_commit("docker/* setup") rails_command("db:create") - if use_active_admin == 'yes' + if use_active_admin run "rails generate devise:install" rails_command("db:migrate") run "rails generate active_admin:install AdminUser" @@ -90,6 +106,15 @@ def gemfile_requirement(name) req && req.gsub("'", %(")).strip.sub(/^,\s*"/, ', "') end +def ask_questions + use_react + use_active_admin + use_slack_notification + git_repo_url + app_domain + admin_email +end + def git_repo_url @git_repo_url ||= ask_with_default("What is the git remote URL for this project?", :blue, "skip") end @@ -102,14 +127,22 @@ def admin_email @admin_email ||= ask_with_default("What is the admin's email address? (for SSL Certificate)", :blue, "admin@example.com") end -def slack_notification - @slack_notification ||= ask_with_default("Would you like to use Slack as a notification service?", :blue, "yes") +def use_slack_notification + @use_slack_notification ||= ask_with_default("Would you like to use Slack as a notification service?", :blue, "yes") + @use_slack_notification == "yes" end def use_active_admin @use_active_admin ||= ask_with_default("Would you like to use ActiveAdmin as admin?", :blue, "yes") + @use_active_admin == "yes" +end + +def use_react + @use_react ||= ask_with_default("Would you like to use React as front-end? (if not stimulus.js will be installed)", :blue, "yes") + @use_react == "yes" end + def ask_with_default(question, color, default) return default unless $stdin.tty? question = (question.split("?") << " [#{default}]?").join @@ -130,13 +163,14 @@ def git_commit(msg) git add: "-A ." git commit: "-n -m '#{msg}'" + puts set_color msg, :green end def git_push git :init unless preexisting_git_repo? git add: "-A ." - git commit: "-n -m 'Project initalized'" + git commit: "-n -m 'initializing project'" if git_repo_specified? git remote: "add origin #{git_repo_url.shellescape}" git remote: "add upstream #{git_repo_url.shellescape}" @@ -144,10 +178,11 @@ def git_push end end -def run_bundle - run 'bin/spring stop' - p "Template setted." - return +def run_bundle; end +def run_webpack + puts set_color "All set!====================", :green + puts set_color "Start by running 'rails hot'", :green + puts set_color "============================", :green end apply_template!