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 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
- 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.
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). Defines the relationship between this stage and others (or their upstream counterparts). |
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
- **Note:** If a stage declares `hooks`, bindings must reference a specific hook (`build:run`), not the stage name alone (`build`).
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` 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.
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
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## The Problem
8
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.
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 you can just run `nx run <project>:build` and let the projects determine what "building" means to it.
64
+ Now `nx run <project>:build` runs whatever "building" means for each project.
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ export class LifecycleCli {
12
12
  type: 'json'
13
13
  }
14
14
  });
15
- let cli = yargs().scriptName('lifecycle').option({
15
+ let cli = yargs().scriptName('nx-lifecycle').option({
16
16
  cwd: {
17
17
  type: 'string',
18
18
  default: '.',
@@ -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.values(context.projectsConfigurations.projects).map((projectConfig)=>({
6
- name: projectConfig.name,
7
- root: projectConfig.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 })=>!dependencies && projects === undefined && stageNames.includes(target)).map(({ target })=>target));
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
- * @throws {Error} when the config file is invalid
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(/^\^/u, '');
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 registeredTargets){
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.1.4",
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.com/JacobLey/leyman/tree/main/apps/nx-lifecycle#readme",
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.1",
65
- "fast-equals": "^6.0.4",
66
- "format-file": "^1.0.3",
67
- "haywire": "^2.0.2",
68
- "haywire-launcher": "^1.1.0",
69
- "juniper": "^2.4.1",
70
- "juniper-validator": "^0.5.0",
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.2",
73
- "unknown-error": "^0.1.0",
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.2",
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.1",
88
+ "barrelify": "^2.2.2",
88
89
  "c8": "^12.0.0",
89
- "eslint": "^10.12.0",
90
+ "eslint": "^10.13.0",
90
91
  "expect-type": "^1.4.0",
91
- "load-populate-files": "^1.1.2",
92
- "mocha": "^12.0.2",
93
- "mocha-chain": "^1.0.1",
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
  }