@vzn/vx 0.0.22 → 0.0.24

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 (2) hide show
  1. package/package.json +5 -5
  2. package/src/cli/watch.ts +74 -29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "description": "An open, extensible monorepo task runner.",
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.22",
21
- "@vzn/vx-linux-arm64": "0.0.22",
22
- "@vzn/vx-darwin-x64": "0.0.22",
23
- "@vzn/vx-darwin-arm64": "0.0.22"
20
+ "@vzn/vx-linux-x64": "0.0.24",
21
+ "@vzn/vx-linux-arm64": "0.0.24",
22
+ "@vzn/vx-darwin-x64": "0.0.24",
23
+ "@vzn/vx-darwin-arm64": "0.0.24"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/watch.ts CHANGED
@@ -504,9 +504,20 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
504
504
  // closing it properly needs the workspaceWide decision made without loading
505
505
  // configs.
506
506
  process.stdout.write('vx watch: initial run...\n\n')
507
- const initial = await runOrchestrator(opts)
507
+ // A run that throws (a config that does not parse) is a failed cycle, as
508
+ // it is once the loop runs: watch keeps watching, so the fix re-runs.
509
+ // Uncaught, it ended watch at start while the same break mid-watch did
510
+ // not (item 1017).
511
+ let held: HeldPersistent | undefined
512
+ try {
513
+ held = (await runOrchestrator(opts)).persistent
514
+ } catch (err) {
515
+ process.stderr.write(
516
+ `vx watch: cycle failed: ${err instanceof Error ? err.message : String(err)}\n`,
517
+ )
518
+ }
508
519
  if (stop.signal.aborted) {
509
- await initial.persistent?.stop(forwardedSignal(stop.signal.reason))
520
+ await held?.stop(forwardedSignal(stop.signal.reason))
510
521
  return 0
511
522
  }
512
523
 
@@ -519,7 +530,7 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
519
530
  const ws = await loadCliWorkspace(workspaceRoot)
520
531
  return await runWatchLoop({
521
532
  opts,
522
- held: initial.persistent,
533
+ held,
523
534
  stop: stop.signal,
524
535
  workspaceRoot,
525
536
  projects: watched,
@@ -538,11 +549,13 @@ export async function watchCmd(args: readonly string[]): Promise<number> {
538
549
  // scope is the one resolved at start; a new package joins it only as a
539
550
  // dependency of it.
540
551
  rediscover: async () => {
541
- const all = await listProjects(await loadWorkspace(workspaceRoot))
552
+ const workspace = await loadWorkspace(workspaceRoot)
553
+ const all = await listProjects(workspace)
542
554
  const sweep = await sweepConfigs(all, workspaceRoot, load)
543
555
  const now = await watchedProjects(workspaceRoot, all, inScope(all), load, sweep.staged)
544
556
  return {
545
557
  projects: now,
558
+ memberBases: memberBaseDirs(workspace),
546
559
  workspaceWide: sweep.workspaceWide,
547
560
  workspaceInputs: sweep.workspaceInputs,
548
561
  outputs: sweep.outputs,
@@ -751,7 +764,10 @@ export function makeRootEventFilter(
751
764
  // this line load-bearing.
752
765
  if (
753
766
  !rel.includes('/') &&
754
- (isWorkspaceFingerprintFile(rel) || isWorkspaceConfigFile(rel) || claimedRootFiles.has(rel))
767
+ (isWorkspaceFingerprintFile(rel) ||
768
+ isWorkspaceConfigFile(rel) ||
769
+ rel === 'package.json' ||
770
+ claimedRootFiles.has(rel))
755
771
  ) {
756
772
  return true
757
773
  }
@@ -793,7 +809,7 @@ interface WatchLoopArgs {
793
809
  * loaded once per process, so the set is fixed for the loop.
794
810
  */
795
811
  claimedRootFiles: ReadonlySet<string>
796
- /** The directory each `<dir>/*` package glob names; a member coming or going there is a cycle. */
812
+ /** The directory each `<dir>/*` package glob names at start; a member coming or going there is a cycle. */
797
813
  memberBases: readonly string[]
798
814
  /** Every package's directory, in scope or not: a member base's other entries are packages still to come. */
799
815
  packageDirs: ReadonlySet<string>
@@ -803,6 +819,7 @@ interface WatchLoopArgs {
803
819
 
804
820
  interface Rediscovered {
805
821
  projects: readonly ProjectMeta[]
822
+ memberBases: readonly string[]
806
823
  workspaceWide: boolean
807
824
  workspaceInputs: readonly string[]
808
825
  outputs: ReadonlyMap<string, readonly string[]>
@@ -822,21 +839,29 @@ const PROJECT_CONFIGS: ReadonlySet<string> = new Set(PROJECT_CONFIG_FILENAMES)
822
839
  * watcher, a new output must stop being an event) or the workspace config
823
840
  * (a plugin's `project` stage). Until item 891 only a member directory
824
841
  * coming or going re-read the set, so each of these waited for a restart
825
- * while the loop looked alive.
842
+ * while the loop looked alive. A root fingerprint file too, for
843
+ * `pnpm-workspace.yaml`: a glob added there was a cycle that ran the new
844
+ * packages and watched none of them (item 1018).
826
845
  */
827
846
  function shapesWatchedSet(filename: string): boolean {
828
847
  const base = path.basename(filename)
829
- return base === 'package.json' || PROJECT_CONFIGS.has(base) || isWorkspaceConfigFile(base)
848
+ return (
849
+ base === 'package.json' ||
850
+ isWorkspaceFingerprintFile(base) ||
851
+ PROJECT_CONFIGS.has(base) ||
852
+ isWorkspaceConfigFile(base)
853
+ )
830
854
  }
831
855
 
832
856
  const CLOSED: WatchHandle = { close() {} }
833
857
 
834
858
  async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
835
- const { opts, stop, workspaceRoot, projects, cacheDir, memberBases } = args
859
+ const { opts, stop, workspaceRoot, projects, cacheDir } = args
836
860
  // The watched set as of the last cycle: `rearm` replaces these after an
837
861
  // event that can change it, and every filter below reads the current one.
838
862
  let workspaceWide = args.workspaceWide
839
863
  let packageDirs = args.packageDirs
864
+ let memberBases = args.memberBases
840
865
  let projectDirs = args.projectDirs
841
866
  let workspaceInputs = args.workspaceInputs
842
867
  let outputs = args.outputs
@@ -1174,6 +1199,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
1174
1199
  if (
1175
1200
  isWorkspaceFingerprintFile(filename) ||
1176
1201
  isWorkspaceConfigFile(filename) ||
1202
+ filename === 'package.json' ||
1177
1203
  args.claimedRootFiles.has(filename)
1178
1204
  ) {
1179
1205
  if (shapesWatchedSet(filename) || filename === LOCKFILE_NAME) reread = true
@@ -1323,6 +1349,8 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
1323
1349
  configImportFiles = next.configImports
1324
1350
  wsConfigImportFiles = next.workspaceConfigImports
1325
1351
  packageDirs = next.packageDirs
1352
+ memberBases = next.memberBases
1353
+ armBases()
1326
1354
  isIgnoredPath = makeWatchIgnore(cacheDir, outputs, inputs)
1327
1355
  matters = makeRootEventFilter(
1328
1356
  workspaceRoot,
@@ -1362,28 +1390,45 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
1362
1390
  // triggers re-reads the workspace (`rearm`) so the new package's own
1363
1391
  // edits are cycles from then on. Until 2026-09-10 the watched set was
1364
1392
  // fixed when the loop armed: the next cycle ran the new package, and
1365
- // every edit inside it after that was silence.
1366
- for (const base of memberBases) {
1367
- let members = memberEntries(base)
1368
- try {
1369
- arm(base, false, (filename) => {
1370
- if (isIgnoredWatchPath(filename)) return
1371
- // Only a member coming or going. On macOS a non-recursive watcher
1372
- // also reports a member whose CONTENTS changed (FSEvents names the
1373
- // directory a write landed in), so a task writing into its own
1374
- // project — or the arm's own probe file — read as a member event and
1375
- // cost an uncached task one execution per cycle (CI, 2026-09-10).
1376
- const now = memberEntries(base)
1377
- if (sameMembers(members, now)) return
1378
- members = now
1379
- reread = true
1380
- trigger(`${path.relative(workspaceRoot, base)}/${filename}`, path.join(base, filename))
1381
- })
1382
- } catch (err) {
1383
- const msg = err instanceof Error ? err.message : String(err)
1384
- process.stderr.write(`vx watch: cannot watch ${base}: ${msg}\n`)
1393
+ // every edit inside it after that was silence. The bases themselves are
1394
+ // re-read with the set: a glob added to the list watched nothing new
1395
+ // until a restart (item 1018).
1396
+ const baseArms = new Map<string, WatchHandle>()
1397
+ const armBases = (): void => {
1398
+ const want = new Set(memberBases)
1399
+ for (const [base, handle] of baseArms) {
1400
+ if (want.has(base)) continue
1401
+ handle.close()
1402
+ baseArms.delete(base)
1403
+ }
1404
+ for (const base of want) {
1405
+ if (baseArms.has(base)) continue
1406
+ let members = memberEntries(base)
1407
+ try {
1408
+ baseArms.set(
1409
+ base,
1410
+ arm(base, false, (filename) => {
1411
+ if (isIgnoredWatchPath(filename)) return
1412
+ // Only a member coming or going. On macOS a non-recursive watcher
1413
+ // also reports a member whose CONTENTS changed (FSEvents names the
1414
+ // directory a write landed in), so a task writing into its own
1415
+ // project — or the arm's own probe file — read as a member event
1416
+ // and cost an uncached task one execution per cycle (CI,
1417
+ // 2026-09-10).
1418
+ const now = memberEntries(base)
1419
+ if (sameMembers(members, now)) return
1420
+ members = now
1421
+ reread = true
1422
+ trigger(`${path.relative(workspaceRoot, base)}/${filename}`, path.join(base, filename))
1423
+ }),
1424
+ )
1425
+ } catch (err) {
1426
+ const msg = err instanceof Error ? err.message : String(err)
1427
+ process.stderr.write(`vx watch: cannot watch ${base}: ${msg}\n`)
1428
+ }
1385
1429
  }
1386
1430
  }
1431
+ armBases()
1387
1432
  armPending(false)
1388
1433
  armImports()
1389
1434