@vzn/vx-migrate 0.0.626 → 0.0.628

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
@@ -219,8 +219,8 @@ bunx @vzn/vx-migrate --help # the usage, exit 0
219
219
  It is the one command a repo needs (`pnpx @vzn/vx-migrate` in a pnpm repo works the same):
220
220
 
221
221
  - In a terminal it asks which adoption you want: **native** (the default, a `vx.config.ts` per package) or **keep** (the workspace file `vx init` writes, declaring `turbo()` or `nx()`). `--native` / `--keep` answer it; with no terminal (CI, a pipe) it is native.
222
- - With no `vx.workspace.*` yet, it writes one declaring the plugins the repo calls for (`src/workspace-plugins.ts`): the `@vzn/vx-lockfile` factory for the lockfile (`pnpm()`, `bun()`, `npm()`, `yarn()`), `scheduleHistoryPlugin()`, and `github()` when `.github/workflows` exists, from `@vzn/vx-ci`, each installed beside `@vzn/vx` at vx-migrate's own version. Keep adds them to the file `vx init` writes, or to one already in that shape; any other workspace file is the user's and left alone.
223
- - It installs what the written files import with the repo's own manager (`packageManager`, else the lockfile): `@vzn/vx`, the declared plugins' packages, and `@vzn/vx-migrate` for keep or when a written task runs its `nx-exec` or `nx-env` bin (`pnpm add -D -w`, `yarn add -D -W` on Yarn 1 and without `-W` on Yarn 2+, `bun add -d`, `npm install -D`). A package the root both lists and has installed at vx-migrate's own version is left alone; any other is installed at that version. `--no-install` skips it.
222
+ - With no `vx.workspace.*` yet, it writes one declaring the plugins the repo calls for (`src/workspace-plugins.ts`): the `@vzn/vx-lockfile` factory for the lockfile (`pnpm()`, `bun()`, `npm()`, `yarn()`), `scheduleHistoryPlugin()`, and `github()` when `.github/workflows` exists, from `@vzn/vx-ci`, each installed beside `@vzn/vx` at vx-migrate's own version. Keep adds them to the file `vx init` writes, or to one already in that shape; any other workspace file is the user's and left alone. Native over a file vx wrote (a keep adoption's) rewrites it: `turbo()` and `nx()` go, since the configs written replace them, and the other plugins stay.
223
+ - It installs what the written files import with the repo's own manager (`packageManager`, else the lockfile): `@vzn/vx`, the declared plugins' packages, and `@vzn/vx-migrate` for keep or when a written task runs its `nx-exec` or `nx-env` bin (`pnpm add -D -w`, `yarn add -D -W` on Yarn 1 and without `-W` on Yarn 2+, `bun add -d`, `npm install -D`). A package the root both lists and has installed at vx-migrate's own version is left alone; any other is installed at that version. Native removes a listed `@vzn/vx-migrate` when nothing written needs it (no `nx-exec` / `nx-env` task, no `turboCache()` or `nxCache()` left in the workspace file): `pnpm remove -w`, `yarn remove` (`-W` on Yarn 1), `bun remove`, `npm uninstall`. `--no-install` skips both.
224
224
 
225
225
  A Lerna repo is an Nx one: Lerna 6+ runs `lerna run` on Nx's task runner over the graph `nx graph` exports, nx.json or not, so a `lerna.json` with no nx.json and no turbo.json is mapped from that graph, and keep writes the workspace file declaring `nx()` (`vx init` adopts by nx.json, and would map the scripts). Lerna is the installed one, else the root manifest's range; with neither, the lerna.json is another tool's (lerna-lite reads it too, and runs no Nx). A root whose scripts never `lerna run` runs its tasks another way and publishes with Lerna (webdriverio: `run-s`, `pnpm -r`), so its scripts stay the source. Not where Lerna runs its own runner: `useNx: false`, or Lerna 5 without `useNx: true`; those are `vx init`'s scripts mapping. Beside turbo.json, Turbo runs the tasks and Lerna only publishes: turbo.json is the source.
226
226
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx-migrate",
3
- "version": "0.0.626",
3
+ "version": "0.0.628",
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.626"
42
+ "@vzn/vx": "^0.0.628"
43
43
  },
44
44
  "repository": {
45
45
  "type": "git",
package/src/adopt.ts CHANGED
@@ -111,14 +111,9 @@ export function missingPackages(
111
111
  wanted: readonly string[],
112
112
  version: string | null = null,
113
113
  ): string[] {
114
- const pj = JSON.parse(readText(path.join(root, 'package.json')) || '{}') as Record<
115
- string,
116
- Record<string, unknown> | undefined
117
- >
118
- const listed = (name: string) =>
119
- ['dependencies', 'devDependencies'].some((f) => pj[f]?.[name] !== undefined)
114
+ const listed = new Set(listedPackages(root, wanted))
120
115
  return wanted.filter((p) => {
121
- if (!listed(p)) return true
116
+ if (!listed.has(p)) return true
122
117
  const installed = readText(path.join(root, 'node_modules', p, 'package.json'))
123
118
  if (installed === '') return true
124
119
  if (version === null) return false
@@ -126,17 +121,64 @@ export function missingPackages(
126
121
  })
127
122
  }
128
123
 
124
+ /** argv that removes `packages` from the root's dev dependencies. */
125
+ export function removeArgv(
126
+ root: string,
127
+ pm: PackageManager,
128
+ packages: readonly string[],
129
+ ): string[] {
130
+ switch (pm) {
131
+ case 'pnpm':
132
+ return ['pnpm', 'remove', '-w', ...packages]
133
+ case 'yarn':
134
+ return readText(path.join(root, 'yarn.lock')).includes('__metadata:')
135
+ ? ['yarn', 'remove', ...packages]
136
+ : ['yarn', 'remove', '-W', ...packages]
137
+ case 'bun':
138
+ return ['bun', 'remove', ...packages]
139
+ case 'npm':
140
+ return ['npm', 'uninstall', ...packages]
141
+ }
142
+ }
143
+
144
+ /** The `packages` the root package.json lists. */
145
+ export function listedPackages(root: string, packages: readonly string[]): string[] {
146
+ const pj = JSON.parse(readText(path.join(root, 'package.json')) || '{}') as Record<
147
+ string,
148
+ Record<string, unknown> | undefined
149
+ >
150
+ return packages.filter((p) =>
151
+ ['dependencies', 'devDependencies'].some((f) => pj[f]?.[p] !== undefined),
152
+ )
153
+ }
154
+
129
155
  /** Install `packages` (at `version`, given one) with the repo's manager, its output on the terminal. */
130
- export async function install(
156
+ export function install(
131
157
  root: string,
132
158
  packages: readonly string[],
133
159
  version: string | null = null,
134
160
  ): Promise<string> {
135
- const argv = installArgv(
161
+ return runManager(
136
162
  root,
137
- packageManagerOf(root, process.env['npm_config_user_agent']),
138
- version === null ? packages : packages.map((p) => `${p}@${version}`),
163
+ installArgv(
164
+ root,
165
+ packageManagerOf(root, process.env['npm_config_user_agent']),
166
+ version === null ? packages : packages.map((p) => `${p}@${version}`),
167
+ ),
168
+ 'installing vx',
139
169
  )
170
+ }
171
+
172
+ /** Remove `packages` with the repo's manager. */
173
+ export function uninstall(root: string, packages: readonly string[]): Promise<string> {
174
+ return runManager(
175
+ root,
176
+ removeArgv(root, packageManagerOf(root, process.env['npm_config_user_agent']), packages),
177
+ `removing ${packages.join(' ')}`,
178
+ )
179
+ }
180
+
181
+ async function runManager(root: string, argv: string[], what: string): Promise<string> {
140
182
  const line = argv.join(' ')
141
183
  process.stdout.write(`vx-migrate: ${line}\n`)
142
184
  let code: number
@@ -146,9 +188,7 @@ export async function install(
146
188
  code = -1
147
189
  }
148
190
  if (code !== 0) {
149
- throw new UserError(
150
- `installing vx failed (${line} exited ${code}); run it, then vx-migrate again`,
151
- )
191
+ throw new UserError(`${what} failed (${line} exited ${code}); run it, then vx-migrate again`)
152
192
  }
153
193
  return line
154
194
  }
package/src/migrate.ts CHANGED
@@ -31,17 +31,20 @@ import { readNxJson } from './nx/nx-map.js'
31
31
  import {
32
32
  type AdoptionMode,
33
33
  install,
34
+ listedPackages,
34
35
  missingPackages,
35
36
  MODE_QUESTION,
36
37
  ownVersion,
37
38
  packageManagerOf,
38
39
  parseModeAnswer,
40
+ uninstall,
39
41
  } from './adopt.js'
40
42
  import { migrateTurbo } from './migrate-turbo.js'
41
43
  import { migrateViteTask } from './migrate-vite-task.js'
42
44
  import { turboConfigFile } from './turbo/turbo-map.js'
43
45
  import {
44
46
  extendWorkspaceFile,
47
+ generatedPlugins,
45
48
  undeclared,
46
49
  renderWorkspaceFile,
47
50
  workspaceFileAt,
@@ -182,9 +185,20 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
182
185
  if (mode === 'keep' && runner !== 'vite-task') return keep(root, runner, hasTurbo, lerna, parsed)
183
186
 
184
187
  const format: MigrationFormat = parsed.mjs ? 'mjs' : 'ts'
185
- // No workspace file yet: this run writes one, declaring the plugins the
186
- // repo calls for and the run settings nx.json holds.
187
- const writesWorkspace = workspaceFileAt(root) === undefined
188
+ // No workspace file yet, or one vx wrote (a `--keep` adoption's): this run
189
+ // writes it, declaring the plugins the repo calls for and the run settings
190
+ // nx.json holds. turbo() and nx() go: the configs written here replace them.
191
+ const existingWorkspace = workspaceFileAt(root)
192
+ const existingFormat = /\.(ts|mjs)$/.exec(existingWorkspace ?? '')?.[1] as
193
+ | MigrationFormat
194
+ | undefined
195
+ const adopted =
196
+ existingWorkspace === undefined
197
+ ? []
198
+ : existingFormat === undefined
199
+ ? null
200
+ : generatedPlugins(readFileSync(path.join(root, existingWorkspace), 'utf8'))
201
+ const writesWorkspace = adopted !== null
188
202
  let source: string
189
203
  let plan: MigrationPlan
190
204
  if (runner === 'vite-task') {
@@ -238,8 +252,11 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
238
252
  existsSync(path.join(p.dir, configName))),
239
253
  ) ||
240
254
  plan.extraFiles.some((f) => existsSync(path.join(root, f.relPath))))
241
- // Drop the note that told the user to declare the lockfile plugin.
242
- const plugins = writesWorkspace ? workspacePlugins(root) : []
255
+ const kept = (adopted ?? []).filter((p) => !ADOPTION_PLUGINS.has(`${p.pkg}#${p.factory}`))
256
+ const plugins = writesWorkspace
257
+ ? [...kept, ...workspacePlugins(root).filter((p) => !kept.some((k) => k.factory === p.factory))]
258
+ : []
259
+ let workspaceText: string | undefined
243
260
  if (writesWorkspace) {
244
261
  const fields =
245
262
  runner === 'nx'
@@ -247,16 +264,18 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
247
264
  f.source === undefined ? [] : [{ field: f.field, source: f.source }],
248
265
  )
249
266
  : []
267
+ workspaceText = renderWorkspaceFile(plugins, existingFormat ?? format, fields)
268
+ // Drop the notes that told the user to declare the lockfile plugin and
269
+ // to remove turbo() or nx().
250
270
  plan = {
251
271
  ...plan,
252
- headerNotes: plan.headerNotes.filter((n) => !n.includes('from @vzn/vx-lockfile')),
253
- extraFiles: [
254
- ...plan.extraFiles,
255
- {
256
- relPath: `vx.workspace.${format}`,
257
- contents: renderWorkspaceFile(plugins, format, fields),
258
- },
259
- ],
272
+ headerNotes: plan.headerNotes.filter(
273
+ (n) => !n.includes('from @vzn/vx-lockfile') && !n.includes(' still declares '),
274
+ ),
275
+ extraFiles:
276
+ existingWorkspace === undefined
277
+ ? [...plan.extraFiles, { relPath: `vx.workspace.${format}`, contents: workspaceText }]
278
+ : plan.extraFiles,
260
279
  }
261
280
  }
262
281
  // An executor target is an `nx-exec` line and a `.env` one an `nx-env`
@@ -267,13 +286,26 @@ export async function migrateCmd(args: readonly string[]): Promise<number> {
267
286
  return typeof cmd === 'string' && /^nx-(exec|env) /.test(cmd)
268
287
  }),
269
288
  )
289
+ // Nothing written imports vx-migrate or runs its bins: a `--keep`
290
+ // adoption's install of it is dropped with turbo() / nx().
291
+ const needsSelf =
292
+ runsBins ||
293
+ (workspaceText ?? readFileSync(path.join(root, existingWorkspace!), 'utf8')).includes(
294
+ "'@vzn/vx-migrate'",
295
+ )
270
296
  const headerNotes = refused
271
297
  ? []
272
- : await prepareRepo(
273
- root,
274
- ['@vzn/vx', ...(runsBins ? ['@vzn/vx-migrate'] : []), ...plugins.map((p) => p.pkg)],
275
- parsed,
276
- )
298
+ : [
299
+ ...(existingWorkspace !== undefined && workspaceText !== undefined
300
+ ? await rewriteWorkspace(root, existingWorkspace, workspaceText, adopted!, parsed.dry)
301
+ : []),
302
+ ...(await prepareRepo(
303
+ root,
304
+ ['@vzn/vx', ...(needsSelf ? ['@vzn/vx-migrate'] : []), ...plugins.map((p) => p.pkg)],
305
+ parsed,
306
+ needsSelf ? [] : ['@vzn/vx-migrate'],
307
+ )),
308
+ ]
277
309
  return applyMigration({
278
310
  root,
279
311
  metas,
@@ -312,22 +344,52 @@ async function askMode(
312
344
  }
313
345
  }
314
346
 
347
+ const ADOPTION_PLUGINS = new Set(['@vzn/vx-migrate#turbo', '@vzn/vx-migrate#nx'])
348
+
315
349
  /**
316
- * Install what the written files import. Returns the report's line;
317
- * installs nothing under `--dry`.
350
+ * A `--keep` adoption's workspace file, written over for a native one.
351
+ * Returns the report's line; writes nothing under `--dry`.
352
+ */
353
+ async function rewriteWorkspace(
354
+ root: string,
355
+ file: string,
356
+ text: string,
357
+ before: readonly WorkspacePlugin[],
358
+ dry: boolean,
359
+ ): Promise<string[]> {
360
+ if (readFileSync(path.join(root, file), 'utf8') === text) return []
361
+ const dropped = before.filter((p) => ADOPTION_PLUGINS.has(`${p.pkg}#${p.factory}`))
362
+ const names = dropped.map((p) => `${p.factory}()`).join(', ')
363
+ if (dry)
364
+ return [`would ${dropped.length > 0 ? `drop ${names} from` : 'rewrite'} ${file} (dry run)`]
365
+ await Bun.write(path.join(root, file), text)
366
+ return [`${dropped.length > 0 ? `dropped ${names} from` : 'rewrote'} ${file}`]
367
+ }
368
+
369
+ /**
370
+ * Install what the written files import and remove what they no longer
371
+ * do. Returns the report's lines; changes nothing under `--dry`.
318
372
  */
319
373
  async function prepareRepo(
320
374
  root: string,
321
375
  wanted: readonly string[],
322
376
  args: MigrateArgs,
377
+ unwanted: readonly string[] = [],
323
378
  ): Promise<string[]> {
379
+ if (args.noInstall === true) return []
324
380
  const notes: string[] = []
325
381
  const version = ownVersion()
326
- const missing = args.noInstall === true ? [] : missingPackages(root, wanted, version)
382
+ const missing = missingPackages(root, wanted, version)
327
383
  if (missing.length > 0) {
328
384
  if (args.dry) notes.push(`would install ${missing.join(' ')} (dry run)`)
329
385
  else notes.push(`installed ${missing.join(' ')} (${await install(root, missing, version)})`)
330
386
  }
387
+ // Last: the package removed may be the one running this.
388
+ const listed = listedPackages(root, unwanted)
389
+ if (listed.length > 0) {
390
+ if (args.dry) notes.push(`would remove ${listed.join(' ')} (dry run)`)
391
+ else notes.push(`removed ${listed.join(' ')} (${await uninstall(root, listed)})`)
392
+ }
331
393
  return notes
332
394
  }
333
395
 
@@ -89,3 +89,32 @@ export function extendWorkspaceFile(
89
89
  `}${tail}`,
90
90
  ].join('\n')
91
91
  }
92
+
93
+ /**
94
+ * The plugins of a workspace file vx wrote (`vx init`, `--keep`, a native
95
+ * migration): `@vzn` imports and argument-less calls, nothing else. Null
96
+ * for any other file, which is the user's and left alone.
97
+ */
98
+ export function generatedPlugins(text: string): WorkspacePlugin[] | null {
99
+ const pkgOf = new Map<string, string>()
100
+ const body: string[] = []
101
+ for (const line of text.split('\n')) {
102
+ const m = /^import \{ ([\w, ]+) \} from '(@vzn\/[\w./-]+)'$/.exec(line)
103
+ if (m !== null) for (const name of m[1]!.split(',')) pkgOf.set(name.trim(), m[2]!)
104
+ else if (line !== "import type { WorkspaceConfig } from '@vzn/vx/config'") body.push(line)
105
+ }
106
+ const m =
107
+ /^export default \{\s*plugins:\s*\[((?:\s*\w+\(\)\s*,?)*)\s*\],?\s*\}(?: satisfies WorkspaceConfig)?$/.exec(
108
+ body.join('\n').trim(),
109
+ )
110
+ if (m === null) return null
111
+ const factories = [...m[1]!.matchAll(/(\w+)\(\)/g)].map((c) => c[1]!)
112
+ // No plugin at all is a file vx never wrote.
113
+ if (
114
+ factories.length === 0 ||
115
+ factories.length !== pkgOf.size ||
116
+ factories.some((f) => !pkgOf.has(f))
117
+ )
118
+ return null
119
+ return factories.map((factory) => ({ pkg: pkgOf.get(factory)!, factory }))
120
+ }