@vzn/vx-migrate 0.0.634 → 0.0.636

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
@@ -1,6 +1,8 @@
1
1
  # @vzn/vx-migrate
2
2
 
3
- Everything for adopting [`@vzn/vx`](https://github.com/vznjs/vx) from Turborepo, Nx or Vite Task, in one package with zero dependencies:
3
+ Website: <https://vznjs.github.io/vx/> · this plugin: <https://vznjs.github.io/vx/plugins/vx-migrate/> · source: <https://github.com/vznjs/vx>
4
+
5
+ Everything for adopting [`@vzn/vx`](https://vznjs.github.io/vx/) from Turborepo, Nx or Vite Task, in one package with zero dependencies:
4
6
 
5
7
  - **`turbo()`** — a temporary start for a Turbo repository: the plugin fills vx's `project` stage from `turbo.json` and each package's `package.json` scripts until the migrator writes native config.
6
8
  - **`nx()`** — the same temporary start for an Nx repository, filled from Nx's resolved project graph. Executor targets (`@nx/js:tsc`, `@nx/vite:build`, your own) run as themselves through **`nx-exec`**, one executor per process.
@@ -232,7 +234,7 @@ Your `package.json` scripts are never edited. `--dry` installs nothing and says
232
234
 
233
235
  ### Turbo
234
236
 
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.
237
+ 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 config that a `tsconfig*.json` in its directory or above (below the root) takes in, where that project is composite or sets `rootDir`, declares the values it uses instead: `tsc` refuses a file outside the package (TS6059, TS6307; withastro/astro's `scripts/` and the `performance/` project its `packages/astro/tsconfig.test.json` takes in). 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
238
 
237
239
  | Turborepo | vx |
238
240
  | ----------------------------------- | ------------------------------------------------------------------------------------------------- |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx-migrate",
3
- "version": "0.0.634",
3
+ "version": "0.0.636",
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": [
@@ -8,6 +8,7 @@
8
8
  "vx-plugin",
9
9
  "monorepo",
10
10
  "turborepo",
11
+ "turbo",
11
12
  "nx",
12
13
  "migration",
13
14
  "remote-cache"
@@ -39,7 +40,7 @@
39
40
  "access": "public"
40
41
  },
41
42
  "peerDependencies": {
42
- "@vzn/vx": "^0.0.634"
43
+ "@vzn/vx": "^0.0.636"
43
44
  },
44
45
  "repository": {
45
46
  "type": "git",
@@ -80,7 +80,9 @@ export async function migrateTurbo(
80
80
  ])
81
81
  for (const t of p.tasks) for (const kind of t.uses) used.add(PRESET_NAMES[kind])
82
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}`)
83
+ const sealed =
84
+ used.size > 0 &&
85
+ sealsConfig(root, path.join(path.resolve(root, p.dir), `vx.config.${format}`))
84
86
  if (used.size > 0 && !sealed) imported = true
85
87
  if (sealed) anySealed = true
86
88
  return {
@@ -1,15 +1,18 @@
1
- // A package whose tsconfig is a composite project (or sets `rootDir`)
2
- // refuses a source file outside its directory: withastro/astro's
1
+ // A tsconfig that is a composite project (or sets `rootDir`) refuses a
2
+ // source file outside its directory: withastro/astro's
3
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.
4
+ // import of the root `vx-preset.mjs` failed the build (TS6059, TS6307). So
5
+ // did `packages/astro/tsconfig.test.json`, which takes in the nested
6
+ // `performance/vx.config.mjs`. A config such a project takes in declares
7
+ // the preset values it uses itself.
6
8
 
7
- import { readFileSync } from 'node:fs'
9
+ import { readdirSync, readFileSync } from 'node:fs'
8
10
  import path from 'node:path'
9
11
 
10
12
  type TsConfig = {
11
13
  extends?: unknown
12
14
  include?: unknown
15
+ exclude?: unknown
13
16
  files?: unknown
14
17
  compilerOptions?: { composite?: unknown; rootDir?: unknown }
15
18
  }
@@ -22,33 +25,65 @@ function read(file: string): TsConfig | null {
22
25
  }
23
26
  }
24
27
 
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
28
+ /** A tsconfig glob as `Bun.Glob` reads it, relative to the tsconfig's own directory. */
29
+ function pattern(p: string): string {
30
+ const rel = p.replace(/^\$\{configDir\}\/?/, '').replace(/^\.\//, '')
31
+ if (rel === '' || rel === '.') return '**/*'
32
+ // A directory names everything under it, as tsc reads it.
33
+ return /[*?{]/.test(rel) || /\.[A-Za-z0-9]+$/.test(rel) ? rel : `${rel.replace(/\/$/, '')}/**/*`
34
+ }
35
+
36
+ /** Does the project `file` states take in `rel` (relative to its directory) and refuse files outside it? */
37
+ function seals(file: string, rel: string): boolean {
38
+ let sealed: boolean | undefined
39
+ let include: unknown
40
+ let exclude: unknown
41
+ let files: unknown
42
+ let at: TsConfig | null = read(file)
43
+ let from = path.dirname(file)
44
+ // The nearest file that states a field decides it, as `extends` resolves.
34
45
  for (let depth = 0; at !== null && depth < 8; depth++) {
35
46
  const o = at.compilerOptions
36
- if (o?.composite === true || typeof o?.rootDir === 'string') {
37
- sealed = true
38
- break
39
- }
47
+ if (sealed === undefined && o?.composite !== undefined) sealed = o.composite === true
48
+ if (sealed !== true && typeof o?.rootDir === 'string') sealed = true
49
+ include ??= at.include
50
+ exclude ??= at.exclude
51
+ files ??= at.files
40
52
  const ext = at.extends
41
- // A package name (`@tsconfig/node20`) seals nothing a repo would ship.
53
+ // A package (`@tsconfig/node20`) is not followed: what it seals is rare.
42
54
  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)
55
+ const next = path.resolve(from, ext.endsWith('.json') ? ext : `${ext}.json`)
56
+ from = path.dirname(next)
57
+ at = read(next)
58
+ }
59
+ if (sealed !== true) return false
60
+ const hit = (list: unknown): boolean =>
61
+ Array.isArray(list) &&
62
+ list.some((g) => typeof g === 'string' && new Bun.Glob(pattern(g)).match(rel))
63
+ if (hit(exclude)) return false
64
+ if (Array.isArray(files) && files.some((f) => typeof f === 'string' && pattern(f) === rel))
65
+ return true
66
+ if (Array.isArray(include)) return hit(include)
67
+ // `files` alone lists exactly what it compiles; neither means everything.
68
+ return !Array.isArray(files)
69
+ }
70
+
71
+ /**
72
+ * Is `config` (absolute) taken in by a sealing tsconfig (`tsconfig*.json`)
73
+ * in its directory or one above it, below `root`? The root's own project
74
+ * holds the preset, so it refuses nothing.
75
+ */
76
+ export function sealsConfig(root: string, config: string): boolean {
77
+ for (let dir = path.dirname(config); dir !== root && dir.startsWith(root);) {
78
+ let names: string[] = []
79
+ try {
80
+ names = readdirSync(dir).filter((n) => /^tsconfig.*\.json$/.test(n))
81
+ } catch {}
82
+ const rel = path.relative(dir, config).split(path.sep).join('/')
83
+ if (names.some((n) => seals(path.join(dir, n), rel))) return true
84
+ const up = path.dirname(dir)
85
+ if (up === dir) break
86
+ dir = up
46
87
  }
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)
88
+ return false
54
89
  }