Why nx-lifecycle?
The problem
Nx's dependsOn lets a target wait for other targets. As a monorepo grows, maintaining it by hand becomes a burden.
Hand-written dependsOn doesn't scale
Take a TypeScript build (tsc) followed by a test runner (mocha):
{
"targets": {
"tsc": {
"dependsOn": ["^tsc"]
},
"mocha": {
"dependsOn": ["tsc"]
}
}
}mocha waits for tsc, and tsc waits for its upstream projects to build first.
Now add a post-build step, say populate-files generating a JSON config from the compiled JavaScript. It must run after tsc but before mocha. So:
mochamust now depend onpopulate-files, nottsc.tscmust now depend on the upstreampopulate-filesof its dependencies, but only in projects that have that step.
One new target meant changing two existing ones, depending on what each upstream project implements. With dozens of projects and targets, every new step multiplies the changes.
It compounds
- Projects implement targets differently: TypeScript, Java, Go.
- Some projects have
populate-files, others don't. - A new
lint,typecheckorcodegenstep in one project means auditing every other project'sdependsOn. - There is no single place that states the intended order of operations. It is spread across every
project.json.
dependsOn becomes a web of implementation details, rather than a statement of intent.
The solution
nx-lifecycle separates what runs from how:
- Stages declare the workflow once:
buildwith hookspre,runandpost, thentestwith hooksrunandreport. They state the intended order, independent of any tool. - Bindings declare once which concrete targets (
tsc,eslint) run in which hook. Each project opts in by declaring those targets. The mapping lives with the tool, not in every target that depends on it. - Wiring is derived. The plugin infers every
dependsOnwhen Nx builds the project graph, or the executor writes them intonx.jsonandproject.json.
Adding a step means adding one binding, and every dependsOn downstream of it is recomputed. With the plugin there is nothing to keep in sync. With the executor the generated files are committed, and CI checks they are up to date.
Then nx run <project>:build runs whatever building means for that project.