@vzn/vx 0.0.597 → 0.0.599

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.
@@ -15,7 +15,7 @@ import type { Cache, CacheLayer, CachePolicy } from '../cache/index.js'
15
15
  import { PLUGIN_PACKAGE, type ProjectConfig, type WorkspaceConfig } from '../config.js'
16
16
  import type { TaskExecutor } from '../exec/index.js'
17
17
  import type { TaskNode, TaskOutcome } from '../graph/index.js'
18
- import { nearest, UserError } from '../util/index.js'
18
+ import { maskedLine, nearest, UserError } from '../util/index.js'
19
19
  import type { ProjectMeta } from '../workspace/index.js'
20
20
  import type { EventBus, RunStartInfo } from './events.js'
21
21
  import type { TelemetryContext, TelemetrySink } from './telemetry.js'
@@ -716,7 +716,10 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
716
716
  for (const d of disposers) d()
717
717
  throw new PluginSetupError(
718
718
  plugin,
719
- `plugin '${plugin.name}' failed in setup: ${err instanceof Error ? err.message : String(err)}`,
719
+ // Masked at the source, as plugin-host.ts's `safe()` does (L-11).
720
+ maskedLine(
721
+ `plugin '${plugin.name}' failed in setup: ${err instanceof Error ? err.message : String(err)}`,
722
+ ),
720
723
  )
721
724
  }
722
725
  }
@@ -379,9 +379,11 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
379
379
  // it, and the run said no project did (C-3). Only what is still
380
380
  // unjudged pays for the rest of the workspace.
381
381
  unresolvedTasks = undeclaredIn(unresolvedTasks, projects)
382
+ // By name, not by count: a config-less project in the scope counts as
383
+ // loaded, and a changed one hid an unloaded `test` declarer (X-129).
382
384
  if (
383
- projects.size < projectsWithConfigs.length &&
384
- unresolvedTasks.some((t) => !t.includes('#'))
385
+ unresolvedTasks.some((t) => !t.includes('#')) &&
386
+ projectsWithConfigs.some((m) => !projects.has(m.name))
385
387
  ) {
386
388
  unresolvedTasks = await declaredNowhere(unresolvedTasks, () =>
387
389
  loadProjects({ ...loadArgs, seeds: 'all', closure: false, staged: projects }),
@@ -321,7 +321,7 @@ const DEFAULT_BUILD: TaskConfig = Object.freeze({
321
321
  inputs: Object.freeze({ files: Object.freeze(['**']) }),
322
322
  outputs: Object.freeze({ files: Object.freeze([]) }),
323
323
  }),
324
- }) as unknown as TaskConfig
324
+ })
325
325
 
326
326
  /** The default `build`, which a menu of the tasks a user declared leaves out. */
327
327
  export function isDefaultBuild(task: TaskConfig | undefined): boolean {
@@ -379,7 +379,7 @@ function ciLinks(provider: string, env: Record<string, string | undefined>): CiL
379
379
  default:
380
380
  return {}
381
381
  }
382
- return Object.fromEntries(Object.entries(links).filter(([, x]) => x !== undefined)) as CiLinks
382
+ return Object.fromEntries(Object.entries(links).filter(([, x]) => x !== undefined))
383
383
  }
384
384
 
385
385
  /** Host name (null on failure) + platform + arch. */
@@ -66,7 +66,8 @@ import { rmdirSync, unlinkSync } from 'node:fs'
66
66
  import { mkdir, readdir, readFile, rename, rm, rmdir, unlink, writeFile } from 'node:fs/promises'
67
67
  import os from 'node:os'
68
68
  import path from 'node:path'
69
- import { isTmpdirRefusal, procfsIsOwn, TMPDIR_HINT, xxh3hex } from '../util/index.js'
69
+ import { VX_RUN_TASK_ENV, VX_RUN_WORKSPACE_ENV } from '../exec/index.js'
70
+ import { isTmpdirRefusal, procfsIsOwn, TMPDIR_HINT, UserError, xxh3hex } from '../util/index.js'
70
71
 
71
72
  /** Runs in this process currently holding the lock, per lock directory. */
72
73
  const heldHere = new Map<string, number>()
@@ -329,6 +330,14 @@ export async function acquireRunLock(
329
330
  opts: RunLockOptions,
330
331
  ): Promise<() => Promise<void>> {
331
332
  const lockDir = runLockPath(workspaceRoot, opts.dir)
333
+ // A task's own process (`vx cache prune` in a task) would wait for the
334
+ // run that started it, which holds the lock until that task ends.
335
+ const outer = process.env[VX_RUN_WORKSPACE_ENV]
336
+ if (outer !== undefined && runLockPath(outer, opts.dir) === lockDir) {
337
+ throw new UserError(
338
+ `this vx runs inside task ${process.env[VX_RUN_TASK_ENV] ?? '<unknown>'} of a vx run on this workspace, which holds its run lock until it ends — run it before or after that run, not from one of its tasks`,
339
+ )
340
+ }
332
341
  const started = Date.now()
333
342
  let said = false
334
343
  const release = async (): Promise<void> => {
@@ -606,8 +606,12 @@ export function configEvalKeyFromIdentities(a: {
606
606
  * `node_modules`, the config itself excluded. `vx watch` watches these: a
607
607
  * shared preset outside the project (`../../shared/preset.mjs`, the way
608
608
  * configs compose) changed what a run evaluates and no watcher saw it
609
- * (item 949). A file that cannot be read or resolved ends its branch.
609
+ * (item 949). A file that cannot be read or resolved ends its branch; one
610
+ * that cannot be resolved is still listed, by the name it would have.
610
611
  */
612
+ // The names an extensionless relative import may resolve to, as Bun tries them.
613
+ const IMPORT_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json']
614
+
611
615
  export async function configImports(configPath: string): Promise<string[]> {
612
616
  const seen = new Set<string>([configPath])
613
617
  const out: string[] = []
@@ -635,6 +639,16 @@ export async function configImports(configPath: string): Promise<string[]> {
635
639
  dir ??= realDirOf(file)
636
640
  resolved = realPath(Bun.resolveSync(spec, dir))
637
641
  } catch {
642
+ // Missing for now, still imported: the config that names it fails
643
+ // to load until it is back, and a list without it left `vx watch`
644
+ // deaf to its return. Named as written, and not walked.
645
+ if (dir === undefined) continue
646
+ for (const ext of path.extname(spec) === '' ? IMPORT_EXTENSIONS : ['']) {
647
+ const missing = path.resolve(dir, spec + ext)
648
+ if (seen.has(missing)) continue
649
+ seen.add(missing)
650
+ out.push(missing)
651
+ }
638
652
  continue
639
653
  }
640
654
  if (seen.has(resolved) || resolved.split(path.sep).includes('node_modules')) continue
@@ -103,6 +103,23 @@ const nonJsonPaths = ${nonJsonPaths.toString()}
103
103
  throw new Error('process.exit(' + (code ?? '') + ') in a config: ' + ${JSON.stringify(CONFIG_EXIT)})
104
104
  }
105
105
  }
106
+ // Bun's resolver reads \\ as a separator, so a config under a\\b is served
107
+ // from the path as written (project-loader.ts's LITERAL_QUERY), read now.
108
+ let literal = false
109
+ const serveLiteral = () => {
110
+ if (literal) return
111
+ literal = true
112
+ globalThis.Bun.plugin({
113
+ name: 'vx-config-literal',
114
+ setup(build) {
115
+ build.onResolve({ filter: /\\?vx-literal$/ }, (args) => ({ path: args.path, namespace: 'file' }))
116
+ build.onLoad({ filter: /\\?vx-literal$/ }, async (args) => {
117
+ const file = args.path.slice(0, -'?vx-literal'.length)
118
+ return { contents: await globalThis.Bun.file(file).text(), loader: /\\.[cm]?ts$/.test(file) ? 'ts' : 'js' }
119
+ })
120
+ },
121
+ })
122
+ }
106
123
  self.onmessage = async (e) => {
107
124
  const { id, path, env } = e.data
108
125
  // A Worker starts with the process's STARTUP environment, not the
@@ -155,7 +172,9 @@ self.onmessage = async (e) => {
155
172
  if (umaskIn !== null && globalThis.process.umask() !== umaskIn) globalThis.process.umask(umaskIn)
156
173
  }
157
174
  try {
158
- const ns = await import(path)
175
+ const literalPath = path.includes('\\\\')
176
+ if (literalPath) serveLiteral()
177
+ const ns = await import(literalPath ? path + '?vx-literal' : path)
159
178
  // Awaited as the in-process load's async return flattens it: a Promise
160
179
  // default loaded on a run and was refused as "an instance of Promise"
161
180
  // on every later evaluation in the process (D-5).
@@ -87,7 +87,12 @@ function assertDefaultObject(mod: unknown, kind: string, configPath: string): vo
87
87
  const heldSources = new Map<string, { source: string; uses: number }>()
88
88
  let serving = false
89
89
 
90
- const HELD_QUERY = /\?vx-held=([0-9a-f]+)$/
90
+ const HELD_QUERY = /\?vx-(?:held|literal)=([0-9a-f]+)$/
91
+
92
+ // Bun's resolver reads `\` as a separator, even in a file: URL, so a
93
+ // config under `a\b` was looked for under `a/b`. Resolving it here keeps
94
+ // the path as written; only the served load can take it.
95
+ const LITERAL_QUERY = /\?vx-literal=[0-9a-f]+$/
91
96
 
92
97
  function serveHeldSources(): void {
93
98
  if (serving) return
@@ -95,6 +100,7 @@ function serveHeldSources(): void {
95
100
  Bun.plugin({
96
101
  name: 'vx-config-bytes',
97
102
  setup(build) {
103
+ build.onResolve({ filter: LITERAL_QUERY }, (args) => ({ path: args.path, namespace: 'file' }))
98
104
  build.onLoad({ filter: HELD_QUERY }, (args) => ({
99
105
  contents: heldSources.get(HELD_QUERY.exec(args.path)![1]!)!.source,
100
106
  loader: /\.[cm]?ts\?/.test(args.path) ? 'ts' : 'js',
@@ -136,7 +142,7 @@ const COMMONJS_HINT =
136
142
  /\b(?:module|exports|require|this|__dirname|__filename)\b|\\|\bexport\s*=(?!=)/
137
143
 
138
144
  /** vx's module-cache query, which no user wrote: stripped from anything shown to them. */
139
- const BUST_QUERY = /\?vx-(?:bust|held)=[^'"\s]*/g
145
+ const BUST_QUERY = /\?vx-(?:bust|held|literal)=[^'"\s]*/g
140
146
 
141
147
  // Bun has native TS / ESM execution — no transpiler dep needed. We fold
142
148
  // a short content hash into the import URL as a cache-bust key so that:
@@ -157,10 +163,18 @@ async function loadDefaultExport(
157
163
  // URL yet, and a repeat load (the one that could replay an evaluation
158
164
  // made under earlier env values) re-evaluates in a worker instead
159
165
  // (`loadedConfigs`, item 678).
166
+ const literal = configPath.includes('\\')
160
167
  const source =
161
- kind === 'Project' ? servableSource(bytes, /\.[cm]?ts$/.test(configPath) ? 'ts' : 'js') : null
168
+ kind === 'Project' || literal
169
+ ? servableSource(bytes, /\.[cm]?ts$/.test(configPath) ? 'ts' : 'js')
170
+ : null
171
+ if (literal && source === null) {
172
+ throw new UserError(
173
+ `${kind} config ${configPath} sits under a path that holds a backslash, which Bun loads only as an ES module in UTF-8`,
174
+ )
175
+ }
162
176
  const hash = xxh3hex(bytes)
163
- const specifier = `${configPath}?vx-${source !== null ? 'held' : 'bust'}=${hash}`
177
+ const specifier = `${configPath}?vx-${literal ? 'literal' : source !== null ? 'held' : 'bust'}=${hash}`
164
178
  if (source !== null) {
165
179
  serveHeldSources()
166
180
  holdSource(hash, source)