Skip to content

Instantly share code, notes, and snippets.

@0xjmp
Last active July 18, 2020 15:21
Show Gist options
  • Save 0xjmp/5b4b9f9c699576b4c2eb393fec9d6fe6 to your computer and use it in GitHub Desktop.
Save 0xjmp/5b4b9f9c699576b4c2eb393fec9d6fe6 to your computer and use it in GitHub Desktop.
How to set up a Rails and Webpack app for fun and profit
error_log /freedom_project/nginx/error.log;
worker_rlimit_nofile 8192;
events {
worker_connections 1024;
}
http {
access_log /freedom_project/nginx/access.log;
client_max_body_size 128M;
proxy_max_temp_file_size 0;
proxy_buffering off;
server_names_hash_bucket_size 256;
server {
listen 80;
server_name freedom.com www.freedom.com;
location / {
proxy_pass http://localhost:8888/;
}
location /api {
proxy_pass http://localhost:5000;
}
}
}

Overview

I encourage you to NOT use my webpack.*.js files and rely on webpack init for your front-end because the files above have a lot of added plugins that are specific to the project I copied from. That command will give you a working webpack.config.js for your local and production environment to start with

At the very least, they can hopefully show you that you don't need to do anything particularly fancy in order to make this all work.

Your rails app is just a rails app. Your webpack app is just a webpack app. They are conjoined in development by nginx. No env vars or CORS config required.

You can run everything at once with: foreman start.

File Structure

nginx.conf
nginx/error.log
nginx/access.log
client/webpack.config
client/public/
api/Rakefile
Procfile

Deployment

This comes down to where you are deploying to (AWS, Heroku, etc). All you need to understand is that npm run build:production produces a public/ folder. All files there are static and just need to be served to your users. https://github.com/heroku/heroku-buildpack-static.git is great for this. Here is my static.json file:

{
  "root": "public/",
  "canonical_host": "www.freedom.com",
  "clean_urls": true,
  "headers": {
    "/": {
      "Cache-Control": "no-store, no-cache"
    },
    "/**.xml": {
      "Content-Type": "application/xml"
    }
  },
  "https_only": true,
  "proxies": {
    "/api/": {
      "origin": "https://${API_APP_NAME}.herokuapp.com/api/"
    }
  }
}

Again, it uses nginx under the hood so no special CORS configuration needed! API_APP_NAME is just a bash environment variable. It should point to where you are hosting your rails API, whether that's in another heroku app like above, or a different url.

api: sh -c "cd api && bundle exec rails s -p5000"
nginx: /usr/bin/nginx -c nginx.conf
client: sh -c "cd client && npm start"
"use strict";
const webpack = require('webpack');
const path = require('path');
const loadersConf = require('./webpack.loaders');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HOST = process.env.HOST || "127.0.0.1";
const PORT = process.env.PORT || "8888";
module.exports = {
mode: 'development',
entry: [
// POLYFILL: Set up an ES6-ish environment
// 'babel-polyfill', // The entire babel-polyfill
// Or pick es6 features needed (included into babel-polyfill)
'core-js/fn/promise',
'core-js/es6/object',
'core-js/es6/array',
'./src/index.jsx', // your app's entry point
],
devtool: process.env.WEBPACK_DEVTOOL || 'eval-source-map',
output: {
publicPath: '/',
path: path.join(__dirname, 'public'),
filename: 'bundle.js'
},
module: {
rules: loadersConf
},
resolve: {
extensions: ['.js', '.jsx'],
modules: [
path.join(__dirname, "src"),
path.join(__dirname, "node_modules"), // the old 'fallback' option (needed for npm link-ed packages)
],
alias: {
"styles": path.resolve(__dirname, 'styles/'),
}
},
devServer: {
contentBase: "./public",
// do not print bundle build stats
noInfo: true,
// enable HMR
hot: true,
// embed the webpack-dev-server runtime into the bundle
inline: true,
// serve index.html in place of 404 responses to allow HTML5 history
historyApiFallback: true,
port: PORT,
host: HOST
},
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin(),
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
ignoreOrder: true,
}),
new HtmlWebpackPlugin({
template: './src/template.html',
files: {
css: ['style.css'],
js: [ "bundle.js"],
chunks: {
head: {
entry: '',
css: 'style.css',
},
main: {
entry: 'bundle.js',
css: []
},
}
}
}),
new FaviconsWebpackPlugin('./src/images/favicon.ico'),
]
};
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// Options passed to node-sass
const sassIncludePaths = [
path.resolve(__dirname, 'styles'),
];
// These files will be imported in every sass file
const sassResourcesPaths = [
path.resolve(__dirname, 'styles/abstracts/_variables.sass'),
path.resolve(__dirname, 'styles/abstracts/_mixins.sass'),
];
// noinspection WebpackConfigHighlighting
module.exports = [
// =========
// = Babel =
// =========
// Load jsx extensions with babel so we can use
// 'import' instead of 'require' and es6 syntax
{
test: /\.jsx?$/,
include: path.resolve(__dirname, 'src'),
loader: "babel-loader",
options: {
// This is a feature of `babel-loader` for Webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
plugins: ['react-hot-loader/babel'],
}
},
// =========
// = Fonts =
// =========
{
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
exclude: path.resolve(__dirname, "node_modules"),
use: ["file-loader"]
},
{
test: /\.(woff|woff2)$/,
exclude: path.resolve(__dirname, "node_modules"),
use: [
{
loader: "url-loader",
options: {prefix: "font", limit: 5000}
}
]
},
{
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
exclude: path.resolve(__dirname, "node_modules"),
use: [
{
loader: "url-loader",
options: {
prefix: "font",
limit: 10000,
mimetype: "application/octet-stream"
}
}
]
},
// ==========
// = Images =
// ==========
{
test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
exclude: path.resolve(__dirname, "node_modules"),
use: [
{
loader: "url-loader",
options: {
limit: 10000,
mimetype: "image/svg+xml"
}
}
]
},
{
test: /\.(jpg|png|gif|svg)$/,
loader: 'image-webpack-loader',
enforce: 'pre'
},
{
test: /\.(png|jpg|gif)$/i,
exclude: path.resolve(__dirname, "node_modules"),
use: [
{
loader: "url-loader",
options: {
limit: 1,
name: "[path][name].[contenthash].[ext]"
}
}
]
},
// ==========
// = Videos =
// ==========
{
test: /\.mp4$/,
use: 'file-loader?name=videos/[name].[contenthash].[ext]',
},
// ==========
// = Styles =
// ==========
// Global CSS (from node_modules)
// ==============================
{
test: /\.css$/,
include: path.resolve(__dirname, "node_modules"),
use: [
{
loader: "style-loader"
},
{
loader: 'css-loader'
}
]
},
// SASS (from app)
{
test: /\.(sass|scss)$/,
include: path.resolve(__dirname, 'styles/base'),
use: [
{
loader: MiniCssExtractPlugin.loader,
},
{
loader: "css-loader",
options: {
sourceMap: true,
camelCase: "dashes",
importLoaders: 1
}
},
{
loader: "postcss-loader",
options: {
sourceMap: "inline",
}
},
{
loader: "sass-loader",
options: {
sourceMap: true,
sassOptions: {
includePaths: sassIncludePaths,
indentedSyntax: "sass",
outputStyle: "expanded",
},
}
},
{
loader: "sass-resources-loader",
options: {
resources: sassResourcesPaths
}
}
]
},
];
var webpack = require('webpack');
var path = require('path');
var loaders = require('./webpack.loaders');
var TerserJSPlugin = require('terser-webpack-plugin');
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
var OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
var FaviconsWebpackPlugin = require('favicons-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
mode: 'production',
entry: [
'./src/index.jsx'
],
output: {
publicPath: './',
path: path.join(__dirname, 'public'),
filename: '[name].[contenthash].js'
},
resolve: {
extensions: ['.js', '.jsx'],
alias: {
"styles": path.resolve(__dirname, 'styles/'),
}
},
module: {
rules: loaders
},
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
moduleIds: 'hashed',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true,
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
plugins: [
new WebpackCleanupPlugin(),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.OccurrenceOrderPlugin(),
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].[contenthash].css',
ignoreOrder: true,
}),
new HtmlWebpackPlugin({
template: './src/template.html',
files: {
css: ['style.css'],
js: ['bundle.js'],
}
}),
new FaviconsWebpackPlugin('./src/images/favicon.ico'),
new CopyWebpackPlugin([
{ from: './src/sitemap.xml', to: 'sitemap.xml' },
{ from: './src/robots.txt', to: 'robots.txt' },
]),
]
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment