@next/codemod 16.3.0-canary.8 → 16.3.0-canary.80

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/bin/agents-md.js CHANGED
@@ -59,8 +59,17 @@ async function runAgentsMd(options) {
59
59
  targetFile = promptedOptions.targetFile;
60
60
  }
61
61
  const claudeMdPath = path_1.default.join(cwd, targetFile);
62
- const docsPath = path_1.default.join(cwd, DOCS_DIR_NAME);
63
- const docsLinkPath = `./${DOCS_DIR_NAME}`;
62
+ // Next.js >= 16.2.0 ships its docs inside the published package. When the
63
+ // installed version matches the requested one, index the bundled docs
64
+ // directly instead of downloading a copy into .next-docs.
65
+ const bundledDocs = (0, agents_md_1.getBundledDocsInfo)(cwd);
66
+ const useBundledDocs = bundledDocs !== null && bundledDocs.version === nextjsVersion;
67
+ const docsPath = useBundledDocs
68
+ ? bundledDocs.docsPath
69
+ : path_1.default.join(cwd, DOCS_DIR_NAME);
70
+ const docsLinkPath = useBundledDocs
71
+ ? (0, agents_md_1.getBundledDocsLinkPath)(cwd, bundledDocs.docsPath)
72
+ : `./${DOCS_DIR_NAME}`;
64
73
  let sizeBefore = 0;
65
74
  let isNewFile = true;
66
75
  let existingContent = '';
@@ -69,14 +78,19 @@ async function runAgentsMd(options) {
69
78
  sizeBefore = Buffer.byteLength(existingContent, 'utf-8');
70
79
  isNewFile = false;
71
80
  }
72
- console.log(`\nDownloading Next.js ${picocolors_1.default.cyan(nextjsVersion)} documentation to ${picocolors_1.default.cyan(DOCS_DIR_NAME)}...`);
73
- const pullResult = await (0, agents_md_1.pullDocs)({
74
- cwd,
75
- version: nextjsVersion,
76
- docsDir: docsPath,
77
- });
78
- if (!pullResult.success) {
79
- throw new shared_1.BadInput(`Failed to pull docs: ${pullResult.error}`);
81
+ if (useBundledDocs) {
82
+ console.log(`\nUsing the docs bundled with Next.js ${picocolors_1.default.cyan(nextjsVersion)} at ${picocolors_1.default.cyan(docsLinkPath)} (no download needed).`);
83
+ }
84
+ else {
85
+ console.log(`\nDownloading Next.js ${picocolors_1.default.cyan(nextjsVersion)} documentation to ${picocolors_1.default.cyan(DOCS_DIR_NAME)}...`);
86
+ const pullResult = await (0, agents_md_1.pullDocs)({
87
+ cwd,
88
+ version: nextjsVersion,
89
+ docsDir: docsPath,
90
+ });
91
+ if (!pullResult.success) {
92
+ throw new shared_1.BadInput(`Failed to pull docs: ${pullResult.error}`);
93
+ }
80
94
  }
81
95
  const docFiles = (0, agents_md_1.collectDocFiles)(docsPath);
82
96
  const sections = (0, agents_md_1.buildDocTree)(docFiles);
@@ -88,13 +102,15 @@ async function runAgentsMd(options) {
88
102
  const newContent = (0, agents_md_1.injectIntoClaudeMd)(existingContent, indexContent);
89
103
  fs_1.default.writeFileSync(claudeMdPath, newContent, 'utf-8');
90
104
  const sizeAfter = Buffer.byteLength(newContent, 'utf-8');
91
- const gitignoreResult = (0, agents_md_1.ensureGitignoreEntry)(cwd);
105
+ // .next-docs only exists on the download path; bundled docs live in
106
+ // node_modules, which is already ignored.
107
+ const gitignoreResult = useBundledDocs ? null : (0, agents_md_1.ensureGitignoreEntry)(cwd);
92
108
  const action = isNewFile ? 'Created' : 'Updated';
93
109
  const sizeInfo = isNewFile
94
110
  ? formatSize(sizeAfter)
95
111
  : `${formatSize(sizeBefore)} → ${formatSize(sizeAfter)}`;
96
112
  console.log(`${picocolors_1.default.green('✓')} ${action} ${picocolors_1.default.bold(targetFile)} (${sizeInfo})`);
97
- if (gitignoreResult.updated) {
113
+ if (gitignoreResult?.updated) {
98
114
  console.log(`${picocolors_1.default.green('✓')} Added ${picocolors_1.default.bold(DOCS_DIR_NAME)} to .gitignore`);
99
115
  }
100
116
  console.log('');
@@ -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',
@@ -500,6 +565,16 @@ function writeOverridesField(packageJson, packageManager, overrides) {
500
565
  }
501
566
  }
502
567
  else if (packageManager === 'pnpm') {
568
+ // pnpm v11 silently ignores `pnpm.overrides` in package.json. The
569
+ // canonical location moved to `pnpm-workspace.yaml#overrides`.
570
+ // See https://pnpm.io/settings and https://github.com/pnpm/pnpm/issues/11536.
571
+ // When the version cannot be detected, assume the current (v11+) layout
572
+ // since that's the surface where silently-dropped overrides hurt most.
573
+ const pnpmMajorVersion = (0, handle_package_1.getPnpmMajorVersion)();
574
+ if (pnpmMajorVersion === null || pnpmMajorVersion >= 11) {
575
+ writePnpmWorkspaceOverrides(overrides);
576
+ return;
577
+ }
503
578
  // pnpm supports pnpm.overrides and pnpm.resolutions
504
579
  if (packageJson.resolutions) {
505
580
  for (const [key, value] of entries) {
@@ -545,6 +620,28 @@ function writeOverridesField(packageJson, packageManager, overrides) {
545
620
  }
546
621
  }
547
622
  }
623
+ function writePnpmWorkspaceOverrides(overrides) {
624
+ // Deferred require so `js-yaml` is only loaded when we hit the pnpm v11+
625
+ // branch (i.e. not for npm/yarn/bun/pnpm-v10 upgrades). The package is CJS,
626
+ // so a sync `require()` keeps this function synchronous.
627
+ const yaml = require('js-yaml');
628
+ const filePath = path_1.default.join(cwd, 'pnpm-workspace.yaml');
629
+ let doc = {};
630
+ if (fs_1.default.existsSync(filePath)) {
631
+ const existing = fs_1.default.readFileSync(filePath, 'utf8');
632
+ const parsed = yaml.load(existing);
633
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
634
+ doc = parsed;
635
+ }
636
+ }
637
+ if (!doc.overrides || typeof doc.overrides !== 'object') {
638
+ doc.overrides = {};
639
+ }
640
+ for (const [key, value] of Object.entries(overrides)) {
641
+ doc.overrides[key] = value;
642
+ }
643
+ fs_1.default.writeFileSync(filePath, yaml.dump(doc));
644
+ }
548
645
  function warnDependenciesOutOfRange(appPackageJson, versionMapping) {
549
646
  const allDirectDependencies = {
550
647
  ...appPackageJson.dependencies,
@@ -293,6 +293,123 @@ This is my project documentation.
293
293
  }
294
294
  }, 30000) // Increase timeout for git clone
295
295
 
296
+ describe('bundled docs (Next.js >= 16.2.0)', () => {
297
+ // Simulate an install of a Next.js version that ships docs inside the
298
+ // published package at node_modules/next/dist/docs.
299
+ function setupBundledNext(projectDir, version) {
300
+ const nextDir = path.join(projectDir, 'node_modules', 'next')
301
+ const gettingStartedDir = path.join(
302
+ nextDir,
303
+ 'dist',
304
+ 'docs',
305
+ '01-app',
306
+ '01-getting-started'
307
+ )
308
+ fs.mkdirSync(gettingStartedDir, { recursive: true })
309
+ fs.writeFileSync(
310
+ path.join(nextDir, 'package.json'),
311
+ JSON.stringify({ name: 'next', version })
312
+ )
313
+ fs.writeFileSync(
314
+ path.join(nextDir, 'dist', 'docs', 'index.md'),
315
+ '# Next.js Docs'
316
+ )
317
+ fs.writeFileSync(
318
+ path.join(gettingStartedDir, '01-installation.md'),
319
+ '# Installation'
320
+ )
321
+ fs.writeFileSync(
322
+ path.join(gettingStartedDir, '02-project-structure.md'),
323
+ '# Project Structure'
324
+ )
325
+ }
326
+
327
+ it('indexes bundled docs instead of downloading when installed Next.js ships them', async () => {
328
+ setupBundledNext(testProjectDir, '16.2.0')
329
+
330
+ const originalCwd = process.cwd()
331
+ process.chdir(testProjectDir)
332
+
333
+ try {
334
+ await runAgentsMd({ output: 'CLAUDE.md' })
335
+
336
+ // No .next-docs copy and no .gitignore entry for it
337
+ expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
338
+ false
339
+ )
340
+ expect(fs.existsSync(path.join(testProjectDir, '.gitignore'))).toBe(
341
+ false
342
+ )
343
+
344
+ const claudeMdContent = fs.readFileSync(
345
+ path.join(testProjectDir, 'CLAUDE.md'),
346
+ 'utf-8'
347
+ )
348
+ expect(claudeMdContent).toContain(
349
+ 'root: ./node_modules/next/dist/docs'
350
+ )
351
+ expect(claudeMdContent).toContain('01-installation.md')
352
+
353
+ const output = consoleOutput.join('\n')
354
+ expect(output).toContain('bundled with Next.js')
355
+ expect(output).not.toContain('Downloading')
356
+ } finally {
357
+ process.chdir(originalCwd)
358
+ }
359
+ })
360
+
361
+ it('uses bundled docs when --version matches the installed version', async () => {
362
+ setupBundledNext(testProjectDir, '16.2.0')
363
+
364
+ const originalCwd = process.cwd()
365
+ process.chdir(testProjectDir)
366
+
367
+ try {
368
+ await runAgentsMd({ version: '16.2.0', output: 'AGENTS.md' })
369
+
370
+ expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
371
+ false
372
+ )
373
+
374
+ const agentsMdContent = fs.readFileSync(
375
+ path.join(testProjectDir, 'AGENTS.md'),
376
+ 'utf-8'
377
+ )
378
+ expect(agentsMdContent).toContain(
379
+ 'root: ./node_modules/next/dist/docs'
380
+ )
381
+ } finally {
382
+ process.chdir(originalCwd)
383
+ }
384
+ })
385
+
386
+ it('falls back to downloading when --version differs from the installed version', async () => {
387
+ setupBundledNext(testProjectDir, '16.2.0')
388
+
389
+ const originalCwd = process.cwd()
390
+ process.chdir(testProjectDir)
391
+
392
+ try {
393
+ await runAgentsMd({ version: '15.0.0', output: 'CLAUDE.md' })
394
+
395
+ expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
396
+ true
397
+ )
398
+
399
+ const claudeMdContent = fs.readFileSync(
400
+ path.join(testProjectDir, 'CLAUDE.md'),
401
+ 'utf-8'
402
+ )
403
+ expect(claudeMdContent).toContain('root: ./.next-docs')
404
+
405
+ const output = consoleOutput.join('\n')
406
+ expect(output).toContain('Downloading')
407
+ } finally {
408
+ process.chdir(originalCwd)
409
+ }
410
+ }, 30000) // Increase timeout for git clone
411
+ })
412
+
296
413
  describe('getNextjsVersion', () => {
297
414
  const fixturesDir = path.join(__dirname, 'fixtures/agents-md')
298
415
 
package/lib/agents-md.js CHANGED
@@ -10,6 +10,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.getNextjsVersion = getNextjsVersion;
13
+ exports.getBundledDocsInfo = getBundledDocsInfo;
14
+ exports.getBundledDocsLinkPath = getBundledDocsLinkPath;
13
15
  exports.pullDocs = pullDocs;
14
16
  exports.collectDocFiles = collectDocFiles;
15
17
  exports.buildDocTree = buildDocTree;
@@ -45,6 +47,36 @@ function getNextjsVersion(cwd) {
45
47
  };
46
48
  }
47
49
  }
50
+ /**
51
+ * Next.js ships its documentation inside the published package (at
52
+ * `dist/docs`) since 16.2.0. When the install resolved from `cwd` has
53
+ * bundled docs, the index can point at them directly instead of
54
+ * downloading a copy into `.next-docs`.
55
+ */
56
+ function getBundledDocsInfo(cwd) {
57
+ try {
58
+ const nextPkgPath = require.resolve('next/package.json', { paths: [cwd] });
59
+ const pkg = JSON.parse(fs_1.default.readFileSync(nextPkgPath, 'utf-8'));
60
+ const docsPath = path_1.default.join(path_1.default.dirname(nextPkgPath), 'dist', 'docs');
61
+ if (!pkg.version || collectDocFiles(docsPath).length === 0) {
62
+ return null;
63
+ }
64
+ return { docsPath, version: pkg.version };
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ function getBundledDocsLinkPath(cwd, docsPath) {
71
+ // Prefer the conventional path when it resolves from the project
72
+ // (covers hoisted installs; pnpm exposes next via a node_modules symlink).
73
+ const conventional = path_1.default.join(cwd, 'node_modules', 'next', 'dist', 'docs');
74
+ if (fs_1.default.existsSync(conventional)) {
75
+ return './node_modules/next/dist/docs';
76
+ }
77
+ const relative = path_1.default.relative(cwd, docsPath).replace(/\\/g, '/');
78
+ return relative.startsWith('.') ? relative : `./${relative}`;
79
+ }
48
80
  function versionToGitHubTag(version) {
49
81
  return version.startsWith('v') ? version : `v${version}`;
50
82
  }
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getPackageManagerVersion = getPackageManagerVersion;
7
+ exports.getPnpmMajorVersion = getPnpmMajorVersion;
6
8
  exports.getPkgManager = getPkgManager;
7
9
  exports.uninstallPackage = uninstallPackage;
8
10
  exports.installPackages = installPackages;
@@ -10,7 +12,49 @@ exports.runInstallation = runInstallation;
10
12
  exports.addPackageDependency = addPackageDependency;
11
13
  const find_up_1 = __importDefault(require("find-up"));
12
14
  const execa_1 = __importDefault(require("execa"));
15
+ const node_child_process_1 = require("node:child_process");
13
16
  const node_path_1 = require("node:path");
17
+ /**
18
+ * Get the full version string for the given package manager.
19
+ *
20
+ * First tries to parse from `npm_config_user_agent` (e.g., "pnpm/9.13.2 npm/? ..."),
21
+ * then falls back to spawning `<packageManager> --version`.
22
+ *
23
+ * Returns null if unable to determine the version.
24
+ *
25
+ * Mirrors `packages/create-next-app/helpers/get-pkg-manager.ts`.
26
+ */
27
+ function getPackageManagerVersion(packageManager) {
28
+ const userAgent = process.env.npm_config_user_agent || '';
29
+ const userAgentMatch = userAgent.match(new RegExp(`${packageManager}/([\\d.]+[\\w.-]*)`));
30
+ if (userAgentMatch) {
31
+ return userAgentMatch[1];
32
+ }
33
+ try {
34
+ const version = (0, node_child_process_1.execSync)(`${packageManager} --version`, {
35
+ encoding: 'utf8',
36
+ stdio: ['pipe', 'pipe', 'ignore'],
37
+ }).trim();
38
+ if (/^\d+\.\d+\.\d+/.test(version)) {
39
+ return version;
40
+ }
41
+ }
42
+ catch {
43
+ // package manager not available or failed to run
44
+ }
45
+ return null;
46
+ }
47
+ /**
48
+ * Get the major version of pnpm being used.
49
+ * Returns null if unable to determine the version.
50
+ */
51
+ function getPnpmMajorVersion() {
52
+ const version = getPackageManagerVersion('pnpm');
53
+ if (!version)
54
+ return null;
55
+ const major = parseInt(version.split('.')[0], 10);
56
+ return Number.isNaN(major) ? null : major;
57
+ }
14
58
  function getPkgManager(baseDir) {
15
59
  try {
16
60
  const lockFile = find_up_1.default.sync([
package/lib/utils.js CHANGED
@@ -132,5 +132,10 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
132
132
  value: 'remove-experimental-ppr',
133
133
  version: '16.0.0-canary.11',
134
134
  },
135
+ {
136
+ title: 'Add `export const instant = false` to App Router pages and layouts to ease Cache Components adoption',
137
+ value: 'cache-components-instant-false',
138
+ version: '16.3.0',
139
+ },
135
140
  ];
136
141
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/codemod",
3
- "version": "16.3.0-canary.8",
3
+ "version": "16.3.0-canary.80",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,6 +14,7 @@
14
14
  "find-up": "4.1.0",
15
15
  "globby": "11.0.1",
16
16
  "is-git-clean": "1.1.0",
17
+ "js-yaml": "4.2.0",
17
18
  "jscodeshift": "17.0.0",
18
19
  "picocolors": "1.0.0",
19
20
  "prompts": "2.4.2",
@@ -27,19 +28,19 @@
27
28
  "lib/**/*.js",
28
29
  "lib/cra-to-next/gitignore"
29
30
  ],
30
- "scripts": {
31
- "build": "pnpm tsc -d -p tsconfig.json",
32
- "prepublishOnly": "cd ../../ && turbo run build",
33
- "dev": "pnpm tsc -d -w -p tsconfig.json",
34
- "test": "jest",
35
- "test:upgrade-fixture": "./scripts/test-upgrade-fixture.sh"
36
- },
37
31
  "bin": "./bin/next-codemod.js",
38
32
  "devDependencies": {
39
33
  "@types/find-up": "4.0.0",
34
+ "@types/js-yaml": "4.0.9",
40
35
  "@types/jscodeshift": "0.11.0",
41
36
  "@types/prompts": "2.4.2",
42
37
  "@types/semver": "7.3.1",
43
38
  "typescript": "6.0.2"
39
+ },
40
+ "scripts": {
41
+ "build": "pnpm tsc -d -p tsconfig.json",
42
+ "dev": "pnpm tsc -d -w -p tsconfig.json",
43
+ "test": "jest",
44
+ "test:upgrade-fixture": "./scripts/test-upgrade-fixture.sh"
44
45
  }
45
- }
46
+ }
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = transformer;
4
+ const parser_1 = require("../lib/parser");
5
+ /**
6
+ * Blanket-inserts `export const instant = false` into every App Router `page`,
7
+ * `layout`, and `default` file so they're marked as allowed to block when
8
+ * `cacheComponents` is enabled. Each opt-out is meant to be walked back, one
9
+ * route at a time, using the companion adoption skill.
10
+ *
11
+ * - Skips files that already declare or export `instant` in any form (never
12
+ * overrides existing config or appends a duplicate binding).
13
+ * - Skips Client/Server Component modules (`"use client"` / `"use server"`):
14
+ * `instant` is a Server Component route segment config, so exporting it from
15
+ * those modules is a build error.
16
+ * - Targets `page` / `layout` / `default` only (not `route` — `instant` does
17
+ * not apply to route handlers). `default.tsx` is the parallel-route fallback,
18
+ * a server segment that accepts route segment config like the other two.
19
+ */
20
+ function transformer(file, _api) {
21
+ if (process.env.NODE_ENV !== 'test' &&
22
+ !/(^|[/\\])app[/\\].*?(page|layout|default)\.[^/\\]+$/.test(file.path)) {
23
+ return file.source;
24
+ }
25
+ const j = (0, parser_1.createParserFromPath)(file.path);
26
+ const root = j(file.source);
27
+ // Bail on Client/Server Component modules. `instant` is a Server Component
28
+ // route segment config; exporting it from a `"use client"` (or `"use server"`)
29
+ // module fails the build. Parsers represent the directive either in
30
+ // `program.directives` or as a leading string-literal `ExpressionStatement`.
31
+ const program = root.get().node.program;
32
+ const isClientOrServerDirective = (value) => value === 'use client' || value === 'use server';
33
+ let hasModuleDirective = (program.directives ?? []).some((d) => isClientOrServerDirective(d?.value?.value));
34
+ if (!hasModuleDirective) {
35
+ for (const node of program.body) {
36
+ if (node.type !== 'ExpressionStatement' ||
37
+ (node.expression?.type !== 'StringLiteral' &&
38
+ node.expression?.type !== 'Literal')) {
39
+ // Directives must lead the module; stop at the first non-directive.
40
+ break;
41
+ }
42
+ if (isClientOrServerDirective(node.expression.value)) {
43
+ hasModuleDirective = true;
44
+ break;
45
+ }
46
+ }
47
+ }
48
+ if (hasModuleDirective) {
49
+ return file.source;
50
+ }
51
+ // Bail if `instant` already exists in any form, so we never append a
52
+ // duplicate declaration (which would be a `SyntaxError`). This covers:
53
+ // export const instant = ...
54
+ // export const a = 1, instant = ... (any declarator position)
55
+ // const instant = ... (local binding)
56
+ // const { instant } = ... (destructured binding)
57
+ // export { instant }
58
+ // export { foo as instant }
59
+ // export function instant() {} / export class instant {}
60
+ const bindsInstant = (node) => {
61
+ switch (node?.type) {
62
+ case 'Identifier':
63
+ return node.name === 'instant';
64
+ case 'ObjectPattern':
65
+ return node.properties.some((prop) => prop.type === 'RestElement'
66
+ ? bindsInstant(prop.argument)
67
+ : bindsInstant(prop.value ?? prop.argument));
68
+ case 'ArrayPattern':
69
+ return node.elements.some((el) => el != null && bindsInstant(el));
70
+ case 'AssignmentPattern':
71
+ return bindsInstant(node.left);
72
+ case 'RestElement':
73
+ return bindsInstant(node.argument);
74
+ default:
75
+ return false;
76
+ }
77
+ };
78
+ const hasInstantBinding = root
79
+ .find(j.VariableDeclarator)
80
+ .filter((p) => bindsInstant(p.node.id))
81
+ .size() > 0 ||
82
+ root.find(j.ExportSpecifier, { exported: { name: 'instant' } }).size() >
83
+ 0 ||
84
+ root.find(j.FunctionDeclaration, { id: { name: 'instant' } }).size() > 0 ||
85
+ root.find(j.ClassDeclaration, { id: { name: 'instant' } }).size() > 0;
86
+ if (hasInstantBinding) {
87
+ return file.source;
88
+ }
89
+ // Build `export const instant = false`. The two `//` comments above it
90
+ // (TODO + See:) are attached as leading comments on the declaration so
91
+ // recast prints them right above it.
92
+ const todoComment = j.commentLine(' TODO: Cache Components adoption. Refactor this route so this opt-out can be removed.', true, false);
93
+ const seeComment = j.commentLine(' See: https://nextjs.org/docs/app/guides/migrating-to-cache-components', true, false);
94
+ const instantExport = j.exportNamedDeclaration(j.variableDeclaration('const', [
95
+ j.variableDeclarator(j.identifier('instant'), j.booleanLiteral(false)),
96
+ ]));
97
+ instantExport.comments = [todoComment, seeComment];
98
+ // Insert after the last top-level import, or at the top of the module
99
+ // if there are no imports.
100
+ const body = program.body;
101
+ let lastImportIndex = -1;
102
+ for (let i = 0; i < body.length; i++) {
103
+ if (body[i].type === 'ImportDeclaration')
104
+ lastImportIndex = i;
105
+ }
106
+ if (lastImportIndex !== -1) {
107
+ body.splice(lastImportIndex + 1, 0, instantExport);
108
+ }
109
+ else if (body.length > 0) {
110
+ // No imports. Inserting at index 0 would steal any file-level leading
111
+ // comments (e.g. `// @ts-nocheck`) from `body[0]` because recast
112
+ // attributes them to whatever is first. Move those leading comments
113
+ // off `body[0]` onto the new export *before* its TODO/See: lines, so
114
+ // they print in their original position.
115
+ const first = body[0];
116
+ const allComments = (first.comments ?? []);
117
+ const firstLeading = allComments.filter((c) => c.leading === true);
118
+ if (firstLeading.length > 0) {
119
+ first.comments = allComments.filter((c) => c.leading !== true);
120
+ instantExport.comments = [...firstLeading, todoComment, seeComment];
121
+ }
122
+ body.unshift(instantExport);
123
+ }
124
+ else {
125
+ body.push(instantExport);
126
+ }
127
+ return root.toSource();
128
+ }
129
+ //# sourceMappingURL=cache-components-instant-false.js.map
@@ -4,8 +4,12 @@
4
4
  * the new top-level `turbopack` configuration.
5
5
  *
6
6
  * It moves most properties from experimental.turbo to the top-level turbopack
7
- * property, with special handling for certain properties like memoryLimit, minify,
7
+ * property, with special handling for certain properties like minify,
8
8
  * treeShaking, and sourceMaps which become experimental.turbopack* properties instead.
9
+ *
10
+ * The `memoryLimit` option is no longer supported, so it is removed entirely in
11
+ * both its old location (`experimental.turbo.memoryLimit`) and the location the
12
+ * previous version of this codemod produced (`experimental.turbopackMemoryLimit`).
9
13
  */
10
14
  Object.defineProperty(exports, "__esModule", { value: true });
11
15
  exports.default = transformer;
@@ -13,11 +17,15 @@ const parser_1 = require("../lib/parser");
13
17
  const utils_1 = require("./lib/utils");
14
18
  // Properties that need to be moved to experimental.turbopack*
15
19
  const RENAMED_EXPERIMENTAL_PROPERTIES = {
16
- memoryLimit: 'turbopackMemoryLimit',
17
20
  minify: 'turbopackMinify',
18
21
  treeShaking: 'turbopackTreeShaking',
19
22
  sourceMaps: 'turbopackSourceMaps',
20
23
  };
24
+ // `memoryLimit` is no longer supported and is removed entirely. We drop it under
25
+ // both its old `experimental.turbo.*` name and the `experimental.*` name produced
26
+ // by the previous version of this codemod.
27
+ const REMOVED_TURBO_PROPERTY = 'memoryLimit';
28
+ const REMOVED_EXPERIMENTAL_PROPERTY = 'turbopackMemoryLimit';
21
29
  function transformer(file, _api, options) {
22
30
  const j = (0, parser_1.createParserFromPath)(file.path);
23
31
  const root = j(file.source);
@@ -56,6 +64,13 @@ function transformer(file, _api, options) {
56
64
  const regularProps = [];
57
65
  const specialProps = [];
58
66
  turboObj.properties.forEach((prop) => {
67
+ if (isStaticProperty(prop) &&
68
+ prop.key &&
69
+ prop.key.type === 'Identifier' &&
70
+ prop.key.name === REMOVED_TURBO_PROPERTY) {
71
+ // Drop: `memoryLimit` is no longer supported.
72
+ return;
73
+ }
59
74
  if (isStaticProperty(prop) &&
60
75
  prop.key &&
61
76
  prop.key.type === 'Identifier' &&
@@ -71,7 +86,10 @@ function transformer(file, _api, options) {
71
86
  const existingProps = experimentalObj.properties.filter((prop) => !(isStaticProperty(prop) &&
72
87
  prop.key &&
73
88
  prop.key.type === 'Identifier' &&
74
- prop.key.name === 'turbo'));
89
+ // Drop the `turbo` object (moved above) and a pre-existing
90
+ // `turbopackMemoryLimit` (no longer supported).
91
+ (prop.key.name === 'turbo' ||
92
+ prop.key.name === REMOVED_EXPERIMENTAL_PROPERTY)));
75
93
  experimentalObj.properties = [...existingProps, ...specialProps];
76
94
  // If experimental has no properties, remove it
77
95
  if (experimentalObj.properties.length === 0) {
@@ -132,7 +150,13 @@ function transformer(file, _api, options) {
132
150
  else {
133
151
  return;
134
152
  }
135
- // For special properties like memoryLimit, minify, etc.
153
+ // `memoryLimit` is no longer supported: remove the assignment entirely.
154
+ if (propName === REMOVED_TURBO_PROPERTY) {
155
+ j(path).remove();
156
+ hasChanges = true;
157
+ return;
158
+ }
159
+ // For special properties like minify, treeShaking, etc.
136
160
  if (propName && RENAMED_EXPERIMENTAL_PROPERTIES[propName]) {
137
161
  const newAssignment = j.assignmentExpression('=', j.memberExpression(j.memberExpression(j.identifier(varName), j.identifier('experimental')), j.identifier(RENAMED_EXPERIMENTAL_PROPERTIES[propName])), path.node.right);
138
162
  j(path).replaceWith(newAssignment);
@@ -195,6 +219,26 @@ function transformer(file, _api, options) {
195
219
  hasChanges = true;
196
220
  }
197
221
  });
222
+ // Remove `config.experimental.turbopackMemoryLimit = value` assignments left
223
+ // behind by the previous version of this codemod (no longer supported).
224
+ root
225
+ .find(j.AssignmentExpression, {
226
+ left: {
227
+ type: 'MemberExpression',
228
+ object: {
229
+ type: 'MemberExpression',
230
+ property: { type: 'Identifier', name: 'experimental' },
231
+ },
232
+ property: {
233
+ type: 'Identifier',
234
+ name: REMOVED_EXPERIMENTAL_PROPERTY,
235
+ },
236
+ },
237
+ })
238
+ .forEach((path) => {
239
+ j(path).remove();
240
+ hasChanges = true;
241
+ });
198
242
  // Only return a string if we changed the AST, otherwise return the original source
199
243
  return hasChanges ? root.toSource(options) : file.source;
200
244
  }