@vzn/vx-migrate 0.0.578 → 0.0.579
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 +1 -1
- package/package.json +2 -2
- package/src/shared-outputs.ts +22 -57
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Rules:
|
|
|
63
63
|
- `env` / `passThroughEnv`: explicit names go to `cache.inputs.env` (env only) and `exec.env.passThrough` (both, plus both globals); a wildcard expands over the run's environment under `turbo()`, which maps where the tasks run (a new variable name such a wildcard matches maps afresh); the CLI, which writes for another environment, lists the names the task's package and its workspace dependencies spell (a root task: the whole repo; clerk's `E2E_*`), noted once per entry, and one nothing spells is a todo; `*` is the only wildcard, as in Turbo, so `\*`, a leading `\!`, `?` and `[` are literal: unkey's `NEXT_PUBLIC_\*` names one variable, which vx cannot key, so it is dropped and reported once for the workspace with the count of tasks naming it (openstatus's root `build` env `\*` had written one todo into each of 45 configs; `turbo()`: only when the run's environment sets it, as Turbo hashes nothing otherwise); and a `!` entry (openstatus' `!NEXT_PUBLIC_VERCEL_URL`) removes the names it matches from its list, with no todo, since vx matches no name it is not given. A name both a global list and the task's own list carry is listed once.
|
|
64
64
|
- Framework inference (Turbo hashes and passes Next's `NEXT_PUBLIC_*`, Vite's `VITE_*`, … for a package that depends on the framework): `turbo()` expands the prefix over the run's environment; the CLI writes the names the tracked source and `.env.example` of the package and its workspace dependencies spell (the bare prefix, as a `startsWith` test spells it, is none) (Next bundles theirs: cal.com's web, 23 alone, 58 with them), and a note asks for any only an installed dependency reads.
|
|
65
65
|
|
|
66
|
-
- **Two tasks of one package on one output path** (strapi's `build`, `build:code` and `build:types`, all on `dist/**`): vx cleans a task's outputs before it runs and before a restore, so the loader refuses two cached tasks whose outputs provably overlap. The mapping resolves it before the file is written — the task with a `^` edge keeps its cache (the first declared when none has one);
|
|
66
|
+
- **Two tasks of one package on one output path** (strapi's `build`, `build:code` and `build:types`, all on `dist/**`): vx cleans a task's outputs before it runs and before a restore, so the loader refuses two cached tasks whose outputs provably overlap. Under vx's default `rules.exclusiveOutputs` an edge between the two changes nothing (twenty's `build:individual` writing into `build`'s `dist`). The mapping resolves it before the file is written — the task with a `^` edge keeps its cache (the first declared when none has one); the rest run uncached with a todo naming the keeper and the fix, their own output path. Same rule for Nx targets.
|
|
67
67
|
- **Two packages' tasks on one workspace output** (cal.com's shared `post-install` writes `../../node_modules/@prisma/client/**` from every package with the script): core refuses two cached tasks on one path with no edge between them, so `turbo()` (and `nx()`) keeps the first (in package and task order) cached and runs the rest uncached, each with a todo naming the keeper. A package's own outputs count at their workspace path: typescript-eslint's root project caches `dist` and each package's typecheck `dist/packages/<name>`, one path twice. Edges across packages are not read, so an ordered pair loses its cache too.
|
|
68
68
|
- `cache: false` or `persistent: true` → no `cache` block; a persistent task gets `exec.persistent: {}` and, when some task depends on it, the consumer's `persistentTodo`.
|
|
69
69
|
- A task's `description` (Turbo 2.11.5's schema) is the vx task's `description`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-migrate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.579",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Adopt @vzn/vx from Turborepo or Nx: run the repo unchanged, migrate it to vx.config.ts, and keep your Turbo or Nx remote cache",
|
|
6
6
|
"keywords": [
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@vzn/vx": "^0.0.
|
|
42
|
+
"@vzn/vx": "^0.0.579"
|
|
43
43
|
},
|
|
44
44
|
"repository": {
|
|
45
45
|
"type": "git",
|
package/src/shared-outputs.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
// Two targets of one project on one output path. vx cleans a task's
|
|
2
2
|
// declared outputs before it runs and before a cache-hit restore, so the
|
|
3
|
-
// loader refuses two cached tasks whose outputs provably overlap
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
3
|
+
// loader refuses two cached tasks whose outputs provably overlap, an edge
|
|
4
|
+
// between them or not under core's default `rules.exclusiveOutputs` (X-53).
|
|
5
|
+
// A workspace may turn the rule off (item 588's additive shape), but the
|
|
6
|
+
// mapping never sees the workspace's rules, so it maps for the default and
|
|
7
|
+
// loads under either.
|
|
8
|
+
// strapi (2026-09-11) declares `build`, `build:code` and `build:types` all
|
|
9
|
+
// on `dist/**`, and twenty's `build:individual` writes into `build`'s
|
|
10
|
+
// `dist`; the refusal came at load time, after the migration had reported
|
|
11
|
+
// clean. The mapping resolves it: the task with a `^` edge keeps its cache
|
|
12
|
+
// (it is the one a dependant waits for), or the first declared when none
|
|
13
|
+
// has one; every other task whose outputs overlap a kept task's runs
|
|
12
14
|
// uncached, with a todo that names the keeper and the fix (its own output
|
|
13
|
-
// path
|
|
15
|
+
// path).
|
|
14
16
|
//
|
|
15
17
|
// The overlap question is core's own `outputsOverlap`, asked through the
|
|
16
18
|
// façade. It used to be a COPY of it here, and the copy stopped being the
|
|
@@ -162,8 +164,6 @@ interface Cached {
|
|
|
162
164
|
name: string
|
|
163
165
|
files: string[]
|
|
164
166
|
hasUpstreamEdge: boolean
|
|
165
|
-
/** Same-project `dependsOn` names (no `^`), for the ordering test. */
|
|
166
|
-
localDeps: string[]
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
function cachedOutputs(t: GeneratedTask, index: number): Cached | null {
|
|
@@ -179,10 +179,7 @@ function cachedOutputs(t: GeneratedTask, index: number): Cached | null {
|
|
|
179
179
|
const deps = task['dependsOn']
|
|
180
180
|
const hasUpstreamEdge =
|
|
181
181
|
Array.isArray(deps) && deps.some((d) => typeof d === 'string' && d.startsWith('^'))
|
|
182
|
-
|
|
183
|
-
? deps.filter((d): d is string => typeof d === 'string' && !d.startsWith('^'))
|
|
184
|
-
: []
|
|
185
|
-
return { index, name: t.name, files: strings, hasUpstreamEdge, localDeps }
|
|
182
|
+
return { index, name: t.name, files: strings, hasUpstreamEdge }
|
|
186
183
|
}
|
|
187
184
|
|
|
188
185
|
/**
|
|
@@ -196,33 +193,6 @@ export function resolveSharedOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
|
|
|
196
193
|
const c = cachedOutputs(t, i)
|
|
197
194
|
if (c !== null) cached.push(c)
|
|
198
195
|
})
|
|
199
|
-
// Does `from` reach `to` through same-project edges? The generated
|
|
200
|
-
// tasks of one project are the whole graph here.
|
|
201
|
-
// Every task, cached or not: a hop through an uncached group task is an
|
|
202
|
-
// edge too.
|
|
203
|
-
const depsByName = new Map<string, string[]>()
|
|
204
|
-
for (const t of tasks) {
|
|
205
|
-
const deps = t.task?.['dependsOn']
|
|
206
|
-
depsByName.set(
|
|
207
|
-
t.name,
|
|
208
|
-
Array.isArray(deps)
|
|
209
|
-
? deps.filter((d): d is string => typeof d === 'string' && !d.startsWith('^'))
|
|
210
|
-
: [],
|
|
211
|
-
)
|
|
212
|
-
}
|
|
213
|
-
const reaches = (from: Cached, to: Cached): boolean => {
|
|
214
|
-
const seen = new Set<string>()
|
|
215
|
-
const stack = [...from.localDeps]
|
|
216
|
-
while (stack.length > 0) {
|
|
217
|
-
const n = stack.pop()!
|
|
218
|
-
if (n === to.name) return true
|
|
219
|
-
if (seen.has(n)) continue
|
|
220
|
-
seen.add(n)
|
|
221
|
-
for (const d of depsByName.get(n) ?? []) stack.push(d)
|
|
222
|
-
}
|
|
223
|
-
return false
|
|
224
|
-
}
|
|
225
|
-
const ordered = (x: Cached, y: Cached): boolean => reaches(x, y) || reaches(y, x)
|
|
226
196
|
const dropped = new Set<number>()
|
|
227
197
|
for (const a of cached) {
|
|
228
198
|
if (dropped.has(a.index)) continue
|
|
@@ -238,25 +208,22 @@ export function resolveSharedOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
|
|
|
238
208
|
const kept: Cached[] = [keeper]
|
|
239
209
|
for (const c of all) {
|
|
240
210
|
if (c === keeper) continue
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const unordered = kept.find(
|
|
244
|
-
(k) => c.files.some((gb) => k.files.some((ga) => outputsOverlap(ga, gb))) && !ordered(c, k),
|
|
211
|
+
const clash = kept.find((k) =>
|
|
212
|
+
c.files.some((gb) => k.files.some((ga) => outputsOverlap(ga, gb))),
|
|
245
213
|
)
|
|
246
|
-
if (
|
|
214
|
+
if (clash === undefined) {
|
|
247
215
|
kept.push(c)
|
|
248
216
|
continue
|
|
249
217
|
}
|
|
250
218
|
dropped.add(c.index)
|
|
251
219
|
const t = tasks[c.index]!
|
|
252
|
-
const shared = c.files.find((gb) =>
|
|
220
|
+
const shared = c.files.find((gb) => clash.files.some((ga) => outputsOverlap(ga, gb)))!
|
|
253
221
|
delete t.task!['cache']
|
|
254
222
|
t.todos.push(
|
|
255
|
-
`declares the output ${JSON.stringify(shared)} that ${JSON.stringify(
|
|
223
|
+
`declares the output ${JSON.stringify(shared)} that ${JSON.stringify(clash.name)} also ` +
|
|
256
224
|
"declares — vx cleans a task's outputs before it runs and before a restore, so two " +
|
|
257
225
|
"cached tasks on one path would delete each other's work; this one runs uncached. " +
|
|
258
|
-
|
|
259
|
-
'so vx orders them and caches what this one adds.',
|
|
226
|
+
'Give it its own output path to cache it.',
|
|
260
227
|
)
|
|
261
228
|
}
|
|
262
229
|
}
|
|
@@ -272,9 +239,8 @@ export function resolveSharedOutputs(tasks: GeneratedTask[]): GeneratedTask[] {
|
|
|
272
239
|
* workspace path: typescript-eslint's root project caches `dist` and every
|
|
273
240
|
* package's typecheck `dist/packages/<name>`, and core refused the run
|
|
274
241
|
* over the nesting. Keepers are the first in project and task order;
|
|
275
|
-
* every later task on a kept path runs uncached, with a todo
|
|
276
|
-
*
|
|
277
|
-
* mapping does not hold, so an ordered pair loses its cache too.
|
|
242
|
+
* every later task on a kept path runs uncached, with a todo, edge or not,
|
|
243
|
+
* as core refuses the pair either way.
|
|
278
244
|
*/
|
|
279
245
|
export function resolveSharedWorkspaceOutputs(
|
|
280
246
|
root: string,
|
|
@@ -335,8 +301,7 @@ export function resolveSharedWorkspaceOutputs(
|
|
|
335
301
|
rel === '' ? g.replace(/^(\.\/)+/, '') : `${rel}/${g.replace(/^(\.\/)+/, '')}`,
|
|
336
302
|
)
|
|
337
303
|
if (ws.length === 0 && own.length === 0) continue
|
|
338
|
-
// Two own outputs of one project are resolveSharedOutputs'
|
|
339
|
-
// reads the edges that order them.
|
|
304
|
+
// Two own outputs of one project are resolveSharedOutputs'.
|
|
340
305
|
const overlap = (a: string[], b: string[]): string | undefined =>
|
|
341
306
|
a.find((g) => b.some((h) => outputsOverlap(h, g)))
|
|
342
307
|
let clash: (typeof kept)[number] | undefined
|