openedx / openedx/paragon

`--source-tokens-only` builds leak core tokens transitively referenced from Paragon's bundled `themes/light/` files

Open
#4,281 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
140
Forks
100
Avg merge
1h 3m
Merged PRs (30d)
30

Description

[!NOTE]
Generated by Claude

Summary

When a brand runs paragon build-tokens --source-tokens-only against a source tree that contains zero theme-variant source tokens, the resulting themes/<variant>/variables.css contains --pgn-* declarations the brand never authored. Four core Paragon tokens (sometimes more, depending on the brand) leak into the output via the isReferencedByThemeVariant annotation, because that annotation is triggered by Paragon's own bundled themes/light/** files (which are pulled in via coreConfig.include) rather than by the consumer's source tokens.

Reproduction

A minimal brand whose only source token is an app token with a literal value (no references):

brand/
  tokens/src/apps/some-plugin/foo.json    # one $value: "11" $type: "number" token
package.json scripts:
  build-tokens: paragon build-tokens --source ./tokens/src --build-dir ./paragon/css --source-tokens-only --exclude-core

After npm run build-tokens:

Expected: paragon/css/themes/light/variables.css contains no --pgn-* declarations, since the brand declares no theme-variant source tokens and its sole app token doesn't reference any Paragon tokens. (The app token correctly lands in apps/some-plugin/variables.css instead.)

Actual:

:root {
  --pgn-size-btn-focus-width: 2px;
  --pgn-size-input-btn-focus-width: 1px;
  --pgn-spacing-input-btn-padding-y: 0.5625rem;
  --pgn-spacing-form-input-padding-y-base: var(--pgn-spacing-input-btn-padding-y);
}

None of these declarations come from the brand. They come from tokens/src/core/components/.

The same leak is visible in @openedx/brand-openedx's published dist/light.css — there the four tokens still appear, but the symptom is ordering: --pgn-size-btn-focus-width, --pgn-size-input-btn-focus-width, and --pgn-spacing-input-btn-padding-y are at lines 7–9, ahead of every other size/spacing token in the file. They're emitted out-of-band, ahead of their natural sort cohort, because the annotation mutates them before the rest of the traversal. The contents are the same as a full build would produce, so the visible symptom is just ordering — but the underlying leak is identical.

Root cause

tokens/style-dictionary.js, the pgn-annotate-token-extensions-with-references preprocessor (lines ~181–205) registers two extension annotations:

Annotation filter (gates the referencing token)
isReferencedBySourceToken tkn => tkn.isSource
isReferencedByThemeVariant tkn => themes.some(theme => tkn.filePath.includes(theme))

The first is correctly gated on isSource, so a referencing token only triggers annotation if it's part of the consumer's source. The second isn't — it fires for any token whose filePath contains a theme name. That includes Paragon's own tokens/src/themes/light/** files, which are pulled into the build via coreConfig.include even when the consumer has no theme source files of their own.

The output filter isSource.<themeVariant> (registered around lines 446–461) then has this clause:

return token.filePath.includes(themeVariant) || isReferencedByThemeVariant;

The || isReferencedByThemeVariant branch is correct in intent — when a brand does override a theme token whose value resolves to {spacing.input.btn.padding.y}, the underlying core token needs to be emitted too so the var(...) reference resolves at runtime. But because the annotation isn't gated on isSource, the bypass also fires for core tokens referenced by Paragon's bundled theme files.

The walker in tokens/utils.js (annotateReferencedTokenExtensions) is also intentionally transitive — once a referenced token is marked, its own references are pushed onto the stack and follow the chain, which is why --pgn-spacing-form-input-padding-y-base drags --pgn-spacing-input-btn-padding-y along with it.

The four leak chains, concretely

  1. --pgn-size-btn-focus-width (defined in core/components/Button/core.json) ← referenced ~18× by themes/light/components/IconButton.json (box-shadow.spread).
  2. --pgn-size-input-btn-focus-width (defined in core/components/general/input.json) ← referenced by themes/light/components/general/input.json:16.
  3. --pgn-spacing-form-input-padding-y-base (defined in core/components/Form/spacing.json) ← referenced by themes/light/components/Form/other.json:107.
  4. --pgn-spacing-input-btn-padding-y (defined in core/components/general/input.json) — pulled in transitively via #3, since form.input.padding.y.base's value is {spacing.input.btn.padding.y}.

Other similar core tokens (e.g. input-btn-padding-x, input-btn-padding-y-sm) don't leak because no themes/light/** file happens to reference them.

Suggested fix

Gate isReferencedByThemeVariant on isSource, mirroring the gating on isReferencedBySourceToken:

 {
   name: 'isReferencedByThemeVariant',
-  filter: tkn => themes.some(theme => tkn.filePath.includes(theme)),
+  filter: tkn => tkn.isSource && themes.some(theme => tkn.filePath.includes(theme)),
   referenceTokenFilter: tkn => !themes.some(theme => tkn.filePath.includes(theme)),
 },

That keeps the legitimate brand-override case working — when a brand declares a source theme token that references a core token, the annotation still propagates — but stops Paragon's own bundled theme files from triggering it.

Suggested test

Given a fixture brand built with --source-tokens-only whose only source tokens are app tokens that don't reference any Paragon core/theme tokens (e.g. literal-valued apps/<name>/ tokens, since Style Dictionary needs at least one source token to drive the build), the resulting themes/<variant>/variables.css should contain no --pgn-* declarations. The qualifier matters: if an app token did reference a Paragon token, that core token would correctly be emitted via the isReferencedBySourceToken chain.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in tokens/style-dictionary.js at the pgn-annotate-token-extensions-with-references preprocessor and inspect annotateReferencedTokenExtensions in tokens/utils.js. Run the source-tokens-only fixture described in the issue, then verify that themes//variables.css contains no --pgn-* declarations for a literal-only app token while referenced brand theme tokens still resolve correctly.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
build-system
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.