@next/codemod 16.4.0-canary.8 → 16.4.0

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
@@ -7,3 +7,14 @@ Codemods are transformations that run on your codebase programmatically. This al
7
7
  ## Documentation
8
8
 
9
9
  Visit [nextjs.org/docs/advanced-features/codemods](https://nextjs.org/docs/app/guides/upgrading/codemods) to view the documentation for this package.
10
+
11
+ ## Skip optional feature adoption
12
+
13
+ `upgrade --skip-adoption` skips codemods marked as feature adoption in the
14
+ registry while keeping version migrations and normal dependency selection.
15
+ Currently this excludes `cache-components-instant-false` and the
16
+ `remove-partial-prefetch` cleanup used after adopting partial prefetching.
17
+ Both transforms can still be run explicitly.
18
+
19
+ Combine it with `--yes` for an unattended version upgrade. Without
20
+ `--skip-adoption`, the existing upgrade selections are unchanged.
package/bin/agents-md.js CHANGED
@@ -37,7 +37,7 @@ async function runAgentsMd(options) {
37
37
  // --version provided: --output is required
38
38
  if (!options.output) {
39
39
  throw new shared_1.BadInput('When using --version, --output is also required.\n' +
40
- 'Example: npx @next/codemod agents-md --version 15.1.3 --output CLAUDE.md');
40
+ 'Example: npx @next/codemod agents-md --version 15.1.3 --output AGENTS.md');
41
41
  }
42
42
  nextjsVersion = options.version;
43
43
  targetFile = options.output;
@@ -58,7 +58,7 @@ async function runAgentsMd(options) {
58
58
  nextjsVersion = promptedOptions.nextVersion;
59
59
  targetFile = promptedOptions.targetFile;
60
60
  }
61
- const claudeMdPath = path_1.default.join(cwd, targetFile);
61
+ const agentsMdPath = path_1.default.join(cwd, targetFile);
62
62
  // Next.js >= 16.2.0 ships its docs inside the published package. When the
63
63
  // installed version matches the requested one, index the bundled docs
64
64
  // directly instead of downloading a copy into .next-docs.
@@ -73,8 +73,8 @@ async function runAgentsMd(options) {
73
73
  let sizeBefore = 0;
74
74
  let isNewFile = true;
75
75
  let existingContent = '';
76
- if (fs_1.default.existsSync(claudeMdPath)) {
77
- existingContent = fs_1.default.readFileSync(claudeMdPath, 'utf-8');
76
+ if (fs_1.default.existsSync(agentsMdPath)) {
77
+ existingContent = fs_1.default.readFileSync(agentsMdPath, 'utf-8');
78
78
  sizeBefore = Buffer.byteLength(existingContent, 'utf-8');
79
79
  isNewFile = false;
80
80
  }
@@ -94,13 +94,13 @@ async function runAgentsMd(options) {
94
94
  }
95
95
  const docFiles = (0, agents_md_1.collectDocFiles)(docsPath);
96
96
  const sections = (0, agents_md_1.buildDocTree)(docFiles);
97
- const indexContent = (0, agents_md_1.generateClaudeMdIndex)({
97
+ const indexContent = (0, agents_md_1.generateAgentsMdIndex)({
98
98
  docsPath: docsLinkPath,
99
99
  sections,
100
100
  outputFile: targetFile,
101
101
  });
102
- const newContent = (0, agents_md_1.injectIntoClaudeMd)(existingContent, indexContent);
103
- fs_1.default.writeFileSync(claudeMdPath, newContent, 'utf-8');
102
+ const newContent = (0, agents_md_1.injectIntoAgentsMd)(existingContent, indexContent);
103
+ fs_1.default.writeFileSync(agentsMdPath, newContent, 'utf-8');
104
104
  const sizeAfter = Buffer.byteLength(newContent, 'utf-8');
105
105
  // .next-docs only exists on the download path; bundled docs live in
106
106
  // node_modules, which is already ignored.
@@ -136,7 +136,6 @@ async function promptForOptions(cwd) {
136
136
  name: 'targetFile',
137
137
  message: 'Target markdown file',
138
138
  choices: [
139
- { title: 'CLAUDE.md', value: 'CLAUDE.md' },
140
139
  { title: 'AGENTS.md', value: 'AGENTS.md' },
141
140
  { title: 'Custom...', value: '__custom__' },
142
141
  ],
@@ -154,7 +153,7 @@ async function promptForOptions(cwd) {
154
153
  type: 'text',
155
154
  name: 'customFile',
156
155
  message: 'Enter custom file path',
157
- initial: 'CLAUDE.md',
156
+ initial: 'AGENTS.md',
158
157
  validate: (value) => value.trim() ? true : 'Please enter a file path',
159
158
  }, { onCancel: utils_1.onCancel });
160
159
  if (customResponse.customFile === undefined) {
@@ -44,6 +44,9 @@ program
44
44
  .usage('[revision] [options]')
45
45
  .option('--verbose', 'Verbose output', false)
46
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)
47
+ .option('--skip-adoption', 'Skip optional feature-adoption codemods while applying version migrations.', false)
48
+ .option('--skip-react-upgrade', 'Keep React dependencies, types, and overrides unchanged and skip React codemods.', false)
49
+ .option('--skip-eslint-upgrade', 'Keep ESLint dependencies unchanged and skip the next-lint-to-eslint-cli codemod.', false)
47
50
  .action(async (revision, options) => {
48
51
  try {
49
52
  await (0, upgrade_1.runUpgrade)(revision, options);
@@ -62,7 +65,7 @@ program
62
65
  .command('agents-md')
63
66
  .description('Generate Next.js documentation index for AI coding agents (Claude, Cursor, etc.).')
64
67
  .option('--version <version>', 'Next.js version (auto-detected if not provided)')
65
- .option('--output <file>', 'Target file path (e.g., CLAUDE.md, AGENTS.md)')
68
+ .option('--output <file>', 'Target file path (e.g., AGENTS.md)')
66
69
  .action(async (options) => {
67
70
  try {
68
71
  await (0, agents_md_1.runAgentsMd)(options);
package/bin/transform.js CHANGED
@@ -56,7 +56,7 @@ async function runTransform(transform, path, options) {
56
56
  }, { onCancel: utils_1.onCancel });
57
57
  transformer = res.transformer;
58
58
  }
59
- if (transformer === 'next-request-geo-ip') {
59
+ if (transformer === 'next-request-geo-ip' && !options.nonInteractive) {
60
60
  const { isAppDeployedToVercel } = await (0, prompts_1.default)({
61
61
  type: 'confirm',
62
62
  name: 'isAppDeployedToVercel',
@@ -99,7 +99,7 @@ async function runTransform(transform, path, options) {
99
99
  args.push('--parser=tsx');
100
100
  args.push('--ignore-pattern=**/node_modules/**');
101
101
  args.push('--ignore-pattern=**/.next/**');
102
- args.push('--extensions=tsx,ts,jsx,js');
102
+ args.push('--extensions=tsx,ts,jsx,js,mjs');
103
103
  args = args.concat(['--transform', transformerPath]);
104
104
  if (jscodeshift) {
105
105
  args = args.concat(jscodeshift);
@@ -109,7 +109,10 @@ async function runTransform(transform, path, options) {
109
109
  const execaChildProcess = (0, execa_1.default)(exports.jscodeshiftExecutable, args, {
110
110
  // include ANSI color codes
111
111
  // Note: execa merges env with existing env by default.
112
- env: process.stdout.isTTY ? { FORCE_COLOR: 'true' } : {},
112
+ env: {
113
+ ...process.env,
114
+ ...(process.stdout.isTTY ? { FORCE_COLOR: 'true' } : {}),
115
+ },
113
116
  });
114
117
  // "\n" + "a\n" + "b\n"
115
118
  let lastThreeLineBreaks = '';
package/bin/upgrade.js CHANGED
@@ -190,6 +190,7 @@ async function runUpgrade(revision, options) {
190
190
  // x-ref(release): https://github.com/vercel/next.js/releases/tag/v14.3.0-canary.45
191
191
  (0, semver_1.compare)(targetNextVersion, '14.3.0-canary.45') >= 0 &&
192
192
  installedReactVersion.startsWith('18') &&
193
+ !options.skipReactUpgrade &&
193
194
  // Pure App Router always uses React 19
194
195
  // The mixed case is tricky to handle from a types perspective.
195
196
  // We'll recommend to upgrade in the prompt but users can decide to try 18.
@@ -218,20 +219,23 @@ async function runUpgrade(revision, options) {
218
219
  // E.g. in peerDependencies we could have `^18.2.0 || ^19.0.0 || 20.0.0-canary`
219
220
  // If we'd just `npm add` that, the manifest would read the same version query.
220
221
  // This is basically a `npm --save-exact react@$versionQuery` that works for every package manager.
221
- const targetReactVersion = shouldStayOnReact18
222
- ? '18.3.1'
223
- : await loadHighestNPMVersionMatching(`react@${targetNextPackageJson.peerDependencies['react']}`);
222
+ const targetReactVersion = options.skipReactUpgrade
223
+ ? installedReactVersion
224
+ : shouldStayOnReact18
225
+ ? '18.3.1'
226
+ : await loadHighestNPMVersionMatching(`react@${targetNextPackageJson.peerDependencies['react']}`);
224
227
  if ((0, semver_1.compare)(targetNextVersion, '15.0.0-canary') >= 0 &&
225
228
  (0, semver_1.compare)(targetNextVersion, '16.0.0-canary') < 0) {
226
229
  await suggestTurbopack(appPackageJson, targetNextVersion, nonInteractive);
227
230
  }
228
- const codemods = await suggestCodemods(installedNextVersion, targetNextVersion, nonInteractive);
231
+ const codemods = await suggestCodemods(installedNextVersion, targetNextVersion, nonInteractive, options.skipAdoption);
229
232
  const packageManager = (0, handle_package_1.getPkgManager)(cwd);
230
233
  let shouldRunReactCodemods = false;
231
234
  let shouldRunReactTypesCodemods = false;
232
235
  let execCommand = 'npx --yes';
233
236
  // The following React codemods are for React 19
234
- if (!shouldStayOnReact18 &&
237
+ if (!options.skipReactUpgrade &&
238
+ !shouldStayOnReact18 &&
235
239
  (0, semver_1.compare)(targetReactVersion, '19.0.0-0') >= 0 &&
236
240
  (0, semver_1.compare)(installedReactVersion, '19.0.0-0') < 0) {
237
241
  shouldRunReactCodemods = await suggestReactCodemods(nonInteractive);
@@ -248,19 +252,31 @@ async function runUpgrade(revision, options) {
248
252
  };
249
253
  const versionMapping = {
250
254
  next: { version: targetNextVersion, required: true },
251
- react: { version: targetReactVersion, required: true },
252
- 'react-dom': { version: targetReactVersion, required: true },
253
- 'react-is': { version: targetReactVersion, required: false },
254
255
  };
256
+ // Recovery retries must preserve the repaired React declarations and pins.
257
+ if (!options.skipReactUpgrade) {
258
+ Object.assign(versionMapping, {
259
+ react: { version: targetReactVersion, required: true },
260
+ 'react-dom': { version: targetReactVersion, required: true },
261
+ 'react-is': { version: targetReactVersion, required: false },
262
+ });
263
+ }
255
264
  for (const optionalNextjsPackage of optionalNextjsPackages) {
265
+ // Preserve the lint stack together; its newer config can require newer ESLint.
266
+ if (options.skipEslintUpgrade &&
267
+ (optionalNextjsPackage === 'eslint-config-next' ||
268
+ optionalNextjsPackage === '@next/eslint-plugin-next')) {
269
+ continue;
270
+ }
256
271
  versionMapping[optionalNextjsPackage] = {
257
272
  version: targetNextVersion,
258
273
  required: false,
259
274
  };
260
275
  }
261
- if (targetReactVersion.startsWith('19.0.0-canary') ||
262
- targetReactVersion.startsWith('19.0.0-beta') ||
263
- targetReactVersion.startsWith('19.0.0-rc')) {
276
+ if (!options.skipReactUpgrade &&
277
+ (targetReactVersion.startsWith('19.0.0-canary') ||
278
+ targetReactVersion.startsWith('19.0.0-beta') ||
279
+ targetReactVersion.startsWith('19.0.0-rc'))) {
264
280
  const [targetReactTypesVersion, targetReactDOMTypesVersion] = await Promise.all([
265
281
  loadHighestNPMVersionMatching(`types-react@rc`),
266
282
  loadHighestNPMVersionMatching(`types-react-dom@rc`),
@@ -278,7 +294,7 @@ async function runUpgrade(revision, options) {
278
294
  };
279
295
  }
280
296
  }
281
- else {
297
+ else if (!options.skipReactUpgrade) {
282
298
  const [targetReactTypesVersion, targetReactDOMTypesVersion] = await Promise.all([
283
299
  loadHighestNPMVersionMatching(`@types/react@${targetNextPackageJson.peerDependencies['react']}`),
284
300
  loadHighestNPMVersionMatching(`@types/react-dom@${targetNextPackageJson.peerDependencies['react']}`),
@@ -306,7 +322,9 @@ async function runUpgrade(revision, options) {
306
322
  // Only act when the project is actually using `eslint-config-next` — we
307
323
  // don't want to silently upgrade eslint majors for projects that use
308
324
  // eslint for unrelated reasons.
309
- if (allDependencies['eslint'] && allDependencies['eslint-config-next']) {
325
+ if (!options.skipEslintUpgrade &&
326
+ allDependencies['eslint'] &&
327
+ allDependencies['eslint-config-next']) {
310
328
  try {
311
329
  const eslintConfigNextPeerDepsJSON = (0, child_process_1.execSync)(`npm --silent view "eslint-config-next@${targetNextVersion}" peerDependencies --json`, { encoding: 'utf-8' });
312
330
  const eslintConfigNextPeerDeps = eslintConfigNextPeerDepsJSON.trim() === ''
@@ -314,7 +332,13 @@ async function runUpgrade(revision, options) {
314
332
  : JSON.parse(eslintConfigNextPeerDepsJSON);
315
333
  const eslintRange = eslintConfigNextPeerDeps?.eslint;
316
334
  if (eslintRange) {
317
- const targetEslintVersion = await loadHighestNPMVersionMatching(`eslint@${eslintRange}`);
335
+ // TODO: Target ESLint 10 once eslint-config-next's plugins, especially
336
+ // eslint-plugin-react, support its API removals (e.g. context.getFilename).
337
+ const cappedRange = eslintRange
338
+ .split('||')
339
+ .map((range) => `${range.trim()} <10`)
340
+ .join(' || ');
341
+ const targetEslintVersion = await loadHighestNPMVersionMatching(`eslint@${cappedRange}`);
318
342
  versionMapping['eslint'] = {
319
343
  version: targetEslintVersion,
320
344
  required: false,
@@ -330,10 +354,10 @@ async function runUpgrade(revision, options) {
330
354
  // Even though we only need those if we alias `@types/react` to types-react,
331
355
  // we still do it out of safety due to https://github.com/microsoft/DefinitelyTyped-tools/issues/433.
332
356
  const overrides = {};
333
- if (allDependencies['@types/react']) {
357
+ if (!options.skipReactUpgrade && allDependencies['@types/react']) {
334
358
  overrides['@types/react'] = versionMapping['@types/react'].version;
335
359
  }
336
- if (allDependencies['@types/react-dom']) {
360
+ if (!options.skipReactUpgrade && allDependencies['@types/react-dom']) {
337
361
  overrides['@types/react-dom'] = versionMapping['@types/react-dom'].version;
338
362
  }
339
363
  writeOverridesField(appPackageJson, packageManager, overrides);
@@ -356,8 +380,17 @@ async function runUpgrade(revision, options) {
356
380
  // Common IDE formatters would add a newline as well.
357
381
  os.EOL);
358
382
  (0, handle_package_1.runInstallation)(packageManager, { cwd });
359
- for (const codemod of codemods) {
360
- await (0, transform_1.runTransform)(codemod, cwd, { force: true, verbose });
383
+ // Opting out skips the lint migration as well as its dependency upgrades.
384
+ const selectedCodemods = codemods.filter((codemod) => !options.skipEslintUpgrade || codemod !== 'next-lint-to-eslint-cli');
385
+ if (selectedCodemods.length !== codemods.length) {
386
+ console.log('Skipping next-lint-to-eslint-cli. Complete any required lint migration manually before verification.');
387
+ }
388
+ for (const codemod of selectedCodemods) {
389
+ await (0, transform_1.runTransform)(codemod, cwd, {
390
+ force: true,
391
+ verbose,
392
+ nonInteractive,
393
+ });
361
394
  }
362
395
  // To reduce user-side burden of selecting which codemods to run as it needs additional
363
396
  // understanding of the codemods, we run all of the applicable codemods.
@@ -367,7 +400,14 @@ async function runUpgrade(revision, options) {
367
400
  // https://github.com/codemod-com/codemod/blob/c0cf00d13161a0ec0965b6cc6bc5d54076839cc8/apps/cli/src/flags.ts#L160
368
401
  // `--allow-dirty` is required because the upgrade above modified package.json
369
402
  // and the lockfile; the recipe refuses to run on a dirty tree otherwise.
370
- (0, child_process_1.execSync)(`${execCommand} codemod@latest react/19/migration-recipe --no-interactive --allow-dirty`, { stdio: 'inherit' });
403
+ try {
404
+ (0, child_process_1.execSync)(`${execCommand} codemod@latest react/19/migration-recipe --no-interactive --allow-dirty`, { stdio: 'inherit' });
405
+ }
406
+ catch (error) {
407
+ // TODO: Remove this fallback once codemod publishes a Linux binary that
408
+ // supports the glibc versions used by our upgrade environments.
409
+ console.warn(new Error(`${picocolors_1.default.yellow('⚠')} The React 19 codemod could not run. Continue the upgrade and review the React 19 migration guide manually.`, { cause: error }));
410
+ }
371
411
  }
372
412
  if (shouldRunReactTypesCodemods) {
373
413
  // https://react.dev/blog/2024/04/25/react-19-upgrade-guide#typescript-changes
@@ -378,12 +418,12 @@ async function runUpgrade(revision, options) {
378
418
  });
379
419
  }
380
420
  console.log(); // new line
381
- if (codemods.length > 0) {
421
+ if (selectedCodemods.length > 0) {
382
422
  console.log(`${picocolors_1.default.green('✔')} Codemods have been applied successfully.`);
383
423
  }
384
424
  try {
385
425
  if ((0, agents_md_1.refreshAgentRulesBlock)(cwd) === 'refreshed') {
386
- console.log(`${picocolors_1.default.green('✔')} Refreshed the managed agent-rules block in AGENTS.md / CLAUDE.md to match the upgraded Next.js.`);
426
+ console.log(`${picocolors_1.default.green('✔')} Refreshed the managed agent-rules block in AGENTS.md to match the upgraded Next.js.`);
387
427
  }
388
428
  }
389
429
  catch {
@@ -489,7 +529,7 @@ async function suggestTurbopack(packageJson, targetNextVersion, nonInteractive)
489
529
  packageJson.scripts['dev'] =
490
530
  responseCustomDevScript.customDevScript || devScript;
491
531
  }
492
- async function suggestCodemods(initialNextVersion, targetNextVersion, nonInteractive) {
532
+ async function suggestCodemods(initialNextVersion, targetNextVersion, nonInteractive, skipAdoption = false) {
493
533
  // example:
494
534
  // codemod version: 15.0.0-canary.45
495
535
  // 14.3 -> 15.0.0-canary.45: apply
@@ -508,7 +548,7 @@ async function suggestCodemods(initialNextVersion, targetNextVersion, nonInterac
508
548
  if (targetVersionIndex === -1) {
509
549
  targetVersionIndex = utils_1.TRANSFORMER_INQUIRER_CHOICES.length;
510
550
  }
511
- const relevantCodemods = utils_1.TRANSFORMER_INQUIRER_CHOICES.slice(initialVersionIndex, targetVersionIndex);
551
+ const relevantCodemods = utils_1.TRANSFORMER_INQUIRER_CHOICES.slice(initialVersionIndex, targetVersionIndex).filter((codemod) => !skipAdoption || !codemod.adoption);
512
552
  if (relevantCodemods.length === 0) {
513
553
  return [];
514
554
  }
@@ -11,7 +11,7 @@ const { getNextjsVersion } = require('../../lib/agents-md')
11
11
  * TRUE E2E TESTS
12
12
  * These tests invoke the actual CLI entry point (runAgentsMd),
13
13
  * simulating what happens when a user runs:
14
- * npx @next/codemod agents-md --version 15.0.0 --output CLAUDE.md
14
+ * npx @next/codemod agents-md --version 15.0.0 --output AGENTS.md
15
15
  */
16
16
  describe('agents-md e2e (CLI invocation)', () => {
17
17
  let testProjectDir
@@ -45,7 +45,7 @@ describe('agents-md e2e (CLI invocation)', () => {
45
45
  }
46
46
  })
47
47
 
48
- it('creates CLAUDE.md and .next-docs directory when run with --version and --output', async () => {
48
+ it('creates AGENTS.md and .next-docs directory when run with --version and --output', async () => {
49
49
  // Create a minimal package.json (not required, but realistic)
50
50
  const packageJson = {
51
51
  name: 'test-project',
@@ -67,7 +67,7 @@ describe('agents-md e2e (CLI invocation)', () => {
67
67
  // Run the actual CLI command
68
68
  await runAgentsMd({
69
69
  version: '15.0.0',
70
- output: 'CLAUDE.md',
70
+ output: 'AGENTS.md',
71
71
  })
72
72
 
73
73
  // Verify .next-docs directory was created and populated
@@ -83,20 +83,20 @@ describe('agents-md e2e (CLI invocation)', () => {
83
83
  )
84
84
  expect(mdxFiles.length).toBeGreaterThan(0)
85
85
 
86
- // Verify CLAUDE.md was created
87
- const claudeMdPath = path.join(testProjectDir, 'CLAUDE.md')
88
- expect(fs.existsSync(claudeMdPath)).toBe(true)
86
+ // Verify AGENTS.md was created
87
+ const agentsMdPath = path.join(testProjectDir, 'AGENTS.md')
88
+ expect(fs.existsSync(agentsMdPath)).toBe(true)
89
89
 
90
- const claudeMdContent = fs.readFileSync(claudeMdPath, 'utf-8')
90
+ const agentsMdContent = fs.readFileSync(agentsMdPath, 'utf-8')
91
91
 
92
92
  // Verify content structure
93
- expect(claudeMdContent).toContain('<!-- NEXT-AGENTS-MD-START -->')
94
- expect(claudeMdContent).toContain('<!-- NEXT-AGENTS-MD-END -->')
95
- expect(claudeMdContent).toContain('[Next.js Docs Index]')
96
- expect(claudeMdContent).toContain('root: ./.next-docs')
93
+ expect(agentsMdContent).toContain('<!-- NEXT-AGENTS-MD-START -->')
94
+ expect(agentsMdContent).toContain('<!-- NEXT-AGENTS-MD-END -->')
95
+ expect(agentsMdContent).toContain('[Next.js Docs Index]')
96
+ expect(agentsMdContent).toContain('root: ./.next-docs')
97
97
 
98
98
  // Verify paths are normalized to forward slashes (cross-platform)
99
- const lines = claudeMdContent.split('|')
99
+ const lines = agentsMdContent.split('|')
100
100
  const pathLines = lines.filter((line) => line.includes(':'))
101
101
  pathLines.forEach((line) => {
102
102
  // Should not contain Windows backslashes in the output
@@ -117,19 +117,19 @@ describe('agents-md e2e (CLI invocation)', () => {
117
117
  const output = consoleOutput.join('\n')
118
118
  expect(output).toContain('Downloading Next.js')
119
119
  expect(output).toContain('15.0.0')
120
- expect(output).toContain('CLAUDE.md')
120
+ expect(output).toContain('AGENTS.md')
121
121
  } finally {
122
122
  // Restore original directory
123
123
  process.chdir(originalCwd)
124
124
  }
125
125
  })
126
126
 
127
- it('updates existing CLAUDE.md without losing content', async () => {
127
+ it('updates existing AGENTS.md without losing content', async () => {
128
128
  const originalCwd = process.cwd()
129
129
  process.chdir(testProjectDir)
130
130
 
131
131
  try {
132
- // Create existing CLAUDE.md with custom content
132
+ // Create existing AGENTS.md with custom content
133
133
  const existingContent = `# My Project
134
134
 
135
135
  This is my project documentation.
@@ -138,31 +138,28 @@ This is my project documentation.
138
138
  - Feature 1
139
139
  - Feature 2
140
140
  `
141
- fs.writeFileSync(
142
- path.join(testProjectDir, 'CLAUDE.md'),
143
- existingContent
144
- )
141
+ fs.writeFileSync(path.join(testProjectDir, 'AGENTS.md'), existingContent)
145
142
 
146
143
  // Run CLI
147
144
  await runAgentsMd({
148
145
  version: '15.0.0',
149
- output: 'CLAUDE.md',
146
+ output: 'AGENTS.md',
150
147
  })
151
148
 
152
149
  // Verify file was updated, not replaced
153
- const claudeMdContent = fs.readFileSync(
154
- path.join(testProjectDir, 'CLAUDE.md'),
150
+ const agentsMdContent = fs.readFileSync(
151
+ path.join(testProjectDir, 'AGENTS.md'),
155
152
  'utf-8'
156
153
  )
157
154
 
158
155
  // Original content should still be there
159
- expect(claudeMdContent).toContain('# My Project')
160
- expect(claudeMdContent).toContain('This is my project documentation.')
161
- expect(claudeMdContent).toContain('## Features')
156
+ expect(agentsMdContent).toContain('# My Project')
157
+ expect(agentsMdContent).toContain('This is my project documentation.')
158
+ expect(agentsMdContent).toContain('## Features')
162
159
 
163
160
  // New index should be injected
164
- expect(claudeMdContent).toContain('<!-- NEXT-AGENTS-MD-START -->')
165
- expect(claudeMdContent).toContain('[Next.js Docs Index]')
161
+ expect(agentsMdContent).toContain('<!-- NEXT-AGENTS-MD-START -->')
162
+ expect(agentsMdContent).toContain('[Next.js Docs Index]')
166
163
  } finally {
167
164
  process.chdir(originalCwd)
168
165
  }
@@ -179,9 +176,8 @@ This is my project documentation.
179
176
  output: 'AGENTS.md',
180
177
  })
181
178
 
182
- // Verify AGENTS.md was created (not CLAUDE.md)
179
+ // Verify AGENTS.md was created
183
180
  expect(fs.existsSync(path.join(testProjectDir, 'AGENTS.md'))).toBe(true)
184
- expect(fs.existsSync(path.join(testProjectDir, 'CLAUDE.md'))).toBe(false)
185
181
 
186
182
  const agentsMdContent = fs.readFileSync(
187
183
  path.join(testProjectDir, 'AGENTS.md'),
@@ -216,11 +212,11 @@ This is my project documentation.
216
212
  // Run from subdirectory - should create files in CWD (subdirectory)
217
213
  await runAgentsMd({
218
214
  version: '15.0.0',
219
- output: 'CLAUDE.md',
215
+ output: 'AGENTS.md',
220
216
  })
221
217
 
222
218
  // Verify files created in subdirectory
223
- expect(fs.existsSync(path.join(subDir, 'CLAUDE.md'))).toBe(true)
219
+ expect(fs.existsSync(path.join(subDir, 'AGENTS.md'))).toBe(true)
224
220
  expect(fs.existsSync(path.join(subDir, '.next-docs'))).toBe(true)
225
221
  } finally {
226
222
  process.chdir(originalCwd)
@@ -234,20 +230,20 @@ This is my project documentation.
234
230
  try {
235
231
  await runAgentsMd({
236
232
  version: '15.0.0',
237
- output: 'CLAUDE.md',
233
+ output: 'AGENTS.md',
238
234
  })
239
235
 
240
- const claudeMdContent = fs.readFileSync(
241
- path.join(testProjectDir, 'CLAUDE.md'),
236
+ const agentsMdContent = fs.readFileSync(
237
+ path.join(testProjectDir, 'AGENTS.md'),
242
238
  'utf-8'
243
239
  )
244
240
 
245
241
  // Extract the index content between markers
246
242
  const startMarker = '<!-- NEXT-AGENTS-MD-START -->'
247
243
  const endMarker = '<!-- NEXT-AGENTS-MD-END -->'
248
- const startIdx = claudeMdContent.indexOf(startMarker) + startMarker.length
249
- const endIdx = claudeMdContent.indexOf(endMarker)
250
- const indexContent = claudeMdContent.slice(startIdx, endIdx)
244
+ const startIdx = agentsMdContent.indexOf(startMarker) + startMarker.length
245
+ const endIdx = agentsMdContent.indexOf(endMarker)
246
+ const indexContent = agentsMdContent.slice(startIdx, endIdx)
251
247
 
252
248
  // Parse the index (format: "dir:{file1,file2}|dir2:{file3}")
253
249
  const sections = indexContent.split('|').filter((s) => s.includes(':'))
@@ -276,7 +272,7 @@ This is my project documentation.
276
272
  // Use a known stable version
277
273
  await runAgentsMd({
278
274
  version: '14.2.0',
279
- output: 'CLAUDE.md',
275
+ output: 'AGENTS.md',
280
276
  })
281
277
 
282
278
  // Verify docs were downloaded
@@ -331,7 +327,7 @@ This is my project documentation.
331
327
  process.chdir(testProjectDir)
332
328
 
333
329
  try {
334
- await runAgentsMd({ output: 'CLAUDE.md' })
330
+ await runAgentsMd({ output: 'AGENTS.md' })
335
331
 
336
332
  // No .next-docs copy and no .gitignore entry for it
337
333
  expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
@@ -341,14 +337,14 @@ This is my project documentation.
341
337
  false
342
338
  )
343
339
 
344
- const claudeMdContent = fs.readFileSync(
345
- path.join(testProjectDir, 'CLAUDE.md'),
340
+ const agentsMdContent = fs.readFileSync(
341
+ path.join(testProjectDir, 'AGENTS.md'),
346
342
  'utf-8'
347
343
  )
348
- expect(claudeMdContent).toContain(
344
+ expect(agentsMdContent).toContain(
349
345
  'root: ./node_modules/next/dist/docs'
350
346
  )
351
- expect(claudeMdContent).toContain('01-installation.md')
347
+ expect(agentsMdContent).toContain('01-installation.md')
352
348
 
353
349
  const output = consoleOutput.join('\n')
354
350
  expect(output).toContain('bundled with Next.js')
@@ -390,17 +386,17 @@ This is my project documentation.
390
386
  process.chdir(testProjectDir)
391
387
 
392
388
  try {
393
- await runAgentsMd({ version: '15.0.0', output: 'CLAUDE.md' })
389
+ await runAgentsMd({ version: '15.0.0', output: 'AGENTS.md' })
394
390
 
395
391
  expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
396
392
  true
397
393
  )
398
394
 
399
- const claudeMdContent = fs.readFileSync(
400
- path.join(testProjectDir, 'CLAUDE.md'),
395
+ const agentsMdContent = fs.readFileSync(
396
+ path.join(testProjectDir, 'AGENTS.md'),
401
397
  'utf-8'
402
398
  )
403
- expect(claudeMdContent).toContain('root: ./.next-docs')
399
+ expect(agentsMdContent).toContain('root: ./.next-docs')
404
400
 
405
401
  const output = consoleOutput.join('\n')
406
402
  expect(output).toContain('Downloading')
package/lib/agents-md.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * agents-md: Generate Next.js documentation index for AI coding agents.
4
4
  *
5
5
  * Downloads docs from GitHub via git sparse-checkout, builds a compact
6
- * index of all doc files, and injects it into CLAUDE.md or AGENTS.md.
6
+ * index of all doc files, and injects it into AGENTS.md.
7
7
  */
8
8
  var __importDefault = (this && this.__importDefault) || function (mod) {
9
9
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -16,8 +16,8 @@ exports.getBundledDocsLinkPath = getBundledDocsLinkPath;
16
16
  exports.pullDocs = pullDocs;
17
17
  exports.collectDocFiles = collectDocFiles;
18
18
  exports.buildDocTree = buildDocTree;
19
- exports.generateClaudeMdIndex = generateClaudeMdIndex;
20
- exports.injectIntoClaudeMd = injectIntoClaudeMd;
19
+ exports.generateAgentsMdIndex = generateAgentsMdIndex;
20
+ exports.injectIntoAgentsMd = injectIntoAgentsMd;
21
21
  exports.ensureGitignoreEntry = ensureGitignoreEntry;
22
22
  const execa_1 = __importDefault(require("execa"));
23
23
  const fs_1 = __importDefault(require("fs"));
@@ -26,8 +26,7 @@ const os_1 = __importDefault(require("os"));
26
26
  const AGENT_RULES_START_MARKER = '<!-- BEGIN:nextjs-agent-rules -->';
27
27
  /**
28
28
  * After an upgrade, refresh the managed agent-rules block in
29
- * AGENTS.md / CLAUDE.md so its content matches the Next.js version
30
- * that is now installed.
29
+ * AGENTS.md so its content matches the Next.js version that is now installed.
31
30
  *
32
31
  * Delegates to the installed package's own generator
33
32
  * (`next/dist/server/lib/generate-agent-files`), so the block text is
@@ -39,17 +38,14 @@ const AGENT_RULES_START_MARKER = '<!-- BEGIN:nextjs-agent-rules -->';
39
38
  * (< 16.3).
40
39
  */
41
40
  function refreshAgentRulesBlock(cwd) {
42
- const hostsBlock = ['AGENTS.md', 'CLAUDE.md'].some((file) => {
43
- try {
44
- return fs_1.default
45
- .readFileSync(path_1.default.join(cwd, file), 'utf-8')
46
- .includes(AGENT_RULES_START_MARKER);
47
- }
48
- catch {
49
- return false;
50
- }
51
- });
52
- if (!hostsBlock)
41
+ let agentsMdContent;
42
+ try {
43
+ agentsMdContent = fs_1.default.readFileSync(path_1.default.join(cwd, 'AGENTS.md'), 'utf-8');
44
+ }
45
+ catch {
46
+ return 'skipped';
47
+ }
48
+ if (!agentsMdContent.includes(AGENT_RULES_START_MARKER))
53
49
  return 'skipped';
54
50
  let writeAgentFiles;
55
51
  try {
@@ -62,9 +58,7 @@ function refreshAgentRulesBlock(cwd) {
62
58
  return 'skipped';
63
59
  }
64
60
  const result = writeAgentFiles(cwd);
65
- return result.agentsMd === 'updated' || result.claudeMd === 'updated'
66
- ? 'refreshed'
67
- : 'current';
61
+ return result.agentsMd === 'updated' ? 'refreshed' : 'current';
68
62
  }
69
63
  function getNextjsVersion(cwd) {
70
64
  try {
@@ -263,13 +257,13 @@ function buildDocTree(files) {
263
257
  }
264
258
  return sortedSections;
265
259
  }
266
- function generateClaudeMdIndex(data) {
260
+ function generateAgentsMdIndex(data) {
267
261
  const { docsPath, sections, outputFile } = data;
268
262
  const parts = [];
269
263
  parts.push('[Next.js Docs Index]');
270
264
  parts.push(`root: ${docsPath}`);
271
265
  parts.push('STOP. What you remember about Next.js is WRONG for this project. Always search docs and read before any task.');
272
- const targetFile = outputFile || 'CLAUDE.md';
266
+ const targetFile = outputFile || 'AGENTS.md';
273
267
  parts.push(`If docs missing, run this command first: npx @next/codemod agents-md --output ${targetFile}`);
274
268
  const allFiles = collectAllFilesFromSections(sections);
275
269
  const grouped = groupByDirectory(allFiles);
@@ -312,17 +306,17 @@ function hasExistingIndex(content) {
312
306
  function wrapWithMarkers(content) {
313
307
  return `${START_MARKER}${content}${END_MARKER}`;
314
308
  }
315
- function injectIntoClaudeMd(claudeMdContent, indexContent) {
309
+ function injectIntoAgentsMd(agentsMdContent, indexContent) {
316
310
  const wrappedContent = wrapWithMarkers(indexContent);
317
- if (hasExistingIndex(claudeMdContent)) {
318
- const startIdx = claudeMdContent.indexOf(START_MARKER);
319
- const endIdx = claudeMdContent.indexOf(END_MARKER) + END_MARKER.length;
320
- return (claudeMdContent.slice(0, startIdx) +
311
+ if (hasExistingIndex(agentsMdContent)) {
312
+ const startIdx = agentsMdContent.indexOf(START_MARKER);
313
+ const endIdx = agentsMdContent.indexOf(END_MARKER) + END_MARKER.length;
314
+ return (agentsMdContent.slice(0, startIdx) +
321
315
  wrappedContent +
322
- claudeMdContent.slice(endIdx));
316
+ agentsMdContent.slice(endIdx));
323
317
  }
324
- const separator = claudeMdContent.endsWith('\n') ? '\n' : '\n\n';
325
- return claudeMdContent + separator + wrappedContent + '\n';
318
+ const separator = agentsMdContent.endsWith('\n') ? '\n' : '\n\n';
319
+ return agentsMdContent + separator + wrappedContent + '\n';
326
320
  }
327
321
  const GITIGNORE_ENTRY = '.next-docs/';
328
322
  function ensureGitignoreEntry(cwd) {
@@ -13,6 +13,7 @@ exports.addPackageDependency = addPackageDependency;
13
13
  const find_up_1 = __importDefault(require("find-up"));
14
14
  const execa_1 = __importDefault(require("execa"));
15
15
  const node_child_process_1 = require("node:child_process");
16
+ const node_fs_1 = require("node:fs");
16
17
  const node_path_1 = require("node:path");
17
18
  /**
18
19
  * Get the full version string for the given package manager.
@@ -57,6 +58,17 @@ function getPnpmMajorVersion() {
57
58
  }
58
59
  function getPkgManager(baseDir) {
59
60
  try {
61
+ // The app's declared manager takes precedence over the command launcher.
62
+ const manifestPath = (0, node_path_1.join)(baseDir, 'package.json');
63
+ if ((0, node_fs_1.existsSync)(manifestPath)) {
64
+ const { packageManager } = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
65
+ const match = typeof packageManager === 'string'
66
+ ? packageManager.match(/^(npm|pnpm|yarn|bun)@/)
67
+ : null;
68
+ if (match) {
69
+ return match[1];
70
+ }
71
+ }
60
72
  const userAgent = process.env.npm_config_user_agent;
61
73
  if (userAgent) {
62
74
  if (userAgent.startsWith('yarn')) {
package/lib/utils.js CHANGED
@@ -3,11 +3,13 @@ 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.TRANSFORMER_INQUIRER_CHOICES = void 0;
6
+ exports.TRANSFORMER_INQUIRER_CHOICES = exports.NEXT_CODEMOD_IGNORE_ERROR_PREFIX = exports.NEXT_CODEMOD_ERROR_PREFIX = void 0;
7
7
  exports.checkGitStatus = checkGitStatus;
8
8
  exports.onCancel = onCancel;
9
9
  const picocolors_1 = require("picocolors");
10
10
  const is_git_clean_1 = __importDefault(require("is-git-clean"));
11
+ exports.NEXT_CODEMOD_ERROR_PREFIX = '@next-codemod-error';
12
+ exports.NEXT_CODEMOD_IGNORE_ERROR_PREFIX = '@next-codemod-ignore';
11
13
  function checkGitStatus(force) {
12
14
  let clean = false;
13
15
  let errorMessage = 'Unable to determine if git directory is clean';
@@ -136,11 +138,13 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
136
138
  title: 'Add `export const instant = false` to App Router pages and layouts to ease Cache Components adoption',
137
139
  value: 'cache-components-instant-false',
138
140
  version: '16.3.0',
141
+ adoption: true,
139
142
  },
140
143
  {
141
144
  title: "Remove `export const prefetch = 'partial'` Route Segment Config from App Router pages and layouts after enabling `partialPrefetching` globally",
142
145
  value: 'remove-partial-prefetch',
143
146
  version: '16.3.0',
147
+ adoption: true,
144
148
  },
145
149
  ];
146
150
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next/codemod",
3
- "version": "16.4.0-canary.8",
3
+ "version": "16.4.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = transformer;
4
4
  const parser_1 = require("../lib/parser");
5
+ const utils_1 = require("../lib/utils");
5
6
  /**
6
7
  * Blanket-inserts `export const instant = false` into every App Router `page`,
7
8
  * `layout`, and `default` file so they're marked as allowed to block when
@@ -19,7 +20,7 @@ const parser_1 = require("../lib/parser");
19
20
  */
20
21
  function transformer(file, _api) {
21
22
  if (process.env.NODE_ENV !== 'test' &&
22
- !/(^|[/\\])app[/\\].*?(page|layout|default)\.[^/\\]+$/.test(file.path)) {
23
+ !/(^|[/\\])app[/\\](?:.*[/\\])?(page|layout|default)\.[^/\\]+$/.test(file.path)) {
23
24
  return file.source;
24
25
  }
25
26
  const j = (0, parser_1.createParserFromPath)(file.path);
@@ -86,15 +87,16 @@ function transformer(file, _api) {
86
87
  if (hasInstantBinding) {
87
88
  return file.source;
88
89
  }
89
- // Build `export const instant = false`. The two `//` comments above it
90
- // (TODO + See:) are attached as leading comments on the declaration so
90
+ // Build `export const instant = false`. The ignore reason, removal condition,
91
+ // and guide link are attached as leading comments on the declaration so
91
92
  // 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 ignoreComment = j.commentLine(` ${utils_1.NEXT_CODEMOD_IGNORE_ERROR_PREFIX} Cache Components adoption: this segment temporarily allows blocking.`, true, false);
94
+ const removalComment = j.commentLine(' Remove this opt-out after verifying the segment passes validation without it.', true, false);
93
95
  const seeComment = j.commentLine(' See: https://nextjs.org/docs/app/guides/migrating-to-cache-components', true, false);
94
96
  const instantExport = j.exportNamedDeclaration(j.variableDeclaration('const', [
95
97
  j.variableDeclarator(j.identifier('instant'), j.booleanLiteral(false)),
96
98
  ]));
97
- instantExport.comments = [todoComment, seeComment];
99
+ instantExport.comments = [ignoreComment, removalComment, seeComment];
98
100
  // Insert after the last top-level import, or at the top of the module
99
101
  // if there are no imports.
100
102
  const body = program.body;
@@ -110,14 +112,19 @@ function transformer(file, _api) {
110
112
  // No imports. Inserting at index 0 would steal any file-level leading
111
113
  // comments (e.g. `// @ts-nocheck`) from `body[0]` because recast
112
114
  // attributes them to whatever is first. Move those leading comments
113
- // off `body[0]` onto the new export *before* its TODO/See: lines, so
115
+ // off `body[0]` onto the new export *before* its adoption comments, so
114
116
  // they print in their original position.
115
117
  const first = body[0];
116
118
  const allComments = (first.comments ?? []);
117
119
  const firstLeading = allComments.filter((c) => c.leading === true);
118
120
  if (firstLeading.length > 0) {
119
121
  first.comments = allComments.filter((c) => c.leading !== true);
120
- instantExport.comments = [...firstLeading, todoComment, seeComment];
122
+ instantExport.comments = [
123
+ ...firstLeading,
124
+ ignoreComment,
125
+ removalComment,
126
+ seeComment,
127
+ ];
121
128
  }
122
129
  body.unshift(instantExport);
123
130
  }
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.transformDynamicAPI = transformDynamicAPI;
4
4
  const utils_1 = require("./utils");
5
5
  const parser_1 = require("../../../lib/parser");
6
- const DYNAMIC_IMPORT_WARN_COMMENT = ` @next-codemod-error The APIs under 'next/headers' are async now, need to be manually awaited. `;
6
+ const utils_2 = require("../../../lib/utils");
7
+ const DYNAMIC_IMPORT_WARN_COMMENT = ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} The APIs under 'next/headers' are async now, need to be manually awaited. `;
7
8
  function findDynamicImportsAndComment(root, j) {
8
9
  let modified = false;
9
10
  // find all the dynamic imports of `next/headers`,
@@ -146,12 +147,12 @@ function transformDynamicAPI(source, _api, filePath) {
146
147
  needsReactUseImport = true;
147
148
  }
148
149
  else {
149
- const casted = castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` ${utils_1.NEXT_CODEMOD_ERROR_PREFIX} Manually await this call and refactor the function to be async `);
150
+ const casted = castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} Manually await this call and refactor the function to be async `);
150
151
  modified ||= casted;
151
152
  }
152
153
  }
153
154
  else {
154
- const casted = castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` ${utils_1.NEXT_CODEMOD_ERROR_PREFIX} please manually await this call, codemod cannot transform due to undetermined async scope `);
155
+ const casted = castTypesOrAddComment(j, path, originRequestApiName, root, filePath, insertedTypes, ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} please manually await this call, codemod cannot transform due to undetermined async scope `);
155
156
  modified ||= casted;
156
157
  }
157
158
  }
@@ -227,6 +228,18 @@ function castTypesOrAddComment(j, path, originRequestApiName, root, filePath, in
227
228
  e.g. `<path>` is cookies(), convert it to `(<path> as unknown as UnsafeUnwrappedCookies)`
228
229
  */
229
230
  const targetType = API_CAST_TYPE_MAP[originRequestApiName];
231
+ const repairComment = ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} Await this API and update its callers; remove the temporary ${targetType} cast after repairing the migration. `;
232
+ const parentCast = path.parentPath?.node;
233
+ const outerCast = path.parentPath?.parentPath?.node;
234
+ if (j.TSAsExpression.check(parentCast) &&
235
+ j.TSAsExpression.check(outerCast) &&
236
+ j.TSTypeReference.check(outerCast.typeAnnotation) &&
237
+ j.Identifier.check(outerCast.typeAnnotation.typeName) &&
238
+ outerCast.typeAnnotation.typeName.name === targetType) {
239
+ // Re-parsing attaches the leading marker to the outer cast.
240
+ return (0, utils_1.insertCommentOnce)(outerCast, j, repairComment);
241
+ }
242
+ (0, utils_1.insertCommentOnce)(path.node, j, repairComment);
230
243
  const newCastExpression = j.tsAsExpression(j.tsAsExpression(path.node, j.tsUnknownKeyword()), j.tsTypeReference(j.identifier(targetType)));
231
244
  // Replace the original expression with the new cast expression,
232
245
  // also wrap () around the new cast expression.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.transformDynamicProps = transformDynamicProps;
4
4
  const utils_1 = require("./utils");
5
5
  const parser_1 = require("../../../lib/parser");
6
+ const utils_2 = require("../../../lib/utils");
6
7
  const PAGE_PROPS = 'props';
7
8
  // Find all the member access of the prop, and await them
8
9
  // e.g. If there's argument `props`, find all the member access of props.<name>.
@@ -40,7 +41,7 @@ function awaitMemberAccessOfProp(propIdName, path, j) {
40
41
  parentScopeOfMemberAccess.node !== path.node) {
41
42
  // If it's not able to convert, add a comment to the prop access to warn the user
42
43
  // e.g. the parent scope is sync, await keyword can't be applied
43
- const comment = ` ${utils_1.NEXT_CODEMOD_ERROR_PREFIX} '${propIdName}.${memberProperty.name}' is accessed without awaiting.`;
44
+ const comment = ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} '${propIdName}.${memberProperty.name}' is accessed without awaiting.`;
44
45
  (0, utils_1.insertCommentOnce)(member, j, comment);
45
46
  return;
46
47
  }
@@ -121,7 +122,7 @@ function commentOnMatchedReExports(root, j) {
121
122
  specifier.exported.name === 'default')) {
122
123
  if (j.Literal.check(path.value.source)) {
123
124
  const localName = specifier.local.name;
124
- const commentInserted = (0, utils_1.insertCommentOnce)(specifier, j, ` ${utils_1.NEXT_CODEMOD_ERROR_PREFIX} \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\``);
125
+ const commentInserted = (0, utils_1.insertCommentOnce)(specifier, j, ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\``);
125
126
  modified ||= commentInserted;
126
127
  }
127
128
  else if (path.value.source === null) {
@@ -134,7 +135,7 @@ function commentOnMatchedReExports(root, j) {
134
135
  return importPath.value.specifiers.some((importSpecifier) => importSpecifier.local.name === localName);
135
136
  });
136
137
  if (importDeclaration.size() > 0) {
137
- const commentInserted = (0, utils_1.insertCommentOnce)(specifier, j, ` ${utils_1.NEXT_CODEMOD_ERROR_PREFIX} \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\``);
138
+ const commentInserted = (0, utils_1.insertCommentOnce)(specifier, j, ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} \`${localName}\` export is re-exported. Check if this component uses \`params\` or \`searchParams\``);
138
139
  modified ||= commentInserted;
139
140
  }
140
141
  }
@@ -410,7 +411,7 @@ function transformDynamicProps(source, _api, filePath) {
410
411
  // find the argument `currentParam`
411
412
  const args = callExpression.value.arguments;
412
413
  const propPassedAsArg = args.find((arg) => j.Identifier.check(arg) && arg.name === argName);
413
- const comment = ` ${utils_1.NEXT_CODEMOD_ERROR_PREFIX} '${argName}' is passed as an argument. Any asynchronous properties of 'props' must be awaited when accessed. `;
414
+ const comment = ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} '${argName}' is passed as an argument. Any asynchronous properties of 'props' must be awaited when accessed. `;
414
415
  const inserted = (0, utils_1.insertCommentOnce)(propPassedAsArg, j, comment);
415
416
  modified ||= inserted;
416
417
  });
@@ -750,7 +751,7 @@ function commentSpreadProps(path, propsIdentifierName, j) {
750
751
  const objSpreadProperties = functionBodyCollection.find(j.SpreadElement, {
751
752
  argument: { name: propsIdentifierName },
752
753
  });
753
- const comment = ` ${utils_1.NEXT_CODEMOD_ERROR_PREFIX} '${propsIdentifierName}' is used with spread syntax (...). Any asynchronous properties of '${propsIdentifierName}' must be awaited when accessed. `;
754
+ const comment = ` ${utils_2.NEXT_CODEMOD_ERROR_PREFIX} '${propsIdentifierName}' is used with spread syntax (...). Any asynchronous properties of '${propsIdentifierName}' must be awaited when accessed. `;
754
755
  // Add comment before it
755
756
  jsxSpreadProperties.forEach((spread) => {
756
757
  const inserted = (0, utils_1.insertCommentOnce)(spread.value, j, comment);
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isReactHookName = exports.TARGET_PROP_NAMES = exports.TARGET_NAMED_EXPORTS = exports.TARGET_ROUTE_EXPORTS = exports.NEXT_CODEMOD_ERROR_PREFIX = exports.NEXTJS_ENTRY_FILES = void 0;
3
+ exports.isReactHookName = exports.TARGET_PROP_NAMES = exports.TARGET_NAMED_EXPORTS = exports.TARGET_ROUTE_EXPORTS = exports.NEXTJS_ENTRY_FILES = void 0;
4
4
  exports.isFunctionType = isFunctionType;
5
5
  exports.isMatchedFunctionExported = isMatchedFunctionExported;
6
6
  exports.determineClientDirective = determineClientDirective;
@@ -19,8 +19,7 @@ exports.containsReactHooksCallExpressions = containsReactHooksCallExpressions;
19
19
  exports.isParentUseCallExpression = isParentUseCallExpression;
20
20
  exports.isParentPromiseAllCallExpression = isParentPromiseAllCallExpression;
21
21
  exports.NEXTJS_ENTRY_FILES = /([\\/]|^)(page|layout|route|default)\.(t|j)sx?$/;
22
- exports.NEXT_CODEMOD_ERROR_PREFIX = '@next-codemod-error';
23
- const NEXT_CODEMOD_IGNORE_ERROR_PREFIX = '@next-codemod-ignore';
22
+ const utils_1 = require("../../../lib/utils");
24
23
  exports.TARGET_ROUTE_EXPORTS = new Set([
25
24
  'GET',
26
25
  'POST',
@@ -332,13 +331,13 @@ function wrapParentheseIfNeeded(hasChainAccess, j, expression) {
332
331
  function existsComment(comments, comment) {
333
332
  const isCodemodErrorComment = comment
334
333
  .trim()
335
- .startsWith(exports.NEXT_CODEMOD_ERROR_PREFIX);
334
+ .startsWith(utils_1.NEXT_CODEMOD_ERROR_PREFIX);
336
335
  let hasIgnoreComment = false;
337
336
  let hasComment = false;
338
337
  if (comments) {
339
338
  comments.forEach((commentNode) => {
340
339
  const currentComment = commentNode.value;
341
- if (currentComment.trim().startsWith(NEXT_CODEMOD_IGNORE_ERROR_PREFIX)) {
340
+ if (currentComment.trim().startsWith(utils_1.NEXT_CODEMOD_IGNORE_ERROR_PREFIX)) {
342
341
  hasIgnoreComment = true;
343
342
  }
344
343
  if (currentComment === comment) {
@@ -4,7 +4,7 @@
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.default = transformer;
6
6
  const parser_1 = require("../lib/parser");
7
- const utils_1 = require("./lib/async-request-api/utils");
7
+ const utils_1 = require("../lib/utils");
8
8
  function transformer(file, _api) {
9
9
  const j = (0, parser_1.createParserFromPath)(file.path);
10
10
  const $j = j(file.source);
@@ -836,6 +836,13 @@ function transformer(files, options = {}) {
836
836
  console.error('Error: package.json not found in the specified directory');
837
837
  return;
838
838
  }
839
+ const packageJsonContent = (0, node_fs_1.readFileSync)(packageJsonPath, 'utf8');
840
+ const packageJson = JSON.parse(packageJsonContent);
841
+ const usesNextLint = Object.values(packageJson.scripts || {}).some((script) => typeof script === 'string' && script.includes('next lint'));
842
+ if (!usesNextLint) {
843
+ console.log('Skipping migration: no next lint script found in package.json.');
844
+ return;
845
+ }
839
846
  const isTypeScript = detectTypeScript(projectRoot);
840
847
  console.log('Migrating from next lint to the ESLint CLI...');
841
848
  // Check for existing ESLint config
@@ -897,7 +904,6 @@ function transformer(files, options = {}) {
897
904
  console.log(' Please manually ensure your ESLint config includes the Next.js configurations');
898
905
  }
899
906
  }
900
- const packageJsonContent = (0, node_fs_1.readFileSync)(packageJsonPath, 'utf8');
901
907
  const result = updatePackageJsonScripts(packageJsonContent);
902
908
  if (result.updated) {
903
909
  try {