@vzn/vx 0.0.18 → 0.0.22
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/LICENSE +1 -1
- package/README.md +107 -70
- package/package.json +10 -20
- package/src/bin.ts +64 -5
- package/src/cache/archive.ts +279 -28
- package/src/cache/cache.ts +952 -1584
- package/src/cache/chained-cache.ts +14 -10
- package/src/cache/config-evals.ts +112 -0
- package/src/cache/file-hashes.ts +290 -0
- package/src/cache/git-inputs.ts +966 -0
- package/src/cache/index.ts +23 -7
- package/src/cache/inputs.ts +553 -739
- package/src/cache/key-fold.ts +232 -0
- package/src/cache/layer.ts +674 -0
- package/src/cache/layered-cache.ts +211 -49
- package/src/cache/output-index.ts +323 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +152 -0
- package/src/cache/zstd.ts +183 -0
- package/src/cli/cache.ts +88 -30
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/format.ts +4 -15
- package/src/cli/help.ts +76 -38
- package/src/cli/index.ts +82 -17
- package/src/cli/info.ts +162 -91
- package/src/cli/init.ts +87 -0
- package/src/cli/last.ts +130 -23
- package/src/cli/lock.ts +36 -18
- package/src/cli/plan-format.ts +18 -7
- package/src/cli/plugin-commands.ts +34 -12
- package/src/cli/run.ts +138 -313
- package/src/cli/select.ts +342 -0
- package/src/cli/show.ts +151 -71
- package/src/cli/upgrade.ts +136 -15
- package/src/cli/watch.ts +1175 -116
- package/src/cli/why.ts +51 -29
- package/src/cli/workspace-config.ts +138 -0
- package/src/config.ts +183 -126
- package/src/exec/env.ts +11 -0
- package/src/exec/executor.ts +62 -31
- package/src/exec/index.ts +16 -2
- package/src/exec/kill-tree.ts +281 -0
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +361 -77
- package/src/exec/sandbox-binds.ts +211 -0
- package/src/exec/sandbox-paths.ts +70 -0
- package/src/exec/sandbox-runtime.ts +1152 -415
- package/src/exec/sandbox-violations.ts +293 -0
- package/src/graph/dependency-spec.ts +14 -14
- package/src/graph/index.ts +6 -4
- package/src/graph/priorities.ts +130 -0
- package/src/graph/scheduler.ts +332 -329
- package/src/graph/task-graph.ts +552 -188
- package/src/index.ts +111 -102
- package/src/orchestrator/admission.ts +161 -0
- package/src/orchestrator/colors.ts +8 -3
- package/src/orchestrator/deferred-outputs.ts +25 -7
- package/src/orchestrator/doctor.ts +363 -0
- package/src/orchestrator/events.ts +111 -8
- package/src/orchestrator/excluded-keys.ts +155 -0
- package/src/orchestrator/execute-task.ts +450 -565
- package/src/orchestrator/failure-mode.ts +169 -28
- package/src/orchestrator/failure-recap.ts +121 -0
- package/src/orchestrator/fingerprint-watch.ts +89 -0
- package/src/orchestrator/framed-output.ts +121 -49
- package/src/orchestrator/history.ts +123 -104
- package/src/orchestrator/hit-restore.ts +285 -0
- package/src/orchestrator/index.ts +42 -5
- package/src/orchestrator/keyed-projects.ts +89 -0
- package/src/orchestrator/local-shortcircuit.ts +83 -23
- package/src/orchestrator/lockfile-claim.ts +401 -0
- package/src/orchestrator/logger.ts +128 -41
- package/src/orchestrator/metrics.ts +201 -72
- package/src/orchestrator/miss-save.ts +232 -0
- package/src/orchestrator/options.ts +63 -34
- package/src/orchestrator/persistent.ts +99 -0
- package/src/orchestrator/placement.ts +212 -0
- package/src/orchestrator/plan.ts +24 -11
- package/src/orchestrator/plugin-host.ts +200 -49
- package/src/orchestrator/plugin.ts +190 -14
- package/src/orchestrator/prepare.ts +168 -137
- package/src/orchestrator/projects.ts +270 -0
- package/src/orchestrator/remote-prefetch.ts +22 -2
- package/src/orchestrator/run-artifacts.ts +34 -3
- package/src/orchestrator/run-context.ts +20 -5
- package/src/orchestrator/run-lock.ts +349 -0
- package/src/orchestrator/run-records.ts +132 -0
- package/src/orchestrator/run-report.ts +6 -4
- package/src/orchestrator/run.ts +467 -611
- package/src/orchestrator/sandbox-request.ts +569 -0
- package/src/orchestrator/save-lane.ts +44 -0
- package/src/orchestrator/shell-verdict.ts +117 -0
- package/src/orchestrator/signals.ts +167 -0
- package/src/orchestrator/stable-keys.ts +259 -10
- package/src/orchestrator/status-line.ts +35 -2
- package/src/orchestrator/summary.ts +115 -25
- package/src/orchestrator/task-hash.ts +103 -21
- package/src/orchestrator/task-log-buffer.ts +15 -18
- package/src/orchestrator/telemetry-host.ts +27 -8
- package/src/orchestrator/telemetry.ts +71 -44
- package/src/orchestrator/upstream.ts +91 -45
- package/src/util/bun-version.ts +43 -0
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +49 -0
- package/src/util/errors.ts +86 -0
- package/src/util/hash.ts +10 -1
- package/src/util/index.ts +42 -5
- package/src/util/paths.ts +168 -5
- package/src/util/procfs.ts +20 -0
- package/src/util/settle.ts +23 -2
- package/src/util/size.ts +33 -0
- package/src/util/task-id.ts +20 -0
- package/src/util/timing.ts +11 -1
- package/src/util/verbs.ts +39 -0
- package/src/util/which.ts +48 -0
- package/src/workspace/affected.ts +368 -60
- package/src/workspace/config-cache.ts +271 -32
- package/src/workspace/config-eval.ts +60 -16
- package/src/workspace/config-imports.ts +131 -9
- package/src/workspace/config-schema.ts +1033 -0
- package/src/workspace/filter.ts +69 -17
- package/src/workspace/fingerprint.ts +59 -7
- package/src/workspace/index.ts +37 -3
- package/src/workspace/json-data.ts +90 -0
- package/src/workspace/load-reads.ts +32 -0
- package/src/workspace/lockfile.ts +41 -14
- package/src/workspace/migrate-scripts.ts +187 -0
- package/src/workspace/migration.ts +353 -0
- package/src/workspace/package-graph.ts +239 -31
- package/src/workspace/project-loader.ts +295 -815
- package/src/workspace/workspace.ts +266 -31
- package/plugins/local-cache/index.ts +0 -2
- package/plugins/local-executor/index.ts +0 -2
- package/plugins/schedule-history/index.ts +0 -2
- package/src/cache/cas-backend.ts +0 -115
- package/src/cache/digest.ts +0 -32
- package/src/cli/migrate-emit.ts +0 -21
- package/src/cli/migrate-nx.ts +0 -473
- package/src/cli/migrate-scripts.ts +0 -127
- package/src/cli/migrate-turbo.ts +0 -401
- package/src/cli/migrate.ts +0 -305
- package/src/cli/prune.ts +0 -291
- package/src/orchestrator/missing-plugin.ts +0 -9
- package/src/orchestrator/resources.ts +0 -45
- package/src/orchestrator/verify.ts +0 -174
- package/src/plugins/local-cache/index.ts +0 -11
- package/src/plugins/local-executor/index.ts +0 -45
- package/src/plugins/schedule-history/index.ts +0 -109
package/src/cli/migrate-turbo.ts
DELETED
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
// Turbo → vx mapping. Reads turbo.json (tasks in turbo 2, pipeline in
|
|
2
|
-
// turbo 1), per-package turbo.json `extends` overlays, and each
|
|
3
|
-
// package's scripts. A task is emitted for a package only when the
|
|
4
|
-
// package declares the script (turbo semantics). Global fields become
|
|
5
|
-
// a root vx-preset.ts that configs import and spread — TypeScript
|
|
6
|
-
// composition replaces turbo's global config.
|
|
7
|
-
|
|
8
|
-
import path from 'node:path'
|
|
9
|
-
import { relPosix, UserError } from '../util/index.js'
|
|
10
|
-
import type { ProjectMeta } from '../workspace/index.js'
|
|
11
|
-
import { quote } from './migrate-emit.js'
|
|
12
|
-
import type { GeneratedProject, GeneratedTask, MigrationPlan, RawExpr } from './migrate.js'
|
|
13
|
-
|
|
14
|
-
interface TurboTask {
|
|
15
|
-
dependsOn?: string[]
|
|
16
|
-
inputs?: string[]
|
|
17
|
-
outputs?: string[]
|
|
18
|
-
env?: string[]
|
|
19
|
-
passThroughEnv?: string[]
|
|
20
|
-
cache?: boolean
|
|
21
|
-
persistent?: boolean
|
|
22
|
-
[key: string]: unknown
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface TurboJson {
|
|
26
|
-
tasks?: Record<string, TurboTask>
|
|
27
|
-
pipeline?: Record<string, TurboTask>
|
|
28
|
-
globalDependencies?: string[]
|
|
29
|
-
globalEnv?: string[]
|
|
30
|
-
globalPassThroughEnv?: string[]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const KNOWN_TASK_KEYS = new Set([
|
|
34
|
-
'dependsOn',
|
|
35
|
-
'inputs',
|
|
36
|
-
'outputs',
|
|
37
|
-
'env',
|
|
38
|
-
'passThroughEnv',
|
|
39
|
-
'cache',
|
|
40
|
-
'persistent',
|
|
41
|
-
'extends',
|
|
42
|
-
])
|
|
43
|
-
|
|
44
|
-
const PRESET_FILE = 'vx-preset.ts'
|
|
45
|
-
|
|
46
|
-
interface Globals {
|
|
47
|
-
inputs: boolean
|
|
48
|
-
env: boolean
|
|
49
|
-
pass: boolean
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function readTurboJson(file: string, root: string): Promise<TurboJson> {
|
|
53
|
-
const text = await Bun.file(file).text()
|
|
54
|
-
try {
|
|
55
|
-
// turbo.json allows comments + trailing commas.
|
|
56
|
-
return (Bun.JSONC.parse(text) ?? {}) as TurboJson
|
|
57
|
-
} catch (err) {
|
|
58
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
59
|
-
throw new UserError(`failed to parse ${relPosix(root, file)}: ${msg}`)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function tasksOf(cfg: TurboJson): Record<string, TurboTask> {
|
|
64
|
-
return cfg.tasks ?? cfg.pipeline ?? {}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function scriptsOf(meta: ProjectMeta): Record<string, unknown> {
|
|
68
|
-
// package.json is a system boundary — a script value is whatever the
|
|
69
|
-
// file holds, not necessarily a string.
|
|
70
|
-
return (meta.packageJson as unknown as { scripts?: Record<string, unknown> }).scripts ?? {}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** A script value that can become `exec.command` verbatim. */
|
|
74
|
-
function usableScript(value: unknown): value is string {
|
|
75
|
-
return typeof value === 'string' && value.length > 0
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function describeScript(value: unknown): string {
|
|
79
|
-
if (value === null) return 'null'
|
|
80
|
-
if (typeof value === 'string') return 'an empty string'
|
|
81
|
-
if (Array.isArray(value)) return 'an array'
|
|
82
|
-
return `a ${typeof value}`
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Declared task names for a package: plain root keys, `pkg#name` keys
|
|
86
|
-
* for this package, and per-package turbo.json keys — in that order. */
|
|
87
|
-
function taskNamesFor(
|
|
88
|
-
pkgName: string,
|
|
89
|
-
rootTasks: Record<string, TurboTask>,
|
|
90
|
-
pkgTasks: Record<string, TurboTask> | undefined,
|
|
91
|
-
): string[] {
|
|
92
|
-
const names: string[] = []
|
|
93
|
-
const push = (n: string): void => {
|
|
94
|
-
if (!names.includes(n)) names.push(n)
|
|
95
|
-
}
|
|
96
|
-
for (const key of Object.keys(rootTasks)) {
|
|
97
|
-
if (!key.includes('#')) push(key)
|
|
98
|
-
else if (key.startsWith(`${pkgName}#`)) push(key.slice(pkgName.length + 1))
|
|
99
|
-
}
|
|
100
|
-
for (const key of Object.keys(pkgTasks ?? {})) {
|
|
101
|
-
if (!key.includes('#')) push(key)
|
|
102
|
-
}
|
|
103
|
-
return names
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export async function migrateTurbo(
|
|
107
|
-
root: string,
|
|
108
|
-
metas: readonly ProjectMeta[],
|
|
109
|
-
): Promise<MigrationPlan> {
|
|
110
|
-
const rootCfg = await readTurboJson(path.join(root, 'turbo.json'), root)
|
|
111
|
-
const rootTasks = tasksOf(rootCfg)
|
|
112
|
-
|
|
113
|
-
const globalInputs = rootCfg.globalDependencies ?? []
|
|
114
|
-
const globalEnv = rootCfg.globalEnv ?? []
|
|
115
|
-
const globalPass = rootCfg.globalPassThroughEnv ?? []
|
|
116
|
-
const globals: Globals = {
|
|
117
|
-
inputs: globalInputs.length > 0,
|
|
118
|
-
env: globalEnv.length > 0,
|
|
119
|
-
pass: globalPass.length > 0,
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const pkgTasksByName = new Map<string, Record<string, TurboTask>>()
|
|
123
|
-
for (const meta of metas) {
|
|
124
|
-
const file = path.join(meta.dir, 'turbo.json')
|
|
125
|
-
if (await Bun.file(file).exists()) {
|
|
126
|
-
pkgTasksByName.set(meta.name, tasksOf(await readTurboJson(file, root)))
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const notes: string[] = []
|
|
131
|
-
for (const key of Object.keys(rootTasks)) {
|
|
132
|
-
if (key.startsWith('//#')) {
|
|
133
|
-
notes.push(`note: root task ${key} not migrated — vx has no workspace-root tasks`)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// First pass: which tasks does each package emit? Needed so dependsOn
|
|
138
|
-
// edges can be validated/dropped against the real emitted set.
|
|
139
|
-
const emitted = new Map<string, Set<string>>()
|
|
140
|
-
for (const meta of metas) {
|
|
141
|
-
const scripts = scriptsOf(meta)
|
|
142
|
-
const set = new Set<string>()
|
|
143
|
-
for (const name of taskNamesFor(meta.name, rootTasks, pkgTasksByName.get(meta.name))) {
|
|
144
|
-
if (usableScript(scripts[name])) set.add(name)
|
|
145
|
-
}
|
|
146
|
-
emitted.set(meta.name, set)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const projects: GeneratedProject[] = []
|
|
150
|
-
for (const meta of metas) {
|
|
151
|
-
const scripts = scriptsOf(meta)
|
|
152
|
-
const pkgTasks = pkgTasksByName.get(meta.name)
|
|
153
|
-
const own = emitted.get(meta.name)!
|
|
154
|
-
const used = new Set<string>()
|
|
155
|
-
const tasks: GeneratedTask[] = []
|
|
156
|
-
for (const name of taskNamesFor(meta.name, rootTasks, pkgTasks)) {
|
|
157
|
-
const script = scripts[name]
|
|
158
|
-
if (!usableScript(script)) {
|
|
159
|
-
// The turbo task exists and so does the script KEY, but its value
|
|
160
|
-
// can't become a command. Report it instead of emitting
|
|
161
|
-
// `command: 42` / `command: null` — the first writes a config that
|
|
162
|
-
// fails to load, the second used to abort the whole migration in
|
|
163
|
-
// the emitter, and both landed AFTER "migrated clean, 0 TODOs".
|
|
164
|
-
// An ABSENT script stays silent: turbo skips the task there too.
|
|
165
|
-
if (script !== undefined) {
|
|
166
|
-
tasks.push({
|
|
167
|
-
name,
|
|
168
|
-
todos: [
|
|
169
|
-
`package.json script ${JSON.stringify(name)} is ${describeScript(script)}, not a ` +
|
|
170
|
-
'non-empty command string — task skipped; write the command by hand',
|
|
171
|
-
],
|
|
172
|
-
task: null,
|
|
173
|
-
})
|
|
174
|
-
}
|
|
175
|
-
continue
|
|
176
|
-
}
|
|
177
|
-
const def: TurboTask = {
|
|
178
|
-
...rootTasks[name],
|
|
179
|
-
...rootTasks[`${meta.name}#${name}`],
|
|
180
|
-
...pkgTasks?.[name],
|
|
181
|
-
}
|
|
182
|
-
tasks.push(buildTask(name, def, script, own, emitted, globals, used))
|
|
183
|
-
}
|
|
184
|
-
projects.push({
|
|
185
|
-
name: meta.name,
|
|
186
|
-
dir: meta.dir,
|
|
187
|
-
importLines: presetImportLines(used, root, meta.dir),
|
|
188
|
-
tasks,
|
|
189
|
-
})
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const extraFiles: MigrationPlan['extraFiles'] = []
|
|
193
|
-
if (globals.inputs || globals.env || globals.pass) {
|
|
194
|
-
extraFiles.push({
|
|
195
|
-
relPath: PRESET_FILE,
|
|
196
|
-
contents: renderPreset(globalInputs, globalEnv, globalPass),
|
|
197
|
-
})
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return { headerNotes: [], projects, extraFiles, notes }
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function buildTask(
|
|
204
|
-
name: string,
|
|
205
|
-
def: TurboTask,
|
|
206
|
-
command: string,
|
|
207
|
-
own: ReadonlySet<string>,
|
|
208
|
-
emitted: ReadonlyMap<string, ReadonlySet<string>>,
|
|
209
|
-
globals: Globals,
|
|
210
|
-
used: Set<string>,
|
|
211
|
-
): GeneratedTask {
|
|
212
|
-
const todos: string[] = []
|
|
213
|
-
const persistent = def.persistent === true
|
|
214
|
-
const cacheEnabled = def.cache !== false && !persistent
|
|
215
|
-
|
|
216
|
-
for (const [key, value] of Object.entries(def)) {
|
|
217
|
-
if (KNOWN_TASK_KEYS.has(key)) continue
|
|
218
|
-
todos.push(
|
|
219
|
-
`turbo key ${JSON.stringify(key)} (${JSON.stringify(value)}) has no vx equivalent — ` +
|
|
220
|
-
'map it manually',
|
|
221
|
-
)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const deps: string[] = []
|
|
225
|
-
for (const d of def.dependsOn ?? []) {
|
|
226
|
-
if (d.includes('$TURBO_ROOT$')) {
|
|
227
|
-
todos.push(
|
|
228
|
-
`dependsOn ${JSON.stringify(d)} uses $TURBO_ROOT$ — vx has no workspace-root tasks; ` +
|
|
229
|
-
'restructure manually',
|
|
230
|
-
)
|
|
231
|
-
continue
|
|
232
|
-
}
|
|
233
|
-
if (d.startsWith('^')) {
|
|
234
|
-
deps.push(d)
|
|
235
|
-
continue
|
|
236
|
-
}
|
|
237
|
-
const hashAt = d.indexOf('#')
|
|
238
|
-
if (hashAt !== -1) {
|
|
239
|
-
const pkg = d.slice(0, hashAt)
|
|
240
|
-
const task = d.slice(hashAt + 1)
|
|
241
|
-
if (emitted.get(pkg)?.has(task)) deps.push(d)
|
|
242
|
-
else
|
|
243
|
-
todos.push(
|
|
244
|
-
`dependsOn ${JSON.stringify(d)}: ${pkg} declares no ${task} script — edge dropped`,
|
|
245
|
-
)
|
|
246
|
-
continue
|
|
247
|
-
}
|
|
248
|
-
// Same-project dep on a script this package lacks: turbo silently
|
|
249
|
-
// skips the task there, so the edge simply doesn't exist.
|
|
250
|
-
if (own.has(d)) deps.push(d)
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const envNames: string[] = []
|
|
254
|
-
for (const e of def.env ?? []) {
|
|
255
|
-
if (/[*?[\]!]/.test(e)) {
|
|
256
|
-
todos.push(
|
|
257
|
-
`env ${JSON.stringify(e)}: wildcards are not supported in vx env names — ` +
|
|
258
|
-
'list explicit names in cache.inputs.env + exec.env.passThrough',
|
|
259
|
-
)
|
|
260
|
-
} else envNames.push(e)
|
|
261
|
-
}
|
|
262
|
-
const passNames: string[] = []
|
|
263
|
-
for (const e of def.passThroughEnv ?? []) {
|
|
264
|
-
if (/[*?[\]!]/.test(e)) {
|
|
265
|
-
todos.push(
|
|
266
|
-
`passThroughEnv ${JSON.stringify(e)}: wildcards are not supported — list explicit names`,
|
|
267
|
-
)
|
|
268
|
-
} else passNames.push(e)
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const passThrough: (string | RawExpr)[] = []
|
|
272
|
-
if (globals.env) {
|
|
273
|
-
passThrough.push({ raw: '...globalEnvInputs' })
|
|
274
|
-
used.add('globalEnvInputs')
|
|
275
|
-
}
|
|
276
|
-
if (globals.pass) {
|
|
277
|
-
passThrough.push({ raw: '...globalPassThroughEnv' })
|
|
278
|
-
used.add('globalPassThroughEnv')
|
|
279
|
-
}
|
|
280
|
-
passThrough.push(...envNames, ...passNames)
|
|
281
|
-
|
|
282
|
-
const exec: Record<string, unknown> = { command }
|
|
283
|
-
if (passThrough.length > 0) exec.env = { passThrough }
|
|
284
|
-
if (persistent) {
|
|
285
|
-
exec.persistent = {}
|
|
286
|
-
todos.push(
|
|
287
|
-
'persistent task — set persistent.readyWhen (regex matched against output) so ' +
|
|
288
|
-
'dependents unblock on readiness, and consider exec.timeout to bound the wait',
|
|
289
|
-
)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const task: Record<string, unknown> = { exec }
|
|
293
|
-
if (deps.length > 0) task.dependsOn = deps
|
|
294
|
-
|
|
295
|
-
if (cacheEnabled) {
|
|
296
|
-
const files: (string | RawExpr)[] = []
|
|
297
|
-
const wsFiles: (string | RawExpr)[] = []
|
|
298
|
-
// globalDependencies are workspace-root-relative by definition —
|
|
299
|
-
// they map to inputs.workspaceFiles, not project-relative files.
|
|
300
|
-
if (globals.inputs) {
|
|
301
|
-
wsFiles.push({ raw: '...globalInputs' })
|
|
302
|
-
used.add('globalInputs')
|
|
303
|
-
}
|
|
304
|
-
if (def.inputs === undefined) {
|
|
305
|
-
// Turbo's default input set is every package file.
|
|
306
|
-
files.push('**/*')
|
|
307
|
-
} else {
|
|
308
|
-
for (const i of def.inputs) {
|
|
309
|
-
if (i === '$TURBO_DEFAULT$') {
|
|
310
|
-
files.push('**/*')
|
|
311
|
-
continue
|
|
312
|
-
}
|
|
313
|
-
const neg = i.startsWith('!')
|
|
314
|
-
const body = neg ? i.slice(1) : i
|
|
315
|
-
if (body.startsWith('$TURBO_ROOT$/')) {
|
|
316
|
-
wsFiles.push((neg ? '!' : '') + body.slice('$TURBO_ROOT$/'.length))
|
|
317
|
-
} else if (i.includes('$TURBO_ROOT$')) {
|
|
318
|
-
todos.push(
|
|
319
|
-
`input ${JSON.stringify(i)}: $TURBO_ROOT$ only maps as a '$TURBO_ROOT$/<path>' ` +
|
|
320
|
-
'prefix (→ cache.inputs.workspaceFiles) — map manually',
|
|
321
|
-
)
|
|
322
|
-
} else files.push(i)
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const outFiles: string[] = []
|
|
327
|
-
const wsOutFiles: string[] = []
|
|
328
|
-
for (const o of def.outputs ?? []) {
|
|
329
|
-
if (o.startsWith('!')) {
|
|
330
|
-
todos.push(
|
|
331
|
-
`output ${JSON.stringify(o)}: vx outputs have no negation — narrow the positive ` +
|
|
332
|
-
'globs instead',
|
|
333
|
-
)
|
|
334
|
-
} else if (o.startsWith('$TURBO_ROOT$/')) {
|
|
335
|
-
wsOutFiles.push(o.slice('$TURBO_ROOT$/'.length))
|
|
336
|
-
} else if (o.includes('$TURBO_ROOT$')) {
|
|
337
|
-
todos.push(
|
|
338
|
-
`output ${JSON.stringify(o)}: $TURBO_ROOT$ only maps as a '$TURBO_ROOT$/<path>' ` +
|
|
339
|
-
'prefix (→ cache.outputs.workspaceFiles) — map manually',
|
|
340
|
-
)
|
|
341
|
-
} else outFiles.push(o)
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const cacheEnv: (string | RawExpr)[] = []
|
|
345
|
-
if (globals.env) cacheEnv.push({ raw: '...globalEnvInputs' })
|
|
346
|
-
cacheEnv.push(...envNames)
|
|
347
|
-
|
|
348
|
-
const inputs: Record<string, unknown> = { files }
|
|
349
|
-
if (wsFiles.length > 0) inputs.workspaceFiles = wsFiles
|
|
350
|
-
if (cacheEnv.length > 0) inputs.env = cacheEnv
|
|
351
|
-
const outputs: Record<string, unknown> = { files: outFiles }
|
|
352
|
-
if (wsOutFiles.length > 0) outputs.workspaceFiles = wsOutFiles
|
|
353
|
-
task.cache = { inputs, outputs }
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return { name, todos, task }
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function presetImportLines(used: ReadonlySet<string>, root: string, dir: string): string[] {
|
|
360
|
-
if (used.size === 0) return []
|
|
361
|
-
const rel = relPosix(dir, path.join(root, PRESET_FILE))
|
|
362
|
-
const spec = rel.startsWith('.') ? rel : `./${rel}`
|
|
363
|
-
return [`import { ${[...used].sort().join(', ')} } from '${spec}'`]
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function renderPreset(inputs: string[], env: string[], pass: string[]): string {
|
|
367
|
-
// Escape each entry via the shared `quote()` — a turbo.json global (a file
|
|
368
|
-
// glob, or an env name a user hand-wrote) may contain a `'`/`\`/newline that
|
|
369
|
-
// would otherwise splice into a malformed, unloadable `vx-preset.ts`.
|
|
370
|
-
const arr = (xs: string[]): string => `[${xs.map(quote).join(', ')}]`
|
|
371
|
-
const lines = [
|
|
372
|
-
'// Generated by `vx migrate` from turbo.json. TypeScript composition',
|
|
373
|
-
"// replaces turbo's global fields: each vx.config.ts imports these",
|
|
374
|
-
'// arrays and spreads them into the matching task fields.',
|
|
375
|
-
]
|
|
376
|
-
if (inputs.length > 0) {
|
|
377
|
-
lines.push(
|
|
378
|
-
'',
|
|
379
|
-
'// From globalDependencies — workspace-root-relative, spread into each',
|
|
380
|
-
'// task’s cache.inputs.workspaceFiles (the $TURBO_ROOT$ equivalent).',
|
|
381
|
-
`export const globalInputs = ${arr(inputs)}`,
|
|
382
|
-
)
|
|
383
|
-
}
|
|
384
|
-
if (env.length > 0) {
|
|
385
|
-
lines.push(
|
|
386
|
-
'',
|
|
387
|
-
'// From globalEnv: cache inputs AND passed through to every task',
|
|
388
|
-
'// (vx child environments are isolated; see docs/schema.md).',
|
|
389
|
-
`export const globalEnvInputs = ${arr(env)}`,
|
|
390
|
-
)
|
|
391
|
-
}
|
|
392
|
-
if (pass.length > 0) {
|
|
393
|
-
lines.push(
|
|
394
|
-
'',
|
|
395
|
-
'// From globalPassThroughEnv: forwarded to every task, never hashed.',
|
|
396
|
-
`export const globalPassThroughEnv = ${arr(pass)}`,
|
|
397
|
-
)
|
|
398
|
-
}
|
|
399
|
-
lines.push('')
|
|
400
|
-
return lines.join('\n')
|
|
401
|
-
}
|
package/src/cli/migrate.ts
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
// `vx migrate [--dry] [--force]` — generate per-package vx.config.ts
|
|
2
|
-
// from an existing Turbo or Nx setup. Source auto-detect: turbo.json →
|
|
3
|
-
// Turbo; .nx/workspace-data/project-graph.json → Nx (the resolved
|
|
4
|
-
// snapshot). Mappers live in migrate-turbo.ts / migrate-nx.ts and
|
|
5
|
-
// return an IR; this file owns detection, TS emission, the overwrite
|
|
6
|
-
// guard, and the final report.
|
|
7
|
-
|
|
8
|
-
import path from 'node:path'
|
|
9
|
-
import { relPosix, UserError } from '../util/index.js'
|
|
10
|
-
import { findWorkspaceRoot, listProjects, loadWorkspace } from '../workspace/index.js'
|
|
11
|
-
import { quote } from './migrate-emit.js'
|
|
12
|
-
import { migrateNx } from './migrate-nx.js'
|
|
13
|
-
import { migrateScripts } from './migrate-scripts.js'
|
|
14
|
-
import { migrateTurbo } from './migrate-turbo.js'
|
|
15
|
-
|
|
16
|
-
export interface MigrateArgs {
|
|
17
|
-
dry: boolean
|
|
18
|
-
force: boolean
|
|
19
|
-
from?: 'turbo' | 'nx' | 'scripts'
|
|
20
|
-
error?: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function parseMigrateArgs(args: readonly string[]): MigrateArgs {
|
|
24
|
-
const out: MigrateArgs = { dry: false, force: false }
|
|
25
|
-
for (let i = 0; i < args.length; i++) {
|
|
26
|
-
const a = args[i]
|
|
27
|
-
if (a === '--dry') out.dry = true
|
|
28
|
-
else if (a === '--force') out.force = true
|
|
29
|
-
else if (a === '--from' || a?.startsWith('--from=')) {
|
|
30
|
-
const v = a === '--from' ? args[++i] : a.slice('--from='.length)
|
|
31
|
-
if (v !== 'turbo' && v !== 'nx' && v !== 'scripts') {
|
|
32
|
-
return { ...out, error: `--from must be turbo, nx or scripts` }
|
|
33
|
-
}
|
|
34
|
-
out.from = v
|
|
35
|
-
} else if (a?.startsWith('-')) return { ...out, error: `unknown flag: ${a}` }
|
|
36
|
-
else return { ...out, error: `unexpected argument: ${a}` }
|
|
37
|
-
}
|
|
38
|
-
return out
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Verbatim TS expression spliced into a generated array (preset spreads). */
|
|
42
|
-
export interface RawExpr {
|
|
43
|
-
readonly raw: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface GeneratedTask {
|
|
47
|
-
name: string
|
|
48
|
-
/** Rendered as `// TODO(vx-migrate): …` above the task + listed in the report. */
|
|
49
|
-
todos: string[]
|
|
50
|
-
/** TaskConfig-shaped object; arrays may contain RawExpr splices.
|
|
51
|
-
* null = target has no vx representation (skipped; todos explain). */
|
|
52
|
-
task: Record<string, unknown> | null
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface GeneratedProject {
|
|
56
|
-
/** package.json name */
|
|
57
|
-
name: string
|
|
58
|
-
/** absolute project dir */
|
|
59
|
-
dir: string
|
|
60
|
-
importLines: string[]
|
|
61
|
-
tasks: GeneratedTask[]
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface MigrationPlan {
|
|
65
|
-
/** report lines printed right under the source line */
|
|
66
|
-
headerNotes: string[]
|
|
67
|
-
projects: GeneratedProject[]
|
|
68
|
-
/** extra root-relative files (e.g. the preset) */
|
|
69
|
-
extraFiles: { relPath: string; contents: string }[]
|
|
70
|
-
/** trailing report lines (e.g. implicit Nx deps) */
|
|
71
|
-
notes: string[]
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export async function migrateCmd(
|
|
75
|
-
args: readonly string[],
|
|
76
|
-
opts: { init?: boolean } = {},
|
|
77
|
-
): Promise<number> {
|
|
78
|
-
const parsed = parseMigrateArgs(args)
|
|
79
|
-
if (parsed.error) {
|
|
80
|
-
process.stderr.write(`vx migrate: ${parsed.error}\n`)
|
|
81
|
-
return 1
|
|
82
|
-
}
|
|
83
|
-
const root = await findWorkspaceRoot(process.cwd())
|
|
84
|
-
const metas = await listProjects(await loadWorkspace(root))
|
|
85
|
-
|
|
86
|
-
const hasTurbo = await Bun.file(path.join(root, 'turbo.json')).exists()
|
|
87
|
-
const graphRel = path.join('.nx', 'workspace-data', 'project-graph.json')
|
|
88
|
-
const hasGraph = await Bun.file(path.join(root, graphRel)).exists()
|
|
89
|
-
const hasNxJson = await Bun.file(path.join(root, 'nx.json')).exists()
|
|
90
|
-
|
|
91
|
-
// Evaluating teams routinely have both runners checked in — never
|
|
92
|
-
// ask anyone to delete anything; --from disambiguates.
|
|
93
|
-
if (parsed.from === undefined && hasTurbo && (hasGraph || hasNxJson)) {
|
|
94
|
-
throw new UserError(
|
|
95
|
-
'both turbo.json and an nx workspace are present — pass --from turbo or --from nx',
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
if (parsed.from === 'turbo' && !hasTurbo) {
|
|
99
|
-
throw new UserError('--from turbo, but no turbo.json at the workspace root')
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
let source: string
|
|
103
|
-
let plan: MigrationPlan
|
|
104
|
-
if (parsed.from === 'scripts') {
|
|
105
|
-
source = 'package.json scripts'
|
|
106
|
-
plan = migrateScripts(metas)
|
|
107
|
-
} else if (parsed.from === 'nx' || (parsed.from === undefined && !hasTurbo)) {
|
|
108
|
-
if (hasGraph) {
|
|
109
|
-
source = '.nx/workspace-data/project-graph.json'
|
|
110
|
-
plan = await migrateNx(root, metas)
|
|
111
|
-
} else if (hasNxJson || parsed.from === 'nx') {
|
|
112
|
-
// Modern Nx stores the graph in SQLite — the JSON snapshot only
|
|
113
|
-
// exists when exported explicitly.
|
|
114
|
-
throw new UserError(
|
|
115
|
-
'no resolved Nx graph found — export one with ' +
|
|
116
|
-
'`nx graph --file=.nx/workspace-data/project-graph.json`, then re-run vx migrate',
|
|
117
|
-
)
|
|
118
|
-
} else {
|
|
119
|
-
// A workspace from nowhere: the scripts are the source (`vx init`).
|
|
120
|
-
source = 'package.json scripts'
|
|
121
|
-
plan = migrateScripts(metas)
|
|
122
|
-
}
|
|
123
|
-
} else {
|
|
124
|
-
source = 'turbo.json'
|
|
125
|
-
plan = await migrateTurbo(root, metas)
|
|
126
|
-
}
|
|
127
|
-
// `vx init` on a workspace with no scripts still has a job: the
|
|
128
|
-
// workspace file, which every run needs, and a worked example of the
|
|
129
|
-
// config the user writes next. `vx migrate` with nothing to convert is
|
|
130
|
-
// an error: the user asked for a conversion that has no input.
|
|
131
|
-
const empty = plan.projects.length === 0
|
|
132
|
-
if (empty && opts.init !== true) {
|
|
133
|
-
throw new UserError(
|
|
134
|
-
`nothing to migrate: no ${source === 'package.json scripts' ? 'package.json scripts in any workspace member' : 'tasks in ' + source}`,
|
|
135
|
-
)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const files: { relPath: string; abs: string; contents: string }[] = []
|
|
139
|
-
for (const p of plan.projects) {
|
|
140
|
-
if (p.tasks.length === 0) continue
|
|
141
|
-
const abs = path.join(p.dir, 'vx.config.ts')
|
|
142
|
-
files.push({ relPath: relPosix(root, abs), abs, contents: renderConfigFile(source, p) })
|
|
143
|
-
}
|
|
144
|
-
for (const f of plan.extraFiles) {
|
|
145
|
-
files.push({ relPath: f.relPath, abs: path.join(root, f.relPath), contents: f.contents })
|
|
146
|
-
}
|
|
147
|
-
// A migrated workspace must declare its executor and cache — nothing is
|
|
148
|
-
// applied by default. Emit the workspace file unless the repo already has
|
|
149
|
-
// one in any supported extension.
|
|
150
|
-
const hasWorkspaceFile = (
|
|
151
|
-
await Promise.all(
|
|
152
|
-
['vx.workspace.ts', 'vx.workspace.mjs', 'vx.workspace.js'].map((n) =>
|
|
153
|
-
Bun.file(path.join(root, n)).exists(),
|
|
154
|
-
),
|
|
155
|
-
)
|
|
156
|
-
).some(Boolean)
|
|
157
|
-
if (!hasWorkspaceFile) {
|
|
158
|
-
const abs = path.join(root, 'vx.workspace.ts')
|
|
159
|
-
files.push({ relPath: relPosix(root, abs), abs, contents: WORKSPACE_FILE })
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (!parsed.dry && !parsed.force) {
|
|
163
|
-
const conflicts = new Set<string>()
|
|
164
|
-
// A discovered project with ANY existing vx config (.ts/.mjs/.js) — refuse
|
|
165
|
-
// so we never shadow a hand-written config with a fresh .ts.
|
|
166
|
-
for (const p of plan.projects) {
|
|
167
|
-
if (p.tasks.length === 0) continue
|
|
168
|
-
const meta = metas.find((m) => m.dir === p.dir)
|
|
169
|
-
if (meta?.configPath) conflicts.add(relPosix(root, meta.configPath))
|
|
170
|
-
}
|
|
171
|
-
// ALSO stat every actual write target. A SYNTHESIZED project (e.g. the Nx
|
|
172
|
-
// workspace-root node, dir === root) has no discovered meta, so the meta
|
|
173
|
-
// scan alone would miss an existing vx.config.ts at that path and clobber
|
|
174
|
-
// it. This also covers the extraFiles (vx-preset.ts).
|
|
175
|
-
for (const f of files) {
|
|
176
|
-
if (await Bun.file(f.abs).exists()) conflicts.add(f.relPath)
|
|
177
|
-
}
|
|
178
|
-
if (conflicts.size > 0) {
|
|
179
|
-
throw new UserError(
|
|
180
|
-
'refusing to overwrite existing files (pass --force to overwrite):\n' +
|
|
181
|
-
` ${[...conflicts].join('\n ')}`,
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (parsed.dry) {
|
|
187
|
-
for (const f of files) {
|
|
188
|
-
process.stdout.write(`── ${f.relPath} ──\n${f.contents}\n`)
|
|
189
|
-
}
|
|
190
|
-
} else {
|
|
191
|
-
for (const f of files) await Bun.write(f.abs, f.contents)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const todoList: string[] = []
|
|
195
|
-
let clean = 0
|
|
196
|
-
for (const p of plan.projects) {
|
|
197
|
-
for (const t of p.tasks) {
|
|
198
|
-
if (t.todos.length === 0 && t.task !== null) clean++
|
|
199
|
-
for (const reason of t.todos) todoList.push(`${p.name}#${t.name}: ${reason}`)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
const report: string[] = []
|
|
203
|
-
if (empty) {
|
|
204
|
-
report.push(
|
|
205
|
-
'vx init: no package.json scripts to turn into tasks.',
|
|
206
|
-
hasWorkspaceFile
|
|
207
|
-
? 'vx.workspace.ts already exists.'
|
|
208
|
-
: parsed.dry
|
|
209
|
-
? 'would write vx.workspace.ts (dry run, nothing written).'
|
|
210
|
-
: 'wrote vx.workspace.ts.',
|
|
211
|
-
'Declare tasks in a vx.config.ts beside a package.json — your own command, for example:',
|
|
212
|
-
'',
|
|
213
|
-
...EXAMPLE_CONFIG.trimEnd()
|
|
214
|
-
.split('\n')
|
|
215
|
-
.map((l) => ` ${l}`),
|
|
216
|
-
)
|
|
217
|
-
} else {
|
|
218
|
-
report.push(`vx migrate: ${source} → vx.config.ts`)
|
|
219
|
-
for (const n of plan.headerNotes) report.push(`note: ${n}`)
|
|
220
|
-
report.push(
|
|
221
|
-
'',
|
|
222
|
-
`${clean} task${clean === 1 ? '' : 's'} migrated clean, ` +
|
|
223
|
-
`${todoList.length} TODO${todoList.length === 1 ? '' : 's'}${todoList.length > 0 ? ':' : ''}`,
|
|
224
|
-
)
|
|
225
|
-
for (const line of todoList) report.push(` ${line}`)
|
|
226
|
-
report.push(...plan.notes)
|
|
227
|
-
report.push(parsed.dry ? 'files (dry run, nothing written):' : 'files written:')
|
|
228
|
-
for (const f of files) report.push(` ${f.relPath}`)
|
|
229
|
-
}
|
|
230
|
-
const firstTask =
|
|
231
|
-
plan.projects.flatMap((p) => p.tasks.map((t) => t.name)).find((n) => n === 'build') ??
|
|
232
|
-
plan.projects[0]?.tasks[0]?.name ??
|
|
233
|
-
'build'
|
|
234
|
-
report.push('', `next: vx run ${firstTask} --all`)
|
|
235
|
-
process.stdout.write(`${report.join('\n')}\n`)
|
|
236
|
-
return 0
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const EXAMPLE_CONFIG = `import type { ProjectConfig } from '@vzn/vx'
|
|
240
|
-
|
|
241
|
-
export default {
|
|
242
|
-
tasks: {
|
|
243
|
-
build: {
|
|
244
|
-
exec: { command: 'tsc -b' },
|
|
245
|
-
dependsOn: ['^build'],
|
|
246
|
-
cache: { inputs: { files: ['src/**'] }, outputs: { files: ['dist/**'] } },
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
} satisfies ProjectConfig
|
|
250
|
-
`
|
|
251
|
-
|
|
252
|
-
// ─── TS emission ──────────────────────────────────────────────────────
|
|
253
|
-
|
|
254
|
-
const WORKSPACE_FILE = `import { defineWorkspace } from '@vzn/vx'
|
|
255
|
-
import { localExecutorPlugin } from '@vzn/vx/plugins/local-executor'
|
|
256
|
-
import { localCachePlugin } from '@vzn/vx/plugins/local-cache'
|
|
257
|
-
|
|
258
|
-
// Nothing runs that is not declared here. Put a remote executor or cache
|
|
259
|
-
// plugin BEFORE the local one to prefer it.
|
|
260
|
-
export default defineWorkspace({ plugins: [localExecutorPlugin(), localCachePlugin()] })
|
|
261
|
-
`
|
|
262
|
-
|
|
263
|
-
const IDENT = /^[A-Za-z_$][\w$]*$/
|
|
264
|
-
|
|
265
|
-
function isRawExpr(v: unknown): v is RawExpr {
|
|
266
|
-
return typeof v === 'object' && v !== null && typeof (v as RawExpr).raw === 'string'
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function renderValue(v: unknown, indent: string): string {
|
|
270
|
-
if (isRawExpr(v)) return v.raw
|
|
271
|
-
// `Object.entries(null)` throws, which would abort the whole migration
|
|
272
|
-
// with a raw stack mid-write. Mappers no longer produce a null, but a
|
|
273
|
-
// literal is a readable thing to leave behind if one ever does.
|
|
274
|
-
if (v === null) return 'null'
|
|
275
|
-
if (typeof v === 'string') return quote(v)
|
|
276
|
-
if (typeof v === 'number' || typeof v === 'boolean') return String(v)
|
|
277
|
-
if (Array.isArray(v)) return `[${v.map((x) => renderValue(x, indent)).join(', ')}]`
|
|
278
|
-
const entries = Object.entries(v as Record<string, unknown>).filter(([, x]) => x !== undefined)
|
|
279
|
-
if (entries.length === 0) return '{}'
|
|
280
|
-
const inner = `${indent} `
|
|
281
|
-
const body = entries.map(
|
|
282
|
-
([k, x]) => `${inner}${IDENT.test(k) ? k : quote(k)}: ${renderValue(x, inner)},`,
|
|
283
|
-
)
|
|
284
|
-
return `{\n${body.join('\n')}\n${indent}}`
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function renderConfigFile(source: string, p: GeneratedProject): string {
|
|
288
|
-
// A type-only import: the editor type-checks against the installed
|
|
289
|
-
// package, and Bun erases it, so the config loads in a workspace that
|
|
290
|
-
// runs the vx binary without the package installed.
|
|
291
|
-
const lines: string[] = [
|
|
292
|
-
`// Generated by \`vx migrate\` from ${source}. Review the TODO(vx-migrate) comments.`,
|
|
293
|
-
"import type { ProjectConfig } from '@vzn/vx'",
|
|
294
|
-
]
|
|
295
|
-
if (p.importLines.length > 0) lines.push(...p.importLines)
|
|
296
|
-
lines.push('', 'export default {', ' tasks: {')
|
|
297
|
-
for (const t of p.tasks) {
|
|
298
|
-
for (const todo of t.todos) lines.push(` // TODO(vx-migrate): ${todo}`)
|
|
299
|
-
if (t.task === null) continue // skipped target — the TODO above explains
|
|
300
|
-
const key = IDENT.test(t.name) ? t.name : quote(t.name)
|
|
301
|
-
lines.push(` ${key}: ${renderValue(t.task, ' ')},`)
|
|
302
|
-
}
|
|
303
|
-
lines.push(' },', '} satisfies ProjectConfig', '')
|
|
304
|
-
return lines.join('\n')
|
|
305
|
-
}
|