Skip to content

[WIP] Build process for frontendbase apps alternative explicit version - #283

Closed
holaontiveros wants to merge 10 commits into
overhangio:releasefrom
WGU-Open-edX:frontend-base-b-side
Closed

[WIP] Build process for frontendbase apps alternative explicit version#283
holaontiveros wants to merge 10 commits into
overhangio:releasefrom
WGU-Open-edX:frontend-base-b-side

Conversation

@holaontiveros

@holaontiveros holaontiveros commented Mar 10, 2026

Copy link
Copy Markdown

Basic details

This is the first step for the build process of the frontend apps

  • Adds a new filter that will be used to know which apps should be frontendbase
  • Adds a patch to prevent translations being pulled for template site (while I figure out how that should work)
  • Modify a couple of utility functions to allow frontent-template-site to be mounted (while we agree on specific semantics)
  • Modifies Caddyfile so if the app is a frontentbase it directs to template-site instead of the old app folder
  • installs all the frontendapps that are configured with a repository attribute which means they need to be build explicitly, pack and installed on template site

Assumptions

This use multi stage builds if there's frontend apps that have the repository attribute configured which means that if you manually declare a repository for those apps the process will try to build it, pack it and install it.

** This may be network / RAM intensive ** so in case you run into something like:

image

disable a couple MFEs or reduce the amount of parallelism in docker build config (details about this can be found at the readme look for parallelism in the dev section)

Also for now, the aggregator repo needs to have the same public path as the app_name for the app in this case and for now that's template-site which means PUBLIC_PATH=/template-site/ because that will allow all the assets to be served properly.

How to add a frontendapp

In order to mark something as a frontendbase app you need a plugin that looks like:

from tutormfe.hooks import FRONTEND_APPS

@FRONTEND_APPS.add()
def _add_frontend_apps(apps):
    apps["learner-dashboard"] = {}
    apps["authn"] = {}
    apps["instructor"] = {} 
    return apps

and if you need some of those to be manually build and installed in the template site:

@FRONTEND_APPS.add()
def _add_frontend_apps(apps):
    apps["learner-dashboard"] = {
        "repository": "https://github.com/openedx/frontend-app-learner-dashboard.git",
        "version": "frontend-base",
        "port": "8080",
    }
    apps["authn"] = {
        "repository": "https://github.com/openedx/frontend-app-authn.git",
        "version": "frontend-base",
        "port": "8080",
    }
    return apps

notice that the portis there but it's just to match the contract for the Dockerfile

Any app that it's on this list will be considered a fronend-app so when Caddy shows the content for it, it will show the one on template-site instead of the normal app.

@arbrandes arbrandes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, Javier!

In this first pass, I made several inline comments, but as I write this some of them are already obsolete lol. Anyway, the gist of it comes down to:

  1. We should not depend on an arbitrary prefix to distinguish frontend apps from regular MFEs. They should be "their own thing" as much as possible.
  2. We need a separate section/loop of the Dockerfile from the one that builds regular MFEs, because the requirements and expectations, though similar, are not quite the same.
  3. tutor-mfe needs its own minimal version of frontend-template-site. As in, it should be in this repository, not in a separate one. We'd just call it frontend-site, or something like that. This is because we're going to have to let Tutor customize certain files via patches: notably, package.json and index.html.

And now the important bit:

  1. NPM workspaces: I believe we can reconcile the explicit build being proposed here (which I prefer) with some of the advantages of the npm prepare approach you suggest in #282 with NPM workspaces - but even better. The basic plan is the following:

package.json differences from frontend-template-site

Add a workspaces field and add the build:deps script:

{
  "workspaces": [
    "packages/*"
  ],
  "scripts": {
    "build:deps": "npm run build --workspaces --if-present",
  }
}

Dependencies like @openedx/frontend-app-learner-dashboard remain listed under dependencies with their registry versions. When a matching package exists under packages/, npm resolves the dependency via the workspace checkout instead of fetching from the registry.

Local checkouts

Apps that need building (and only apps that need building) are checked out into frontend-site/packages/. This can be done in the aforementioned separate loop in the Dockerfile, all in a single layer.

When no packages are checked out, npm install falls back to the registry as usual — the same package.json works both ways.

Build Workflow

This is the single install/build step in the Dockerfile after everything is checked out:

npm install                     # hoists and deduplicates all dependencies, including devDependencies like tsc and tsc-alias
npm run build:deps                   # builds the workspace packages (and just those)
npm run build            # build the site

npm run build --workspaces runs each package's build script in topological order. --if-present skips packages that don't have one.

Advantages Over prepare in Each Package

  • Shared build tools: tsc, tsc-alias, and other build dependencies are installed once at the root, not independently in each package. Faster installs, less disk usage.
  • Explicit build step: build:deps is a deliberate action, not a side effect of npm install. Easier to reason about and debug.
  • No devDependency churn: With prepare on git dependencies, npm installs each package's devDependencies, runs the build, then prunes them. Workspaces skip this install-build-prune cycle entirely.
  • Graceful fallback: Without packages/ checkouts, the project installs normally from the registry.

uri strip_prefix /{{ app_name }}
{%- if is_frontend_app_enabled(app_name) %}
# {{ app_name }} - using frontend-apps approach
root * /openedx/dist/template-site

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about calling it frontend-site?

Suggested change
root * /openedx/dist/template-site
root * /openedx/dist/frontend-site

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, this is currently based on the naming for the especific repo, so given that it's called "template-site" it ends up with that name, so it's a matter of changing the config

Comment thread tutormfe/hooks.py
MFE_ATTRS_TYPE = t.Dict[
t.Literal["repository", "port", "version"], t.Union["str", int]
]
FRONTEND_TEMPLATE_SITE_ATTRS_TYPE = t.Dict[

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I misunderstood the purpose, this is about individual apps, not the whole site, right?

Suggested change
FRONTEND_TEMPLATE_SITE_ATTRS_TYPE = t.Dict[
FRONTEND_APP_ATTRS_TYPE = t.Dict[

Comment thread tutormfe/plugin.py
# "repository": "https://github.com/WGU-Open-edX/frontend-template-site.git",
# "version": "initial",
# "port": 8080,
# }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this commented out bit?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had it to remember to add the template site as a default app, BUT if we are going with base files to be patched then yeah I'll just kill this part

Comment thread tutormfe/plugin.py
Comment on lines +103 to +107
# List will need
## Apps that are only frontend-apps
## Apps that are only MFEs
## Apps with unique ones (all old mfes + instruct)
## 1 and 2 with 1 having something like a different identifier

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without reading the code, this comment is hard to understand. What are "apps with unique ones"? What are 1 and 2?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, It was for my own read when I was iteraring through different approachces, I'll clean it as soon as we get to the final draft

Comment thread tutormfe/plugin.py


@tutor_hooks.lru_cache
def get_frontend_apps(apps_to_build: bool = False) -> dict[str, FRONTEND_TEMPLATE_SITE_ATTRS_TYPE]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apps_to_build sounds like a list, but apparently it's a boolean. Let's call it something like only_apps_to_build, then?

Suggested change
def get_frontend_apps(apps_to_build: bool = False) -> dict[str, FRONTEND_TEMPLATE_SITE_ATTRS_TYPE]:
def get_frontend_apps(only_apps_to_build: bool = False) -> dict[str, FRONTEND_TEMPLATE_SITE_ATTRS_TYPE]:

}
handle @mfe_{{ app_name }} {
uri strip_prefix /{{ app_name }}
{%- if is_frontend_app_enabled(app_name) %}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shorter and clearer, I think:

Suggested change
{%- if is_frontend_app_enabled(app_name) %}
{%- if is_frontend_app(path) %}

{{ patch("mfe-dockerfile-base") }}

{% for app_name, app in iter_mfes() %}
{% for app_name, app in iter_all_apps() %}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity, as suggested elsewhere:

Suggested change
{% for app_name, app in iter_all_apps() %}
{% for app_name, app in iter_mfes_and_apps_to_build() %}


# While we figure out how translations will be managed in template site
# if it's either template-site or starts with frontend-app, we skip pulling translations
{% if app_name != "template-site" and not app_name.startswith("frontend-app") %}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of distinguishing between MFE and frontend app just based on a string prefix. I think we should own up to the difference and have a separate loop just for frontend apps. For example, I suspect apps don't need the pre- and post-npm-install patches.

Also, I don't think frontend apps will be subject to the same MFE_COMMON_VERSION (we're not tagging frontend-base apps with release/verawood.1). This is another reason to have a separate list just for them.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can definitely separate them, I just didn't want to start with something separated without having everything rolling and being able to know how we can "optimize it"

I mean so the new separate loop does only the things we know we want it to do

{{ patch("mfe-dockerfile-pre-npm-build") }}
{{ patch("mfe-dockerfile-pre-npm-build-{}".format(app_name)) }}

{% if is_frontend_app_to_build(app_name) %}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_frontend_app() should be sufficient because this list only contains things to build already, but as noted in the above comment, I actually think we should have a separate loop just for frontend apps and avoid the check altogether.

######## {{ app_name }} (production)
FROM {{ app_name }}-common AS {{ app_name }}-prod

{% if app_name == "template-site" %}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The frontend site should not be an item on a configurable list. It should be a hard-coded part of the Dockerfile.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we'll have package.json, and a site.config.build.tsx tempaltes that we'll use as a base always and patch on top of it?

@arbrandes

Copy link
Copy Markdown
Collaborator

Forgot an important bit:

I don't know if we'll have to build the list of dependencies in the frontend-site package.json dynamically from the plugin data, or if we can manage with separate npm installs. My hunch is that building the package.json dynamically (from package names and loose versions such as ^1) will be cleaner. We can still give users pre- and post-npm-install patches, though.

@arbrandes

Copy link
Copy Markdown
Collaborator

Closing in favor of #284.

@arbrandes arbrandes closed this Apr 2, 2026
@ahmed-arb ahmed-arb moved this from Pending Triage to Won't fix in Tutor project management Apr 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Won't fix

Development

Successfully merging this pull request may close these issues.

4 participants