discourse/app/controllers/theme_javascripts_controller.rb
David Taylor be3d6a56ce
DEV: Introduce minification and source maps for Theme JS (#18646)
Theme javascript is now minified using Terser, just like our core/plugin JS bundles. This reduces the amount of data sent over the network.

This commit also introduces sourcemaps for theme JS. Browser developer tools will now be able show each source file separately when browsing, and also in backtraces.

For theme test JS, the sourcemap is inlined for simplicity. Network load is not a concern for tests.
2022-10-18 18:20:10 +01:00

124 lines
3.3 KiB
Ruby

# frozen_string_literal: true
class ThemeJavascriptsController < ApplicationController
DISK_CACHE_PATH = "#{Rails.root}/tmp/javascript-cache"
TESTS_DISK_CACHE_PATH = "#{Rails.root}/tmp/javascript-cache/tests"
skip_before_action(
:check_xhr,
:handle_theme,
:preload_json,
:redirect_to_login_if_required,
:verify_authenticity_token,
only: [:show, :show_map, :show_tests]
)
before_action :is_asset_path, :no_cookies, :apply_cdn_headers, only: [:show, :show_map, :show_tests]
def show
raise Discourse::NotFound unless last_modified.present?
return render body: nil, status: 304 if not_modified?
# Security: safe due to route constraint
cache_file = "#{DISK_CACHE_PATH}/#{params[:digest]}.js"
write_if_not_cached(cache_file) do
content, has_source_map = query.pluck_first(:content, "source_map IS NOT NULL")
if has_source_map
content += "\n//# sourceMappingURL=#{params[:digest]}.map?__ws=#{Discourse.current_hostname}\n"
end
content
end
serve_file(cache_file)
end
def show_map
raise Discourse::NotFound unless last_modified.present?
return render body: nil, status: 304 if not_modified?
# Security: safe due to route constraint
cache_file = "#{DISK_CACHE_PATH}/#{params[:digest]}.map"
write_if_not_cached(cache_file) do
query.pluck_first(:source_map)
end
serve_file(cache_file)
end
def show_tests
digest = params[:digest]
raise Discourse::NotFound if !digest.match?(/^\h{40}$/)
theme = Theme.find_by(id: params[:theme_id])
raise Discourse::NotFound if theme.blank?
content, content_digest = theme.baked_js_tests_with_digest
raise Discourse::NotFound if content.blank? || content_digest != digest
@cache_file = "#{TESTS_DISK_CACHE_PATH}/#{digest}.js"
return render body: nil, status: 304 if not_modified?
write_if_not_cached(@cache_file) do
content
end
serve_file @cache_file
end
private
def query
@query ||= JavascriptCache.where(digest: params[:digest]).limit(1)
end
def last_modified
@last_modified ||= begin
if params[:action].to_s == "show_tests"
File.exist?(@cache_file) ? File.ctime(@cache_file) : nil
else
query.pluck_first(:updated_at)
end
end
end
def not_modified?
cache_time =
begin
Time.rfc2822(request.env["HTTP_IF_MODIFIED_SINCE"])
rescue ArgumentError
nil
end
cache_time && last_modified && last_modified <= cache_time
end
def set_cache_control_headers
if Rails.env.development?
response.headers['Last-Modified'] = Time.zone.now.httpdate
immutable_for(1.second)
else
response.headers['Last-Modified'] = last_modified.httpdate if last_modified
immutable_for(1.year)
end
end
def write_if_not_cached(cache_file)
unless File.exist?(cache_file)
content = yield
raise Discourse::NotFound if content.nil?
FileUtils.mkdir_p(File.dirname(cache_file))
File.write(cache_file, content)
end
end
def serve_file(cache_file)
# this is only required for NGINX X-SendFile it seems
response.headers["Content-Length"] = File.size(cache_file).to_s
set_cache_control_headers
type = cache_file.end_with?(".map") ? "application/json" : "text/javascript"
send_file(cache_file, type: type, disposition: :inline)
end
end