@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/turbo-cache/oidc.ts
CHANGED
|
@@ -48,13 +48,11 @@ export function oidcSettings(
|
|
|
48
48
|
): Omit<OidcConfig, 'idTokenUrl' | 'idTokenBearer'> {
|
|
49
49
|
const o = oidc === true ? {} : oidc
|
|
50
50
|
if (typeof o !== 'object' || o === null)
|
|
51
|
-
throw new Error(
|
|
52
|
-
'vx/turbo-cache: oidc must be true or { tokenUrl, revokeUrl, audience, params }',
|
|
53
|
-
)
|
|
51
|
+
throw new Error('turboCache(): oidc must be true or { tokenUrl, revokeUrl, audience, params }')
|
|
54
52
|
if (!vercel && o.tokenUrl === undefined)
|
|
55
|
-
throw new Error("
|
|
53
|
+
throw new Error("turboCache(): oidc needs tokenUrl for a cache that is not Vercel's")
|
|
56
54
|
if (vercel && team === undefined)
|
|
57
|
-
throw new Error('
|
|
55
|
+
throw new Error('turboCache(): oidc on Vercel needs teamId or teamSlug (TURBO_TEAM)')
|
|
58
56
|
const revokeUrl = o.revokeUrl ?? (vercel ? VERCEL_REVOKE_URL : undefined)
|
|
59
57
|
return {
|
|
60
58
|
tokenUrl: o.tokenUrl ?? VERCEL_TOKEN_URL,
|
|
@@ -101,22 +99,44 @@ const form = (fields: Record<string, string>) => ({
|
|
|
101
99
|
/** Where a URL points, never its query: GitHub's carries a runtime id. */
|
|
102
100
|
const where = (url: string) => (URL.canParse(url) ? new URL(url).host : 'the endpoint')
|
|
103
101
|
|
|
102
|
+
/**
|
|
103
|
+
* A refusal's own words: RFC 6749's `error` and `error_description`, or
|
|
104
|
+
* GitHub's `message`. Only those fields, which name a fault and never
|
|
105
|
+
* carry a token; any other body is left unread. "answered HTTP 400" alone
|
|
106
|
+
* did not say the audience or the grant was wrong.
|
|
107
|
+
*/
|
|
108
|
+
async function refused(res: Response, what: string): Promise<Error> {
|
|
109
|
+
// quiet: a body that is not JSON says nothing; the status still names the refusal
|
|
110
|
+
const body = (await res.json().catch(() => null)) as Record<string, unknown> | null
|
|
111
|
+
const text = (v: unknown): string | undefined =>
|
|
112
|
+
typeof v === 'string' && v.trim() !== ''
|
|
113
|
+
? v.replace(/\s+/g, ' ').trim().slice(0, 200)
|
|
114
|
+
: undefined
|
|
115
|
+
const said = [text(body?.['error']), text(body?.['error_description']) ?? text(body?.['message'])]
|
|
116
|
+
.filter((s) => s !== undefined)
|
|
117
|
+
.join(': ')
|
|
118
|
+
return new Error(`${what} answered HTTP ${res.status}${said === '' ? '' : `: ${said}`}`)
|
|
119
|
+
}
|
|
120
|
+
|
|
104
121
|
async function field(res: Response, name: string, what: string): Promise<string> {
|
|
105
|
-
if (!res.ok)
|
|
106
|
-
await res.body?.cancel()
|
|
107
|
-
throw new Error(`${what} answered HTTP ${res.status}`)
|
|
108
|
-
}
|
|
122
|
+
if (!res.ok) throw await refused(res, what)
|
|
109
123
|
const value = ((await res.json().catch(() => null)) as Record<string, unknown> | null)?.[name]
|
|
110
124
|
if (typeof value !== 'string' || value === '') throw new Error(`${what} sent no ${name}`)
|
|
111
125
|
return value
|
|
112
126
|
}
|
|
113
127
|
|
|
128
|
+
/** An exchanged token, and when it ends (epoch ms) when the server says. */
|
|
129
|
+
export interface Exchanged {
|
|
130
|
+
readonly token: string
|
|
131
|
+
readonly expiresAt?: number
|
|
132
|
+
}
|
|
133
|
+
|
|
114
134
|
/** The job's OIDC token, exchanged for the cache's access token. Errors never quote a token. */
|
|
115
135
|
export async function exchangeOidc(
|
|
116
136
|
cfg: OidcConfig,
|
|
117
137
|
fetchImpl: typeof fetch = fetch,
|
|
118
138
|
wait: (ms: number) => Promise<void> = Bun.sleep,
|
|
119
|
-
): Promise<
|
|
139
|
+
): Promise<Exchanged> {
|
|
120
140
|
const idUrl = new URL(cfg.idTokenUrl)
|
|
121
141
|
if (cfg.audience !== undefined) idUrl.searchParams.set('audience', cfg.audience)
|
|
122
142
|
const idToken = await field(
|
|
@@ -129,22 +149,28 @@ export async function exchangeOidc(
|
|
|
129
149
|
'value',
|
|
130
150
|
`GitHub's OIDC endpoint (${where(cfg.idTokenUrl)})`,
|
|
131
151
|
)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
wait,
|
|
144
|
-
),
|
|
145
|
-
'access_token',
|
|
146
|
-
`the token exchange (${where(cfg.tokenUrl)})`,
|
|
152
|
+
const res = await send(
|
|
153
|
+
fetchImpl,
|
|
154
|
+
cfg.tokenUrl,
|
|
155
|
+
form({
|
|
156
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
|
|
157
|
+
subject_token: idToken,
|
|
158
|
+
subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
|
|
159
|
+
requested_token_type: 'urn:ietf:params:oauth:token-type:access_token',
|
|
160
|
+
...cfg.params,
|
|
161
|
+
}),
|
|
162
|
+
wait,
|
|
147
163
|
)
|
|
164
|
+
const what = `the token exchange (${where(cfg.tokenUrl)})`
|
|
165
|
+
if (!res.ok) throw await refused(res, what)
|
|
166
|
+
const body = (await res.json().catch(() => null)) as Record<string, unknown> | null
|
|
167
|
+
const token = body?.['access_token']
|
|
168
|
+
if (typeof token !== 'string' || token === '') throw new Error(`${what} sent no access_token`)
|
|
169
|
+
const ttl = body?.['expires_in']
|
|
170
|
+
return {
|
|
171
|
+
token,
|
|
172
|
+
...(typeof ttl === 'number' && ttl > 0 ? { expiresAt: Date.now() + ttl * 1000 } : {}),
|
|
173
|
+
}
|
|
148
174
|
}
|
|
149
175
|
|
|
150
176
|
/** Revoke the exchanged token; a throw is the caller's warning. */
|
|
@@ -166,6 +192,6 @@ export async function revokeOidc(
|
|
|
166
192
|
}),
|
|
167
193
|
wait,
|
|
168
194
|
)
|
|
195
|
+
if (!res.ok) throw await refused(res, `revoking at ${where(cfg.revokeUrl)}`)
|
|
169
196
|
await res.body?.cancel()
|
|
170
|
-
if (!res.ok) throw new Error(`revoking at ${where(cfg.revokeUrl)} answered HTTP ${res.status}`)
|
|
171
197
|
}
|
package/src/turbo-cache/scope.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ArtifactInfo, type RemoteCacheLayer, UserError } from '@vzn/vx'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* How a remote keeps branches apart (2026-10-20). `'header'`: plain keys,
|
|
@@ -10,9 +10,10 @@ import type { ArtifactInfo, RemoteCacheLayer } from '@vzn/vx'
|
|
|
10
10
|
*/
|
|
11
11
|
export type RemoteScope = 'header' | 'key'
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/** `factory`: the option's owner as a workspace calls it, `turboCache`. */
|
|
14
|
+
export function remoteScope(factory: string, scope: unknown): RemoteScope {
|
|
14
15
|
if (scope === undefined || scope === 'header' || scope === 'key') return scope ?? 'header'
|
|
15
|
-
throw new
|
|
16
|
+
throw new UserError(`${factory}(): scope must be 'header' or 'key', got ${JSON.stringify(scope)}`)
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
/** A server caps the read header at this many refs (the server rules), so vx never sends more. */
|
|
@@ -57,6 +58,7 @@ export function keyedByRef(inner: RemoteCacheLayer, allRefs: readonly string[]):
|
|
|
57
58
|
return {
|
|
58
59
|
...(inner.endpoint !== undefined ? { endpoint: inner.endpoint } : {}),
|
|
59
60
|
...(inner.open !== undefined ? { open: (ctx) => inner.open!(ctx) } : {}),
|
|
61
|
+
...(inner.lend !== undefined ? { lend: () => inner.lend!() } : {}),
|
|
60
62
|
has: firstHeld,
|
|
61
63
|
...(hasMany !== undefined
|
|
62
64
|
? {
|
|
@@ -9,7 +9,8 @@ import { existsSync } from 'node:fs'
|
|
|
9
9
|
import path from 'node:path'
|
|
10
10
|
import { type GeneratedProject, type GeneratedTask, UserError } from '@vzn/vx'
|
|
11
11
|
import type { PackageMeta } from '../packages.js'
|
|
12
|
-
import {
|
|
12
|
+
import { refuseTaskNames } from '../plugin-gaps.js'
|
|
13
|
+
import { hasGluedGlobstar, minimatchToVx, withoutTakenBack } from '../glob-grammar.js'
|
|
13
14
|
import { packageScripts, relPosix } from '../paths.js'
|
|
14
15
|
import { scriptCommand, yarnPnp } from '../script-command.js'
|
|
15
16
|
import {
|
|
@@ -70,9 +71,11 @@ function viteConfigFile(dir: string): string | null {
|
|
|
70
71
|
|
|
71
72
|
// Bun's resolver reads `\` as a separator, even in a file: URL, so a config
|
|
72
73
|
// under `a\b` was looked for under `a/b`. Resolved here, the path stays as
|
|
73
|
-
// written; the file is read from it.
|
|
74
|
-
|
|
74
|
+
// written; the file is read from it, found by its number: Bun 1.4.4 hands
|
|
75
|
+
// onLoad the path with `\` made `/`.
|
|
76
|
+
const LITERAL_QUERY = /\?vx-literal=(\d+)$/
|
|
75
77
|
let servingLiteral = false
|
|
78
|
+
const literals: string[] = []
|
|
76
79
|
|
|
77
80
|
function importAsWritten(file: string): Promise<{ default?: unknown }> {
|
|
78
81
|
if (!file.includes('\\')) return import(file)
|
|
@@ -86,7 +89,7 @@ function importAsWritten(file: string): Promise<{ default?: unknown }> {
|
|
|
86
89
|
namespace: 'file',
|
|
87
90
|
}))
|
|
88
91
|
build.onLoad({ filter: LITERAL_QUERY }, async (args) => {
|
|
89
|
-
const at = args.path
|
|
92
|
+
const at = literals[Number(LITERAL_QUERY.exec(args.path)![1])]!
|
|
90
93
|
return {
|
|
91
94
|
contents: await Bun.file(at).text(),
|
|
92
95
|
loader: /\.[cm]?ts$/.test(at) ? 'ts' : 'js',
|
|
@@ -95,7 +98,7 @@ function importAsWritten(file: string): Promise<{ default?: unknown }> {
|
|
|
95
98
|
},
|
|
96
99
|
})
|
|
97
100
|
}
|
|
98
|
-
return import(`${file}?vx-literal`)
|
|
101
|
+
return import(`${file}?vx-literal=${literals.push(file) - 1}`)
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
/**
|
|
@@ -384,6 +387,7 @@ export async function mapViteTaskWorkspace(
|
|
|
384
387
|
`workspace dependencies spell (${n} task${n === 1 ? '' : 's'}) — add any other a task reads to ` +
|
|
385
388
|
'inputs.env and exec.env.passThrough',
|
|
386
389
|
)
|
|
390
|
+
refuseTaskNames(projects, 'task')
|
|
387
391
|
return { projects, notes }
|
|
388
392
|
}
|
|
389
393
|
|
|
@@ -551,7 +555,8 @@ function globs(
|
|
|
551
555
|
} else continue
|
|
552
556
|
const neg = pattern.startsWith('!')
|
|
553
557
|
const raw = neg ? pattern.slice(1) : pattern
|
|
554
|
-
|
|
558
|
+
// wax refuses a glued `**` (`src/**.ts`), and so does core.
|
|
559
|
+
const wax = hasGluedGlobstar(raw) ? null : minimatchToVx(raw, neg)
|
|
555
560
|
if (wax === null) {
|
|
556
561
|
todos.push(
|
|
557
562
|
`${kind} ${JSON.stringify(pattern)}: glob syntax vx cannot take — ` +
|
package/src/workspace-plugins.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The plugins a migrated workspace declares, from what the repo shows
|
|
2
|
-
// (
|
|
2
|
+
// (2026-10-06: "install and migrate fully … use the plugins"). Each
|
|
3
3
|
// is inert where it does not apply, so declaring one costs a run nothing,
|
|
4
4
|
// and each is installed beside vx at vx-migrate's own version (plugins
|
|
5
5
|
// pin their core peer to the release they shipped with).
|
|
@@ -23,9 +23,12 @@ export function workspacePlugins(root: string, runner?: 'turbo' | 'nx'): Workspa
|
|
|
23
23
|
out.push({ pkg: '@vzn/vx-migrate', factory: REMOTE_CACHE[runner] })
|
|
24
24
|
const lock = lockfilePlugin(root)
|
|
25
25
|
if (lock !== undefined) out.push({ pkg: '@vzn/vx-lockfile', factory: lock.factory })
|
|
26
|
-
out.push({ pkg: '@vzn/vx-
|
|
26
|
+
out.push({ pkg: '@vzn/vx-history', factory: 'vxHistory' })
|
|
27
27
|
if (existsSync(path.join(root, '.github', 'workflows')))
|
|
28
28
|
out.push({ pkg: '@vzn/vx-ci', factory: 'github' })
|
|
29
|
+
// A remote cache on CI reads the refs only these name (2026-10-25).
|
|
30
|
+
if (existsSync(path.join(root, '.gitlab-ci.yml')))
|
|
31
|
+
out.push({ pkg: '@vzn/vx-ci', factory: 'gitlab' })
|
|
29
32
|
return out
|
|
30
33
|
}
|
|
31
34
|
|