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

Conversation

adamstankiewicz
Copy link
Member

@adamstankiewicz adamstankiewicz commented Feb 15, 2023

Description:

Implementation POC/prototype for ADR on loading a common, external stylesheet for MFEs.

  • Introduces useParagonTheme in AppProvider to load/inject <link> elements for the core theme CSS and any theme variant CSS into the HTML document.
  • Exposes the app theme state and a way for applications to expose functionality to toggle between theme variants to consumers via AppContext.
  • Supports a dark theme variant (even though Paragon itself does not yet have a dark theme variant).
  • Supports a few mechanisms for determining which theme variant to make "active" and visible to the user:
    • When an application sets a theme variant programmatically (e.g., by exposing a button to the user to switch between light and dark modes), the selected theme variant is stored in localStorage so it persists/loads across sessions and page refreshes.
    • When no theme variant is selected in localStorage, checks if the user's System Preferences has a preference for dark mode via a @media query for prefers-color-scheme: dark. If dark color scheme is preferred, the default configured dark theme variant is made visible, if one exists.
      • When the prefers-color-scheme: dark query changes (i.e., user updates their System Preferences), loads the appropriate default theme variant corresponding to the system preference in real time.
    • When either the dark mode theme variant doesn't exist or the default light theme is actively visible, finds the default configured light theme variant.

See theming.md for documentation and more details.

Merge checklist:

  • Consider running your code modifications in the included example app within frontend-platform. This can be done by running npm start and opening http://localhost:8080.
  • Consider testing your code modifications in another local micro-frontend using local aliases configured via the module.config.js file in frontend-build.
  • Verify your commit title/body conforms to the conventional commits format (e.g., fix, feat) and is appropriate for your code change. Consider whether your code is a breaking change, and modify your commit accordingly.

Post merge:

  • After the build finishes for the merged commit, verify the new release has been pushed to NPM.

@adamstankiewicz adamstankiewicz changed the title Ags/inject theme css feat: adds support for loading external theme CSS for MFEs Feb 15, 2023
Comment on lines 90 to 81
if (!appThemeState?.isThemeLoaded) {
return null;
}
Copy link
Member Author

@adamstankiewicz adamstankiewicz Feb 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[inform] Don't render the app until the core theme CSS and theme variant(s) CSS is loaded. This helps avoid a Flash of Unstyled Content (FoUC) on initial page load.

@codecov
Copy link

codecov bot commented Feb 15, 2023

Codecov Report

Attention: 107 lines in your changes are missing coverage. Please review.

Comparison is base (3a93d60) 83.22% compared to head (cf10278) 79.05%.

❗ Current head cf10278 differs from pull request most recent head 3345dc0. Consider uploading reports for the commit 3345dc0 to get more accurate results

Files Patch % Lines
src/react/hooks/paragon/useParagonTheme.js 3.12% 23 Missing and 8 partials ⚠️
src/react/hooks/paragon/useParagonThemeVariants.js 78.64% 20 Missing and 2 partials ⚠️
src/react/hooks/paragon/utils.js 24.13% 16 Missing and 6 partials ⚠️
src/react/hooks/paragon/useParagonThemeCore.js 82.19% 12 Missing and 1 partial ⚠️
src/react/reducers.js 23.07% 9 Missing and 1 partial ⚠️
src/react/hooks/paragon/useParagonThemeUrls.js 73.33% 6 Missing and 2 partials ⚠️
src/react/hooks/useAppEvent.js 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #440      +/-   ##
==========================================
- Coverage   83.22%   79.05%   -4.18%     
==========================================
  Files          40       48       +8     
  Lines        1073     1351     +278     
  Branches      197      283      +86     
==========================================
+ Hits          893     1068     +175     
- Misses        168      251      +83     
- Partials       12       32      +20     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@regisb
Copy link

regisb commented Feb 16, 2023

I don't know enough about MFEs to comment about the details of this change, but what I understand is that it will require just two settings to customise the CSS, which is great.

One thing, though: this new extension mechanism will only be as good as its documentation. So I urge you to spend some time to document it as best you can. Actually, I suggest that you write the docs before finalizing the implementation, in README-driven-development style. For instance, you should describe about Tutor users can create and integrate new themes.

Reading the docs will allow us to comment on this PR in a much more informed way. Also, writing the docs will help you realize whether the new extension mechanism integrates well with the workflows of platform administrators and Open edX developers.

@ghassanmas
Copy link
Member

ghassanmas commented Feb 16, 2023

I have a couple of question,

@arbrandes
Copy link
Contributor

@adamstankiewicz, some stuff that came up out of a high-level discussion with tCRIL engineering:

  • Support and longevity: we should consider the case where somebody is still running Palm after 5 years. The required stylesheets must still be hosted somewhere, and the implication is also that they will have to be versioned.
  • While we can envision the advantages of having themes be built separately and thus potentially reusable across versions, maybe this should be an optional behavior: say, if APP_THEME_CORE_URL is set, the stylesheet is fetched accordingly, but if not, then the app would expect to find it in the webpack bundle. (This also addresses the issue of longevity.)
  • Performance: there's nothing to say this proposal wouldn't be performant if done well, but we should keep UX in mind. With the dynamic config API currently in place, it could mean making the user wait for two requests in sequence (not counting the very first GET), any of which could fail: first, for the configuration itself, and after this one is complete, for the stylesheets. And this is not some theoretical possibility: it would be the default behavior for most Tutor deployments.

These aren't necessarily formal objections by tCRIL, by the way. Just adding some stuff to keep in mind. :)

@adamstankiewicz
Copy link
Member Author

adamstankiewicz commented Feb 16, 2023

@arbrandes Thanks for sharing the input!

Support and longevity: we should consider the case where somebody is still running Palm after 5 years. The required stylesheets must still be hosted somewhere, and the implication is also that they will have to be versioned.

If we did end up using/recommending jsDelivr as an off-the-shelf free CDN supporting versions already aligned with NPM/Github releases, they guarantee any file from any version of the NPM package will remain accessible forever. They even support version fallback (say, a file is deleted in v1.0.2 that was previously there in v1.0.1, if you try to access that now-deleted file on v1.0.2, jsDelivr is smart enough to fallback to the version of the file in v1.0.1.

While we can envision the advantages of having themes be built separately and thus potentially reusable across versions, maybe this should be an optional behavior: say, if APP_THEME_CORE_URL is set, the stylesheet is fetched accordingly, but if not, then the app would expect to find it in the webpack bundle. (This also addresses the issue of longevity.)

I was actually just chatting with @brian-smith-tcril about this a bit, too 😄 generally, though, +1 to the input and I'd extend it to say that we might be able to fallback to the installed CSS within the webpack bundle should the external CSS fail to load for whatever reason so at least the app renders with some styles, even if those styles may be from a slightly older version than what might be upstream in the CDN, for example.

I believe this latter bit around falling back to locally installed CSS addresses at the third feedback item as well regarding performance? If the external CSS files are on a CDN(e.g., jsDelivr), the first few requests to a particular version of the theme may take a bit longer but subsequent requests to the same versions/files in the future would be cached and nearly instantaneous to load. If it errors, falling back to slightly out of date CSS would at least still make the application functional.

See https://www.jsdelivr.com/documentation for more details on how jsDelivr is production-ready (e.g., if Cloudflare goes down, jsDelivr instantly reverts to another CDN provider instead, has support for China, etc.).

@adamstankiewicz
Copy link
Member Author

@ghassanmas's questions above were largely answered in the Frontend Working Group meeting earlier today. To recap though:

If loading the theme/style from cdn, would that be enough for the app (to run UX/UI wise) without including the default app.*css file generated on each bundle. i.e. does the theme has enough css or just stuff to override? I am referring openedx/frontend-build#266 (comment)

The core theme CSS (core.css) intends to be an all-encompassing theme containing all of the Paragon styles necessary the foundational parts of the Open edX theme including layout, typography, component styles, etc. The light theme variant CSS (light.css) - in the future, possibly an additional dark.css for dark mode support - would define just the CSS variables necessary for the light theme variant (the default). These CSS variables are used within core.css.

Assuming applications don't have their own custom styles, applications indeed could not have any custom SCSS/CSS files at all, only relying on the external theme CSS.

If applications do have their own custom styles (hopefully not overriding Paragon styles!), @edx/frontend-build (via Webpack) will bundle the application's CSS to .css file(s) and inject it after the external theme CSS.

By doing so, the application .css has higher priority over the theme CSS files and will (generally) take precedence.

Will each MFE has to own file or one file is enough for all the MFEs for example app learning has its own scss file https://github.com/openedx/frontend-app-learning/blob/master/src/index.scss

The externally hosted, already-compiled theme CSS will need to versioning according to semantic versioning and aligned to NPM/Github releases. In our current MFE architecture, MFEs would use the latest release for the major version compatible with the installed @edx/paragon version in the MFE. For example, if the frontend-app-profile MFE has @edx/paragon@21 installed in its package.json file, the MFE should also be using the latest v21 of the external theme CSS to ensure compatibility.

@adamstankiewicz
Copy link
Member Author

adamstankiewicz commented Feb 16, 2023

Reading the docs will allow us to comment on this PR in a much more informed way. Also, writing the docs will help you realize whether the new extension mechanism integrates well with the workflows of platform administrators and Open edX developers.

@regisb Noted! Yes, documentation is definitely a critical component of this project to move from current state of theming to runtime theming via design tokens. While there is no specific documentation to this frontend-platform implementation quite yet, the existing ADR proposing a related approach helped give some motivation/context for this PR. That said, I've taken an action item to:

  • Write a brief ADR to go alongside this implementation in the same PR, likely borrowing certain aspects of the existing ADR linked above, describing the motivation/context, decision, etc.
  • Include a "how to" doc in the repo with more specifics about "How do I use this theming extension?" for consumers.

Also worth noting there is a separate documentation effort needed for how to do the Paragon theming with design tokens and CSS variables 😄

@felipemontoya
Copy link
Member

I can't comment on the implementation, but the overall approach, and the concept of the core and light css variable bundles seem very appropriate to me. Thanks for getting this done.

@regisb
Copy link

regisb commented Feb 23, 2023

Thanks for your answer @adamstankiewicz! I think that the extension docs should live close to the rest of the developer docs: https://docs.openedx.org/en/latest/developers/index.html

@xitij2000
Copy link

The core theme CSS (core.css) intends to be an all-encompassing theme containing all of the Paragon styles necessary the foundational parts of the Open edX theme including layout, typography, component styles, etc. The light theme variant CSS (light.css) - in the future, possibly an additional dark.css for dark mode support - would define just the CSS variables necessary for the light theme variant (the default). These CSS variables are used within core.css.

Considering that it's now possible to have richer JSON-based config thanks to the config API. (i.e. the config API can return a JSON for any setting). Perhaps this code can be made more generic to load any kind of theme?

i.e. the config api can return a structure like:

{
  "core": "https...",
  "variants":  {
     "light": "https...",
     "dark": "https...",
     "high-contrast": "https..."
  }
}

Which is the structure being built here:

https://github.com/openedx/frontend-platform/pull/440/files#diff-e986d374444ced0f5162cc6cb8b3dac6cbd228f339851f2a99a4aec545559d87R71-R78

If the config API isn't enabled, then it can continue to fall back to the APP_THEME_LIGHT_URL.

@adamstankiewicz
Copy link
Member Author

@xitij2000 Great suggestion! Yeah, agreed this should prefer any runtime config settings, and otherwise fall back to the regular ol' environment variables. I can't imagine it'd be too much of a lift to support the runtime config use case, to use an already created object structure representing the theme core/variant URLs, if it exists.

I'm not immediately sure what that implementation might look like as I'm not all that familiar with the config API outside of how frontend-platform calls it; is this something you might want to take a stab at (feel free to put some pseudocode or otherwise together as a PR to this one!)?

Out of curiosity... I'm not actually sure, is the runtime config used in production by anyone at this point (AFAIK it's not being used by 2U/edX yet)?

@felipemontoya
Copy link
Member

We are already using the runtime config in prod for one medium sized instance with ~70 multitenant sites. So far so good with its usage.

@xitij2000
Copy link

I'm not immediately sure what that implementation might look like as I'm not all that familiar with the config API outside of how frontend-platform calls it; is this something you might want to take a stab at (feel free to put some pseudocode or otherwise together as a PR to this one!)?

Sure! I'll put together something based on this PR and link it here when ready.

@adamstankiewicz
Copy link
Member Author

Sure! I'll put together something based on this PR and link it here when ready.

@xitij2000 I ended up putting together a possible solution to prefer config from the MFE config API; otherwise, fallback to environment variables. I just pushed up the latest changes to this PR. The runtime config vs. environment variable config approaches are described in a new docs/how_tos/theming.md document.

dcoa and others added 3 commits July 21, 2023 13:23
* test: add testing to useParagonThemeCore
* test: add test to useThemeVariants hook
* fix: Paragon definition and remove onload mock
* test: change test message to be clear
@dcoa
Copy link
Contributor

dcoa commented Jul 27, 2023

I was testing system preference configuration and it works when default and brandOverride are defined but when I use only default values like:

"PARAGON_THEME_URLS": {
                "core": {
                    "url": "https://cdn.jsdelivr.net/npm/@edx/[email protected]/dist/core.min.css"
                },
                "defaults": {
                    "dark": "dark",
                    "light": "light"
                },
                "variants": {
                    "dark": {
                        "url": "https://cdn.jsdelivr.net/npm/@edx/[email protected]/dist/dark.min.css""
                    },
                    "light": {
                        "url": "https://cdn.jsdelivr.net/npm/@edx/[email protected]/dist/light.min.css"
                    }
                }
            }

can not change the theme properly:

scrnli_7_27_2023_4-51-10.PM.webm

} else {
const updatedStylesheetRel = generateStylesheetRelAttr(themeVariant);
existingThemeVariantLink.rel = updatedStylesheetRel;
existingThemeVariantBrandLink.rel = updatedStylesheetRel;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need an extra conditional when brand link is not defined

Suggested change
existingThemeVariantBrandLink.rel = updatedStylesheetRel;
if (existingThemeVariantBrandLink) { existingThemeVariantBrandLink.rel = updatedStylesheetRel; }

return;
}
const getParagonThemeCoreLink = () => document.head.querySelector('link[data-paragon-theme-core="true"');
const existingCoreThemeLink = document.head.querySelector(`link[href='${themeCore.urls.default}']`);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been testing this while migrating an MFE to using runtime theming, and I think these queries are causing issues in some cases.

The frontend-build PR is adding these links a <link rel=preload which is also picked up by such queries, meaning that it doesn't attempt to add the actual links.

I think this can be fixed as follows:

Suggested change
const existingCoreThemeLink = document.head.querySelector(`link[href='${themeCore.urls.default}']`);
const existingCoreThemeLink = document.head.querySelector(`link[href='${themeCore.urls.default}'][rel=stylesheet]`);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a similar change will need to be applied to other queries as well.

xitij2000 added a commit to open-craft/frontend-platform that referenced this pull request Aug 30, 2023
ghassanmas added a commit to ghassanmas/frontend-app-learning that referenced this pull request Oct 6, 2023
  This PR purpose is to test/demo parago design tokens simliar
  to this one for the profile openedx/frontend-app-profile/pull/764

  it override the following depns as seen in package.json

  - paragon alpha
  - openedx/frontend-build/pull/365
  - openedx/frontend-platform/pull/440
  - openedx/frontend-component-header/pull/351
  - openedx/frontend-component-footer/pull/303

 Conclousion so far:

 - There is an extra library that learning depends on which
  needs to support paragon; `frontend-lib-learning-assistant`
  and also `frontend-lib-special-exams`

 - It would be great to have a gudie or a document to look at,
 while doing the conversion that would **map variable from
 who it was used/named before to the name in design tokens**

 - I was stuck in the end with compliation error, that
 wepack couldn't find `Modal` exported from paragon.
@Mashal-m
Copy link
Contributor

Hey @adamstankiewicz , What is the current status of this PR, is it ready to review and merge?
Could you please resolve conflicts?

Co-authored-by: monteri <lansevermore>
Comment on lines +20 to +22
return function cleanup() {
unsubscribe(subscriptionToken);
};

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is possible to change this to an arrow function?

Suggested change
return function cleanup() {
unsubscribe(subscriptionToken);
};
return () => unsubscribe(subscriptionToken);

@xitij2000
Copy link

@adamstankiewicz Is there any way we can help get this closer to merging? We're currently using this in conjunction with patched versions of MFEs and it's working pretty well.

@arbrandes
Copy link
Contributor

This was discussed today in an FWG meeting, It sounds like this will be taken forward as part of a Design Token Axim Funded Contribution.

@adamstankiewicz
Copy link
Member Author

Closed in favor of openedx/frontend-build#546, which picked up where this PR left off :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.