Friction points in Paragon

Hello everyone.

For the past week, I’ve been working on @edly-io/brand-openedx for Paragon 23, and I’ve hit a few friction points I’ve ranted about in the #general Slack channel.

I’ve been told to share them in a more appropriate place, so I’m writing them down here, in no particular order:

Error Messages

If you create a theme by any name other than light, and define a colour design token called primary:

{
  "$type": "color",
  "color": {
    "primary": {
      "base": {
        "$value": "#15376D"
      }
    }
  }
}

the following error occurs on npm run build-tokens:

css
✔︎ ./paragon/build/core/variables.css
✔︎ ./paragon/build/core/custom-media-breakpoints.css
An error occurred: TypeError: Cannot read properties of undefined (reading 'default')

The error message is meaningless, there’s no stack trace, no file name, no line number, no key path. (Here, it’s easy to see the culprit is the primary colour, but with a real-world color.json file, I had to bisect the JSON, which is an ordeal because it doesn’t support comments. JSON should never be used as a configuration format.)

The exact same file in a folder called light instead of dark is processed without error, because it inherits Paragon’s light theme tokens.

You’d think it’s looking for a default value for color.primary.base, because you’ve seen it defined in some design token files, but no. It’s looking for color.primary.base.actions.default (notice how it doesn’t complain about the missing actions object, only its default key.) It has to be defined like this:

{
  "$type": "color",
  "color": {
    "primary": {
      "base": {
        "$value": "#15376D",
        "actions": {
          "default": "{color.action.default.primary.base}"
        }
      }
    },
    "action": {
      "default": {
        "primary": {
          "base": {
            "modify": [
              {
                "type": "darken",
                "amount": "0.1"
              }
            ],
            "$value": "{color.primary.base}"
          }
        }
      }
    }
  }
}

I don’t understand this at all. If it’s not inheriting from the light theme, why is it looking for an actions object? And if it is, why is it not finding it? A colour named differently is perfectly happy without any of this.

But the bigger problem is not this missing actions object, it’s the uselessness of the error message. It’s just impossible to guess where the problem lies when such an error occurs, and the --verbose flag is no help.

Interrupting the Build

The build-scss command cannot be interrupted. It doesn’t respond to signals. When it takes two minutes and up to build with the core, it’s a problem. Adding process.on( SIG(TERM|INT), ... ) in the code does not help, it’s the sass compilation that hogs the shell.

Missing Folder

build-scss does not create its destination folder, dist/. If you invoke the command after removing the folder, it will display an error, but will still tread on for two minutes compiling the SCSS, before failing and discarding it. And if you’re fortunate to see the error, remember, you can’t interrupt the process anyway.

Wrong and meaningless argument help text

Some help text is wrong. Other is just not helpful.

Here the help text for --source is the one for --replacementType:

replace-variables
  -s, --source Default: ''
    Type of replacement: usage or definition. If set to "definition" the command will only update SCSS variables definitions with CSS variables, if set to "usage" - all occurrences of SCSS variables will we replaced

  -t, --replacementType [usage|definition], Default: definition
    Type of replacement: usage or definition. If set to "definition" the command will only update SCSS variables definitions with CSS variables, if set to "usage" - all occurrences of SCSS variables will we replaced

This does not mean anything:

build-tokens
  --source-tokens-only Default: false
    Include only source design tokens in the build.

This gives no indication about what it actually does, or why I would desire this, or not:

build-scss
  --defaultThemeVariants Default: light
    Specifies default theme variants. Defaults to a single 'light' theme variant.
    You can provide multiple default theme variants by passing multiple values, for
    example: `--defaultThemeVariants light dark`

Moreover, commands will happily accept and ignore flags it doesn’t know about, making typos difficult to detect.

Should I open issues on the repo? I don’t think I can do PRs, except maybe for the --source option of replace-variables, because I don’t do node development.

I wonder if some of your issues is that (iirc) the edly brand package doesn’t use design tokens - I believe this sample brand package is what leverages DTs - sample-plugin/brand at main · openedx/sample-plugin · GitHub

cc @brian.smith

First and foremost @oscherler thank you for reporting these issues!

While Paragon 23/design tokens have been in use by mutliple organizations for a while now, most of the people who have interacted with this have been directly involved with building out design tokens functionality, or following along with the progress very closely. Understanding these pain points is vital for providing a good theme author experience in Paragon 23.

I’m sorry this experience has been painful for you, and I hope we can use these learnings to ensure future theme authors don’t suffer from the same pain points!


Some of the issues reported in this are directly related to tokens. It’s possible some of the problems are coming from non-token styles, but I’d like to try to better understand each of the reported pain points.


Do you have a minimal repro for this? I tried cloning latest GitHub - openedx/brand-openedx and adding that exact file to paragon/tokens/src/themes/other/global/color.json ran npm run build-tokens with both an unmodified version of the command in brand-openedx’s package.json and with it modified to be

- "build-tokens": "paragon build-tokens --source ./paragon/tokens/ --build-dir ./paragon/build -t light",
+ "build-tokens": "paragon build-tokens --source ./paragon/tokens/ --build-dir ./paragon/build -t other",

and encountered no errors.


I was able to reproduce this and have created an issue on the Paragon repo to track it Can't interrupt `build-scss` · Issue #3949 · openedx/paragon · GitHub


I’ll need to do some investigation to see why this isn’t integrated into the build-scss command. The Makefile in brand-openedx currently has a line that seems to be working around that limitation.

.PHONY: build
build:
	rm -rf dist && mkdir dist
	npm run build-tokens
	npm run build-scss

I made an issue on the Paragon repo to track this `build-scss` requires `dist` directory exists, still runs after showing error · Issue #3950 · openedx/paragon · GitHub


Great catch. We have useful info in lib/replace-variables.js, but the strings you’re seeing are coming from bin/paragon-scripts.js and appear to be copypasta. I made an issue on the Paragon repo for it incorrect help text for `replace-variables` · Issue #3951 · openedx/paragon · GitHub


It seems we used to have useful help text for this one, but it got lost in [BD-46] feat: move CLI design tokens commands to paragon CLI by monteri · Pull Request #2609 · openedx/paragon · GitHub

Before that PR, build-tokens.js had:

.option('--source-tokens-only', 'If provided, only tokens from --source will be included in the output; Paragon tokens will be used for references but not included in the output.')

I’ve created an issue for this on the Paragon repo improve help text for `source-tokens-only` · Issue #3952 · openedx/paragon · GitHub


This is definitely not well documented. I created an issue on the Paragon repo for it document `defaultThemeVariants` · Issue #3953 · openedx/paragon · GitHub


Great call out. I made an issue on the Paragon repo for it [CLI] add error handling for incorrect arguments · Issue #3954 · openedx/paragon · GitHub


Thanks again @oscherler for reporting all of this! This feedback is hugely appreciated and will go a long way towards improving developer experience for theme authors!

Sure, here you go: openedx-repros/paragon-actions at main · obscherler/openedx-repros · GitHub

Thank you for the minimal repro! I was able to reproduce the error when running

nvm use
npm ci
npm run build-other

in there.


I do agree that the error message of

An error occurred: TypeError: Cannot read properties of undefined (reading 'default')

isn’t helpful. The error is coming from style-dictionary, and our error logging was hiding the stack.

I made a PR that updates Paragon’s error handling to instead display

An error occurred: TypeError: Cannot read properties of undefined (reading 'default')
TypeError: Cannot read properties of undefined (reading 'default')
    at /home/bsmith/code/openedx-repros/paragon-actions/node_modules/@openedx/paragon/tokens/style-dictionary.js:291:68
    at Array.forEach (<anonymous>)
    at Object.format (/home/bsmith/code/openedx-repros/paragon-actions/node_modules/@openedx/paragon/tokens/style-dictionary.js:281:17)
    at async StyleDictionary.formatFile (file:///home/bsmith/code/openedx-repros/paragon-actions/node_modules/style-dictionary/lib/StyleDictionary.js:734:30)
    at async Promise.all (index 1)
    at async StyleDictionary.formatPlatform (file:///home/bsmith/code/openedx-repros/paragon-actions/node_modules/style-dictionary/lib/StyleDictionary.js:834:28)
    at async StyleDictionary.buildPlatform (file:///home/bsmith/code/openedx-repros/paragon-actions/node_modules/style-dictionary/lib/StyleDictionary.js:919:19)
    at async Promise.all (index 0)
    at async StyleDictionary.buildAllPlatforms (file:///home/bsmith/code/openedx-repros/paragon-actions/node_modules/style-dictionary/lib/StyleDictionary.js:952:7)
    at async /home/bsmith/code/openedx-repros/paragon-actions/node_modules/@openedx/paragon/lib/build-tokens.js:171:5

That being said, I’m not sure how helpful that stack would have been for addressing your issue. I can look into more style-dictionary logging options, but from what I can tell Paragon’s --verbose flag for build-tokens should already be setting that properly.


I was able to fix the error (edit: this change makes it so the css doesn’t include values from the tokens) in your minimal repro repo for other by comparing package.json to the one in the brand-openedx repo.

diff --git a/paragon-actions/package.json b/paragon-actions/package.json
index 9946db0..44f76ff 100644
--- a/paragon-actions/package.json
+++ b/paragon-actions/package.json
@@ -2,7 +2,7 @@
   "name": "paragon-action-repro",
   "scripts": {
     "build-light": "paragon build-tokens --source ./paragon/tokens/src --build-dir ./paragon/build --themes light --verbose",
-    "build-other": "paragon build-tokens --source ./paragon/tokens/src --build-dir ./paragon/build --themes other --verbose",
+    "build-other": "paragon build-tokens --source ./paragon/tokens --build-dir ./paragon/build --themes other --verbose",
     "build-fixed": "paragon build-tokens --source ./paragon/tokens/src --build-dir ./paragon/build --themes fixed --verbose"
   },
   "devDependencies": {

I don’t think the call stack from the style-dictionary error would have helped me find that fix, and I’m not sure what Paragon could be checking to catch this earlier.

Edit: The call stack is helpful.


I’m continuing my investigation…

It looks like this line is the culprit:

const ref = sdUtils.getReferences(token.original.actions.default, dictionary.tokens)[0];

I’m doing more digging into that now.

I made an issue for the actions being undefined problem color tokens without `actions`? · Issue #3962 · openedx/paragon · GitHub

I documented what I’ve found so far there.