@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.
@@ -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 `vx.workspace.ts` (the
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 — LayeredCache
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 `vx.workspace.ts`; the local store stays the floor beneath it:
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(ctx): CacheLayer | undefined {
232
+ cache(): RemoteCacheLayer | undefined {
235
233
  const config = resolveNxCacheConfig(options)
236
- if (config === undefined) return undefined
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
+ }