{"id":20037751,"url":"https://github.com/openedx/frontend-plugin-framework","last_synced_at":"2025-05-05T06:31:20.408Z","repository":{"id":206097472,"uuid":"713573504","full_name":"openedx/frontend-plugin-framework","owner":"openedx","description":"An experimental framework for micro-frontend plugins.","archived":false,"fork":false,"pushed_at":"2024-10-31T03:02:57.000Z","size":892,"stargazers_count":6,"open_issues_count":23,"forks_count":11,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-10-31T04:16:57.017Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/openedx.png","metadata":{"files":{"readme":"README.rst","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-02T19:50:48.000Z","updated_at":"2024-10-04T11:30:31.000Z","dependencies_parsed_at":"2023-12-18T10:26:27.774Z","dependency_job_id":"deba2391-69f0-4fe6-bc89-967da258ef6c","html_url":"https://github.com/openedx/frontend-plugin-framework","commit_stats":null,"previous_names":["openedx/frontend-plugin-framework"],"tags_count":14,"template":false,"template_full_name":"openedx/frontend-template-application","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openedx%2Ffrontend-plugin-framework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openedx%2Ffrontend-plugin-framework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openedx%2Ffrontend-plugin-framework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openedx%2Ffrontend-plugin-framework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/openedx","download_url":"https://codeload.github.com/openedx/frontend-plugin-framework/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224428785,"owners_count":17309585,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-13T10:21:49.751Z","updated_at":"2025-05-05T06:31:20.400Z","avatar_url":"https://github.com/openedx.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"Frontend Plugin Framework\n##########################\n\n|license-badge| |status-badge| |ci-badge| |codecov-badge|\n\n.. |license-badge| image:: https://img.shields.io/github/license/openedx/frontend-plugin-framework.svg\n    :target: https://github.com/openedx/frontend-plugin-framework/blob/master/LICENSE\n    :alt: License\n\n.. |status-badge| image:: https://img.shields.io/badge/Status-Maintained-brightgreen\n\n.. |ci-badge| image:: https://github.com/openedx/frontend-plugin-framework/actions/workflows/ci.yml/badge.svg\n    :target: https://github.com/openedx/frontend-plugin-framework/actions/workflows/ci.yml\n    :alt: Continuous Integration\n\n.. |codecov-badge| image:: https://codecov.io/github/openedx/frontend-plugin-framework/coverage.svg?branch=master\n    :target: https://codecov.io/github/openedx/frontend-plugin-framework?branch=master\n    :alt: Codecov\n\nPurpose\n=======\n\nThe Frontend Plugin Framework is designed to be an extension point to customize an Open edX MFE. This framework supports two types of plugins: iFrame-based and \"Direct\" plugins.\n\nA Direct plugin allows for a component in the Host MFE -- or a React dependency -- to be made into a plugin and inserted in a plugin slot within the Host MFE.\n\nThe iFrame-based Plugin allows for a component that lives in another MFE (the Child MFE) to be plugged into a slot in\nthe Host MFE.\n\nThe primary way this is made possible is through JS-based configurations, where the changes to a plugin slot are defined\n(see 'Plugin Operations').\n\nGetting Started\n===============\nUsing the Example Apps\n----------------------\n\n1. Run ``make requirements`` in the root directory.\n\n2. Run ``npm run start`` in the root directory.\n\n3. Open another terminal and run ``npm run start:example`` to start the example app. You can visit http://localhost:8080 to see the example app.\n\n4. Make change to the existing code, everything should be hot reloaded.\n\nAdd Library Dependency\n----------------------\n\nAdd ``openedx/frontend-plugin-framework`` to the ``package.json`` of both Host and Child MFEs.\n\nHost Micro-frontend (MFE)\n-------------------------\n\nHost MFEs define ``PluginSlot`` components in areas of the UI where they intend to accept plugin extensions.\nThe Host MFE, and thus the maintainers of the Host MFE, are responsible for deciding where it is acceptable to add a\nplugin slot.\n\nThe slot also determines the dimensions and responsiveness of each plugin, and supports passing any additional\ndata to the plugin as part of its contract.\n\n  .. code-block::\n\n    \u003cHostApp\u003e\n      \u003cRoute path=\"/page1\"\u003e\n        \u003cSomeHostContent /\u003e\n        \u003cPluginSlot\n          id=\"sidebar\" // this `id` is referenced in the JS-based config\n          pluginProps={{ // these props are passed along to each plugin\n            className: 'flex-grow-1',\n            title: 'example plugins',\n          }}\n          style={{\n            height: 700,\n          }}\n        \u003e\n          \u003cSideBar\n            propExampleA: 'edX Sidebar',\n            propExampleB: SomeIcon,\n          \u003e\n        \u003c/PluginSlot \u003e\n      \u003c/Route\u003e\n      \u003cRoute path=\"/page2\"\u003e\n        \u003cOtherRouteContent /\u003e\n      \u003c/Route\u003e\n    \u003c/HostApp\u003e\n\nHost MFE JS-based Configuration\n-------------------------------\n\nMicro-frontends that would like to use the Plugin Framework need to use a JavaScript-based config named ``env.config``\nwith either ``.js`` or ``.jsx`` as the extension. Technically, only the Host MFE requires an ``env.config.js`` file\nas that is where the plugin slot's configuration is defined.\n\nHowever, note that any Child MFE can theoretically contain one or more ``PluginSlot`` components themselves,\nthereby making it both a Child MFE and a Host MFE. In this instance, the Child MFE would need its own ``env.config.js``\nfile as well to define its plugin slots.\n\n  .. code-block::\n\n    // env.config.js\n\n    import { DIRECT_PLUGIN, IFRAME_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';\n\n    // import any additional dependencies or functions to be used for each plugin operation\n    import Sidebar from './widgets/social/Sidebar';\n    import SocialMediaLink from './widgets/social/SocialMediaLink';\n    import { wrapSidebar, modifySidebar } from './widgets/social/utils';\n    import { SomeIcon } from '@openedx/paragon/icons';\n\n    const config = {\n      // additional environment variables\n      pluginSlots: {\n        sidebar: { // plugin slot id\n          keepDefault: true,\n          plugins: [\n            {\n              op: PLUGIN_OPERATIONS.Insert,\n              widget: {\n                id: 'social_media_link',\n                type: DIRECT_PLUGIN,\n                priority: 10,\n                RenderWidget: SocialMediaLink,\n              },\n            },\n            {\n              op: PLUGIN_OPERATIONS.Wrap,\n              widgetId: 'default_contents',\n              wrapper: wrapSidebar,\n            },\n            {\n              op: PLUGIN_OPERATIONS.Modify,\n              widgetId: 'social_media_link',\n              fn: modifySidebar,\n            },\n          ]\n        }\n      }\n    }\n\n    export default config;\n\nFor more information on how JS based configuration works, see:\n\n* `config.js`_ file in Frontend Platform\n* Frontend Build ADR on `JavaScript-based environment configuration`_\n* Frontend Platform ADR to `Promote JavaScript file configuration and deprecate environment variable configuration`_\n\n.. _config.js: https://github.com/openedx/frontend-platform/blob/master/src/config.js\n.. _JavaScript-based environment configuration: https://github.com/openedx/frontend-platform/blob/master/docs/decisions/0007-javascript-file-configuration.rst\n.. _Promote JavaScript file configuration and deprecate environment variable configuration: https://github.com/openedx/frontend-platform/blob/master/docs/decisions/0007-javascript-file-configuration.rst\n\nPriority\n````````\n\nThe priority property determines where the widgets should be placed based on a 1-100 scale. A widget with a priority of 10\nwill appear above a widget with a priority of 20.\n\nDefault Content\n```````````````\n\nThe component that is wrapped by a Plugin Slot is referred to as the \"default content\". In order to render this content,\nthe ``keepDefault`` boolean in the slot should be set to ``true``. For organizations who aren't using the Plugin Slot\n(and therefore aren't defining a slot via JS config), ``keepDefault`` will default to ``true``, thus ensuring that the developer\nexperience isn't affected; the only change to note is that the component is now wrapped in a Plugin Slot.\n\nIf you need to use a plugin operation (e.g. Wrap, Hide, Modify) on default content, the ``widgetId`` you would use to refer to the content is ``defaults_contents``.\n\nNote: The default content will have a priority of 50, allowing for any plugins to appear before or after the default content.\n\nPlugin Operations\n`````````````````\n\nThere are four plugin operations that each require specific properties.\n\nInsert a Direct Plugin\n''''''''''''''''''''''\n\nThe Insert operation will add a widget in the plugin slot. The contents required for a Direct Plugin is the same as\nis demonstrated in the Default Contents section above, with the ``content`` key being optional.\n\n  .. code-block::\n\n    /*\n      * {String} op - Name of plugin operation\n      * {Object} widget - The component to be inserted into the slot\n    */\n\n    {\n      op: PLUGIN_OPERATIONS.Insert,\n      widget: {\n        id: 'social_media_link',\n        type: DIRECT_PLUGIN,\n        priority: 10,\n        RenderWidget: SocialMediaLink,\n      }\n    }\n\nInsert an iFrame Plugin\n'''''''''''''''''''''''\n\nThe Insert operation will add a widget in the plugin slot. The contents required for an iFrame Plugin is the same as\nis demonstrated in the Default Contents section above.\n\n  .. code-block::\n\n    /*\n      * {String} op - Name of plugin operation\n      * {Object} widget - The component to be inserted into the slot\n    */\n\n    {\n      op: PLUGIN_OPERATIONS.Insert,\n      widget: {\n        id: 'enterprise_navbar',\n        type: IFRAME_PLUGIN,\n        priority: 30,\n        url: 'http://{child_mfe_url}/plugin_iframe',\n        title: 'Login with XYZ',\n      }\n    }\n\nModify\n''''''\n\nThe Modify operation allows us to modify the contents of a widget, including its id, type, content, RenderWidget function,\nor its priority. The operation requires the id of the widget that will be modified and a function to make those changes.\n\n  .. code-block::\n\n    const modifyWidget = (widget) =\u003e {\n      widget.content = {\n        propExampleA: 'University XYZ Sidebar',\n        propExampleB: SomeOtherIcon,\n      };\n      return widget;\n    };\n\n    /*\n      * {String} op - Name of plugin operation\n      * {String} widgetId - The widget id needed for referencing when using Modify/Wrap/Hide\n      * {Function} fn - The function to call that can modify the widget's contents and properties\n    */\n\n    {\n      op: PLUGIN_OPERATIONS.Modify,\n      widgetId: 'sidebar_plugin',\n      fn: modifyWidget,\n    }\n\nWrap\n''''\n\nUnlike Modify, the Wrap operation adds a React component around the widget, and a single widget can receive more than\none wrap operation. Each wrapper function takes in a ``component`` and ``id`` prop.\n\n  .. code-block::\n\n    const wrapWidget = ({ component, idx }) =\u003e (\n      \u003cdiv className=\"bg-warning\" data-testid={`wrapper${idx + 1}`} key={idx}\u003e\n        \u003cp\u003eThis is a wrapper component that is placed around the widget.\u003c/p\u003e\n        {component}\n        \u003cp\u003eWith this wrapper, you can add anything before or after the widget.\u003c/p\u003e\n      \u003c/div\u003e\n    );\n\n    /*\n      * {String} op - Name of plugin operation\n      * {String} widgetId - The widget id needed for referencing when using Modify/Wrap/Hide\n      * {Function} wrapper - The function to call that can wrap the widget with a React component\n    */\n\n    {\n      op: PLUGIN_OPERATIONS.Wrap,\n      widgetId: 'default_content_in_slot',\n      wrapper: wrapWidget,\n    }\n\nHide\n''''\n\nThe Hide operation will simply hide whatever content is desired. This is generally used for the default content.\n\n  .. code-block::\n\n    /*\n      * {String} op - Name of plugin operation\n      * {String} widgetId - The widget id needed for referencing when using Modify/Wrap/Hide\n    */\n\n    {\n      op: PLUGIN_OPERATIONS.Hide,\n      widgetId: 'some_undesired_plugin',\n    }\n\nUsing a Child Micro-frontend (MFE) for iFrame-based Plugins\n-----------------------------------------------------------\n\nThe Child MFE is no different than any other MFE except that it can define a `Plugin` component that can then be pass into the Host MFE\nas an iFrame-based plugin via a route.\nThis component communicates (via ``postMessage``) with the Host MFE and resizes its content to match the dimensions\navailable in the Host's plugin slot.\n\nFallback Behavior\n-----------------\n\nSetting a Fallback component\n````````````````````````````\nThe two main places to configure a fallback component for a given implementation are in the PluginSlot props and in the JS configuration. The JS configuration fallback will be prioritized over the PluginSlot props fallback.\n\nPluginSlot props\n````````````````\nThis is ideally used when the same fallback should be applied to all of the plugins in the `PluginSlot`. To configure, set the `slotErrorFallbackComponent` prop in the `PluginSlot` to a React component. This will replace the default `\u003cErrorPage /\u003e` component from frontend-platform.\n\n  .. code-block::\n    \u003cPluginSlot\n      id='my-plugin-slot'\n      slotErrorFallbackComponent={\u003cMyCustomFallbackComponent /\u003e}\n    /\u003e\n\nJS configuration\n````````````````\nCan be used when setting a fallback for a specific plugin within a slot. Set the `errorFallbackComponent` field for the specific plugin to the custom fallback component in the JS configuration. This will be prioritized over any other fallback components.\n\n  .. code-block::\n    const config = {\n      pluginSlots: {\n        my_plugin_slot: {\n          keepDefault: false,\n          plugins: [\n            {\n              op: PLUGIN_OPERATIONS.Insert,\n              widget: {\n                id: 'this_is_a_plugin',\n                type: DIRECT_PLUGIN,\n                priority: 60,\n                RenderWidget: ReactPluginComponent,\n                errorFallbackComponent: MyCustomFallbackComponent,\n              },\n            },\n          ],\n        },\n      },\n    };\n\niFrame-based Plugins\n''''''''''''''''''''\nIt's notoriously difficult to know in the Host MFE when an iFrame has failed to load.\nBecause of security sandboxing, the host isn't allowed to know the HTTP status of the request or to inspect what was\nloaded, so we have to rely on waiting for a ``postMessage`` event from within the iFrame to know it has successfully loaded.\nA fallback component can be provided to the Plugin that is wrapped around the component, as noted below.\nOtherwise, the `default Error fallback from Frontend Platform`_ would be used.\n\n  .. code-block::\n\n    \u003cMyMFE\u003e\n      \u003cRoute path=\"/mainContent\"\u003e\n          \u003cMyMainContent /\u003e\n      \u003c/Route\u003e\n      \u003cRoute path=\"/plugin1\"\u003e\n        \u003cPlugin fallbackComponent={\u003cOtherFallback /\u003e}\u003e\n          \u003cMyCustomContent /\u003e\n        \u003c/Plugin\u003e\n      \u003c/Route\u003e\n    \u003c/MyMFE\u003e\n\n.. _default Error fallback from Frontend Platform: https://github.com/openedx/frontend-platform/blob/master/src/react/ErrorBoundary.jsx\n\nKnown Issues\n============\n\nDevelopment Roadmap\n===================\n\nThe main priority in developing this library is to extract components from a Host MFE to:\n\n#. allow for teams to develop experimental features without impeding on any other team's work or the core functionality of the Host MFE.\n#. allow for customizing/extending the functionality of a Host MFE without having org-specific functionality in an open-source project.\n\nGetting Help\n============\n\nIf you're having trouble, we have discussion forums at\nhttps://discuss.openedx.org where you can connect with others in the community.\n\nOur real-time conversations are on Slack. You can request a `Slack\ninvitation`_, then join our `community Slack workspace`_.  Because this is a\nfrontend repository, the best place to discuss it would be in the `#wg-frontend\nchannel`_.\n\nFor anything non-trivial, the best path is to open an issue in this repository\nwith as many details about the issue you are facing as you can provide.\n\nhttps://github.com/openedx/frontend-plugin-framework/issues\n\nFor more information about these options, see the `Getting Help`_ page.\n\n.. _Slack invitation: https://openedx.org/slack\n.. _community Slack workspace: https://openedx.slack.com/\n.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6\n.. _Getting Help: https://openedx.org/getting-help\n\nLicense\n=======\n\nThe code in this repository is licensed under the AGPLv3 unless otherwise\nnoted.\n\nPlease see `LICENSE \u003cLICENSE\u003e`_ for details.\n\nContributing\n============\n\nContributions are very welcome.  Please read `How To Contribute`_ for details.\n\n.. _How To Contribute: https://openedx.org/r/how-to-contribute\n\nThis project is currently accepting all types of contributions, bug fixes,\nsecurity fixes, maintenance work, or new features.  However, please make sure\nto have a discussion about your new feature idea with the maintainers prior to\nbeginning development to maximize the chances of your change being accepted.\nYou can start a conversation by creating a new issue on this repo summarizing\nyour idea.\n\nThe Open edX Code of Conduct\n============================\n\nAll community members are expected to follow the `Open edX Code of Conduct`_.\n\n.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/\n\nPeople\n======\n\nThe assigned maintainers for this component and other project details may be\nfound in `Backstage`_. Backstage pulls this data from the ``catalog-info.yaml``\nfile in this repo.\n\n.. _Backstage: https://open-edx-backstage.herokuapp.com/catalog/default/component/frontend-plugin-framework\n\nReporting Security Issues\n=========================\n\nPlease do not report security issues in public.  Email security@openedx.org instead.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenedx%2Ffrontend-plugin-framework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopenedx%2Ffrontend-plugin-framework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenedx%2Ffrontend-plugin-framework/lists"}