@vzn/vx-migrate 0.0.646 → 0.0.649

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/src/nx/index.ts CHANGED
@@ -69,7 +69,8 @@ const ROOT_GRAPH_FILE =
69
69
  export interface NxPluginOptions {
70
70
  /**
71
71
  * Where `nx.json` lives. Defaults to the workspace root; a repo whose Nx
72
- * workspace sits elsewhere names the directory here.
72
+ * workspace sits elsewhere names the directory here, relative to the workspace
73
+ * root or absolute.
73
74
  */
74
75
  readonly root?: string
75
76
  /**
@@ -111,6 +112,11 @@ function refuseExecutors(executors: NxExecutors | undefined): void {
111
112
  /** The plugin: the adoption skeleton over `mapNxWorkspace`, one mapping per run. */
112
113
  export function nx(options: NxPluginOptions = {}): VxPlugin {
113
114
  refuseUnknownOptions('nx()', options, NX_PLUGIN_KEYS)
115
+ if (options.root === '') throw new UserError('nx() option "root" is a directory, not empty')
116
+ // Against the workspace root, as vx reads every path: against the cwd,
117
+ // `vx run` from a package dir read another directory.
118
+ const rootOf = (workspaceRoot: string): string =>
119
+ options.root === undefined ? workspaceRoot : path.resolve(workspaceRoot, options.root)
114
120
  refuseExecutors(options.executors)
115
121
  // One graph load per RUN, shared by `discover` and `project`: the run
116
122
  // hands both stages the same projects array (discover's, grown by what
@@ -134,7 +140,7 @@ export function nx(options: NxPluginOptions = {}): VxPlugin {
134
140
  return adoptionPlugin(
135
141
  import.meta,
136
142
  async (ctx) => {
137
- const root = options.root ?? ctx.workspaceRoot
143
+ const root = rootOf(ctx.workspaceRoot)
138
144
  return mapAll(
139
145
  root,
140
146
  ctx.projects,
@@ -146,7 +152,7 @@ export function nx(options: NxPluginOptions = {}): VxPlugin {
146
152
  options.root === undefined ? ['nx.json'] : [],
147
153
  {
148
154
  async config(workspace, ctx) {
149
- const root = options.root ?? ctx.workspaceRoot
155
+ const root = rootOf(ctx.workspaceRoot)
150
156
  const nxBaseCommit = Bun.env['NX_BASE']?.trim()
151
157
  if (nxBaseCommit) ctx.setBranch({ base: nxBaseCommit })
152
158
  if (workspace.baseBranch === undefined) {
@@ -174,7 +180,7 @@ export function nx(options: NxPluginOptions = {}): VxPlugin {
174
180
  // project, by its package.json name or else its Nx name; a name a
175
181
  // project already holds stays unattached, and the mapping says so.
176
182
  async discover(ctx) {
177
- const root = options.root ?? ctx.workspaceRoot
183
+ const root = rootOf(ctx.workspaceRoot)
178
184
  const members = await memberProjects(ctx.workspaceRoot)
179
185
  // Core's status is the workspace's: a root elsewhere asks git itself.
180
186
  const changes = options.root === undefined ? () => ctx.worktreeChanges() : undefined
@@ -369,6 +375,10 @@ async function nxReads(
369
375
  String(process.env['NX_LOAD_DOT_ENV_FILES']),
370
376
  JSON.stringify(installed),
371
377
  await yarnrcText(root),
378
+ // Whether the manager runs a script's `pre` / `post` hooks (`runsScriptHooks`).
379
+ ...(await Promise.all(
380
+ ['package.json', '.npmrc', 'pnpm-workspace.yaml'].map((f) => textOf(path.join(root, f))),
381
+ )),
372
382
  ...notes,
373
383
  ]
374
384
  }
@@ -414,7 +424,7 @@ async function index(
414
424
  }
415
425
  if (unattached.length > 0) {
416
426
  notes.push(
417
- `Nx project(s) ${unattached.join(', ')} have no workspace package to attach targets to ` +
427
+ `${unattached.length === 1 ? 'Nx project' : 'Nx projects'} ${unattached.join(', ')} ${unattached.length === 1 ? 'has' : 'have'} no workspace package to attach targets to ` +
418
428
  '(a package holds the name, or the directory is gone) — run those targets with nx',
419
429
  )
420
430
  }
@@ -574,10 +584,14 @@ async function graphInputKey(
574
584
  // stray write at the root (a report, a log) is not, and counting it
575
585
  // re-exported the graph on every run after it.
576
586
  // A `project.json` anywhere: a new one is a project no root lists yet.
587
+ // A `.gitignore` anywhere too: Nx's project walk honours each one, so an
588
+ // edit between project roots hides or shows a project.
577
589
  const roots = dirs.map((d) => relPosix(root, d))
578
590
  const graphFile = (p: string): boolean =>
579
591
  p === 'project.json' ||
580
592
  p.endsWith('/project.json') ||
593
+ p === '.gitignore' ||
594
+ p.endsWith('/.gitignore') ||
581
595
  roots.some((r) => r === '' || p.startsWith(`${r}/`)) ||
582
596
  (!p.includes('/') && ROOT_GRAPH_FILE.test(p))
583
597
  const listed = [
@@ -7,8 +7,10 @@
7
7
  // (`dependentTasksOutputFiles`) is nothing; `externalDependencies` is a todo saying
8
8
  // so. Extracted from `buildTask` in item 606.
9
9
 
10
+ import path from 'node:path'
10
11
  import { nxWorkspacePath, underProject, withProjectDir } from './nx-outputs.js'
11
- import { minimatchToVx } from '../glob-grammar.js'
12
+ import type { TrackedKinds } from '../tracked-outputs.js'
13
+ import { gluedGlobstarAsStar, minimatchToVx, nxBareGroups } from '../glob-grammar.js'
12
14
  import { shellQuote } from '../nx-command.js'
13
15
 
14
16
  export interface NxInputs {
@@ -24,8 +26,15 @@ export interface NxInputs {
24
26
  * representable in vx" (walked the Nx path, 2026-09-20).
25
27
  */
26
28
  readonly runtimeCmds: string[]
27
- /** `^name` / `{ input, dependencies | projects }`: whose `name` to fold. */
28
- readonly upstream: Array<{ readonly name: string; readonly of: 'deps' | readonly string[] }>
29
+ /**
30
+ * `^name` / `{ input, dependencies | projects }`: whose `name` to fold;
31
+ * `depsOf`, the listed projects' dependencies' rather than theirs.
32
+ */
33
+ readonly upstream: Array<{
34
+ readonly name: string
35
+ readonly of: 'deps' | readonly string[]
36
+ readonly depsOf?: true
37
+ }>
29
38
  /** `{ dependentTasksOutputFiles }` globs: keyed only through a `dependsOn` edge. */
30
39
  readonly dependentOutputs: string[]
31
40
  }
@@ -69,6 +78,8 @@ export function expandNxInputs(
69
78
  readonly rel: string
70
79
  readonly name: string
71
80
  readonly outputs?: readonly string[]
81
+ /** git's tracked files by directory; unknown, every output counts as ignored. */
82
+ readonly tracked?: (rel: string) => TrackedKinds
72
83
  /** The root package.json's dependency names: every task's key holds them. */
73
84
  readonly rootDeps?: ReadonlySet<string>
74
85
  },
@@ -99,9 +110,24 @@ export function expandNxInputs(
99
110
  }
100
111
  // Nx's file map skips gitignored files, so it hashes nothing here
101
112
  // either: a changed `dist` file is a cache hit under Nx 23.3 (YL-1).
113
+ // An output git tracks (a generator writing into `src`) is hashed,
114
+ // and dropping it keyed `build` on no file (stale hit, 2026-10-28).
102
115
  const lit = withProjectDir(p, at.rel, false)
103
- if (neg === '' && at.outputs?.some((o) => lit === o || lit.startsWith(`${o}/`))) return
104
- const g = minimatchToVx(p, neg !== '')
116
+ const ignored = (o: string): boolean => {
117
+ if (at.tracked === undefined) return true
118
+ const dir = path.posix.dirname(o)
119
+ return (
120
+ at.tracked(o).files.length === 0 &&
121
+ !at.tracked(dir).files.includes(dir === '.' ? o : o.slice(dir.length + 1))
122
+ )
123
+ }
124
+ if (
125
+ neg === '' &&
126
+ at.outputs?.some((o) => (lit === o || lit.startsWith(`${o}/`)) && ignored(o))
127
+ )
128
+ return
129
+ const bare = nxBareGroups(p)
130
+ const g = bare === null ? null : minimatchToVx(gluedGlobstarAsStar(bare), neg !== '')
105
131
  if (g === null) {
106
132
  todos.push(`input ${JSON.stringify(entry)}: glob syntax vx cannot take — map manually`)
107
133
  return
@@ -115,7 +141,7 @@ export function expandNxInputs(
115
141
  ? null
116
142
  : underProject(g, at.rel)
117
143
  if (own === null) into.wsFiles.push(neg + withProjectDir(g, at.rel, true))
118
- else into.files.push(neg + withProjectDir(own, at.rel, true))
144
+ else into.files.push(neg + (own === '' ? '**/*' : withProjectDir(own, at.rel, true)))
119
145
  return
120
146
  }
121
147
  // Bare string = named-input reference.
@@ -199,17 +225,26 @@ export function expandNxInputs(
199
225
  // Nx 23 still reads the pre-17 spellings `projects: "dependencies"`
200
226
  // (`^input`) and `projects: "self"` (the project's own); taken as
201
227
  // project names, each was a todo and the input was dropped.
202
- if (o.dependencies === true || o.projects === 'dependencies') {
228
+ const listed =
229
+ o.projects !== undefined && o.projects !== 'self' && o.projects !== 'dependencies'
230
+ if (o.projects === 'dependencies' || (o.dependencies === true && !listed)) {
203
231
  into.upstream.push({ name: o.input, of: 'deps' })
204
232
  return
205
233
  }
206
- if (o.projects !== undefined && o.projects !== 'self') {
234
+ if (listed) {
207
235
  const of = typeof o.projects === 'string' ? [o.projects] : o.projects
208
236
  if (!Array.isArray(of) || !of.every((p) => typeof p === 'string')) {
209
237
  todos.push(`input ${JSON.stringify(entry)} not representable in vx`)
210
238
  return
211
239
  }
212
- into.upstream.push({ name: o.input, of: of as string[] })
240
+ // Beside `projects`, Nx 24 reads `dependencies: true` as those
241
+ // projects' dependencies and Nx 23 as this project's: read as
242
+ // this project's alone, a listed project's dependency edit was a
243
+ // hit under Nx 24. Both are keyed, never less than either.
244
+ if (o.dependencies === true) {
245
+ into.upstream.push({ name: o.input, of: 'deps' })
246
+ into.upstream.push({ name: o.input, of: of as string[], depsOf: true })
247
+ } else into.upstream.push({ name: o.input, of: of as string[] })
213
248
  return
214
249
  }
215
250
  expand(o.input, seen)
package/src/nx/nx-map.ts CHANGED
@@ -20,12 +20,14 @@ import { existsSync } from 'node:fs'
20
20
  import path from 'node:path'
21
21
  import { type Duration, type GeneratedProject, type GeneratedTask, UserError } from '@vzn/vx'
22
22
  import type { PackageMeta } from '../packages.js'
23
+ import { refuseTaskNames } from '../plugin-gaps.js'
23
24
  import { withoutTakenBack } from '../glob-grammar.js'
24
25
  import { manifestGraph, type ManifestGraph } from '../manifest-graph.js'
25
26
  import { mapRunCommands, shellQuote } from '../nx-command.js'
26
- import { scriptCommand, yarnPnp } from '../script-command.js'
27
+ import { type Owner, runsScriptHooks, scriptCommand, yarnPnp, yarnRun } from '../script-command.js'
27
28
  import {
28
29
  ownFileTakeBacks,
30
+ takingBack,
29
31
  excludeSiblingOutputs,
30
32
  excludeWorkspaceOutputs,
31
33
  resolveSharedOutputs,
@@ -274,6 +276,8 @@ type MapOpts = MapNxOptions & {
274
276
  /** The root TypeScript config Nx folds into every task's hash, if any. */
275
277
  readonly rootTsConfig: string | undefined
276
278
  readonly pnp: boolean
279
+ /** Whether the manager owning a directory runs `pre` / `post` hooks (`runsScriptHooks`). */
280
+ readonly hooks: (dir: string) => boolean
277
281
  /** Tasks per `syncGenerators` list, said once in the notes. */
278
282
  readonly syncTasks: Map<string, number>
279
283
  }
@@ -325,6 +329,7 @@ export async function mapNxWorkspace(
325
329
  const g = graph
326
330
 
327
331
  const { namedInputs, cacheable, globalSync } = await readNxJsonFacts(root)
332
+ const hookMemo = new Map<string, Owner>()
328
333
  const mapOpts: MapOpts = {
329
334
  ...opts,
330
335
  cacheable: new Set([...opts.cacheable, ...cacheable]),
@@ -333,6 +338,7 @@ export async function mapNxWorkspace(
333
338
  existsSync(path.join(root, f)),
334
339
  ),
335
340
  pnp: yarnPnp(root),
341
+ hooks: (dir) => runsScriptHooks(dir, hookMemo) !== null,
336
342
  syncTasks: new Map(),
337
343
  }
338
344
 
@@ -362,6 +368,8 @@ export async function mapNxWorkspace(
362
368
  const relRoot = normRel(node?.data?.root ?? '')
363
369
  if (node?.data?.targets === undefined) continue
364
370
  const dir = relRoot === '' || relRoot === '.' ? root : path.join(root, relRoot)
371
+ const rel = path.relative(root, dir)
372
+ if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) continue
365
373
  let pkg: PackageMeta['packageJson'] = { name: nodeName }
366
374
  try {
367
375
  pkg = (await Bun.file(path.join(dir, 'package.json')).json()) as PackageMeta['packageJson']
@@ -386,7 +394,7 @@ export async function mapNxWorkspace(
386
394
  nodeNameOf.set(synthetic, nodeName)
387
395
  }
388
396
 
389
- const upstream = planNxUpstream(nodeMap, g.dependencies, namedInputs, metaByNode)
397
+ const upstream = planNxUpstream(nodeMap, g.dependencies, namedInputs, metaByNode, opts.tracked)
390
398
 
391
399
  const taskNameFor: TaskNameFor = (project, target, configuration) => {
392
400
  const t = nodeMap[project]?.data?.targets?.[target]
@@ -571,7 +579,7 @@ export async function mapNxWorkspace(
571
579
  )
572
580
  followNxGraph(t.task, nodeName, meta.name, nxEdges, metaByNode, emittedIds, reached)
573
581
  }
574
- dropUnheldDeps(t, emitted, emittedIds)
582
+ dropUnheldDeps(t, meta.name, emitted, emittedIds)
575
583
  const reader = dependentReaders.get(t)
576
584
  if (reader !== undefined && !reachesProducer(t, meta.name, emittedIds, reached)) {
577
585
  delete t.task!['cache']
@@ -615,6 +623,7 @@ export async function mapNxWorkspace(
615
623
  `\`syncGenerators\` (${gens}) on ${n} task${n === 1 ? '' : 's'}: ` +
616
624
  `Nx runs them before those targets, and vx does not — ${syncAdvice}`,
617
625
  )
626
+ refuseTaskNames(projects, 'target')
618
627
  return { projects, notes }
619
628
  }
620
629
 
@@ -753,6 +762,7 @@ export async function readNxJsonFacts(root: string): Promise<NxJsonFacts> {
753
762
 
754
763
  function dropUnheldDeps(
755
764
  t: GeneratedTask,
765
+ owner: string,
756
766
  emitted: ReadonlySet<string>,
757
767
  emittedIds: ReadonlySet<string>,
758
768
  ): void {
@@ -766,7 +776,9 @@ function dropUnheldDeps(
766
776
  if (d.startsWith('^')) return emitted.has(d.slice(1))
767
777
  // `pkg#task` on a project no package holds (the root project with no
768
778
  // vx.config) refused the whole run as "no such project".
769
- if (!d.includes('#') || emittedIds.has(d)) return true
779
+ // A same-project target written as no task (an `nx:noop` with nothing
780
+ // to chain) is nothing Nx runs; kept, `vx lock` refused the project.
781
+ if (emittedIds.has(d.includes('#') ? d : `${owner}#${d}`)) return true
770
782
  t.todos.push(
771
783
  `dependsOn ${JSON.stringify(d)}: no vx project runs it — edge dropped, and the key misses it`,
772
784
  )
@@ -822,6 +834,7 @@ function buildTask(
822
834
  todos,
823
835
  dotenv,
824
836
  opts.pnp,
837
+ opts.hooks(meta.dir),
825
838
  meta.packageJson,
826
839
  opts.manifestField,
827
840
  opts.executors,
@@ -836,6 +849,7 @@ function buildTask(
836
849
  rel: projectRel,
837
850
  name: projectName,
838
851
  outputs: nxProjectOutputs(nodeMap[nodeName]?.data?.targets, projectRel, projectName),
852
+ ...(opts.tracked === undefined ? {} : { tracked: opts.tracked }),
839
853
  rootDeps: opts.rootDeps,
840
854
  }
841
855
  expandNxInputs(target.inputs ?? [], upstream.namedOf(nodeName), at, inputs, todos)
@@ -864,7 +878,10 @@ function buildTask(
864
878
  patterns,
865
879
  Object.entries(nodeMap).map(([name, n]) => ({
866
880
  name,
867
- tags: nxTags(n),
881
+ // Every tag Nx matches, its own `npm:*` too: nxTags is what vx writes.
882
+ tags: Array.isArray(n?.data?.tags)
883
+ ? n.data.tags.filter((t): t is string => typeof t === 'string')
884
+ : [],
868
885
  ...(typeof n?.data?.root === 'string' ? { root: n.data.root } : {}),
869
886
  })),
870
887
  ),
@@ -874,6 +891,12 @@ function buildTask(
874
891
  : { node: nodeName, configuration: variant.configuration! },
875
892
  options,
876
893
  )
894
+ // Nx draws no edge from a task to itself (`task.id !== selfTaskId`): a
895
+ // `projects: "*"` or a tag the project carries named it, and vx refused
896
+ // the workspace as a cycle (2026-10-28).
897
+ for (const own of [variant.name, `${meta.name}#${variant.name}`]) {
898
+ for (let i = deps.indexOf(own); i !== -1; i = deps.indexOf(own)) deps.splice(i, 1)
899
+ }
877
900
  // A nested `nx <target>` runs the target's default configuration, whatever this one runs.
878
901
  for (const c of mapped?.nxCalls ?? []) {
879
902
  const edge =
@@ -893,7 +916,7 @@ function buildTask(
893
916
  // vx cleans outputs before a run and Nx never does. With git's file list
894
917
  // known, a wildcard-first output (`**/*.d.ts`) stays cached: core never
895
918
  // cleans a committed file, so what the clean removes is what the task
896
- // writes (owner, 2026-10-03). Without it, the sources cannot be told apart.
919
+ // writes (2026-10-03). Without it, the sources cannot be told apart.
897
920
  const wild =
898
921
  opts.tracked === undefined
899
922
  ? (wildcardOutput(outFiles) ?? wildcardOutput(wsOutFiles))
@@ -978,7 +1001,7 @@ function buildTask(
978
1001
  // dependencies again. `LERNA_PACKAGE_NAME` is the project too: Lerna
979
1002
  // runs on Nx's runner and documents it to scripts. Only a command that
980
1003
  // still runs Nx or Lerna gets them: a native task names no other runner
981
- // (owner, 2026-10-22). A run-commands `env` still wins, as in Nx.
1004
+ // (2026-10-22). A run-commands `env` still wins, as in Nx.
982
1005
  const define = {
983
1006
  ...(RUNS_NX.test(mapped.command)
984
1007
  ? {
@@ -1020,8 +1043,8 @@ function buildTask(
1020
1043
  if (inputs.runtimeCmds.length > 0) keyed.workspaceRuntime = inputs.runtimeCmds
1021
1044
  task.inputs = keyed
1022
1045
  if (cacheEnabled) {
1023
- const outputs: Record<string, unknown> = { files: outFiles }
1024
- if (wsOutFiles.length > 0) outputs.workspaceFiles = wsOutFiles
1046
+ const outputs: Record<string, unknown> = { files: takingBack(outFiles) }
1047
+ if (wsOutFiles.length > 0) outputs.workspaceFiles = takingBack(wsOutFiles)
1025
1048
  task.cache = { outputs }
1026
1049
  }
1027
1050
 
@@ -1129,6 +1152,7 @@ function mapCommand(
1129
1152
  todos: string[],
1130
1153
  dotenv: readonly string[] | null,
1131
1154
  pnp: boolean,
1155
+ hooks: boolean,
1132
1156
  manifest: { readonly name?: unknown; readonly version?: unknown },
1133
1157
  manifestField: MapNxOptions['manifestField'],
1134
1158
  executors: NxExecutors | undefined,
@@ -1199,10 +1223,11 @@ function mapCommand(
1199
1223
  // (novu's `test:watch: ""`, 2026-09-11); as a command it is a config
1200
1224
  // that refuses to load, so it is the placeholder with its todo.
1201
1225
  if (body !== undefined && body.length > 0) {
1202
- const command = scriptCommand(script, body, scripts, pnp)
1226
+ // Nx runs `<pm> run <script>`: the hooks fold only where the manager runs them.
1227
+ const command = scriptCommand(script, body, scripts, pnp, hooks)
1203
1228
  // `yarn run <name>` sets its own.
1204
1229
  const env =
1205
- command === `yarn run ${script}`
1230
+ command === yarnRun(script)
1206
1231
  ? {}
1207
1232
  : npmScriptEnv(command, script, command !== body, manifest, manifestField, todos)
1208
1233
  return shell(command, undefined, { env, readyWhen: undefined })
@@ -3,7 +3,7 @@
3
3
 
4
4
  import path from 'node:path'
5
5
  import { isLiteralPattern } from '@vzn/vx'
6
- import { literalGlob, minimatchToVx } from '../glob-grammar.js'
6
+ import { gluedGlobstarAsStar, literalGlob, minimatchToVx, nxBareGroups } from '../glob-grammar.js'
7
7
  import { takingBack } from '../shared-outputs.js'
8
8
 
9
9
  interface NxOutputs {
@@ -59,11 +59,19 @@ export function withProjectDir(p: string, projectRel: string, asGlob: boolean):
59
59
  })
60
60
  }
61
61
 
62
- /** A workspace path relative to the project dir, or null when it lies outside. */
62
+ /**
63
+ * A workspace path relative to the project dir, or null when it lies
64
+ * outside; `''` is the project dir itself, which Nx reads as the whole tree
65
+ * (`path_or_everything_under`). As a workspace path, `{projectRoot}` cached
66
+ * a literal core reads as another project's, and `{projectRoot}/` the empty
67
+ * path, which saved and keyed nothing.
68
+ */
63
69
  export function underProject(p: string, projectRel: string): string | null {
64
70
  if (projectRel === '.' || projectRel === '') return p
65
- for (const dir of [masked(projectRel), projectRel])
66
- if (p.startsWith(`${dir}/`)) return p.slice(dir.length + 1)
71
+ for (const dir of [masked(projectRel), projectRel]) {
72
+ if (p === dir) return ''
73
+ if (p.startsWith(`${dir}/`)) return p.slice(dir.length + 1).replace(/\/+$/, '')
74
+ }
67
75
  return null
68
76
  }
69
77
 
@@ -191,7 +199,7 @@ export function mapNxOutputs(
191
199
  const gs = g.slice(gn.length)
192
200
  const own = underProject(gs, projectRel)
193
201
  if (own !== null) {
194
- outFiles.push(gn + withProjectDir(own, projectRel, true))
202
+ outFiles.push(gn + (own === '' ? '**' : withProjectDir(own, projectRel, true)))
195
203
  continue
196
204
  }
197
205
  const m = path.posix.normalize(gs).replace(/^\.\//, '')
@@ -215,7 +223,9 @@ export function mapNxOutputs(
215
223
  */
216
224
  function vxOutputGlobs(o: string): string[] | null {
217
225
  const negated = o.startsWith('!')
218
- const segs = o.slice(negated ? 1 : 0).split('/')
226
+ const bare = nxBareGroups(o.slice(negated ? 1 : 0))
227
+ if (bare === null) return null
228
+ const segs = gluedGlobstarAsStar(bare).split('/')
219
229
  const at = segs.findIndex((seg) => /^!\([^()|{},]+(\|[^()|{},]+)*\)$/.test(seg))
220
230
  if (at !== -1) {
221
231
  if (negated) return null
@@ -18,6 +18,7 @@
18
18
  // and edges only out of the cycle.
19
19
  import type { GeneratedTask } from '@vzn/vx'
20
20
  import type { PackageMeta } from '../packages.js'
21
+ import type { TrackedKinds } from '../tracked-outputs.js'
21
22
  import { emptyNxInputs, expandNxInputs, type NxInputs } from './nx-inputs.js'
22
23
  import { nxProjectOutputs } from './nx-outputs.js'
23
24
  import { matchNxProjects } from './nx-deps.js'
@@ -72,6 +73,7 @@ export function planNxUpstream(
72
73
  dependencies: unknown,
73
74
  workspaceNamed: Readonly<Record<string, unknown[]>> | null,
74
75
  metaByNode: ReadonlyMap<string, PackageMeta>,
76
+ tracked?: (rel: string) => TrackedKinds,
75
77
  ): NxUpstream {
76
78
  const isProject = (n: string): boolean => typeof nodes[n]?.data?.root === 'string'
77
79
  const projectTags = Object.entries(nodes).map(([name, n]) => ({
@@ -221,6 +223,7 @@ export function planNxUpstream(
221
223
  rel,
222
224
  name: nm,
223
225
  outputs: nxProjectOutputs(nodes[node]?.data?.targets, rel || '.', nm),
226
+ ...(tracked === undefined ? {} : { tracked }),
224
227
  }
225
228
  expandNxInputs([name], namedOf(node), at, into, todos)
226
229
  }
@@ -289,6 +292,7 @@ export function planNxUpstream(
289
292
  into: NxInputs,
290
293
  todos: string[],
291
294
  edges: Set<string>,
295
+ depsOf = false,
292
296
  ): void => {
293
297
  // Nx reads the list as `dependsOn`'s `projects` (`findMatchingProjects`):
294
298
  // `*` patterns, `tag:` and `!` exclusions. Looked up as names, a
@@ -302,6 +306,10 @@ export function planNxUpstream(
302
306
  for (const p of literal)
303
307
  todos.push(`input project ${JSON.stringify(p)} is not a graph node — map manually`)
304
308
  for (const p of matchNxProjects(of, projectTags)) {
309
+ if (depsOf) {
310
+ fold(p, name, new Set(), into, todos, edges)
311
+ continue
312
+ }
305
313
  const u = { name }
306
314
  const meta = metaByNode.get(p)
307
315
  if (meta !== undefined) {
@@ -319,7 +327,7 @@ export function planNxUpstream(
319
327
  const edges = new Set<string>()
320
328
  for (const u of into.upstream) {
321
329
  if (u.of === 'deps') fold(node, u.name, new Set(), into, todos, edges)
322
- else foldProjects(u.name, u.of, into, todos, edges)
330
+ else foldProjects(u.name, u.of, into, todos, edges, u.depsOf)
323
331
  }
324
332
  return [...edges]
325
333
  }
@@ -337,7 +345,7 @@ export function planNxUpstream(
337
345
  fold(node, name, peers, inputs, todos, edges)
338
346
  // A nested `^other` in this project's `name` input.
339
347
  for (const u of inputs.upstream) {
340
- if (u.of !== 'deps') foldProjects(u.name, u.of, inputs, todos, edges)
348
+ if (u.of !== 'deps') foldProjects(u.name, u.of, inputs, todos, edges, u.depsOf)
341
349
  else if (u.name !== name) fold(node, u.name, peers, inputs, todos, edges)
342
350
  }
343
351
  // Nx hashes a fileset that matches nothing as nothing; core warns of