@next/codemod 16.3.0-preview.0 → 16.3.0-preview.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agents-md.js +28 -12
- package/bin/next-codemod.js +1 -0
- package/bin/upgrade.js +135 -29
- package/lib/__tests__/agents-md-e2e.test.js +117 -0
- package/lib/agents-md.js +76 -0
- package/lib/handle-package.js +62 -3
- package/lib/utils.js +10 -0
- package/package.json +10 -9
- package/transforms/cache-components-instant-false.js +129 -0
- package/transforms/next-experimental-turbo-to-turbopack.js +1 -1
- package/transforms/remove-partial-prefetch.js +199 -0
package/bin/agents-md.js
CHANGED
|
@@ -59,8 +59,17 @@ async function runAgentsMd(options) {
|
|
|
59
59
|
targetFile = promptedOptions.targetFile;
|
|
60
60
|
}
|
|
61
61
|
const claudeMdPath = path_1.default.join(cwd, targetFile);
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// Next.js >= 16.2.0 ships its docs inside the published package. When the
|
|
63
|
+
// installed version matches the requested one, index the bundled docs
|
|
64
|
+
// directly instead of downloading a copy into .next-docs.
|
|
65
|
+
const bundledDocs = (0, agents_md_1.getBundledDocsInfo)(cwd);
|
|
66
|
+
const useBundledDocs = bundledDocs !== null && bundledDocs.version === nextjsVersion;
|
|
67
|
+
const docsPath = useBundledDocs
|
|
68
|
+
? bundledDocs.docsPath
|
|
69
|
+
: path_1.default.join(cwd, DOCS_DIR_NAME);
|
|
70
|
+
const docsLinkPath = useBundledDocs
|
|
71
|
+
? (0, agents_md_1.getBundledDocsLinkPath)(cwd, bundledDocs.docsPath)
|
|
72
|
+
: `./${DOCS_DIR_NAME}`;
|
|
64
73
|
let sizeBefore = 0;
|
|
65
74
|
let isNewFile = true;
|
|
66
75
|
let existingContent = '';
|
|
@@ -69,14 +78,19 @@ async function runAgentsMd(options) {
|
|
|
69
78
|
sizeBefore = Buffer.byteLength(existingContent, 'utf-8');
|
|
70
79
|
isNewFile = false;
|
|
71
80
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
if (useBundledDocs) {
|
|
82
|
+
console.log(`\nUsing the docs bundled with Next.js ${picocolors_1.default.cyan(nextjsVersion)} at ${picocolors_1.default.cyan(docsLinkPath)} (no download needed).`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
console.log(`\nDownloading Next.js ${picocolors_1.default.cyan(nextjsVersion)} documentation to ${picocolors_1.default.cyan(DOCS_DIR_NAME)}...`);
|
|
86
|
+
const pullResult = await (0, agents_md_1.pullDocs)({
|
|
87
|
+
cwd,
|
|
88
|
+
version: nextjsVersion,
|
|
89
|
+
docsDir: docsPath,
|
|
90
|
+
});
|
|
91
|
+
if (!pullResult.success) {
|
|
92
|
+
throw new shared_1.BadInput(`Failed to pull docs: ${pullResult.error}`);
|
|
93
|
+
}
|
|
80
94
|
}
|
|
81
95
|
const docFiles = (0, agents_md_1.collectDocFiles)(docsPath);
|
|
82
96
|
const sections = (0, agents_md_1.buildDocTree)(docFiles);
|
|
@@ -88,13 +102,15 @@ async function runAgentsMd(options) {
|
|
|
88
102
|
const newContent = (0, agents_md_1.injectIntoClaudeMd)(existingContent, indexContent);
|
|
89
103
|
fs_1.default.writeFileSync(claudeMdPath, newContent, 'utf-8');
|
|
90
104
|
const sizeAfter = Buffer.byteLength(newContent, 'utf-8');
|
|
91
|
-
|
|
105
|
+
// .next-docs only exists on the download path; bundled docs live in
|
|
106
|
+
// node_modules, which is already ignored.
|
|
107
|
+
const gitignoreResult = useBundledDocs ? null : (0, agents_md_1.ensureGitignoreEntry)(cwd);
|
|
92
108
|
const action = isNewFile ? 'Created' : 'Updated';
|
|
93
109
|
const sizeInfo = isNewFile
|
|
94
110
|
? formatSize(sizeAfter)
|
|
95
111
|
: `${formatSize(sizeBefore)} → ${formatSize(sizeAfter)}`;
|
|
96
112
|
console.log(`${picocolors_1.default.green('✓')} ${action} ${picocolors_1.default.bold(targetFile)} (${sizeInfo})`);
|
|
97
|
-
if (gitignoreResult
|
|
113
|
+
if (gitignoreResult?.updated) {
|
|
98
114
|
console.log(`${picocolors_1.default.green('✓')} Added ${picocolors_1.default.bold(DOCS_DIR_NAME)} to .gitignore`);
|
|
99
115
|
}
|
|
100
116
|
console.log('');
|
package/bin/next-codemod.js
CHANGED
|
@@ -43,6 +43,7 @@ program
|
|
|
43
43
|
.argument('[revision]', 'Specify the upgrade type ("patch", "minor", "major"), an NPM dist tag (e.g. "latest", "canary", "rc"), or an exact version (e.g. "15.0.0"). Defaults to "minor".')
|
|
44
44
|
.usage('[revision] [options]')
|
|
45
45
|
.option('--verbose', 'Verbose output', false)
|
|
46
|
+
.option('-y, --yes', 'Skip every interactive prompt and accept its default. Also auto-enabled when stdin is not a TTY (e.g. running under an agent or in CI).', false)
|
|
46
47
|
.action(async (revision, options) => {
|
|
47
48
|
try {
|
|
48
49
|
await (0, upgrade_1.runUpgrade)(revision, options);
|
package/bin/upgrade.js
CHANGED
|
@@ -47,6 +47,7 @@ const picocolors_1 = __importDefault(require("picocolors"));
|
|
|
47
47
|
const handle_package_1 = require("../lib/handle-package");
|
|
48
48
|
const transform_1 = require("./transform");
|
|
49
49
|
const utils_1 = require("../lib/utils");
|
|
50
|
+
const agents_md_1 = require("../lib/agents-md");
|
|
50
51
|
const shared_1 = require("./shared");
|
|
51
52
|
const optionalNextjsPackages = [
|
|
52
53
|
'create-next-app',
|
|
@@ -114,6 +115,10 @@ function resolveSemanticRevision(revision, installedVersion) {
|
|
|
114
115
|
}
|
|
115
116
|
async function runUpgrade(revision, options) {
|
|
116
117
|
const { verbose } = options;
|
|
118
|
+
const nonInteractive = options.yes === true || !process.stdin.isTTY;
|
|
119
|
+
if (nonInteractive) {
|
|
120
|
+
console.log(` Running in non-interactive mode. Every prompt will accept its default.`);
|
|
121
|
+
}
|
|
117
122
|
const appPackageJsonPath = path_1.default.resolve(cwd, 'package.json');
|
|
118
123
|
let appPackageJson = JSON.parse(fs_1.default.readFileSync(appPackageJsonPath, 'utf8'));
|
|
119
124
|
const installedNextVersion = getInstalledNextVersion();
|
|
@@ -189,18 +194,24 @@ async function runUpgrade(revision, options) {
|
|
|
189
194
|
// The mixed case is tricky to handle from a types perspective.
|
|
190
195
|
// We'll recommend to upgrade in the prompt but users can decide to try 18.
|
|
191
196
|
!isPureAppRouter) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
197
|
+
if (nonInteractive) {
|
|
198
|
+
// Default: upgrade React past 18.
|
|
199
|
+
shouldStayOnReact18 = false;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const shouldStayOnReact18Res = await (0, prompts_1.default)({
|
|
203
|
+
type: 'confirm',
|
|
204
|
+
name: 'shouldStayOnReact18',
|
|
205
|
+
message: `Do you prefer to stay on React 18?` +
|
|
206
|
+
(isMixedApp
|
|
207
|
+
? " Since you're using both pages/ and app/, we recommend upgrading React to use a consistent version throughout your app."
|
|
208
|
+
: ''),
|
|
209
|
+
initial: false,
|
|
210
|
+
active: 'Yes',
|
|
211
|
+
inactive: 'No',
|
|
212
|
+
}, { onCancel: utils_1.onCancel });
|
|
213
|
+
shouldStayOnReact18 = shouldStayOnReact18Res.shouldStayOnReact18;
|
|
214
|
+
}
|
|
204
215
|
}
|
|
205
216
|
// We're resolving a specific version here to avoid including "ugly" version queries
|
|
206
217
|
// in the manifest.
|
|
@@ -212,9 +223,9 @@ async function runUpgrade(revision, options) {
|
|
|
212
223
|
: await loadHighestNPMVersionMatching(`react@${targetNextPackageJson.peerDependencies['react']}`);
|
|
213
224
|
if ((0, semver_1.compare)(targetNextVersion, '15.0.0-canary') >= 0 &&
|
|
214
225
|
(0, semver_1.compare)(targetNextVersion, '16.0.0-canary') < 0) {
|
|
215
|
-
await suggestTurbopack(appPackageJson, targetNextVersion);
|
|
226
|
+
await suggestTurbopack(appPackageJson, targetNextVersion, nonInteractive);
|
|
216
227
|
}
|
|
217
|
-
const codemods = await suggestCodemods(installedNextVersion, targetNextVersion);
|
|
228
|
+
const codemods = await suggestCodemods(installedNextVersion, targetNextVersion, nonInteractive);
|
|
218
229
|
const packageManager = (0, handle_package_1.getPkgManager)(cwd);
|
|
219
230
|
let shouldRunReactCodemods = false;
|
|
220
231
|
let shouldRunReactTypesCodemods = false;
|
|
@@ -223,8 +234,9 @@ async function runUpgrade(revision, options) {
|
|
|
223
234
|
if (!shouldStayOnReact18 &&
|
|
224
235
|
(0, semver_1.compare)(targetReactVersion, '19.0.0-0') >= 0 &&
|
|
225
236
|
(0, semver_1.compare)(installedReactVersion, '19.0.0-0') < 0) {
|
|
226
|
-
shouldRunReactCodemods = await suggestReactCodemods();
|
|
227
|
-
shouldRunReactTypesCodemods =
|
|
237
|
+
shouldRunReactCodemods = await suggestReactCodemods(nonInteractive);
|
|
238
|
+
shouldRunReactTypesCodemods =
|
|
239
|
+
await suggestReactTypesCodemods(nonInteractive);
|
|
228
240
|
execCommand = getNpxCommand(packageManager);
|
|
229
241
|
}
|
|
230
242
|
fs_1.default.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2));
|
|
@@ -284,6 +296,37 @@ async function runUpgrade(revision, options) {
|
|
|
284
296
|
};
|
|
285
297
|
}
|
|
286
298
|
}
|
|
299
|
+
// Bump `eslint` alongside `eslint-config-next` so the install doesn't fail
|
|
300
|
+
// on a peer-dep mismatch. e.g. `eslint-config-next@16.x` requires
|
|
301
|
+
// `eslint@>=9`, but a project upgrading from Next 15 will still have
|
|
302
|
+
// `eslint@^8` from create-next-app. Skip silently if anything goes wrong;
|
|
303
|
+
// the worst case is the user hits the same peer-dep error they would have
|
|
304
|
+
// without this bump.
|
|
305
|
+
//
|
|
306
|
+
// Only act when the project is actually using `eslint-config-next` — we
|
|
307
|
+
// don't want to silently upgrade eslint majors for projects that use
|
|
308
|
+
// eslint for unrelated reasons.
|
|
309
|
+
if (allDependencies['eslint'] && allDependencies['eslint-config-next']) {
|
|
310
|
+
try {
|
|
311
|
+
const eslintConfigNextPeerDepsJSON = (0, child_process_1.execSync)(`npm --silent view "eslint-config-next@${targetNextVersion}" peerDependencies --json`, { encoding: 'utf-8' });
|
|
312
|
+
const eslintConfigNextPeerDeps = eslintConfigNextPeerDepsJSON.trim() === ''
|
|
313
|
+
? {}
|
|
314
|
+
: JSON.parse(eslintConfigNextPeerDepsJSON);
|
|
315
|
+
const eslintRange = eslintConfigNextPeerDeps?.eslint;
|
|
316
|
+
if (eslintRange) {
|
|
317
|
+
const targetEslintVersion = await loadHighestNPMVersionMatching(`eslint@${eslintRange}`);
|
|
318
|
+
versionMapping['eslint'] = {
|
|
319
|
+
version: targetEslintVersion,
|
|
320
|
+
required: false,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (e) {
|
|
325
|
+
if (verbose) {
|
|
326
|
+
console.warn(` Could not determine eslint peer range from eslint-config-next@${targetNextVersion}. Leaving eslint version alone.`, e);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
287
330
|
// Even though we only need those if we alias `@types/react` to types-react,
|
|
288
331
|
// we still do it out of safety due to https://github.com/microsoft/DefinitelyTyped-tools/issues/433.
|
|
289
332
|
const overrides = {};
|
|
@@ -320,10 +363,11 @@ async function runUpgrade(revision, options) {
|
|
|
320
363
|
// understanding of the codemods, we run all of the applicable codemods.
|
|
321
364
|
if (shouldRunReactCodemods) {
|
|
322
365
|
// https://react.dev/blog/2024/04/25/react-19-upgrade-guide#run-all-react-19-codemods
|
|
323
|
-
(0, child_process_1.execSync)(
|
|
324
366
|
// `--no-interactive` skips the interactive prompt that asks for confirmation
|
|
325
367
|
// https://github.com/codemod-com/codemod/blob/c0cf00d13161a0ec0965b6cc6bc5d54076839cc8/apps/cli/src/flags.ts#L160
|
|
326
|
-
|
|
368
|
+
// `--allow-dirty` is required because the upgrade above modified package.json
|
|
369
|
+
// 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' });
|
|
327
371
|
}
|
|
328
372
|
if (shouldRunReactTypesCodemods) {
|
|
329
373
|
// https://react.dev/blog/2024/04/25/react-19-upgrade-guide#typescript-changes
|
|
@@ -337,6 +381,14 @@ async function runUpgrade(revision, options) {
|
|
|
337
381
|
if (codemods.length > 0) {
|
|
338
382
|
console.log(`${picocolors_1.default.green('✔')} Codemods have been applied successfully.`);
|
|
339
383
|
}
|
|
384
|
+
try {
|
|
385
|
+
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.`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// The block refresh is best-effort — never fail the upgrade over it.
|
|
391
|
+
}
|
|
340
392
|
warnDependenciesOutOfRange(appPackageJson, versionMapping);
|
|
341
393
|
endMessage(targetNextVersion);
|
|
342
394
|
}
|
|
@@ -383,7 +435,7 @@ function isUsingAppDir(projectPath) {
|
|
|
383
435
|
* 3. Otherwise, we ask the user to manually add `--turbopack` to their dev command,
|
|
384
436
|
* showing the current dev command as the initial value.
|
|
385
437
|
*/
|
|
386
|
-
async function suggestTurbopack(packageJson, targetNextVersion) {
|
|
438
|
+
async function suggestTurbopack(packageJson, targetNextVersion, nonInteractive) {
|
|
387
439
|
const devScript = packageJson.scripts?.['dev'];
|
|
388
440
|
// Turbopack flag was changed from `--turbo` to `--turbopack` in v15.0.1-canary.3
|
|
389
441
|
// PR: https://github.com/vercel/next.js/pull/71657
|
|
@@ -406,19 +458,28 @@ async function suggestTurbopack(packageJson, targetNextVersion) {
|
|
|
406
458
|
}
|
|
407
459
|
return;
|
|
408
460
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
461
|
+
let enable = true;
|
|
462
|
+
if (!nonInteractive) {
|
|
463
|
+
const responseTurbopack = await (0, prompts_1.default)({
|
|
464
|
+
type: 'confirm',
|
|
465
|
+
name: 'enable',
|
|
466
|
+
message: `Enable Turbopack for ${picocolors_1.default.bold('next dev')}?`,
|
|
467
|
+
initial: true,
|
|
468
|
+
}, { onCancel: utils_1.onCancel });
|
|
469
|
+
enable = responseTurbopack.enable;
|
|
470
|
+
}
|
|
471
|
+
if (!enable) {
|
|
416
472
|
return;
|
|
417
473
|
}
|
|
418
474
|
packageJson.scripts['dev'] = devScript.replace('next dev', `next dev ${turboPackFlag}`);
|
|
419
475
|
return;
|
|
420
476
|
}
|
|
421
477
|
console.log(`${picocolors_1.default.yellow('⚠')} Could not find "${picocolors_1.default.bold('next dev')}" in your dev script.`);
|
|
478
|
+
if (nonInteractive) {
|
|
479
|
+
// Without a TTY we can't ask the user for a replacement script.
|
|
480
|
+
// Keep the existing dev script untouched.
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
422
483
|
const responseCustomDevScript = await (0, prompts_1.default)({
|
|
423
484
|
type: 'text',
|
|
424
485
|
name: 'customDevScript',
|
|
@@ -428,7 +489,7 @@ async function suggestTurbopack(packageJson, targetNextVersion) {
|
|
|
428
489
|
packageJson.scripts['dev'] =
|
|
429
490
|
responseCustomDevScript.customDevScript || devScript;
|
|
430
491
|
}
|
|
431
|
-
async function suggestCodemods(initialNextVersion, targetNextVersion) {
|
|
492
|
+
async function suggestCodemods(initialNextVersion, targetNextVersion, nonInteractive) {
|
|
432
493
|
// example:
|
|
433
494
|
// codemod version: 15.0.0-canary.45
|
|
434
495
|
// 14.3 -> 15.0.0-canary.45: apply
|
|
@@ -451,6 +512,13 @@ async function suggestCodemods(initialNextVersion, targetNextVersion) {
|
|
|
451
512
|
if (relevantCodemods.length === 0) {
|
|
452
513
|
return [];
|
|
453
514
|
}
|
|
515
|
+
if (nonInteractive) {
|
|
516
|
+
// Default: apply every recommended codemod, matching `selected: true` below.
|
|
517
|
+
const all = relevantCodemods.map(({ value }) => value);
|
|
518
|
+
console.log(` Applying all ${picocolors_1.default.blue('codemods')} recommended for your upgrade:\n` +
|
|
519
|
+
all.map((value) => ` - ${value}`).join('\n'));
|
|
520
|
+
return all;
|
|
521
|
+
}
|
|
454
522
|
const { codemods } = await (0, prompts_1.default)({
|
|
455
523
|
type: 'multiselect',
|
|
456
524
|
name: 'codemods',
|
|
@@ -466,7 +534,10 @@ async function suggestCodemods(initialNextVersion, targetNextVersion) {
|
|
|
466
534
|
}, { onCancel: utils_1.onCancel });
|
|
467
535
|
return codemods;
|
|
468
536
|
}
|
|
469
|
-
async function suggestReactCodemods() {
|
|
537
|
+
async function suggestReactCodemods(nonInteractive) {
|
|
538
|
+
if (nonInteractive) {
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
470
541
|
const { runReactCodemod } = await (0, prompts_1.default)({
|
|
471
542
|
type: 'confirm',
|
|
472
543
|
name: 'runReactCodemod',
|
|
@@ -475,7 +546,10 @@ async function suggestReactCodemods() {
|
|
|
475
546
|
}, { onCancel: utils_1.onCancel });
|
|
476
547
|
return runReactCodemod;
|
|
477
548
|
}
|
|
478
|
-
async function suggestReactTypesCodemods() {
|
|
549
|
+
async function suggestReactTypesCodemods(nonInteractive) {
|
|
550
|
+
if (nonInteractive) {
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
479
553
|
const { runReactTypesCodemod } = await (0, prompts_1.default)({
|
|
480
554
|
type: 'confirm',
|
|
481
555
|
name: 'runReactTypesCodemod',
|
|
@@ -500,6 +574,16 @@ function writeOverridesField(packageJson, packageManager, overrides) {
|
|
|
500
574
|
}
|
|
501
575
|
}
|
|
502
576
|
else if (packageManager === 'pnpm') {
|
|
577
|
+
// pnpm v11 silently ignores `pnpm.overrides` in package.json. The
|
|
578
|
+
// canonical location moved to `pnpm-workspace.yaml#overrides`.
|
|
579
|
+
// See https://pnpm.io/settings and https://github.com/pnpm/pnpm/issues/11536.
|
|
580
|
+
// When the version cannot be detected, assume the current (v11+) layout
|
|
581
|
+
// since that's the surface where silently-dropped overrides hurt most.
|
|
582
|
+
const pnpmMajorVersion = (0, handle_package_1.getPnpmMajorVersion)();
|
|
583
|
+
if (pnpmMajorVersion === null || pnpmMajorVersion >= 11) {
|
|
584
|
+
writePnpmWorkspaceOverrides(overrides);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
503
587
|
// pnpm supports pnpm.overrides and pnpm.resolutions
|
|
504
588
|
if (packageJson.resolutions) {
|
|
505
589
|
for (const [key, value] of entries) {
|
|
@@ -545,6 +629,28 @@ function writeOverridesField(packageJson, packageManager, overrides) {
|
|
|
545
629
|
}
|
|
546
630
|
}
|
|
547
631
|
}
|
|
632
|
+
function writePnpmWorkspaceOverrides(overrides) {
|
|
633
|
+
// Deferred require so `js-yaml` is only loaded when we hit the pnpm v11+
|
|
634
|
+
// branch (i.e. not for npm/yarn/bun/pnpm-v10 upgrades). The package is CJS,
|
|
635
|
+
// so a sync `require()` keeps this function synchronous.
|
|
636
|
+
const yaml = require('js-yaml');
|
|
637
|
+
const filePath = path_1.default.join(cwd, 'pnpm-workspace.yaml');
|
|
638
|
+
let doc = {};
|
|
639
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
640
|
+
const existing = fs_1.default.readFileSync(filePath, 'utf8');
|
|
641
|
+
const parsed = yaml.load(existing);
|
|
642
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
643
|
+
doc = parsed;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (!doc.overrides || typeof doc.overrides !== 'object') {
|
|
647
|
+
doc.overrides = {};
|
|
648
|
+
}
|
|
649
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
650
|
+
doc.overrides[key] = value;
|
|
651
|
+
}
|
|
652
|
+
fs_1.default.writeFileSync(filePath, yaml.dump(doc));
|
|
653
|
+
}
|
|
548
654
|
function warnDependenciesOutOfRange(appPackageJson, versionMapping) {
|
|
549
655
|
const allDirectDependencies = {
|
|
550
656
|
...appPackageJson.dependencies,
|
|
@@ -293,6 +293,123 @@ This is my project documentation.
|
|
|
293
293
|
}
|
|
294
294
|
}, 30000) // Increase timeout for git clone
|
|
295
295
|
|
|
296
|
+
describe('bundled docs (Next.js >= 16.2.0)', () => {
|
|
297
|
+
// Simulate an install of a Next.js version that ships docs inside the
|
|
298
|
+
// published package at node_modules/next/dist/docs.
|
|
299
|
+
function setupBundledNext(projectDir, version) {
|
|
300
|
+
const nextDir = path.join(projectDir, 'node_modules', 'next')
|
|
301
|
+
const gettingStartedDir = path.join(
|
|
302
|
+
nextDir,
|
|
303
|
+
'dist',
|
|
304
|
+
'docs',
|
|
305
|
+
'01-app',
|
|
306
|
+
'01-getting-started'
|
|
307
|
+
)
|
|
308
|
+
fs.mkdirSync(gettingStartedDir, { recursive: true })
|
|
309
|
+
fs.writeFileSync(
|
|
310
|
+
path.join(nextDir, 'package.json'),
|
|
311
|
+
JSON.stringify({ name: 'next', version })
|
|
312
|
+
)
|
|
313
|
+
fs.writeFileSync(
|
|
314
|
+
path.join(nextDir, 'dist', 'docs', 'index.md'),
|
|
315
|
+
'# Next.js Docs'
|
|
316
|
+
)
|
|
317
|
+
fs.writeFileSync(
|
|
318
|
+
path.join(gettingStartedDir, '01-installation.md'),
|
|
319
|
+
'# Installation'
|
|
320
|
+
)
|
|
321
|
+
fs.writeFileSync(
|
|
322
|
+
path.join(gettingStartedDir, '02-project-structure.md'),
|
|
323
|
+
'# Project Structure'
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
it('indexes bundled docs instead of downloading when installed Next.js ships them', async () => {
|
|
328
|
+
setupBundledNext(testProjectDir, '16.2.0')
|
|
329
|
+
|
|
330
|
+
const originalCwd = process.cwd()
|
|
331
|
+
process.chdir(testProjectDir)
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
await runAgentsMd({ output: 'CLAUDE.md' })
|
|
335
|
+
|
|
336
|
+
// No .next-docs copy and no .gitignore entry for it
|
|
337
|
+
expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
|
|
338
|
+
false
|
|
339
|
+
)
|
|
340
|
+
expect(fs.existsSync(path.join(testProjectDir, '.gitignore'))).toBe(
|
|
341
|
+
false
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
const claudeMdContent = fs.readFileSync(
|
|
345
|
+
path.join(testProjectDir, 'CLAUDE.md'),
|
|
346
|
+
'utf-8'
|
|
347
|
+
)
|
|
348
|
+
expect(claudeMdContent).toContain(
|
|
349
|
+
'root: ./node_modules/next/dist/docs'
|
|
350
|
+
)
|
|
351
|
+
expect(claudeMdContent).toContain('01-installation.md')
|
|
352
|
+
|
|
353
|
+
const output = consoleOutput.join('\n')
|
|
354
|
+
expect(output).toContain('bundled with Next.js')
|
|
355
|
+
expect(output).not.toContain('Downloading')
|
|
356
|
+
} finally {
|
|
357
|
+
process.chdir(originalCwd)
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('uses bundled docs when --version matches the installed version', async () => {
|
|
362
|
+
setupBundledNext(testProjectDir, '16.2.0')
|
|
363
|
+
|
|
364
|
+
const originalCwd = process.cwd()
|
|
365
|
+
process.chdir(testProjectDir)
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
await runAgentsMd({ version: '16.2.0', output: 'AGENTS.md' })
|
|
369
|
+
|
|
370
|
+
expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
|
|
371
|
+
false
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
const agentsMdContent = fs.readFileSync(
|
|
375
|
+
path.join(testProjectDir, 'AGENTS.md'),
|
|
376
|
+
'utf-8'
|
|
377
|
+
)
|
|
378
|
+
expect(agentsMdContent).toContain(
|
|
379
|
+
'root: ./node_modules/next/dist/docs'
|
|
380
|
+
)
|
|
381
|
+
} finally {
|
|
382
|
+
process.chdir(originalCwd)
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('falls back to downloading when --version differs from the installed version', async () => {
|
|
387
|
+
setupBundledNext(testProjectDir, '16.2.0')
|
|
388
|
+
|
|
389
|
+
const originalCwd = process.cwd()
|
|
390
|
+
process.chdir(testProjectDir)
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
await runAgentsMd({ version: '15.0.0', output: 'CLAUDE.md' })
|
|
394
|
+
|
|
395
|
+
expect(fs.existsSync(path.join(testProjectDir, '.next-docs'))).toBe(
|
|
396
|
+
true
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
const claudeMdContent = fs.readFileSync(
|
|
400
|
+
path.join(testProjectDir, 'CLAUDE.md'),
|
|
401
|
+
'utf-8'
|
|
402
|
+
)
|
|
403
|
+
expect(claudeMdContent).toContain('root: ./.next-docs')
|
|
404
|
+
|
|
405
|
+
const output = consoleOutput.join('\n')
|
|
406
|
+
expect(output).toContain('Downloading')
|
|
407
|
+
} finally {
|
|
408
|
+
process.chdir(originalCwd)
|
|
409
|
+
}
|
|
410
|
+
}, 30000) // Increase timeout for git clone
|
|
411
|
+
})
|
|
412
|
+
|
|
296
413
|
describe('getNextjsVersion', () => {
|
|
297
414
|
const fixturesDir = path.join(__dirname, 'fixtures/agents-md')
|
|
298
415
|
|
package/lib/agents-md.js
CHANGED
|
@@ -9,7 +9,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
9
9
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.refreshAgentRulesBlock = refreshAgentRulesBlock;
|
|
12
13
|
exports.getNextjsVersion = getNextjsVersion;
|
|
14
|
+
exports.getBundledDocsInfo = getBundledDocsInfo;
|
|
15
|
+
exports.getBundledDocsLinkPath = getBundledDocsLinkPath;
|
|
13
16
|
exports.pullDocs = pullDocs;
|
|
14
17
|
exports.collectDocFiles = collectDocFiles;
|
|
15
18
|
exports.buildDocTree = buildDocTree;
|
|
@@ -20,6 +23,49 @@ const execa_1 = __importDefault(require("execa"));
|
|
|
20
23
|
const fs_1 = __importDefault(require("fs"));
|
|
21
24
|
const path_1 = __importDefault(require("path"));
|
|
22
25
|
const os_1 = __importDefault(require("os"));
|
|
26
|
+
const AGENT_RULES_START_MARKER = '<!-- BEGIN:nextjs-agent-rules -->';
|
|
27
|
+
/**
|
|
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.
|
|
31
|
+
*
|
|
32
|
+
* Delegates to the installed package's own generator
|
|
33
|
+
* (`next/dist/server/lib/generate-agent-files`), so the block text is
|
|
34
|
+
* always the one shipped with that version — this codemod never
|
|
35
|
+
* carries its own copy. Returns `'refreshed'` when a file was
|
|
36
|
+
* rewritten, `'current'` when the block was already up to date, and
|
|
37
|
+
* `'skipped'` when there is nothing to do: the project never adopted
|
|
38
|
+
* the managed block, or the installed Next.js predates the generator
|
|
39
|
+
* (< 16.3).
|
|
40
|
+
*/
|
|
41
|
+
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)
|
|
53
|
+
return 'skipped';
|
|
54
|
+
let writeAgentFiles;
|
|
55
|
+
try {
|
|
56
|
+
const generatorPath = require.resolve('next/dist/server/lib/generate-agent-files', { paths: [cwd] });
|
|
57
|
+
writeAgentFiles = require(generatorPath).writeAgentFiles;
|
|
58
|
+
if (typeof writeAgentFiles !== 'function')
|
|
59
|
+
return 'skipped';
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return 'skipped';
|
|
63
|
+
}
|
|
64
|
+
const result = writeAgentFiles(cwd);
|
|
65
|
+
return result.agentsMd === 'updated' || result.claudeMd === 'updated'
|
|
66
|
+
? 'refreshed'
|
|
67
|
+
: 'current';
|
|
68
|
+
}
|
|
23
69
|
function getNextjsVersion(cwd) {
|
|
24
70
|
try {
|
|
25
71
|
const nextPkgPath = require.resolve('next/package.json', { paths: [cwd] });
|
|
@@ -45,6 +91,36 @@ function getNextjsVersion(cwd) {
|
|
|
45
91
|
};
|
|
46
92
|
}
|
|
47
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Next.js ships its documentation inside the published package (at
|
|
96
|
+
* `dist/docs`) since 16.2.0. When the install resolved from `cwd` has
|
|
97
|
+
* bundled docs, the index can point at them directly instead of
|
|
98
|
+
* downloading a copy into `.next-docs`.
|
|
99
|
+
*/
|
|
100
|
+
function getBundledDocsInfo(cwd) {
|
|
101
|
+
try {
|
|
102
|
+
const nextPkgPath = require.resolve('next/package.json', { paths: [cwd] });
|
|
103
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(nextPkgPath, 'utf-8'));
|
|
104
|
+
const docsPath = path_1.default.join(path_1.default.dirname(nextPkgPath), 'dist', 'docs');
|
|
105
|
+
if (!pkg.version || collectDocFiles(docsPath).length === 0) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return { docsPath, version: pkg.version };
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function getBundledDocsLinkPath(cwd, docsPath) {
|
|
115
|
+
// Prefer the conventional path when it resolves from the project
|
|
116
|
+
// (covers hoisted installs; pnpm exposes next via a node_modules symlink).
|
|
117
|
+
const conventional = path_1.default.join(cwd, 'node_modules', 'next', 'dist', 'docs');
|
|
118
|
+
if (fs_1.default.existsSync(conventional)) {
|
|
119
|
+
return './node_modules/next/dist/docs';
|
|
120
|
+
}
|
|
121
|
+
const relative = path_1.default.relative(cwd, docsPath).replace(/\\/g, '/');
|
|
122
|
+
return relative.startsWith('.') ? relative : `./${relative}`;
|
|
123
|
+
}
|
|
48
124
|
function versionToGitHubTag(version) {
|
|
49
125
|
return version.startsWith('v') ? version : `v${version}`;
|
|
50
126
|
}
|
package/lib/handle-package.js
CHANGED
|
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getPackageManagerVersion = getPackageManagerVersion;
|
|
7
|
+
exports.getPnpmMajorVersion = getPnpmMajorVersion;
|
|
6
8
|
exports.getPkgManager = getPkgManager;
|
|
7
9
|
exports.uninstallPackage = uninstallPackage;
|
|
8
10
|
exports.installPackages = installPackages;
|
|
@@ -10,20 +12,75 @@ exports.runInstallation = runInstallation;
|
|
|
10
12
|
exports.addPackageDependency = addPackageDependency;
|
|
11
13
|
const find_up_1 = __importDefault(require("find-up"));
|
|
12
14
|
const execa_1 = __importDefault(require("execa"));
|
|
15
|
+
const node_child_process_1 = require("node:child_process");
|
|
13
16
|
const node_path_1 = require("node:path");
|
|
17
|
+
/**
|
|
18
|
+
* Get the full version string for the given package manager.
|
|
19
|
+
*
|
|
20
|
+
* First tries to parse from `npm_config_user_agent` (e.g., "pnpm/9.13.2 npm/? ..."),
|
|
21
|
+
* then falls back to spawning `<packageManager> --version`.
|
|
22
|
+
*
|
|
23
|
+
* Returns null if unable to determine the version.
|
|
24
|
+
*
|
|
25
|
+
* Mirrors `packages/create-next-app/helpers/get-pkg-manager.ts`.
|
|
26
|
+
*/
|
|
27
|
+
function getPackageManagerVersion(packageManager) {
|
|
28
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
29
|
+
const userAgentMatch = userAgent.match(new RegExp(`${packageManager}/([\\d.]+[\\w.-]*)`));
|
|
30
|
+
if (userAgentMatch) {
|
|
31
|
+
return userAgentMatch[1];
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const version = (0, node_child_process_1.execSync)(`${packageManager} --version`, {
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
37
|
+
}).trim();
|
|
38
|
+
if (/^\d+\.\d+\.\d+/.test(version)) {
|
|
39
|
+
return version;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// package manager not available or failed to run
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the major version of pnpm being used.
|
|
49
|
+
* Returns null if unable to determine the version.
|
|
50
|
+
*/
|
|
51
|
+
function getPnpmMajorVersion() {
|
|
52
|
+
const version = getPackageManagerVersion('pnpm');
|
|
53
|
+
if (!version)
|
|
54
|
+
return null;
|
|
55
|
+
const major = parseInt(version.split('.')[0], 10);
|
|
56
|
+
return Number.isNaN(major) ? null : major;
|
|
57
|
+
}
|
|
14
58
|
function getPkgManager(baseDir) {
|
|
15
59
|
try {
|
|
60
|
+
const userAgent = process.env.npm_config_user_agent;
|
|
61
|
+
if (userAgent) {
|
|
62
|
+
if (userAgent.startsWith('yarn')) {
|
|
63
|
+
return 'yarn';
|
|
64
|
+
}
|
|
65
|
+
else if (userAgent.startsWith('pnpm')) {
|
|
66
|
+
return 'pnpm';
|
|
67
|
+
}
|
|
68
|
+
else if (userAgent.startsWith('bun')) {
|
|
69
|
+
return 'bun';
|
|
70
|
+
}
|
|
71
|
+
else if (userAgent.startsWith('npm')) {
|
|
72
|
+
return 'npm';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
16
75
|
const lockFile = find_up_1.default.sync([
|
|
17
|
-
'package-lock.json',
|
|
18
76
|
'yarn.lock',
|
|
19
77
|
'pnpm-lock.yaml',
|
|
20
78
|
'bun.lock',
|
|
21
79
|
'bun.lockb',
|
|
80
|
+
'package-lock.json',
|
|
22
81
|
], { cwd: baseDir });
|
|
23
82
|
if (lockFile) {
|
|
24
83
|
switch ((0, node_path_1.basename)(lockFile)) {
|
|
25
|
-
case 'package-lock.json':
|
|
26
|
-
return 'npm';
|
|
27
84
|
case 'yarn.lock':
|
|
28
85
|
return 'yarn';
|
|
29
86
|
case 'pnpm-lock.yaml':
|
|
@@ -31,6 +88,8 @@ function getPkgManager(baseDir) {
|
|
|
31
88
|
case 'bun.lock':
|
|
32
89
|
case 'bun.lockb':
|
|
33
90
|
return 'bun';
|
|
91
|
+
case 'package-lock.json':
|
|
92
|
+
return 'npm';
|
|
34
93
|
default:
|
|
35
94
|
return 'npm';
|
|
36
95
|
}
|
package/lib/utils.js
CHANGED
|
@@ -132,5 +132,15 @@ exports.TRANSFORMER_INQUIRER_CHOICES = [
|
|
|
132
132
|
value: 'remove-experimental-ppr',
|
|
133
133
|
version: '16.0.0-canary.11',
|
|
134
134
|
},
|
|
135
|
+
{
|
|
136
|
+
title: 'Add `export const instant = false` to App Router pages and layouts to ease Cache Components adoption',
|
|
137
|
+
value: 'cache-components-instant-false',
|
|
138
|
+
version: '16.3.0',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
title: "Remove `export const prefetch = 'partial'` Route Segment Config from App Router pages and layouts after enabling `partialPrefetching` globally",
|
|
142
|
+
value: 'remove-partial-prefetch',
|
|
143
|
+
version: '16.3.0',
|
|
144
|
+
},
|
|
135
145
|
];
|
|
136
146
|
//# sourceMappingURL=utils.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@next/codemod",
|
|
3
|
-
"version": "16.3.0-preview.
|
|
3
|
+
"version": "16.3.0-preview.10",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"find-up": "4.1.0",
|
|
15
15
|
"globby": "11.0.1",
|
|
16
16
|
"is-git-clean": "1.1.0",
|
|
17
|
+
"js-yaml": "4.2.0",
|
|
17
18
|
"jscodeshift": "17.0.0",
|
|
18
19
|
"picocolors": "1.0.0",
|
|
19
20
|
"prompts": "2.4.2",
|
|
@@ -27,19 +28,19 @@
|
|
|
27
28
|
"lib/**/*.js",
|
|
28
29
|
"lib/cra-to-next/gitignore"
|
|
29
30
|
],
|
|
30
|
-
"scripts": {
|
|
31
|
-
"build": "pnpm tsc -d -p tsconfig.json",
|
|
32
|
-
"prepublishOnly": "cd ../../ && turbo run build",
|
|
33
|
-
"dev": "pnpm tsc -d -w -p tsconfig.json",
|
|
34
|
-
"test": "jest",
|
|
35
|
-
"test:upgrade-fixture": "./scripts/test-upgrade-fixture.sh"
|
|
36
|
-
},
|
|
37
31
|
"bin": "./bin/next-codemod.js",
|
|
38
32
|
"devDependencies": {
|
|
39
33
|
"@types/find-up": "4.0.0",
|
|
34
|
+
"@types/js-yaml": "4.0.9",
|
|
40
35
|
"@types/jscodeshift": "0.11.0",
|
|
41
36
|
"@types/prompts": "2.4.2",
|
|
42
37
|
"@types/semver": "7.3.1",
|
|
43
38
|
"typescript": "6.0.2"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "pnpm tsc -d -p tsconfig.json",
|
|
42
|
+
"dev": "pnpm tsc -d -w -p tsconfig.json",
|
|
43
|
+
"test": "jest",
|
|
44
|
+
"test:upgrade-fixture": "./scripts/test-upgrade-fixture.sh"
|
|
44
45
|
}
|
|
45
|
-
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = transformer;
|
|
4
|
+
const parser_1 = require("../lib/parser");
|
|
5
|
+
/**
|
|
6
|
+
* Blanket-inserts `export const instant = false` into every App Router `page`,
|
|
7
|
+
* `layout`, and `default` file so they're marked as allowed to block when
|
|
8
|
+
* `cacheComponents` is enabled. Each opt-out is meant to be walked back, one
|
|
9
|
+
* route at a time, using the companion adoption skill.
|
|
10
|
+
*
|
|
11
|
+
* - Skips files that already declare or export `instant` in any form (never
|
|
12
|
+
* overrides existing config or appends a duplicate binding).
|
|
13
|
+
* - Skips Client/Server Component modules (`"use client"` / `"use server"`):
|
|
14
|
+
* `instant` is a Server Component route segment config, so exporting it from
|
|
15
|
+
* those modules is a build error.
|
|
16
|
+
* - Targets `page` / `layout` / `default` only (not `route` — `instant` does
|
|
17
|
+
* not apply to route handlers). `default.tsx` is the parallel-route fallback,
|
|
18
|
+
* a server segment that accepts route segment config like the other two.
|
|
19
|
+
*/
|
|
20
|
+
function transformer(file, _api) {
|
|
21
|
+
if (process.env.NODE_ENV !== 'test' &&
|
|
22
|
+
!/(^|[/\\])app[/\\].*?(page|layout|default)\.[^/\\]+$/.test(file.path)) {
|
|
23
|
+
return file.source;
|
|
24
|
+
}
|
|
25
|
+
const j = (0, parser_1.createParserFromPath)(file.path);
|
|
26
|
+
const root = j(file.source);
|
|
27
|
+
// Bail on Client/Server Component modules. `instant` is a Server Component
|
|
28
|
+
// route segment config; exporting it from a `"use client"` (or `"use server"`)
|
|
29
|
+
// module fails the build. Parsers represent the directive either in
|
|
30
|
+
// `program.directives` or as a leading string-literal `ExpressionStatement`.
|
|
31
|
+
const program = root.get().node.program;
|
|
32
|
+
const isClientOrServerDirective = (value) => value === 'use client' || value === 'use server';
|
|
33
|
+
let hasModuleDirective = (program.directives ?? []).some((d) => isClientOrServerDirective(d?.value?.value));
|
|
34
|
+
if (!hasModuleDirective) {
|
|
35
|
+
for (const node of program.body) {
|
|
36
|
+
if (node.type !== 'ExpressionStatement' ||
|
|
37
|
+
(node.expression?.type !== 'StringLiteral' &&
|
|
38
|
+
node.expression?.type !== 'Literal')) {
|
|
39
|
+
// Directives must lead the module; stop at the first non-directive.
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
if (isClientOrServerDirective(node.expression.value)) {
|
|
43
|
+
hasModuleDirective = true;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (hasModuleDirective) {
|
|
49
|
+
return file.source;
|
|
50
|
+
}
|
|
51
|
+
// Bail if `instant` already exists in any form, so we never append a
|
|
52
|
+
// duplicate declaration (which would be a `SyntaxError`). This covers:
|
|
53
|
+
// export const instant = ...
|
|
54
|
+
// export const a = 1, instant = ... (any declarator position)
|
|
55
|
+
// const instant = ... (local binding)
|
|
56
|
+
// const { instant } = ... (destructured binding)
|
|
57
|
+
// export { instant }
|
|
58
|
+
// export { foo as instant }
|
|
59
|
+
// export function instant() {} / export class instant {}
|
|
60
|
+
const bindsInstant = (node) => {
|
|
61
|
+
switch (node?.type) {
|
|
62
|
+
case 'Identifier':
|
|
63
|
+
return node.name === 'instant';
|
|
64
|
+
case 'ObjectPattern':
|
|
65
|
+
return node.properties.some((prop) => prop.type === 'RestElement'
|
|
66
|
+
? bindsInstant(prop.argument)
|
|
67
|
+
: bindsInstant(prop.value ?? prop.argument));
|
|
68
|
+
case 'ArrayPattern':
|
|
69
|
+
return node.elements.some((el) => el != null && bindsInstant(el));
|
|
70
|
+
case 'AssignmentPattern':
|
|
71
|
+
return bindsInstant(node.left);
|
|
72
|
+
case 'RestElement':
|
|
73
|
+
return bindsInstant(node.argument);
|
|
74
|
+
default:
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const hasInstantBinding = root
|
|
79
|
+
.find(j.VariableDeclarator)
|
|
80
|
+
.filter((p) => bindsInstant(p.node.id))
|
|
81
|
+
.size() > 0 ||
|
|
82
|
+
root.find(j.ExportSpecifier, { exported: { name: 'instant' } }).size() >
|
|
83
|
+
0 ||
|
|
84
|
+
root.find(j.FunctionDeclaration, { id: { name: 'instant' } }).size() > 0 ||
|
|
85
|
+
root.find(j.ClassDeclaration, { id: { name: 'instant' } }).size() > 0;
|
|
86
|
+
if (hasInstantBinding) {
|
|
87
|
+
return file.source;
|
|
88
|
+
}
|
|
89
|
+
// Build `export const instant = false`. The two `//` comments above it
|
|
90
|
+
// (TODO + See:) are attached as leading comments on the declaration so
|
|
91
|
+
// recast prints them right above it.
|
|
92
|
+
const todoComment = j.commentLine(' TODO: Cache Components adoption. Refactor this route so this opt-out can be removed.', true, false);
|
|
93
|
+
const seeComment = j.commentLine(' See: https://nextjs.org/docs/app/guides/migrating-to-cache-components', true, false);
|
|
94
|
+
const instantExport = j.exportNamedDeclaration(j.variableDeclaration('const', [
|
|
95
|
+
j.variableDeclarator(j.identifier('instant'), j.booleanLiteral(false)),
|
|
96
|
+
]));
|
|
97
|
+
instantExport.comments = [todoComment, seeComment];
|
|
98
|
+
// Insert after the last top-level import, or at the top of the module
|
|
99
|
+
// if there are no imports.
|
|
100
|
+
const body = program.body;
|
|
101
|
+
let lastImportIndex = -1;
|
|
102
|
+
for (let i = 0; i < body.length; i++) {
|
|
103
|
+
if (body[i].type === 'ImportDeclaration')
|
|
104
|
+
lastImportIndex = i;
|
|
105
|
+
}
|
|
106
|
+
if (lastImportIndex !== -1) {
|
|
107
|
+
body.splice(lastImportIndex + 1, 0, instantExport);
|
|
108
|
+
}
|
|
109
|
+
else if (body.length > 0) {
|
|
110
|
+
// No imports. Inserting at index 0 would steal any file-level leading
|
|
111
|
+
// comments (e.g. `// @ts-nocheck`) from `body[0]` because recast
|
|
112
|
+
// attributes them to whatever is first. Move those leading comments
|
|
113
|
+
// off `body[0]` onto the new export *before* its TODO/See: lines, so
|
|
114
|
+
// they print in their original position.
|
|
115
|
+
const first = body[0];
|
|
116
|
+
const allComments = (first.comments ?? []);
|
|
117
|
+
const firstLeading = allComments.filter((c) => c.leading === true);
|
|
118
|
+
if (firstLeading.length > 0) {
|
|
119
|
+
first.comments = allComments.filter((c) => c.leading !== true);
|
|
120
|
+
instantExport.comments = [...firstLeading, todoComment, seeComment];
|
|
121
|
+
}
|
|
122
|
+
body.unshift(instantExport);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
body.push(instantExport);
|
|
126
|
+
}
|
|
127
|
+
return root.toSource();
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=cache-components-instant-false.js.map
|
|
@@ -18,7 +18,7 @@ const utils_1 = require("./lib/utils");
|
|
|
18
18
|
// Properties that need to be moved to experimental.turbopack*
|
|
19
19
|
const RENAMED_EXPERIMENTAL_PROPERTIES = {
|
|
20
20
|
minify: 'turbopackMinify',
|
|
21
|
-
treeShaking: '
|
|
21
|
+
treeShaking: 'turbopackModuleFragments',
|
|
22
22
|
sourceMaps: 'turbopackSourceMaps',
|
|
23
23
|
};
|
|
24
24
|
// `memoryLimit` is no longer supported and is removed entirely. We drop it under
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = transformer;
|
|
4
|
+
const parser_1 = require("../lib/parser");
|
|
5
|
+
// Route Segment Config name and the only value this codemod strips.
|
|
6
|
+
const CONFIG_NAME = 'prefetch';
|
|
7
|
+
const TARGET_VALUE = 'partial';
|
|
8
|
+
// Unwrap `'partial' as const` / `'partial' satisfies T` so the value guard
|
|
9
|
+
// still matches when the export is annotated.
|
|
10
|
+
function unwrap(j, node) {
|
|
11
|
+
if ((j.TSAsExpression && j.TSAsExpression.check(node)) ||
|
|
12
|
+
(j.TSSatisfiesExpression && j.TSSatisfiesExpression.check(node))) {
|
|
13
|
+
return node.expression;
|
|
14
|
+
}
|
|
15
|
+
return node;
|
|
16
|
+
}
|
|
17
|
+
// Only `prefetch = 'partial'` matches. A different value such as
|
|
18
|
+
// `prefetch = 'allow-runtime'` is a legitimate config and is left untouched.
|
|
19
|
+
function isTargetPrefetch(j, decl) {
|
|
20
|
+
if (!j.VariableDeclarator.check(decl) || !j.Identifier.check(decl.id)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (decl.id.name !== CONFIG_NAME || !decl.init) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const init = unwrap(j, decl.init);
|
|
27
|
+
return ((j.StringLiteral.check(init) && init.value === TARGET_VALUE) ||
|
|
28
|
+
(j.Literal.check(init) && init.value === TARGET_VALUE));
|
|
29
|
+
}
|
|
30
|
+
// Drop only the `prefetch = 'partial'` declarator, leaving any sibling
|
|
31
|
+
// declarators (e.g. `export const runtime = 'edge', prefetch = 'partial'`)
|
|
32
|
+
// intact. Returns the number of declarators left so the caller can remove the
|
|
33
|
+
// whole statement when it becomes empty.
|
|
34
|
+
function stripTargetDeclarators(j, declaration) {
|
|
35
|
+
declaration.declarations = declaration.declarations.filter((decl) => !isTargetPrefetch(j, decl));
|
|
36
|
+
return declaration.declarations.length;
|
|
37
|
+
}
|
|
38
|
+
// Removing a statement also removes the comments attached above it — which
|
|
39
|
+
// may be a user's note or a deliberate `// TODO(...)` marker. Reattach the
|
|
40
|
+
// leading comments to the next statement (or the previous one when the
|
|
41
|
+
// removed statement is last) so they survive the removal.
|
|
42
|
+
function preserveLeadingComments(path) {
|
|
43
|
+
const comments = path.node.comments?.filter((comment) => comment.leading);
|
|
44
|
+
if (!comments?.length) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const body = path.parent.node.body;
|
|
48
|
+
if (!Array.isArray(body)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const index = body.indexOf(path.node);
|
|
52
|
+
const next = body[index + 1];
|
|
53
|
+
const prev = body[index - 1];
|
|
54
|
+
if (next) {
|
|
55
|
+
next.comments = [...comments, ...(next.comments ?? [])];
|
|
56
|
+
}
|
|
57
|
+
else if (prev) {
|
|
58
|
+
for (const comment of comments) {
|
|
59
|
+
comment.leading = false;
|
|
60
|
+
comment.trailing = true;
|
|
61
|
+
}
|
|
62
|
+
prev.comments = [...(prev.comments ?? []), ...comments];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function transformer(file, _api) {
|
|
66
|
+
// Run on App Router page/layout files, except for test environment. The
|
|
67
|
+
// `prefetch` Route Segment Config only applies to pages and layouts, so
|
|
68
|
+
// route handlers are intentionally excluded.
|
|
69
|
+
// `(^|[/\\])app` matches both an absolute path and a relative `app/...` path
|
|
70
|
+
// (what `npx @next/codemod ... ./app` passes), so top-level app files aren't
|
|
71
|
+
// silently skipped.
|
|
72
|
+
if (process.env.NODE_ENV !== 'test' &&
|
|
73
|
+
!/(^|[/\\])app[/\\](?:.*[/\\])?(page|layout)(\.[^/\\]*)?$/.test(file.path)) {
|
|
74
|
+
return file.source;
|
|
75
|
+
}
|
|
76
|
+
const j = (0, parser_1.createParserFromPath)(file.path);
|
|
77
|
+
const root = j(file.source);
|
|
78
|
+
let hasChanges = false;
|
|
79
|
+
// `export const prefetch = 'partial'` (possibly alongside other configs in
|
|
80
|
+
// the same statement).
|
|
81
|
+
root
|
|
82
|
+
.find(j.ExportNamedDeclaration, {
|
|
83
|
+
declaration: { type: 'VariableDeclaration' },
|
|
84
|
+
})
|
|
85
|
+
.filter((path) => {
|
|
86
|
+
const declaration = path.node.declaration;
|
|
87
|
+
return (j.VariableDeclaration.check(declaration) &&
|
|
88
|
+
declaration.declarations.some((decl) => isTargetPrefetch(j, decl)));
|
|
89
|
+
})
|
|
90
|
+
.forEach((path) => {
|
|
91
|
+
const declaration = path.node.declaration;
|
|
92
|
+
const remaining = stripTargetDeclarators(j, declaration);
|
|
93
|
+
// Remove the whole export only when nothing else was declared with it.
|
|
94
|
+
if (remaining === 0) {
|
|
95
|
+
preserveLeadingComments(path);
|
|
96
|
+
j(path).remove();
|
|
97
|
+
}
|
|
98
|
+
hasChanges = true;
|
|
99
|
+
});
|
|
100
|
+
// Bare `const prefetch = 'partial'` declarations are only Route Segment
|
|
101
|
+
// Configs when the file also exports them as `prefetch` via a local
|
|
102
|
+
// `export { prefetch }`. Re-exports (`export { prefetch } from '...'`) bind
|
|
103
|
+
// another module's value, and aliased exports export a different name
|
|
104
|
+
// (`export { prefetch as other }`) or a different binding
|
|
105
|
+
// (`export { other as prefetch }`), so neither counts. When an aliased
|
|
106
|
+
// export shares the `prefetch` binding, removing the declaration would
|
|
107
|
+
// break it, so the whole file is left untouched.
|
|
108
|
+
let hasPlainPrefetchExportSpecifier = false;
|
|
109
|
+
let hasAliasedPrefetchBinding = false;
|
|
110
|
+
root
|
|
111
|
+
.find(j.ExportNamedDeclaration)
|
|
112
|
+
.filter((path) => !path.node.source)
|
|
113
|
+
.forEach((path) => {
|
|
114
|
+
for (const spec of path.node.specifiers ?? []) {
|
|
115
|
+
if (!j.ExportSpecifier.check(spec) ||
|
|
116
|
+
!j.Identifier.check(spec.local) ||
|
|
117
|
+
spec.local.name !== CONFIG_NAME) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (j.Identifier.check(spec.exported) &&
|
|
121
|
+
spec.exported.name === CONFIG_NAME) {
|
|
122
|
+
hasPlainPrefetchExportSpecifier = true;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
hasAliasedPrefetchBinding = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
const hasLocalPrefetchExportSpecifier = hasPlainPrefetchExportSpecifier && !hasAliasedPrefetchBinding;
|
|
130
|
+
// Track that we removed a bare declaration so we only drop the matching
|
|
131
|
+
// export specifier below.
|
|
132
|
+
let removedBareDeclaration = false;
|
|
133
|
+
if (hasLocalPrefetchExportSpecifier) {
|
|
134
|
+
root
|
|
135
|
+
.find(j.VariableDeclaration)
|
|
136
|
+
.filter((path) => {
|
|
137
|
+
// `export const prefetch` is handled above; skip it here.
|
|
138
|
+
if (j.ExportNamedDeclaration.check(path.parent.node)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
// Only top-level declarations can be the exported config. A local
|
|
142
|
+
// `const prefetch` inside a function or block is unrelated code.
|
|
143
|
+
if (!j.Program.check(path.parent.node)) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return path.node.declarations.some((decl) => isTargetPrefetch(j, decl));
|
|
147
|
+
})
|
|
148
|
+
.forEach((path) => {
|
|
149
|
+
const remaining = stripTargetDeclarators(j, path.node);
|
|
150
|
+
if (remaining === 0) {
|
|
151
|
+
preserveLeadingComments(path);
|
|
152
|
+
j(path).remove();
|
|
153
|
+
}
|
|
154
|
+
removedBareDeclaration = true;
|
|
155
|
+
hasChanges = true;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// Handle `export { prefetch }` and `export { prefetch, other }`, but only
|
|
159
|
+
// when the paired declaration was the `'partial'` one we removed above.
|
|
160
|
+
if (removedBareDeclaration) {
|
|
161
|
+
root
|
|
162
|
+
.find(j.ExportNamedDeclaration)
|
|
163
|
+
// Skip re-exports (`export { prefetch } from '...'`): their specifiers
|
|
164
|
+
// reference another module's binding, not the declaration we removed.
|
|
165
|
+
.filter((path) => !path.node.source && Boolean(path.node.specifiers?.length))
|
|
166
|
+
.forEach((path) => {
|
|
167
|
+
const specifiers = path.node.specifiers;
|
|
168
|
+
if (!specifiers)
|
|
169
|
+
return;
|
|
170
|
+
const filteredSpecifiers = specifiers.filter((spec) => {
|
|
171
|
+
// Remove only the plain `export { prefetch }` specifier. Aliased
|
|
172
|
+
// specifiers export a different name or bind a different value, so
|
|
173
|
+
// they aren't the Route Segment Config.
|
|
174
|
+
if (j.ExportSpecifier.check(spec) &&
|
|
175
|
+
j.Identifier.check(spec.local) &&
|
|
176
|
+
j.Identifier.check(spec.exported)) {
|
|
177
|
+
return !(spec.local.name === CONFIG_NAME &&
|
|
178
|
+
spec.exported.name === CONFIG_NAME);
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
});
|
|
182
|
+
if (filteredSpecifiers.length !== specifiers.length) {
|
|
183
|
+
hasChanges = true;
|
|
184
|
+
if (filteredSpecifiers.length === 0) {
|
|
185
|
+
preserveLeadingComments(path);
|
|
186
|
+
j(path).remove();
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
path.node.specifiers = filteredSpecifiers;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (hasChanges) {
|
|
195
|
+
return root.toSource();
|
|
196
|
+
}
|
|
197
|
+
return file.source;
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=remove-partial-prefetch.js.map
|