Skip to content

Instantly share code, notes, and snippets.

@aileron
Created November 18, 2024 02:53
Show Gist options
  • Save aileron/8145ff378add31577f3e6b95ad7f6b1a to your computer and use it in GitHub Desktop.
Save aileron/8145ff378add31577f3e6b95ad7f6b1a to your computer and use it in GitHub Desktop.
This script sets up a new Rails project with modern frontend stack including Inertia.js, React, Tailwind CSS, and shadcn/ui components.
#!/bin/bash
# Exit immediately if a command exits with a non-zero status
set -e
# Get directory name from argument, default is "my-app"
APP_NAME=${1:-"my-app"}
# Check if directory already exists
if [ -d "$APP_NAME" ]; then
echo "Error: Directory $APP_NAME already exists."
exit 1
fi
echo "=== Starting setup ==="
echo "Application name: $APP_NAME"
# Create Rails application
rails new $APP_NAME --css=tailwind --database=postgresql --skip-javascript
# Move into the application directory
cd $APP_NAME
# Add gems to Gemfile
cat << EOF >> Gemfile
# Vite
gem 'vite_rails'
# Inertia adapter for Rails
gem 'inertia_rails'
# Development tools
gem 'foreman'
gem 'ridgepole'
EOF
# Install gems
bundle install
# Install Vite
bundle exec vite install
# Setup package.json
npm init -y
npm pkg set type="module"
# Pin Node.js version using Volta
volta pin node@20
volta pin npm@10
# Install NPM packages
volta run npm install \
react \
react-dom \
@inertiajs/react \
@vitejs/plugin-react \
@types/react \
@types/react-dom \
typescript \
autoprefixer \
postcss \
tailwindcss \
class-variance-authority \
clsx \
@radix-ui/react-icons \
tailwind-merge \
lucide-react \
@radix-ui/react-slot \
tailwindcss-animate \
shadcn \
vite \
vite-plugin-ruby
# Create directory structure
mkdir -p app/frontend/{components/ui,pages,lib}
# Create TypeScript config
cat << EOF > tsconfig.json
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["./app/frontend/*"]
}
},
"include": ["app/frontend/**/*.ts", "app/frontend/**/*.tsx"],
"references": [{ "path": "./tsconfig.node.json" }]
}
EOF
cat << EOF > tsconfig.node.json
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
EOF
# Create Vite config
cat << EOF > vite.config.ts
import { defineConfig } from 'vite'
import RubyPlugin from 'vite-plugin-ruby'
import react from '@vitejs/plugin-react'
import path from 'path'
import tailwindcss from 'tailwindcss'
import autoprefixer from 'autoprefixer'
export default defineConfig({
plugins: [
RubyPlugin(),
react()
],
resolve: {
alias: {
'@': path.resolve(__dirname, './app/frontend')
}
},
css: {
postcss: {
plugins: [
tailwindcss,
autoprefixer
]
}
}
})
EOF
# Create Vite Ruby config
cat << EOF > config/vite.json
{
"all": {
"sourceCodeDir": "app/frontend",
"watchAdditionalPaths": []
}
}
EOF
# Create utility functions
cat << EOF > app/frontend/lib/utils.ts
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
EOF
# Create React entry point
cat << EOF > app/frontend/entrypoints/application.ts
import './application.css'
import './application.tsx'
EOF
cat << EOF > app/frontend/entrypoints/application.tsx
import React from 'react'
import { createRoot } from 'react-dom/client'
import { createInertiaApp } from '@inertiajs/react'
document.addEventListener('DOMContentLoaded', () => {
createInertiaApp({
resolve: name => {
const pages = import.meta.glob<any>('../pages/**/*.tsx', { eager: true })
return pages[\`../pages/\${name}.tsx\`]
},
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />)
},
})
})
EOF
# Create Home page
cat << EOF > app/frontend/pages/Home.tsx
import React from 'react'
import { Button } from "@/components/ui/button";
interface HomeProps {
message?: string
}
export default function Home({ message = "Welcome" }: HomeProps) {
return (
<div className="p-8">
<h1 className="text-3xl font-bold text-blue-600">
{message}!
</h1>
<p className="mt-4">
Built with Rails + Inertia + React + Tailwind CSS + shadcn/ui
</p>
<Button
onClick={() => alert('TEST')}
variant="outline"
className="w-full"
>
TEST
</Button>
</div>
)
}
EOF
# Create CSS file
cat << EOF > app/frontend/entrypoints/application.css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
EOF
# Create layout directory and base layout
mkdir -p app/views/layouts
cat << EOF > app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>Taskmaster</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= vite_client_tag %>
<%= vite_javascript_tag 'application.ts' %>
</head>
<body>
<%= yield %>
</body>
</html>
EOF
# Create PostCSS config
cat << EOF > postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
EOF
# Create Tailwind config
cat << EOF > tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ["class"],
content: [
'./app/views/**/*.erb',
'./app/frontend/**/*.{js,jsx,ts,tsx}'
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}
EOF
# Create Procfile.dev
cat << EOF > Procfile.dev
web: bin/rails server -p 3000
vite: bin/vite dev
EOF
# Generate controller
rails generate controller home index --skip-routes
# Update routes
echo "Rails.application.routes.draw do
root 'home#index'
end" > config/routes.rb
# Update ApplicationController
cat << EOF > app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include InertiaRails::Controller
end
EOF
# Update HomeController
cat << EOF > app/controllers/home_controller.rb
class HomeController < ApplicationController
def index
render inertia: 'Home', props: {
message: 'Welcome'
}
end
end
EOF
# Create components.json for shadcn/ui CLI
cat << EOF > components.json
{
"\$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/frontend/entrypoints/application.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
EOF
# Initialize git
git init
git add .
git commit -m "Initial commit: Rails + Vite + React + TypeScript + shadcn/ui"
echo "=== Setup completed! ==="
echo "To start your application:"
echo "1. Set up your database:"
echo " rails db:create"
echo ""
echo "2. To add shadcn ui components, use:"
echo " Example: npx shadcn add button"
echo ""
echo "3. Check build and server startup"
echo " bin/vite build --clear --mode=development"
echo " bin/rails s"
echo ""
echo "4. Start the development server:"
echo " foreman start -f Procfile.dev"
echo ""
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment