@vzn/vx-migrate 0.0.650 → 0.0.652
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 +30 -25
- package/package.json +2 -2
- package/src/adopt.ts +6 -4
- package/src/bin.ts +6 -6
- package/src/migrate-scripts.ts +2 -2
- package/src/migrate-turbo.ts +11 -4
- package/src/migrate.ts +4 -3
- package/src/nx/nx-map.ts +22 -7
- package/src/package-fs.ts +18 -2
- package/src/remote-refusal.ts +6 -1
- package/src/shared-outputs.ts +22 -16
- package/src/tracked-outputs.ts +1 -1
- package/src/turbo/index.ts +47 -0
- package/src/turbo/root-engines.ts +19 -0
- package/src/turbo/turbo-map.ts +210 -29
- package/src/turbo/turbo-reads.ts +95 -4
- package/src/turbo-cache/index.ts +38 -11
- package/src/vite-task/vite-task-map.ts +3 -2
package/src/shared-outputs.ts
CHANGED
|
@@ -179,8 +179,8 @@ interface Cached {
|
|
|
179
179
|
function cachedOutputs(t: GeneratedTask, index: number): Cached | null {
|
|
180
180
|
const task = t.task
|
|
181
181
|
if (task === null) return null
|
|
182
|
-
const
|
|
183
|
-
const files =
|
|
182
|
+
const outputs = task['outputs'] as { files?: unknown } | undefined
|
|
183
|
+
const files = outputs?.files
|
|
184
184
|
if (!Array.isArray(files)) return null
|
|
185
185
|
// Where a task's outputs might land is its positive globs, as core reads
|
|
186
186
|
// them: a `!` compiled as a glob is true of every other path.
|
|
@@ -267,9 +267,7 @@ export function resolveSharedOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
|
|
|
267
267
|
for (const { k, gb } of pairs) {
|
|
268
268
|
if (k.back.has(gb)) continue
|
|
269
269
|
k.back.add(gb)
|
|
270
|
-
;(tasks[k.index]!.task!['
|
|
271
|
-
`!${gb}`,
|
|
272
|
-
)
|
|
270
|
+
;(tasks[k.index]!.task!['outputs'] as { files: string[] }).files.push(`!${gb}`)
|
|
273
271
|
}
|
|
274
272
|
kept.push(c)
|
|
275
273
|
continue
|
|
@@ -289,14 +287,14 @@ export function resolveSharedOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
|
|
|
289
287
|
sharedFiles.every((g) => isLiteralPattern(g) && TSBUILDINFO.test(g))
|
|
290
288
|
) {
|
|
291
289
|
c.files = c.files.filter((g) => !sharedFiles.includes(g))
|
|
292
|
-
const outputs =
|
|
290
|
+
const outputs = t.task!['outputs'] as { files: string[] }
|
|
293
291
|
outputs.files = outputs.files.filter((g) => !sharedFiles.includes(g))
|
|
294
292
|
kept.push(c)
|
|
295
293
|
continue
|
|
296
294
|
}
|
|
297
295
|
dropped.add(c.index)
|
|
298
296
|
const shared = c.files.find((gb) => clash.files.some((ga) => hits(clash, ga, gb)))!
|
|
299
|
-
|
|
297
|
+
uncache(t.task!)
|
|
300
298
|
t.todos.push(
|
|
301
299
|
`declares the output ${JSON.stringify(shared)} that ${JSON.stringify(clash.name)} also ` +
|
|
302
300
|
"declares — vx cleans a task's outputs before it runs and before a restore, so two " +
|
|
@@ -369,10 +367,18 @@ export const isKeyedGroup = (t: { readonly task: Record<string, unknown> | null
|
|
|
369
367
|
* alone, its incomplete key still let a dependant hit stale.
|
|
370
368
|
*/
|
|
371
369
|
export function unkey(task: Record<string, unknown>): void {
|
|
372
|
-
|
|
370
|
+
uncache(task)
|
|
373
371
|
delete task['inputs']
|
|
374
372
|
}
|
|
375
373
|
|
|
374
|
+
/** No cache: `outputs` and `inputs.stability` go with it, as they mean nothing without. */
|
|
375
|
+
export function uncache(task: Record<string, unknown>): void {
|
|
376
|
+
delete task['cache']
|
|
377
|
+
delete task['outputs']
|
|
378
|
+
const inputs = task['inputs'] as Record<string, unknown> | undefined
|
|
379
|
+
if (inputs !== undefined) delete inputs['stability']
|
|
380
|
+
}
|
|
381
|
+
|
|
376
382
|
/** A task's `inputs` block, when it has one with a `files` list. */
|
|
377
383
|
function inputsOf(t: GeneratedTask): { files: string[]; workspaceFiles?: unknown } | undefined {
|
|
378
384
|
const inputs = t.task?.['inputs'] as { files?: unknown } | undefined
|
|
@@ -457,11 +463,11 @@ export function resolveSharedWorkspaceOutputs(
|
|
|
457
463
|
for (const p of projects) {
|
|
458
464
|
const rel = relPosix(root, p.dir)
|
|
459
465
|
for (const t of p.tasks) {
|
|
460
|
-
const
|
|
461
|
-
| {
|
|
466
|
+
const outputs = t.task?.['outputs'] as
|
|
467
|
+
| { files?: unknown; workspaceFiles?: unknown }
|
|
462
468
|
| undefined
|
|
463
|
-
const ws = positive(
|
|
464
|
-
const own = positive(
|
|
469
|
+
const ws = positive(outputs?.workspaceFiles)
|
|
470
|
+
const own = positive(outputs?.files).map((g) =>
|
|
465
471
|
rel === '' ? g.replace(/^(\.\/)+/, '') : `${rel}/${g.replace(/^(\.\/)+/, '')}`,
|
|
466
472
|
)
|
|
467
473
|
if (ws.length === 0 && own.length === 0) continue
|
|
@@ -485,7 +491,7 @@ export function resolveSharedWorkspaceOutputs(
|
|
|
485
491
|
kept.push({ id: `${p.name}#${t.name}`, project: p.name, ws, own })
|
|
486
492
|
continue
|
|
487
493
|
}
|
|
488
|
-
|
|
494
|
+
uncache(t.task!)
|
|
489
495
|
t.todos.push(
|
|
490
496
|
`declares the workspace output ${JSON.stringify(shared)} that ${clash.id} also declares — ` +
|
|
491
497
|
"vx cleans a task's outputs before it runs and before a restore, so two cached tasks " +
|
|
@@ -525,9 +531,9 @@ export function excludeWorkspaceOutputs(
|
|
|
525
531
|
for (const p of projects) {
|
|
526
532
|
const rel = relPosix(root, p.dir)
|
|
527
533
|
for (const t of p.tasks) {
|
|
528
|
-
const outputs =
|
|
529
|
-
|
|
530
|
-
|
|
534
|
+
const outputs = t.task?.['outputs'] as
|
|
535
|
+
| { files?: unknown; workspaceFiles?: unknown }
|
|
536
|
+
| undefined
|
|
531
537
|
const globs = [
|
|
532
538
|
...positive(outputs?.workspaceFiles),
|
|
533
539
|
...positive(outputs?.files).map((g) =>
|
package/src/tracked-outputs.ts
CHANGED
|
@@ -266,7 +266,7 @@ export function spareTrackedOutputs(
|
|
|
266
266
|
const rel = relPosix(root, p.dir)
|
|
267
267
|
const own = rel === '' ? tracked : filesUnder(tracked, rel).map((f) => f.slice(rel.length + 1))
|
|
268
268
|
for (const t of p.tasks) {
|
|
269
|
-
const outputs =
|
|
269
|
+
const outputs = t.task?.['outputs'] as Outputs | undefined
|
|
270
270
|
if (outputs === undefined) continue
|
|
271
271
|
const hits: [key: 'files' | 'workspaceFiles', hit: string[]][] = []
|
|
272
272
|
for (const [key, base] of [
|
package/src/turbo/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { onceReader, type ReadText, tscBuildInfo } from '../tsc-buildinfo.js'
|
|
|
24
24
|
import type * as TurboMap from './turbo-map.js'
|
|
25
25
|
import type { TurboMappedProject } from './turbo-map.js'
|
|
26
26
|
import {
|
|
27
|
+
ENV_CONDITION_KEYS,
|
|
27
28
|
envNamesThatMap,
|
|
28
29
|
microfrontendsConfigs,
|
|
29
30
|
rootTaskProject,
|
|
@@ -71,12 +72,24 @@ export function turbo(options: TurboPluginOptions = {}): VxPlugin {
|
|
|
71
72
|
const keys = await workspaceKeys(rootOf(ctx.workspaceRoot))
|
|
72
73
|
if (workspace.run?.concurrency === undefined && keys.concurrency !== undefined)
|
|
73
74
|
workspace.run = { ...workspace.run, concurrency: keys.concurrency }
|
|
75
|
+
// `TURBO_FORCE` (`true` or `1`, as Turbo reads it) is `turbo run --force`.
|
|
76
|
+
const force = Bun.env['TURBO_FORCE']?.trim()
|
|
77
|
+
if (force === 'true' || force === '1')
|
|
78
|
+
workspace.cache = { ...workspace.cache, policy: forcedPolicy(workspace.cache?.policy) }
|
|
74
79
|
// `turbo run --affected` compares with TURBO_SCM_BASE when set, and
|
|
75
80
|
// on GitHub Actions with what the event names.
|
|
76
81
|
const base =
|
|
77
82
|
Bun.env['TURBO_SCM_BASE']?.trim() ||
|
|
78
83
|
githubActionsBase(rootOf(ctx.workspaceRoot), keys.remoteBaseFallback === true)
|
|
79
84
|
if (base) ctx.setBranch({ base })
|
|
85
|
+
// ...up to TURBO_SCM_HEAD. vx's head is the checkout (HEAD and its
|
|
86
|
+
// working tree): a head at HEAD's commit is that, any other is said.
|
|
87
|
+
const head = Bun.env['TURBO_SCM_HEAD']?.trim()
|
|
88
|
+
if (head && !atHead(rootOf(ctx.workspaceRoot), head))
|
|
89
|
+
ctx.warn(
|
|
90
|
+
`TURBO_SCM_HEAD (${head}) is not the checked-out commit: --affected compares ` +
|
|
91
|
+
`the base with the checkout, so check out ${head} to compare with it.`,
|
|
92
|
+
)
|
|
80
93
|
},
|
|
81
94
|
// Turbo's packages are the package manager's members, each a project
|
|
82
95
|
// with no vx.config written. `//#task` keys run on the root, as Turbo
|
|
@@ -99,6 +112,18 @@ export function turbo(options: TurboPluginOptions = {}): VxPlugin {
|
|
|
99
112
|
|
|
100
113
|
const UNKNOWN_SHA = '0'.repeat(40)
|
|
101
114
|
|
|
115
|
+
/** Whether `ref` names HEAD's commit. */
|
|
116
|
+
function atHead(root: string, ref: string): boolean {
|
|
117
|
+
// An option is no ref; rev-parse prints `--end-of-options` back (git 2.43).
|
|
118
|
+
if (ref.startsWith('-')) return false
|
|
119
|
+
const proc = Bun.spawnSync(['git', 'rev-parse', 'HEAD', `${ref}^{commit}`], {
|
|
120
|
+
cwd: root,
|
|
121
|
+
stderr: 'ignore',
|
|
122
|
+
})
|
|
123
|
+
const [head, at] = proc.stdout.toString().split('\n')
|
|
124
|
+
return proc.exitCode === 0 && head === at
|
|
125
|
+
}
|
|
126
|
+
|
|
102
127
|
/**
|
|
103
128
|
* Turbo's `--affected` base on GitHub Actions when `TURBO_SCM_BASE` is
|
|
104
129
|
* unset (`get_github_base_ref`): a pull request's `GITHUB_BASE_REF`, else
|
|
@@ -185,6 +210,25 @@ async function workspaceKeys(root: string): Promise<{
|
|
|
185
210
|
return out
|
|
186
211
|
}
|
|
187
212
|
|
|
213
|
+
/**
|
|
214
|
+
* A policy with every read off, as `--force` is: no hit is replayed. The
|
|
215
|
+
* remote is written only where the workspace's own policy names it, so a
|
|
216
|
+
* laptop's forced run never writes the cache CI reads.
|
|
217
|
+
*/
|
|
218
|
+
function forcedPolicy(spec = ''): string {
|
|
219
|
+
const flags = new Map([
|
|
220
|
+
['local', 'w'],
|
|
221
|
+
['remote', ''],
|
|
222
|
+
])
|
|
223
|
+
for (const seg of spec.split(',')) {
|
|
224
|
+
if (seg.trim() === '') continue
|
|
225
|
+
const m = /^(local|remote):([rw]*)$/.exec(seg.trim())
|
|
226
|
+
if (m === null) return spec // core refuses it, naming `cache.policy`
|
|
227
|
+
flags.set(m[1]!, m[2]!.replace('r', ''))
|
|
228
|
+
}
|
|
229
|
+
return [...flags].map(([layer, f]) => `${layer}:${f}`).join(',')
|
|
230
|
+
}
|
|
231
|
+
|
|
188
232
|
const envNamesNow = (): string[] => Object.keys(process.env).sort()
|
|
189
233
|
|
|
190
234
|
const textOf = (file: string): Promise<string> =>
|
|
@@ -223,6 +267,8 @@ async function run(root: string, projects: readonly ProjectMeta[]): Promise<Adop
|
|
|
223
267
|
// An env wildcard expands over these; a new name it matches maps afresh.
|
|
224
268
|
JSON.stringify(envNames),
|
|
225
269
|
process.env['TURBO_CI_VENDOR_ENV_KEY'] ?? '',
|
|
270
|
+
// A framework conditional keys its names on these values (skew protection).
|
|
271
|
+
JSON.stringify(ENV_CONDITION_KEYS.map((k) => process.env[k] ?? null)),
|
|
226
272
|
process.env['TURBO_ENV_MODE'] ?? '',
|
|
227
273
|
...configs,
|
|
228
274
|
yarnrc,
|
|
@@ -265,6 +311,7 @@ async function mapAll(
|
|
|
265
311
|
splice: (_kind, values) => values,
|
|
266
312
|
persistentTodo: PERSISTENT_NOTE,
|
|
267
313
|
envNames,
|
|
314
|
+
envValue: (name) => process.env[name],
|
|
268
315
|
vendorEnvPrefix: process.env['TURBO_CI_VENDOR_ENV_KEY'] ?? '',
|
|
269
316
|
envMode: process.env['TURBO_ENV_MODE'] ?? '',
|
|
270
317
|
...(tracked === null ? {} : { tracked: trackedKinds(tracked) }),
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The root manifest's `engines`, compacted, printed by one `awk` at the
|
|
3
|
+
* workspace root: Turbo folds that value into every task's global hash
|
|
4
|
+
* (crates/turborepo-task-hash/src/global_hash.rs), so raising
|
|
5
|
+
* `engines.node` re-ran every task under `turbo run` and replayed a hit
|
|
6
|
+
* under vx. A probe reads it live, so `turbo()` and the config the
|
|
7
|
+
* migration writes key it alike; it prints only the `engines` value, so
|
|
8
|
+
* no other root edit re-keys a task. A top-level JSON scanner, strings
|
|
9
|
+
* and escapes skipped, whitespace outside strings dropped: a reformat
|
|
10
|
+
* keys alike, a reorder misses (never a stale hit). One process, no Node.
|
|
11
|
+
*/
|
|
12
|
+
export const ROOT_ENGINES_PROBE =
|
|
13
|
+
'LC_ALL=C awk \'BEGIN{RS="\\001"}{n=length($0);d=0;q=0;c=0;o="";for(i=1;i<=n;i++){x=substr($0,i,1);if(c&&(q||x!~/[ \\t\\r\\n]/))o=o x;if(q){if(x=="\\\\"){i++;if(c)o=o substr($0,i,1)}else if(x=="\\""){q=0;if(d==1&&!c)k=substr($0,s,i-s)}continue}if(x=="\\""){q=1;s=i+1}else if(x=="{"||x=="[")d++;else if(x=="}"||x=="]"){d--;if(c&&d==1){print o;exit}}else if(x==":"&&d==1&&!c&&k=="engines")c=1}}\' package.json'
|
|
14
|
+
|
|
15
|
+
/** Whether Turbo hashes the root's `engines`: a non-empty object, as it reads one. */
|
|
16
|
+
export function keysRootEngines(rootPackageJson: unknown): boolean {
|
|
17
|
+
const engines = (rootPackageJson as { engines?: unknown } | null)?.engines
|
|
18
|
+
return typeof engines === 'object' && engines !== null && Object.keys(engines).length > 0
|
|
19
|
+
}
|
package/src/turbo/turbo-map.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
resolveSharedOutputs,
|
|
25
25
|
resolveSharedWorkspaceOutputs,
|
|
26
26
|
takingBack,
|
|
27
|
+
uncache,
|
|
27
28
|
unkey,
|
|
28
29
|
wildcardCleanTodo,
|
|
29
30
|
wildcardOutput,
|
|
@@ -36,9 +37,12 @@ import {
|
|
|
36
37
|
keyable,
|
|
37
38
|
microfrontendsConfigs,
|
|
38
39
|
ROOT,
|
|
40
|
+
TURBO_BUILTIN_PASS_THROUGH,
|
|
41
|
+
TURBO_DEFAULT_ENV,
|
|
39
42
|
turboConfigFile,
|
|
40
43
|
} from './turbo-reads.js'
|
|
41
44
|
import type { TrackedKinds } from '../tracked-outputs.js'
|
|
45
|
+
import { keysRootEngines, ROOT_ENGINES_PROBE } from './root-engines.js'
|
|
42
46
|
import {
|
|
43
47
|
DOTENV_PROBE,
|
|
44
48
|
DOTENV_PROBE_TOP,
|
|
@@ -72,6 +76,8 @@ interface TurboJson {
|
|
|
72
76
|
/** Turbo 2.11 `futureFlags.globalConfiguration`: the global lists live here. */
|
|
73
77
|
global?: { inputs?: string[]; env?: string[]; passThroughEnv?: string[]; envMode?: unknown }
|
|
74
78
|
envMode?: unknown
|
|
79
|
+
/** Turbo 2.11.6: the package's labels, which `--filter tag:` selects by. */
|
|
80
|
+
tags?: string[]
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
const SHELL_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
@@ -133,6 +139,18 @@ function explicitEnv(
|
|
|
133
139
|
return out.filter((n) => !excluded.some((re) => re.test(n)))
|
|
134
140
|
}
|
|
135
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The literal names of Turbo's builtin pass-through list, the same under
|
|
144
|
+
* `turbo()` as in written configs: `exec.env.passThrough` is keyed by name,
|
|
145
|
+
* so a list matched against the live environment keyed each machine apart
|
|
146
|
+
* and missed the cache `turbo()` filled once the configs were written. A
|
|
147
|
+
* wildcard family is a note. A name that is no shell variable name
|
|
148
|
+
* (`ProgramFiles(x86)`) is left out: core refuses it there.
|
|
149
|
+
*/
|
|
150
|
+
const BUILTIN_PASS_THROUGH = TURBO_BUILTIN_PASS_THROUGH.filter((e) =>
|
|
151
|
+
/^[A-Za-z_][A-Za-z0-9_]*$/.test(e),
|
|
152
|
+
)
|
|
153
|
+
|
|
136
154
|
/** A gap's message: a wildcard, or a literal name core cannot key. */
|
|
137
155
|
const envGap = (field: string, entry: string, literal: string | null, list: string): string =>
|
|
138
156
|
literal === null
|
|
@@ -161,8 +179,11 @@ const KNOWN_TASK_KEYS = new Set([
|
|
|
161
179
|
// watch` keeps it up and restarts it only when its config changes or it
|
|
162
180
|
// dies, so either value maps to nothing.
|
|
163
181
|
'interruptible',
|
|
164
|
-
//
|
|
182
|
+
// Turbo 2.11.6 selects a task by them (`--filter tag:`); a vx tag belongs
|
|
183
|
+
// to a project, so only the package-level `tags` map (`packageTags`).
|
|
165
184
|
'tags',
|
|
185
|
+
// Turbo parses it for external CI tooling and acts on nothing.
|
|
186
|
+
'experimentalCI',
|
|
166
187
|
])
|
|
167
188
|
|
|
168
189
|
// Turbo's per-task `outputLogs` against vx's per-run `--output-logs`.
|
|
@@ -191,6 +212,8 @@ export interface TurboMappedTask {
|
|
|
191
212
|
export interface TurboMappedProject {
|
|
192
213
|
name: string
|
|
193
214
|
dir: string
|
|
215
|
+
/** The package's turbo.json `tags`, its vx project tags. */
|
|
216
|
+
tags?: string[]
|
|
194
217
|
tasks: TurboMappedTask[]
|
|
195
218
|
}
|
|
196
219
|
|
|
@@ -222,6 +245,12 @@ export interface MapTurboOptions {
|
|
|
222
245
|
* Absent (`bunx @vzn/vx-migrate` writes files), a wildcard is a todo.
|
|
223
246
|
*/
|
|
224
247
|
envNames?: readonly string[]
|
|
248
|
+
/**
|
|
249
|
+
* A variable's value where the tasks will run, read only for a framework
|
|
250
|
+
* conditional's key (Turbo's `envConditionals`). Absent (written configs),
|
|
251
|
+
* every conditional name is keyed: more misses, never a stale hit.
|
|
252
|
+
*/
|
|
253
|
+
envValue?: (name: string) => string | undefined
|
|
225
254
|
/**
|
|
226
255
|
* Without `envNames` (written configs): the upper-case names the files
|
|
227
256
|
* under `dirs` spell, which a framework's `*` prefix is matched against.
|
|
@@ -354,10 +383,12 @@ const TOP_LISTS = [
|
|
|
354
383
|
'globalPassThroughEnv',
|
|
355
384
|
'globalDotEnv',
|
|
356
385
|
'extends',
|
|
386
|
+
'tags',
|
|
357
387
|
]
|
|
358
388
|
const GLOBAL_LISTS = ['inputs', 'env', 'passThroughEnv']
|
|
359
389
|
const TASK_LISTS = ['dependsOn', 'outputs', 'env', 'passThroughEnv', 'with', 'dotEnv']
|
|
360
390
|
const TASK_FLAGS = ['cache', 'persistent', 'interactive', 'interruptible']
|
|
391
|
+
const ENV_LISTS = new Set(['globalEnv', 'globalPassThroughEnv', 'env', 'passThroughEnv'])
|
|
361
392
|
|
|
362
393
|
/**
|
|
363
394
|
* The shape of every field the mapper reads, refused by name. A number
|
|
@@ -373,12 +404,47 @@ function checkTurboShape(cfg: unknown, label: string): void {
|
|
|
373
404
|
if (v !== undefined && (!Array.isArray(v) || v.some((x) => typeof x !== 'string')))
|
|
374
405
|
refuse(at, 'an array of strings')
|
|
375
406
|
}
|
|
407
|
+
// Turbo refuses `$NAME` in an env list (`InvalidEnvPrefix`), its own
|
|
408
|
+
// `$TURBO_EXTENDS$` aside; mapped, core keyed a variable literally
|
|
409
|
+
// named `$NAME`, which is never set.
|
|
410
|
+
const envList = (v: unknown, at: string): void => {
|
|
411
|
+
list(v, at)
|
|
412
|
+
const bad = (v as string[] | undefined)?.find(
|
|
413
|
+
(x) => x.startsWith('$') && x !== '$TURBO_EXTENDS$',
|
|
414
|
+
)
|
|
415
|
+
if (bad !== undefined)
|
|
416
|
+
throw new UserError(
|
|
417
|
+
`${label}: ${at} holds ${JSON.stringify(bad)}: Turbo refuses a "$" before an env name — write ${JSON.stringify(bad.slice(1))}`,
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
// Turbo refuses an absolute glob (`AbsolutePathInConfig`), read past its
|
|
421
|
+
// `!` and `$TURBO_ROOT$/`; mapped, core refused it naming the project,
|
|
422
|
+
// as if its own config held it.
|
|
423
|
+
const paths = (v: unknown, at: string): void => {
|
|
424
|
+
for (const x of Array.isArray(v) ? v : []) {
|
|
425
|
+
const globs: unknown[] = isRecord(x) && Array.isArray(x['globs']) ? x['globs'] : [x]
|
|
426
|
+
for (const g of globs) {
|
|
427
|
+
if (typeof g !== 'string') continue
|
|
428
|
+
const bare = g.replace(/^!/, '').replace(/^\$TURBO_ROOT\$\//, '')
|
|
429
|
+
if (path.isAbsolute(bare))
|
|
430
|
+
throw new UserError(
|
|
431
|
+
`${label}: ${at} holds ${JSON.stringify(g)}: Turbo refuses an absolute path — name it relative to the package, or to the root with $TURBO_ROOT$/`,
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
376
436
|
if (!isRecord(cfg)) refuse('the file', 'a JSON object')
|
|
377
437
|
const c = cfg as Record<string, unknown>
|
|
378
|
-
for (const k of TOP_LISTS) list(c[k], k)
|
|
438
|
+
for (const k of TOP_LISTS) (ENV_LISTS.has(k) ? envList : list)(c[k], k)
|
|
439
|
+
paths(c['globalDependencies'], 'globalDependencies')
|
|
379
440
|
if (c['global'] !== undefined) {
|
|
380
441
|
if (!isRecord(c['global'])) refuse('global', 'an object')
|
|
381
|
-
for (const k of GLOBAL_LISTS)
|
|
442
|
+
for (const k of GLOBAL_LISTS)
|
|
443
|
+
(ENV_LISTS.has(k) ? envList : list)(
|
|
444
|
+
(c['global'] as Record<string, unknown>)[k],
|
|
445
|
+
`global.${k}`,
|
|
446
|
+
)
|
|
447
|
+
paths((c['global'] as Record<string, unknown>)['inputs'], 'global.inputs')
|
|
382
448
|
}
|
|
383
449
|
for (const field of ['tasks', 'pipeline']) {
|
|
384
450
|
const tasks = c[field]
|
|
@@ -388,7 +454,7 @@ function checkTurboShape(cfg: unknown, label: string): void {
|
|
|
388
454
|
const at = `${field}.${JSON.stringify(name)}`
|
|
389
455
|
if (!isRecord(def)) refuse(at, 'an object')
|
|
390
456
|
const d = def as Record<string, unknown>
|
|
391
|
-
for (const k of TASK_LISTS) list(d[k], `${at}.${k}`)
|
|
457
|
+
for (const k of TASK_LISTS) (ENV_LISTS.has(k) ? envList : list)(d[k], `${at}.${k}`)
|
|
392
458
|
// Turbo 2.11 adds `{ mode, globs, withDefaults }` entries (`flatInputs`).
|
|
393
459
|
const inputs = d['inputs']
|
|
394
460
|
if (
|
|
@@ -396,6 +462,8 @@ function checkTurboShape(cfg: unknown, label: string): void {
|
|
|
396
462
|
(!Array.isArray(inputs) || inputs.some((x) => typeof x !== 'string' && !isRecord(x)))
|
|
397
463
|
)
|
|
398
464
|
refuse(`${at}.inputs`, 'an array of globs')
|
|
465
|
+
paths(inputs, `${at}.inputs`)
|
|
466
|
+
paths(d['outputs'], `${at}.outputs`)
|
|
399
467
|
for (const k of TASK_FLAGS)
|
|
400
468
|
if (d[k] !== undefined && typeof d[k] !== 'boolean') refuse(`${at}.${k}`, 'true or false')
|
|
401
469
|
for (const k of ['outputLogs', 'outputMode'])
|
|
@@ -404,6 +472,16 @@ function checkTurboShape(cfg: unknown, label: string): void {
|
|
|
404
472
|
}
|
|
405
473
|
}
|
|
406
474
|
|
|
475
|
+
/**
|
|
476
|
+
* A package's top-level turbo.json `tags`, which Turbo 2.11.6's `--filter
|
|
477
|
+
* tag:<label>` selects every task of the package by, as vx's `tag:` does a
|
|
478
|
+
* project's. Its own file only: Turbo inherits no package tag, and the
|
|
479
|
+
* root's label the root package. Blank ones dropped, as vx refuses them.
|
|
480
|
+
*/
|
|
481
|
+
function packageTags(cfg: TurboJson | undefined): string[] {
|
|
482
|
+
return [...new Set((cfg?.tags ?? []).filter((t) => t.trim() !== ''))]
|
|
483
|
+
}
|
|
484
|
+
|
|
407
485
|
function tasksOf(cfg: TurboJson): Record<string, TurboTask> {
|
|
408
486
|
return cfg.tasks ?? cfg.pipeline ?? {}
|
|
409
487
|
}
|
|
@@ -762,6 +840,22 @@ async function rootPackageJson(root: string): Promise<PackageMeta['packageJson']
|
|
|
762
840
|
return (pkg !== null && typeof pkg === 'object' ? pkg : {}) as PackageMeta['packageJson']
|
|
763
841
|
}
|
|
764
842
|
|
|
843
|
+
/**
|
|
844
|
+
* `turboConfigFile`, refusing a directory that holds both files as Turbo
|
|
845
|
+
* does (vercel/turborepo#10083): `turbo.json` shadowed the `.jsonc` beside
|
|
846
|
+
* it in silence, so an edit there keyed and ran nothing.
|
|
847
|
+
*/
|
|
848
|
+
async function onlyConfigFile(dir: string, root: string): Promise<string | null> {
|
|
849
|
+
const file = await turboConfigFile(dir)
|
|
850
|
+
if (file?.endsWith('.json') === true && (await Bun.file(`${file}c`).exists())) {
|
|
851
|
+
const where = dir === root ? 'the workspace root' : relPosix(root, dir)
|
|
852
|
+
throw new UserError(
|
|
853
|
+
`${where} holds both turbo.json and turbo.jsonc: Turbo refuses this — remove one`,
|
|
854
|
+
)
|
|
855
|
+
}
|
|
856
|
+
return file
|
|
857
|
+
}
|
|
858
|
+
|
|
765
859
|
export async function mapTurboWorkspace(
|
|
766
860
|
root: string,
|
|
767
861
|
metas: readonly PackageMeta[],
|
|
@@ -769,7 +863,16 @@ export async function mapTurboWorkspace(
|
|
|
769
863
|
): Promise<TurboMapping> {
|
|
770
864
|
// A workspace with no Turbo config answered with a bare ENOENT and a stack
|
|
771
865
|
// (item 1043): turbo() declared in the wrong repo, or its file removed.
|
|
772
|
-
const rootFile = await
|
|
866
|
+
const rootFile = await onlyConfigFile(root, root)
|
|
867
|
+
// Turbo reads its root config from the file TURBO_ROOT_TURBO_JSON names,
|
|
868
|
+
// against the cwd (vercel/turborepo#9087); reading the root's in its
|
|
869
|
+
// place ran another task graph than `turbo run`.
|
|
870
|
+
const named = Bun.env['TURBO_ROOT_TURBO_JSON']
|
|
871
|
+
if (named && (rootFile === null || path.resolve(named) !== rootFile)) {
|
|
872
|
+
throw new UserError(
|
|
873
|
+
`TURBO_ROOT_TURBO_JSON names ${path.resolve(named)}: turbo() reads only the workspace root's turbo.json or turbo.jsonc — unset it, or move that config to the root`,
|
|
874
|
+
)
|
|
875
|
+
}
|
|
773
876
|
if (rootFile === null) {
|
|
774
877
|
throw new UserError(
|
|
775
878
|
`no turbo.json or turbo.jsonc at the workspace root (${root}): turbo() maps a Turbo repo's config — add one, or remove turbo() from .vx/workspace.ts`,
|
|
@@ -829,6 +932,7 @@ export async function mapTurboWorkspace(
|
|
|
829
932
|
// over git's files keyed nothing: the workspace probe keys them (item
|
|
830
933
|
// 1032).
|
|
831
934
|
const rootMeta = metas.find((m) => path.resolve(m.dir) === path.resolve(root))
|
|
935
|
+
const rootPkg = rootMeta?.packageJson ?? (await rootPackageJson(root))
|
|
832
936
|
const globalFiles = [
|
|
833
937
|
...globalDeps.filter((d) => envDependency(d) === null).map(turboGlobstar),
|
|
834
938
|
...(rootCfg.globalDotEnv ?? []),
|
|
@@ -837,7 +941,7 @@ export async function mapTurboWorkspace(
|
|
|
837
941
|
// packages that depend on the database package).
|
|
838
942
|
...(rootCfg.tasks === undefined && rootCfg.pipeline !== undefined
|
|
839
943
|
? []
|
|
840
|
-
: rootDependencyGlobs(root,
|
|
944
|
+
: rootDependencyGlobs(root, rootPkg, metas)),
|
|
841
945
|
...microfrontendsConfigs(root, [root, ...metas.map((m) => m.dir)]).map((c) => c.rel),
|
|
842
946
|
]
|
|
843
947
|
const rootDotenv = globalFiles.filter((f) => isDotenvGlob(f))
|
|
@@ -851,18 +955,31 @@ export async function mapTurboWorkspace(
|
|
|
851
955
|
const globals = {
|
|
852
956
|
inputs: globalFiles.filter((f) => !isDotenvGlob(f) && !globalIgnored.includes(f)),
|
|
853
957
|
env: [
|
|
854
|
-
|
|
958
|
+
// Turbo's defaults sit before the user's list, so its `!` takes them back.
|
|
959
|
+
...envNames('globalEnv', [...TURBO_DEFAULT_ENV, ...(rootCfg.globalEnv ?? [])]),
|
|
855
960
|
...globalDeps.flatMap((d) => envDependency(d) ?? []),
|
|
856
961
|
],
|
|
857
|
-
pass:
|
|
962
|
+
pass: [
|
|
963
|
+
...envNames('globalPassThroughEnv', rootCfg.globalPassThroughEnv ?? []),
|
|
964
|
+
...BUILTIN_PASS_THROUGH,
|
|
965
|
+
],
|
|
858
966
|
}
|
|
859
967
|
|
|
968
|
+
// A written config names no variable by pattern: Turbo's builtin
|
|
969
|
+
// families are the user's to list, once, for the tasks that read them.
|
|
970
|
+
if (opts.envNames === undefined && opts.sourceNames !== undefined)
|
|
971
|
+
notes.push(
|
|
972
|
+
`Turbo passes every ${TURBO_BUILTIN_PASS_THROUGH.filter((e) => e.endsWith('*')).join(', ')} ` +
|
|
973
|
+
'variable to each task; the configs pass its literal names only — add the ones a task ' +
|
|
974
|
+
'reads to exec.env.passThrough',
|
|
975
|
+
)
|
|
976
|
+
|
|
860
977
|
// The workspace root as a project (a `vx.config` there, D-39) holds
|
|
861
978
|
// Turbo's `//#task`s (`rootMeta`, above); Turbo runs no plain task in the root package.
|
|
862
979
|
const files = new Map<string, TurboJson>([[ROOT, rootCfg]])
|
|
863
980
|
for (const meta of metas) {
|
|
864
981
|
if (meta === rootMeta) continue
|
|
865
|
-
const file = await
|
|
982
|
+
const file = await onlyConfigFile(meta.dir, root)
|
|
866
983
|
if (file !== null) files.set(meta.name, await readTurboJson(file, root))
|
|
867
984
|
}
|
|
868
985
|
|
|
@@ -1113,6 +1230,8 @@ export async function mapTurboWorkspace(
|
|
|
1113
1230
|
const head = e.slice(0, -1)
|
|
1114
1231
|
return live.filter((n) => n.startsWith(head)).sort()
|
|
1115
1232
|
})
|
|
1233
|
+
for (const c of fw.conditionals ?? [])
|
|
1234
|
+
if (opts.envValue?.(c.key) === c.value) names.push(...c.include)
|
|
1116
1235
|
inferredOf.set(m.name, vendor ? names.filter((n) => !n.startsWith(vendor)) : names)
|
|
1117
1236
|
} else if (opts.sourceNames !== undefined) {
|
|
1118
1237
|
// A written config cannot ask the run's environment, and with the
|
|
@@ -1125,11 +1244,15 @@ export async function mapTurboWorkspace(
|
|
|
1125
1244
|
m.name,
|
|
1126
1245
|
// The bare prefix is no name: formbricks' web spells `NEXT_PUBLIC_`
|
|
1127
1246
|
// in a `startsWith` test, and the configs keyed a variable of it.
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1247
|
+
[
|
|
1248
|
+
...fw.env.flatMap((e) => {
|
|
1249
|
+
if (!e.endsWith('*')) return [e]
|
|
1250
|
+
const head = e.slice(0, -1)
|
|
1251
|
+
return spelled.filter((n) => n.startsWith(head) && n.length > head.length)
|
|
1252
|
+
}),
|
|
1253
|
+
// Written for another environment: keyed whatever the condition.
|
|
1254
|
+
...(fw.conditionals ?? []).flatMap((c) => c.include),
|
|
1255
|
+
],
|
|
1133
1256
|
)
|
|
1134
1257
|
sourcedOf.set(fw, [...(sourcedOf.get(fw) ?? []), m.name])
|
|
1135
1258
|
} else usersOf.set(fw, [...(usersOf.get(fw) ?? []), m.name])
|
|
@@ -1323,7 +1446,8 @@ export async function mapTurboWorkspace(
|
|
|
1323
1446
|
tasks.push(mapped)
|
|
1324
1447
|
}
|
|
1325
1448
|
excludeSiblingOutputs(resolveSharedOutputs(tasks))
|
|
1326
|
-
|
|
1449
|
+
const tags = packageTags(meta === rootMeta ? rootCfg : files.get(meta.name))
|
|
1450
|
+
projects.push({ name: meta.name, dir: meta.dir, ...(tags.length > 0 ? { tags } : {}), tasks })
|
|
1327
1451
|
}
|
|
1328
1452
|
|
|
1329
1453
|
// Turbo 2.11's `dependencyOutputs` input hashes the named files after
|
|
@@ -1426,6 +1550,14 @@ export async function mapTurboWorkspace(
|
|
|
1426
1550
|
}
|
|
1427
1551
|
}
|
|
1428
1552
|
|
|
1553
|
+
if (keysRootEngines(rootPkg))
|
|
1554
|
+
for (const p of projects)
|
|
1555
|
+
for (const t of p.tasks) {
|
|
1556
|
+
const inputs = t.task?.['inputs'] as Record<string, unknown> | undefined
|
|
1557
|
+
if (inputs === undefined) continue
|
|
1558
|
+
const probes = (inputs['workspaceRuntime'] as unknown[] | undefined) ?? []
|
|
1559
|
+
inputs['workspaceRuntime'] = [...probes, ROOT_ENGINES_PROBE]
|
|
1560
|
+
}
|
|
1429
1561
|
nestedInputs(root, projects)
|
|
1430
1562
|
for (const [entry, n] of wildcardUses)
|
|
1431
1563
|
notes.push(
|
|
@@ -1446,9 +1578,42 @@ export async function mapTurboWorkspace(
|
|
|
1446
1578
|
pruneUnreachedPersistentNotes(projects, metas, opts.persistentTodo)
|
|
1447
1579
|
pruneOrphanPersistentNotes(projects, opts.persistentTodo)
|
|
1448
1580
|
refuseTaskNames(projects, 'task')
|
|
1581
|
+
sortOrderFree(projects)
|
|
1449
1582
|
return { projects, notes, globals }
|
|
1450
1583
|
}
|
|
1451
1584
|
|
|
1585
|
+
function byText(a: unknown, b: unknown): number {
|
|
1586
|
+
const x = typeof a === 'string' ? a : JSON.stringify(a)
|
|
1587
|
+
const y = typeof b === 'string' ? b : JSON.stringify(b)
|
|
1588
|
+
return x < y ? -1 : x > y ? 1 : 0
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function negationsLast(a: unknown, b: unknown): number {
|
|
1592
|
+
const neg = (g: unknown) => Number(typeof g === 'string' && g.startsWith('!'))
|
|
1593
|
+
return neg(a) - neg(b) || byText(a, b)
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
/**
|
|
1597
|
+
* Turbo sorts a task's `outputs` and `dependsOn` before hashing, so
|
|
1598
|
+
* reordering them in turbo.json keeps the hit (vercel/turborepo#2064).
|
|
1599
|
+
* Core keys a config's lists as written, and gives neither list's order a
|
|
1600
|
+
* meaning: an edge set, and a `!` output takes back from the positives
|
|
1601
|
+
* wherever it stands (`outputMatcher`). Sorted here, negations last, so
|
|
1602
|
+
* the reorder keys the same and a written config reads the same.
|
|
1603
|
+
*/
|
|
1604
|
+
function sortOrderFree(projects: readonly TurboMappedProject[]): void {
|
|
1605
|
+
for (const p of projects)
|
|
1606
|
+
for (const t of p.tasks) {
|
|
1607
|
+
const task = t.task
|
|
1608
|
+
if (task === null) continue
|
|
1609
|
+
if (Array.isArray(task['dependsOn'])) task['dependsOn'] = [...task['dependsOn']].sort(byText)
|
|
1610
|
+
const outputs = task['outputs'] as Record<string, unknown> | undefined
|
|
1611
|
+
if (outputs === undefined) continue
|
|
1612
|
+
for (const k of ['files', 'workspaceFiles'])
|
|
1613
|
+
if (Array.isArray(outputs[k])) outputs[k] = [...outputs[k]].sort(negationsLast)
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1452
1617
|
/**
|
|
1453
1618
|
* Turbo hashes a package's files as git lists them, nested workspace
|
|
1454
1619
|
* packages included (probed on 2.11.6: `a#build` keys `n/x.ts` of a
|
|
@@ -1658,16 +1823,12 @@ function keyOnlyGroup(t: TurboMappedTask): TurboMappedTask {
|
|
|
1658
1823
|
if (task === null) return t
|
|
1659
1824
|
const exec = task['exec'] as Record<string, unknown>
|
|
1660
1825
|
if (exec['persistent'] !== undefined || exec['interactive'] !== undefined) return t
|
|
1661
|
-
const
|
|
1662
|
-
| { outputs: { files: unknown[]; workspaceFiles?: unknown[] } }
|
|
1663
|
-
| undefined
|
|
1826
|
+
const outputs = task['outputs'] as { files: unknown[]; workspaceFiles?: unknown[] } | undefined
|
|
1664
1827
|
if (task['inputs'] === undefined && task['dependsOn'] === undefined) return t
|
|
1665
|
-
if (
|
|
1666
|
-
cache !== undefined &&
|
|
1667
|
-
(cache.outputs.files.length > 0 || cache.outputs.workspaceFiles !== undefined)
|
|
1668
|
-
)
|
|
1828
|
+
if (outputs !== undefined && (outputs.files.length > 0 || outputs.workspaceFiles !== undefined))
|
|
1669
1829
|
return t
|
|
1670
|
-
const { exec: _exec,
|
|
1830
|
+
const { exec: _exec, ...group } = task
|
|
1831
|
+
uncache(group)
|
|
1671
1832
|
return { ...t, task: group }
|
|
1672
1833
|
}
|
|
1673
1834
|
|
|
@@ -1800,6 +1961,12 @@ function buildTask(
|
|
|
1800
1961
|
todos.push(
|
|
1801
1962
|
`dependsOn ${JSON.stringify(d)}: no workspace package is named ${pkg} — edge dropped`,
|
|
1802
1963
|
)
|
|
1964
|
+
// Turbo runs a root task only when turbo.json names it `//#x`; the
|
|
1965
|
+
// script alone is not enough (vercel/turborepo#3592).
|
|
1966
|
+
else if (d.startsWith(`${ROOT}#`))
|
|
1967
|
+
todos.push(
|
|
1968
|
+
`dependsOn ${JSON.stringify(d)}: turbo.json defines no "${d}" task — edge dropped`,
|
|
1969
|
+
)
|
|
1803
1970
|
else
|
|
1804
1971
|
todos.push(
|
|
1805
1972
|
`dependsOn ${JSON.stringify(d)}: ${pkg} declares no ${task} script — edge dropped`,
|
|
@@ -1961,6 +2128,14 @@ function buildTask(
|
|
|
1961
2128
|
if (files.length > 0 && files.every((f) => typeof f === 'string' && f.startsWith('!'))) {
|
|
1962
2129
|
files.unshift('**/*')
|
|
1963
2130
|
}
|
|
2131
|
+
// Turbo keys the package's own package.json beside any explicit
|
|
2132
|
+
// `inputs` (PACKAGE_CONFIG_INPUTS, turborepo#1832): without it, an edit
|
|
2133
|
+
// to its `exports` or `type` replayed the old build. A glob naming it
|
|
2134
|
+
// already, or an exclusion of it, is left as written.
|
|
2135
|
+
const named = files.some(
|
|
2136
|
+
(g) => typeof g === 'string' && new Bun.Glob(g.replace(/^!/, '')).match('package.json'),
|
|
2137
|
+
)
|
|
2138
|
+
if (!named) files.push('package.json')
|
|
1964
2139
|
}
|
|
1965
2140
|
// Turbo 1's task `dotEnv`: package-relative `.env` files it hashes,
|
|
1966
2141
|
// gitignored as a rule, so read as files they keyed nothing (item 1032).
|
|
@@ -2038,7 +2213,7 @@ function buildTask(
|
|
|
2038
2213
|
} else if (o.includes('$TURBO_ROOT$')) {
|
|
2039
2214
|
todos.push(
|
|
2040
2215
|
`output ${JSON.stringify(o)}: $TURBO_ROOT$ only maps as a '$TURBO_ROOT$/<path>' ` +
|
|
2041
|
-
'prefix (→
|
|
2216
|
+
'prefix (→ outputs.workspaceFiles) — map manually',
|
|
2042
2217
|
)
|
|
2043
2218
|
} else outFiles.push(...signed(o))
|
|
2044
2219
|
}
|
|
@@ -2072,12 +2247,18 @@ function buildTask(
|
|
|
2072
2247
|
const rewritesManifest = takeBacks.includes('!package.json')
|
|
2073
2248
|
|
|
2074
2249
|
if (atRoot) wsOutFiles.unshift(...outFiles.splice(0))
|
|
2075
|
-
|
|
2076
|
-
|
|
2250
|
+
// Sorted here, before the passes that take them back from other tasks'
|
|
2251
|
+
// inputs, so those `!` entries come in this order too (`sortOrderFree`).
|
|
2252
|
+
const outputs: Record<string, unknown> = { files: takingBack(outFiles).sort(negationsLast) }
|
|
2253
|
+
const ws = takingBack(wsOutFiles).sort(negationsLast)
|
|
2077
2254
|
if (ws.length > 0) outputs.workspaceFiles = ws
|
|
2078
|
-
task.
|
|
2079
|
-
|
|
2080
|
-
|
|
2255
|
+
task.outputs = outputs
|
|
2256
|
+
task.cache = true
|
|
2257
|
+
if (rewritesManifest) {
|
|
2258
|
+
task.inputs = {
|
|
2259
|
+
...(task.inputs as Record<string, unknown>),
|
|
2260
|
+
stability: { strategy: 'mutable' },
|
|
2261
|
+
}
|
|
2081
2262
|
}
|
|
2082
2263
|
|
|
2083
2264
|
return { name, todos, task, uses }
|