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 (
+
+ );
+}
+
+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 @@
+
+
+
+ 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!