@vzn/vx-migrate 0.0.645 → 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 +77 -73
- package/package.json +3 -3
- package/src/adopt.ts +53 -12
- package/src/adoption-plugin.ts +9 -11
- package/src/bin.ts +21 -1
- package/src/glob-grammar.ts +82 -0
- package/src/index.ts +36 -10
- package/src/mapping-cache.ts +14 -6
- package/src/migrate-nx.ts +7 -4
- package/src/migrate-scripts.ts +10 -102
- package/src/migrate-turbo.ts +2 -6
- package/src/migrate.ts +57 -38
- package/src/nx/index.ts +19 -5
- package/src/nx/nx-inputs.ts +44 -9
- package/src/nx/nx-map.ts +65 -25
- package/src/nx/nx-outputs.ts +16 -6
- package/src/nx/nx-upstream.ts +15 -7
- 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 +24 -12
- package/src/tracked-outputs.ts +11 -7
- package/src/turbo/index.ts +33 -17
- package/src/turbo/turbo-map.ts +97 -192
- package/src/turbo/turbo-reads.ts +183 -0
- package/src/turbo-cache/index.ts +261 -102
- package/src/turbo-cache/oidc.ts +52 -26
- 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/migrate.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// existing Turbo or Nx setup (`--keep`: the workspace file that reads it
|
|
4
4
|
// live instead), from Vite Task, or from package.json scripts, and vx
|
|
5
5
|
// installed with the repo's manager. `vx init` hands every repo with a
|
|
6
|
-
// package.json here (
|
|
6
|
+
// package.json here (2026-10-14: core reads no package.json).
|
|
7
7
|
// Without either mode flag a terminal is asked; anything else is native. Source auto-detect: turbo.json → Turbo;
|
|
8
8
|
// .nx/workspace-data/project-graph.json, nx.json or a Lerna-on-Nx lerna.json → Nx (the resolved
|
|
9
9
|
// graph, exported by nx if absent); `vite-plus` in the root package.json → Vite Task:
|
|
@@ -19,6 +19,7 @@ import path from 'node:path'
|
|
|
19
19
|
import {
|
|
20
20
|
applyMigration,
|
|
21
21
|
findWorkspaceRoot,
|
|
22
|
+
isStarterConfig,
|
|
22
23
|
isUserError,
|
|
23
24
|
VERSION,
|
|
24
25
|
type MigrationFormat,
|
|
@@ -53,14 +54,13 @@ import {
|
|
|
53
54
|
missingPackages,
|
|
54
55
|
MODE_QUESTION,
|
|
55
56
|
ownVersion,
|
|
56
|
-
type PackageManager,
|
|
57
57
|
packageManagerOf,
|
|
58
58
|
parseModeAnswer,
|
|
59
59
|
uninstall,
|
|
60
60
|
} from './adopt.js'
|
|
61
61
|
import { migrateTurbo } from './migrate-turbo.js'
|
|
62
62
|
import { migrateViteTask } from './migrate-vite-task.js'
|
|
63
|
-
import { turboConfigFile } from './turbo/turbo-
|
|
63
|
+
import { turboConfigFile } from './turbo/turbo-reads.js'
|
|
64
64
|
import { lockfilePlugin } from './workspace-notes.js'
|
|
65
65
|
import {
|
|
66
66
|
extendWorkspaceFile,
|
|
@@ -102,8 +102,10 @@ export function parseMigrateArgs(args: readonly string[]): MigrateArgs {
|
|
|
102
102
|
else if (a === '--no-install') out.noInstall = true
|
|
103
103
|
else if (a === '--format' || a?.startsWith('--format=')) {
|
|
104
104
|
const v = a === '--format' ? args[++i] : a.slice('--format='.length)
|
|
105
|
+
if (v === undefined || v === '')
|
|
106
|
+
return { ...out, error: '--format requires a value: pretty or json' }
|
|
105
107
|
if (v !== 'json' && v !== 'pretty') {
|
|
106
|
-
return { ...out, error: `--format must be pretty or json (got ${v
|
|
108
|
+
return { ...out, error: `--format must be pretty or json (got ${v})` }
|
|
107
109
|
}
|
|
108
110
|
out.json = v === 'json'
|
|
109
111
|
} else if (a === '--native' || a === '--keep') {
|
|
@@ -114,8 +116,10 @@ export function parseMigrateArgs(args: readonly string[]): MigrateArgs {
|
|
|
114
116
|
out.mode = mode
|
|
115
117
|
} else if (a === '--from' || a?.startsWith('--from=')) {
|
|
116
118
|
const v = a === '--from' ? args[++i] : a.slice('--from='.length)
|
|
119
|
+
if (v === undefined || v === '')
|
|
120
|
+
return { ...out, error: '--from requires a value: turbo, nx, vite-task or scripts' }
|
|
117
121
|
if (v !== 'turbo' && v !== 'nx' && v !== 'vite-task' && v !== 'scripts') {
|
|
118
|
-
return { ...out, error:
|
|
122
|
+
return { ...out, error: `--from must be turbo, nx, vite-task or scripts (got ${v})` }
|
|
119
123
|
}
|
|
120
124
|
out.from = v
|
|
121
125
|
} else if (a === '--help' || a === '-h') return { ...out, help: true }
|
|
@@ -144,13 +148,11 @@ async function noRootReason(cwd: string, err: Error): Promise<Error> {
|
|
|
144
148
|
export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
145
149
|
const parsed = parseMigrateArgs(args)
|
|
146
150
|
if (parsed.help) {
|
|
147
|
-
|
|
151
|
+
ui.say('info', USAGE, 'out')
|
|
148
152
|
return 0
|
|
149
153
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return 1
|
|
153
|
-
}
|
|
154
|
+
// Thrown, so the bin answers it as JSON too when that was asked.
|
|
155
|
+
if (parsed.error) throw new UserError(parsed.error, 'VX_E_USAGE')
|
|
154
156
|
const reads: LoadReads = new Map()
|
|
155
157
|
// An Nx repo with no package.json (Nx pinned in `.nx/installation`):
|
|
156
158
|
// nothing is installable, so its configs run on core alone.
|
|
@@ -163,7 +165,13 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
163
165
|
})
|
|
164
166
|
const workspace = bare ? { root, packageGlobs: [] } : await loadPackageWorkspace(root, reads)
|
|
165
167
|
const nameless: string[] = []
|
|
166
|
-
|
|
168
|
+
// `vx init`'s starter at the root holds no task: mapped as if absent, so
|
|
169
|
+
// the root's own scripts become its tasks.
|
|
170
|
+
const metas = bare
|
|
171
|
+
? []
|
|
172
|
+
: (await discoverPackages(workspace, nameless)).filter(
|
|
173
|
+
(m) => !(m.dir === root && m.configPath !== null && isStarterConfig(m.configPath)),
|
|
174
|
+
)
|
|
167
175
|
|
|
168
176
|
const turboFile = await turboConfigFile(root)
|
|
169
177
|
const hasTurbo = turboFile !== null
|
|
@@ -358,7 +366,7 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
358
366
|
// adoption's install of it is dropped with turbo() / nx().
|
|
359
367
|
const finalWorkspace = workspaceText ?? readFileSync(path.join(root, existingWorkspace!), 'utf8')
|
|
360
368
|
// Imports parsed, not matched: a formatter's double quotes hid it, and
|
|
361
|
-
// the file still imported the package removed (
|
|
369
|
+
// the file still imported the package removed (2026-10-16).
|
|
362
370
|
const imported = importedPackages(finalWorkspace)
|
|
363
371
|
const needsSelf = runsBins || imported.includes('@vzn/vx-migrate')
|
|
364
372
|
const cacheNote =
|
|
@@ -386,7 +394,7 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
386
394
|
needsSelf ? [] : ['@vzn/vx-migrate'],
|
|
387
395
|
)),
|
|
388
396
|
]
|
|
389
|
-
|
|
397
|
+
const code = await applyMigration({
|
|
390
398
|
root,
|
|
391
399
|
metas,
|
|
392
400
|
plan,
|
|
@@ -401,6 +409,7 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
401
409
|
// The written workspace file imports plugins `--no-install` left out.
|
|
402
410
|
...(parsed.noInstall === true ? {} : { lock: lockWorkspace }),
|
|
403
411
|
})
|
|
412
|
+
return exitAfter(code, steps)
|
|
404
413
|
}
|
|
405
414
|
|
|
406
415
|
/**
|
|
@@ -418,8 +427,10 @@ async function lockWorkspace(root: string): Promise<{ code: number; said: string
|
|
|
418
427
|
p.exited,
|
|
419
428
|
])
|
|
420
429
|
// A success's warnings print above the report; a refusal is said whole.
|
|
430
|
+
// A success is a report row, matched as text: the colour FORCE_COLOR
|
|
431
|
+
// gave the child would hide its `✓ vx lock` mark from that match.
|
|
421
432
|
if (code === 0) process.stderr.write(err)
|
|
422
|
-
return { code, said: code === 0 ? out : out + err }
|
|
433
|
+
return { code, said: code === 0 ? Bun.stripANSI(out) : out + err }
|
|
423
434
|
}
|
|
424
435
|
|
|
425
436
|
/** A report row: what the tool did, e.g. `['ran', 'bun add -d …']`. */
|
|
@@ -500,17 +511,36 @@ async function prepareRepo(
|
|
|
500
511
|
const missing = missingPackages(root, all, version)
|
|
501
512
|
if (missing.length > 0) {
|
|
502
513
|
steps.push(
|
|
503
|
-
args.dry ? ['install', missing.join(' ')] :
|
|
514
|
+
args.dry ? ['install', missing.join(' ')] : await ran(() => install(root, missing, version)),
|
|
504
515
|
)
|
|
505
516
|
}
|
|
506
517
|
// Last: the package removed may be the one running this.
|
|
507
518
|
const listed = listedPackages(root, unwanted)
|
|
508
519
|
if (listed.length > 0) {
|
|
509
|
-
steps.push(args.dry ? ['remove', listed.join(' ')] :
|
|
520
|
+
steps.push(args.dry ? ['remove', listed.join(' ')] : await ran(() => uninstall(root, listed)))
|
|
510
521
|
}
|
|
511
522
|
return steps
|
|
512
523
|
}
|
|
513
524
|
|
|
525
|
+
/**
|
|
526
|
+
* A manager step's row. Its failure is a `failed` row, not the end of the
|
|
527
|
+
* migration: the configs are still written and the report names what to
|
|
528
|
+
* re-run, where a throw left a repo with neither.
|
|
529
|
+
*/
|
|
530
|
+
async function ran(step: () => Promise<string>): Promise<Step> {
|
|
531
|
+
try {
|
|
532
|
+
return ['ran', await step()]
|
|
533
|
+
} catch (e) {
|
|
534
|
+
if (!isUserError(e)) throw e
|
|
535
|
+
return ['failed', e.message]
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** The exit a migration ends with: 1 when a manager step failed, whatever the rest did. */
|
|
540
|
+
function exitAfter(code: number, steps: readonly Step[]): number {
|
|
541
|
+
return code === 0 && steps.some(([label]) => label === 'failed') ? 1 : code
|
|
542
|
+
}
|
|
543
|
+
|
|
514
544
|
/**
|
|
515
545
|
* `--keep`: `.vx/workspace.ts` declaring `turbo()` or `nx()`, so the
|
|
516
546
|
* runner's own config stays the source, beside the plugins the repo calls
|
|
@@ -554,7 +584,7 @@ async function keep(
|
|
|
554
584
|
args,
|
|
555
585
|
)
|
|
556
586
|
if (args.json)
|
|
557
|
-
for (const [label, value] of steps)
|
|
587
|
+
for (const [label, value] of steps) ui.say('info', `vx-migrate: ${label}: ${value}`)
|
|
558
588
|
const ignore = existsSync(path.join(root, '.vx', '.gitignore')) ? [] : ['.vx/.gitignore']
|
|
559
589
|
if (!args.dry) {
|
|
560
590
|
if (before === undefined) {
|
|
@@ -603,7 +633,7 @@ async function keep(
|
|
|
603
633
|
)
|
|
604
634
|
return 0
|
|
605
635
|
}
|
|
606
|
-
if (args.dry && before === undefined)
|
|
636
|
+
if (args.dry && before === undefined) ui.print('out', ui.excerpt(name, text))
|
|
607
637
|
// A run reads only the lock; with the plugins installed it is written
|
|
608
638
|
// now, else `next` names `vx lock` after the install.
|
|
609
639
|
const lock = !args.dry && !next.includes(' lock && ') ? await lockWorkspace(root) : undefined
|
|
@@ -626,7 +656,7 @@ async function keep(
|
|
|
626
656
|
],
|
|
627
657
|
]
|
|
628
658
|
ui.print('out', ui.report(`vx-migrate ${VERSION}`, rows))
|
|
629
|
-
return lock?.code ?? 0
|
|
659
|
+
return exitAfter(lock?.code ?? 0, steps)
|
|
630
660
|
}
|
|
631
661
|
|
|
632
662
|
/** `vx lock`'s outcome as the report's row. */
|
|
@@ -642,14 +672,6 @@ function lockRow(lock: { code: number; said: string }): ui.FieldRow {
|
|
|
642
672
|
/** `.vx/.gitignore`'s text, as core writes it: the machine-local cache and restore staging stay out of git. */
|
|
643
673
|
const DOT_VX_GITIGNORE = '/cache\n/tmp\n'
|
|
644
674
|
|
|
645
|
-
// npx and bunx name the package: their bare `vx` is a stranger's npm package.
|
|
646
|
-
const EXEC: Record<PackageManager, string> = {
|
|
647
|
-
pnpm: 'pnpm vx',
|
|
648
|
-
yarn: 'yarn vx',
|
|
649
|
-
bun: 'bunx @vzn/vx',
|
|
650
|
-
npm: 'npx @vzn/vx',
|
|
651
|
-
}
|
|
652
|
-
|
|
653
675
|
/**
|
|
654
676
|
* Install what the workspace file imports, if missing, then run the repo's
|
|
655
677
|
* build. Pinned to this release: every @vzn package releases in lockstep,
|
|
@@ -666,12 +688,12 @@ export function adoptionNext(
|
|
|
666
688
|
const missing = needs.filter(
|
|
667
689
|
(p) => !existsSync(path.join(root, 'node_modules', p, 'package.json')),
|
|
668
690
|
)
|
|
669
|
-
//
|
|
670
|
-
|
|
671
|
-
const run =
|
|
691
|
+
// `vx init` installs vx globally first: vx alone, never a package
|
|
692
|
+
// manager's runner (2026-10-22: vx owns the command).
|
|
693
|
+
const run = `vx run ${firstTask(path.join(root, source), runner)} --all`
|
|
672
694
|
if (missing.length === 0) return run
|
|
673
695
|
const pinned = version === null ? missing : missing.map((p) => `${p}@${version}`)
|
|
674
|
-
return `${installArgv(root, pm, pinned).join(' ')} &&
|
|
696
|
+
return `${installArgv(root, pm, pinned).join(' ')} && vx lock && ${run}`
|
|
675
697
|
}
|
|
676
698
|
|
|
677
699
|
/**
|
|
@@ -696,7 +718,7 @@ function firstTask(file: string, runner: 'turbo' | 'nx'): string {
|
|
|
696
718
|
/**
|
|
697
719
|
* package.json scripts → one vx.config.ts per package, and the workspace
|
|
698
720
|
* file declaring the lockfile plugin its lockfile calls for: `vx init`'s
|
|
699
|
-
* mapper until core stopped reading package.json (
|
|
721
|
+
* mapper until core stopped reading package.json (2026-10-14).
|
|
700
722
|
*/
|
|
701
723
|
async function scripts(
|
|
702
724
|
root: string,
|
|
@@ -734,12 +756,8 @@ async function scripts(
|
|
|
734
756
|
`the workspace globs (${globs.map((g) => `"${g}"`).join(', ')}) match no package.json: add a package under one, or fix the glob`,
|
|
735
757
|
]
|
|
736
758
|
: []
|
|
737
|
-
const installed = await prepareRepo(
|
|
738
|
-
|
|
739
|
-
plugins.map((p) => p.pkg),
|
|
740
|
-
args,
|
|
741
|
-
)
|
|
742
|
-
return applyMigration({
|
|
759
|
+
const installed = await prepareRepo(root, [...new Set(plugins.map((p) => p.pkg))], args)
|
|
760
|
+
const code = await applyMigration({
|
|
743
761
|
root,
|
|
744
762
|
metas,
|
|
745
763
|
plan,
|
|
@@ -757,6 +775,7 @@ async function scripts(
|
|
|
757
775
|
plugins,
|
|
758
776
|
...(args.noInstall === true ? {} : { lock: lockWorkspace }),
|
|
759
777
|
})
|
|
778
|
+
return exitAfter(code, installed)
|
|
760
779
|
}
|
|
761
780
|
|
|
762
781
|
/**
|
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)
|