@vzn/vx 0.0.33 → 0.0.35
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 +5 -5
- package/src/cli/select.ts +21 -1
- package/src/orchestrator/plugin-host.ts +15 -1
- package/src/orchestrator/prepare.ts +152 -141
- package/src/workspace/filter.ts +14 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.35",
|
|
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.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.35",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.35",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.35",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.35"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cli/select.ts
CHANGED
|
@@ -202,7 +202,12 @@ export async function resolveFilters(
|
|
|
202
202
|
): Promise<FilterResolution> {
|
|
203
203
|
const root = await findWorkspaceRoot(cwd)
|
|
204
204
|
const projects = await loadWorkspaceProjects(cwd)
|
|
205
|
-
|
|
205
|
+
let parsed: ReturnType<typeof parseFilter>[]
|
|
206
|
+
try {
|
|
207
|
+
parsed = raw.map((r) => parseFilter(r, root))
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return { error: err instanceof Error ? err.message : String(err) }
|
|
210
|
+
}
|
|
206
211
|
const walksGraph = parsed.some((f) => f.withDeps || f.withDependents || f.onlyDeps)
|
|
207
212
|
// Every reader of the staged configs in this pass — the `pkg#task`
|
|
208
213
|
// edge walk, the `workspaceFiles` owners of a changed path — shares
|
|
@@ -244,6 +249,7 @@ export async function resolveFilters(
|
|
|
244
249
|
}
|
|
245
250
|
|
|
246
251
|
const unmatched: string[] = []
|
|
252
|
+
const emptyWalks: string[] = []
|
|
247
253
|
const selected = applyFilters({
|
|
248
254
|
filters: parsed,
|
|
249
255
|
projects,
|
|
@@ -255,6 +261,17 @@ export async function resolveFilters(
|
|
|
255
261
|
onNoMatch: (f) => {
|
|
256
262
|
if (f.gitSince === undefined) unmatched.push(f.raw)
|
|
257
263
|
},
|
|
264
|
+
// A pattern that matched, with nothing on the walk it asked for: the
|
|
265
|
+
// fact to say, where "no projects matched" read as a typo (item 1030).
|
|
266
|
+
onEmptyWalk: (f, matched) => {
|
|
267
|
+
if (f.negate || f.gitSince !== undefined) return
|
|
268
|
+
const which = matched.join(', ')
|
|
269
|
+
emptyWalks.push(
|
|
270
|
+
f.onlyDeps
|
|
271
|
+
? `filter "${f.raw}" matched ${which}, which depends on no project`
|
|
272
|
+
: `filter "${f.raw}" matched ${which}, and no project depends on it`,
|
|
273
|
+
)
|
|
274
|
+
},
|
|
258
275
|
})
|
|
259
276
|
if (selected.size === 0) {
|
|
260
277
|
// "Nothing changed" is a legitimate outcome, not a typo — a docs-only
|
|
@@ -273,6 +290,9 @@ export async function resolveFilters(
|
|
|
273
290
|
: ` — ${self.gitSince} is HEAD itself: compare with the branch you merge into (--affected=origin/main) or the previous commit (--affected=HEAD~1)`
|
|
274
291
|
return { empty: `nothing affected since ${refs}${hint}` }
|
|
275
292
|
}
|
|
293
|
+
if (unmatched.length === 0 && emptyWalks.length > 0) {
|
|
294
|
+
return { error: `no projects selected: ${emptyWalks.join('; ')}` }
|
|
295
|
+
}
|
|
276
296
|
// One line, not a warning per pattern and then an error saying the same:
|
|
277
297
|
// the patterns are in the error, and the nearest project name is the
|
|
278
298
|
// hint a typo needs.
|
|
@@ -188,7 +188,21 @@ export async function applyKeyHooks(
|
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
if (parts.length > 0) {
|
|
191
|
-
|
|
191
|
+
const byName = (a: readonly [string, string], b: readonly [string, string]): number =>
|
|
192
|
+
a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0
|
|
193
|
+
parts.sort(byName)
|
|
194
|
+
// A plugin's name is its package's, and one package may define two
|
|
195
|
+
// plugins (`@vzn/vx-migrate`'s `turbo()` and `turboCache()`): two
|
|
196
|
+
// parts named alike folded both values under one name, and `vx why`
|
|
197
|
+
// could not say which moved (item 1028). The repeats take `#2`, `#3`
|
|
198
|
+
// in value order, so declaration order still keys nothing.
|
|
199
|
+
let previous = ''
|
|
200
|
+
let n = 0
|
|
201
|
+
for (const [i, [name, value]] of parts.entries()) {
|
|
202
|
+
n = name === previous ? n + 1 : 1
|
|
203
|
+
previous = name
|
|
204
|
+
if (n > 1) parts[i] = [`${name}#${n}`, value]
|
|
205
|
+
}
|
|
192
206
|
node.keyParts = parts
|
|
193
207
|
}
|
|
194
208
|
}
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
fingerprintClaims,
|
|
52
52
|
hasHook,
|
|
53
53
|
resolveCache,
|
|
54
|
+
teardownPlugins,
|
|
54
55
|
} from './plugin-host.js'
|
|
55
56
|
import { loadProjects, loadWorkspacePlugins, type LoadedProjects } from './projects.js'
|
|
56
57
|
import { keyExcludedDependencies } from './excluded-keys.js'
|
|
@@ -335,33 +336,152 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
335
336
|
// layer's own truthful answer; `LayeredCache` sets it, a bare `Cache`
|
|
336
337
|
// doesn't, and a third-party layer opts in when it really has a remote.
|
|
337
338
|
const hasRemoteLayer = cache.hasRemote === true
|
|
339
|
+
// From here the plugins' factories have run and the cache is open: a
|
|
340
|
+
// throw (a stage hook, a `^name` nobody declares, a cycle, an unknown
|
|
341
|
+
// exclude name) closed neither and tore nothing down, once per failed
|
|
342
|
+
// cycle under `vx watch` (item 1029). run() owns both only once this
|
|
343
|
+
// returns.
|
|
344
|
+
try {
|
|
345
|
+
const gitFilesCache = new GitFilesCache()
|
|
346
|
+
// Bulk-populate via a single `git ls-files` at the workspace root —
|
|
347
|
+
// partitions the output by project. Avoids one fork+exec per project
|
|
348
|
+
// (~5-10ms each on Linux; the dominant cold-start cost on big
|
|
349
|
+
// monorepos). When any loaded task declares inputs.workspaceFiles,
|
|
350
|
+
// the enumeration must see every file from the root (no pathspec
|
|
351
|
+
// scoping) and additionally stores a workspace-wide partition.
|
|
352
|
+
const usesWorkspaceInputs = [...projects.values()].some((p) =>
|
|
353
|
+
Object.values(p.config.tasks ?? {}).some(
|
|
354
|
+
(t) => (t.cache?.inputs.workspaceFiles?.length ?? 0) > 0,
|
|
355
|
+
),
|
|
356
|
+
)
|
|
357
|
+
const projectDirs = [...projects.values()].map((p) => p.dir)
|
|
358
|
+
const enumeration = await (earlyGit ??
|
|
359
|
+
startGitEnumeration(
|
|
360
|
+
workspaceRoot,
|
|
361
|
+
gitPathspecs(workspaceRoot, projectDirs, usesWorkspaceInputs),
|
|
362
|
+
))
|
|
363
|
+
applyGitEnumeration(enumeration, workspaceRoot, projectDirs, gitFilesCache, usesWorkspaceInputs)
|
|
364
|
+
mark('git enumeration')
|
|
365
|
+
const hashCache = createHashCache()
|
|
366
|
+
const fingerprintWatch = new FingerprintWatch(workspaceRoot, fingerprints, fingerprintsAt)
|
|
338
367
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
368
|
+
// Empty-cases bookkeeping. We still construct the cache + fingerprint
|
|
369
|
+
// so the caller's try/finally pattern can close it uniformly.
|
|
370
|
+
if (requested.length === 0) {
|
|
371
|
+
return {
|
|
372
|
+
workspaceRoot,
|
|
373
|
+
workspaceConfig,
|
|
374
|
+
plugins,
|
|
375
|
+
cacheDir,
|
|
376
|
+
cache,
|
|
377
|
+
localCache,
|
|
378
|
+
hasRemoteLayer,
|
|
379
|
+
priorities: new Map(),
|
|
380
|
+
nodes: new Map(),
|
|
381
|
+
unresolvedTasks,
|
|
382
|
+
projects,
|
|
383
|
+
anyProjectConfig: projectsWithConfigs.length > 0,
|
|
384
|
+
workspaceFingerprint,
|
|
385
|
+
fingerprintWatch,
|
|
386
|
+
nestedDirsByProject,
|
|
387
|
+
gitFilesCache,
|
|
388
|
+
hashCache,
|
|
389
|
+
workspaceProjectCount: projectMetas.length,
|
|
390
|
+
empty:
|
|
391
|
+
options.selectedByDiff === true && unresolvedTasks.length === 0
|
|
392
|
+
? 'none-affected'
|
|
393
|
+
: 'no-tasks-declared',
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// The whole graph, whatever `--exclude-dependencies` says: the stages
|
|
398
|
+
// below shape and key the tasks it drops as a full run would, because
|
|
399
|
+
// a dropped task is still keyed (excluded-keys.ts). A scoped load is not
|
|
400
|
+
// the whole workspace: a `^name` nothing loaded declares may be declared
|
|
401
|
+
// by a config the scope left out, so the builder hands it back instead of
|
|
402
|
+
// refusing it, and the rest decide.
|
|
403
|
+
const unproven: Array<[taskId: string, name: string]> = []
|
|
404
|
+
const nodes = buildTaskGraph({
|
|
405
|
+
projects,
|
|
406
|
+
packageGraph,
|
|
407
|
+
requested,
|
|
408
|
+
...(projects.size < projectsWithConfigs.length
|
|
409
|
+
? { undeclaredDeps: (id: string, name: string) => void unproven.push([id, name]) }
|
|
410
|
+
: {}),
|
|
411
|
+
})
|
|
412
|
+
if (unproven.length > 0) {
|
|
413
|
+
await refuseUndeclaredDeps(unproven, () =>
|
|
414
|
+
loadProjects({ ...loadArgs, seeds: 'all', closure: false, staged: projects }),
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
// An `--exclude-dependencies` name no project declares drops nothing:
|
|
418
|
+
// `=biuld` planned the whole chain and ran it, exit 0 (item 1026). Every
|
|
419
|
+
// other name the user types must resolve; so must this one. After the
|
|
420
|
+
// graph, so a config error (`^biuld` nobody declares) is named first.
|
|
421
|
+
const excludeNames = options.excludeDependencies
|
|
422
|
+
if (Array.isArray(excludeNames) && excludeNames.length > 0) {
|
|
423
|
+
const declared = new Set<string>()
|
|
424
|
+
for (const p of projects.values())
|
|
425
|
+
for (const t of Object.keys(p.config.tasks ?? {})) declared.add(t)
|
|
426
|
+
let unknown = excludeNames.filter((n) => !declared.has(n))
|
|
427
|
+
if (unknown.length > 0 && projects.size < projectsWithConfigs.length) {
|
|
428
|
+
unknown = await declaredNowhere(unknown, () =>
|
|
429
|
+
loadProjects({ ...loadArgs, seeds: 'all', closure: false, staged: projects }),
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
if (unknown.length > 0) {
|
|
433
|
+
cache.close()
|
|
434
|
+
const hints = new Set(unknown.flatMap((n) => nearest(n, declared) ?? []))
|
|
435
|
+
const hint = hints.size === 0 ? '' : ` Did you mean ${[...hints].join(', ')}?`
|
|
436
|
+
throw new UserError(
|
|
437
|
+
`--exclude-dependencies names a task no project declares: ${unknown.join(', ')}.${hint}`,
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
// The graph is built; what follows is the plugins' (graph, key,
|
|
442
|
+
// schedule). Two rows, so a plugin's key stage reads as its own cost
|
|
443
|
+
// and not as graph building — a lockfile plugin's 1000 stats per run
|
|
444
|
+
// hid inside one `prepare (graph)` row until 2026-09-10.
|
|
445
|
+
mark('build graph')
|
|
446
|
+
if (hasHook(plugins, 'graph')) {
|
|
447
|
+
await applyGraphHooks(plugins, nodes, {
|
|
448
|
+
workspaceRoot,
|
|
449
|
+
cacheDir,
|
|
450
|
+
warn: (m) => log.status(m),
|
|
451
|
+
requested: [...nodes.values()].filter((n) => n.requested).map((n) => n.id),
|
|
452
|
+
})
|
|
453
|
+
}
|
|
454
|
+
if (hasHook(plugins, 'key')) {
|
|
455
|
+
await applyKeyHooks(plugins, nodes, { workspaceRoot, cacheDir, warn: (m) => log.status(m) })
|
|
456
|
+
}
|
|
457
|
+
const exclude = options.excludeDependencies
|
|
458
|
+
if (exclude !== undefined && (exclude === 'all' || exclude.length > 0)) {
|
|
459
|
+
const { keyOnly, dropped } = excludeDependencies(nodes, exclude)
|
|
460
|
+
if (dropped.size > 0) {
|
|
461
|
+
await keyExcludedDependencies({
|
|
462
|
+
nodes,
|
|
463
|
+
keyOnly,
|
|
464
|
+
dropped,
|
|
465
|
+
cache,
|
|
466
|
+
workspaceRoot,
|
|
467
|
+
workspaceFingerprint,
|
|
468
|
+
forwardArgs: options.forwardArgs,
|
|
469
|
+
nestedDirsByProject,
|
|
470
|
+
gitFilesCache,
|
|
471
|
+
hashCache,
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
let priorities: ReadonlyMap<string, number> = new Map()
|
|
476
|
+
if (hasHook(plugins, 'schedule')) {
|
|
477
|
+
priorities = await applyScheduleHooks(plugins, nodes, {
|
|
478
|
+
workspaceRoot,
|
|
479
|
+
cacheDir,
|
|
480
|
+
warn: (m) => log.status(m),
|
|
481
|
+
localCache,
|
|
482
|
+
})
|
|
483
|
+
}
|
|
361
484
|
|
|
362
|
-
// Empty-cases bookkeeping. We still construct the cache + fingerprint
|
|
363
|
-
// so the caller's try/finally pattern can close it uniformly.
|
|
364
|
-
if (requested.length === 0) {
|
|
365
485
|
return {
|
|
366
486
|
workspaceRoot,
|
|
367
487
|
workspaceConfig,
|
|
@@ -370,8 +490,8 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
370
490
|
cache,
|
|
371
491
|
localCache,
|
|
372
492
|
hasRemoteLayer,
|
|
373
|
-
priorities
|
|
374
|
-
nodes
|
|
493
|
+
priorities,
|
|
494
|
+
nodes,
|
|
375
495
|
unresolvedTasks,
|
|
376
496
|
projects,
|
|
377
497
|
anyProjectConfig: projectsWithConfigs.length > 0,
|
|
@@ -381,121 +501,12 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
381
501
|
gitFilesCache,
|
|
382
502
|
hashCache,
|
|
383
503
|
workspaceProjectCount: projectMetas.length,
|
|
384
|
-
empty:
|
|
385
|
-
options.selectedByDiff === true && unresolvedTasks.length === 0
|
|
386
|
-
? 'none-affected'
|
|
387
|
-
: 'no-tasks-declared',
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// The whole graph, whatever `--exclude-dependencies` says: the stages
|
|
392
|
-
// below shape and key the tasks it drops as a full run would, because
|
|
393
|
-
// a dropped task is still keyed (excluded-keys.ts). A scoped load is not
|
|
394
|
-
// the whole workspace: a `^name` nothing loaded declares may be declared
|
|
395
|
-
// by a config the scope left out, so the builder hands it back instead of
|
|
396
|
-
// refusing it, and the rest decide.
|
|
397
|
-
const unproven: Array<[taskId: string, name: string]> = []
|
|
398
|
-
const nodes = buildTaskGraph({
|
|
399
|
-
projects,
|
|
400
|
-
packageGraph,
|
|
401
|
-
requested,
|
|
402
|
-
...(projects.size < projectsWithConfigs.length
|
|
403
|
-
? { undeclaredDeps: (id: string, name: string) => void unproven.push([id, name]) }
|
|
404
|
-
: {}),
|
|
405
|
-
})
|
|
406
|
-
if (unproven.length > 0) {
|
|
407
|
-
await refuseUndeclaredDeps(unproven, () =>
|
|
408
|
-
loadProjects({ ...loadArgs, seeds: 'all', closure: false, staged: projects }),
|
|
409
|
-
)
|
|
410
|
-
}
|
|
411
|
-
// An `--exclude-dependencies` name no project declares drops nothing:
|
|
412
|
-
// `=biuld` planned the whole chain and ran it, exit 0 (item 1026). Every
|
|
413
|
-
// other name the user types must resolve; so must this one. After the
|
|
414
|
-
// graph, so a config error (`^biuld` nobody declares) is named first.
|
|
415
|
-
const excludeNames = options.excludeDependencies
|
|
416
|
-
if (Array.isArray(excludeNames) && excludeNames.length > 0) {
|
|
417
|
-
const declared = new Set<string>()
|
|
418
|
-
for (const p of projects.values())
|
|
419
|
-
for (const t of Object.keys(p.config.tasks ?? {})) declared.add(t)
|
|
420
|
-
let unknown = excludeNames.filter((n) => !declared.has(n))
|
|
421
|
-
if (unknown.length > 0 && projects.size < projectsWithConfigs.length) {
|
|
422
|
-
unknown = await declaredNowhere(unknown, () =>
|
|
423
|
-
loadProjects({ ...loadArgs, seeds: 'all', closure: false, staged: projects }),
|
|
424
|
-
)
|
|
504
|
+
empty: nodes.size === 0 ? 'empty-graph' : null,
|
|
425
505
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
throw new UserError(
|
|
431
|
-
`--exclude-dependencies names a task no project declares: ${unknown.join(', ')}.${hint}`,
|
|
432
|
-
)
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
// The graph is built; what follows is the plugins' (graph, key,
|
|
436
|
-
// schedule). Two rows, so a plugin's key stage reads as its own cost
|
|
437
|
-
// and not as graph building — a lockfile plugin's 1000 stats per run
|
|
438
|
-
// hid inside one `prepare (graph)` row until 2026-09-10.
|
|
439
|
-
mark('build graph')
|
|
440
|
-
if (hasHook(plugins, 'graph')) {
|
|
441
|
-
await applyGraphHooks(plugins, nodes, {
|
|
442
|
-
workspaceRoot,
|
|
443
|
-
cacheDir,
|
|
444
|
-
warn: (m) => log.status(m),
|
|
445
|
-
requested: [...nodes.values()].filter((n) => n.requested).map((n) => n.id),
|
|
446
|
-
})
|
|
447
|
-
}
|
|
448
|
-
if (hasHook(plugins, 'key')) {
|
|
449
|
-
await applyKeyHooks(plugins, nodes, { workspaceRoot, cacheDir, warn: (m) => log.status(m) })
|
|
450
|
-
}
|
|
451
|
-
const exclude = options.excludeDependencies
|
|
452
|
-
if (exclude !== undefined && (exclude === 'all' || exclude.length > 0)) {
|
|
453
|
-
const { keyOnly, dropped } = excludeDependencies(nodes, exclude)
|
|
454
|
-
if (dropped.size > 0) {
|
|
455
|
-
await keyExcludedDependencies({
|
|
456
|
-
nodes,
|
|
457
|
-
keyOnly,
|
|
458
|
-
dropped,
|
|
459
|
-
cache,
|
|
460
|
-
workspaceRoot,
|
|
461
|
-
workspaceFingerprint,
|
|
462
|
-
forwardArgs: options.forwardArgs,
|
|
463
|
-
nestedDirsByProject,
|
|
464
|
-
gitFilesCache,
|
|
465
|
-
hashCache,
|
|
466
|
-
})
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
let priorities: ReadonlyMap<string, number> = new Map()
|
|
470
|
-
if (hasHook(plugins, 'schedule')) {
|
|
471
|
-
priorities = await applyScheduleHooks(plugins, nodes, {
|
|
472
|
-
workspaceRoot,
|
|
473
|
-
cacheDir,
|
|
474
|
-
warn: (m) => log.status(m),
|
|
475
|
-
localCache,
|
|
476
|
-
})
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
return {
|
|
480
|
-
workspaceRoot,
|
|
481
|
-
workspaceConfig,
|
|
482
|
-
plugins,
|
|
483
|
-
cacheDir,
|
|
484
|
-
cache,
|
|
485
|
-
localCache,
|
|
486
|
-
hasRemoteLayer,
|
|
487
|
-
priorities,
|
|
488
|
-
nodes,
|
|
489
|
-
unresolvedTasks,
|
|
490
|
-
projects,
|
|
491
|
-
anyProjectConfig: projectsWithConfigs.length > 0,
|
|
492
|
-
workspaceFingerprint,
|
|
493
|
-
fingerprintWatch,
|
|
494
|
-
nestedDirsByProject,
|
|
495
|
-
gitFilesCache,
|
|
496
|
-
hashCache,
|
|
497
|
-
workspaceProjectCount: projectMetas.length,
|
|
498
|
-
empty: nodes.size === 0 ? 'empty-graph' : null,
|
|
506
|
+
} catch (err) {
|
|
507
|
+
await teardownPlugins(plugins, (m) => log.status(m))
|
|
508
|
+
cache.close()
|
|
509
|
+
throw err
|
|
499
510
|
}
|
|
500
511
|
}
|
|
501
512
|
|
package/src/workspace/filter.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// given, the base set is "all projects" and excluded packages are removed.
|
|
18
18
|
|
|
19
19
|
import path from 'node:path'
|
|
20
|
-
import { BUN_GLOB_WILDCARDS } from '../util/index.js'
|
|
20
|
+
import { BUN_GLOB_WILDCARDS, UserError } from '../util/index.js'
|
|
21
21
|
import type { PackageGraph } from './package-graph.js'
|
|
22
22
|
import type { ProjectMeta } from './workspace.js'
|
|
23
23
|
|
|
@@ -82,6 +82,12 @@ export function parseFilter(raw: string, workspaceRoot: string): ParsedFilter {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// `...`, `!`, `^...`: the operators with no project between them. An
|
|
86
|
+
// empty name glob matched nothing and was hinted "Did you mean a?", and
|
|
87
|
+
// an empty exclude (`!$UNSET`) excluded nothing, so every project ran
|
|
88
|
+
// with one warning line (item 1030).
|
|
89
|
+
if (s === '') throw new UserError(`filter "${raw}" names no project`)
|
|
90
|
+
|
|
85
91
|
let isPath = false
|
|
86
92
|
let matcher = s
|
|
87
93
|
let pathGlob: Bun.Glob | undefined
|
|
@@ -191,6 +197,12 @@ export interface ApplyFiltersOptions {
|
|
|
191
197
|
* several filters silently under-selects.
|
|
192
198
|
*/
|
|
193
199
|
onNoMatch?: (filter: ParsedFilter) => void
|
|
200
|
+
/**
|
|
201
|
+
* Called once per filter whose pattern matched projects but whose walk
|
|
202
|
+
* selected none (`...^c` for a `c` nothing depends on), with what it
|
|
203
|
+
* matched. Reported as a no-match, it read as a typo (item 1030).
|
|
204
|
+
*/
|
|
205
|
+
onEmptyWalk?: (filter: ParsedFilter, matched: readonly string[]) => void
|
|
194
206
|
}
|
|
195
207
|
|
|
196
208
|
export function applyFilters(opts: ApplyFiltersOptions): Set<string> {
|
|
@@ -216,6 +228,7 @@ export function applyFilters(opts: ApplyFiltersOptions): Set<string> {
|
|
|
216
228
|
for (const d of opts.graph.transitiveDependents(name)) expanded.add(d)
|
|
217
229
|
}
|
|
218
230
|
}
|
|
231
|
+
if (matched.length > 0 && expanded.size === 0) opts.onEmptyWalk?.(f, matched)
|
|
219
232
|
if (f.negate) excludes.push(expanded)
|
|
220
233
|
else for (const name of expanded) selected.add(name)
|
|
221
234
|
}
|