You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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):
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.)
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:
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:
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
--pgn-size-btn-focus-width (defined in core/components/Button/core.json) ← referenced ~18× by themes/light/components/IconButton.json (box-shadow.spread).
--pgn-size-input-btn-focus-width (defined in core/components/general/input.json) ← referenced by themes/light/components/general/input.json:16.
--pgn-spacing-form-input-padding-y-base (defined in core/components/Form/spacing.json) ← referenced by themes/light/components/Form/other.json:107.
--pgn-spacing-input-btn-padding-y (defined in core/components/general/input.json) — pulled in transitively via Setting up checkbox #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:
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.
Note
Generated by Claude
Summary
When a brand runs
paragon build-tokens --source-tokens-onlyagainst a source tree that contains zero theme-variant source tokens, the resultingthemes/<variant>/variables.csscontains--pgn-*declarations the brand never authored. Four core Paragon tokens (sometimes more, depending on the brand) leak into the output via theisReferencedByThemeVariantannotation, because that annotation is triggered by Paragon's own bundledthemes/light/**files (which are pulled in viacoreConfig.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):
After
npm run build-tokens:Expected:
paragon/css/themes/light/variables.csscontains 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 inapps/some-plugin/variables.cssinstead.)Actual:
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 publisheddist/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-yare 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, thepgn-annotate-token-extensions-with-referencespreprocessor (lines ~181–205) registers two extension annotations:filter(gates the referencing token)isReferencedBySourceTokentkn => tkn.isSourceisReferencedByThemeVarianttkn => 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 owntokens/src/themes/light/**files, which are pulled into the build viacoreConfig.includeeven 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:The
|| isReferencedByThemeVariantbranch 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 thevar(...)reference resolves at runtime. But because the annotation isn't gated onisSource, 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-basedrags--pgn-spacing-input-btn-padding-yalong with it.The four leak chains, concretely
--pgn-size-btn-focus-width(defined incore/components/Button/core.json) ← referenced ~18× bythemes/light/components/IconButton.json(box-shadow.spread).--pgn-size-input-btn-focus-width(defined incore/components/general/input.json) ← referenced bythemes/light/components/general/input.json:16.--pgn-spacing-form-input-padding-y-base(defined incore/components/Form/spacing.json) ← referenced bythemes/light/components/Form/other.json:107.--pgn-spacing-input-btn-padding-y(defined incore/components/general/input.json) — pulled in transitively via Setting up checkbox #3, sinceform.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 nothemes/light/**file happens to reference them.Suggested fix
Gate
isReferencedByThemeVariantonisSource, mirroring the gating onisReferencedBySourceToken:{ 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-onlywhose only source tokens are app tokens that don't reference any Paragon core/theme tokens (e.g. literal-valuedapps/<name>/tokens, since Style Dictionary needs at least one source token to drive the build), the resultingthemes/<variant>/variables.cssshould contain no--pgn-*declarations. The qualifier matters: if an app token did reference a Paragon token, that core token would correctly be emitted via theisReferencedBySourceTokenchain.