@vzn/vx 0.0.241 → 0.0.242

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.241",
3
+ "version": "0.0.242",
4
4
  "description": "A fast, correct task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,10 +17,10 @@
17
17
  "node": ">=18"
18
18
  },
19
19
  "optionalDependencies": {
20
- "@vzn/vx-linux-x64": "0.0.241",
21
- "@vzn/vx-linux-arm64": "0.0.241",
22
- "@vzn/vx-darwin-x64": "0.0.241",
23
- "@vzn/vx-darwin-arm64": "0.0.241"
20
+ "@vzn/vx-linux-x64": "0.0.242",
21
+ "@vzn/vx-linux-arm64": "0.0.242",
22
+ "@vzn/vx-darwin-x64": "0.0.242",
23
+ "@vzn/vx-darwin-arm64": "0.0.242"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -847,6 +847,11 @@ export interface GitEnumeration {
847
847
  trusted: Map<string, string>
848
848
  /** Whether the worktree had uncommitted changes; null when `git status` failed. */
849
849
  dirty: boolean | null
850
+ /**
851
+ * What `git status` listed, workspace-relative: modified, staged or deleted
852
+ * paths (both sides of a rename) and untracked files. Null when it failed.
853
+ */
854
+ changed: readonly string[] | null
850
855
  /** Workspace-relative paths whose names are not UTF-8 (`decodeGitZ`). */
851
856
  undecodable: readonly string[]
852
857
  /** `Date.now()` before the spawns: what `trusted` says is true as of no earlier. */
@@ -1038,7 +1043,39 @@ export async function startGitEnumeration(
1038
1043
  if (parsedStatus !== null && parsedStatus.undecodable.size > 0) {
1039
1044
  undecodable.push(...stripPrefixFromSet(parsedStatus.undecodable, gitPrefix))
1040
1045
  }
1041
- return { all, trusted, dirty: worktreeDirty, undecodable, startedAtMs }
1046
+ return {
1047
+ all,
1048
+ trusted,
1049
+ dirty: worktreeDirty,
1050
+ changed: dirty === null ? null : [...dirty, ...untracked],
1051
+ undecodable,
1052
+ startedAtMs,
1053
+ }
1054
+ }
1055
+
1056
+ /** A run's whole-tree enumeration, started on its first ask and shared after. */
1057
+ export interface LazyGitEnumeration {
1058
+ start(): Promise<GitEnumeration>
1059
+ /** The enumeration, once something started it. */
1060
+ readonly started: Promise<GitEnumeration> | undefined
1061
+ }
1062
+
1063
+ export function lazyGitEnumeration(workspaceRoot: string): LazyGitEnumeration {
1064
+ let started: Promise<GitEnumeration> | undefined
1065
+ return {
1066
+ start() {
1067
+ if (started === undefined) {
1068
+ started = startGitEnumeration(workspaceRoot, ['.'])
1069
+ // Its first reader may never await it (a config throws first); the
1070
+ // one that does still sees the error.
1071
+ started.catch(() => {})
1072
+ }
1073
+ return started
1074
+ },
1075
+ get started() {
1076
+ return started
1077
+ },
1078
+ }
1042
1079
  }
1043
1080
 
1044
1081
  /** The partition half of `populateGitFilesCache`: store per-project slices of one enumeration. */
@@ -51,6 +51,8 @@ export {
51
51
  gitPathspecs,
52
52
  repoRootOf,
53
53
  startGitEnumeration,
54
+ lazyGitEnumeration,
55
+ type LazyGitEnumeration,
54
56
  type GitEnumeration,
55
57
  } from './git-inputs.js'
56
58
  export { LayeredCache, type RemoteCacheLayer } from './layered-cache.js'
@@ -259,6 +259,14 @@ export interface DiscoverContext extends WorkspaceHookContext {
259
259
  readonly cacheDir: string
260
260
  /** The projects found so far: core's, then earlier plugins'. */
261
261
  readonly projects: readonly ProjectMeta[]
262
+ /**
263
+ * What `git status` lists in the workspace, workspace-relative: modified,
264
+ * staged or deleted paths (both sides of a rename) and untracked files;
265
+ * null outside a git worktree. It is the run's own enumeration, so a
266
+ * plugin whose answer depends on the worktree (nx()'s graph key) reads it
267
+ * here instead of walking the tree a second time.
268
+ */
269
+ worktreeChanges(): Promise<readonly string[] | null>
262
270
  }
263
271
 
264
272
  export interface NamedProject {
@@ -21,7 +21,7 @@ import {
21
21
  applyGitEnumeration,
22
22
  gitPathspecs,
23
23
  startGitEnumeration,
24
- type GitEnumeration,
24
+ lazyGitEnumeration,
25
25
  } from '../cache/index.js'
26
26
  import {
27
27
  buildPackageGraph,
@@ -54,6 +54,7 @@ import {
54
54
  } from './plugin-host.js'
55
55
  import {
56
56
  discoverProjects,
57
+ gitOfDiscovery,
57
58
  loadProjects,
58
59
  loadWorkspacePlugins,
59
60
  type LoadedProjects,
@@ -176,13 +177,15 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
176
177
  // waits: its pathspecs depend on which projects the configs pull in.
177
178
  const unscoped =
178
179
  options.projects === undefined && options.tasks.some((spec) => spec.indexOf('#') <= 0)
179
- const earlyGit: Promise<GitEnumeration> | undefined = unscoped
180
- ? startGitEnumeration(workspaceRoot, ['.'])
181
- : undefined
182
- // A broken config below throws before this is awaited; the detached
183
- // handler keeps that from surfacing as an unhandled rejection (the real
184
- // await further down still sees the error).
185
- earlyGit?.catch(() => {})
180
+ // A `discover` hook that reads the worktree (nx()'s graph key) starts the
181
+ // whole-tree enumeration a scoped run would otherwise scope later, here or
182
+ // in the CLI's selection pass; the run reuses it instead of walking the
183
+ // tree twice (G-75).
184
+ const reused = options.discovered?.root === workspaceRoot ? options.discovered : undefined
185
+ const git =
186
+ (reused !== undefined ? gitOfDiscovery(reused.projects) : undefined) ??
187
+ lazyGitEnumeration(workspaceRoot)
188
+ if (unscoped) void git.start()
186
189
  const workspace = await loadWorkspace(workspaceRoot, reads)
187
190
  const { workspaceConfig, plugins } = await loadWorkspacePlugins(workspaceRoot, (m) =>
188
191
  log.status(m),
@@ -194,9 +197,9 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
194
197
  ? path.resolve(options.cwd, options.cacheDir)
195
198
  : resolveCacheDir(workspaceRoot, workspaceConfig)
196
199
  const projectMetas =
197
- options.discovered?.root === workspaceRoot
198
- ? options.discovered.projects
199
- : await discoverProjects(workspace, plugins, cacheDir, (m) => log.status(m))
200
+ reused !== undefined
201
+ ? reused.projects
202
+ : await discoverProjects(workspace, plugins, cacheDir, (m) => log.status(m), git)
200
203
  mark('discover projects')
201
204
 
202
205
  // SCOPED config loading: configs are programs, and evaluating 1090
@@ -377,7 +380,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
377
380
  ),
378
381
  )
379
382
  const projectDirs = [...projects.values()].map((p) => p.dir)
380
- const enumeration = await (earlyGit ??
383
+ const enumeration = await (git.started ??
381
384
  startGitEnumeration(
382
385
  workspaceRoot,
383
386
  gitPathspecs(workspaceRoot, projectDirs, usesWorkspaceInputs),
@@ -18,7 +18,7 @@ import {
18
18
  type Workspace,
19
19
  } from '../workspace/index.js'
20
20
  import type { WorkspaceConfig } from '../config.js'
21
- import { Cache } from '../cache/index.js'
21
+ import { Cache, lazyGitEnumeration, type LazyGitEnumeration } from '../cache/index.js'
22
22
  import { UserError } from '../util/index.js'
23
23
  import {
24
24
  buildPackageGraph,
@@ -56,6 +56,17 @@ export async function loadWorkspacePlugins(
56
56
  return { workspaceConfig, plugins }
57
57
  }
58
58
 
59
+ /**
60
+ * The enumeration a discovery's `discover` hooks shared, by the array it
61
+ * returned: a run handed that array (`RunOptions.discovered`, the CLI's
62
+ * `--filter` pass) reuses the walk a hook started instead of a second one.
63
+ */
64
+ const discoveredGit = new WeakMap<readonly ProjectMeta[], LazyGitEnumeration>()
65
+
66
+ export function gitOfDiscovery(projects: readonly ProjectMeta[]): LazyGitEnumeration | undefined {
67
+ return discoveredGit.get(projects)
68
+ }
69
+
59
70
  /**
60
71
  * The workspace's projects: core's discovery, then the plugin `discover`
61
72
  * stage. With no plugin declaring it, `listProjects` alone.
@@ -65,13 +76,21 @@ export async function discoverProjects(
65
76
  plugins: readonly VxPlugin[],
66
77
  cacheDir: string,
67
78
  warn: (message: string) => void,
79
+ /** The run's enumeration, which `DiscoverContext.worktreeChanges` reads (and starts). */
80
+ git: LazyGitEnumeration = lazyGitEnumeration(workspace.root),
68
81
  ): Promise<ProjectMeta[]> {
69
82
  const projects = await listProjects(workspace)
70
83
  if (!hasHook(plugins, 'discover')) return projects
84
+ discoveredGit.set(projects, git)
85
+ const worktreeChanges = (): Promise<readonly string[] | null> =>
86
+ git.start().then(
87
+ (e) => e.changed,
88
+ () => null,
89
+ )
71
90
  await applyDiscoverHooks(
72
91
  plugins,
73
92
  projects,
74
- { workspaceRoot: workspace.root, cacheDir, warn },
93
+ { workspaceRoot: workspace.root, cacheDir, warn, worktreeChanges },
75
94
  (named, plugin) => namedProject(workspace, projects, named, plugin.name),
76
95
  )
77
96
  return projects.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))