sam

looking at discourse/discourse explain the new patterns for js dependency distribution, investigate how mathjax is distributed and how fullcalendar is. Reply with a quick doc of “old way”, “new way” and how to port mathjax to new pattern

GitHub_Helper_bot
Thinking

GitHub search files
Searched for ‘mathjax’ in discourse/discourse/main

GitHub search files
Searched for ‘fullcalendar’ in discourse/discourse/main

GitHub code search
Searched for ‘fullcalendar’ in discourse/discourse

GitHub file content
Retrieved content of frontend/discourse/app/static/full-calendar-bundle.js, frontend/discourse/app/lib/load-full-calendar.js, frontend/discourse/package.json, plugins/discourse-math/assets/javascripts/initializers/discourse-math-mathjax.js from discourse/discourse@main

Now let me look at how fullcalendar is actually consumed in the calendar plugin:

Thinking

GitHub file content
Retrieved content of plugins/discourse-calendar/assets/javascripts/discourse/components/full-calendar.gjs from discourse/discourse@main

Now let me search for more context on the static bundle patterns and how the old way worked:

Thinking

GitHub search files
Searched for ‘static, bundle’ in discourse/discourse/main

GitHub code search
Searched for ‘loadScript public’ in discourse/discourse

GitHub file content
Retrieved content of frontend/discourse/app/static/ace-editor-bundle.js, frontend/discourse/app/lib/load-script.js from discourse/discourse@main

GitHub code search
Searched for ‘ace-editor-bundle’ in discourse/discourse


JS Dependency Distribution: Old Way vs New Way

Overview

Discourse has moved from vendoring/public folder distribution to proper npm dependencies with static bundles for large external libraries. Here’s how FullCalendar (new pattern) compares to MathJax (old pattern).


Old Way (MathJax Pattern)

MathJax is distributed by vendoring the entire library into the plugin’s public/ folder:

Structure:

plugins/discourse-math/
├── public/
│   └── mathjax/
│       ├── MathJax.2.7.5.js
│       └── extensions/
│           └── ... (dozens of files)

Loading mechanism (discourse-math-mathjax.js):

import loadScript from "discourse/lib/load-script";

function ensureMathJax(opts) {
  initMathJax(opts);
  return loadScript("/plugins/discourse-math/mathjax/MathJax.2.7.5.js");
}

Problems:

  • :cross_mark: Manually vendored files (version 2.7.5 is ancient, MathJax 3+ is current)
  • :cross_mark: No automatic dependency updates via npm/pnpm
  • :cross_mark: Large blob of files checked into git
  • :cross_mark: Uses global window.MathJax instead of ES modules
  • :cross_mark: loadScript() injects a <script> tag at runtime

New Way (FullCalendar Pattern)

FullCalendar uses npm packages with a static bundle pattern:

1. Dependencies in package.json

frontend/discourse/package.json:

{
  "dependencies": {
    "@fullcalendar/core": "^6.1.19",
    "@fullcalendar/daygrid": "^6.1.19",
    "@fullcalendar/list": "^6.1.19",
    "@fullcalendar/moment": "^6.1.19",
    "@fullcalendar/moment-timezone": "^6.1.19",
    "@fullcalendar/timegrid": "^6.1.19"
  }
}

2. Static Bundle File

frontend/discourse/app/static/full-calendar-bundle.js:

export { Calendar } from "@fullcalendar/core";
export { default as DayGrid } from "@fullcalendar/daygrid";
export { default as TimeGrid } from "@fullcalendar/timegrid";
export { default as List } from "@fullcalendar/list";
export { default as MomentPlugin } from "@fullcalendar/moment";
export { default as MomentTimezonePlugin } from "@fullcalendar/moment-timezone";

3. Lazy Loader Helper

frontend/discourse/app/lib/load-full-calendar.js:

import { waitForPromise } from "@ember/test-waiters";

export default async function loadFullCalendar() {
  const promise = import("discourse/static/full-calendar-bundle");
  waitForPromise(promise);
  return await promise;
}

4. Usage in Components

full-calendar.gjs:

import loadFullCalendar from "discourse/lib/load-full-calendar";

export default class FullCalendar extends Component {
  @action
  async setupCalendar(element) {
    const calendarModule = await loadFullCalendar();

    this.calendar = new calendarModule.Calendar(element, {
      plugins: [
        calendarModule.DayGrid,
        calendarModule.TimeGrid,
        // ...
      ],
      // ...
    });
  }
}

Benefits:

  • :white_check_mark: Proper npm packages with semver
  • :white_check_mark: Automatic updates via Dependabot
  • :white_check_mark: Webpack/Embroider tree-shakes & bundles
  • :white_check_mark: ES module exports (no globals)
  • :white_check_mark: Lazy-loaded via dynamic import()
  • :white_check_mark: waitForPromise integration for tests

How to Port MathJax to New Pattern

Step 1: Add npm dependency

Add MathJax 3.x to frontend/discourse/package.json:

{
  "dependencies": {
    "mathjax": "^3.2.2"
  }
}

Step 2: Create static bundle

Create frontend/discourse/app/static/mathjax-bundle.js:

// MathJax 3 has a different API - example for TeX to SVG
import { mathjax } from "mathjax-full/js/mathjax";
import { TeX } from "mathjax-full/js/input/tex";
import { SVG } from "mathjax-full/js/output/svg";
import { browserAdaptor } from "mathjax-full/js/adaptors/browserAdaptor";
import { RegisterHTMLHandler } from "mathjax-full/js/handlers/html";

export { mathjax, TeX, SVG, browserAdaptor, RegisterHTMLHandler };

Or for simpler use with mathjax-full’s built-in component:

export * from "mathjax-full/js/mathjax";
export { TeX } from "mathjax-full/js/input/tex/tex";
export { CHTML } from "mathjax-full/js/output/chtml/chtml";
// ... export what you need

Step 3: Create loader helper

Create frontend/discourse/app/lib/load-mathjax.js:

import { waitForPromise } from "@ember/test-waiters";

export default async function loadMathJax() {
  const promise = import("discourse/static/mathjax-bundle");
  waitForPromise(promise);
  return await promise;
}

Step 4: Update the plugin initializer

Update plugins/discourse-math/assets/javascripts/initializers/discourse-math-mathjax.js:

import loadMathJax from "discourse/lib/load-mathjax";

let mathjaxInstance = null;

async function ensureMathJax(opts) {
  if (mathjaxInstance) {
    return mathjaxInstance;
  }
  
  const { mathjax, TeX, CHTML, browserAdaptor, RegisterHTMLHandler } = 
    await loadMathJax();
  
  const adaptor = browserAdaptor();
  RegisterHTMLHandler(adaptor);
  
  mathjaxInstance = mathjax.document(document, {
    InputJax: new TeX({ /* options */ }),
    OutputJax: new CHTML({ /* options */ })
  });
  
  return mathjaxInstance;
}

// Update decorate() to use MathJax 3 API
async function decorate(elem, isPreview, opts) {
  const mjDoc = await ensureMathJax(opts);
  // Use mjDoc.render() or similar MathJax 3 methods
}

Step 5: Delete vendored files

Remove the entire plugins/discourse-math/public/mathjax/ directory.


Summary Table

Aspect Old (MathJax) New (FullCalendar)
Package source Vendored in public/ npm in package.json
Version 2.7.5 (manual) ^6.1.19 (semver)
Loading loadScript() + <script> tag import() dynamic ES module
Exports Global window.MathJax ES module exports
Bundle location /plugins/.../mathjax/ app/static/*-bundle.js
Updates Manual copy Dependabot PRs
Tree-shaking None Webpack optimized