Skip to content

Instantly share code, notes, and snippets.

@julik
Created February 17, 2026 13:06
Show Gist options
  • Select an option

  • Save julik/7db5a3737ba262c3a5b544e6ece0adc8 to your computer and use it in GitHub Desktop.

Select an option

Save julik/7db5a3737ba262c3a5b544e6ece0adc8 to your computer and use it in GitHub Desktop.
# frozen_string_literal: true
module GenevaDrive
module Admin
# Serves static assets from the engine's public/ directory.
# This keeps all engine URLs under the user-configured mount path,
# avoiding any conflicts with the host application's URL namespace.
class AssetsController < ActionController::Base
# Static assets don't need CSRF protection and must allow
# cross-origin requests (for ES module imports)
skip_forgery_protection
skip_before_action :verify_authenticity_token, raise: false
# Define allowed extensions and their content types
CONTENT_TYPES = {
".js" => "application/javascript",
".mjs" => "application/javascript",
".css" => "text/css"
}.freeze
# Extensions eligible for import-path rewriting
JS_EXTENSIONS = %w[.js .mjs].to_set.freeze
# Matches quoted strings that look like relative/absolute JS import paths.
# Handles ./ ../ / but NOT protocol-relative //
# $1 = quote char, $2 = path including extension
JS_IMPORT_RE = /(["'])(\.{0,2}\/(?!\/)[^"']*\.(?:js|mjs|es))\1/
# Matches url() in CSS with relative/absolute paths.
# Handles url(./path), url("./path"), url('../path'), url(/path)
# but NOT url(data:...), url(https://...), url(//...)
# $1 = opening (quote or empty), $2 = path, $3 = closing (quote or empty)
CSS_URL_RE = /url\((\s*["']?)(\.{0,2}\/(?!\/)[^)"']*?)(\s*["']?\s*)\)/
def show
# Reconstruct the path from the splat parameter
path = params[:path]
path = "#{path}.#{params[:format]}" if params[:format].present?
# Validate extension
extension = File.extname(path)
content_type = CONTENT_TYPES[extension]
return head(:not_found) unless content_type
# Resolve the file path within the engine's public directory
public_root = Engine.root.join("public")
file_path = public_root.join(path).cleanpath
# Security: ensure the resolved path is still within public/
return head(:not_found) unless file_path.to_s.start_with?(public_root.to_s + "/")
# Check file exists
return head(:not_found) unless file_path.exist? && file_path.file?
# Etag is always computed over the unprocessed file
raw_bytes = File.binread(file_path)
fresh_when strong_etag: Digest::SHA1.digest(raw_bytes), public: true, cache_control: {max_age: 5.days, must_revalidate: true}
# Rewrite relative asset references so the cachebuster cascades
# through the entire dependency graph (JS imports, CSS url()s).
# Always inject the version tag – either from the request URL or
# computed from the engine version – so imports cascade even when
# the entry-point script tag omits ?v=.
tag = params[:v].presence || Engine.version_tag
if JS_EXTENSIONS.include?(extension)
send_data rewrite_js_imports(raw_bytes, tag),
type: content_type, disposition: :inline
elsif extension == ".css"
send_data rewrite_css_urls(raw_bytes, tag),
type: content_type, disposition: :inline
else
send_file file_path, type: content_type, disposition: :inline
end
end
private
def rewrite_js_imports(source, version_tag)
source.gsub(JS_IMPORT_RE) do
"#{$1}#{$2}?v=#{version_tag}#{$1}"
end
end
def rewrite_css_urls(source, version_tag)
source.gsub(CSS_URL_RE) do
"url(#{$1}#{$2}?v=#{version_tag}#{$3})"
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment