@vzn/vx 0.0.142 → 0.0.143

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
@@ -32,7 +32,8 @@ Same graph, commands and concurrency: [how it is measured](https://vznjs.github.
32
32
 
33
33
  ## Try it on your repo
34
34
 
35
- In a Turborepo or Nx workspace, add one file:
35
+ In a Turborepo or Nx workspace, add one file (`npx @vzn/vx init` writes
36
+ it and prints the command to run next):
36
37
 
37
38
  ```ts
38
39
  // vx.workspace.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.142",
3
+ "version": "0.0.143",
4
4
  "description": "A fast, correct task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,10 +17,10 @@
17
17
  "node": ">=18"
18
18
  },
19
19
  "optionalDependencies": {
20
- "@vzn/vx-linux-x64": "0.0.142",
21
- "@vzn/vx-linux-arm64": "0.0.142",
22
- "@vzn/vx-darwin-x64": "0.0.142",
23
- "@vzn/vx-darwin-arm64": "0.0.142"
20
+ "@vzn/vx-linux-x64": "0.0.143",
21
+ "@vzn/vx-linux-arm64": "0.0.143",
22
+ "@vzn/vx-darwin-x64": "0.0.143",
23
+ "@vzn/vx-darwin-arm64": "0.0.143"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/help.ts CHANGED
@@ -157,7 +157,8 @@ export function helpText(pluginCommands: readonly string[] = []): string {
157
157
  'Migration:',
158
158
  ' vx init Generate vx.workspace.ts + one vx.config.ts per package',
159
159
  ' from package.json scripts. Unmappable settings become',
160
- ' TODO(vx-migrate) comments.',
160
+ ' TODO(vx-migrate) comments. Beside turbo.json or nx.json:',
161
+ ' only vx.workspace.ts, declaring turbo() or nx().',
161
162
  ' --dry Print the generated files instead of writing them.',
162
163
  ' --force Overwrite existing vx.config.* files.',
163
164
  ' --plugin <seam> Instead: write plugins/<seam>.ts, a runnable plugin, and its',
package/src/cli/init.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  // `vx init --plugin <seam>` writes a runnable plugin for one seam and its
6
6
  // test instead (plugin-templates.ts, the examples the gate runs).
7
7
 
8
- import { mkdir } from 'node:fs/promises'
8
+ import { existsSync, readFileSync } from 'node:fs'
9
+ import { mkdir, unlink } from 'node:fs/promises'
9
10
  import path from 'node:path'
10
11
  import { flagHint, seeHelp } from './help.js'
11
12
  import { PLUGIN_TEMPLATES } from './plugin-templates.js'
@@ -17,6 +18,7 @@ import {
17
18
  listProjects,
18
19
  loadWorkspace,
19
20
  migrateScripts,
21
+ WORKSPACE_CONFIG_FILENAMES,
20
22
  } from '../workspace/index.js'
21
23
 
22
24
  export interface InitArgs {
@@ -76,7 +78,6 @@ export async function initCmd(args: readonly string[]): Promise<number> {
76
78
  // richer source (dependsOn, inputs, outputs) and was ignored without a
77
79
  // word — the walkthrough on a Turbo repo (2026-09-09) got the scripts'
78
80
  // TODOs and none of the edges turbo.json already declared.
79
- const notes: string[] = []
80
81
  // Turbo 2.5+ reads `turbo.jsonc` as well (item 938).
81
82
  let turbo: string | undefined
82
83
  for (const name of ['turbo.json', 'turbo.jsonc']) {
@@ -85,21 +86,14 @@ export async function initCmd(args: readonly string[]): Promise<number> {
85
86
  break
86
87
  }
87
88
  }
88
- if (turbo !== undefined) {
89
- notes.push(
90
- `${turbo} found and not read — ` +
91
- '`bunx @vzn/vx-migrate` maps it (dependsOn, inputs, ' +
92
- 'outputs), or `plugins: [turbo()]` from @vzn/vx-migrate runs it with nothing written',
93
- )
94
- } else if (
95
- (await Bun.file(path.join(root, 'nx.json')).exists()) ||
96
- (await Bun.file(path.join(root, '.nx', 'workspace-data', 'project-graph.json')).exists())
97
- ) {
98
- notes.push(
99
- 'an Nx workspace found and not read — `bunx @vzn/vx-migrate --from nx` maps its ' +
100
- 'exported project graph, or `plugins: [nx()]` from @vzn/vx-migrate runs it with ' +
101
- 'nothing written',
102
- )
89
+ // A Turbo or Nx repo already says its tasks: the runner's own config is
90
+ // the source, and `turbo()` / `nx()` read it live. Writing a config per
91
+ // package from the scripts dropped every edge turbo.json declares (the
92
+ // first-five-minutes walk, 2026-09-28); the workspace file alone is the
93
+ // whole adoption, and `bunx @vzn/vx-migrate` stays for freezing it.
94
+ if (turbo !== undefined) return adopt(root, 'turbo', turbo, parsed)
95
+ if (await Bun.file(path.join(root, 'nx.json')).exists()) {
96
+ return adopt(root, 'nx', 'nx.json', parsed)
103
97
  }
104
98
  return applyMigration({
105
99
  root,
@@ -110,7 +104,6 @@ export async function initCmd(args: readonly string[]): Promise<number> {
110
104
  dry: parsed.dry,
111
105
  force: parsed.force,
112
106
  init: true,
113
- notes,
114
107
  format: parsed.mjs ? 'mjs' : 'ts',
115
108
  })
116
109
  }
@@ -143,3 +136,92 @@ async function scaffoldPlugin(seam: string, args: InitArgs): Promise<number> {
143
136
  )
144
137
  return 0
145
138
  }
139
+
140
+ /**
141
+ * `vx init` in a Turbo or Nx repo: `vx.workspace.ts` declaring the plugin
142
+ * that runs the repo as it is, nothing else, and the one command that gets
143
+ * from here to a run.
144
+ */
145
+ async function adopt(
146
+ root: string,
147
+ runner: 'turbo' | 'nx',
148
+ source: string,
149
+ args: InitArgs,
150
+ ): Promise<number> {
151
+ const name = `vx.workspace.${args.mjs ? 'mjs' : 'ts'}`
152
+ const body = `import { ${runner} } from '@vzn/vx-migrate'\n\nexport default { plugins: [${runner}()] }`
153
+ const text = args.mjs
154
+ ? `${body}\n`
155
+ : `import type { WorkspaceConfig } from '@vzn/vx'\n${body} satisfies WorkspaceConfig\n`
156
+ const existing = WORKSPACE_CONFIG_FILENAMES.find((f) => existsSync(path.join(root, f)))
157
+ if (existing !== undefined && !args.force) {
158
+ const declared = readFileSync(path.join(root, existing), 'utf8').includes(`${runner}(`)
159
+ if (!declared) {
160
+ throw new UserError(
161
+ `vx init: ${existing} exists; add ${runner}() from @vzn/vx-migrate to its plugins, or --force replaces it`,
162
+ )
163
+ }
164
+ } else if (args.dry) {
165
+ process.stdout.write(`── ${name} ──\n${text}\n`)
166
+ } else {
167
+ if (existing !== undefined && existing !== name) await unlink(path.join(root, existing))
168
+ await Bun.write(path.join(root, name), text)
169
+ }
170
+ const wrote =
171
+ existing !== undefined && !args.force
172
+ ? `${existing} already declares ${runner}().`
173
+ : args.dry
174
+ ? `would write ${name} (dry run, nothing written).`
175
+ : `wrote ${name}.`
176
+ process.stdout.write(
177
+ `vx init: ${source} found — ${runner}() from @vzn/vx-migrate runs this repo as it is; nothing else written.\n` +
178
+ `${wrote}\n\nnext: ${adoptionNext(root, runner, source)}\n`,
179
+ )
180
+ return 0
181
+ }
182
+
183
+ type PackageManager = 'pnpm' | 'yarn' | 'bun' | 'npm'
184
+
185
+ const LOCKFILES: ReadonlyArray<[string, PackageManager]> = [
186
+ ['pnpm-lock.yaml', 'pnpm'],
187
+ ['yarn.lock', 'yarn'],
188
+ ['bun.lock', 'bun'],
189
+ ['bun.lockb', 'bun'],
190
+ ['package-lock.json', 'npm'],
191
+ ]
192
+
193
+ const INSTALL: Record<PackageManager, string> = {
194
+ pnpm: 'pnpm add -D -w',
195
+ yarn: 'yarn add -D',
196
+ bun: 'bun add -d',
197
+ npm: 'npm install -D',
198
+ }
199
+
200
+ const EXEC: Record<PackageManager, string> = { pnpm: 'pnpm', yarn: 'yarn', bun: 'bunx', npm: 'npx' }
201
+
202
+ /** Install what the workspace file imports, if missing, then run the repo's build. */
203
+ export function adoptionNext(root: string, runner: 'turbo' | 'nx', source: string): string {
204
+ const pm = LOCKFILES.find(([f]) => existsSync(path.join(root, f)))?.[1] ?? 'npm'
205
+ const missing = ['@vzn/vx', '@vzn/vx-migrate'].filter(
206
+ (p) => !existsSync(path.join(root, 'node_modules', p, 'package.json')),
207
+ )
208
+ // A global vx (no runner started this one) runs the workspace's plugins as they are.
209
+ const vx = process.env['npm_config_user_agent'] === undefined ? 'vx' : `${EXEC[pm]} vx`
210
+ const task = firstTask(path.join(root, source), runner)
211
+ const run = `${vx} run ${task} --all`
212
+ return missing.length === 0 ? run : `${INSTALL[pm]} ${missing.join(' ')} && ${run}`
213
+ }
214
+
215
+ /** `build` when the runner's config names it, else the first task it names. */
216
+ function firstTask(file: string, runner: 'turbo' | 'nx'): string {
217
+ let text: string
218
+ try {
219
+ text = readFileSync(file, 'utf8')
220
+ } catch {
221
+ return 'build'
222
+ }
223
+ if (/"build"\s*:/.test(text)) return 'build'
224
+ const field = runner === 'turbo' ? '(?:tasks|pipeline)' : 'targetDefaults'
225
+ const first = new RegExp(`"${field}"\\s*:\\s*\\{\\s*"([^"]+)"`).exec(text)?.[1]
226
+ return first ?? 'build'
227
+ }