341 lines
11 KiB
Ruby
341 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
task "assets:precompile:prereqs" do
|
|
if %w[profile production].exclude? Rails.env
|
|
raise "rake assets:precompile should only be run in RAILS_ENV=production, you are risking unminified assets"
|
|
end
|
|
end
|
|
|
|
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)
|
|
|
|
# If `JOBS` env is not set, `thread-loader` defaults to the number of CPUs - 1 on the machine but we want to cap it
|
|
# at 2 because benchmarking has shown that anything beyond 2 does not improve build times or the increase is marginal.
|
|
# Therefore, we cap it so that we don't spawn more processes than necessary.
|
|
jobs_env_count = (2 if !ENV["JOBS"].present? && Etc.nprocessors > 2)
|
|
|
|
compile_command = "CI=1 pnpm --dir=app/assets/javascripts/discourse ember build"
|
|
|
|
heap_size_limit = check_node_heap_size_limit
|
|
|
|
if heap_size_limit < 2048
|
|
STDERR.puts "Node.js heap_size_limit (#{heap_size_limit}) is less than 2048MB. Setting --max-old-space-size=2048 and CHEAP_SOURCE_MAPS=1"
|
|
jobs_env_count = 0
|
|
|
|
compile_command =
|
|
"CI=1 NODE_OPTIONS='--max-old-space-size=2048' CHEAP_SOURCE_MAPS=1 #{compile_command}"
|
|
end
|
|
|
|
ember_env = ENV["EMBER_ENV"] || "production"
|
|
compile_command = "#{compile_command} -prod" if ember_env == "production"
|
|
compile_command = "JOBS=#{jobs_env_count} #{compile_command}" if jobs_env_count
|
|
|
|
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:prereqs
|
|
assets:precompile:build
|
|
] do
|
|
require "uglifier"
|
|
require "open3"
|
|
|
|
# Ensure we ALWAYS do a clean build
|
|
# We use many .erbs that get out of date quickly, especially with plugins
|
|
STDERR.puts "Purging temp files"
|
|
`rm -fr #{Rails.root}/tmp/cache`
|
|
|
|
$node_compress = !ENV["SKIP_NODE_UGLIFY"]
|
|
|
|
unless ENV["USE_SPROCKETS_UGLIFY"]
|
|
$bypass_sprockets_uglify = true
|
|
Rails.configuration.assets.js_compressor = nil
|
|
Rails.configuration.assets.gzip = false
|
|
end
|
|
|
|
STDERR.puts "Bundling assets"
|
|
|
|
# in the past we applied a patch that removed asset postfixes, but it is terrible practice
|
|
# leaving very complicated build issues
|
|
# https://github.com/rails/sprockets-rails/issues/49
|
|
|
|
require "sprockets"
|
|
require "digest/sha1"
|
|
end
|
|
|
|
task "assets:precompile:css" => "environment" do
|
|
class Sprockets::Manifest
|
|
def reload
|
|
@filename = find_directory_manifest(@directory)
|
|
@data = json_decode(File.read(@filename))
|
|
end
|
|
end
|
|
|
|
# cause on boot we loaded a blank manifest,
|
|
# we need to know where all the assets are to precompile CSS
|
|
# cause CSS uses asset_path
|
|
Rails.application.assets_manifest.reload
|
|
|
|
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 check_node_heap_size_limit
|
|
output, status =
|
|
Open3.capture2("node", "-e", "console.log(v8.getHeapStatistics().heap_size_limit/1024/1024)")
|
|
raise "Failed to fetch node memory limit" if status != 0
|
|
output.to_f
|
|
end
|
|
|
|
def assets_path
|
|
"#{Rails.root}/public/assets"
|
|
end
|
|
|
|
def global_path_klass
|
|
@global_path_klass ||= Class.new { extend GlobalPath }
|
|
end
|
|
|
|
def cdn_path(p)
|
|
global_path_klass.cdn_path(p)
|
|
end
|
|
|
|
def cdn_relative_path(p)
|
|
global_path_klass.cdn_relative_path(p)
|
|
end
|
|
|
|
def compress_node(from, to)
|
|
to_path = "#{assets_path}/#{to}"
|
|
assets = cdn_relative_path("/assets")
|
|
assets_additional_path = (d = File.dirname(from)) == "." ? "" : "/#{d}"
|
|
source_map_root = assets + assets_additional_path
|
|
source_map_url = "#{File.basename(to)}.map"
|
|
base_source_map = assets_path + assets_additional_path
|
|
|
|
cmd = <<~SH
|
|
pnpm terser '#{assets_path}/#{from}' -m -c -o '#{to_path}' --source-map "base='#{base_source_map}',root='#{source_map_root}',url='#{source_map_url}',includeSources=true"
|
|
SH
|
|
|
|
STDERR.puts cmd
|
|
result = `#{cmd} 2>&1`
|
|
unless $?.success?
|
|
STDERR.puts result
|
|
exit 1
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
def compress_ruby(from, to)
|
|
data = File.read("#{assets_path}/#{from}")
|
|
|
|
uglified, map =
|
|
Uglifier.new(
|
|
comments: :none,
|
|
source_map: {
|
|
filename: File.basename(from),
|
|
output_filename: File.basename(to),
|
|
},
|
|
).compile_with_map(data)
|
|
dest = "#{assets_path}/#{to}"
|
|
|
|
File.write(dest, uglified << "\n//# sourceMappingURL=#{cdn_path "/assets/#{to}.map"}")
|
|
File.write(dest + ".map", map)
|
|
|
|
GC.start
|
|
end
|
|
|
|
def gzip(path)
|
|
STDERR.puts "gzip -f -c -9 #{path} > #{path}.gz"
|
|
STDERR.puts `gzip -f -c -9 #{path} > #{path}.gz`.strip
|
|
raise "gzip compression failed: exit code #{$?.exitstatus}" if $?.exitstatus != 0
|
|
end
|
|
|
|
# different brotli versions use different parameters
|
|
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)
|
|
STDERR.puts brotli_command(path)
|
|
STDERR.puts `#{brotli_command(path)}`
|
|
raise "brotli compression failed: exit code #{$?.exitstatus}" if $?.exitstatus != 0
|
|
STDERR.puts `chmod +r #{path}.br`.strip
|
|
raise "chmod failed: exit code #{$?.exitstatus}" if $?.exitstatus != 0
|
|
end
|
|
|
|
def max_compress?(path, locales)
|
|
return false if Rails.configuration.assets.skip_minification.include? path
|
|
return false if EmberCli.is_ember_cli_asset?(path)
|
|
return true if path.exclude? "locales/"
|
|
|
|
path_locale = path.delete_prefix("locales/").delete_suffix(".js")
|
|
return true if locales.include? path_locale
|
|
|
|
false
|
|
end
|
|
|
|
def compress(from, to)
|
|
$node_compress ? compress_node(from, to) : compress_ruby(from, to)
|
|
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
|
|
if $bypass_sprockets_uglify
|
|
puts "Compressing Javascript and Generating Source Maps"
|
|
manifest = Sprockets::Manifest.new(assets_path)
|
|
|
|
locales = Set.new(["en"])
|
|
|
|
RailsMultisite::ConnectionManagement.each_connection do |db|
|
|
locales.add(SiteSetting.default_locale)
|
|
end
|
|
|
|
log_task_duration("Done compressing all JS files") do
|
|
concurrent? do |proc|
|
|
manifest
|
|
.files
|
|
.select { |k, v| k =~ /\.js\z/ }
|
|
.each do |file, info|
|
|
path = "#{assets_path}/#{file}"
|
|
_file =
|
|
(
|
|
if (d = File.dirname(file)) == "."
|
|
"_#{file}"
|
|
else
|
|
"#{d}/_#{File.basename(file)}"
|
|
end
|
|
)
|
|
_path = "#{assets_path}/#{_file}"
|
|
max_compress = max_compress?(info["logical_path"], locales)
|
|
if File.exist?(_path)
|
|
STDERR.puts "Skipping: #{file} already compressed"
|
|
elsif file.include? "discourse/tests"
|
|
STDERR.puts "Skipping: #{file}"
|
|
else
|
|
proc.call do
|
|
log_task_duration(file) do
|
|
STDERR.puts "Compressing: #{file}"
|
|
|
|
if max_compress
|
|
FileUtils.mv(path, _path)
|
|
compress(_file, file)
|
|
end
|
|
|
|
info["size"] = File.size(path)
|
|
info["mtime"] = File.mtime(path).iso8601
|
|
gzip(path)
|
|
brotli(path)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# protected
|
|
manifest.send :save
|
|
|
|
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
|
|
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
|