@vzn/vx-migrate 0.0.635 → 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-cache/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// The wire is Nx's; the bytes are vx's own artifacts under vx's own keys,
|
|
6
6
|
// so the server is storage — an Nx binary cannot read them.
|
|
7
7
|
//
|
|
8
|
-
// Nothing is on by default: declare `nxCache()` in
|
|
8
|
+
// Nothing is on by default: declare `nxCache()` in `.vx/workspace.ts` (the
|
|
9
9
|
// local store is the floor beneath it) and give it a server (options, or Nx's own
|
|
10
10
|
// environment variables so a self-hosted setup carries over). Without one
|
|
11
11
|
// the plugin DECLINES and the run stays local.
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
// Imports core only through the public `@vzn/vx` specifier.
|
|
14
14
|
import {
|
|
15
15
|
definePlugin,
|
|
16
|
-
LayeredCache,
|
|
17
|
-
type CacheLayer,
|
|
18
16
|
type RemoteCacheLayer,
|
|
19
17
|
type VxPlugin,
|
|
20
18
|
refuseUnknownOptions,
|
|
@@ -95,7 +93,7 @@ export function resolveNxCacheConfig(
|
|
|
95
93
|
* `get` of the same hash, which no caller makes. Bodies stream: `get` returns the `fetch` Response and `put`
|
|
96
94
|
* sends the Blob core hands it. `put` treats `409` as success:
|
|
97
95
|
* the record is immutable and content-addressed, so "already there" is the
|
|
98
|
-
* outcome wanted. An auth failure (401/403) throws ONCE —
|
|
96
|
+
* outcome wanted. An auth failure (401/403) throws ONCE — core
|
|
99
97
|
* reports it — and then turns the layer off for the rest of the process
|
|
100
98
|
* (a write's 403 turns off writes alone: the read-only token);
|
|
101
99
|
* the requests already in flight when it lands degrade in silence rather
|
|
@@ -220,7 +218,7 @@ const NX_CACHE_KEYS: PluginOptionKinds<NxCacheOptions> = {
|
|
|
220
218
|
}
|
|
221
219
|
|
|
222
220
|
/**
|
|
223
|
-
* Declare in
|
|
221
|
+
* Declare in `.vx/workspace.ts`; the local store stays the floor beneath it:
|
|
224
222
|
*
|
|
225
223
|
* ```ts
|
|
226
224
|
* plugins: [nxCache({ server: 'https://cache.example.com', accessToken: process.env.CACHE_TOKEN })]
|
|
@@ -231,13 +229,9 @@ const NX_CACHE_KEYS: PluginOptionKinds<NxCacheOptions> = {
|
|
|
231
229
|
export function nxCache(options: NxCacheOptions = {}): VxPlugin {
|
|
232
230
|
refuseUnknownOptions('nxCache()', options, NX_CACHE_KEYS)
|
|
233
231
|
return definePlugin(import.meta, {
|
|
234
|
-
cache(
|
|
232
|
+
cache(): RemoteCacheLayer | undefined {
|
|
235
233
|
const config = resolveNxCacheConfig(options)
|
|
236
|
-
|
|
237
|
-
return new LayeredCache(ctx.localCache, new NxRemoteCache(config), {
|
|
238
|
-
policy: ctx.policy,
|
|
239
|
-
onRemoteError: (err) => ctx.warn(`vx/nx-cache: ${err.message}`),
|
|
240
|
-
})
|
|
234
|
+
return config === undefined ? undefined : new NxRemoteCache(config)
|
|
241
235
|
},
|
|
242
236
|
})
|
|
243
237
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// What packages.ts needs of the file system and of globs, as core has
|
|
2
|
+
// them: core's copies are not on the façade, and these left core with the
|
|
3
|
+
// package-manager discovery (M, 2026-10-14).
|
|
4
|
+
|
|
5
|
+
import { readlinkSync, realpathSync } from 'node:fs'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { UserError } from '@vzn/vx'
|
|
8
|
+
|
|
9
|
+
/** An `EACCES`, `EPERM` or `EROFS`: not this user's to read or write. */
|
|
10
|
+
export function isPermissionError(err: unknown): err is NodeJS.ErrnoException {
|
|
11
|
+
if (!(err instanceof Error)) return false
|
|
12
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
13
|
+
return code === 'EACCES' || code === 'EPERM' || code === 'EROFS'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** `EMFILE` or `ENFILE`: out of file descriptors, never "no file here". */
|
|
17
|
+
export function isOutOfFds(err: unknown): err is NodeJS.ErrnoException {
|
|
18
|
+
if (!(err instanceof Error)) return false
|
|
19
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
20
|
+
return code === 'EMFILE' || code === 'ENFILE'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `Bun.Glob`'s own alphabet, the class included: member globs follow npm/pnpm. */
|
|
24
|
+
export const BUN_GLOB_WILDCARDS = /[*?[\]{}]/
|
|
25
|
+
|
|
26
|
+
/** An extglob group (`!(…)`, `@(…)`, …), which `Bun.Glob` does not read. */
|
|
27
|
+
export const EXTGLOB = /[!@+*?]\(/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The spellings npm and pnpm accept that a matcher fed the raw string
|
|
31
|
+
* turns into nothing: a leading `./`, an inner `/./`, a doubled `//`, and a
|
|
32
|
+
* trailing `/` on a pattern. Applied after an optional `!`.
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeBunGlob(glob: string): string {
|
|
35
|
+
const neg = glob.startsWith('!')
|
|
36
|
+
let g = neg ? glob.slice(1) : glob
|
|
37
|
+
g = g.replace(/\/{2,}/g, '/').replace(/(^|\/)(\.\/)+/g, '$1')
|
|
38
|
+
if (g === '.') g = ''
|
|
39
|
+
if (BUN_GLOB_WILDCARDS.test(g) && g.endsWith('/')) g = `${g.replace(/\/+$/, '')}/**`
|
|
40
|
+
return neg ? `!${g}` : g
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `pattern` with its first brace group expanded, recursively, while that
|
|
45
|
+
* group holds a `/`: `Bun.Glob`'s scan finds nothing for one.
|
|
46
|
+
*/
|
|
47
|
+
export function slashBraceExpansions(pattern: string): string[] {
|
|
48
|
+
if (pattern.includes('\\')) return [pattern]
|
|
49
|
+
for (let open = pattern.indexOf('{'); open !== -1; open = pattern.indexOf('{', open + 1)) {
|
|
50
|
+
let depth = 0
|
|
51
|
+
const cuts: number[] = []
|
|
52
|
+
let close = -1
|
|
53
|
+
for (let i = open; i < pattern.length && close === -1; i++) {
|
|
54
|
+
const c = pattern[i]
|
|
55
|
+
if (c === '{') depth++
|
|
56
|
+
else if (c === '}' && --depth === 0) close = i
|
|
57
|
+
else if (c === ',' && depth === 1) cuts.push(i)
|
|
58
|
+
}
|
|
59
|
+
if (close === -1) return [pattern]
|
|
60
|
+
if (cuts.length === 0 || !pattern.slice(open + 1, close).includes('/')) continue
|
|
61
|
+
const head = pattern.slice(0, open)
|
|
62
|
+
const tail = pattern.slice(close + 1)
|
|
63
|
+
const bounds = [open, ...cuts, close]
|
|
64
|
+
const out: string[] = []
|
|
65
|
+
for (let k = 0; k + 1 < bounds.length; k++) {
|
|
66
|
+
out.push(...slashBraceExpansions(head + pattern.slice(bounds[k]! + 1, bounds[k + 1]) + tail))
|
|
67
|
+
}
|
|
68
|
+
return [...new Set(out)]
|
|
69
|
+
}
|
|
70
|
+
return [pattern]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* `realpathSync`, for a path holding a backslash too: Bun's answers ENOENT
|
|
75
|
+
* for any such path (1.4.2), so its links are read one by one.
|
|
76
|
+
*/
|
|
77
|
+
export function realpathOf(p: string): string {
|
|
78
|
+
try {
|
|
79
|
+
return realpathSync(p)
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (!p.includes('\\')) throw err
|
|
82
|
+
// Not `path.resolve`: it folds `link/..` lexically, and the kernel
|
|
83
|
+
// takes `..` from where the link leads.
|
|
84
|
+
const abs = path.isAbsolute(p) ? p : `${process.cwd()}${path.sep}${p}`
|
|
85
|
+
let at = path.parse(abs).root
|
|
86
|
+
const pending = abs.slice(at.length).split(path.sep).filter(Boolean).reverse()
|
|
87
|
+
let hops = 0
|
|
88
|
+
while (pending.length > 0) {
|
|
89
|
+
const next = path.join(at, pending.pop()!)
|
|
90
|
+
let link: string
|
|
91
|
+
try {
|
|
92
|
+
link = readlinkSync(next)
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if ((e as NodeJS.ErrnoException).code !== 'EINVAL') throw e
|
|
95
|
+
at = next
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
// Linux's own bound on links followed in one lookup (MAXSYMLINKS).
|
|
99
|
+
if (++hops > 40) {
|
|
100
|
+
throw Object.assign(
|
|
101
|
+
new Error(`ELOOP: too many symbolic links encountered, realpath '${p}'`),
|
|
102
|
+
{
|
|
103
|
+
code: 'ELOOP',
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
if (path.isAbsolute(link)) at = path.parse(link).root
|
|
108
|
+
pending.push(...link.split('/').filter(Boolean).reverse())
|
|
109
|
+
}
|
|
110
|
+
return at
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The git top-level holding `dir`, or null outside git: the nearest `.git`
|
|
116
|
+
* up from it. A submodule's `.git` file points into its superproject's
|
|
117
|
+
* `.git/modules`, so the walk goes on to the superproject's.
|
|
118
|
+
*/
|
|
119
|
+
export async function gitTop(dir: string): Promise<string | null> {
|
|
120
|
+
for (let d = dir; ; d = d.slice(0, d.lastIndexOf('/'))) {
|
|
121
|
+
if (await Bun.file(`${d}/.git/HEAD`).exists()) return d || '/'
|
|
122
|
+
const file = Bun.file(`${d}/.git`)
|
|
123
|
+
if (await file.exists()) {
|
|
124
|
+
const gitdir = await file.text().catch(() => '')
|
|
125
|
+
if (!/(^|[\s/])\.git\/modules\//.test(gitdir)) return d || '/'
|
|
126
|
+
}
|
|
127
|
+
if (d === '') return null
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The files one discovery has read, by absolute path: the bytes, or null when none is there. */
|
|
132
|
+
export type LoadReads = Map<string, Promise<Uint8Array | null>>
|
|
133
|
+
|
|
134
|
+
/** `file`'s bytes, read at most once per `reads`; null when there is no file there. */
|
|
135
|
+
export function readOnce(reads: LoadReads | undefined, file: string): Promise<Uint8Array | null> {
|
|
136
|
+
const held = reads?.get(file)
|
|
137
|
+
if (held !== undefined) return held
|
|
138
|
+
const f = Bun.file(file)
|
|
139
|
+
const pending = f
|
|
140
|
+
.exists()
|
|
141
|
+
.then((there) => (there ? f.bytes().catch((err: unknown) => unreadable(err, file)) : null))
|
|
142
|
+
reads?.set(file, pending)
|
|
143
|
+
return pending
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A read the file system refused, named as a read; anything else rethrown. */
|
|
147
|
+
export function unreadable(err: unknown, file: string): never {
|
|
148
|
+
if (isPermissionError(err)) {
|
|
149
|
+
throw new UserError(`${file}: not readable by this user (${err.code})`)
|
|
150
|
+
}
|
|
151
|
+
throw err
|
|
152
|
+
}
|