Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
76 changes: 76 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,82 @@ For instance:
Refer to the `patch catalog <#template-patch-catalog>`_ below for more details.


Configuring External Scripts
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

External scripts are a frontend-platform feature that allows script loaders to be configured via ``env.config.jsx``. A loader is a JavaScript class with a ``constructor({ config })`` and a ``loadScript()`` method. This plugin provides the ``EXTERNAL_SCRIPTS`` hook so that Tutor plugins can register loaders for MFEs without resorting to patches.

The hook works similarly to ``PLUGIN_SLOTS``. Each item is a tuple of ``(mfe_name, loader_class)``, where ``mfe_name`` is either ``"all"`` (to apply to every MFE) or the name of a specific MFE, and ``loader_class`` is the name of a loader class that will be added to the ``externalScripts`` config array. Frontend-platform instantiates the class at runtime and passes the MFE's runtime config to its constructor.

For instance, to load a `CookieYes <https://www.cookieyes.com/>`_ consent banner across all MFEs, define a loader directly in ``env.config.jsx``:

.. code-block:: python

from tutormfe.hooks import EXTERNAL_SCRIPTS
from tutor import hooks

hooks.Filters.ENV_PATCHES.add_item(
(
"mfe-env-config-buildtime-definitions",
"""
class CookieYesLoader {
constructor() {}

loadScript() {
const script = document.createElement('script');
script.id = 'cookieyes';
script.src = 'https://cdn-cookieyes.com/client_data/YOUR_SITE_ID/script.js';
document.head.appendChild(script);
}
}
""",
)
)

EXTERNAL_SCRIPTS.add_items([
(
"all",
"CookieYesLoader",
),
])

The ``CookieYesLoader`` class is defined via the ``mfe-env-config-buildtime-definitions`` patch, and the ``EXTERNAL_SCRIPTS`` hook wires it into the configuration. In this case, the CookieYes site ID is hardcoded directly in the loader. If you need to read a value from the MFE runtime configuration instead, accept ``{ config }`` in the constructor and reference the appropriate key - the built-in ``GoogleAnalyticsLoader`` in ``@openedx/frontend-platform/scripts`` does this with ``config.GOOGLE_ANALYTICS_4_ID``, for example. You can import it with the ``mfe-env-config-buildtime-imports`` patch and use it with ``EXTERNAL_SCRIPTS`` in the same way.

You can also target a specific MFE. For example, to load a custom script only on the learning MFE:

.. code-block:: python

from tutormfe.hooks import EXTERNAL_SCRIPTS
from tutor import hooks

hooks.Filters.ENV_PATCHES.add_item(
(
"mfe-dockerfile-post-npm-install",
"""
RUN npm install @myorg/custom-script-loader
""",
)
)

hooks.Filters.ENV_PATCHES.add_item(
(
"mfe-env-config-buildtime-imports",
"""
import { CustomScriptLoader } from '@myorg/custom-script-loader';
""",
)
)

EXTERNAL_SCRIPTS.add_items([
(
"learning",
"CustomScriptLoader",
),
])

Note that if no external scripts are configured, the ``externalScripts`` key is not set in the config at all, so any MFE-level defaults are preserved.


Hosting extra static files
~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
1 change: 1 addition & 0 deletions changelog.d/20260415_external_scripts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- [Feature] Add `EXTERNAL_SCRIPTS` hook for configuring MFE external scripts via `env.config.jsx`. (by @arbrandes)
2 changes: 2 additions & 0 deletions tutormfe/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
MFE_APPS: Filter[dict[str, MFE_ATTRS_TYPE], []] = Filter()

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

EXTERNAL_SCRIPTS: Filter[list[tuple[str, str]], []] = Filter()
20 changes: 19 additions & 1 deletion tutormfe/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from tutor.types import Config, get_typed

from .__about__ import __version__
from .hooks import MFE_APPS, MFE_ATTRS_TYPE, PLUGIN_SLOTS
from .hooks import EXTERNAL_SCRIPTS, MFE_APPS, MFE_ATTRS_TYPE, PLUGIN_SLOTS

# Handle version suffix in main mode, just like tutor core
if __version_suffix__:
Expand Down Expand Up @@ -125,6 +125,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_external_scripts(mfe_name: str) -> list[str]:
"""
This function is cached for performance.
"""
return [i[-1] for i in EXTERNAL_SCRIPTS.iterate() if i[0] == mfe_name]


def iter_mfes() -> t.Iterable[tuple[str, MFE_ATTRS_TYPE]]:
"""
Yield:
Expand All @@ -143,6 +151,15 @@ def iter_plugin_slots(mfe_name: str) -> t.Iterable[tuple[str, str]]:
yield from get_plugin_slots(mfe_name)


def iter_external_scripts(mfe_name: str) -> t.Iterable[str]:
"""
Yield:

(script_config)
"""
yield from get_external_scripts(mfe_name)


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

Expand All @@ -157,6 +174,7 @@ def get_mfe(mfe_name: str) -> t.Union[MFE_ATTRS_TYPE, t.Any]:
("get_mfe", get_mfe),
("iter_mfes", iter_mfes),
("iter_plugin_slots", iter_plugin_slots),
("iter_external_scripts", iter_external_scripts),
("is_mfe_enabled", is_mfe_enabled),
("MFEMountData", MFEMountData),
]
Expand Down
20 changes: 20 additions & 0 deletions tutormfe/templates/mfe/build/mfe/env.config.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ function addPlugins(config, slot_name, plugins) {
config.pluginSlots[slot_name].plugins.push(...plugins);
}

function addExternalScripts(config, scripts) {
if (!config.externalScripts) {
config.externalScripts = [];
}

config.externalScripts.push(...scripts);
}

{{- patch("mfe-env-config-buildtime-definitions") }}

async function setConfig () {
Expand Down Expand Up @@ -45,6 +53,18 @@ async function setConfig () {
{{- patch("mfe-env-config-runtime-final") }}
} catch (err) { console.error("env.config.jsx failed to apply: ", err);}

{%- for script_config in iter_external_scripts("all") %}
addExternalScripts(config, [{{ script_config }}]);
{%- endfor %}

{%- for app_name, _ in iter_mfes() %}
if (process.env.APP_ID == '{{ app_name }}') {
{%- for script_config in iter_external_scripts(app_name) %}
addExternalScripts(config, [{{ script_config }}]);
{%- endfor %}
}
{%- endfor %}

return config;
}

Expand Down
Loading