There are many reasons for having a monorepo but working with them may be cumbersome. Workspace provides a set of tools for working with big monorepos efficiently.
What is a workspace?
A workspace is nothing more than a git repository hosting multiple mix projects in any arbitrary folder structure. Path dependencies are used for cross project dependencies similarly to umbrellas.
The central concept of the workspace is the workspace graph. This is a directed acyclic graph where the nodes represent projects and the edges the dependencies between projects.
Workspace uses git to get the changes between two revisions. Knowing which files have changed we can deduce which project is modified or affected:
Running tasks
You can run any command from the root on all or a subset of the workspace projects. You can also run time consuming tasks only on the affected projects significantly improving CI times for large codebases.
Checking your workspace
When your workspace grows, it can go out of hand quickly. You need a way to check that your projects are properly defined. With the workspace.check task you can among other:
Ensure that specific dependencies are set on all projects, e.g. ex_doc.
Ensure that external dependencies versions match the expected ones.
Verify that no forbidden dependencies are defined.
One of the main purposes of monorepos is to help you split your codebase into independent, cohesive and reusable packages. You can enforce a clean architecture by tagging your projects and enforcing boundaries between them.
Creating a workspace
You can create a new workspace with the workspace.new scaffolder:
The workspace repo is itself a workspace and comes with two extra small packages.
cli_options - provides an opinionated way to parse CLI options based on a schema similarly to NimbleOptions
cascade - is a small library for generating code for templates.
Disclaimer
Despite being stable (we successfully use it internally to manage a massive elixir codebase consisting of hundreds of packages) this is still a 0.x.x project and breaking changes may happen.
Very interesting! How do you deal with IDE integration? Something specific? It does not work with ElixirLS on a hello-world example with package_a and package_b.
[Info - 21:16:12] Updating incremental PLT
[Warn - 21:16:13] error: module PackageB is not loaded and could not be found
Yes, they do. But I have to open each package as single folder in VSC for ElixirLS to work properly. So I thought maybe you have some tips how to open the full workspace in an IDE and still have it working. But that is probably a deep rabbit hole.
Also, a separate _build folder is maintained per package folder, right?
Would be curious about a small workspace example project with CI configs, some boundary checks and suggested folder layout for a bigger project. If that is not too much effort.
Looks really solid, I like that it provides enough structure and flexibility! Thanks for open-sourcing it.
Yes, they do. But I have to open each package as single folder in VSC for ElixirLS to work properly. So I thought maybe you have some tips how to open the full workspace in an IDE and still have it working. But that is probably a deep rabbit hole.
Indeed, this is something that can be worked on.
Also, a separate _build folder is maintained per package folder, right?
This is up to you. We use common deps and _build paths, and also enforce it using workspace checks
Would be curious about a small workspace example project with CI configs, some boundary checks and suggested folder layout for a bigger project. If that is not too much effort.
Workspace is dogfooding workspace so it is a simple example. It’s github CI is a nice example. You will see that in PRs only the affected projects are tested, while on the main branch the tasks are ran across all projects.
Regarding the project structure it’s up to you and how you wish to organize your code. In our case we prefer a domain oriented shallow folder structure. I will release the workspace demo project I used for the screenshots, which also includes some boundary checks.
Very cool. I’ll definitely be giving this ago after switching to a monorepo recently.
Are you using vscode’s workspaces? In the main menu there is “add folder to workspace”. I’ve done this for each folder and have an extension installed that opens a terminal for each one.
Yeah, vscode workspaces could be a solution. I was hoping for something a bit simpler, but with monorepos there is no such thing. Maybe it’s good enough though.
Do it once, commit the json config to your source control and you’re done. Not sure how much simpler we can get than native IDE support? You could write a script to generate the workspace config file if you want.
Wow, something like this-- the dependency checks, and boundary enforcement-- could help my organization’s goal in wrangling the various project repos, to get back to one monorepo.
Question: How well does this work with umbrella apps? Any special considerations or notes?
ie. most project repos are individual apps, but there exists one umbrella app. We were thinking of migrating all other projects under the umbrella.
Question: How well does this work with umbrella apps? Any special considerations or notes?
ie. most project repos are individual apps, but there exists one umbrella app. We were thinking of migrating all other projects under the umbrella.
It will not work with umbrellas since it is an umbrella alternative. You can move all umbrella apps out of the umbrella in any folder you wish, and it will work.
Hey @cmo, I’m trying to use workspaces, and ElixirLS is only picking up the top-level folder. Packages in nested folder are not recognized. Do you have any extra configuration to make it work?
It seems to work, when I include 2 subfolders. I get 2 ElixirLS processes, one for each elixir folder. But including the top-folder also into workspaces seems to confuse ElixirLS
@pnezis you might want to explain the requirement to quote task args, e.g. mix workspace.run -t "deps.update --all" and the intricacies and pros/cons of using a common deps folder in the docs.
@cmo Although quoting works I would suggest to pass the task arguments after the return separator --. This way you will avoid escaping quotes. Thanks for bringing this up, I will update the docs accordingly.
mix workspace.run -t deps.update -- --all
Regarding the common deps path I have not included this in purpose, I don’t want to enforce any conventions. But you are right that a mention in the docs about the various options regarding the build artifacts folders would help. I will try to find an appropriate wording and will add it.