-
Notifications
You must be signed in to change notification settings - Fork 65
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 #689
base: master
Are you sure you want to change the base?
Conversation
Thanks for the pull request, @dcoa! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.
|
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #689 +/- ##
==========================================
+ Coverage 83.41% 84.14% +0.73%
==========================================
Files 40 48 +8
Lines 1073 1394 +321
Branches 197 291 +94
==========================================
+ Hits 895 1173 +278
- Misses 166 206 +40
- Partials 12 15 +3 ☔ View full report in Codecov by Sentry. |
aee582b
to
e81b549
Compare
docs/how_tos/theming.md
Outdated
@@ -0,0 +1,204 @@ | |||
# Theming support with `@edx/paragon` and `@edx/brand` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we need to refactor @edx/
to @openedx/
in docs, as since December npm packages for @openedx/paragon, @openedx/frontend-build, @openedx/frontend-plugin-framework, @openedx/brand-openedx
are published in the scope of @openedx/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@brian-smith-tcril I couldn't find frontend-platform among npm published packages, shouldn't it be published with @openedx scope as well?
https://www.npmjs.com/search?q=%40openedx
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that one is still only published to the @edx
scope. I agree it would be good to make the switch but it hasn't made it to the top of the priority list yet.
package.json
Outdated
"@openedx/frontend-build": "14.0.3", | ||
"@openedx/paragon": "22.3.2", | ||
"@openedx/frontend-build": "github:edunext/frontend-build#dcoa/design-tokens-support", | ||
"@openedx/paragon": "npm:@openedx/paragon@^22.0.0-alpha.25", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might worth to wait for the Paragon sync PR to be merged, so we won't downgrade paragon requirements, and still will be able to use the latest version of paragon.
5b4c843
to
ed293e3
Compare
@dcoa I'm getting an error during build related to the
I tried to solve it by changing the export to Have you encountered such a problem? |
@PKulkoRaccoonGang this is related to frontend-build changes, I'm checking right now This line is making that the app is not rendering in frontend-platform https://github.com/openedx/frontend-build/pull/546/files#diff-22e13ddf245ea4fa81ca4d686dddf46fa9cbf70fbbff99991cccbf0c8ff82316R175 If you remove it, you will be able to see the app. Update I made an update to webpack dev config for example app and is working now. |
9ebae07
to
94e4a62
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dcoa This looks great! I have already started working on Update openedx/frontend-app-discussions
. I left a few questions about the operation of some features and the code in general.
`@openedx/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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[question]: Is there now support for adding a dark/light theme (or other custom themes) when using locally installed Paragon CSS?
For example:
@use "@openedx/paragon/dist/core.min.css" as paragonCore;
@use "@openedx/paragon/dist/light.min.css" as paragonLight;
@use "@openedx/paragon/dist/dark.min.css" as paragonDark;
This works great using JS and MFE runtime configurations, but I have not been able to test it for local themes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hook will fallback in the installed themes files if the URL is not provided or is not working.
If you define only the CSS inside SCSS file you can use the prefers-color-scheme
to support dark/light modes. But you can not create a custom switch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! It would be great to explicitly indicate exactly how it is possible to use different imports for a light/dark theme in the documentation. I plan to do this in the migration documentation and for Paragon.
For example:
@use "@openedx/paragon/dist/core.min.css" as paragonCore;
@import url('@openedx/paragon/dist/light.min.css') (prefers-color-scheme: light);
@import url('@my-brand/dark.min.css') (prefers-color-scheme: dark);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
100% agree that the information in your example should be part of the Paragon and migration documents.
This is a different approach, the explanation of how to use variants is below, in the title Basic theme URL configuration.
I will improve one of the examples to use dark mode, to make it more explicit.
|
||
### 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), `@openedx/paragon` will attempt to fallback to the relevant files installed in `node_modules` from the consuming application. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@PKulkoRaccoonGang thanks for the feedback, the fallback url was missing the publicPath
I solved it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don’t receive undefined
, but I couldn’t load styles from the locally installed Paragon package in node_modules
. Could you check if getParagonThemeCss
works correctly in a PR related to a frontend-build?
![image](https://private-user-images.githubusercontent.com/93188219/335510870-c07a5684-df1b-4a20-9e5f-f8427b7b9d49.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg4ODgxOTAsIm5iZiI6MTczODg4Nzg5MCwicGF0aCI6Ii85MzE4ODIxOS8zMzU1MTA4NzAtYzA3YTU2ODQtZGYxYi00YTIwLTllNWYtZjg0MjdiN2I5ZDQ5LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjA3VDAwMjQ1MFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiYWQ3ZmU4MDBlN2Q3YTg2YTMyMGVlOWQwZTFmNzVmNWJjMzU3NTUwNjJjY2Y5ODNkYTJkMjlhYzlkNDJkODMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.hl7y0TVae_UPLw2OZ1e3ukFQEwkOqEBjPtL_nVbiZoE)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem here is related to tutor-mfe, they define BASE_URL
as the MFE_HOST
(you can see here) thing that is not 100% true in dev mode because the URL needs the PORT
that is specific for each MFE (you can see here )
You can add in the settings:
MFE_CONFIG_OVERRIDES = {
"discussions": {
"BASE_URL": "http://apps.local.edly.io:2002"
}
}
docs/how_tos/theming.md
Outdated
### Basic theme URL configuration | ||
|
||
Paragon supports 2 mechanisms for configuring the Paragon theme urls: | ||
* JavaScript-based configuration via `env.config.js` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[question]: This approach works great in MFE, but I encountered a problem in frontend-build/example
, unfortunately, the styles are not loading for me. Is everything working correctly?
For example: I tried adding a Button component with Paragon and noticed that the styles with Paragon were not being applied to this component
|
||
```shell | ||
https://cdn.jsdelivr.net/npm/@openedx/paragon@$paragonVersion/dist/core.min.css | ||
https://cdn.jsdelivr.net/npm/@openedx/paragon@$paragonVersion/dist/light.min.css |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[question]: Is it worth adding an example of importing in SCSS format here? This also applies to the local import example @openedx/brand-openedx
For example:
@use "@openedx/paragon/dist/core.min.css" as paragonCore;
@use "@openedx/paragon/dist/light.min.css" as paragonLight;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, because the main intention here is to identify the version and load the external resource compatible with that specific version.
For example, my app has paragon 23.0.0-alpha.2
, then the hook will replace $paragonVersion
.
This is the definition
https://cdn.jsdelivr.net/npm/@openedx/paragon@$paragonVersion/dist/core.min.css
after processing
https://cdn.jsdelivr.net/npm/@openedx/paragon@$23.0.0-alpha.2/dist/core.min.css
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation 💯
global.PARAGON_THEME = { | ||
paragon: { | ||
version: '1.0.0', | ||
themeUrls: { | ||
core: { | ||
fileName: 'core.min.css', | ||
}, | ||
defaults: { | ||
light: 'light', | ||
}, | ||
variants: { | ||
light: { | ||
fileName: 'light.min.css', | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit]: We also use this configuration for the test in the file src/react/hooks/paragon/useParagonThemeVariants.test.js
I suggest putting this code into a separate file and reusing it to test both hooks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can get rid of this understanding that is being exported by frontend-build (including for testing)
Note: Once the frontend-build PR has been merged we need to update the devDependencies
and peerDependencies
}); | ||
|
||
it('returns expected object when configuration is valid (both Paragon + brand)', () => { | ||
const config = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[optional]: I suggest reusing the config constant, which refers to an object with the necessary configuration for tests in this file. This will allow us to get rid of code duplication. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer leaving this section as it is. The important part here is PARAGON_THEME_URLS
, which changes in each test. It's easier to understand this way, I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good, I agree with you, thanks
src/react/AppProvider.test.jsx
Outdated
}); | ||
|
||
it('calls useParagonTheme', () => { | ||
const Component = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[question]: Can we declare a global Сomponent constant for a block with paragon theme and brand
tests and reuse it in tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can declare the global component but this test doesn't cover the useParagonTheme behavior so it is not necessary to declare paragon theme and brand
. This is focused on the setThemeVariant
function and the theme state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, thanks
a6a9211
to
229d79a
Compare
06fc4e6
to
837858e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this is looking great! I left a suggestion to clarify the documentation is referring to design tokens, and a few comments noting PRs that need to land before this can.
Thank you so much for this!
@@ -0,0 +1,250 @@ | |||
# Theming support with `@openedx/paragon` and `@openedx/brand-openedx` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# Theming support with `@openedx/paragon` and `@openedx/brand-openedx` | |
# Theming support with `@openedx/paragon` and `@openedx/brand-openedx` | |
> [!IMPORTANT] | |
> This document describes theming with design tokens. | |
> Information on theming MFEs that do not yet have design tokens support: | |
> * https://github.com/openedx/brand-openedx | |
> Information on the design tokens project: | |
> * https://github.com/openedx/paragon/blob/master/docs/decisions/0019-scaling-styles-with-design-tokens.rst | |
> * https://github.com/openedx/paragon/tree/alpha?tab=readme-ov-file#design-tokens |
package.json
Outdated
@@ -76,7 +77,7 @@ | |||
}, | |||
"peerDependencies": { | |||
"@openedx/frontend-build": ">= 14.0.0", | |||
"@openedx/paragon": ">= 21.5.7 < 23.0.0", | |||
"@openedx/paragon": " 23.0.0-alpha.1 || >= 21.5.7 < 23.0.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note
Waiting on Paragon alpha to be merged to master
2565f02
to
ae5b53f
Compare
92c3c83
to
1f90ea5
Compare
1f90ea5
to
e7eaffe
Compare
Hi @brian-smith-tcril, I updated the peerDependencies (limited to < 24) and rebased the code to master, in that way we can start the final review :) |
That's wonderful news @dcoa! I'm more than happy to start on the code side of the final review here! Is there still a PR to an MFE (using Paragon 22) somewhere? Having a sandbox up that verifies this won't break anything when it lands makes merging this a lot less scary! |
@brian-smith-tcril we have this one openedx/frontend-app-discussions#731 but is not updated, if you can help me with that or if you prefer I can open a new one. |
That works! I'd be happy to update that one (or open a new one if updating it becomes a pain) Edit: created openedx/frontend-app-discussions#750 (it seems to be working as expected) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this is looking awesome! Super happy to see all the puzzle pieces coming together!
I left a few comments in there, mostly just notes in places where naming/comments don't make what's happening entirely clear.
Thank you so much for this!
@@ -2,7 +2,6 @@ | |||
.idea | |||
.vscode | |||
coverage | |||
dist |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a reminder to myself that this needs to be changed back before this PR can land.
useEffect(() => { | ||
// Call `onLoad` once both the paragon and brand theme variant are loaded. | ||
if (isParagonThemeVariantLoaded && isBrandThemeVariantLoaded) { | ||
onLoad(); | ||
} | ||
}, [isParagonThemeVariantLoaded, isBrandThemeVariantLoaded, onLoad]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same note here about naming/description as my comment on useParagonThemeCore
return url.replaceAll(wildcardKeyword, localVersion); | ||
}; | ||
|
||
export const isEmptyObject = (obj) => !obj || Object.keys(obj).length === 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like someone did some investigation into performance of different ways to test for empty objects in js (https://stackoverflow.com/a/59787784) and found that using for-in is the fastest
export const isEmptyObject = (obj) => !obj || Object.keys(obj).length === 0; | |
export const isEmptyObject = (obj) => { | |
for (var i in obj) { | |
return false; | |
} | |
return true; | |
} |
I don't think this is a required change at all, more just something fun I found when looking into it!
4204364
to
45642c7
Compare
* @returns An array containing 2 elements: 1) an object containing the app | ||
* theme state, and 2) a dispatch function to mutate the app theme state. | ||
*/ | ||
const useParagonTheme = (config) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit/consideration] Likely could simplify the usage of this hook slightly, as I believe getConfig
could be called directly within this hook, where appropriate, versus needing to pass the config
object as a function parameter.
For example, would it make sense to call getConfig
within useParagonThemeUrls
directly instead of passing config
variable around?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think this make sense because the configuration variables tend to be loaded at the init (ideally once) and not change during the user navigation. I will made the suggested change.
if (!paragonThemeState?.isThemeLoaded) { | ||
return null; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[sanity check] Given when this code path is executed, the entire MFE app is rendered as a blank white screen, I'm wondering if we have sufficient observability for when we return null
here, after any actual async loading has finished/resolved.
For example, if there's an issue with the CDN and we also fail to fallback to using the CSS source files from the locally installed @openedx/paragon
and @edx/brand
packages, and the MFE returns a blank white screen, will we sufficiently get alerted to it via logError
, etc.?
useEffect(() => { | ||
// Call `onLoad` once both the paragon and brand theme core are loaded. | ||
if (isParagonThemeCoreLoaded && isBrandThemeCoreLoaded) { | ||
onLoad(); | ||
} | ||
}, [isParagonThemeCoreLoaded, isBrandThemeCoreLoaded, onLoad]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would something like resolve
or complete
be more generically applicable to the cases of legit loading vs the other scenarios "loading" was set truthy?
For example
setIsParagonThemeCoreResolved
andonResolve
setIsParagonThemeCoreComplete
andonComplete
Rationale: indicates that the async process is "done" vs. "loaded".
logError(`Failed to load core theme CSS from ${url}`); | ||
if (isFallbackThemeUrl) { | ||
logError(`Could not load core theme CSS from ${url} or fallback URL. Aborting.`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[curious] Any concerns around having two logError
function calls within the same code path? I.e., if isFallbackThemeUrl
is truthy here, would we have logged basically the same error twice (with slightly differing error messages)?
if (isBrandOverride) { | ||
setIsBrandThemeCoreLoaded(true); | ||
} else { | ||
setIsParagonThemeCoreLoaded(true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[curious] Do we want to ensure coverage on the critical code paths within useParagonThemeCore
, e.g. around loading? There's a few CodeCov annotations about missing coverage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, working on the overall coverage :)
logError(`Failed to load theme variant (${themeVariant}) CSS from ${value.urls.default}`); | ||
if (isFallbackThemeUrl) { | ||
logError(`Could not load theme variant (${themeVariant}) CSS from fallback URL. Aborting.`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[curious] Similar question as above, regarding potential for calling logError
twice for largely the same error, when isFallbackThemeUrl
is truthy. Should we only call logError
once in this case?
logError(`Failed to load theme variant (${themeVariant}) CSS from ${value.urls.default}`); | ||
if (isFallbackThemeUrl) { | ||
logError(`Could not load theme variant (${themeVariant}) CSS from fallback URL. Aborting.`); | ||
if (isBrandOverride) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar question around test coverage on some of the uncovered code paths within this hook.
src/react/index.js
Outdated
@@ -13,4 +13,5 @@ export { default as ErrorBoundary } from './ErrorBoundary'; | |||
export { default as ErrorPage } from './ErrorPage'; | |||
export { default as LoginRedirect } from './LoginRedirect'; | |||
export { default as PageWrap } from './PageWrap'; | |||
export { useAppEvent } from './hooks'; | |||
export { useAppEvent, useParagonTheme } from './hooks'; | |||
export { paragonThemeActions } from './reducers'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[sanity check / clarification] Can you expand on why we've want to export paragonThemeActions
to consumers? I believe the intent for consumers to manage switching theme variants is to use the paragonTheme.setThemeVariant
function exposed within AppContext
?
When will consumers use the exported paragonThemeActions
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't think of a specific case where exporting paragonThemeActions
would be necessary, if the AppContext
is already exporting the main function to change the theme. In that context, useParagonTheme
could be remove from the export list, as the current state is also exported by the context.
45642c7
to
54c6e00
Compare
b2d3856
to
bc22970
Compare
@@ -137,6 +137,16 @@ function extractRegex(envVar) { | |||
return undefined; | |||
} | |||
|
|||
function parseParagonThemeUrls(paragonUrlsJson) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes sense to check for a falsy value before trying to parse. Not having process.env.PARAGON_THEME_URLS
set is not an error state, but should still return {}
.
Then, assuming we make it past the falsy check, we can go into the try/catch
. It would also be good to log errors that we catch in there.
I think specifically checking for err
being a SyntaxError
exception (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#exceptions) would be good. So something like
try {
return JSON.parse(paragonUrlsJson);
} catch (err) {
if (err instanceof SyntaxError) {
// log something about not being able to parse the json and it possibly being invalid.
// maybe also log something about what to expect themes to look like when {} is returned
return {};
} else {
// not sure what to do here, if we're getting a different error type maybe we should just throw it?
}
}
a29f484
to
8300338
Compare
2179fbf
to
1cff3e4
Compare
Description:
This PR updates the original one #440 closer to the master branch and adds some extra tests.
Please read the original PR for additional context.
Warning
Merge checklist:
frontend-platform
. This can be done by runningnpm start
and opening http://localhost:8080.module.config.js
file infrontend-build
.fix
,feat
) and is appropriate for your code change. Consider whether your code is a breaking change, and modify your commit accordingly.Post merge: