Ember.js Frontend Boot & Structure
Discourse's Ember frontend boots from frontend/discourse/app/app.js, which stitches together an AMD-style module registry (core, plugins, and themes all get define()'d into it), reads server-serialized JSON via the PreloadStore, and then walks every registered module to find and register initializers before Ember starts rendering. Plugins and themes hook into this same flow either through discourse-style initializer naming conventions or by calling withPluginApi(), which hands them a shared PluginApi instance. Understanding this file explains how core, plugin, and theme JavaScript all end up wired into one running application at boot time.
The Discourse Application class
frontend/discourse/app/app.js · lines 147–188Discourse subclasses Ember's Application, rooted at #main, and swaps in a custom Resolver (buildResolver) that knows how to look up modules coming from plugins and themes, not just core.
start() is Ember's application boot hook. Discourse uses it to do some environment setup (event normalization, scroll restoration) and then calls loadInitializers(this) — the call that actually discovers and registers every initializer from core, plugins, and themes.
ready() is Ember's post-boot lifecycle hook; here it just stamps a performance mark once the application instance has fully started.
class Discourse extends Application { modulePrefix = "discourse"; rootElement = "#main"; inspector = setupInspector(this); customEvents = { paste: "paste", }; Resolver = buildResolver("discourse"); // Start up the Discourse application by running all the initializers we've defined. start() { printDebugInfo(); document.querySelectorAll("noscript").forEach((el) => el.remove()); // Rewire event handling to eliminate event delegation for better compat // between Glimmer and Classic components. normalizeEmberEventHandling(this); if (Error.stackTraceLimit) { // We need Errors to have full stack traces for `lib/source-identifier` Error.stackTraceLimit = Infinity; } // Our scroll-manager service takes care of storing and restoring scroll position. // Disable browser handling: window.history.scrollRestoration = "manual"; loadInitializers(this); } _registerPluginCode(version, code) { _pluginCallbacks.push({ version, code }); } ready() { performance.mark("discourse-ready"); }}
function loadInitializers(app) { let initializers = []; let instanceInitializers = []; let discourseInitializers = []; let discourseInstanceInitializers = []; for (let moduleName of Object.keys(requirejs.entries)) { if (moduleName.startsWith("discourse/") && !moduleName.endsWith("-test")) { // In discourse core, initializers follow standard Ember conventions if (moduleName.startsWith("discourse/initializers/")) { initializers.push(moduleName); } else if (moduleName.startsWith("discourse/instance-initializers/")) { instanceInitializers.push(moduleName); } else { // https://meta.discourse.org/t/updating-our-initializer-naming-patterns/241919 // // For historical reasons, the naming conventions in plugins and themes // differs from Ember: // // | Ember | Discourse | | // | initializers | pre-initializers | runs once per app load | // | instance-initializers | (api-)initializers | runs once per app boot | // // In addition, the arguments to the initialize function is different – // Ember initializers get either the `Application` or `ApplicationInstance` // as the only argument, but the "discourse style" gets an extra container // argument preceding that. const themeId = moduleThemeId(moduleName); if ( themeId !== undefined || moduleName.startsWith("discourse/plugins/") ) { if (moduleName.includes("/pre-initializers/")) { discourseInitializers.push([moduleName, themeId]); } else if ( moduleName.includes("/initializers/") || moduleName.includes("/api-initializers/") ) { discourseInstanceInitializers.push([moduleName, themeId]); } } } } }
frontend/discourse/app/app.js · lines 221–267function getPluginApi() { const owner = getOwnerWithFallback(this); let pluginApi = owner.lookup("plugin-api:main"); if (!pluginApi) { pluginApi = new _PluginApi(owner); owner.registry.register("plugin-api:main", pluginApi, { instantiate: false, }); } else { // If we are re-using an instance, make sure the container is correct pluginApi.container = owner; } return pluginApi;} /** * Executes the provided callback function with the `PluginApi` object. * * @param {(api: _PluginApi, opts: object) => any} apiCodeCallback - The callback function to execute * @param {object} [opts] - Optional additional options to pass to the callback function. * @returns {any} The result of the `callback` function, if executed */export function withPluginApi(apiCodeCallback, opts) { if (typeof arguments[0] === "string") { // Old path. First argument is the version string. Silently ignore. [, apiCodeCallback, opts] = arguments; } opts = opts || {}; return apiCodeCallback(getPluginApi(), opts);}
frontend/discourse/app/lib/plugin-api.gjs · lines 3635–3668Check your understanding
Answered in place — nothing is graded, everything is explained. 0 / 2 passed
Which Application method actually triggers discovery and registration of every initializer in app.js?
A theme ships a module named discourse/theme-12/api-initializers/my-feature. How does loadInitializers() treat it?