I have some ideas here. I work with the NX Extensions every day, and IMHO, leaning on, "that's not the mono repo way" is fine as an ideal but fails in practice. You WILL sooner or later want to make a breaking change to a lib, and you don't want all your tests in the apps failing when you do it (prevent you from releasing, etc, which you may not have the luxury of time to clean up). This might not be exactly what the OP is getting at, but I think with a little change in thinking these solutions might serve. I've tried 'em all, they seem to work.
Note that I am aware of the google way (I worked there). I ran into this problem on the first project I worked on. Their dev environment provides an analog of "if you could have different package.jsons in each app..." so comparing NX to Google's internal system as an absolute 1:1 is a bit off.
Qualifier: this is not "the answer". It just shares some ways I've overcome these problems.
- Branch the entire repo, call it a version. Make your breaking change, see it all fall apart. Fix everything, update from master, looks good, merge it. Optional: release from this branch.
That "devbranch" thing works for most "breaking lib change" kind of things.
In tsconfig.json, provided you aliased your library (@myorg/blah), your app will point at the path you configure. In master, build your lib (ng-packagr). Using the output config, you can call the dist whatever you want (@myorg/blah-v1, v2, as many as you want). Point tsconfig at it (tsconfig will be pointing to the non-build path). Master will now use a locked, known version of the lib (just DON'T REBUILD IT WITH CHANGES). You are now free to abuse your master library as you see fit. To keep an "everything in master working" mindset, you would branch master and then make this change, which would allow you to work on the lib independent of the apps that use it with master untouched.
Build your library (versioned, and assumes ng-packagr), npm pack it (you now have a tarball), do what you want with it. Branch master, remove the path entry from tsconfig, add an install entry to package.json (you can install from a file), and your apps should pick it up (again, the import aliases should match). You can also do the install in master (known working version as a tarball), and again abuse your libs as you want.
That latter solution I tested a little and I saw it works, but if you don't have to pack a tarball and mess with package.json, why bother. Good option to know though, and it doesn't incur the downside of not being able to rebuild the library (as you'll overwrite the known working version unless you change the output target).
Using these ideas, I can pretty much get us out of any breaking change jam and provide at least provide one other known working version of any given lib.
I'll put these out there too though:
A primary benefit of a mono repo is that it forces you to avoid incurring the technical debt that multiple lib versions cause, which is SEVERE if you let it go for any period of time. If you let it go long enough, sooner or later you'll have problems with the overall version of Angular, which is a problem you WANT to avoid.
If it gets to the point that you have an app that requires a lot of drift, IMHO, you'd be best served by creating a new repo, dumping the code into it, wiping it from your main repo, wait until it gets into shape, and put it back when it's ready (if ever).
And, remember, when you are working on a library, you need to think a bit differently. It's shared code, and every time you make a change to it, it can't be breaking. Instead of renaming that input, create another one and point it at the original (backward compat), and that sort of thing. Decorator patterns, all that. A breaking change in a lib should NOT be a casually committed thing.
Hope that helps.