mirror of
https://github.com/discourse/discourse.git
synced 2026-09-05 04:40:41 -05:00
DEV: Replace JS build system with Rolldown (#35963)
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>
This commit is contained in:
co-authored by
Jarek Radosz
Chris Manson
parent
64ee16f7b9
commit
9527868295
@@ -6,8 +6,7 @@
|
||||
"postStartCommand": "./.devcontainer/scripts/start.rb",
|
||||
"forwardPorts": [
|
||||
9292, // bin/pitchfork
|
||||
3000, // bin/rails s
|
||||
4200, // ember-cli
|
||||
3000, // bin/dev
|
||||
8025, // mailhog
|
||||
9229 // chrome remote debug
|
||||
],
|
||||
|
||||
@@ -26,7 +26,7 @@ puts <<~TXT
|
||||
1. Cmd/Ctrl + Shift + B to run the shortcuts/boot-dev task
|
||||
2. Wait for the server to start
|
||||
3. Run the "dev/admin/create" task once to create an admin account
|
||||
4. Open your browser to http://localhost:4200
|
||||
4. Open your browser to http://localhost:3000
|
||||
|
||||
Running tests:
|
||||
Run the "deps/testing" task once to install Playwright + discourse_test DB
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
|
||||
# Front-end
|
||||
dist
|
||||
frontend/discourse/tmp/compat-prebuild
|
||||
node_modules
|
||||
yarn-error.log
|
||||
.pnpm-store
|
||||
@@ -123,6 +124,7 @@ openapi/*
|
||||
|
||||
# direnv.net
|
||||
.direnv
|
||||
/app/assets/javascripts/discourse/tmp
|
||||
|
||||
# Types
|
||||
frontend/discourse-types/declarations
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
"line-stream": "0.0.0",
|
||||
"messageformat": "0.1.5",
|
||||
"regenerator-transform": "0.10.1",
|
||||
"source-map": "0.1.43",
|
||||
"sourcemap-validator": "1.1.1",
|
||||
"spawn-command": "0.0.2",
|
||||
"taffydb": "2.6.2"
|
||||
|
||||
@@ -123,7 +123,7 @@ end
|
||||
|
||||
**When a system test fails, diagnose before fixing.** Guessing at fixes without understanding the failure burns retries and lands the wrong patch.
|
||||
|
||||
If you changed frontend code (`.js` / `.hbs` / `.gjs` / `.gts`) and the behavior suggests your changes aren't being picked up, the asset build is stale. Run `bin/ember-cli --build` to rebuild, then re-run the test. This is only needed when `bin/ember-cli` isn't already running in the background.
|
||||
If you changed frontend code (`.js` / `.hbs` / `.gjs` / `.gts`) and the behavior suggests your changes aren't being picked up, the asset build is stale. Run `pnpm build` to rebuild, then re-run the test. This is only needed when `bin/dev` isn't already running in the background.
|
||||
|
||||
For runtime visibility, add `puts "DEBUG: …"` in Ruby (controllers, models, services, jobs) or `console.log("DEBUG: …")` in JavaScript (components, services, routes). Place logs at the entry point of the code path, around conditional branches, and at the line where the failure occurs. Re-run with documentation format so the output reads cleanly:
|
||||
|
||||
@@ -139,7 +139,7 @@ Common failure patterns:
|
||||
|---|---|---|
|
||||
| Element not found | Selector wrong, element not rendered, timing | `console.log` in the component, double-check the selector in the test |
|
||||
| Unexpected content | Wrong data, rendering issue | `puts` in the controller/serializer to check data flow |
|
||||
| JS changes not reflected | Assets not rebuilt | Run `bin/ember-cli --build` |
|
||||
| JS changes not reflected | Assets not rebuilt | Run `pnpm build` |
|
||||
| Flaky pass/fail | Timing issue | Add waits, check for async operations |
|
||||
| 404/500 in test | Route or controller issue | `puts` in the route handler, check server logs |
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -59,7 +59,7 @@
|
||||
{
|
||||
"label": "dev/server",
|
||||
"type": "shell",
|
||||
"command": "bin/ember-cli -u",
|
||||
"command": "bin/dev",
|
||||
"options": {
|
||||
"env":{
|
||||
"DISCOURSE_DEV_ALLOW_ANON_TO_IMPERSONATE": "1"
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
body.requires-ember-cli {
|
||||
margin: 2rem;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: lightgrey;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
@@ -119,24 +119,15 @@ class ApplicationController < ActionController::Base
|
||||
response.headers.delete("X-Frame-Options") if SiteSetting.allow_embedding_site_in_an_iframe
|
||||
end
|
||||
|
||||
def ember_cli_required?
|
||||
Rails.env.development? && ENV["ALLOW_EMBER_CLI_PROXY_BYPASS"] != "1" &&
|
||||
request.headers["X-Discourse-Ember-CLI"] != "true"
|
||||
end
|
||||
|
||||
def application_layout
|
||||
ember_cli_required? ? "ember_cli" : "application"
|
||||
end
|
||||
|
||||
def set_layout
|
||||
case request.headers["Discourse-Render"]
|
||||
when "desktop"
|
||||
return application_layout
|
||||
return "application"
|
||||
when "crawler"
|
||||
return "crawler"
|
||||
end
|
||||
|
||||
use_crawler_layout? ? "crawler" : application_layout
|
||||
use_crawler_layout? ? "crawler" : "application"
|
||||
end
|
||||
|
||||
class RenderEmpty < StandardError
|
||||
@@ -149,6 +140,12 @@ class ApplicationController < ActionController::Base
|
||||
with_resolved_locale { render "default/empty" }
|
||||
end
|
||||
|
||||
rescue_from EmberCli::BuildError do |e|
|
||||
@build_error = e.details
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
render "default/build_error", layout: false, status: :service_unavailable
|
||||
end
|
||||
|
||||
rescue_from ArgumentError do |e|
|
||||
if e.message == "string contains null byte"
|
||||
raise Discourse::InvalidParameters, e.message
|
||||
|
||||
@@ -15,81 +15,4 @@ class BootstrapController < ApplicationController
|
||||
};
|
||||
JS
|
||||
end
|
||||
|
||||
def core_css_for_tests
|
||||
targets = %w[color_definitions common desktop admin]
|
||||
render_css_for_tests(targets)
|
||||
end
|
||||
|
||||
def plugin_test_info
|
||||
target = params[:target]
|
||||
|
||||
required_plugins = []
|
||||
testing_plugins = []
|
||||
|
||||
if target == "all" || target == "plugins"
|
||||
required_plugins.push(*Discourse.plugins.map(&:directory_name))
|
||||
testing_plugins.push(*Discourse.plugins.map(&:directory_name))
|
||||
elsif target == "core"
|
||||
# no plugins
|
||||
elsif target_plugin = Discourse.plugins.find { |p| p.directory_name == target }
|
||||
required_plugins << target_plugin.directory_name
|
||||
testing_plugins << target_plugin.directory_name
|
||||
|
||||
target_plugin.test_required_plugins&.map do |plugin_name|
|
||||
additional_plugin = Discourse.plugins.find { |p| p.directory_name == plugin_name }
|
||||
required_plugins << additional_plugin.directory_name if additional_plugin
|
||||
end
|
||||
|
||||
required_plugins.push(*QunitController::ALWAYS_LOADED_PLUGINS)
|
||||
else
|
||||
return render plain: "Target '#{target}' not found", status: :not_found
|
||||
end
|
||||
|
||||
plugin_js_string =
|
||||
render_to_string partial: "layouts/plugin_js",
|
||||
locals: {
|
||||
opts: {
|
||||
include_disabled: true,
|
||||
include_admin_asset: true,
|
||||
include_test_assets_for: testing_plugins,
|
||||
only: required_plugins,
|
||||
},
|
||||
},
|
||||
formats: [:html],
|
||||
layout: false
|
||||
|
||||
plugin_css_string =
|
||||
Discourse
|
||||
.find_plugin_css_assets(
|
||||
include_disabled: true,
|
||||
desktop_view: true,
|
||||
include_admin: true,
|
||||
only: required_plugins,
|
||||
)
|
||||
.map { |file| helpers.discourse_stylesheet_link_tag(file) }
|
||||
.join("\n")
|
||||
|
||||
render json: {
|
||||
all_plugins: Discourse.plugins.map(&:directory_name),
|
||||
html: "#{plugin_js_string}\n#{plugin_css_string}",
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_css_for_tests(targets)
|
||||
urls =
|
||||
targets.map do |target|
|
||||
details = Stylesheet::Manager.new().stylesheet_details(target, "all")
|
||||
details[0][:new_href]
|
||||
end
|
||||
|
||||
stylesheet = <<~CSS
|
||||
/* For use in tests only */
|
||||
#{urls.map { |url| "@import \"#{url}\";" }.join("\n")}
|
||||
CSS
|
||||
|
||||
render plain: stylesheet, content_type: "text/css"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,49 @@ class QunitController < ApplicationController
|
||||
redirect_to_login_if_required
|
||||
redirect_to_profile_if_required
|
||||
]
|
||||
|
||||
layout false
|
||||
around_action :ensure_locale_en
|
||||
|
||||
def index
|
||||
raise Discourse::NotFound.new if !can_see_theme_qunit?
|
||||
@suggested_themes =
|
||||
Theme
|
||||
.where(id: ThemeField.where(target_id: Theme.targets[:tests_js]).distinct.pluck(:theme_id))
|
||||
.order(updated_at: :desc)
|
||||
.pluck(:id, :name)
|
||||
end
|
||||
|
||||
def core
|
||||
@has_test_bundle = EmberCli.has_tests?
|
||||
request.env[:resolved_theme_id] = nil
|
||||
|
||||
target = params[:target] || "core"
|
||||
|
||||
@required_plugins = []
|
||||
@testing_plugins = []
|
||||
|
||||
if target == "plugins"
|
||||
@required_plugins.push(*Discourse.plugins.map(&:directory_name))
|
||||
@testing_plugins.push(*Discourse.plugins.map(&:directory_name))
|
||||
elsif target == "core"
|
||||
# no plugins
|
||||
elsif target_plugin = Discourse.plugins.find { |p| p.directory_name == target }
|
||||
@required_plugins << target_plugin.directory_name
|
||||
@testing_plugins << target_plugin.directory_name
|
||||
|
||||
target_plugin.test_required_plugins&.map do |plugin_name|
|
||||
additional_plugin = Discourse.plugins.find { |p| p.directory_name == plugin_name }
|
||||
@required_plugins << additional_plugin.directory_name if additional_plugin
|
||||
end
|
||||
|
||||
@required_plugins.push(*QunitController::ALWAYS_LOADED_PLUGINS)
|
||||
else
|
||||
return render plain: "Target '#{target}' not found", status: :not_found
|
||||
end
|
||||
|
||||
render "qunit"
|
||||
end
|
||||
|
||||
def theme
|
||||
raise Discourse::NotFound.new if !can_see_theme_qunit?
|
||||
@@ -18,7 +60,6 @@ class QunitController < ApplicationController
|
||||
@has_test_bundle = EmberCli.has_tests?
|
||||
|
||||
param_key = nil
|
||||
@suggested_themes = nil
|
||||
if (id = get_param(:id)).present?
|
||||
theme = Theme.find_by(id: id.to_i)
|
||||
param_key = :id
|
||||
@@ -37,17 +78,6 @@ class QunitController < ApplicationController
|
||||
)
|
||||
end
|
||||
|
||||
if !param_key
|
||||
@suggested_themes =
|
||||
Theme
|
||||
.where(
|
||||
id: ThemeField.where(target_id: Theme.targets[:tests_js]).distinct.pluck(:theme_id),
|
||||
)
|
||||
.order(updated_at: :desc)
|
||||
.pluck(:id, :name)
|
||||
return
|
||||
end
|
||||
|
||||
about_json =
|
||||
JSON.parse(theme.theme_fields.find_by(target_id: Theme.targets[:about])&.value || "{}")
|
||||
@required_plugins =
|
||||
@@ -59,6 +89,8 @@ class QunitController < ApplicationController
|
||||
|
||||
request.env[:resolved_theme_id] = theme.id
|
||||
request.env[:skip_theme_ids_transformation] = true
|
||||
|
||||
render "qunit"
|
||||
end
|
||||
|
||||
protected
|
||||
@@ -73,4 +105,8 @@ class QunitController < ApplicationController
|
||||
def get_param(key)
|
||||
params[:"theme_#{key}"] || params[key]
|
||||
end
|
||||
|
||||
def ensure_locale_en
|
||||
I18n.with_locale(:en) { yield }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,7 +13,7 @@ class RobotsTxtController < ApplicationController
|
||||
DISALLOWED_PATHS = %w[
|
||||
/admin/
|
||||
/auth/
|
||||
/assets/browser-update*.js
|
||||
/assets/js/browser-update*.js
|
||||
/email/
|
||||
/session
|
||||
/user-api-key
|
||||
|
||||
@@ -130,39 +130,30 @@ module ApplicationHelper
|
||||
end
|
||||
|
||||
if is_brotli_req?
|
||||
if path.include?("/assets/js/")
|
||||
path = path.sub("/assets/js/", "/assets/br/")
|
||||
else
|
||||
path = path.sub(/\.([^.]+)\z/, '.br.\1')
|
||||
end
|
||||
path = path.sub("/assets/js/", "/assets/br/")
|
||||
elsif is_gzip_req?
|
||||
if path.include?("/assets/js/")
|
||||
path = path.sub("/assets/js/", "/assets/gz/")
|
||||
else
|
||||
path = path.sub(/\.([^.]+)\z/, '.gz.\1')
|
||||
end
|
||||
path = path.sub("/assets/js/", "/assets/gz/")
|
||||
end
|
||||
end
|
||||
|
||||
path
|
||||
end
|
||||
|
||||
def preload_script(script, attrs: {})
|
||||
scripts = []
|
||||
def preload_script(script, type_module: false, attrs: {})
|
||||
resolved_script = EmberCli.script_chunks[script]&.first || script
|
||||
path = script_asset_path(resolved_script)
|
||||
preload_script_url(path, entrypoint: script, type_module:, attrs:).html_safe
|
||||
end
|
||||
|
||||
if chunks = EmberCli.script_chunks[script]
|
||||
scripts.push(*chunks)
|
||||
else
|
||||
scripts.push(script)
|
||||
end
|
||||
def module_preloads_for(*scripts)
|
||||
resolved_preload_scripts =
|
||||
scripts.compact.flat_map { |script| EmberCli.script_chunks[script] }.compact.uniq
|
||||
|
||||
scripts
|
||||
.map do |name|
|
||||
path = script_asset_path(name)
|
||||
preload_script_url(path, entrypoint: script, attrs: attrs)
|
||||
end
|
||||
.join("\n")
|
||||
.html_safe
|
||||
modulepreload_tags = resolved_preload_scripts.map { |script| <<~HTML }
|
||||
<link rel="modulepreload" href="#{script_asset_path script}" nonce="#{csp_nonce_placeholder}">
|
||||
HTML
|
||||
|
||||
modulepreload_tags.join("\n").html_safe
|
||||
end
|
||||
|
||||
def preload_script_url(url, entrypoint: nil, type_module: false, attrs: nil)
|
||||
@@ -995,12 +986,14 @@ module ApplicationHelper
|
||||
svg_sprite_path: SvgSprite.path(theme_id),
|
||||
media_optimization_bundle:
|
||||
script_asset_path(
|
||||
EmberCli.script_chunks["media-optimization-bundle"]&.first || "media-optimization-bundle",
|
||||
EmberCli.script_chunks["media-optimization-bundle"]&.first ||
|
||||
"/media-optimization-bundle.js",
|
||||
),
|
||||
enable_js_error_reporting: GlobalSetting.enable_js_error_reporting,
|
||||
color_scheme_is_dark: dark_color_scheme?,
|
||||
user_color_scheme_id: user_scheme_id || -1,
|
||||
user_dark_scheme_id: user_dark_scheme_id || -1,
|
||||
is_staff: staff?,
|
||||
}
|
||||
|
||||
if Rails.env.development?
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
data-nonce="<%= csp_nonce_placeholder %>"
|
||||
data-container-id="<%= SiteSetting.gtm_container_id %>" />
|
||||
|
||||
<%= preload_script 'google-tag-manager' %>
|
||||
<%= preload_script 'js/google-tag-manager' %>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
} %>
|
||||
|
||||
<% if SiteSetting.ga_version == "v3_analytics" %>
|
||||
<%= preload_script "google-universal-analytics-v3" %>
|
||||
<%= preload_script "js/google-universal-analytics-v3" %>
|
||||
<% elsif SiteSetting.ga_version == "v4_gtag" %>
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=<%= SiteSetting.ga_universal_tracking_code %>" nonce="<%= csp_nonce_placeholder %>"></script>
|
||||
<%= preload_script "google-universal-analytics-v4" %>
|
||||
<%= preload_script "js/google-universal-analytics-v4" %>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Frontend build error</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: #1e1e1e;
|
||||
color: #f0f0f0;
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
main {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
color: #ff6b6b;
|
||||
margin-top: 0;
|
||||
}
|
||||
pre {
|
||||
background: #111;
|
||||
color: #f0f0f0;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Frontend build error</h1>
|
||||
|
||||
<% error = @build_error["error"] || {} %>
|
||||
<% if error["messageHtml"] %>
|
||||
<pre><%= error["messageHtml"].html_safe %></pre>
|
||||
<% elsif error["message"] %>
|
||||
<pre><%= error["message"] %></pre>
|
||||
<% end %>
|
||||
|
||||
<script nonce="<%= csp_nonce_placeholder %>">
|
||||
const clientId = crypto.randomUUID();
|
||||
const SEP = "\r\n|\r\n";
|
||||
let lastId = -1;
|
||||
|
||||
(async function poll() {
|
||||
try {
|
||||
const res = await fetch(`/message-bus/${clientId}/poll`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({ "/file-change": String(lastId) }),
|
||||
});
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
for await (const chunk of res.body) {
|
||||
buffer += decoder.decode(chunk, { stream: true });
|
||||
let sep;
|
||||
while ((sep = buffer.indexOf(SEP)) !== -1) {
|
||||
const batch = JSON.parse(buffer.slice(0, sep));
|
||||
buffer = buffer.slice(sep + SEP.length);
|
||||
for (const m of batch) {
|
||||
if (m.channel === "/file-change") {
|
||||
return window.location.reload();
|
||||
} else if (m.channel === "/__status") {
|
||||
lastId = m.data["/file-change"] ?? lastId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
poll();
|
||||
})();
|
||||
</script>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= preload_script('onpopstate-handler') %>
|
||||
<%= preload_script('js/onpopstate-handler') %>
|
||||
<%- end %>
|
||||
|
||||
<%- if @group&.dig(:allow_membership_requests) %>
|
||||
@@ -61,5 +61,3 @@
|
||||
<%= build_plugin_html 'server:not-found-before-topics' %>
|
||||
|
||||
<%= @topics_partial %>
|
||||
|
||||
|
||||
|
||||
@@ -31,12 +31,11 @@
|
||||
|
||||
<%= build_plugin_html 'server:before-script-load' %>
|
||||
|
||||
<% add_resource_preload_list(script_asset_path("start-discourse"), "script") %>
|
||||
<% add_resource_preload_list(script_asset_path("browser-update"), "script") %>
|
||||
<link rel="preload" href="<%= script_asset_path "start-discourse" %>" as="script" nonce="<%= csp_nonce_placeholder %>">
|
||||
<link rel="preload" href="<%= script_asset_path "browser-update" %>" as="script" nonce="<%= csp_nonce_placeholder %>">
|
||||
|
||||
<%= preload_script 'browser-detect' %>
|
||||
<script nonce="<%= csp_nonce_placeholder %>">
|
||||
window.EmberENV = {
|
||||
_DEFAULT_ASYNC_OBSERVERS: true,
|
||||
};
|
||||
</script>
|
||||
|
||||
<%= preload_script_url ExtraLocalesController.url("main"), type_module: true %>
|
||||
<%= preload_script_url ExtraLocalesController.url("mf"), type_module: true %>
|
||||
@@ -53,8 +52,13 @@
|
||||
<%= theme_translations_lookup %>
|
||||
<%- end %>
|
||||
|
||||
<%= preload_script "vendor" %>
|
||||
<%= preload_script "discourse" %>
|
||||
<link rel="preload" href="<%= script_asset_path "js/browser-update" %>" as="script" nonce="<%= csp_nonce_placeholder %>">
|
||||
|
||||
<%= preload_script 'js/browser-detect' %>
|
||||
|
||||
<%= preload_script "vendor", type_module: true %>
|
||||
|
||||
<%= module_preloads_for "discourse", (staff? ? "admin/compat-modules" : nil) %>
|
||||
|
||||
<%- if staff? %>
|
||||
<% EmberCli.script_chunks["chunk.admin"]&.each do |script_name| %>
|
||||
@@ -143,12 +147,12 @@
|
||||
</form>
|
||||
<% end %>
|
||||
|
||||
<script defer src="<%= script_asset_path "start-discourse" %>" nonce="<%= csp_nonce_placeholder %>"></script>
|
||||
<%= preload_script "discourse", type_module: true %>
|
||||
|
||||
<script defer src="<%= script_asset_path "js/browser-update" %>" nonce="<%= csp_nonce_placeholder %>"></script>
|
||||
|
||||
<%= yield :data %>
|
||||
|
||||
<script defer src="<%= script_asset_path "browser-update" %>" nonce="<%= csp_nonce_placeholder %>"></script>
|
||||
|
||||
<%- unless customization_disabled? %>
|
||||
<%= theme_lookup("body_tag") %>
|
||||
<%- end %>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<%- end %>
|
||||
|
||||
<meta id="data-embedded" data-referer="<%= @data_referer %>">
|
||||
<%= preload_script 'embed-application' %>
|
||||
<%= preload_script 'js/embed-application' %>
|
||||
|
||||
<%= yield :head %>
|
||||
</head>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><%= content_for?(:title) ? yield(:title) : SiteSetting.title %></title>
|
||||
|
||||
<%= discourse_stylesheet_link_tag(:ember_cli) %>
|
||||
|
||||
</head>
|
||||
<body class="requires-ember-cli">
|
||||
<div class='warning'>
|
||||
<h1>Ember CLI is Required in Development Mode</h1>
|
||||
|
||||
<p>To run Ember CLI in development mode, please do the following:</p>
|
||||
|
||||
<pre><code>$ bin/ember-cli</code></pre>
|
||||
|
||||
<p>Then visit the following URL to use Discourse:</p>
|
||||
|
||||
<h3><a href="http://<%= Discourse.current_hostname %>:4200<%= Discourse.base_path %>">http://<%= Discourse.current_hostname %>:4200<%= Discourse.base_path %></a></h3>
|
||||
|
||||
<p>To disable this warning and allow direct Rails access, start the server with <code>ALLOW_EMBER_CLI_PROXY_BYPASS=1</code></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -18,7 +18,7 @@
|
||||
<%= build_plugin_html 'server:before-head-close' %>
|
||||
<%- end -%>
|
||||
|
||||
<%= preload_script "pageview" %>
|
||||
<%= preload_script "js/pageview" %>
|
||||
</head>
|
||||
<body class="no-ember <%= @custom_body_class %>">
|
||||
<%- if allow_plugins? %>
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<%= render_google_tag_manager_head_code %>
|
||||
<%= render_google_universal_analytics_code %>
|
||||
<%= render_adobe_analytics_tags_code %>
|
||||
<%= preload_script 'publish' %>
|
||||
<%= preload_script 'pageview' %>
|
||||
<%= preload_script 'js/publish' %>
|
||||
<%= preload_script 'js/pageview' %>
|
||||
</head>
|
||||
<body class="<%= @body_classes.to_a.join(' ') %>">
|
||||
<%= theme_lookup("header") %>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Theme QUnit Tests</title>
|
||||
<%= discourse_color_scheme_stylesheets %>
|
||||
<meta name="color-scheme" content="light dark">
|
||||
|
||||
<style>
|
||||
html {
|
||||
font-family: Arial;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Theme QUnit Test Runner</h2>
|
||||
|
||||
<%- if @suggested_themes.empty? %>
|
||||
<p>Cannot find any theme tests.</p>
|
||||
<%- else %>
|
||||
<h3>Select a theme/component: </h3>
|
||||
<%- @suggested_themes.each do |(id, name)| %>
|
||||
<h4><%= link_to name, theme_qunit_path(id: id) %></h4>
|
||||
<%- end %>
|
||||
<%- end %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,75 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Theme QUnit Test Runner</title>
|
||||
<title>Discourse QUnit Test Runner</title>
|
||||
<%= discourse_color_scheme_stylesheets %>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
|
||||
<%- if @has_test_bundle && !@suggested_themes %>
|
||||
<%- if @has_test_bundle %>
|
||||
<%= preload_script_url ExtraLocalesController.url("main"), type_module: true %>
|
||||
<%= preload_script_url ExtraLocalesController.url("mf"), type_module: true %>
|
||||
<%= preload_script_url ExtraLocalesController.url("admin"), type_module: true %>
|
||||
<%= preload_script_url ExtraLocalesController.url("wizard"), type_module: true %>
|
||||
<%= preload_script_url "/bootstrap/site-settings-for-tests.js", type_module: true %>
|
||||
<%= theme_translations_lookup %>
|
||||
|
||||
<%= preload_script "vendor" %>
|
||||
<script nonce="<%= csp_nonce_placeholder %>">
|
||||
window._discourseQunitPluginNames = <%= Discourse.plugins.map(&:directory_name).to_json.html_safe %>;
|
||||
</script>
|
||||
|
||||
<%= tag.iframe srcdoc: tag.script(
|
||||
src: script_asset_path(EmberCli.script_chunks["qunit-live-reload"].first),
|
||||
nonce: csp_nonce_placeholder,
|
||||
type: "module",
|
||||
), style: "display: none;" %>
|
||||
|
||||
<%= preload_script "test-support" %>
|
||||
<%= preload_script "discourse" %>
|
||||
<%= preload_script "test" %>
|
||||
|
||||
<%= render "layouts/plugin_js",
|
||||
opts: {
|
||||
include_disabled: true,
|
||||
include_admin_asset: true,
|
||||
include_test_assets_for: @testing_plugins,
|
||||
only: @required_plugins
|
||||
}
|
||||
%>
|
||||
<%= preload_script_url "/bootstrap/site-settings-for-tests.js", type_module: true %>
|
||||
|
||||
<%= theme_lookup("head_tag") %>
|
||||
<%= theme_tests %>
|
||||
|
||||
<%= tag.meta id: 'data-discourse-setup', data: client_side_setup_data %>
|
||||
<meta property="og:title" content="">
|
||||
<meta property="og:url" content="">
|
||||
<meta name="twitter:title" content="">
|
||||
<meta name="twitter:url" content="/">
|
||||
<meta name="discourse/config/environment" content="<%=u discourse_config_environment(testing: true) %>" />
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<link rel="canonical" href="/">
|
||||
|
||||
<style>
|
||||
<%= File.read("#{Rails.root}/frontend/discourse/node_modules/qunit/qunit/qunit.css").html_safe %>
|
||||
|
||||
#ember-testing * {
|
||||
-webkit-transition: none !important;
|
||||
-moz-transition: none !important;
|
||||
-o-transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<%= discourse_stylesheet_link_tag(:common, theme_id: nil) %>
|
||||
<%= discourse_stylesheet_link_tag(:desktop, theme_id: nil) %>
|
||||
<%= discourse_stylesheet_link_tag(:admin) %>
|
||||
<%= discourse_stylesheet_link_tag(:wizard) %>
|
||||
|
||||
<%- Discourse.find_plugin_css_assets(include_disabled: true, only: @required_plugins, mobile_view: false, desktop_view: true, request: request).each do |file| %>
|
||||
<%= discourse_stylesheet_link_tag(file) %>
|
||||
<%- end %>
|
||||
<%- else %>
|
||||
<style>
|
||||
html {
|
||||
font-family: Arial;
|
||||
}
|
||||
</style>
|
||||
<%- end %>
|
||||
|
||||
<%- if params['testem'] %>
|
||||
<script defer src="/assets/testem.js" nonce="<%= csp_nonce_placeholder %>"></script>
|
||||
<script defer src="/testem.js" nonce="<%= csp_nonce_placeholder %>"></script>
|
||||
<%- end %>
|
||||
</head>
|
||||
<body>
|
||||
<%- if !@has_test_bundle %>
|
||||
This is a production installation of Discourse, and cannot be used for theme testing.
|
||||
For more information, see <a href="https://meta.discourse.org/t/66857">this guide</a>.
|
||||
<% elsif @suggested_themes %>
|
||||
<h2>Theme QUnit Test Runner</h2>
|
||||
|
||||
<%- if @suggested_themes.empty? %>
|
||||
<p>Cannot find any theme tests.</p>
|
||||
<%- else %>
|
||||
<h3>Select a theme/component: </h3>
|
||||
<%- @suggested_themes.each do |(id, name)| %>
|
||||
<h4><%= link_to name, theme_qunit_path(id: id) %></h4>
|
||||
<%- end %>
|
||||
<%- end %>
|
||||
<% else %>
|
||||
<%= preload_script "scripts/discourse-test-listen-boot" %>
|
||||
<%= preload_script "scripts/discourse-boot" %>
|
||||
<discourse-assets-icons></discourse-assets-icons>
|
||||
|
||||
<script nonce="<%= csp_nonce_placeholder %>">
|
||||
window.EmberENV = {
|
||||
_DEFAULT_ASYNC_OBSERVERS: true,
|
||||
};
|
||||
</script>
|
||||
|
||||
<%= preload_script "test-entrypoint", type_module: true %>
|
||||
<%- end %>
|
||||
|
||||
<%= discourse_stylesheet_link_tag("qunit-custom", theme_id: nil) %>
|
||||
@@ -197,7 +197,7 @@
|
||||
|
||||
<% if @topic_view.print %>
|
||||
<% content_for :after_body do %>
|
||||
<%= preload_script('print-page') %>
|
||||
<%= preload_script('js/print-page') %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,3 +1,89 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
|
||||
bin/ember-cli -u
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
const { parseArgs } = require("util");
|
||||
const concurrently = require("concurrently");
|
||||
|
||||
const RAILS_ROOT = path.resolve(__dirname, "..");
|
||||
|
||||
let nodeModulesOutdated = false;
|
||||
try {
|
||||
const installed = fs.readFileSync(
|
||||
path.join(RAILS_ROOT, "node_modules/.pnpm/lock.yaml"),
|
||||
"utf8"
|
||||
);
|
||||
const lockfile = fs.readFileSync(
|
||||
path.join(RAILS_ROOT, "pnpm-lock.yaml"),
|
||||
"utf8"
|
||||
);
|
||||
nodeModulesOutdated = installed !== lockfile;
|
||||
} catch (e) {
|
||||
nodeModulesOutdated = true;
|
||||
}
|
||||
|
||||
if (nodeModulesOutdated) {
|
||||
console.log(
|
||||
"[bin/dev] Detected outdated or missing node_modules. Running pnpm install..."
|
||||
);
|
||||
const result = spawnSync("pnpm", [`--dir=${RAILS_ROOT}`, "install"], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
if (result.error && result.error.code === "ENOENT") {
|
||||
console.error("pnpm is not installed. run `npm install -g pnpm`");
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const serverEnv = { ...process.env };
|
||||
serverEnv.UNICORN_PORT ||= "3000";
|
||||
|
||||
if (process.env.CODESPACE_NAME) {
|
||||
serverEnv.DISCOURSE_PORT = "443";
|
||||
serverEnv.DISCOURSE_FORCE_HTTPS = "1";
|
||||
serverEnv.DISCOURSE_FORCE_HOSTNAME = `${process.env.CODESPACE_NAME}-${serverEnv.UNICORN_PORT}.${process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}`;
|
||||
}
|
||||
|
||||
const { values: args } = parseArgs({
|
||||
options: { only: { type: "string" } },
|
||||
});
|
||||
|
||||
const commands = [];
|
||||
|
||||
if (!args.only || args.only === "rails") {
|
||||
commands.push({
|
||||
name: "rails",
|
||||
command: path.join(RAILS_ROOT, "bin/pitchfork"),
|
||||
cwd: RAILS_ROOT,
|
||||
env: serverEnv,
|
||||
prefixColor: "#22c4f1",
|
||||
});
|
||||
}
|
||||
|
||||
if (!args.only || args.only === "ember") {
|
||||
commands.push({
|
||||
name: "ember",
|
||||
command: "./rolldown.mjs",
|
||||
cwd: path.join(RAILS_ROOT, "frontend/discourse"),
|
||||
prefixColor: "#f27a21",
|
||||
});
|
||||
}
|
||||
|
||||
if (commands.length === 0) {
|
||||
console.error(`[bin/dev] --only must be 'rails' or 'ember'`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { result } = concurrently(commands, {
|
||||
killOthersOn: ["success", "failure"],
|
||||
killSignal: "SIGTERM",
|
||||
});
|
||||
|
||||
result.then(
|
||||
() => process.exit(0),
|
||||
() => process.exit(1)
|
||||
);
|
||||
|
||||
@@ -100,7 +100,6 @@ fi
|
||||
docker run -d \
|
||||
-p $local_publish:8025:8025 \
|
||||
-p $local_publish:3000:3000 \
|
||||
-p $local_publish:4200:4200 \
|
||||
-p $local_publish:9292:9292 \
|
||||
-p $local_publish:9405:9405 \
|
||||
-v "$DATA_DIR:/shared/postgres_data:delegated" \
|
||||
@@ -128,4 +127,3 @@ if [ "${initialize}" = "initialize" ]; then
|
||||
echo "Creating admin user..."
|
||||
"${SCRIPTPATH}/rake" admin:create
|
||||
fi
|
||||
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
exec "$(dirname "$0")/exec" bin/dev "$@"
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
exec "$(dirname "$0")/exec" bin/ember-cli "$@"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
exec "$(dirname "$0")/exec" pnpm "$@"
|
||||
+84
-162
@@ -1,182 +1,104 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "pathname"
|
||||
require "open3"
|
||||
require "webrick"
|
||||
|
||||
RAILS_ROOT = File.expand_path("../../", Pathname.new(__FILE__).realpath)
|
||||
PORT = ENV["UNICORN_PORT"] ||= "3000"
|
||||
HOSTNAME = ENV["DISCOURSE_HOSTNAME"] ||= "127.0.0.1"
|
||||
CUSTOM_ARGS = %w[--try --test --build --server --unicorn -u --forward-host]
|
||||
PROXY =
|
||||
if ARGV.include?("--try")
|
||||
"https://try.discourse.org"
|
||||
else
|
||||
"http://#{HOSTNAME}:#{PORT}"
|
||||
end
|
||||
|
||||
def process_running?(pid)
|
||||
!!Process.kill(0, pid)
|
||||
rescue Errno::ESRCH
|
||||
false
|
||||
end
|
||||
|
||||
def kill_tree(root_pid, signal)
|
||||
return unless root_pid
|
||||
children = `pgrep -P #{root_pid} 2>/dev/null`.to_s.split.map(&:to_i)
|
||||
children.each { |c| kill_tree(c, signal) }
|
||||
Process.kill(signal, root_pid)
|
||||
rescue Errno::ESRCH, Errno::EPERM
|
||||
# already gone
|
||||
end
|
||||
|
||||
command =
|
||||
if ARGV.include?("--test")
|
||||
"test"
|
||||
elsif ARGV.include?("--build")
|
||||
"build"
|
||||
else
|
||||
"server"
|
||||
end
|
||||
|
||||
class String
|
||||
def cyan
|
||||
"\e[36m#{self}\e[0m"
|
||||
end
|
||||
|
||||
def red
|
||||
"\033[31m#{self}\e[0m"
|
||||
end
|
||||
end
|
||||
ALLOWED_ARGS = %w[-u --build]
|
||||
EXTRA_ARGS = ARGV - ALLOWED_ARGS
|
||||
|
||||
if ARGV.include?("-h") || ARGV.include?("--help")
|
||||
puts "ember-cli OPTIONS"
|
||||
puts "#{"--try".cyan} To proxy try.discourse.org"
|
||||
puts "#{"--test".cyan} To run the test suite"
|
||||
puts "#{"--server, -u".cyan} To run a server as well"
|
||||
puts "The rest of the arguments are passed to ember server per:", ""
|
||||
exec "pnpm ember #{command} --help"
|
||||
puts <<~MSG
|
||||
bin/ember-cli is deprecated. Use bin/dev instead.
|
||||
|
||||
Usage (backwards-compatible):
|
||||
bin/ember-cli Runs `bin/dev --only ember` (frontend bundler only)
|
||||
bin/ember-cli -u Runs `bin/dev` (Rails + frontend bundler)
|
||||
bin/ember-cli --build Runs `pnpm build` (one-off frontend build, no watching)
|
||||
MSG
|
||||
exit 0
|
||||
end
|
||||
|
||||
args = ["--dir=frontend/discourse", "ember", command] + (ARGV - CUSTOM_ARGS)
|
||||
abort <<~MSG if !EXTRA_ARGS.empty?
|
||||
bin/ember-cli no longer accepts arguments: #{EXTRA_ARGS.join(" ")}
|
||||
|
||||
if !args.include?("test") && !args.include?("build") && !args.include?("--proxy")
|
||||
args << "--proxy"
|
||||
args << PROXY
|
||||
end
|
||||
Discourse now runs from a single port via `bin/dev`. Please switch to:
|
||||
bin/dev
|
||||
|
||||
node_modules_outdated =
|
||||
begin
|
||||
File.read("node_modules/.pnpm/lock.yaml") != File.read("pnpm-lock.yaml")
|
||||
rescue Errno::ENOENT
|
||||
true
|
||||
bin/ember-cli is retained only for backwards compatibility and supports:
|
||||
bin/ember-cli (equivalent to `bin/dev --only ember`)
|
||||
bin/ember-cli -u (equivalent to `bin/dev`)
|
||||
bin/ember-cli --build (equivalent to `pnpm build`)
|
||||
MSG
|
||||
|
||||
child_command, display_command, run_stub_server =
|
||||
if ARGV.include?("--build")
|
||||
[%w[pnpm build], "pnpm build", false]
|
||||
elsif ARGV.include?("-u")
|
||||
[[File.expand_path("dev", __dir__)], "bin/dev", true]
|
||||
else
|
||||
[[File.expand_path("dev", __dir__), "--only", "ember"], "bin/dev --only ember", true]
|
||||
end
|
||||
|
||||
if node_modules_outdated
|
||||
puts "[bin/ember-cli] Detected outdated or missing node_modules. Running pnpm install..."
|
||||
warn "\e[31m[bin/ember-cli] DEPRECATED: please use `#{display_command}` instead...\e[0m"
|
||||
|
||||
if !system "pnpm", "--dir=#{RAILS_ROOT}", "install"
|
||||
if !system("command -v pnpm >/dev/null;")
|
||||
abort "pnpm is not installed. run `npm install -g pnpm`"
|
||||
end
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
stub_port = (ENV["EMBER_CLI_STUB_PORT"] || 4200).to_i
|
||||
target_port = ENV["UNICORN_PORT"] || "3000"
|
||||
|
||||
pnpm_env = {
|
||||
"TERM" => "dumb", # simple output from ember-cli, so we can parse/forward it more easily
|
||||
}
|
||||
pnpm_env["FORWARD_HOST"] = "true" if ARGV.include?("--forward-host")
|
||||
stub_message = <<~MSG
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Ember CLI Removed</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; max-width: 36rem; margin: 4rem auto; padding: 0 1rem; color: #222; line-height: 1.5; }
|
||||
h1 { font-size: 1.25rem; margin-bottom: 1rem; }
|
||||
code { background: #f3f3f3; padding: 0.1rem 0.35rem; border-radius: 3px; font-size: 0.95em; }
|
||||
a { color: #08c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Ember CLI has been replaced with Rolldown</h1>
|
||||
<p>Discourse is now served from a single port in development. Visit <a href="http://localhost:#{target_port}">http://localhost:#{target_port}</a>.</p>
|
||||
<p>To start Rails and the Rolldown build in a single command, use <code>bin/dev</code>.</p>
|
||||
<p>To launch the standalone frontend bundler, use <code>bin/dev --only ember</code>.</p>
|
||||
</body>
|
||||
</html>
|
||||
MSG
|
||||
|
||||
if ARGV.include?("-u") || ARGV.include?("--server") || ARGV.include?("--unicorn")
|
||||
server_env = { "DISCOURSE_PORT" => ENV["DISCOURSE_PORT"] || "4200" }
|
||||
|
||||
if command == "server" && ENV["CODESPACE_NAME"]
|
||||
server_env.merge!(
|
||||
{
|
||||
"DISCOURSE_PORT" => "443",
|
||||
"DISCOURSE_FORCE_HTTPS" => "1",
|
||||
"DISCOURSE_FORCE_HOSTNAME" =>
|
||||
"#{ENV["CODESPACE_NAME"]}-4200.#{ENV["GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"]}",
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
# Own process group so terminal Ctrl+C only hits bin/ember-cli.
|
||||
# The INT trap below is the only signaller.
|
||||
server_pid = spawn(server_env, "#{__dir__}/pitchfork", pgroup: true)
|
||||
ember_cli_pid = nil
|
||||
sigint_received = false
|
||||
|
||||
Thread.new do
|
||||
Open3.popen2e(pnpm_env, "pnpm", *args.to_a.flatten) do |i, oe, t|
|
||||
ember_cli_pid = t.pid
|
||||
puts "Ember CLI running on PID: #{ember_cli_pid}"
|
||||
oe.each do |line|
|
||||
if line.include?("\e[32m200\e") || line.include?("\e[36m304\e[0m") ||
|
||||
line.include?("POST /message-bus")
|
||||
# skip 200s and 304s and message bus
|
||||
else
|
||||
puts "[ember-cli] #{line}"
|
||||
end
|
||||
end
|
||||
end
|
||||
# On Ctrl+C the INT trap already TERMs the server. Only do it here if
|
||||
# ember-cli exited on its own.
|
||||
if !sigint_received && process_running?(server_pid)
|
||||
puts "[bin/ember-cli] ember-cli process stopped. Terminating server."
|
||||
Process.kill("TERM", server_pid)
|
||||
stub_server =
|
||||
if run_stub_server
|
||||
begin
|
||||
WEBrick::HTTPServer.new(
|
||||
Port: stub_port,
|
||||
BindAddress: "127.0.0.1",
|
||||
Logger: WEBrick::Log.new(File::NULL),
|
||||
AccessLog: [],
|
||||
)
|
||||
rescue Errno::EADDRINUSE
|
||||
warn "[bin/ember-cli] Port #{stub_port} already in use; skipping deprecation stub server."
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
int_count = 0
|
||||
trap("INT") do
|
||||
sigint_received = true
|
||||
int_count += 1
|
||||
|
||||
if int_count == 1
|
||||
# TERM the supervisor. Its trap forwards to the master, which gracefully
|
||||
# stops workers and Demon::Sidekiq.
|
||||
begin
|
||||
Process.kill("TERM", server_pid)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
|
||||
if ember_cli_pid
|
||||
begin
|
||||
Process.kill("TERM", ember_cli_pid)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# If shutdown drags, show the force-quit option.
|
||||
Thread.new do
|
||||
sleep 2
|
||||
if process_running?(server_pid)
|
||||
puts "\n[bin/ember-cli] Shutting down... press Ctrl+C again to force quit."
|
||||
end
|
||||
end
|
||||
else
|
||||
puts "\n[bin/ember-cli] Forcing shutdown..."
|
||||
# Catch orphans (e.g. stuck sidekiq).
|
||||
kill_tree(server_pid, "KILL")
|
||||
kill_tree(ember_cli_pid, "KILL") if ember_cli_pid
|
||||
exit!(1)
|
||||
end
|
||||
if stub_server
|
||||
stub_server.mount_proc "/" do |_req, res|
|
||||
res.status = 410
|
||||
res["Content-Type"] = "text/html; charset=utf-8"
|
||||
res.body = stub_message
|
||||
end
|
||||
|
||||
Process.wait(server_pid)
|
||||
|
||||
# Drain pgroup so shutdown messages don't print after the exit.
|
||||
deadline = Time.now + 10
|
||||
sleep 0.05 while Time.now < deadline && !`pgrep -g #{server_pid} 2>/dev/null`.strip.empty?
|
||||
|
||||
if ember_cli_pid && process_running?(ember_cli_pid)
|
||||
puts "[bin/ember-cli] server process stopped. Terminating ember-cli."
|
||||
Process.kill("TERM", ember_cli_pid)
|
||||
end
|
||||
else
|
||||
exec(pnpm_env, "pnpm", *args.to_a.flatten)
|
||||
Thread.new { stub_server.start }
|
||||
end
|
||||
|
||||
child_pid = spawn(*child_command)
|
||||
|
||||
# Children in the foreground process group already receive INT from the terminal;
|
||||
# swallow it here so we can clean up the stub server.
|
||||
trap("INT") {}
|
||||
|
||||
begin
|
||||
Process.wait(child_pid)
|
||||
ensure
|
||||
stub_server&.shutdown
|
||||
end
|
||||
|
||||
exit($?.exitstatus || 0)
|
||||
|
||||
@@ -503,11 +503,9 @@ class QunitRunner
|
||||
def build_theme_test_pages(query)
|
||||
pages =
|
||||
if ENV["THEME_IDS"] && !ENV["THEME_IDS"].empty?
|
||||
ENV["THEME_IDS"]
|
||||
.split("|")
|
||||
.map { |theme_id| "#{@qunit_path}?#{query}&testem=1&id=#{theme_id}" }
|
||||
ENV["THEME_IDS"].split("|").map { |theme_id| "#{@qunit_path}?#{query}&id=#{theme_id}" }
|
||||
else
|
||||
["#{@qunit_path}?#{query}&testem=1"]
|
||||
["#{@qunit_path}?#{query}"]
|
||||
end
|
||||
|
||||
pages.shuffle.join(",")
|
||||
@@ -532,6 +530,7 @@ class QunitRunner
|
||||
params["theme_id"] = @theme_id if @theme_id
|
||||
params["target"] = resolved_target if resolved_target && !@plugin_targets.any?
|
||||
params["report_requests"] = "1" if @report_requests
|
||||
params["testem"] = "1"
|
||||
|
||||
encode_query_string(params)
|
||||
end
|
||||
|
||||
@@ -54,7 +54,8 @@ if defined?(Rack::MiniProfiler) && defined?(Rack::MiniProfiler::Config)
|
||||
/topics/timings
|
||||
/uploads/
|
||||
/user_avatar/
|
||||
].map { |path| "#{Discourse.base_path}#{path}" }.concat([/.*theme-qunit/])
|
||||
/theme-qunit/
|
||||
].map { |path| "#{Discourse.base_path}#{path}" }.concat([%r{/(\d+/)?(theme-qunit|tests)}])
|
||||
|
||||
# we DO NOT WANT mini-profiler loading on anything but real desktops and laptops
|
||||
# so let's rule out all handheld, tablet, and mobile devices
|
||||
|
||||
@@ -12,6 +12,8 @@ Rails.application.config.assets.version = "2-#{GlobalSetting.asset_url_salt}"
|
||||
Rails.application.config.assets.paths.push(
|
||||
"#{Rails.public_path.join("javascripts")}",
|
||||
"#{Rails.root.join("frontend/discourse/dist/assets")}",
|
||||
"#{Rails.root.join("frontend/discourse/dist/@embroider/virtual")}",
|
||||
"#{Rails.root.join("frontend/discourse/scripts")}",
|
||||
)
|
||||
|
||||
Rails.application.config.assets.paths.push(
|
||||
@@ -29,3 +31,5 @@ Rails.application.config.assets.excluded_paths.push(
|
||||
Rails.application.config.assets.compilers.filter! do |type, compiler|
|
||||
type == "text/javascript" && compiler == Propshaft::Compiler::SourceMappingUrls
|
||||
end
|
||||
|
||||
Mime::Type.register "application/wasm", :wasm
|
||||
|
||||
@@ -137,6 +137,8 @@ before_service_worker_ready do |server, service_worker|
|
||||
end
|
||||
|
||||
if Rails.env.development?
|
||||
EmberCli.watch!
|
||||
|
||||
workers = server.worker_processes
|
||||
parts = ["#{workers} worker#{"s" if workers != 1}"]
|
||||
parts << "#{sidekiqs} sidekiq#{"s" if sidekiqs != 1}" if sidekiqs > 0
|
||||
|
||||
+12
-10
@@ -25,9 +25,7 @@ Discourse::Application.routes.draw do
|
||||
get "/404-body" => "exceptions#not_found_body"
|
||||
|
||||
if Rails.env.local?
|
||||
get "/bootstrap/core-css-for-tests.css" => "bootstrap#core_css_for_tests"
|
||||
get "/bootstrap/site-settings-for-tests.js" => "bootstrap#site_settings_for_tests"
|
||||
get "/bootstrap/plugin-test-info" => "bootstrap#plugin_test_info"
|
||||
end
|
||||
|
||||
# This is not a valid production route and is causing routing errors to be raised in
|
||||
@@ -1903,17 +1901,21 @@ Discourse::Application.routes.draw do
|
||||
get "/dev-mode" => "dev_mode#index"
|
||||
post "/dev-mode" => "dev_mode#enter", :as => "dev_mode_enter"
|
||||
|
||||
get "/theme-qunit" => "qunit#index",
|
||||
:constraints => ->(req) do
|
||||
req.params["id"].nil? && req.params["name"].nil? && req.params["url"].nil?
|
||||
end
|
||||
get "/theme-qunit" => "qunit#theme"
|
||||
get "/theme-tests", to: redirect("/theme-qunit")
|
||||
|
||||
# This is a special route that is used when theme QUnit tests are run through testem which appends a testem_id to the
|
||||
# path. Unfortunately, testem's proxy support does not allow us to easily remove this from the path, so we have to
|
||||
# handle it here.
|
||||
if Rails.env.development?
|
||||
get "/testem-theme-qunit/:testem_id/theme-qunit" => "qunit#theme",
|
||||
:constraints => {
|
||||
testem_id: /\d+/,
|
||||
}
|
||||
if Rails.env.local?
|
||||
get "/tests" => "qunit#core"
|
||||
|
||||
# This is a special route that is used when theme QUnit tests are run through testem which appends a testem_id to the
|
||||
# path. Unfortunately, testem's proxy support does not allow us to easily remove this from the path, so we have to
|
||||
# handle it here.
|
||||
get "/:testem_id/theme-qunit" => "qunit#theme", :constraints => { testem_id: /\d+/ }
|
||||
get "/:testem_id/tests" => "qunit#core", :constraints => { testem_id: /\d+/ }
|
||||
end
|
||||
|
||||
post "/push_notifications/subscribe" => "push_notification#subscribe"
|
||||
|
||||
@@ -34,7 +34,7 @@ Dev Containers can be used in a number of different IDEs, or directly using thei
|
||||
|
||||
1. Run the `dev/admin/create` task. Open the command palette and search for "Tasks: Run Tasks". It will present a menu of tasks; select `dev/admin/create` off of that list. You'll be prompted to enter an email address and a password for your admin user.
|
||||
|
||||
1. Visit `http://localhost:4200` in your browser to see your new Discourse instance
|
||||
1. Visit `http://localhost:3000` in your browser to see your new Discourse instance
|
||||
|
||||
1. All done! You can now make changes to Discourse's source code and see them reflected in the preview.
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ id: github-codespaces
|
||||
|
||||
1. Run the `dev/admin/create` task. Open the command palette with <kbd>Cmd/Ctrl + Shift + P</kbd> and search for "Tasks: Run Tasks". It will present a menu of tasks; select `dev/admin/create` off of that list. You'll be prompted to enter an email address and a password for your admin user.
|
||||
|
||||
1. Visit the "Ports" tab, and click the :globe_with_meridians: button for port 4200. This will open a new tab showing your development copy of Discourse
|
||||
1. Visit the "Ports" tab, and click the :globe_with_meridians: button for port 3000. This will open a new tab showing your development copy of Discourse
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -70,10 +70,10 @@ d/boot_dev --init
|
||||
d/rails s
|
||||
|
||||
# And in a separate terminal
|
||||
d/ember-cli
|
||||
d/dev --only ember
|
||||
```
|
||||
|
||||
...then open a browser on http://localhost:4200 and _voila!_, you should see Discourse.
|
||||
...then open a browser on http://localhost:3000 and _voila!_, you should see Discourse.
|
||||
|
||||
## Plugin Symlinks
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ RAILS_ENV=test bundle exec rake db:create db:migrate
|
||||
|
||||
Start rails + Ember servers, you have two options here.
|
||||
|
||||
**Option 1**: using two separate Terminal tabs/windows, run Rails and Ember CLI separately via
|
||||
**Option 1**: using two separate Terminal tabs/windows, run Rails and the frontend bundler separately via
|
||||
|
||||
```sh
|
||||
bundle exec rails server
|
||||
@@ -86,16 +86,16 @@ bundle exec rails server
|
||||
and
|
||||
|
||||
```sh
|
||||
bin/ember-cli
|
||||
bin/dev --only ember
|
||||
```
|
||||
|
||||
**Option 2**: using only one Terminal tab/window:
|
||||
|
||||
```sh
|
||||
bin/ember-cli -u # will run the Pitchfork server in the background
|
||||
bin/dev # runs Pitchfork and the frontend bundler together
|
||||
```
|
||||
|
||||
:tada: You should now be able to navigate to [http://localhost:4200](http://localhost:4200) to see your local Discourse installation. (Note that the first load can take up to a minute as the server is warmed up.)
|
||||
:tada: You should now be able to navigate to [http://localhost:3000](http://localhost:3000) to see your local Discourse installation. (Note that the first load can take up to a minute as the server is warmed up.)
|
||||
|
||||
You can also try running the specs:
|
||||
|
||||
|
||||
@@ -96,20 +96,20 @@ bin/rails db:migrate
|
||||
RAILS_ENV=test bin/rails db:create db:migrate
|
||||
```
|
||||
|
||||
Start rails and ember server:
|
||||
Start rails and the frontend bundler:
|
||||
|
||||
```sh
|
||||
bin/ember-cli -u
|
||||
bin/dev
|
||||
```
|
||||
|
||||
If the images are not appearing, use this command instead:
|
||||
(_you can also specify an IP if you are working on a remote server_)
|
||||
|
||||
```sh
|
||||
DISCOURSE_HOSTNAME=localhost UNICORN_LISTENER=localhost:3000 bin/ember-cli -u
|
||||
DISCOURSE_HOSTNAME=localhost UNICORN_LISTENER=localhost:3000 bin/dev
|
||||
```
|
||||
|
||||
You should now be able to navigate to [http://localhost:4200](http://localhost:4200) to see your local Discourse installation.
|
||||
You should now be able to navigate to [http://localhost:3000](http://localhost:3000) to see your local Discourse installation.
|
||||
|
||||
## Create New Admin
|
||||
|
||||
|
||||
@@ -110,15 +110,13 @@ bundle exec rails server
|
||||
|
||||
You should now be able to connect with your Discourse app on [http://localhost:3000](http://localhost:3000) - try it out!
|
||||
|
||||
**Starting with Discourse 2.5+ EmberCLI is required in development and these additional steps will be required:**
|
||||
|
||||
In a separate terminal instance, navigate to your discourse folder (`cd ~/discourse`) and run:
|
||||
|
||||
```sh
|
||||
bin/ember-cli
|
||||
bin/dev --only ember
|
||||
```
|
||||
|
||||
You should now be able to navigate to [http://localhost:4200](http://localhost:4200) to see your local Discourse installation.
|
||||
This starts the frontend bundler; the app remains served from [http://localhost:3000](http://localhost:3000).
|
||||
[/quote]
|
||||
|
||||
## Creating a Command to Start Discourse
|
||||
|
||||
@@ -16,6 +16,6 @@ If you've followed the [instructions to set up your local discourse](https://met
|
||||
|
||||
1. Re-start the server.
|
||||
|
||||
1. If the plugin has settings, you can edit them by going to `http://localhost:4200/admin/plugins` and clicking on "Settings" next to its name.
|
||||
1. If the plugin has settings, you can edit them by going to `http://localhost:3000/admin/plugins` and clicking on "Settings" next to its name.
|
||||
|
||||
If you'd like to install a plugin in production, [follow this guide](https://meta.discourse.org/t/install-plugins-in-discourse/19157/1).
|
||||
|
||||
@@ -33,10 +33,10 @@ rake db:migrate
|
||||
|
||||
_In general, however, running rake tasks without `RAILS_DB` set will target the default site._
|
||||
|
||||
To access the site, you'll need to run ember-cli with the `--forward-host` option.
|
||||
To access the site, start the dev server:
|
||||
|
||||
```sh
|
||||
bin/ember-cli -u --forward-host
|
||||
bin/dev
|
||||
```
|
||||
|
||||
You may now be able to view the your new site at http://alternate.localhost:4200, but if you cannot, you may need to add `alternate.localhost` to your `/etc/hosts` file or equivalent.
|
||||
You may now be able to view the your new site at http://alternate.localhost:3000, but if you cannot, you may need to add `alternate.localhost` to your `/etc/hosts` file or equivalent.
|
||||
|
||||
@@ -37,21 +37,21 @@ To obtain two different URLs, there are two main approaches:
|
||||
If your change is small enough to be easily feature-flagged, then you could add logic to toggle it based on a **URL query parameter**. Then your two urls could be
|
||||
|
||||
```
|
||||
http://localhost:4200?flag=before
|
||||
http://localhost:4200?flag=after
|
||||
http://localhost:3000?flag=before
|
||||
http://localhost:3000?flag=after
|
||||
```
|
||||
|
||||
If the change is too large for that, then you could clone Discourse into a second directory and launch a **second copy of ember-cli**. It can be proxied to the same Rails server using a command like
|
||||
If the change is too large for that, then you could clone Discourse into a second directory and launch a **second copy of rails**.
|
||||
|
||||
```sh
|
||||
EMBER_ENV=production pnpm ember serve --port 4201 --proxy http://localhost:3000
|
||||
EMBER_ENV=production UNICORN_PORT=3001 bin/dev
|
||||
```
|
||||
|
||||
And then your two URLs would be
|
||||
|
||||
```
|
||||
http://localhost:4200
|
||||
http://localhost:4201
|
||||
http://localhost:3000
|
||||
http://localhost:3001
|
||||
```
|
||||
|
||||
If you take this approach, make sure that both copies of the app have the performance telemetry you introduced in step 1 of this guide
|
||||
@@ -72,11 +72,11 @@ This is my `bench.json` file, which will take 300 samples of each target:
|
||||
},
|
||||
"expand": [
|
||||
{
|
||||
"url": "http://localhost:4200",
|
||||
"url": "http://localhost:3000",
|
||||
"name": "before"
|
||||
},
|
||||
{
|
||||
"url": "http://localhost:4201",
|
||||
"url": "http://localhost:3001",
|
||||
"name": "after"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -10,13 +10,13 @@ Rails system specs are used to simulate the actions of a real user using the app
|
||||
|
||||
We currently only support running system specs in Chrome, make sure you have Chrome installed before proceeding. Run `pnpm i` to ensure Playwright is correctly set up.
|
||||
|
||||
Since the Discourse app is an Ember Single Page Application, there are some unique constraints and challenges to writing system specs. It's important to keep in mind that you should always be observing for changes in the DOM in your tests, not manually waiting for things to happen or adding artificial sleep time. Also, the JavaScript build is separate from the Rails server, which means you must be running Ember CLI when writing system specs.
|
||||
Since the Discourse app is an Ember Single Page Application, there are some unique constraints and challenges to writing system specs. It's important to keep in mind that you should always be observing for changes in the DOM in your tests, not manually waiting for things to happen or adding artificial sleep time. Also, the JavaScript build is separate from the Rails server, which means you must be running the frontend bundler when writing system specs.
|
||||
|
||||
## Running system specs
|
||||
|
||||
Any system spec can be run with the `bin/rspec FILENAME.rb` command. By default the specs are run in a headless version of Chrome, meaning no browser window will open while the spec is running.
|
||||
|
||||
> :warning: If you do not already have the Discourse rails server running with `bin/ember-cli -u`, you will need to run `bin/ember-cli --build` after every JavaScript change to see these reflected in the headless browser. **It is recommended you just keep your local server running while writing system specs.**
|
||||
> :warning: If you do not already have the Discourse rails server running with `bin/dev`, you will need to run `pnpm --dir=frontend/discourse build` after every JavaScript change to see these reflected in the headless browser. **It is recommended you just keep your local server running while writing system specs.**
|
||||
>
|
||||
> Also, ensure you run rails migrations any time you make modifications to your local database schema.
|
||||
|
||||
|
||||
@@ -34,11 +34,11 @@ Once you've created this file, you should restart your local server and the plug
|
||||
|
||||
### An important Gotcha!
|
||||
|
||||
If you're used to regular rails development you might notice that plugins aren't quite as nice when it comes to reloading. In general, when you make changes to your plugin, you should <kbd>Ctrl</kbd>+<kbd>c</kbd> the server to stop it running, then run it again using `bin/ember-cli -u`.
|
||||
If you're used to regular rails development you might notice that plugins aren't quite as nice when it comes to reloading. In general, when you make changes to your plugin, you should <kbd>Ctrl</kbd>+<kbd>c</kbd> the server to stop it running, then run it again using `bin/dev`.
|
||||
|
||||
### My changes weren't picked up! :warning:
|
||||
|
||||
Sometimes the cache isn't cleared fully, especially when you create new files or delete old files. To get around this issue, remove your `tmp` folder and start rails again. On a mac you can do it in one command: `rm -rf tmp; bin/ember-cli -u`.
|
||||
Sometimes the cache isn't cleared fully, especially when you create new files or delete old files. To get around this issue, remove your `tmp` folder and start rails again. On a mac you can do it in one command: `rm -rf tmp; bin/dev`.
|
||||
|
||||
### Checking that your plugin was loaded
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Previous tutorial: https://meta.discourse.org/t/developing-discourse-plugins-par
|
||||
|
||||
Did you know that Discourse has two large test suites for its code base? On the server side, our Ruby code has a test suite that uses [rspec](https://rspec.info/). For the browser application, we have a [qunit](https://qunitjs.com/) suite that has [ember-testing](https://guides.emberjs.com/release/testing/testing-application/) included.
|
||||
|
||||
Assuming you have a development environment set up, if you visit the `http://localhost:4200/tests` URL you will start running the JavaScript test suite in your browser. One fun aspect is that you can see it testing the application in a miniature window in the bottom right corner:
|
||||
Assuming you have a development environment set up, if you visit the `http://localhost:3000/tests` URL you will start running the JavaScript test suite in your browser. One fun aspect is that you can see it testing the application in a miniature window in the bottom right corner:
|
||||
|
||||
<img src="//assets-meta-cdck-prod-meta.s3.dualstack.us-west-1.amazonaws.com/original/3X/6/2/62a63eca67d134def1580fd9fbd84ff62b531ee1.png" width="690" height="481">
|
||||
|
||||
@@ -76,7 +76,7 @@ After we simulate a click on the button, we can check whether the tentacle appea
|
||||
assert.ok(exists(".tentacle"), "the tentacle wants to rule the world!");
|
||||
```
|
||||
|
||||
Not too bad is it? You can try the test yourself by visiting `http://localhost:4200/tests?qunit_single_plugin=purple-tentacle&qunit_skip_core=1` on your development machine. You should very quickly see the purple tentacle appear and all tests will pass.
|
||||
Not too bad is it? You can try the test yourself by visiting `http://localhost:3000/tests?qunit_single_plugin=purple-tentacle&qunit_skip_core=1` on your development machine. You should very quickly see the purple tentacle appear and all tests will pass.
|
||||
|
||||
If you want to run the plugin qunit tests on the command line using PhantomJS, you can run
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ In your local terminal, navigate to a directory where you want to store your new
|
||||
|
||||
When setup is done, you'll see `✅ Done!`, and you'll be prompted to start "watching" the theme. Answer yes, and then work through the watching setup.
|
||||
|
||||
For the root URL, enter the base URL of the Discourse site you'd like to sync the theme to. For a local development environment, use something like `http://localhost:4200`. For a production site, use something like `https://meta.discourse.org`. Or for Theme Creator, use `https://discourse.theme-creator.io`.
|
||||
For the root URL, enter the base URL of the Discourse site you'd like to sync the theme to. For a local development environment, use something like `http://localhost:3000`. For a production site, use something like `https://meta.discourse.org`. Or for Theme Creator, use `https://discourse.theme-creator.io`.
|
||||
|
||||
Now you'll need to generate an API key. For a local development environment or production forum, visit the "API Keys" section of the admin panel, choose "Add API key", and create one associated with your user account, and a "Global" scope. Then paste it into the `discourse_theme` CLI.
|
||||
|
||||
|
||||
@@ -30,4 +30,13 @@ export default [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
babelOptions: {
|
||||
configFile: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
engine-strict = true
|
||||
@@ -1,329 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const express = require("express");
|
||||
const cleanBaseURL = require("clean-base-url");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const fsPromises = fs.promises;
|
||||
const { Buffer } = require("node:buffer");
|
||||
const { env } = require("node:process");
|
||||
const { glob } = require("glob");
|
||||
const { HTMLRewriter } = require("html-rewriter-wasm");
|
||||
|
||||
async function listDistAssets(outputPath) {
|
||||
const files = await glob("**/*.js", {
|
||||
nodir: true,
|
||||
cwd: `${outputPath}/assets`,
|
||||
});
|
||||
return new Set(files);
|
||||
}
|
||||
|
||||
function updateScriptReferences({
|
||||
chunkInfos,
|
||||
rewriter,
|
||||
selector,
|
||||
attribute,
|
||||
baseURL,
|
||||
distAssets,
|
||||
}) {
|
||||
const handledEntrypoints = new Set();
|
||||
|
||||
rewriter.on(selector, {
|
||||
element(element) {
|
||||
const entrypointName = element.getAttribute("data-discourse-entrypoint");
|
||||
|
||||
if (handledEntrypoints.has(entrypointName)) {
|
||||
element.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
let chunks = chunkInfos[`assets/${entrypointName}.js`]?.assets;
|
||||
if (!chunks) {
|
||||
if (distAssets.has(`${entrypointName}.js`)) {
|
||||
chunks = [`assets/${entrypointName}.js`];
|
||||
} else if (entrypointName === "vendor") {
|
||||
// support embroider-fingerprinted vendor when running with `-prod` flag
|
||||
const vendorFilename = [...distAssets].find((key) =>
|
||||
key.startsWith("vendor.")
|
||||
);
|
||||
chunks = [`assets/${vendorFilename}`];
|
||||
} else {
|
||||
// Not an ember-cli asset, do not rewrite
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const newElements = chunks.map((chunk) => {
|
||||
let newElement = `<${element.tagName}`;
|
||||
|
||||
for (const [attr, value] of element.attributes) {
|
||||
if (attr === attribute) {
|
||||
newElement += ` ${attribute}="${baseURL}${chunk}"`;
|
||||
} else if (value === "") {
|
||||
newElement += ` ${attr}`;
|
||||
} else {
|
||||
newElement += ` ${attr}="${value}"`;
|
||||
}
|
||||
}
|
||||
|
||||
newElement += ` data-ember-cli-rewritten="true"`;
|
||||
newElement += `>`;
|
||||
|
||||
if (element.tagName === "script") {
|
||||
newElement += `</script>`;
|
||||
}
|
||||
|
||||
return newElement;
|
||||
});
|
||||
|
||||
if (
|
||||
entrypointName === "discourse" &&
|
||||
element.tagName.toLowerCase() === "script"
|
||||
) {
|
||||
let nonce = "";
|
||||
for (const [attr, value] of element.attributes) {
|
||||
if (attr === "nonce") {
|
||||
nonce = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!nonce) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
"Expected to find a nonce= attribute on the main discourse script tag, but none was found. ember-cli-live-reload may not work correctly."
|
||||
);
|
||||
}
|
||||
|
||||
// ember-cli-live-reload doesn't select ports correctly, so we use _lr/livereload directly
|
||||
// (important for cloud development environments like GitHub CodeSpaces)
|
||||
newElements.unshift(
|
||||
`<script nonce="${nonce}">window.LiveReloadOptions = { "path": "_lr/livereload", "host": location.hostname, "port": location.port || (location.protocol === "https:" ? 443 : 80), "https": location.protocol === "https:" }</script>`,
|
||||
`<script async src="/_lr/livereload.js" nonce="${nonce}"></script>`
|
||||
);
|
||||
}
|
||||
|
||||
element.replace(newElements.join("\n"), { html: true });
|
||||
|
||||
handledEntrypoints.add(entrypointName);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleRequest(proxy, baseURL, req, res, outputPath) {
|
||||
// x-forwarded-host is used in e.g. GitHub CodeSpaces
|
||||
let originalHost = req.headers["x-forwarded-host"] || req.headers.host;
|
||||
|
||||
if (env["FORWARD_HOST"] === "true") {
|
||||
if (/^localhost(\:|$)/.test(originalHost)) {
|
||||
// Can't access default site in multisite via "localhost", redirect to 127.0.0.1
|
||||
res.redirect(
|
||||
307,
|
||||
`http://${originalHost.replace("localhost", "127.0.0.1")}${req.path}`
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
req.headers.host = originalHost;
|
||||
}
|
||||
} else {
|
||||
req.headers.host = new URL(proxy).host;
|
||||
}
|
||||
|
||||
if (req.headers["Origin"]) {
|
||||
req.headers["Origin"] = req.headers["Origin"]
|
||||
.replace(req.headers.host, originalHost)
|
||||
.replace(/^https/, "http");
|
||||
}
|
||||
|
||||
if (req.headers["Referer"]) {
|
||||
req.headers["Referer"] = req.headers["Referer"]
|
||||
.replace(req.headers.host, originalHost)
|
||||
.replace(/^https/, "http");
|
||||
}
|
||||
|
||||
let url = `${proxy}${req.path}`;
|
||||
const queryLoc = req.url.indexOf("?");
|
||||
if (queryLoc !== -1) {
|
||||
url += req.url.slice(queryLoc);
|
||||
}
|
||||
|
||||
if (req.method === "GET") {
|
||||
req.headers["X-Discourse-Ember-CLI"] = "true";
|
||||
}
|
||||
|
||||
const { default: fetch } = await import("node-fetch");
|
||||
const response = await fetch(url, {
|
||||
method: req.method,
|
||||
body: /GET|HEAD/.test(req.method) ? null : req.body,
|
||||
headers: req.headers,
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
response.headers.forEach((value, header) => {
|
||||
if (header === "set-cookie") {
|
||||
// Special handling to get array of multiple Set-Cookie header values
|
||||
// per https://github.com/node-fetch/node-fetch/issues/251#issuecomment-428143940
|
||||
res.set("set-cookie", response.headers.raw()["set-cookie"]);
|
||||
} else {
|
||||
res.set(header, value);
|
||||
}
|
||||
});
|
||||
res.set("content-encoding", null);
|
||||
|
||||
const location = response.headers.get("location");
|
||||
if (location) {
|
||||
const newLocation = location.replace(proxy, `http://${originalHost}`);
|
||||
res.set("location", newLocation);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type");
|
||||
const isHTML = contentType?.startsWith("text/html");
|
||||
|
||||
res.status(response.status);
|
||||
|
||||
if (isHTML) {
|
||||
const [responseText, chunkInfoText, distAssets] = await Promise.all([
|
||||
response.text(),
|
||||
fsPromises.readFile(`${outputPath}/assets.json`, "utf-8"),
|
||||
listDistAssets(outputPath),
|
||||
]);
|
||||
|
||||
const chunkInfos = JSON.parse(chunkInfoText);
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
let output = "";
|
||||
const rewriter = new HTMLRewriter((outputChunk) => {
|
||||
output += decoder.decode(outputChunk);
|
||||
});
|
||||
|
||||
updateScriptReferences({
|
||||
chunkInfos,
|
||||
rewriter,
|
||||
selector: "script[data-discourse-entrypoint]",
|
||||
attribute: "src",
|
||||
baseURL,
|
||||
distAssets,
|
||||
});
|
||||
|
||||
updateScriptReferences({
|
||||
chunkInfos,
|
||||
rewriter,
|
||||
selector: "link[rel=preload][data-discourse-entrypoint]",
|
||||
attribute: "href",
|
||||
baseURL,
|
||||
distAssets,
|
||||
});
|
||||
|
||||
try {
|
||||
await rewriter.write(encoder.encode(responseText));
|
||||
await rewriter.end();
|
||||
} finally {
|
||||
rewriter.free();
|
||||
}
|
||||
|
||||
res.send(output);
|
||||
} else {
|
||||
res.send(Buffer.from(await response.arrayBuffer()));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: require("./package").name,
|
||||
|
||||
isDevelopingAddon() {
|
||||
return true;
|
||||
},
|
||||
|
||||
serverMiddleware(config) {
|
||||
const app = config.app;
|
||||
let { proxy, rootURL, baseURL } = config.options;
|
||||
const outputPath = config.options.path ?? config.options.outputPath;
|
||||
|
||||
if (!proxy) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`
|
||||
Discourse can't be run without a \`--proxy\` setting, because it needs a Rails application
|
||||
to serve API requests. For example:
|
||||
|
||||
pnpm ember serve --proxy "http://localhost:3000"\n`);
|
||||
throw "--proxy argument is required";
|
||||
}
|
||||
|
||||
baseURL = rootURL === "" ? "/" : cleanBaseURL(rootURL || baseURL);
|
||||
|
||||
const rawMiddleware = express.raw({ type: () => true, limit: "100mb" });
|
||||
const pathRestrictedRawMiddleware = (req, res, next) => {
|
||||
if (this.shouldHandleRequest(req, baseURL)) {
|
||||
return rawMiddleware(req, res, next);
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
};
|
||||
|
||||
app.use(
|
||||
"/favicon.ico",
|
||||
express.static(
|
||||
path.join(
|
||||
__dirname,
|
||||
"../../../../../../public/images/discourse-logo-sketch-small.png"
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
app.use(pathRestrictedRawMiddleware, async (req, res, next) => {
|
||||
try {
|
||||
if (this.shouldHandleRequest(req, baseURL)) {
|
||||
await handleRequest(proxy, baseURL, req, res, outputPath);
|
||||
} else {
|
||||
// Fixes issues when using e.g. "localhost" instead of loopback IP address
|
||||
req.headers.host = "127.0.0.1";
|
||||
}
|
||||
} catch (error) {
|
||||
res.send(`
|
||||
<html>
|
||||
<h1>Discourse Ember CLI Proxy Error</h1>
|
||||
<pre><code>${error.stack}</code></pre>
|
||||
</html>
|
||||
`);
|
||||
} finally {
|
||||
if (!res.headersSent) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
shouldHandleRequest(request, baseURL) {
|
||||
if (
|
||||
[
|
||||
`${baseURL}tests/index.html`,
|
||||
`${baseURL}ember-cli-live-reload.js`,
|
||||
`${baseURL}testem.js`,
|
||||
`${baseURL}assets/test-i18n.js`,
|
||||
].includes(request.path)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All JS assets are served by Ember CLI, except for
|
||||
// plugin assets which end in _extra.js
|
||||
if (
|
||||
request.path.startsWith(`${baseURL}assets/`) &&
|
||||
!request.path.endsWith("_extra.js")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request.path.startsWith(`${baseURL}_lr/`)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request.path.startsWith(`${baseURL}message-bus/`)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "custom-proxy",
|
||||
"version": "1.0.0",
|
||||
"description": "Express.js middleware which injects ember-cli asset URLs into Discourse's HTML",
|
||||
"author": "Discourse",
|
||||
"license": "GPL-2.0-only",
|
||||
"keywords": [
|
||||
"ember-addon"
|
||||
],
|
||||
"ember-addon": {
|
||||
"before": [
|
||||
"broccoli-serve-files",
|
||||
"proxy-server-middleware"
|
||||
],
|
||||
"after": [
|
||||
"broccoli-watcher"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"clean-base-url": "^1.0.0",
|
||||
"express": "^5.2.1",
|
||||
"glob": "^13.0.6",
|
||||
"html-entities": "^2.6.0",
|
||||
"html-rewriter-wasm": "^0.4.1",
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20",
|
||||
"npm": "please-use-pnpm",
|
||||
"yarn": "please-use-pnpm",
|
||||
"pnpm": "^10"
|
||||
}
|
||||
}
|
||||
@@ -20,15 +20,14 @@
|
||||
"dependencies": {
|
||||
"@embroider/addon-shim": "^1.9.0",
|
||||
"discourse-i18n": "workspace:1.0.0",
|
||||
"ember-auto-import": "^2.13.1",
|
||||
"markdown-it": "^14.1.1",
|
||||
"pretty-text": "workspace:1.0.0",
|
||||
"xss": "^1.0.15"
|
||||
"xss": "^1.0.15",
|
||||
"discourse": "workspace:0.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"discourse-i18n": "workspace:1.0.0",
|
||||
"pretty-text": "workspace:1.0.0",
|
||||
"discourse": "workspace:0.0.0",
|
||||
"xss": "*"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
const rawModules = import.meta.glob("./**/*.{gjs,js}", { eager: true });
|
||||
|
||||
const adminCompatModules = {};
|
||||
for (let [key, mod] of Object.entries(rawModules)) {
|
||||
key = key.replace(/\.(gjs|js)$/, "");
|
||||
adminCompatModules[key] = mod;
|
||||
}
|
||||
|
||||
export default adminCompatModules;
|
||||
@@ -1,5 +1,4 @@
|
||||
import EmberObject from "@ember/object";
|
||||
import MessageBus from "message-bus-client";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
|
||||
export default class Backup extends EmberObject {
|
||||
@@ -15,7 +14,7 @@ export default class Backup extends EmberObject {
|
||||
type: "POST",
|
||||
data: {
|
||||
with_uploads: withUploads,
|
||||
client_id: MessageBus.clientId,
|
||||
client_id: window.MessageBus.clientId,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -39,7 +38,7 @@ export default class Backup extends EmberObject {
|
||||
restore() {
|
||||
return ajax("/admin/backups/" + this.filename + "/restore", {
|
||||
type: "POST",
|
||||
data: { client_id: MessageBus.clientId },
|
||||
data: { client_id: window.MessageBus.clientId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
import "./global-compat";
|
||||
import "./setup-deprecation-workflow";
|
||||
import "./array-shim";
|
||||
import "decorator-transforms/globals";
|
||||
import "./loader-shims";
|
||||
import "./ui-kit-shims";
|
||||
import "./module-shims";
|
||||
import "./discourse-common-loader-shims";
|
||||
import "./global-compat";
|
||||
import dialogHolderCompatModules from "discourse/dialog-holder/dialog-holder-compat-modules";
|
||||
import floatKitCompatModules from "discourse/float-kit/float-kit-compat-modules";
|
||||
import selectKitCompatModules from "discourse/select-kit/select-kit-compat-modules";
|
||||
import truthHelperCompatModules from "discourse/truth-helpers/truth-helpers-compat-modules";
|
||||
defineModules("select-kit", selectKitCompatModules);
|
||||
defineModules("float-kit", floatKitCompatModules);
|
||||
defineModules("truth-helpers", truthHelperCompatModules);
|
||||
defineModules("dialog-holder", dialogHolderCompatModules);
|
||||
|
||||
import embroiderCompatModules from "@embroider/virtual/compat-modules";
|
||||
import { registerDiscourseImplicitInjections } from "discourse/lib/implicit-injections";
|
||||
import { defineModules } from "./lib/loader-shim";
|
||||
|
||||
// Register Discourse's standard implicit injections on common framework classes.
|
||||
registerDiscourseImplicitInjections();
|
||||
@@ -22,13 +16,34 @@ registerDiscourseImplicitInjections();
|
||||
import { DEBUG } from "@glimmer/env";
|
||||
import Application from "@ember/application";
|
||||
import { VERSION } from "@ember/version";
|
||||
import setupInspector from "@embroider/legacy-inspector-support/ember-source-4.12";
|
||||
import { importSync } from "@embroider/macros";
|
||||
import require from "require";
|
||||
import { normalizeEmberEventHandling } from "discourse/lib/ember-events";
|
||||
import { isRailsTesting, isTesting } from "discourse/lib/environment";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import { populatePreloadStore } from "discourse/lib/preload-store";
|
||||
import { buildResolver } from "discourse/resolver";
|
||||
|
||||
populatePreloadStore();
|
||||
|
||||
defineModules(null, embroiderCompatModules);
|
||||
|
||||
import dialogHolderCompatModules from "discourse/dialog-holder/compat-modules";
|
||||
|
||||
defineModules("discourse/dialog-holder", dialogHolderCompatModules);
|
||||
|
||||
import floatKitCompatModules from "discourse/float-kit/compat-modules";
|
||||
|
||||
defineModules("discourse/float-kit", floatKitCompatModules);
|
||||
|
||||
import selectKitCompatModules from "discourse/select-kit/compat-modules";
|
||||
|
||||
defineModules("discourse/select-kit", selectKitCompatModules);
|
||||
|
||||
import truthHelpersCompatModules from "discourse/truth-helpers/compat-modules";
|
||||
|
||||
defineModules("discourse/truth-helpers", truthHelpersCompatModules);
|
||||
|
||||
const _pluginCallbacks = [];
|
||||
let _unhandledThemeErrors = [];
|
||||
|
||||
@@ -41,8 +56,7 @@ window.moduleBroker = {
|
||||
async function loadThemeFromModulePreload(link) {
|
||||
const themeId = link.dataset.themeId;
|
||||
try {
|
||||
const compatModules = (await import(/* webpackIgnore: true */ link.href))
|
||||
.default;
|
||||
const compatModules = (await import(/* @vite-ignore */ link.href)).default;
|
||||
for (const [key, mod] of Object.entries(compatModules)) {
|
||||
define(`discourse/theme-${themeId}/${key}`, () => mod);
|
||||
}
|
||||
@@ -50,7 +64,7 @@ async function loadThemeFromModulePreload(link) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Failed to load theme ${link.dataset.themeId} from ${link.href}`,
|
||||
String(error)
|
||||
window.Testem ? String(error) : error
|
||||
);
|
||||
|
||||
if (DEBUG && (isRailsTesting() || isTesting())) {
|
||||
@@ -64,8 +78,7 @@ async function loadThemeFromModulePreload(link) {
|
||||
async function loadPluginFromModulePreload(link) {
|
||||
const pluginName = link.dataset.pluginName;
|
||||
try {
|
||||
const compatModules = (await import(/* webpackIgnore: true */ link.href))
|
||||
.default;
|
||||
const compatModules = (await import(/* @vite-ignore */ link.href)).default;
|
||||
for (const [key, mod] of Object.entries(compatModules)) {
|
||||
define(`discourse/plugins/${pluginName}/${key}`, () => mod);
|
||||
}
|
||||
@@ -73,7 +86,7 @@ async function loadPluginFromModulePreload(link) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`Failed to load plugin ${link.dataset.pluginName} from ${link.href}`,
|
||||
error
|
||||
String(error)
|
||||
);
|
||||
|
||||
if (DEBUG) {
|
||||
@@ -100,20 +113,10 @@ export async function loadThemesAndPlugins() {
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
function defineModules(name, compatModules) {
|
||||
for (const [key, mod] of Object.entries(compatModules)) {
|
||||
define(`discourse/${name}/${key.slice(2)}`, () => mod);
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadAdmin() {
|
||||
defineModules(
|
||||
"admin",
|
||||
(
|
||||
await import(
|
||||
/* webpackChunkName: "admin" */ "discourse/admin/admin-compat-modules"
|
||||
)
|
||||
).default
|
||||
"discourse/admin",
|
||||
(await import("discourse/admin/compat-modules")).default
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,6 +124,8 @@ class Discourse extends Application {
|
||||
modulePrefix = "discourse";
|
||||
rootElement = "#main";
|
||||
|
||||
inspector = setupInspector(this);
|
||||
|
||||
customEvents = {
|
||||
paste: "paste",
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import loadConfigFromMeta from "@embroider/config-meta-loader";
|
||||
import { isTesting } from "@embroider/macros";
|
||||
|
||||
let output;
|
||||
|
||||
if (isTesting()) {
|
||||
output = {
|
||||
modulePrefix: "discourse",
|
||||
rootURL: "/",
|
||||
locationType: "none",
|
||||
APP: {
|
||||
autoboot: false,
|
||||
rootElement: "#ember-testing",
|
||||
},
|
||||
EmberENV: {
|
||||
_DEFAULT_ASYNC_OBSERVERS: true,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
output = loadConfigFromMeta("discourse");
|
||||
}
|
||||
|
||||
export default output;
|
||||
@@ -1,24 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!--
|
||||
👋 Greetings Discourse Developer. This html file is used by ember-cli/embroider to define our JS entrypoints,
|
||||
but it is never actually used in development or production. Instead, we generate an `assets.json` file which
|
||||
is ingested by Rails and used to generate the correct <script> tags.
|
||||
|
||||
When ember-cli is used as a proxy, we use the rails-generated HTML and replace urls in script/link tags
|
||||
with the local ember-cli versions.
|
||||
-->
|
||||
<meta charset="utf-8">
|
||||
<title>Discourse - Ember CLI</title>
|
||||
|
||||
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css" />
|
||||
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/discourse.css" />
|
||||
|
||||
<script defer src="{{rootURL}}assets/vendor.js"></script>
|
||||
|
||||
<script defer src="{{rootURL}}assets/discourse.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,8 +1,10 @@
|
||||
import config from "discourse/config/environment";
|
||||
|
||||
export default {
|
||||
initialize() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.get("safe_mode")?.split(",").includes("deprecation_errors")) {
|
||||
window.EmberENV.RAISE_ON_DEPRECATION = true;
|
||||
config.RAISE_ON_DEPRECATION = true;
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ import * as environment from "discourse/lib/environment";
|
||||
import { setDefaultOwner } from "discourse/lib/get-owner";
|
||||
import { setupS3CDN, setupURL } from "discourse/lib/get-url";
|
||||
import { setIconList } from "discourse/lib/icon-library";
|
||||
import PreloadStore from "discourse/lib/preload-store";
|
||||
import { setURLContainer } from "discourse/lib/url";
|
||||
import Session from "discourse/models/session";
|
||||
import I18n from "discourse-i18n";
|
||||
@@ -32,26 +31,6 @@ export default {
|
||||
setupData = setupDataElement.dataset;
|
||||
}
|
||||
|
||||
let preloaded;
|
||||
const preloadedDataElement = document.getElementById("data-preloaded");
|
||||
if (preloadedDataElement) {
|
||||
preloaded = JSON.parse(preloadedDataElement.dataset.preloaded);
|
||||
}
|
||||
|
||||
const keys = Object.keys(preloaded);
|
||||
if (keys.length === 0) {
|
||||
throw "No preload data found in #data-preloaded. Unable to boot Discourse.";
|
||||
}
|
||||
|
||||
keys.forEach(function (key) {
|
||||
PreloadStore.store(key, JSON.parse(preloaded[key]));
|
||||
|
||||
if (setupData.debugPreloadedAppData === "true") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(key, PreloadStore.get(key));
|
||||
}
|
||||
});
|
||||
|
||||
setupURL(setupData.cdn, setupData.baseUrl, setupData.baseUri);
|
||||
|
||||
// the `if DEBUG` forces the code inside the conditional block to be tree-shaken in
|
||||
|
||||
@@ -30,12 +30,15 @@ export default function deprecated(msg, options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
let config;
|
||||
if (require.has("discourse/config/environment")) {
|
||||
config = require("discourse/config/environment").default;
|
||||
}
|
||||
|
||||
const raiseError =
|
||||
options.raiseError ||
|
||||
DeprecationWorkflow.shouldThrow(
|
||||
id,
|
||||
globalThis.EmberENV?.RAISE_ON_DEPRECATION
|
||||
);
|
||||
(config &&
|
||||
DeprecationWorkflow.shouldThrow(id, config.RAISE_ON_DEPRECATION));
|
||||
|
||||
const formattedMessage = buildDeprecationMessage(msg, options, raiseError);
|
||||
const resolvedConsolePrefix = getConsolePrefix(source);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { helper } from "@ember/component/helper";
|
||||
import { deprecate } from "@ember/debug";
|
||||
|
||||
/**
|
||||
* Calls @ember/debug `deprecate` for each provided set of `deprecate` params.
|
||||
*/
|
||||
const deprecationsHelper = helper(([deprecationsJson]) => {
|
||||
for (const deprecation of JSON.parse(deprecationsJson)) {
|
||||
deprecate(...deprecation);
|
||||
}
|
||||
});
|
||||
|
||||
export default deprecationsHelper;
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isTesting as embroiderIsTesting } from "@embroider/macros";
|
||||
import deprecated from "discourse/lib/deprecated";
|
||||
|
||||
export const INPUT_DELAY = 250;
|
||||
@@ -14,7 +15,7 @@ export function setEnvironment(e) {
|
||||
* Returns true if running in the qunit test harness
|
||||
*/
|
||||
export function isTesting() {
|
||||
return environment === "qunit-testing";
|
||||
return embroiderIsTesting();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -106,7 +106,7 @@ async function loadLanguageInitializer(langFile) {
|
||||
// Load site-specific language bundle generated by Rails HighlightJsController
|
||||
// URL constructor used to add hostname to relative URLs
|
||||
const url = new URL(getURLWithCDN(langFile), window.location);
|
||||
const module = await import(/* webpackIgnore: true */ url);
|
||||
const module = await import(/* @vite-ignore */ url);
|
||||
return module.default;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,5 +3,6 @@ import { waitForPromise } from "@ember/test-waiters";
|
||||
export default async function loadAce() {
|
||||
const promise = import("discourse/static/ace-editor-bundle");
|
||||
waitForPromise(promise);
|
||||
return await promise;
|
||||
|
||||
return (await promise).default;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "../loader";
|
||||
|
||||
// Webpack has bugs, using globalThis is the safest
|
||||
// https://github.com/embroider-build/embroider/issues/1545
|
||||
let { define: __define__, require: __require__ } = globalThis;
|
||||
@@ -56,3 +58,12 @@ export default function loaderShim(pkg, callback) {
|
||||
__define__(pkg, callback);
|
||||
}
|
||||
}
|
||||
|
||||
export function defineModules(name, compatModules) {
|
||||
for (let [key, mod] of Object.entries(compatModules)) {
|
||||
if (key.startsWith("./")) {
|
||||
key = key.slice(2);
|
||||
}
|
||||
define(`${name ? `${name}/` : ""}${key}`, () => mod);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// We can insert data into the PreloadStore when the document is loaded.
|
||||
// The data can be accessed once by a key, after which it is removed
|
||||
import { Promise } from "rsvp";
|
||||
import { isTesting } from "discourse/lib/environment";
|
||||
|
||||
export default {
|
||||
const PreloadStore = {
|
||||
data: new Map(),
|
||||
|
||||
store(key, value) {
|
||||
@@ -56,3 +57,33 @@ export default {
|
||||
this.data = new Map();
|
||||
},
|
||||
};
|
||||
|
||||
export function populatePreloadStore() {
|
||||
let setupData;
|
||||
const setupDataElement = document.getElementById("data-discourse-setup");
|
||||
if (setupDataElement) {
|
||||
setupData = setupDataElement.dataset;
|
||||
}
|
||||
|
||||
let preloaded;
|
||||
const preloadedDataElement = document.getElementById("data-preloaded");
|
||||
if (preloadedDataElement) {
|
||||
preloaded = JSON.parse(preloadedDataElement.dataset.preloaded);
|
||||
}
|
||||
|
||||
const keys = preloaded ? Object.keys(preloaded) : [];
|
||||
if (keys.length === 0 && !isTesting()) {
|
||||
throw "No preload data found in #data-preloaded. Unable to boot Discourse.";
|
||||
}
|
||||
|
||||
keys.forEach(function (key) {
|
||||
PreloadStore.store(key, JSON.parse(preloaded[key]));
|
||||
|
||||
if (setupData.debugPreloadedAppData === "true") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(key, PreloadStore.get(key));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default PreloadStore;
|
||||
|
||||
@@ -18,7 +18,7 @@ export function loadSprites(spritePath, spriteName) {
|
||||
spriteContainer.appendChild(sprites);
|
||||
}
|
||||
|
||||
loadScript(spritePath).then(() => {
|
||||
return loadScript(spritePath).then(() => {
|
||||
sprites.innerHTML = window.__svg_sprite;
|
||||
// we got to clean up here... this is one giant string
|
||||
delete window.__svg_sprite;
|
||||
|
||||
@@ -51,6 +51,9 @@ loaderShim("@ember/render-modifiers/modifiers/did-insert", () =>
|
||||
loaderShim("@ember/render-modifiers/modifiers/did-update", () =>
|
||||
importSync("@ember/render-modifiers/modifiers/did-update")
|
||||
);
|
||||
loaderShim("@ember/render-modifiers/modifiers/will-destroy", () =>
|
||||
importSync("@ember/render-modifiers/modifiers/will-destroy")
|
||||
);
|
||||
loaderShim("@ember/routing", () => importSync("@ember/routing"));
|
||||
loaderShim("@ember/routing/route", () => importSync("@ember/routing/route"));
|
||||
loaderShim("@ember/runloop", () => importSync("@ember/runloop"));
|
||||
@@ -77,44 +80,96 @@ loaderShim("ember-route-template", () => importSync("ember-route-template"));
|
||||
loaderShim("ember", () => importSync("ember"));
|
||||
loaderShim("jquery", () => importSync("jquery"));
|
||||
loaderShim("js-yaml", () => importSync("js-yaml"));
|
||||
loaderShim("message-bus-client", () => importSync("message-bus-client"));
|
||||
loaderShim("moment", () => importSync("moment"));
|
||||
loaderShim("rsvp", () => importSync("rsvp"));
|
||||
loaderShim("truth-helpers", () => importSync("truth-helpers"));
|
||||
loaderShim("discourse/truth-helpers", () =>
|
||||
importSync("discourse/truth-helpers")
|
||||
);
|
||||
loaderShim("truth-helpers", () => importSync("discourse/truth-helpers"));
|
||||
loaderShim("truth-helpers/helpers/and", () =>
|
||||
importSync("truth-helpers/helpers/and")
|
||||
importSync("discourse/truth-helpers/helpers/and")
|
||||
);
|
||||
loaderShim("truth-helpers/helpers/eq", () =>
|
||||
importSync("truth-helpers/helpers/eq")
|
||||
importSync("discourse/truth-helpers/helpers/eq")
|
||||
);
|
||||
loaderShim("truth-helpers/helpers/gt", () =>
|
||||
importSync("truth-helpers/helpers/gt")
|
||||
importSync("discourse/truth-helpers/helpers/gt")
|
||||
);
|
||||
loaderShim("truth-helpers/helpers/gte", () =>
|
||||
importSync("truth-helpers/helpers/gte")
|
||||
importSync("discourse/truth-helpers/helpers/gte")
|
||||
);
|
||||
loaderShim("truth-helpers/helpers/includes", () =>
|
||||
importSync("truth-helpers/helpers/includes")
|
||||
importSync("discourse/truth-helpers/helpers/includes")
|
||||
);
|
||||
loaderShim("truth-helpers/helpers/lt", () =>
|
||||
importSync("truth-helpers/helpers/lt")
|
||||
importSync("discourse/truth-helpers/helpers/lt")
|
||||
);
|
||||
loaderShim("truth-helpers/helpers/lte", () =>
|
||||
importSync("truth-helpers/helpers/lte")
|
||||
importSync("discourse/truth-helpers/helpers/lte")
|
||||
);
|
||||
loaderShim("truth-helpers/helpers/not-eq", () =>
|
||||
importSync("truth-helpers/helpers/not-eq")
|
||||
importSync("discourse/truth-helpers/helpers/not-eq")
|
||||
);
|
||||
loaderShim("truth-helpers/helpers/not", () =>
|
||||
importSync("truth-helpers/helpers/not")
|
||||
importSync("discourse/truth-helpers/helpers/not")
|
||||
);
|
||||
loaderShim("truth-helpers/helpers/or", () =>
|
||||
importSync("truth-helpers/helpers/or")
|
||||
importSync("discourse/truth-helpers/helpers/or")
|
||||
);
|
||||
loaderShim("xss", () => importSync("xss"));
|
||||
loaderShim("ember-this-fallback/deprecations-helper", () =>
|
||||
importSync("./lib/ember-this-fallback-deprecation-helper")
|
||||
);
|
||||
loaderShim("pretty-text/allow-lister", () =>
|
||||
importSync("pretty-text/allow-lister")
|
||||
);
|
||||
loaderShim("pretty-text/censored-words", () =>
|
||||
importSync("pretty-text/censored-words")
|
||||
);
|
||||
loaderShim("pretty-text/emoji", () => importSync("pretty-text/emoji"));
|
||||
loaderShim("pretty-text/emoji/data", () =>
|
||||
importSync("pretty-text/emoji/data")
|
||||
);
|
||||
loaderShim("pretty-text/emoji/version", () =>
|
||||
importSync("pretty-text/emoji/version")
|
||||
);
|
||||
loaderShim("pretty-text/guid", () => importSync("pretty-text/guid"));
|
||||
loaderShim("pretty-text/inline-oneboxer", () =>
|
||||
importSync("pretty-text/inline-oneboxer")
|
||||
);
|
||||
loaderShim("pretty-text/mentions", () => importSync("pretty-text/mentions"));
|
||||
loaderShim("pretty-text/oneboxer", () => importSync("pretty-text/oneboxer"));
|
||||
loaderShim("pretty-text/oneboxer-cache", () =>
|
||||
importSync("pretty-text/oneboxer-cache")
|
||||
);
|
||||
loaderShim("pretty-text/pretty-text", () =>
|
||||
importSync("pretty-text/pretty-text")
|
||||
);
|
||||
loaderShim("pretty-text/sanitizer", () => importSync("pretty-text/sanitizer"));
|
||||
loaderShim("pretty-text/text-replace", () =>
|
||||
importSync("pretty-text/text-replace")
|
||||
);
|
||||
loaderShim("pretty-text/upload-short-url", () =>
|
||||
importSync("pretty-text/upload-short-url")
|
||||
);
|
||||
loaderShim("@ember-decorators/component", () =>
|
||||
importSync("@ember-decorators/component")
|
||||
);
|
||||
loaderShim("@ember-decorators/object", () =>
|
||||
importSync("@ember-decorators/object")
|
||||
);
|
||||
loaderShim("discourse/lib/transformer/registry", () =>
|
||||
importSync("discourse/lib/registry/transformers")
|
||||
);
|
||||
loaderShim("discourse/modifiers/did-insert", () =>
|
||||
importSync("@ember/render-modifiers/modifiers/did-insert")
|
||||
);
|
||||
loaderShim("discourse/modifiers/did-update", () =>
|
||||
importSync("@ember/render-modifiers/modifiers/did-update")
|
||||
);
|
||||
loaderShim("discourse/modifiers/will-destroy", () =>
|
||||
importSync("@ember/render-modifiers/modifiers/will-destroy")
|
||||
);
|
||||
loaderShim("ember-this-fallback/deprecations-helper", () =>
|
||||
importSync("./lib/ember-this-fallback/deprecations-helper")
|
||||
);
|
||||
@@ -127,3 +182,12 @@ loaderShim("ember-this-fallback/this-fallback-helper", () =>
|
||||
loaderShim("ember-this-fallback/try-lookup-helper", () =>
|
||||
importSync("./lib/ember-this-fallback/try-lookup-helper")
|
||||
);
|
||||
loaderShim("ember-buffered-proxy/helpers", () =>
|
||||
importSync("ember-buffered-proxy/helpers")
|
||||
);
|
||||
loaderShim("ember-buffered-proxy/mixin", () =>
|
||||
importSync("ember-buffered-proxy/mixin")
|
||||
);
|
||||
loaderShim("ember-buffered-proxy/proxy", () =>
|
||||
importSync("ember-buffered-proxy/proxy")
|
||||
);
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
/* eslint-disable */
|
||||
|
||||
function dict() {
|
||||
let obj = Object.create(null);
|
||||
obj["__"] = undefined;
|
||||
delete obj["__"];
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Save off the original values of these globals, so we can restore them if someone asks us to
|
||||
// var oldGlobals = {
|
||||
// loader: loader,
|
||||
// define: define,
|
||||
// requireModule: requireModule,
|
||||
// require: require,
|
||||
// requirejs: requirejs,
|
||||
// };
|
||||
|
||||
window.requirejs =
|
||||
window.require =
|
||||
window.requireModule =
|
||||
function (id) {
|
||||
let pending = [];
|
||||
let mod = findModule(id, "(require)", pending);
|
||||
|
||||
for (let i = pending.length - 1; i >= 0; i--) {
|
||||
pending[i].exports();
|
||||
}
|
||||
|
||||
return mod.module.exports;
|
||||
};
|
||||
|
||||
window.loader = {
|
||||
noConflict: function (aliases) {
|
||||
// var oldName, newName;
|
||||
// for (oldName in aliases) {
|
||||
// if (aliases.hasOwnProperty(oldName)) {
|
||||
// if (oldGlobals.hasOwnProperty(oldName)) {
|
||||
// newName = aliases[oldName];
|
||||
// global[newName] = global[oldName];
|
||||
// global[oldName] = oldGlobals[oldName];
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
},
|
||||
// Option to enable or disable the generation of default exports
|
||||
makeDefaultExport: true,
|
||||
};
|
||||
|
||||
let registry = dict();
|
||||
let seen = dict();
|
||||
|
||||
let uuid = 0;
|
||||
|
||||
function unsupportedModule(length) {
|
||||
throw new Error(
|
||||
"an unsupported module was defined, expected `define(id, deps, module)` instead got: `" +
|
||||
length +
|
||||
"` arguments to define`"
|
||||
);
|
||||
}
|
||||
|
||||
let defaultDeps = ["require", "exports", "module"];
|
||||
|
||||
function Module(id, deps, callback, alias) {
|
||||
this.uuid = uuid++;
|
||||
this.id = id;
|
||||
this.deps = !deps.length && callback.length ? defaultDeps : deps;
|
||||
this.module = { exports: {} };
|
||||
this.callback = callback;
|
||||
this.hasExportsAsDep = false;
|
||||
this.isAlias = alias;
|
||||
this.reified = new Array(deps.length);
|
||||
|
||||
/*
|
||||
Each module normally passes through these states, in order:
|
||||
new : initial state
|
||||
pending : this module is scheduled to be executed
|
||||
reifying : this module's dependencies are being executed
|
||||
reified : this module's dependencies finished executing successfully
|
||||
errored : this module's dependencies failed to execute
|
||||
finalized : this module executed successfully
|
||||
*/
|
||||
this.state = "new";
|
||||
}
|
||||
|
||||
Module.prototype.makeDefaultExport = function () {
|
||||
let exports = this.module.exports;
|
||||
if (
|
||||
exports !== null &&
|
||||
(typeof exports === "object" || typeof exports === "function") &&
|
||||
exports["default"] === undefined &&
|
||||
Object.isExtensible(exports)
|
||||
) {
|
||||
exports["default"] = exports;
|
||||
}
|
||||
};
|
||||
|
||||
Module.prototype.exports = function () {
|
||||
// if finalized, there is no work to do. If reifying, there is a
|
||||
// circular dependency so we must return our (partial) exports.
|
||||
if (this.state === "finalized" || this.state === "reifying") {
|
||||
return this.module.exports;
|
||||
}
|
||||
|
||||
if (window.loader.wrapModules) {
|
||||
this.callback = window.loader.wrapModules(this.id, this.callback);
|
||||
}
|
||||
|
||||
this.reify();
|
||||
|
||||
let result = this.callback.apply(this, this.reified);
|
||||
this.reified.length = 0;
|
||||
this.state = "finalized";
|
||||
|
||||
if (!(this.hasExportsAsDep && result === undefined)) {
|
||||
this.module.exports = result;
|
||||
}
|
||||
if (window.loader.makeDefaultExport) {
|
||||
this.makeDefaultExport();
|
||||
}
|
||||
return this.module.exports;
|
||||
};
|
||||
|
||||
Module.prototype.unsee = function () {
|
||||
this.state = "new";
|
||||
this.module = { exports: {} };
|
||||
};
|
||||
|
||||
Module.prototype.reify = function () {
|
||||
if (this.state === "reified") {
|
||||
return;
|
||||
}
|
||||
this.state = "reifying";
|
||||
try {
|
||||
this.reified = this._reify();
|
||||
this.state = "reified";
|
||||
} finally {
|
||||
if (this.state === "reifying") {
|
||||
this.state = "errored";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Module.prototype._reify = function () {
|
||||
let reified = this.reified.slice();
|
||||
for (let i = 0; i < reified.length; i++) {
|
||||
let mod = reified[i];
|
||||
reified[i] = mod.exports ? mod.exports : mod.module.exports();
|
||||
}
|
||||
return reified;
|
||||
};
|
||||
|
||||
Module.prototype.findDeps = function (pending) {
|
||||
if (this.state !== "new") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state = "pending";
|
||||
|
||||
let deps = this.deps;
|
||||
|
||||
for (let i = 0; i < deps.length; i++) {
|
||||
let dep = deps[i];
|
||||
let entry = (this.reified[i] = { exports: undefined, module: undefined });
|
||||
if (dep === "exports") {
|
||||
this.hasExportsAsDep = true;
|
||||
entry.exports = this.module.exports;
|
||||
} else if (dep === "require") {
|
||||
entry.exports = this.makeRequire();
|
||||
} else if (dep === "module") {
|
||||
entry.exports = this.module;
|
||||
} else {
|
||||
entry.module = findModule(resolve(dep, this.id), this.id, pending);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Module.prototype.makeRequire = function () {
|
||||
let id = this.id;
|
||||
let r = function (dep) {
|
||||
return window.require(resolve(dep, id));
|
||||
};
|
||||
r["default"] = r;
|
||||
r.moduleId = id;
|
||||
r.has = function (dep) {
|
||||
return has(resolve(dep, id));
|
||||
};
|
||||
return r;
|
||||
};
|
||||
|
||||
window.define = function (id, deps, callback) {
|
||||
let module = registry[id];
|
||||
|
||||
// If a module for this id has already been defined and is in any state
|
||||
// other than `new` (meaning it has been or is currently being required),
|
||||
// then we return early to avoid redefinition.
|
||||
if (module && module.state !== "new") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (arguments.length < 2) {
|
||||
unsupportedModule(arguments.length);
|
||||
}
|
||||
|
||||
if (!Array.isArray(deps)) {
|
||||
callback = deps;
|
||||
deps = [];
|
||||
}
|
||||
|
||||
if (callback instanceof Alias) {
|
||||
registry[id] = new Module(callback.id, deps, callback, true);
|
||||
} else {
|
||||
registry[id] = new Module(id, deps, callback, false);
|
||||
}
|
||||
};
|
||||
|
||||
window.define.exports = function (name, defaultExport) {
|
||||
let module = registry[name];
|
||||
|
||||
// If a module for this name has already been defined and is in any state
|
||||
// other than `new` (meaning it has been or is currently being required),
|
||||
// then we return early to avoid redefinition.
|
||||
if (module && module.state !== "new") {
|
||||
return;
|
||||
}
|
||||
|
||||
module = new Module(name, [], noop, null);
|
||||
module.module.exports = defaultExport;
|
||||
module.state = "finalized";
|
||||
registry[name] = module;
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
function noop() {}
|
||||
// we don't support all of AMD
|
||||
// define.amd = {};
|
||||
|
||||
function Alias(id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
window.define.alias = function (id, target) {
|
||||
if (arguments.length === 2) {
|
||||
return window.define(target, new Alias(id));
|
||||
}
|
||||
|
||||
return new Alias(id);
|
||||
};
|
||||
|
||||
function missingModule(id, referrer) {
|
||||
throw new Error(
|
||||
"Could not find module `" + id + "` imported from `" + referrer + "`"
|
||||
);
|
||||
}
|
||||
|
||||
function findModule(id, referrer, pending) {
|
||||
let mod = registry[id] || registry[id + "/index"];
|
||||
|
||||
while (mod && mod.isAlias) {
|
||||
mod = registry[mod.id] || registry[mod.id + "/index"];
|
||||
}
|
||||
|
||||
if (!mod) {
|
||||
missingModule(id, referrer);
|
||||
}
|
||||
|
||||
if (pending && mod.state !== "pending" && mod.state !== "finalized") {
|
||||
mod.findDeps(pending);
|
||||
pending.push(mod);
|
||||
}
|
||||
return mod;
|
||||
}
|
||||
|
||||
function resolve(child, id) {
|
||||
if (child.charAt(0) !== ".") {
|
||||
return child;
|
||||
}
|
||||
|
||||
let parts = child.split("/");
|
||||
let nameParts = id.split("/");
|
||||
let parentBase = nameParts.slice(0, -1);
|
||||
|
||||
for (let i = 0, l = parts.length; i < l; i++) {
|
||||
let part = parts[i];
|
||||
|
||||
if (part === "..") {
|
||||
if (parentBase.length === 0) {
|
||||
throw new Error("Cannot access parent module of root");
|
||||
}
|
||||
parentBase.pop();
|
||||
} else if (part === ".") {
|
||||
continue;
|
||||
} else {
|
||||
parentBase.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
return parentBase.join("/");
|
||||
}
|
||||
|
||||
function has(id) {
|
||||
return !!(registry[id] || registry[id + "/index"]);
|
||||
}
|
||||
|
||||
window.requirejs.entries = window.requirejs._eak_seen = registry;
|
||||
window.requirejs.has = has;
|
||||
window.requirejs.unsee = function (id) {
|
||||
findModule(id, "(unsee)", false).unsee();
|
||||
};
|
||||
|
||||
window.requirejs.clear = function () {
|
||||
window.requirejs.entries = window.requirejs._eak_seen = registry = dict();
|
||||
seen = dict();
|
||||
};
|
||||
|
||||
// This code primes the JS engine for good performance by warming the
|
||||
// JIT compiler for these functions.
|
||||
// define('foo', function () {});
|
||||
// define('foo/bar', [], function () {});
|
||||
// define('foo/asdf', ['module', 'exports', 'require'], function (
|
||||
// module,
|
||||
// exports,
|
||||
// require
|
||||
// ) {
|
||||
// if (require.has('foo/bar')) {
|
||||
// require('foo/bar');
|
||||
// }
|
||||
// });
|
||||
// define('foo/baz', [], define.alias('foo'));
|
||||
// define('foo/quz', define.alias('foo'));
|
||||
// define.alias('foo', 'foo/qux');
|
||||
// define('foo/bar', [
|
||||
// 'foo',
|
||||
// './quz',
|
||||
// './baz',
|
||||
// './asdf',
|
||||
// './bar',
|
||||
// '../foo',
|
||||
// ], function () {});
|
||||
// define('foo/main', ['foo/bar'], function () {});
|
||||
// define.exports('foo/exports', {});
|
||||
|
||||
// require('foo/exports');
|
||||
// require('foo/main');
|
||||
// require.unsee('foo/bar');
|
||||
|
||||
requirejs.clear();
|
||||
@@ -1,3 +1,4 @@
|
||||
import { dasherize } from "@ember/string";
|
||||
import EmbroiderRouter from "@embroider/router";
|
||||
import { isTesting } from "discourse/lib/environment";
|
||||
import getURL from "discourse/lib/get-url";
|
||||
@@ -7,6 +8,11 @@ import applyRouterHomepageOverrides from "./lib/homepage-router-overrides";
|
||||
class BareRouter extends EmbroiderRouter {
|
||||
location = isTesting() ? "none" : "history";
|
||||
|
||||
lazyRoute(routeName) {
|
||||
routeName = dasherize(routeName);
|
||||
return super.lazyRoute(routeName);
|
||||
}
|
||||
|
||||
setupRouter() {
|
||||
const didSetup = super.setupRouter(...arguments);
|
||||
if (didSetup) {
|
||||
|
||||
@@ -367,6 +367,12 @@ export function buildResolver(baseName) {
|
||||
}
|
||||
}
|
||||
|
||||
addModules(modules) {
|
||||
for (const [key, value] of Object.entries(modules)) {
|
||||
define(key, () => value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a list of template path candidates based on various naming conventions.
|
||||
* Supports legacy naming patterns (underscored, dasherized, etc.) for backwards compatibility.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MessageBus from "message-bus-client";
|
||||
import "message-bus-client";
|
||||
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||
|
||||
@disableImplicitInjections
|
||||
@@ -6,6 +6,6 @@ export default class MessageBusService {
|
||||
static isServiceFactory = true;
|
||||
|
||||
static create() {
|
||||
return MessageBus;
|
||||
return window.MessageBus;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from "ace-builds/src-noconflict/ace";
|
||||
export { default } from "ace-builds/src-noconflict/ace";
|
||||
|
||||
import "ace-builds/src-noconflict/mode-scss";
|
||||
import "ace-builds/src-noconflict/mode-html";
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
const { buildMacros } = require("@embroider/macros/babel");
|
||||
|
||||
const macros = buildMacros({
|
||||
configure(macrosConfig) {
|
||||
macrosConfig.setGlobalConfig(__filename, "@embroider/core", {
|
||||
active: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
[
|
||||
"babel-plugin-ember-template-compilation",
|
||||
{
|
||||
compilerPath: "ember-source/ember-template-compiler/index.js",
|
||||
enableLegacyModules: [
|
||||
"ember-cli-htmlbars",
|
||||
"ember-cli-htmlbars-inline-precompile",
|
||||
"htmlbars-inline-precompile",
|
||||
],
|
||||
transforms: [...macros.templateMacros],
|
||||
},
|
||||
],
|
||||
[
|
||||
"module:decorator-transforms",
|
||||
{
|
||||
runtime: {
|
||||
import: require.resolve("decorator-transforms/runtime-esm"),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"@babel/plugin-transform-runtime",
|
||||
{
|
||||
absoluteRuntime: __dirname,
|
||||
useESModules: true,
|
||||
regenerator: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
require.resolve("babel-plugin-debug-macros"),
|
||||
{
|
||||
flags: [
|
||||
{
|
||||
source: "@glimmer/env",
|
||||
flags: {
|
||||
DEBUG: process.env.EMBER_ENV !== "production",
|
||||
CI: !!process.env.CI,
|
||||
},
|
||||
},
|
||||
],
|
||||
debugTools: {
|
||||
isDebug: process.env.EMBER_ENV !== "production",
|
||||
source: "@ember/debug",
|
||||
assertPredicateIndex: 1,
|
||||
},
|
||||
externalizeHelpers: {
|
||||
module: "@ember/debug",
|
||||
},
|
||||
},
|
||||
"@ember/debug stripping",
|
||||
],
|
||||
...macros.babelMacros,
|
||||
],
|
||||
|
||||
generatorOpts: {
|
||||
compact: false,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
const rawModules = import.meta.glob("./**/*.{gjs,js}", { eager: true });
|
||||
|
||||
const compatModules = {};
|
||||
for (let [key, mod] of Object.entries(rawModules)) {
|
||||
key = key.replace(/\.(gjs|js)$/, "");
|
||||
compatModules[key] = mod;
|
||||
}
|
||||
|
||||
export default compatModules;
|
||||
@@ -0,0 +1,25 @@
|
||||
import App, { loadAdmin, loadThemesAndPlugins } from "discourse/app";
|
||||
|
||||
(async function () {
|
||||
if (window.unsupportedBrowser) {
|
||||
throw "Unsupported browser detected";
|
||||
}
|
||||
|
||||
let element = document.querySelector(
|
||||
`meta[name="discourse/config/environment"]`
|
||||
);
|
||||
const config = JSON.parse(
|
||||
decodeURIComponent(element.getAttribute("content"))
|
||||
);
|
||||
|
||||
performance.mark("discourse-init");
|
||||
|
||||
if (document.querySelector('#data-discourse-setup[data-is-staff="true"]')) {
|
||||
await loadAdmin();
|
||||
}
|
||||
|
||||
await loadThemesAndPlugins();
|
||||
|
||||
const app = App.create(config.detail);
|
||||
app.start();
|
||||
})();
|
||||
@@ -1,287 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const EmberApp = require("ember-cli/lib/broccoli/ember-app");
|
||||
const path = require("path");
|
||||
const mergeTrees = require("broccoli-merge-trees");
|
||||
const generateScriptsTree = require("./lib/scripts");
|
||||
const funnel = require("broccoli-funnel");
|
||||
const DeprecationSilencer = require("deprecation-silencer");
|
||||
const { compatBuild } = require("@embroider/compat");
|
||||
const { Webpack } = require("@embroider/webpack");
|
||||
const { StatsWriterPlugin } = require("webpack-stats-plugin");
|
||||
const { RetryChunkLoadPlugin } = require("webpack-retry-chunk-load-plugin");
|
||||
const withSideWatch = require("./lib/with-side-watch");
|
||||
const crypto = require("crypto");
|
||||
const commonBabelConfig = require("./lib/common-babel-config");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const {
|
||||
CustomizeChunkUrlPlugin,
|
||||
} = require("./lib/webpack-customize-chunk-url-plugin");
|
||||
const { BroccoliMergeFiles } = require("broccoli-merge-files");
|
||||
const { mkdirSync } = require("fs");
|
||||
|
||||
process.env.BROCCOLI_ENABLED_MEMOIZE = true;
|
||||
|
||||
function compatModulesFor(name) {
|
||||
return funnel(
|
||||
new BroccoliMergeFiles([name], {
|
||||
outputFileName: `${name}-compat-modules.js`,
|
||||
async merge(files) {
|
||||
const lines = [`const compatModules = {};`];
|
||||
|
||||
let i = 1;
|
||||
for (const [filename] of files) {
|
||||
const withoutExtension = filename.replace(/\..*$/, "");
|
||||
lines.push(
|
||||
`import * as Module${i} from "./${withoutExtension}";`,
|
||||
`compatModules["./${withoutExtension}"] = Module${i};`
|
||||
);
|
||||
i++;
|
||||
}
|
||||
|
||||
lines.push("export default compatModules;");
|
||||
|
||||
return lines.join("\n");
|
||||
},
|
||||
}),
|
||||
{ destDir: name }
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = function (defaults) {
|
||||
const discourseRoot = path.resolve("../..");
|
||||
mkdirSync(path.join(discourseRoot, "app/assets/generated"), {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
// Silence deprecations which we are aware of - see `lib/deprecation-silencer.js`
|
||||
DeprecationSilencer.silence(console, "warn");
|
||||
DeprecationSilencer.silence(console, "log");
|
||||
DeprecationSilencer.silence(defaults.project.ui, "writeWarnLine");
|
||||
|
||||
const isProduction = EmberApp.env().includes("production");
|
||||
|
||||
const app = new EmberApp(defaults, {
|
||||
autoRun: false,
|
||||
"ember-qunit": {
|
||||
insertContentForTestBody: false,
|
||||
},
|
||||
"ember-template-imports": {
|
||||
inline_source_map: true,
|
||||
},
|
||||
sourcemaps: {
|
||||
// There seems to be a bug with broccoli-concat when sourcemaps are disabled
|
||||
// that causes the `app.import` statements below to fail in production mode.
|
||||
// This forces the use of `fast-sourcemap-concat` which works in production.
|
||||
enabled: true,
|
||||
},
|
||||
fingerprint: {
|
||||
// Handled by Rails asset pipeline
|
||||
enabled: false,
|
||||
},
|
||||
SRI: {
|
||||
// We don't use SRI in Rails. Disable here to match:
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
"ember-cli-deprecation-workflow": {
|
||||
enabled: true,
|
||||
},
|
||||
|
||||
"ember-cli-terser": {
|
||||
enabled: isProduction,
|
||||
exclude: ["**/highlightjs/*", "**/javascripts/*"],
|
||||
},
|
||||
|
||||
...commonBabelConfig(),
|
||||
|
||||
trees: {
|
||||
app: withSideWatch(
|
||||
mergeTrees([
|
||||
"app",
|
||||
funnel("admin", { destDir: "admin" }),
|
||||
compatModulesFor("admin"),
|
||||
funnel("select-kit", { destDir: "select-kit" }),
|
||||
compatModulesFor("select-kit"),
|
||||
funnel("float-kit", { destDir: "float-kit" }),
|
||||
compatModulesFor("float-kit"),
|
||||
funnel("truth-helpers", { destDir: "truth-helpers" }),
|
||||
compatModulesFor("truth-helpers"),
|
||||
funnel("dialog-holder", { destDir: "dialog-holder" }),
|
||||
compatModulesFor("dialog-holder"),
|
||||
]),
|
||||
{
|
||||
watching: ["../discourse-markdown-it", "../../app/assets/generated"],
|
||||
}
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
// WARNING: We should only import scripts here if they are not in NPM.
|
||||
app.import(discourseRoot + "/frontend/polyfills.js");
|
||||
|
||||
app.import(
|
||||
discourseRoot + "/frontend/discourse/public/assets/scripts/module-shims.js"
|
||||
);
|
||||
|
||||
app.project.liveReloadFilterPatterns = [/.*\.scss/];
|
||||
|
||||
const terserPlugin = app.project.findAddonByName("ember-cli-terser");
|
||||
const applyTerser = (tree) => terserPlugin.postprocessTree("all", tree);
|
||||
|
||||
let extraPublicTrees = [
|
||||
funnel(`${discourseRoot}/public/javascripts`, { destDir: "javascripts" }),
|
||||
applyTerser(generateScriptsTree(app)),
|
||||
];
|
||||
|
||||
const assetCachebuster = process.env["DISCOURSE_ASSET_URL_SALT"] || "";
|
||||
const cachebusterHash = crypto
|
||||
.createHash("md5")
|
||||
.update(assetCachebuster)
|
||||
.digest("hex")
|
||||
.slice(0, 8);
|
||||
|
||||
const appTree = compatBuild(app, Webpack, {
|
||||
staticEmberSource: true,
|
||||
splitAtRoutes: ["wizard"],
|
||||
staticAppPaths: [
|
||||
"static",
|
||||
"admin",
|
||||
"select-kit",
|
||||
"float-kit",
|
||||
"truth-helpers",
|
||||
"dialog-holder",
|
||||
],
|
||||
packagerOptions: {
|
||||
webpackConfig: {
|
||||
devtool:
|
||||
process.env.CHEAP_SOURCE_MAPS === "1"
|
||||
? "cheap-source-map"
|
||||
: "source-map",
|
||||
output: {
|
||||
publicPath: "auto",
|
||||
filename: `assets/chunk.[chunkhash].${cachebusterHash}.js`,
|
||||
chunkFilename: `assets/chunk.[chunkhash].${cachebusterHash}.js`,
|
||||
assetModuleFilename: `assets/chunk.[hash].${cachebusterHash}[ext][query]`,
|
||||
},
|
||||
optimization: {
|
||||
minimize: isProduction,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
minify: TerserPlugin.swcMinify,
|
||||
}),
|
||||
],
|
||||
},
|
||||
cache: isProduction
|
||||
? false
|
||||
: {
|
||||
type: "memory",
|
||||
maxGenerations: 1,
|
||||
},
|
||||
entry: {
|
||||
"assets/media-optimization-bundle.js": {
|
||||
import: "./static/media-optimization-bundle",
|
||||
chunkLoading: "import-scripts",
|
||||
runtime: false,
|
||||
},
|
||||
},
|
||||
externals: [
|
||||
function ({ context, request }, callback) {
|
||||
if (
|
||||
context.includes("discourse-markdown-it/src") &&
|
||||
request.startsWith("discourse/")
|
||||
) {
|
||||
// v1 ember apps can't be imported from addons. Workaround via commonjs.
|
||||
// Won't be necessary once we move to a v2 app.
|
||||
callback(null, request, "commonjs");
|
||||
} else if (
|
||||
!request.includes("-embroider-implicit") &&
|
||||
(request.startsWith("discourse/plugins/") ||
|
||||
request.startsWith("discourse/theme-"))
|
||||
) {
|
||||
callback(null, request, "commonjs");
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
],
|
||||
module: {
|
||||
parser: {
|
||||
javascript: {
|
||||
exportsPresence: "error",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
// The server use this output to map each asset to its chunks
|
||||
new StatsWriterPlugin({
|
||||
filename: "assets.json",
|
||||
stats: {
|
||||
all: false,
|
||||
entrypoints: true,
|
||||
chunks: true,
|
||||
},
|
||||
transform({ chunks, entrypoints }) {
|
||||
let names = Object.keys(entrypoints);
|
||||
let output = {};
|
||||
|
||||
for (let name of names.sort()) {
|
||||
let assets = entrypoints[name].assets.map(
|
||||
(asset) => asset.name
|
||||
);
|
||||
|
||||
let parent = names.find((parentName) =>
|
||||
name.startsWith(parentName + "/")
|
||||
);
|
||||
|
||||
if (parent) {
|
||||
name = name.slice(parent.length + 1);
|
||||
output[parent][name] = { assets };
|
||||
} else {
|
||||
output[name] = { assets };
|
||||
}
|
||||
}
|
||||
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.entry) {
|
||||
continue;
|
||||
}
|
||||
for (const name of chunk.names) {
|
||||
const outputName = `assets/chunk.${name}.js`;
|
||||
output[outputName] ??= { assets: [] };
|
||||
output[outputName].assets.push(...chunk.files);
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(output, null, 2);
|
||||
},
|
||||
}),
|
||||
new CustomizeChunkUrlPlugin(),
|
||||
new RetryChunkLoadPlugin({
|
||||
retryDelay: 200,
|
||||
maxRetries: 2,
|
||||
}),
|
||||
],
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
skipBabel: [
|
||||
{
|
||||
package: "qunit",
|
||||
},
|
||||
{
|
||||
package: "sinon",
|
||||
},
|
||||
{
|
||||
package: "@json-editor/json-editor",
|
||||
},
|
||||
{
|
||||
package: "ace-builds",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return mergeTrees([appTree, mergeTrees(extraPublicTrees)]);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
const rawModules = import.meta.glob("./**/*.{gjs,js}", { eager: true });
|
||||
|
||||
const compatModules = {};
|
||||
for (let [key, mod] of Object.entries(rawModules)) {
|
||||
key = key.replace(/\.(gjs|js)$/, "");
|
||||
compatModules[key] = mod;
|
||||
}
|
||||
|
||||
export default compatModules;
|
||||
@@ -1,22 +0,0 @@
|
||||
module.exports = function generateCommonBabelConfig() {
|
||||
return {
|
||||
"ember-cli-babel": {
|
||||
throwUnlessParallelizable: true,
|
||||
disableDecoratorTransforms: true,
|
||||
},
|
||||
|
||||
babel: {
|
||||
sourceMaps: false,
|
||||
plugins: [
|
||||
require.resolve("deprecation-silencer"),
|
||||
[
|
||||
require.resolve("decorator-transforms"),
|
||||
{
|
||||
runEarly: true,
|
||||
},
|
||||
],
|
||||
require.resolve("./babel-transform-module-renames"),
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { buildResolverOptions } from "@embroider/core/module-resolver-options";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export default function writeResolverConfig(config, extra) {
|
||||
const resolverPath = path.resolve("./node_modules/.embroider/resolver.json");
|
||||
const embroiderDir = path.resolve("./node_modules/.embroider");
|
||||
|
||||
if (fs.existsSync(embroiderDir)) {
|
||||
fs.rmSync(embroiderDir, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
const embroiderResolverOptions = {
|
||||
...buildResolverOptions(config),
|
||||
...extra,
|
||||
};
|
||||
|
||||
fs.mkdirSync(path.dirname(resolverPath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
resolverPath,
|
||||
JSON.stringify(embroiderResolverOptions, null, 2)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { and, code, id, include, not, or } from "@rolldown/pluginutils";
|
||||
import { babel } from "@rollup/plugin-babel";
|
||||
|
||||
const babelRequiredImports = [
|
||||
// Templates
|
||||
"@ember/template-compiler",
|
||||
"@ember/template-compilation",
|
||||
"ember-cli-htmlbars",
|
||||
"ember-cli-htmlbars-inline-precompile",
|
||||
"htmlbars-inline-precompile",
|
||||
|
||||
// Macros
|
||||
"@embroider/macros",
|
||||
"@glimmer/env",
|
||||
"@ember/debug",
|
||||
"@ember/application/deprecations",
|
||||
];
|
||||
|
||||
function escapeRegExp(s) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
const importsRegex = new RegExp(
|
||||
babelRequiredImports.map(escapeRegExp).join("|")
|
||||
);
|
||||
|
||||
const decoratorRegex = /(?<![\w'"`])(?<!\*\s)(?<!\/\/[^\n]*)@\w+/;
|
||||
// └────┬─────┘└───┬───┘└──────┬──────┘└┬─┘
|
||||
// │ │ │ │
|
||||
// │ │ │ └── the `@decorator`
|
||||
// │ │ └── not on a `//` line comment
|
||||
// │ └── not a JSDoc tag (`* @param`)
|
||||
// └── not mid-identifier or inside a string
|
||||
|
||||
const nodeModulesPattern = /\/node_modules\//;
|
||||
|
||||
export default function maybeBabel(config) {
|
||||
const plugin = babel(config);
|
||||
|
||||
// Extract existing regex filter from babel plugin
|
||||
const extensionRegex = plugin.transform.filter.id;
|
||||
|
||||
plugin.transform.filter = [
|
||||
include(
|
||||
and(
|
||||
id(extensionRegex), // Is one of the babel-supported extensions
|
||||
or(
|
||||
code(importsRegex), // Imports one of our listed modules
|
||||
and(not(id(nodeModulesPattern)), code(decoratorRegex)) // Is local app code which uses a decorator
|
||||
)
|
||||
)
|
||||
),
|
||||
];
|
||||
return plugin;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ResolverLoader } from "@embroider/core";
|
||||
import { resolver, templateTag } from "@embroider/vite";
|
||||
import { id, importerId, include, or } from "@rolldown/pluginutils";
|
||||
import { viteAliasPlugin } from "rolldown/experimental";
|
||||
|
||||
function toSpecifier(s) {
|
||||
return s.replace(/(?:\/index)?\.js$/, "");
|
||||
}
|
||||
|
||||
/*
|
||||
* Embroider's resolver handles a number of static module aliases. For example - the
|
||||
* fake packages like `@ember/component`. We can fetch that list from the resolver upfront
|
||||
* and pipe it into the highly-optimized viteAliasPlugin. This means that they will be
|
||||
* resolved entirely on the rust side - no round-trip to node.
|
||||
*/
|
||||
function staticEmbroiderAliases() {
|
||||
const { renameModules = {} } = new ResolverLoader(process.cwd()).resolver
|
||||
.options;
|
||||
const entries = Object.entries(renameModules).map(([find, replacement]) => ({
|
||||
find: toSpecifier(find),
|
||||
replacement: toSpecifier(replacement),
|
||||
}));
|
||||
return viteAliasPlugin({ entries });
|
||||
}
|
||||
|
||||
/*
|
||||
* By default, the embroider resolver runs for every resolveId call.
|
||||
* We have moved its static lookup rules into the highly-optimised viteAliasPlugin,
|
||||
* and we have no need for its hbs-related synthetic modules or app-tree merging.
|
||||
* Therefore we can filter the plugin so that it's only called for `@embroider/*`
|
||||
* virtual modules, and for any imports *from* a -embroider- module.
|
||||
*/
|
||||
function filteredEmberResolver() {
|
||||
const plugin = resolver();
|
||||
plugin.resolveId = {
|
||||
filter: [
|
||||
include(
|
||||
or(
|
||||
// @embroider/* could be dynamic modules
|
||||
id(/^@embroider\//),
|
||||
// /-embroider-* need access to the app-tree-merge result so that addon-contributed app-tree-merge modules are available in compatModules
|
||||
importerId(/\/-embroider-/)
|
||||
)
|
||||
),
|
||||
],
|
||||
handler: plugin.resolveId,
|
||||
};
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop-in replacement for `@embroider/vite`'s `ember()`.
|
||||
* Skips the two vite-specific plugins, since we're using rolldown.
|
||||
* Uses an optimized strategy for the resolver.
|
||||
*/
|
||||
export default function optimizedEmber() {
|
||||
return [staticEmbroiderAliases(), templateTag(), filteredEmberResolver()];
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
const mergeTrees = require("broccoli-merge-trees");
|
||||
const funnel = require("broccoli-funnel");
|
||||
const concat = require("broccoli-concat");
|
||||
const fs = require("fs");
|
||||
|
||||
// Each file under `scripts/{name}.js` is run through babel, sourcemapped, and then output to `/assets/{name}.js
|
||||
module.exports = function scriptsTree(app) {
|
||||
let babelAddon = app.project.findAddonByName("ember-cli-babel");
|
||||
let babelConfig = {
|
||||
babel: { sourceMaps: "inline" },
|
||||
"ember-cli-babel": { compileModules: false },
|
||||
};
|
||||
|
||||
const trees = [];
|
||||
|
||||
const scripts = fs
|
||||
.readdirSync("scripts", { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isFile());
|
||||
|
||||
for (let script of scripts) {
|
||||
let source = funnel(`scripts`, {
|
||||
files: [script.name],
|
||||
destDir: "scripts",
|
||||
});
|
||||
|
||||
// Babel will append a base64 sourcemap to the file
|
||||
let transpiled = babelAddon.transpileTree(source, babelConfig);
|
||||
|
||||
// We don't actually need to concat any source files... but this will move the base64
|
||||
// source map into its own file
|
||||
let transpiledWithDecodedSourcemap = concat(transpiled, {
|
||||
outputFile: `assets/${script.name}`,
|
||||
});
|
||||
|
||||
trees.push(transpiledWithDecodedSourcemap);
|
||||
}
|
||||
|
||||
// start-discourse.js is a combination of start-app and discourse-boot
|
||||
let startDiscourseTree = funnel(`public/assets/scripts`, {
|
||||
files: ["start-app.js", "discourse-boot.js"],
|
||||
destDir: "scripts",
|
||||
});
|
||||
startDiscourseTree = babelAddon.transpileTree(
|
||||
startDiscourseTree,
|
||||
babelConfig
|
||||
);
|
||||
startDiscourseTree = concat(startDiscourseTree, {
|
||||
outputFile: `assets/start-discourse.js`,
|
||||
headerFiles: [`scripts/start-app.js`],
|
||||
inputFiles: [`scripts/discourse-boot.js`],
|
||||
});
|
||||
trees.push(startDiscourseTree);
|
||||
|
||||
return mergeTrees(trees);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
const TEST_FILE_RE = /tests\/(?!helpers\/).*-test\.(?:gjs|js)$/;
|
||||
|
||||
export default function wrapTestModulesPlugin() {
|
||||
return {
|
||||
name: "wrap-test-modules",
|
||||
transform: {
|
||||
filter: { id: TEST_FILE_RE },
|
||||
handler(code, id, { magicString }) {
|
||||
const ast = this.parse(code);
|
||||
|
||||
let lastImportEnd = 0;
|
||||
for (const node of ast.body) {
|
||||
if (node.type === "ImportDeclaration") {
|
||||
lastImportEnd = node.end;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastImportEnd >= code.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
magicString.appendLeft(
|
||||
lastImportEnd,
|
||||
"\n\nexport default function () {\n"
|
||||
);
|
||||
magicString.append("\n}\n");
|
||||
|
||||
return {
|
||||
code: magicString,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
-15
@@ -43,16 +43,6 @@ function logIfDebug(...messages) {
|
||||
}
|
||||
}
|
||||
|
||||
function setPublicPath(settings) {
|
||||
// This variable assignment is re-written by webpack at build time.
|
||||
// It ensures that the WASM files are loaded from the CDN, just like this JS entrypoint.
|
||||
// eslint-disable-next-line no-undef
|
||||
__webpack_public_path__ = new URL(
|
||||
`${settings.mediaOptimizationBundle}/../..`,
|
||||
location.href
|
||||
).toString();
|
||||
}
|
||||
|
||||
function buildMozJpegOptions(settings) {
|
||||
return {
|
||||
quality: settings.encode_quality,
|
||||
@@ -124,7 +114,6 @@ globalThis.optimize = async function (
|
||||
originalFileSize,
|
||||
settings
|
||||
) {
|
||||
setPublicPath(settings);
|
||||
const mozJpegOptions = buildMozJpegOptions(settings);
|
||||
|
||||
const initialSize = imageData.byteLength;
|
||||
@@ -176,8 +165,6 @@ globalThis.convert = async function (
|
||||
originalFileSize,
|
||||
settings
|
||||
) {
|
||||
setPublicPath(settings);
|
||||
|
||||
logIfDebug(`Converting ${fileName} (${fileType}, ${originalFileSize} bytes)`);
|
||||
|
||||
let imageData;
|
||||
@@ -240,8 +227,6 @@ globalThis.convertAnimated = async function (
|
||||
originalFileSize,
|
||||
settings
|
||||
) {
|
||||
setPublicPath(settings);
|
||||
|
||||
logIfDebug(
|
||||
`Converting animated ${fileName} (${originalFileSize} bytes) to animated WEBP`
|
||||
);
|
||||
@@ -20,9 +20,8 @@
|
||||
"test": "tests"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "ember build",
|
||||
"start": "ember serve",
|
||||
"test": "ember test"
|
||||
"build": "rolldown -c rolldown.config.mjs",
|
||||
"start": "./rolldown.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.20.1",
|
||||
@@ -31,6 +30,14 @@
|
||||
"@codemirror/language": "^6.12.3",
|
||||
"@codemirror/state": "^6.6.0",
|
||||
"@codemirror/view": "^6.40.0",
|
||||
"@discourse/gif": "^1.0.0",
|
||||
"@discourse/heic": "^1.0.0",
|
||||
"@discourse/jpeg": "^1.0.0",
|
||||
"@discourse/jxl": "^1.0.0",
|
||||
"@discourse/png": "^3.1.1",
|
||||
"@discourse/resize": "^2.1.0",
|
||||
"@discourse/webp": "^1.0.0",
|
||||
"@embroider/legacy-inspector-support": "^0.1.3",
|
||||
"@faker-js/faker": "^10.4.0",
|
||||
"@fullcalendar/core": "^6.1.20",
|
||||
"@fullcalendar/daygrid": "^6.1.20",
|
||||
@@ -48,13 +55,6 @@
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"@lezer/javascript": "^1.5.4",
|
||||
"@lezer/lr": "^1.4.8",
|
||||
"@discourse/gif": "^1.0.0",
|
||||
"@discourse/heic": "^1.0.0",
|
||||
"@discourse/jpeg": "^1.0.0",
|
||||
"@discourse/jxl": "^1.0.0",
|
||||
"@discourse/png": "^3.1.1",
|
||||
"@discourse/resize": "^2.1.0",
|
||||
"@discourse/webp": "^1.0.0",
|
||||
"ace-builds": "^1.43.6",
|
||||
"chart.js": "4.5.1",
|
||||
"chartjs-adapter-moment": "^1.0.1",
|
||||
@@ -104,22 +104,28 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.5",
|
||||
"@babel/plugin-transform-runtime": "^7.28.5",
|
||||
"@babel/standalone": "^7.29.4",
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@discourse/itsatrap": "^2.0.10",
|
||||
"@ember-compat/tracked-built-ins": "^0.9.1",
|
||||
"@ember/optional-features": "^3.0.0",
|
||||
"@ember-decorators/component": "^6.1.1",
|
||||
"@ember-decorators/object": "^6.1.1",
|
||||
"@ember-decorators/utils": "^6.1.1",
|
||||
"@ember/render-modifiers": "^3.0.0",
|
||||
"@ember/string": "^4.0.1",
|
||||
"@ember/test-helpers": "^5.4.2",
|
||||
"@ember/test-waiters": "^4.1.1",
|
||||
"@embroider/compat": "^3.9.3",
|
||||
"@embroider/core": "^3.5.9",
|
||||
"@embroider/macros": "^1.16.12",
|
||||
"@embroider/router": "^2.1.12",
|
||||
"@embroider/webpack": "^4.1.0",
|
||||
"@embroider/compat": "^4.1.19",
|
||||
"@embroider/config-meta-loader": "1.0.0",
|
||||
"@embroider/core": "^4.6.0",
|
||||
"@embroider/macros": "^1.20.3",
|
||||
"@embroider/router": "^3.0.6",
|
||||
"@embroider/vite": "^1.7.2",
|
||||
"@floating-ui/dom": "^1.7.5",
|
||||
"@glimmer/component": "^2.0.0",
|
||||
"@rolldown/pluginutils": "1.0.0",
|
||||
"@rollup/plugin-babel": "github:davidtaylorhq/rollup-plugins#babel-parallel-built&path:/packages/babel",
|
||||
"@swc/core": "^1.15.33",
|
||||
"@types/jquery": "^3.5.33",
|
||||
"@types/qunit": "^2.19.13",
|
||||
@@ -131,33 +137,21 @@
|
||||
"@uppy/utils": "^7.2.0",
|
||||
"@uppy/xhr-upload": "^5.2.0",
|
||||
"a11y-dialog": "8.1.5",
|
||||
"babel-import-util": "^3.0.1",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"babel-plugin-debug-macros": "^2.0.0",
|
||||
"babel-plugin-ember-template-compilation": "^4.0.0",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"broccoli-merge-files": "^0.8.0",
|
||||
"custom-proxy": "workspace:1.0.0",
|
||||
"deprecation-silencer": "workspace:1.0.0",
|
||||
"discourse-i18n": "workspace:1.0.0",
|
||||
"discourse-i18n": "workspace:*",
|
||||
"discourse-markdown-it": "workspace:1.0.0",
|
||||
"ember-async-data": "^2.0.1",
|
||||
"ember-auto-import": "^2.13.1",
|
||||
"ember-buffered-proxy": "^2.1.1",
|
||||
"ember-cli": "~6.12.0",
|
||||
"ember-cli-app-version": "^7.0.0",
|
||||
"ember-cli-babel": "^8.2.0",
|
||||
"ember-cli-deprecation-workflow": "^4.0.1",
|
||||
"ember-cli-htmlbars": "^7.0.1",
|
||||
"ember-cli-inject-live-reload": "^2.1.0",
|
||||
"ember-cli-progress-ci": "workspace:1.0.0",
|
||||
"ember-cli-sri": "^2.1.1",
|
||||
"ember-cli-terser": "^4.0.2",
|
||||
"ember-decorators": "^6.1.1",
|
||||
"ember-exam": "^10.1.0",
|
||||
"ember-load-initializers": "^3.0.1",
|
||||
"ember-modifier": "^4.3.0",
|
||||
"ember-qunit": "^9.0.4",
|
||||
"ember-template-imports": "^4.4.0",
|
||||
"ember-test-selectors": "^7.1.0",
|
||||
"jquery": "^3.7.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"jsuites": "^5.13.3",
|
||||
@@ -166,15 +160,12 @@
|
||||
"pretender": "^3.4.7",
|
||||
"qunit": "^2.25.0",
|
||||
"qunit-dom": "^3.5.1",
|
||||
"rolldown": "1.0.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sinon": "^22.0.0",
|
||||
"source-map": "^0.7.6",
|
||||
"terser": "^5.47.1",
|
||||
"testem": "^3.20.0",
|
||||
"typescript": "^5.9.3",
|
||||
"util": "^0.12.5",
|
||||
"webpack": "5.99.9",
|
||||
"webpack-retry-chunk-load-plugin": "^3.1.1",
|
||||
"webpack-stats-plugin": "^1.1.3",
|
||||
"xss": "^1.0.15"
|
||||
},
|
||||
"engines": {
|
||||
@@ -185,5 +176,9 @@
|
||||
},
|
||||
"ember": {
|
||||
"edition": "octane"
|
||||
},
|
||||
"ember-addon": {
|
||||
"type": "app",
|
||||
"version": 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,13 @@ module.exports = function patchTestemOutput() {
|
||||
|
||||
const label = labelForRunner(this);
|
||||
socket.on("browser-console", (type, ...args) => {
|
||||
console.log(`[${label}] [${type}] ${args.join(" ")}`);
|
||||
if (type === "group") {
|
||||
type = `「group」`;
|
||||
} else {
|
||||
type = `[${type}]`;
|
||||
}
|
||||
|
||||
console.log(`[${label}] ${type} ${args.join(" ")}`);
|
||||
});
|
||||
socket.on("top-level-error", (msg, url, line) => {
|
||||
console.log(`[${label}] [error] ${msg} at ${url}:${line}`);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
(function () {
|
||||
if (window.unsupportedBrowser) {
|
||||
throw "Unsupported browser detected";
|
||||
}
|
||||
|
||||
let element = document.querySelector(
|
||||
`meta[name="discourse/config/environment"]`
|
||||
);
|
||||
const config = JSON.parse(
|
||||
decodeURIComponent(element.getAttribute("content"))
|
||||
);
|
||||
const event = new CustomEvent("discourse-init", { detail: config });
|
||||
document.dispatchEvent(event);
|
||||
})();
|
||||
@@ -1,9 +0,0 @@
|
||||
const environment = require("discourse/lib/environment");
|
||||
const { withSilencedDeprecations } = require("discourse/lib/deprecated");
|
||||
|
||||
environment.setEnvironment("qunit-testing");
|
||||
require("discourse/deprecation-workflow").default.setEnvironment(environment);
|
||||
|
||||
withSilencedDeprecations("discourse.native-array-extensions.[]", () => {
|
||||
require("discourse/tests/test-boot-ember-cli");
|
||||
});
|
||||
@@ -1,62 +0,0 @@
|
||||
const dynamicJsTemplate = document.querySelector("#dynamic-test-js");
|
||||
const outputNode = document.querySelector("discourse-dynamic-test-js");
|
||||
|
||||
const params = new URLSearchParams(document.location.search);
|
||||
const target = params.get("target") || "core";
|
||||
|
||||
if (target === "theme-qunit") {
|
||||
window.location.href = window.location.origin + "/theme-qunit";
|
||||
}
|
||||
|
||||
(async function setup() {
|
||||
const rootUrl = document.querySelector("link[rel='canonical']").href;
|
||||
const response = await fetch(
|
||||
`${rootUrl}bootstrap/plugin-test-info.json?target=${target}`
|
||||
);
|
||||
const pluginTestInfo = await response.json();
|
||||
|
||||
dynamicJsTemplate.content.firstElementChild.insertAdjacentHTML(
|
||||
"beforebegin",
|
||||
pluginTestInfo.html
|
||||
);
|
||||
|
||||
window._discourseQunitPluginNames = pluginTestInfo.all_plugins;
|
||||
|
||||
for (const element of dynamicJsTemplate.content.childNodes) {
|
||||
if (
|
||||
element.tagName === "SCRIPT" &&
|
||||
element.innerHTML.includes("EmberENV.TESTS_FILE_LOADED")
|
||||
) {
|
||||
// Inline script introduced by ember-cli. Incompatible with CSP and our custom plugin JS loading system
|
||||
// https://github.com/ember-cli/ember-cli/blob/04a38fda2c/lib/utilities/ember-app-utils.js#L131
|
||||
// We re-implement in test-boot-ember-cli.js
|
||||
continue;
|
||||
}
|
||||
|
||||
if (element.type === "importmap") {
|
||||
const importmap = document.createElement("script");
|
||||
importmap.type = "importmap";
|
||||
importmap.textContent = element.textContent;
|
||||
outputNode.append(importmap);
|
||||
continue;
|
||||
} else if (element.tagName === "SCRIPT") {
|
||||
const script = document.createElement("script");
|
||||
script.src = element.src;
|
||||
for (const [key, value] of Object.entries(element.dataset)) {
|
||||
script.dataset[key] = value;
|
||||
}
|
||||
script.defer = element.defer;
|
||||
script.async = false; // Weirdly, this is true by default when programmatically creating script tags
|
||||
outputNode.append(script);
|
||||
continue;
|
||||
}
|
||||
|
||||
const clone = element.cloneNode(true);
|
||||
|
||||
outputNode.appendChild(clone);
|
||||
|
||||
if (clone.tagName === "LINK" && clone["rel"] === "stylesheet") {
|
||||
await new Promise((resolve) => (clone.onload = resolve));
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -1,20 +0,0 @@
|
||||
document.addEventListener("discourse-init", async (e) => {
|
||||
performance.mark("discourse-init");
|
||||
const config = e.detail;
|
||||
const { default: klass, loadThemesAndPlugins, loadAdmin } = require(
|
||||
`${config.modulePrefix}/app`
|
||||
);
|
||||
|
||||
if (
|
||||
document.querySelector(
|
||||
'link[rel="preload"][data-discourse-entrypoint="admin"]'
|
||||
)
|
||||
) {
|
||||
await loadAdmin();
|
||||
}
|
||||
|
||||
await loadThemesAndPlugins();
|
||||
|
||||
const app = klass.create(config);
|
||||
app.start();
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import "message-bus-client";
|
||||
import jQuery from "jquery";
|
||||
|
||||
window.jQuery = jQuery;
|
||||
|
||||
window.MessageBus.ajax = jQuery.ajax;
|
||||
window.MessageBus.subscribe("/file-change", () => {
|
||||
window.parent.location.reload();
|
||||
});
|
||||
@@ -0,0 +1,249 @@
|
||||
import * as fs from "fs";
|
||||
import { basename, relative } from "path";
|
||||
import { viteAliasPlugin, viteImportGlobPlugin } from "rolldown/experimental";
|
||||
import writeResolverConfig from "./lib/embroider-vite-resolver-options.mjs";
|
||||
import maybeBabel from "./lib/maybe-babel.mjs";
|
||||
import optimizedEmber from "./lib/optimized-ember.mjs";
|
||||
import wrapTestModulesPlugin from "./lib/wrap-test-modules-plugin.mjs";
|
||||
|
||||
writeResolverConfig(
|
||||
{
|
||||
staticAppPaths: ["static", "admin"],
|
||||
splitAtRoutes: [{ type: "string", value: "wizard" }],
|
||||
},
|
||||
{
|
||||
options: {
|
||||
staticInvokables: false,
|
||||
allowUnsafeDynamicComponents: false,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const extensions = [".gjs", ".mjs", ".js", ".mts", ".gts", ".ts", ".hbs"];
|
||||
|
||||
const aliases = [
|
||||
{ find: "pretty-text", replacement: "pretty-text/addon" },
|
||||
{
|
||||
find: "ember-buffered-proxy/helpers",
|
||||
replacement: "ember-buffered-proxy/addon/helpers",
|
||||
},
|
||||
{
|
||||
find: "ember-buffered-proxy/mixin",
|
||||
replacement: "ember-buffered-proxy/addon/mixin",
|
||||
},
|
||||
{
|
||||
find: "ember-buffered-proxy/proxy",
|
||||
replacement: "ember-buffered-proxy/addon/proxy",
|
||||
},
|
||||
|
||||
{
|
||||
find: "@ember-decorators/object",
|
||||
replacement: "@ember-decorators/object/addon",
|
||||
},
|
||||
{
|
||||
find: "@ember-decorators/utils/decorator",
|
||||
replacement: "@ember-decorators/utils/addon/decorator",
|
||||
},
|
||||
{
|
||||
find: "@ember-decorators/utils/collapse-proto",
|
||||
replacement: "@ember-decorators/utils/addon/collapse-proto",
|
||||
},
|
||||
{
|
||||
find: "@ember-decorators/component",
|
||||
replacement: "@ember-decorators/component/addon",
|
||||
},
|
||||
|
||||
{
|
||||
find: "ember-exam/test-support/load",
|
||||
replacement: "ember-exam/addon-test-support/load",
|
||||
},
|
||||
{
|
||||
find: "@ember/render-modifiers",
|
||||
replacement: "@ember/render-modifiers/addon",
|
||||
},
|
||||
];
|
||||
|
||||
export function buildConfig({ devMode } = {}) {
|
||||
const isProduction = process.env.EMBER_ENV === "production";
|
||||
|
||||
if (!isProduction) {
|
||||
process.env.NODE_ENV = "development";
|
||||
}
|
||||
|
||||
return {
|
||||
resolve: {
|
||||
extensions,
|
||||
},
|
||||
experimental: {
|
||||
incrementalBuild: true,
|
||||
resolveNewUrlToAsset: true,
|
||||
nativeMagicString: true,
|
||||
},
|
||||
moduleTypes: {
|
||||
".wasm": "asset",
|
||||
},
|
||||
input: {
|
||||
discourse: "discourse.js",
|
||||
vendor: "vendor.js",
|
||||
"media-optimization-bundle": "media-optimization-bundle.js",
|
||||
...(!isProduction || process.env.FORCE_BUILD_TESTS
|
||||
? {
|
||||
"test-entrypoint": "tests/test-entrypoint.js",
|
||||
"qunit-live-reload": "qunit-live-reload.js",
|
||||
}
|
||||
: undefined),
|
||||
},
|
||||
output: {
|
||||
minify: isProduction,
|
||||
dir: "dist",
|
||||
sourcemap: true,
|
||||
cleanDir: !devMode,
|
||||
hashCharacters: "base36",
|
||||
assetFileNames: (asset) => {
|
||||
if (asset.names?.some((n) => n.endsWith(".wasm"))) {
|
||||
return "assets/wasm/[name]-[hash].digested[extname]";
|
||||
}
|
||||
return "assets/js/[name]-[hash].digested[extname]";
|
||||
},
|
||||
chunkFileNames: "assets/js/[name]-[hash].digested.js",
|
||||
entryFileNames: "assets/js/[name]-[hash].digested.js",
|
||||
},
|
||||
watch: {
|
||||
clearScreen: false,
|
||||
},
|
||||
preserveEntrySignatures: "strict",
|
||||
plugins: [
|
||||
viteAliasPlugin({ entries: aliases }),
|
||||
optimizedEmber(),
|
||||
viteImportGlobPlugin(),
|
||||
maybeBabel({
|
||||
babelHelpers: "runtime",
|
||||
extensions,
|
||||
parallel: true,
|
||||
skipPreflightCheck: true, // Skip per-file config verification
|
||||
babelrc: false, // Skip per-file `.babelrc`/`.babelignore` checks
|
||||
}),
|
||||
wrapTestModulesPlugin(),
|
||||
{
|
||||
name: "resolve-externals",
|
||||
resolveId(source) {
|
||||
if (
|
||||
source.startsWith("/extra-locales/") ||
|
||||
source.startsWith("/bootstrap/")
|
||||
) {
|
||||
return { external: true, id: source };
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "css-loader",
|
||||
transform: {
|
||||
filter: {
|
||||
id: /\.css$/,
|
||||
},
|
||||
handler(code, id) {
|
||||
return {
|
||||
code: `
|
||||
const style = document.createElement("style");
|
||||
style.innerHTML = ${JSON.stringify(code)};
|
||||
style.dataset.rolldownModuleId = ${JSON.stringify(relative(import.meta.dirname, id))};
|
||||
document.head.append(style);
|
||||
`,
|
||||
moduleType: "js",
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "move-sourcemaps",
|
||||
generateBundle(_options, bundle) {
|
||||
const mapEntries = Object.entries(bundle).filter(([f]) =>
|
||||
f.endsWith(".map")
|
||||
);
|
||||
|
||||
for (const [oldFileName, asset] of mapEntries) {
|
||||
// assets/js/foo.js.map → assets/map/foo.js.map
|
||||
const newFileName = oldFileName.replace(
|
||||
/^assets\/js\//,
|
||||
"assets/map/"
|
||||
);
|
||||
|
||||
this.emitFile({
|
||||
type: "asset",
|
||||
fileName: newFileName,
|
||||
source: asset.source,
|
||||
});
|
||||
|
||||
delete bundle[oldFileName];
|
||||
|
||||
// Patch the corresponding JS chunk
|
||||
const jsFileName = oldFileName.slice(0, -4);
|
||||
const chunk = bundle[jsFileName];
|
||||
if (chunk?.code) {
|
||||
chunk.code = chunk.code.replace(
|
||||
/\/\/# sourceMappingURL=.+/,
|
||||
`//# sourceMappingURL=../map/${basename(oldFileName)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bundle-manifest",
|
||||
generateBundle(_outputOptions, bundle) {
|
||||
const manifest = {
|
||||
entrypoints: {},
|
||||
dynamicEntrypoints: {},
|
||||
chunks: {},
|
||||
};
|
||||
|
||||
for (const [fileName, chunk] of Object.entries(bundle)) {
|
||||
if (chunk.type !== "chunk") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const facadeModuleId = chunk.facadeModuleId
|
||||
? relative(import.meta.dirname, chunk.facadeModuleId)
|
||||
: null;
|
||||
|
||||
if (chunk.isEntry) {
|
||||
manifest.entrypoints[chunk.name] = fileName;
|
||||
} else if (
|
||||
chunk.isDynamicEntry &&
|
||||
facadeModuleId &&
|
||||
!facadeModuleId.startsWith("../")
|
||||
) {
|
||||
manifest.dynamicEntrypoints[facadeModuleId] = fileName;
|
||||
}
|
||||
|
||||
manifest.chunks[fileName] = {
|
||||
file: fileName,
|
||||
facadeModuleId,
|
||||
name: chunk.name,
|
||||
isEntry: chunk.isEntry,
|
||||
isDynamicEntry: chunk.isDynamicEntry,
|
||||
imports: chunk.imports,
|
||||
};
|
||||
}
|
||||
|
||||
if (devMode) {
|
||||
// Workaround rolldown devEngine bug?
|
||||
fs.mkdirSync("./dist/manifest", { recursive: true });
|
||||
fs.writeFileSync(
|
||||
"./dist/manifest/manifest.json",
|
||||
JSON.stringify(manifest, null, 2)
|
||||
);
|
||||
} else {
|
||||
this.emitFile({
|
||||
type: "asset",
|
||||
fileName: "manifest/manifest.json",
|
||||
source: JSON.stringify(manifest, null, 2),
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export default buildConfig({ devMode: false });
|
||||
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import AnsiToHtml from "ansi-to-html";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { dev } from "rolldown/experimental";
|
||||
import { buildConfig } from "./rolldown.config.mjs";
|
||||
|
||||
const ansiConverter = new AnsiToHtml({ newline: true, escapeXML: true });
|
||||
const CWD_PREFIX = `${process.cwd()}/`;
|
||||
const MANIFEST_DIR = "./dist/manifest";
|
||||
const BUILD_STATUS_FILE = `${MANIFEST_DIR}/build.json`;
|
||||
|
||||
let buildStart = Date.now();
|
||||
let initialBuild = true;
|
||||
let hasError = false;
|
||||
let pendingChangedFiles = [];
|
||||
|
||||
function ansiToHtml(str) {
|
||||
if (str != null) {
|
||||
return ansiConverter.toHtml(str);
|
||||
}
|
||||
}
|
||||
|
||||
function stripCwd(file) {
|
||||
const relative = path.relative(CWD_PREFIX, file);
|
||||
return relative;
|
||||
}
|
||||
|
||||
function writeBuildStatus(status) {
|
||||
fs.mkdirSync(MANIFEST_DIR, { recursive: true });
|
||||
const payload = {
|
||||
pid: process.pid,
|
||||
timestamp: new Date().toISOString(),
|
||||
...status,
|
||||
};
|
||||
fs.writeFileSync(BUILD_STATUS_FILE, JSON.stringify(payload, null, 2));
|
||||
}
|
||||
|
||||
function serializeError(err) {
|
||||
const base = (rawMessage) => ({
|
||||
message: rawMessage,
|
||||
messageHtml: ansiToHtml(rawMessage),
|
||||
});
|
||||
if (err == null) {
|
||||
return base("Unknown error");
|
||||
}
|
||||
if (err instanceof Error) {
|
||||
return { ...base(err.message), name: err.name };
|
||||
}
|
||||
if (typeof err === "object") {
|
||||
return {
|
||||
...base(err.message ?? String(err)),
|
||||
location: err.loc || err.location,
|
||||
frame: ansiToHtml(err.frame || err.codeFrame),
|
||||
id: err.id,
|
||||
};
|
||||
}
|
||||
return base(String(err));
|
||||
}
|
||||
|
||||
fs.rmSync("./dist", { recursive: true, force: true });
|
||||
fs.mkdirSync("./dist");
|
||||
writeBuildStatus({ status: "building" });
|
||||
|
||||
console.log("Starting rolldown dev server...");
|
||||
|
||||
const resolvedConfig = buildConfig({ devMode: true });
|
||||
const devEngine = await dev(resolvedConfig, resolvedConfig.output, {
|
||||
// Avoid `rebuildStrategy: "always"` — it panics in scan_stage_cache when
|
||||
// recovering from a parse error. Drive rebuilds manually using ensureLatestBuildOutput.
|
||||
onHmrUpdates(result) {
|
||||
if (result instanceof Error) {
|
||||
console.error("Build error:", result.message);
|
||||
writeBuildStatus({
|
||||
status: "error",
|
||||
error: serializeError(result),
|
||||
});
|
||||
hasError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
pendingChangedFiles = result.changedFiles.map(stripCwd);
|
||||
hasError = false;
|
||||
buildStart = Date.now();
|
||||
console.log(`Rebuilding (${pendingChangedFiles.length} changed)...`);
|
||||
devEngine.ensureLatestBuildOutput();
|
||||
},
|
||||
|
||||
onOutput(result) {
|
||||
if (hasError) {
|
||||
return;
|
||||
} else if (result instanceof Error) {
|
||||
console.error("Build error:", result.message);
|
||||
writeBuildStatus({
|
||||
status: "error",
|
||||
error: serializeError(result),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsed = ((Date.now() - buildStart) / 1000).toFixed(2);
|
||||
const count = result.output.length;
|
||||
if (initialBuild) {
|
||||
initialBuild = false;
|
||||
console.log(`Initial build complete in ${elapsed}s (${count} files)`);
|
||||
} else {
|
||||
console.log(
|
||||
`Rebuild complete in ${elapsed}s (${count} files): ${pendingChangedFiles.join(", ")}`
|
||||
);
|
||||
}
|
||||
writeBuildStatus({ status: "ok" });
|
||||
},
|
||||
});
|
||||
|
||||
await devEngine.run();
|
||||
|
||||
// run() resolves after the initial build and rolldown's native watcher
|
||||
// doesn't hold the process open reliably
|
||||
setInterval(() => {}, 0x7fffffff);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user