@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 +1 -1
- package/package.json +2 -2
- package/src/migrate-turbo.ts +25 -20
- package/src/sealed-tsconfig.ts +54 -0
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.
|
|
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.
|
|
42
|
+
"@vzn/vx": "^0.0.634"
|
|
43
43
|
},
|
|
44
44
|
"repository": {
|
|
45
45
|
"type": "git",
|
package/src/migrate-turbo.ts
CHANGED
|
@@ -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
|
-
...
|
|
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:
|
|
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
|
-
|
|
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
|
|
318
|
-
|
|
319
|
-
if (
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
for (const
|
|
323
|
-
|
|
324
|
-
return
|
|
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
|
+
}
|