@vzn/vx 0.0.410 → 0.0.412
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 +20 -22
- package/package.json +5 -5
- package/src/workspace/project-loader.ts +69 -10
package/README.md
CHANGED
|
@@ -34,11 +34,11 @@ Same graph, commands and concurrency: [how it is measured](https://vznjs.github.
|
|
|
34
34
|
|
|
35
35
|
<!-- bench:end -->
|
|
36
36
|
|
|
37
|
-
##
|
|
37
|
+
## What vx is
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
A task runner and build cache for JavaScript monorepos.
|
|
40
|
+
Each task is one shell command with declared inputs and outputs; vx runs
|
|
41
|
+
them in dependency order and replays results from a content-addressed cache.
|
|
42
42
|
|
|
43
43
|
## Install
|
|
44
44
|
|
|
@@ -103,6 +103,12 @@ start, not a way to run the repo. The numbers above are native vx
|
|
|
103
103
|
config. `@vzn/vx-migrate`'s first npm publish is pending.
|
|
104
104
|
[Migration guide](https://vznjs.github.io/vx/guides/migrate/).
|
|
105
105
|
|
|
106
|
+
## Why it is faster
|
|
107
|
+
|
|
108
|
+
- **Nothing boots per task:** no daemon, no Node process; vx spawns your command.
|
|
109
|
+
- **No source-file reads to hash:** on a clean tree, keys come from git's index; each project's `package.json` and the lockfile are still read.
|
|
110
|
+
- **One pass:** one git walk per run; each stat and lookup happens once.
|
|
111
|
+
|
|
106
112
|
## More than speed
|
|
107
113
|
|
|
108
114
|
- **A cache you can trust:** TypeScript configs are evaluated before
|
|
@@ -111,22 +117,21 @@ config. `@vzn/vx-migrate`'s first npm publish is pending.
|
|
|
111
117
|
- **Sandboxed tasks:** opt in per task; a workspace file the task did
|
|
112
118
|
not declare is out of its reach, so it cannot poison the cache.
|
|
113
119
|
- **Clean exits:** Ctrl-C reaps each task's process group.
|
|
114
|
-
- **Plugins at every stage
|
|
120
|
+
- **Plugins at every stage** (14 hooks, `commands` included): remote cache, remote execution, telemetry,
|
|
115
121
|
CLI verbs. Core ships none and needs none.
|
|
116
122
|
- **Free:** MIT. No paywall, no cloud, no account.
|
|
117
123
|
|
|
118
124
|
## How it compares
|
|
119
125
|
|
|
120
|
-
|
|
|
121
|
-
|
|
|
122
|
-
| Config
|
|
123
|
-
| Output ownership
|
|
124
|
-
| Clean-tree hashing
|
|
125
|
-
| Daemon
|
|
126
|
-
| Per-task sandbox
|
|
127
|
-
| Plugin API
|
|
128
|
-
|
|
|
129
|
-
| Install | **Single binary**; no Bun, Node optional | npm + Node | npm + Node |
|
|
126
|
+
| | vx | Turborepo | Nx |
|
|
127
|
+
| ------------------ | ----------------------------------------- | ------------------------------ | ---------------- |
|
|
128
|
+
| Config | TypeScript, evaluated into the cache key | JSON (static) | JSON (static) |
|
|
129
|
+
| Output ownership | **Strict**: wiped before exec and restore | Additive (stale files survive) | Additive |
|
|
130
|
+
| Clean-tree hashing | **No source reads** (git index OIDs) | git OIDs | re-hash / daemon |
|
|
131
|
+
| Daemon | **None** | None for `turbo run` | On by default |
|
|
132
|
+
| Per-task sandbox | **Yes**: kernel-level, opt-in | No | No |
|
|
133
|
+
| Plugin API | **Yes**: every pipeline stage | No | Yes (TS-tied) |
|
|
134
|
+
| Install | **Single binary**; no Bun, Node optional | npm + Node | npm + Node |
|
|
130
135
|
|
|
131
136
|
Every gap, and when another tool is the better pick:
|
|
132
137
|
[comparison](packages/vx/docs/comparison.md) ·
|
|
@@ -157,13 +162,6 @@ Write your own: [plugin guide](https://vznjs.github.io/vx/guides/plugins/) ·
|
|
|
157
162
|
vx. The schema may still change; 1.0's promise:
|
|
158
163
|
[versioning-1.0.md](packages/vx/docs/design/versioning-1.0.md).
|
|
159
164
|
|
|
160
|
-
| Surface | Maturity |
|
|
161
|
-
| -------------------------------------------------- | -------------------- |
|
|
162
|
-
| Core task runner + caching | **production-ready** |
|
|
163
|
-
| Plugin pipeline (14 hooks, `commands` included) | **shippable** |
|
|
164
|
-
| `vx init` / `@vzn/vx-migrate` (scripts; Turbo, Nx) | **shippable** |
|
|
165
|
-
| First-party plugins (table above) | **shippable** |
|
|
166
|
-
|
|
167
165
|
## Contributing
|
|
168
166
|
|
|
169
167
|
```sh
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.412",
|
|
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.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.412",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.412",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.412",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.412"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|