@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.608",
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.608",
25
- "@vzn/vx-linux-arm64": "0.0.608",
26
- "@vzn/vx-darwin-x64": "0.0.608",
27
- "@vzn/vx-darwin-arm64": "0.0.608"
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"
@@ -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 realpathSync(p)
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
- const all = await listProjects(workspace)
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
- // `listProjects` sorts by name — stable lockfile diffs for free. Defined,
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,
@@ -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 = await realpath(entry.dir)
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)