@vzn/vx-migrate 0.0.591 → 0.0.594
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 +2 -1
- package/package.json +2 -2
- package/src/adoption-plugin.ts +2 -1
- package/src/nx/nx-deps.ts +1 -1
- package/src/nx/nx-map.ts +18 -2
- package/src/nx-command.ts +36 -4
- package/src/paths.ts +5 -3
- package/src/turbo/turbo-map.ts +98 -3
- package/src/workspace-notes.ts +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ Rules:
|
|
|
58
58
|
- `dependsOn`: `^x` passes through when some package runs `x`, and is dropped when none does (Turbo gives it no edges; core refuses a `^x` no project declares as a typo); `pkg#task` is kept only when `pkg` emits `task` (else a todo: edge dropped); a same-package task Turbo defines but the package has no script for is Turbo's no-op node, so its own edges pass through in its place (`test → codegen → ^build` with no `codegen` script is `test → ^build`); another package's `^x` or `pkg#x` reaches such a node through a group task when its edges name a task of its own package or another (with-tailwind's `ui` has no `build` script; its `build` builds `build:styles` and `build:components`, and `web#build` waits on them), and so does one whose `^` edge names another task (rallly's script-less `build` holds `^db:generate`), while one whose only edge is `^` to its own name needs none, since core's `^x` walks past a package without `x`; a no-op node nothing else reaches runs nothing, its own edges included, where Turbo still runs them (kitchen-sink's `turbo run test` builds `@repo/ui` for `admin`, which has no `test` script; `vx run test --all` does not, and `vx run build test --all` does both); a name Turbo does not define has no edge; `$TURBO_ROOT$` deps are a todo. Root `//#task`s run in the workspace root, as under Turbo: `turbo()` names the root a project through core's `discover` stage when turbo.json declares one (no `vx.config` needed; a root no member glob lists), and `//#x` in `dependsOn` is an edge to it, named by the root `package.json` `name`; a root task's globs are the workspace's (`workspaceFiles`), since Turbo hashes a root task over the whole repo; with no root project (a root `package.json` with no `name`, or one a package holds) they are a note and such an edge a todo. Turbo runs no plain task in the root package of a monorepo, and neither does `turbo()`; in a single-package repo (no workspaces, Turbo's `non-monorepo` example) turbo.json's plain tasks run on the root package, as under Turbo.
|
|
59
59
|
- A transit node (Turbo's documented pattern, its with-vitest example: `transit: { dependsOn: ["^transit"] }` with no script anywhere, and `test: { dependsOn: ["transit"] }`) is a key-only task in each package that defines it: it runs `true`, cached and keyed on its inputs, with its `^transit` edge, so a dependant's `test` re-keys on its dependencies' sources as Turbo re-hashes it. A `^` task that some package has a script for, or that nothing depends on, is no transit node. A package without the script of a `^` task others run gets the same key-only task, as Turbo hashes a no-op node there: an edit to it re-keys its dependants. A `build` whose only edge is `^build` is not written: core gives every project with no `build` that node (solid's three script-less packages each got a `build` running `true`).
|
|
60
60
|
- `with` (tasks Turbo runs alongside, `web#dev` with `api#dev`): an edge to each sidecar that is persistent, so vx starts the task once the sidecar has spawned and runs the sidecar only when the task runs; a sidecar that ends would be waited for, so it is a todo instead, and a pair that names each other keeps one edge (two would be a cycle). A task with no script whose `with` names persistent sidecars (Turbo's with-tailwind example: `ui` has no `dev` script, its `dev` starts `dev:styles` and `dev:components`) is a group task that depends on them.
|
|
61
|
-
- `inputs`: a structured entry (Turbo 2.11) is its `globs`, plus `**/*` with `withDefaults`, for `startup` and `jit` alike; `dependencyOutputs` adds none (vx folds each dependency's key). Then: absent or `[]` → `**/*` (Turbo's default); `$TURBO_DEFAULT$` → `**/*`; exclusions alone narrow `**/*`; `$TURBO_ROOT$/<path>` → `cache.inputs.workspaceFiles` (negation kept); any other `$TURBO_ROOT$` use is a todo. `globalDependencies` and Turbo 1's `globalDotEnv` land in `workspaceFiles` too, and a task's Turbo 1 `dotEnv` in its `files`. Turbo 1's `$NAME` entries, in `globalDependencies` or a task's `dependsOn`, are env vars: they join `cache.inputs.env` and `exec.env.passThrough`. A package nested in the task's package (cal.com's apps inside `@calcom/app-store`) is hashed by Turbo with its parent, and core's globs stop at a project: each glob that reaches one is also listed in `cache.inputs.workspaceFiles`. Turbo's glob grammar is translated as Nx's is (`*.[jt]s` is `*.{[jt],j,t}s`); an input with no safe form widens to `**/*` with a todo. A `.env`-shaped input (`.env*`, `.env.local`, a task's Turbo 1 `dotEnv`, create-turbo's `globalDependencies: ["**/.env.*local"]`) is gitignored as a rule, so it is keyed by a probe that hashes the `.env` files it can name (`cache.inputs.runtime`) or, for a root entry, the files its root-relative globs name, hidden ones too, as Turbo's `*` matches them (`workspaceRuntime`; `**/<name>` is a `find -name` that skips `node_modules`, and another `**` walks the workspace), rather than as a file glob git never reports. A package whose `.env` globs all sit at its root gets a one-shell probe of that directory; one below the root walks the package.
|
|
61
|
+
- `inputs`: a structured entry (Turbo 2.11) is its `globs`, plus `**/*` with `withDefaults`, for `startup` and `jit` alike; `dependencyOutputs` adds none where the producer is cached (vx folds each dependency's key); from a `cache: false` producer (vercel/vercel's `//#generate:cache-keys`, which records the host) the named files, or its outputs, are a `cache.inputs.workspaceRuntime` probe, and `cache.inputs.tasks` leaves the producer out, so core answers the probe after it ran. Then: absent or `[]` → `**/*` (Turbo's default); `$TURBO_DEFAULT$` → `**/*`; exclusions alone narrow `**/*`; `$TURBO_ROOT$/<path>` → `cache.inputs.workspaceFiles` (negation kept); any other `$TURBO_ROOT$` use is a todo. `globalDependencies` and Turbo 1's `globalDotEnv` land in `workspaceFiles` too, and a task's Turbo 1 `dotEnv` in its `files`. Turbo 1's `$NAME` entries, in `globalDependencies` or a task's `dependsOn`, are env vars: they join `cache.inputs.env` and `exec.env.passThrough`. A package nested in the task's package (cal.com's apps inside `@calcom/app-store`) is hashed by Turbo with its parent, and core's globs stop at a project: each glob that reaches one is also listed in `cache.inputs.workspaceFiles`. Turbo's glob grammar is translated as Nx's is (`*.[jt]s` is `*.{[jt],j,t}s`); an input with no safe form widens to `**/*` with a todo. A `.env`-shaped input (`.env*`, `.env.local`, a task's Turbo 1 `dotEnv`, create-turbo's `globalDependencies: ["**/.env.*local"]`) is gitignored as a rule, so it is keyed by a probe that hashes the `.env` files it can name (`cache.inputs.runtime`) or, for a root entry, the files its root-relative globs name, hidden ones too, as Turbo's `*` matches them (`workspaceRuntime`; `**/<name>` is a `find -name` that skips `node_modules`, and another `**` walks the workspace), rather than as a file glob git never reports. A package whose `.env` globs all sit at its root gets a one-shell probe of that directory; one below the root walks the package.
|
|
62
62
|
- `outputs`: `$TURBO_ROOT$/<path>` → `cache.outputs.workspaceFiles`; a negated output rides beside its positives and takes its paths back from the clean, the save and the restore (Next's `.next/**` minus `!.next/cache/**` keeps the cache), and one with no positive beside it takes back nothing and is dropped. Past its first segment an output takes Turbo's grammar (`dist/**/*.[cm]js`); the first stays a literal (a route directory). An output whose first segment is a wildcard (`**/*.d.ts`, `*/**`) runs the task uncached with a todo: Turbo never cleans an output, vx cleans it before every run, and such a glob reaches the sources. One segment with a literal extension the package tracks no file of (`*.xml`, `*.tsbuildinfo`) reaches only top-level artifacts and stays cached, and so does `**/<dir>/**` when the package tracks nothing under a directory of that name (vercel/ai's `**/dist/**`; `node_modules` is never an output), and so does a first segment no tracked top-level entry matches (tldraw's `dist-*/**`). A wildcard first segment with a literal rest (clerk's `*/package.json`, the subpath stubs it commits) reaches only files of that name: each committed one, up to 16, is taken back with `!` and the task stays cached; more, and it runs uncached. An output that covers the package's own `package.json` (trpc's client build lists it: the build rewrites `exports`) runs the task uncached with a todo: vx would delete the manifest before every run, and core refuses that config. `nx()` takes the manifest back with `!package.json` instead and keeps the cache (owner, 2026-10-03). So does one that covers the package's vx.config; `turbo()` and `nx()` check the config the package has, and `vx-migrate` the one it writes, so sanity's `*.js` shims stay cached beside none. `nx()` keeps a wildcard-first output cached instead: each committed file it reaches is taken back with `!` (past sixteen, uncached with a todo), and the clean removes the rest, what the task writes (owner, 2026-10-03). A committed file under an output (typescript-eslint's `data/sponsors.json` in a cached `data`) is taken back with `!` — Turbo and Nx never clean an output, vx does — so it survives the clean and the task keeps its cache; past sixteen such files the task runs uncached with a todo. `turbo()` and `nx()` both do this, and so do the configs `vx-migrate` writes from turbo.json or an Nx graph (each take-back is a todo there, as it names files a later commit may move).
|
|
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.
|
|
@@ -105,6 +105,7 @@ Nx's own option handling, rendered as one POSIX `sh` line (`vx show` prints it):
|
|
|
105
105
|
- **An argument replaces its option:** an option the arguments after `vx run … --` name (`--region=us`, `--region us`, `--no-region`, `-r` for a one-letter option; no camel-case expansion) is not appended, as Nx drops it; the line decides once it has them. `tests/nx-run-commands-override-live.test.ts` holds it to `nx run`.
|
|
106
106
|
- **Environment:** `env` is `exec.env.define` (so it is in the key), and `color: true` sets `FORCE_COLOR=true`, as Nx does (nx#20465). `envFile` is loaded after the task's `.env` files (below), a name they already set winning, as in Nx (nx#23581).
|
|
107
107
|
- **`readyWhen`** makes the task persistent with that string as its `readyWhen`, so dependents start once it is printed; several strings, all of which Nx waits for, run the line under `nx-env --ready-when` (one flag per string), which passes the output through and prints `nx-env: ready` once every string has appeared on stdout or stderr; that line is the task's `readyWhen`.
|
|
108
|
+
- **A leading Nx call** (`nx <target> <project>`, `nx run <project>:<target>`, through `npx` / `pnpm` / `yarn` / `bunx` or not) in a serial line, or as its one command, is a `dependsOn` edge to that target (its default configuration, as Nx runs it) and leaves the line: ngrx's `build` opens with `nx build-package <project>`. One Nx would hand options or arguments to, one after another command, and one in a parallel line stay commands.
|
|
108
109
|
- **Reported, not reproduced:** per-command `prefix` / `color` decoration, `streamOutput: false`. Decoration in a serial run (`parallel: false`), which Nx refuses, is the failing placeholder with Nx's reason, as `readyWhen` there is. Display-only options (`usePty`, `tty`, `verbose`) change nothing here: vx runs every task without a pseudo-terminal.
|
|
109
110
|
|
|
110
111
|
`tests/nx-exec-live.test.ts` holds each of these shapes to Nx's own run-commands executor on the same options and arguments.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-migrate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.594",
|
|
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.594"
|
|
43
43
|
},
|
|
44
44
|
"repository": {
|
|
45
45
|
"type": "git",
|
package/src/adoption-plugin.ts
CHANGED
|
@@ -111,7 +111,8 @@ export function adoptionPlugin(
|
|
|
111
111
|
// only reads a stage-given task after validating it. The copy was
|
|
112
112
|
// the per-process memo's guard and cost 10 ms per run at 1,000
|
|
113
113
|
// projects, 3,000 clones (item 609).
|
|
114
|
-
config.tasks
|
|
114
|
+
if (!Object.hasOwn(config.tasks, t.name))
|
|
115
|
+
config.tasks[t.name] = t.task as unknown as TaskConfig
|
|
115
116
|
}
|
|
116
117
|
},
|
|
117
118
|
// The root files the mapping reads (`turbo.json`, `nx.json`) shape every
|
package/src/nx/nx-deps.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type TaskNameFor = (project: string, target: string, configuration: strin
|
|
|
19
19
|
* "depends on …#codegen but no such task is declared". Dropped here as
|
|
20
20
|
* Nx drops it, for this project and a named one alike.
|
|
21
21
|
*/
|
|
22
|
-
|
|
22
|
+
type HasTarget = (project: string, target: string) => boolean
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* The graph nodes a `projects` list names, ported from Nx's
|
package/src/nx/nx-map.ts
CHANGED
|
@@ -800,6 +800,10 @@ function buildTask(
|
|
|
800
800
|
meta.packageJson,
|
|
801
801
|
opts.manifestField,
|
|
802
802
|
opts.executors,
|
|
803
|
+
(p, t) =>
|
|
804
|
+
!(p === nodeName && t === targetName) &&
|
|
805
|
+
metaByNode.has(p) &&
|
|
806
|
+
Object.hasOwn(nodeMap[p]?.data?.targets ?? {}, t),
|
|
803
807
|
)
|
|
804
808
|
|
|
805
809
|
const inputs = emptyNxInputs()
|
|
@@ -845,6 +849,12 @@ function buildTask(
|
|
|
845
849
|
: { node: nodeName, configuration: variant.configuration! },
|
|
846
850
|
options,
|
|
847
851
|
)
|
|
852
|
+
// A nested `nx <target>` runs the target's default configuration, whatever this one runs.
|
|
853
|
+
for (const c of mapped?.nxCalls ?? []) {
|
|
854
|
+
const edge =
|
|
855
|
+
c.project === nodeName ? c.target : `${metaByNode.get(c.project)!.name}#${c.target}`
|
|
856
|
+
if (!deps.includes(edge)) deps.push(edge)
|
|
857
|
+
}
|
|
848
858
|
|
|
849
859
|
// Nx's rule, not a guess: a target is cached when it says `cache: true`
|
|
850
860
|
// (Nx ≥ 17 writes it into the graph from `cacheableOperations` too —
|
|
@@ -1003,6 +1013,8 @@ interface MappedCommand {
|
|
|
1003
1013
|
readonly envInputs: readonly string[]
|
|
1004
1014
|
/** A translated executor's `exec.timeout`. */
|
|
1005
1015
|
readonly timeout?: number
|
|
1016
|
+
/** The Nx targets a leading `nx <target> <project>` ran: dependency edges (nx-command.ts). */
|
|
1017
|
+
readonly nxCalls?: readonly { readonly project: string; readonly target: string }[]
|
|
1006
1018
|
}
|
|
1007
1019
|
|
|
1008
1020
|
/**
|
|
@@ -1025,6 +1037,7 @@ function mapCommand(
|
|
|
1025
1037
|
manifest: { readonly name?: unknown; readonly version?: unknown },
|
|
1026
1038
|
manifestField: MapNxOptions['manifestField'],
|
|
1027
1039
|
executors: NxExecutors | undefined,
|
|
1040
|
+
isTarget: (project: string, target: string) => boolean,
|
|
1028
1041
|
): MappedCommand | null {
|
|
1029
1042
|
const executor = target.executor
|
|
1030
1043
|
if (executor === 'nx:noop') {
|
|
@@ -1063,7 +1076,7 @@ function mapCommand(
|
|
|
1063
1076
|
if (executor === 'nx:run-commands' || plain) {
|
|
1064
1077
|
const rc = mapRunCommands(
|
|
1065
1078
|
plain ? { ...options, command: target.command } : options,
|
|
1066
|
-
{ projectRel, projectName },
|
|
1079
|
+
{ projectRel, projectName, isTarget },
|
|
1067
1080
|
todos,
|
|
1068
1081
|
)
|
|
1069
1082
|
if (rc === null) return line(PLACEHOLDER)
|
|
@@ -1075,7 +1088,10 @@ function mapCommand(
|
|
|
1075
1088
|
: path.posix.isAbsolute(rc.envFile)
|
|
1076
1089
|
? rc.envFile
|
|
1077
1090
|
: relPosix(projectRel, path.posix.normalize(rc.envFile))
|
|
1078
|
-
return
|
|
1091
|
+
return {
|
|
1092
|
+
...shell(rc.command, envFile, { env: rc.env, readyWhen: rc.readyWhen }, rc.readyAll),
|
|
1093
|
+
...(rc.nxCalls === undefined ? {} : { nxCalls: rc.nxCalls }),
|
|
1094
|
+
}
|
|
1079
1095
|
}
|
|
1080
1096
|
if (executor === 'nx:run-script') {
|
|
1081
1097
|
const script = typeof options.script === 'string' ? options.script : targetName
|
package/src/nx-command.ts
CHANGED
|
@@ -40,13 +40,15 @@ interface NxCommandContext {
|
|
|
40
40
|
readonly projectRel: string
|
|
41
41
|
/** The Nx project name, what `{projectName}` expands to. */
|
|
42
42
|
readonly projectName: string
|
|
43
|
+
/** Whether a vx task runs Nx project `project`'s `target`: what a leading `nx <target> <project>` may become. */
|
|
44
|
+
readonly isTarget?: (project: string, target: string) => boolean
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/** The line `nx-env --ready-when` prints once every string is seen (`READY` in nx-env.cjs). */
|
|
46
48
|
const NX_ENV_READY = '^nx-env: ready$'
|
|
47
49
|
|
|
48
50
|
/** A run-commands target as vx runs it. */
|
|
49
|
-
|
|
51
|
+
interface RunCommandsLine {
|
|
50
52
|
readonly command: string
|
|
51
53
|
/** `exec.env.define`: the target's `env`, and `FORCE_COLOR` under `color`. */
|
|
52
54
|
readonly env: Readonly<Record<string, string>>
|
|
@@ -60,8 +62,18 @@ export interface RunCommandsLine {
|
|
|
60
62
|
readonly readyAll?: readonly string[]
|
|
61
63
|
/** `envFile` as declared: workspace-root-relative, or absolute. */
|
|
62
64
|
readonly envFile: string | undefined
|
|
65
|
+
/**
|
|
66
|
+
* The Nx targets the line's leading `nx <target> <project>` /
|
|
67
|
+
* `nx run <project>:<target>` commands ran, taken out of the line: each
|
|
68
|
+
* is a dependency edge instead.
|
|
69
|
+
*/
|
|
70
|
+
readonly nxCalls?: readonly { readonly project: string; readonly target: string }[]
|
|
63
71
|
}
|
|
64
72
|
|
|
73
|
+
/** `nx <target> <project>` or `nx run <project>:<target>`, through a package runner or not, with nothing else. */
|
|
74
|
+
const NX_CALL =
|
|
75
|
+
/^(?:(?:npx|bunx|pnpm(?: exec)?|yarn)\s+)?nx\s+(?:run\s+([\w@./-]+):([\w:.-]+)|([\w:.-]+)\s+([\w@./-]+))$/
|
|
76
|
+
|
|
65
77
|
/** The options run-commands consumes itself; every other one is forwarded to each command. */
|
|
66
78
|
const PROP_KEYS: ReadonlySet<string> = new Set([
|
|
67
79
|
'command',
|
|
@@ -306,8 +318,6 @@ export function mapRunCommands(
|
|
|
306
318
|
'nx:run-commands: per-command `prefix` / `color` output decoration is not reproduced',
|
|
307
319
|
)
|
|
308
320
|
}
|
|
309
|
-
if (entries.length === 0) return { command: NOOP, env, readyWhen: undefined, envFile }
|
|
310
|
-
|
|
311
321
|
const argsOption = Array.isArray(options['args'])
|
|
312
322
|
? options['args'].join(' ')
|
|
313
323
|
: typeof options['args'] === 'string'
|
|
@@ -328,6 +338,27 @@ export function mapRunCommands(
|
|
|
328
338
|
? options['forwardAllArgs']
|
|
329
339
|
: true
|
|
330
340
|
|
|
341
|
+
// A command that runs another target under Nx, ahead of the rest of a
|
|
342
|
+
// serial line (ngrx's `build` opens with `nx build-package <project>`),
|
|
343
|
+
// ran Nx inside the task and kept two cached tasks on one output with no
|
|
344
|
+
// edge between them; as an edge, the target runs first as Nx ran it. One
|
|
345
|
+
// Nx would hand options or arguments to stays a command.
|
|
346
|
+
const nxCalls: { project: string; target: string }[] = []
|
|
347
|
+
if ((!parallel || entries.length === 1) && readyWhen.length === 0 && ctx.isTarget) {
|
|
348
|
+
while (entries.length > 0) {
|
|
349
|
+
const e = entries[0]!
|
|
350
|
+
const m = NX_CALL.exec(e.command.trim())
|
|
351
|
+
if (m === null || (forwardAll(e) && (unknown.length > 0 || argsOption))) break
|
|
352
|
+
const project = m[1] ?? m[4]!
|
|
353
|
+
const target = m[2] ?? m[3]!
|
|
354
|
+
if (!ctx.isTarget(project, target)) break
|
|
355
|
+
nxCalls.push({ project, target })
|
|
356
|
+
entries.shift()
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const calls = nxCalls.length > 0 ? { nxCalls } : {}
|
|
360
|
+
if (entries.length === 0) return { command: NOOP, env, readyWhen: undefined, envFile, ...calls }
|
|
361
|
+
|
|
331
362
|
const commands: Interpolated[] = []
|
|
332
363
|
let named = false
|
|
333
364
|
for (const e of entries) {
|
|
@@ -384,7 +415,7 @@ export function mapRunCommands(
|
|
|
384
415
|
const only = commands.length === 1 ? commands[0]! : undefined
|
|
385
416
|
if (only !== undefined && only.runtime === 'append' && only.baked.length === 0) {
|
|
386
417
|
const line = only.tail === undefined ? only.text : `${only.text} ${only.tail}`
|
|
387
|
-
return { command: `${cd}${line}`, env, readyWhen: pattern, envFile, ...ready }
|
|
418
|
+
return { command: `${cd}${line}`, env, readyWhen: pattern, envFile, ...ready, ...calls }
|
|
388
419
|
}
|
|
389
420
|
// A subshell per command, as Nx gives each a shell of its own; a comment
|
|
390
421
|
// in one must not swallow the `)` that closes it.
|
|
@@ -437,6 +468,7 @@ export function mapRunCommands(
|
|
|
437
468
|
readyWhen: pattern,
|
|
438
469
|
envFile,
|
|
439
470
|
...ready,
|
|
471
|
+
...calls,
|
|
440
472
|
}
|
|
441
473
|
}
|
|
442
474
|
|
package/src/paths.ts
CHANGED
|
@@ -27,10 +27,12 @@ export function relPosix(from: string, to: string): string {
|
|
|
27
27
|
* holds: a string or an array would enumerate its INDICES as script names,
|
|
28
28
|
* and indexing one by a task named `0` yields a single character that reads
|
|
29
29
|
* as a usable command. Core's `migrate-scripts.ts` guards this read; these
|
|
30
|
-
* mappers did not.
|
|
30
|
+
* mappers did not. Null-prototype, so a turbo.json task or Nx target named
|
|
31
|
+
* `constructor` or `toString` finds no script rather than Object's method.
|
|
31
32
|
*/
|
|
32
33
|
export function packageScripts(meta: ProjectMeta): Record<string, unknown> {
|
|
33
34
|
const raw = (meta.packageJson as unknown as { scripts?: unknown }).scripts
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
const scripts: Record<string, unknown> = Object.create(null)
|
|
36
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return scripts
|
|
37
|
+
return Object.assign(scripts, raw)
|
|
36
38
|
}
|
package/src/turbo/turbo-map.ts
CHANGED
|
@@ -500,6 +500,11 @@ function tasksOf(cfg: TurboJson): Record<string, TurboTask> {
|
|
|
500
500
|
return cfg.tasks ?? cfg.pipeline ?? {}
|
|
501
501
|
}
|
|
502
502
|
|
|
503
|
+
/** A turbo.json's own task `key`: a plain index reads `constructor` off Object. */
|
|
504
|
+
function taskAt(tasks: Record<string, TurboTask>, key: string): TurboTask | undefined {
|
|
505
|
+
return Object.hasOwn(tasks, key) ? tasks[key] : undefined
|
|
506
|
+
}
|
|
507
|
+
|
|
503
508
|
function declares(meta: ProjectMeta, dependency: string): boolean {
|
|
504
509
|
const pj = meta.packageJson as unknown as Record<string, unknown>
|
|
505
510
|
// Turbo counts every kind but a peer in a monorepo.
|
|
@@ -627,7 +632,7 @@ function taskDefined(
|
|
|
627
632
|
const cfg = files.get(at)
|
|
628
633
|
if (cfg === undefined) return at === ROOT ? 'none' : taskDefined(pkgName, name, files, ROOT, seen)
|
|
629
634
|
const tasks = tasksOf(cfg)
|
|
630
|
-
const def = tasks
|
|
635
|
+
const def = taskAt(tasks, `${pkgName}#${name}`) ?? taskAt(tasks, name)
|
|
631
636
|
if (def !== undefined) return optedOut(def) ? 'excluded' : 'found'
|
|
632
637
|
if (at === ROOT) return 'none'
|
|
633
638
|
for (const parent of parentsOf(cfg)) {
|
|
@@ -666,7 +671,9 @@ function taskNamesFor(
|
|
|
666
671
|
function definitionOf(pkgName: string, name: string, chain: readonly TurboJson[]): TurboTask {
|
|
667
672
|
const defs = chain.map((cfg, i) => {
|
|
668
673
|
const tasks = tasksOf(cfg)
|
|
669
|
-
return i === 0
|
|
674
|
+
return i === 0
|
|
675
|
+
? (taskAt(tasks, `${pkgName}#${name}`) ?? taskAt(tasks, name))
|
|
676
|
+
: taskAt(tasks, name)
|
|
670
677
|
})
|
|
671
678
|
let from = 0
|
|
672
679
|
for (let i = defs.length - 1; i > 0; i--) {
|
|
@@ -676,16 +683,43 @@ function definitionOf(pkgName: string, name: string, chain: readonly TurboJson[]
|
|
|
676
683
|
}
|
|
677
684
|
}
|
|
678
685
|
let def: TurboTask = {}
|
|
686
|
+
let fromDeps: DependencyOutputs | null = null
|
|
679
687
|
for (const d of defs.slice(from)) {
|
|
680
688
|
if (d === undefined) continue
|
|
681
689
|
const own: TurboTask = { ...d }
|
|
682
690
|
delete own.extends
|
|
683
|
-
if (Array.isArray(own.inputs))
|
|
691
|
+
if (Array.isArray(own.inputs)) {
|
|
692
|
+
const found = dependencyOutputsOf(own.inputs)
|
|
693
|
+
if (found !== null || !own.inputs.includes(TURBO_EXTENDS)) fromDeps = found
|
|
694
|
+
own.inputs = flatInputs(own.inputs)
|
|
695
|
+
}
|
|
684
696
|
def = withOverlay(def, own)
|
|
685
697
|
}
|
|
698
|
+
if (fromDeps !== null) dependencyOutputs.set(def, fromDeps)
|
|
686
699
|
return def
|
|
687
700
|
}
|
|
688
701
|
|
|
702
|
+
/** A task's `{ mode: "dependencyOutputs" }` input: the producers it names, and which of their files. */
|
|
703
|
+
interface DependencyOutputs {
|
|
704
|
+
from: readonly string[] | null
|
|
705
|
+
globs: readonly string[]
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/** Held beside a resolved definition, out of the keys the unknown-key check reads. */
|
|
709
|
+
const dependencyOutputs = new WeakMap<TurboTask, DependencyOutputs>()
|
|
710
|
+
|
|
711
|
+
function dependencyOutputsOf(inputs: readonly unknown[]): DependencyOutputs | null {
|
|
712
|
+
for (const i of inputs) {
|
|
713
|
+
if (i === null || typeof i !== 'object') continue
|
|
714
|
+
const { mode, from, globs } = i as { mode?: unknown; from?: unknown; globs?: unknown }
|
|
715
|
+
if (mode !== 'dependencyOutputs') continue
|
|
716
|
+
const strings = (v: unknown): string[] =>
|
|
717
|
+
Array.isArray(v) ? v.filter((g): g is string => typeof g === 'string') : []
|
|
718
|
+
return { from: Array.isArray(from) ? strings(from) : null, globs: strings(globs) }
|
|
719
|
+
}
|
|
720
|
+
return null
|
|
721
|
+
}
|
|
722
|
+
|
|
689
723
|
/**
|
|
690
724
|
* Turbo 2.11's structured inputs as the strings they stand for: a
|
|
691
725
|
* `startup` or `jit` entry is its globs (and every file with
|
|
@@ -1381,6 +1415,67 @@ export async function mapTurboWorkspace(
|
|
|
1381
1415
|
projects.push({ name: meta.name, dir: meta.dir, tasks })
|
|
1382
1416
|
}
|
|
1383
1417
|
|
|
1418
|
+
// Turbo 2.11's `dependencyOutputs` input hashes the named files after
|
|
1419
|
+
// their producer ran. A cached producer's key already reaches the reader
|
|
1420
|
+
// through `dependsOn`, its outputs following from it; an uncached one's
|
|
1421
|
+
// (vercel/vercel's `//#generate:cache-keys`, which records the host) do
|
|
1422
|
+
// not, and a test cached on one platform hit on another. The reader
|
|
1423
|
+
// keys those files by a probe at the workspace root.
|
|
1424
|
+
const metaNamed = new Map(metas.map((m) => [m.name, m]))
|
|
1425
|
+
for (const meta of metas) {
|
|
1426
|
+
const { defFor } = definitions(meta)
|
|
1427
|
+
for (const t of projects.find((p) => p.name === meta.name)?.tasks ?? []) {
|
|
1428
|
+
const def = defFor(t.name)
|
|
1429
|
+
const dep = def === undefined ? undefined : dependencyOutputs.get(def)
|
|
1430
|
+
const inputs = (t.task?.['cache'] as { inputs?: Record<string, unknown> } | undefined)?.inputs
|
|
1431
|
+
if (dep === undefined || inputs === undefined) continue
|
|
1432
|
+
const rels: string[] = []
|
|
1433
|
+
const unfold = new Set<string>()
|
|
1434
|
+
for (const sel of dep.from ?? def!.dependsOn ?? []) {
|
|
1435
|
+
if (envDependency(sel) !== null) continue
|
|
1436
|
+
const hash = sel.indexOf('#')
|
|
1437
|
+
const producers: [ProjectMeta | undefined, string][] = sel.startsWith('^')
|
|
1438
|
+
? metas.filter((m) => declares(meta, m.name)).map((m) => [m, sel.slice(1)])
|
|
1439
|
+
: hash === -1
|
|
1440
|
+
? [[meta, sel]]
|
|
1441
|
+
: [
|
|
1442
|
+
[
|
|
1443
|
+
sel.startsWith(`${ROOT}#`) ? rootMeta : metaNamed.get(sel.slice(0, hash)),
|
|
1444
|
+
sel.slice(hash + 1),
|
|
1445
|
+
],
|
|
1446
|
+
]
|
|
1447
|
+
for (const [pm, task] of producers) {
|
|
1448
|
+
if (pm === undefined) continue
|
|
1449
|
+
const pdef = definitions(pm).defFor(task)
|
|
1450
|
+
if (pdef?.cache !== false) continue
|
|
1451
|
+
if (!sel.startsWith('^')) unfold.add(`${pm.name}#${task}`)
|
|
1452
|
+
const at = relPosix(root, pm.dir)
|
|
1453
|
+
for (const g of dep.globs.length > 0 ? dep.globs : (pdef.outputs ?? []))
|
|
1454
|
+
if (!g.startsWith('!')) rels.push(at === '' ? g : `${at}/${g}`)
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (rels.length === 0) continue
|
|
1458
|
+
const probe = rels.every(isLiteral) ? ignoredFilesProbe(rels) : dotenvGlobsProbe(rels)
|
|
1459
|
+
if (probe === null) {
|
|
1460
|
+
delete t.task!['cache']
|
|
1461
|
+
t.todos.push(
|
|
1462
|
+
`inputs read ${rels.map((r) => JSON.stringify(r)).join(', ')} from an uncached ` +
|
|
1463
|
+
'dependency, a glob vx cannot key by a probe — task runs uncached; key it in a vx.config to cache it',
|
|
1464
|
+
)
|
|
1465
|
+
continue
|
|
1466
|
+
}
|
|
1467
|
+
const probes = (inputs['workspaceRuntime'] as unknown[] | undefined) ?? []
|
|
1468
|
+
inputs['workspaceRuntime'] = [...probes, probe]
|
|
1469
|
+
// Folded, the producer's key "covers" what it writes, and core answers
|
|
1470
|
+
// the probe before it runs: the previous run's bytes. Left out of the
|
|
1471
|
+
// fold, the probe waits for it (core's late probes, X-34).
|
|
1472
|
+
const deps = (t.task!['dependsOn'] as string[] | undefined) ?? []
|
|
1473
|
+
const id = (d: string) => (d.startsWith('^') || d.includes('#') ? d : `${meta.name}#${d}`)
|
|
1474
|
+
if (deps.some((d) => unfold.has(id(d))))
|
|
1475
|
+
inputs['tasks'] = deps.filter((d) => !unfold.has(id(d)))
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1384
1479
|
if (opts.ignored !== undefined) {
|
|
1385
1480
|
const ignored = await opts.ignored([
|
|
1386
1481
|
...new Set(literals.tasks.flatMap((l) => [...l.rels.keys()])),
|
package/src/workspace-notes.ts
CHANGED
|
@@ -24,7 +24,7 @@ export function lockfilePlugin(root: string): { file: string; factory: string }
|
|
|
24
24
|
return lock === undefined ? undefined : { file: lock[0], factory: lock[1] }
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
interface AdoptedTool {
|
|
28
28
|
/** The adoption plugin `vx init` declares: `turbo`, `nx`. */
|
|
29
29
|
readonly plugin: string
|
|
30
30
|
/** The file it reads every run: `turbo.json`, `nx.json`. */
|