Discourse Onboarding LabChapter V — Ember.js Frontend Boot & Structure0/6Contents
Chapter V

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.


walkthrough

The Discourse Application class

frontend/discourse/app/app.js · lines 147188
lines 147–157

Discourse 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.

lines 160–179

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.

lines 185–187

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");  }}
step 1 of 3
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 221267
Figure I · loadInitializers walks every module name known to the AMD registry (requirejs.entries). Core code follows plain Ember naming (initializers/, instance-initializers/), but modules coming from a theme (moduleThemeId) or from discourse/plugins/ are classified using Discourse's own pre-initializer / (api-)initializer naming, since plugin and theme authors write to that convention instead of Ember's.
function 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 36353668
Figure II · Every place plugin/theme code hooks into Discourse — app.js's discourse-style initializers, apiInitializer(), or legacy <script> plugin registrations — ultimately calls withPluginApi, which looks up (or lazily creates) one PluginApi instance per container and passes it to the plugin's callback. This is the single object plugin/theme JS uses to register hooks into core.
app.js boot flow
app.js entry / Discourse classfrontend/discourse/app/app.js
PreloadStore…iscourse/app/lib/preload-store.js
AMD module registry…/discourse/app/lib/loader-shim.js
loadInitializersfrontend/discourse/app/app.js
PluginApi…/discourse/app/lib/plugin-api.gjs
Figure III · Click a node to see what it does, where it lives in the code, and its labeled connections — the annotation opens beside it.
checkpoint

Check 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?