@vzn/vx-migrate 0.0.646 → 0.0.649
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 +68 -61
- package/package.json +3 -3
- package/src/adopt.ts +53 -12
- package/src/bin.ts +21 -1
- package/src/glob-grammar.ts +82 -0
- package/src/migrate-nx.ts +7 -4
- package/src/migrate-scripts.ts +10 -102
- package/src/migrate.ts +53 -25
- package/src/nx/index.ts +19 -5
- package/src/nx/nx-inputs.ts +44 -9
- package/src/nx/nx-map.ts +36 -11
- package/src/nx/nx-outputs.ts +16 -6
- package/src/nx/nx-upstream.ts +10 -2
- 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 +8 -2
- package/src/tracked-outputs.ts +1 -1
- package/src/turbo/index.ts +18 -8
- package/src/turbo/turbo-map.ts +89 -15
- package/src/turbo-cache/index.ts +205 -89
- package/src/turbo-cache/oidc.ts +25 -14
- 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/script-command.ts
CHANGED
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
// the shims `yarn run` writes, so inlined, `node -e "require('left-pad')"`
|
|
21
21
|
// was MODULE_NOT_FOUND (2026-09-28). Every script runs as `yarn run <name>`.
|
|
22
22
|
|
|
23
|
-
import { readFileSync } from 'node:fs'
|
|
23
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
24
24
|
import path from 'node:path'
|
|
25
25
|
import { foldScriptHooks } from '@vzn/vx'
|
|
26
|
+
import { shellQuote } from './nx-command.js'
|
|
26
27
|
|
|
27
28
|
const YARN_RUN_BUILTIN = /(^|&&|\|\||;|\(|\|)\s*run\s/
|
|
28
29
|
const LIFECYCLE = /^(pre|post)(install|publish|pack|version)$|^(prepare|prepublishOnly|install)$/
|
|
@@ -36,15 +37,22 @@ export function scriptCommand(
|
|
|
36
37
|
body: string,
|
|
37
38
|
scripts: Readonly<Record<string, unknown>> = {},
|
|
38
39
|
pnp = false,
|
|
40
|
+
/** `runsScriptHooks`: false where the manager runs no `pre` / `post` hook. */
|
|
41
|
+
hooks = true,
|
|
39
42
|
): string {
|
|
40
|
-
if (pnp) return
|
|
43
|
+
if (pnp) return yarnRun(name)
|
|
41
44
|
const hook = (h: string): string | undefined =>
|
|
42
|
-
!LIFECYCLE.test(h) && usable(scripts[h]) ? scripts[h] : undefined
|
|
45
|
+
hooks && !LIFECYCLE.test(h) && usable(scripts[h]) ? scripts[h] : undefined
|
|
43
46
|
const parts = [hook(`pre${name}`), body, hook(`post${name}`)]
|
|
44
|
-
if (parts.some((p) => p !== undefined && YARN_RUN_BUILTIN.test(p))) return
|
|
47
|
+
if (parts.some((p) => p !== undefined && YARN_RUN_BUILTIN.test(p))) return yarnRun(name)
|
|
45
48
|
return foldScriptHooks(parts[0], body, parts[2])
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
/** `yarn run <name>`, the name one shell word: `a b` ran script `a` with an argument. */
|
|
52
|
+
export function yarnRun(name: string): string {
|
|
53
|
+
return `yarn run ${shellQuote(name)}`
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
/**
|
|
49
57
|
* Yarn ≥ 2 links Plug'n'Play unless `.yarnrc.yml` names another `nodeLinker`.
|
|
50
58
|
* Read once per mapping; synchronous so every mapper, the synchronous ones
|
|
@@ -67,3 +75,101 @@ export async function yarnrcText(root: string): Promise<string> {
|
|
|
67
75
|
.text()
|
|
68
76
|
.catch(() => '\0absent')
|
|
69
77
|
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The package manager that owns `dir`, when it runs `pre` / `post` hooks
|
|
81
|
+
* around a script; null when it runs none. The manager is the nearest `packageManager` field or
|
|
82
|
+
* lockfile, and its settings are read beside it, as each reads them for a
|
|
83
|
+
* member too. Probed with a `prebuild` / `build` / `postbuild` trio: Yarn
|
|
84
|
+
* 2+ runs none (D-31); npm runs none under `ignore-scripts=true` in
|
|
85
|
+
* `.npmrc`; pnpm runs none under `enable-pre-post-scripts=false` there or
|
|
86
|
+
* `enablePrePostScripts: false` in `pnpm-workspace.yaml` (D-33); Bun and
|
|
87
|
+
* Yarn 1 run them whatever those say. No manager found is npm's default.
|
|
88
|
+
*/
|
|
89
|
+
export function runsScriptHooks(dir: string, memo: Map<string, Owner>): string | null {
|
|
90
|
+
const { manager, at } = ownerOf(dir, memo)
|
|
91
|
+
const read = (f: string): string | undefined => {
|
|
92
|
+
try {
|
|
93
|
+
return readFileSync(path.join(at, f), 'utf8')
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const npmrc = read('.npmrc') ?? ''
|
|
99
|
+
const set = (key: string, value: string): boolean =>
|
|
100
|
+
new RegExp(`^\\s*${key}\\s*=\\s*${value}\\s*$`, 'm').test(npmrc)
|
|
101
|
+
const runs =
|
|
102
|
+
manager === 'berry'
|
|
103
|
+
? false
|
|
104
|
+
: manager === 'npm'
|
|
105
|
+
? !set('ignore-scripts', 'true')
|
|
106
|
+
: manager === 'pnpm'
|
|
107
|
+
? !set('enable-pre-post-scripts', 'false') && !pnpmSkipsHooks(read('pnpm-workspace.yaml'))
|
|
108
|
+
: true
|
|
109
|
+
return runs ? manager : null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* `enablePrePostScripts: false` in `pnpm-workspace.yaml`, read as pnpm reads
|
|
114
|
+
* it, as YAML: a line regex missed `false # hooks off` and a flow mapping,
|
|
115
|
+
* which pnpm 10 honours, and folded hooks pnpm does not run.
|
|
116
|
+
*/
|
|
117
|
+
function pnpmSkipsHooks(yaml: string | undefined): boolean {
|
|
118
|
+
if (yaml === undefined) return false
|
|
119
|
+
try {
|
|
120
|
+
const parsed = Bun.YAML.parse(yaml) as { enablePrePostScripts?: unknown } | null
|
|
121
|
+
return parsed?.enablePrePostScripts === false
|
|
122
|
+
} catch {
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The package manager that owns a directory, and the directory that says so. */
|
|
128
|
+
export interface Owner {
|
|
129
|
+
manager: string
|
|
130
|
+
at: string
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The nearest `packageManager` field or lockfile from `dir` up: `berry` for
|
|
135
|
+
* Yarn 2+, `yarn` for Yarn 1, else the manager's name; none found is npm.
|
|
136
|
+
*/
|
|
137
|
+
export function ownerOf(dir: string, memo: Map<string, Owner>): Owner {
|
|
138
|
+
const known = memo.get(dir)
|
|
139
|
+
if (known !== undefined) return known
|
|
140
|
+
const read = (f: string): string | undefined => {
|
|
141
|
+
try {
|
|
142
|
+
return readFileSync(path.join(dir, f), 'utf8')
|
|
143
|
+
} catch {
|
|
144
|
+
return undefined
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
let manager: string | undefined
|
|
148
|
+
try {
|
|
149
|
+
// A byte-order mark is stripped, as npm, pnpm and discovery strip it:
|
|
150
|
+
// JSON.parse threw on one and the field went unread.
|
|
151
|
+
const manifest = (read('package.json') ?? '').replace(/^\uFEFF/, '')
|
|
152
|
+
const pm = (JSON.parse(manifest) as { packageManager?: unknown }).packageManager
|
|
153
|
+
// A manager vx knows nothing of (zod's `nub@0.8.3`) says nothing about
|
|
154
|
+
// hooks: the lockfile beside it does, and "nub ran `postbuild`" was a
|
|
155
|
+
// claim nothing had checked (D-96).
|
|
156
|
+
if (typeof pm === 'string' && /^(npm|pnpm|yarn|bun)@/.test(pm))
|
|
157
|
+
manager = /^yarn@([2-9]|\d{2,})/.test(pm) ? 'berry' : pm.split('@')[0]
|
|
158
|
+
} catch {}
|
|
159
|
+
if (manager === undefined) {
|
|
160
|
+
const yarnLock = read('yarn.lock')
|
|
161
|
+
if (yarnLock !== undefined) manager = yarnLock.includes('\n__metadata:') ? 'berry' : 'yarn'
|
|
162
|
+
else if (existsSync(path.join(dir, 'pnpm-lock.yaml'))) manager = 'pnpm'
|
|
163
|
+
else if (existsSync(path.join(dir, 'package-lock.json'))) manager = 'npm'
|
|
164
|
+
else if (['bun.lock', 'bun.lockb'].some((f) => existsSync(path.join(dir, f)))) manager = 'bun'
|
|
165
|
+
}
|
|
166
|
+
const up = path.dirname(dir)
|
|
167
|
+
const owner =
|
|
168
|
+
manager !== undefined
|
|
169
|
+
? { manager, at: dir }
|
|
170
|
+
: up === dir
|
|
171
|
+
? { manager: 'npm', at: dir }
|
|
172
|
+
: ownerOf(up, memo)
|
|
173
|
+
memo.set(dir, owner)
|
|
174
|
+
return owner
|
|
175
|
+
}
|
package/src/shared-outputs.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
// file exists to prevent (item 445).
|
|
24
24
|
|
|
25
25
|
import { isLiteralPattern, outputsOverlap, type GeneratedTask } from '@vzn/vx'
|
|
26
|
+
import { withoutTakenBack } from './glob-grammar.js'
|
|
26
27
|
import { relPosix } from './paths.js'
|
|
27
28
|
import type { TrackedKinds } from './tracked-outputs.js'
|
|
28
29
|
|
|
@@ -612,7 +613,12 @@ export function excludeWorkspaceOutputs(
|
|
|
612
613
|
}
|
|
613
614
|
}
|
|
614
615
|
|
|
615
|
-
/**
|
|
616
|
+
/**
|
|
617
|
+
* A `!` output with no positive beside it takes back nothing, and core
|
|
618
|
+
* refuses a list of them alone; a literal one takes back was never saved,
|
|
619
|
+
* and core refuses it too (trpc's `package.json`, taken back).
|
|
620
|
+
*/
|
|
616
621
|
export function takingBack(globs: readonly string[]): string[] {
|
|
617
|
-
|
|
622
|
+
const kept = withoutTakenBack(globs)
|
|
623
|
+
return kept.some((g) => !g.startsWith('!')) ? kept : []
|
|
618
624
|
}
|
package/src/tracked-outputs.ts
CHANGED
|
@@ -222,7 +222,7 @@ interface Outputs {
|
|
|
222
222
|
/**
|
|
223
223
|
* Fit a mapped task's outputs to the committed files they cover, in place,
|
|
224
224
|
* and return one `[pkg#task, todo]` per task this leaves uncached. Core
|
|
225
|
-
* never cleans a committed file (
|
|
225
|
+
* never cleans a committed file (2026-10-14), so
|
|
226
226
|
* the outputs keep them; what is left is the readers' `!` that hides one.
|
|
227
227
|
* `tracked` is root-relative.
|
|
228
228
|
*/
|
package/src/turbo/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ import path from 'node:path'
|
|
|
14
14
|
import type { ProjectMeta, VxPlugin } from '@vzn/vx'
|
|
15
15
|
import type { PackageMeta } from '../packages.js'
|
|
16
16
|
import { memberProjects, withManifests } from '../manifests.js'
|
|
17
|
-
import { refuseUnknownOptions, type PluginOptionKinds } from '@vzn/vx'
|
|
17
|
+
import { refuseUnknownOptions, UserError, type PluginOptionKinds } from '@vzn/vx'
|
|
18
18
|
import { type AdoptionRun, adoptionPlugin } from '../adoption-plugin.js'
|
|
19
19
|
import type { AdoptionMapping } from '../mapping-cache.js'
|
|
20
20
|
import { collectGaps } from '../plugin-gaps.js'
|
|
@@ -38,7 +38,8 @@ const PERSISTENT_NOTE =
|
|
|
38
38
|
export interface TurboPluginOptions {
|
|
39
39
|
/**
|
|
40
40
|
* Where turbo.json lives. Defaults to the workspace root; a repo whose
|
|
41
|
-
* turbo.json sits elsewhere names the directory here
|
|
41
|
+
* turbo.json sits elsewhere names the directory here, relative to the workspace
|
|
42
|
+
* root or absolute.
|
|
42
43
|
*/
|
|
43
44
|
readonly root?: string
|
|
44
45
|
}
|
|
@@ -54,22 +55,27 @@ const TURBO_PLUGIN_KEYS: PluginOptionKinds<TurboPluginOptions> = {
|
|
|
54
55
|
*/
|
|
55
56
|
export function turbo(options: TurboPluginOptions = {}): VxPlugin {
|
|
56
57
|
refuseUnknownOptions('turbo()', options, TURBO_PLUGIN_KEYS)
|
|
58
|
+
if (options.root === '') throw new UserError('turbo() option "root" is a directory, not empty')
|
|
59
|
+
// Against the workspace root, as vx reads every path: against the cwd,
|
|
60
|
+
// `vx run` from a package dir read another directory.
|
|
61
|
+
const rootOf = (workspaceRoot: string): string =>
|
|
62
|
+
options.root === undefined ? workspaceRoot : path.resolve(workspaceRoot, options.root)
|
|
57
63
|
return adoptionPlugin(
|
|
58
64
|
import.meta,
|
|
59
|
-
(ctx) => run(
|
|
65
|
+
(ctx) => run(rootOf(ctx.workspaceRoot), ctx.projects),
|
|
60
66
|
// At the workspace root only: a claim is a root name (a `root` elsewhere
|
|
61
67
|
// is not claimed; its edits select as any unowned file does).
|
|
62
68
|
options.root === undefined ? ['turbo.json', 'turbo.jsonc'] : [],
|
|
63
69
|
{
|
|
64
70
|
async config(workspace, ctx) {
|
|
65
|
-
const keys = await workspaceKeys(
|
|
71
|
+
const keys = await workspaceKeys(rootOf(ctx.workspaceRoot))
|
|
66
72
|
if (workspace.run?.concurrency === undefined && keys.concurrency !== undefined)
|
|
67
73
|
workspace.run = { ...workspace.run, concurrency: keys.concurrency }
|
|
68
74
|
// `turbo run --affected` compares with TURBO_SCM_BASE when set, and
|
|
69
75
|
// on GitHub Actions with what the event names.
|
|
70
76
|
const base =
|
|
71
77
|
Bun.env['TURBO_SCM_BASE']?.trim() ||
|
|
72
|
-
githubActionsBase(
|
|
78
|
+
githubActionsBase(rootOf(ctx.workspaceRoot), keys.remoteBaseFallback === true)
|
|
73
79
|
if (base) ctx.setBranch({ base })
|
|
74
80
|
},
|
|
75
81
|
// Turbo's packages are the package manager's members, each a project
|
|
@@ -189,17 +195,19 @@ const textOf = (file: string): Promise<string> =>
|
|
|
189
195
|
/**
|
|
190
196
|
* Everything the mapping reads: the root's and each package's
|
|
191
197
|
* `turbo.json` / `turbo.jsonc`, every package manifest, the root's,
|
|
192
|
-
* `.yarnrc.yml` and the microfrontends configs.
|
|
198
|
+
* `.yarnrc.yml`, `.npmrc`, `pnpm-workspace.yaml` and the microfrontends configs.
|
|
193
199
|
*/
|
|
194
200
|
async function run(root: string, projects: readonly ProjectMeta[]): Promise<AdoptionRun> {
|
|
195
201
|
const dirs = [root, ...projects.map((m) => m.dir)]
|
|
196
|
-
const [metas, configs, yarnrc, rootManifest] = await Promise.all([
|
|
202
|
+
const [metas, configs, yarnrc, rootManifest, hookConfigs] = await Promise.all([
|
|
197
203
|
withManifests(projects),
|
|
198
204
|
Promise.all(
|
|
199
205
|
dirs.flatMap((d) => ['turbo.json', 'turbo.jsonc'].map((f) => textOf(path.join(d, f)))),
|
|
200
206
|
),
|
|
201
207
|
textOf(path.join(root, '.yarnrc.yml')),
|
|
202
208
|
textOf(path.join(root, 'package.json')),
|
|
209
|
+
// Whether the manager runs `pre` / `post` hooks, so whether they fold.
|
|
210
|
+
Promise.all(['.npmrc', 'pnpm-workspace.yaml'].map((f) => textOf(path.join(root, f)))),
|
|
203
211
|
])
|
|
204
212
|
const envNames = envNamesThatMap(configs, envNamesNow())
|
|
205
213
|
// The tsconfigs a `tsc` script reads are the mapping's inputs too.
|
|
@@ -218,7 +226,9 @@ async function run(root: string, projects: readonly ProjectMeta[]): Promise<Adop
|
|
|
218
226
|
process.env['TURBO_ENV_MODE'] ?? '',
|
|
219
227
|
...configs,
|
|
220
228
|
yarnrc,
|
|
221
|
-
//
|
|
229
|
+
// Whether pnpm or npm runs a script's `pre` / `post` hooks (`runsScriptHooks`).
|
|
230
|
+
...hookConfigs,
|
|
231
|
+
// Its dependencies are global inputs, a member or not; its `packageManager` names who runs hooks.
|
|
222
232
|
rootManifest,
|
|
223
233
|
JSON.stringify(microfrontendsConfigs(root, dirs)),
|
|
224
234
|
// A config file added beside mapped tasks changes what an output may cover.
|
package/src/turbo/turbo-map.ts
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
import path from 'node:path'
|
|
14
14
|
import { isLiteralPattern, pruneOrphanPersistentNotes, UserError } from '@vzn/vx'
|
|
15
15
|
import type { PackageMeta } from '../packages.js'
|
|
16
|
-
import {
|
|
16
|
+
import { refuseTaskNames } from '../plugin-gaps.js'
|
|
17
|
+
import { apartTakeBacks, literalGlob, minimatchToVx, withoutTakenBack } from '../glob-grammar.js'
|
|
17
18
|
import { shellQuote } from '../nx-command.js'
|
|
18
|
-
import { scriptCommand, yarnPnp } from '../script-command.js'
|
|
19
|
+
import { type Owner, runsScriptHooks, scriptCommand, yarnPnp } from '../script-command.js'
|
|
19
20
|
import {
|
|
20
21
|
ownFileTakeBacks,
|
|
21
22
|
excludeSiblingOutputs,
|
|
@@ -299,6 +300,14 @@ function covers(globs: readonly string[], rel: string): boolean {
|
|
|
299
300
|
)
|
|
300
301
|
}
|
|
301
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Turbo reads `**` glued to text as the deep glob: `**x` as `**\/*x`, `x**`
|
|
305
|
+
* as `x*\/**` (vercel/turborepo#5431). Core refuses the glued spelling, so
|
|
306
|
+
* copied verbatim it failed the load.
|
|
307
|
+
*/
|
|
308
|
+
const turboGlobstar = (glob: string): string =>
|
|
309
|
+
glob.replace(/\*\*([^*/]+)/g, '**/*$1').replace(/(?!^!)([^*/]+)\*\*/g, '$1*/**')
|
|
310
|
+
|
|
302
311
|
/** No glob syntax: one path, which git may ignore. */
|
|
303
312
|
const isLiteral = (glob: string): boolean => !/[*?[\]{}()!]/.test(glob)
|
|
304
313
|
|
|
@@ -768,6 +777,7 @@ export async function mapTurboWorkspace(
|
|
|
768
777
|
}
|
|
769
778
|
const rootCfg = withGlobal(await readTurboJson(rootFile, root))
|
|
770
779
|
const pnp = yarnPnp(root)
|
|
780
|
+
const hookMemo = new Map<string, Owner>()
|
|
771
781
|
const rootTasks = tasksOf(rootCfg)
|
|
772
782
|
|
|
773
783
|
// Turbo 1 lists an env var as `$NAME` among `globalDependencies` (and a
|
|
@@ -820,7 +830,7 @@ export async function mapTurboWorkspace(
|
|
|
820
830
|
// 1032).
|
|
821
831
|
const rootMeta = metas.find((m) => path.resolve(m.dir) === path.resolve(root))
|
|
822
832
|
const globalFiles = [
|
|
823
|
-
...globalDeps.filter((d) => envDependency(d) === null),
|
|
833
|
+
...globalDeps.filter((d) => envDependency(d) === null).map(turboGlobstar),
|
|
824
834
|
...(rootCfg.globalDotEnv ?? []),
|
|
825
835
|
// Turbo 2 added the root's dependencies to its global hash; 1.13.4's
|
|
826
836
|
// leaves them out (trigger.dev: a Prisma migration re-keyed only the
|
|
@@ -1165,7 +1175,10 @@ export async function mapTurboWorkspace(
|
|
|
1165
1175
|
}
|
|
1166
1176
|
|
|
1167
1177
|
const projects: TurboMappedProject[] = []
|
|
1168
|
-
|
|
1178
|
+
// Turbo 2.11's `global.inputs` joins each task's own inputs, where its
|
|
1179
|
+
// `!` entries reach them; `globalDependencies` is hashed apart.
|
|
1180
|
+
const globalPrepended = new Set(rootCfg.global?.inputs ?? [])
|
|
1181
|
+
const literals = { tasks: [] as Literals[], globalIgnored, globalPrepended }
|
|
1169
1182
|
for (const meta of metas) {
|
|
1170
1183
|
const scripts = packageScripts(meta)
|
|
1171
1184
|
const own = runnable.get(meta.name)!
|
|
@@ -1277,7 +1290,16 @@ export async function mapTurboWorkspace(
|
|
|
1277
1290
|
}
|
|
1278
1291
|
continue
|
|
1279
1292
|
}
|
|
1280
|
-
|
|
1293
|
+
// Turbo runs `<pm> run <name>`: the hooks fold only where the manager runs them.
|
|
1294
|
+
const command =
|
|
1295
|
+
override ??
|
|
1296
|
+
scriptCommand(
|
|
1297
|
+
name,
|
|
1298
|
+
script as string,
|
|
1299
|
+
scripts,
|
|
1300
|
+
pnp,
|
|
1301
|
+
runsScriptHooks(meta.dir, hookMemo) !== null,
|
|
1302
|
+
)
|
|
1281
1303
|
const mapped = buildTask(
|
|
1282
1304
|
literals,
|
|
1283
1305
|
name,
|
|
@@ -1311,6 +1333,35 @@ export async function mapTurboWorkspace(
|
|
|
1311
1333
|
// not, and a test cached on one platform hit on another. The reader
|
|
1312
1334
|
// keys those files by a probe at the workspace root.
|
|
1313
1335
|
const metaNamed = new Map(metas.map((m) => [m.name, m]))
|
|
1336
|
+
|
|
1337
|
+
// An entry group's `//#task` edge is the root's, and the root has no
|
|
1338
|
+
// group of that name (it runs only `//#` tasks): left to `entryEdges`,
|
|
1339
|
+
// `deploy → //#format` ran no format, where `turbo run deploy` runs it.
|
|
1340
|
+
// The root gets the group, edges to its own tasks only, unless it
|
|
1341
|
+
// declares the name itself.
|
|
1342
|
+
const rootProject =
|
|
1343
|
+
rootMeta === undefined || singlePackage
|
|
1344
|
+
? undefined
|
|
1345
|
+
: projects.find((p) => p.name === rootMeta.name)
|
|
1346
|
+
if (rootProject !== undefined) {
|
|
1347
|
+
const rootRuns = emitted.get(rootProject.name)!
|
|
1348
|
+
const groups = new Map<string, string[]>()
|
|
1349
|
+
for (const id of entry) {
|
|
1350
|
+
const at = id.indexOf('#')
|
|
1351
|
+
const pkg = metaNamed.get(id.slice(0, at))
|
|
1352
|
+
const name = id.slice(at + 1)
|
|
1353
|
+
if (pkg === undefined || pkg === rootMeta || rootRuns.has(name)) continue
|
|
1354
|
+
for (const d of definitions(pkg).defFor(name)?.dependsOn ?? []) {
|
|
1355
|
+
const task = d.slice(ROOT.length + 1)
|
|
1356
|
+
if (!d.startsWith(`${ROOT}#`) || !rootRuns.has(task)) continue
|
|
1357
|
+
const edges = groups.get(name) ?? []
|
|
1358
|
+
if (!edges.includes(task)) edges.push(task)
|
|
1359
|
+
groups.set(name, edges)
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
for (const [name, dependsOn] of groups)
|
|
1363
|
+
rootProject.tasks.push({ name, todos: [], task: { dependsOn }, uses: new Set() })
|
|
1364
|
+
}
|
|
1314
1365
|
for (const meta of metas) {
|
|
1315
1366
|
const { defFor } = definitions(meta)
|
|
1316
1367
|
for (const t of projects.find((p) => p.name === meta.name)?.tasks ?? []) {
|
|
@@ -1394,6 +1445,7 @@ export async function mapTurboWorkspace(
|
|
|
1394
1445
|
excludeWorkspaceOutputs(root, projects, (e) => opaque.get(JSON.stringify(e)) ?? [])
|
|
1395
1446
|
pruneUnreachedPersistentNotes(projects, metas, opts.persistentTodo)
|
|
1396
1447
|
pruneOrphanPersistentNotes(projects, opts.persistentTodo)
|
|
1448
|
+
refuseTaskNames(projects, 'task')
|
|
1397
1449
|
return { projects, notes, globals }
|
|
1398
1450
|
}
|
|
1399
1451
|
|
|
@@ -1620,7 +1672,11 @@ function keyOnlyGroup(t: TurboMappedTask): TurboMappedTask {
|
|
|
1620
1672
|
}
|
|
1621
1673
|
|
|
1622
1674
|
function buildTask(
|
|
1623
|
-
literals: {
|
|
1675
|
+
literals: {
|
|
1676
|
+
readonly tasks: Literals[]
|
|
1677
|
+
readonly globalIgnored: readonly string[]
|
|
1678
|
+
readonly globalPrepended: ReadonlySet<string>
|
|
1679
|
+
},
|
|
1624
1680
|
name: string,
|
|
1625
1681
|
def: TurboTask,
|
|
1626
1682
|
command: string,
|
|
@@ -1836,9 +1892,9 @@ function buildTask(
|
|
|
1836
1892
|
let pkgDotenvDeep = false
|
|
1837
1893
|
// The root-relative `.env` globs, probed at the workspace root.
|
|
1838
1894
|
const wsDotenv = [...rootDotenv]
|
|
1839
|
-
//
|
|
1840
|
-
//
|
|
1841
|
-
const wsFiles:
|
|
1895
|
+
// The task's own root-relative inputs; globalDependencies, root-relative
|
|
1896
|
+
// by definition, join them in inputs.workspaceFiles.
|
|
1897
|
+
const wsFiles: string[] = []
|
|
1842
1898
|
// Root-relative path → the entry naming it.
|
|
1843
1899
|
const rels = new Map<string, Named>(literals.globalIgnored.map((f) => [f, [null, f]]))
|
|
1844
1900
|
const atRoot = pkgDir === '' || pkgDir === '.'
|
|
@@ -1848,7 +1904,7 @@ function buildTask(
|
|
|
1848
1904
|
// in the package and an edit replayed a stale build (item 936).
|
|
1849
1905
|
files.push('**/*')
|
|
1850
1906
|
} else {
|
|
1851
|
-
for (const i of def.inputs) {
|
|
1907
|
+
for (const i of def.inputs.map(turboGlobstar)) {
|
|
1852
1908
|
if (i === '$TURBO_DEFAULT$') {
|
|
1853
1909
|
files.push('**/*')
|
|
1854
1910
|
continue
|
|
@@ -1917,9 +1973,27 @@ function buildTask(
|
|
|
1917
1973
|
// project's own globs at every member (D-39), so as `files` a root
|
|
1918
1974
|
// `oxlint` over the repo replayed green after a member file broke.
|
|
1919
1975
|
// A root task's globs are the workspace's, outputs too.
|
|
1920
|
-
|
|
1976
|
+
const head = atRoot ? files.splice(0) : []
|
|
1977
|
+
const joined = (g: string) => literals.globalPrepended.has(g)
|
|
1978
|
+
const [apartKept, ownKept] = apartTakeBacks<unknown>(
|
|
1979
|
+
globals.inputs.filter((g) => !joined(g)),
|
|
1980
|
+
[...globals.inputs.filter(joined), ...head, ...wsFiles],
|
|
1981
|
+
)
|
|
1982
|
+
const kept = new Set([...apartKept, ...ownKept])
|
|
1983
|
+
const globalKept = globals.inputs.filter((g) => kept.has(g))
|
|
1984
|
+
// A global exclusion dropped for this task cannot leave an opaque
|
|
1985
|
+
// preset spread: the task lists the global values itself.
|
|
1986
|
+
const inline = globalKept.length !== globals.inputs.length
|
|
1987
|
+
const allWs = uniq(
|
|
1988
|
+
[
|
|
1989
|
+
...head.filter((e) => typeof e !== 'string' || kept.has(e)),
|
|
1990
|
+
...(inline ? globalKept : global('inputs')),
|
|
1991
|
+
...wsFiles.filter((e) => kept.has(e)),
|
|
1992
|
+
],
|
|
1993
|
+
inline ? [] : hidden('inputs'),
|
|
1994
|
+
)
|
|
1921
1995
|
const inputs: Record<string, unknown> = { files: withoutTakenBack(files) }
|
|
1922
|
-
if (
|
|
1996
|
+
if (allWs.length > 0) inputs.workspaceFiles = withoutTakenBack(allWs)
|
|
1923
1997
|
if (keyEnv.length > 0) inputs.env = keyEnv
|
|
1924
1998
|
if (pkgDotenv) inputs.runtime = [pkgDotenvDeep ? DOTENV_PROBE : DOTENV_PROBE_TOP]
|
|
1925
1999
|
if (wsDotenv.length > 0) inputs.workspaceRuntime = [dotenvGlobsProbe(wsDotenv) ?? DOTENV_PROBE]
|
|
@@ -1936,7 +2010,7 @@ function buildTask(
|
|
|
1936
2010
|
for (const raw of def.outputs ?? []) {
|
|
1937
2011
|
// Turbo's grammar: an extglob in `dist/**/*.@(js|mjs)` matched
|
|
1938
2012
|
// nothing, so a hit restored nothing (item 1031).
|
|
1939
|
-
const mapped = minimatchToVx(raw, raw.startsWith('!'))
|
|
2013
|
+
const mapped = minimatchToVx(turboGlobstar(raw), raw.startsWith('!'))
|
|
1940
2014
|
if (mapped === null) {
|
|
1941
2015
|
todos.push(
|
|
1942
2016
|
`output ${JSON.stringify(raw)}: glob syntax vx cannot take — task runs uncached; ` +
|
|
@@ -1970,7 +2044,7 @@ function buildTask(
|
|
|
1970
2044
|
}
|
|
1971
2045
|
|
|
1972
2046
|
// With git's file list known, a wildcard-first output stays cached as
|
|
1973
|
-
// nx() keeps it (
|
|
2047
|
+
// nx() keeps it (2026-10-03; turbo() 2026-10-14): core never
|
|
1974
2048
|
// cleans a committed file. Turbo never cleans, so the untracked files
|
|
1975
2049
|
// it reaches are named. An X added for `X/**` is judged with it.
|
|
1976
2050
|
const wild = wildcardOutput(
|
|
@@ -1991,7 +2065,7 @@ function buildTask(
|
|
|
1991
2065
|
if (!covers(outFiles, rel)) outFiles.push(literalGlob(rel))
|
|
1992
2066
|
// An output over the project's own package.json (trpc's builds list
|
|
1993
2067
|
// it: tsdown rewrites `exports`) is taken back and keeps the cache, as
|
|
1994
|
-
// nx() does (
|
|
2068
|
+
// nx() does (2026-10-03). The build rewrites an input the key
|
|
1995
2069
|
// folds, so its identical rewrite counts as unchanged ('mutable').
|
|
1996
2070
|
const takeBacks = ownFileTakeBacks(outFiles, opts.ownConfig?.(pkgDir))
|
|
1997
2071
|
outFiles.push(...takeBacks)
|