I've worked on some projects where the central functionality was supposed to be shared accross several clients. The main goal was to reuse as much as possible, while ensuring the application would still be easily maintained, highly + quickly customizable and improved continually.
Here's a simplified overview of the architecture. Let me know in the comments if you need some more clarification on anything discussed here.
The base project and its delivered products would need to fulfill some characteristics:
0) Use Angular 2+ because that's what everyone knows. Deploy it in a Docker container.
1) Allow changes to the functionallity via json config files.
2) Colors pallete, assets, spacing, radius and fonts should be 100% customizable.
3) Prefer Sass/Less/CSS variables overrides first. Styling overrides can come second.
4) Don't create a repo for the base theme, to avoid adding complexity to initial development and further maintenance work.
5) Although we should reuse most components, we should be able to override those as well.
6) Allow that different products be based on different tags of the generic (base) product, avoiding breaking changes.
7) Have a completely new set of localization languages.
Symlinks to the rescue
To make this work, we came up with a way to load any theme repository from a fixed folder name.
On our generic code, everything that is overriding the base application is considered to be in a folder called theme inside the generic product.
Then we create a Symbolic Link (or Symlink) called theme that points to a client repository somewhere in the computer.
Now, loading any type of configuration file, style overrides, assets become fairly easy to do.
Points 1, 2, 3 and 7 are done!
Initial Base Theme
Where does the symlink should point to initially, then?
Easy! Just point your symlink to your src folder and continue loading things from the theme symlink.
Symlink for base and theme repos
Point 4... done!
This one was a bit more complicated. We decided to use Gulp for this task.
We added some config files on the theme repo to let Gulp now which components we want to override. In parallel we create a new version of the component that we want to make the changes to.
Once we launch the Gulp script, it'll read the config file and, if a component override is necessary, it'll swap the component import for the new component.
Since Angular CLI will block your build if you have unused components, we also have to comment out the base component.
Another script does the inverse work if we want to go back to using the generic component.
Point 5 accomplished!
Delivering Different Versions
Our builds are handled by our CI/CD flow with Jenkins and once we had 2 clients running on the same code base, we realized we would have to make full QA passes every time a minor theme update was made, since a new generic code would also be shipped with possible breaking changes.
We definitely needed to let the theme repo decide which version it needed somehow.
Now we have an arrow pointing in the opposite direction of the symlink. A git submodule on the theme repo was added for the base project.
We now can checkout a certain branch, tag or commit in the theme repo and this is the version that will be used during the CI builds. We only need to update this submodule if we need to bring in a bug fix or new feature from the base repository.
Point 6 OK!