@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/adopt.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// which kind of adoption (native configs, or the runner's config read live),
|
|
3
3
|
// and installing vx with the repo's own package manager (solidjs/solid,
|
|
4
4
|
// 2026-10-04: the configs were written with no vx installed to run them).
|
|
5
|
-
// The repo's own scripts are never edited (
|
|
5
|
+
// The repo's own scripts are never edited (2026-10-06).
|
|
6
6
|
|
|
7
7
|
import { existsSync, readFileSync } from 'node:fs'
|
|
8
8
|
import path from 'node:path'
|
|
@@ -63,6 +63,32 @@ export function packageManagerOf(root: string, userAgent?: string): PackageManag
|
|
|
63
63
|
)
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* pnpm's `-w` where pnpm-workspace.yaml makes `root` a workspace: pnpm
|
|
68
|
+
* refuses it in a single-package repo ("--workspace-root may only be used
|
|
69
|
+
* inside a workspace").
|
|
70
|
+
*/
|
|
71
|
+
function pnpmRoot(root: string): string[] {
|
|
72
|
+
return existsSync(path.join(root, 'pnpm-workspace.yaml')) ? ['-w'] : []
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Yarn 2+: its lockfile carries `__metadata`, and before the first install
|
|
77
|
+
* there is none, so `packageManager` names it (`-W` was refused there).
|
|
78
|
+
*/
|
|
79
|
+
function berry(root: string): boolean {
|
|
80
|
+
const lock = readText(path.join(root, 'yarn.lock'))
|
|
81
|
+
if (lock !== '') return lock.includes('__metadata:')
|
|
82
|
+
try {
|
|
83
|
+
const pm = (
|
|
84
|
+
JSON.parse(readText(path.join(root, 'package.json')) || '{}') as { packageManager?: unknown }
|
|
85
|
+
).packageManager
|
|
86
|
+
return typeof pm === 'string' && /^yarn@([2-9]|\d{2,})/.test(pm)
|
|
87
|
+
} catch {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
66
92
|
/** argv that adds `packages` as root dev dependencies. */
|
|
67
93
|
export function installArgv(
|
|
68
94
|
root: string,
|
|
@@ -71,11 +97,11 @@ export function installArgv(
|
|
|
71
97
|
): string[] {
|
|
72
98
|
switch (pm) {
|
|
73
99
|
case 'pnpm':
|
|
74
|
-
return ['pnpm', 'add', '-D',
|
|
100
|
+
return ['pnpm', 'add', '-D', ...pnpmRoot(root), ...packages]
|
|
75
101
|
case 'yarn':
|
|
76
102
|
// Yarn 1 refuses `add` at a workspace root without `-W`; Berry has no
|
|
77
|
-
// such flag.
|
|
78
|
-
return
|
|
103
|
+
// such flag.
|
|
104
|
+
return berry(root)
|
|
79
105
|
? ['yarn', 'add', '-D', ...packages]
|
|
80
106
|
: ['yarn', 'add', '-D', '-W', ...packages]
|
|
81
107
|
case 'bun':
|
|
@@ -104,7 +130,7 @@ export function ownVersion(): string | null {
|
|
|
104
130
|
* (at `version`, given one). Either alone is not adopted: a reset
|
|
105
131
|
* package.json leaves node_modules behind, and a listing is not an
|
|
106
132
|
* install; on solidjs/solid vx-migrate 0.0.512 skipped @vzn/vx that
|
|
107
|
-
* package.json did not list (
|
|
133
|
+
* package.json did not list (2026-10-06).
|
|
108
134
|
*/
|
|
109
135
|
export function missingPackages(
|
|
110
136
|
root: string,
|
|
@@ -129,11 +155,9 @@ export function removeArgv(
|
|
|
129
155
|
): string[] {
|
|
130
156
|
switch (pm) {
|
|
131
157
|
case 'pnpm':
|
|
132
|
-
return ['pnpm', 'remove',
|
|
158
|
+
return ['pnpm', 'remove', ...pnpmRoot(root), ...packages]
|
|
133
159
|
case 'yarn':
|
|
134
|
-
return
|
|
135
|
-
? ['yarn', 'remove', ...packages]
|
|
136
|
-
: ['yarn', 'remove', '-W', ...packages]
|
|
160
|
+
return berry(root) ? ['yarn', 'remove', ...packages] : ['yarn', 'remove', '-W', ...packages]
|
|
137
161
|
case 'bun':
|
|
138
162
|
return ['bun', 'remove', ...packages]
|
|
139
163
|
case 'npm':
|
|
@@ -156,7 +180,7 @@ export function listedPackages(root: string, packages: readonly string[]): strin
|
|
|
156
180
|
* The `@vzn/vx-*` plugins the root lists from the registry (a `workspace:`
|
|
157
181
|
* or `file:` link is the repo's own; a platform binary comes with core).
|
|
158
182
|
* Every @vzn package releases together: an install moves them with core,
|
|
159
|
-
* as `vx upgrade` does (
|
|
183
|
+
* as `vx upgrade` does (2026-10-16: "always install latest and upgrade all").
|
|
160
184
|
*/
|
|
161
185
|
export function listedPlugins(root: string): string[] {
|
|
162
186
|
const pj = JSON.parse(readText(path.join(root, 'package.json')) || '{}') as Record<
|
|
@@ -179,7 +203,7 @@ export function listedPlugins(root: string): string[] {
|
|
|
179
203
|
* The `@vzn/vx*` packages a workspace file imports, parsed, not matched (a
|
|
180
204
|
* formatter's quotes hid a match). Each is installed with core: a file an
|
|
181
205
|
* earlier init wrote imports @vzn/vx-lockfile that package.json need not
|
|
182
|
-
* list, and the lock could not find it (
|
|
206
|
+
* list, and the lock could not find it (2026-10-16).
|
|
183
207
|
*/
|
|
184
208
|
export function importedPackages(text: string): string[] {
|
|
185
209
|
const out = new Set<string>()
|
|
@@ -279,9 +303,26 @@ async function runManager(
|
|
|
279
303
|
if (live) process.stdout.write('\r\x1b[2K\x1b[1A')
|
|
280
304
|
if (code !== 0) {
|
|
281
305
|
process.stderr.write(said)
|
|
306
|
+
const missing = notServed(said)
|
|
282
307
|
throw new UserError(
|
|
283
|
-
|
|
308
|
+
missing === undefined
|
|
309
|
+
? `${what} failed (${line} exited ${code}${failed}); run it, then vx lock${code > 0 ? ranHint : ''}`
|
|
310
|
+
: `${what} failed: the npm registry does not serve ${missing} yet (404), as a release can take minutes to reach it; run vx init again`,
|
|
284
311
|
)
|
|
285
312
|
}
|
|
286
313
|
return line
|
|
287
314
|
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The package a manager's output says the registry answered 404 for, as
|
|
318
|
+
* `name@version` (core's `vx init` reads its installs the same way): a
|
|
319
|
+
* release's platform tarball stays unserved for minutes after publish.
|
|
320
|
+
*/
|
|
321
|
+
export function notServed(output: string): string | undefined {
|
|
322
|
+
for (const line of output.split('\n')) {
|
|
323
|
+
if (!line.includes('404')) continue
|
|
324
|
+
const m = /https?:\/\/\S+?\/((?:@[^/\s]+\/)?[^/\s]+)\/-\/[^/\s]+?-(\d[^/\s]*?)\.tgz/.exec(line)
|
|
325
|
+
if (m !== null) return `${decodeURIComponent(m[1]!)}@${m[2]}`
|
|
326
|
+
}
|
|
327
|
+
return undefined
|
|
328
|
+
}
|
package/src/adoption-plugin.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// skeleton until item 592; what differs is only how the mapping is made.
|
|
7
7
|
|
|
8
8
|
import { definePlugin, type ProjectHookContext, type VxPlugin } from '@vzn/vx'
|
|
9
|
-
import { type AdoptionMapping, cachedMapping } from './mapping-cache.js'
|
|
9
|
+
import { type AdoptionMapping, cachedMapping, codeId } from './mapping-cache.js'
|
|
10
10
|
import { warnGaps } from './plugin-gaps.js'
|
|
11
11
|
import { headStamp, spareTrackedOutputs, trackedFiles } from './tracked-outputs.js'
|
|
12
12
|
|
|
@@ -76,18 +76,16 @@ export function adoptionPlugin(
|
|
|
76
76
|
async project(config, ctx) {
|
|
77
77
|
if (mappedFor !== ctx.projects) {
|
|
78
78
|
mappedFor = ctx.projects
|
|
79
|
+
// The key's other reads overlap the plugin's own.
|
|
80
|
+
void codeId()
|
|
81
|
+
const stamp = headStamp(ctx.workspaceRoot)
|
|
79
82
|
mapping = mapRun(ctx).then(async (r) =>
|
|
80
83
|
r.spareTracked === true
|
|
81
|
-
? cachedMapping(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
let read: Promise<readonly string[] | null> | undefined
|
|
87
|
-
const tracked = () => (read ??= trackedFiles(ctx.workspaceRoot))
|
|
88
|
-
return spare(ctx, await r.map(tracked), await tracked())
|
|
89
|
-
},
|
|
90
|
-
)
|
|
84
|
+
? cachedMapping(ctx.cacheDir, r.name, [...r.reads, await stamp], async () => {
|
|
85
|
+
let read: Promise<readonly string[] | null> | undefined
|
|
86
|
+
const tracked = () => (read ??= trackedFiles(ctx.workspaceRoot))
|
|
87
|
+
return spare(ctx, await r.map(tracked), await tracked())
|
|
88
|
+
})
|
|
91
89
|
: cachedMapping(ctx.cacheDir, r.name, r.reads, () =>
|
|
92
90
|
r.map(() => trackedFiles(ctx.workspaceRoot)),
|
|
93
91
|
),
|
package/src/bin.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// 2026-10-16).
|
|
11
11
|
|
|
12
12
|
import path from 'node:path'
|
|
13
|
+
import * as core from '@vzn/vx'
|
|
13
14
|
import { isUserError, UserError, VERSION } from '@vzn/vx'
|
|
14
15
|
import { ownVersion } from './adopt.js'
|
|
15
16
|
|
|
@@ -26,8 +27,27 @@ try {
|
|
|
26
27
|
const { migrateCmd } = await import('./migrate.js')
|
|
27
28
|
process.exitCode = await migrateCmd(process.argv.slice(2))
|
|
28
29
|
} catch (err) {
|
|
30
|
+
// Asked for JSON (`vx init --dry --format json` hands a Turbo or Nx repo
|
|
31
|
+
// here), a refusal is core's error document on stdout too
|
|
32
|
+
// (schemas/error.json): stdout was empty.
|
|
33
|
+
const args = process.argv.slice(2)
|
|
34
|
+
if (args.some((a, i) => a === '--format=json' || (a === '--format' && args[i + 1] === 'json'))) {
|
|
35
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
36
|
+
const own = isUserError(err) ? (err as { code?: unknown }).code : 'VX_E_INTERNAL'
|
|
37
|
+
const code = typeof own === 'string' ? own : 'VX_E_REFUSED'
|
|
38
|
+
const docs = code.startsWith('VX_E_')
|
|
39
|
+
? { docs: `https://vznjs.github.io/vx/cli/#${code.toLowerCase()}` }
|
|
40
|
+
: {}
|
|
41
|
+
process.stdout.write(
|
|
42
|
+
`${JSON.stringify({ ok: false, error: { code, message: `vx-migrate: ${message}`, ...docs } })}\n`,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
29
45
|
if (isUserError(err)) {
|
|
30
|
-
|
|
46
|
+
// `ui` by namespace: a core older than it has none, and its refusal
|
|
47
|
+
// still reaches the reader.
|
|
48
|
+
const ui = (core as Partial<typeof core>).ui
|
|
49
|
+
if (ui === undefined) process.stderr.write(`vx-migrate: ${err.message}\n`)
|
|
50
|
+
else ui.say('error', `vx-migrate: ${err.message}`)
|
|
31
51
|
process.exitCode = 1
|
|
32
52
|
} else {
|
|
33
53
|
throw err
|
package/src/glob-grammar.ts
CHANGED
|
@@ -27,6 +27,51 @@ export function minimatchToVx(glob: string, negated: boolean): string | null {
|
|
|
27
27
|
return out.includes('\0') || /[?@+*!]\(/.test(out) ? null : out
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// A group no extglob kind opens and no backslash escapes.
|
|
31
|
+
const BARE_GROUP = /(?<![\\?@+*!])\(([^()]*)\)/g
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* An Nx glob's bare groups in vx's grammar, or null when one has no safe
|
|
35
|
+
* form. Nx's native glob reads `(a|b)` as `{a,b}` and `(a)` as `a`, so
|
|
36
|
+
* `dist/*.(js|map)` is the two extensions there; vx and minimatch read the
|
|
37
|
+
* parentheses as literal text, and the glob matched nothing. Turbo's
|
|
38
|
+
* grammar has no such group, so this is the Nx adapter's alone.
|
|
39
|
+
*/
|
|
40
|
+
export function nxBareGroups(glob: string): string | null {
|
|
41
|
+
if (!glob.includes('(')) return glob
|
|
42
|
+
let unsafe = false
|
|
43
|
+
const out = glob.replace(BARE_GROUP, (_whole, body: string) => {
|
|
44
|
+
const alts = body.split('|')
|
|
45
|
+
if (alts.some((a) => /[{},]/.test(a))) unsafe = true
|
|
46
|
+
return alts.length === 1 ? alts[0]! : `{${alts.join(',')}}`
|
|
47
|
+
})
|
|
48
|
+
return unsafe ? null : out
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A segment (or brace arm) holding `**` and other text: `**.ts`, `a**`. */
|
|
52
|
+
const glued = (seg: string): boolean => seg.includes('**') && /[^*]/.test(seg)
|
|
53
|
+
|
|
54
|
+
/** Does `glob` glue `**` to other text, which core refuses? */
|
|
55
|
+
export const hasGluedGlobstar = (glob: string): boolean =>
|
|
56
|
+
glob
|
|
57
|
+
.replace(/^!/, '')
|
|
58
|
+
.split(/[/{},]/)
|
|
59
|
+
.some(glued)
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A glued `**` as minimatch and Nx's matcher (globset, literal separator)
|
|
63
|
+
* read it: `*`, one level.
|
|
64
|
+
*/
|
|
65
|
+
export function gluedGlobstarAsStar(glob: string): string {
|
|
66
|
+
const neg = glob.startsWith('!') ? '!' : ''
|
|
67
|
+
return (
|
|
68
|
+
neg +
|
|
69
|
+
glob
|
|
70
|
+
.slice(neg.length)
|
|
71
|
+
.replace(/[^/{},]+/g, (seg) => (glued(seg) ? seg.replace(/\*{2,}/g, '*') : seg))
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
30
75
|
/**
|
|
31
76
|
* `list` without a literal that one of its `!` entries takes back, and
|
|
32
77
|
* without a repeat. Turbo, Nx and vx all subtract an exclusion wherever it
|
|
@@ -53,6 +98,43 @@ export function withoutTakenBack<T>(entries: readonly T[]): T[] {
|
|
|
53
98
|
})
|
|
54
99
|
}
|
|
55
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Could a path match both globs? False only when, before either reaches a
|
|
103
|
+
* wildcard, a segment of one differs from the other's; a literal also
|
|
104
|
+
* matches its directory's files, as a `!` entry takes them back.
|
|
105
|
+
*/
|
|
106
|
+
function mayOverlap(a: string, b: string): boolean {
|
|
107
|
+
const wild = /[*?{}[\]\\]/
|
|
108
|
+
const sa = a.replace(/^\.\//, '').split('/')
|
|
109
|
+
const sb = b.replace(/^\.\//, '').split('/')
|
|
110
|
+
for (let i = 0; i < Math.min(sa.length, sb.length); i++) {
|
|
111
|
+
if (wild.test(sa[i]!) || wild.test(sb[i]!)) return true
|
|
112
|
+
if (sa[i] !== sb[i]) return false
|
|
113
|
+
}
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Two lists core reads as one, each with its own `!` entries: Turbo
|
|
119
|
+
* subtracts a task's exclusions from the task's own inputs only, and a
|
|
120
|
+
* `globalDependencies` exclusion from the global hash only. One list in
|
|
121
|
+
* vx subtracts every `!` from every entry, so each list loses an exclusion
|
|
122
|
+
* that may reach a positive entry of the other: it keys more files (a
|
|
123
|
+
* missed hit), where kept it keyed fewer (a stale one).
|
|
124
|
+
*/
|
|
125
|
+
export function apartTakeBacks<T>(a: readonly T[], b: readonly T[]): [T[], T[]] {
|
|
126
|
+
const positives = (l: readonly T[]) =>
|
|
127
|
+
l.flatMap((e) => (typeof e === 'string' && !e.startsWith('!') ? [e] : []))
|
|
128
|
+
const keep = (l: readonly T[], other: readonly string[]) =>
|
|
129
|
+
l.filter(
|
|
130
|
+
(e) =>
|
|
131
|
+
typeof e !== 'string' ||
|
|
132
|
+
!e.startsWith('!') ||
|
|
133
|
+
!other.some((p) => mayOverlap(e.slice(1), p)),
|
|
134
|
+
)
|
|
135
|
+
return [keep(a, positives(b)), keep(b, positives(a))]
|
|
136
|
+
}
|
|
137
|
+
|
|
56
138
|
/**
|
|
57
139
|
* A path from the file system as a task glob that names only itself: a dir
|
|
58
140
|
* named `my{app}` read as a brace matched `myapp` instead, and `app/[id]` a
|
package/src/index.ts
CHANGED
|
@@ -2,14 +2,40 @@
|
|
|
2
2
|
// runs unchanged) and the two remote caches speaking Turbo's and Nx's
|
|
3
3
|
// wire. The CLI (`bunx @vzn/vx-migrate`) is `migrate.ts`; mappers and
|
|
4
4
|
// cache clients are internal (1.0 freezes what this file exports).
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
//
|
|
6
|
+
// Each factory loads its module on first call: a workspace evaluates this
|
|
7
|
+
// file on every run, and a Turbo repo paid ~4 ms per run transpiling nx()
|
|
8
|
+
// and both cache clients it never calls (solid, 2026-10-22).
|
|
9
|
+
import type { VxPlugin } from '@vzn/vx'
|
|
10
|
+
import type * as Turbo from './turbo/index.js'
|
|
11
|
+
import type { TurboPluginOptions } from './turbo/index.js'
|
|
12
|
+
import type { NxPluginOptions } from './nx/index.js'
|
|
13
|
+
import type { TurboCacheOptions } from './turbo-cache/index.js'
|
|
14
|
+
import type { NxCacheOptions } from './nx-cache/index.js'
|
|
15
|
+
import type * as Nx from './nx/index.js'
|
|
16
|
+
import type * as TurboCache from './turbo-cache/index.js'
|
|
17
|
+
import type * as NxCache from './nx-cache/index.js'
|
|
18
|
+
|
|
19
|
+
export type { TurboPluginOptions } from './turbo/index.js'
|
|
20
|
+
export type {
|
|
21
|
+
NxExecutors,
|
|
22
|
+
NxExecutorTarget,
|
|
23
|
+
NxExecutorTranslation,
|
|
24
|
+
NxExecutorTranslator,
|
|
25
|
+
NxPluginOptions,
|
|
13
26
|
} from './nx/index.js'
|
|
14
|
-
export {
|
|
15
|
-
export {
|
|
27
|
+
export type { TurboCacheOptions } from './turbo-cache/index.js'
|
|
28
|
+
export type { NxCacheOptions } from './nx-cache/index.js'
|
|
29
|
+
|
|
30
|
+
export function turbo(options: TurboPluginOptions = {}): VxPlugin {
|
|
31
|
+
return (require('./turbo/index.js') as typeof Turbo).turbo(options)
|
|
32
|
+
}
|
|
33
|
+
export function nx(options: NxPluginOptions = {}): VxPlugin {
|
|
34
|
+
return (require('./nx/index.js') as typeof Nx).nx(options)
|
|
35
|
+
}
|
|
36
|
+
export function turboCache(options: TurboCacheOptions = {}): VxPlugin {
|
|
37
|
+
return (require('./turbo-cache/index.js') as typeof TurboCache).turboCache(options)
|
|
38
|
+
}
|
|
39
|
+
export function nxCache(options: NxCacheOptions = {}): VxPlugin {
|
|
40
|
+
return (require('./nx-cache/index.js') as typeof NxCache).nxCache(options)
|
|
41
|
+
}
|
package/src/mapping-cache.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// package's sources and the core helpers the mappers call: a new
|
|
9
9
|
// `@vzn/vx-migrate` or `@vzn/vx` maps afresh.
|
|
10
10
|
|
|
11
|
+
import { statSync } from 'node:fs'
|
|
11
12
|
import { mkdir, rename } from 'node:fs/promises'
|
|
12
13
|
import path from 'node:path'
|
|
13
14
|
import { foldScriptHooks, isLiteralPattern, outputsOverlap, VERSION } from '@vzn/vx'
|
|
@@ -30,8 +31,8 @@ export interface AdoptionMapping {
|
|
|
30
31
|
|
|
31
32
|
let code: Promise<string> | undefined
|
|
32
33
|
|
|
33
|
-
/** This package's sources and the core helpers the mappers call,
|
|
34
|
-
function codeId(): Promise<string> {
|
|
34
|
+
/** This package's sources (by size and mtime) and the core helpers the mappers call, once per process. */
|
|
35
|
+
export function codeId(): Promise<string> {
|
|
35
36
|
code ??= (async () => {
|
|
36
37
|
const parts: string[] = [
|
|
37
38
|
VERSION,
|
|
@@ -40,7 +41,13 @@ function codeId(): Promise<string> {
|
|
|
40
41
|
const files: string[] = []
|
|
41
42
|
for await (const f of new Bun.Glob('**/*.{ts,cjs}').scan(import.meta.dir)) files.push(f)
|
|
42
43
|
files.sort()
|
|
43
|
-
|
|
44
|
+
// Size and mtime, not bytes: reading the 650 kB of sources cost ~10 ms
|
|
45
|
+
// on every run, more than the mapping hit it guards saves on a small
|
|
46
|
+
// repo (solid, 2026-10-22). An edit moves both.
|
|
47
|
+
for (const f of files) {
|
|
48
|
+
const s = statSync(path.join(import.meta.dir, f))
|
|
49
|
+
parts.push(f, `${s.size}:${s.mtimeMs}`)
|
|
50
|
+
}
|
|
44
51
|
return hex(parts.join('\0'))
|
|
45
52
|
})()
|
|
46
53
|
return code
|
|
@@ -71,11 +78,12 @@ export async function cachedMapping(
|
|
|
71
78
|
parts: readonly string[],
|
|
72
79
|
map: () => Promise<AdoptionMapping>,
|
|
73
80
|
): Promise<AdoptionMapping> {
|
|
74
|
-
const key = hex([await codeId(), ...parts].join('\0'))
|
|
75
81
|
const file = path.join(cacheDir, `vx-migrate-${name}-mapping.json`)
|
|
76
|
-
const
|
|
82
|
+
const reading = Bun.file(file)
|
|
77
83
|
.json()
|
|
78
|
-
.catch(() => null)
|
|
84
|
+
.catch(() => null) as Promise<Held | null>
|
|
85
|
+
const key = hex([await codeId(), ...parts].join('\0'))
|
|
86
|
+
const held = await reading
|
|
79
87
|
if (held?.key === key) {
|
|
80
88
|
return {
|
|
81
89
|
byName: new Map(held.byName),
|
package/src/migrate-nx.ts
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
import { stat } from 'node:fs/promises'
|
|
5
5
|
import path from 'node:path'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type GeneratedProject,
|
|
8
|
+
type MigrationFormat,
|
|
9
|
+
type MigrationPlan,
|
|
10
|
+
quoteTsLiteral,
|
|
11
|
+
} from '@vzn/vx'
|
|
7
12
|
import type { PackageMeta } from './packages.js'
|
|
8
13
|
import { mapNxWorkspace, parseNxGraph, readNxJson } from './nx/nx-map.js'
|
|
9
14
|
import { spareTrackedOutputs, trackedFiles, trackedKinds } from './tracked-outputs.js'
|
|
@@ -172,8 +177,6 @@ export interface NxWorkspaceField {
|
|
|
172
177
|
readonly note: string
|
|
173
178
|
}
|
|
174
179
|
|
|
175
|
-
const quoted = (s: string) => `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
176
|
-
|
|
177
180
|
/**
|
|
178
181
|
* nx.json's run-wide settings `nx()` applies live: each a field of the
|
|
179
182
|
* written `.vx/workspace.ts`, or a migrated repo that ran one task at a time
|
|
@@ -202,7 +205,7 @@ export function nxWorkspaceFields(json: Record<string, unknown> | undefined): Nx
|
|
|
202
205
|
const branch = base.trim().replace(/^origin\//, '')
|
|
203
206
|
out.push({
|
|
204
207
|
field: 'baseBranch',
|
|
205
|
-
source:
|
|
208
|
+
source: quoteTsLiteral(branch),
|
|
206
209
|
note: `nx.json \`defaultBase\` ${JSON.stringify(base.trim())}: add \`baseBranch: ${JSON.stringify(branch)}\` to .vx/workspace.ts`,
|
|
207
210
|
})
|
|
208
211
|
}
|
package/src/migrate-scripts.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
} from '@vzn/vx'
|
|
40
40
|
import type { PackageMeta } from './packages.js'
|
|
41
41
|
import { relPosix } from './paths.js'
|
|
42
|
+
import { type Owner, ownerOf, runsScriptHooks } from './script-command.js'
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* Task names that conventionally never exit. Every mapper guesses the same
|
|
@@ -128,19 +129,24 @@ const LIFECYCLE = /^(pre|post)(install|publish|pack|version)$|^(prepare|prepubli
|
|
|
128
129
|
/**
|
|
129
130
|
* The script a command delegates to when it is NOTHING but a package-manager
|
|
130
131
|
* invocation of another script: `npm run x`, `pnpm x`, `yarn run x`, `bun run
|
|
131
|
-
* x`, plus npm's bare `npm test` / `npm start
|
|
132
|
-
*
|
|
132
|
+
* x`, plus npm's bare `npm test` / `npm start` / `npm stop`. npm's aliases
|
|
133
|
+
* (`run-script`, `rum`, `urn`; `t`, `tst`) and pnpm's (`run-script`; `t`,
|
|
134
|
+
* `tst`) are the same commands: read verbatim, `npm run-script build` built
|
|
135
|
+
* again beside the `build` task. Flags, arguments or a chain make it a real
|
|
136
|
+
* command again, which is left verbatim.
|
|
133
137
|
*/
|
|
134
138
|
export function delegatedScript(command: string): string | null {
|
|
135
139
|
const m =
|
|
136
|
-
/^(?:(?:npm run|(pnpm|pn|yarn|bun)(?: (run))?) ([^\s&|;<>()$`'"\\]+)|npm (test|start))$/.exec(
|
|
140
|
+
/^(?:(?:npm (?:run|run-script|rum|urn)|(pnpm|pn|yarn|bun)(?: (run|run-script))?) ([^\s&|;<>()$`'"\\]+)|npm (test|t|tst|start|stop))$/.exec(
|
|
137
141
|
command.trim(),
|
|
138
142
|
)
|
|
139
143
|
if (m === null) return null
|
|
140
|
-
if (m[4] !== undefined) return m[4]
|
|
144
|
+
if (m[4] !== undefined) return m[4] === 't' || m[4] === 'tst' ? 'test' : m[4]
|
|
141
145
|
const [, alias, run, name] = m
|
|
142
146
|
// `pn` is pnpm's own short name (pnpm 11; pnpm/pnpm's scripts run it).
|
|
143
147
|
const manager = alias === 'pn' ? 'pnpm' : alias
|
|
148
|
+
if (run === 'run-script' && manager !== 'pnpm') return null
|
|
149
|
+
if (manager === 'pnpm' && run === undefined && (name === 't' || name === 'tst')) return 'test'
|
|
144
150
|
// Bare, the manager's own command wins over a script of that name:
|
|
145
151
|
// `bun test` is Bun's test runner and `bun build` its bundler, never the
|
|
146
152
|
// `test` / `build` script, and a group over the script ran the wrong
|
|
@@ -177,104 +183,6 @@ const OWN_COMMANDS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
|
177
183
|
),
|
|
178
184
|
}
|
|
179
185
|
|
|
180
|
-
/**
|
|
181
|
-
* The package manager that owns `dir`, when it runs `pre` / `post` hooks
|
|
182
|
-
* around a script; null when it runs none. The manager is the nearest `packageManager` field or
|
|
183
|
-
* lockfile, and its settings are read beside it, as each reads them for a
|
|
184
|
-
* member too. Probed with a `prebuild` / `build` / `postbuild` trio: Yarn
|
|
185
|
-
* 2+ runs none (D-31); npm runs none under `ignore-scripts=true` in
|
|
186
|
-
* `.npmrc`; pnpm runs none under `enable-pre-post-scripts=false` there or
|
|
187
|
-
* `enablePrePostScripts: false` in `pnpm-workspace.yaml` (D-33); Bun and
|
|
188
|
-
* Yarn 1 run them whatever those say. No manager found is npm's default.
|
|
189
|
-
*/
|
|
190
|
-
function runsScriptHooks(dir: string, memo: Map<string, Owner>): string | null {
|
|
191
|
-
const { manager, at } = ownerOf(dir, memo)
|
|
192
|
-
const read = (f: string): string | undefined => {
|
|
193
|
-
try {
|
|
194
|
-
return readFileSync(path.join(at, f), 'utf8')
|
|
195
|
-
} catch {
|
|
196
|
-
return undefined
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
const npmrc = read('.npmrc') ?? ''
|
|
200
|
-
const set = (key: string, value: string): boolean =>
|
|
201
|
-
new RegExp(`^\\s*${key}\\s*=\\s*${value}\\s*$`, 'm').test(npmrc)
|
|
202
|
-
const runs =
|
|
203
|
-
manager === 'berry'
|
|
204
|
-
? false
|
|
205
|
-
: manager === 'npm'
|
|
206
|
-
? !set('ignore-scripts', 'true')
|
|
207
|
-
: manager === 'pnpm'
|
|
208
|
-
? !set('enable-pre-post-scripts', 'false') && !pnpmSkipsHooks(read('pnpm-workspace.yaml'))
|
|
209
|
-
: true
|
|
210
|
-
return runs ? manager : null
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* `enablePrePostScripts: false` in `pnpm-workspace.yaml`, read as pnpm reads
|
|
215
|
-
* it, as YAML: a line regex missed `false # hooks off` and a flow mapping,
|
|
216
|
-
* which pnpm 10 honours, and folded hooks pnpm does not run.
|
|
217
|
-
*/
|
|
218
|
-
function pnpmSkipsHooks(yaml: string | undefined): boolean {
|
|
219
|
-
if (yaml === undefined) return false
|
|
220
|
-
try {
|
|
221
|
-
const parsed = Bun.YAML.parse(yaml) as { enablePrePostScripts?: unknown } | null
|
|
222
|
-
return parsed?.enablePrePostScripts === false
|
|
223
|
-
} catch {
|
|
224
|
-
return false
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** The package manager that owns a directory, and the directory that says so. */
|
|
229
|
-
interface Owner {
|
|
230
|
-
manager: string
|
|
231
|
-
at: string
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* The nearest `packageManager` field or lockfile from `dir` up: `berry` for
|
|
236
|
-
* Yarn 2+, `yarn` for Yarn 1, else the manager's name; none found is npm.
|
|
237
|
-
*/
|
|
238
|
-
function ownerOf(dir: string, memo: Map<string, Owner>): Owner {
|
|
239
|
-
const known = memo.get(dir)
|
|
240
|
-
if (known !== undefined) return known
|
|
241
|
-
const read = (f: string): string | undefined => {
|
|
242
|
-
try {
|
|
243
|
-
return readFileSync(path.join(dir, f), 'utf8')
|
|
244
|
-
} catch {
|
|
245
|
-
return undefined
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
let manager: string | undefined
|
|
249
|
-
try {
|
|
250
|
-
// A byte-order mark is stripped, as npm, pnpm and discovery strip it:
|
|
251
|
-
// JSON.parse threw on one and the field went unread.
|
|
252
|
-
const manifest = (read('package.json') ?? '').replace(/^\uFEFF/, '')
|
|
253
|
-
const pm = (JSON.parse(manifest) as { packageManager?: unknown }).packageManager
|
|
254
|
-
// A manager vx knows nothing of (zod's `nub@0.8.3`) says nothing about
|
|
255
|
-
// hooks: the lockfile beside it does, and "nub ran `postbuild`" was a
|
|
256
|
-
// claim nothing had checked (D-96).
|
|
257
|
-
if (typeof pm === 'string' && /^(npm|pnpm|yarn|bun)@/.test(pm))
|
|
258
|
-
manager = /^yarn@([2-9]|\d{2,})/.test(pm) ? 'berry' : pm.split('@')[0]
|
|
259
|
-
} catch {}
|
|
260
|
-
if (manager === undefined) {
|
|
261
|
-
const yarnLock = read('yarn.lock')
|
|
262
|
-
if (yarnLock !== undefined) manager = yarnLock.includes('\n__metadata:') ? 'berry' : 'yarn'
|
|
263
|
-
else if (existsSync(path.join(dir, 'pnpm-lock.yaml'))) manager = 'pnpm'
|
|
264
|
-
else if (existsSync(path.join(dir, 'package-lock.json'))) manager = 'npm'
|
|
265
|
-
else if (['bun.lock', 'bun.lockb'].some((f) => existsSync(path.join(dir, f)))) manager = 'bun'
|
|
266
|
-
}
|
|
267
|
-
const up = path.dirname(dir)
|
|
268
|
-
const owner =
|
|
269
|
-
manager !== undefined
|
|
270
|
-
? { manager, at: dir }
|
|
271
|
-
: up === dir
|
|
272
|
-
? { manager: 'npm', at: dir }
|
|
273
|
-
: ownerOf(up, memo)
|
|
274
|
-
memo.set(dir, owner)
|
|
275
|
-
return owner
|
|
276
|
-
}
|
|
277
|
-
|
|
278
186
|
/**
|
|
279
187
|
* Yarn 2+ runs a script in its own shell, where `run <script>` is `yarn
|
|
280
188
|
* run <script>`; vx's shell has no `run`, and 23 of berry's scripts
|
package/src/migrate-turbo.ts
CHANGED
|
@@ -12,12 +12,8 @@ import {
|
|
|
12
12
|
quoteTsLiteral as quote,
|
|
13
13
|
} from '@vzn/vx'
|
|
14
14
|
import type { PackageMeta } from './packages.js'
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
rootTaskProject,
|
|
18
|
-
type TurboGlobal,
|
|
19
|
-
type TurboMappedProject,
|
|
20
|
-
} from './turbo/turbo-map.js'
|
|
15
|
+
import { mapTurboWorkspace, type TurboGlobal, type TurboMappedProject } from './turbo/turbo-map.js'
|
|
16
|
+
import { rootTaskProject } from './turbo/turbo-reads.js'
|
|
21
17
|
import { relPosix } from './paths.js'
|
|
22
18
|
import { gitIgnored, spareTrackedOutputs, trackedFiles, trackedKinds } from './tracked-outputs.js'
|
|
23
19
|
import { DOTENV_GLOBS_HEAD, DOTENV_PROBE, DOTENV_PROBE_TOP } from './dotenv-probe.js'
|