Skip to content

Conversation

@Ang-m4
Copy link
Contributor

@Ang-m4 Ang-m4 commented Jul 4, 2025

Description

This pull request introduces a Paragon static file server for the Tutor platform, enabling the serving of compiled themes. Key changes include adding a reverse proxy configuration for static files, enhancing theme compilation with PostCSS, and updating the plugin's settings to support the new server.

Key Changes

  • Theme compilation minimization 92ed5c9

    Added PostCSS to the package.json dependencies to generate a minified bundle of styles, facilitating the hosting of a single file in the current themes folder. The entrypoint script was also updated to run the task based on the generated files.

  • Add new Nginx service for hosting the files f8ae77a

    Added a new service to expose the minified files via NGINX by mounting the COMPILED_THEMES volume. This enables using the styles in both the dev and local environments. In production, the service remains internal to the Docker Compose network and isn’t exposed externally.

    We’ve decided to proceed with a dedicated NGINX service due to the complexity of serving files through the existing LMS or Caddy services. It’s important to note that service initialization differs significantly between dev and local environments, so introducing a new service that operates in both contexts is the best solution.

  • Add reverse proxy configuration (local environment) f8ae77a

    Added a new Caddy rule in the LMS to route all requests under PARAGON_STATIC_URL_PREFIX to the NGINX service, ensuring that only the minified files are served.

  • Edit development and production settings for the lms service 495a875

    Updated the MFE_CONFIG setting to include the PARAGON_THEME_URLS object based on PARAGON_ENABLED_THEMES, with URLs generated dynamically for each environment (local/dev). Please Please note that in dev environments the Caddy service is no longer available, and the base URL for the themes becomes http://localhost:<exposed_port>.

Note

Documentation updates, testing, and Kubernetes environment integration will be delivered in separate PRs to simplify reviews.

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Jul 4, 2025
@openedx-webhooks
Copy link

openedx-webhooks commented Jul 4, 2025

Thanks for the pull request, @Ang-m4!

This repository is currently maintained by @arbrandes.

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.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@Ang-m4
Copy link
Contributor Author

Ang-m4 commented Jul 4, 2025

Hi @Alec4r!
i would like you to take a look at this, please let me know what you think!

@Ang-m4 Ang-m4 marked this pull request as ready for review July 7, 2025 17:51
@Ang-m4 Ang-m4 requested a review from brian-smith-tcril July 8, 2025 14:21
@mphilbrick211 mphilbrick211 moved this from Needs Triage to Ready for Review in Contributions Jul 15, 2025
@Asespinel
Copy link

This PR cleanly introduces a well-scoped, dedicated Paragon static file server that enables serving compiled, minified themes through a robust and secure NGINX setup. The implementation follows Tutor plugin best practices with clear separation of concerns for dev and production environments, dynamic configuration of theme URLs, and efficient theme compilation using PostCSS. The reverse proxy configuration is precise and minimizes exposure, ensuring only the necessary assets are served. Overall, the solution is modular, maintainable, and aligns well with the platform’s architecture.

@brian-smith-tcril
Copy link
Contributor

We’ve decided to proceed with a dedicated NGINX service due to the complexity of serving files through the existing LMS or Caddy services. It’s important to note that service initialization differs significantly between dev and local environments, so introducing a new service that operates in both contexts is the best solution.

@Ang-m4 could you elaborate on this? I agree that trying to serve the files through the existing LMS service doesn't make sense, but I'd think Caddy would be more than capable of serving the files.

I also think that using Caddy would make it easier to integrate this plugin into the tutor-mfe plugin in the future if the tutor-mfe plugin maintainers decide that is desired.

@Ang-m4
Copy link
Contributor Author

Ang-m4 commented Jul 21, 2025

Hi @brian-smith-tcril , thanks for the question! Let me elaborate.

The primary issue with using Caddy is that the service is only available in the local environment. The dev environment doesn't launch any Caddy containers, which would restrict the plugin's file hosting feature to just one environment. I would think our goal is to have a solution that operates consistently across both contexts.

Additionally, we would face challenges attaching the volume containing the CSS files to the Caddy service, as there is no straightforward Tutor patch template for that purpose.

@brian-smith-tcril
Copy link
Contributor

The primary issue with using Caddy is that the service is only available in the local environment. The dev environment doesn't launch any Caddy containers, which would restrict the plugin's file hosting feature to just one environment. I would think our goal is to have a solution that operates consistently across both contexts.

I could be mistaken but that sounds incorrect to me. My understanding is that when someone runs tutor dev start/tutor dev launch without any local MFE checkouts mounted the bundles are still served by Caddy.

@Ang-m4
Copy link
Contributor Author

Ang-m4 commented Jul 22, 2025

@brian-smith-tcril , I apologize for the delayed response. To clarify my initial comment, I was referring to the general Caddy service that is only available in the local environment. As you rightly pointed out, there is a Caddy-based service in the MFE plugin that serves in both dev and local environments and is explicitly configured for MFE assets.

I think the approach to add a new, dedicated service is correct. We implemented an NGINX service as it's lightweight and familiar, but if you prefer Caddy for consistency with the MFE plugin, I am happy to adjust the base image.

Please let me know what you think.

@brian-smith-tcril
Copy link
Contributor

brian-smith-tcril commented Jul 22, 2025

I apologize for the delayed response.

No worries at all!

To clarify my initial comment, I was referring to the general Caddy service that is only available in the local environment. As you rightly pointed out, there is a Caddy-based service in the MFE plugin that serves in both dev and local environments and is explicitly configured for MFE assets.

Thanks for that clarification! Sounds like we're on the same page about that now!

I think the approach to add a new, dedicated service is correct.

I 100% agree. Trying to hook into the Caddy service in the MFE plugin would get messy fast.

if you prefer Caddy for consistency with the MFE plugin, I am happy to adjust the base image.

My concern is definitely coming from a "for consistency" perspective. My thought was that using Caddy in both this plugin and the tutor-mfe plugin would make it easier to integrate this plugin into tutor-mfe in the future if that is desired.

It is also possible that using Caddy vs NGINX wouldn't actually change the amount of effort required to integrate the functionality from this plugin into the tutor-mfe plugin.

I'd like to loop in @arbrandes here - any thoughts on having this plugin host the generated css via NGINX vs Caddy?

@Ang-m4
Copy link
Contributor Author

Ang-m4 commented Jul 23, 2025

Hi @brian-smith-tcril,
I've gone ahead and updated the service's base image from NGINX to Caddy. You made a great point about consistency, and since the change required minimal effort, it felt like the right move.

I've already tested this against the latest version of Tutor, and everything is working as expected.

Copy link
Contributor

@brian-smith-tcril brian-smith-tcril left a comment

Choose a reason for hiding this comment

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

Overall this looks great! I left a couple comments with questions but nothing major. Thank you so much for putting this together!

Comment on lines +44 to +69
build_css_bundle() {
# This function builds a CSS bundle using PostCSS.
# It takes the path to the index.css file as an argument.
# Usage: build_css_bundle <index_css_file>

local index_css_file="$1"

local bundle_directory=$(dirname "$index_css_file")
local bundle_name=$(basename "$bundle_directory")
local minified_output_file="$bundle_directory/${bundle_name}.min.css"

if npx postcss "$index_css_file" \
--use postcss-import \
--use postcss-custom-media \
--use postcss-combine-duplicated-selectors \
--use postcss-minify \
--no-map \
--output "$minified_output_file"; then

echo "Successfully created bundle: $bundle_name"
return 0
else
echo "Failed to build CSS bundle: $bundle_name" >&2
return 1
fi
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not opposed to having this live here, but if we think postcss bundling paragon generated css will be a common thing then maybe it makes sense to build that functionality into the Paragon CLI. @PKulkoRaccoonGang thoughts?

- add an upper limit version for tutor dependency
- create a shared template for the mfe config on both dev and local envs
- update the commonly used port for the hosting service to 12400
@arbrandes
Copy link
Contributor

@Ang-m4, @brian-smith-tcril, I'd like to revisit the idea of re-using the tutor-mfe Caddy container for this. It has a Caddyfile hook that could be used. What it doesn't have is a hook for the docker-compose service so as to add another volume, but I don't see why anybody would object to adding one here:

https://github.com/overhangio/tutor-mfe/blob/ff74ea3a883e3e336b60ecfb8e40e27bf8ffbf0a/tutormfe/patches/local-docker-compose-services

If you submit a PR there, pointing back here as an explanation for the reason, I'd be glad to review it!

@brian-smith-tcril
Copy link
Contributor

@arbrandes that is great news! It sounds like it won't be as messy as I thought it might be!

@Ang-m4
Copy link
Contributor Author

Ang-m4 commented Jul 24, 2025

@arbrandes, @brian-smith-tcril, i think that it might be a great idea, but we should consider that we might need to make some more interesting changes regarding the k8s implementation.

For the basic dev and local environments, we just have to add the volume as you suggested and change the mfe.unmounted conditional to always launch the mfe service if there are files to host. That is not a big deal, especially with your support on that.

My main consideration is how we'll handle the Kubernetes env. In our current approach, we use a configMapGenerator to bundle the static Paragon files and mount them as a volume to the pod. If we reuse the tutor-mfe pod, we'll need to find the best way to get our files into it. My only slight concern is that we might end up adding too much Paragon-specific logic to the tutor-mfe plugin, but it's an approach I'm happy to explore if you believe that won't be the case.

Also, just a quick heads-up: this change will naturally add to the scope of Milestone 4, as we'll need to document this new integration properly. It's not a blocker at all, just something to keep in mind for our timeline.

@brian-smith-tcril
Copy link
Contributor

My general thought is that hosting stylesheets that MFEs load at runtime falls well within the scope of the tutor-mfe plugin. I could see arguments that the token building aspect of this plugin is out of scope of the tutor-mfe plugin, but I would think the stylesheet hosting aspect would be quite uncontroversial.

I'm fine moving forward with either approach. If the first version of this plugin handles the stylesheet hosting independently and doesn't integrate with the tutor-mfe plugin that's OK with me. My main goal is that we don't build something that becomes difficult to integrate into the tutor-mfe plugin if/when we decide to do so in the future.

As for the k8s stuff I'll need to defer to @arbrandes - thoughts?

@Alec4r
Copy link
Contributor

Alec4r commented Jul 28, 2025

@brian-smith-tcril If I remember correctly, we discussed this in the past and agreed to keep it separate from tutor-mfe to avoid delays or potential controversy.

I'm okay with using Caddy, I believe it's the best approach. However, I'm not fully on board with adding any part of the tutor-paragon login logic directly into the tutor-mfe plugin. That would introduce a direct dependency from tutor-paragon to tutor-mfe, which I don't think is necessary.

In the end, we'd essentially be doing the same thing: adding a reverse proxy with Caddy for local and dev environments. And for Kubernetes, we’d still need to patch this file in tutor-mfe:
https://github.com/overhangio/tutor-mfe/blob/ff74ea3a883e3e336b60ecfb8e40e27bf8ffbf0a/tutormfe/patches/k8s-deployments#L28
to mount the volume for tutor-paragon, which would also require a patch in tutor-mfe.

@Ang-m4 could you try this approach? Maybe spend up to 2 hours on it and let me know if it definitely works or if it needs further adjustments.

@brian-smith-tcril
Copy link
Contributor

If I remember correctly, we discussed this in the past and agreed to keep it separate from tutor-mfe to avoid delays or potential controversy.

Yes, but there are different levels of separation. There also wasn't as much engagement on the forum post as I would have liked to see.

I'm not fully on board with adding any part of the tutor-paragon login logic directly into the tutor-mfe plugin. That would introduce a direct dependency from tutor-paragon to tutor-mfe, which I don't think is necessary.

This PR already includes logic that modifies the MFE config. I know that isn't a hard dependency on the tutor-mfe plugin, but I don't see the use case for this plugin for someone that doesn't have the tutor-mfe plugin installed.


The main benefit of the standalone approach in my opinion is that it allows the functionality to be built out before the conversations about what aspects of the functionality fit into the tutor-mfe plugin are had. I do want to make sure that we are building the standalone solution in a manner that makes integrating the functionality into tutor-mfe possible if that is the path we decide to go down later down the line (after this project is completed).

I'd still like to defer to @arbrandes, because it sounds like adding the ability to host extra css within the tutor-mfe caddy container won't be controversial.

@Ang-m4
Copy link
Contributor Author

Ang-m4 commented Jul 29, 2025

Hi @brian-smith-tcril,

After taking a closer look at the implementation of the tutor-mfe plugin, we decided to create the necessary patches for our plugin to work properly with the mfe container. We've opened a separate PR for those changes here: overhangio/tutor-mfe#264

I would appreciate it if you or @arbrandes could take a look at the proposed changes, especially those for the dev environment. Once that PR is approved and merged, I think we can move forward with this one.

Let me know what you think!

CC. @Alec4r

@sarina sarina changed the title feat: add paragon hosting configuration for dev and local environments feat: add paragon hosting configuration for dev and local environments (FC-87) Aug 28, 2025
@sarina sarina added the FC Relates to an Axim Funded Contribution project label Aug 28, 2025
@sarina
Copy link

sarina commented Aug 28, 2025

Now that overhangio/tutor-mfe#264 has been merged, what additional work does this PR require? Is this the last piece of work for FC-87?

@Ang-m4
Copy link
Contributor Author

Ang-m4 commented Aug 29, 2025

Hi @sarina, @brian-smith-tcril, thanks for following up!

Now that overhangio/tutor-mfe#264 has been merged, I updated this PR to add the tutor-mfe dependency and make full use of the new patches introduced there. I also included some integration tests to validate the hosted files setup.

The remaining piece for closing Milestone 3 is the Kubernetes environment integration, which I’m currently working on and will deliver shortly.

Regarding Milestone 4, we have this current PR in progress: #39

@Ang-m4
Copy link
Contributor Author

Ang-m4 commented Sep 1, 2025

Hi @sarina, @brian-smith-tcril

Just a quick note: I opened a new PR overhangio/tutor-mfe#267.

While working on the Kubernetes implementation, I realized that an additional patch was missing in order for the mfe-k8s-volumes feature to work properly.

This doesn’t affect the current PR, but it is necessary for the further Kubernetes implementation.

@brian-smith-tcril
Copy link
Contributor

brian-smith-tcril commented Sep 2, 2025

Edit: my mistake, I missed the tutor images build paragon-builder step after updating tutor and tutor-mfe to a version with the patch commit.

When trying to test this locally I ran into an issue with a docker pull. Any idea why that might be happening?

(tutorenv) bsmith@aximdev:~/code/tutorworkspace$ tutor dev do paragon-build-tokens
docker compose -f /home/bsmith/.local/share/tutor/env/local/docker-compose.yml -f /home/bsmith/.local/share/tutor/env/local/docker-compose.prod.yml --project-name tutor_local ls --format json
docker compose -f /home/bsmith/.local/share/tutor/env/local/docker-compose.yml -f /home/bsmith/.local/share/tutor/env/dev/docker-compose.yml --project-name tutor_dev -f /home/bsmith/.local/share/tutor/env/local/docker-compose.jobs.yml -f /home/bsmith/.local/share/tutor/env/dev/docker-compose.jobs.yml run --rm paragon-builder-job sh -e -c ''
[+] Running 1/1
 ✘ paragon-builder-job Error pull access denied for paragon-builder, repository does not exist or may require 'docker login': denied: requested access to the resource is denied                                                                                                                                                                                                                                                   5.5s 
Error response from daemon: pull access denied for paragon-builder, repository does not exist or may require 'docker login': denied: requested access to the resource is denied
Error: Command failed with status 1: docker compose -f /home/bsmith/.local/share/tutor/env/local/docker-compose.yml -f /home/bsmith/.local/share/tutor/env/dev/docker-compose.yml --project-name tutor_dev -f /home/bsmith/.local/share/tutor/env/local/docker-compose.jobs.yml -f /home/bsmith/.local/share/tutor/env/dev/docker-compose.jobs.yml run --rm paragon-builder-job sh -e -c 

@brian-smith-tcril
Copy link
Contributor

brian-smith-tcril commented Sep 2, 2025

I'm having trouble verifying this locally when following the instructions in https://github.com/eduNEXT/openedx-tutor-plugins/blob/afg/styles-hosting-setup/plugins/tutor-contrib-paragon/README.rst

I have been able to:

I haven't been able to:

  • ❌ See the theme applied when running an MFE

I have been unsure of:

  • ❔ What URL I should visit to verify the CSS is being hosted properly
  • ❔ When I need to tutor config save/tutor dev stop/tutor dev start

Hopefully some answers to these questions will help improve the documentation so future users of this plugin can have a smooth experience!

@Ang-m4
Copy link
Contributor Author

Ang-m4 commented Sep 2, 2025

Thanks for the detailed feedback, @brian-smith-tcril
I am sorry I missed the “How to test” section in the PR description. Currently we are working on Milestone 4: docs, which covers this kind of instructions at #39

http://<MFE_HOST>:<PORT>/static/paragon/themes/<theme>/<theme>.min.css

for example:

http://apps.local.openedx.io:8002/static/paragon/themes/light/light.min.css

Regarding when to execute the commands, I’d say it’s optional unless you want to overwrite some variables (e.g. PARAGON_ENABLED_THEMES=["light","dark"]). In that case, I would run tutor config save (with the --set flag) and then tutor dev stop && tutor dev start to make sure the changes are applied.

I’ll also add this clarification as part of Milestone 4, so we make sure the documentation is clearer for future users.

@brian-smith-tcril
Copy link
Contributor

brian-smith-tcril commented Sep 3, 2025

I was able to figure a bit more out!

Things that are definitely working:

Things I still haven't seen:

  • ❌ Theme being applied when running an MFE

After looking into the browser inspector I think I realize why this wasn't working. I noticed that the styles on the MFE didn't mention CSS variables at all. I then realized that this makes sense because in order to have overhangio/tutor-mfe@b4a783a in my local checkout of tutor-mfe I had to switch from using the main branch to the release branch, and the release branch doesn't use the design tokens versions of the MFEs.

I'll do a little more testing today but I expect it to all go smoothly now that I know what to look for! Thanks for being patient with me as I figured out my local dev environment issues!

@brian-smith-tcril
Copy link
Contributor

Progress!

I was able to get theme styles to load properly by:

When running tutor dev do paragon-build-tokens without --exclude-core I encountered strange styling

Screenshot From 2025-09-03 15-27-22

My original thought was that this may be an upstream issue with Paragon itself and not this plugin, but I encountered a similar issue with 2U's elm-theme which makes me a bit less sure.

I would expect a theme without core tokens to be able to load just the theme styles via CDN or this plugin and not require core to also be on the CDN/hosted in the plugin, but I would also expect it to not break in the case that core styles are hosted.


I then decided to test out building 2U's elm-theme. I grabbed the v1.11.1 release, ran tutor dev do paragon-build-tokens, and encountered an "Unexpected '/'. Escaping special characters with \ may help." error:

Full output
docker compose -f /home/bsmith/.local/share/tutor/env/local/docker-compose.yml -f /home/bsmith/.local/share/tutor/env/local/docker-compose.prod.yml --project-name tutor_local ls --format json
docker compose -f /home/bsmith/.local/share/tutor/env/local/docker-compose.yml -f /home/bsmith/.local/share/tutor/env/dev/docker-compose.yml --project-name tutor_dev -f /home/bsmith/.local/share/tutor/env/local/docker-compose.jobs.yml -f /home/bsmith/.local/share/tutor/env/dev/docker-compose.jobs.yml run --rm paragon-builder-job sh -e -c ''

Token collisions detected (1):
Use log.verbosity "verbose" or use CLI option --verbose for more details.
Refer to: https://styledictionary.com/reference/logging/

css
! /tmp/paragon-build.cDbhjk/core/variables.css, does not exist
! /tmp/paragon-build.cDbhjk/core/custom-media-breakpoints.css, does not exist
- /tmp/paragon-build.cDbhjk

css
⚠️ /tmp/paragon-build.cDbhjk/core/variables.css
While building core/variables.css, token collisions were found; output may be unexpected. Ignore this warning if intentional.

Use log.verbosity "verbose" or use CLI option --verbose for more details.
Refer to: https://styledictionary.com/reference/logging/
⚠️ /tmp/paragon-build.cDbhjk/core/custom-media-breakpoints.css
While building core/custom-media-breakpoints.css, token collisions were found; output may be unexpected. Ignore this warning if intentional.

Use log.verbosity "verbose" or use CLI option --verbose for more details.
Refer to: https://styledictionary.com/reference/logging/
[a11y] Warning: Failed to sufficiently darken token pgn-color-btn-hover-text-light to pass contrast ratio of 4.5:1.
    Background color: #87857e
    Attempted foreground color: #222222
[a11y] Warning: Failed to sufficiently brighten token pgn-color-btn-hover-text-warning to pass contrast ratio of 4.5:1.
    Background color: #998200
    Attempted foreground color: #ffffff

css
! /tmp/paragon-build.cDbhjk/themes/light/variables.css, does not exist
! /tmp/paragon-build.cDbhjk/themes/light/utility-classes.css, does not exist
! /tmp/paragon-build.cDbhjk/themes/light/overrides/component-button-variants.css, does not exist

css
✔︎ /tmp/paragon-build.cDbhjk/themes/light/variables.css
✔︎ /tmp/paragon-build.cDbhjk/themes/light/utility-classes.css
✔︎ /tmp/paragon-build.cDbhjk/themes/light/overrides/component-button-variants.css
Successfully created bundle: core
Error: Unexpected '/'. Escaping special characters with \ may help.
    at /tmp/paragon-build.cDbhjk/themes/light/overrides/component-button-variants.css:5:1
    at Root._error (/paragon-builder/node_modules/postcss-selector-parser/dist/parser.js:131:16)
    at Root.error (/paragon-builder/node_modules/postcss-selector-parser/dist/selectors/root.js:30:19)
    at Parser.error (/paragon-builder/node_modules/postcss-selector-parser/dist/parser.js:598:21)
    at Parser.unexpected (/paragon-builder/node_modules/postcss-selector-parser/dist/parser.js:612:17)
    at Parser.combinator (/paragon-builder/node_modules/postcss-selector-parser/dist/parser.js:526:12)
    at Parser.parse (/paragon-builder/node_modules/postcss-selector-parser/dist/parser.js:900:14)
    at Parser.loop (/paragon-builder/node_modules/postcss-selector-parser/dist/parser.js:856:12)
    at new Parser (/paragon-builder/node_modules/postcss-selector-parser/dist/parser.js:124:10)
    at Processor._root (/paragon-builder/node_modules/postcss-selector-parser/dist/processor.js:40:18)
    at Processor._runSync (/paragon-builder/node_modules/postcss-selector-parser/dist/processor.js:78:21) {
  postcssNode: <ref *1> Rule {
    raws: { before: '', between: ' ', semicolon: true, after: '\n' },
    type: 'rule',
    nodes: [
      [Declaration], [Declaration],
      [Declaration], [Declaration],
      [Declaration], [Declaration],
      [Declaration], [Declaration],
      [Declaration], [Declaration],
      [Declaration], [Declaration],
      [Declaration], [Declaration],
      [Declaration], [Declaration]
    ],
    parent: Root {
      raws: [Object],
      type: 'root',
      nodes: [Array],
      source: [Object],
      lastEach: 4,
      indexes: [Object],
      [Symbol(isClean)]: false,
      [Symbol(my)]: true
    },
    source: { input: [Input], start: [Object], end: [Object] },
    selector: '// Alert\n' +
      '\n' +
      '.pgn__alert-message-wrapper .pgn__alert-actions .btn-primary,\n' +
      '.pgn__alert-message-wrapper-stacked .pgn__alert-actions .btn-primary',
    lastEach: 2,
    indexes: {},
    proxyCache: [Circular *1],
    [Symbol(isClean)]: true,
    [Symbol(my)]: true
  }
}
Failed to build CSS bundle: light

Error: Command failed with status 1: docker compose -f /home/bsmith/.local/share/tutor/env/local/docker-compose.yml -f /home/bsmith/.local/share/tutor/env/dev/docker-compose.yml --project-name tutor_dev -f /home/bsmith/.local/share/tutor/env/local/docker-compose.jobs.yml -f /home/bsmith/.local/share/tutor/env/dev/docker-compose.jobs.yml run --rm paragon-builder-job sh -e -c 

I pulled the v1.11.1 tag down locally and was able to run npm run build without any issues. I checked the build-tokens line in elm-theme's package.json and saw --source-tokens-only. I then ran tutor dev do paragon-build-tokens --source-tokens-only and was able to build without errors.

I then encountered a similar styling issue to the one I encountered when using the minimal theme without --exclude-core

image

I decided to try to put the styles from the dist directory of the v1.11.1 release of elm-theme (https://registry.npmjs.org/@edx/elm-theme/-/elm-theme-1.11.1.tgz) directly in the corresponding /env/plugins/paragon/compiled-themes/ directories and encountered the same visual issue.


I'm not sure how much (if any) of the issues I encountered are actual problems with the plugin, and I definitely don't want to block this PR on things that are not the fault of this plugin.

I do, however, think that the 2 theme examples I tried are ones where having a "happy path" documented for this plugin would be great. That doesn't need to happen before this PR can land, but it'll be good to note any findings related to getting those working with this plugin during testing.

To merge this PR, I'd like to see a theme that includes core.min.css when compiled working with this plugin.

It's possible that this is user error on my part, so if there's something that I'm doing wrong please let me know! Also, if there's a theme that you have been testing with that includes core.min.css when compiled that is working as expected that I should try that'd be great too!

Thanks again for your patience! It's super exciting to see all of this come together!

@brian-smith-tcril
Copy link
Contributor

@Ang-m4 friendly ping! Just wondering if you saw my previous comment.

@brian-smith-tcril
Copy link
Contributor

For additional context: in prior testing I was able to get the 2U elm theme loading in from jsdelivr using the following tutor plugin

import json
from tutor import hooks

paragon_theme_urls = {
    "core": {
        "urls": {
            "brandOverride": "https://cdn.jsdelivr.net/npm/@edx/[email protected]/dist/core.min.css"
        },
    },
    "defaults": {
        "light": "light",
    },
    "variants": {
        "light": {
            "urls": {
                "default": "https://cdn.jsdelivr.net/npm/@openedx/paragon@$paragonVersion/dist/light.min.css",
                "brandOverride": "https://cdn.jsdelivr.net/npm/@edx/[email protected]/dist/light.min.css"
            }
        }
    }
}

fstring = f"""
MFE_CONFIG["PARAGON_THEME_URLS"] = {json.dumps(paragon_theme_urls)}
"""

hooks.Filters.ENV_PATCHES.add_item(
    (
        "mfe-lms-common-settings",
        fstring
    )
)

@Ang-m4
Copy link
Contributor Author

Ang-m4 commented Sep 9, 2025

Hi @brian-smith-tcril ,

I was able to replicate the broken views you mentioned. After checking the PARAGON_THEME_URLS structure, it seems like I was defining the setting wrong, we should be using brandOverrides, since that applies custom variables on top of Paragon’s defaults.

The issue was that default was being redefined under the core section, which caused the styles to break. By switching to brandOverrides, this conflict is avoided and the overrides apply cleanly on top of the core Paragon styles.

I tested this using the authn MFE, and it seems to be rendering correctly, I also tested the learning MFE with both elm-theme and your purple-branded test theme, all worked as expected.

Steps I used:

  • tutor dev/local do paragon-build-tokens --source-tokens-only (with 2U’s elm-theme)
  • tutor dev/local restart

Let me know if you spot any other details or concerns, and thanks a lot for taking the time to test everything so thoroughly.


image
image

@brian-smith-tcril
Copy link
Contributor

brian-smith-tcril commented Sep 9, 2025

That is great news @Ang-m4!

It sounds like the one last missing puzzle piece here is that default value. That is used to load the base Paragon styles. When that value isn't set, the version of Paragon bundled into the MFE is loaded.

Using bundled Paragon has a few drawbacks:

  • Each bundle has its own copy, so the user needs to download Paragon multiple times
  • The version of Paragon may not be as new as the one the theme author used when making their overrides, so new non-breaking features added to Paragon that the theme author is relying on might not exist in the bundled version.

This means that not using bundled Paragon should be the default.

This gets a little more complex when considering the $paragonVersion wildcard.

While it isn't something we hope for, it is definitely possible that MFEs in a release will have slightly different versions of Paragon bundled. Using the $paragonVersion wildcard means each MFE will request its installed version. This works seamlessly when using jsdelivr, as we can just use the URL structure of https://cdn.jsdelivr.net/npm/@openedx/paragon@$paragonVersion/dist/core.min.css.

I do think that using $paragonVersion as the default for default makes sense, but that means that we need to know what version of Paragon each MFE is using so we can host them all.

@Ang-m4
Copy link
Contributor Author

Ang-m4 commented Sep 9, 2025

@brian-smith-tcril, Got it, thanks for clarifying.

I’ll update default to point to:

https://cdn.jsdelivr.net/npm/@openedx/paragon@$paragonVersion/dist/core.min.css

so each MFE loads the Paragon version it ships with.

Quick question, I’m not that familiar with jsDelivr: can we be certain that all Paragon versions will be available there? When you say “we need to know what version of Paragon each MFE is using so we can host them all”, do you mean hosted in jsDelivr?

@brian-smith-tcril
Copy link
Contributor

brian-smith-tcril commented Sep 9, 2025

I’m not that familiar with jsDelivr: can we be certain that all Paragon versions will be available there?

Yes. If it's on npm it's on jsdelivr. Their homepage (https://www.jsdelivr.com/) has some good documentation.

When you say “we need to know what version of Paragon each MFE is using so we can host them all”, do you mean hosted in jsDelivr?

That is not what I mean, and that's what makes this tricky.


Thinking this through

One of the goals of this plugin is to allow site operators to host themes without relying on an external CDN. If we want this plugin to also support the goal of not needing to load the same version of Paragon base styles multiple times because Paragon is bundled in multiple MFEs, then we need to host those Paragon base styles here too.

So the tricky part is knowing what versions we need to host. I don't know a great way to do this off the top of my head, so any suggestions are more than welcome, but I can also do a bit of digging into tutor-mfe to see if I can think of a solid way to determine this.

If we can't figure out what versions we need beforehand, I can think of a few ways to handle this:

  1. Directly use the jsdelivr URL.
  2. Local cache of jsdelivr - if we don't have the requested version, download it from jsdelivr and host it
  3. Only host 1 version. This would basically be the local equivalent of setting the jsdelivr url to https://cdn.jsdelivr.net/npm/@openedx/paragon@23/dist/core.min.css (where it would get the latest version of Paragon 23 styles). I think this could get tricky. (This makes the least sense to me, so I think we should rule it out)
  4. Just don't set it. Let MFEs use their bundled base styles.

One thing that's nice about the frontend-platform implementation is that it will fall back to the bundled styles if it fails to load the Paragon base styles from the provided URL, so that makes these more viable.

Of the 3 non-crossed-out options, I think 2 matches the goal the most, but would also take the most effort to implement. I also have some of the same concerns about it as I do with option 1.

If we are only working with options 1 and 4, I have a hard time picking a default. Site operators that are comfortable loading styles from jsdelivr would provide the best experience to users with option 1, but having that as a default means site operators that aren't comfortable with that might accidentally do so.


An ideal world

An ideal scenario for me would be to have default for core set to something like {{ PARAGON_BASE_URL }}/base/$paragonVersion/core.min.css, and default for the light theme set to something like {{ PARAGON_BASE_URL }}/base/$paragonVersion/light.min.css, and have it "just work." That, however, would require making sure we have base/$paragonVersion directories and the correct files for every version of Paragon an MFE requests.


A path forward (aka. tl;dr - let's do this)

Realistically, however, I think our best path forward is to default to option 4 (Just don't set default. Let MFEs use their bundled base styles). This ensures nobody unexpectedly hits a CDN. It will be less performant for end users than using jsdelivr, but we can make note of that and provide documentation on other options.

Documentation we can provide:


I know this was a long one, thanks for being patient with me as I thought this through!

Copy link
Contributor

@brian-smith-tcril brian-smith-tcril left a comment

Choose a reason for hiding this comment

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

I'm approving this as it has worked in my testing and currently implements what I described in the "tl;dr - let's do this" section of my previous comment.

I have created a follow-up issue for adding documentation for site operators that want to use non-bundled base Paragon styles #44

@brian-smith-tcril brian-smith-tcril merged commit a9504cd into openedx:main Sep 10, 2025
3 checks passed
@github-project-automation github-project-automation bot moved this from Ready for Review to Done in Contributions Sep 10, 2025
Ang-m4 added a commit to eduNEXT/openedx-tutor-plugins that referenced this pull request Sep 15, 2025
…s (FC-87) (openedx#38)

* feat: add bundle minification build step for hosting

* feat: add paragon static server and caddy configuration

* feat: add configuration for Paragon theme URLs in dev and local

* feat: add conditional logic for serving compiled themes

* feat: update static server from nginx to caddy for serving compiled themes

* fix: update tutor dependency to remove upper version limit

* feat: update Paragon theme configuration and static server settings

- add an upper limit version for tutor dependency
- create a shared template for the mfe config on both dev and local envs
- update the commonly used port for the hosting service to 12400

* feat: add tutor-mfe service integration

* fix: integration test dependencies

* feat: add integration tests for hosted files

* fix(test): update LMS_HOST variable for hosting tests

* fix: update MFE_CONFIG to use 'brandOverride' for theme URLs

feat: add paragon static server and caddy configuration

feat: add conditional logic for serving compiled themes

feat: update static server from nginx to caddy for serving compiled themes

feat: update Paragon theme configuration and static server settings

- add an upper limit version for tutor dependency
- create a shared template for the mfe config on both dev and local envs
- update the commonly used port for the hosting service to 12400

feat: add tutor-mfe service integration
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

FC Relates to an Axim Funded Contribution project open-source-contribution PR author is not from Axim or 2U

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

7 participants