Custom Widget Version Control Best Practices

403
5
Jump to solution
3 weeks ago
DavidSolari
MVP Regular Contributor

I'm getting started with custom widget development and I want to make sure I'm handling my source data as best as possible:

  1. For a single organization, should I do a monorepo or one repository per widget?
  2. At what folder "level" do I create the repository? I assume for multi-repository workflows each widget folder is a repository but how high up should you go for a monorepo?
  3. The docs mention a "shared-code" directory that isn't compiled into a widget but can hold common code for all widgets. Is it a good idea to use that or to create standard node packages and load them as standard dependencies? Does this change depending on how you structure the repositories?
Tags (2)
0 Kudos
1 Solution

Accepted Solutions
adamsimple
Occasional Contributor

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 😊

View solution in original post

Tags (1)
5 Replies
adamsimple
Occasional Contributor

One question before I get into specifics: how many devs are working on these widgets, and are they all on the same team or spread across teams? My advice shifts depending on the answer, especially on monorepo vs multi-repo.

DavidSolari
MVP Regular Contributor

Right now I'm the sole widget dev on the team, but if it's not too onerous for one guy I'd like to have something that supports multiple devs on the same team for the future. What I can say for sure is it's not multi-team, if we need to integrate someone else's widget source into our collection I'm fine with dumping it into a monorepo.

0 Kudos
adamsimple
Occasional Contributor

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 😊

Tags (1)
DavidSolari
MVP Regular Contributor

This is an incredible amount of info, thank you so much! This should be more than enough for me to get started.

0 Kudos
SunshineLuke90
Occasional Contributor

I'll share my own experience, and link to my personal widgets and apps repositories, which have CI/CD set up for app deployments, and widget deployments. I'll also note that Esri recommends a slightly different approach than what I've been doing, as they recommend tracking the whole "your-extensions" folder at once, using a custom manifest.json within the git repo, rather than just having the widgets folder.

What I've been doing:

I've taken an approach that enables full CI/CD across apps and widgets, using 2 repos (widgets, apps). The widgets git repository is cloned to client/your-extensions/, and the apps repository is cloned to server/public/. Each of the widgets that I've developed is designed to be used within any Experience Builder Developer Edition application, and be configurable by a user with no coding experience. To that end, I have a machine that runs the experience builder windows service, and the widgets and apps repositories are cloned to that machine's installation. This allows users to be able to build and edit a developer edition application with no coding experience, never having to touch the command line, while developers can actually handle updating the apps, and doing CI. My repositories are public, and have examples of CI/CD, a .gitignore file, licensing, documentation, and more. Those repositories aren't the ones that my organization uses operationally, but the setup is near identical, and has been working well for us.

Using NPM

Part of what I have also been working on is making widget sharing significantly easier. Using npm packages that aren't included in developer edition is easy, just navigate (cd) to the specific widget that needs the npm package, run 'npm init', and then npm install <packagename> in the same folder. This will create a node_modules folder in the widget, and a package.json folder in the widget as well, to manage the npm package that is used in the widget. Make sure in your git repository that you include a .gitignore, and add node_modules/ in the .gitignore file, and you won't have any worries about the massive modules folder being tracked. If you do all of this, you can also publish your widget on npm as it's own package, which will make it easy for other developers and GIS teams to use the code that you've developed, using the CLI that I have developed.

Esri's Approach

Esri does have a built in approach for using a git repository in your project. Their preference would be to consolidate your entire project into a monorepo, storing apps, widgets, and themes in the same repository. The only thing that you need to make that work, and for webpack to pick up and compile your widgets is a manifest.json file located at the root of your monorepo. Gavin Rehkemper (Esri) has a prime example of this approach here, and it includes documentation for how to set it up.