discourse/lib/tasks/assets.rake
David Taylor fcaa068b87
DEV: Publish and enable use of pre-built JS assets (#33973)
Building the Discourse ember app is resource-intensive process. This
commit introduces a framework for us to build these assets centrally,
and make them available for people to download.

On every commit to `main`, a new GitHub actions workflow will build
development & production versions of the core assets, and publish them
as a github release under the `discourse/discourse-assets` repository. A
separate repository is being used to avoid polluting the main
`discourse/discourse` repository with one-git-tag-per-release.

The `assemble_ember_build.rb` script is updated to fetch the relevant
asset bundle. Requests are made to `get.discourse.org`, which then
redirects to GitHub releases. This redirection service is being used so
that we have the option to switch away from GitHub releases in future
without breaking existing Discourse installations.

For now, this behavior can be enabled by setting
`DISCOURSE_DOWNLOAD_PRE_BUILT_ASSETS=1`. In the near future, we hope to
make this the default, with opt-out via
`DISCOURSE_DOWNLOAD_PRE_BUILT_ASSETS=0`.
2025-07-31 14:22:51 +01:00

166 lines
5.3 KiB
Ruby

# frozen_string_literal: true
task "assets:precompile:build" do
if ENV["SKIP_EMBER_CLI_COMPILE"] != "1"
ember_version = ENV["EMBER_VERSION"] || "5"
raise "Unknown ember version '#{ember_version}'" if !%w[5].include?(ember_version)
compile_command = "EMBER_ENV=production #{Rails.root}/script/assemble_ember_build.rb"
only_ember_precompile_build_remaining = (ARGV.last == "assets:precompile:build")
only_assets_precompile_remaining = (ARGV.last == "assets:precompile")
# Using exec to free up Rails app memory during ember build
if only_ember_precompile_build_remaining
exec "#{compile_command}"
elsif only_assets_precompile_remaining
exec "#{compile_command} && SKIP_EMBER_CLI_COMPILE=1 bin/rake assets:precompile"
else
system compile_command, exception: true
EmberCli.clear_cache!
end
end
end
task "assets:precompile:before": %w[environment assets:precompile:build]
task "assets:precompile:css" => "environment" do
if ENV["DONT_PRECOMPILE_CSS"] == "1" || ENV["SKIP_DB_AND_REDIS"] == "1"
STDERR.puts "Skipping CSS precompilation, ensure CSS lives in a shared directory across hosts"
else
STDERR.puts "Start compiling CSS: #{Time.zone.now}"
RailsMultisite::ConnectionManagement.each_connection do |db|
# CSS will get precompiled during first request if tables do not exist.
if ActiveRecord::Base.connection.table_exists?(Theme.table_name)
STDERR.puts "-------------"
STDERR.puts "Compiling CSS for #{db} #{Time.zone.now}"
begin
Stylesheet::Manager.recalculate_fs_asset_cachebuster!
Stylesheet::Manager.precompile_css if db == "default"
Stylesheet::Manager.precompile_theme_css
rescue PG::UndefinedColumn, ActiveModel::MissingAttributeError, NoMethodError => e
STDERR.puts "#{e.class} #{e.message}: #{e.backtrace.join("\n")}"
STDERR.puts "Skipping precompilation of CSS cause schema is old, you are precompiling prior to running migrations."
end
end
end
STDERR.puts "Done compiling CSS: #{Time.zone.now}"
end
end
task "assets:flush_sw" => "environment" do
begin
hostname = Discourse.current_hostname
default_port = SiteSetting.force_https? ? 443 : 80
port = SiteSetting.port.to_i > 0 ? SiteSetting.port : default_port
STDERR.puts "Flushing service worker script"
`curl -s -m 1 --resolve '#{hostname}:#{port}:127.0.0.1' #{Discourse.base_url}/service-worker.js > /dev/null`
STDERR.puts "done"
rescue StandardError
STDERR.puts "Warning: unable to flush service worker script"
end
end
def assets_path
"#{Rails.root}/public/assets"
end
def gzip(path)
cmd = "gzip -f -c -9 #{path} > #{path}.gz"
system cmd, exception: true
end
def brotli_command(path)
compression_quality = ENV["DISCOURSE_ASSETS_PRECOMPILE_DEFAULT_BROTLI_QUALITY"] || "6"
"brotli -f --quality=#{compression_quality} #{path} --output=#{path}.br"
end
def brotli(path)
system brotli_command(path), exception: true
end
def concurrent?
if ENV["SPROCKETS_CONCURRENT"] == "1"
concurrent_compressors = []
executor = Concurrent::FixedThreadPool.new(Concurrent.processor_count)
yield(
Proc.new do |&block|
concurrent_compressors << Concurrent::Future.execute(executor: executor) { block.call }
end
)
concurrent_compressors.each(&:wait!)
else
yield(Proc.new { |&block| block.call })
end
end
def current_timestamp
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
def log_task_duration(task_description, &task)
task_start = current_timestamp
task.call
STDERR.puts "Done '#{task_description}' : #{(current_timestamp - task_start).round(2)} secs"
STDERR.puts
end
task "assets:precompile:compress_js": "environment" do
puts "Compressing JavaScript files"
load_path = Rails.application.assets.load_path
log_task_duration("Done compressing all JS files") do
concurrent? do |proc|
load_path
.assets
.select { |asset| asset.logical_path.extname == ".js" }
.each do |asset|
digested_path = asset.digested_path.to_s
if digested_path.include? "discourse/tests"
STDERR.puts "Skipping: #{digested_path}"
next
end
proc.call do
log_task_duration(digested_path) do
STDERR.puts "Compressing: #{digested_path}"
file_path = "public/assets/#{digested_path}"
gzip(file_path)
brotli(file_path)
end
end
end
end
end
if GlobalSetting.fallback_assets_path.present?
begin
FileUtils.cp_r("#{Rails.root}/public/assets/.", GlobalSetting.fallback_assets_path)
rescue => e
STDERR.puts "Failed to backup assets to #{GlobalSetting.fallback_assets_path}"
STDERR.puts e
STDERR.puts e.backtrace
end
end
end
task "assets:precompile:theme_transpiler": "environment" do
DiscourseJsProcessor::Transpiler.build_production_theme_transpiler
end
# Run these tasks **before** Rails' "assets:precompile" task
task "assets:precompile": %w[assets:precompile:before assets:precompile:theme_transpiler]
# Run these tasks **after** Rails' "assets:precompile" task
Rake::Task["assets:precompile"].enhance do
Rake::Task["assets:precompile:compress_js"].invoke
Rake::Task["assets:precompile:css"].invoke
Rake::Task["maxminddb:refresh"].invoke
end