@vzn/vx 0.0.18 → 0.0.21

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 (121) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +99 -65
  3. package/package.json +10 -20
  4. package/src/bin.ts +12 -2
  5. package/src/cache/archive.ts +120 -18
  6. package/src/cache/cache.ts +313 -1324
  7. package/src/cache/chained-cache.ts +0 -8
  8. package/src/cache/config-evals.ts +89 -0
  9. package/src/cache/file-hashes.ts +262 -0
  10. package/src/cache/git-inputs.ts +637 -0
  11. package/src/cache/index.ts +16 -7
  12. package/src/cache/inputs.ts +246 -733
  13. package/src/cache/layer.ts +604 -0
  14. package/src/cache/layered-cache.ts +12 -17
  15. package/src/cache/output-index.ts +224 -0
  16. package/src/cache/policy.ts +84 -0
  17. package/src/cache/run-history.ts +147 -0
  18. package/src/cache/zstd.ts +160 -0
  19. package/src/cli/cache.ts +39 -11
  20. package/src/cli/completions.ts +104 -0
  21. package/src/cli/core-alias.ts +25 -0
  22. package/src/cli/help.ts +64 -36
  23. package/src/cli/index.ts +54 -15
  24. package/src/cli/info.ts +120 -91
  25. package/src/cli/init.ts +75 -0
  26. package/src/cli/last.ts +38 -6
  27. package/src/cli/lock.ts +2 -1
  28. package/src/cli/plan-format.ts +14 -3
  29. package/src/cli/plugin-commands.ts +15 -8
  30. package/src/cli/run.ts +84 -302
  31. package/src/cli/select.ts +332 -0
  32. package/src/cli/show.ts +144 -69
  33. package/src/cli/upgrade.ts +2 -1
  34. package/src/cli/watch.ts +647 -97
  35. package/src/cli/why.ts +44 -14
  36. package/src/cli/workspace-config.ts +126 -0
  37. package/src/config.ts +156 -136
  38. package/src/exec/env.ts +11 -0
  39. package/src/exec/executor.ts +11 -26
  40. package/src/exec/index.ts +6 -2
  41. package/src/{plugins/local-executor/index.ts → exec/local-executor.ts} +12 -16
  42. package/src/exec/runner.ts +57 -18
  43. package/src/exec/sandbox-binds.ts +182 -0
  44. package/src/exec/sandbox-paths.ts +50 -0
  45. package/src/exec/sandbox-runtime.ts +582 -370
  46. package/src/exec/sandbox-violations.ts +280 -0
  47. package/src/graph/index.ts +1 -9
  48. package/src/graph/scheduler.ts +158 -203
  49. package/src/graph/task-graph.ts +2 -2
  50. package/src/index.ts +97 -102
  51. package/src/orchestrator/admission.ts +137 -0
  52. package/src/orchestrator/deferred-outputs.ts +3 -0
  53. package/src/orchestrator/doctor.ts +255 -0
  54. package/src/orchestrator/events.ts +42 -8
  55. package/src/orchestrator/execute-task.ts +145 -554
  56. package/src/orchestrator/failure-mode.ts +169 -28
  57. package/src/orchestrator/framed-output.ts +41 -36
  58. package/src/orchestrator/history.ts +118 -98
  59. package/src/orchestrator/hit-restore.ts +221 -0
  60. package/src/orchestrator/index.ts +35 -4
  61. package/src/orchestrator/lockfile-claim.ts +281 -0
  62. package/src/orchestrator/logger.ts +13 -22
  63. package/src/orchestrator/metrics.ts +53 -20
  64. package/src/orchestrator/miss-save.ts +169 -0
  65. package/src/orchestrator/options.ts +34 -31
  66. package/src/orchestrator/persistent.ts +79 -0
  67. package/src/orchestrator/placement.ts +182 -0
  68. package/src/orchestrator/plugin-host.ts +195 -38
  69. package/src/orchestrator/plugin.ts +162 -9
  70. package/src/orchestrator/prepare.ts +77 -132
  71. package/src/orchestrator/projects.ts +259 -0
  72. package/src/orchestrator/remote-prefetch.ts +1 -1
  73. package/src/orchestrator/run-artifacts.ts +25 -3
  74. package/src/orchestrator/run-context.ts +1 -1
  75. package/src/orchestrator/run-records.ts +149 -0
  76. package/src/orchestrator/run-report.ts +2 -2
  77. package/src/orchestrator/run.ts +246 -597
  78. package/src/orchestrator/sandbox-request.ts +243 -0
  79. package/src/orchestrator/save-lane.ts +47 -0
  80. package/src/orchestrator/signals.ts +111 -0
  81. package/src/orchestrator/stable-keys.ts +60 -3
  82. package/src/orchestrator/status-line.ts +2 -1
  83. package/src/orchestrator/summary.ts +68 -25
  84. package/src/orchestrator/task-hash.ts +20 -18
  85. package/src/orchestrator/task-log-buffer.ts +8 -11
  86. package/src/orchestrator/telemetry-host.ts +11 -1
  87. package/src/orchestrator/telemetry.ts +3 -17
  88. package/src/util/cgroup.ts +160 -0
  89. package/src/util/edit-distance.ts +49 -0
  90. package/src/util/index.ts +11 -3
  91. package/src/util/paths.ts +23 -2
  92. package/src/util/settle.ts +19 -0
  93. package/src/util/verbs.ts +39 -0
  94. package/src/workspace/affected.ts +111 -11
  95. package/src/workspace/config-cache.ts +11 -2
  96. package/src/workspace/config-eval.ts +11 -4
  97. package/src/workspace/config-imports.ts +5 -4
  98. package/src/workspace/config-schema.ts +837 -0
  99. package/src/workspace/filter.ts +43 -6
  100. package/src/workspace/fingerprint.ts +37 -3
  101. package/src/workspace/index.ts +30 -3
  102. package/src/workspace/lockfile.ts +12 -7
  103. package/src/{cli → workspace}/migrate-scripts.ts +26 -19
  104. package/src/{cli/migrate.ts → workspace/migration.ts} +133 -124
  105. package/src/workspace/package-graph.ts +98 -28
  106. package/src/workspace/project-loader.ts +76 -746
  107. package/src/workspace/workspace.ts +98 -13
  108. package/plugins/local-cache/index.ts +0 -2
  109. package/plugins/local-executor/index.ts +0 -2
  110. package/plugins/schedule-history/index.ts +0 -2
  111. package/src/cache/cas-backend.ts +0 -115
  112. package/src/cache/digest.ts +0 -32
  113. package/src/cli/migrate-emit.ts +0 -21
  114. package/src/cli/migrate-nx.ts +0 -473
  115. package/src/cli/migrate-turbo.ts +0 -401
  116. package/src/cli/prune.ts +0 -291
  117. package/src/orchestrator/missing-plugin.ts +0 -9
  118. package/src/orchestrator/resources.ts +0 -45
  119. package/src/orchestrator/verify.ts +0 -174
  120. package/src/plugins/local-cache/index.ts +0 -11
  121. 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
- }