@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/index.ts
CHANGED
|
@@ -14,21 +14,21 @@ 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'
|
|
21
21
|
import { gitIgnored, trackedKinds } from '../tracked-outputs.js'
|
|
22
22
|
import { packageScripts, relPosix } from '../paths.js'
|
|
23
23
|
import { onceReader, type ReadText, tscBuildInfo } from '../tsc-buildinfo.js'
|
|
24
|
+
import type * as TurboMap from './turbo-map.js'
|
|
25
|
+
import type { TurboMappedProject } from './turbo-map.js'
|
|
24
26
|
import {
|
|
25
27
|
envNamesThatMap,
|
|
26
|
-
mapTurboWorkspace,
|
|
27
28
|
microfrontendsConfigs,
|
|
28
29
|
rootTaskProject,
|
|
29
30
|
turboConfigFile,
|
|
30
|
-
|
|
31
|
-
} from './turbo-map.js'
|
|
31
|
+
} from './turbo-reads.js'
|
|
32
32
|
|
|
33
33
|
/** The note every persistent task carries; like every gap, reported once per run for all its tasks. */
|
|
34
34
|
const PERSISTENT_NOTE =
|
|
@@ -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,14 +195,20 @@ 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
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
201
|
+
const dirs = [root, ...projects.map((m) => m.dir)]
|
|
202
|
+
const [metas, configs, yarnrc, rootManifest, hookConfigs] = await Promise.all([
|
|
203
|
+
withManifests(projects),
|
|
204
|
+
Promise.all(
|
|
205
|
+
dirs.flatMap((d) => ['turbo.json', 'turbo.jsonc'].map((f) => textOf(path.join(d, f)))),
|
|
206
|
+
),
|
|
207
|
+
textOf(path.join(root, '.yarnrc.yml')),
|
|
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)))),
|
|
211
|
+
])
|
|
200
212
|
const envNames = envNamesThatMap(configs, envNamesNow())
|
|
201
213
|
// The tsconfigs a `tsc` script reads are the mapping's inputs too.
|
|
202
214
|
const tsc = onceReader()
|
|
@@ -213,9 +225,11 @@ async function run(root: string, projects: readonly ProjectMeta[]): Promise<Adop
|
|
|
213
225
|
process.env['TURBO_CI_VENDOR_ENV_KEY'] ?? '',
|
|
214
226
|
process.env['TURBO_ENV_MODE'] ?? '',
|
|
215
227
|
...configs,
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
|
|
228
|
+
yarnrc,
|
|
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.
|
|
232
|
+
rootManifest,
|
|
219
233
|
JSON.stringify(microfrontendsConfigs(root, dirs)),
|
|
220
234
|
// A config file added beside mapped tasks changes what an output may cover.
|
|
221
235
|
JSON.stringify(metas.map((m) => [m.name, m.dir, m.packageJson, m.configPath])),
|
|
@@ -244,6 +258,8 @@ async function mapAll(
|
|
|
244
258
|
m.configPath === null ? [] : [[relPosix(root, m.dir), path.basename(m.configPath)] as const],
|
|
245
259
|
),
|
|
246
260
|
)
|
|
261
|
+
// Only a mapping the kept one no longer holds loads the mapper.
|
|
262
|
+
const { mapTurboWorkspace } = require('./turbo-map.js') as typeof TurboMap
|
|
247
263
|
const mapped = await mapTurboWorkspace(root, metas, {
|
|
248
264
|
// Inline: the values themselves, where `bunx @vzn/vx-migrate` splices a preset import.
|
|
249
265
|
splice: (_kind, values) => values,
|
package/src/turbo/turbo-map.ts
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
// written.
|
|
11
11
|
// The consumer decides what a global becomes through `splice`.
|
|
12
12
|
|
|
13
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
14
13
|
import path from 'node:path'
|
|
15
14
|
import { isLiteralPattern, pruneOrphanPersistentNotes, UserError } from '@vzn/vx'
|
|
16
15
|
import type { PackageMeta } from '../packages.js'
|
|
17
|
-
import {
|
|
16
|
+
import { refuseTaskNames } from '../plugin-gaps.js'
|
|
17
|
+
import { apartTakeBacks, literalGlob, minimatchToVx, withoutTakenBack } from '../glob-grammar.js'
|
|
18
18
|
import { shellQuote } from '../nx-command.js'
|
|
19
|
-
import { scriptCommand, yarnPnp } from '../script-command.js'
|
|
19
|
+
import { type Owner, runsScriptHooks, scriptCommand, yarnPnp } from '../script-command.js'
|
|
20
20
|
import {
|
|
21
21
|
ownFileTakeBacks,
|
|
22
22
|
excludeSiblingOutputs,
|
|
@@ -31,6 +31,13 @@ import {
|
|
|
31
31
|
} from '../shared-outputs.js'
|
|
32
32
|
import { manifestGraph, type ManifestGraph } from '../manifest-graph.js'
|
|
33
33
|
import { isRecord, packageScripts, relPosix } from '../paths.js'
|
|
34
|
+
import {
|
|
35
|
+
FRAMEWORK_ENV,
|
|
36
|
+
keyable,
|
|
37
|
+
microfrontendsConfigs,
|
|
38
|
+
ROOT,
|
|
39
|
+
turboConfigFile,
|
|
40
|
+
} from './turbo-reads.js'
|
|
34
41
|
import type { TrackedKinds } from '../tracked-outputs.js'
|
|
35
42
|
import {
|
|
36
43
|
DOTENV_PROBE,
|
|
@@ -69,34 +76,6 @@ interface TurboJson {
|
|
|
69
76
|
|
|
70
77
|
const SHELL_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
71
78
|
|
|
72
|
-
/** A name core takes in `inputs.env` and `exec.env.passThrough`. */
|
|
73
|
-
const keyable = (name: string): boolean =>
|
|
74
|
-
name.length > 0 && !name.startsWith('!') && !/[*?[\]{}=\0]/.test(name)
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* The names of `names` a live mapping can read differently: each a `*` in
|
|
78
|
-
* one of `configs` (turbo.json texts, any string, a superset) or in Turbo's
|
|
79
|
-
* framework table matches, and each core cannot key (a literal one is
|
|
80
|
-
* dropped only when no variable has it). The kept mapping keyed on every
|
|
81
|
-
* name, so any variable that came or went (a CI step's, `VX_DEBUG_TIMING`)
|
|
82
|
-
* remapped the workspace: ~330 ms a run on vercel/ai.
|
|
83
|
-
*/
|
|
84
|
-
export function envNamesThatMap(configs: readonly string[], names: readonly string[]): string[] {
|
|
85
|
-
const patterns = new Set(FRAMEWORK_ENV.flatMap((f) => f.env).filter((e) => e.includes('*')))
|
|
86
|
-
for (const text of configs)
|
|
87
|
-
for (const m of text.matchAll(/"((?:[^"\\]|\\.)*)"/g))
|
|
88
|
-
if (m[1]!.includes('*')) patterns.add(m[1]!)
|
|
89
|
-
// Backslashes and a leading `!` dropped, every `*` a wildcard: wider
|
|
90
|
-
// than Turbo's reading, never narrower.
|
|
91
|
-
const res = [...patterns].map(
|
|
92
|
-
(p) =>
|
|
93
|
-
new RegExp(
|
|
94
|
-
`^${p.replaceAll('\\', '').replace(/^!/, '').split('*').map(RegExp.escape).join('.*')}$`,
|
|
95
|
-
),
|
|
96
|
-
)
|
|
97
|
-
return names.filter((n) => !keyable(n) || res.some((re) => re.test(n)))
|
|
98
|
-
}
|
|
99
|
-
|
|
100
79
|
/**
|
|
101
80
|
* A Turbo env list's explicit names, read as Turbo reads them
|
|
102
81
|
* (`wildcard_to_regex_pattern`): `*` is the one wildcard, `\*` a
|
|
@@ -195,83 +174,6 @@ const KNOWN_TASK_KEYS = new Set([
|
|
|
195
174
|
const OUTPUT_LOGS_DEFAULT = 'new-only'
|
|
196
175
|
const OUTPUT_LOGS_RUN_FLAG = new Set(['full', 'hash-only', 'errors-only', 'none'])
|
|
197
176
|
|
|
198
|
-
// Turbo 2's framework inference: a package that depends on a framework
|
|
199
|
-
// has its variables hashed into its tasks and passed to them, with
|
|
200
|
-
// nothing in turbo.json saying so. vx env names are explicit, so the
|
|
201
|
-
// variables were stripped in silence and a build that inlines them
|
|
202
|
-
// (Next's `NEXT_PUBLIC_*`) read empty values (item 940). A live mapping
|
|
203
|
-
// (`envNames`) infers them as Turbo does; the migrate CLI lists the names
|
|
204
|
-
// the package's own files spell (`sourceNames`) and says so in a note.
|
|
205
|
-
// Turbo's own table (`packages/turbo-types/src/json/frameworks.json`),
|
|
206
|
-
// in its order: a package takes the FIRST framework it matches, `all`
|
|
207
|
-
// needing every dependency and `some` any one.
|
|
208
|
-
const NITRO_ENV = [
|
|
209
|
-
'NITRO_*',
|
|
210
|
-
'SERVER_*',
|
|
211
|
-
'AWS_APP_ID',
|
|
212
|
-
'INPUT_AZURE_STATIC_WEB_APPS_API_TOKEN',
|
|
213
|
-
'CLEAVR',
|
|
214
|
-
'CF_PAGES',
|
|
215
|
-
'FIREBASE_APP_HOSTING',
|
|
216
|
-
'NETLIFY',
|
|
217
|
-
'STORMKIT',
|
|
218
|
-
'NOW_BUILDER',
|
|
219
|
-
'ZEABUR',
|
|
220
|
-
'RENDER',
|
|
221
|
-
]
|
|
222
|
-
const FRAMEWORK_ENV: ReadonlyArray<{
|
|
223
|
-
slug: string
|
|
224
|
-
env: readonly string[]
|
|
225
|
-
all: boolean
|
|
226
|
-
dependencies: readonly string[]
|
|
227
|
-
}> = [
|
|
228
|
-
{ slug: 'astro', env: ['PUBLIC_*'], all: true, dependencies: ['astro'] },
|
|
229
|
-
{ slug: 'blitzjs', env: ['NEXT_PUBLIC_*'], all: true, dependencies: ['blitz'] },
|
|
230
|
-
{
|
|
231
|
-
slug: 'create-react-app',
|
|
232
|
-
env: ['REACT_APP_*'],
|
|
233
|
-
all: false,
|
|
234
|
-
dependencies: ['react-scripts', 'react-dev-utils'],
|
|
235
|
-
},
|
|
236
|
-
{ slug: 'expo', env: ['EXPO_PUBLIC_*'], all: true, dependencies: ['expo'] },
|
|
237
|
-
{ slug: 'gatsby', env: ['GATSBY_*'], all: true, dependencies: ['gatsby'] },
|
|
238
|
-
{
|
|
239
|
-
slug: 'nextjs',
|
|
240
|
-
env: ['NEXT_PUBLIC_*', 'NEXT_DEPLOYMENT_ID'],
|
|
241
|
-
all: true,
|
|
242
|
-
dependencies: ['next'],
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
slug: 'nitro',
|
|
246
|
-
env: NITRO_ENV,
|
|
247
|
-
all: false,
|
|
248
|
-
dependencies: ['nitropack', 'nitropack-nightly', 'nitro', 'nitro-nightly'],
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
slug: 'nuxtjs',
|
|
252
|
-
env: ['NUXT_*', 'NUXT_ENV_*', ...NITRO_ENV, 'LAUNCH_EDITOR'],
|
|
253
|
-
all: false,
|
|
254
|
-
dependencies: ['nuxt', 'nuxt-edge', 'nuxt3', 'nuxt3-edge'],
|
|
255
|
-
},
|
|
256
|
-
{ slug: 'redwoodjs', env: ['REDWOOD_ENV_*'], all: true, dependencies: ['@redwoodjs/core'] },
|
|
257
|
-
{
|
|
258
|
-
slug: 'remix',
|
|
259
|
-
env: ['REMIX_*'],
|
|
260
|
-
all: false,
|
|
261
|
-
dependencies: ['@remix-run/dev', '@remix-run/react', '@remix-run/serve', '@react-router/dev'],
|
|
262
|
-
},
|
|
263
|
-
{ slug: 'sanity', env: ['SANITY_STUDIO_*'], all: true, dependencies: ['@sanity/cli'] },
|
|
264
|
-
{ slug: 'solidstart', env: ['VITE_*'], all: true, dependencies: ['solid-js', 'solid-start'] },
|
|
265
|
-
{ slug: 'sveltekit', env: ['VITE_*', 'PUBLIC_*'], all: true, dependencies: ['@sveltejs/kit'] },
|
|
266
|
-
{ slug: 'vite', env: ['VITE_*'], all: true, dependencies: ['vite'] },
|
|
267
|
-
{
|
|
268
|
-
slug: 'vue',
|
|
269
|
-
env: ['VUE_APP_*', 'LAUNCH_EDITOR'],
|
|
270
|
-
all: true,
|
|
271
|
-
dependencies: ['@vue/cli-service'],
|
|
272
|
-
},
|
|
273
|
-
]
|
|
274
|
-
|
|
275
177
|
/** The three global fields of turbo.json a task may draw on. */
|
|
276
178
|
export type TurboGlobal = 'inputs' | 'env' | 'pass'
|
|
277
179
|
|
|
@@ -398,6 +300,14 @@ function covers(globs: readonly string[], rel: string): boolean {
|
|
|
398
300
|
)
|
|
399
301
|
}
|
|
400
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
|
+
|
|
401
311
|
/** No glob syntax: one path, which git may ignore. */
|
|
402
312
|
const isLiteral = (glob: string): boolean => !/[*?[\]{}()!]/.test(glob)
|
|
403
313
|
|
|
@@ -424,45 +334,6 @@ function keyIgnored(
|
|
|
424
334
|
inputs.workspaceRuntime = [...probes, ignoredFilesProbe(hits.map(([rel]) => rel))]
|
|
425
335
|
}
|
|
426
336
|
|
|
427
|
-
/**
|
|
428
|
-
* A directory's Turbo config: `turbo.json`, else `turbo.jsonc` (Turbo 2.5+
|
|
429
|
-
* reads either). Only `turbo.json` was looked for, so a `turbo.jsonc` root
|
|
430
|
-
* failed the run with ENOENT and a package's `turbo.jsonc` overlay was
|
|
431
|
-
* skipped in silence — its `inputs` then keyed nothing (item 938).
|
|
432
|
-
*/
|
|
433
|
-
export async function turboConfigFile(dir: string): Promise<string | null> {
|
|
434
|
-
for (const name of ['turbo.json', 'turbo.jsonc']) {
|
|
435
|
-
const file = path.join(dir, name)
|
|
436
|
-
if (await Bun.file(file).exists()) return file
|
|
437
|
-
}
|
|
438
|
-
return null
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* The workspace root as a project to name when turbo.json declares `//#`
|
|
443
|
-
* tasks: its package name and directory. Null with no such task, a
|
|
444
|
-
* nameless root, or a file the mapping refuses (it names the file).
|
|
445
|
-
*/
|
|
446
|
-
export async function rootTaskProject(root: string): Promise<{ name: string; dir: string } | null> {
|
|
447
|
-
const file = await turboConfigFile(root)
|
|
448
|
-
if (file === null) return null
|
|
449
|
-
let turbo: { tasks?: unknown; pipeline?: unknown } | null
|
|
450
|
-
let pkg: { name?: unknown } | null
|
|
451
|
-
try {
|
|
452
|
-
turbo = Bun.JSONC.parse(await Bun.file(file).text()) as typeof turbo
|
|
453
|
-
pkg = (await Bun.file(path.join(root, 'package.json')).json()) as typeof pkg
|
|
454
|
-
} catch {
|
|
455
|
-
return null
|
|
456
|
-
}
|
|
457
|
-
const tasks = turbo?.tasks ?? turbo?.pipeline
|
|
458
|
-
const hasRootTask =
|
|
459
|
-
typeof tasks === 'object' &&
|
|
460
|
-
tasks !== null &&
|
|
461
|
-
Object.keys(tasks).some((k) => k.startsWith(`${ROOT}#`))
|
|
462
|
-
if (!hasRootTask || typeof pkg?.name !== 'string' || pkg.name === '') return null
|
|
463
|
-
return { name: pkg.name, dir: root }
|
|
464
|
-
}
|
|
465
|
-
|
|
466
337
|
async function readTurboJson(file: string, root: string): Promise<TurboJson> {
|
|
467
338
|
const text = await Bun.file(file).text()
|
|
468
339
|
let parsed: unknown
|
|
@@ -606,7 +477,6 @@ function badCommand(def: TurboTask): boolean {
|
|
|
606
477
|
|
|
607
478
|
/** Declared task names for a package: plain root keys, `pkg#name` keys
|
|
608
479
|
* for this package, and per-package turbo.json keys — in that order. */
|
|
609
|
-
const ROOT = '//'
|
|
610
480
|
|
|
611
481
|
/**
|
|
612
482
|
* A package's turbo.json files, root first and its own last, as Turbo's
|
|
@@ -892,36 +762,6 @@ async function rootPackageJson(root: string): Promise<PackageMeta['packageJson']
|
|
|
892
762
|
return (pkg !== null && typeof pkg === 'object' ? pkg : {}) as PackageMeta['packageJson']
|
|
893
763
|
}
|
|
894
764
|
|
|
895
|
-
/**
|
|
896
|
-
* The microfrontends configs Turbo hashes into every task: each package's
|
|
897
|
-
* (the root's too) `microfrontends.json`, else `microfrontends.jsonc`, or
|
|
898
|
-
* the one name `VC_MICROFRONTENDS_CONFIG_FILE_NAME` gives, unless it is a
|
|
899
|
-
* child's `{ "partOf": … }` (Turbo's `update_turbo_json` appends them to
|
|
900
|
-
* the root's global deps). Unread, an edit to `web`'s routes replayed every
|
|
901
|
-
* sibling app's build (with-microfrontends, Turbo 2.11's dry run moves all
|
|
902
|
-
* 18 tasks). Paths are root-relative; `text` is what the mapping read.
|
|
903
|
-
*/
|
|
904
|
-
export function microfrontendsConfigs(
|
|
905
|
-
root: string,
|
|
906
|
-
dirs: readonly string[],
|
|
907
|
-
custom = process.env['VC_MICROFRONTENDS_CONFIG_FILE_NAME'] ?? '',
|
|
908
|
-
): { rel: string; text: string }[] {
|
|
909
|
-
const names = custom === '' ? ['microfrontends.json', 'microfrontends.jsonc'] : [custom]
|
|
910
|
-
const out: { rel: string; text: string }[] = []
|
|
911
|
-
for (const dir of new Set(dirs.map((d) => path.resolve(d)))) {
|
|
912
|
-
const file = names.map((n) => path.join(dir, n)).find((f) => existsSync(f))
|
|
913
|
-
if (file === undefined) continue
|
|
914
|
-
const text = readFileSync(file, 'utf8')
|
|
915
|
-
let child = false
|
|
916
|
-
try {
|
|
917
|
-
const parsed = Bun.JSONC.parse(text) as { partOf?: unknown } | null
|
|
918
|
-
child = typeof parsed?.partOf === 'string'
|
|
919
|
-
} catch {}
|
|
920
|
-
if (!child) out.push({ rel: relPosix(root, file), text })
|
|
921
|
-
}
|
|
922
|
-
return out.sort((a, b) => (a.rel < b.rel ? -1 : 1))
|
|
923
|
-
}
|
|
924
|
-
|
|
925
765
|
export async function mapTurboWorkspace(
|
|
926
766
|
root: string,
|
|
927
767
|
metas: readonly PackageMeta[],
|
|
@@ -937,6 +777,7 @@ export async function mapTurboWorkspace(
|
|
|
937
777
|
}
|
|
938
778
|
const rootCfg = withGlobal(await readTurboJson(rootFile, root))
|
|
939
779
|
const pnp = yarnPnp(root)
|
|
780
|
+
const hookMemo = new Map<string, Owner>()
|
|
940
781
|
const rootTasks = tasksOf(rootCfg)
|
|
941
782
|
|
|
942
783
|
// Turbo 1 lists an env var as `$NAME` among `globalDependencies` (and a
|
|
@@ -989,7 +830,7 @@ export async function mapTurboWorkspace(
|
|
|
989
830
|
// 1032).
|
|
990
831
|
const rootMeta = metas.find((m) => path.resolve(m.dir) === path.resolve(root))
|
|
991
832
|
const globalFiles = [
|
|
992
|
-
...globalDeps.filter((d) => envDependency(d) === null),
|
|
833
|
+
...globalDeps.filter((d) => envDependency(d) === null).map(turboGlobstar),
|
|
993
834
|
...(rootCfg.globalDotEnv ?? []),
|
|
994
835
|
// Turbo 2 added the root's dependencies to its global hash; 1.13.4's
|
|
995
836
|
// leaves them out (trigger.dev: a Prisma migration re-keyed only the
|
|
@@ -1177,7 +1018,7 @@ export async function mapTurboWorkspace(
|
|
|
1177
1018
|
// over its files, so `test` keys on its dependencies' sources. Dropped,
|
|
1178
1019
|
// vx keyed `test` on its own files alone: a dependency's edit was a hit.
|
|
1179
1020
|
// Each package has it as a key-only task, a keyed group, as nx()'s
|
|
1180
|
-
// `
|
|
1021
|
+
// `inputs:*` twins are.
|
|
1181
1022
|
const withScript = new Set<string>()
|
|
1182
1023
|
for (const set of runnable.values()) for (const name of set) withScript.add(name)
|
|
1183
1024
|
const sameRefs = new Set<string>()
|
|
@@ -1334,7 +1175,10 @@ export async function mapTurboWorkspace(
|
|
|
1334
1175
|
}
|
|
1335
1176
|
|
|
1336
1177
|
const projects: TurboMappedProject[] = []
|
|
1337
|
-
|
|
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 }
|
|
1338
1182
|
for (const meta of metas) {
|
|
1339
1183
|
const scripts = packageScripts(meta)
|
|
1340
1184
|
const own = runnable.get(meta.name)!
|
|
@@ -1446,7 +1290,16 @@ export async function mapTurboWorkspace(
|
|
|
1446
1290
|
}
|
|
1447
1291
|
continue
|
|
1448
1292
|
}
|
|
1449
|
-
|
|
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
|
+
)
|
|
1450
1303
|
const mapped = buildTask(
|
|
1451
1304
|
literals,
|
|
1452
1305
|
name,
|
|
@@ -1480,6 +1333,35 @@ export async function mapTurboWorkspace(
|
|
|
1480
1333
|
// not, and a test cached on one platform hit on another. The reader
|
|
1481
1334
|
// keys those files by a probe at the workspace root.
|
|
1482
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
|
+
}
|
|
1483
1365
|
for (const meta of metas) {
|
|
1484
1366
|
const { defFor } = definitions(meta)
|
|
1485
1367
|
for (const t of projects.find((p) => p.name === meta.name)?.tasks ?? []) {
|
|
@@ -1563,6 +1445,7 @@ export async function mapTurboWorkspace(
|
|
|
1563
1445
|
excludeWorkspaceOutputs(root, projects, (e) => opaque.get(JSON.stringify(e)) ?? [])
|
|
1564
1446
|
pruneUnreachedPersistentNotes(projects, metas, opts.persistentTodo)
|
|
1565
1447
|
pruneOrphanPersistentNotes(projects, opts.persistentTodo)
|
|
1448
|
+
refuseTaskNames(projects, 'task')
|
|
1566
1449
|
return { projects, notes, globals }
|
|
1567
1450
|
}
|
|
1568
1451
|
|
|
@@ -1789,7 +1672,11 @@ function keyOnlyGroup(t: TurboMappedTask): TurboMappedTask {
|
|
|
1789
1672
|
}
|
|
1790
1673
|
|
|
1791
1674
|
function buildTask(
|
|
1792
|
-
literals: {
|
|
1675
|
+
literals: {
|
|
1676
|
+
readonly tasks: Literals[]
|
|
1677
|
+
readonly globalIgnored: readonly string[]
|
|
1678
|
+
readonly globalPrepended: ReadonlySet<string>
|
|
1679
|
+
},
|
|
1793
1680
|
name: string,
|
|
1794
1681
|
def: TurboTask,
|
|
1795
1682
|
command: string,
|
|
@@ -2005,9 +1892,9 @@ function buildTask(
|
|
|
2005
1892
|
let pkgDotenvDeep = false
|
|
2006
1893
|
// The root-relative `.env` globs, probed at the workspace root.
|
|
2007
1894
|
const wsDotenv = [...rootDotenv]
|
|
2008
|
-
//
|
|
2009
|
-
//
|
|
2010
|
-
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[] = []
|
|
2011
1898
|
// Root-relative path → the entry naming it.
|
|
2012
1899
|
const rels = new Map<string, Named>(literals.globalIgnored.map((f) => [f, [null, f]]))
|
|
2013
1900
|
const atRoot = pkgDir === '' || pkgDir === '.'
|
|
@@ -2017,7 +1904,7 @@ function buildTask(
|
|
|
2017
1904
|
// in the package and an edit replayed a stale build (item 936).
|
|
2018
1905
|
files.push('**/*')
|
|
2019
1906
|
} else {
|
|
2020
|
-
for (const i of def.inputs) {
|
|
1907
|
+
for (const i of def.inputs.map(turboGlobstar)) {
|
|
2021
1908
|
if (i === '$TURBO_DEFAULT$') {
|
|
2022
1909
|
files.push('**/*')
|
|
2023
1910
|
continue
|
|
@@ -2086,9 +1973,27 @@ function buildTask(
|
|
|
2086
1973
|
// project's own globs at every member (D-39), so as `files` a root
|
|
2087
1974
|
// `oxlint` over the repo replayed green after a member file broke.
|
|
2088
1975
|
// A root task's globs are the workspace's, outputs too.
|
|
2089
|
-
|
|
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
|
+
)
|
|
2090
1995
|
const inputs: Record<string, unknown> = { files: withoutTakenBack(files) }
|
|
2091
|
-
if (
|
|
1996
|
+
if (allWs.length > 0) inputs.workspaceFiles = withoutTakenBack(allWs)
|
|
2092
1997
|
if (keyEnv.length > 0) inputs.env = keyEnv
|
|
2093
1998
|
if (pkgDotenv) inputs.runtime = [pkgDotenvDeep ? DOTENV_PROBE : DOTENV_PROBE_TOP]
|
|
2094
1999
|
if (wsDotenv.length > 0) inputs.workspaceRuntime = [dotenvGlobsProbe(wsDotenv) ?? DOTENV_PROBE]
|
|
@@ -2105,7 +2010,7 @@ function buildTask(
|
|
|
2105
2010
|
for (const raw of def.outputs ?? []) {
|
|
2106
2011
|
// Turbo's grammar: an extglob in `dist/**/*.@(js|mjs)` matched
|
|
2107
2012
|
// nothing, so a hit restored nothing (item 1031).
|
|
2108
|
-
const mapped = minimatchToVx(raw, raw.startsWith('!'))
|
|
2013
|
+
const mapped = minimatchToVx(turboGlobstar(raw), raw.startsWith('!'))
|
|
2109
2014
|
if (mapped === null) {
|
|
2110
2015
|
todos.push(
|
|
2111
2016
|
`output ${JSON.stringify(raw)}: glob syntax vx cannot take — task runs uncached; ` +
|
|
@@ -2139,7 +2044,7 @@ function buildTask(
|
|
|
2139
2044
|
}
|
|
2140
2045
|
|
|
2141
2046
|
// With git's file list known, a wildcard-first output stays cached as
|
|
2142
|
-
// nx() keeps it (
|
|
2047
|
+
// nx() keeps it (2026-10-03; turbo() 2026-10-14): core never
|
|
2143
2048
|
// cleans a committed file. Turbo never cleans, so the untracked files
|
|
2144
2049
|
// it reaches are named. An X added for `X/**` is judged with it.
|
|
2145
2050
|
const wild = wildcardOutput(
|
|
@@ -2160,7 +2065,7 @@ function buildTask(
|
|
|
2160
2065
|
if (!covers(outFiles, rel)) outFiles.push(literalGlob(rel))
|
|
2161
2066
|
// An output over the project's own package.json (trpc's builds list
|
|
2162
2067
|
// it: tsdown rewrites `exports`) is taken back and keeps the cache, as
|
|
2163
|
-
// nx() does (
|
|
2068
|
+
// nx() does (2026-10-03). The build rewrites an input the key
|
|
2164
2069
|
// folds, so its identical rewrite counts as unchanged ('mutable').
|
|
2165
2070
|
const takeBacks = ownFileTakeBacks(outFiles, opts.ownConfig?.(pkgDir))
|
|
2166
2071
|
outFiles.push(...takeBacks)
|