@vzn/vx-migrate 0.0.644 → 0.0.646
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/LICENSE +1 -1
- package/README.md +14 -16
- package/package.json +2 -2
- package/src/adoption-plugin.ts +9 -11
- package/src/index.ts +36 -10
- package/src/mapping-cache.ts +14 -6
- package/src/migrate-turbo.ts +2 -6
- package/src/migrate.ts +5 -14
- package/src/nx/nx-map.ts +30 -15
- package/src/nx/nx-upstream.ts +5 -5
- package/src/nx-cache/index.ts +13 -18
- package/src/shared-outputs.ts +16 -10
- package/src/tracked-outputs.ts +10 -6
- package/src/turbo/index.ts +16 -10
- package/src/turbo/turbo-map.ts +8 -177
- package/src/turbo/turbo-reads.ts +183 -0
- package/src/turbo-cache/index.ts +107 -57
- package/src/turbo-cache/oidc.ts +31 -16
- package/src/turbo-cache/scope.ts +89 -0
- package/src/remote-scope.ts +0 -12
package/src/shared-outputs.ts
CHANGED
|
@@ -192,18 +192,24 @@ function cachedOutputs(t: GeneratedTask, index: number): Cached | null {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
|
-
* Can the keeper's `ga` give `gb`'s tree back? Only when
|
|
196
|
-
* directory at any depth (`**\/dist`, `**\/dist/**`) and
|
|
197
|
-
* head names no such directory:
|
|
198
|
-
*
|
|
199
|
-
* beside types' `types
|
|
200
|
-
* vx the slower of the two.
|
|
195
|
+
* Can the keeper's `ga` give `gb`'s tree back? Only when one of the two is
|
|
196
|
+
* a directory at any depth (`**\/dist`, `**\/dist/**`) and the other's
|
|
197
|
+
* literal head names no such directory: they meet only where one tree
|
|
198
|
+
* nests in the other, which neither task writes. solid's build outputs
|
|
199
|
+
* `dist/**` and `**\/dist` beside types' `types/**` and `**\/types`; Turbo
|
|
200
|
+
* caches both, and uncaching types made vx the slower of the two.
|
|
201
201
|
*/
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
const DIR_ANYWHERE = /^\*\*\/([^*?[\]{}()!/\\]+)(?:\/\*\*)?$/
|
|
203
|
+
|
|
204
|
+
const separable = (ga: string, gb: string): boolean => apart(ga, gb) || apart(gb, ga)
|
|
205
|
+
|
|
206
|
+
function apart(dir: string, other: string): boolean {
|
|
207
|
+
const name = DIR_ANYWHERE.exec(dir)?.[1]
|
|
204
208
|
if (name === undefined) return false
|
|
209
|
+
const otherName = DIR_ANYWHERE.exec(other)?.[1]
|
|
210
|
+
if (otherName !== undefined) return otherName !== name
|
|
205
211
|
const head: string[] = []
|
|
206
|
-
for (const seg of
|
|
212
|
+
for (const seg of other.replace(/^(\.\/)+/, '').split('/')) {
|
|
207
213
|
if (!isLiteralPattern(seg) || seg === '') break
|
|
208
214
|
head.push(seg)
|
|
209
215
|
}
|
|
@@ -348,7 +354,7 @@ function prefixIndex(): {
|
|
|
348
354
|
}
|
|
349
355
|
|
|
350
356
|
/**
|
|
351
|
-
* A keyed group (`cache`, no `exec`: the `
|
|
357
|
+
* A keyed group (`cache`, no `exec`: the `inputs:<name>` twins, Turbo's
|
|
352
358
|
* transit nodes) runs nothing, so core lets its key read what a task
|
|
353
359
|
* writes (`detectInputOverlaps` exempts it). Uncaching one left `{}`, a
|
|
354
360
|
* task core refuses, and every verb failed.
|
package/src/tracked-outputs.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { lstat, readFile } from 'node:fs/promises'
|
|
|
11
11
|
import path from 'node:path'
|
|
12
12
|
import { isLiteralPattern, outputsOverlap } from '@vzn/vx'
|
|
13
13
|
import { relPosix } from './paths.js'
|
|
14
|
-
import
|
|
14
|
+
import type * as SharedOutputs from './shared-outputs.js'
|
|
15
15
|
|
|
16
16
|
/** The files git tracks under `root`, root-relative; null outside a repo or without git. */
|
|
17
17
|
export async function trackedFiles(root: string): Promise<string[] | null> {
|
|
@@ -135,11 +135,13 @@ export async function headStamp(root: string): Promise<string> {
|
|
|
135
135
|
const m = /^gitdir: (.*)$/m.exec(await readFile(dotGit, 'utf8').catch(() => ''))
|
|
136
136
|
if (m !== null) gitDir = path.resolve(dir, m[1]!.trim())
|
|
137
137
|
}
|
|
138
|
-
const head = await
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
const [head, log, index, common] = await Promise.all([
|
|
139
|
+
readFile(path.join(gitDir, 'HEAD'), 'utf8').catch(() => ''),
|
|
140
|
+
lstat(path.join(gitDir, 'logs', 'HEAD')).catch(() => null),
|
|
141
|
+
lstat(path.join(gitDir, 'index')).catch(() => null),
|
|
142
|
+
// A worktree's own stack holds its HEAD; the common one, its branches.
|
|
143
|
+
readFile(path.join(gitDir, 'commondir'), 'utf8').catch(() => null),
|
|
144
|
+
])
|
|
143
145
|
const tables = await Promise.all(
|
|
144
146
|
[gitDir, ...(common === null ? [] : [path.resolve(gitDir, common.trim())])].map((d) =>
|
|
145
147
|
readFile(path.join(d, 'reftable', 'tables.list'), 'utf8').catch(() => ''),
|
|
@@ -236,6 +238,8 @@ export function spareTrackedOutputs(
|
|
|
236
238
|
}[],
|
|
237
239
|
tracked: readonly string[],
|
|
238
240
|
): [string, string][] {
|
|
241
|
+
// Loaded here, on a remap only: a kept mapping's run never reads it.
|
|
242
|
+
const { isKeyedGroup, unkey } = require('./shared-outputs.js') as typeof SharedOutputs
|
|
239
243
|
const todos: [string, string][] = []
|
|
240
244
|
const trackedSet = new Set(tracked)
|
|
241
245
|
/** Drop the `!` entries readers carry for a committed file no output names any more. */
|
package/src/turbo/index.ts
CHANGED
|
@@ -21,14 +21,14 @@ 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 =
|
|
@@ -192,11 +192,15 @@ const textOf = (file: string): Promise<string> =>
|
|
|
192
192
|
* `.yarnrc.yml` and the microfrontends configs.
|
|
193
193
|
*/
|
|
194
194
|
async function run(root: string, projects: readonly ProjectMeta[]): Promise<AdoptionRun> {
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
const dirs = [root, ...projects.map((m) => m.dir)]
|
|
196
|
+
const [metas, configs, yarnrc, rootManifest] = await Promise.all([
|
|
197
|
+
withManifests(projects),
|
|
198
|
+
Promise.all(
|
|
199
|
+
dirs.flatMap((d) => ['turbo.json', 'turbo.jsonc'].map((f) => textOf(path.join(d, f)))),
|
|
200
|
+
),
|
|
201
|
+
textOf(path.join(root, '.yarnrc.yml')),
|
|
202
|
+
textOf(path.join(root, 'package.json')),
|
|
203
|
+
])
|
|
200
204
|
const envNames = envNamesThatMap(configs, envNamesNow())
|
|
201
205
|
// The tsconfigs a `tsc` script reads are the mapping's inputs too.
|
|
202
206
|
const tsc = onceReader()
|
|
@@ -213,9 +217,9 @@ async function run(root: string, projects: readonly ProjectMeta[]): Promise<Adop
|
|
|
213
217
|
process.env['TURBO_CI_VENDOR_ENV_KEY'] ?? '',
|
|
214
218
|
process.env['TURBO_ENV_MODE'] ?? '',
|
|
215
219
|
...configs,
|
|
216
|
-
|
|
220
|
+
yarnrc,
|
|
217
221
|
// Its dependencies are global inputs, a member or not.
|
|
218
|
-
|
|
222
|
+
rootManifest,
|
|
219
223
|
JSON.stringify(microfrontendsConfigs(root, dirs)),
|
|
220
224
|
// A config file added beside mapped tasks changes what an output may cover.
|
|
221
225
|
JSON.stringify(metas.map((m) => [m.name, m.dir, m.packageJson, m.configPath])),
|
|
@@ -244,6 +248,8 @@ async function mapAll(
|
|
|
244
248
|
m.configPath === null ? [] : [[relPosix(root, m.dir), path.basename(m.configPath)] as const],
|
|
245
249
|
),
|
|
246
250
|
)
|
|
251
|
+
// Only a mapping the kept one no longer holds loads the mapper.
|
|
252
|
+
const { mapTurboWorkspace } = require('./turbo-map.js') as typeof TurboMap
|
|
247
253
|
const mapped = await mapTurboWorkspace(root, metas, {
|
|
248
254
|
// Inline: the values themselves, where `bunx @vzn/vx-migrate` splices a preset import.
|
|
249
255
|
splice: (_kind, values) => values,
|
package/src/turbo/turbo-map.ts
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
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'
|
|
@@ -31,6 +30,13 @@ import {
|
|
|
31
30
|
} from '../shared-outputs.js'
|
|
32
31
|
import { manifestGraph, type ManifestGraph } from '../manifest-graph.js'
|
|
33
32
|
import { isRecord, packageScripts, relPosix } from '../paths.js'
|
|
33
|
+
import {
|
|
34
|
+
FRAMEWORK_ENV,
|
|
35
|
+
keyable,
|
|
36
|
+
microfrontendsConfigs,
|
|
37
|
+
ROOT,
|
|
38
|
+
turboConfigFile,
|
|
39
|
+
} from './turbo-reads.js'
|
|
34
40
|
import type { TrackedKinds } from '../tracked-outputs.js'
|
|
35
41
|
import {
|
|
36
42
|
DOTENV_PROBE,
|
|
@@ -69,34 +75,6 @@ interface TurboJson {
|
|
|
69
75
|
|
|
70
76
|
const SHELL_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
71
77
|
|
|
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
78
|
/**
|
|
101
79
|
* A Turbo env list's explicit names, read as Turbo reads them
|
|
102
80
|
* (`wildcard_to_regex_pattern`): `*` is the one wildcard, `\*` a
|
|
@@ -195,83 +173,6 @@ const KNOWN_TASK_KEYS = new Set([
|
|
|
195
173
|
const OUTPUT_LOGS_DEFAULT = 'new-only'
|
|
196
174
|
const OUTPUT_LOGS_RUN_FLAG = new Set(['full', 'hash-only', 'errors-only', 'none'])
|
|
197
175
|
|
|
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
176
|
/** The three global fields of turbo.json a task may draw on. */
|
|
276
177
|
export type TurboGlobal = 'inputs' | 'env' | 'pass'
|
|
277
178
|
|
|
@@ -424,45 +325,6 @@ function keyIgnored(
|
|
|
424
325
|
inputs.workspaceRuntime = [...probes, ignoredFilesProbe(hits.map(([rel]) => rel))]
|
|
425
326
|
}
|
|
426
327
|
|
|
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
328
|
async function readTurboJson(file: string, root: string): Promise<TurboJson> {
|
|
467
329
|
const text = await Bun.file(file).text()
|
|
468
330
|
let parsed: unknown
|
|
@@ -606,7 +468,6 @@ function badCommand(def: TurboTask): boolean {
|
|
|
606
468
|
|
|
607
469
|
/** Declared task names for a package: plain root keys, `pkg#name` keys
|
|
608
470
|
* for this package, and per-package turbo.json keys — in that order. */
|
|
609
|
-
const ROOT = '//'
|
|
610
471
|
|
|
611
472
|
/**
|
|
612
473
|
* A package's turbo.json files, root first and its own last, as Turbo's
|
|
@@ -892,36 +753,6 @@ async function rootPackageJson(root: string): Promise<PackageMeta['packageJson']
|
|
|
892
753
|
return (pkg !== null && typeof pkg === 'object' ? pkg : {}) as PackageMeta['packageJson']
|
|
893
754
|
}
|
|
894
755
|
|
|
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
756
|
export async function mapTurboWorkspace(
|
|
926
757
|
root: string,
|
|
927
758
|
metas: readonly PackageMeta[],
|
|
@@ -1177,7 +1008,7 @@ export async function mapTurboWorkspace(
|
|
|
1177
1008
|
// over its files, so `test` keys on its dependencies' sources. Dropped,
|
|
1178
1009
|
// vx keyed `test` on its own files alone: a dependency's edit was a hit.
|
|
1179
1010
|
// Each package has it as a key-only task, a keyed group, as nx()'s
|
|
1180
|
-
// `
|
|
1011
|
+
// `inputs:*` twins are.
|
|
1181
1012
|
const withScript = new Set<string>()
|
|
1182
1013
|
for (const set of runnable.values()) for (const name of set) withScript.add(name)
|
|
1183
1014
|
const sameRefs = new Set<string>()
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// What a turbo() run reads before it knows whether its kept mapping still
|
|
2
|
+
// holds: the mapping itself (`turbo-map.ts`, ~240 KB with what it imports)
|
|
3
|
+
// loads only when it does not.
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { relPosix } from '../paths.js'
|
|
7
|
+
|
|
8
|
+
/** Turbo's name for the workspace root in `//#task` and `extends`. */
|
|
9
|
+
export const ROOT = '//'
|
|
10
|
+
|
|
11
|
+
// Turbo 2's framework inference: a package that depends on a framework
|
|
12
|
+
// has its variables hashed into its tasks and passed to them, with
|
|
13
|
+
// nothing in turbo.json saying so. vx env names are explicit, so the
|
|
14
|
+
// variables were stripped in silence and a build that inlines them
|
|
15
|
+
// (Next's `NEXT_PUBLIC_*`) read empty values (item 940). A live mapping
|
|
16
|
+
// (`envNames`) infers them as Turbo does; the migrate CLI lists the names
|
|
17
|
+
// the package's own files spell (`sourceNames`) and says so in a note.
|
|
18
|
+
// Turbo's own table (`packages/turbo-types/src/json/frameworks.json`),
|
|
19
|
+
// in its order: a package takes the FIRST framework it matches, `all`
|
|
20
|
+
// needing every dependency and `some` any one.
|
|
21
|
+
const NITRO_ENV = [
|
|
22
|
+
'NITRO_*',
|
|
23
|
+
'SERVER_*',
|
|
24
|
+
'AWS_APP_ID',
|
|
25
|
+
'INPUT_AZURE_STATIC_WEB_APPS_API_TOKEN',
|
|
26
|
+
'CLEAVR',
|
|
27
|
+
'CF_PAGES',
|
|
28
|
+
'FIREBASE_APP_HOSTING',
|
|
29
|
+
'NETLIFY',
|
|
30
|
+
'STORMKIT',
|
|
31
|
+
'NOW_BUILDER',
|
|
32
|
+
'ZEABUR',
|
|
33
|
+
'RENDER',
|
|
34
|
+
]
|
|
35
|
+
export const FRAMEWORK_ENV: ReadonlyArray<{
|
|
36
|
+
slug: string
|
|
37
|
+
env: readonly string[]
|
|
38
|
+
all: boolean
|
|
39
|
+
dependencies: readonly string[]
|
|
40
|
+
}> = [
|
|
41
|
+
{ slug: 'astro', env: ['PUBLIC_*'], all: true, dependencies: ['astro'] },
|
|
42
|
+
{ slug: 'blitzjs', env: ['NEXT_PUBLIC_*'], all: true, dependencies: ['blitz'] },
|
|
43
|
+
{
|
|
44
|
+
slug: 'create-react-app',
|
|
45
|
+
env: ['REACT_APP_*'],
|
|
46
|
+
all: false,
|
|
47
|
+
dependencies: ['react-scripts', 'react-dev-utils'],
|
|
48
|
+
},
|
|
49
|
+
{ slug: 'expo', env: ['EXPO_PUBLIC_*'], all: true, dependencies: ['expo'] },
|
|
50
|
+
{ slug: 'gatsby', env: ['GATSBY_*'], all: true, dependencies: ['gatsby'] },
|
|
51
|
+
{
|
|
52
|
+
slug: 'nextjs',
|
|
53
|
+
env: ['NEXT_PUBLIC_*', 'NEXT_DEPLOYMENT_ID'],
|
|
54
|
+
all: true,
|
|
55
|
+
dependencies: ['next'],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
slug: 'nitro',
|
|
59
|
+
env: NITRO_ENV,
|
|
60
|
+
all: false,
|
|
61
|
+
dependencies: ['nitropack', 'nitropack-nightly', 'nitro', 'nitro-nightly'],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
slug: 'nuxtjs',
|
|
65
|
+
env: ['NUXT_*', 'NUXT_ENV_*', ...NITRO_ENV, 'LAUNCH_EDITOR'],
|
|
66
|
+
all: false,
|
|
67
|
+
dependencies: ['nuxt', 'nuxt-edge', 'nuxt3', 'nuxt3-edge'],
|
|
68
|
+
},
|
|
69
|
+
{ slug: 'redwoodjs', env: ['REDWOOD_ENV_*'], all: true, dependencies: ['@redwoodjs/core'] },
|
|
70
|
+
{
|
|
71
|
+
slug: 'remix',
|
|
72
|
+
env: ['REMIX_*'],
|
|
73
|
+
all: false,
|
|
74
|
+
dependencies: ['@remix-run/dev', '@remix-run/react', '@remix-run/serve', '@react-router/dev'],
|
|
75
|
+
},
|
|
76
|
+
{ slug: 'sanity', env: ['SANITY_STUDIO_*'], all: true, dependencies: ['@sanity/cli'] },
|
|
77
|
+
{ slug: 'solidstart', env: ['VITE_*'], all: true, dependencies: ['solid-js', 'solid-start'] },
|
|
78
|
+
{ slug: 'sveltekit', env: ['VITE_*', 'PUBLIC_*'], all: true, dependencies: ['@sveltejs/kit'] },
|
|
79
|
+
{ slug: 'vite', env: ['VITE_*'], all: true, dependencies: ['vite'] },
|
|
80
|
+
{
|
|
81
|
+
slug: 'vue',
|
|
82
|
+
env: ['VUE_APP_*', 'LAUNCH_EDITOR'],
|
|
83
|
+
all: true,
|
|
84
|
+
dependencies: ['@vue/cli-service'],
|
|
85
|
+
},
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
/** A name core takes in `inputs.env` and `exec.env.passThrough`. */
|
|
89
|
+
export const keyable = (name: string): boolean =>
|
|
90
|
+
name.length > 0 && !name.startsWith('!') && !/[*?[\]{}=\0]/.test(name)
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The names of `names` a live mapping can read differently: each a `*` in
|
|
94
|
+
* one of `configs` (turbo.json texts, any string, a superset) or in Turbo's
|
|
95
|
+
* framework table matches, and each core cannot key (a literal one is
|
|
96
|
+
* dropped only when no variable has it). The kept mapping keyed on every
|
|
97
|
+
* name, so any variable that came or went (a CI step's, `VX_DEBUG_TIMING`)
|
|
98
|
+
* remapped the workspace: ~330 ms a run on vercel/ai.
|
|
99
|
+
*/
|
|
100
|
+
export function envNamesThatMap(configs: readonly string[], names: readonly string[]): string[] {
|
|
101
|
+
const patterns = new Set(FRAMEWORK_ENV.flatMap((f) => f.env).filter((e) => e.includes('*')))
|
|
102
|
+
for (const text of configs)
|
|
103
|
+
for (const m of text.matchAll(/"((?:[^"\\]|\\.)*)"/g))
|
|
104
|
+
if (m[1]!.includes('*')) patterns.add(m[1]!)
|
|
105
|
+
// Backslashes and a leading `!` dropped, every `*` a wildcard: wider
|
|
106
|
+
// than Turbo's reading, never narrower.
|
|
107
|
+
const res = [...patterns].map(
|
|
108
|
+
(p) =>
|
|
109
|
+
new RegExp(
|
|
110
|
+
`^${p.replaceAll('\\', '').replace(/^!/, '').split('*').map(RegExp.escape).join('.*')}$`,
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
return names.filter((n) => !keyable(n) || res.some((re) => re.test(n)))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A directory's Turbo config: `turbo.json`, else `turbo.jsonc` (Turbo 2.5+
|
|
118
|
+
* reads either). Only `turbo.json` was looked for, so a `turbo.jsonc` root
|
|
119
|
+
* failed the run with ENOENT and a package's `turbo.jsonc` overlay was
|
|
120
|
+
* skipped in silence — its `inputs` then keyed nothing (item 938).
|
|
121
|
+
*/
|
|
122
|
+
export async function turboConfigFile(dir: string): Promise<string | null> {
|
|
123
|
+
for (const name of ['turbo.json', 'turbo.jsonc']) {
|
|
124
|
+
const file = path.join(dir, name)
|
|
125
|
+
if (await Bun.file(file).exists()) return file
|
|
126
|
+
}
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The workspace root as a project to name when turbo.json declares `//#`
|
|
132
|
+
* tasks: its package name and directory. Null with no such task, a
|
|
133
|
+
* nameless root, or a file the mapping refuses (it names the file).
|
|
134
|
+
*/
|
|
135
|
+
export async function rootTaskProject(root: string): Promise<{ name: string; dir: string } | null> {
|
|
136
|
+
const file = await turboConfigFile(root)
|
|
137
|
+
if (file === null) return null
|
|
138
|
+
let turbo: { tasks?: unknown; pipeline?: unknown } | null
|
|
139
|
+
let pkg: { name?: unknown } | null
|
|
140
|
+
try {
|
|
141
|
+
turbo = Bun.JSONC.parse(await Bun.file(file).text()) as typeof turbo
|
|
142
|
+
pkg = (await Bun.file(path.join(root, 'package.json')).json()) as typeof pkg
|
|
143
|
+
} catch {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
const tasks = turbo?.tasks ?? turbo?.pipeline
|
|
147
|
+
const hasRootTask =
|
|
148
|
+
typeof tasks === 'object' &&
|
|
149
|
+
tasks !== null &&
|
|
150
|
+
Object.keys(tasks).some((k) => k.startsWith(`${ROOT}#`))
|
|
151
|
+
if (!hasRootTask || typeof pkg?.name !== 'string' || pkg.name === '') return null
|
|
152
|
+
return { name: pkg.name, dir: root }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The microfrontends configs Turbo hashes into every task: each package's
|
|
157
|
+
* (the root's too) `microfrontends.json`, else `microfrontends.jsonc`, or
|
|
158
|
+
* the one name `VC_MICROFRONTENDS_CONFIG_FILE_NAME` gives, unless it is a
|
|
159
|
+
* child's `{ "partOf": … }` (Turbo's `update_turbo_json` appends them to
|
|
160
|
+
* the root's global deps). Unread, an edit to `web`'s routes replayed every
|
|
161
|
+
* sibling app's build (with-microfrontends, Turbo 2.11's dry run moves all
|
|
162
|
+
* 18 tasks). Paths are root-relative; `text` is what the mapping read.
|
|
163
|
+
*/
|
|
164
|
+
export function microfrontendsConfigs(
|
|
165
|
+
root: string,
|
|
166
|
+
dirs: readonly string[],
|
|
167
|
+
custom = process.env['VC_MICROFRONTENDS_CONFIG_FILE_NAME'] ?? '',
|
|
168
|
+
): { rel: string; text: string }[] {
|
|
169
|
+
const names = custom === '' ? ['microfrontends.json', 'microfrontends.jsonc'] : [custom]
|
|
170
|
+
const out: { rel: string; text: string }[] = []
|
|
171
|
+
for (const dir of new Set(dirs.map((d) => path.resolve(d)))) {
|
|
172
|
+
const file = names.map((n) => path.join(dir, n)).find((f) => existsSync(f))
|
|
173
|
+
if (file === undefined) continue
|
|
174
|
+
const text = readFileSync(file, 'utf8')
|
|
175
|
+
let child = false
|
|
176
|
+
try {
|
|
177
|
+
const parsed = Bun.JSONC.parse(text) as { partOf?: unknown } | null
|
|
178
|
+
child = typeof parsed?.partOf === 'string'
|
|
179
|
+
} catch {}
|
|
180
|
+
if (!child) out.push({ rel: relPosix(root, file), text })
|
|
181
|
+
}
|
|
182
|
+
return out.sort((a, b) => (a.rel < b.rel ? -1 : 1))
|
|
183
|
+
}
|