@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.
Files changed (149) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -70
  3. package/package.json +10 -20
  4. package/src/bin.ts +64 -5
  5. package/src/cache/archive.ts +279 -28
  6. package/src/cache/cache.ts +952 -1584
  7. package/src/cache/chained-cache.ts +14 -10
  8. package/src/cache/config-evals.ts +112 -0
  9. package/src/cache/file-hashes.ts +290 -0
  10. package/src/cache/git-inputs.ts +966 -0
  11. package/src/cache/index.ts +23 -7
  12. package/src/cache/inputs.ts +553 -739
  13. package/src/cache/key-fold.ts +232 -0
  14. package/src/cache/layer.ts +674 -0
  15. package/src/cache/layered-cache.ts +211 -49
  16. package/src/cache/output-index.ts +323 -0
  17. package/src/cache/policy.ts +84 -0
  18. package/src/cache/run-history.ts +152 -0
  19. package/src/cache/zstd.ts +183 -0
  20. package/src/cli/cache.ts +88 -30
  21. package/src/cli/completions.ts +104 -0
  22. package/src/cli/core-alias.ts +25 -0
  23. package/src/cli/format.ts +4 -15
  24. package/src/cli/help.ts +76 -38
  25. package/src/cli/index.ts +82 -17
  26. package/src/cli/info.ts +162 -91
  27. package/src/cli/init.ts +87 -0
  28. package/src/cli/last.ts +130 -23
  29. package/src/cli/lock.ts +36 -18
  30. package/src/cli/plan-format.ts +18 -7
  31. package/src/cli/plugin-commands.ts +34 -12
  32. package/src/cli/run.ts +138 -313
  33. package/src/cli/select.ts +342 -0
  34. package/src/cli/show.ts +151 -71
  35. package/src/cli/upgrade.ts +136 -15
  36. package/src/cli/watch.ts +1175 -116
  37. package/src/cli/why.ts +51 -29
  38. package/src/cli/workspace-config.ts +138 -0
  39. package/src/config.ts +183 -126
  40. package/src/exec/env.ts +11 -0
  41. package/src/exec/executor.ts +62 -31
  42. package/src/exec/index.ts +16 -2
  43. package/src/exec/kill-tree.ts +281 -0
  44. package/src/exec/local-executor.ts +41 -0
  45. package/src/exec/runner.ts +361 -77
  46. package/src/exec/sandbox-binds.ts +211 -0
  47. package/src/exec/sandbox-paths.ts +70 -0
  48. package/src/exec/sandbox-runtime.ts +1152 -415
  49. package/src/exec/sandbox-violations.ts +293 -0
  50. package/src/graph/dependency-spec.ts +14 -14
  51. package/src/graph/index.ts +6 -4
  52. package/src/graph/priorities.ts +130 -0
  53. package/src/graph/scheduler.ts +332 -329
  54. package/src/graph/task-graph.ts +552 -188
  55. package/src/index.ts +111 -102
  56. package/src/orchestrator/admission.ts +161 -0
  57. package/src/orchestrator/colors.ts +8 -3
  58. package/src/orchestrator/deferred-outputs.ts +25 -7
  59. package/src/orchestrator/doctor.ts +363 -0
  60. package/src/orchestrator/events.ts +111 -8
  61. package/src/orchestrator/excluded-keys.ts +155 -0
  62. package/src/orchestrator/execute-task.ts +450 -565
  63. package/src/orchestrator/failure-mode.ts +169 -28
  64. package/src/orchestrator/failure-recap.ts +121 -0
  65. package/src/orchestrator/fingerprint-watch.ts +89 -0
  66. package/src/orchestrator/framed-output.ts +121 -49
  67. package/src/orchestrator/history.ts +123 -104
  68. package/src/orchestrator/hit-restore.ts +285 -0
  69. package/src/orchestrator/index.ts +42 -5
  70. package/src/orchestrator/keyed-projects.ts +89 -0
  71. package/src/orchestrator/local-shortcircuit.ts +83 -23
  72. package/src/orchestrator/lockfile-claim.ts +401 -0
  73. package/src/orchestrator/logger.ts +128 -41
  74. package/src/orchestrator/metrics.ts +201 -72
  75. package/src/orchestrator/miss-save.ts +232 -0
  76. package/src/orchestrator/options.ts +63 -34
  77. package/src/orchestrator/persistent.ts +99 -0
  78. package/src/orchestrator/placement.ts +212 -0
  79. package/src/orchestrator/plan.ts +24 -11
  80. package/src/orchestrator/plugin-host.ts +200 -49
  81. package/src/orchestrator/plugin.ts +190 -14
  82. package/src/orchestrator/prepare.ts +168 -137
  83. package/src/orchestrator/projects.ts +270 -0
  84. package/src/orchestrator/remote-prefetch.ts +22 -2
  85. package/src/orchestrator/run-artifacts.ts +34 -3
  86. package/src/orchestrator/run-context.ts +20 -5
  87. package/src/orchestrator/run-lock.ts +349 -0
  88. package/src/orchestrator/run-records.ts +132 -0
  89. package/src/orchestrator/run-report.ts +6 -4
  90. package/src/orchestrator/run.ts +467 -611
  91. package/src/orchestrator/sandbox-request.ts +569 -0
  92. package/src/orchestrator/save-lane.ts +44 -0
  93. package/src/orchestrator/shell-verdict.ts +117 -0
  94. package/src/orchestrator/signals.ts +167 -0
  95. package/src/orchestrator/stable-keys.ts +259 -10
  96. package/src/orchestrator/status-line.ts +35 -2
  97. package/src/orchestrator/summary.ts +115 -25
  98. package/src/orchestrator/task-hash.ts +103 -21
  99. package/src/orchestrator/task-log-buffer.ts +15 -18
  100. package/src/orchestrator/telemetry-host.ts +27 -8
  101. package/src/orchestrator/telemetry.ts +71 -44
  102. package/src/orchestrator/upstream.ts +91 -45
  103. package/src/util/bun-version.ts +43 -0
  104. package/src/util/cgroup.ts +160 -0
  105. package/src/util/edit-distance.ts +49 -0
  106. package/src/util/errors.ts +86 -0
  107. package/src/util/hash.ts +10 -1
  108. package/src/util/index.ts +42 -5
  109. package/src/util/paths.ts +168 -5
  110. package/src/util/procfs.ts +20 -0
  111. package/src/util/settle.ts +23 -2
  112. package/src/util/size.ts +33 -0
  113. package/src/util/task-id.ts +20 -0
  114. package/src/util/timing.ts +11 -1
  115. package/src/util/verbs.ts +39 -0
  116. package/src/util/which.ts +48 -0
  117. package/src/workspace/affected.ts +368 -60
  118. package/src/workspace/config-cache.ts +271 -32
  119. package/src/workspace/config-eval.ts +60 -16
  120. package/src/workspace/config-imports.ts +131 -9
  121. package/src/workspace/config-schema.ts +1033 -0
  122. package/src/workspace/filter.ts +69 -17
  123. package/src/workspace/fingerprint.ts +59 -7
  124. package/src/workspace/index.ts +37 -3
  125. package/src/workspace/json-data.ts +90 -0
  126. package/src/workspace/load-reads.ts +32 -0
  127. package/src/workspace/lockfile.ts +41 -14
  128. package/src/workspace/migrate-scripts.ts +187 -0
  129. package/src/workspace/migration.ts +353 -0
  130. package/src/workspace/package-graph.ts +239 -31
  131. package/src/workspace/project-loader.ts +295 -815
  132. package/src/workspace/workspace.ts +266 -31
  133. package/plugins/local-cache/index.ts +0 -2
  134. package/plugins/local-executor/index.ts +0 -2
  135. package/plugins/schedule-history/index.ts +0 -2
  136. package/src/cache/cas-backend.ts +0 -115
  137. package/src/cache/digest.ts +0 -32
  138. package/src/cli/migrate-emit.ts +0 -21
  139. package/src/cli/migrate-nx.ts +0 -473
  140. package/src/cli/migrate-scripts.ts +0 -127
  141. package/src/cli/migrate-turbo.ts +0 -401
  142. package/src/cli/migrate.ts +0 -305
  143. package/src/cli/prune.ts +0 -291
  144. package/src/orchestrator/missing-plugin.ts +0 -9
  145. package/src/orchestrator/resources.ts +0 -45
  146. package/src/orchestrator/verify.ts +0 -174
  147. package/src/plugins/local-cache/index.ts +0 -11
  148. package/src/plugins/local-executor/index.ts +0 -45
  149. package/src/plugins/schedule-history/index.ts +0 -109
@@ -1,473 +0,0 @@
1
- // Nx → vx mapping, from the resolved project-graph snapshot ONLY
2
- // (.nx/workspace-data/project-graph.json). Static by design: targets
3
- // that Nx plugins infer at runtime are frozen as the snapshot saw
4
- // them. Named inputs expand from nx.json when readable; the graph's
5
- // dependency edges are ignored (vx derives package edges from
6
- // package.json manifests) except for one report line counting edges
7
- // with no manifest counterpart.
8
-
9
- import path from 'node:path'
10
- import { relPosix, UserError } from '../util/index.js'
11
- import type { ProjectMeta } from '../workspace/index.js'
12
- import type { GeneratedProject, GeneratedTask, MigrationPlan } from './migrate.js'
13
-
14
- const PLACEHOLDER = "echo 'TODO(vx-migrate): fill in' && exit 1"
15
- const GRAPH_REL = '.nx/workspace-data/project-graph.json'
16
-
17
- interface NxTarget {
18
- executor?: string
19
- command?: string
20
- options?: Record<string, unknown>
21
- inputs?: unknown[]
22
- outputs?: string[]
23
- dependsOn?: unknown[]
24
- cache?: boolean
25
- }
26
-
27
- interface NxNode {
28
- name?: string
29
- data?: { root?: string; targets?: Record<string, NxTarget> }
30
- }
31
-
32
- type NxEdge = { source?: string; target?: string }
33
-
34
- function normRel(p: string): string {
35
- return p === '' || p === '.' ? '.' : p.replace(/\/$/, '')
36
- }
37
-
38
- export async function migrateNx(
39
- root: string,
40
- metas: readonly ProjectMeta[],
41
- ): Promise<MigrationPlan> {
42
- const graphPath = path.join(root, GRAPH_REL)
43
- let parsed: unknown
44
- try {
45
- parsed = JSON.parse(await Bun.file(graphPath).text())
46
- } catch (err) {
47
- const msg = err instanceof Error ? err.message : String(err)
48
- throw new UserError(`failed to parse ${GRAPH_REL}: ${msg}`)
49
- }
50
- // Both {graph:{nodes,dependencies}} and top-level {nodes,dependencies}
51
- // exist across nx versions.
52
- const g = ((parsed as { graph?: unknown }).graph ?? parsed) as {
53
- nodes?: unknown
54
- dependencies?: unknown
55
- }
56
- const nodes = g?.nodes
57
- if (typeof nodes !== 'object' || nodes === null) {
58
- throw new UserError(
59
- `${GRAPH_REL}: unrecognized shape — expected { graph: { nodes, dependencies } } ` +
60
- 'or { nodes, dependencies }',
61
- )
62
- }
63
- const nodeMap = nodes as Record<string, NxNode>
64
-
65
- const namedInputs = await readNamedInputs(root)
66
-
67
- const metaByRel = new Map<string, ProjectMeta>()
68
- for (const meta of metas) metaByRel.set(normRel(relPosix(root, meta.dir)), meta)
69
- const metaByNode = new Map<string, ProjectMeta>()
70
- const nodeByMeta = new Map<ProjectMeta, NxNode>()
71
- for (const [nodeName, node] of Object.entries(nodeMap)) {
72
- const meta = metaByRel.get(normRel(node?.data?.root ?? ''))
73
- if (meta) {
74
- metaByNode.set(nodeName, meta)
75
- nodeByMeta.set(meta, node)
76
- }
77
- }
78
-
79
- // Graph nodes with no discovered counterpart: the ROOT project is
80
- // the common case — `packages/*` discovery never lists the
81
- // workspace root, but Nx routinely defines root targets. Synthesize
82
- // a meta for any unmatched node whose root resolves inside the
83
- // workspace, so its targets migrate too.
84
- const allMetas: ProjectMeta[] = [...metas]
85
- for (const [nodeName, node] of Object.entries(nodeMap)) {
86
- if (metaByNode.has(nodeName)) continue
87
- const relRoot = normRel(node?.data?.root ?? '')
88
- if (node?.data?.targets === undefined) continue
89
- const dir = relRoot === '' || relRoot === '.' ? root : path.join(root, relRoot)
90
- let pkg: ProjectMeta['packageJson'] = { name: nodeName }
91
- try {
92
- pkg = (await Bun.file(path.join(dir, 'package.json')).json()) as ProjectMeta['packageJson']
93
- } catch {
94
- // No manifest at the node root — keep the synthetic one.
95
- }
96
- const synthetic: ProjectMeta = {
97
- name: pkg.name || nodeName,
98
- dir,
99
- packageJson: pkg,
100
- configPath: null,
101
- }
102
- allMetas.push(synthetic)
103
- metaByNode.set(nodeName, synthetic)
104
- nodeByMeta.set(synthetic, node)
105
- }
106
-
107
- const projects: GeneratedProject[] = []
108
- for (const meta of allMetas) {
109
- const node = nodeByMeta.get(meta)
110
- const targets = node?.data?.targets
111
- if (!targets) continue
112
- const tasks: GeneratedTask[] = []
113
- for (const [targetName, target] of Object.entries(targets)) {
114
- const t = buildTask(root, meta, targetName, target, namedInputs, metaByNode)
115
- // task === null → no vx representation; surface the reason in
116
- // the report but emit nothing into the config.
117
- if (t.task === null && t.todos.length > 0) {
118
- tasks.push(t)
119
- continue
120
- }
121
- tasks.push(t)
122
- }
123
- projects.push({ name: meta.name, dir: meta.dir, importLines: [], tasks })
124
- }
125
-
126
- const notes: string[] = []
127
- const implicit = countImplicitDeps(g?.dependencies, metaByNode)
128
- if (implicit > 0) {
129
- notes.push(
130
- `${implicit} implicit Nx dep${implicit === 1 ? '' : 's'} not representable; review dependsOn`,
131
- )
132
- }
133
-
134
- return {
135
- headerNotes: [
136
- 'migrating from the resolved project-graph snapshot — plugin-inferred targets ' +
137
- 'are frozen as static config',
138
- ],
139
- projects,
140
- extraFiles: [],
141
- notes,
142
- }
143
- }
144
-
145
- async function readNamedInputs(root: string): Promise<Record<string, unknown[]> | null> {
146
- const file = Bun.file(path.join(root, 'nx.json'))
147
- if (!(await file.exists())) return null
148
- try {
149
- const parsed = Bun.JSONC.parse(await file.text()) as { namedInputs?: unknown }
150
- const named = parsed?.namedInputs
151
- if (typeof named !== 'object' || named === null) return null
152
- return named as Record<string, unknown[]>
153
- } catch {
154
- // Unreadable nx.json just degrades named-input refs to TODOs.
155
- return null
156
- }
157
- }
158
-
159
- function buildTask(
160
- root: string,
161
- meta: ProjectMeta,
162
- targetName: string,
163
- target: NxTarget,
164
- namedInputs: Record<string, unknown[]> | null,
165
- metaByNode: ReadonlyMap<string, ProjectMeta>,
166
- ): GeneratedTask {
167
- const todos: string[] = []
168
- const options = target.options ?? {}
169
- const projectRel = normRel(relPosix(root, meta.dir))
170
- const scripts =
171
- (meta.packageJson as unknown as { scripts?: Record<string, string> }).scripts ?? {}
172
-
173
- const command = mapCommand(targetName, target, options, projectRel, scripts, todos)
174
-
175
- const files: string[] = []
176
- const wsFiles: string[] = []
177
- const envNames: string[] = []
178
- const expandInput = (entry: unknown, seen: Set<string>): void => {
179
- if (typeof entry === 'string') {
180
- let s = entry
181
- let neg = ''
182
- if (s.startsWith('!')) {
183
- neg = '!'
184
- s = s.slice(1)
185
- }
186
- if (s.startsWith('{projectRoot}/')) {
187
- files.push(neg + s.slice('{projectRoot}/'.length))
188
- return
189
- }
190
- if (s.startsWith('{workspaceRoot}/')) {
191
- wsFiles.push(neg + s.slice('{workspaceRoot}/'.length))
192
- return
193
- }
194
- if (s.startsWith('^')) {
195
- todos.push(
196
- `deps-input ${JSON.stringify(entry)}: vx folds upstream outputs into the cache ` +
197
- 'key via dependsOn automatically — usually safe to drop',
198
- )
199
- return
200
- }
201
- if (s.includes('{')) {
202
- todos.push(`input ${JSON.stringify(entry)} uses a token vx does not support`)
203
- return
204
- }
205
- // Bare string = named-input reference.
206
- const named = namedInputs?.[s]
207
- if (named === undefined) {
208
- todos.push(
209
- `named input ${JSON.stringify(s)} not found in nx.json — declare its globs manually`,
210
- )
211
- return
212
- }
213
- if (seen.has(s)) return
214
- seen.add(s)
215
- for (const e of named) expandInput(e, seen)
216
- return
217
- }
218
- if (entry && typeof entry === 'object') {
219
- const o = entry as Record<string, unknown>
220
- if (typeof o.env === 'string') {
221
- envNames.push(o.env)
222
- return
223
- }
224
- if (typeof o.fileset === 'string') {
225
- expandInput(o.fileset, seen)
226
- return
227
- }
228
- if (o.externalDependencies !== undefined) {
229
- todos.push(
230
- `input {externalDependencies: ${JSON.stringify(o.externalDependencies)}}: vx hashes ` +
231
- "the project's package.json into every key — usually safe to drop",
232
- )
233
- return
234
- }
235
- if (o.dependentTasksOutputFiles !== undefined) {
236
- todos.push(
237
- 'input {dependentTasksOutputFiles: …}: vx folds upstream outputs into the cache ' +
238
- 'key via dependsOn automatically',
239
- )
240
- return
241
- }
242
- if (typeof o.input === 'string') {
243
- if (o.dependencies === true || o.projects !== undefined) {
244
- todos.push(
245
- `deps-input ${JSON.stringify(entry)}: vx folds upstream via dependsOn automatically`,
246
- )
247
- return
248
- }
249
- expandInput(o.input, seen)
250
- return
251
- }
252
- }
253
- todos.push(`input ${JSON.stringify(entry)} not representable in vx`)
254
- }
255
- for (const entry of target.inputs ?? []) expandInput(entry, new Set())
256
-
257
- const outFiles: string[] = []
258
- const wsOutFiles: string[] = []
259
- // Heuristic: a bare directory path captures its whole subtree.
260
- const dirGlob = (rel: string): string => {
261
- const last = rel.split('/').at(-1)!
262
- return !rel.includes('*') && !last.includes('.') ? `${rel}/**` : rel
263
- }
264
- const pushOut = (rel: string): void => {
265
- outFiles.push(dirGlob(rel))
266
- }
267
- for (const o of target.outputs ?? []) {
268
- let s = o
269
- const optTok = /\{options\.([^}]+)\}/.exec(s)
270
- if (optTok) {
271
- const v = options[optTok[1]!]
272
- if (typeof v !== 'string') {
273
- todos.push(
274
- `output ${JSON.stringify(o)}: option ${JSON.stringify(optTok[1])} is not a literal ` +
275
- 'string — resolve manually',
276
- )
277
- continue
278
- }
279
- s = s.replace(optTok[0], v)
280
- }
281
- if (s.startsWith('{projectRoot}/')) {
282
- pushOut(s.slice('{projectRoot}/'.length))
283
- continue
284
- }
285
- if (s.startsWith('{workspaceRoot}/')) {
286
- wsOutFiles.push(dirGlob(s.slice('{workspaceRoot}/'.length)))
287
- continue
288
- }
289
- if (s.includes('{')) {
290
- todos.push(`output ${JSON.stringify(o)} uses a token vx does not support`)
291
- continue
292
- }
293
- // Plain paths resolve against the workspace root in nx.
294
- if (projectRel === '.') pushOut(s)
295
- else if (s.startsWith(`${projectRel}/`)) pushOut(s.slice(projectRel.length + 1))
296
- else {
297
- todos.push(
298
- `output ${JSON.stringify(o)} falls outside the project dir — declare it in ` +
299
- 'cache.outputs.workspaceFiles (workspace-root-relative) if intended',
300
- )
301
- }
302
- }
303
-
304
- const deps: string[] = []
305
- for (const d of target.dependsOn ?? []) {
306
- if (typeof d === 'string') {
307
- deps.push(d)
308
- continue
309
- }
310
- if (d && typeof d === 'object') {
311
- const o = d as Record<string, unknown>
312
- const t = typeof o.target === 'string' ? o.target : undefined
313
- if (t === undefined) {
314
- todos.push(`dependsOn ${JSON.stringify(d)} has no target — dropped`)
315
- continue
316
- }
317
- if (o.params !== undefined) {
318
- todos.push(
319
- `dependsOn ${JSON.stringify(t)}: params forwarding is not supported — forward args ` +
320
- 'via `vx run … -- args` instead',
321
- )
322
- }
323
- const projects = o.projects ?? (o.dependencies === true ? 'dependencies' : undefined)
324
- if (projects === undefined || projects === 'self') deps.push(t)
325
- else if (projects === 'dependencies') deps.push(`^${t}`)
326
- else if (Array.isArray(projects)) {
327
- for (const p of projects) {
328
- const m = typeof p === 'string' ? metaByNode.get(p) : undefined
329
- if (m) deps.push(`${m.name}#${t}`)
330
- else {
331
- todos.push(
332
- `dependsOn project ${JSON.stringify(p)} is not a workspace package — edge dropped`,
333
- )
334
- }
335
- }
336
- } else todos.push(`dependsOn ${JSON.stringify(d)} not representable in vx`)
337
- continue
338
- }
339
- todos.push(`dependsOn ${JSON.stringify(d)} not representable in vx`)
340
- }
341
-
342
- const cacheEnabled =
343
- target.cache === true ||
344
- (target.cache === undefined && (target.inputs !== undefined || target.outputs !== undefined))
345
-
346
- if (command === null) {
347
- // nx:noop → vx group task: dependsOn only, no exec, no cache
348
- // (vx forbids cache on groups). A noop with nothing to chain has
349
- // no vx representation — skipped with a report line.
350
- if (deps.length === 0) {
351
- todos.push('nx:noop target with no dependsOn — nothing to represent; skipped')
352
- return { name: targetName, task: null, todos }
353
- }
354
- return { name: targetName, task: { dependsOn: deps }, todos }
355
- }
356
-
357
- const exec: Record<string, unknown> = { command }
358
- if (envNames.length > 0) exec.env = { passThrough: envNames }
359
- const task: Record<string, unknown> = { exec }
360
- if (deps.length > 0) task.dependsOn = deps
361
- if (cacheEnabled) {
362
- if (target.inputs === undefined && files.length === 0) {
363
- files.push('**/*')
364
- todos.push(
365
- "cache enabled with no declared inputs — defaulting to ['**/*']; narrow to the real input set",
366
- )
367
- }
368
- const inputs: Record<string, unknown> = { files }
369
- if (wsFiles.length > 0) inputs.workspaceFiles = wsFiles
370
- if (envNames.length > 0) inputs.env = envNames
371
- const outputs: Record<string, unknown> = { files: outFiles }
372
- if (wsOutFiles.length > 0) outputs.workspaceFiles = wsOutFiles
373
- task.cache = { inputs, outputs }
374
- }
375
-
376
- return { name: targetName, todos, task }
377
- }
378
-
379
- function mapCommand(
380
- targetName: string,
381
- target: NxTarget,
382
- options: Record<string, unknown>,
383
- projectRel: string,
384
- scripts: Record<string, string>,
385
- todos: string[],
386
- ): string | null {
387
- const executor = target.executor
388
- if (executor === 'nx:noop') {
389
- // No command by definition — the vx equivalent is a group task
390
- // (handled by the caller; nothing to map here).
391
- return null
392
- }
393
- if (executor === 'nx:run-commands') {
394
- if (typeof options.cwd === 'string' && normRel(options.cwd) !== projectRel) {
395
- todos.push(
396
- `run-commands cwd ${JSON.stringify(options.cwd)} differs from the project root — vx ` +
397
- 'runs commands from the project dir; adjust the command',
398
- )
399
- }
400
- const cmds = options.commands
401
- if (Array.isArray(cmds) && cmds.length > 0) {
402
- const parts = cmds
403
- .map((c) => (typeof c === 'string' ? c : ((c as { command?: unknown }).command as string)))
404
- .filter((c): c is string => typeof c === 'string' && c.length > 0)
405
- if (parts.length > 0) return parts.join(' && ')
406
- }
407
- if (typeof options.command === 'string') return options.command
408
- todos.push(`nx:run-commands target has no command — options: ${JSON.stringify(options)}`)
409
- return PLACEHOLDER
410
- }
411
- if (executor === 'nx:run-script') {
412
- const script = typeof options.script === 'string' ? options.script : targetName
413
- const body = scripts[script]
414
- if (body !== undefined) return body
415
- todos.push(`nx:run-script: package.json has no ${JSON.stringify(script)} script`)
416
- return PLACEHOLDER
417
- }
418
- if (executor === undefined && typeof target.command === 'string') return target.command
419
- // The common executors wrap one CLI each; the task runs on the first
420
- // try and the TODO still asks for a look, since executor options
421
- // (config paths, watch flags) are not carried over.
422
- const known = executor === undefined ? undefined : KNOWN_EXECUTORS[executor]
423
- if (known !== undefined) {
424
- todos.push(
425
- `command ${JSON.stringify(known)} mapped from executor ${JSON.stringify(executor)} — ` +
426
- `verify it against the executor's options: ${JSON.stringify(options)}`,
427
- )
428
- return known
429
- }
430
- todos.push(
431
- `executor ${JSON.stringify(executor ?? '(none)')} has no shell equivalent — fill in the ` +
432
- `CLI command. options: ${JSON.stringify(options)}`,
433
- )
434
- return PLACEHOLDER
435
- }
436
-
437
- /** Nx executors whose CLI is unambiguous; anything else is a placeholder. */
438
- const KNOWN_EXECUTORS: Record<string, string> = {
439
- '@nx/vite:build': 'vite build',
440
- '@nx/vite:dev-server': 'vite',
441
- '@nx/vite:preview-server': 'vite preview',
442
- '@nx/vite:test': 'vitest run',
443
- '@nx/vitest:test': 'vitest run',
444
- '@nx/jest:jest': 'jest',
445
- '@nx/eslint:lint': 'eslint .',
446
- '@nx/js:tsc': 'tsc -p tsconfig.json',
447
- }
448
-
449
- function countImplicitDeps(
450
- dependencies: unknown,
451
- metaByNode: ReadonlyMap<string, ProjectMeta>,
452
- ): number {
453
- if (typeof dependencies !== 'object' || dependencies === null) return 0
454
- let count = 0
455
- for (const [source, edges] of Object.entries(dependencies as Record<string, NxEdge[]>)) {
456
- const sm = metaByNode.get(source)
457
- if (!sm || !Array.isArray(edges)) continue
458
- const manifest = {
459
- ...sm.packageJson.dependencies,
460
- ...sm.packageJson.devDependencies,
461
- ...sm.packageJson.peerDependencies,
462
- ...sm.packageJson.optionalDependencies,
463
- }
464
- const seen = new Set<string>()
465
- for (const edge of edges) {
466
- const tm = typeof edge?.target === 'string' ? metaByNode.get(edge.target) : undefined
467
- if (!tm || tm === sm || seen.has(tm.name)) continue
468
- seen.add(tm.name)
469
- if (manifest[tm.name] === undefined) count++
470
- }
471
- }
472
- return count
473
- }
@@ -1,127 +0,0 @@
1
- // package.json scripts → vx tasks: the source for a workspace that comes
2
- // from nowhere (no turbo.json, no nx). `vx init` is this mapper.
3
- //
4
- // Scripts carry a command and nothing else, so the mapping is honest about
5
- // what it cannot know: every task gets `exec.command` verbatim; `build`
6
- // gets the conventional `dependsOn: ['^build']` and `test` / `lint` /
7
- // `typecheck` wait for `build` when the package has one; a dev-server
8
- // shaped script becomes persistent. Caching is opt-in and needs declared
9
- // inputs AND outputs, which a script cannot tell us — so `build` is
10
- // emitted with a cache block whose inputs are the whole project and whose
11
- // outputs are EMPTY, under a TODO naming what to fill in. A cache block
12
- // that guessed `dist/**` would restore the wrong tree for every package
13
- // that writes somewhere else, and a wrong restore is the worst failure
14
- // vx has.
15
- //
16
- // Two npm conventions are mapped rather than copied, because copying them
17
- // loses behaviour: `pre<x>` / `post<x>` hooks, which npm runs around `x`
18
- // without being named, are folded into `x`'s command in that order (a
19
- // standalone `prebuild` task is one `vx run build` never runs — and the
20
- // hook is usually `rimraf dist`); and a script that is nothing but
21
- // `<pm> run <other>` becomes a GROUP over `<other>`, so the graph sees the
22
- // dependency instead of a package-manager subprocess it cannot cache.
23
-
24
- import type { ProjectMeta } from '../workspace/index.js'
25
- import type { GeneratedProject, GeneratedTask, MigrationPlan } from './migrate.js'
26
-
27
- const PERSISTENT = new Set(['dev', 'start', 'serve', 'watch', 'preview'])
28
- const AFTER_BUILD = new Set(['test', 'lint', 'typecheck', 'check', 'e2e'])
29
- const LIFECYCLE = /^(pre|post)(install|publish|pack|version)$|^(prepare|prepublishOnly|install)$/
30
-
31
- /**
32
- * The script a command delegates to when it is NOTHING but a package-manager
33
- * invocation of another script: `npm run x`, `pnpm x`, `yarn run x`, `bun run
34
- * x`, plus npm's bare `npm test` / `npm start`. Flags, arguments or a chain
35
- * make it a real command again, which is left verbatim.
36
- */
37
- export function delegatedScript(command: string): string | null {
38
- const m =
39
- /^(?:(?:npm run|pnpm(?: run)?|yarn(?: run)?|bun(?: run)?) ([^\s&|;<>()$`'"\\]+)|npm (test|start))$/.exec(
40
- command.trim(),
41
- )
42
- return m === null ? null : (m[1] ?? m[2])!
43
- }
44
-
45
- function scriptsOf(meta: ProjectMeta): Record<string, unknown> {
46
- // package.json is a boundary: `scripts` is whatever the file holds. A
47
- // string or an array would enumerate its indices as script names.
48
- const raw = (meta.packageJson as unknown as { scripts?: unknown }).scripts
49
- if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return {}
50
- return raw as Record<string, unknown>
51
- }
52
-
53
- export function migrateScripts(metas: readonly ProjectMeta[]): MigrationPlan {
54
- const projects: GeneratedProject[] = []
55
- for (const meta of metas) {
56
- const scripts = scriptsOf(meta)
57
- const names = Object.keys(scripts).filter(
58
- (n) => typeof scripts[n] === 'string' && scripts[n] !== '',
59
- )
60
- if (names.length === 0) continue
61
- const hasBuild = names.includes('build')
62
- const has = (n: string): boolean => names.includes(n)
63
- const tasks: GeneratedTask[] = []
64
- for (const name of names) {
65
- // Lifecycle hooks are npm's, not tasks anyone runs by name.
66
- if (LIFECYCLE.test(name)) continue
67
- // A hook of a script that exists rides inside that script's command.
68
- const hookOf = /^(pre|post)(.+)$/.exec(name)
69
- if (hookOf !== null && has(hookOf[2]!) && !LIFECYCLE.test(hookOf[2]!)) continue
70
-
71
- const todos: string[] = []
72
- const own = scripts[name] as string
73
- const delegate = delegatedScript(own)
74
- // npm lifecycle hooks (`prepack`, `prepublishOnly`, …) belong to the
75
- // package manager and never ride inside a task — `pack` stays alone.
76
- const hook = (h: string): boolean => has(h) && !LIFECYCLE.test(h)
77
- const hooks = [`pre${name}`, `post${name}`].filter(hook)
78
- if (delegate !== null && has(delegate) && delegate !== name && hooks.length === 0) {
79
- // A group: no exec, the graph runs the target. `npm test` calling
80
- // `vitest` through `npm run test:unit` is two tasks, not a subprocess.
81
- const task: Record<string, unknown> = { dependsOn: [delegate] }
82
- if (AFTER_BUILD.has(name) && hasBuild && delegate !== 'build') {
83
- task['dependsOn'] = [delegate, 'build']
84
- }
85
- tasks.push({ name, todos, task })
86
- continue
87
- }
88
-
89
- const command = [
90
- ...(hook(`pre${name}`) ? [scripts[`pre${name}`] as string] : []),
91
- own,
92
- ...(hook(`post${name}`) ? [scripts[`post${name}`] as string] : []),
93
- ].join(' && ')
94
- if (hooks.length > 0) {
95
- todos.push(
96
- `npm ran ${hooks.map((h) => `\`${h}\``).join(' and ')} around this script without being asked; folded into the command in that order`,
97
- )
98
- }
99
- const task: Record<string, unknown> = { exec: { command } }
100
- if (PERSISTENT.has(name)) {
101
- task['exec'] = { command, persistent: {} }
102
- todos.push(
103
- 'persistent: add `readyWhen: "<line the server prints when ready>"` so dependents wait for it',
104
- )
105
- }
106
- if (name === 'build') {
107
- task['dependsOn'] = ['^build']
108
- task['cache'] = { inputs: { files: ['**/*'] }, outputs: { files: [] } }
109
- todos.push(
110
- "cache: inputs default to the whole project ('**/*'); narrow them (e.g. 'src/**') and declare outputs (e.g. 'dist/**') — nothing is cached until outputs are declared",
111
- )
112
- } else if (AFTER_BUILD.has(name) && hasBuild) {
113
- task['dependsOn'] = ['build']
114
- }
115
- tasks.push({ name, todos, task })
116
- }
117
- if (tasks.length > 0) projects.push({ name: meta.name, dir: meta.dir, importLines: [], tasks })
118
- }
119
- return {
120
- headerNotes: [
121
- 'each script became a task with its command verbatim; caching needs declared inputs and outputs, so only `build` got a cache block, with empty outputs to fill in',
122
- ],
123
- projects,
124
- extraFiles: [],
125
- notes: [],
126
- }
127
- }