nx-lifecycle 1.1.4 → 1.2.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.
- package/README.md +13 -8
- package/WHY-NX-LIFECYCLE.md +2 -2
- package/dist/cli.js +1 -1
- package/dist/executors/lifecycle/lifecycle.js +3 -3
- package/dist/lifecycle/explain.js +2 -2
- package/dist/lifecycle/normalizer.d.ts +1 -1
- package/dist/lifecycle/processor.d.ts +1 -1
- package/dist/lifecycle/processor.js +3 -3
- package/dist/plugin/lifecycle-plugin.js +6 -6
- package/package.json +19 -18
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Declare your workflow once — `build` runs hooks `pre` → `run` → `post`, `t
|
|
|
14
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
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
16
|
|
|
17
|
-
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).
|
|
17
|
+
Guides and the full reference are on the [docs site](https://jacobley.github.io/leyman/packages/nx-lifecycle/). 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).
|
|
18
18
|
|
|
19
19
|
## Contents
|
|
20
20
|
|
|
@@ -44,7 +44,7 @@ npm i -D nx-lifecycle
|
|
|
44
44
|
- **[Plugin](#plugin)** (recommended): infers the lifecycle targets whenever Nx builds the project graph. Nothing is written to files.
|
|
45
45
|
- **[Executor](#executor)**: writes the lifecycle targets into `nx.json` and every `project.json`, to be committed. CI checks they are up to date.
|
|
46
46
|
|
|
47
|
-
Both take the same [stages](#stages) and [bindings](#bindings).
|
|
47
|
+
Both take the same [stages](#stages) and [bindings](#bindings). Requires Node.js 24 or later. `nx` and `@nx/devkit` (`^23.3.0`) are regular dependencies of `nx-lifecycle` (not peers), so use the same Nx major version in the workspace.
|
|
48
48
|
|
|
49
49
|
## Plugin
|
|
50
50
|
|
|
@@ -77,7 +77,9 @@ Add the plugin to `nx.json`, with stages and bindings as its options. Install `n
|
|
|
77
77
|
}
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
The options are typed as `LifecyclePluginOptions`: `import type { LifecyclePluginOptions } from 'nx-lifecycle/plugin'`, e.g. `const options = { stages, bindings } satisfies LifecyclePluginOptions;`.
|
|
81
|
+
|
|
82
|
+
Every project with a `project.json` gets the [stage targets](#generated-targets), even if it declares nothing bound to them. 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.
|
|
81
83
|
|
|
82
84
|
### Overriding inferred targets
|
|
83
85
|
|
|
@@ -213,9 +215,11 @@ Declares the abstract workflow stages that `nx-lifecycle` will own. Each key is
|
|
|
213
215
|
| Field | Type | Default | Description |
|
|
214
216
|
|-------|------|---------|-------------|
|
|
215
217
|
| `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. |
|
|
216
|
-
| `dependsOn` | `dependsOn[]` | — | Upstream dependencies for this stage, using the same format as Nx's native [`dependsOn`](https://nx.dev/reference/project-configuration#dependson)
|
|
218
|
+
| `dependsOn` | `dependsOn[]` | — | Upstream dependencies for this stage, using the same format as Nx's native [`dependsOn`](https://nx.dev/reference/project-configuration#dependson): `"build"` (this project's stage), `"^build"` (the stage of every dependency project), `{ "target": "build", "projects": ["my-server"] }`. Can name a stage or any other target, but not another stage's anchor or hook (`^build:post` is rejected: depend on `build`). |
|
|
219
|
+
|
|
220
|
+
**Note:** If a stage declares `hooks`, bindings must reference a specific hook (`build:run`), not the stage name alone (`build`). A stage without hooks still generates an anchor: `lint:_ → eslint → lint`.
|
|
217
221
|
|
|
218
|
-
|
|
222
|
+
The config is checked whenever Nx builds the project graph, and fails for a binding that names a hook that doesn't exist, a stage that has hooks, an anchor (`build:_`) or a lifecycle target, and for a stage `dependsOn` on another stage's anchor or hook.
|
|
219
223
|
|
|
220
224
|
```json
|
|
221
225
|
{
|
|
@@ -312,7 +316,7 @@ Executor only.
|
|
|
312
316
|
|------|---------|
|
|
313
317
|
| `boolean` | `true` in CI, `false` otherwise |
|
|
314
318
|
|
|
315
|
-
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.
|
|
319
|
+
When `true`, the executor writes nothing and 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.
|
|
316
320
|
|
|
317
321
|
### `dryRun`
|
|
318
322
|
|
|
@@ -326,7 +330,7 @@ When `true`, performs all computation but skips writing any files. Can still fai
|
|
|
326
330
|
|
|
327
331
|
## CLI
|
|
328
332
|
|
|
329
|
-
`nx-lifecycle`
|
|
333
|
+
`nx-lifecycle` installs an `nx-lifecycle` command (also available as `lifecycle`). It does what the executor does. Configuration must be provided via a config file — inline options are not supported from the CLI.
|
|
330
334
|
|
|
331
335
|
```sh
|
|
332
336
|
pnpx nx-lifecycle --help
|
|
@@ -337,7 +341,8 @@ pnpx nx-lifecycle --config-file ./my-lifecycle.json --dry-run
|
|
|
337
341
|
|------|------|---------|-------------|
|
|
338
342
|
| `--config-file` | `string` | `lifecycle.json` | Path to the lifecycle config file. |
|
|
339
343
|
| `--dry-run` | `boolean` | `false` | Compute changes without writing files. |
|
|
340
|
-
| `--check` | `boolean` | `true` in CI | Fail if any file is out of sync. |
|
|
344
|
+
| `--check`, `--ci` | `boolean` | `true` in CI | Fail if any file is out of sync. |
|
|
345
|
+
| `--cwd` | `string` | `.` | Directory other paths are resolved from. |
|
|
341
346
|
|
|
342
347
|
### `explain`
|
|
343
348
|
|
package/WHY-NX-LIFECYCLE.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## The Problem
|
|
8
8
|
|
|
9
|
-
Nx's `dependsOn` field
|
|
9
|
+
Nx's `dependsOn` field 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
10
|
|
|
11
11
|
### Manual `dependsOn` doesn't scale
|
|
12
12
|
|
|
@@ -61,4 +61,4 @@ The `dependsOn` fields become a tangled web of implementation details rather tha
|
|
|
61
61
|
|
|
62
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
63
|
|
|
64
|
-
Now
|
|
64
|
+
Now `nx run <project>:build` runs whatever "building" means for each project.
|
package/dist/cli.js
CHANGED
|
@@ -2,9 +2,9 @@ import { lifecycleInternal } from '../../lifecycle/lifecycle-internal.js';
|
|
|
2
2
|
export const lifecycle = async (options, context)=>{
|
|
3
3
|
await lifecycleInternal(options, {
|
|
4
4
|
root: context.root,
|
|
5
|
-
projects: Object.
|
|
6
|
-
name
|
|
7
|
-
root
|
|
5
|
+
projects: Object.entries(context.projectsConfigurations.projects).map(([name, { root }])=>({
|
|
6
|
+
name,
|
|
7
|
+
root
|
|
8
8
|
}))
|
|
9
9
|
});
|
|
10
10
|
return {
|
|
@@ -39,7 +39,7 @@ const resolveUpstream = (graph, projectName, target)=>{
|
|
|
39
39
|
};
|
|
40
40
|
const explainDependency = (graph, projectName, dependency)=>{
|
|
41
41
|
const { target, dependencies, projects } = normalizeDependency(dependency);
|
|
42
|
-
if (dependencies) {
|
|
42
|
+
if (dependencies === true) {
|
|
43
43
|
return {
|
|
44
44
|
label: `^${target}`,
|
|
45
45
|
upstream: resolveUpstream(graph, projectName, target)
|
|
@@ -93,7 +93,7 @@ export const explainLifecycle = ({ plan, graph, project, stage })=>{
|
|
|
93
93
|
const stageDependencies = new Map();
|
|
94
94
|
for (const stageName of stageNames){
|
|
95
95
|
const anchor = plan.lifecycleTargets.get(`${stageName}:_`);
|
|
96
|
-
stageDependencies.set(stageName, anchor.dependsOn.map((dependency)=>normalizeDependency(dependency)).filter(({ target, dependencies, projects })
|
|
96
|
+
stageDependencies.set(stageName, anchor.dependsOn.map((dependency)=>normalizeDependency(dependency)).filter(({ target, dependencies, projects })=>dependencies !== true && projects === undefined && stageNames.includes(target)).map(({ target })=>target));
|
|
97
97
|
}
|
|
98
98
|
const explainTarget = (targetName, previousHook)=>({
|
|
99
99
|
name: targetName,
|
|
@@ -16,7 +16,7 @@ export interface NormalizedOptions {
|
|
|
16
16
|
*
|
|
17
17
|
* @param options - options, or where to load them from
|
|
18
18
|
* @returns stages and bindings, with any options passed directly
|
|
19
|
-
* @
|
|
19
|
+
* @rejects {Error} when the config file is invalid
|
|
20
20
|
*/
|
|
21
21
|
export declare const loadOptions: (options: LifecycleOptionsOrConfig) => Promise<LifecycleOptions>;
|
|
22
22
|
/**
|
|
@@ -36,7 +36,7 @@ export interface LifecyclePlan {
|
|
|
36
36
|
*
|
|
37
37
|
* @param options - stages and bindings
|
|
38
38
|
* @returns lifecycle targets and bound targets
|
|
39
|
-
* @throws if stages or bindings are inconsistent
|
|
39
|
+
* @throws {Error} if stages or bindings are inconsistent
|
|
40
40
|
*/
|
|
41
41
|
export declare const planLifecycle: (options: ProcessorOptions) => LifecyclePlan;
|
|
42
42
|
export declare const processNxAndProjectJsons: ({ nxJson, projectJsons, options, }: {
|
|
@@ -76,7 +76,7 @@ const calculateTargetsToRemove = ({ lifecycleTargets, nxJson })=>{
|
|
|
76
76
|
return targetsToRemove;
|
|
77
77
|
};
|
|
78
78
|
const getDependencyTarget = (dependency)=>typeof dependency === 'string' ? dependency : dependency.target;
|
|
79
|
-
const normalizeDependencyTarget = (dependencyTarget)=>dependencyTarget.replace(/^\^/
|
|
79
|
+
const normalizeDependencyTarget = (dependencyTarget)=>dependencyTarget.replace(/^\^/v, '');
|
|
80
80
|
const removeDependencyTargets = ({ target, targetsToRemove })=>(target.dependsOn ?? []).filter((dependency)=>!targetsToRemove.has(normalizeDependencyTarget(getDependencyTarget(dependency))));
|
|
81
81
|
const validateLifecycleDependencies = ({ lifecycleTargets, options })=>{
|
|
82
82
|
for (const [stageName, { dependsOn }] of Object.entries(options.stages)){
|
|
@@ -131,7 +131,8 @@ const processNxJson = ({ nxJson, lifecycleTargets, registeredTargets, targetsToR
|
|
|
131
131
|
lifecycles[targetName] = {
|
|
132
132
|
executor: NOOP_EXECUTOR,
|
|
133
133
|
dependsOn: [
|
|
134
|
-
...target.dependsOn
|
|
134
|
+
...target.dependsOn,
|
|
135
|
+
...registeredTargets.entries().filter(([, { name }])=>name === targetName).map(([boundTarget])=>boundTarget)
|
|
135
136
|
],
|
|
136
137
|
configurations: {
|
|
137
138
|
__lifecycle: {}
|
|
@@ -159,7 +160,6 @@ const processNxJson = ({ nxJson, lifecycleTargets, registeredTargets, targetsToR
|
|
|
159
160
|
const registeredTarget = targetDefaults[targetName] ?? {};
|
|
160
161
|
registeredTarget.dependsOn ??= [];
|
|
161
162
|
registeredTarget.dependsOn.push(lifecycleTarget.previousHook);
|
|
162
|
-
lifecycles[lifecycleTarget.name].dependsOn?.push(targetName);
|
|
163
163
|
targetDefaults[targetName] = registeredTarget;
|
|
164
164
|
}
|
|
165
165
|
Object.assign(targetDefaults, lifecycles);
|
|
@@ -69,22 +69,22 @@ const createProjectNode = (projectJsonPath, { lifecycleTargets, registeredTarget
|
|
|
69
69
|
assertProjectJson(projectJson);
|
|
70
70
|
const declaredTargets = projectJson.targets ?? {};
|
|
71
71
|
const targets = {};
|
|
72
|
+
const boundTargets = [
|
|
73
|
+
...registeredTargets
|
|
74
|
+
].filter(([targetName])=>Object.hasOwn(declaredTargets, targetName));
|
|
72
75
|
for (const [targetName, lifecycleTarget] of lifecycleTargets){
|
|
73
76
|
targets[targetName] = {
|
|
74
77
|
executor: NOOP_EXECUTOR,
|
|
75
78
|
dependsOn: [
|
|
76
|
-
...lifecycleTarget.dependsOn
|
|
79
|
+
...lifecycleTarget.dependsOn,
|
|
80
|
+
...boundTargets.filter(([, { name }])=>name === targetName).map(([boundTarget])=>boundTarget)
|
|
77
81
|
],
|
|
78
82
|
metadata: {
|
|
79
83
|
description: describeTarget(lifecycleTarget)
|
|
80
84
|
}
|
|
81
85
|
};
|
|
82
86
|
}
|
|
83
|
-
for (const [targetName, lifecycleTarget] of
|
|
84
|
-
if (!Object.hasOwn(declaredTargets, targetName)) {
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
targets[lifecycleTarget.name].dependsOn.push(targetName);
|
|
87
|
+
for (const [targetName, lifecycleTarget] of boundTargets){
|
|
88
88
|
targets[targetName] = {
|
|
89
89
|
dependsOn: [
|
|
90
90
|
lifecycleTarget.previousHook
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nx-lifecycle",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Nx plugin that derives every dependsOn from declared lifecycle stages (build, test, …) and the targets bound to them.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "git+https://github.com/JacobLey/leyman.git",
|
|
14
14
|
"directory": "apps/nx-lifecycle"
|
|
15
15
|
},
|
|
16
|
-
"homepage": "https://github.
|
|
16
|
+
"homepage": "https://jacobley.github.io/leyman/packages/nx-lifecycle/",
|
|
17
17
|
"bugs": {
|
|
18
18
|
"url": "https://github.com/JacobLey/leyman/issues"
|
|
19
19
|
},
|
|
@@ -56,21 +56,22 @@
|
|
|
56
56
|
"#internal/*": "./dist/*"
|
|
57
57
|
},
|
|
58
58
|
"bin": {
|
|
59
|
-
"lifecycle": "./bin.mjs"
|
|
59
|
+
"lifecycle": "./bin.mjs",
|
|
60
|
+
"nx-lifecycle": "./bin.mjs"
|
|
60
61
|
},
|
|
61
62
|
"dependencies": {
|
|
62
63
|
"@nx/devkit": "^23.3.0",
|
|
63
64
|
"ci-info": "^4.4.0",
|
|
64
|
-
"entry-script": "^4.1.
|
|
65
|
-
"fast-equals": "^6.
|
|
66
|
-
"format-file": "^1.0
|
|
67
|
-
"haywire": "^2.0.
|
|
68
|
-
"haywire-launcher": "^1.1.
|
|
69
|
-
"juniper": "^2.
|
|
70
|
-
"juniper-validator": "^0.5.
|
|
65
|
+
"entry-script": "^4.1.2",
|
|
66
|
+
"fast-equals": "^6.2.1",
|
|
67
|
+
"format-file": "^1.1.0",
|
|
68
|
+
"haywire": "^2.0.4",
|
|
69
|
+
"haywire-launcher": "^1.1.1",
|
|
70
|
+
"juniper": "^2.5.0",
|
|
71
|
+
"juniper-validator": "^0.5.1",
|
|
71
72
|
"nx": "^23.3.0",
|
|
72
|
-
"parse-cwd": "^2.0.
|
|
73
|
-
"unknown-error": "^0.1.
|
|
73
|
+
"parse-cwd": "^2.0.3",
|
|
74
|
+
"unknown-error": "^0.1.1",
|
|
74
75
|
"yargs": "^18.2.0"
|
|
75
76
|
},
|
|
76
77
|
"devDependencies": {
|
|
@@ -78,19 +79,19 @@
|
|
|
78
79
|
"@leyman/eslint-config": "^0.0.1",
|
|
79
80
|
"@leyman/expect": "^0.0.1",
|
|
80
81
|
"@swc/cli": "^0.8.1",
|
|
81
|
-
"@swc/core": "^1.16.
|
|
82
|
+
"@swc/core": "^1.16.18",
|
|
82
83
|
"@types/mocha": "^10.0.10",
|
|
83
84
|
"@types/node": "^24.19.0",
|
|
84
85
|
"@types/sinon": "^22.0.0",
|
|
85
86
|
"@types/yargs": "^17.0.35",
|
|
86
87
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
87
|
-
"barrelify": "^2.2.
|
|
88
|
+
"barrelify": "^2.2.2",
|
|
88
89
|
"c8": "^12.0.0",
|
|
89
|
-
"eslint": "^10.
|
|
90
|
+
"eslint": "^10.13.0",
|
|
90
91
|
"expect-type": "^1.4.0",
|
|
91
|
-
"load-populate-files": "^1.1.
|
|
92
|
-
"mocha": "^12.0.
|
|
93
|
-
"mocha-chain": "^1.0.
|
|
92
|
+
"load-populate-files": "^1.1.3",
|
|
93
|
+
"mocha": "^12.0.3",
|
|
94
|
+
"mocha-chain": "^1.0.2",
|
|
94
95
|
"sinon": "^22.1.0",
|
|
95
96
|
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
|
96
97
|
}
|