@vzn/vx 0.0.645 → 0.0.648
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/README.md +39 -50
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +326 -65
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +65 -4
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +282 -0
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
// Two rules keep the walk small, and the second is the one that makes it
|
|
17
17
|
// affordable at all:
|
|
18
18
|
//
|
|
19
|
-
// -
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
19
|
+
// - LOCAL specifiers only (`localTarget`): relative, package.json
|
|
20
|
+
// `#imports`, or a tsconfig `paths` / `baseUrl` alias, each of which
|
|
21
|
+
// Bun loads from disk (D-27). Any other bare specifier is a package; it
|
|
22
|
+
// moves when the lockfile moves, which a lockfile plugin's key covers.
|
|
23
23
|
// - One reversed BFS from every config at once, each file read once
|
|
24
24
|
// however many configs reach it. It descends through every file in
|
|
25
25
|
// the workspace, another project's included: a site's `vx.config.ts`
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
// first file of another project, and containment selected core alone
|
|
29
29
|
// while the site's key moved (X-128).
|
|
30
30
|
//
|
|
31
|
-
// `configImports` walks one config's
|
|
32
|
-
// files `vx watch` watches.
|
|
31
|
+
// `configImports` walks one config's local closure forward, by the same
|
|
32
|
+
// rule, for the files `vx watch` watches.
|
|
33
33
|
|
|
34
34
|
import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
35
35
|
import { realpath } from 'node:fs/promises'
|
|
@@ -323,20 +323,149 @@ function selfReference(pkg: string, fromDir: string): boolean {
|
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
/** The nearest `package.json`'s directory and `imports` map, per directory. */
|
|
327
|
+
type ImportsMemo = Map<string, { dir: string; imports: Record<string, unknown> } | null>
|
|
328
|
+
|
|
329
|
+
/** What one walk learns once: tsconfig paths and package `imports` per directory. */
|
|
330
|
+
interface LookupMemo {
|
|
331
|
+
tsconfig: TsconfigMemo
|
|
332
|
+
imports: ImportsMemo
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function lookupMemo(): LookupMemo {
|
|
336
|
+
return { tsconfig: new Map(), imports: new Map() }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The `imports` map of the package `dir` sits in: its nearest `package.json`'s. */
|
|
340
|
+
function importsAt(
|
|
341
|
+
dir: string,
|
|
342
|
+
memo: ImportsMemo,
|
|
343
|
+
): { dir: string; imports: Record<string, unknown> } | null {
|
|
344
|
+
const known = memo.get(dir)
|
|
345
|
+
if (known !== undefined) return known
|
|
346
|
+
let out: { dir: string; imports: Record<string, unknown> } | null = null
|
|
347
|
+
const manifest = path.join(dir, 'package.json')
|
|
348
|
+
const up = path.dirname(dir)
|
|
349
|
+
if (existsSync(manifest)) {
|
|
350
|
+
try {
|
|
351
|
+
const json = JSON.parse(readFileSync(manifest, 'utf8').replace(/^/, '')) as {
|
|
352
|
+
imports?: unknown
|
|
353
|
+
}
|
|
354
|
+
if (typeof json.imports === 'object' && json.imports !== null) {
|
|
355
|
+
out = { dir, imports: json.imports as Record<string, unknown> }
|
|
356
|
+
}
|
|
357
|
+
} catch {}
|
|
358
|
+
} else if (up !== dir) {
|
|
359
|
+
out = importsAt(up, memo)
|
|
360
|
+
}
|
|
361
|
+
memo.set(dir, out)
|
|
362
|
+
return out
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Every string a package `imports` target holds, through arrays and conditions. */
|
|
366
|
+
function targetStrings(target: unknown): string[] {
|
|
367
|
+
if (typeof target === 'string') return [target]
|
|
368
|
+
if (Array.isArray(target)) return target.flatMap(targetStrings)
|
|
369
|
+
if (typeof target === 'object' && target !== null)
|
|
370
|
+
return Object.values(target).flatMap(targetStrings)
|
|
371
|
+
return []
|
|
372
|
+
}
|
|
373
|
+
|
|
326
374
|
/**
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
375
|
+
* The paths a `#` specifier's entry names in the nearest `package.json`
|
|
376
|
+
* `imports` (exact key, else the `*` pattern with the longest head, as
|
|
377
|
+
* Node picks), and whether every target is local. An entry mapping to a
|
|
378
|
+
* package is the lockfile's, as any bare specifier.
|
|
331
379
|
*/
|
|
332
|
-
|
|
380
|
+
function packageImportPaths(
|
|
381
|
+
spec: string,
|
|
382
|
+
fromDir: string,
|
|
383
|
+
memo: ImportsMemo,
|
|
384
|
+
): { paths: string[]; allLocal: boolean } | undefined {
|
|
385
|
+
const scope = importsAt(fromDir, memo)
|
|
386
|
+
if (scope === null) return undefined
|
|
387
|
+
let target: unknown = scope.imports[spec]
|
|
388
|
+
let hole = ''
|
|
389
|
+
if (target === undefined) {
|
|
390
|
+
let head = ''
|
|
391
|
+
for (const [key, t] of Object.entries(scope.imports)) {
|
|
392
|
+
const star = key.indexOf('*')
|
|
393
|
+
if (star === -1 || star < head.length) continue
|
|
394
|
+
const h = key.slice(0, star)
|
|
395
|
+
const tail = key.slice(star + 1)
|
|
396
|
+
if (spec.length < key.length - 1 || !spec.startsWith(h) || !spec.endsWith(tail)) continue
|
|
397
|
+
head = h
|
|
398
|
+
target = t
|
|
399
|
+
hole = spec.slice(h.length, spec.length - tail.length)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const strings = targetStrings(target)
|
|
403
|
+
const local = strings.filter((s) => s.startsWith('./'))
|
|
404
|
+
if (local.length === 0) return undefined
|
|
405
|
+
return {
|
|
406
|
+
paths: local.map((s) => path.resolve(scope.dir, s.replaceAll('*', hole))),
|
|
407
|
+
allLocal: local.length === strings.length,
|
|
408
|
+
}
|
|
409
|
+
}
|
|
333
410
|
|
|
334
411
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
412
|
+
* The local file `spec` names from `fromDir`, by the rules Bun loads one
|
|
413
|
+
* with: a relative path (`.`, `..`, `./x`, `../x`), a package.json
|
|
414
|
+
* `#imports` entry, or a tsconfig `paths` / `baseUrl` alias (D-27).
|
|
415
|
+
* `file` is the hit, realpath'd as `Bun.resolveSync` names one. With none, `named` lists the paths the
|
|
416
|
+
* specifier names (and, bare of an extension, the files Bun would have
|
|
417
|
+
* tried): most often the change itself deleted or renamed the target, and
|
|
418
|
+
* the config importing it is the project the change broke (item 958).
|
|
419
|
+
* Undefined for a package, a builtin or `@vzn/vx`.
|
|
337
420
|
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
421
|
+
* Both walks ask this, so what `--affected` follows and what `vx watch`
|
|
422
|
+
* watches cannot part: the watch walk once kept `./` and `../` alone.
|
|
423
|
+
*/
|
|
424
|
+
function localTarget(
|
|
425
|
+
spec: string,
|
|
426
|
+
fromDir: string,
|
|
427
|
+
memo: LookupMemo,
|
|
428
|
+
): { file: string } | { named: string[] } | undefined {
|
|
429
|
+
if (spec === '.' || spec === '..' || spec.startsWith('./') || spec.startsWith('../')) {
|
|
430
|
+
try {
|
|
431
|
+
return { file: Bun.resolveSync(spec, fromDir) }
|
|
432
|
+
} catch {
|
|
433
|
+
return { named: expansions(path.resolve(fromDir, spec)) }
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (spec.startsWith('#')) {
|
|
437
|
+
const entry = packageImportPaths(spec, fromDir, memo.imports)
|
|
438
|
+
if (entry === undefined) return undefined
|
|
439
|
+
// Bun picks the condition; it is asked only when no condition names a
|
|
440
|
+
// package, which it would fetch from the registry with no
|
|
441
|
+
// node_modules above.
|
|
442
|
+
if (entry.allLocal) {
|
|
443
|
+
try {
|
|
444
|
+
return { file: Bun.resolveSync(spec, fromDir) }
|
|
445
|
+
} catch {}
|
|
446
|
+
}
|
|
447
|
+
return { named: entry.paths.flatMap(expansions) }
|
|
448
|
+
}
|
|
449
|
+
if (spec.startsWith('.') || spec.startsWith('/') || !needsLookup(spec)) return undefined
|
|
450
|
+
const bases = tsconfigBases(spec, fromDir, memo.tsconfig)
|
|
451
|
+
if (bases.length === 0) return undefined
|
|
452
|
+
const hit = bases.map(fileAt).find((f) => f !== undefined)
|
|
453
|
+
return hit !== undefined ? { file: realpathOr(hit) } : { named: bases.flatMap(expansions) }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* A textual pass before the scan: a local specifier opens with a quoted
|
|
458
|
+
* `./`, `../`, `.`, `..` or `#`, or spells one with an escape, and most
|
|
459
|
+
* configs hold none, so the scan is skipped for them (D-23). A candidate in
|
|
460
|
+
* a comment or string still goes to the scan, which decides.
|
|
461
|
+
*/
|
|
462
|
+
const LOCAL_CANDIDATE = /["'`](?:\.\.?(?:\/|["'`])|#|[^"'`\n]*\\)/
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Absolute targets of the local specifiers in `source` ({@link localTarget}).
|
|
466
|
+
*
|
|
467
|
+
* Paths come back realpath'd, as `Bun.resolveSync` names them — see the
|
|
468
|
+
* caller, which realpaths everything it compares against for exactly that
|
|
340
469
|
* reason. `import type` is erased by `scanImports` and so contributes no edge,
|
|
341
470
|
* which is correct: an erased import cannot move a resolved value.
|
|
342
471
|
*/
|
|
@@ -344,10 +473,9 @@ function scanLocalImports(
|
|
|
344
473
|
source: string,
|
|
345
474
|
fromDir: string,
|
|
346
475
|
loader: 'ts' | 'js',
|
|
347
|
-
memo:
|
|
476
|
+
memo: LookupMemo,
|
|
348
477
|
): string[] {
|
|
349
|
-
|
|
350
|
-
if (!aliased && !RELATIVE_CANDIDATE.test(source)) return []
|
|
478
|
+
if (!LOCAL_CANDIDATE.test(source) && !hasAliasCandidate(source, fromDir, memo.tsconfig)) return []
|
|
351
479
|
let specifiers: string[]
|
|
352
480
|
try {
|
|
353
481
|
specifiers = scanner(loader)
|
|
@@ -358,27 +486,11 @@ function scanLocalImports(
|
|
|
358
486
|
}
|
|
359
487
|
const out: string[] = []
|
|
360
488
|
for (const spec of specifiers) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const hit = bases.map(fileAt).find((f) => f !== undefined)
|
|
367
|
-
if (hit !== undefined) out.push(realpathOr(hit))
|
|
368
|
-
else out.push(...bases.flatMap(expansions))
|
|
369
|
-
continue
|
|
370
|
-
}
|
|
371
|
-
try {
|
|
372
|
-
out.push(Bun.resolveSync(spec, fromDir))
|
|
373
|
-
} catch {
|
|
374
|
-
// Unresolvable: most often the change itself deleted or renamed the
|
|
375
|
-
// target, and the config that imports it is exactly the project the
|
|
376
|
-
// change broke. The edge is the path the specifier names (and, bare
|
|
377
|
-
// of an extension, the files Bun would have tried), so the deleted
|
|
378
|
-
// path in the diff still reaches its importer (item 958). Selection
|
|
379
|
-
// may widen; it is never hashed.
|
|
380
|
-
out.push(...expansions(path.resolve(fromDir, spec)))
|
|
381
|
-
}
|
|
489
|
+
// Selection may widen on a named path; it is never hashed.
|
|
490
|
+
const t = localTarget(spec, fromDir, memo)
|
|
491
|
+
if (t === undefined) continue
|
|
492
|
+
if ('file' in t) out.push(t.file)
|
|
493
|
+
else out.push(...t.named)
|
|
382
494
|
}
|
|
383
495
|
return out
|
|
384
496
|
}
|
|
@@ -396,6 +508,14 @@ export interface ConfigImportOwnersArgs {
|
|
|
396
508
|
changed: readonly string[]
|
|
397
509
|
/** Already-selected projects; their configs need no scan. */
|
|
398
510
|
skip: ReadonlySet<string>
|
|
511
|
+
/** The workspace config files: one of `changed` they import is {@link ConfigImportOwners.workspace}. */
|
|
512
|
+
workspaceConfigs?: readonly string[]
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export interface ConfigImportOwners {
|
|
516
|
+
owners: Set<string>
|
|
517
|
+
/** A changed file a workspace config imports, which can re-key any task. */
|
|
518
|
+
workspace?: string
|
|
399
519
|
}
|
|
400
520
|
|
|
401
521
|
const TS_EXT = new Set(['.ts', '.mts', '.cts'])
|
|
@@ -404,9 +524,11 @@ const TS_EXT = new Set(['.ts', '.mts', '.cts'])
|
|
|
404
524
|
const RESOLVED_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json']
|
|
405
525
|
|
|
406
526
|
/** Projects whose config file transitively imports one of `changed`. */
|
|
407
|
-
export async function configImportOwners(a: ConfigImportOwnersArgs): Promise<
|
|
527
|
+
export async function configImportOwners(a: ConfigImportOwnersArgs): Promise<ConfigImportOwners> {
|
|
408
528
|
const selected = new Set<string>()
|
|
409
|
-
|
|
529
|
+
const wsConfigs = a.workspaceConfigs ?? []
|
|
530
|
+
if (a.changed.length === 0) return { owners: selected }
|
|
531
|
+
if (a.skip.size === a.projects.length && wsConfigs.length === 0) return { owners: selected }
|
|
410
532
|
const workspaceRoot = await realpath(a.workspaceRoot).catch(() => a.workspaceRoot)
|
|
411
533
|
|
|
412
534
|
const roots = new Map<string, string>() // abs config path → project name
|
|
@@ -417,16 +539,19 @@ export async function configImportOwners(a: ConfigImportOwnersArgs): Promise<Set
|
|
|
417
539
|
roots.set(await realpath(cfg).catch(() => path.resolve(cfg)), p.name)
|
|
418
540
|
}),
|
|
419
541
|
)
|
|
420
|
-
|
|
542
|
+
const wsRoots = new Set(
|
|
543
|
+
await Promise.all(wsConfigs.map((f) => realpath(f).catch(() => path.resolve(f)))),
|
|
544
|
+
)
|
|
545
|
+
if (roots.size === 0 && wsRoots.size === 0) return { owners: selected }
|
|
421
546
|
|
|
422
547
|
// target → the files that import it. Reversed up front so one BFS from the
|
|
423
548
|
// changed set answers every root at once, instead of a walk per root.
|
|
424
|
-
const
|
|
549
|
+
const memo = lookupMemo()
|
|
425
550
|
const importedBy = new Map<string, string[]>()
|
|
426
551
|
const visited = new Set<string>()
|
|
427
552
|
// A level at a time, its files read together: one awaited read per
|
|
428
553
|
// config put 5,000 of them 200 ms behind (D-23).
|
|
429
|
-
let frontier = [...roots.keys()]
|
|
554
|
+
let frontier = [...roots.keys(), ...wsRoots]
|
|
430
555
|
while (frontier.length > 0) {
|
|
431
556
|
const read = await Promise.all(
|
|
432
557
|
frontier.map(async (file) => {
|
|
@@ -447,7 +572,7 @@ export async function configImportOwners(a: ConfigImportOwnersArgs): Promise<Set
|
|
|
447
572
|
if (entry === null) continue
|
|
448
573
|
const { file, source } = entry
|
|
449
574
|
const loader = TS_EXT.has(path.extname(file)) ? 'ts' : 'js'
|
|
450
|
-
for (const target of scanLocalImports(source, path.dirname(file), loader,
|
|
575
|
+
for (const target of scanLocalImports(source, path.dirname(file), loader, memo)) {
|
|
451
576
|
if (!target.startsWith(workspaceRoot + path.sep)) continue
|
|
452
577
|
if (target.split(path.sep).includes('node_modules')) continue
|
|
453
578
|
const list = importedBy.get(target)
|
|
@@ -458,17 +583,21 @@ export async function configImportOwners(a: ConfigImportOwnersArgs): Promise<Set
|
|
|
458
583
|
}
|
|
459
584
|
}
|
|
460
585
|
|
|
586
|
+
let workspace: string | undefined
|
|
461
587
|
const seen = new Set<string>()
|
|
462
|
-
|
|
588
|
+
// Each entry carries the changed file it came from, to name the one that
|
|
589
|
+
// reaches a workspace config.
|
|
590
|
+
const walk = a.changed.map((rel) => [path.resolve(workspaceRoot, rel), rel] as const)
|
|
463
591
|
while (walk.length > 0) {
|
|
464
|
-
const file = walk.pop()!
|
|
592
|
+
const [file, from] = walk.pop()!
|
|
465
593
|
if (seen.has(file)) continue
|
|
466
594
|
seen.add(file)
|
|
467
595
|
const owner = roots.get(file)
|
|
468
596
|
if (owner !== undefined) selected.add(owner)
|
|
469
|
-
|
|
597
|
+
if (wsRoots.has(file)) workspace ??= from
|
|
598
|
+
for (const importer of importedBy.get(file) ?? []) walk.push([importer, from])
|
|
470
599
|
}
|
|
471
|
-
return selected
|
|
600
|
+
return workspace === undefined ? { owners: selected } : { owners: selected, workspace }
|
|
472
601
|
}
|
|
473
602
|
|
|
474
603
|
/**
|
|
@@ -482,24 +611,34 @@ function realDirOf(file: string): string {
|
|
|
482
611
|
}
|
|
483
612
|
|
|
484
613
|
/**
|
|
485
|
-
* Every file a config imports by
|
|
614
|
+
* Every file a config imports by local specifier, transitively, outside
|
|
486
615
|
* `node_modules`, the config itself excluded. `vx watch` watches these: a
|
|
487
616
|
* shared preset outside the project (`../../shared/preset.mjs`, the way
|
|
488
617
|
* configs compose) changed what a run evaluates and no watcher saw it
|
|
489
618
|
* (item 949). A file that cannot be read ends its branch; an import that
|
|
490
619
|
* cannot be resolved is still listed, by the names it would have.
|
|
620
|
+
* `configSource` is the config's, when the caller has read it; `read` reads the
|
|
621
|
+
* rest, for a caller that keeps what was read.
|
|
491
622
|
*/
|
|
492
|
-
export async function configImports(
|
|
623
|
+
export async function configImports(
|
|
624
|
+
configPath: string,
|
|
625
|
+
configSource?: string,
|
|
626
|
+
read: (file: string) => Promise<string> = (file) => Bun.file(file).text(),
|
|
627
|
+
): Promise<string[]> {
|
|
493
628
|
const seen = new Set<string>([configPath])
|
|
629
|
+
const memo = lookupMemo()
|
|
494
630
|
const out: string[] = []
|
|
495
631
|
const queue = [configPath]
|
|
632
|
+
let given = configSource
|
|
496
633
|
while (queue.length > 0) {
|
|
497
634
|
const file = queue.shift()!
|
|
498
635
|
let source: string
|
|
499
636
|
try {
|
|
500
|
-
source = await
|
|
637
|
+
source = given ?? (await read(file))
|
|
501
638
|
} catch {
|
|
502
639
|
continue
|
|
640
|
+
} finally {
|
|
641
|
+
given = undefined
|
|
503
642
|
}
|
|
504
643
|
let specs: string[]
|
|
505
644
|
try {
|
|
@@ -516,24 +655,26 @@ export async function configImports(configPath: string): Promise<string[]> {
|
|
|
516
655
|
}
|
|
517
656
|
let dir: string | undefined
|
|
518
657
|
for (const spec of specs) {
|
|
519
|
-
|
|
520
|
-
let resolved: string
|
|
658
|
+
let t: ReturnType<typeof localTarget>
|
|
521
659
|
try {
|
|
522
660
|
dir ??= realDirOf(file)
|
|
523
|
-
|
|
661
|
+
t = localTarget(spec, dir, memo)
|
|
524
662
|
} catch {
|
|
663
|
+
continue
|
|
664
|
+
}
|
|
665
|
+
if (t === undefined) continue
|
|
666
|
+
if (!('file' in t)) {
|
|
525
667
|
// Missing for now, still imported: the config that names it fails
|
|
526
668
|
// to load until it is back, and a list without it left `vx watch`
|
|
527
669
|
// deaf to its return. Named as written, and not walked.
|
|
528
|
-
|
|
529
|
-
for (const ext of path.extname(spec) === '' ? RESOLVED_EXTENSIONS : ['']) {
|
|
530
|
-
const missing = path.resolve(dir, spec + ext)
|
|
670
|
+
for (const missing of t.named) {
|
|
531
671
|
if (seen.has(missing)) continue
|
|
532
672
|
seen.add(missing)
|
|
533
673
|
out.push(missing)
|
|
534
674
|
}
|
|
535
675
|
continue
|
|
536
676
|
}
|
|
677
|
+
const resolved = realpathOr(t.file)
|
|
537
678
|
if (seen.has(resolved) || resolved.split(path.sep).includes('node_modules')) continue
|
|
538
679
|
seen.add(resolved)
|
|
539
680
|
out.push(resolved)
|