Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds support for loading external theme CSS for MFEs #440

Closed
wants to merge 59 commits into from
Closed
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
3cde02d
feat: add ability to dynamically load theme overrides
viktorrusakov Feb 2, 2023
f41dcb2
feat: adds support for loading external theme CSS for MFEs
adamstankiewicz Feb 15, 2023
2b19931
fix: handle missing theme config
adamstankiewicz Feb 15, 2023
347957b
fix: add env vars to env files
adamstankiewicz Feb 15, 2023
ef52e41
fix: remove unused code
adamstankiewicz Feb 15, 2023
2849bfd
chore: wip commit
adamstankiewicz Feb 24, 2023
a298bf5
fix: prefer runtime config for paragon theme
adamstankiewicz Feb 27, 2023
697e43b
fix: rebase on master and resolve package conflicts
adamstankiewicz Feb 27, 2023
fbfd722
fix: revert to original webpack.dev.config config
adamstankiewicz Feb 27, 2023
a2b3f99
chore: clean up unnecessary comment line
adamstankiewicz Feb 27, 2023
b477c8d
fix: grammar
adamstankiewicz May 20, 2023
916c1b3
Merge branch 'master' into ags/inject-theme-css
adamstankiewicz May 20, 2023
4b2038c
fix: remove duplicate import
adamstankiewicz May 20, 2023
26fbfac
chore: refresh package-lock.json to lockfileVersion 3
adamstankiewicz May 20, 2023
b259760
fix: clean up link nodes in document head when no longer needed
adamstankiewicz May 20, 2023
155bf03
fix: fallback to locally installed core and light theme css urls
adamstankiewicz May 27, 2023
e5ee81e
docs: update to docs
adamstankiewicz May 27, 2023
813169d
chore: update docs about
adamstankiewicz May 27, 2023
a714d49
fix: expose setThemeVariant
adamstankiewicz May 27, 2023
1e13ac3
Merge branch 'master' into ags/inject-theme-css
adamstankiewicz May 27, 2023
e2b0df9
docs: update docs
adamstankiewicz May 27, 2023
ea99e3c
fix: rebase with master and update based on PARAGON changes
adamstankiewicz May 28, 2023
00fd9c2
chore: remove support for env vars config for paragon dynamic theming
adamstankiewicz May 29, 2023
0da2cfa
Merge branch 'master' into ags/inject-theme-css
adamstankiewicz May 29, 2023
f24c336
chore: clean up package-lock
adamstankiewicz May 29, 2023
84b34ee
fix: updates
adamstankiewicz May 29, 2023
f9aa947
fix: one more update
adamstankiewicz May 29, 2023
fb70be9
fix: refresh package-lock.json
adamstankiewicz May 29, 2023
e0768ff
fix: refresh package-lock.json pt2
adamstankiewicz May 29, 2023
4c5f358
fix: updates
adamstankiewicz May 29, 2023
c957eb5
fix: update package-lock.json
adamstankiewicz May 29, 2023
2c386ac
fix: make it theme variant agnostic
adamstankiewicz May 31, 2023
340e259
docs: update howto theming guide
adamstankiewicz May 31, 2023
9b5bfa5
fix: ensure app loads without styles if the PARAGON_THEME_URLS and fa…
adamstankiewicz May 31, 2023
59401f5
fix: ensure fallback theme links are removed if they also error
adamstankiewicz May 31, 2023
46cb39a
docs: add link to mfe runtime config api adr
adamstankiewicz May 31, 2023
ebbe03a
fix: don't attempt to load paragon css urls if PARAGON_THEME_URLS is …
adamstankiewicz Jun 1, 2023
494ad62
Merge branch 'master' into ags/inject-theme-css
adamstankiewicz Jun 1, 2023
74f9f8f
fix: brand overrides
adamstankiewicz Jun 5, 2023
e3e5fe5
docs: fix code example
adamstankiewicz Jun 5, 2023
902e8f4
docs: add missing comma
adamstankiewicz Jun 5, 2023
0c73b5b
docs: update how to
adamstankiewicz Jun 5, 2023
b380c18
Merge branch 'master' into ags/inject-theme-css
adamstankiewicz Jul 15, 2023
67a0a1d
feat: support dark mode
adamstankiewicz Jul 15, 2023
c30ae3f
chore: update package-lock.json
adamstankiewicz Jul 15, 2023
e2a115e
chore: update package-lock.json take 2
adamstankiewicz Jul 15, 2023
33a8377
chore: remove console.log statements
adamstankiewicz Jul 15, 2023
45c727e
fix: ignore system preference change when theme variant set in locals…
adamstankiewicz Jul 16, 2023
f6d633c
chore: add tests for updates to AppProvider
adamstankiewicz Jul 16, 2023
d65acff
chore: update react-intl to pass peer dependencies after pinning all …
adamstankiewicz Jul 16, 2023
efdf60c
chore: split hooks.js up into separate files and begin some related t…
adamstankiewicz Jul 16, 2023
e96de6b
test: add testing to useParagonTheme hooks (#514)
dcoa Jul 21, 2023
8f39517
Merge branch 'master' into ags/inject-theme-css
adamstankiewicz Jul 21, 2023
cf10278
chore: update package-lock.json
adamstankiewicz Jul 21, 2023
8f0edb4
fix: update links in head and *isLoaded to true (#534)
monteri Dec 9, 2023
084a4ed
Merge branch 'master' into ags/inject-theme-css
adamstankiewicz Dec 9, 2023
b5f1588
test: add test to useParagonTheme hook and paragon utils (#525)
dcoa Dec 9, 2023
2b2772e
Merge branch 'ags/inject-theme-css' of github.com:openedx/frontend-pl…
adamstankiewicz Dec 9, 2023
3345dc0
fix: resolve lockfileVersion conflict after merge with master
adamstankiewicz Dec 9, 2023
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
4 changes: 2 additions & 2 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ STUDIO_BASE_URL=http://localhost:18010
MARKETING_SITE_BASE_URL=http://localhost:18000
ORDER_HISTORY_URL=http://localhost:1996/orders
REFRESH_ACCESS_TOKEN_ENDPOINT=http://localhost:18000/login_refresh
SEGMENT_KEY=''
SEGMENT_KEY=
SITE_NAME=localhost
USER_INFO_COOKIE_NAME=edx-user-info
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
Expand All @@ -27,4 +27,4 @@ FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
IGNORED_ERROR_REGEX=
MFE_CONFIG_API_URL=
APP_ID=
SUPPORT_URL=https://support.edx.org
SUPPORT_URL=https://support.edx.org
Binary file added docs/how_tos/assets/paragon-theme-loader.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
204 changes: 204 additions & 0 deletions docs/how_tos/theming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# Theming support with `@edx/paragon` and `@edx/brand`

## Overview

This document serves as a guide to using `@edx/frontend-platform` to support MFE theming with Paragon using theme CSS loaded externally (e.g., from a CDN).

To do this, configured URLs pointing to relevant CSS files from `@edx/paragon` and (optionally) `@edx/brand` are loaded and injected to the HTML document at runtime. This differs than the consuming application importing the styles from `@edx/paragon` and `@edx/brand` directly, which includes these styles in the application's production assets.

By serving CSS loaded externally, consuming applications of Paragon no longer need to be responsible for compiling the theme SCSS to CSS themselves and instead use a pre-compiled CSS file. In doing so, this allows making changes to the Paragon theme without needing to necessarily re-build and re-deploy all consuming applications.

### Dark mode and theme variant preferences

`@edx/frontend-platform` supports both `light` (required) and `dark` (optional) theme variants. The choice of which theme variant should be applied on page load is based on the following preference cascade:

1. **Get theme preference from localStorage.** Supports persisting and loading the user's preference for their selected theme variant, until cleared.
1. **Detect user system settings.** Rely on the `prefers-color-scheme` media query to detect if the user's system indicates a preference for dark mode. If so, use the default dark theme variant, if one is configured.
1. **Use default theme variant as configured (see below).** Otherwise, load the default theme variant as configured by the `defaults` option described below.

Whenever the current theme variant changes, an attrivbute `data-paragon-theme-variant="*"` is updated on the `<html>` element. This attribute enables applications' both JS and CSS to have knowledge of the currently applied theme variant.

### Supporting custom theme variants beyond `light` and `dark`

If your use case necessitates additional variants beyond the default supported `light` and `dark` theme variants, you may pass any number of custom theme variants. Custom theme variants will work the user's persisted localStorage setting (i.e., if a user switches to a custom theme variant, the MFE will continue to load the custom theme variant by default). By supporting custom theme variants, it also supports having multiple or alternative `light` and/or `dark` theme variants.

### Performance implications

There is also a meaningful improvement in performance as loading the compiled theme CSS from an external CDN means micro-frontends (MFEs) can include cached styles instead of needing to load essentially duplicate theme styles included in each individual MFE as users navigate across the platform.

However, as the styles from `@edx/paragon` and `@edx/brand` get loaded at runtime by `@edx/frontend-platform`, the associated CSS files do not get processed through the consuming application's Webpack build process (e.g., if the MFE used PurgeCSS or any custom PostCSS plugins specifically for Paragon).

### Falling back to styles installed in consuming application

If any of the configured external `PARAGON_THEME_URLS` fail to load for whatever reason (e.g., CDN is down, URL is incorrectly configured), `@edx/paragon` will attempt to fallback to the relevant files installed in `node_modules` from the consuming application.

## Technical architecture

![overview of paragon theme loader](./assets/paragon-theme-loader.png "Paragon theme loader")

## Development

### Basic theme URL configuration

Paragon supports 2 mechanisms for configuring the Paragon theme urls:
* JavaScript-based configuration via `env.config.js`
* MFE runtime configuration API via `edx-platform`

Using either configuration mechanism, a `PARAGON_THEME_URLS` configuration setting must be created to point to the externally hosted Paragon theme CSS files, e.g.:

```json
{
"core": {
"url": "https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.min.css"
},
"defaults": {
"light": "light",
},
"variants": {
"light": {
"url": "https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/light.min.css",
}
}
}
```

### Configuration options

The `PARAGON_THEME_URLS` configuration object supports using only the default styles from `@edx/paragon` or, optionally, extended/overridden styles via `@edx/brand`. To utilize `@edx/brand` overrides, see the `core.urls` and `variants.*.urls` options below.

The `dark` theme variant options are optional.

| Property | Data Type | Description |
| -------- | ----------- | ----------- |
| `core` | Object | Metadata about the core styles from `@edx/paragon` and `@edx/brand`. |
| `core.url` | String | URL for the `core.css` file from `@edx/paragon`. |
| `core.urls` | Object | URL(s) for the `core.css` files from `@edx/paragon` CSS and (optionally) `@edx/brand`. |
| `core.urls.default` | String | URL for the `core.css` file from `@edx/paragon`. |
| `core.urls.brandOverride` | Object | URL for the `core.css` file from `@edx/brand`. |
| `defaults` | Object | Mapping of theme variants to Paragon's default supported light and dark theme variants. |
| `defaults.light` | String | Default `light` theme variant from the theme variants in the `variants` object. |
| `defaults.dark` | String | Default `dark` theme variant from the theme variants in the `variants` object. |
| `variants` | Object | Metadata about each supported theme variant. |
| `variants.light` | Object | Metadata about the light theme variant styles from `@edx/paragon` and (optionally)`@edx/brand`. |
| `variants.light.url` | String | URL for the `light.css` file from `@edx/paragon`. |
| `variants.light.urls` | Object | URL(s) for the `light.css` files from `@edx/paragon` CSS and (optionally) `@edx/brand`. |
| `variants.light.urls.default` | String | URL for the `light.css` file from `@edx/paragon`. |
| `variants.light.urls.brandOverride` | String | URL for the `light.css` file from `@edx/brand`. |
| `variants.dark` | Object | Metadata about the dark theme variant styles from `@edx/paragon` and (optionally)`@edx/brand`. |
| `variants.dark.url` | String | URL for the `dark.css` file from `@edx/paragon`. |
| `variants.dark.urls` | Object | URL(s) for the `dark.css` files from `@edx/paragon` CSS and (optionally) `@edx/brand`. |
| `variants.dark.urls.default` | String | URL for the `dark.css` file from `@edx/paragon`. |
| `variants.dark.urls.brandOverride` | String | URL for the `dark.css` file from `@edx/brand`. |

### JavaScript-based configuration

One approach to configuring the `PARAGON_THEME_URLS` is to create a `env.config.js` file in the root of the repository. The configuration is defined as a JavaScript file, which affords consumers to use more complex data types, amongst other benefits.

To use this JavaScript-based configuration approach, you may set a `PARAGON_THEME_URLS` configuration variable in a `env.config.js` file:

```js
const config = {
PARAGON_THEME_URLS: {
core: {
url: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.min.css',
},
defaults: {
light: 'light',
},
variants: {
light: {
url: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/light.min.css',
},
},
},
};

export default config;
```

### MFE runtime configuration API

`@edx/frontend-platform` additionally supports loading application configuration from the MFE runtime configuration API via `edx-platform`. The configuration is served by the `http://localhost:18000/api/mfe_config/v1` API endpoint. For more information, refer to [this documentation](https://github.com/openedx/edx-platform/blob/master/lms/djangoapps/mfe_config_api/docs/decisions/0001-mfe-config-api.rst) about the MFE runtime configuration API, please see these docs.

The application configuration may be setup via Django settings as follows:

```python
ENABLE_MFE_CONFIG_API = True
MFE_CONFIG = {}
MFE_CONFIG_OVERRIDES = {
# The below key represented the `APP_ID` defined in your MFE
'profile': {
'PARAGON_THEME_URLS': {
'core': {
'url': 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.min.css',
},
'defaults': {
'light': 'light',
},
'variants': {
'light': {
'url': 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/light.min.css',
},
},
},
},
}
```

### Locally installed `@edx/paragon`

If you would like to use the same version of the Paragon CSS urls as the locally installed `@edx/paragon`, the configuration for the Paragon CSS urls may contain a wildcard `$paragonVersion` which gets replaced with the locally installed version of `@edx/paragon` in the consuming application, e.g.:

```shell
https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.min.css
https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/light.min.css
```

In the event the other Paragon CSS urls are configured via one of the other documented mechanisms, but they fail to load (e.g., the CDN url throws a 404), `@edx/frontend-platform` will attempt to fallback to injecting the locally installed Paragon CSS from the consuming application into the HTML document.

## Usage with `@edx/brand`

The core Paragon design tokens and styles may be optionally overriden by utilizing `@edx/brand`, which allows theme authors to customize the default Paragon theme to match the look and feel of their custom brand.

This override mechanism works by compiling the design tokens defined in `@edx/brand` with the the core Paragon tokens to generate overrides to Paragon's default CSS variables, and then compiling the output CSS with any SCSS theme customizations not possible through a design token override.

The CSS urls for `@edx/brand` overrides will be applied after the core Paragon theme urls load, thus overriding any previously set CSS variables and/or styles.

To enable `@edx/brand` overrides, the `PARAGON_THEME_URLS` setting may be configured as following:

```js
const config = {
PARAGON_THEME_URLS: {
core: {
urls: {
default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.min.css',
brandOverride: 'https://cdn.jsdelivr.net/npm/@edx/brand-edx.org@#brandVersion/dist/core.min.css',
},
},
defaults: {
light: 'light',
},
variants: {
light: {
urls: {
default: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/light.min.css',
brandOverride: 'https://cdn.jsdelivr.net/npm/@edx/brand-edx.org@$brandVersion/dist/light.min.css',
},
},
},
},
};

export default config;
```

### Locally installed `@edx/brand`

If you would like to use the same version of the brand override CSS urls as the locally installed `@edx/brand`, the configuration for the brand override CSS urls may contain a wildcard `$brandVersion` which gets replaced with the locally installed version of `@edx/brand` in the consuming application, e.g.:

```shell
https://cdn.jsdelivr.net/npm/@edx/brand@$brandVersion/dist/core.min.css
https://cdn.jsdelivr.net/npm/@edx/brand@$brandVersion/dist/light.min.css
```

In the event the other brand override CSS urls are configured via one of the other documented mechanisms, but they fail to load (e.g., the CDN is down), `@edx/frontend-platform` will attempt to fallback to injecting the locally installed brand override CSS urls from the consuming application into the HTML document.
3 changes: 2 additions & 1 deletion example/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import {
import { APP_INIT_ERROR, APP_READY, initialize } from '@edx/frontend-platform';
import { subscribe } from '@edx/frontend-platform/pubSub';

import './index.scss';
import ExamplePage from './ExamplePage';
import AuthenticatedPage from './AuthenticatedPage';

import './index.scss';

subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider>
Expand Down
Loading