nx-lifecycle 0.2.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.npmignore +20 -0
  2. package/README.md +242 -120
  3. package/WHY-NX-LIFECYCLE.md +64 -0
  4. package/dist/bin.d.ts +1 -2
  5. package/dist/bin.js +1 -14
  6. package/dist/cli.d.ts +1 -5
  7. package/dist/cli.js +9 -22
  8. package/dist/commands/explain-command.d.ts +12 -0
  9. package/dist/commands/explain-command.js +57 -0
  10. package/dist/commands/lib/types.d.ts +0 -3
  11. package/dist/commands/lifecycle-command.d.ts +2 -12
  12. package/dist/commands/lifecycle-command.js +13 -22
  13. package/dist/executors/lifecycle/index.d.ts +1 -4
  14. package/dist/executors/lifecycle/index.js +1 -8
  15. package/dist/executors/lifecycle/lifecycle.d.ts +8 -9
  16. package/dist/executors/lifecycle/lifecycle.js +13 -20
  17. package/dist/lifecycle/explain.d.ts +68 -0
  18. package/dist/lifecycle/explain.js +160 -0
  19. package/dist/lifecycle/lifecycle-internal.d.ts +4 -12
  20. package/dist/lifecycle/lifecycle-internal.js +93 -93
  21. package/dist/lifecycle/normalizer.d.ts +14 -8
  22. package/dist/lifecycle/normalizer.js +29 -36
  23. package/dist/lifecycle/processor.d.ts +41 -5
  24. package/dist/lifecycle/processor.js +36 -35
  25. package/dist/lifecycle/schema.d.ts +468 -66
  26. package/dist/lifecycle/schema.js +30 -21
  27. package/dist/plugin/index.d.ts +7 -0
  28. package/dist/plugin/index.js +4 -0
  29. package/dist/plugin/lifecycle-plugin.d.ts +24 -0
  30. package/dist/plugin/lifecycle-plugin.js +145 -0
  31. package/dist/{executors/lifecycle/index.d.cts → plugin/schema.d.ts} +32 -55
  32. package/dist/plugin/schema.js +19 -0
  33. package/dist/schemas/configurations.d.ts +5 -5
  34. package/dist/schemas/depend-on.d.ts +39 -9
  35. package/dist/schemas/depend-on.js +4 -2
  36. package/dist/schemas/empty.d.ts +2 -2
  37. package/dist/schemas/empty.js +2 -2
  38. package/dist/schemas/nx-json.d.ts +42 -20
  39. package/dist/schemas/nx-json.js +2 -4
  40. package/dist/schemas/project-json.d.ts +42 -20
  41. package/dist/schemas/project-json.js +2 -4
  42. package/dist/schemas/target.d.ts +79 -25
  43. package/executors.json +2 -2
  44. package/out/nx-executor/schema.json +148 -0
  45. package/out/schema.json +114 -0
  46. package/package.json +43 -41
  47. package/dist/commands/index.d.ts +0 -5
  48. package/dist/commands/index.js +0 -14
  49. package/dist/commands/lib/dependencies.d.ts +0 -10
  50. package/dist/commands/lib/dependencies.js +0 -12
  51. package/dist/executors/lifecycle/index.cjs +0 -15
  52. package/dist/lifecycle/depedencies.d.ts +0 -15
  53. package/dist/lifecycle/depedencies.js +0 -14
  54. package/dist/lifecycle/index.d.ts +0 -5
  55. package/dist/lifecycle/index.js +0 -25
  56. package/dist/schemas/lib/ajv.d.ts +0 -3
  57. package/dist/schemas/lib/ajv.js +0 -9
package/.npmignore ADDED
@@ -0,0 +1,20 @@
1
+ CHANGELOG.md
2
+ project.json
3
+ tsconfig.json
4
+
5
+ coverage/**
6
+ eslint.config.js
7
+
8
+ dist/tsconfig.tsbuildinfo
9
+ dist/**/*.map
10
+ dist-test/**
11
+ test/**
12
+
13
+ src/**
14
+ !src/executors/lifecycle/schema.json
15
+
16
+ dist/file-content.js
17
+ dist/file-content.d.ts
18
+
19
+ dist/executors/lifecycle/schema.js
20
+ dist/executors/lifecycle/schema.d.ts
package/README.md CHANGED
@@ -1,99 +1,156 @@
1
1
  <div style="text-align:center">
2
2
 
3
3
  # nx-lifecycle
4
- An [Nx](https://nx.dev/) plugin that injects specific targets into high level workflows.
4
+ [Nx](https://nx.dev/) plugin that derives every `dependsOn` from declared lifecycle stages (build, test, …) and the targets bound to them.
5
5
 
6
6
  [![npm package](https://badge.fury.io/js/nx-lifecycle.svg)](https://www.npmjs.com/package/nx-lifecycle)
7
- [![License](https://img.shields.io/npm/l/nx-lifecycle.svg)](https://github.com/JacobLey/leyman/blob/main/tools/nx-lifecycle/LICENSE)
7
+ [![License](https://img.shields.io/npm/l/nx-lifecycle.svg)](https://github.com/JacobLey/leyman/blob/main/apps/nx-lifecycle/LICENSE)
8
8
 
9
9
  </div>
10
10
 
11
- ## Table of Contents
11
+ Declare your workflow once — `build` runs hooks `pre` → `run` → `post`, `test` runs after `build` — then bind tools to hooks (`tsc` → `build:run`, `mocha` → `test:run`). Then `nx run <project>:build` runs the right steps, in the right order, in any project.
12
12
 
13
- - [Introduction](#introduction)
14
- - [Usage](#usage)
15
- - [API](#api)
13
+ - **Add a step in one place** — binding a new target (codegen, lint, a post-build step) rewires every dependent automatically, across every project.
14
+ - **Per-project implementations** — each project opts into the bound targets it declares, so TypeScript, Go and Java projects can all answer to the same `build` and `test`.
15
+ - **Nothing to commit** — the plugin infers targets when Nx builds the project graph, and fails with the fix if a project's `dependsOn` breaks the wiring. An executor that writes the config to files is also available.
16
+
17
+ **Compared to** hand-written `dependsOn` in `targetDefaults` and each `project.json`: there, adding a step between two existing ones means editing every target that depended on the first, in every project that has it.
18
+
19
+ For the problem this solves and design rationale, see [WHY-NX-LIFECYCLE.md](https://github.com/JacobLey/leyman/blob/main/apps/nx-lifecycle/WHY-NX-LIFECYCLE.md).
20
+
21
+ ## Contents
22
+
23
+ - [Install](#install)
24
+ - [Plugin](#plugin)
25
+ - [Overriding inferred targets](#overriding-inferred-targets)
26
+ - [Limitations](#limitations)
27
+ - [Migrating from the executor](#migrating-from-the-executor)
28
+ - [Executor](#executor)
29
+ - [Configuration](#configuration)
30
+ - [stages](#stages)
31
+ - [bindings](#bindings)
16
32
  - [check](#check)
17
33
  - [dryRun](#dryrun)
34
+ - [CLI](#cli)
35
+ - [explain](#explain)
18
36
 
19
- ## Introduction
37
+ ## Install
20
38
 
21
- Nx targets achieve a specific task, such as loading external config, codegen, type checking, formatting, test execution or enforcing code coverage.
39
+ ```sh
40
+ npm i -D nx-lifecycle
41
+ ```
22
42
 
23
- Some of these tasks depend on each other, others can be executed in parallel.
43
+ `nx-lifecycle` wires targets in one of two ways:
24
44
 
25
- Nx uses the target's [dependsOn](https://nx.dev/reference/project-configuration#dependson) field to natively manage this, and will execute tasks whenever all dependencies are successfully complete.
45
+ - **[Plugin](#plugin)** (recommended): infers the lifecycle targets whenever Nx builds the project graph. Nothing is written to files.
46
+ - **[Executor](#executor)**: writes the lifecycle targets into `nx.json` and every `project.json`, to be committed. CI checks they are up to date.
26
47
 
27
- In practice, this quickly becomes unweildy. In order to properly configure a given task, you have to have full context about all other tasks (including those of your dependencies). You also need to make sure all dependents are updated whenever you add another step to a task.
48
+ Both take the same [stages](#stages) and [bindings](#bindings).
28
49
 
29
- For example, perhaps your code contains a typescript build step [tsc](https://www.typescriptlang.org/docs/handbook/compiler-options.html), followed by a unit test [mocha](https://mochajs.org/). An initial config will look like:
50
+ ## Plugin
51
+
52
+ Add the plugin to `nx.json`, with stages and bindings as its options. Install `nx-lifecycle` in the workspace root `package.json`, where Nx resolves plugins.
30
53
 
31
54
  ```json
55
+ // nx.json
32
56
  {
33
- "targets": {
34
- "tsc": {
35
- "dependsOn": ["^tsc"]
36
- },
37
- "mocha": {
38
- "dependsOn": ["tsc"]
39
- },
40
- }
57
+ "plugins": [
58
+ {
59
+ "plugin": "nx-lifecycle/plugin",
60
+ "options": {
61
+ "stages": {
62
+ "build": {
63
+ "hooks": ["pre", "run", "post"],
64
+ "dependsOn": ["^build"]
65
+ },
66
+ "test": {
67
+ "hooks": ["run", "report"],
68
+ "dependsOn": ["build"]
69
+ }
70
+ },
71
+ "bindings": {
72
+ "tsc": "build:run",
73
+ "mocha": "test:run"
74
+ }
75
+ }
76
+ }
77
+ ]
41
78
  }
42
79
  ```
43
80
 
44
- Your mocha task depends on the tsc first, and tsc depends on any of _its_ dependencies to be built first.
81
+ Every project with a `project.json` gets the [stage targets](#generated-targets). A bound target is wired in a project when its `project.json` declares it, e.g. `"tsc": {}`; the implementation can still come from `targetDefaults`. `nx show project <name>` lists the inferred targets.
45
82
 
46
- Now what if you need want to add a step _after_ to your build? Perhaps you want to use your recently built JS code to [generate a JSON config](https://www.npmjs.com/package/load-populate-files).
83
+ ### Overriding inferred targets
47
84
 
48
- Now your mocha command needs to update to depend on that populate step, rather than `tsc`. Your `tsc` command _also_ needs to update its dependencies to depend on that task, rather than `tsc`. But only if your dependencies include this populate step!
85
+ Nx applies `nx.json` `targetDefaults` and `project.json` on top of inferred targets. A `dependsOn` there replaces the inferred one rather than merging with it. So:
49
86
 
50
- Instead of simply adding this new task, you now need to conditionally manage the dependecones on two other tasks! Imagine how this problem can grow as we begin to support more tasks like type checking, code coverage, or codegen. Each implemented slightly different in each package, especailly if you use more than one language in your monorepo!
87
+ - Don't define the stage targets (`build`, `build:_`, `build:run`…) yourself.
88
+ - If a bound target sets its own `dependsOn`, include its lifecycle dependency: the hook before its own, or `<stage>:_` for the first hook.
51
89
 
52
- `nx-lifecycle` is the solution to this problem, by breaking down the solution into two steps:
53
-
54
- ### High level tasks
55
-
56
- We define abstract workflows that can optionally be used across any and all projects, regardless of language, purpose, or implementation.
57
-
58
- An example would breaking up general steps around a build/compile step, a formatter/linter, and test execution/coverage. We can fairly safely say that the build step will depend on our dependencies build steps, and test execution will depend on our own projects build step.
90
+ ```json
91
+ // project.json, with "e2e-test": "test:run"
92
+ {
93
+ "targets": {
94
+ "e2e-test": {
95
+ "dependsOn": [{ "target": "build", "projects": ["my-server"] }, "test:_"]
96
+ }
97
+ }
98
+ }
99
+ ```
59
100
 
60
- Then we split up these high level tasks into even simpler tasks.
61
- An example would be our test suite may contain both unit-tests and functional tests, and then a final step to enforce the resulting code coverage achieves acceptable levels.
101
+ Every time Nx builds the project graph, the plugin checks the merged configuration, and fails with the entry to add if a bound target lost its lifecycle dependency.
62
102
 
63
- Note these steps are still abstract, and it is not necessary that every project is capable of implementing every task.
103
+ ### Limitations
64
104
 
65
- ### Bind implementions to tasks
105
+ - Only projects with a `project.json` get lifecycle targets.
106
+ - A target created by another Nx plugin is only wired if it is also declared in `project.json`.
107
+ - Nx must load the plugin before it can build anything, so a workspace can't use the plugin from its own unbuilt source. Depend on a published version.
66
108
 
67
- Now we can bind project specific implementations to each abstract task.
109
+ ### Migrating from the executor
68
110
 
69
- In the original example, perhaps we bind our `tsc` target to a `build:run` step, and our `load-populate-files` step to a `build:post` step.
111
+ 1. Add the plugin to `nx.json`, with the options of the executor.
112
+ 2. Remove the `lifecycle` target that runs the executor.
113
+ 3. In `nx.json` `targetDefaults`, delete every target with a `__lifecycle` configuration, and remove lifecycle targets from the `dependsOn` of bound targets. Delete `dependsOn` if nothing else is left in it, as an empty list replaces the inferred one.
114
+ 4. Optionally delete the lifecycle targets (`"build:_": {}`…) from each `project.json`. They are empty, so they don't override anything.
115
+ 5. Run `nx show projects`. The plugin reports any target that still overrides its wiring.
70
116
 
71
- Then our `mocha` target can be bound to a `test:unit` step.
117
+ ## Executor
72
118
 
73
- You can bind multiple targets to a single step. For example if your code also includes some Java projects, you might also bind your [@nx/gradle](https://nx.dev/nx-api/gradle]) target to `build:run`.
119
+ Register it as a target in a `project.json`. Because `lifecycle` manages dependencies for the rest of your targets, it should live in a project outside your normal build/test graph — for example, a root-level management package. See [Nx-lifecycle's own monorepo config](https://github.com/JacobLey/leyman/blob/main/leyman/main/lifecycle.json) for a real example.
74
120
 
75
- ### Instrumenting your `nx.json` and `project.json`
121
+ ```json
122
+ {
123
+ "targets": {
124
+ "lifecycle": {
125
+ "executor": "nx-lifecycle:lifecycle",
126
+ "options": {
127
+ "cwd": "{projectRoot}",
128
+ "configFile": "lifecycle.json"
129
+ }
130
+ }
131
+ }
132
+ }
133
+ ```
76
134
 
77
- Now that you configured your abstract tasks and bound implementations, `nx-lifecycle` will update your `nx.json` and relevant `project.json` files will all the `dependsOn` to make your code run efficiently and deterministically!
135
+ `nx-lifecycle` reads your stage and binding configuration, then writes `dependsOn` entries into `nx.json` and all relevant `project.json` files. The generated targets use the [noop](https://nx.dev/nx-api/nx/executors/noop) executor and should never be invoked directly or have their configuration edited by hand. Commit the generated output to version control, and rerun the executor whenever you add, remove, or rename targets.
78
136
 
79
- Any time you want to add or remove targets for a project, simply re-run `nx-lifecycle`'s plugin and all dependencies are up to date!
137
+ You may declare any of your own targets as depending on a lifecycle-managed target. That is the intended use: your targets reference abstract stage targets, and `nx-lifecycle` ensures those stages wire up to the correct concrete implementations.
80
138
 
81
- ## Installation
139
+ Configuration can be provided in two ways:
82
140
 
83
- `npm i nx-lifecycle`
141
+ - **Inline** — pass `stages` and `bindings` directly in the executor `options` inside `project.json`
142
+ - **Config file** — point the executor at a separate JSON file (defaults to `lifecycle.json` in the project root)
84
143
 
85
- Register it as a target in a `project.json`:
86
144
  ```json
145
+ // project.json — inline config
87
146
  {
88
147
  "targets": {
89
148
  "lifecycle": {
90
149
  "executor": "nx-lifecycle:lifecycle",
91
150
  "options": {
92
- "cwd": "{projectRoot}",
93
- "configFile": "lifecycle.json"
94
151
  "stages": {
95
- "build": {
96
- "hooks": ["pre", "run", "post"],
152
+ "build": {
153
+ "hooks": ["pre", "run", "post"],
97
154
  "dependsOn": ["^build"]
98
155
  },
99
156
  "test": {
@@ -111,49 +168,27 @@ Register it as a target in a `project.json`:
111
168
  }
112
169
  ```
113
170
 
114
- Note that this target only needs to be run once (not once per project) so it is recommended to put this somewhere like a root `project.json` to own this. [Nx-lifecycle's own monorepo is managed in a non-published "main" package](https://github.com/JacobLey/leyman/blob/main/leyman/main/lifecycle.json).
115
-
116
- ## Usage
117
-
118
- Because `lifecycle` manages target dependencies itself, it probably should not be executed along with targets that are normally executed, like the `build` and `test` targets in the example above.
119
-
120
- For example, this project's use of `nx-lifecycle` is part of a [local-only project](https://github.com/JacobLey/leyman/blob/main/leyman/main/project.json#L15) that is never deployed anywhere, and exists only to manage the repo itself.
121
-
122
- `nx-lifecycle` will produce additional targets in your `nx.json` and `project.json`s. These will all be backed by the [noop](https://nx.dev/nx-api/nx/executors/noop) executor, and have no need to invoke caching. In fact, you should never directly manage these commands, their dependencies, or their configurations. These changes should be included in your version control.
123
-
124
- You may declare one-off targets that _depend_ on these targets though, that is the whole point! You should able to declare a target depends on the build step, without having to explicitly manage how the build script is implemented.
125
-
126
- `nx-lifecycle` will use the `nx.json` to assign defaults, and will assign defaults to your targets that are assigned there. However that does not guarantee every target is actually executable by every project. You must declare the target in your `project.json` as well. Do not worry about copying `nx-lifecycle`'s targets over.
127
-
128
- Instead of providing all the stages and bindings in the package.json file, it is also possible to manage that in a separate file, and point the plugin at it.
129
-
130
- ### As a plugin
131
-
132
- Specify `nx-lifecycle:lifecycle` as the executor. You can pass the `lifecycle.json` config file there, or may provide the config JSON directly inline as the options.
133
-
134
171
  ```json
135
- // project.json
172
+ // project.json — file-based config
136
173
  {
137
174
  "targets": {
138
175
  "lifecycle": {
139
176
  "executor": "nx-lifecycle:lifecycle",
140
177
  "options": {
141
178
  "cwd": "{projectRoot}",
142
- "configFile": "my-lifecycle-config.json" // defaults to "lifecycle.json"
179
+ "configFile": "my-lifecycle-config.json"
143
180
  }
144
181
  }
145
182
  }
146
183
  }
147
184
  ```
148
185
 
149
- Where the file is the options object
150
-
151
186
  ```json
152
- // lifecycle.json
187
+ // lifecycle.json — the config file
153
188
  {
154
189
  "stages": {
155
- "build": {
156
- "hooks": ["pre", "run", "post"],
190
+ "build": {
191
+ "hooks": ["pre", "run", "post"],
157
192
  "dependsOn": ["^build"]
158
193
  },
159
194
  "test": {
@@ -168,81 +203,168 @@ Where the file is the options object
168
203
  }
169
204
  ```
170
205
 
171
- ### As a CLI
172
-
173
- Alternatively, you may invoke this plugin as a CLI command. Similar to above, it is _recommended_ to include this dependency in some project that is outside the scope of your "normal" packages. Although it could conceptually be installed globally as well.
206
+ ## Configuration
174
207
 
175
- `pnpx nx-lifecycle --help`
176
- `pnpx nx-lifecycle --config-file ./my-lifecycle.json --dry-run`
208
+ ### `stages`
177
209
 
178
- It is not possible to pass the configuration directly to the CLI, and must use a config file.
210
+ Required. No default.
179
211
 
180
- ## Configuration
212
+ Declares the abstract workflow stages that `nx-lifecycle` will own. Each key is a stage name; the value configures that stage's structure and upstream dependencies.
181
213
 
182
- Both the CLI and the plugin support providing a config file to manage lifecycle options. This file (defaults to `lifecycle.json`) contains configuraiton that specifies the different stages and bindings for your projects.
214
+ | Field | Type | Default | Description |
215
+ |-------|------|---------|-------------|
216
+ | `hooks` | `string[]` | — | Ordered list of sub-steps within the stage. The resulting target names are `stage:hook` (e.g. `build:run`). If omitted, the stage has no sub-steps and is referenced directly by name. |
217
+ | `dependsOn` | `dependsOn[]` | — | Upstream dependencies for this stage, using the same format as Nx's native [`dependsOn`](https://nx.dev/reference/project-configuration#dependson). Defines the relationship between this stage and others (or their upstream counterparts). |
183
218
 
184
- The lifecycle executor can also provide all these options directly in the `project.json` if preferred.
219
+ **Note:** If a stage declares `hooks`, bindings must reference a specific hook (`build:run`), not the stage name alone (`build`).
185
220
 
186
- ### configuration.stages
221
+ ```json
222
+ {
223
+ "stages": {
224
+ "build": {
225
+ "hooks": ["pre", "run", "post"],
226
+ "dependsOn": ["^build"]
227
+ },
228
+ "test": {
229
+ "hooks": ["run", "report"],
230
+ "dependsOn": ["build"]
231
+ }
232
+ }
233
+ }
234
+ ```
187
235
 
188
- Required. No default value.
236
+ A stage without hooks (referenced directly by name):
189
237
 
190
- This is the [high level tasks](#high-level-tasks) logic as described above. Declares the generic targets to be owned by `nx-lifecycle`, which represent workflows with potentially multiple steps.
238
+ ```json
239
+ {
240
+ "stages": {
241
+ "build": {
242
+ "dependsOn": ["^build"]
243
+ }
244
+ },
245
+ "bindings": {
246
+ "tsc": "build"
247
+ }
248
+ }
249
+ ```
191
250
 
192
- It is an object, where every key is the name of a stage. The value is another object that optionally has two fields:
251
+ #### Generated targets
193
252
 
194
- - `steps`
195
- - A list of "sub-stages" to be executed in order. The resulting name of the target will be delimeted by a colon: `stage:step`.
196
- - `dependsOn`
197
- - Accepts the same values as normal Nx [dependsOn](https://nx.dev/reference/project-configuration#dependson). Used to define relationship between stages, and their dependencies.
253
+ Each stage generates a chain of `noop` targets. For `build` with hooks `pre`, `run`, `post`:
198
254
 
199
- ### configuration.bindings
255
+ ```
256
+ build:_ → build:pre → build:run → build:post → build
257
+ ```
200
258
 
201
- Required. No default value.
259
+ - `build:_` is the stage's **anchor**. It carries the stage's `dependsOn`, so every hook waits for it. Nothing can be bound to it.
260
+ - Each hook depends on the previous one, and a bound target depends on the hook before its own. Targets bound to the same hook run in parallel.
261
+ - `build` is the stage's public entry point. Other stages and your own targets should depend on it, not on its hooks.
202
262
 
203
- This is [bind implementions to tasks](#bind-implementions-to-tasks) logic as described above.
263
+ ### `bindings`
204
264
 
205
- The value is an object mapping, where the key is the name of your specific target implementations (e.g. `tsc` or `mocha`) and the value is the stage it maps to.
265
+ Required. No default.
206
266
 
207
- __\*\*NOTE\*\*__ If the bound stage has `steps` defined, then you must bind to a specific step. If there are no steps, then you reference the stage directly.
267
+ Maps your project's concrete target names to lifecycle stage hooks. Each key is the name of an existing Nx target (e.g. `tsc`, `mocha`); the value is the stage or stage hook it should be bound to.
208
268
 
209
269
  ```json
210
- // lifecycle.json
211
270
  {
212
- "stages": {
213
- "build": {
214
- "dependsOn": ["^build"]
215
- },
216
- "test": {
217
- "hooks": ["run", "report"],
218
- "dependsOn": ["build"]
219
- }
220
- },
221
271
  "bindings": {
222
- "tsc": "build",
272
+ "tsc": "build:run",
223
273
  "mocha": "test:run"
224
274
  }
225
275
  }
226
276
  ```
227
277
 
228
- ### configuration.check
278
+ Multiple targets from different languages or toolchains can bind to the same hook:
229
279
 
230
- `boolean`. Default `true` in CI environments, otherwise `false`.
280
+ ```json
281
+ {
282
+ "bindings": {
283
+ "tsc": "build:run",
284
+ "gradle": "build:run"
285
+ }
286
+ }
287
+ ```
288
+
289
+ A binding applies to every project that defines the target, through `targetDefaults` or its own `project.json`. A project opts in by defining it (e.g. `"tsc": {}`). A warning is logged for a binding that no `project.json` declares, since it is usually a typo. Targets inferred by Nx plugins are not declared in `project.json`, so they trigger the warning too.
290
+
291
+ #### Project-specific dependencies
292
+
293
+ With the plugin, see [Overriding inferred targets](#overriding-inferred-targets) instead. With the executor, a project can't change a stage's `dependsOn`, but it can add dependencies to its own bound targets. `nx-lifecycle` keeps any `dependsOn` entry that isn't a lifecycle target and appends the hook dependency after it:
294
+
295
+ ```json
296
+ // project.json
297
+ {
298
+ "targets": {
299
+ "e2e-test": {
300
+ "dependsOn": [{ "target": "build", "projects": ["my-server"] }]
301
+ }
302
+ }
303
+ }
304
+ ```
305
+
306
+ Use this sparingly. If many projects need the same dependency, it belongs in the stage.
307
+
308
+ ### `check`
309
+
310
+ Executor only.
311
+
312
+ | Type | Default |
313
+ |------|---------|
314
+ | `boolean` | `true` in CI, `false` otherwise |
315
+
316
+ When `true`, the executor fails if `nx.json` or any `project.json` is out of sync with what `nx-lifecycle` would generate. Use this in CI to verify that generated configs are committed and up to date before deployment proceeds.
231
317
 
232
- If `true`, the executor will fail if the `nx.json` or any `project.json` is out of sync with what `nx-lifecycle` believes is correct.
318
+ ### `dryRun`
233
319
 
234
- Useful for ensuring that generated configs are up to date in version control before proceeding with deployments.
320
+ Executor only.
235
321
 
236
- ### configuration.dryRun
322
+ | Type | Default |
323
+ |------|---------|
324
+ | `boolean` | `false` |
237
325
 
238
- `boolean`. Default `false`.
326
+ When `true`, performs all computation but skips writing any files. Can still fail when `check` is `true`.
239
327
 
240
- If `true`, will do everything except actually write the updated config files.
328
+ ## CLI
241
329
 
242
- Can still fail if `check` is `true`.
330
+ `nx-lifecycle` can also be invoked directly as a CLI command. Configuration must be provided via a config file — inline options are not supported from the CLI.
243
331
 
244
- ## Executors
332
+ ```sh
333
+ pnpx nx-lifecycle --help
334
+ pnpx nx-lifecycle --config-file ./my-lifecycle.json --dry-run
335
+ ```
336
+
337
+ | Flag | Type | Default | Description |
338
+ |------|------|---------|-------------|
339
+ | `--config-file` | `string` | `lifecycle.json` | Path to the lifecycle config file. |
340
+ | `--dry-run` | `boolean` | `false` | Compute changes without writing files. |
341
+ | `--check` | `boolean` | `true` in CI | Fail if any file is out of sync. |
342
+
343
+ ### `explain`
344
+
345
+ Prints what runs, in which order, for the stages of a project: each stage after the stages it depends on, its hooks, and the bound targets the project declares. A `^` dependency lists the upstream projects it runs.
245
346
 
246
- ### lifecycle
347
+ ```sh
348
+ pnpx nx-lifecycle explain my-lib # every stage
349
+ pnpx nx-lifecycle explain my-lib test # test, and the stages it runs after
350
+ ```
351
+
352
+ ```
353
+ my-lib
354
+
355
+ 1. build
356
+ after: ^build (my-utils)
357
+ build:pre (nothing bound)
358
+ build:run
359
+ tsc
360
+
361
+ 2. test
362
+ after: build
363
+ test:run
364
+ mocha
365
+ e2e-test (also after: my-server:build)
366
+ test:report
367
+ coverage-report
368
+ ```
247
369
 
248
- Will update all `project.json`s and the `nx.json` with declared stages, and wire up dependencies to your bound targets, ensuring that you can reference your abstract targets, Nx will invoke the required targets in dependency order as necessary.
370
+ Stages and bindings come from the [plugin](#plugin) options in `nx.json`, or else from `--config-file` (default `lifecycle.json`). Bound targets and their other dependencies come from the project as Nx resolves it, after `targetDefaults` and `project.json`.
@@ -0,0 +1,64 @@
1
+ <div style="text-align:center">
2
+
3
+ # Why nx-lifecycle?
4
+
5
+ </div>
6
+
7
+ ## The Problem
8
+
9
+ Nx's `dependsOn` field is powerful — it lets you declare that a target must wait for other targets to complete before it runs. But as a monorepo grows, managing those dependencies by hand becomes a significant maintenance burden.
10
+
11
+ ### Manual `dependsOn` doesn't scale
12
+
13
+ Consider a simple setup: a TypeScript build step (`tsc`) followed by a unit test runner (`mocha`).
14
+
15
+ ```json
16
+ {
17
+ "targets": {
18
+ "tsc": {
19
+ "dependsOn": ["^tsc"]
20
+ },
21
+ "mocha": {
22
+ "dependsOn": ["tsc"]
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ This is fine. `mocha` waits for `tsc`, and `tsc` waits for its upstream dependencies to build first.
29
+
30
+ Now you need to add a post-build step — say, a [`populate-files`](https://www.npmjs.com/package/load-populate-files) step that uses the freshly-compiled JS to generate a JSON config.
31
+
32
+ The new step must run after `tsc` but before `mocha`. So:
33
+
34
+ - `mocha` must now depend on `populate-files`, not `tsc`
35
+ - `tsc` must now depend on the upstream `populate-files` of _its_ dependencies (but only for packages that actually have that step)
36
+
37
+ Adding one new target required updating the dependency declarations on two existing targets — and conditionally, depending on what each dependency package implements. In a monorepo with dozens of packages and targets, every new step multiplies the update surface.
38
+
39
+ ### The compounding problem
40
+
41
+ The situation gets worse as the repo grows:
42
+
43
+ - Different packages implement targets differently (TypeScript vs Java vs Go)
44
+ - Some packages have `populate-files`, others don't
45
+ - A new `lint`, `typecheck`, or `codegen` step in one package requires auditing every other package's `dependsOn` to see what needs updating
46
+ - There is no single place to reason about the _intended_ order of operations — it's spread across every `project.json`
47
+
48
+ The `dependsOn` fields become a tangled web of implementation details rather than a clear expression of intent.
49
+
50
+ ---
51
+
52
+ ## The Solution
53
+
54
+ `nx-lifecycle` separates the _what_ from the _how_:
55
+
56
+ 1. **Stages** — You declare abstract, high-level workflows once (e.g. `build` with hooks `pre`, `run`, `post`; `test` with hooks `run`, `report`). These stages express the _intended_ order of operations, independent of any specific tooling.
57
+
58
+ 2. **Bindings** — You declare once which concrete targets (e.g. `tsc`, `eslint`) map to which stage and hook. Each project opts in by defining those targets. The mapping lives with the tool, not scattered across every dependent.
59
+
60
+ 3. **Wiring** — `nx-lifecycle` reads the stages and bindings, then derives every `dependsOn`: its Nx plugin infers them when Nx builds the project graph, or its executor writes them into `nx.json` and `project.json`.
61
+
62
+ The result: adding a new step means updating one binding. Every downstream `dependsOn` is recomputed. With the plugin this happens every time Nx builds the project graph, so there is nothing to keep in sync. With the executor the generated configs are committed, and CI checks they are up to date.
63
+
64
+ Now you can just run `nx run <project>:build` and let the projects determine what "building" means to it.
package/dist/bin.d.ts CHANGED
@@ -1,3 +1,2 @@
1
- declare const _default: import("haywire-launcher").WrapperMain;
2
- export default _default;
1
+ export { LifecycleCli as default } from './cli.js';
3
2
  //# sourceMappingURL=bin.d.ts.map
package/dist/bin.js CHANGED
@@ -1,16 +1,3 @@
1
- import { EntryScript } from "entry-script";
2
- import { bind, singletonScope } from 'haywire';
3
- import { launch } from 'haywire-launcher';
4
- import { LifecycleCli } from './cli.js';
5
- import { commandsId, commandsModule } from './commands/index.js';
6
- import { consoleErrorId, consoleLogId, exitCodeId } from './commands/lib/dependencies.js';
7
- export default launch(commandsModule.addBinding(bind(EntryScript).withDependencies([
8
- LifecycleCli
9
- ]).withProvider((cli)=>cli).scoped(singletonScope)).addBinding(bind(LifecycleCli).withDependencies([
10
- commandsId.supplier(),
11
- consoleLogId,
12
- consoleErrorId,
13
- exitCodeId
14
- ]).withConstructorProvider().scoped(singletonScope)).toContainer());
1
+ export { LifecycleCli as default } from './cli.js';
15
2
 
16
3
  //# sourceMappingURL=bin.js.map
package/dist/cli.d.ts CHANGED
@@ -1,7 +1,4 @@
1
1
  import { EntryScript } from 'entry-script';
2
- import type { Supplier } from 'haywire';
3
- import type { ConsoleLog, ExitCode } from './commands/lib/dependencies.js';
4
- import type { AbstractCommand } from './commands/lib/types.js';
5
2
  /**
6
3
  * Lifecycle CLI. Run `./bin.mjs --help` for options.
7
4
  *
@@ -9,7 +6,6 @@ import type { AbstractCommand } from './commands/lib/types.js';
9
6
  */
10
7
  export declare class LifecycleCli extends EntryScript {
11
8
  #private;
12
- constructor(getCommands: Supplier<AbstractCommand[]>, logger: ConsoleLog, errorLogger: ConsoleLog, exitCode: ExitCode);
13
9
  /**
14
10
  * Entry point to CLI script.
15
11
  *
@@ -17,6 +13,6 @@ export declare class LifecycleCli extends EntryScript {
17
13
  *
18
14
  * @param argv - process arguments
19
15
  */
20
- main(argv: string[]): Promise<void>;
16
+ static main(argv: string[]): Promise<void>;
21
17
  }
22
18
  //# sourceMappingURL=cli.d.ts.map