@vzn/vx 0.0.411 → 0.0.413

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.411",
3
+ "version": "0.0.413",
4
4
  "description": "The fastest task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,10 +21,10 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@vzn/vx-linux-x64": "0.0.411",
25
- "@vzn/vx-linux-arm64": "0.0.411",
26
- "@vzn/vx-darwin-x64": "0.0.411",
27
- "@vzn/vx-darwin-arm64": "0.0.411"
24
+ "@vzn/vx-linux-x64": "0.0.413",
25
+ "@vzn/vx-linux-arm64": "0.0.413",
26
+ "@vzn/vx-darwin-x64": "0.0.413",
27
+ "@vzn/vx-darwin-arm64": "0.0.413"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -10,7 +10,7 @@ import { unlink } from 'node:fs/promises'
10
10
  import path from 'node:path'
11
11
  import { executablePath, relPosix, UserError } from '../util/index.js'
12
12
  import type { ProjectMeta } from './workspace.js'
13
- import { loadWorkspace, unreachedHint, unreachedPackages } from './workspace.js'
13
+ import { loadWorkspace, reachesManifest, unreachedHint, unreachedPackages } from './workspace.js'
14
14
  import { WORKSPACE_CONFIG_FILENAMES } from './project-loader.js'
15
15
 
16
16
  /**
@@ -357,8 +357,10 @@ export async function applyMigration(args: ApplyMigrationArgs): Promise<number>
357
357
  // Globs that reach no package: "no scripts" was true and named nothing
358
358
  // to fix (M-42).
359
359
  const globs = workspace?.packageGlobs.filter((g) => g !== '.') ?? []
360
+ // Asked of the globs, not of `metas`: a nameless or shared-name member
361
+ // is matched yet absent from `metas` (M-46).
360
362
  const noMembers =
361
- globs.length > 0 && metas.every((m) => path.resolve(m.dir) === path.resolve(root))
363
+ workspace !== undefined && globs.length > 0 && !(await reachesManifest(workspace))
362
364
  ? [
363
365
  `the workspace globs (${globs.map((g) => `"${g}"`).join(', ')}) match no package.json: add a package under one, or fix the glob`,
364
366
  ]
@@ -456,6 +456,7 @@ export async function loadProjectConfigs(
456
456
  // More than one evaluation in flight: a change seen after one load may
457
457
  // be another's.
458
458
  let overlapping = false
459
+ let tainted = false
459
460
  const loadOne = async (entry: (typeof prepared)[number]): Promise<Loaded> => {
460
461
  const { configPath, cacheKey } = entry
461
462
  // A fast key that missed: the closure is stale or the file changed.
@@ -500,7 +501,7 @@ export async function loadProjectConfigs(
500
501
  const changed = repeat
501
502
  ? []
502
503
  : [
503
- ...restoreBuiltins(builtins),
504
+ ...restoreBuiltins(builtins, overlapping),
504
505
  ...restoreEnv(env),
505
506
  ...restoreCwd(cwd),
506
507
  ...restoreUmask(umask),
@@ -585,20 +586,30 @@ export async function loadProjectConfigs(
585
586
  ...restoreUmask(umask),
586
587
  ]
587
588
  : []
588
- if (first?.failed instanceof ChangedInRound) {
589
+ // Overlapping loads check most built-ins only here, so a change one
590
+ // config made may have broken another's load: it is refused first, and
591
+ // nothing the round evaluated is stored.
592
+ const changedInRound =
593
+ first?.failed instanceof ChangedInRound
594
+ ? first.failed.changed
595
+ : overlapping && changed.length > 0
596
+ ? changed
597
+ : undefined
598
+ if (changedInRound !== undefined) {
599
+ tainted = true
589
600
  for (const i of misses) {
590
601
  const configPath = prepared[i]!.configPath
591
602
  const own = await builtinsChangedBy(configPath)
592
603
  if (own.length > 0) throw builtinsChanged(own, configPath)
593
604
  }
594
- throw builtinsChanged(first.failed.changed)
605
+ throw builtinsChanged(changedInRound)
595
606
  }
596
607
  if (first !== undefined) throw first.failed
597
608
  if (changed.length > 0) throw builtinsChanged(changed)
598
609
  return results.map((r) => (r as Loaded).config)
599
610
  } finally {
600
611
  endRound()
601
- if (store !== undefined) {
612
+ if (store !== undefined && !tainted) {
602
613
  if (evals.length > 0) {
603
614
  if (store.putConfigEvals !== undefined) store.putConfigEvals(evals)
604
615
  else for (const [k, json] of evals) store.putConfigEval(k, json)
@@ -655,10 +666,11 @@ interface OwnProperties {
655
666
 
656
667
  interface BuiltinSnapshot {
657
668
  readonly prims: Primitives
658
- readonly props: readonly OwnProperties[]
669
+ /** Undefined for a watched name the snapshot leaves out. */
670
+ readonly props: readonly (OwnProperties | undefined)[]
659
671
  }
660
672
 
661
- function builtinSnapshot(): BuiltinSnapshot {
673
+ function builtinSnapshot(leaveOut?: string): BuiltinSnapshot {
662
674
  const prims: Primitives = {
663
675
  ownKeys: Reflect.ownKeys,
664
676
  descriptorOf: Object.getOwnPropertyDescriptor,
@@ -668,8 +680,12 @@ function builtinSnapshot(): BuiltinSnapshot {
668
680
  same: Object.is,
669
681
  keyName: String,
670
682
  }
671
- const props: OwnProperties[] = []
683
+ const props: (OwnProperties | undefined)[] = []
672
684
  for (let i = 0; i < WATCHED_BUILTINS.length; i++) {
685
+ if (WATCHED_BUILTINS[i]![0] === leaveOut) {
686
+ props[i] = undefined
687
+ continue
688
+ }
673
689
  const proto = WATCHED_BUILTINS[i]![1]
674
690
  const keys = prims.ownKeys(proto)
675
691
  const descriptors: PropertyDescriptor[] = []
@@ -727,15 +743,36 @@ function builtinsChanged(changed: readonly string[], configPath?: string): UserE
727
743
  )
728
744
  }
729
745
 
746
+ /**
747
+ * The built-ins checked after each of several overlapping loads: the ones
748
+ * the loader itself reads through between loads (its promises, maps, sets,
749
+ * JSON and keys). The rest are checked once, at the round's end: all of
750
+ * them after every load was ~73 µs a config, a quarter of a cold load of
751
+ * 1,000 (2026-10-03). A lone load checks them all. By index, decided at
752
+ * module load: a lookup through a `Set` is one a config can replace (D-124).
753
+ */
754
+ const EVERY_LOAD: readonly boolean[] = WATCHED_BUILTINS.map(([name]) =>
755
+ [
756
+ 'Object.prototype',
757
+ 'JSON',
758
+ 'Promise.prototype',
759
+ 'Map.prototype',
760
+ 'Set.prototype',
761
+ 'Bun.hash',
762
+ ].includes(name),
763
+ )
764
+
730
765
  /** Puts back what changed since `before`, naming each property it put back. */
731
- function restoreBuiltins(before: BuiltinSnapshot | undefined): string[] {
766
+ function restoreBuiltins(before: BuiltinSnapshot | undefined, everyLoadOnly = false): string[] {
732
767
  const changed: string[] = []
733
768
  if (before === undefined) return changed
734
769
  const p = before.prims
735
770
  for (let i = 0; i < WATCHED_BUILTINS.length; i++) {
736
771
  const name = WATCHED_BUILTINS[i]![0]
772
+ if (everyLoadOnly && !EVERY_LOAD[i]) continue
737
773
  const proto = WATCHED_BUILTINS[i]![1]
738
- const was = before.props[i]!
774
+ const was = before.props[i]
775
+ if (was === undefined) continue
739
776
  const keys = p.ownKeys(proto)
740
777
  if (unchanged(p, proto, keys, was)) continue
741
778
  // Slow path, a change only: a linear lookup keeps it off `Map`.
@@ -890,7 +927,29 @@ export async function loadWorkspaceConfig(root: string): Promise<WorkspaceConfig
890
927
  const since = workspaceLoadedAt.get(configPath)
891
928
  if (since !== undefined) await refuseStaleWorkspaceImports(configPath, since)
892
929
  const startedAt = Date.now()
893
- const mod = (await loadDefaultExport(configPath, 'Workspace', bytes)) as WorkspaceConfig
930
+ // The same guard a project config's first load has: the workspace config
931
+ // runs in this process too, and its `Object.prototype.exec` ran in a
932
+ // project's task under a key that never saw it (D-126). Its globals are
933
+ // left out: it loads first in every run, filtered or not, so none
934
+ // depends on what else loaded (D-122's case), and plugins' tests and
935
+ // tools hand state through them.
936
+ const builtins = builtinSnapshot('globalThis')
937
+ const env = { ...process.env }
938
+ const cwd = process.cwd()
939
+ const umask = currentUmask()
940
+ let mod: WorkspaceConfig
941
+ try {
942
+ mod = (await loadDefaultExport(configPath, 'Workspace', bytes)) as WorkspaceConfig
943
+ } finally {
944
+ const changed = [
945
+ ...restoreBuiltins(builtins),
946
+ ...restoreEnv(env),
947
+ ...restoreCwd(cwd),
948
+ ...restoreUmask(umask),
949
+ ]
950
+ // eslint-disable-next-line no-unsafe-finally -- the refusal outranks the load's own error
951
+ if (changed.length > 0) throw builtinsChanged(changed, configPath)
952
+ }
894
953
  // Checked again once awaited, as a project config is: a Promise default
895
954
  // passed the first check, and `Promise.resolve(null)` crashed the
896
955
  // validator with a stack while `Promise.resolve(42)` loaded as no
@@ -395,6 +395,28 @@ export function memberBaseDirs(workspace: Workspace): string[] {
395
395
  return [...bases]
396
396
  }
397
397
 
398
+ /**
399
+ * True when a member glob reaches a `package.json` other than the root's,
400
+ * addressable or not: a nameless manifest, or two sharing a name, is
401
+ * matched and left out, not unmatched (M-46).
402
+ */
403
+ export async function reachesManifest(workspace: Workspace): Promise<boolean> {
404
+ const { positive, negative } = splitPackageGlobs(workspace.packageGlobs)
405
+ const root = path.resolve(workspace.root)
406
+ for (const pattern of positive) {
407
+ for (const dir of await memberDirs(workspace.root, pattern)) {
408
+ if (path.resolve(dir) === root) continue
409
+ if (negative.length > 0 && excludedBy(relPosix(workspace.root, dir), negative)) continue
410
+ try {
411
+ if ((await stat(path.join(dir, 'package.json'))).isFile()) return true
412
+ } catch (err) {
413
+ absent(err, undefined)
414
+ }
415
+ }
416
+ }
417
+ return false
418
+ }
419
+
398
420
  /**
399
421
  * The directories a workspace glob names. For the `<dir>/*` shape this is
400
422
  * one readdir of `<dir>` — the same answer `Bun.Glob` gives, at a third of