@vzn/vx 0.0.608 → 0.0.610
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 +5 -5
- package/src/cache/inputs.ts +2 -1
- package/src/cli/lock.ts +9 -3
- package/src/orchestrator/projects.ts +16 -0
- package/src/util/index.ts +1 -1
- package/src/util/real-path.ts +49 -1
- package/src/workspace/workspace.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.610",
|
|
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.610",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.610",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.610",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.610"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cache/inputs.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
isLiteralPattern,
|
|
29
29
|
normalizeGlob,
|
|
30
30
|
outputMatcher,
|
|
31
|
+
realpathOf,
|
|
31
32
|
relPosix,
|
|
32
33
|
shellArgv,
|
|
33
34
|
slashBraceExpansions,
|
|
@@ -1142,7 +1143,7 @@ export async function cleanWorkspaceOutputs(args: {
|
|
|
1142
1143
|
|
|
1143
1144
|
function realOrNull(p: string): string | null {
|
|
1144
1145
|
try {
|
|
1145
|
-
return
|
|
1146
|
+
return realpathOf(p)
|
|
1146
1147
|
} catch {
|
|
1147
1148
|
return null
|
|
1148
1149
|
}
|
package/src/cli/lock.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { relPosix, secretMask, xxh3hex } from '../util/index.js'
|
|
|
8
8
|
import {
|
|
9
9
|
findWorkspaceRoot,
|
|
10
10
|
type LoadReads,
|
|
11
|
-
listProjects,
|
|
12
11
|
loadProjectConfig,
|
|
13
12
|
loadWorkspace,
|
|
14
13
|
LOCKFILE_NAME,
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
type LockfileEntry,
|
|
20
19
|
type ProjectMeta,
|
|
21
20
|
} from '../workspace/index.js'
|
|
21
|
+
import { discoverCliProjects } from './workspace-config.js'
|
|
22
22
|
|
|
23
23
|
interface LockArgs {
|
|
24
24
|
check: boolean
|
|
@@ -49,7 +49,13 @@ export async function lockCmd(args: readonly string[]): Promise<number> {
|
|
|
49
49
|
const reads: LoadReads = new Map()
|
|
50
50
|
const root = await findWorkspaceRoot(process.cwd(), reads)
|
|
51
51
|
const workspace = await loadWorkspace(root, reads)
|
|
52
|
-
|
|
52
|
+
// As a run discovers them: a project a plugin's `discover` stage adds
|
|
53
|
+
// was left out of the lock, and every `--frozen` run then refused it
|
|
54
|
+
// with "run vx lock", which changed nothing (X-143).
|
|
55
|
+
// Sorted by name, as core's discovery is, for stable lockfile diffs.
|
|
56
|
+
const all = (await discoverCliProjects(workspace)).sort((a, b) =>
|
|
57
|
+
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
58
|
+
)
|
|
53
59
|
const metas = all.filter((m): m is ConfiguredMeta => m.configPath !== null)
|
|
54
60
|
// A project with no vx.config has nothing to freeze: its tasks (a plugin's,
|
|
55
61
|
// or none) load live under --frozen too, so both verbs say how many the
|
|
@@ -83,7 +89,7 @@ function bareNote(bare: number): string {
|
|
|
83
89
|
|
|
84
90
|
async function writeLock(root: string, metas: ConfiguredMeta[], bare: number): Promise<number> {
|
|
85
91
|
const entries = await Promise.all(metas.map((m) => evaluateEntry(root, m)))
|
|
86
|
-
//
|
|
92
|
+
// Sorted by name above — stable lockfile diffs. Defined,
|
|
87
93
|
// not assigned: a project named `__proto__` set the map's prototype (D-152).
|
|
88
94
|
const projects: Record<string, LockfileEntry> = Object.fromEntries(
|
|
89
95
|
metas.map((m, i) => [m.name, entries[i]!]),
|
|
@@ -9,6 +9,7 @@ import type { ProjectConfig, TaskConfig } from '../config.js'
|
|
|
9
9
|
import {
|
|
10
10
|
frozenProjectConfigs,
|
|
11
11
|
loadProjectConfigs,
|
|
12
|
+
LOCKFILE_NAME,
|
|
12
13
|
validateProjectConfig,
|
|
13
14
|
type LoadProjectConfigOptions,
|
|
14
15
|
type LoadReads,
|
|
@@ -243,6 +244,21 @@ export async function loadProjects(args: LoadProjectsArgs): Promise<LoadedProjec
|
|
|
243
244
|
// alike. With no root project it stays, and the graph's refusal says so.
|
|
244
245
|
const root = path.resolve(workspaceRoot)
|
|
245
246
|
const rootName = args.projectMetas.find((m) => path.resolve(m.dir) === root)?.name
|
|
247
|
+
// A project the lock holds whose config file is gone ran nothing under
|
|
248
|
+
// --frozen, green, though the lock promises what runs is what was
|
|
249
|
+
// locked (X-144); a run never even asks such a project for a task.
|
|
250
|
+
// Refused as a moved config is (D-88). With no lock at all, nothing was
|
|
251
|
+
// locked: a repo of config-less projects needs none.
|
|
252
|
+
if (lock) {
|
|
253
|
+
const bare = args.projectMetas.filter((m) => m.configPath === null)
|
|
254
|
+
const held = bare.length === 0 ? null : await lock().catch(() => null)
|
|
255
|
+
const gone = held === null ? undefined : bare.find((m) => Object.hasOwn(held.projects, m.name))
|
|
256
|
+
if (gone !== undefined && held !== null)
|
|
257
|
+
throw new UserError(
|
|
258
|
+
`${LOCKFILE_NAME} locks "${gone.name}" at ${held.projects[gone.name]!.configPath}, but it has no config now — ` +
|
|
259
|
+
`run \`vx lock\` to refresh, or delete ${LOCKFILE_NAME}`,
|
|
260
|
+
)
|
|
261
|
+
}
|
|
246
262
|
const projects = new Map<string, ProjectEntry>()
|
|
247
263
|
while (pending.length > 0) {
|
|
248
264
|
const round = pending.splice(0, pending.length)
|
package/src/util/index.ts
CHANGED
|
@@ -74,7 +74,7 @@ export { isUnsupportedBun, MIN_BUN, unsupportedBunMessage } from './bun-version.
|
|
|
74
74
|
export { executablePath, shellArgv, taskShell } from './which.js'
|
|
75
75
|
export { procfsIsOwn } from './procfs.js'
|
|
76
76
|
export { hangupIgnored } from './hangup.js'
|
|
77
|
-
export { realPath } from './real-path.js'
|
|
77
|
+
export { realPath, realpathOf } from './real-path.js'
|
|
78
78
|
export {
|
|
79
79
|
maskedCommand,
|
|
80
80
|
maskedEmitter,
|
package/src/util/real-path.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { realpathSync } from 'node:fs'
|
|
1
|
+
import { readlinkSync, realpathSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A path's canonical spelling: the OS's own answer for the final path,
|
|
@@ -7,3 +8,50 @@ import { realpathSync } from 'node:fs'
|
|
|
7
8
|
export function realPath(p: string): string {
|
|
8
9
|
return realpathSync.native(p)
|
|
9
10
|
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `realpathSync`, for a path holding a backslash too: Bun's answers ENOENT
|
|
14
|
+
* for any such path (1.4.2), so an output under `dist/a\b/` was dropped
|
|
15
|
+
* from its artifact. Such a path's links are read one by one. Throws as
|
|
16
|
+
* `realpathSync` does when the path is not there.
|
|
17
|
+
*/
|
|
18
|
+
export function realpathOf(p: string): string {
|
|
19
|
+
try {
|
|
20
|
+
return realpathSync(p)
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (!p.includes('\\')) throw err
|
|
23
|
+
return resolveLinks(p)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveLinks(p: string): string {
|
|
28
|
+
// Not `path.resolve`: it folds `link/..` lexically, and the kernel
|
|
29
|
+
// takes `..` from where the link leads.
|
|
30
|
+
const abs = path.isAbsolute(p) ? p : `${process.cwd()}${path.sep}${p}`
|
|
31
|
+
let at = path.parse(abs).root
|
|
32
|
+
const pending = abs.slice(at.length).split(path.sep).filter(Boolean).reverse()
|
|
33
|
+
let hops = 0
|
|
34
|
+
while (pending.length > 0) {
|
|
35
|
+
const next = path.join(at, pending.pop()!)
|
|
36
|
+
let link: string
|
|
37
|
+
try {
|
|
38
|
+
link = readlinkSync(next)
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if ((err as NodeJS.ErrnoException).code !== 'EINVAL') throw err
|
|
41
|
+
at = next
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
// Linux's own bound on links followed in one lookup (MAXSYMLINKS).
|
|
45
|
+
if (++hops > 40) {
|
|
46
|
+
throw Object.assign(
|
|
47
|
+
new Error(`ELOOP: too many symbolic links encountered, realpath '${p}'`),
|
|
48
|
+
{
|
|
49
|
+
code: 'ELOOP',
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
if (path.isAbsolute(link)) at = path.parse(link).root
|
|
54
|
+
pending.push(...link.split('/').filter(Boolean).reverse())
|
|
55
|
+
}
|
|
56
|
+
return at
|
|
57
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
slashBraceExpansions,
|
|
13
13
|
UserError,
|
|
14
14
|
normalizeBunGlob,
|
|
15
|
+
realpathOf,
|
|
15
16
|
} from '../util/index.js'
|
|
16
17
|
import { type LoadReads, readOnce, unreadable } from './load-reads.js'
|
|
17
18
|
import { repoIdOf } from './repo-id.js'
|
|
@@ -897,7 +898,7 @@ async function oneEntryPerPackage<T extends { dir: string }>(
|
|
|
897
898
|
): Promise<T[]> {
|
|
898
899
|
const byReal = new Map<string, T[]>()
|
|
899
900
|
for (const entry of group) {
|
|
900
|
-
const real =
|
|
901
|
+
const real = realpathOf(entry.dir)
|
|
901
902
|
const same = byReal.get(real)
|
|
902
903
|
if (same === undefined) byReal.set(real, [entry])
|
|
903
904
|
else same.push(entry)
|