mirror of
https://github.com/discourse/discourse.git
synced 2026-08-11 02:59:07 +08:00
This replaces the old ember-cli build with a modern Rolldown build. In local testing, this provides an 80% improvement in build times, while remaining 100% backwards compatible for themes and plugins. As part of this move, we have decided to stop using a proxy in front of Discourse for development. Development should now be done directly against the Rails server. `bin/ember-cli -u` has been replaced with `bin/dev`. This will launch Rails on `:3000`, and will run the rolldown build in the background. Log output from both processes will be shown with an appropriate prefix. You should visit `:3000` in your browser. `:4200` will no longer serve anything. To help with migration, `bin/ember-cli` is now a backwards-compatible shim. It will print help information, and will launch a lightweight server on `:4200` with instructions to move to `:3000`. If you prefer to launch Rails and the JS build as separate commands, you can still do that. Rails boot commands are unchanged, and the rolldown development builder can be run using `bin/dev --only ember`. https://meta.discourse.org/t/403908 --------- Co-authored-by: Jarek Radosz <jarek@cvx.dev> Co-authored-by: Chris Manson <chris@manson.ie>
176 lines
5 KiB
Ruby
Vendored
176 lines
5 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
class EmberCli < ActiveSupport::CurrentAttributes
|
|
# Cache which persists for the duration of a request
|
|
attribute :request_cache
|
|
|
|
class BuildError < StandardError
|
|
attr_reader :details
|
|
|
|
def initialize(details)
|
|
@details = details
|
|
super(details.dig("error", "message") || "Frontend build failed")
|
|
end
|
|
end
|
|
|
|
def self.dist_dir
|
|
"#{Rails.root.join("frontend/discourse/dist")}"
|
|
end
|
|
|
|
def self.assets
|
|
raise_if_build_error!
|
|
cache[:assets] ||= Dir.glob("**/*.{js,map,txt,css}", base: "#{dist_dir}/assets")
|
|
end
|
|
|
|
def self.script_chunks(exception: true)
|
|
raise_if_build_error! if exception
|
|
return cache[:script_chunks] if cache[:script_chunks]
|
|
|
|
entrypoints = {}
|
|
|
|
manifest = read_manifest!(exception: exception)
|
|
return {} if manifest.nil?
|
|
|
|
{
|
|
**manifest["entrypoints"],
|
|
**manifest["dynamicEntrypoints"],
|
|
}.each do |entry_name, entry_filename|
|
|
entrypoints[entry_name.delete_suffix(".js")] = deep_imports_for(
|
|
chunk_filename: entry_filename,
|
|
chunks: manifest["chunks"],
|
|
).map { it.delete_prefix("assets/").delete_suffix(".js") }
|
|
end
|
|
|
|
cache[:script_chunks] = entrypoints
|
|
end
|
|
|
|
def self.deep_imports_for(chunk_filename:, chunks:, seen: Set.new)
|
|
return [] unless seen.add?(chunk_filename)
|
|
[
|
|
chunk_filename,
|
|
*chunks
|
|
.dig(chunk_filename, "imports")
|
|
&.flat_map { |import| deep_imports_for(chunk_filename: import, chunks:, seen:) },
|
|
]
|
|
end
|
|
|
|
BUILD_WAIT_TIMEOUT = 20.0
|
|
BUILD_POLL_INTERVAL = 0.05
|
|
|
|
def self.build_error
|
|
return nil unless Rails.env.local?
|
|
return cache[:build_error] if cache.key?(:build_error)
|
|
|
|
cache[:build_error] = wait_for_build
|
|
end
|
|
|
|
# Polls dist/manifest/build.json until the rolldown server reports a final
|
|
# state (ok/error/crashed), the process dies, or we hit BUILD_WAIT_TIMEOUT.
|
|
# Returns nil when everything is fine; returns an error payload otherwise.
|
|
def self.wait_for_build
|
|
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + BUILD_WAIT_TIMEOUT
|
|
loop do
|
|
data = read_build_status
|
|
return nil if data.nil?
|
|
|
|
if data["pid"] && !pid_alive?(data["pid"])
|
|
return data.merge("status" => "crashed", "error" => plain_error(<<~MSG))
|
|
Rolldown process (pid #{data["pid"]}) is no longer running.
|
|
Start it with `pnpm --dir frontend/discourse start`.
|
|
MSG
|
|
end
|
|
|
|
case data["status"]
|
|
when "error", "crashed"
|
|
return data
|
|
when "building"
|
|
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
return(
|
|
data.merge(
|
|
"status" => "crashed",
|
|
"error" =>
|
|
plain_error("Frontend build did not complete within #{BUILD_WAIT_TIMEOUT.to_i}s."),
|
|
)
|
|
)
|
|
end
|
|
sleep BUILD_POLL_INTERVAL
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.read_build_status
|
|
data = JSON.parse(File.read("#{dist_dir}/manifest/build.json"))
|
|
data.is_a?(Hash) ? data : nil
|
|
rescue Errno::ENOENT, JSON::ParserError
|
|
nil
|
|
end
|
|
|
|
def self.pid_alive?(pid)
|
|
Process.kill(0, pid.to_i)
|
|
true
|
|
rescue Errno::ESRCH, Errno::EPERM, ArgumentError, RangeError
|
|
false
|
|
end
|
|
|
|
def self.raise_if_build_error!
|
|
if (details = build_error)
|
|
raise BuildError.new(details)
|
|
end
|
|
end
|
|
|
|
# Reads the rolldown manifest. Returns the parsed hash, or nil when the
|
|
# caller has opted out of raising. When `exception:` is true and the manifest
|
|
# is missing, raises BuildError so the dev error page is shown.
|
|
def self.read_manifest!(exception:)
|
|
JSON.parse(File.read("#{dist_dir}/manifest/manifest.json"))
|
|
rescue Errno::ENOENT
|
|
if exception && Rails.env.local?
|
|
raise BuildError.new("status" => "missing", "error" => plain_error(<<~MSG))
|
|
No frontend build found at #{dist_dir}/manifest/manifest.json.
|
|
Start the dev server with `pnpm --dir frontend/discourse start`.
|
|
MSG
|
|
end
|
|
nil
|
|
end
|
|
|
|
# Build an error payload from plain text, populating both `message` (used in
|
|
# the raised exception's message) and `messageHtml` (rendered in the dev
|
|
# error view, HTML-escaped here since there are no ANSI codes to convert).
|
|
def self.plain_error(text)
|
|
{ "message" => text, "messageHtml" => ERB::Util.html_escape(text) }
|
|
end
|
|
|
|
def self.is_ember_cli_asset?(name)
|
|
assets.include?(name) || script_chunks.values.flatten.include?(name.delete_suffix(".js"))
|
|
end
|
|
|
|
def self.has_tests?
|
|
script_chunks["test-entrypoint"].present?
|
|
end
|
|
|
|
def self.cache
|
|
if Rails.env.development?
|
|
self.request_cache ||= {}
|
|
else
|
|
@production_cache ||= {}
|
|
end
|
|
end
|
|
|
|
def self.clear_cache!
|
|
self.request_cache = nil
|
|
@production_cache = nil
|
|
end
|
|
|
|
def self.watch!
|
|
FileUtils.mkdir_p("#{dist_dir}/manifest")
|
|
Listen
|
|
.to("#{dist_dir}/manifest") do |modified, added, removed|
|
|
next if modified.size == 0 && added.size == 0
|
|
next if read_build_status["status"] == "building"
|
|
MessageBus.publish("/file-change", ["refresh"])
|
|
end
|
|
.start
|
|
end
|
|
end
|