@smoothbricks/cli 0.10.6 โ†’ 0.10.7

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 (45) hide show
  1. package/dist/cli.js +1 -0
  2. package/dist/github-ci/index.d.ts +1 -0
  3. package/dist/github-ci/index.d.ts.map +1 -1
  4. package/dist/github-ci/index.js +23 -2
  5. package/dist/lib/json.d.ts +1 -1
  6. package/dist/lib/json.d.ts.map +1 -1
  7. package/dist/lib/json.js +3 -18
  8. package/dist/lib/workspace.d.ts.map +1 -1
  9. package/dist/lib/workspace.js +1 -0
  10. package/dist/monorepo/managed-files.d.ts +6 -22
  11. package/dist/monorepo/managed-files.d.ts.map +1 -1
  12. package/dist/monorepo/managed-files.js +12 -47
  13. package/dist/monorepo/package-policy.js +2 -2
  14. package/dist/monorepo/packs/index.d.ts.map +1 -1
  15. package/dist/monorepo/packs/index.js +2 -0
  16. package/dist/monorepo/publish-workflow.js +1 -1
  17. package/dist/monorepo/runtime.d.ts +2 -2
  18. package/dist/monorepo/runtime.d.ts.map +1 -1
  19. package/dist/monorepo/runtime.js +11 -16
  20. package/dist/monorepo/tool-validation.d.ts.map +1 -1
  21. package/dist/monorepo/tool-validation.js +20 -23
  22. package/dist/nx/index.d.ts +7 -4
  23. package/dist/nx/index.d.ts.map +1 -1
  24. package/dist/nx/index.js +117 -56
  25. package/managed/raw/tooling/devenv +71 -0
  26. package/package.json +4 -4
  27. package/src/cli.ts +8 -1
  28. package/src/github-ci/index.test.ts +50 -1
  29. package/src/github-ci/index.ts +31 -2
  30. package/src/lib/json.ts +1 -1
  31. package/src/lib/workspace.ts +1 -0
  32. package/src/monorepo/__tests__/publish-workflow.test.ts +11 -3
  33. package/src/monorepo/managed-files.test.ts +31 -22
  34. package/src/monorepo/managed-files.ts +15 -68
  35. package/src/monorepo/package-policy.test.ts +31 -0
  36. package/src/monorepo/package-policy.ts +2 -2
  37. package/src/monorepo/packs/index.ts +2 -0
  38. package/src/monorepo/publish-workflow.ts +5 -0
  39. package/src/monorepo/runtime.test.ts +23 -17
  40. package/src/monorepo/runtime.ts +12 -17
  41. package/src/monorepo/tool-validation.test.ts +15 -8
  42. package/src/monorepo/tool-validation.ts +20 -25
  43. package/src/nx/index.test.ts +45 -14
  44. package/src/nx/index.ts +133 -64
  45. package/managed/raw/patches/ttsc@0.19.3.patch +0 -67
@@ -1,4 +1,4 @@
1
- import { type NxProjectJson, type NxTargetConfig } from '../lib/json.js';
1
+ import type { NxProjectJson, NxTargetConfig, NxTargetOptions } from '../lib/json.js';
2
2
  export interface ProjectTargets {
3
3
  project: string;
4
4
  root?: string;
@@ -6,6 +6,7 @@ export interface ProjectTargets {
6
6
  buildDependsOn?: string[];
7
7
  targetDependencies?: Map<string, string[]>;
8
8
  targetExecutors?: Map<string, string>;
9
+ targetOptions?: Map<string, NxTargetOptions>;
9
10
  targetOutputs?: Map<string, string[]>;
10
11
  targetScripts?: Map<string, string>;
11
12
  }
@@ -14,24 +15,26 @@ export interface CommandInvocation {
14
15
  args: string[];
15
16
  }
16
17
  export declare function nxResetCommand(): CommandInvocation;
17
- export declare function nxShowProjectCommand(project: string): CommandInvocation;
18
18
  export declare function nxCacheDirectories(root: string): string[];
19
19
  export declare function targetNamesFromNxProjectJson(value: NxProjectJson | null | undefined): string[];
20
20
  export declare function projectRootFromNxProjectJson(value: NxProjectJson | null | undefined): string | undefined;
21
- export declare function buildDependsOnFromNxProjectJson(value: NxProjectJson | null | undefined): string[] | undefined;
22
21
  export declare function targetDependenciesFromNxProjectJson(value: NxProjectJson | null | undefined): Map<string, string[]>;
23
22
  export declare function targetExecutorsFromNxProjectJson(value: NxProjectJson | null | undefined): Map<string, string>;
23
+ export declare function targetOptionsFromNxProjectJson(value: NxProjectJson | null | undefined): Map<string, NxTargetOptions>;
24
24
  export declare function targetOutputsFromNxProjectJson(value: NxProjectJson | null | undefined): Map<string, string[]>;
25
25
  export declare function targetScriptsFromNxProjectJson(value: NxProjectJson | null | undefined): Map<string, string>;
26
26
  export declare function formatProjectTargetLines(projects: ProjectTargets[]): string;
27
27
  export declare function projectNamesWithTarget(projects: ProjectTargets[], target: string): string[];
28
- export declare function projectNamesFromNxShowProjectsOutput(output: string): string[];
29
28
  export declare function listTargets(root: string): Promise<void>;
30
29
  export declare function listProjects(root: string, options: {
31
30
  withTarget?: string;
32
31
  }): Promise<void>;
33
32
  export declare function resetCache(root: string): Promise<void>;
34
33
  export declare function cleanCache(root: string): Promise<void>;
34
+ export type NxProjects = Readonly<Record<string, NxProjectJson>>;
35
+ export declare function loadNxProjects(root: string): Promise<NxProjects>;
36
+ export declare function targetNamesFromProjects(projects: NxProjects): string[];
37
+ export declare function projectTargetsFromNxProjects(projects: NxProjects): ProjectTargets[];
35
38
  export declare function readProjectTargets(root: string): Promise<ProjectTargets[]>;
36
39
  export type { NxProjectJson, NxTargetConfig };
37
40
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/nx/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,cAAc,EAGpB,MAAM,gBAAgB,CAAC;AAGxB,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC3C,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,wBAAgB,cAAc,IAAI,iBAAiB,CAElD;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAEvE;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEzD;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CAG9F;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAExG;AAED,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,GAAG,SAAS,CAE7G;AAED,wBAAgB,mCAAmC,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAwBlH;AAsBD,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAY7G;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAE7G;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAY3G;AA0BD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,MAAM,CAK3E;AAED,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,cAAc,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAK3F;AAED,wBAAgB,oCAAoC,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAsB7E;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAK7D;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAQhG;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG5D;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAa5D;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAGhF;AA2BD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/nx/index.ts"],"names":[],"mappings":"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"}
package/dist/nx/index.js CHANGED
@@ -1,15 +1,13 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { lstat, rm } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- import { $ } from 'bun';
5
- import { parseNxProjectJsonText, parseStringArrayText, } from '../lib/json.js';
6
- import { decode, run } from '../lib/run.js';
4
+ // Nx package exports are CLI-oriented; this is the module the CLI uses for graph + daemon IPC.
5
+ import { createProjectGraphAsync } from 'nx/src/project-graph/project-graph.js';
6
+ import { workspaceRoot as primaryWorkspaceRoot } from 'nx/src/utils/workspace-root.js';
7
+ import { run, runResult } from '../lib/run.js';
7
8
  export function nxResetCommand() {
8
9
  return { command: 'nx', args: ['reset'] };
9
10
  }
10
- export function nxShowProjectCommand(project) {
11
- return { command: 'nx', args: ['show', 'project', project, '--json'] };
12
- }
13
11
  export function nxCacheDirectories(root) {
14
12
  return [join(root, '.nx/cache'), join(root, '.nx/workspace-data'), join(root, 'node_modules/.cache/nx')];
15
13
  }
@@ -20,9 +18,6 @@ export function targetNamesFromNxProjectJson(value) {
20
18
  export function projectRootFromNxProjectJson(value) {
21
19
  return typeof value?.root === 'string' ? value.root : undefined;
22
20
  }
23
- export function buildDependsOnFromNxProjectJson(value) {
24
- return targetDependenciesFromNxProjectJson(value).get('build');
25
- }
26
21
  export function targetDependenciesFromNxProjectJson(value) {
27
22
  const targets = value?.targets;
28
23
  const dependencies = new Map();
@@ -79,6 +74,19 @@ export function targetExecutorsFromNxProjectJson(value) {
79
74
  }
80
75
  return executors;
81
76
  }
77
+ export function targetOptionsFromNxProjectJson(value) {
78
+ const targets = value?.targets;
79
+ const options = new Map();
80
+ if (!targets) {
81
+ return options;
82
+ }
83
+ for (const [targetName, target] of Object.entries(targets)) {
84
+ if (target.options) {
85
+ options.set(targetName, target.options);
86
+ }
87
+ }
88
+ return options;
89
+ }
82
90
  export function targetOutputsFromNxProjectJson(value) {
83
91
  return targetStringArraysFromNxProjectJson(value, 'outputs');
84
92
  }
@@ -127,28 +135,6 @@ export function projectNamesWithTarget(projects, target) {
127
135
  .map((project) => project.project)
128
136
  .sort((a, b) => a.localeCompare(b));
129
137
  }
130
- export function projectNamesFromNxShowProjectsOutput(output) {
131
- const trimmed = output.trim();
132
- if (!trimmed) {
133
- return [];
134
- }
135
- if (trimmed.startsWith('[')) {
136
- try {
137
- const parsed = parseStringArrayText(trimmed);
138
- if (parsed) {
139
- return parsed.sort((a, b) => a.localeCompare(b));
140
- }
141
- }
142
- catch {
143
- // Fall through to legacy newline parsing for older Nx output shapes.
144
- }
145
- }
146
- return trimmed
147
- .split('\n')
148
- .map((line) => line.trim())
149
- .filter(Boolean)
150
- .sort((a, b) => a.localeCompare(b));
151
- }
152
138
  export async function listTargets(root) {
153
139
  const output = formatProjectTargetLines(await readProjectTargets(root));
154
140
  if (output) {
@@ -182,30 +168,105 @@ export async function cleanCache(root) {
182
168
  console.log(`Removed ${path}`);
183
169
  }
184
170
  }
171
+ /**
172
+ * Load the resolved project graph through Nx's API. The current workspace uses the
173
+ * daemon in-process; foreign roots use an isolated process because Nx snapshots its
174
+ * workspace cache paths when the module loads.
175
+ */
176
+ let graphLoadTail = Promise.resolve();
177
+ export function loadNxProjects(root) {
178
+ const result = graphLoadTail.then(() => loadNxProjectsNow(root));
179
+ graphLoadTail = result.then(() => undefined, () => undefined);
180
+ return result;
181
+ }
182
+ async function loadNxProjectsNow(root) {
183
+ if (root !== primaryWorkspaceRoot) {
184
+ return loadForeignNxProjects(root);
185
+ }
186
+ const graph = await createProjectGraphAsync({ exitOnError: false });
187
+ return nxProjectsFromNodes(graph.nodes);
188
+ }
189
+ const FOREIGN_GRAPH_SCRIPT = `
190
+ import('nx/src/project-graph/project-graph.js')
191
+ .then(async ({ createProjectGraphAsync }) => {
192
+ const graph = await createProjectGraphAsync({ exitOnError: false });
193
+ const projects = Object.fromEntries(
194
+ Object.entries(graph.nodes).map(([name, node]) => [
195
+ name,
196
+ { ...node.data, name: node.data?.name ?? name, targets: node.data?.targets ?? {} },
197
+ ]),
198
+ );
199
+ process.stdout.write(JSON.stringify(projects));
200
+ })
201
+ .catch((error) => {
202
+ console.error(error);
203
+ process.exitCode = 1;
204
+ });
205
+ `;
206
+ async function loadForeignNxProjects(root) {
207
+ const result = await runResult(process.execPath, ['--eval', FOREIGN_GRAPH_SCRIPT], root, {
208
+ NX_DAEMON: 'false',
209
+ NX_ISOLATE_PLUGINS: 'false',
210
+ NX_WORKSPACE_ROOT_PATH: root,
211
+ });
212
+ if (result.exitCode !== 0) {
213
+ throw new Error(`Failed to load Nx project graph for ${root}: ${result.stderr.trim()}`);
214
+ }
215
+ const parsed = JSON.parse(result.stdout);
216
+ if (!isNxProjects(parsed)) {
217
+ throw new Error(`Nx returned an invalid project graph for ${root}`);
218
+ }
219
+ return parsed;
220
+ }
221
+ function nxProjectsFromNodes(nodes) {
222
+ const projects = {};
223
+ for (const [name, node] of Object.entries(nodes)) {
224
+ projects[name] = {
225
+ ...node.data,
226
+ name: node.data.name ?? name,
227
+ targets: node.data.targets ?? {},
228
+ };
229
+ }
230
+ return projects;
231
+ }
232
+ function isNxProjects(value) {
233
+ if (!isRecord(value))
234
+ return false;
235
+ return Object.values(value).every((project) => isRecord(project) &&
236
+ (project.name === undefined || typeof project.name === 'string') &&
237
+ (project.root === undefined || typeof project.root === 'string') &&
238
+ (project.targets === undefined || (isRecord(project.targets) && Object.values(project.targets).every(isRecord))));
239
+ }
240
+ function isRecord(value) {
241
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
242
+ }
243
+ export function targetNamesFromProjects(projects) {
244
+ const names = new Set();
245
+ for (const project of Object.values(projects)) {
246
+ for (const targetName of targetNamesFromNxProjectJson(project)) {
247
+ names.add(targetName);
248
+ }
249
+ }
250
+ return [...names];
251
+ }
252
+ export function projectTargetsFromNxProjects(projects) {
253
+ return Object.entries(projects)
254
+ .sort(([left], [right]) => left.localeCompare(right))
255
+ .map(([project, metadata]) => {
256
+ const targetDependencies = targetDependenciesFromNxProjectJson(metadata);
257
+ return {
258
+ project,
259
+ root: projectRootFromNxProjectJson(metadata),
260
+ targets: targetNamesFromNxProjectJson(metadata),
261
+ buildDependsOn: targetDependencies.get('build'),
262
+ targetDependencies,
263
+ targetExecutors: targetExecutorsFromNxProjectJson(metadata),
264
+ targetOptions: targetOptionsFromNxProjectJson(metadata),
265
+ targetOutputs: targetOutputsFromNxProjectJson(metadata),
266
+ targetScripts: targetScriptsFromNxProjectJson(metadata),
267
+ };
268
+ });
269
+ }
185
270
  export async function readProjectTargets(root) {
186
- const projects = await readNxProjectNames(root);
187
- return Promise.all(projects.map((project) => readProjectTarget(root, project)));
188
- }
189
- async function readNxProjectNames(root) {
190
- const result = await $ `nx show projects`.cwd(root).quiet();
191
- return projectNamesFromNxShowProjectsOutput(decode(result.stdout));
192
- }
193
- async function readProjectTarget(root, project) {
194
- const command = nxShowProjectCommand(project);
195
- const result = await $ `${command.command} ${command.args}`.cwd(root).quiet();
196
- const parsed = parseNxProjectJsonText(decode(result.stdout));
197
- if (!parsed) {
198
- throw new Error(`Unable to inspect Nx project ${project}.`);
199
- }
200
- const targetDependencies = targetDependenciesFromNxProjectJson(parsed);
201
- return {
202
- project,
203
- root: projectRootFromNxProjectJson(parsed),
204
- targets: targetNamesFromNxProjectJson(parsed),
205
- buildDependsOn: targetDependencies.get('build'),
206
- targetDependencies,
207
- targetExecutors: targetExecutorsFromNxProjectJson(parsed),
208
- targetOutputs: targetOutputsFromNxProjectJson(parsed),
209
- targetScripts: targetScriptsFromNxProjectJson(parsed),
210
- };
271
+ return projectTargetsFromNxProjects(await loadNxProjects(root));
211
272
  }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # Wrapper script to run devenv from anywhere in the monorepo
3
+ # This allows us to keep all devenv files in tooling/direnv
4
+
5
+ # Save the current working directory
6
+ ORIGINAL_PWD="$(pwd)"
7
+ SELF_PATH="$(realpath "$0")"
8
+
9
+ # Find the root of the git repository
10
+ TOOLING_DIR="$(dirname "$SELF_PATH")"
11
+ DEVENV_DIR="$TOOLING_DIR/direnv"
12
+ if [ ! -d "$DEVENV_DIR" ]; then
13
+ echo "Error: Could not find devenv directory at $DEVENV_DIR" >&2
14
+ exit 1
15
+ fi
16
+
17
+ # Find the actual Nix-built devenv binary, skipping every wrapper script.
18
+ # WHY: PATH routinely holds more than one of these wrappers at once โ€” this
19
+ # repo's tooling/devenv, sibling repos' wrappers, and direnv-stacked parent
20
+ # environments. A plain "not equal to SELF_PATH" check happily selects a
21
+ # *different* wrapper and exec's it, so `devenv` bounces into the wrong repo
22
+ # (or self-execs forever on cheap checks like `devenv version`). The real devenv
23
+ # is always the Nix-managed executable under /nix/store, so match that and ignore
24
+ # every wrapper regardless of PATH ordering. (Matching the store path is also more
25
+ # robust than a "is it a binary" test: Nix sometimes ships tools as makeWrapper
26
+ # shell scripts, which are still the correct entry point.)
27
+ DEVENV_BIN=""
28
+ while IFS= read -r candidate; do
29
+ candidate_path="$(realpath "$candidate")"
30
+ case "$candidate_path" in
31
+ /nix/store/*)
32
+ if [ -f "$candidate_path" ] && [ -x "$candidate_path" ]; then
33
+ DEVENV_BIN="$candidate_path"
34
+ break
35
+ fi
36
+ ;;
37
+ esac
38
+ done < <(which -a devenv 2>/dev/null)
39
+
40
+ if [ -z "$DEVENV_BIN" ]; then
41
+ echo "Error: Could not find the Nix-built devenv binary (under /nix/store) in PATH" >&2
42
+ exit 1
43
+ fi
44
+
45
+ # If we're in a project, just run devenv
46
+ if [ -f devenv.nix ]; then
47
+ exec "$DEVENV_BIN" "$@"
48
+ fi
49
+
50
+ # Change to the tooling/direnv directory where devenv files are located
51
+ cd "$DEVENV_DIR" || exit 1
52
+
53
+ # Export the original directory so enterShell can use it
54
+ export DEVENV_SHELL_PWD="$ORIGINAL_PWD"
55
+
56
+ # Run the actual devenv command with all arguments passed through.
57
+ # For updates, refresh nvfetcher-managed overlay sources first so packages like
58
+ # jmunch-mcp and jcodemunch-mcp are current before devenv.lock is updated.
59
+ if [ "${1:-}" = "update" ]; then
60
+ if [ -f "$DEVENV_DIR/nixpkgs-overlay/nvfetcher.toml" ]; then
61
+ echo "Running nvfetcher for nixpkgs-overlay/..."
62
+ (cd "$DEVENV_DIR/nixpkgs-overlay" && echo "
63
+ [keys]
64
+ github = \"$(gh auth token)\"
65
+ " > .gh-auth && (nvfetcher -k .gh-auth -o _sources || true) && rm .gh-auth)
66
+ fi
67
+
68
+ exec "$DEVENV_BIN" "$@"
69
+ fi
70
+
71
+ exec "$DEVENV_BIN" "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smoothbricks/cli",
3
- "version": "0.10.6",
3
+ "version": "0.10.7",
4
4
  "type": "module",
5
5
  "description": "SmoothBricks monorepo automation CLI",
6
6
  "bin": {
@@ -64,8 +64,8 @@
64
64
  "dependencies": {
65
65
  "@arethetypeswrong/core": "^0.18.2",
66
66
  "@prettier/sync": "^0.6.1",
67
- "@smoothbricks/nx-plugin": "0.3.4",
68
- "@smoothbricks/validation": "0.1.4",
67
+ "@smoothbricks/nx-plugin": "0.3.5",
68
+ "@smoothbricks/validation": "0.1.5",
69
69
  "commander": "^14.0.3",
70
70
  "prettier": "^3.6.1",
71
71
  "publint": "^0.3.18",
@@ -73,7 +73,7 @@
73
73
  "sherif": "^1.11.1",
74
74
  "toml-eslint-parser": "^1.0.3",
75
75
  "tslib": "^2.8.1",
76
- "typia": "13.1.19"
76
+ "typia": "13.2.0"
77
77
  },
78
78
  "devDependencies": {
79
79
  "fast-check": "^4.4.0"
package/src/cli.ts CHANGED
@@ -348,10 +348,17 @@ function buildProgram(): Command {
348
348
  .command('nx-run-many')
349
349
  .requiredOption('--targets <targets>')
350
350
  .option('--projects <projects>')
351
+ .option('--projects-with-targets <targets>', 'select projects owning any comma-separated target or target glob')
351
352
  .option('--configuration <configuration>')
352
353
  .option('--collect-outputs <directory>')
353
354
  .action(
354
- async (options: { targets: string; projects?: string; configuration?: string; collectOutputs?: string }) => {
355
+ async (options: {
356
+ targets: string;
357
+ projects?: string;
358
+ projectsWithTargets?: string;
359
+ configuration?: string;
360
+ collectOutputs?: string;
361
+ }) => {
355
362
  const { githubCiNxRunMany } = await import('./github-ci/index.js');
356
363
  await githubCiNxRunMany(await findRepoRoot(), options);
357
364
  },
@@ -53,6 +53,28 @@ describe('GitHub CI Nx target expansion', () => {
53
53
  ]);
54
54
  });
55
55
 
56
+ it('selects test owners from target globs and fails if any selected project lacks test', () => {
57
+ const platformProjects = projects.map((project) =>
58
+ project.project === 'mobile' ? { ...project, targets: [...project.targets, 'test'] } : project,
59
+ );
60
+ const expanded = expandNxTargetRuns(platformProjects, {
61
+ targets: 'test',
62
+ projectsWithTargets: '*-macos,*-ios',
63
+ });
64
+
65
+ expect(expanded.runs.map((run) => [run.target, run.projects.map((project) => project.project)])).toEqual([
66
+ ['test', ['desktop', 'mobile']],
67
+ ]);
68
+ const [testRun] = expanded.runs;
69
+ if (!testRun) {
70
+ throw new Error('Expected a test target run.');
71
+ }
72
+ expect(nxRunManyArgs(testRun)).toEqual(['run-many', '-t', 'test', '--projects=desktop,mobile', '--parallel=100%']);
73
+ expect(() => expandNxTargetRuns(projects, { targets: 'test', projectsWithTargets: '*-macos,*-ios' })).toThrow(
74
+ 'Nx target test is missing for project(s) selected by --projects-with-targets *-macos,*-ios: mobile.',
75
+ );
76
+ });
77
+
56
78
  it('reports a zero-match glob as an empty run set', () => {
57
79
  expect(expandNxTargetRuns(projects, { targets: '*-windows' })).toEqual({
58
80
  runs: [],
@@ -155,6 +177,21 @@ describe('collected Nx outputs', () => {
155
177
  });
156
178
  });
157
179
 
180
+ it('runs test for projects selected by platform target ownership', async () => {
181
+ await withNxRunManyFixture(async ({ root }) => {
182
+ await writeFile(join(root, 'packages/app/test-target.ts'), "await Bun.write('test-ran.txt', 'tested');\n");
183
+ await writeFile(join(root, 'packages/app/build-target.ts'), "throw new Error('build must not run here');\n");
184
+
185
+ await githubCiNxRunMany(root, {
186
+ targets: 'test',
187
+ projectsWithTargets: '*-macos,*-ios',
188
+ });
189
+
190
+ await expect(readFile(join(root, 'test-ran.txt'), 'utf8')).resolves.toBe('tested');
191
+ await expect(readFile(join(root, 'build-ran.txt'), 'utf8')).rejects.toThrow();
192
+ });
193
+ });
194
+
158
195
  it('applies an empty manifest after artifact transport omits its empty workspace directory', async () => {
159
196
  await withOutputFixture(async ({ root, artifact }) => {
160
197
  const manifest = await collectNxOutputs(root, artifact, [], SOURCE_SHA);
@@ -472,7 +509,19 @@ async function withNxRunManyFixture(
472
509
  join(root, 'packages/app/package.json'),
473
510
  JSON.stringify({
474
511
  name: '@fixture/app',
475
- nx: { name: 'app', targets: { build: { executor: 'nx:noop' } } },
512
+ nx: {
513
+ name: 'app',
514
+ targets: {
515
+ 'build-macos': {
516
+ executor: 'nx:run-commands',
517
+ options: { command: 'bun packages/app/build-target.ts', cwd: '.' },
518
+ },
519
+ test: {
520
+ executor: 'nx:run-commands',
521
+ options: { command: 'bun packages/app/test-target.ts', cwd: '.' },
522
+ },
523
+ },
524
+ },
476
525
  }),
477
526
  );
478
527
  await $`git init --quiet`.cwd(root);
@@ -168,6 +168,7 @@ export interface NxRunManyOptions {
168
168
  configuration?: string;
169
169
  collectOutputs?: string;
170
170
  allowEmptyProjects?: boolean;
171
+ projectsWithTargets?: string;
171
172
  }
172
173
 
173
174
  export interface ExpandedNxTargetRuns {
@@ -176,10 +177,18 @@ export interface ExpandedNxTargetRuns {
176
177
  }
177
178
 
178
179
  export function expandNxTargetRuns(projects: ProjectTargets[], options: NxRunManyOptions): ExpandedNxTargetRuns {
179
- const selectedProjects = selectProjects(projects, options.projects);
180
- if (options.projects !== undefined && selectedProjects.length === 0 && options.allowEmptyProjects !== true) {
180
+ const namedProjects = selectProjects(projects, options.projects);
181
+ if (options.projects !== undefined && namedProjects.length === 0 && options.allowEmptyProjects !== true) {
181
182
  throw new Error(`No Nx projects matched --projects ${options.projects}.`);
182
183
  }
184
+ const selectedProjects = selectProjectsWithTargets(namedProjects, options.projectsWithTargets);
185
+ if (
186
+ options.projectsWithTargets !== undefined &&
187
+ selectedProjects.length === 0 &&
188
+ options.allowEmptyProjects !== true
189
+ ) {
190
+ throw new Error(`No Nx projects own targets matching --projects-with-targets ${options.projectsWithTargets}.`);
191
+ }
183
192
  const selectedTargetNames = [...new Set(selectedProjects.flatMap((project) => project.targets))].sort((a, b) =>
184
193
  a.localeCompare(b),
185
194
  );
@@ -195,6 +204,16 @@ export function expandNxTargetRuns(projects: ProjectTargets[], options: NxRunMan
195
204
  unmatchedGlobs.push(targetPattern);
196
205
  }
197
206
  for (const target of targets) {
207
+ if (!isGlob && options.projectsWithTargets !== undefined) {
208
+ const missingProjects = selectedProjects
209
+ .filter((project) => !project.targets.includes(target))
210
+ .map((project) => project.project);
211
+ if (missingProjects.length > 0) {
212
+ throw new Error(
213
+ `Nx target ${target} is missing for project(s) selected by --projects-with-targets ${options.projectsWithTargets}: ${missingProjects.join(', ')}.`,
214
+ );
215
+ }
216
+ }
198
217
  if (addedTargets.has(target)) {
199
218
  continue;
200
219
  }
@@ -321,6 +340,16 @@ function selectProjects(projects: ProjectTargets[], selectors: string | undefine
321
340
  .sort((left, right) => left.project.localeCompare(right.project));
322
341
  }
323
342
 
343
+ function selectProjectsWithTargets(projects: ProjectTargets[], selectors: string | undefined): ProjectTargets[] {
344
+ if (selectors === undefined) {
345
+ return projects;
346
+ }
347
+ const patterns = commaSeparatedValues(selectors);
348
+ return projects.filter((project) =>
349
+ project.targets.some((target) => patterns.some((pattern) => new Bun.Glob(pattern).match(target))),
350
+ );
351
+ }
352
+
324
353
  function commaSeparatedValues(value: string): string[] {
325
354
  return value
326
355
  .split(',')
package/src/lib/json.ts CHANGED
@@ -80,7 +80,7 @@ export type NxDependsOn = string | NxDependsOnObject;
80
80
  /** Nx target options are executor-specific open bags. */
81
81
  export type NxTargetOptions = Record<string, unknown>;
82
82
 
83
- export type NxInput = string | Record<string, unknown>;
83
+ export type NxInput = string | object;
84
84
 
85
85
  export interface NxTargetConfig {
86
86
  executor?: string;
@@ -129,6 +129,7 @@ function listWorkspacePackageJsonPaths(root: string, workspacePatterns: string[]
129
129
  const paths: string[] = [];
130
130
  for (const pattern of workspacePatterns) {
131
131
  if (!pattern.endsWith('/*')) {
132
+ paths.push(join(root, pattern, 'package.json'));
132
133
  continue;
133
134
  }
134
135
  const parent = join(root, pattern.slice(0, -2));
@@ -99,7 +99,7 @@ describe('publish workflow definition', () => {
99
99
 
100
100
  expect(stepAnchorNumbers(singleJob)).toEqual(Array.from({ length: 15 }, (_, index) => index + 1));
101
101
  expect(stepAnchorNumbers(linuxCandidate)).toEqual(Array.from({ length: 19 }, (_, index) => index + 1));
102
- expect(stepAnchorNumbers(macosPlatform)).toEqual(Array.from({ length: 7 }, (_, index) => index + 1));
102
+ expect(stepAnchorNumbers(macosPlatform)).toEqual(Array.from({ length: 8 }, (_, index) => index + 1));
103
103
  expect(stepAnchorNumbers(finalJob)).toEqual(Array.from({ length: 14 }, (_, index) => index + 1));
104
104
  expect(singleJob).toContain('# Step 14\n - name: ๐Ÿ“ฆ Publish release (${{ steps.version.outputs.mode }})');
105
105
  expect(singleJob).toContain('# Step 15\n - name: ๐Ÿงน Cleanup and cache Nix/devenv');
@@ -109,7 +109,7 @@ describe('publish workflow definition', () => {
109
109
  );
110
110
  expect(linuxCandidate).toContain('# Step 19\n - name: ๐Ÿงน Cleanup and cache Nix/devenv');
111
111
  expect(macosPlatform).toContain('# Step 5\n - name: ๐ŸŽ Build selected macOS and iOS release outputs');
112
- expect(macosPlatform).toContain('# Step 7\n - name: ๐Ÿงน Cleanup and cache Nix/devenv');
112
+ expect(macosPlatform).toContain('# Step 8\n - name: ๐Ÿงน Cleanup and cache Nix/devenv');
113
113
  expect(finalJob).toContain('# Step 7\n - name: ๐Ÿงฏ Repair pending releases');
114
114
  expect(finalJob).toContain('# Step 8\n - name: โ™ป๏ธ Restore validated release state');
115
115
  expect(finalJob).toContain('# Step 9\n - name: ๐Ÿ“ฆ Apply verified Linux outputs');
@@ -156,12 +156,20 @@ describe('publish workflow definition', () => {
156
156
  expect(linuxCandidate).toContain(
157
157
  `smoo github-ci nx-run-many --targets "${LINUX_PLATFORM_TARGET_GLOBS.join(',')}" --projects`,
158
158
  );
159
- expect(macosPlatform).not.toContain('smoo github-ci nx-run-many');
160
159
  expect(foldedRunCommand(macosPlatform, '๐ŸŽ Build selected macOS and iOS release outputs')).toBe(
161
160
  `smoo release build-platform-outputs --bump "\${{ inputs.bump }}" --ref "\${{ github.sha }}" --targets "${MACOS_PLATFORM_TARGET_GLOBS.join(
162
161
  ',',
163
162
  )}" --output "\${{ runner.temp }}/macos-platform-outputs"`,
164
163
  );
164
+ expect(macosPlatform).toContain(
165
+ `run: smoo github-ci nx-run-many --targets test --projects-with-targets "${MACOS_PLATFORM_TARGET_GLOBS.join(',')}"`,
166
+ );
167
+ expect(macosPlatform.indexOf('- name: ๐ŸŽ Build selected macOS and iOS release outputs')).toBeLessThan(
168
+ macosPlatform.indexOf('- name: ๐Ÿงช Unit test macOS and iOS packages'),
169
+ );
170
+ expect(macosPlatform.indexOf('- name: ๐Ÿงช Unit test macOS and iOS packages')).toBeLessThan(
171
+ macosPlatform.indexOf('- name: ๐Ÿ“ค Upload macOS platform outputs'),
172
+ );
165
173
  expect(finalJob).not.toContain('smoo release version');
166
174
  expect(finalJob).not.toContain('nx-run-many');
167
175
  expect(foldedRunCommand(finalJob, '๐Ÿงฏ Repair pending releases')).toBe(