@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 +11 -0
- package/bin/agents-md.js +8 -9
- package/bin/next-codemod.js +4 -1
- package/bin/transform.js +6 -3
- package/bin/upgrade.js +63 -23
- package/lib/__tests__/agents-md-e2e.test.js +43 -47
- package/lib/agents-md.js +23 -29
- package/lib/handle-package.js +12 -0
- package/lib/utils.js +5 -1
- package/package.json +1 -1
- package/transforms/cache-components-instant-false.js +14 -7
- package/transforms/lib/async-request-api/next-async-dynamic-api.js +16 -3
- package/transforms/lib/async-request-api/next-async-dynamic-prop.js +6 -5
- package/transforms/lib/async-request-api/utils.js +4 -5
- package/transforms/new-link.js +1 -1
- package/transforms/next-lint-to-eslint-cli.js +7 -1
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
|
|
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
|
|
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(
|
|
77
|
-
existingContent = fs_1.default.readFileSync(
|
|
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.
|
|
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.
|
|
103
|
-
fs_1.default.writeFileSync(
|
|
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: '
|
|
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) {
|
package/bin/next-codemod.js
CHANGED
|
@@ -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.,
|
|
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:
|
|
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 =
|
|
222
|
-
?
|
|
223
|
-
:
|
|
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 (!
|
|
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 (
|
|
262
|
-
targetReactVersion.startsWith('19.0.0-
|
|
263
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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: '
|
|
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
|
|
87
|
-
const
|
|
88
|
-
expect(fs.existsSync(
|
|
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
|
|
90
|
+
const agentsMdContent = fs.readFileSync(agentsMdPath, 'utf-8')
|
|
91
91
|
|
|
92
92
|
// Verify content structure
|
|
93
|
-
expect(
|
|
94
|
-
expect(
|
|
95
|
-
expect(
|
|
96
|
-
expect(
|
|
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 =
|
|
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('
|
|
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
|
|
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
|
|
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: '
|
|
146
|
+
output: 'AGENTS.md',
|
|
150
147
|
})
|
|
151
148
|
|
|
152
149
|
// Verify file was updated, not replaced
|
|
153
|
-
const
|
|
154
|
-
path.join(testProjectDir, '
|
|
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(
|
|
160
|
-
expect(
|
|
161
|
-
expect(
|
|
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(
|
|
165
|
-
expect(
|
|
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
|
|
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: '
|
|
215
|
+
output: 'AGENTS.md',
|
|
220
216
|
})
|
|
221
217
|
|
|
222
218
|
// Verify files created in subdirectory
|
|
223
|
-
expect(fs.existsSync(path.join(subDir, '
|
|
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: '
|
|
233
|
+
output: 'AGENTS.md',
|
|
238
234
|
})
|
|
239
235
|
|
|
240
|
-
const
|
|
241
|
-
path.join(testProjectDir, '
|
|
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 =
|
|
249
|
-
const endIdx =
|
|
250
|
-
const indexContent =
|
|
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: '
|
|
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: '
|
|
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
|
|
345
|
-
path.join(testProjectDir, '
|
|
340
|
+
const agentsMdContent = fs.readFileSync(
|
|
341
|
+
path.join(testProjectDir, 'AGENTS.md'),
|
|
346
342
|
'utf-8'
|
|
347
343
|
)
|
|
348
|
-
expect(
|
|
344
|
+
expect(agentsMdContent).toContain(
|
|
349
345
|
'root: ./node_modules/next/dist/docs'
|
|
350
346
|
)
|
|
351
|
-
expect(
|
|
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: '
|
|
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
|
|
400
|
-
path.join(testProjectDir, '
|
|
395
|
+
const agentsMdContent = fs.readFileSync(
|
|
396
|
+
path.join(testProjectDir, 'AGENTS.md'),
|
|
401
397
|
'utf-8'
|
|
402
398
|
)
|
|
403
|
-
expect(
|
|
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
|
|
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.
|
|
20
|
-
exports.
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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'
|
|
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
|
|
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 || '
|
|
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
|
|
309
|
+
function injectIntoAgentsMd(agentsMdContent, indexContent) {
|
|
316
310
|
const wrappedContent = wrapWithMarkers(indexContent);
|
|
317
|
-
if (hasExistingIndex(
|
|
318
|
-
const startIdx =
|
|
319
|
-
const endIdx =
|
|
320
|
-
return (
|
|
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
|
-
|
|
316
|
+
agentsMdContent.slice(endIdx));
|
|
323
317
|
}
|
|
324
|
-
const separator =
|
|
325
|
-
return
|
|
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) {
|
package/lib/handle-package.js
CHANGED
|
@@ -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
|
@@ -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[/\\]
|
|
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
|
|
90
|
-
//
|
|
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
|
|
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 = [
|
|
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
|
|
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 = [
|
|
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
|
|
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, ` ${
|
|
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, ` ${
|
|
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 = ` ${
|
|
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, ` ${
|
|
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, ` ${
|
|
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 = ` ${
|
|
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 = ` ${
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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) {
|
package/transforms/new-link.js
CHANGED
|
@@ -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("
|
|
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 {
|