@vzn/vx-migrate 0.0.633 → 0.0.634

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 CHANGED
@@ -232,7 +232,7 @@ Your `package.json` scripts are never edited. `--dry` installs nothing and says
232
232
 
233
233
  ### Turbo
234
234
 
235
- Reads the root pipeline (`tasks` in Turbo 2, `pipeline` in Turbo 1), per-package `turbo.json` `extends` overlays and each package's scripts, through the same mapper `turbo()` runs live — so the written configs say what the plugin was doing. Turbo's global fields become a generated root `vx-preset.ts` each config imports and spreads: TypeScript composition replaces global config. So does a task's `env` that several configs share (`buildEnv`), where inline it would repeat in every package (vercel/ai: 63 names, twice in each of ~100 configs, half the output). Turbo's `//#` root tasks are written to a `vx.config.ts` at the workspace root, which makes the root a project (core's D-39), and a package task's `//#x` edge reaches it. When a `vx.workspace.ts` already exists without one, the report names the `@vzn/vx-lockfile` plugin for the repo's lockfile: Turbo keys each package on its own lockfile entries, and core keys every task on the whole file until a plugin claims it.
235
+ Reads the root pipeline (`tasks` in Turbo 2, `pipeline` in Turbo 1), per-package `turbo.json` `extends` overlays and each package's scripts, through the same mapper `turbo()` runs live — so the written configs say what the plugin was doing. Turbo's global fields become a generated root `vx-preset.ts` each config imports and spreads: TypeScript composition replaces global config. A package whose `tsconfig.json` is a composite project (or sets `rootDir`) and takes in its config declares the values it uses instead: `tsc` refuses a file outside the package (TS6059, TS6307; withastro/astro's `scripts/`). So does a task's `env` that several configs share (`buildEnv`), where inline it would repeat in every package (vercel/ai: 63 names, twice in each of ~100 configs, half the output). Turbo's `//#` root tasks are written to a `vx.config.ts` at the workspace root, which makes the root a project (core's D-39), and a package task's `//#x` edge reaches it. When a `vx.workspace.ts` already exists without one, the report names the `@vzn/vx-lockfile` plugin for the repo's lockfile: Turbo keys each package on its own lockfile entries, and core keys every task on the whole file until a plugin claims it.
236
236
 
237
237
  | Turborepo | vx |
238
238
  | ----------------------------------- | ------------------------------------------------------------------------------------------------- |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx-migrate",
3
- "version": "0.0.633",
3
+ "version": "0.0.634",
4
4
  "license": "MIT",
5
5
  "description": "Adopt @vzn/vx from Turborepo or Nx: run the repo unchanged, migrate it to vx.config.ts, and keep your Turbo or Nx remote cache",
6
6
  "keywords": [
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "peerDependencies": {
42
- "@vzn/vx": "^0.0.633"
42
+ "@vzn/vx": "^0.0.634"
43
43
  },
44
44
  "repository": {
45
45
  "type": "git",
@@ -23,6 +23,7 @@ import { gitIgnored, spareTrackedOutputs, trackedFiles, trackedKinds } from './t
23
23
  import { DOTENV_GLOBS_HEAD, DOTENV_PROBE, DOTENV_PROBE_TOP } from './dotenv-probe.js'
24
24
  import { adoptedToolNotes } from './workspace-notes.js'
25
25
  import { spelledNames } from './spelled-env.js'
26
+ import { sealsConfig } from './sealed-tsconfig.js'
26
27
 
27
28
  /** What a task's `npm_package_*` read: the manifest, so a bump reaches them. */
28
29
  const MANIFEST_IMPORT = "import pkg from './package.json' with { type: 'json' }"
@@ -67,6 +68,11 @@ export async function migrateTurbo(
67
68
 
68
69
  const shared = hoistTaskEnv(mapping.projects)
69
70
  const probes = nameProbes(mapping.projects)
71
+ const { inputs, env, pass } = mapping.globals
72
+ const decls = presetDeclarations(inputs, env, pass, shared.lists, probes.named)
73
+ // Written unless every config that uses it declares its values itself.
74
+ let imported = false
75
+ let anySealed = false
70
76
  const projects: GeneratedProject[] = mapping.projects.map((p) => {
71
77
  const used = new Set<string>([
72
78
  ...(shared.usedBy.get(p.name) ?? []),
@@ -74,29 +80,27 @@ export async function migrateTurbo(
74
80
  ])
75
81
  for (const t of p.tasks) for (const kind of t.uses) used.add(PRESET_NAMES[kind])
76
82
  const readsManifest = p.tasks.some((t) => JSON.stringify(t.task ?? {}).includes('"pkg.'))
83
+ const sealed = used.size > 0 && sealsConfig(path.resolve(root, p.dir), `vx.config.${format}`)
84
+ if (used.size > 0 && !sealed) imported = true
85
+ if (sealed) anySealed = true
77
86
  return {
78
87
  name: p.name,
79
88
  dir: p.dir,
80
89
  importLines: [
81
90
  ...(readsManifest ? [MANIFEST_IMPORT] : []),
82
- ...presetImportLines(used, root, p.dir, format),
91
+ ...(sealed
92
+ ? decls.filter(([name]) => used.has(name)).map(([, line]) => line)
93
+ : presetImportLines(used, root, p.dir, format)),
83
94
  ],
84
95
  tasks: p.tasks.map(({ name, todos, task }) => ({ name, todos, task })),
85
96
  }
86
97
  })
87
98
 
88
- const { inputs, env, pass } = mapping.globals
89
99
  const extraFiles: MigrationPlan['extraFiles'] = []
90
- if (
91
- inputs.length > 0 ||
92
- env.length > 0 ||
93
- pass.length > 0 ||
94
- shared.lists.length > 0 ||
95
- probes.named.length > 0
96
- ) {
100
+ if (decls.length > 0 && (imported || !anySealed)) {
97
101
  extraFiles.push({
98
102
  relPath: presetFile(format),
99
- contents: renderPreset(inputs, env, pass, shared.lists, probes.named),
103
+ contents: [...decls.map(([, line]) => `export ${line}`), ''].join('\n'),
100
104
  })
101
105
  }
102
106
 
@@ -303,23 +307,24 @@ function nameProbes(projects: readonly TurboMappedProject[]): {
303
307
  return { named: named.sort((a, b) => order.indexOf(a) - order.indexOf(b)), usedBy }
304
308
  }
305
309
 
306
- function renderPreset(
310
+ /** Each preset value as `[name, 'const name = …']`, in the preset's order. */
311
+ function presetDeclarations(
307
312
  inputs: string[],
308
313
  env: string[],
309
314
  pass: string[],
310
315
  shared: readonly SharedList[],
311
316
  probes: readonly Probe[],
312
- ): string {
317
+ ): [string, string][] {
313
318
  // Escape each entry via the shared `quote()` — a turbo.json global (a file
314
319
  // glob, or an env name a user hand-wrote) may contain a `'`/`\`/newline that
315
320
  // would otherwise splice into a malformed, unloadable `vx-preset.ts`.
316
321
  const arr = (xs: string[]): string => `[${xs.map(quote).join(', ')}]`
317
- const lines: string[] = []
318
- if (inputs.length > 0) lines.push(`export const globalInputs = ${arr(inputs)}`)
319
- if (env.length > 0) lines.push(`export const globalEnvInputs = ${arr(env)}`)
320
- if (pass.length > 0) lines.push(`export const globalPassThroughEnv = ${arr(pass)}`)
321
- for (const l of shared) lines.push(`export const ${l.name} = ${arr([...l.values])}`)
322
- for (const probe of probes) lines.push(`export const ${probe.name} = ${quote(probe.command)}`)
323
- lines.push('')
324
- return lines.join('\n')
322
+ const out: [string, string][] = []
323
+ const add = (name: string, value: string) => out.push([name, `const ${name} = ${value}`])
324
+ if (inputs.length > 0) add('globalInputs', arr(inputs))
325
+ if (env.length > 0) add('globalEnvInputs', arr(env))
326
+ if (pass.length > 0) add('globalPassThroughEnv', arr(pass))
327
+ for (const l of shared) add(l.name, arr([...l.values]))
328
+ for (const probe of probes) add(probe.name, quote(probe.command))
329
+ return out
325
330
  }
@@ -0,0 +1,54 @@
1
+ // A package whose tsconfig is a composite project (or sets `rootDir`)
2
+ // refuses a source file outside its directory: withastro/astro's
3
+ // `scripts/tsconfig.json` type-checked the written `vx.config.mjs`, and its
4
+ // import of the root `vx-preset.mjs` failed the build (TS6059, TS6307). Such
5
+ // a package's config declares the preset values it uses itself.
6
+
7
+ import { readFileSync } from 'node:fs'
8
+ import path from 'node:path'
9
+
10
+ type TsConfig = {
11
+ extends?: unknown
12
+ include?: unknown
13
+ files?: unknown
14
+ compilerOptions?: { composite?: unknown; rootDir?: unknown }
15
+ }
16
+
17
+ function read(file: string): TsConfig | null {
18
+ try {
19
+ return (Bun.JSONC.parse(readFileSync(file, 'utf8')) ?? {}) as TsConfig
20
+ } catch {
21
+ return null
22
+ }
23
+ }
24
+
25
+ /** Does `dir/tsconfig.json` take `dir/<config>` into a project that refuses files outside `dir`? */
26
+ export function sealsConfig(dir: string, config: string): boolean {
27
+ const own = read(path.join(dir, 'tsconfig.json'))
28
+ if (own === null) return false
29
+ // `include` and `files` are not inherited past the file that sets them
30
+ // relative to it; the compiler options are, through `extends`.
31
+ let sealed = false
32
+ let at: TsConfig | null = own
33
+ let from = dir
34
+ for (let depth = 0; at !== null && depth < 8; depth++) {
35
+ const o = at.compilerOptions
36
+ if (o?.composite === true || typeof o?.rootDir === 'string') {
37
+ sealed = true
38
+ break
39
+ }
40
+ const ext = at.extends
41
+ // A package name (`@tsconfig/node20`) seals nothing a repo would ship.
42
+ if (typeof ext !== 'string' || !ext.startsWith('.')) break
43
+ const file = path.resolve(from, ext.endsWith('.json') ? ext : `${ext}.json`)
44
+ from = path.dirname(file)
45
+ at = read(file)
46
+ }
47
+ if (!sealed) return false
48
+ if (Array.isArray(own.include))
49
+ return own.include.some(
50
+ (g) => typeof g === 'string' && new Bun.Glob(g.replace(/^\.\//, '')).match(config),
51
+ )
52
+ // `files` alone lists exactly what it compiles.
53
+ return !Array.isArray(own.files)
54
+ }