@vzn/vx-migrate 0.0.636 → 0.0.637
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 +294 -42
- package/package.json +3 -3
- package/src/adopt.ts +23 -8
- package/src/manifest-graph.ts +65 -0
- package/src/manifests.ts +52 -0
- package/src/mapping-cache.ts +2 -8
- package/src/migrate-nx.ts +17 -16
- package/src/migrate-scripts.ts +1261 -0
- package/src/migrate-turbo.ts +6 -7
- package/src/migrate-vite-task.ts +5 -4
- package/src/migrate.ts +376 -139
- package/src/nx/index.ts +77 -20
- package/src/nx/nx-boundaries.ts +191 -0
- package/src/nx/nx-deps.ts +3 -3
- package/src/nx/nx-map.ts +37 -41
- package/src/nx/nx-upstream.ts +3 -2
- package/src/nx-cache/index.ts +5 -11
- package/src/package-fs.ts +152 -0
- package/src/packages.ts +806 -0
- package/src/paths.ts +2 -2
- package/src/remote-cache.ts +118 -0
- package/src/shared-outputs.ts +63 -19
- package/src/tracked-outputs.ts +54 -35
- package/src/turbo/index.ts +32 -20
- package/src/turbo/turbo-map.ts +96 -56
- package/src/turbo-cache/index.ts +14 -20
- package/src/vite-task/vite-task-map.ts +13 -12
- package/src/workspace-notes.ts +12 -6
- package/src/workspace-plugins.ts +41 -11
package/src/nx/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { stat } from 'node:fs/promises'
|
|
|
23
23
|
import { availableParallelism } from 'node:os'
|
|
24
24
|
import path from 'node:path'
|
|
25
25
|
import {
|
|
26
|
+
type DiscoveredProject,
|
|
26
27
|
refuseUnknownOptions,
|
|
27
28
|
type PluginOptionKinds,
|
|
28
29
|
type GeneratedProject,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
UserError,
|
|
31
32
|
type VxPlugin,
|
|
32
33
|
} from '@vzn/vx'
|
|
34
|
+
import type { PackageMeta } from '../packages.js'
|
|
33
35
|
import { type AdoptionRun, adoptionPlugin } from '../adoption-plugin.js'
|
|
34
36
|
import { collectGaps } from '../plugin-gaps.js'
|
|
35
37
|
import {
|
|
@@ -43,6 +45,7 @@ import {
|
|
|
43
45
|
} from './nx-map.js'
|
|
44
46
|
import { exportGraph } from './export-graph.js'
|
|
45
47
|
import { lernaJsonText } from './lerna.js'
|
|
48
|
+
import { nxDependencyRules } from './nx-boundaries.js'
|
|
46
49
|
export type {
|
|
47
50
|
NxExecutors,
|
|
48
51
|
NxExecutorTarget,
|
|
@@ -52,6 +55,7 @@ export type {
|
|
|
52
55
|
import { listDotenv } from './nx-dotenv.js'
|
|
53
56
|
import { trackedKinds } from '../tracked-outputs.js'
|
|
54
57
|
import { relPosix } from '../paths.js'
|
|
58
|
+
import { memberProjects, withManifests } from '../manifests.js'
|
|
55
59
|
import type { AdoptionMapping } from '../mapping-cache.js'
|
|
56
60
|
import { yarnrcText } from '../script-command.js'
|
|
57
61
|
|
|
@@ -119,10 +123,12 @@ export function nx(options: NxPluginOptions = {}): VxPlugin {
|
|
|
119
123
|
cacheDir: string,
|
|
120
124
|
projects: readonly ProjectMeta[],
|
|
121
125
|
changes?: WorktreeChanges,
|
|
126
|
+
members: readonly DiscoveredProject[] = [],
|
|
122
127
|
) => {
|
|
123
128
|
if (graphFor !== projects) {
|
|
124
129
|
graphFor = projects
|
|
125
|
-
|
|
130
|
+
const also = members.map((m) => ({ name: m.name ?? '', dir: m.dir, configPath: null }))
|
|
131
|
+
graph = loadGraph(root, cacheDir, [...projects, ...also], options.graph, changes)
|
|
126
132
|
}
|
|
127
133
|
return graph!
|
|
128
134
|
}
|
|
@@ -142,33 +148,45 @@ export function nx(options: NxPluginOptions = {}): VxPlugin {
|
|
|
142
148
|
{
|
|
143
149
|
async config(workspace, ctx) {
|
|
144
150
|
const root = options.root ?? ctx.workspaceRoot
|
|
145
|
-
if (workspace.affectedBase === undefined) {
|
|
151
|
+
if (workspace.run?.affectedBase === undefined) {
|
|
146
152
|
const base = await nxBase(root)
|
|
147
|
-
if (base !== undefined) workspace.
|
|
153
|
+
if (base !== undefined) workspace.run = { ...workspace.run, affectedBase: base }
|
|
148
154
|
}
|
|
149
|
-
if (workspace.concurrency === undefined) {
|
|
155
|
+
if (workspace.run?.concurrency === undefined) {
|
|
150
156
|
const parallel = await nxParallel(root)
|
|
151
|
-
if (parallel !== undefined) workspace.
|
|
157
|
+
if (parallel !== undefined) workspace.run = { ...workspace.run, concurrency: parallel }
|
|
152
158
|
}
|
|
153
|
-
if (workspace.
|
|
159
|
+
if (workspace.run?.rules?.dependencies === undefined) {
|
|
160
|
+
const dependencies = await nxDependencyRules(root)
|
|
161
|
+
if (dependencies !== undefined && dependencies.length > 0)
|
|
162
|
+
workspace.run = {
|
|
163
|
+
...workspace.run,
|
|
164
|
+
rules: { ...workspace.run?.rules, dependencies },
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (workspace.cache?.retention === undefined) {
|
|
154
168
|
const maxSize = await nxMaxCacheSize(root)
|
|
155
|
-
if (maxSize !== undefined)
|
|
169
|
+
if (maxSize !== undefined)
|
|
170
|
+
workspace.cache = { ...workspace.cache, retention: { maxSize } }
|
|
156
171
|
}
|
|
157
172
|
},
|
|
158
|
-
//
|
|
159
|
-
//
|
|
173
|
+
// The package manager's members are projects, each with no vx.config
|
|
174
|
+
// written. An integrated Nx repo keeps projects out of that list
|
|
175
|
+
// (analogjs: `project.json` libraries no glob names), and their
|
|
160
176
|
// targets had nowhere to attach: 1 of 21 `build` tasks ran. Each
|
|
161
|
-
// graph node with targets at a directory
|
|
162
|
-
//
|
|
177
|
+
// graph node with targets at a directory neither found is named a
|
|
178
|
+
// project, by its package.json name or else its Nx name; a name a
|
|
163
179
|
// project already holds stays unattached, and the mapping says so.
|
|
164
180
|
async discover(ctx) {
|
|
165
181
|
const root = options.root ?? ctx.workspaceRoot
|
|
182
|
+
const members = await memberProjects(ctx.workspaceRoot)
|
|
166
183
|
// Core's status is the workspace's: a root elsewhere asks git itself.
|
|
167
184
|
const changes = options.root === undefined ? () => ctx.worktreeChanges() : undefined
|
|
168
|
-
const { graph } = await graphOf(root, ctx.cacheDir, ctx.projects, changes)
|
|
169
|
-
const
|
|
170
|
-
const
|
|
171
|
-
const
|
|
185
|
+
const { graph } = await graphOf(root, ctx.cacheDir, ctx.projects, changes, members)
|
|
186
|
+
const found = [...ctx.projects, ...members]
|
|
187
|
+
const dirs = new Set(found.map((m) => path.resolve(m.dir)))
|
|
188
|
+
const names = new Set(found.map((m) => m.name))
|
|
189
|
+
const named = members.map((m) => ({ dir: path.resolve(m.dir), name: m.name! }))
|
|
172
190
|
for (const [nodeName, node] of Object.entries(graph.nodes)) {
|
|
173
191
|
if (node?.data?.targets === undefined) continue
|
|
174
192
|
const dir = path.resolve(root, node.data.root ?? '')
|
|
@@ -190,12 +208,45 @@ export function nx(options: NxPluginOptions = {}): VxPlugin {
|
|
|
190
208
|
dirs.add(dir)
|
|
191
209
|
named.push({ dir, name })
|
|
192
210
|
}
|
|
193
|
-
|
|
211
|
+
const entries = withGraphEdges(root, graph, dirs, named)
|
|
212
|
+
const covered = new Set(entries.map((e) => e.dir))
|
|
213
|
+
return [...entries, ...named.filter((n) => !covered.has(n.dir))]
|
|
194
214
|
},
|
|
195
215
|
},
|
|
196
216
|
)
|
|
197
217
|
}
|
|
198
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Every project the graph and the workspace share, with its Nx graph edges
|
|
221
|
+
* (package deps, `implicitDependencies`, imports) as `deps`: nx() is an Nx
|
|
222
|
+
* repo's edge source, so `^task` follows Nx's graph with no lockfile plugin.
|
|
223
|
+
*/
|
|
224
|
+
function withGraphEdges(
|
|
225
|
+
root: string,
|
|
226
|
+
graph: NxGraph,
|
|
227
|
+
dirs: ReadonlySet<string>,
|
|
228
|
+
named: ReadonlyArray<{ dir: string; name: string }>,
|
|
229
|
+
): DiscoveredProject[] {
|
|
230
|
+
const dirOf = new Map<string, string>()
|
|
231
|
+
for (const [nodeName, node] of Object.entries(graph.nodes)) {
|
|
232
|
+
const dir = path.resolve(root, node?.data?.root ?? '')
|
|
233
|
+
if (dirs.has(dir)) dirOf.set(nodeName, dir)
|
|
234
|
+
}
|
|
235
|
+
const nameOf = new Map(named.map((n) => [n.dir, n.name]))
|
|
236
|
+
const edges = (graph.dependencies ?? {}) as Record<string, ReadonlyArray<{ target?: unknown }>>
|
|
237
|
+
const out: DiscoveredProject[] = []
|
|
238
|
+
for (const [nodeName, dir] of dirOf) {
|
|
239
|
+
const deps = new Set<string>()
|
|
240
|
+
for (const e of edges[nodeName] ?? []) {
|
|
241
|
+
const to = typeof e?.target === 'string' ? dirOf.get(e.target) : undefined
|
|
242
|
+
if (to !== undefined && to !== dir) deps.add(to)
|
|
243
|
+
}
|
|
244
|
+
const name = nameOf.get(dir)
|
|
245
|
+
out.push({ dir, ...(name === undefined ? {} : { name }), deps: [...deps].sort() })
|
|
246
|
+
}
|
|
247
|
+
return out
|
|
248
|
+
}
|
|
249
|
+
|
|
199
250
|
function isWithin(root: string, dir: string): boolean {
|
|
200
251
|
const rel = path.relative(root, dir)
|
|
201
252
|
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))
|
|
@@ -268,10 +319,11 @@ interface LoadedGraph {
|
|
|
268
319
|
|
|
269
320
|
async function mapAll(
|
|
270
321
|
root: string,
|
|
271
|
-
|
|
322
|
+
projects: readonly ProjectMeta[],
|
|
272
323
|
loaded: LoadedGraph,
|
|
273
324
|
executors: NxExecutors | undefined,
|
|
274
325
|
): Promise<AdoptionRun> {
|
|
326
|
+
const metas = await withManifests(projects)
|
|
275
327
|
const { text, graph, notes } = loaded
|
|
276
328
|
const reads = await nxReads(root, metas, text, graph, notes)
|
|
277
329
|
// The mapping is cached on what it read; a translator is code it ran.
|
|
@@ -304,7 +356,7 @@ const textOf = (file: string): Promise<string> =>
|
|
|
304
356
|
*/
|
|
305
357
|
async function nxReads(
|
|
306
358
|
root: string,
|
|
307
|
-
metas: readonly
|
|
359
|
+
metas: readonly PackageMeta[],
|
|
308
360
|
graphText: string,
|
|
309
361
|
graph: NxGraph,
|
|
310
362
|
notes: readonly string[],
|
|
@@ -346,7 +398,7 @@ async function nxReads(
|
|
|
346
398
|
|
|
347
399
|
async function index(
|
|
348
400
|
root: string,
|
|
349
|
-
metas: readonly
|
|
401
|
+
metas: readonly PackageMeta[],
|
|
350
402
|
graph: NxGraph,
|
|
351
403
|
loadNotes: readonly string[],
|
|
352
404
|
trackedFiles: () => Promise<readonly string[] | null>,
|
|
@@ -535,6 +587,9 @@ async function graphInputKey(
|
|
|
535
587
|
// and stays in.
|
|
536
588
|
const nxCache = (p: string): boolean =>
|
|
537
589
|
p.startsWith('.nx/cache/') || p.startsWith('.nx/workspace-data/')
|
|
590
|
+
// vx's own directory (the lock `vx lock` rewrites, the workspace file):
|
|
591
|
+
// Nx reads nothing there, and under a root project each lock re-exported.
|
|
592
|
+
const vxDir = (p: string): boolean => p.startsWith('.vx/')
|
|
538
593
|
// What can move the graph: a file under a project root (its sources, the
|
|
539
594
|
// configs a plugin infers targets from), or a root file Nx reads. A task's
|
|
540
595
|
// stray write at the root (a report, a log) is not, and counting it
|
|
@@ -546,7 +601,9 @@ async function graphInputKey(
|
|
|
546
601
|
p.endsWith('/project.json') ||
|
|
547
602
|
roots.some((r) => r === '' || p.startsWith(`${r}/`)) ||
|
|
548
603
|
(!p.includes('/') && ROOT_GRAPH_FILE.test(p))
|
|
549
|
-
const listed = [
|
|
604
|
+
const listed = [
|
|
605
|
+
...new Set(changed.filter((p) => !inCache(p) && !nxCache(p) && !vxDir(p) && graphFile(p))),
|
|
606
|
+
]
|
|
550
607
|
// A manifest needs no read of its own: git lists it when it is edited, and
|
|
551
608
|
// HEAD moves when an edit is committed (reading all 2,000 cost 50 ms at
|
|
552
609
|
// 1,000 projects). nx.json's chain does: a base may sit in node_modules,
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
/** `run.rules.dependencies`' entry, as core takes it. */
|
|
4
|
+
interface DependencyRule {
|
|
5
|
+
from: string
|
|
6
|
+
allow?: string[]
|
|
7
|
+
deny?: string[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const RULE_NAMES = ['@nx/enforce-module-boundaries', '@nrwl/nx/enforce-module-boundaries']
|
|
11
|
+
const FLAT = ['eslint.config.mjs', 'eslint.config.js', 'eslint.config.cjs', 'eslint.config.ts']
|
|
12
|
+
const LEGACY = ['.eslintrc.json', '.eslintrc']
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The root ESLint config's `@nx/enforce-module-boundaries` `depConstraints`,
|
|
16
|
+
* as vx's lock-time dependency rules. A flat config is read, never run:
|
|
17
|
+
* importing it loads ESLint's plugins on every vx run, for a rule only
|
|
18
|
+
* `vx lock` checks. So the constraints must be a literal array there; a
|
|
19
|
+
* spread or a variable maps nothing. Undefined when no rule is found.
|
|
20
|
+
*/
|
|
21
|
+
export async function nxDependencyRules(root: string): Promise<DependencyRule[] | undefined> {
|
|
22
|
+
for (const name of [...FLAT, ...LEGACY]) {
|
|
23
|
+
const file = Bun.file(path.join(root, name))
|
|
24
|
+
if (!(await file.exists())) continue
|
|
25
|
+
const text = await file.text()
|
|
26
|
+
const constraints = LEGACY.includes(name) ? fromJson(text) : fromSource(text)
|
|
27
|
+
return constraints === undefined ? undefined : mapConstraints(constraints)
|
|
28
|
+
}
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `rules` and each `overrides[].rules` of a JSON config, first rule found. */
|
|
33
|
+
function fromJson(text: string): unknown[] | undefined {
|
|
34
|
+
let json: unknown
|
|
35
|
+
try {
|
|
36
|
+
json = Bun.JSONC.parse(text)
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
const configs = [json, ...((json as { overrides?: unknown[] })?.overrides ?? [])]
|
|
41
|
+
for (const c of configs) {
|
|
42
|
+
const rules = (c as { rules?: Record<string, unknown> } | null)?.rules
|
|
43
|
+
for (const r of RULE_NAMES) {
|
|
44
|
+
const entry = rules?.[r]
|
|
45
|
+
const options = Array.isArray(entry) ? entry[1] : undefined
|
|
46
|
+
const dc = (options as { depConstraints?: unknown } | undefined)?.depConstraints
|
|
47
|
+
if (Array.isArray(dc)) return dc
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The literal after the rule's `depConstraints:` in JS source. */
|
|
54
|
+
function fromSource(text: string): unknown[] | undefined {
|
|
55
|
+
for (const r of RULE_NAMES) {
|
|
56
|
+
const at = text.indexOf(r)
|
|
57
|
+
if (at === -1) continue
|
|
58
|
+
const m = /depConstraints['"]?\s*:\s*\[/.exec(text.slice(at))
|
|
59
|
+
if (m === null) return undefined
|
|
60
|
+
try {
|
|
61
|
+
const v = new Literal(text, at + m.index + m[0].length - 1).value()
|
|
62
|
+
return Array.isArray(v) ? v : undefined
|
|
63
|
+
} catch {
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Nx's constraint as a vx rule: `sourceTag` is `from`,
|
|
72
|
+
* `onlyDependOnLibsWithTags` is `allow`, `notDependOnLibsWithTags` is
|
|
73
|
+
* `deny`; `*` means any project. A constraint vx cannot say is left out:
|
|
74
|
+
* `allSourceTags` (every tag at once), a `/regex/` tag, or one that bans
|
|
75
|
+
* only external imports.
|
|
76
|
+
*/
|
|
77
|
+
function mapConstraints(constraints: unknown[]): DependencyRule[] {
|
|
78
|
+
const out: DependencyRule[] = []
|
|
79
|
+
const sel = (t: unknown) => (t === '*' ? '*' : `tag:${String(t)}`)
|
|
80
|
+
const tags = (v: unknown): string[] | undefined =>
|
|
81
|
+
Array.isArray(v) && v.every((t) => typeof t === 'string' && t !== '' && !t.startsWith('/'))
|
|
82
|
+
? v.map(sel)
|
|
83
|
+
: undefined
|
|
84
|
+
for (const c of constraints) {
|
|
85
|
+
const { sourceTag, onlyDependOnLibsWithTags, notDependOnLibsWithTags } = (c ?? {}) as Record<
|
|
86
|
+
string,
|
|
87
|
+
unknown
|
|
88
|
+
>
|
|
89
|
+
if (typeof sourceTag !== 'string' || sourceTag === '' || sourceTag.startsWith('/')) continue
|
|
90
|
+
const allow = tags(onlyDependOnLibsWithTags)
|
|
91
|
+
const deny = tags(notDependOnLibsWithTags)
|
|
92
|
+
if (allow === undefined && deny === undefined) continue
|
|
93
|
+
out.push({
|
|
94
|
+
from: sel(sourceTag),
|
|
95
|
+
...(allow !== undefined ? { allow } : {}),
|
|
96
|
+
...(deny !== undefined ? { deny } : {}),
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
return out
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A JS literal: arrays, objects, strings, numbers, booleans, null. Anything else throws. */
|
|
103
|
+
class Literal {
|
|
104
|
+
constructor(
|
|
105
|
+
private readonly s: string,
|
|
106
|
+
private i: number,
|
|
107
|
+
) {}
|
|
108
|
+
|
|
109
|
+
value(): unknown {
|
|
110
|
+
this.skip()
|
|
111
|
+
const c = this.s[this.i]
|
|
112
|
+
if (c === '[') {
|
|
113
|
+
this.i++
|
|
114
|
+
const out: unknown[] = []
|
|
115
|
+
while (!this.eat(']')) {
|
|
116
|
+
out.push(this.value())
|
|
117
|
+
if (!this.eat(',')) {
|
|
118
|
+
this.expect(']')
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out
|
|
123
|
+
}
|
|
124
|
+
if (c === '{') {
|
|
125
|
+
this.i++
|
|
126
|
+
const out: Record<string, unknown> = {}
|
|
127
|
+
while (!this.eat('}')) {
|
|
128
|
+
const key = this.key()
|
|
129
|
+
this.expect(':')
|
|
130
|
+
out[key] = this.value()
|
|
131
|
+
if (!this.eat(',')) {
|
|
132
|
+
this.expect('}')
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return out
|
|
137
|
+
}
|
|
138
|
+
if (c === "'" || c === '"' || c === '`') return this.string(c)
|
|
139
|
+
const m = this.match(/true|false|null|-?\d+(?:\.\d+)?/y)
|
|
140
|
+
if (m === null) throw new Error('not a literal')
|
|
141
|
+
return JSON.parse(m)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private key(): string {
|
|
145
|
+
this.skip()
|
|
146
|
+
const c = this.s[this.i]
|
|
147
|
+
if (c === "'" || c === '"') return this.string(c)
|
|
148
|
+
const m = this.match(/[A-Za-z_$][\w$]*/y)
|
|
149
|
+
if (m === null) throw new Error('not a key')
|
|
150
|
+
return m
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private string(q: string): string {
|
|
154
|
+
let out = ''
|
|
155
|
+
for (this.i++; this.i < this.s.length; this.i++) {
|
|
156
|
+
const c = this.s[this.i]!
|
|
157
|
+
if (c === q) {
|
|
158
|
+
this.i++
|
|
159
|
+
return out
|
|
160
|
+
}
|
|
161
|
+
if (c === '\\') out += this.s[++this.i]
|
|
162
|
+
else if (q === '`' && c === '$' && this.s[this.i + 1] === '{') throw new Error('template')
|
|
163
|
+
else out += c
|
|
164
|
+
}
|
|
165
|
+
throw new Error('unterminated')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private skip(): void {
|
|
169
|
+
while (this.match(/\s+|\/\/[^\n]*|\/\*[\s\S]*?\*\//y) !== null);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The sticky `re`'s match at the cursor, consumed; null when none. */
|
|
173
|
+
private match(re: RegExp): string | null {
|
|
174
|
+
re.lastIndex = this.i
|
|
175
|
+
const m = re.exec(this.s)
|
|
176
|
+
if (m === null || m[0] === '') return null
|
|
177
|
+
this.i += m[0].length
|
|
178
|
+
return m[0]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private eat(c: string): boolean {
|
|
182
|
+
this.skip()
|
|
183
|
+
if (this.s[this.i] !== c) return false
|
|
184
|
+
this.i++
|
|
185
|
+
return true
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private expect(c: string): void {
|
|
189
|
+
if (!this.eat(c)) throw new Error(`expected ${c}`)
|
|
190
|
+
}
|
|
191
|
+
}
|
package/src/nx/nx-deps.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// vx-migrate itself wrote (walked the Nx path, 2026-09-20). Extracted from
|
|
7
7
|
// `buildTask` in item 606; the rules are unchanged.
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type { PackageMeta } from '../packages.js'
|
|
10
10
|
|
|
11
11
|
/** The vx task an Nx `project:target:configuration` reaches, or null when the target lacks it. */
|
|
12
12
|
export type TaskNameFor = (project: string, target: string, configuration: string) => string | null
|
|
@@ -16,7 +16,7 @@ export type TaskNameFor = (project: string, target: string, configuration: strin
|
|
|
16
16
|
* target (`processTasksForSingleProject`), and says nothing otherwise:
|
|
17
17
|
* nx-examples' `targetDefaults` give every `typecheck` a `codegen` that
|
|
18
18
|
* one project declares. Passed through, core refused the whole run —
|
|
19
|
-
* "
|
|
19
|
+
* "dependsOn …#codegen but no such task is declared". Dropped here as
|
|
20
20
|
* Nx drops it, for this project and a named one alike.
|
|
21
21
|
*/
|
|
22
22
|
type HasTarget = (project: string, target: string) => boolean
|
|
@@ -142,7 +142,7 @@ function expandTargetGlobs(
|
|
|
142
142
|
|
|
143
143
|
export function mapNxDeps(
|
|
144
144
|
raw: readonly unknown[],
|
|
145
|
-
metaByNode: ReadonlyMap<string,
|
|
145
|
+
metaByNode: ReadonlyMap<string, PackageMeta>,
|
|
146
146
|
ownTarget: (name: string) => boolean,
|
|
147
147
|
taskNameFor: TaskNameFor,
|
|
148
148
|
hasTarget: HasTarget,
|
package/src/nx/nx-map.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// consumers: `bunx @vzn/vx-migrate --from nx` writes it to files, and the
|
|
3
3
|
// `nx()` plugin runs it live. Static by design: targets that Nx plugins
|
|
4
4
|
// infer at runtime are whatever the graph says. Named inputs expand from
|
|
5
|
-
// nx.json when readable; the graph's dependency edges are ignored (vx
|
|
6
|
-
//
|
|
7
|
-
// report line counting edges with no
|
|
5
|
+
// nx.json when readable; the graph's dependency edges are ignored (vx's
|
|
6
|
+
// package edges come from a lockfile plugin, which installs what the
|
|
7
|
+
// manifests name) except for one report line counting edges with no
|
|
8
|
+
// manifest counterpart.
|
|
8
9
|
//
|
|
9
10
|
// Executors: `nx:run-commands`, a plain `command` and `nx:run-script` are
|
|
10
11
|
// shell; `nx:noop` is a group; an executor the workspace translates
|
|
@@ -16,14 +17,10 @@
|
|
|
16
17
|
// default configuration is folded into the base task.
|
|
17
18
|
|
|
18
19
|
import path from 'node:path'
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
type GeneratedProject,
|
|
22
|
-
type GeneratedTask,
|
|
23
|
-
type ProjectMeta,
|
|
24
|
-
UserError,
|
|
25
|
-
} from '@vzn/vx'
|
|
20
|
+
import { type Duration, type GeneratedProject, type GeneratedTask, UserError } from '@vzn/vx'
|
|
21
|
+
import type { PackageMeta } from '../packages.js'
|
|
26
22
|
import { withoutTakenBack } from '../glob-grammar.js'
|
|
23
|
+
import { manifestGraph, type ManifestGraph } from '../manifest-graph.js'
|
|
27
24
|
import { mapRunCommands, shellQuote } from '../nx-command.js'
|
|
28
25
|
import { scriptCommand, yarnPnp } from '../script-command.js'
|
|
29
26
|
import {
|
|
@@ -251,8 +248,8 @@ export interface NxExecutorTarget {
|
|
|
251
248
|
/** What a translated target runs as. The command runs in the project dir. */
|
|
252
249
|
export interface NxExecutorTranslation {
|
|
253
250
|
readonly command: string
|
|
254
|
-
/** `exec.timeout`,
|
|
255
|
-
readonly timeout?:
|
|
251
|
+
/** `exec.timeout`, a duration with its unit (`'10m'`). */
|
|
252
|
+
readonly timeout?: Duration
|
|
256
253
|
/** Defined in the task's environment, as a run-commands `env` is. */
|
|
257
254
|
readonly env?: Readonly<Record<string, string>>
|
|
258
255
|
}
|
|
@@ -298,7 +295,7 @@ function normRel(p: string): string {
|
|
|
298
295
|
|
|
299
296
|
export async function mapNxWorkspace(
|
|
300
297
|
root: string,
|
|
301
|
-
metas: readonly
|
|
298
|
+
metas: readonly PackageMeta[],
|
|
302
299
|
graph: NxGraph,
|
|
303
300
|
opts: MapNxOptions,
|
|
304
301
|
): Promise<NxMapping> {
|
|
@@ -324,11 +321,11 @@ export async function mapNxWorkspace(
|
|
|
324
321
|
syncTasks: new Map(),
|
|
325
322
|
}
|
|
326
323
|
|
|
327
|
-
const metaByRel = new Map<string,
|
|
324
|
+
const metaByRel = new Map<string, PackageMeta>()
|
|
328
325
|
for (const meta of metas) metaByRel.set(normRel(relPosix(root, meta.dir)), meta)
|
|
329
|
-
const metaByNode = new Map<string,
|
|
330
|
-
const nodeByMeta = new Map<
|
|
331
|
-
const nodeNameOf = new Map<
|
|
326
|
+
const metaByNode = new Map<string, PackageMeta>()
|
|
327
|
+
const nodeByMeta = new Map<PackageMeta, NxNode>()
|
|
328
|
+
const nodeNameOf = new Map<PackageMeta, string>()
|
|
332
329
|
for (const [nodeName, node] of Object.entries(nodeMap)) {
|
|
333
330
|
const meta = metaByRel.get(normRel(node?.data?.root ?? ''))
|
|
334
331
|
if (meta) {
|
|
@@ -343,16 +340,16 @@ export async function mapNxWorkspace(
|
|
|
343
340
|
// workspace root, but Nx routinely defines root targets. Synthesize
|
|
344
341
|
// a meta for any unmatched node whose root resolves inside the
|
|
345
342
|
// workspace, so its targets migrate too.
|
|
346
|
-
const allMetas:
|
|
343
|
+
const allMetas: PackageMeta[] = [...metas]
|
|
347
344
|
const taken = new Set(metas.map((m) => m.name))
|
|
348
345
|
for (const [nodeName, node] of Object.entries(nodeMap)) {
|
|
349
346
|
if (metaByNode.has(nodeName)) continue
|
|
350
347
|
const relRoot = normRel(node?.data?.root ?? '')
|
|
351
348
|
if (node?.data?.targets === undefined) continue
|
|
352
349
|
const dir = relRoot === '' || relRoot === '.' ? root : path.join(root, relRoot)
|
|
353
|
-
let pkg:
|
|
350
|
+
let pkg: PackageMeta['packageJson'] = { name: nodeName }
|
|
354
351
|
try {
|
|
355
|
-
pkg = (await Bun.file(path.join(dir, 'package.json')).json()) as
|
|
352
|
+
pkg = (await Bun.file(path.join(dir, 'package.json')).json()) as PackageMeta['packageJson']
|
|
356
353
|
} catch {
|
|
357
354
|
// No manifest at the node root — keep the synthetic one.
|
|
358
355
|
}
|
|
@@ -362,7 +359,7 @@ export async function mapNxWorkspace(
|
|
|
362
359
|
const name = [pkg.name, nodeName].find((n) => n && !taken.has(n))
|
|
363
360
|
if (name === undefined) continue
|
|
364
361
|
taken.add(name)
|
|
365
|
-
const synthetic:
|
|
362
|
+
const synthetic: PackageMeta = {
|
|
366
363
|
name,
|
|
367
364
|
dir,
|
|
368
365
|
packageJson: pkg,
|
|
@@ -388,7 +385,7 @@ export async function mapNxWorkspace(
|
|
|
388
385
|
|
|
389
386
|
// Once per project, not per task and variant: `path.relative` was a
|
|
390
387
|
// fifth of the mapping at 1,000 projects (item 608).
|
|
391
|
-
const relOf = new Map<
|
|
388
|
+
const relOf = new Map<PackageMeta, string>()
|
|
392
389
|
for (const meta of allMetas) {
|
|
393
390
|
if (nodeByMeta.get(meta)?.data?.targets) relOf.set(meta, normRel(relPosix(root, meta.dir)))
|
|
394
391
|
}
|
|
@@ -399,7 +396,7 @@ export async function mapNxWorkspace(
|
|
|
399
396
|
? null
|
|
400
397
|
: await listDotenv(root, [...relOf.values()])
|
|
401
398
|
|
|
402
|
-
const mapped: Array<{ meta:
|
|
399
|
+
const mapped: Array<{ meta: PackageMeta; tasks: GeneratedTask[] }> = []
|
|
403
400
|
let releasePublish = 0
|
|
404
401
|
// A configuration variant's task (`build:production`) and what it runs.
|
|
405
402
|
const configured = new WeakMap<GeneratedTask, string>()
|
|
@@ -521,11 +518,11 @@ export async function mapNxWorkspace(
|
|
|
521
518
|
nxClosureMemo.set(from, (r = seen))
|
|
522
519
|
return r
|
|
523
520
|
}
|
|
524
|
-
// What vx's `^` already reaches: a manifest path
|
|
525
|
-
// another project, orders `^x` and folds its
|
|
526
|
-
// on the first `^` edge.
|
|
527
|
-
let pkgGraph:
|
|
528
|
-
const packageGraph = () => (pkgGraph ??=
|
|
521
|
+
// What vx's `^` already reaches: a manifest path (the lockfile plugin's
|
|
522
|
+
// edges), direct or through another project, orders `^x` and folds its
|
|
523
|
+
// key (item 931). Built once, on the first `^` edge.
|
|
524
|
+
let pkgGraph: ManifestGraph | undefined
|
|
525
|
+
const packageGraph = () => (pkgGraph ??= manifestGraph(metas))
|
|
529
526
|
let reach: ((name: string) => ReadonlySet<string>) | undefined
|
|
530
527
|
const reached = (name: string): ReadonlySet<string> => {
|
|
531
528
|
if (reach === undefined) {
|
|
@@ -776,7 +773,7 @@ function allTargetNames(nodeMap: Readonly<Record<string, NxNode>>): string[] {
|
|
|
776
773
|
}
|
|
777
774
|
|
|
778
775
|
function buildTask(
|
|
779
|
-
meta:
|
|
776
|
+
meta: PackageMeta,
|
|
780
777
|
projectRel: string,
|
|
781
778
|
scripts: Record<string, unknown>,
|
|
782
779
|
projectName: string,
|
|
@@ -785,7 +782,7 @@ function buildTask(
|
|
|
785
782
|
variant: Variant,
|
|
786
783
|
nodeName: string,
|
|
787
784
|
upstream: NxUpstream,
|
|
788
|
-
metaByNode: ReadonlyMap<string,
|
|
785
|
+
metaByNode: ReadonlyMap<string, PackageMeta>,
|
|
789
786
|
nodeMap: Readonly<Record<string, NxNode>>,
|
|
790
787
|
taskNameFor: TaskNameFor,
|
|
791
788
|
opts: MapOpts,
|
|
@@ -874,9 +871,8 @@ function buildTask(
|
|
|
874
871
|
const readyWhen = mapped?.readyWhen
|
|
875
872
|
const persistent = readyWhen !== undefined || persistentTarget(target)
|
|
876
873
|
// vx cleans outputs before a run and Nx never does. With git's file list
|
|
877
|
-
// known, a wildcard-first output (`**/*.d.ts`) stays cached:
|
|
878
|
-
// committed file
|
|
879
|
-
// (`spareTrackedOutputs`), and what the clean removes is what the task
|
|
874
|
+
// known, a wildcard-first output (`**/*.d.ts`) stays cached: core never
|
|
875
|
+
// cleans a committed file, so what the clean removes is what the task
|
|
880
876
|
// writes (owner, 2026-10-03). Without it, the sources cannot be told apart.
|
|
881
877
|
const wild =
|
|
882
878
|
opts.tracked === undefined
|
|
@@ -1023,7 +1019,7 @@ interface MappedCommand {
|
|
|
1023
1019
|
/** The `.env` files the command loads, relative to the project dir: key inputs. */
|
|
1024
1020
|
readonly envInputs: readonly string[]
|
|
1025
1021
|
/** A translated executor's `exec.timeout`. */
|
|
1026
|
-
readonly timeout?:
|
|
1022
|
+
readonly timeout?: Duration
|
|
1027
1023
|
/** The Nx targets a leading `nx <target> <project>` ran: dependency edges (nx-command.ts). */
|
|
1028
1024
|
readonly nxCalls?: readonly { readonly project: string; readonly target: string }[]
|
|
1029
1025
|
}
|
|
@@ -1169,7 +1165,7 @@ function mapCommand(
|
|
|
1169
1165
|
function translation(
|
|
1170
1166
|
t: unknown,
|
|
1171
1167
|
at: string,
|
|
1172
|
-
): { command: string; env: Record<string, unknown>; timeout:
|
|
1168
|
+
): { command: string; env: Record<string, unknown>; timeout: Duration | undefined } {
|
|
1173
1169
|
if (!isRecord(t)) throw new UserError(`${at}: must return { command } or undefined`)
|
|
1174
1170
|
for (const key of Object.keys(t))
|
|
1175
1171
|
if (key !== 'command' && key !== 'timeout' && key !== 'env')
|
|
@@ -1179,13 +1175,13 @@ function translation(
|
|
|
1179
1175
|
const timeout = t.timeout
|
|
1180
1176
|
if (
|
|
1181
1177
|
timeout !== undefined &&
|
|
1182
|
-
!(typeof timeout === '
|
|
1178
|
+
!(typeof timeout === 'string' && /^[1-9]\d*(ms|s|m|h|d)$/.test(timeout))
|
|
1183
1179
|
)
|
|
1184
|
-
throw new UserError(`${at}: timeout must be a
|
|
1180
|
+
throw new UserError(`${at}: timeout must be a duration with its unit, like '30s' or '10m'`)
|
|
1185
1181
|
const env = t.env ?? {}
|
|
1186
1182
|
if (!isRecord(env) || Object.values(env).some((v) => typeof v !== 'string'))
|
|
1187
1183
|
throw new UserError(`${at}: env must map names to strings`)
|
|
1188
|
-
return { command: t.command, env, timeout }
|
|
1184
|
+
return { command: t.command, env, timeout: timeout as Duration | undefined }
|
|
1189
1185
|
}
|
|
1190
1186
|
|
|
1191
1187
|
/**
|
|
@@ -1354,7 +1350,7 @@ function passConfiguration(
|
|
|
1354
1350
|
nodeName: string,
|
|
1355
1351
|
configuration: string,
|
|
1356
1352
|
nxEdges: (from: string, target: string) => readonly string[],
|
|
1357
|
-
metaByNode: ReadonlyMap<string,
|
|
1353
|
+
metaByNode: ReadonlyMap<string, PackageMeta>,
|
|
1358
1354
|
emittedIds: ReadonlySet<string>,
|
|
1359
1355
|
taskNameFor: TaskNameFor,
|
|
1360
1356
|
): void {
|
|
@@ -1394,7 +1390,7 @@ function narrowToNxGraph(
|
|
|
1394
1390
|
pkgName: string,
|
|
1395
1391
|
nxClosure: (node: string) => ReadonlySet<string>,
|
|
1396
1392
|
nxEdges: (from: string, target: string) => readonly string[],
|
|
1397
|
-
metaByNode: ReadonlyMap<string,
|
|
1393
|
+
metaByNode: ReadonlyMap<string, PackageMeta>,
|
|
1398
1394
|
emittedIds: ReadonlySet<string>,
|
|
1399
1395
|
directDeps: (pkg: string) => readonly string[],
|
|
1400
1396
|
): void {
|
|
@@ -1436,7 +1432,7 @@ function followNxGraph(
|
|
|
1436
1432
|
nodeName: string,
|
|
1437
1433
|
pkgName: string,
|
|
1438
1434
|
nxEdges: (from: string, target: string) => readonly string[],
|
|
1439
|
-
metaByNode: ReadonlyMap<string,
|
|
1435
|
+
metaByNode: ReadonlyMap<string, PackageMeta>,
|
|
1440
1436
|
emittedIds: ReadonlySet<string>,
|
|
1441
1437
|
reached: (name: string) => ReadonlySet<string>,
|
|
1442
1438
|
): void {
|
package/src/nx/nx-upstream.ts
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
// goes on through it. Nx allows a project cycle and vx's task graph does
|
|
17
17
|
// not: inside a cycle a twin carries its peers' files as workspace globs
|
|
18
18
|
// and edges only out of the cycle.
|
|
19
|
-
import type { GeneratedTask
|
|
19
|
+
import type { GeneratedTask } from '@vzn/vx'
|
|
20
|
+
import type { PackageMeta } from '../packages.js'
|
|
20
21
|
import { emptyNxInputs, expandNxInputs, type NxInputs } from './nx-inputs.js'
|
|
21
22
|
import { nxProjectOutputs } from './nx-outputs.js'
|
|
22
23
|
import { matchNxProjects } from './nx-deps.js'
|
|
@@ -70,7 +71,7 @@ export function planNxUpstream(
|
|
|
70
71
|
nodes: Readonly<Record<string, GraphNode>>,
|
|
71
72
|
dependencies: unknown,
|
|
72
73
|
workspaceNamed: Readonly<Record<string, unknown[]>> | null,
|
|
73
|
-
metaByNode: ReadonlyMap<string,
|
|
74
|
+
metaByNode: ReadonlyMap<string, PackageMeta>,
|
|
74
75
|
): NxUpstream {
|
|
75
76
|
const isProject = (n: string): boolean => typeof nodes[n]?.data?.root === 'string'
|
|
76
77
|
const projectTags = Object.entries(nodes).map(([name, n]) => ({
|