@vzn/vx-migrate 0.0.652 → 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 +42 -33
- package/package.json +2 -2
- package/src/migrate-scripts.ts +11 -5
- package/src/migrate-turbo.ts +8 -1
- package/src/migrate.ts +102 -18
- package/src/nx/index.ts +61 -11
- package/src/nx/nx-inputs.ts +48 -24
- package/src/nx/nx-map.ts +199 -14
- package/src/nx/nx-outputs.ts +6 -1
- package/src/nx/nx-upstream.ts +11 -11
- package/src/nx-cache/index.ts +42 -28
- package/src/packages.ts +51 -1
- package/src/remote-refusal.ts +49 -4
- package/src/shared-outputs.ts +64 -9
- package/src/tracked-outputs.ts +58 -19
- package/src/turbo/index.ts +2 -1
- package/src/turbo/turbo-map.ts +157 -22
- package/src/turbo-cache/index.ts +71 -40
- package/src/turbo-cache/oidc.ts +15 -7
- package/src/workspace-notes.ts +19 -8
- package/src/workspace-plugins.ts +11 -3
package/src/shared-outputs.ts
CHANGED
|
@@ -294,10 +294,11 @@ export function resolveSharedOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
|
|
|
294
294
|
}
|
|
295
295
|
dropped.add(c.index)
|
|
296
296
|
const shared = c.files.find((gb) => clash.files.some((ga) => hits(clash, ga, gb)))!
|
|
297
|
+
const theirs = clash.files.find((ga) => hits(clash, ga, shared))!
|
|
297
298
|
uncache(t.task!)
|
|
298
299
|
t.todos.push(
|
|
299
|
-
`declares the output ${
|
|
300
|
-
"
|
|
300
|
+
`declares the output ${sharing(shared, theirs, JSON.stringify(clash.name))} — ` +
|
|
301
|
+
"vx cleans a task's outputs before it runs and before a restore, so two " +
|
|
301
302
|
"cached tasks on one path would delete each other's work; this one runs uncached. " +
|
|
302
303
|
'Give it its own output path to cache it.',
|
|
303
304
|
)
|
|
@@ -306,6 +307,13 @@ export function resolveSharedOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
|
|
|
306
307
|
return tasks
|
|
307
308
|
}
|
|
308
309
|
|
|
310
|
+
/** `"<mine>" that <owner> also declares`, or both globs when they only overlap. */
|
|
311
|
+
function sharing(mine: string, theirs: string, owner: string): string {
|
|
312
|
+
return mine === theirs
|
|
313
|
+
? `${JSON.stringify(mine)} that ${owner} also declares`
|
|
314
|
+
: `${JSON.stringify(mine)}, which overlaps the output ${JSON.stringify(theirs)} that ${owner} declares`
|
|
315
|
+
}
|
|
316
|
+
|
|
309
317
|
/**
|
|
310
318
|
* Two globs can match one path only when one's literal prefix is a
|
|
311
319
|
* directory of the other's (`packages/*\/dist/**` and `packages/a/dist/**`),
|
|
@@ -397,10 +405,17 @@ function inputsOf(t: GeneratedTask): { files: string[]; workspaceFiles?: unknown
|
|
|
397
405
|
* files git tracks, and a build's outputs are not, so the adopted tool
|
|
398
406
|
* never keyed them either. A literal input another task writes cannot be
|
|
399
407
|
* taken back (core refuses a `!` over a literal input), so that task runs
|
|
400
|
-
* uncached with a todo.
|
|
401
|
-
*
|
|
408
|
+
* uncached with a todo. Under `walksGlobs` (Turbo) so does a glob that
|
|
409
|
+
* names the output's place (`gen/**` beside a `gen` writing `gen/**`)
|
|
410
|
+
* when no `dependsOn` path reaches the writer: Turbo walks an explicit
|
|
411
|
+
* glob on disk, and with no edge the writer's key does not reach the
|
|
412
|
+
* reader's either, so taken back it replayed the old output. A glob led
|
|
413
|
+
* by `**` walks the whole package, as the default does, and keeps the
|
|
414
|
+
* `!`. Nx hashes no gitignored file, and its mapper drops an input under
|
|
415
|
+
* an untracked output before this pass. A reader whose `!` hides a
|
|
416
|
+
* committed file is the tracked pass's (`spareTrackedOutputs`).
|
|
402
417
|
*/
|
|
403
|
-
export function excludeSiblingOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
|
|
418
|
+
export function excludeSiblingOutputs(tasks: GeneratedTask[], walksGlobs = false): GeneratedTask[] {
|
|
404
419
|
const writers = tasks.flatMap((t, index) => {
|
|
405
420
|
const c = cachedOutputs(t, index)
|
|
406
421
|
return c === null ? [] : [c]
|
|
@@ -427,6 +442,18 @@ export function excludeSiblingOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
|
|
|
427
442
|
)
|
|
428
443
|
return
|
|
429
444
|
}
|
|
445
|
+
const named = walksGlobs
|
|
446
|
+
? read.find((g) => !g.replace(/^(\.\/)+/, '').startsWith('**'))
|
|
447
|
+
: undefined
|
|
448
|
+
if (named !== undefined && !isKeyedGroup(t) && !reaches(tasks, index, w.index)) {
|
|
449
|
+
unkey(t.task!)
|
|
450
|
+
t.todos.push(
|
|
451
|
+
`reads ${JSON.stringify(named)}, which ${JSON.stringify(w.name)} writes and no ` +
|
|
452
|
+
'dependsOn reaches — vx keys a task only on files no other task writes, so it runs ' +
|
|
453
|
+
'uncached; add the edge or read the source instead in a vx.config to cache it',
|
|
454
|
+
)
|
|
455
|
+
return
|
|
456
|
+
}
|
|
430
457
|
inputs.files.push(`!${out}`)
|
|
431
458
|
}
|
|
432
459
|
}
|
|
@@ -434,6 +461,26 @@ export function excludeSiblingOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
|
|
|
434
461
|
return tasks
|
|
435
462
|
}
|
|
436
463
|
|
|
464
|
+
/** Does a same-project `dependsOn` path lead from task `from` to task `to`? */
|
|
465
|
+
function reaches(tasks: readonly GeneratedTask[], from: number, to: number): boolean {
|
|
466
|
+
const byName = new Map(tasks.map((t, i) => [t.name, i]))
|
|
467
|
+
const seen = new Set([from])
|
|
468
|
+
const queue = [from]
|
|
469
|
+
for (let i = queue.shift(); i !== undefined; i = queue.shift()) {
|
|
470
|
+
const deps = tasks[i]!.task?.['dependsOn']
|
|
471
|
+
if (!Array.isArray(deps)) continue
|
|
472
|
+
for (const d of deps) {
|
|
473
|
+
const next = typeof d === 'string' ? byName.get(d) : undefined
|
|
474
|
+
if (next === to) return true
|
|
475
|
+
if (next !== undefined && !seen.has(next)) {
|
|
476
|
+
seen.add(next)
|
|
477
|
+
queue.push(next)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return false
|
|
482
|
+
}
|
|
483
|
+
|
|
437
484
|
/**
|
|
438
485
|
* The same across projects, for workspace outputs: cal.com's shared
|
|
439
486
|
* `post-install` writes `../../node_modules/@prisma/client/**` from every
|
|
@@ -476,11 +523,19 @@ export function resolveSharedWorkspaceOutputs(
|
|
|
476
523
|
a.find((g) => b.some((h) => outputsOverlap(h, g)))
|
|
477
524
|
let clash: (typeof kept)[number] | undefined
|
|
478
525
|
let shared: string | undefined
|
|
526
|
+
let theirs: string | undefined
|
|
479
527
|
for (const i of candidates([...ws, ...own])) {
|
|
480
528
|
const k = kept[i]!
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
529
|
+
const pools: [string[], string[]][] = [
|
|
530
|
+
[ws, [...k.ws, ...k.own]],
|
|
531
|
+
[own, k.project === p.name ? k.ws : [...k.ws, ...k.own]],
|
|
532
|
+
]
|
|
533
|
+
for (const [mine, other] of pools) {
|
|
534
|
+
shared = overlap(mine, other)
|
|
535
|
+
if (shared === undefined) continue
|
|
536
|
+
theirs = other.find((h) => outputsOverlap(h, shared!))
|
|
537
|
+
break
|
|
538
|
+
}
|
|
484
539
|
if (shared !== undefined) {
|
|
485
540
|
clash = k
|
|
486
541
|
break
|
|
@@ -493,7 +548,7 @@ export function resolveSharedWorkspaceOutputs(
|
|
|
493
548
|
}
|
|
494
549
|
uncache(t.task!)
|
|
495
550
|
t.todos.push(
|
|
496
|
-
`declares the workspace output ${
|
|
551
|
+
`declares the workspace output ${sharing(shared!, theirs!, clash.id)} — ` +
|
|
497
552
|
"vx cleans a task's outputs before it runs and before a restore, so two cached tasks " +
|
|
498
553
|
"on one path would delete each other's work; this one runs uncached. Give it its own " +
|
|
499
554
|
'output path to cache it.',
|
package/src/tracked-outputs.ts
CHANGED
|
@@ -72,6 +72,47 @@ export async function gitIgnored(root: string, rels: readonly string[]): Promise
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* What git ignores under the fixed prefixes of the root-relative `globs`:
|
|
77
|
+
* each ignored file, and a directory ignored whole (or holding only
|
|
78
|
+
* ignored files) as one `dir/` entry (`--directory`: `dist/` is never
|
|
79
|
+
* walked). Entries under `node_modules` (the lockfile keys an install) or
|
|
80
|
+
* vx's own `.vx/` are left out. One spawn; empty outside a repo or
|
|
81
|
+
* without git.
|
|
82
|
+
*/
|
|
83
|
+
export async function gitIgnoredUnder(root: string, globs: readonly string[]): Promise<string[]> {
|
|
84
|
+
if (globs.length === 0) return []
|
|
85
|
+
const prefixes = [...new Set(globs.map((g) => literalPrefix(g) || '.'))].sort()
|
|
86
|
+
// A prefix under another adds nothing; `ls-files` takes no pathspec file.
|
|
87
|
+
const kept = prefixes.includes('.')
|
|
88
|
+
? ['.']
|
|
89
|
+
: prefixes.filter((p, i) => !prefixes.slice(0, i).some((q) => p.startsWith(`${q}/`)))
|
|
90
|
+
try {
|
|
91
|
+
const p = Bun.spawn(
|
|
92
|
+
[
|
|
93
|
+
'git',
|
|
94
|
+
'ls-files',
|
|
95
|
+
'-z',
|
|
96
|
+
'--others',
|
|
97
|
+
'--ignored',
|
|
98
|
+
'--exclude-standard',
|
|
99
|
+
'--directory',
|
|
100
|
+
'--no-empty-directory',
|
|
101
|
+
'--',
|
|
102
|
+
...kept,
|
|
103
|
+
],
|
|
104
|
+
{ cwd: root, stdout: 'pipe', stderr: 'ignore' },
|
|
105
|
+
)
|
|
106
|
+
const out = await new Response(p.stdout).text()
|
|
107
|
+
if ((await p.exited) !== 0) return []
|
|
108
|
+
return out
|
|
109
|
+
.split('\0')
|
|
110
|
+
.filter((f) => f !== '' && !f.startsWith('.vx/') && !f.split('/').includes('node_modules'))
|
|
111
|
+
} catch {
|
|
112
|
+
return []
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
75
116
|
/**
|
|
76
117
|
* What a project's tracked files are: their extensions (lower case, no
|
|
77
118
|
* dot), directory names at any depth, and top-level entry names.
|
|
@@ -165,18 +206,20 @@ function literalPrefix(glob: string): string {
|
|
|
165
206
|
return out.join('/')
|
|
166
207
|
}
|
|
167
208
|
|
|
168
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* The tracked `files` (relative to the same base as `globs`) the positive
|
|
211
|
+
* globs cover, even one the globs' own `!` takes back: a reader's take-back
|
|
212
|
+
* is of the positives alone (`excludeSiblingOutputs`), so its `!*\/**`
|
|
213
|
+
* beside a `*\/**` minus `!src/**` output still hid src.
|
|
214
|
+
*/
|
|
169
215
|
function coveredTracked(globs: readonly string[], files: readonly string[]): string[] {
|
|
170
|
-
const positive = globs.filter((g) => !g.startsWith('!'))
|
|
171
|
-
const taken = globs.filter((g) => g.startsWith('!')).map((g) => g.slice(1))
|
|
172
216
|
const hit = new Set<string>()
|
|
173
|
-
for (const g of
|
|
217
|
+
for (const g of globs) {
|
|
218
|
+
if (g.startsWith('!')) continue
|
|
174
219
|
const prefix = literalPrefix(g)
|
|
175
220
|
for (const f of files) {
|
|
176
221
|
if (prefix !== '' && f !== prefix && !f.startsWith(`${prefix}/`)) continue
|
|
177
|
-
if (
|
|
178
|
-
if (taken.some((t) => outputsOverlap(t, f))) continue
|
|
179
|
-
hit.add(f)
|
|
222
|
+
if (outputsOverlap(g, f)) hit.add(f)
|
|
180
223
|
}
|
|
181
224
|
}
|
|
182
225
|
return [...hit].sort()
|
|
@@ -275,20 +318,16 @@ export function spareTrackedOutputs(
|
|
|
275
318
|
] as const) {
|
|
276
319
|
const globs = outputs[key]
|
|
277
320
|
if (!Array.isArray(globs) || globs.length === 0) continue
|
|
278
|
-
|
|
321
|
+
const strings = globs.filter((g): g is string => typeof g === 'string')
|
|
279
322
|
// An output naming one committed file (eslint's
|
|
280
|
-
// `eslint-suppressions.json
|
|
281
|
-
//
|
|
282
|
-
// uncached (hono).
|
|
323
|
+
// `eslint-suppressions.json`, a codegen's `gen.ts`) is also a
|
|
324
|
+
// source: the `!` a reader's inputs got for it goes, or every
|
|
325
|
+
// reader ran uncached (hono). The entry stays: dropped, a task
|
|
326
|
+
// that rewrites the file never saved.
|
|
283
327
|
const rootedOf = (g: string) => (key === 'files' && rel !== '' ? `${rel}/${g}` : g)
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (sources.size > 0) {
|
|
288
|
-
strings = strings.filter((g) => !sources.has(g.startsWith('!') ? g.slice(1) : g))
|
|
289
|
-
outputs[key] = strings
|
|
290
|
-
for (const g of sources) unhide(key === 'files' ? p : null, rootedOf(g))
|
|
291
|
-
}
|
|
328
|
+
for (const g of strings)
|
|
329
|
+
if (isLiteralPattern(g) && trackedSet.has(rootedOf(g)))
|
|
330
|
+
unhide(key === 'files' ? p : null, rootedOf(g))
|
|
292
331
|
const hit = coveredTracked(strings, base)
|
|
293
332
|
if (hit.length > 0) hits.push([key, hit])
|
|
294
333
|
}
|
package/src/turbo/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { refuseUnknownOptions, UserError, type PluginOptionKinds } from '@vzn/vx
|
|
|
18
18
|
import { type AdoptionRun, adoptionPlugin } from '../adoption-plugin.js'
|
|
19
19
|
import type { AdoptionMapping } from '../mapping-cache.js'
|
|
20
20
|
import { collectGaps } from '../plugin-gaps.js'
|
|
21
|
-
import { gitIgnored, trackedKinds } from '../tracked-outputs.js'
|
|
21
|
+
import { gitIgnored, gitIgnoredUnder, trackedKinds } from '../tracked-outputs.js'
|
|
22
22
|
import { packageScripts, relPosix } from '../paths.js'
|
|
23
23
|
import { onceReader, type ReadText, tscBuildInfo } from '../tsc-buildinfo.js'
|
|
24
24
|
import type * as TurboMap from './turbo-map.js'
|
|
@@ -317,6 +317,7 @@ async function mapAll(
|
|
|
317
317
|
...(tracked === null ? {} : { tracked: trackedKinds(tracked) }),
|
|
318
318
|
ownConfig: (rel) => configs.get(rel === '.' ? '' : rel) ?? null,
|
|
319
319
|
ignored: (rels) => gitIgnored(root, rels),
|
|
320
|
+
ignoredUnder: (globs) => gitIgnoredUnder(root, globs),
|
|
320
321
|
buildInfo: (rel, command) => tscBuildInfo(read, path.join(root, rel), command),
|
|
321
322
|
})
|
|
322
323
|
const byName = new Map<string, TurboMappedProject>()
|
package/src/turbo/turbo-map.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// The consumer decides what a global becomes through `splice`.
|
|
12
12
|
|
|
13
13
|
import path from 'node:path'
|
|
14
|
-
import { isLiteralPattern, pruneOrphanPersistentNotes, UserError } from '@vzn/vx'
|
|
14
|
+
import { isLiteralPattern, outputsOverlap, pruneOrphanPersistentNotes, UserError } from '@vzn/vx'
|
|
15
15
|
import type { PackageMeta } from '../packages.js'
|
|
16
16
|
import { refuseTaskNames } from '../plugin-gaps.js'
|
|
17
17
|
import { apartTakeBacks, literalGlob, minimatchToVx, withoutTakenBack } from '../glob-grammar.js'
|
|
@@ -280,6 +280,12 @@ export interface MapTurboOptions {
|
|
|
280
280
|
* path among them is keyed by a probe. Absent, none is.
|
|
281
281
|
*/
|
|
282
282
|
ignored?: (rels: readonly string[]) => Promise<ReadonlySet<string>>
|
|
283
|
+
/**
|
|
284
|
+
* What git ignores under the root-relative globs' fixed prefixes
|
|
285
|
+
* (`gitIgnoredUnder`): files, and `dir/` for a directory ignored whole.
|
|
286
|
+
* Absent, an explicit glob keys none of them.
|
|
287
|
+
*/
|
|
288
|
+
ignoredUnder?: (globs: readonly string[]) => Promise<readonly string[]>
|
|
283
289
|
/**
|
|
284
290
|
* The `.tsbuildinfo` files a root-relative package's command writes,
|
|
285
291
|
* relative to the package (`tscBuildInfo`). Absent, none.
|
|
@@ -295,6 +301,12 @@ export interface MapTurboOptions {
|
|
|
295
301
|
interface Literals {
|
|
296
302
|
readonly inputs: Record<string, unknown>
|
|
297
303
|
readonly rels: ReadonlyMap<string, Named>
|
|
304
|
+
/** The explicit globs, root-relative, with the list that holds them. */
|
|
305
|
+
readonly globs: ReadonlyMap<string, 'files' | 'workspaceFiles'>
|
|
306
|
+
/** The global `!` entries, which a preset spread may hide from the lists. */
|
|
307
|
+
readonly taken: readonly string[]
|
|
308
|
+
/** The package, root-relative (`files` entries are relative to it). */
|
|
309
|
+
readonly pkgDir: string
|
|
298
310
|
}
|
|
299
311
|
type Named = readonly ['files' | 'workspaceFiles' | null, string]
|
|
300
312
|
|
|
@@ -313,6 +325,18 @@ function zeroSegments(glob: string): string | null {
|
|
|
313
325
|
return head === '' || isLiteralPattern(head.replace(/\\./g, '_')) ? null : head
|
|
314
326
|
}
|
|
315
327
|
|
|
328
|
+
/**
|
|
329
|
+
* A glob with its `.` and `..` segments collapsed, as Turbo cleans the path
|
|
330
|
+
* before it walks (turborepo-globwalk's `collapse_path`): `src/../../shared/**`
|
|
331
|
+
* is `../shared/**`, `./src/**` is `src/**`. A `$TURBO_ROOT$/` prefix is
|
|
332
|
+
* the root, not a segment.
|
|
333
|
+
*/
|
|
334
|
+
function collapseDots(glob: string): string {
|
|
335
|
+
if (glob.startsWith('$TURBO_ROOT$/'))
|
|
336
|
+
return '$TURBO_ROOT$/' + collapseDots(glob.slice('$TURBO_ROOT$/'.length))
|
|
337
|
+
return path.posix.normalize(glob)
|
|
338
|
+
}
|
|
339
|
+
|
|
316
340
|
/** A glob, `!` or not, and the X its `X/**` also matches in Turbo (`zeroSegments`). */
|
|
317
341
|
function withZeroSegments(glob: string): string[] {
|
|
318
342
|
const neg = glob.startsWith('!') ? '!' : ''
|
|
@@ -363,6 +387,70 @@ function keyIgnored(
|
|
|
363
387
|
inputs.workspaceRuntime = [...probes, ignoredFilesProbe(hits.map(([rel]) => rel))]
|
|
364
388
|
}
|
|
365
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Turbo walks an explicit input glob on disk, gitignored files and all
|
|
392
|
+
* (`$TURBO_DEFAULT$` alone skips them); core's globs filter what git
|
|
393
|
+
* lists, so `src/**` over an ignored `src/gen.ts` keyed nothing and an
|
|
394
|
+
* edit replayed the old output. Each ignored path a glob matches is named
|
|
395
|
+
* outright, which core reads from disk: a file as itself, a directory
|
|
396
|
+
* ignored whole as the glob's rest under it (`src/out/**`). A file a `!`
|
|
397
|
+
* takes back stays out (core refuses a literal one covers).
|
|
398
|
+
*/
|
|
399
|
+
function keyIgnoredGlobbed(
|
|
400
|
+
l: Literals,
|
|
401
|
+
under: readonly string[],
|
|
402
|
+
projectDirs: readonly string[],
|
|
403
|
+
): void {
|
|
404
|
+
// The lists as they stand: a sibling's output taken back since counts.
|
|
405
|
+
const current = (['files', 'workspaceFiles'] as const).flatMap((kind) =>
|
|
406
|
+
((l.inputs[kind] as unknown[] | undefined) ?? []).flatMap((e) =>
|
|
407
|
+
typeof e === 'string' && e.startsWith('!')
|
|
408
|
+
? [kind === 'files' && l.pkgDir !== '' ? `${l.pkgDir}/${e.slice(1)}` : e.slice(1)]
|
|
409
|
+
: [],
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
const taken = [...l.taken, ...current].map((t) => new Bun.Glob(t))
|
|
413
|
+
for (const [glob, kind] of l.globs) {
|
|
414
|
+
const matcher = new Bun.Glob(glob)
|
|
415
|
+
const at = kind === 'files' && l.pkgDir !== '' ? `${l.pkgDir}/` : ''
|
|
416
|
+
for (const e of under) {
|
|
417
|
+
if (!e.startsWith(at)) continue
|
|
418
|
+
// A package nested in this one is another project, which core's
|
|
419
|
+
// globs stop at: its path is named from the workspace root.
|
|
420
|
+
const nested = at !== '' && projectDirs.some((d) => d.startsWith(at) && e.startsWith(`${d}/`))
|
|
421
|
+
const base = nested ? '' : at
|
|
422
|
+
const entry = e.endsWith('/')
|
|
423
|
+
? globUnder(glob.slice(at.length), e.slice(at.length, -1))
|
|
424
|
+
: matcher.match(e) && !taken.some((t) => t.match(e))
|
|
425
|
+
? literalGlob(e.slice(base.length))
|
|
426
|
+
: null
|
|
427
|
+
if (entry === null) continue
|
|
428
|
+
const into = nested ? 'workspaceFiles' : kind
|
|
429
|
+
const named = nested && e.endsWith('/') ? `${l.pkgDir}/${entry}` : entry
|
|
430
|
+
const list = (l.inputs[into] as unknown[] | undefined) ?? []
|
|
431
|
+
if (!list.includes(named)) l.inputs[into] = [...list, named]
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* `glob`'s rest below `dir`, which it reaches into (`a/**\/*.ts` under
|
|
438
|
+
* `a/gen`: `a/gen/**\/*.ts`), else null: the paths of the glob under it.
|
|
439
|
+
*/
|
|
440
|
+
function globUnder(glob: string, dir: string): string | null {
|
|
441
|
+
// A brace holding a `/` spans segments; such a glob is not split here.
|
|
442
|
+
if (/\{[^}]*\//.test(glob)) return null
|
|
443
|
+
const gs = glob.split('/')
|
|
444
|
+
const ds = dir.split('/')
|
|
445
|
+
for (let i = 0; i < ds.length; i++) {
|
|
446
|
+
const g = gs[i]
|
|
447
|
+
if (g === undefined) return null
|
|
448
|
+
if (g === '**') return [...ds.map(literalGlob), ...gs.slice(i)].join('/')
|
|
449
|
+
if (!new Bun.Glob(g).match(ds[i]!)) return null
|
|
450
|
+
}
|
|
451
|
+
return gs.length > ds.length ? [...ds.map(literalGlob), ...gs.slice(ds.length)].join('/') : null
|
|
452
|
+
}
|
|
453
|
+
|
|
366
454
|
async function readTurboJson(file: string, root: string): Promise<TurboJson> {
|
|
367
455
|
const text = await Bun.file(file).text()
|
|
368
456
|
let parsed: unknown
|
|
@@ -1174,6 +1262,8 @@ export async function mapTurboWorkspace(
|
|
|
1174
1262
|
// whose own edges lack `^name` (opencode's `build: { dependsOn: [] }`)
|
|
1175
1263
|
// is key-only too: Turbo's `^build` stops at it, and core's walked past
|
|
1176
1264
|
// it to the builds below, running and keying ones Turbo never waits on.
|
|
1265
|
+
// `cache: false` does not stop it: a key-only node stores nothing, and
|
|
1266
|
+
// Turbo's no-op hashes the package's files cached or not.
|
|
1177
1267
|
const keyOnly = new Map<string, Set<string>>()
|
|
1178
1268
|
for (const meta of metas) {
|
|
1179
1269
|
const scripts = packageScripts(meta)
|
|
@@ -1185,7 +1275,7 @@ export async function mapTurboWorkspace(
|
|
|
1185
1275
|
if (!(caretSelf.has(name) || stops) || !withScript.has(name) || transit.has(name)) continue
|
|
1186
1276
|
if (scripts[name] !== undefined || commandOverride(def) !== undefined) continue
|
|
1187
1277
|
if (sidecarGroups.has(`${meta.name}#${name}`)) continue
|
|
1188
|
-
if (def?.
|
|
1278
|
+
if (def?.persistent === true) continue
|
|
1189
1279
|
emitted.get(meta.name)!.add(name)
|
|
1190
1280
|
runnable.get(meta.name)!.add(name)
|
|
1191
1281
|
let set = keyOnly.get(meta.name)
|
|
@@ -1315,8 +1405,12 @@ export async function mapTurboWorkspace(
|
|
|
1315
1405
|
// Core gives a project with no `build` this very node (a group behind
|
|
1316
1406
|
// `^build`, keyed on the project's files), so writing it is noise:
|
|
1317
1407
|
// solid's three script-less packages each got a key-only `build`.
|
|
1408
|
+
// Written configs only: a bare `build` skips core's node (X-102), so
|
|
1409
|
+
// under turbo(), which writes nothing, `vx run build` scoped to such a
|
|
1410
|
+
// package was refused where Turbo runs its dependencies' builds.
|
|
1318
1411
|
const noopEdges = defFor(name)!.dependsOn ?? []
|
|
1319
1412
|
if (
|
|
1413
|
+
opts.envNames === undefined &&
|
|
1320
1414
|
noop &&
|
|
1321
1415
|
name === 'build' &&
|
|
1322
1416
|
noopEdges.includes('^build') &&
|
|
@@ -1445,7 +1539,7 @@ export async function mapTurboWorkspace(
|
|
|
1445
1539
|
npmScriptEnv(mapped.task, name, command === script, meta, opts)
|
|
1446
1540
|
tasks.push(mapped)
|
|
1447
1541
|
}
|
|
1448
|
-
excludeSiblingOutputs(resolveSharedOutputs(tasks))
|
|
1542
|
+
excludeSiblingOutputs(resolveSharedOutputs(tasks), true)
|
|
1449
1543
|
const tags = packageTags(meta === rootMeta ? rootCfg : files.get(meta.name))
|
|
1450
1544
|
projects.push({ name: meta.name, dir: meta.dir, ...(tags.length > 0 ? { tags } : {}), tasks })
|
|
1451
1545
|
}
|
|
@@ -1512,7 +1606,7 @@ export async function mapTurboWorkspace(
|
|
|
1512
1606
|
if (pm === undefined) continue
|
|
1513
1607
|
const pdef = definitions(pm).defFor(task)
|
|
1514
1608
|
if (pdef?.cache !== false) continue
|
|
1515
|
-
|
|
1609
|
+
unfold.add(`${pm.name}#${task}`)
|
|
1516
1610
|
const at = relPosix(root, pm.dir)
|
|
1517
1611
|
for (const g of dep.globs.length > 0 ? dep.globs : (pdef.outputs ?? []))
|
|
1518
1612
|
if (!g.startsWith('!')) rels.push(at === '' ? g : `${at}/${g}`)
|
|
@@ -1532,11 +1626,15 @@ export async function mapTurboWorkspace(
|
|
|
1532
1626
|
inputs['workspaceRuntime'] = [...probes, probe]
|
|
1533
1627
|
// Folded, the producer's key "covers" what it writes, and core answers
|
|
1534
1628
|
// the probe before it runs: the previous run's bytes. Left out of the
|
|
1535
|
-
// fold, the probe waits for it (core's late probes, X-34).
|
|
1629
|
+
// fold, the probe waits for it (core's late probes, X-34). A `^gen`
|
|
1630
|
+
// edge reaches it too, beside its cached siblings: those stay folded.
|
|
1536
1631
|
const deps = (t.task!['dependsOn'] as string[] | undefined) ?? []
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
1632
|
+
const ids = (d: string): string[] =>
|
|
1633
|
+
d.startsWith('^')
|
|
1634
|
+
? metas.filter((m) => declares(meta, m.name)).map((m) => `${m.name}#${d.slice(1)}`)
|
|
1635
|
+
: [d.includes('#') ? d : `${meta.name}#${d}`]
|
|
1636
|
+
if (deps.some((d) => ids(d).some((i) => unfold.has(i))))
|
|
1637
|
+
inputs['tasks'] = [...deps, ...[...unfold].map((i) => `!${i}`)]
|
|
1540
1638
|
}
|
|
1541
1639
|
}
|
|
1542
1640
|
|
|
@@ -1575,6 +1673,14 @@ export async function mapTurboWorkspace(
|
|
|
1575
1673
|
for (const e of opts.splice('inputs', globals.inputs))
|
|
1576
1674
|
if (typeof e !== 'string') opaque.set(JSON.stringify(e), globals.inputs)
|
|
1577
1675
|
excludeWorkspaceOutputs(root, projects, (e) => opaque.get(JSON.stringify(e)) ?? [])
|
|
1676
|
+
// After every take-back: a path one takes back is not named back.
|
|
1677
|
+
if (opts.ignoredUnder !== undefined) {
|
|
1678
|
+
const under = await opts.ignoredUnder([
|
|
1679
|
+
...new Set(literals.tasks.flatMap((l) => [...l.globs.keys()])),
|
|
1680
|
+
])
|
|
1681
|
+
const dirs = projects.map((p) => relPosix(root, p.dir)).filter((d) => d !== '')
|
|
1682
|
+
if (under.length > 0) for (const l of literals.tasks) keyIgnoredGlobbed(l, under, dirs)
|
|
1683
|
+
}
|
|
1578
1684
|
pruneUnreachedPersistentNotes(projects, metas, opts.persistentTodo)
|
|
1579
1685
|
pruneOrphanPersistentNotes(projects, opts.persistentTodo)
|
|
1580
1686
|
refuseTaskNames(projects, 'task')
|
|
@@ -2082,12 +2188,13 @@ function buildTask(
|
|
|
2082
2188
|
// build (item 1031). A positive glob with no safe
|
|
2083
2189
|
// form widens to every package file; a negation with none is dropped
|
|
2084
2190
|
// (it would exclude fewer files, never more).
|
|
2085
|
-
const
|
|
2086
|
-
if (
|
|
2191
|
+
const vx = minimatchToVx(neg ? i.slice(1) : i, neg)
|
|
2192
|
+
if (vx === null) {
|
|
2087
2193
|
todos.push(`input ${JSON.stringify(i)}: glob syntax vx cannot take — map manually`)
|
|
2088
2194
|
if (!neg) files.push('**/*')
|
|
2089
2195
|
continue
|
|
2090
2196
|
}
|
|
2197
|
+
const wax = collapseDots(vx)
|
|
2091
2198
|
const translated = (neg ? '!' : '') + wax
|
|
2092
2199
|
const body = wax
|
|
2093
2200
|
const up = climbed(translated)
|
|
@@ -2172,8 +2279,17 @@ function buildTask(
|
|
|
2172
2279
|
if (keyEnv.length > 0) inputs.env = keyEnv
|
|
2173
2280
|
if (pkgDotenv) inputs.runtime = [pkgDotenvDeep ? DOTENV_PROBE : DOTENV_PROBE_TOP]
|
|
2174
2281
|
if (wsDotenv.length > 0) inputs.workspaceRuntime = [dotenvGlobsProbe(wsDotenv) ?? DOTENV_PROBE]
|
|
2175
|
-
|
|
2176
|
-
|
|
2282
|
+
const globs = new Map<string, 'files' | 'workspaceFiles'>()
|
|
2283
|
+
for (const [rel, [kind]] of rels)
|
|
2284
|
+
if (!isLiteral(rel)) {
|
|
2285
|
+
rels.delete(rel)
|
|
2286
|
+
if (kind !== null) globs.set(rel, kind)
|
|
2287
|
+
}
|
|
2288
|
+
const taken: string[] = []
|
|
2289
|
+
for (const g of globalKept)
|
|
2290
|
+
if (g.startsWith('!')) taken.push(g.slice(1))
|
|
2291
|
+
else if (!isLiteral(g) && !globs.has(g)) globs.set(g, 'workspaceFiles')
|
|
2292
|
+
literals.tasks.push({ inputs, rels, globs, taken, pkgDir: atRoot ? '' : pkgDir })
|
|
2177
2293
|
// Turbo keys every task on its inputs, cached or not, and a dependant
|
|
2178
2294
|
// folds that key: an uncached task's inputs still move its dependants.
|
|
2179
2295
|
task.inputs = inputs
|
|
@@ -2182,6 +2298,8 @@ function buildTask(
|
|
|
2182
2298
|
const outFiles: string[] = []
|
|
2183
2299
|
const zeroed = new Set<string>()
|
|
2184
2300
|
const wsOutFiles: string[] = []
|
|
2301
|
+
// The user's `!` outputs, not the take-backs added below.
|
|
2302
|
+
const ownNegated = new Set<string>()
|
|
2185
2303
|
for (const raw of def.outputs ?? []) {
|
|
2186
2304
|
// Turbo's grammar: an extglob in `dist/**/*.@(js|mjs)` matched
|
|
2187
2305
|
// nothing, so a hit restored nothing (item 1031).
|
|
@@ -2197,12 +2315,13 @@ function buildTask(
|
|
|
2197
2315
|
// `.next/**` minus `!.next/cache/**` saved the cache and cleaned it
|
|
2198
2316
|
// before every run while vx outputs could not exclude.
|
|
2199
2317
|
const neg = mapped.startsWith('!') ? '!' : ''
|
|
2200
|
-
const o = mapped.slice(neg.length)
|
|
2318
|
+
const o = collapseDots(mapped.slice(neg.length))
|
|
2201
2319
|
const signed = (g: string) => {
|
|
2202
2320
|
const head = zeroSegments(g)
|
|
2203
|
-
|
|
2204
|
-
zeroed.add(neg + head)
|
|
2205
|
-
|
|
2321
|
+
const out = head === null ? [neg + g] : [neg + g, neg + head]
|
|
2322
|
+
if (head !== null) zeroed.add(neg + head)
|
|
2323
|
+
if (neg) for (const e of out) ownNegated.add(e)
|
|
2324
|
+
return out
|
|
2206
2325
|
}
|
|
2207
2326
|
if (o.startsWith('$TURBO_ROOT$/')) {
|
|
2208
2327
|
wsOutFiles.push(...signed(o.slice('$TURBO_ROOT$/'.length)))
|
|
@@ -2253,13 +2372,29 @@ function buildTask(
|
|
|
2253
2372
|
const ws = takingBack(wsOutFiles).sort(negationsLast)
|
|
2254
2373
|
if (ws.length > 0) outputs.workspaceFiles = ws
|
|
2255
2374
|
task.outputs = outputs
|
|
2256
|
-
task
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2375
|
+
// A path a `!` output takes back is still written by the task, often
|
|
2376
|
+
// not gitignored (`dist/skip`): as its own input it changed after the
|
|
2377
|
+
// key was taken, so no run was saved, and no dependant's either. One
|
|
2378
|
+
// over a committed file is source (`*/**` minus `!src/**`, medusa's):
|
|
2379
|
+
// Turbo keys it, and taken back an edit there replayed the old build.
|
|
2380
|
+
// Without git's list, no `!` is known to hold only what the task writes.
|
|
2381
|
+
const subtract = (field: 'files' | 'workspaceFiles', from: readonly string[]) => {
|
|
2382
|
+
const list = inputs[field] as unknown[] | undefined
|
|
2383
|
+
const tracked = opts.tracked?.(field === 'files' ? pkgDir : '').files
|
|
2384
|
+
if (tracked === undefined) return
|
|
2385
|
+
const negs = from.filter(
|
|
2386
|
+
(g) =>
|
|
2387
|
+
ownNegated.has(g) &&
|
|
2388
|
+
!list?.includes(g) &&
|
|
2389
|
+
!tracked.some((f) => outputsOverlap(g.slice(1), f)),
|
|
2390
|
+
)
|
|
2391
|
+
if (list !== undefined && negs.length > 0) inputs[field] = withoutTakenBack([...list, ...negs])
|
|
2262
2392
|
}
|
|
2393
|
+
subtract('files', outputs.files as string[])
|
|
2394
|
+
subtract('workspaceFiles', ws)
|
|
2395
|
+
task.cache = true
|
|
2396
|
+
// In place: the ignored-path passes hold this object.
|
|
2397
|
+
if (rewritesManifest) inputs.stability = { strategy: 'mutable' }
|
|
2263
2398
|
|
|
2264
2399
|
return { name, todos, task, uses }
|
|
2265
2400
|
}
|