@vzn/vx-migrate 0.0.651 → 0.0.653
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 +61 -52
- package/package.json +2 -2
- package/src/migrate-scripts.ts +13 -7
- package/src/migrate-turbo.ts +8 -1
- package/src/migrate.ts +102 -18
- package/src/nx/index.ts +61 -11
- package/src/nx/nx-inputs.ts +48 -24
- package/src/nx/nx-map.ts +204 -20
- package/src/nx/nx-outputs.ts +6 -1
- package/src/nx/nx-upstream.ts +11 -11
- package/src/nx-cache/index.ts +42 -28
- package/src/packages.ts +51 -1
- package/src/remote-refusal.ts +49 -4
- package/src/shared-outputs.ts +86 -25
- package/src/tracked-outputs.ts +59 -20
- package/src/turbo/index.ts +2 -1
- package/src/turbo/turbo-map.ts +165 -29
- package/src/turbo-cache/index.ts +71 -40
- package/src/turbo-cache/oidc.ts +15 -7
- package/src/vite-task/vite-task-map.ts +3 -2
- package/src/workspace-notes.ts +19 -8
- package/src/workspace-plugins.ts +11 -3
package/src/migrate.ts
CHANGED
|
@@ -12,7 +12,18 @@
|
|
|
12
12
|
// The mappers return a plan; core's migration seam
|
|
13
13
|
// (`applyMigration`) renders, guards, writes and reports.
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
closeSync,
|
|
17
|
+
constants,
|
|
18
|
+
existsSync,
|
|
19
|
+
lstatSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
openSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
readlinkSync,
|
|
24
|
+
realpathSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from 'node:fs'
|
|
16
27
|
import { mkdtemp, rm } from 'node:fs/promises'
|
|
17
28
|
import os from 'node:os'
|
|
18
29
|
import path from 'node:path'
|
|
@@ -36,6 +47,7 @@ import {
|
|
|
36
47
|
type PackageMeta,
|
|
37
48
|
type PackageWorkspace,
|
|
38
49
|
reachesManifest,
|
|
50
|
+
undecodableMembers,
|
|
39
51
|
unreachedHint,
|
|
40
52
|
unreachedPackages,
|
|
41
53
|
} from './packages.js'
|
|
@@ -55,6 +67,7 @@ import {
|
|
|
55
67
|
MODE_QUESTION,
|
|
56
68
|
ownVersion,
|
|
57
69
|
packageManagerOf,
|
|
70
|
+
type PackageManager,
|
|
58
71
|
parseModeAnswer,
|
|
59
72
|
uninstall,
|
|
60
73
|
} from './adopt.js'
|
|
@@ -327,9 +340,11 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
|
|
|
327
340
|
writesWorkspace && !bare
|
|
328
341
|
? [
|
|
329
342
|
...kept,
|
|
330
|
-
...workspacePlugins(
|
|
331
|
-
|
|
332
|
-
|
|
343
|
+
...workspacePlugins(
|
|
344
|
+
root,
|
|
345
|
+
runner === 'vite-task' ? undefined : runner,
|
|
346
|
+
installer(root, parsed),
|
|
347
|
+
).filter((p) => !kept.some((k) => k.factory === p.factory)),
|
|
333
348
|
]
|
|
334
349
|
: []
|
|
335
350
|
let workspaceText: string | undefined
|
|
@@ -490,10 +505,60 @@ async function rewriteWorkspace(
|
|
|
490
505
|
const names = dropped.map((p) => `${p.factory}()`).join(', ')
|
|
491
506
|
if (dry)
|
|
492
507
|
return [['config', `would ${dropped.length > 0 ? `drop ${names} from` : 'rewrite'} ${file}`]]
|
|
493
|
-
|
|
508
|
+
writeNoFollow(root, path.join(root, file), text)
|
|
494
509
|
return [['config', `${dropped.length > 0 ? `dropped ${names} from` : 'rewrote'} ${file}`]]
|
|
495
510
|
}
|
|
496
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Refuses a `file` that is a symlink, a dangling one too: a repo can carry
|
|
514
|
+
* `.vx/.gitignore -> /anywhere`, and a write through it lands outside.
|
|
515
|
+
*/
|
|
516
|
+
function refuseSymlink(root: string, file: string): void {
|
|
517
|
+
let link: string
|
|
518
|
+
try {
|
|
519
|
+
if (!lstatSync(file).isSymbolicLink()) return
|
|
520
|
+
link = readlinkSync(file)
|
|
521
|
+
} catch {
|
|
522
|
+
// quiet: nothing there to write through
|
|
523
|
+
return
|
|
524
|
+
}
|
|
525
|
+
throw new UserError(
|
|
526
|
+
`refusing to write ${relPosix(root, file)}: it is a symlink to ${link}; remove the link and run again`,
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Writes `text` to `file`, never through a link: O_NOFOLLOW refuses one
|
|
532
|
+
* put there after `refuseSymlink`'s check.
|
|
533
|
+
*/
|
|
534
|
+
function writeNoFollow(root: string, file: string, text: string): void {
|
|
535
|
+
refuseSymlink(root, file)
|
|
536
|
+
mkdirSync(path.dirname(file), { recursive: true })
|
|
537
|
+
let fd: number
|
|
538
|
+
try {
|
|
539
|
+
fd = openSync(
|
|
540
|
+
file,
|
|
541
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW,
|
|
542
|
+
0o666,
|
|
543
|
+
)
|
|
544
|
+
} catch (err) {
|
|
545
|
+
if ((err as NodeJS.ErrnoException).code === 'ELOOP') refuseSymlink(root, file)
|
|
546
|
+
throw err
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
writeFileSync(fd, text)
|
|
550
|
+
} finally {
|
|
551
|
+
closeSync(fd)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** The manager `prepareRepo` installs with, or none under `--no-install`. */
|
|
556
|
+
function installer(root: string, args: MigrateArgs): PackageManager | undefined {
|
|
557
|
+
return args.noInstall === true
|
|
558
|
+
? undefined
|
|
559
|
+
: packageManagerOf(root, process.env['npm_config_user_agent'])
|
|
560
|
+
}
|
|
561
|
+
|
|
497
562
|
/**
|
|
498
563
|
* Install what the written files import and remove what they no longer
|
|
499
564
|
* do. Returns the report's rows; changes nothing under `--dry`.
|
|
@@ -555,7 +620,10 @@ async function keep(
|
|
|
555
620
|
): Promise<number> {
|
|
556
621
|
const format: MigrationFormat = args.mjs ? 'mjs' : 'ts'
|
|
557
622
|
const name = `.vx/workspace.${format}`
|
|
558
|
-
const all = [
|
|
623
|
+
const all = [
|
|
624
|
+
{ pkg: '@vzn/vx-migrate', factory: runner },
|
|
625
|
+
...workspacePlugins(root, runner, installer(root, args)),
|
|
626
|
+
]
|
|
559
627
|
const existing = workspaceFileAt(root)
|
|
560
628
|
const before =
|
|
561
629
|
existing === undefined || args.force
|
|
@@ -571,6 +639,14 @@ async function keep(
|
|
|
571
639
|
const extended = before === undefined ? null : extendWorkspaceFile(before, all)
|
|
572
640
|
const text = before === undefined ? renderWorkspaceFile(all, format) : (extended ?? before)
|
|
573
641
|
const declares = before === undefined || extended !== null
|
|
642
|
+
const ignore = existsSync(path.join(root, '.vx', '.gitignore')) ? [] : ['.vx/.gitignore']
|
|
643
|
+
const writes = [
|
|
644
|
+
...(before === undefined ? [name] : text !== before ? [existing!] : []),
|
|
645
|
+
...ignore,
|
|
646
|
+
].map((f) => path.join(root, f))
|
|
647
|
+
// Before the install and the first write: a link (a dangling one reads
|
|
648
|
+
// as absent above) is refused with nothing changed.
|
|
649
|
+
if (!args.dry) for (const f of writes) refuseSymlink(root, f)
|
|
574
650
|
const steps = await prepareRepo(
|
|
575
651
|
root,
|
|
576
652
|
[
|
|
@@ -585,14 +661,12 @@ async function keep(
|
|
|
585
661
|
)
|
|
586
662
|
if (args.json)
|
|
587
663
|
for (const [label, value] of steps) ui.say('info', `vx-migrate: ${label}: ${value}`)
|
|
588
|
-
const ignore = existsSync(path.join(root, '.vx', '.gitignore')) ? [] : ['.vx/.gitignore']
|
|
589
664
|
if (!args.dry) {
|
|
590
|
-
if (before === undefined)
|
|
591
|
-
|
|
592
|
-
await Bun.write(path.join(root, name), text)
|
|
593
|
-
} else if (text !== before) await Bun.write(path.join(root, existing!), text)
|
|
665
|
+
if (before === undefined && existing !== undefined && existing !== name)
|
|
666
|
+
await rm(path.join(root, existing))
|
|
594
667
|
// vx's own housekeeping, as a run's first cache open writes it.
|
|
595
|
-
|
|
668
|
+
for (const f of writes)
|
|
669
|
+
writeNoFollow(root, f, f.endsWith('.gitignore') ? DOT_VX_GITIGNORE : text)
|
|
596
670
|
}
|
|
597
671
|
const cache = REMOTE_CACHE[runner]
|
|
598
672
|
const cacheLine =
|
|
@@ -735,7 +809,8 @@ async function scripts(
|
|
|
735
809
|
}
|
|
736
810
|
const unmapped = namelessNotes(root, nameless)
|
|
737
811
|
// Only a workspace file this run writes declares it; one already here is the user's.
|
|
738
|
-
const lockfile =
|
|
812
|
+
const lockfile =
|
|
813
|
+
workspaceFileAt(root) === undefined ? lockfilePlugin(root, installer(root, args)) : undefined
|
|
739
814
|
const plugins =
|
|
740
815
|
lockfile === undefined ? [] : [{ pkg: '@vzn/vx-lockfile', factory: lockfile.factory }]
|
|
741
816
|
const plan = migrateScripts(
|
|
@@ -751,12 +826,21 @@ async function scripts(
|
|
|
751
826
|
// to fix (M-42). Asked of the globs, not of `metas`: a nameless or
|
|
752
827
|
// shared-name member is matched yet absent from `metas` (M-46).
|
|
753
828
|
const globs = workspace.packageGlobs.filter((g) => g !== '.')
|
|
754
|
-
const
|
|
829
|
+
const unread =
|
|
755
830
|
empty && globs.length > 0 && !(await reachesManifest(workspace))
|
|
756
|
-
?
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
831
|
+
? await undecodableMembers(workspace)
|
|
832
|
+
: undefined
|
|
833
|
+
const noMembers =
|
|
834
|
+
unread === undefined
|
|
835
|
+
? []
|
|
836
|
+
: unread.length > 0
|
|
837
|
+
? unread.map(
|
|
838
|
+
(d) =>
|
|
839
|
+
`${d}: the directory's name is not valid UTF-8, so its package.json cannot be read — rename it`,
|
|
840
|
+
)
|
|
841
|
+
: [
|
|
842
|
+
`the workspace globs (${globs.map((g) => `"${g}"`).join(', ')}) match no package.json: add a package under one, or fix the glob`,
|
|
843
|
+
]
|
|
760
844
|
const installed = await prepareRepo(root, [...new Set(plugins.map((p) => p.pkg))], args)
|
|
761
845
|
const code = await applyMigration({
|
|
762
846
|
root,
|
package/src/nx/index.ts
CHANGED
|
@@ -53,18 +53,43 @@ export type {
|
|
|
53
53
|
} from './nx-map.js'
|
|
54
54
|
import { listDotenv } from './nx-dotenv.js'
|
|
55
55
|
import { trackedKinds } from '../tracked-outputs.js'
|
|
56
|
-
import { relPosix } from '../paths.js'
|
|
56
|
+
import { isRecord, relPosix } from '../paths.js'
|
|
57
57
|
import { memberProjects, withManifests } from '../manifests.js'
|
|
58
58
|
import type { AdoptionMapping } from '../mapping-cache.js'
|
|
59
59
|
import { yarnrcText } from '../script-command.js'
|
|
60
|
+
import { lockfilePlugin } from '../workspace-notes.js'
|
|
60
61
|
|
|
61
62
|
/** The snapshot's name under vx's cache dir — local to the machine, like the cache. */
|
|
62
63
|
const SNAPSHOT = 'nx-project-graph.json'
|
|
63
64
|
/** What the snapshot was exported from (`graphInputKey`), beside it. */
|
|
64
65
|
const SNAPSHOT_KEY = 'nx-project-graph.key'
|
|
65
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* The files at the workspace root whose edit can move Nx's graph: Nx's own,
|
|
68
|
+
* and the tool configs an inferring `@nx/*` plugin reads (a root project's
|
|
69
|
+
* `jest.config.ts`, the `jest.preset.js` a project's extends).
|
|
70
|
+
*/
|
|
66
71
|
const ROOT_GRAPH_FILE =
|
|
67
|
-
/^(nx\.json|package\.json|\.nxignore|tsconfig[^/]*\.json|pnpm-workspace\.yaml|pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?)$/
|
|
72
|
+
/^(nx\.json|package\.json|\.nxignore|tsconfig[^/]*\.json|pnpm-workspace\.yaml|pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?|jest\.(config|preset)\.[cm]?[jt]s|\.eslintrc(\.[a-z]+)?|eslint\.config\.[cm]?[jt]s|(vite|vitest|playwright|cypress|webpack|rollup|rspack|next)\.config\.[cm]?[jt]s|vitest\.workspace\.[cm]?[jt]s)$/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Whether a root-relative path can be part of a local plugin nx.json names
|
|
76
|
+
* by path (`./tools/plugin.js`, `{ plugin: './plugins/obj' }`): anything in
|
|
77
|
+
* the plugin's directory, where the modules it imports sit; one at the root
|
|
78
|
+
* is the file itself, with any extension, or its directory.
|
|
79
|
+
*/
|
|
80
|
+
function localPlugins(json: Record<string, unknown> | undefined): (p: string) => boolean {
|
|
81
|
+
const plugins = Array.isArray(json?.['plugins']) ? (json['plugins'] as unknown[]) : []
|
|
82
|
+
const tests = plugins.flatMap((e): ((p: string) => boolean)[] => {
|
|
83
|
+
const spec = typeof e === 'string' ? e : isRecord(e) ? e['plugin'] : undefined
|
|
84
|
+
if (typeof spec !== 'string' || !spec.startsWith('./')) return []
|
|
85
|
+
const file = path.posix.normalize(spec)
|
|
86
|
+
const dir = path.posix.dirname(file)
|
|
87
|
+
if (dir !== '.') return [(p) => p.startsWith(`${dir}/`)]
|
|
88
|
+
const stem = file.replace(/\.[^.]+$/, '')
|
|
89
|
+
return [(p) => p === file || p.startsWith(`${file}/`) || p.startsWith(`${stem}.`)]
|
|
90
|
+
})
|
|
91
|
+
return (p) => tests.some((t) => t(p))
|
|
92
|
+
}
|
|
68
93
|
|
|
69
94
|
export interface NxPluginOptions {
|
|
70
95
|
/**
|
|
@@ -118,6 +143,9 @@ export function nx(options: NxPluginOptions = {}): VxPlugin {
|
|
|
118
143
|
const rootOf = (workspaceRoot: string): string =>
|
|
119
144
|
options.root === undefined ? workspaceRoot : path.resolve(workspaceRoot, options.root)
|
|
120
145
|
refuseExecutors(options.executors)
|
|
146
|
+
// The root files the workspace's plugins claim, read by `config`: a
|
|
147
|
+
// lockfile no plugin claims is in no key unless a task lists it.
|
|
148
|
+
let claimed: ReadonlySet<string> = new Set()
|
|
121
149
|
// One graph load per RUN, shared by `discover` and `project`: the run
|
|
122
150
|
// hands both stages the same projects array (discover's, grown by what
|
|
123
151
|
// it named), so its identity is the run's, as the mapping's is.
|
|
@@ -141,17 +169,26 @@ export function nx(options: NxPluginOptions = {}): VxPlugin {
|
|
|
141
169
|
import.meta,
|
|
142
170
|
async (ctx) => {
|
|
143
171
|
const root = rootOf(ctx.workspaceRoot)
|
|
172
|
+
const lock = lockfilePlugin(root)?.file
|
|
173
|
+
const unclaimed =
|
|
174
|
+
lock !== undefined && !(root === ctx.workspaceRoot && claimed.has(lock)) ? lock : undefined
|
|
144
175
|
return mapAll(
|
|
145
176
|
root,
|
|
146
177
|
ctx.projects,
|
|
147
178
|
await graphOf(root, ctx.cacheDir, ctx.projects),
|
|
148
179
|
options.executors,
|
|
180
|
+
unclaimed,
|
|
149
181
|
)
|
|
150
182
|
},
|
|
151
183
|
// At the workspace root only, as `turbo()` claims its file.
|
|
152
184
|
options.root === undefined ? ['nx.json'] : [],
|
|
153
185
|
{
|
|
154
186
|
async config(workspace, ctx) {
|
|
187
|
+
claimed = new Set(
|
|
188
|
+
(workspace.plugins ?? []).flatMap((p) =>
|
|
189
|
+
p ? ((p as VxPlugin).fingerprint?.files ?? []) : [],
|
|
190
|
+
),
|
|
191
|
+
)
|
|
155
192
|
const root = rootOf(ctx.workspaceRoot)
|
|
156
193
|
const nxBaseCommit = Bun.env['NX_BASE']?.trim()
|
|
157
194
|
if (nxBaseCommit) ctx.setBranch({ base: nxBaseCommit })
|
|
@@ -305,6 +342,7 @@ async function mapAll(
|
|
|
305
342
|
projects: readonly ProjectMeta[],
|
|
306
343
|
loaded: LoadedGraph,
|
|
307
344
|
executors: NxExecutors | undefined,
|
|
345
|
+
lockfile: string | undefined,
|
|
308
346
|
): Promise<AdoptionRun> {
|
|
309
347
|
const metas = await withManifests(projects)
|
|
310
348
|
const { text, graph, notes } = loaded
|
|
@@ -312,11 +350,12 @@ async function mapAll(
|
|
|
312
350
|
// The mapping is cached on what it read; a translator is code it ran.
|
|
313
351
|
if (executors !== undefined)
|
|
314
352
|
reads.push(JSON.stringify(Object.entries(executors).map(([k, fn]) => [k, String(fn)])))
|
|
353
|
+
reads.push(`unclaimed lockfile: ${lockfile ?? ''}`)
|
|
315
354
|
return {
|
|
316
355
|
name: 'nx',
|
|
317
356
|
spareTracked: true,
|
|
318
357
|
reads,
|
|
319
|
-
map: (tracked) => index(root, metas, graph, notes, tracked, executors),
|
|
358
|
+
map: (tracked) => index(root, metas, graph, notes, tracked, executors, lockfile),
|
|
320
359
|
}
|
|
321
360
|
}
|
|
322
361
|
|
|
@@ -335,7 +374,7 @@ const textOf = (file: string): Promise<string> =>
|
|
|
335
374
|
* and the package.json of each graph node no package matches (its
|
|
336
375
|
* synthetic project's name), the `.env` names in every project dir,
|
|
337
376
|
* NX_LOAD_DOT_ENV_FILES, whether the bins the tasks run are installed,
|
|
338
|
-
* and the graph load's own notes.
|
|
377
|
+
* which root tsconfig exists, and the graph load's own notes.
|
|
339
378
|
*/
|
|
340
379
|
async function nxReads(
|
|
341
380
|
root: string,
|
|
@@ -379,6 +418,13 @@ async function nxReads(
|
|
|
379
418
|
...(await Promise.all(
|
|
380
419
|
['package.json', '.npmrc', 'pnpm-workspace.yaml'].map((f) => textOf(path.join(root, f))),
|
|
381
420
|
)),
|
|
421
|
+
// Which root tsconfig every task keys (`rootTsConfig`), decided by
|
|
422
|
+
// existence: an untracked new one changes no git index.
|
|
423
|
+
JSON.stringify(
|
|
424
|
+
await Promise.all(
|
|
425
|
+
['tsconfig.base.json', 'tsconfig.json'].map((f) => Bun.file(path.join(root, f)).exists()),
|
|
426
|
+
),
|
|
427
|
+
),
|
|
382
428
|
...notes,
|
|
383
429
|
]
|
|
384
430
|
}
|
|
@@ -390,6 +436,7 @@ async function index(
|
|
|
390
436
|
loadNotes: readonly string[],
|
|
391
437
|
trackedFiles: () => Promise<readonly string[] | null>,
|
|
392
438
|
executors: NxExecutors | undefined,
|
|
439
|
+
lockfile: string | undefined,
|
|
393
440
|
): Promise<AdoptionMapping> {
|
|
394
441
|
const notes: string[] = []
|
|
395
442
|
const live = [...loadNotes]
|
|
@@ -407,6 +454,7 @@ async function index(
|
|
|
407
454
|
...(tracked === null ? {} : { tracked: trackedKinds(tracked) }),
|
|
408
455
|
ownConfig: (rel) => configs.get(relRoot(rel)) ?? null,
|
|
409
456
|
...(executors === undefined ? {} : { executors }),
|
|
457
|
+
...(lockfile === undefined ? {} : { lockfile }),
|
|
410
458
|
})
|
|
411
459
|
notes.push(...mapped.notes)
|
|
412
460
|
const byName = new Map<string, GeneratedProject>()
|
|
@@ -587,21 +635,23 @@ async function graphInputKey(
|
|
|
587
635
|
// A `.gitignore` anywhere too: Nx's project walk honours each one, so an
|
|
588
636
|
// edit between project roots hides or shows a project.
|
|
589
637
|
const roots = dirs.map((d) => relPosix(root, d))
|
|
638
|
+
// A manifest needs no read of its own: git lists it when it is edited, and
|
|
639
|
+
// HEAD moves when an edit is committed (reading all 2,000 cost 50 ms at
|
|
640
|
+
// 1,000 projects). nx.json's chain does: a base may sit in node_modules,
|
|
641
|
+
// where git does not look.
|
|
642
|
+
const nxJson = await readNxJson(root).catch(() => null)
|
|
643
|
+
const plugin = localPlugins(nxJson?.json)
|
|
590
644
|
const graphFile = (p: string): boolean =>
|
|
591
645
|
p === 'project.json' ||
|
|
592
646
|
p.endsWith('/project.json') ||
|
|
593
647
|
p === '.gitignore' ||
|
|
594
648
|
p.endsWith('/.gitignore') ||
|
|
595
649
|
roots.some((r) => r === '' || p.startsWith(`${r}/`)) ||
|
|
596
|
-
(!p.includes('/') && ROOT_GRAPH_FILE.test(p))
|
|
650
|
+
(!p.includes('/') && ROOT_GRAPH_FILE.test(p)) ||
|
|
651
|
+
plugin(p)
|
|
597
652
|
const listed = [
|
|
598
653
|
...new Set(changed.filter((p) => !inCache(p) && !nxCache(p) && !vxDir(p) && graphFile(p))),
|
|
599
654
|
]
|
|
600
|
-
// A manifest needs no read of its own: git lists it when it is edited, and
|
|
601
|
-
// HEAD moves when an edit is committed (reading all 2,000 cost 50 ms at
|
|
602
|
-
// 1,000 projects). nx.json's chain does: a base may sit in node_modules,
|
|
603
|
-
// where git does not look.
|
|
604
|
-
const nxJson = await readNxJson(root).catch(() => null)
|
|
605
655
|
const files = [
|
|
606
656
|
...(nxJson?.files ?? [path.join(root, 'nx.json')]),
|
|
607
657
|
...listed.sort().map((p) => path.join(root, p)),
|
package/src/nx/nx-inputs.ts
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
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'
|
|
11
10
|
import { nxWorkspacePath, underProject, withProjectDir } from './nx-outputs.js'
|
|
12
|
-
import
|
|
13
|
-
|
|
11
|
+
import {
|
|
12
|
+
apartTakeBacks,
|
|
13
|
+
gluedGlobstarAsStar,
|
|
14
|
+
minimatchToVx,
|
|
15
|
+
nxBareGroups,
|
|
16
|
+
} from '../glob-grammar.js'
|
|
14
17
|
import { shellQuote } from '../nx-command.js'
|
|
15
18
|
|
|
16
19
|
export interface NxInputs {
|
|
@@ -37,6 +40,8 @@ export interface NxInputs {
|
|
|
37
40
|
}>
|
|
38
41
|
/** `{ dependentTasksOutputFiles }` globs: keyed only through a `dependsOn` edge. */
|
|
39
42
|
readonly dependentOutputs: string[]
|
|
43
|
+
/** Those of them marked `transitive`: read from every task below, not only the direct ones. */
|
|
44
|
+
readonly transitiveOutputs: string[]
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
export function emptyNxInputs(): NxInputs {
|
|
@@ -47,6 +52,7 @@ export function emptyNxInputs(): NxInputs {
|
|
|
47
52
|
runtimeCmds: [],
|
|
48
53
|
upstream: [],
|
|
49
54
|
dependentOutputs: [],
|
|
55
|
+
transitiveOutputs: [],
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
|
|
@@ -78,14 +84,19 @@ export function expandNxInputs(
|
|
|
78
84
|
readonly rel: string
|
|
79
85
|
readonly name: string
|
|
80
86
|
readonly outputs?: readonly string[]
|
|
81
|
-
/**
|
|
82
|
-
readonly
|
|
83
|
-
/** The root package.json's dependency names:
|
|
87
|
+
/** The root-relative outputs git ignores; unknown, every output counts as ignored. */
|
|
88
|
+
readonly ignoredOutputs?: ReadonlySet<string>
|
|
89
|
+
/** The root package.json's dependency names: a lockfile plugin keys them into every task. */
|
|
84
90
|
readonly rootDeps?: ReadonlySet<string>
|
|
91
|
+
/** The root lockfile no plugin claims, which nothing keys unless a task lists it. */
|
|
92
|
+
readonly lockfile?: string
|
|
85
93
|
},
|
|
86
94
|
into: NxInputs,
|
|
87
95
|
todos: string[],
|
|
88
96
|
): void {
|
|
97
|
+
// A `{workspaceRoot}` path into the project, kept apart from the
|
|
98
|
+
// project's own set until the end (see there).
|
|
99
|
+
const wsOwn: string[] = []
|
|
89
100
|
const expand = (entry: unknown, seen: Set<string>): void => {
|
|
90
101
|
if (typeof entry === 'string') {
|
|
91
102
|
let s = entry
|
|
@@ -113,14 +124,7 @@ export function expandNxInputs(
|
|
|
113
124
|
// An output git tracks (a generator writing into `src`) is hashed,
|
|
114
125
|
// and dropping it keyed `build` on no file (stale hit, 2026-10-28).
|
|
115
126
|
const lit = withProjectDir(p, at.rel, false)
|
|
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
|
-
}
|
|
127
|
+
const ignored = (o: string): boolean => at.ignoredOutputs?.has(o) ?? true
|
|
124
128
|
if (
|
|
125
129
|
neg === '' &&
|
|
126
130
|
at.outputs?.some((o) => (lit === o || lit.startsWith(`${o}/`)) && ignored(o))
|
|
@@ -141,7 +145,10 @@ export function expandNxInputs(
|
|
|
141
145
|
? null
|
|
142
146
|
: underProject(g, at.rel)
|
|
143
147
|
if (own === null) into.wsFiles.push(neg + withProjectDir(g, at.rel, true))
|
|
144
|
-
else
|
|
148
|
+
else
|
|
149
|
+
(s.startsWith('{workspaceRoot}') ? wsOwn : into.files).push(
|
|
150
|
+
neg + (own === '' ? '**/*' : withProjectDir(own, at.rel, true)),
|
|
151
|
+
)
|
|
145
152
|
return
|
|
146
153
|
}
|
|
147
154
|
// Bare string = named-input reference.
|
|
@@ -192,18 +199,23 @@ export function expandNxInputs(
|
|
|
192
199
|
return
|
|
193
200
|
}
|
|
194
201
|
if (o.externalDependencies !== undefined) {
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
202
|
+
// Nx hashes each named package's resolved version, and core keys no
|
|
203
|
+
// lockfile. With no plugin claiming the file, the task keys it whole,
|
|
204
|
+
// a superset: a `left-pad` bump was a hit where Nx re-ran. A lockfile
|
|
205
|
+
// plugin keys the project's and the root's dependencies, so a name
|
|
206
|
+
// the root declares is keyed there: the todo sat on 32 tasks of
|
|
207
|
+
// analogjs for `eslint`. Nx's inferred targets name root
|
|
208
|
+
// devDependencies (eslint, vitest), not the project's package.json.
|
|
200
209
|
const names = Array.isArray(o.externalDependencies) ? o.externalDependencies : []
|
|
210
|
+
if (at.lockfile !== undefined) {
|
|
211
|
+
expand(`{workspaceRoot}/${at.lockfile}`, seen)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
201
214
|
if (names.length > 0 && names.every((n) => typeof n === 'string' && at.rootDeps?.has(n)))
|
|
202
215
|
return
|
|
203
216
|
todos.push(
|
|
204
|
-
`input {externalDependencies: ${JSON.stringify(o.externalDependencies)}}: vx
|
|
205
|
-
|
|
206
|
-
"project's and the root's dependencies) — safe to drop unless only another project installs one",
|
|
217
|
+
`input {externalDependencies: ${JSON.stringify(o.externalDependencies)}}: a @vzn/vx-lockfile ` +
|
|
218
|
+
"plugin keys each task on its project's and the root's dependencies — one only another project installs is not keyed",
|
|
207
219
|
)
|
|
208
220
|
return
|
|
209
221
|
}
|
|
@@ -214,7 +226,9 @@ export function expandNxInputs(
|
|
|
214
226
|
// The mapper checks an edge is left to carry them.
|
|
215
227
|
if (o.dependentTasksOutputFiles !== undefined) {
|
|
216
228
|
const g = o.dependentTasksOutputFiles
|
|
217
|
-
|
|
229
|
+
const glob = typeof g === 'string' ? g : JSON.stringify(g)
|
|
230
|
+
into.dependentOutputs.push(glob)
|
|
231
|
+
if (o.transitive === true) into.transitiveOutputs.push(glob)
|
|
218
232
|
return
|
|
219
233
|
}
|
|
220
234
|
// `{ workingDirectory }` hashes where Nx was started (`cwd:relative`
|
|
@@ -276,6 +290,16 @@ export function expandNxInputs(
|
|
|
276
290
|
)
|
|
277
291
|
}
|
|
278
292
|
for (const entry of entries) expand(entry, new Set())
|
|
293
|
+
// Nx hashes the `{projectRoot}` and `{workspaceRoot}` filesets as two
|
|
294
|
+
// sets, a `!` acting only inside its own; core subtracts every `!` in
|
|
295
|
+
// `files` from every entry. Production's `!{projectRoot}/jest.config.[jt]s`
|
|
296
|
+
// took out a target's `{workspaceRoot}/libs/a/jest.config.ts`, and an
|
|
297
|
+
// edit to it replayed (stale hit). A `!` that may reach the other set
|
|
298
|
+
// goes: more keyed, never less.
|
|
299
|
+
if (wsOwn.length > 0) {
|
|
300
|
+
const [own, ws] = apartTakeBacks(into.files, wsOwn)
|
|
301
|
+
into.files.splice(0, into.files.length, ...own, ...ws)
|
|
302
|
+
}
|
|
279
303
|
// Nx matches a project fileset with no positive glob against every project
|
|
280
304
|
// file, the negations taken out: nx-recipes' `noMarkdown` is
|
|
281
305
|
// `["!{projectRoot}/**/*.md"]`, and a `.tsx` edit re-runs its build while a
|