@vzn/vx 0.0.342 → 0.0.344

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.342",
3
+ "version": "0.0.344",
4
4
  "description": "A fast, correct 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.342",
25
- "@vzn/vx-linux-arm64": "0.0.342",
26
- "@vzn/vx-darwin-x64": "0.0.342",
27
- "@vzn/vx-darwin-arm64": "0.0.342"
24
+ "@vzn/vx-linux-x64": "0.0.344",
25
+ "@vzn/vx-linux-arm64": "0.0.344",
26
+ "@vzn/vx-darwin-x64": "0.0.344",
27
+ "@vzn/vx-darwin-arm64": "0.0.344"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -1223,6 +1223,7 @@ async function scanUnion(
1223
1223
  // `/`: `{dist,lib/esm}/**` saved an empty artifact, and a hit restored
1224
1224
  // nothing over a cleaned tree (A-10). Expanded here, as discovery does.
1225
1225
  for (const pattern of positive.flatMap(slashBraceExpansions)) {
1226
+ if (absentPrefix(cwd, pattern)) continue
1226
1227
  const glob = globFor(pattern)
1227
1228
  for (const rel of glob.scanSync({ cwd, onlyFiles: false, followSymlinks: false, dot: true })) {
1228
1229
  if (nested(rel) || excludeGlobs.some((g) => g.match(rel))) continue
@@ -1235,6 +1236,23 @@ async function scanUnion(
1235
1236
  return matches
1236
1237
  }
1237
1238
 
1239
+ /**
1240
+ * True when `pattern`'s static directory is not on disk, so nothing can
1241
+ * match under it. A restore into a tree without its outputs scans each
1242
+ * glob twice (the check, then the clean), and a scan of a missing `dist`
1243
+ * cost ~58 µs where the lstat costs a few. Anything this cannot read
1244
+ * plainly (no prefix, an escape, an absolute path, a refused stat) scans.
1245
+ */
1246
+ function absentPrefix(cwd: string, pattern: string): boolean {
1247
+ const prefix = staticPrefix(pattern)
1248
+ if (prefix === '.' || prefix.includes('\\') || path.isAbsolute(prefix)) return false
1249
+ try {
1250
+ return lstatSync(path.join(cwd, prefix), { throwIfNoEntry: false }) === undefined
1251
+ } catch {
1252
+ return false
1253
+ }
1254
+ }
1255
+
1238
1256
  // Compiled once per pattern string for the life of the process: a `Bun.Glob`
1239
1257
  // is immutable, and the same handful of declared globs are compiled again
1240
1258
  // for every task otherwise (three sites, up to thousands of times per run).
@@ -1012,7 +1012,7 @@ export async function wrapSandboxedCommand(
1012
1012
  let wrapped = await wrapForTask(
1013
1013
  SandboxManager,
1014
1014
  inner,
1015
- customConfig,
1015
+ process.platform === 'linux' ? literalReadPaths(customConfig) : customConfig,
1016
1016
  ports.length > 0 || asksUnixSockets(args.config),
1017
1017
  args.config.gitConfig === true,
1018
1018
  )
@@ -1148,6 +1148,30 @@ function asksUnixSockets(c: Pick<ResolvedSandboxConfig, 'unixSockets'>): boolean
1148
1148
  return c.unixSockets === true || (c.unixSockets !== undefined && c.unixSockets.length > 0)
1149
1149
  }
1150
1150
 
1151
+ /**
1152
+ * Linux: the read paths as SRT must be handed them to take each as the
1153
+ * name it is. vx has expanded every grant by then, so each is a path,
1154
+ * but SRT reads any holding `[` as a glob, where a bracket opens a class:
1155
+ * a route granted as `pages/\[id\].tsx` was never mounted (its denial
1156
+ * unreported, a listed grant), and a workspace under `[ws]/` was never
1157
+ * walled. `[[]` is a class of one `[`; a lone `]` is plain text to it.
1158
+ */
1159
+ function literalReadPaths(
1160
+ config: Parameters<SrtModule['SandboxManager']['wrapWithSandbox']>[2],
1161
+ ): Parameters<SrtModule['SandboxManager']['wrapWithSandbox']>[2] {
1162
+ const fs = config?.filesystem
1163
+ if (fs === undefined) return config
1164
+ const escape = (paths: readonly string[]): string[] => paths.map((p) => p.replaceAll('[', '[[]'))
1165
+ return {
1166
+ ...config,
1167
+ filesystem: {
1168
+ ...fs,
1169
+ denyRead: escape(fs.denyRead),
1170
+ ...(fs.allowRead !== undefined ? { allowRead: escape(fs.allowRead) } : {}),
1171
+ },
1172
+ }
1173
+ }
1174
+
1151
1175
  /** SRT's wrap, with the socket lift and the git-config grant this task asked for, or none (L-6, B-41). */
1152
1176
  function wrapForTask(
1153
1177
  SandboxManager: SrtModule['SandboxManager'],
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { readFileSync, statSync } from 'node:fs'
13
13
  import path from 'node:path'
14
- import { FILE_HASH_RACY_MS } from '../cache/index.js'
14
+ import { FILE_HASH_RACY_MS, racyWindowMs } from '../cache/index.js'
15
15
  import { WORKSPACE_FINGERPRINT_FILES, type WorkspaceFingerprints } from '../workspace/index.js'
16
16
  import type { Logger } from './logger.js'
17
17
 
@@ -56,7 +56,10 @@ export class FingerprintWatch {
56
56
  if (before !== undefined) moved.push(f)
57
57
  continue
58
58
  }
59
- if (before !== undefined && ctimeMs < this.at - FILE_HASH_RACY_MS) continue
59
+ // A whole-second stamp widens the window by that second (A-2): a
60
+ // rewrite 400 ms after the read was stamped before it (C-59).
61
+ if (before !== undefined && ctimeMs < this.at - racyWindowMs(ctimeMs, FILE_HASH_RACY_MS))
62
+ continue
60
63
  let now: Uint8Array | undefined
61
64
  try {
62
65
  now = readFileSync(abs)
@@ -38,7 +38,7 @@ export const WORKSPACE_FINGERPRINT_FILES = [
38
38
  'bunfig.toml',
39
39
  ]
40
40
 
41
- // DELIBERATELY ABSENT: `vx.workspace.{ts,mts,js,mjs}`. Everything it can
41
+ // DELIBERATELY ABSENT: `vx.workspace.{ts,mts,js,mjs,cts,cjs}`. Everything it can
42
42
  // declare — `concurrency`, `cacheDir`, `timeout`, and the
43
43
  // plugin list — is placement, storage or observability, never what a
44
44
  // command produces; core's three seams may change WHERE a task runs, not
@@ -23,6 +23,10 @@ export const WORKSPACE_CONFIG_FILENAMES = [
23
23
  'vx.workspace.mts',
24
24
  'vx.workspace.js',
25
25
  'vx.workspace.mjs',
26
+ // A CommonJS config (`module.exports = …`), as Vite and TS name theirs: it
27
+ // was ignored without a word (D-86).
28
+ 'vx.workspace.cts',
29
+ 'vx.workspace.cjs',
26
30
  ]
27
31
 
28
32
  /** Project configs already loaded in this process, by absolute path. */
@@ -660,7 +664,7 @@ async function refuseStaleWorkspaceImports(configPath: string, since: number): P
660
664
  }
661
665
 
662
666
  /**
663
- * Find and load `vx.workspace.{ts,mts,js,mjs}` from the workspace
667
+ * Find and load `vx.workspace.{ts,mts,js,mjs,cts,cjs}` from the workspace
664
668
  * root. Returns `null` if no such file exists (the common case;
665
669
  * the schema is fully optional). Validates the shape and throws
666
670
  * a `UserError` on malformed input.
@@ -35,7 +35,7 @@ export interface ProjectMeta {
35
35
  /** Absolute path to the project directory. */
36
36
  dir: string
37
37
  packageJson: PackageJson
38
- /** Absolute path to vx.config.{ts,mts,js,mjs} or null. */
38
+ /** Absolute path to vx.config.{ts,mts,js,mjs,cts,cjs} or null. */
39
39
  configPath: string | null
40
40
  }
41
41
 
@@ -51,6 +51,10 @@ export const PROJECT_CONFIG_FILENAMES = [
51
51
  'vx.config.mts',
52
52
  'vx.config.js',
53
53
  'vx.config.mjs',
54
+ // A CommonJS config (`module.exports = …`), as Vite and TS name theirs: it
55
+ // was ignored without a word (D-86).
56
+ 'vx.config.cts',
57
+ 'vx.config.cjs',
54
58
  ]
55
59
 
56
60
  const decoder = new TextDecoder()