@vzn/vx 0.0.651 → 0.0.653
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 +3 -4
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +15 -20
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +37 -3
- package/src/cache/archive.ts +197 -32
- package/src/cache/cache.ts +882 -230
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +151 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +12 -1
- package/src/cache/inputs.ts +616 -135
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +29 -16
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/lockfile-memo.ts +2 -1
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +94 -25
- package/src/cache/schema.ts +46 -10
- package/src/cli/cache.ts +149 -36
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +27 -4
- package/src/cli/index.ts +62 -7
- package/src/cli/info.ts +5 -4
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +218 -43
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +70 -28
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +73 -9
- package/src/cli/watch-set.ts +10 -8
- package/src/cli/watch.ts +190 -33
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +56 -30
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +116 -16
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +446 -64
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/definition.ts +16 -3
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +176 -39
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +347 -160
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +39 -6
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +193 -73
- package/src/index.ts +6 -3
- package/src/orchestrator/admission.ts +14 -10
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +42 -13
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +8 -10
- package/src/orchestrator/excluded-keys.ts +15 -3
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +612 -138
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +141 -93
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +53 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +60 -61
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +32 -16
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +82 -16
- package/src/orchestrator/plan.ts +39 -17
- package/src/orchestrator/plugin-host.ts +238 -110
- package/src/orchestrator/plugin.ts +59 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +101 -23
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +95 -74
- package/src/orchestrator/run-report.ts +3 -2
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +531 -122
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +314 -44
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +29 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +34 -27
- package/src/orchestrator/task-artifacts.ts +16 -3
- package/src/orchestrator/task-hash.ts +191 -34
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +85 -13
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +23 -6
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +179 -162
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +108 -31
- package/src/report/plain-output.ts +15 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +61 -18
- package/src/settings.ts +50 -14
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +142 -6
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +51 -7
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +18 -1
- package/src/util/size.ts +12 -2
- package/src/util/tail.ts +4 -1
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +192 -86
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +18 -2
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +58 -31
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
|
@@ -36,8 +36,10 @@
|
|
|
36
36
|
// with the message the first load gives (item 701).
|
|
37
37
|
|
|
38
38
|
import path from 'node:path'
|
|
39
|
-
import
|
|
39
|
+
import * as ui from '../ui/index.js'
|
|
40
|
+
import { MAX_TIMEOUT_MS, parseDuration, testTimerMs, UserError } from '../util/index.js'
|
|
40
41
|
import { nonJsonMessage, nonJsonPaths, type NonJsonValue } from './json-data.js'
|
|
42
|
+
import { changedExports, exportState } from './module-state.js'
|
|
41
43
|
|
|
42
44
|
/** Why a config's `process.exit` throws, on both load paths (D-65). */
|
|
43
45
|
export const CONFIG_EXIT = 'a config exports its object; it cannot end the run'
|
|
@@ -76,6 +78,8 @@ export const WATCHED_BUILTIN_NAMES = [
|
|
|
76
78
|
|
|
77
79
|
const WORKER_SRC = `
|
|
78
80
|
const nonJsonPaths = ${nonJsonPaths.toString()}
|
|
81
|
+
const exportState = ${exportState.toString()}
|
|
82
|
+
const changedExports = ${changedExports.toString()}
|
|
79
83
|
// The parent's stdout is a verb's JSON, and a config printing on a repeat
|
|
80
84
|
// load wrote into it: the parent's own redirect covers the parent thread
|
|
81
85
|
// only (D-64). This worker only evaluates configs, so every route to fd 1
|
|
@@ -171,6 +175,15 @@ self.onmessage = async (e) => {
|
|
|
171
175
|
const umaskBack = () => {
|
|
172
176
|
if (umaskIn !== null && globalThis.process.umask() !== umaskIn) globalThis.process.umask(umaskIn)
|
|
173
177
|
}
|
|
178
|
+
// Asked which shared modules this config changes: each is imported
|
|
179
|
+
// first, so its state before the config's own code runs is known.
|
|
180
|
+
const modules = []
|
|
181
|
+
for (const m of e.data.modules ?? []) {
|
|
182
|
+
try {
|
|
183
|
+
const ns = await import(m)
|
|
184
|
+
modules.push([m, ns, exportState(ns)])
|
|
185
|
+
} catch {}
|
|
186
|
+
}
|
|
174
187
|
try {
|
|
175
188
|
const literalPath = /[\\\\?]/.test(path)
|
|
176
189
|
if (literalPath) {
|
|
@@ -196,6 +209,7 @@ self.onmessage = async (e) => {
|
|
|
196
209
|
json: isObject && nonJson.length === 0 ? JSON.stringify(mod) : null,
|
|
197
210
|
fn: typeof mod === 'function',
|
|
198
211
|
changed: report,
|
|
212
|
+
modules: modules.filter(([, ns, before]) => changedExports(before, exportState(ns)).length > 0).map(([m]) => m),
|
|
199
213
|
})
|
|
200
214
|
} catch (thrown) {
|
|
201
215
|
// Two syntax errors arrive as one AggregateError, whose own message
|
|
@@ -289,6 +303,41 @@ export function evalBudgetMs(): number {
|
|
|
289
303
|
return n !== null && n > 0 && n <= MAX_TIMEOUT_MS ? n : 30_000
|
|
290
304
|
}
|
|
291
305
|
|
|
306
|
+
/** How long a config evaluates before vx says it is still waiting on it. */
|
|
307
|
+
const EVAL_NOTICE_MS = 5_000
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Calls `expire` once `budget` ms pass, and says once, at the notice mark,
|
|
311
|
+
* which config is still evaluating: a top-level await that never settles
|
|
312
|
+
* was otherwise `budget` ms of silence. One timer at a time, re-armed at the
|
|
313
|
+
* notice, so a load costs the one timer it always had. Returns the clear.
|
|
314
|
+
* `hold` keeps the process alive for it: in process, nothing else holds
|
|
315
|
+
* it under a never-settled top-level await, and the run exited silently.
|
|
316
|
+
*/
|
|
317
|
+
export function evalDeadline(
|
|
318
|
+
kind: string,
|
|
319
|
+
configPath: string,
|
|
320
|
+
budget: number,
|
|
321
|
+
expire: () => void,
|
|
322
|
+
hold: boolean,
|
|
323
|
+
): () => void {
|
|
324
|
+
const noticeMs = testTimerMs('CONFIG_EVAL_NOTICE', EVAL_NOTICE_MS)
|
|
325
|
+
let timer: ReturnType<typeof setTimeout>
|
|
326
|
+
if (noticeMs < budget) {
|
|
327
|
+
timer = setTimeout(() => {
|
|
328
|
+
const time = (ms: number): string => (ms < 1000 ? `${ms} ms` : `${ms / 1000} s`)
|
|
329
|
+
ui.sayLine(
|
|
330
|
+
'warn',
|
|
331
|
+
`${kind} config ${configPath} still evaluating after ${time(noticeMs)}; vx gives up at ${time(budget)} (VX_CONFIG_WORKER_TIMEOUT)`,
|
|
332
|
+
)
|
|
333
|
+
timer = setTimeout(expire, budget - noticeMs)
|
|
334
|
+
if (!hold) timer.unref()
|
|
335
|
+
}, noticeMs)
|
|
336
|
+
} else timer = setTimeout(expire, budget)
|
|
337
|
+
if (!hold) timer.unref()
|
|
338
|
+
return () => clearTimeout(timer)
|
|
339
|
+
}
|
|
340
|
+
|
|
292
341
|
const pending = new Map<number, Pending>()
|
|
293
342
|
let worker: Worker | null = null
|
|
294
343
|
let inFlight = 0
|
|
@@ -410,7 +459,7 @@ export async function evaluateConfigFresh(configPath: string): Promise<unknown>
|
|
|
410
459
|
inFlight++
|
|
411
460
|
// Declared out here so the `finally` can clear it on EVERY exit, including a
|
|
412
461
|
// rejection. See the note at the clear itself.
|
|
413
|
-
let
|
|
462
|
+
let clear: (() => void) | undefined
|
|
414
463
|
try {
|
|
415
464
|
const w = acquireWorker()
|
|
416
465
|
// A worker thread the OS kills — memory pressure on a loaded CI box, a
|
|
@@ -422,14 +471,19 @@ export async function evaluateConfigFresh(configPath: string): Promise<unknown>
|
|
|
422
471
|
const budget = evalBudgetMs()
|
|
423
472
|
const reply = await new Promise<WorkerReply>((resolve, reject) => {
|
|
424
473
|
pending.set(id, { resolve, reject, configPath })
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
474
|
+
clear = evalDeadline(
|
|
475
|
+
'Project',
|
|
476
|
+
configPath,
|
|
477
|
+
budget,
|
|
478
|
+
() => {
|
|
479
|
+
rejectAll(new Error(`config worker did not answer within ${budget}ms`))
|
|
480
|
+
if (worker !== null) {
|
|
481
|
+
worker.terminate()
|
|
482
|
+
worker = null
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
false,
|
|
486
|
+
)
|
|
433
487
|
w.postMessage({ id, path: abs, env: { ...process.env } })
|
|
434
488
|
})
|
|
435
489
|
const [nonJson] = reply.nonJson
|
|
@@ -445,7 +499,7 @@ export async function evaluateConfigFresh(configPath: string): Promise<unknown>
|
|
|
445
499
|
// later fired it ran `rejectAll()` and terminated whatever worker was
|
|
446
500
|
// current by then, killing an unrelated healthy round with a timeout
|
|
447
501
|
// message naming a budget nobody set for it.
|
|
448
|
-
|
|
502
|
+
clear?.()
|
|
449
503
|
pending.delete(id)
|
|
450
504
|
retireIfIdle()
|
|
451
505
|
}
|
|
@@ -459,20 +513,40 @@ export async function evaluateConfigFresh(configPath: string): Promise<unknown>
|
|
|
459
513
|
* changes nothing, fails before reporting, or outlives the budget.
|
|
460
514
|
*/
|
|
461
515
|
export async function builtinsChangedBy(configPath: string): Promise<string[]> {
|
|
516
|
+
return (await blame(configPath, { blame: true })).changed ?? []
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Which of `modules` evaluating `configPath` alone changes the exported
|
|
521
|
+
* values of, in a worker of its own: a round that found a module several
|
|
522
|
+
* configs import changed asks each of them in turn, to name the one that
|
|
523
|
+
* changed it. Empty as `builtinsChangedBy` is.
|
|
524
|
+
*/
|
|
525
|
+
export async function modulesChangedBy(
|
|
526
|
+
configPath: string,
|
|
527
|
+
modules: readonly string[],
|
|
528
|
+
): Promise<string[]> {
|
|
529
|
+
return (await blame(configPath, { modules })).modules ?? []
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function blame(
|
|
533
|
+
configPath: string,
|
|
534
|
+
ask: { blame?: true; modules?: readonly string[] },
|
|
535
|
+
): Promise<{ changed?: string[]; modules?: string[] }> {
|
|
462
536
|
const w = new Worker(WORKER_URL)
|
|
463
537
|
try {
|
|
464
|
-
return await new Promise
|
|
465
|
-
const timer = setTimeout(() => resolve(
|
|
538
|
+
return await new Promise((resolve) => {
|
|
539
|
+
const timer = setTimeout(() => resolve({}), evalBudgetMs())
|
|
466
540
|
timer.unref?.()
|
|
467
541
|
w.onmessage = (event: MessageEvent): void => {
|
|
468
542
|
clearTimeout(timer)
|
|
469
|
-
resolve(
|
|
543
|
+
resolve(event.data as { changed?: string[]; modules?: string[] })
|
|
470
544
|
}
|
|
471
545
|
w.onerror = (): void => {
|
|
472
546
|
clearTimeout(timer)
|
|
473
|
-
resolve(
|
|
547
|
+
resolve({})
|
|
474
548
|
}
|
|
475
|
-
w.postMessage({ id: 0, path: path.resolve(configPath), env: { ...process.env },
|
|
549
|
+
w.postMessage({ id: 0, path: path.resolve(configPath), env: { ...process.env }, ...ask })
|
|
476
550
|
})
|
|
477
551
|
} finally {
|
|
478
552
|
w.terminate()
|
|
@@ -474,8 +474,15 @@ function scanLocalImports(
|
|
|
474
474
|
fromDir: string,
|
|
475
475
|
loader: 'ts' | 'js',
|
|
476
476
|
memo: LookupMemo,
|
|
477
|
+
/** Also follow a bare specifier into a package linked from outside `node_modules` ({@link linkedPackageFile}). */
|
|
478
|
+
linked = false,
|
|
477
479
|
): string[] {
|
|
478
|
-
if (
|
|
480
|
+
if (
|
|
481
|
+
!LOCAL_CANDIDATE.test(source) &&
|
|
482
|
+
!hasAliasCandidate(source, fromDir, memo.tsconfig) &&
|
|
483
|
+
!(linked && hasBareCandidate(source))
|
|
484
|
+
)
|
|
485
|
+
return []
|
|
479
486
|
let specifiers: string[]
|
|
480
487
|
try {
|
|
481
488
|
specifiers = scanner(loader)
|
|
@@ -488,13 +495,41 @@ function scanLocalImports(
|
|
|
488
495
|
for (const spec of specifiers) {
|
|
489
496
|
// Selection may widen on a named path; it is never hashed.
|
|
490
497
|
const t = localTarget(spec, fromDir, memo)
|
|
491
|
-
if (t === undefined)
|
|
492
|
-
|
|
498
|
+
if (t === undefined) {
|
|
499
|
+
const file = linked ? linkedPackageFile(spec, fromDir) : undefined
|
|
500
|
+
if (file !== undefined) out.push(file)
|
|
501
|
+
} else if ('file' in t) out.push(t.file)
|
|
493
502
|
else out.push(...t.named)
|
|
494
503
|
}
|
|
495
504
|
return out
|
|
496
505
|
}
|
|
497
506
|
|
|
507
|
+
/**
|
|
508
|
+
* The file a bare `spec` loads when the nearest `node_modules/<package>`
|
|
509
|
+
* above `fromDir` links to a directory outside any `node_modules`: a
|
|
510
|
+
* workspace package (`workspace:*`), whose source the lockfile does not
|
|
511
|
+
* pin. Realpath'd. Undefined for an installed package, or none installed:
|
|
512
|
+
* resolving then could ask the registry.
|
|
513
|
+
*/
|
|
514
|
+
function linkedPackageFile(spec: string, fromDir: string): string | undefined {
|
|
515
|
+
if (spec.startsWith('.') || spec.startsWith('/') || !needsLookup(spec)) return undefined
|
|
516
|
+
const pkg = packageOf(spec)
|
|
517
|
+
for (let dir = fromDir; ;) {
|
|
518
|
+
const installed = path.join(dir, 'node_modules', pkg)
|
|
519
|
+
if (existsSync(installed)) {
|
|
520
|
+
if (realpathOr(installed).split(path.sep).includes('node_modules')) return undefined
|
|
521
|
+
try {
|
|
522
|
+
return realpathOr(Bun.resolveSync(spec, fromDir))
|
|
523
|
+
} catch {
|
|
524
|
+
return undefined
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const up = path.dirname(dir)
|
|
528
|
+
if (up === dir) return undefined
|
|
529
|
+
dir = up
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
498
533
|
export interface ConfigImportOwnersArgs {
|
|
499
534
|
/**
|
|
500
535
|
* Realpath'd HERE, not by the caller. `Bun.resolveSync` returns realpath'd
|
|
@@ -516,6 +551,11 @@ export interface ConfigImportOwners {
|
|
|
516
551
|
owners: Set<string>
|
|
517
552
|
/** A changed file a workspace config imports, which can re-key any task. */
|
|
518
553
|
workspace?: string
|
|
554
|
+
/**
|
|
555
|
+
* The files a workspace config imports above the workspace root,
|
|
556
|
+
* realpath'd: `changed` cannot name them, so the caller asks git.
|
|
557
|
+
*/
|
|
558
|
+
outside?: string[]
|
|
519
559
|
}
|
|
520
560
|
|
|
521
561
|
const TS_EXT = new Set(['.ts', '.mts', '.cts'])
|
|
@@ -527,7 +567,7 @@ const RESOLVED_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs
|
|
|
527
567
|
export async function configImportOwners(a: ConfigImportOwnersArgs): Promise<ConfigImportOwners> {
|
|
528
568
|
const selected = new Set<string>()
|
|
529
569
|
const wsConfigs = a.workspaceConfigs ?? []
|
|
530
|
-
if (a.changed.length === 0) return { owners: selected }
|
|
570
|
+
if (a.changed.length === 0 && wsConfigs.length === 0) return { owners: selected }
|
|
531
571
|
if (a.skip.size === a.projects.length && wsConfigs.length === 0) return { owners: selected }
|
|
532
572
|
const workspaceRoot = await realpath(a.workspaceRoot).catch(() => a.workspaceRoot)
|
|
533
573
|
|
|
@@ -549,39 +589,50 @@ export async function configImportOwners(a: ConfigImportOwnersArgs): Promise<Con
|
|
|
549
589
|
const memo = lookupMemo()
|
|
550
590
|
const importedBy = new Map<string, string[]>()
|
|
551
591
|
const visited = new Set<string>()
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
592
|
+
// The workspace config is loaded live, not from the lock, so its walk
|
|
593
|
+
// goes further: into a workspace package it names (linked into
|
|
594
|
+
// node_modules), and above the root, which `changed` cannot name.
|
|
595
|
+
const outside = new Set<string>()
|
|
596
|
+
const walkImports = async (start: string[], ws: boolean): Promise<void> => {
|
|
597
|
+
// A level at a time, its files read together: one awaited read per
|
|
598
|
+
// config put 5,000 of them 200 ms behind (D-23).
|
|
599
|
+
let frontier = start
|
|
600
|
+
while (frontier.length > 0) {
|
|
601
|
+
const read = await Promise.all(
|
|
602
|
+
frontier.map(async (file) => {
|
|
603
|
+
if (visited.has(file)) return null
|
|
604
|
+
visited.add(file)
|
|
605
|
+
try {
|
|
606
|
+
return { file, source: await Bun.file(file).text() }
|
|
607
|
+
} catch (err) {
|
|
608
|
+
// Out of descriptors is this level's width, not the file: read as
|
|
609
|
+
// no edges, it dropped every project the import selects (D-63).
|
|
610
|
+
if (isOutOfFds(err)) throw err
|
|
611
|
+
return null // unreadable: no edges, and not this pass's problem to report
|
|
612
|
+
}
|
|
613
|
+
}),
|
|
614
|
+
)
|
|
615
|
+
frontier = []
|
|
616
|
+
for (const entry of read) {
|
|
617
|
+
if (entry === null) continue
|
|
618
|
+
const { file, source } = entry
|
|
619
|
+
const loader = TS_EXT.has(path.extname(file)) ? 'ts' : 'js'
|
|
620
|
+
for (const target of scanLocalImports(source, path.dirname(file), loader, memo, ws)) {
|
|
621
|
+
if (target.split(path.sep).includes('node_modules')) continue
|
|
622
|
+
if (!target.startsWith(workspaceRoot + path.sep)) {
|
|
623
|
+
if (!ws) continue
|
|
624
|
+
outside.add(target)
|
|
625
|
+
}
|
|
626
|
+
const list = importedBy.get(target)
|
|
627
|
+
if (list) list.push(file)
|
|
628
|
+
else importedBy.set(target, [file])
|
|
629
|
+
frontier.push(target)
|
|
567
630
|
}
|
|
568
|
-
}),
|
|
569
|
-
)
|
|
570
|
-
frontier = []
|
|
571
|
-
for (const entry of read) {
|
|
572
|
-
if (entry === null) continue
|
|
573
|
-
const { file, source } = entry
|
|
574
|
-
const loader = TS_EXT.has(path.extname(file)) ? 'ts' : 'js'
|
|
575
|
-
for (const target of scanLocalImports(source, path.dirname(file), loader, memo)) {
|
|
576
|
-
if (!target.startsWith(workspaceRoot + path.sep)) continue
|
|
577
|
-
if (target.split(path.sep).includes('node_modules')) continue
|
|
578
|
-
const list = importedBy.get(target)
|
|
579
|
-
if (list) list.push(file)
|
|
580
|
-
else importedBy.set(target, [file])
|
|
581
|
-
frontier.push(target)
|
|
582
631
|
}
|
|
583
632
|
}
|
|
584
633
|
}
|
|
634
|
+
await walkImports([...wsRoots], true)
|
|
635
|
+
await walkImports([...roots.keys()], false)
|
|
585
636
|
|
|
586
637
|
let workspace: string | undefined
|
|
587
638
|
const seen = new Set<string>()
|
|
@@ -597,7 +648,8 @@ export async function configImportOwners(a: ConfigImportOwnersArgs): Promise<Con
|
|
|
597
648
|
if (wsRoots.has(file)) workspace ??= from
|
|
598
649
|
for (const importer of importedBy.get(file) ?? []) walk.push([importer, from])
|
|
599
650
|
}
|
|
600
|
-
|
|
651
|
+
if (workspace !== undefined) return { owners: selected, workspace }
|
|
652
|
+
return outside.size === 0 ? { owners: selected } : { owners: selected, outside: [...outside] }
|
|
601
653
|
}
|
|
602
654
|
|
|
603
655
|
/**
|
|
@@ -618,12 +670,15 @@ function realDirOf(file: string): string {
|
|
|
618
670
|
* (item 949). A file that cannot be read ends its branch; an import that
|
|
619
671
|
* cannot be resolved is still listed, by the names it would have.
|
|
620
672
|
* `configSource` is the config's, when the caller has read it; `read` reads the
|
|
621
|
-
* rest, for a caller that keeps what was read.
|
|
673
|
+
* rest, for a caller that keeps what was read. `statics`, when given,
|
|
674
|
+
* gets each file the config reaches through static imports alone: the ones
|
|
675
|
+
* its evaluation always evaluates, a dynamic `import()` aside.
|
|
622
676
|
*/
|
|
623
677
|
export async function configImports(
|
|
624
678
|
configPath: string,
|
|
625
679
|
configSource?: string,
|
|
626
680
|
read: (file: string) => Promise<string> = (file) => Bun.file(file).text(),
|
|
681
|
+
statics?: Set<string>,
|
|
627
682
|
): Promise<string[]> {
|
|
628
683
|
const seen = new Set<string>([configPath])
|
|
629
684
|
const memo = lookupMemo()
|
|
@@ -640,21 +695,20 @@ export async function configImports(
|
|
|
640
695
|
} finally {
|
|
641
696
|
given = undefined
|
|
642
697
|
}
|
|
643
|
-
let specs: string
|
|
698
|
+
let specs: Array<{ path: string; kind: string }>
|
|
644
699
|
try {
|
|
645
|
-
specs = scanner(TS_EXT.has(path.extname(file)) ? 'ts' : 'js')
|
|
646
|
-
.scanImports(source)
|
|
647
|
-
.map((i) => i.path)
|
|
700
|
+
specs = scanner(TS_EXT.has(path.extname(file)) ? 'ts' : 'js').scanImports(source)
|
|
648
701
|
} catch {
|
|
649
702
|
// A source the parser refuses (mid-edit) keeps the imports a plain
|
|
650
703
|
// match sees: watching too few files is what this list exists to
|
|
651
704
|
// prevent.
|
|
652
705
|
specs = [...source.matchAll(/\bfrom\s*['"]([^'"]+)['"]|\bimport\s*['"]([^'"]+)['"]/g)].map(
|
|
653
|
-
(m) => (m[1] ?? m[2])!,
|
|
706
|
+
(m) => ({ path: (m[1] ?? m[2])!, kind: 'import-statement' }),
|
|
654
707
|
)
|
|
655
708
|
}
|
|
709
|
+
const fromStatic = statics !== undefined && (file === configPath || statics.has(file))
|
|
656
710
|
let dir: string | undefined
|
|
657
|
-
for (const spec of specs) {
|
|
711
|
+
for (const { path: spec, kind } of specs) {
|
|
658
712
|
let t: ReturnType<typeof localTarget>
|
|
659
713
|
try {
|
|
660
714
|
dir ??= realDirOf(file)
|
|
@@ -679,6 +733,7 @@ export async function configImports(
|
|
|
679
733
|
seen.add(resolved)
|
|
680
734
|
out.push(resolved)
|
|
681
735
|
queue.push(resolved)
|
|
736
|
+
if (fromStatic && kind === 'import-statement') statics.add(resolved)
|
|
682
737
|
}
|
|
683
738
|
}
|
|
684
739
|
return out
|