@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 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); a task a same-project edge orders after it stays cached too (vx caches what an ordered dependant ADDS to the tree — twenty's `build:individual` into `build`'s `dist`); the rest run uncached with a todo naming the keeper and the fix, their own output path or that edge. Same rule for Nx targets.
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.578",
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.578"
42
+ "@vzn/vx": "^0.0.579"
43
43
  },
44
44
  "repository": {
45
45
  "type": "git",
@@ -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 — UNLESS
4
- // a same-project edge orders them (core item 588): then the dependant is
5
- // ADDITIVE, keeps its cache, and owns only what its run added. strapi
6
- // (2026-09-11) declares `build`, `build:code` and `build:types` all on
7
- // `dist/**`, and the refusal came at load time, after the migration had
8
- // reported clean. The mapping resolves it: the task with a `^` edge keeps
9
- // its cache (it is the one a dependant waits for), or the first declared
10
- // when none has one; every other task on that path stays cached when an
11
- // edge orders it against every kept task it overlaps, and otherwise runs
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, or the edge).
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
- const localDeps = Array.isArray(deps)
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
- // Additive under core's rule when an edge orders it against every
242
- // kept task whose outputs it overlaps: it stays cached, no todo.
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 (unordered === undefined) {
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) => unordered.files.some((ga) => outputsOverlap(ga, 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(unordered.name)} also ` +
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
- `Give it its own output path to cache it, or a dependsOn edge on ${JSON.stringify(unordered.name)} ` +
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. Edges are
276
- * not read: across projects they are the package graph's, which the
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', which
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