@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.
@@ -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 `ga` is a
196
- * directory at any depth (`**\/dist`, `**\/dist/**`) and `gb`'s literal
197
- * head names no such directory: the two meet only at a `dist` nested in
198
- * `gb`'s tree, which is the sibling's. solid's build outputs `**\/dist`
199
- * beside types' `types/**`; Turbo caches both, and uncaching types made
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
- function separable(ga: string, gb: string): boolean {
203
- const name = /^\*\*\/([^*?[\]{}()!/\\]+)(?:\/\*\*)?$/.exec(ga)?.[1]
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 gb.replace(/^(\.\/)+/, '').split('/')) {
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 `nx-input:<name>` twins, Turbo's
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.
@@ -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 { isKeyedGroup, unkey } from './shared-outputs.js'
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 readFile(path.join(gitDir, 'HEAD'), 'utf8').catch(() => '')
139
- const log = await lstat(path.join(gitDir, 'logs', 'HEAD')).catch(() => null)
140
- const index = await lstat(path.join(gitDir, 'index')).catch(() => null)
141
- // A worktree's own stack holds its HEAD; the common one, its branches.
142
- const common = await readFile(path.join(gitDir, 'commondir'), 'utf8').catch(() => null)
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. */
@@ -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
- type TurboMappedProject,
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 metas = await withManifests(projects)
196
- const dirs = [root, ...metas.map((m) => m.dir)]
197
- const configs = await Promise.all(
198
- dirs.flatMap((d) => ['turbo.json', 'turbo.jsonc'].map((f) => textOf(path.join(d, f)))),
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
- await textOf(path.join(root, '.yarnrc.yml')),
220
+ yarnrc,
217
221
  // Its dependencies are global inputs, a member or not.
218
- await textOf(path.join(root, 'package.json')),
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,
@@ -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
- // `nx-input:*` twins are.
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
+ }