How I do frontend-base development

Since we’ve started landing the frontend-base conversions with the expectation that developers actually use them (I know, what a thought!), I figure this is an opportune time to showcase what my frontend dev flow looks like - with the caveat that it’s one way to do it, not the only way. As a matter of fact:

If you don’t care about frontend development outside Tutor, stop reading!

The builtin tutor-mfe plugin already automates everything: to work on apps so they’re bundled in with the others and with hot-reloading, all you need to do for each one (as per the README) is the usual:

tutor mounts add frontend-app-<name>

.
.
.

Still here? Ok. Let’s start with a question: why would anybody want to do frontend development outside Tutor?

Several possible reasons, a common one being: “I don’t use Tutor”. Mine is simpler: frontend-template-site sits upstream of Tutor, and as one of its maintainers, I want it, as well as apps themselves, to have an independent development story. This is my version of it.

With that in mind, thanks in advance for pointing out any shortcomings! (@braden already has, with consequential improvements.)


How I do frontend-base development

This is how I develop frontend-base and its apps: an npm workspace with local checkouts standing in for the published packages. A site repository is the workspace root and the checkouts live under packages/. All with auto-complete, linting, testing, hot reloading, and a build that stays as close to production as possible.

You’ll need the Node version from the repo’s .nvmrc (currently 24), sudo for the bind mounts, and Tutor for the backend.

1. Check out the repositories as siblings

Start by checking out every involved repo in the same parent directory.

cd ~/src/openedx
git clone https://github.com/openedx/openedx-platform.git
git clone https://github.com/openedx/frontend-base.git
git clone https://github.com/openedx/frontend-app-catalog.git
git clone https://github.com/openedx/frontend-app-authn.git
git clone https://github.com/openedx/frontend-app-learner-dashboard.git
git clone https://github.com/openedx/frontend-template-site.git frontend-site

Note: I like having openedx-platform around not only as reference for API endpoints, but also so I know exactly which version of the backend I’m developing against.

Note: frontend-template-site is checked out as frontend-site on purpose, since that’s the name Tutor expects. A symlink would do just as well.

2. Set up Tutor

The frontends need a backend to talk to. Since we’re developing against master, that means Tutor from main:

pip install git+https://github.com/overhangio/tutor@main
pip install git+https://github.com/overhangio/tutor-mfe@main
tutor plugins disable indigo
tutor config save --interactive
tutor mounts add ~/src/openedx/openedx-platform
tutor images build openedx-dev permissions mfe mfe-dev
tutor dev launch --skip-build --non-interactive
tutor dev stop

Indigo is enabled by default, but it’s not practical for upstream frontend development, so it goes. And to save time (and space!), I only build the images that are needed.

Launch takes care of the rest of the first-time setup. Stop it right after - we’ll bring the pieces we actually need back up in step 9.

Note: Whenever I update the openedx-platform checkout, I rebuild the openedx-dev image and re-run tutor dev launch --skip-build. Some updates don’t require this, but when in doubt…

3. Install dependencies in each checkout

Every frontend checkout gets its own node_modules. Repeat for each repo:

cd frontend-base
npm install

From this point on, I run one editor instance per repository, rooted at the checkout. Auto-complete, type-checking, linting, and tests all just work.

4. cd to the workspace

frontend-site will be the npm workspace root. Its packages/* glob is what will pull the local checkouts into the workspace, composing the shell, the apps, and the routing into something you can actually load in a browser.

cd frontend-site

Note: an app repository can serve as the workspace root too, if you’re only touching that app plus frontend-base and don’t need a site around it.

5. Create the workspace mount points

npm needs each workspace package to be a real directory under packages/. Symlinks won’t work.

You could just check out the apps in there - which gets you hot-reloading - but then you give up the per-repo editing/type-checking/linting. Enter bind mounts.

Create one empty directory per package you want to develop (doesn’t have to be all of them) named exactly after the sibling checkout:

mkdir -p packages/{frontend-base,frontend-app-catalog,frontend-app-authn,frontend-app-learner-dashboard}

Note: packages/ is gitignored, so none of this leaks into commits.

6. Create mount helpers

A simple sudo mount --bind ../frontend-base packages/frontend-base per package works, but there are two problems:

First, bind mounts don’t survive a reboot. Worth a script just for convenience.

Second, each sibling’s own node_modules comes along with the mount, so when you later run npm install in the site, it’ll be pruned in favor of the site’s. That breaks the per-repo dev setup.

This is the fix. I save it somewhere in my path - say, ~/bin/mount-packages - and make it executable:

#!/bin/bash
set -euo pipefail

for pkg_mount in packages/*; do
  pkg=$(basename "$pkg_mount")
  node_overlay=/tmp/${pkg}-node_modules
  node_mount=$pkg_mount/node_modules
  rm -fr "$node_overlay" && mkdir "$node_overlay"
  sudo mount --bind "../$pkg" "$pkg_mount"
  sudo mount --make-private "$pkg_mount"
  mkdir -p "$node_mount"
  sudo mount --bind "$node_overlay" "$node_mount"
done

Explanation: each package’s bind mount gets an empty node_modules overlay. It’s confined to the workspace via --make-private, so npm running from frontend-site can’t affect the sibling’s. Code changes made on the sibling, however, show up immediately in the workspace.

I also have a ~/bin/umount-packages which tears everything down:

#!/bin/bash
set -uo pipefail

for pkg_mount in packages/*; do
  pkg=$(basename "$pkg_mount")
  sudo umount "$pkg_mount/node_modules"
  sudo umount "$pkg_mount"
  rm -fr "/tmp/${pkg}-node_modules"
done

7. Mount the checkouts

From the workspace root, mount everything named under packages/:

mount-packages

8. Install the workspace

Now that the packages are visible, npm can gather all of their dependencies into one shared node_modules at the root. Start from a clean slate:

rm -fr node_modules
npm install

9. Start Tutor without frontend-site

tutor mounts add ~/src/openedx/frontend-site
tutor dev start -d mfe

This disables Tutor’s internal frontend-site in favour of our checkout. And since mfe-dev is explicitly not started, we also forego Tutor’s npm run dev - I prefer to run it myself:

10. Start the dev server

Packages need building in dependency order (i.e., @openedx/frontend-base before everything else), which npm won’t do on its own. However, there’s a built-in command that solves this with Turborepo:

npm run dev:packages

Every package is now watch-built in the right order, and the site picks up the results. Edit anything anywhere and the browser immediately hot-reloads at the usual https://local.openedx.io:8000.

11. Tear down when you’re done

Unmounting isn’t enough on its own: package-lock.json still records the workspace resolutions, so npm would keep wiring the packages up locally. Unmount, clear the lockfile and node_modules, then clean-install:

umount-packages
rmdir packages/*
rm -fr node_modules
git checkout package-lock.json
npm clean-install

Reference

Some more potentially useful information:

Workspace scripts

  • npm run dev:packages - watch-build all workspace packages and start the dev server.
  • npm run build:packages - build all workspace packages in dependency order, once.
  • npm run clean:packages - run each workspace package’s clean.
  • npm run dev:site - bin-link, then start the dev server without watching packages.
  • npm run dev - plain dev server, published dependencies only.

Version placeholders

In their repositories, @openedx/ packages all have the version 0.0.0-dev. The real version number is only assigned at release time. That placeholder doesn’t match a range like ^2.0.0, though, so npm would refuse to use a local checkout. To let it, every @openedx/ dependency also accepts 0.0.0-dev:

"@openedx/frontend-base": "^2.0.0-alpha || 0.0.0-dev"

If you add a new @openedx/ package that you intend to check out as a workspace, add the same suffix.

Bin-linking

npm doesn’t bin-link workspace packages during install, so the openedx CLI that frontend-base ships won’t be in node_modules/.bin after npm i. The workspace root’s own Makefile has a bin-link target that fixes it:

npm rebuild --ignore-scripts @openedx/frontend-base

build:packages, dev:packages, and dev:site all run it first, so you generally don’t need to.

Publishing

Nothing needs undoing before you publish or open a PR. In the workspace checkouts, package.json and the lockfile are never rewritten to point at a local path, so there is no package-*.json cleanup to do.


That’s it!

The how-to is also published to the wiki.

Bump!

After some feedback, I updated the intro to make it clear who this is for, or perhaps more importantly, who this is not for: steer clear if you don’t care about frontend development outside Tutor.