feat!: enforce CSS ownership with cascade layers#234
Conversation
95d3dd2 to
d44b17a
Compare
1171ddf to
dd97151
Compare
Apps must not re-bundle shell or brand styles in their runtime SCSS. Updates the migration guide, theming guide, and stylesheet-import ADR, and revises the test-site reference to demonstrate the correct cascade. Adds a cascade-layer mechanism so sites and brand packages can reliably style apps: without layers, app CSS loaded later (via lazy loading) wins over brand and site overrides at equal specificity, leaving apps effectively un-brandable. The webpack pipeline now wraps every stylesheet in paragon/shell/app/site/brand layers by resolved path, so site and brand always out-rank app styles. Brand is placed last so build-time brand imports match the precedence of runtime brand CSS, which is injected unlayered via <link> tags and thus beats every layered rule. BREAKING CHANGE: Apps no longer export their SCSS through package.json. Sites that imported app stylesheets directly must stop doing so; app styles are loaded by the app itself at runtime. BREAKING CHANGE: SCSS entries using @media (--pgn-size-breakpoint-*) must now @use Paragon's custom-media-breakpoints.css themselves, since each PostCSS pass is its own scope and a missing import fails silently. BREAKING CHANGE: The shell's styles are now exposed as a JS manifest at @openedx/frontend-base/shell/style instead of a SCSS file. Sites must replace '@use \"@openedx/frontend-base/shell/style.scss\"' with 'import \"@openedx/frontend-base/shell/style\"' from their site.config. The manifest keeps Paragon's CSS and the shell's own SCSS as independent webpack modules so each is classified into its own cascade layer. Refs openedx#232 Co-Authored-By: Claude <noreply@anthropic.com>
brian-smith-tcril
left a comment
There was a problem hiding this comment.
Overall this makes a lot of sense. I'm a big fan of the ADR, it definitely makes the "where do styles live and where are they loaded from?" story a lot clearer.
I left a couple comments, a few little questions and a nit.
|
Brian, I just finished testing it via openedx/frontend-template-site#14 (and frontend-base npm run dev, and test-site run dev). It's really nice seeing the layers in action in the Firefox inspector. :) |
brian-smith-tcril
left a comment
There was a problem hiding this comment.
This is great. Looking at the frontend-template-site PR and realizing that the site.scss
@use '@openedx/frontend-base/shell/app.scss' as shell;
@use '@openedx/brand-openedx/core.min.css';
@use '@openedx/brand-openedx/light.min.css';could be shuffled completely and it would all still work is awesome.
The separation of concerns is huge:
site.scss:@usethe things you need to have in the bundle, order doesn't matterpostcssWrapLayer: figure out all the order stuff
|
🎉 This PR is included in version 1.0.0-alpha.33 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Description
Without cascade layers, CSS from lazy-loaded apps is injected after the site's stylesheet and wins over brand and site overrides at equal specificity. The practical consequence is that apps cannot be reliably styled by a brand package or by the composing site: tokens get clobbered and overrides silently lose.
This PR wraps every stylesheet in one of five cascade layers based on its resolved path:
paragonfor@openedx/paragon,shellfor@openedx/frontend-base,appfor anything else fromnode_modules,sitefor the composing site's own source, andbrandfor@(open)?edx/brand*packages. The declared order@layer paragon, shell, app, site, brand;ships at the top ofshell/style.scss, so site and brand always out-rank app styles regardless of load order.brandis placed last so build-time brand imports match the precedence of runtime brand CSS, which is injected unlayered via<link>tags and therefore beats every layered rule. A small PostCSS plugin (with unit tests) handles the wrapping at build time.The shell's styles are now exposed as a JS manifest at
@openedx/frontend-base/shell/stylerather than a single SCSS file. Each import in the manifest (Paragon core, Paragon light, and the shell's own SCSS) is its own webpack module, so each is classified into its own layer by resolved path. This also removes the need for sites to maintain a top-levelsite.scss: the site.config file can import the manifest directly.The stylesheet-import ADR, theming guide, and migration guide are updated to describe the new layering. The test-site reference is updated to demonstrate the correct cascade.
Breaking changes
Apps no longer export their SCSS through
package.json. App stylesheets are an internal implementation detail, imported from the app's own component code and loaded by the app at runtime. Sites that imported app stylesheets directly (e.g.,@use '@some-org/frontend-app-foo/src/style.scss') must stop doing so.SCSS entries using
@media (--pgn-size-breakpoint-*)must now@use '@openedx/paragon/styles/css/core/custom-media-breakpoints.css'themselves. Each PostCSS pass is its own scope, so a missing import fails silently: the unresolved@mediarule never matches any viewport. This applies to every top-level app stylesheet and every component-level.scssimported directly from JS/TS.The shell's styles are now exposed as a JS manifest at
@openedx/frontend-base/shell/styleinstead of a SCSS file. Sites must replace@use '@openedx/frontend-base/shell/style.scss'withimport '@openedx/frontend-base/shell/style'from their site.config. A separate top-levelsite.scssis no longer required.Refs #232.
LLM usage notice
Built with assistance from Claude.