module SvelteOnRails
module Installer
module Utils
def self.install_npm_package
package_name = "@csedl/svelte-on-rails@latest"
puts "Installing #{package_name} via npm..."
if system("npm install #{package_name}")
puts "#{package_name} successfully installed."
else
abort "Failed to install #{package_name}. Please ensure npm is installed and try running 'npm install #{package_name}' manually."
end
end
def self.install_turbo
pkg_js = Rails.root.join("package.json")
package_name = "@hotwired/turbo-rails"
file_content = File.exist?(pkg_js) ? File.read(pkg_js) : ""
if file_content.match?(/#{package_name}/)
puts "#{package_name} is already present in package.json, assuming that it is set up well and working."
else
puts "Installing #{package_name} via npm..."
if system("npm install #{package_name}")
puts "#{package_name} successfully installed."
add_line_to_file(Rails.root.join("app", "frontend", "entrypoints", "application.js"), "import '#{package_name}';")
else
abort "Failed to install #{package_name}. Please ensure npm is installed and try running 'npm install #{package_name}' manually."
end
end
end
def self.create_folder(folder)
if Dir.exist?(folder)
puts "Folder already exists: #{folder}"
else
FileUtils.mkdir_p(folder)
puts "Created folder: #{folder}"
end
end
def self.add_line_to_file(file_path, line)
file_content = File.exist?(file_path) ? File.read(file_path) : ""
if file_content.match?(/#{line}/)
puts "#{line} already present in #{file_path}, nothing changed here."
return
end
File.open(file_path, 'a') do |file|
file.puts(line)
end
puts "added #{line} to #{file_path}."
rescue StandardError => e
puts "Error: #{e.message}"
end
def self.create_file(file_path)
unless File.exist?(file_path)
FileUtils.touch(file_path)
puts "Created empty file at file://#{file_path}"
end
end
def self.create_javascript_initializer
config_path = Rails.root.join("app", "frontend", "initializers", "svelte.js")
if File.exist?(config_path)
puts "Initializer already exists: file://#{config_path}"
else
File.write(config_path, <<~JAVASCRIPT)
import { initializeSvelteComponents, cleanupSvelteComponents } from '@csedl/svelte-on-rails';
const components = import.meta.glob('/javascript/components/**/*.svelte', { eager: true });
const componentsRoot = '/javascript/components';
// Initialize Svelte components
initializeSvelteComponents(componentsRoot, components, true);
// Turbo event listener for page load
document.addEventListener('turbo:load', () => {
initializeSvelteComponents(componentsRoot, components, true);
});
// Turbo event listener for cleanup before page cache
document.addEventListener('turbo:before-cache', () => {
cleanupSvelteComponents(false);
});
JAVASCRIPT
puts "Created initializer file at file://#{config_path}"
end
end
def self.create_svelte_hello_world
file_path = Rails.root.join("app", "frontend", "javascript", "components", "HelloWorld.svelte")
if File.exist?(file_path)
puts "Hello World file already exists: file://#{file_path}"
else
File.write(file_path, <<~HTML)
<script>
export let items
let count = 0;
function increment() {
count += 1;
}
</script>
<h1>Greetings from svelte</h1>
<button on:click={increment}>Increment: {count}</button>
<ul>
{#each items as item}
<li>{item}</li>
{/each}
</ul>
<style>
button {
background-color: darkred;
color: white;
padding: 10px;
border: none;
}
</style>
HTML
puts "Hello World file at file://#{file_path}"
end
end
def self.run_command(command)
Dir.chdir(Rails.root) do
stdout, stderr, status = Open3.capture3(command)
if stderr.present?
puts "Error running command «#{command}»:"
raise stderr
else
puts "#{command} => Success"
end
end
end
def self.check_file_exists(file_path)
unless File.exist?(file_path)
raise "ERROR: File not found: #{file_path}"
end
end
def self.check_folder_exists(folder_path)
unless File.exist?(folder_path)
raise "ERROR: Folder not found: #{folder_path}"
end
end
def self.check_file_not_exists(file_path)
if File.exist?(file_path)
raise "ERROR: File already exists: #{file_path}"
end
end
def self.check_folder_not_exists(folder_path)
if File.exist?(folder_path)
raise "ERROR: Folder already exists: #{folder_path}"
end
end
def self.write_templates(templates, ask_for_overwrite: true, app_root: nil, silent: false)
paths = template_paths(templates, app_root: app_root)
existing = paths.dup.select { |p| File.exist?(p[2]) }
if existing.present? && ask_for_overwrite
begin
puts "#{'File'.pluralize(existing.length)} already exists:\n#{existing.map { |p| p[1] }.join("\n")}.\nOverwrite? (y/n)"
continue = STDIN.gets.chomp.downcase[0]
end until ['y', 'n'].include?(continue)
if continue == 'n'
puts "Skipping write #{'template'.pluralize(templates.length)}."
return
end
end
paths.each do |p|
v = (File.exist?(p[2]) ? 'replaced' : 'created')
FileUtils.mkdir_p(File.dirname(p[2]))
FileUtils.cp(p.first, p[2])
puts "#{v}: #{p[1]}" unless silent
end
end
def self.template_paths(templates, app_root: nil)
paths = []
app_root = app_root_path(app_root)
templates.each do |t|
templates_folder = File.expand_path("../../../templates", __dir__)
template_root = templates_folder + '/' + t
raise "ERROR: Template «#{t}» not found:\n«#{template_root}»" unless File.directory?(template_root)
files = Dir.glob(template_root + '/**/*').select { |e| File.file? e }
files.each do |f|
paths.push(
[
f,
f.gsub(template_root + '/', ''),
app_root.join(f.gsub(template_root + '/', ''))
]
)
end
end
paths
end
def self.ask_yn(question)
begin
puts "#{question} (y/n)"
continue = STDIN.gets.chomp.downcase[0]
end until ['y', 'n'].include?(continue)
continue == 'y'
end
def self.which_root_route(app_root = nil)
# Check if the root route is active (uncommented) or commented out
routes = File.read(app_root_path(app_root).join('config', 'routes.rb'))
m = routes.match(/^\s*root\s+['"]([^'"]+)['"]/m)
if m
m.to_s.match(/^\s*root\s*['"]([^'"]*)['"]/)[1]
end
end
def self.add_route(route, app_root: nil)
file_path = app_root_path(app_root).join('config/routes.rb')
# Read the file content
content = File.read(file_path)
# Split content into lines
lines = content.lines
# Find the index of Rails.application.routes.draw do
ind = -1
lines.each_with_index do |line, i|
if line.match?(/^\s*Rails.application.routes.draw\s*do[\s\S]+$/)
ind = i
end
end
# Insert
if ind >= 0
lines.insert(ind + 1, route)
else
raise "ERROR: Could not find Rails.application.routes.draw do"
end
# Write the modified content back to the file
begin
File.write(file_path, lines.map { |l| l.gsub(/\n/, '') }.join("\n"))
puts "Successfully inserted «root '#{route}'» into '#{file_path}'"
rescue => e
raise "Error writing to #{file_path} => «#{e.message}»"
end
end
def self.route_exists?(target_route)
# check if exists
# Ensure the Rails environment is loaded
# require File.expand_path("../config/environment", __dir__)
# Get all routes from the Rails application
routes = Rails.application.routes.routes
# Check if the route exists
routes.any? do |route|
# Extract the path spec and remove any optional parts or constraints
path = route.path.spec.to_s
# Clean up the path to match the format (remove leading/trailing slashes, etc.)
cleaned_path = path.gsub(/\(.*\)/, "").gsub(/^\/|\/$/, "")
# Check if the cleaned path matches the target route
cleaned_path == target_route
end
end
def self.remove_files(file_paths)
file_paths.each do |f|
if File.exist?(f)
if File.directory?(f)
Dir.delete(f)
puts " • Removed directory: #{f}"
else
File.delete(f)
puts " • Removed file: #{f}"
end
else
puts " • File/Path not found so not removed: #{f}"
end
end
end
def self.remove_line_from_file(file_path, string_to_find, force: false)
# Read the file content
content = File.read(file_path)
# Split content into lines
lines = content.lines
found_lines = []
modified_content = []
lines.each do |line|
if line.match(/^[\s\S]+#{string_to_find}[\s\S]+$/)
found_lines.push(line)
else
modified_content.push(line)
end
end
utils = SvelteOnRails::Installer::Utils
if found_lines.present?
return if !force && utils.ask_yn("Remove lines\n • #{found_lines.join("\n • ")}\n from #{file_path}?")
# Write the modified content back to the file
begin
File.write(file_path, modified_content.map { |l| l.gsub(/\n/, '') }.join("\n"))
puts "Successfully removed #{found_lines.length} #{'line'.pluralize(found_lines.length)}."
rescue => e
raise "Error writing to #{file_path} => «#{e.message}»"
end
else
end
end
def self.app_root_path(app_root = nil)
if app_root
raise "ERROR: app_root must be class Pathname" unless app_root.is_a?(Pathname)
app_root
else
begin
Dir.exist?(Rails.root.join('app'))
Rails.root
rescue => e
raise "ERROR: Could not find Rails.root => #{e}"
end
end
end
end
end
end