That helps. Solo now with room to grow into a small same-team group is a nice spot for a monorepo. Here's what we've landed on since starting custom widget dev back in December, for whatever it's worth.
Monorepo vs per-widget repo
I'd go monorepo. At your scale the benefits stack up and the downsides barely register:
- Atomic cross-widget changes. We recently hit a broken ExB 1.20 method (getSelectedRecords() returning []) that affected three widgets. One commit with shared utility updates beat the heck out of coordinating three repos with pinned dependencies.
- One version-bump story (increment the version number). When our shared-code/ changes, we bump the consuming widgets in the same commit. In a multi-repo world that's coordinated PRs and republishes.
- One webpack config, one test runner, one lint config. ExB's build tooling is opinionated enough that duplicating it per-repo gets old fast.
- Easy absorption of outside widgets. Since you mentioned possibly pulling in someone else's source, a monorepo makes that a drop-in rather than a new repo plus CI plus release dance.
The usual monorepo knocks are blast radius and noisy git history, but those mostly show up at bigger team sizes or when multiple product teams share the repo. For a single team it's a non-issue.
Where to put the git repo
This is where I'd do things differently than we did. Do NOT put .git at the ExB install folder level. We did, and:
- server/public/apps/ is full of your local app configs. Branch switches can clobber hours of builder work.
- node_modules shows up as git noise.
- We ended up writing a whole "separate clone" process doc to safely deploy to a public share repo, because the install folder is too dangerous to check out other branches in.
Put the repo at client/your-extensions/widgets/. That's your actual source. The ExB install is a runtime, not source code. Track just the widgets directory and mount it into the install folder via symlink or rsync when you need to run.
shared-code vs npm packages
We use the ExB-style shared-code/ directory (compiled as its own webpack entry, loaded at runtime). It's worked well for us, with honest caveats:
- ✅ Zero publish overhead. Change it, rebuild, done.
- ✅ ExB's loader handles the runtime import without extra config if you follow the convention.
- :warning: All widgets always use the same version of shared-code. This cuts both ways. Fix a bug or ship a speed improvement in shared-code and every widget gets it for free, which is great. The flip side is you can't hold one widget back on an older version while you update another, so any change has to be tested against every widget that uses it.
- ❌ You can't reuse it outside the project without copy-paste.
- ❌ Debug source maps across the widget/shared-code boundary can be finicky.
Rule of thumb: use shared-code/ if the utilities are tightly coupled to your widget patterns (ExB-specific helpers, your domain model, widget config types). Use a proper npm package (private registry or GitHub Packages) if the code is genuinely reusable, like a general markdown renderer or a color utility that could land in a non-ExB project someday. We've got some shared stuff that should really be npm packages and isn't yet, and we feel that friction whenever another project could use it.
A note on versioning
ExB gives you a version field in manifest.json but leaves the policy entirely up to you. Everyone has their own flavor on this, so take or leave what follows: if you go monorepo plus shared-code/, it's worth deciding up front what you want the version field to mean. Our rule is "when shared-code changes, bump every widget that uses it" so that the manifest version tells us whether a deployed widget has a given shared-code behavior. It's a little tedious to maintain but beats the "is this widget on shared-code v3 or v2?" guessing game. Your mileage may vary depending on how you deploy and track what's in production.
I hope this helps. I’d be happy to discuss any of this in more detail 😊