@next/codemod 16.3.0-canary.71 → 16.3.0-canary.73

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.
@@ -43,6 +43,7 @@ program
43
43
  .argument('[revision]', 'Specify the upgrade type ("patch", "minor", "major"), an NPM dist tag (e.g. "latest", "canary", "rc"), or an exact version (e.g. "15.0.0"). Defaults to "minor".')
44
44
  .usage('[revision] [options]')
45
45
  .option('--verbose', 'Verbose output', false)
46
+ .option('-y, --yes', 'Skip every interactive prompt and accept its default. Also auto-enabled when stdin is not a TTY (e.g. running under an agent or in CI).', false)
46
47
  .action(async (revision, options) => {
47
48
  try {
48
49
  await (0, upgrade_1.runUpgrade)(revision, options);
package/bin/upgrade.js CHANGED
@@ -114,6 +114,10 @@ function resolveSemanticRevision(revision, installedVersion) {
114
114
  }
115
115
  async function runUpgrade(revision, options) {
116
116
  const { verbose } = options;
117
+ const nonInteractive = options.yes === true || !process.stdin.isTTY;
118
+ if (nonInteractive) {
119
+ console.log(` Running in non-interactive mode. Every prompt will accept its default.`);
120
+ }
117
121
  const appPackageJsonPath = path_1.default.resolve(cwd, 'package.json');
118
122
  let appPackageJson = JSON.parse(fs_1.default.readFileSync(appPackageJsonPath, 'utf8'));
119
123
  const installedNextVersion = getInstalledNextVersion();
@@ -189,18 +193,24 @@ async function runUpgrade(revision, options) {
189
193
  // The mixed case is tricky to handle from a types perspective.
190
194
  // We'll recommend to upgrade in the prompt but users can decide to try 18.
191
195
  !isPureAppRouter) {
192
- const shouldStayOnReact18Res = await (0, prompts_1.default)({
193
- type: 'confirm',
194
- name: 'shouldStayOnReact18',
195
- message: `Do you prefer to stay on React 18?` +
196
- (isMixedApp
197
- ? " Since you're using both pages/ and app/, we recommend upgrading React to use a consistent version throughout your app."
198
- : ''),
199
- initial: false,
200
- active: 'Yes',
201
- inactive: 'No',
202
- }, { onCancel: utils_1.onCancel });
203
- shouldStayOnReact18 = shouldStayOnReact18Res.shouldStayOnReact18;
196
+ if (nonInteractive) {
197
+ // Default: upgrade React past 18.
198
+ shouldStayOnReact18 = false;
199
+ }
200
+ else {
201
+ const shouldStayOnReact18Res = await (0, prompts_1.default)({
202
+ type: 'confirm',
203
+ name: 'shouldStayOnReact18',
204
+ message: `Do you prefer to stay on React 18?` +
205
+ (isMixedApp
206
+ ? " Since you're using both pages/ and app/, we recommend upgrading React to use a consistent version throughout your app."
207
+ : ''),
208
+ initial: false,
209
+ active: 'Yes',
210
+ inactive: 'No',
211
+ }, { onCancel: utils_1.onCancel });
212
+ shouldStayOnReact18 = shouldStayOnReact18Res.shouldStayOnReact18;
213
+ }
204
214
  }
205
215
  // We're resolving a specific version here to avoid including "ugly" version queries
206
216
  // in the manifest.
@@ -212,9 +222,9 @@ async function runUpgrade(revision, options) {
212
222
  : await loadHighestNPMVersionMatching(`react@${targetNextPackageJson.peerDependencies['react']}`);
213
223
  if ((0, semver_1.compare)(targetNextVersion, '15.0.0-canary') >= 0 &&
214
224
  (0, semver_1.compare)(targetNextVersion, '16.0.0-canary') < 0) {
215
- await suggestTurbopack(appPackageJson, targetNextVersion);
225
+ await suggestTurbopack(appPackageJson, targetNextVersion, nonInteractive);
216
226
  }
217
- const codemods = await suggestCodemods(installedNextVersion, targetNextVersion);
227
+ const codemods = await suggestCodemods(installedNextVersion, targetNextVersion, nonInteractive);
218
228
  const packageManager = (0, handle_package_1.getPkgManager)(cwd);
219
229
  let shouldRunReactCodemods = false;
220
230
  let shouldRunReactTypesCodemods = false;
@@ -223,8 +233,9 @@ async function runUpgrade(revision, options) {
223
233
  if (!shouldStayOnReact18 &&
224
234
  (0, semver_1.compare)(targetReactVersion, '19.0.0-0') >= 0 &&
225
235
  (0, semver_1.compare)(installedReactVersion, '19.0.0-0') < 0) {
226
- shouldRunReactCodemods = await suggestReactCodemods();
227
- shouldRunReactTypesCodemods = await suggestReactTypesCodemods();
236
+ shouldRunReactCodemods = await suggestReactCodemods(nonInteractive);
237
+ shouldRunReactTypesCodemods =
238
+ await suggestReactTypesCodemods(nonInteractive);
228
239
  execCommand = getNpxCommand(packageManager);
229
240
  }
230
241
  fs_1.default.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2));
@@ -284,6 +295,37 @@ async function runUpgrade(revision, options) {
284
295
  };
285
296
  }
286
297
  }
298
+ // Bump `eslint` alongside `eslint-config-next` so the install doesn't fail
299
+ // on a peer-dep mismatch. e.g. `eslint-config-next@16.x` requires
300
+ // `eslint@>=9`, but a project upgrading from Next 15 will still have
301
+ // `eslint@^8` from create-next-app. Skip silently if anything goes wrong;
302
+ // the worst case is the user hits the same peer-dep error they would have
303
+ // without this bump.
304
+ //
305
+ // Only act when the project is actually using `eslint-config-next` — we
306
+ // don't want to silently upgrade eslint majors for projects that use
307
+ // eslint for unrelated reasons.
308
+ if (allDependencies['eslint'] && allDependencies['eslint-config-next']) {
309
+ try {
310
+ const eslintConfigNextPeerDepsJSON = (0, child_process_1.execSync)(`npm --silent view "eslint-config-next@${targetNextVersion}" peerDependencies --json`, { encoding: 'utf-8' });
311
+ const eslintConfigNextPeerDeps = eslintConfigNextPeerDepsJSON.trim() === ''
312
+ ? {}
313
+ : JSON.parse(eslintConfigNextPeerDepsJSON);
314
+ const eslintRange = eslintConfigNextPeerDeps?.eslint;
315
+ if (eslintRange) {
316
+ const targetEslintVersion = await loadHighestNPMVersionMatching(`eslint@${eslintRange}`);
317
+ versionMapping['eslint'] = {
318
+ version: targetEslintVersion,
319
+ required: false,
320
+ };
321
+ }
322
+ }
323
+ catch (e) {
324
+ if (verbose) {
325
+ console.warn(` Could not determine eslint peer range from eslint-config-next@${targetNextVersion}. Leaving eslint version alone.`, e);
326
+ }
327
+ }
328
+ }
287
329
  // Even though we only need those if we alias `@types/react` to types-react,
288
330
  // we still do it out of safety due to https://github.com/microsoft/DefinitelyTyped-tools/issues/433.
289
331
  const overrides = {};
@@ -320,10 +362,11 @@ async function runUpgrade(revision, options) {
320
362
  // understanding of the codemods, we run all of the applicable codemods.
321
363
  if (shouldRunReactCodemods) {
322
364
  // https://react.dev/blog/2024/04/25/react-19-upgrade-guide#run-all-react-19-codemods
323
- (0, child_process_1.execSync)(
324
365
  // `--no-interactive` skips the interactive prompt that asks for confirmation
325
366
  // https://github.com/codemod-com/codemod/blob/c0cf00d13161a0ec0965b6cc6bc5d54076839cc8/apps/cli/src/flags.ts#L160
326
- `${execCommand} codemod@latest react/19/migration-recipe --no-interactive`, { stdio: 'inherit' });
367
+ // `--allow-dirty` is required because the upgrade above modified package.json
368
+ // and the lockfile; the recipe refuses to run on a dirty tree otherwise.
369
+ (0, child_process_1.execSync)(`${execCommand} codemod@latest react/19/migration-recipe --no-interactive --allow-dirty`, { stdio: 'inherit' });
327
370
  }
328
371
  if (shouldRunReactTypesCodemods) {
329
372
  // https://react.dev/blog/2024/04/25/react-19-upgrade-guide#typescript-changes
@@ -383,7 +426,7 @@ function isUsingAppDir(projectPath) {
383
426
  * 3. Otherwise, we ask the user to manually add `--turbopack` to their dev command,
384
427
  * showing the current dev command as the initial value.
385
428
  */
386
- async function suggestTurbopack(packageJson, targetNextVersion) {
429
+ async function suggestTurbopack(packageJson, targetNextVersion, nonInteractive) {
387
430
  const devScript = packageJson.scripts?.['dev'];
388
431
  // Turbopack flag was changed from `--turbo` to `--turbopack` in v15.0.1-canary.3
389
432
  // PR: https://github.com/vercel/next.js/pull/71657
@@ -406,19 +449,28 @@ async function suggestTurbopack(packageJson, targetNextVersion) {
406
449
  }
407
450
  return;
408
451
  }
409
- const responseTurbopack = await (0, prompts_1.default)({
410
- type: 'confirm',
411
- name: 'enable',
412
- message: `Enable Turbopack for ${picocolors_1.default.bold('next dev')}?`,
413
- initial: true,
414
- }, { onCancel: utils_1.onCancel });
415
- if (!responseTurbopack.enable) {
452
+ let enable = true;
453
+ if (!nonInteractive) {
454
+ const responseTurbopack = await (0, prompts_1.default)({
455
+ type: 'confirm',
456
+ name: 'enable',
457
+ message: `Enable Turbopack for ${picocolors_1.default.bold('next dev')}?`,
458
+ initial: true,
459
+ }, { onCancel: utils_1.onCancel });
460
+ enable = responseTurbopack.enable;
461
+ }
462
+ if (!enable) {
416
463
  return;
417
464
  }
418
465
  packageJson.scripts['dev'] = devScript.replace('next dev', `next dev ${turboPackFlag}`);
419
466
  return;
420
467
  }
421
468
  console.log(`${picocolors_1.default.yellow('⚠')} Could not find "${picocolors_1.default.bold('next dev')}" in your dev script.`);
469
+ if (nonInteractive) {
470
+ // Without a TTY we can't ask the user for a replacement script.
471
+ // Keep the existing dev script untouched.
472
+ return;
473
+ }
422
474
  const responseCustomDevScript = await (0, prompts_1.default)({
423
475
  type: 'text',
424
476
  name: 'customDevScript',
@@ -428,7 +480,7 @@ async function suggestTurbopack(packageJson, targetNextVersion) {
428
480
  packageJson.scripts['dev'] =
429
481
  responseCustomDevScript.customDevScript || devScript;
430
482
  }
431
- async function suggestCodemods(initialNextVersion, targetNextVersion) {
483
+ async function suggestCodemods(initialNextVersion, targetNextVersion, nonInteractive) {
432
484
  // example:
433
485
  // codemod version: 15.0.0-canary.45
434
486
  // 14.3 -> 15.0.0-canary.45: apply
@@ -451,6 +503,13 @@ async function suggestCodemods(initialNextVersion, targetNextVersion) {
451
503
  if (relevantCodemods.length === 0) {
452
504
  return [];
453
505
  }
506
+ if (nonInteractive) {
507
+ // Default: apply every recommended codemod, matching `selected: true` below.
508
+ const all = relevantCodemods.map(({ value }) => value);
509
+ console.log(` Applying all ${picocolors_1.default.blue('codemods')} recommended for your upgrade:\n` +
510
+ all.map((value) => ` - ${value}`).join('\n'));
511
+ return all;
512
+ }
454
513
  const { codemods } = await (0, prompts_1.default)({
455
514
  type: 'multiselect',
456
515
  name: 'codemods',
@@ -466,7 +525,10 @@ async function suggestCodemods(initialNextVersion, targetNextVersion) {
466
525
  }, { onCancel: utils_1.onCancel });
467
526
  return codemods;
468
527
  }
469
- async function suggestReactCodemods() {
528
+ async function suggestReactCodemods(nonInteractive) {
529
+ if (nonInteractive) {
530
+ return true;
531
+ }
470
532
  const { runReactCodemod } = await (0, prompts_1.default)({
471
533
  type: 'confirm',
472
534
  name: 'runReactCodemod',
@@ -475,7 +537,10 @@ async function suggestReactCodemods() {
475
537
  }, { onCancel: utils_1.onCancel });
476
538
  return runReactCodemod;
477
539
  }
478
- async function suggestReactTypesCodemods() {
540
+ async function suggestReactTypesCodemods(nonInteractive) {
541
+ if (nonInteractive) {
542
+ return true;
543
+ }
479
544
  const { runReactTypesCodemod } = await (0, prompts_1.default)({
480
545
  type: 'confirm',
481
546
  name: 'runReactTypesCodemod',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/codemod",
3
- "version": "16.3.0-canary.71",
3
+ "version": "16.3.0-canary.73",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",