@vzn/vx 0.0.397 → 0.0.398

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.397",
3
+ "version": "0.0.398",
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.397",
25
- "@vzn/vx-linux-arm64": "0.0.397",
26
- "@vzn/vx-darwin-x64": "0.0.397",
27
- "@vzn/vx-darwin-arm64": "0.0.397"
24
+ "@vzn/vx-linux-x64": "0.0.398",
25
+ "@vzn/vx-linux-arm64": "0.0.398",
26
+ "@vzn/vx-darwin-x64": "0.0.398",
27
+ "@vzn/vx-darwin-arm64": "0.0.398"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -127,6 +127,9 @@ self.onmessage = async (e) => {
127
127
  const before = watched?.map(([, o]) => own(o))
128
128
  const envBefore = watched ? { ...live } : null
129
129
  const cwdBefore = watched ? globalThis.process.cwd() : null
130
+ // The umask is the process's, not the worker's: a config's umask(0o777)
131
+ // here left every file vx wrote after it 000 (D-125). Put back below.
132
+ const umaskIn = globalThis.process.umask()
130
133
  const same = (a, b) =>
131
134
  a !== undefined && b !== undefined && Object.is(a.value, b.value) && a.get === b.get && a.set === b.set &&
132
135
  a.writable === b.writable && a.enumerable === b.enumerable && a.configurable === b.configurable
@@ -141,8 +144,14 @@ self.onmessage = async (e) => {
141
144
  for (const k of new Set([...Object.keys(envBefore), ...Object.keys(live)]))
142
145
  if (envBefore[k] !== live[k]) out.push('process.env.' + k)
143
146
  if (globalThis.process.cwd() !== cwdBefore) out.push('process.cwd (a chdir)')
147
+ if (globalThis.process.umask() !== umaskIn) out.push('process.umask')
144
148
  return out
145
149
  }
150
+ const umaskBack = () => {
151
+ if (globalThis.process.umask() === umaskIn) return false
152
+ globalThis.process.umask(umaskIn)
153
+ return true
154
+ }
146
155
  try {
147
156
  const ns = await import(path)
148
157
  // Awaited as the in-process load's async return flattens it: a Promise
@@ -158,6 +167,7 @@ self.onmessage = async (e) => {
158
167
  json: isObject && nonJson.length === 0 ? JSON.stringify(mod) : null,
159
168
  fn: typeof mod === 'function',
160
169
  changed: changed(),
170
+ umask: umaskBack(),
161
171
  })
162
172
  } catch (err) {
163
173
  postMessage({
@@ -167,6 +177,7 @@ self.onmessage = async (e) => {
167
177
  message: err?.message ?? String(err),
168
178
  stack: err?.stack ?? null,
169
179
  changed: changed(),
180
+ umask: umaskBack(),
170
181
  position:
171
182
  err?.position && typeof err.position === 'object'
172
183
  ? { file: err.position.file, line: err.position.line, column: err.position.column }
@@ -178,6 +189,9 @@ self.onmessage = async (e) => {
178
189
 
179
190
  const FUNCTION_EXPORT = (): void => {}
180
191
 
192
+ /** A repeat load moved the process's umask; the loader names the config (D-125). */
193
+ export class UmaskChanged extends Error {}
194
+
181
195
  const WORKER_URL = `data:text/javascript,${encodeURIComponent(WORKER_SRC)}`
182
196
 
183
197
  interface WorkerReply {
@@ -186,6 +200,8 @@ interface WorkerReply {
186
200
  json: string | null
187
201
  /** The default export is a function, which JSON cannot carry back. */
188
202
  fn?: boolean
203
+ /** The evaluation moved the process's umask; the worker put it back. */
204
+ umask?: boolean
189
205
  nonJson: NonJsonValue[]
190
206
  name: string
191
207
  message: string
@@ -347,6 +363,7 @@ export async function evaluateConfigFresh(configPath: string): Promise<unknown>
347
363
  timer.unref?.()
348
364
  w.postMessage({ id, path: abs, env: { ...process.env } })
349
365
  })
366
+ if (reply.umask === true) throw new UmaskChanged()
350
367
  const [nonJson] = reply.nonJson
351
368
  if (nonJson !== undefined) throw new UserError(nonJsonMessage(configPath, nonJson))
352
369
  // A function stands in for the one the config exported, so the caller's
@@ -9,6 +9,7 @@ import {
9
9
  CONFIG_EXIT,
10
10
  evalBudgetMs,
11
11
  evaluateConfigFresh,
12
+ UmaskChanged,
12
13
  WATCHED_BUILTIN_NAMES,
13
14
  } from './config-eval.js'
14
15
  import { hasEsmExport, unprovidedBareImports } from './config-imports.js'
@@ -451,6 +452,7 @@ export async function loadProjectConfigs(
451
452
  let builtins: BuiltinSnapshot | undefined
452
453
  let env: Readonly<Record<string, string | undefined>> = {}
453
454
  let cwd = ''
455
+ let umask = -1
454
456
  // More than one evaluation in flight: a change seen after one load may
455
457
  // be another's.
456
458
  let overlapping = false
@@ -489,6 +491,7 @@ export async function loadProjectConfigs(
489
491
  if (repeat) refuseUnprovidedImports(bytes!, configPath, 'Project')
490
492
  const mod = repeat
491
493
  ? await evaluateConfigFresh(configPath).catch((err: unknown) => {
494
+ if (err instanceof UmaskChanged) throw builtinsChanged(['process.umask'], configPath)
492
495
  throw configLoadError(err, configPath, 'Project') ?? err
493
496
  })
494
497
  : await loadDefaultExport(configPath, 'Project', bytes!)
@@ -496,7 +499,12 @@ export async function loadProjectConfigs(
496
499
  // turned the JSON-data walk's own check into "a cyclic reference".
497
500
  const changed = repeat
498
501
  ? []
499
- : [...restoreBuiltins(builtins), ...restoreEnv(env), ...restoreCwd(cwd)]
502
+ : [
503
+ ...restoreBuiltins(builtins),
504
+ ...restoreEnv(env),
505
+ ...restoreCwd(cwd),
506
+ ...restoreUmask(umask),
507
+ ]
500
508
  // Loads overlap, so another config's change can surface after this
501
509
  // one: named here, the refusal blamed the wrong file (D-119). The round
502
510
  // finds the one that made it.
@@ -549,6 +557,7 @@ export async function loadProjectConfigs(
549
557
  builtins = builtinSnapshot()
550
558
  env = { ...process.env }
551
559
  cwd = process.cwd()
560
+ umask = currentUmask()
552
561
  }
553
562
  overlapping = misses.length > 1
554
563
  let next = 0
@@ -569,7 +578,12 @@ export async function loadProjectConfigs(
569
578
  }
570
579
  const changed =
571
580
  misses.length > 0
572
- ? [...restoreBuiltins(builtins), ...restoreEnv(env), ...restoreCwd(cwd)]
581
+ ? [
582
+ ...restoreBuiltins(builtins),
583
+ ...restoreEnv(env),
584
+ ...restoreCwd(cwd),
585
+ ...restoreUmask(umask),
586
+ ]
573
587
  : []
574
588
  if (first?.failed instanceof ChangedInRound) {
575
589
  for (const i of misses) {
@@ -702,6 +716,9 @@ function builtinsChanged(changed: readonly string[], configPath?: string): UserE
702
716
  (changed.some((c) => c.startsWith('process.cwd'))
703
717
  ? '; a task runs in its project directory, and `cd <dir> && …` in `exec.command` moves it'
704
718
  : '') +
719
+ (changed.some((c) => c.startsWith('process.umask'))
720
+ ? '; a task sets its own with `umask <mode> && …` in `exec.command`'
721
+ : '') +
705
722
  (changed.some((c) => c.startsWith('globalThis.'))
706
723
  ? '; a constant configs share goes in a module each one imports'
707
724
  : '')
@@ -785,6 +802,26 @@ function restoreCwd(before: string): string[] {
785
802
  return ['process.cwd (a chdir)']
786
803
  }
787
804
 
805
+ /**
806
+ * A config's `process.umask()` set the mode of every file vx and its tasks
807
+ * wrote after it: a cache artifact and a task's outputs landed `000`, which
808
+ * a user other than root could not read back (D-125). Put back, and named.
809
+ */
810
+ function currentUmask(): number {
811
+ // Through globalThis, as restoreCwd: the playground's shim has no umask.
812
+ const proc = globalThis.process as { umask?: (mask?: number) => number }
813
+ if (typeof proc.umask !== 'function') return -1
814
+ // No argument reads it: setting 0 and back would open a window where a
815
+ // file written by an overlapping write landed world-writable.
816
+ return proc.umask()
817
+ }
818
+
819
+ function restoreUmask(before: number): string[] {
820
+ if (before === -1 || currentUmask() === before) return []
821
+ ;(globalThis.process as { umask: (mask: number) => number }).umask(before)
822
+ return ['process.umask']
823
+ }
824
+
788
825
  function sameDescriptor(p: Primitives, a: PropertyDescriptor, b: PropertyDescriptor): boolean {
789
826
  return (
790
827
  // `globalThis.NaN` is watched (D-122), and NaN !== NaN.