diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f2861fff..9aa6c482 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | + pip install git+https://github.com/overhangio/tutor.git@main pip install .[dev] - name: Test lint, types, and format run: make test diff --git a/README.rst b/README.rst index b2051016..cd58e99f 100644 --- a/README.rst +++ b/README.rst @@ -1010,6 +1010,74 @@ Then, in your Tutor plugin, install the package and register it via the site con This approach keeps your components, styles, and slot operations in a proper package with its own dependencies, tests, and build pipeline, rather than inlining everything through template patches. +Compat shim for FPF-built plugins +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some Tutor plugins ship slot contributions written against the legacy ``frontend-plugin-framework`` API, and many plugin packages still import from ``@edx/frontend-platform``. To make those work on a frontend-base site without requiring each plugin to be ported first, tutor-mfe renders one compatibility module per MFE (``env.config..jsx``) and wires each one into the site as its own shim App via `@openedx/frontend-base-compat `__. The shim translates legacy slot contributions into frontend-base slot operations, and ``site/package.json`` aliases both ``@openedx/frontend-plugin-framework`` and ``@edx/frontend-platform`` as direct dependencies pointing at the shim package, so any FPF- or frontend-platform-built dependency installed transitively also resolves to the shim's drop-in stubs and neither real package is installed. + +The shim does not translate ``PLUGIN_SLOTS`` contributions automatically: site operators or plugin authors choose which contributions to route through it, since not every legacy contribution survives the translation cleanly. The legacy per-MFE ``env.config.jsx`` continues to consume ``PLUGIN_SLOTS`` unchanged, so standalone legacy MFEs render the contribution exactly as before whether or not it is opted in to the shim. + +The coarsest opt-in is by plugin name: ``FRONTEND_COMPAT_PLUGINS`` takes a Tutor plugin name and folds every ``PLUGIN_SLOTS`` contribution registered in that plugin's hook context into the compat shim, regardless of which legacy MFE each contribution targeted. This is useful when wrapping a third-party plugin you don't control (a meta-plugin can opt the whole thing in without touching the upstream code), or when you'd rather not enumerate every slot in your own plugin: + +.. code-block:: python + + from tutormfe.hooks import FRONTEND_COMPAT_PLUGINS + + FRONTEND_COMPAT_PLUGINS.add_item("some-legacy-plugin") + +The tradeoff is bluntness: every slot that plugin contributes goes through the shim, including any whose translation isn't covered by the default maps, so failures surface at bundle time rather than as a clear "this slot isn't supported yet" signal. + +For finer control, ``FRONTEND_COMPAT_SLOTS`` opts in one ``(mfe_name, slot_name, plugin_config)`` triple at a time. Use this when you want explicit per-slot acknowledgement, when only some of a plugin's contributions translate cleanly, or when you want to ship the same contribution to legacy MFEs and the compat shim from a single source: + +.. code-block:: python + + from tutormfe.hooks import PLUGIN_SLOTS, FRONTEND_COMPAT_SLOTS + + MY_SLOT = ( + "course_outline_sidebar.v1", + """ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: 'my-widget', + type: DIRECT_PLUGIN, + RenderWidget: MyComponent, + } + } + """, + ) + + PLUGIN_SLOTS.add_item(("learning", *MY_SLOT)) + FRONTEND_COMPAT_SLOTS.add_item(("learning", *MY_SLOT)) + +``FRONTEND_COMPAT_SLOTS`` carries the same ``mfe_name`` field as ``PLUGIN_SLOTS``, so each contribution is routed to the per-MFE compat module rendered for that MFE and ends up in the legacy app the shim mounts for it. To target multiple MFEs from one source, register one triple per MFE. Once a plugin ships a native ``FRONTEND_SLOTS`` (or app-package) contribution for the same slot, drop the ``FRONTEND_COMPAT_SLOTS`` entry to avoid rendering the widget twice. + +All compat wiring is gated on at least one ``(mfe_name, slot_name, plugin_config)`` triple actually resolving through these filters: vanilla sites, and sites whose opt-ins resolve to no contributions (e.g., a named plugin that ships no ``PLUGIN_SLOTS``), skip the shim wiring entirely and pay no overhead. MFEs with no opted-in contributions get no compat module and no legacy app on the site. + +``FRONTEND_ROUTE_COMPAT_MAPS`` layers ``(mfe_id, route_roles)`` deltas on top of the shim's ``defaultRouteMap``. Use it to declare which frontend-base route roles a legacy MFE renders for, so the shim can scope a legacy app's contributions to the right routes. ``route_roles`` is a JSON-serializable list of role-id strings: + +.. code-block:: python + + from tutormfe.hooks import FRONTEND_ROUTE_COMPAT_MAPS + + FRONTEND_ROUTE_COMPAT_MAPS.add_items([ + ("my-legacy-mfe", ["org.openedx.frontend.role.myCustomRole"]), + ]) + +``FRONTEND_SLOT_COMPAT_MAPS`` and ``FRONTEND_WIDGET_COMPAT_MAPS`` follow the same shape but layer ``(legacy_id, mapping)`` deltas on top of the shim's built-in ``defaultSlotMap`` / ``defaultWidgetMap``. Use them when a plugin's ``PLUGIN_SLOTS`` contribution targets a legacy slot id that the shim doesn't yet cover, or to override a curated mapping site-locally. ``mapping`` is a JSON-serializable dict matching the shim's ``SlotMappingEntry`` / ``WidgetMappingEntry`` shape (see the shim's ADR for the full schema): + +.. code-block:: python + + from tutormfe.hooks import FRONTEND_SLOT_COMPAT_MAPS + + FRONTEND_SLOT_COMPAT_MAPS.add_items([ + ("org.openedx.frontend.aspects.course_outline_sidebar.v1", { + "targetSlotId": "org.openedx.frontend.slot.aspects.outlineSidebar.v1", + }), + ]) + +Identical contributions for the same legacy id are deduplicated silently. Divergent contributions emit a ``tutor config save`` warning and last-wins, so a transient conflict during a plugin upgrade still produces a working bundle. + Frontend-base site development ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/changelog.d/20260502_152826_adolfo_frontend_base_compat.md b/changelog.d/20260502_152826_adolfo_frontend_base_compat.md new file mode 100644 index 00000000..4887a1d7 --- /dev/null +++ b/changelog.d/20260502_152826_adolfo_frontend_base_compat.md @@ -0,0 +1 @@ +- [Feature] Wire FPF-built plugins into the frontend-base site via [@openedx/frontend-base-compat](https://github.com/openedx/frontend-base-compat). (by @arbrandes) diff --git a/tutormfe/hooks.py b/tutormfe/hooks.py index b2ead4f0..4f49a116 100644 --- a/tutormfe/hooks.py +++ b/tutormfe/hooks.py @@ -10,22 +10,36 @@ from tutor.core.hooks import Filter -# TODO(legacy-mfe-removal): MFE_ATTRS_TYPE, MFE_APPS, and PLUGIN_SLOTS all go -# away with the legacy MFE cleanup. See the central TODO block in plugin.py for -# the full list. -MFE_ATTRS_TYPE = t.Dict[t.Literal["repository", "port", "version"], t.Union["str", int]] +EXTERNAL_SCRIPTS: Filter[list[tuple[str, str]], []] = Filter() FRONTEND_APP_ATTRS_TYPE = t.Dict[ t.Literal["npm_package", "npm_version", "enabled", "source"], t.Union[str, bool], ] -MFE_APPS: Filter[dict[str, MFE_ATTRS_TYPE], []] = Filter() - FRONTEND_APPS: Filter[dict[str, FRONTEND_APP_ATTRS_TYPE], []] = Filter() -PLUGIN_SLOTS: Filter[list[tuple[str, str, str]], []] = Filter() +FRONTEND_SLOTS: Filter[list[str], []] = Filter() -EXTERNAL_SCRIPTS: Filter[list[tuple[str, str]], []] = Filter() +# TODO(fpf-removal): FRONTEND_COMPAT_PLUGINS, FRONTEND_COMPAT_SLOTS, +# FRONTEND_SLOT_COMPAT_MAPS, and FRONTEND_WIDGET_COMPAT_MAPS all feed the +# frontend-base compatibility shim and go away with env.config.compat.jsx +# itself. +FRONTEND_COMPAT_PLUGINS: Filter[list[str], []] = Filter() -FRONTEND_SLOTS: Filter[list[str], []] = Filter() +FRONTEND_COMPAT_SLOTS: Filter[list[tuple[str, str, str]], []] = Filter() + +FRONTEND_ROUTE_COMPAT_MAPS: Filter[list[tuple[str, list[str]]], []] = Filter() + +FRONTEND_SLOT_COMPAT_MAPS: Filter[list[tuple[str, dict[str, t.Any]]], []] = Filter() + +FRONTEND_WIDGET_COMPAT_MAPS: Filter[list[tuple[str, dict[str, t.Any]]], []] = Filter() + +# TODO(legacy-mfe-removal): MFE_ATTRS_TYPE, MFE_APPS, and PLUGIN_SLOTS all go +# away with the legacy MFE cleanup. See the central TODO block in plugin.py for +# the full list. +MFE_ATTRS_TYPE = t.Dict[t.Literal["repository", "port", "version"], t.Union["str", int]] + +MFE_APPS: Filter[dict[str, MFE_ATTRS_TYPE], []] = Filter() + +PLUGIN_SLOTS: Filter[list[tuple[str, str, str]], []] = Filter() diff --git a/tutormfe/plugin.py b/tutormfe/plugin.py index 52c64a83..202992fb 100644 --- a/tutormfe/plugin.py +++ b/tutormfe/plugin.py @@ -1,10 +1,12 @@ from __future__ import annotations import os +import re import typing as t from glob import glob import importlib_resources +from tutor import env as tutor_env from tutor import fmt from tutor import hooks as tutor_hooks from tutor.__about__ import __version_suffix__ @@ -18,7 +20,12 @@ EXTERNAL_SCRIPTS, FRONTEND_APP_ATTRS_TYPE, FRONTEND_APPS, + FRONTEND_COMPAT_PLUGINS, + FRONTEND_COMPAT_SLOTS, + FRONTEND_ROUTE_COMPAT_MAPS, + FRONTEND_SLOT_COMPAT_MAPS, FRONTEND_SLOTS, + FRONTEND_WIDGET_COMPAT_MAPS, MFE_APPS, MFE_ATTRS_TYPE, PLUGIN_SLOTS, @@ -211,6 +218,128 @@ def get_frontend_slots() -> list[str]: return FRONTEND_SLOTS.apply([]) +# TODO(fpf-removal): get_frontend_compat_slots, get_frontend_compat_mfes, +# and their iter_* aliases exist to render the per-MFE env.config..jsx +# files, which serve frontend-base sites running the frontend-base +# compatibility shim (@openedx/frontend-base-compat). When FPF deprecation +# completes and no plugin contributes via FRONTEND_COMPAT_SLOTS or +# FRONTEND_COMPAT_PLUGINS, these helpers and the compat template can be +# removed. +@tutor_hooks.lru_cache +def get_frontend_compat_slots(mfe_name: str) -> list[tuple[str, str]]: + """ + Return the (slot_name, plugin_config) pairs to render through the compat + shim for the given MFE. The wildcard "all" is itself a valid mfe_name + here: it returns just the wildcard entries, which land in + env.config.all.jsx and get mounted without an mfeId so they fire + globally. + + Sourced from two filters: + + - FRONTEND_COMPAT_PLUGINS: bulk opt-ins by plugin name. Every PLUGIN_SLOTS + contribution registered in that plugin's hook context is folded in. + - FRONTEND_COMPAT_SLOTS: per-slot opt-ins. The plugin author registers + each (mfe_name, slot_name, plugin_config) triple they want rendered + through the shim. + """ + slots: list[tuple[str, str]] = [] + + for plugin_name in FRONTEND_COMPAT_PLUGINS.iterate(): + context = tutor_hooks.Contexts.app(plugin_name).name + for plugin_mfe, slot_name, plugin_config in PLUGIN_SLOTS.iterate_from_context( + context + ): + if plugin_mfe == mfe_name: + slots.append((slot_name, plugin_config)) + + for compat_mfe, slot_name, plugin_config in FRONTEND_COMPAT_SLOTS.iterate(): + if compat_mfe == mfe_name: + slots.append((slot_name, plugin_config)) + + return slots + + +@tutor_hooks.lru_cache +def get_frontend_compat_mfes() -> list[str]: + """ + Return the sorted list of MFEs that should get a compat file: "all" stays + as a distinct entry here; the site mounts it without an mfeId so its + contributions fire globally. + """ + mfes: set[str] = set() + + for plugin_name in FRONTEND_COMPAT_PLUGINS.iterate(): + context = tutor_hooks.Contexts.app(plugin_name).name + for plugin_mfe, _slot_name, _plugin_config in PLUGIN_SLOTS.iterate_from_context( + context + ): + mfes.add(plugin_mfe) + + for compat_mfe, _slot_name, _plugin_config in FRONTEND_COMPAT_SLOTS.iterate(): + mfes.add(compat_mfe) + + return sorted(mfes) + + +def camelize_mfe_name(mfe_name: str) -> str: + """ + Convert a kebab/snake-case MFE name into a lowerCamelCase identifier + suitable as a prefix on JS export names (e.g. `EnvConfig`). + Examples: 'learner-dashboard' -> 'learnerDashboard', + 'ora_grading' -> 'oraGrading', 'lms' -> 'lms'. + """ + parts = [p for p in re.split(r"[-_]+", mfe_name) if p] + if not parts: + return mfe_name + return parts[0] + "".join(p[:1].upper() + p[1:] for p in parts[1:]) + + +@tutor_hooks.lru_cache +def get_frontend_slot_compat_map() -> dict[str, dict[str, t.Any]]: + return _merge_frontend_compat_maps(FRONTEND_SLOT_COMPAT_MAPS.iterate(), kind="slot") + + +@tutor_hooks.lru_cache +def get_frontend_widget_compat_map() -> dict[str, dict[str, t.Any]]: + return _merge_frontend_compat_maps( + FRONTEND_WIDGET_COMPAT_MAPS.iterate(), kind="widget" + ) + + +@tutor_hooks.lru_cache +def get_frontend_route_compat_map() -> dict[str, list[str]]: + return _merge_frontend_compat_maps( + FRONTEND_ROUTE_COMPAT_MAPS.iterate(), kind="route" + ) + + +def _merge_frontend_compat_maps( + maps: t.Iterable[tuple[str, t.Any]], + kind: str, +) -> dict[str, t.Any]: + """ + Identical contributions dedup silently. Divergent ones warn and + last-wins so a transient conflict during a plugin upgrade still + produces a working bundle. + """ + merged: dict[str, t.Any] = {} + for legacy_id, mapping in maps: + existing = merged.get(legacy_id) + if existing is None: + merged[legacy_id] = mapping + continue + if existing == mapping: + continue + fmt.echo_alert( + f"Conflicting compat {kind}-map extensions for {legacy_id!r}: " + f"{existing!r} overridden by {mapping!r}. " + "Last contribution wins; consider removing one of the plugins " + "or aligning their compat-map contributions." + ) + merged[legacy_id] = mapping + return merged + + # TODO(legacy-mfe-removal) def iter_mfes() -> t.Iterable[tuple[str, MFE_ATTRS_TYPE]]: """ @@ -244,6 +373,22 @@ def iter_plugin_slots(mfe_name: str) -> t.Iterable[tuple[str, str]]: yield from get_plugin_slots(mfe_name) +def iter_frontend_compat_slots(mfe_name: str) -> t.Iterable[tuple[str, str]]: + """ + Yield: + + (slot_name, plugin_config) + """ + yield from get_frontend_compat_slots(mfe_name) + + +def iter_frontend_compat_mfes() -> t.Iterable[str]: + """ + Yield each MFE name that has at least one compat-shim contribution. + """ + yield from get_frontend_compat_mfes() + + def iter_external_scripts(mfe_name: str) -> t.Iterable[str]: """ Yield: @@ -293,7 +438,15 @@ def get_frontend_app(app_name: str) -> t.Union[FRONTEND_APP_ATTRS_TYPE, t.Any]: ("get_frontend_app", get_frontend_app), ("get_frontend_apps", get_frontend_apps), ("iter_plugin_slots", iter_plugin_slots), + ("iter_frontend_compat_slots", iter_frontend_compat_slots), + ("iter_frontend_compat_mfes", iter_frontend_compat_mfes), ("iter_external_scripts", iter_external_scripts), + ("get_frontend_compat_slots", get_frontend_compat_slots), + ("get_frontend_compat_mfes", get_frontend_compat_mfes), + ("get_frontend_slot_compat_map", get_frontend_slot_compat_map), + ("get_frontend_widget_compat_map", get_frontend_widget_compat_map), + ("get_frontend_route_compat_map", get_frontend_route_compat_map), + ("camelize_mfe_name", camelize_mfe_name), ("iter_frontend_slots", iter_frontend_slots), ("is_mfe_enabled", is_mfe_enabled), ("is_frontend_app_enabled", is_frontend_app_enabled), @@ -554,3 +707,30 @@ def _check_mfe_host(config: Config) -> None: "This configuration is not typically recommended and may lead " "to unexpected behavior." ) + + +# TODO(fpf-removal): goes away with env.config.compat.jsx itself. +@tutor_hooks.Actions.ENV_SAVED.add() +def _render_per_mfe_compat_files(root: str, config: Config) -> None: + """ + Render one env.config..jsx into the site source tree per MFE that has + at least one opted-in compat slot, and reap any stale files left behind + when a plugin gets disabled. Tutor's normal template-tree rendering can't + fan one source file out into N rendered files, so we render it once per + MFE with current_mfe set as a Jinja global. + """ + mfes = get_frontend_compat_mfes() + out_dir = os.path.join(root, "plugins", "mfe", "build", "mfe", "site", "src") + if mfes: + renderer = tutor_env.Renderer(config) + for mfe_name in mfes: + renderer.environment.globals["current_mfe"] = mfe_name + rendered = renderer.render_template("mfe/partials/env.config.compat.jsx") + tutor_env.write_to( + rendered, + os.path.join(out_dir, f"env.config.{mfe_name}.jsx"), + ) + valid = {f"env.config.{mfe_name}.jsx" for mfe_name in mfes} + for path in glob(os.path.join(out_dir, "env.config.*.jsx")): + if os.path.basename(path) not in valid: + os.remove(path) diff --git a/tutormfe/templates/mfe/build/mfe/site/package.json b/tutormfe/templates/mfe/build/mfe/site/package.json index 2fb4fdf5..543c9052 100644 --- a/tutormfe/templates/mfe/build/mfe/site/package.json +++ b/tutormfe/templates/mfe/build/mfe/site/package.json @@ -33,8 +33,19 @@ {%- for app_name, app in iter_frontend_apps() if app.get("npm_package") %} "{{ app['npm_package'] }}": "{{ app['npm_version'] }}", {%- endfor %} +{%- if get_frontend_compat_mfes() %} + "@openedx/frontend-base-compat": "^1.0.0-alpha || 0.0.0-dev", + "@edx/frontend-platform": "npm:@openedx/frontend-base-compat@^1.0.0-alpha || 0.0.0-dev", + "@openedx/frontend-plugin-framework": "npm:@openedx/frontend-base-compat@^1.0.0-alpha || 0.0.0-dev", +{%- endif %} "@openedx/frontend-base": "^1.0.0-alpha || 0.0.0-dev" }, +{%- if get_frontend_compat_mfes() %} + "overrides": { + "@edx/frontend-platform": "npm:@openedx/frontend-base-compat@^1.0.0-alpha || 0.0.0-dev", + "@openedx/frontend-plugin-framework": "npm:@openedx/frontend-base-compat@^1.0.0-alpha || 0.0.0-dev" + }, +{%- endif %} "devDependencies": { "@edx/browserslist-config": "latest", "turbo": "^2.8.16" diff --git a/tutormfe/templates/mfe/build/mfe/site/site.config.build.tsx b/tutormfe/templates/mfe/build/mfe/site/site.config.build.tsx index 9720eae3..7bcf7626 100644 --- a/tutormfe/templates/mfe/build/mfe/site/site.config.build.tsx +++ b/tutormfe/templates/mfe/build/mfe/site/site.config.build.tsx @@ -18,6 +18,22 @@ import { instructorDashboardApp } from '@openedx/frontend-app-instructor-dashboa import { notificationsApp } from '@openedx/frontend-app-notifications'; {% endif %} +{% if get_frontend_compat_mfes() %} +import { + createLegacyPluginApp, + defaultRouteMap, + defaultSlotMap, + defaultWidgetMap, +} from '@openedx/frontend-base-compat'; +{%- for mfe in iter_frontend_compat_mfes() %} +import {{ camelize_mfe_name(mfe) }}EnvConfig from './src/env.config.{{ mfe }}.jsx'; +{%- endfor %} + +const routeMap = { ...defaultRouteMap, ...{{ get_frontend_route_compat_map() | tojson }} }; +const slotMap = { ...defaultSlotMap, ...{{ get_frontend_slot_compat_map() | tojson }} }; +const widgetMap = { ...defaultWidgetMap, ...{{ get_frontend_widget_compat_map() | tojson }} }; +{% endif %} + {{ patch("mfe-site-config-imports") }} {{ patch("mfe-site-config-imports-production") }} @@ -62,6 +78,19 @@ addApp(siteConfig, instructorDashboardApp); addApp(siteConfig, notificationsApp); {% endif %} +{%- for mfe in iter_frontend_compat_mfes() %} +addApp(siteConfig, createLegacyPluginApp({ + appId: 'io.edly.frontend.app.compat.{{ camelize_mfe_name(mfe) }}', + envConfig: {{ camelize_mfe_name(mfe) }}EnvConfig, + {%- if mfe != "all" %} + mfeId: '{{ mfe }}', + {%- endif %} + routeMap, + slotMap, + widgetMap, +})); +{%- endfor %} + {{ patch("mfe-site-config") }} {{ patch("mfe-site-config-production") }} diff --git a/tutormfe/templates/mfe/build/mfe/site/site.config.dev.tsx b/tutormfe/templates/mfe/build/mfe/site/site.config.dev.tsx index 47fee0ff..2880d8fa 100644 --- a/tutormfe/templates/mfe/build/mfe/site/site.config.dev.tsx +++ b/tutormfe/templates/mfe/build/mfe/site/site.config.dev.tsx @@ -18,6 +18,22 @@ import { instructorDashboardApp } from '@openedx/frontend-app-instructor-dashboa import { notificationsApp } from '@openedx/frontend-app-notifications'; {% endif %} +{% if get_frontend_compat_mfes() %} +import { + createLegacyPluginApp, + defaultRouteMap, + defaultSlotMap, + defaultWidgetMap, +} from '@openedx/frontend-base-compat'; +{%- for mfe in iter_frontend_compat_mfes() %} +import {{ camelize_mfe_name(mfe) }}EnvConfig from './src/env.config.{{ mfe }}.jsx'; +{%- endfor %} + +const routeMap = { ...defaultRouteMap, ...{{ get_frontend_route_compat_map() | tojson }} }; +const slotMap = { ...defaultSlotMap, ...{{ get_frontend_slot_compat_map() | tojson }} }; +const widgetMap = { ...defaultWidgetMap, ...{{ get_frontend_widget_compat_map() | tojson }} }; +{% endif %} + {{ patch("mfe-site-config-imports") }} {{ patch("mfe-site-config-imports-development") }} @@ -62,6 +78,19 @@ addApp(siteConfig, instructorDashboardApp); addApp(siteConfig, notificationsApp); {% endif %} +{%- for mfe in iter_frontend_compat_mfes() %} +addApp(siteConfig, createLegacyPluginApp({ + appId: 'io.edly.frontend.app.compat.{{ camelize_mfe_name(mfe) }}', + envConfig: {{ camelize_mfe_name(mfe) }}EnvConfig, + {%- if mfe != "all" %} + mfeId: '{{ mfe }}', + {%- endif %} + routeMap, + slotMap, + widgetMap, +})); +{%- endfor %} + {{ patch("mfe-site-config") }} {{ patch("mfe-site-config-development") }} diff --git a/tutormfe/templates/mfe/partials/env.config.compat.jsx b/tutormfe/templates/mfe/partials/env.config.compat.jsx new file mode 100644 index 00000000..598110df --- /dev/null +++ b/tutormfe/templates/mfe/partials/env.config.compat.jsx @@ -0,0 +1,39 @@ +/* + * Generated by tutor-mfe for {{ current_mfe }}. + * + * Frontend-base counterpart of the legacy env.config.jsx. + */ + +{{ patch("mfe-env-config-buildtime-imports") }} + +import { DIRECT_PLUGIN, IFRAME_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; + +function addPlugins(config, slot_name, plugins) { + if (slot_name in config.pluginSlots === false) { + config.pluginSlots[slot_name] = { + keepDefault: true, + plugins: [] + }; + } + config.pluginSlots[slot_name].plugins.push(...plugins); +} + +{{- patch("mfe-env-config-buildtime-definitions") }} + +function setConfig() { + const config = { + pluginSlots: {}, + }; + + {{- patch("mfe-env-config-runtime-definitions") }} + + {%- for slot_name, plugin_config in iter_frontend_compat_slots(current_mfe) %} + addPlugins(config, '{{ slot_name }}', [{{ plugin_config }}]); + {%- endfor %} + + {{- patch("mfe-env-config-runtime-final") }} + + return config; +} + +export default setConfig;