@vzn/vx 0.0.40 → 0.0.42

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.40",
3
+ "version": "0.0.42",
4
4
  "description": "An open, extensible monorepo task runner.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,10 +17,10 @@
17
17
  "node": ">=18"
18
18
  },
19
19
  "optionalDependencies": {
20
- "@vzn/vx-linux-x64": "0.0.40",
21
- "@vzn/vx-linux-arm64": "0.0.40",
22
- "@vzn/vx-darwin-x64": "0.0.40",
23
- "@vzn/vx-darwin-arm64": "0.0.40"
20
+ "@vzn/vx-linux-x64": "0.0.42",
21
+ "@vzn/vx-linux-arm64": "0.0.42",
22
+ "@vzn/vx-darwin-x64": "0.0.42",
23
+ "@vzn/vx-darwin-arm64": "0.0.42"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -127,6 +127,7 @@ export async function loadCliProjects(
127
127
  lock,
128
128
  evalCache: {
129
129
  store: cache,
130
+ workspaceRoot,
130
131
  workspaceFingerprint: await computeWorkspaceFingerprint(workspaceRoot),
131
132
  },
132
133
  warn: warnToStderr,
@@ -148,6 +148,7 @@ export async function collectInfo(cwd: string, opts: CollectInfoOptions = {}): P
148
148
  lock: null,
149
149
  evalCache: {
150
150
  store: cache,
151
+ workspaceRoot: root,
151
152
  workspaceFingerprint: await computeWorkspaceFingerprint(root, reads),
152
153
  },
153
154
  warn,
@@ -258,7 +258,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
258
258
  projectMetas,
259
259
  packageGraph,
260
260
  lock,
261
- evalCache: { store: localCache, workspaceFingerprint: fingerprints.all },
261
+ evalCache: { store: localCache, workspaceRoot, workspaceFingerprint: fingerprints.all },
262
262
  warn: (m: string) => log.status(m),
263
263
  }
264
264
  let loaded: LoadedProjects
@@ -259,6 +259,7 @@ export async function loadResolvedProjects(
259
259
  lock: null,
260
260
  evalCache: {
261
261
  store: cache,
262
+ workspaceRoot,
262
263
  workspaceFingerprint: await computeWorkspaceFingerprint(workspaceRoot, reads),
263
264
  },
264
265
  warn,
@@ -109,6 +109,13 @@ export interface ConfigEvalKeyArgs {
109
109
  */
110
110
  hashFile?: (file: string) => Promise<string>
111
111
  bytes: Uint8Array
112
+ /**
113
+ * The workspace root, as the config path spells it. A symlink BELOW it on
114
+ * the way to the config makes the closure unindexable; one above it
115
+ * (macOS's `/var`) moves nothing the warm path could miss. Absent, any
116
+ * symlink on the way does.
117
+ */
118
+ workspaceRoot?: string | undefined
112
119
  workspaceFingerprint: string
113
120
  }
114
121
 
@@ -132,9 +139,10 @@ const MAX_CLOSURE_FILES = 32
132
139
  // literal naming it was stripped before the test ran; listed 2026-09-27
133
140
  // (item 957). A key assembled at run time (`'constru' + 'ctor'`) still
134
141
  // passes: the gate stops accidental impurity, not a config written to
135
- // defeat it.
142
+ // defeat it. `random` is denied as a word, not as `Math.random`: a
143
+ // destructured `const { random } = Math` was cached as pure (item 1036).
136
144
  const IMPURE_RE =
137
- /\b(?:process|Bun|globalThis|global|self|fetch|Date|Temporal|Intl|crypto|performance|navigator|require|eval|Function|constructor|localeCompare|await|toLocale\w*|Reflect|getPrototypeOf|setPrototypeOf|getOwnPropertyNames|getOwnPropertyDescriptor|getOwnPropertyDescriptors|__proto__|prototype|__defineGetter__|__defineSetter__|__lookupGetter__|__lookupSetter__)\b|import\s*\.\s*meta|Math\s*\.\s*random|\bimport\s*\(/
145
+ /\b(?:process|Bun|globalThis|global|self|fetch|Date|Temporal|Intl|crypto|performance|navigator|require|eval|Function|constructor|localeCompare|await|toLocale\w*|Reflect|getPrototypeOf|setPrototypeOf|getOwnPropertyNames|getOwnPropertyDescriptor|getOwnPropertyDescriptors|__proto__|prototype|__defineGetter__|__defineSetter__|__lookupGetter__|__lookupSetter__|random)\b|import\s*\.\s*meta|\bimport\s*\(/
138
146
 
139
147
  /** Literal text that names a way to `Function` when used as a computed key. */
140
148
  const IMPURE_LITERAL_RE = /constructor|__proto__|prototype/
@@ -143,10 +151,15 @@ const IMPURE_LITERAL_RE = /constructor|__proto__|prototype/
143
151
  // matched on `stripLiterals` output with its strings kept as placeholders
144
152
  // (`\0<n>\0`): a comment is gone, so an import after `/* … */` on its line
145
153
  // is seen, and a string-named binding (`import { 'a-b' as x }`) is a
146
- // placeholder like any other. `[^;]*?` spans newlines, so multi-line
147
- // specifier lists match.
154
+ // placeholder like any other. The body spans newlines, so multi-line
155
+ // specifier lists match, but never another statement's `import` or
156
+ // `export`: in a file without semicolons a lazy `[^;]*?` ran from
157
+ // `export type Mode = …` on to the next line's `from '@vzn/vx'`, and the
158
+ // purity check read `type` and passed an impure import. Nor is a line
159
+ // start required: `} export * from './side.ts'` went unscanned, and the
160
+ // fail-closed count below counted only imports (item 1036).
148
161
  const IMPORT_RE =
149
- /(?:^|[\n;])\s*(?:import|export)\b[^;]*?\bfrom\s*\0(\d+)\0|(?:^|[\n;])\s*import\s*\0(\d+)\0/g
162
+ /\b(?:import|export)\b(?:(?!\b(?:import|export)\b)[^;])*?\bfrom\s*\0(\d+)\0|\bimport\s*\0(\d+)\0/g
150
163
 
151
164
  /**
152
165
  * Every static import in `source` as its specifier and statement, or `null`
@@ -384,6 +397,31 @@ function realDirOf(file: string): string {
384
397
  return realpathSync(path.dirname(file))
385
398
  }
386
399
 
400
+ /** Each workspace root's real path, asked once per process. */
401
+ const realRoots = new Map<string, string>()
402
+
403
+ /**
404
+ * Whether `realDir`, the real directory of `file`, is where its spelling
405
+ * says it is once the root's own links are resolved. A member directory
406
+ * linked in from elsewhere is not: the warm path keys the closure by the
407
+ * paths the last evaluation found, and a link retargeted to a config of
408
+ * the same bytes whose preset differs moves none of them (item 1036).
409
+ */
410
+ function reachedThroughNoLink(file: string, realDir: string, root: string | undefined): boolean {
411
+ const dir = path.dirname(file)
412
+ // Without a root there is nothing to tell a member's own link from one
413
+ // above the tree (macOS's `/tmp` → `/private/tmp`), and comparing the
414
+ // spelling with the real path refused every config under such a link.
415
+ // Every run passes its root; a caller that does not keeps the index.
416
+ if (root === undefined) return true
417
+ let realRoot = realRoots.get(root)
418
+ if (realRoot === undefined) {
419
+ realRoot = realpathSync(root)
420
+ realRoots.set(root, realRoot)
421
+ }
422
+ return path.relative(realRoot, realDir) === path.relative(root, dir)
423
+ }
424
+
387
425
  /** A regular file reached through no symlink: the path IS the file. */
388
426
  function isCanonicalFile(file: string): boolean {
389
427
  try {
@@ -439,10 +477,15 @@ export async function configEvalKey(a: ConfigEvalKeyArgs): Promise<ConfigEvalKey
439
477
  // (item 950). The named file is taken without `Bun.resolveSync`,
440
478
  // whose directory cache answers a retargeted link with its old
441
479
  // target for the rest of the process.
442
- try {
443
- dir ??= realDirOf(file)
444
- } catch {
445
- return null
480
+ if (dir === undefined) {
481
+ try {
482
+ dir = realDirOf(file)
483
+ if (file === a.configPath && !reachedThroughNoLink(file, dir, a.workspaceRoot)) {
484
+ indexable = false
485
+ }
486
+ } catch {
487
+ return null
488
+ }
446
489
  }
447
490
  const named = path.resolve(dir, spec)
448
491
  let resolved: string
@@ -233,7 +233,7 @@ export interface LoadProjectConfigOptions {
233
233
  * Serve a provably-pure config from its cached evaluation (see
234
234
  * config-cache.ts) and store a fresh one for next time. Off for `fresh`.
235
235
  */
236
- evalCache?: { store: ConfigEvalStore; workspaceFingerprint: string }
236
+ evalCache?: { store: ConfigEvalStore; workspaceRoot?: string; workspaceFingerprint: string }
237
237
  }
238
238
 
239
239
  /**
@@ -296,6 +296,7 @@ export async function loadProjectConfigs(
296
296
  : await configEvalKey({
297
297
  configPath,
298
298
  bytes,
299
+ workspaceRoot: evalCache.workspaceRoot,
299
300
  workspaceFingerprint: evalCache.workspaceFingerprint,
300
301
  ...slowKeyHash,
301
302
  })
@@ -352,6 +353,7 @@ export async function loadProjectConfigs(
352
353
  const keyed = await configEvalKey({
353
354
  configPath,
354
355
  bytes,
356
+ workspaceRoot: evalCache!.workspaceRoot,
355
357
  workspaceFingerprint: evalCache!.workspaceFingerprint,
356
358
  ...slowKeyHash,
357
359
  })