@vzn/vx-migrate 0.0.646 → 0.0.648
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -61
- package/package.json +3 -3
- package/src/adopt.ts +53 -12
- package/src/bin.ts +21 -1
- package/src/glob-grammar.ts +82 -0
- package/src/migrate-nx.ts +7 -4
- package/src/migrate-scripts.ts +10 -102
- package/src/migrate.ts +53 -25
- package/src/nx/index.ts +19 -5
- package/src/nx/nx-inputs.ts +44 -9
- package/src/nx/nx-map.ts +36 -11
- package/src/nx/nx-outputs.ts +16 -6
- package/src/nx/nx-upstream.ts +10 -2
- package/src/nx-cache/index.ts +60 -50
- package/src/nx-exec.cjs +2 -2
- package/src/package-fs.ts +6 -1
- package/src/packages.ts +9 -7
- package/src/plugin-gaps.ts +32 -6
- package/src/remote-cache.ts +5 -1
- package/src/remote-deadline.ts +7 -1
- package/src/remote-refusal.ts +31 -0
- package/src/remote-retry.ts +9 -4
- package/src/remote-url.ts +28 -0
- package/src/script-command.ts +110 -4
- package/src/shared-outputs.ts +8 -2
- package/src/tracked-outputs.ts +1 -1
- package/src/turbo/index.ts +18 -8
- package/src/turbo/turbo-map.ts +89 -15
- package/src/turbo-cache/index.ts +205 -89
- package/src/turbo-cache/oidc.ts +25 -14
- package/src/turbo-cache/scope.ts +5 -3
- package/src/vite-task/vite-task-map.ts +11 -6
- package/src/workspace-plugins.ts +5 -2
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 = [
|
package/src/nx/nx-inputs.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
/**
|
|
28
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
// (
|
|
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
|
-
|
|
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 ===
|
|
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 })
|
package/src/nx/nx-outputs.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
|
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
|
package/src/nx/nx-upstream.ts
CHANGED
|
@@ -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
|