├── frontend ├── src │ ├── shared │ │ ├── action-types.js │ │ └── api-client.js │ ├── constants │ │ ├── config.js │ │ └── action-types.js │ ├── index.js │ ├── selectors │ │ └── app.js │ ├── components │ │ └── sample.js │ ├── containers │ │ ├── app.css │ │ └── app.js │ ├── routes │ │ └── index.js │ ├── reducers │ │ └── app.js │ ├── actions │ │ └── app.js │ ├── store │ │ └── index.js │ └── registerServiceWorker.js ├── .dockerignore ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── Dockerfile ├── .babelrc ├── config │ ├── jest │ │ ├── fileTransform.js │ │ └── cssTransform.js │ ├── polyfills.js │ ├── paths.js │ ├── env.js │ ├── webpackDevServer.config.js │ ├── webpack.config.dev.js │ └── webpack.config.prod.js ├── .gitignore ├── scripts │ ├── test.js │ ├── start.js │ └── build.js └── package.json ├── backend ├── .rspec ├── app │ ├── views │ │ └── layouts │ │ │ ├── mailer.text.erb │ │ │ └── mailer.html.erb │ ├── jobs │ │ ├── application_job.rb │ │ └── test_job.rb │ ├── models │ │ ├── application_record.rb │ │ └── user.rb │ ├── channels │ │ └── application_cable │ │ │ ├── channel.rb │ │ │ └── connection.rb │ ├── mailers │ │ └── application_mailer.rb │ └── controllers │ │ ├── application_controller.rb │ │ └── api │ │ └── status_controller.rb ├── public │ └── robots.txt ├── bin │ ├── bundle │ ├── rake │ ├── rails │ ├── spring │ ├── update │ └── setup ├── config │ ├── boot.rb │ ├── spring.rb │ ├── environment.rb │ ├── initializers │ │ ├── mime_types.rb │ │ ├── filter_parameter_logging.rb │ │ ├── application_controller_renderer.rb │ │ ├── backtrace_silencers.rb │ │ ├── wrap_parameters.rb │ │ ├── cors.rb │ │ ├── inflections.rb │ │ └── devise_token_auth.rb │ ├── cable.yml │ ├── routes.rb │ ├── locales │ │ └── en.yml │ ├── application.rb │ ├── secrets.yml │ ├── environments │ │ ├── development.rb │ │ ├── test.rb │ │ └── production.rb │ ├── puma.rb │ └── database.yml ├── .dockerignore ├── config.ru ├── docker-entrypoint.sh ├── Rakefile ├── db │ ├── seeds.rb │ ├── migrate │ │ └── 20180306161431_devise_token_auth_create_users.rb │ └── schema.rb ├── .gitignore ├── Dockerfile ├── Gemfile ├── spec │ └── spec_helper.rb └── Gemfile.lock ├── .gitignore ├── CONTRIBUTING.md ├── config └── nginx │ └── nginx.conf ├── docker-compose.yml └── README.md /frontend/src/shared/action-types.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /backend/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /frontend/src/constants/config.js: -------------------------------------------------------------------------------- 1 | export const baseApiUri = '/api'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | terraform.tfvars 2 | .terraform 3 | *.tfstate* 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea 3 | node_modules 4 | build 5 | npm-debug.log* 6 | npm-error.log* -------------------------------------------------------------------------------- /backend/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | queue_as :default 3 | end 4 | -------------------------------------------------------------------------------- /backend/app/jobs/test_job.rb: -------------------------------------------------------------------------------- 1 | class TestJob < ApplicationJob 2 | def perform 3 | # Do some work 4 | end 5 | end -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaam2/docker-rails-react-starter/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /backend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /backend/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /backend/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /backend/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /backend/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /backend/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /backend/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /backend/config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | tmp/* 2 | log/* 3 | db/*.sqlite3 4 | .git 5 | .byebug_history 6 | 7 | # Dont copy the docker ignore to the VD. Makes no sense 8 | .dockerignore -------------------------------------------------------------------------------- /backend/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /backend/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ -f tmp/pids/server.pid ]; then 5 | rm tmp/pids/server.pid 6 | fi 7 | 8 | exec bundle exec "$@" 9 | -------------------------------------------------------------------------------- /backend/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::API 2 | include DeviseTokenAuth::Concerns::SetUserByToken 3 | end 4 | -------------------------------------------------------------------------------- /backend/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:9.4.0 2 | WORKDIR /usr/src/frontend-app 3 | COPY package*.json ./ 4 | RUN yarn install 5 | COPY . . 6 | EXPOSE 3000 7 | 8 | CMD ["yarn", "start"] -------------------------------------------------------------------------------- /backend/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /frontend/src/constants/action-types.js: -------------------------------------------------------------------------------- 1 | export const ACTION_TYPES = { 2 | APP_LOAD: 'APP_LOAD', 3 | API_STATUS_CHECK_SUCCESS: 'API_STATUS_CHECK_SUCCESS', 4 | API_STATUS_CHECK_FAILURE: 'API_STATUS_CHECK_FAILURE' 5 | }; -------------------------------------------------------------------------------- /backend/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | channel_prefix: backend_production 11 | -------------------------------------------------------------------------------- /backend/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /backend/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /backend/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /backend/config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /backend/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | # Include default devise modules. 3 | devise :database_authenticatable, :registerable, 4 | :recoverable, :rememberable, :trackable, :validatable, 5 | :confirmable, :omniauthable 6 | include DeviseTokenAuth::Concerns::User 7 | end 8 | -------------------------------------------------------------------------------- /backend/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /backend/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react", 4 | "babel-preset-env", 5 | "stage-0" 6 | ], 7 | "plugins": [ 8 | "transform-runtime", 9 | "add-module-exports", 10 | "transform-decorators-legacy" 11 | ], 12 | "env": { 13 | "development": { 14 | "plugins": [] 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /backend/config/routes.rb: -------------------------------------------------------------------------------- 1 | require 'sidekiq/web' 2 | 3 | Rails.application.routes.draw do 4 | root to: 'api/status#index' 5 | 6 | namespace :api do 7 | mount_devise_token_auth_for 'User', at: 'auth' 8 | 9 | namespace :jobs do 10 | mount Sidekiq::Web => '/ui' 11 | end 12 | 13 | get '/', to: 'status#index' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /frontend/config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /backend/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /frontend/config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | import store from './store'; 8 | import routes from './routes'; 9 | 10 | render( 11 | {routes}, 12 | document.getElementById('root') 13 | ); 14 | registerServiceWorker(); 15 | -------------------------------------------------------------------------------- /backend/app/controllers/api/status_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::StatusController < ApplicationController 2 | def index 3 | render json: { status: "OK" } 4 | end 5 | 6 | def job 7 | # Just an example of enqueuing an ActiveJob to the worker container. 8 | # perform_later will cause it to be enqueud to the worker 9 | # whilst perform will cause it to be executed in the current Rails process 10 | TestJob.perform_later 11 | end 12 | end -------------------------------------------------------------------------------- /backend/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /frontend/src/selectors/app.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import _ from 'lodash'; 3 | 4 | export const getAppStore = store => store.app; 5 | 6 | export const getApiStatus = createSelector( 7 | [getAppStore], 8 | (appStore) => { 9 | return _.get(appStore, 'apiStatus'); 10 | } 11 | ); 12 | 13 | export const getLoadedStatus = createSelector( 14 | [getAppStore], 15 | (appStore) => { 16 | return _.get(appStore, 'loaded'); 17 | } 18 | ); -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the boilerplate 2 | 3 | Please clone this repo, and follow the setup steps to get the project up and running. 4 | 5 | If you would like to add a feature, then generally I'll be all for it. But ensure that your feature is as general purpose and universal as possible. This project is intended to be a starter kit, so I want to keep it simple. 6 | 7 | Once you think you have completed your work, please submit a pull request from your branch to this repo. 8 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all logfiles and tempfiles. 11 | /log/* 12 | /tmp/* 13 | 14 | .byebug_history 15 | -------------------------------------------------------------------------------- /frontend/src/components/sample.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Sample extends React.Component { 4 | render() { 5 | return ( 6 |
7 |

8 | Information 9 |

10 |

11 | The Rails application can be found namespaced at http://localhost:8080/api.
The root route is the API status page. 12 |

13 |
14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/containers/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: sans-serif; 4 | text-align: center; 5 | } 6 | 7 | header, main { 8 | padding: 1rem; 9 | } 10 | 11 | header { 12 | background: #333; 13 | min-height: 100px; 14 | color: #fff; 15 | } 16 | 17 | p { 18 | line-height: 1.5; 19 | } 20 | 21 | .StatusPill { 22 | border-radius: 8px; 23 | padding: 0.25rem 0.5rem; 24 | width: 80px; 25 | text-align: center; 26 | margin-left: 15px; 27 | color: white; 28 | font-weight: bold; 29 | background: green; 30 | margin: auto 5px; 31 | } -------------------------------------------------------------------------------- /backend/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /backend/bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == "spring" } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /frontend/src/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route } from 'react-router'; 3 | import { ConnectedRouter } from 'connected-react-router'; 4 | import { history } from '../store'; 5 | 6 | // Layouts 7 | import App from '../containers/app'; 8 | 9 | // Components 10 | import Test from '../components/sample'; 11 | 12 | const routes = ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | 22 | export default routes; -------------------------------------------------------------------------------- /backend/config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Avoid CORS issues when API is called from the frontend app. 4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 5 | 6 | # Read more: https://github.com/cyu/rack-cors 7 | 8 | # Rails.application.config.middleware.insert_before 0, Rack::Cors do 9 | # allow do 10 | # origins 'example.com' 11 | # 12 | # resource '*', 13 | # headers: :any, 14 | # methods: [:get, :post, :put, :patch, :delete, :options, :head] 15 | # end 16 | # end 17 | -------------------------------------------------------------------------------- /frontend/src/reducers/app.js: -------------------------------------------------------------------------------- 1 | import { ACTION_TYPES } from '../constants/action-types'; 2 | 3 | const initialState = { 4 | loaded: false, 5 | apiStatus: null 6 | }; 7 | 8 | export default function app(state = initialState, action) { 9 | switch (action.type) { 10 | case ACTION_TYPES.APP_LOAD: 11 | return { ...state, loaded: true }; 12 | case ACTION_TYPES.API_STATUS_CHECK_SUCCESS: 13 | return { ...state, apiStatus: action.status } 14 | case ACTION_TYPES.API_STATUS_CHECK_FAILURE: 15 | return { ...state, apiStatus: "PROBLEMO" } 16 | default: 17 | return state; 18 | } 19 | } -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.3.3 2 | RUN apt-get update -qq && apt-get install -y --no-install-recommends build-essential libpq-dev 3 | RUN apt-get clean 4 | RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 5 | RUN gem install bundler 6 | 7 | RUN mkdir /usr/src/backend-app 8 | WORKDIR /usr/src/backend-app 9 | 10 | RUN echo "gem: --no-rdoc --no-ri" > /etc/gemrc 11 | ADD Gemfile /usr/src/backend-app/Gemfile 12 | ADD Gemfile.lock /usr/src/backend-app/Gemfile.lock 13 | RUN bundle install --jobs 20 --retry 5 14 | ADD . /usr/src/backend-app 15 | EXPOSE 3000 16 | 17 | COPY ./docker-entrypoint.sh / 18 | RUN chmod +x /docker-entrypoint.sh 19 | 20 | ENTRYPOINT ["/docker-entrypoint.sh"] 21 | CMD ["rails", "s", "-b", "0.0.0.0"] 22 | -------------------------------------------------------------------------------- /backend/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /frontend/src/actions/app.js: -------------------------------------------------------------------------------- 1 | import { ACTION_TYPES } from '../constants/action-types'; 2 | import ApiClient from '../shared/api-client'; 3 | 4 | export function loadApp() { 5 | return { 6 | type: ACTION_TYPES.APP_LOAD, 7 | }; 8 | } 9 | 10 | function ApiSuccess(status) { 11 | return { 12 | type: ACTION_TYPES.API_STATUS_CHECK_SUCCESS, 13 | status 14 | } 15 | } 16 | 17 | function ApiFailure(err) { 18 | return { 19 | type: ACTION_TYPES.API_STATUS_CHECK_FAILURE, 20 | err 21 | } 22 | } 23 | 24 | const STATUS_URI = "/"; 25 | 26 | export function checkApiStatus() { 27 | return function(dispatch) { 28 | return ApiClient.get(STATUS_URI) 29 | .then((response) => { 30 | dispatch(ApiSuccess(response.body.status)); 31 | }, 32 | (error) => { 33 | dispatch(ApiFailure(error)); 34 | }); 35 | } 36 | } -------------------------------------------------------------------------------- /frontend/scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | const argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /backend/bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /backend/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | git_source(:github) do |repo_name| 4 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 5 | "https://github.com/#{repo_name}.git" 6 | end 7 | 8 | gem 'rails', '~> 5.1.5' 9 | gem 'pg', '>= 0.18', '< 2.0' 10 | gem 'puma', '~> 3.7' 11 | 12 | gem 'devise_token_auth' 13 | gem 'omniauth' 14 | gem 'redis' 15 | gem 'sidekiq' 16 | gem 'connection_pool' 17 | 18 | group :development, :test do 19 | gem 'pry' 20 | gem 'pry-rails' 21 | gem 'rspec' 22 | end 23 | 24 | group :development do 25 | gem 'listen', '>= 3.0.5', '< 3.2' 26 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 27 | gem 'spring' 28 | gem 'spring-watcher-listen', '~> 2.0.0' 29 | end 30 | 31 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 32 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 33 | -------------------------------------------------------------------------------- /frontend/src/shared/api-client.js: -------------------------------------------------------------------------------- 1 | import request from 'superagent'; 2 | import { baseApiUri } from '../constants/config'; 3 | 4 | class ApiClient { 5 | constructor(baseUri) { 6 | this.baseUri = baseUri; 7 | } 8 | 9 | get(path, headers = {}, params = {}) { 10 | return this.sendRequest('GET', path, params); 11 | } 12 | 13 | post(path, headers = {}, body = {}) { 14 | return this.sendRequest('POST', path, body); 15 | } 16 | 17 | delete(path, headers = {}, params = {}) { 18 | return this.sendRequest('DELETE', path, params); 19 | } 20 | 21 | patch(path, headers = {}, body = {}) { 22 | return this.sendRequest('PATCH', path, body); 23 | } 24 | 25 | requestUrl(path) { 26 | return `${this.baseUri}/${path}`; 27 | } 28 | 29 | sendRequest(method, path, headers = {}, params = {}) { 30 | return request(method, this.requestUrl(path), params).set(headers); 31 | } 32 | } 33 | 34 | export default new ApiClient(baseApiUri); -------------------------------------------------------------------------------- /frontend/config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | 18 | // In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet. 19 | // We don't polyfill it in the browser--this is user's responsibility. 20 | if (process.env.NODE_ENV === 'test') { 21 | require('raf').polyfill(global); 22 | } 23 | -------------------------------------------------------------------------------- /config/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | # Similar approach we have in production, except that we do 2 | # not serve static files for the client nor assets, we upstream 3 | # everything 4 | 5 | server { 6 | listen 8080; 7 | server_name yourproject.docker; 8 | 9 | location /api { 10 | proxy_set_header Host $host; 11 | proxy_set_header X-Real-IP $remote_addr; 12 | proxy_pass http://backend:3000; 13 | } 14 | 15 | # Other to frontend 16 | location /sockjs-node { 17 | proxy_set_header X-Real-IP $remote_addr; 18 | proxy_set_header X-Forwarded-For $remote_addr; 19 | proxy_set_header Host $host; 20 | 21 | proxy_pass http://frontend:4000; 22 | 23 | proxy_redirect off; 24 | 25 | proxy_http_version 1.1; 26 | proxy_set_header Upgrade $http_upgrade; 27 | proxy_set_header Connection "upgrade"; 28 | } 29 | 30 | location / { 31 | proxy_set_header Host $host; 32 | proxy_set_header X-Real-IP $remote_addr; 33 | proxy_pass http://frontend:4000; 34 | } 35 | } -------------------------------------------------------------------------------- /backend/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /backend/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | 22 | # puts "\n== Copying sample files ==" 23 | # unless File.exist?('config/database.yml') 24 | # cp 'config/database.yml.sample', 'config/database.yml' 25 | # end 26 | 27 | puts "\n== Preparing database ==" 28 | system! 'bin/rails db:setup' 29 | 30 | puts "\n== Removing old logs and tempfiles ==" 31 | system! 'bin/rails log:clear tmp:clear' 32 | 33 | puts "\n== Restarting application server ==" 34 | system! 'bin/rails restart' 35 | end 36 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | // Create final store using all reducers and applying middleware 2 | import { createBrowserHistory } from 'history'; 3 | // Redux utility functions 4 | import { compose, createStore, combineReducers, applyMiddleware } from 'redux'; 5 | import { routerMiddleware, connectRouter } from 'connected-react-router'; 6 | import thunk from 'redux-thunk'; 7 | 8 | // Import all reducers 9 | import app from '../reducers/app'; 10 | 11 | import logger from 'redux-logger'; 12 | 13 | // Configure reducer to store state at state.router 14 | // You can store it elsewhere by specifying a custom `routerStateSelector` 15 | // in the store enhancer below 16 | export const history = createBrowserHistory(); 17 | const reducer = combineReducers({ app }); 18 | 19 | const middlewares = [ 20 | routerMiddleware(history), 21 | thunk, 22 | logger 23 | ]; 24 | 25 | const store = compose( 26 | // Enables your middleware: 27 | // applyMiddleware(thunk), // any Redux middleware, e.g. redux-thunk 28 | applyMiddleware(...middlewares), 29 | // Provides support for DevTools via Chrome extension 30 | window.devToolsExtension ? window.devToolsExtension() : f => f 31 | )(createStore)(connectRouter(history)(reducer)); 32 | 33 | export default store; -------------------------------------------------------------------------------- /backend/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | require "action_cable/engine" 12 | # require "sprockets/railtie" 13 | 14 | # Require the gems listed in Gemfile, including any gems 15 | # you've limited to :test, :development, or :production. 16 | Bundler.require(*Rails.groups) 17 | 18 | module Backend 19 | class Application < Rails::Application 20 | # Initialize configuration defaults for originally generated Rails version. 21 | config.load_defaults 5.1 22 | 23 | config.active_job.queue_adapter = :sidekiq 24 | 25 | # Settings in config/environments/* take precedence over those specified here. 26 | # Application configuration should go into files in config/initializers 27 | # -- all .rb files in that directory are automatically loaded. 28 | 29 | # Only loads a smaller set of middleware suitable for API only apps. 30 | # Middleware like session, flash, cookies can be added back manually. 31 | # Skip views, helpers and assets when generating a new resource. 32 | config.api_only = true 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /backend/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | # Shared secrets are available across all environments. 14 | 15 | # shared: 16 | # api_key: a1B2c3D4e5F6 17 | 18 | # Environmental secrets are only available for that specific environment. 19 | 20 | development: 21 | secret_key_base: b1c317da8ce5a4da280e2c80f39f0439ce804840332a234d47bd50edd3cc5ba9dec6a3023423d73168c2b9f5886c585e66ddda6aba73051ac1921c758533a847 22 | 23 | test: 24 | secret_key_base: e368e9a07bea06b78aadbf976746caa825688ac667da2706ed6dcd4f7638f288702beb6ee93a704c1b08b9b8921c14e570e9e52dfb886ec23c9e124b8e6cedc6 25 | 26 | # Do not keep production secrets in the unencrypted secrets file. 27 | # Instead, either read values from the environment. 28 | # Or, use `bin/rails secrets:setup` to configure encrypted secrets 29 | # and move the `production:` environment over there. 30 | 31 | production: 32 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 33 | -------------------------------------------------------------------------------- /frontend/src/containers/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { loadApp, checkApiStatus } from '../actions/app'; 4 | import * as AppSelectors from '../selectors/app'; 5 | 6 | // eslint-disable-next-line 7 | import styles from './app.css'; 8 | 9 | type Props = { 10 | dispatch: () => void, 11 | loaded: boolean, 12 | children: array 13 | } 14 | 15 | class App extends React.Component { 16 | props: Props; 17 | 18 | componentDidMount() { 19 | this.props.dispatch(loadApp()); 20 | this.props.dispatch(checkApiStatus()); 21 | } 22 | 23 | render() { 24 | if (!this.props.loaded) { 25 | return ( 26 |
27 | Loading.. 28 |
29 | ); 30 | } 31 | 32 | return ( 33 |
34 |
35 |

React + Redux + Rails + Docker Compose

36 | 37 |

38 | With redux, thunk actions, redux action logging 39 |

40 | 41 |

API Status: 42 | 43 | {this.props.apiStatus} 44 | 45 |

46 |
47 | 48 |
49 | {this.props.children} 50 |
51 |
52 | ) 53 | } 54 | } 55 | 56 | export default connect((store, props) => { 57 | return { 58 | loaded: AppSelectors.getLoadedStatus(store), 59 | apiStatus: AppSelectors.getApiStatus(store) 60 | }; 61 | })(App); -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | redis: 5 | image: redis 6 | volumes: 7 | - ./backend/tmp/redis:/data 8 | ports: 9 | - 6379:6379 10 | worker: 11 | build: backend 12 | command: bundle exec sidekiq 13 | environment: 14 | REDIS_URL: redis://redis:6379/0 15 | volumes: 16 | - ./backend:/usr/src/backend-app 17 | depends_on: 18 | - db 19 | - redis 20 | nginx: 21 | image: bitnami/nginx:1.10.2-r1 22 | volumes: 23 | - ./config/nginx:/bitnami/nginx/conf/vhosts 24 | depends_on: 25 | - backend 26 | - frontend 27 | environment: 28 | VIRTUAL_HOST: yourproject.docker 29 | VIRTUAL_PORT: 8080 30 | ports: 31 | - 8080:8080 32 | db: 33 | image: postgres 34 | volumes: 35 | - /var/lib/postgresql/data 36 | environment: 37 | POSTGRES_USER: postgres 38 | POSTGRES_PASSWORD: secret 39 | ALLOW_IP_RANGE: 0.0.0.0/0 40 | ports: 41 | - 54321:5432 42 | backend: 43 | build: backend 44 | depends_on: 45 | - db 46 | - redis 47 | - worker 48 | volumes: 49 | - ./backend:/usr/src/backend-app 50 | environment: 51 | REDIS_URL: redis://redis:6379/0 52 | SIDEKIQ_REDIS_URL: redis://redis:6379/1 53 | frontend: 54 | build: 55 | context: ./frontend/ 56 | depends_on: 57 | - backend 58 | command: npm start 59 | volumes: 60 | - ./frontend/:/usr/src/frontend-app 61 | - ./frontend/node_modules:/usr/src/frontend-app/node_modules 62 | ports: 63 | - "35729:35729" -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /backend/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}" 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | # Don't care if the mailer can't send. 30 | config.action_mailer.raise_delivery_errors = false 31 | 32 | config.action_mailer.perform_caching = false 33 | 34 | # Print deprecation notices to the Rails logger. 35 | config.active_support.deprecation = :log 36 | 37 | # Raise an error on page load if there are pending migrations. 38 | config.active_record.migration_error = :page_load 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | 43 | # Use an evented file watcher to asynchronously detect changes in source code, 44 | # routes, locales, etc. This feature depends on the listen gem. 45 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 46 | end 47 | -------------------------------------------------------------------------------- /backend/db/migrate/20180306161431_devise_token_auth_create_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table(:users) do |t| 4 | ## Required 5 | t.string :provider, :null => false, :default => "email" 6 | t.string :uid, :null => false, :default => "" 7 | 8 | ## Database authenticatable 9 | t.string :encrypted_password, :null => false, :default => "" 10 | 11 | ## Recoverable 12 | t.string :reset_password_token 13 | t.datetime :reset_password_sent_at 14 | 15 | ## Rememberable 16 | t.datetime :remember_created_at 17 | 18 | ## Trackable 19 | t.integer :sign_in_count, :default => 0, :null => false 20 | t.datetime :current_sign_in_at 21 | t.datetime :last_sign_in_at 22 | t.string :current_sign_in_ip 23 | t.string :last_sign_in_ip 24 | 25 | ## Confirmable 26 | t.string :confirmation_token 27 | t.datetime :confirmed_at 28 | t.datetime :confirmation_sent_at 29 | t.string :unconfirmed_email # Only if using reconfirmable 30 | 31 | ## Lockable 32 | # t.integer :failed_attempts, :default => 0, :null => false # Only if lock strategy is :failed_attempts 33 | # t.string :unlock_token # Only if unlock strategy is :email or :both 34 | # t.datetime :locked_at 35 | 36 | ## User Info 37 | t.string :name 38 | t.string :nickname 39 | t.string :image 40 | t.string :email 41 | 42 | ## Tokens 43 | t.json :tokens 44 | 45 | t.timestamps 46 | end 47 | 48 | add_index :users, :email, unique: true 49 | add_index :users, [:uid, :provider], unique: true 50 | add_index :users, :reset_password_token, unique: true 51 | add_index :users, :confirmation_token, unique: true 52 | # add_index :users, :unlock_token, unique: true 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /backend/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /frontend/config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right