Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e39cbac
chore: base configs, to move from lap
holaontiveros Feb 17, 2026
43c6e81
chore: temp to chanto to mac
holaontiveros Feb 18, 2026
76af687
chore: change computers
holaontiveros Feb 19, 2026
7a40e2d
chore: working caddy file (BASE_PATH in site needs /template-site)
holaontiveros Feb 19, 2026
ec75e8a
chore: added unique frontend-apps
holaontiveros Feb 19, 2026
4621a8b
chore: remote patch that won't be erquired with prepare
holaontiveros Feb 19, 2026
31fc159
chore: remove debug
holaontiveros Feb 19, 2026
6b13d73
chore: added base changes for alternative version of mfe build
holaontiveros Mar 5, 2026
aa55054
chore: push to change computer
holaontiveros Mar 10, 2026
5a403a6
fix: bad return get_frontend_apps
holaontiveros Mar 10, 2026
2afc49b
feat: frontend-base build with workspaces
holaontiveros Mar 26, 2026
0bcc14d
chore: cleanup
holaontiveros Mar 26, 2026
16c4908
chore: added missing cache and registry for build
holaontiveros Mar 27, 2026
788f347
chore: remove index.html while in site paths to focus on the assets
holaontiveros Mar 27, 2026
6c2c3da
chore: updated package.json to new version standar
holaontiveros Mar 27, 2026
0ccf4a1
chore: added @app to be more explicit
holaontiveros Mar 27, 2026
ece781a
chore: add {{ PLATFORM_NAME }} to allow title config
holaontiveros Mar 27, 2026
c88c6fa
feat: add appEntryPoints to better configure the default site
holaontiveros Mar 27, 2026
a39733c
chore: cleanup and improvements
holaontiveros Mar 30, 2026
9580bf9
feat: added site level configs
holaontiveros Mar 31, 2026
e2f903d
chore: update app_name to site_name for better semantics
holaontiveros Mar 31, 2026
ac86f38
chore: add small clarification to the site routes
holaontiveros Mar 31, 2026
e3b3658
feat: added config for frontendbase version
holaontiveros Mar 31, 2026
327d455
feat: add paragon version / alias config
holaontiveros Mar 31, 2026
46fa576
chore: better semantic for dockerfile
holaontiveros Mar 31, 2026
7483129
chore: updated site.condfg.dev with latest of the build one
holaontiveros Mar 31, 2026
0eeef3a
feat: add single file config frontend slots
holaontiveros Apr 1, 2026
24d54a0
feat: optimized site layers for better caching
holaontiveros Apr 1, 2026
17a5a67
chore: add missing patch locations
holaontiveros Apr 1, 2026
b52cee6
chore: remove configured apps build if site is not local, improved se…
holaontiveros Apr 2, 2026
065a084
chore: added patch for MICROFRONTEND_URL and updated names for better…
holaontiveros Apr 2, 2026
2b9a182
chore: added patch for MICROFRONTEND_URL and updated names for better…
holaontiveros Apr 2, 2026
7c07801
Merge remote-tracking branch 'refs/remotes/fork/frontend-base-c-side'…
holaontiveros Apr 2, 2026
972d2c5
chore: fixed typings
holaontiveros Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions tutormfe/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@

MFE_ATTRS_TYPE = t.Dict[t.Literal["repository", "port", "version"], t.Union["str", int]]

# Extended MFE type that includes externalRoutes
FRONTEND_SITE_ATTRS_TYPE = t.Dict[
t.Literal["repository", "port", "version", "siteConfig"],
t.Union[str, int, dict],
]

FRONTEND_APP_ATTRS_TYPE = t.Dict[
t.Literal["repository", "version", "site", "appEntryPoints", "appId"],
t.Union["str", int, dict],
]

MFE_APPS: Filter[dict[str, MFE_ATTRS_TYPE], []] = Filter()

# This holds which apps are enabled and if they will build from a custom repo
FRONTEND_APPS: Filter[dict[str, FRONTEND_APP_ATTRS_TYPE], []] = Filter()
# This holds all the possible frontend-sites which by default it's the internal one.
FRONTEND_SITES: Filter[dict[str, FRONTEND_SITE_ATTRS_TYPE], []] = Filter()

PLUGIN_SLOTS: Filter[list[tuple[str, str, str]], []] = Filter()

FRONTEND_SLOTS: Filter[list[tuple[str, str]], []] = Filter()
2 changes: 2 additions & 0 deletions tutormfe/patches/local-docker-compose-dev-services
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ mfe:
{%- if MFE_HOST_EXTRA_FILES %}
- 8002:8002
{%- endif %}
{# TODO: Add here the ports for unmounted site-configs #}
{%- for app_name, app in mfe_data.unmounted %}
- {{ app["port"] }}:8002 # {{ app_name }}
{%- endfor %}
- 8080:8002
{% endif %}
18 changes: 18 additions & 0 deletions tutormfe/patches/openedx-lms-development-settings
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ MFE_CONFIG["ADMIN_CONSOLE_URL"] = ADMIN_CONSOLE_MICROFRONTEND_URL
CATALOG_MICROFRONTEND_URL = "http://{{ MFE_HOST }}:{{ get_mfe("catalog")["port"] }}/catalog"
{% endif %}

{% if is_frontend_app("authn") %}
{% set app = get_frontend_app("authn") %}
{% set site = get_frontend_site(app.get("site", "default")) %}
AUTHN_MICROFRONTEND_URL = "http://{{ MFE_HOST }}:{{ site["port"] }}/authn"
{% endif %}

{% if is_frontend_app("learner-dashboard") %}
{% set app = get_frontend_app("learner-dashboard") %}
{% set site = get_frontend_site(app.get("site", "default")) %}
LEARNER_HOME_MICROFRONTEND_URL = "http://{{ MFE_HOST }}:{{ site["port"] }}/learner-dashboard"
{% endif %}

# Cors configuration
{% for app_name, app in iter_mfes() %}
# {{ app_name }} MFE
Expand All @@ -95,5 +107,11 @@ LOGIN_REDIRECT_WHITELIST.append("{{ MFE_HOST }}:{{ app["port"] }}")
CSRF_TRUSTED_ORIGINS.append("http://{{ MFE_HOST }}:{{ app["port"] }}")
{% endfor %}

{% for app_name, app in iter_frontend_sites() %}
CORS_ORIGIN_WHITELIST.append("http://{{ MFE_HOST }}:{{ app["port"] }}")
LOGIN_REDIRECT_WHITELIST.append("{{ MFE_HOST }}:{{ app["port"] }}")
CSRF_TRUSTED_ORIGINS.append("http://{{ MFE_HOST }}:{{ app["port"] }}")
{% endfor %}

{{ patch("mfe-lms-common-settings") }}
{{ patch("mfe-lms-development-settings") }}
150 changes: 149 additions & 1 deletion tutormfe/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@
from tutor.types import Config, get_typed

from .__about__ import __version__
from .hooks import MFE_APPS, MFE_ATTRS_TYPE, PLUGIN_SLOTS
from .hooks import (
FRONTEND_SITES,
MFE_APPS,
MFE_ATTRS_TYPE,
FRONTEND_APPS,
FRONTEND_APP_ATTRS_TYPE,
PLUGIN_SLOTS,
FRONTEND_SLOTS,
FRONTEND_SITE_ATTRS_TYPE,
)

# Handle version suffix in main mode, just like tutor core
if __version_suffix__:
Expand Down Expand Up @@ -82,6 +91,35 @@
},
}

CORE_FRONTEND_SITES: dict[str, FRONTEND_SITE_ATTRS_TYPE] = {
"default": {
"repository": "local",
"port": 8080,
"siteConfig": {
"siteId": "tutor-frontend-site",
"siteName": "Frontend Template Site",
"accessTokenCookieName": "edx-jwt-cookie-header-payload",
"redirectRoleId": "org.openedx.frontend.role.dashboard",
"frontendBaseVersion": "^1.0.0-alpha",
"paragonVersion": "^23",
"externalRoutes": [
{
"role": "org.openedx.frontend.role.profile",
"url": ":1995/profile/",
},
{
"role": "org.openedx.frontend.role.account",
"url": ":1997/account/",
},
{
"role": "org.openedx.frontend.role.logout",
"url": ":8000/logout",
},
],
},
},
}


# The core MFEs are added with a high priority, such that other users can override or
# remove them.
Expand All @@ -91,6 +129,14 @@ def _add_core_mfe_apps(apps: dict[str, MFE_ATTRS_TYPE]) -> dict[str, MFE_ATTRS_T
return apps


@FRONTEND_SITES.add(priority=tutor_hooks.priorities.HIGH)
def _add_core_frontend_sites(
sites: dict[str, FRONTEND_SITE_ATTRS_TYPE],
) -> dict[str, FRONTEND_SITE_ATTRS_TYPE]:
sites.update(CORE_FRONTEND_SITES)
return sites


@tutor_hooks.lru_cache
def get_mfes() -> dict[str, MFE_ATTRS_TYPE]:
"""
Expand All @@ -99,6 +145,22 @@ def get_mfes() -> dict[str, MFE_ATTRS_TYPE]:
return MFE_APPS.apply({})


@tutor_hooks.lru_cache
def get_frontend_apps() -> dict[str, FRONTEND_APP_ATTRS_TYPE]:
"""
This function is cached for performance.
"""
return FRONTEND_APPS.apply({})


@tutor_hooks.lru_cache
def get_frontend_sites() -> dict[str, FRONTEND_SITE_ATTRS_TYPE]:
"""
This function is cached for performance.
"""
return FRONTEND_SITES.apply({})


class MFEMountData:
"""Stores categorized mounted and unmounted MFEs."""

Expand All @@ -125,6 +187,14 @@ def get_plugin_slots(mfe_name: str) -> list[tuple[str, str]]:
return [i[-2:] for i in PLUGIN_SLOTS.iterate() if i[0] == mfe_name]


@tutor_hooks.lru_cache
def get_frontend_slots() -> list[tuple[str, str]]:
"""
This function is cached for performance.
"""
return FRONTEND_SLOTS.apply([])


def iter_mfes() -> t.Iterable[tuple[str, MFE_ATTRS_TYPE]]:
"""
Yield:
Expand All @@ -134,6 +204,47 @@ def iter_mfes() -> t.Iterable[tuple[str, MFE_ATTRS_TYPE]]:
yield from get_mfes().items()


def iter_frontend_apps() -> t.Iterable[tuple[str, FRONTEND_APP_ATTRS_TYPE]]:
"""
Yield:

(name, dict)
"""
frontend_apps = get_frontend_apps()
for name, attrs in frontend_apps.items():
yield (name, attrs)


def iter_frontend_sites() -> t.Iterable[tuple[str, FRONTEND_SITE_ATTRS_TYPE]]:
"""
Yield:

(name, dict)
"""
frontend_sites = get_frontend_sites()
for name, attrs in frontend_sites.items():
yield (name, attrs)


def iter_paths() -> t.Iterable[tuple[str, FRONTEND_APP_ATTRS_TYPE]]:
"""
Yield:

(name, dict)
"""
mfes = get_mfes()
frontend_apps = get_frontend_apps()

# First yield all MFEs
for name, attrs in mfes.items():
yield (name, attrs)

# Then yield frontend apps that are not already MFEs
for name, attrs in frontend_apps.items():
if name not in mfes:
yield (name, attrs)


def iter_plugin_slots(mfe_name: str) -> t.Iterable[tuple[str, str]]:
"""
Yield:
Expand All @@ -143,21 +254,58 @@ def iter_plugin_slots(mfe_name: str) -> t.Iterable[tuple[str, str]]:
yield from get_plugin_slots(mfe_name)


def iter_frontend_slots() -> t.Iterable[tuple[str, str]]:
"""
Yield:

(slot_name, plugin_config)
"""
yield from get_frontend_slots()


def is_mfe_enabled(mfe_name: str) -> bool:
return mfe_name in get_mfes()


def is_frontend_app(app_name: str) -> bool:
"""
Returns True if the given app_name corresponds to a configured frontend app.
"""
return app_name in get_frontend_apps()


def get_mfe(mfe_name: str) -> t.Union[MFE_ATTRS_TYPE, t.Any]:
return get_mfes().get(mfe_name, {})


def get_frontend_site(site_name: str) -> t.Union[FRONTEND_SITE_ATTRS_TYPE, t.Any]:
"""
Returns the attributes of a configured frontend site.
"""
return get_frontend_sites().get(site_name, {})


def get_frontend_app(app_name: str) -> t.Union[FRONTEND_APP_ATTRS_TYPE, t.Any]:
"""
Returns the attributes of a configured frontend app.
"""
return get_frontend_apps().get(app_name, {})


# Make the mfe functions available within templates
tutor_hooks.Filters.ENV_TEMPLATE_VARIABLES.add_items(
[
("get_mfe", get_mfe),
("iter_mfes", iter_mfes),
("iter_paths", iter_paths),
("iter_frontend_apps", iter_frontend_apps),
("iter_frontend_sites", iter_frontend_sites),
("get_frontend_site", get_frontend_site),
("get_frontend_app", get_frontend_app),
("iter_plugin_slots", iter_plugin_slots),
("iter_frontend_slots", iter_frontend_slots),
("is_mfe_enabled", is_mfe_enabled),
("is_frontend_app", is_frontend_app),
("MFEMountData", MFEMountData),
]
)
Expand Down
33 changes: 32 additions & 1 deletion tutormfe/templates/mfe/apps/mfe/Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,20 @@
redir @authoring /authoring/{re.authoring.1} permanent
{% endif %}

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

{%- if is_frontend_app(app_name) %}
@app_{{ app_name }} {
path /{{ app_name }} /{{ app_name }}/*
}
handle @app_{{ app_name }} {
uri strip_prefix /{{ app_name }}
root * /openedx/dist/site-{{ app.get("site", "default") }}
try_files /{path} /index.html
file_server
}
{%- else %}

@mfe_{{ app_name }} {
path /{{ app_name }} /{{ app_name }}/*
}
Expand All @@ -36,5 +49,23 @@
try_files /{path} /index.html
file_server
}
{%- endif %}
{% endfor %}

# Handle frontend sites, which are meant to serve only static assets, without an index.html.
# When fetched from the proper url handle they'll be returned with a index.html,
# but if someone tries to access them directly (like the chunk links or general assets)
# they'll get the files directy from this block
{% for site_name, app in iter_frontend_sites() %}
@site_{{ site_name }} {
path /site-{{ site_name }} /site-{{ site_name }}/*
}
handle @site_{{ site_name }} {
uri strip_prefix /site-{{ site_name }}
root * /openedx/dist/site-{{ site_name }}
# no index.html because we want to focus on assets in these ones
try_files /{path}
file_server
}
{% endfor %}
}
Loading